minglr 1.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. data/.document +5 -0
  2. data/.gitignore +3 -0
  3. data/LICENSE +20 -0
  4. data/PostInstall.txt +1 -0
  5. data/README.rdoc +87 -0
  6. data/Rakefile +90 -0
  7. data/VERSION.yml +4 -0
  8. data/bin/minglr +29 -0
  9. data/bin/mtx +14 -0
  10. data/cucumber.yml +2 -0
  11. data/features/cards.feature +27 -0
  12. data/features/step_definitions/minglr_steps.rb +34 -0
  13. data/features/step_definitions/shared_steps.rb +69 -0
  14. data/features/users.feature +6 -0
  15. data/lib/minglr.rb +34 -0
  16. data/lib/minglr/action.rb +85 -0
  17. data/lib/minglr/config_parser.rb +51 -0
  18. data/lib/minglr/extensions/array.rb +23 -0
  19. data/lib/minglr/mtx/input_cache.rb +24 -0
  20. data/lib/minglr/mtx/options_parser.rb +58 -0
  21. data/lib/minglr/options_parser.rb +82 -0
  22. data/lib/minglr/resources/attachment.rb +51 -0
  23. data/lib/minglr/resources/base.rb +42 -0
  24. data/lib/minglr/resources/card.rb +117 -0
  25. data/lib/minglr/resources/project.rb +25 -0
  26. data/lib/minglr/resources/property_definition.rb +12 -0
  27. data/lib/minglr/resources/transition_execution.rb +6 -0
  28. data/lib/minglr/resources/user.rb +19 -0
  29. data/minglr.gemspec +120 -0
  30. data/minglrconfig.sample +37 -0
  31. data/tasks/commit.sample.rake +86 -0
  32. data/tasks/svn.sample.rake +27 -0
  33. data/test/action_test.rb +75 -0
  34. data/test/commands_test.rb +111 -0
  35. data/test/config_parser_test.rb +116 -0
  36. data/test/extensions/array_test.rb +41 -0
  37. data/test/options_parser_test.rb +74 -0
  38. data/test/resources/attachment_test.rb +90 -0
  39. data/test/resources/base_test.rb +58 -0
  40. data/test/resources/card_test.rb +199 -0
  41. data/test/resources/project_test.rb +44 -0
  42. data/test/resources/property_definition_test.rb +25 -0
  43. data/test/resources/user_test.rb +32 -0
  44. data/test/test_helper.rb +31 -0
  45. metadata +219 -0
@@ -0,0 +1,85 @@
1
+ module Minglr
2
+
3
+ class Action
4
+
5
+ def self.execute(action, options = [], flag_options = {}, config = {})
6
+ if action.to_s == options[0]
7
+ options.shift
8
+ else
9
+ options.shift
10
+ options.shift
11
+ end
12
+ begin
13
+ Commands.send(action, options, flag_options, config)
14
+ rescue ActiveResource::ResourceNotFound => error
15
+ puts error.message + " for URL '#{Resources::Base.site}' ..."
16
+ end
17
+ end
18
+
19
+ def self.valid_actions
20
+ Commands.methods(false)
21
+ end
22
+
23
+ def self.valid_action?(action)
24
+ valid_actions.include? action
25
+ end
26
+
27
+ class Commands
28
+
29
+ def self.attach(options, flag_options, config)
30
+ raise "Missing card number!" if options.empty?
31
+
32
+ card_number = options.first
33
+ file_name = flag_options[:file_attachment]
34
+ Resources::Attachment.attach(card_number, file_name, config[:username], config[:password])
35
+ end
36
+
37
+ def self.card(options, flag_options, config)
38
+ raise "Missing card number!" if options.empty?
39
+
40
+ card_number = options.first
41
+ Resources::Card.print_card(card_number, config[:status_property])
42
+ end
43
+
44
+ def self.cards(options, flag_options, config)
45
+ Resources::Card.print_all(options, config[:status_property])
46
+ end
47
+
48
+ def self.create(options, flag_options, config)
49
+ Resources::Card.create(flag_options, config[:status_property])
50
+ end
51
+
52
+ def self.fetch(options, flag_options, config)
53
+ raise "Missing card number!" if options.empty?
54
+
55
+ card_number = options.first
56
+ Resources::Attachment.fetch(card_number, config[:username], config[:password])
57
+ end
58
+
59
+ def self.move(options, flag_options, config)
60
+ raise "Missing card number!" if options.empty?
61
+
62
+ card_number = options.first
63
+ Resources::Card.move(card_number, flag_options, config)
64
+ end
65
+
66
+ def self.projects(options, flag_options, config)
67
+ Resources::Project.print_all(options, config[:status_property])
68
+ end
69
+
70
+ def self.update(options, flag_options, config)
71
+ raise "Missing card number!" if options.empty?
72
+
73
+ card_number = options.first
74
+ Resources::Card.update(card_number, flag_options)
75
+ end
76
+
77
+ def self.users(options, flag_options, config)
78
+ Resources::User.print_all(options)
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ end
@@ -0,0 +1,51 @@
1
+ module Minglr
2
+
3
+ class ConfigParser
4
+
5
+ CONFIG_FILE = ".minglrconfig"
6
+ attr_reader :config
7
+
8
+ def self.parse
9
+ config_files = [File.join(ENV["HOME"], CONFIG_FILE), File.join(ENV["PWD"], CONFIG_FILE)]
10
+ config_files.uniq!
11
+ config_files.each do |config_file_name|
12
+ if File.exist?(config_file_name)
13
+ return self.new(File.read(config_file_name)).config
14
+ end
15
+ end
16
+ puts "Unable to find #{CONFIG_FILE} in #{config_files.join(", ")}"
17
+ class_eval("send :exit, 1") # Why is it so hard to mock or stub exit?
18
+ end
19
+
20
+ def initialize(config_contents)
21
+ @config = {}
22
+ @current_section = nil
23
+ config_contents.each_line do |line|
24
+ line = line.strip!
25
+ case line
26
+ when "", /^#.*$/
27
+ next
28
+ when /\[(.*)\]/
29
+ define_section($1.to_s)
30
+ else
31
+ define_var(line)
32
+ end
33
+ end
34
+ @config
35
+ end
36
+
37
+ def define_section(section_name)
38
+ @config[section_name.to_sym] = {} unless @config.has_key?(section_name.to_sym)
39
+ @current_section = section_name.to_sym
40
+ end
41
+
42
+ def define_var(line)
43
+ key, value = line.split("=")
44
+ key.strip!
45
+ value.strip!
46
+ @config[@current_section][key.to_sym] = value
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,23 @@
1
+ module Minglr
2
+
3
+ module Extensions
4
+
5
+ module Array
6
+
7
+ def filter(attributes, words)
8
+ collection = self
9
+ words.each do |word|
10
+ collection = self.select do |element|
11
+ output = ""
12
+ attributes.each { |attribute| output << element.send(attribute).to_s + " " }
13
+ output =~ /#{word}/i
14
+ end
15
+ end
16
+ collection
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,24 @@
1
+ module MTX
2
+ class InputCache
3
+ class << self
4
+ def put(key, content)
5
+ File.open(file_pathname(key), File::CREAT | File::WRONLY | File::TRUNC) { |file| file.write content }
6
+ end
7
+
8
+ def get(key)
9
+ if content = File.read(file_pathname(key))
10
+ return nil if content.blank?
11
+ content
12
+ end
13
+ rescue
14
+ nil
15
+ end
16
+
17
+ protected
18
+
19
+ def file_pathname(key)
20
+ File.join(Dir::tmpdir, "#{key.to_s.gsub(/[^\w]/, '')}.entry")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ module MTX
2
+
3
+ class OptionsParser
4
+
5
+ def self.parse(args, *required_by_command)
6
+ uri_options = {}
7
+ command_options = {}
8
+
9
+ parser = OptionParser.new do |opts|
10
+ opts.banner = "Usage: mtx [options]"
11
+ opts.on("--transition TRANSITION", "Transition name.") do |transition|
12
+ command_options[:transition] = transition
13
+ end
14
+
15
+ opts.on("--card CARD", "Card number.") do |card|
16
+ command_options[:card] = card
17
+ end
18
+
19
+ opts.on("--properties ARGS", Array, "User-entered properties and values for the transition in array format. Must be an even number of comma-delimited values, like \"A,B,'C with spaces','D with spaces'\".") do |args|
20
+ command_options[:properties] = args.in_groups_of(2).map { |key, value| {'name' => key, 'value' => value} }
21
+ end
22
+
23
+ opts.on("--comment COMMENT", "Transition comment. This may be required depending on your transition settings.") do |comment|
24
+ command_options[:comment] = comment
25
+ end
26
+
27
+ opts.on("--username USERNAME", "Mingle username.") do |username|
28
+ uri_options[:username] = username
29
+ end
30
+
31
+ opts.on("--password PASSWORD", "Mingle password.") do |password|
32
+ uri_options[:password] = password
33
+ end
34
+
35
+ opts.on("--host_port HOST_PORT", "Host and port.") do |host_and_port|
36
+ uri_options[:host_and_port] = host_and_port
37
+ end
38
+
39
+ opts.on("--project PROJECT", "Project name.") do |project|
40
+ uri_options[:project] = project
41
+ end
42
+ end
43
+
44
+ parser.parse! args
45
+
46
+ ([:project, :host_and_port] | required_by_command).each do |arg|
47
+ unless command_options[arg] || uri_options[arg]
48
+ # TODO: let commands handle their own errors
49
+ $stderr.puts "Missing command-line argument --#{arg.to_s}, use --help for command-line options."
50
+ exit 1
51
+ end
52
+ end
53
+
54
+ [uri_options, command_options]
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,82 @@
1
+ require "yaml"
2
+
3
+ module Minglr
4
+ class OptionsParser
5
+ def self.parse(args, *required_by_command)
6
+ project_options = []
7
+
8
+ if Resources::Base.site
9
+ begin
10
+ project_options = Resources::PropertyDefinition.project_options
11
+ rescue ActiveResource::UnauthorizedAccess => exception
12
+ puts "Connection #{exception.message} to #{Resources::Base.site.to_s}"
13
+ puts "Did you set 'basic_authentication_enabled: true' in your auth_config.yml file?"
14
+ exit 1 unless MINGLR_ENV == "test"
15
+ end
16
+ end
17
+
18
+ command_options = {}
19
+
20
+ parser = OptionParser.new do |opts|
21
+ opts.banner = "Usage: minglr [action] [options]"
22
+ opts.separator ""
23
+ opts.separator "Valid Commands Are: #{Minglr::Action.valid_actions.join(", ")}"
24
+
25
+ opts.on("-n NAME", String, "Short name of card") do |card_name|
26
+ command_options[:name] = card_name.strip
27
+ end
28
+
29
+ opts.on("-d DESCRIPTION", String, "Description of card") do |card_description|
30
+ command_options[:description] = card_description.strip
31
+ end
32
+
33
+ opts.on("-t TYPE", String, "Type of card") do |card_type|
34
+ command_options[:card_type_name] = card_type.strip
35
+ end
36
+
37
+ opts.on("-c COMMENT", String, "Comment") do |comment|
38
+ command_options[:comment] = comment.strip
39
+ end
40
+
41
+ opts.on("-f FILE", String, "File to attach") do |file|
42
+ command_options[:file_attachment] = file.strip
43
+ end
44
+
45
+ unless project_options.empty?
46
+ opts.separator ""
47
+ opts.separator "Project Specific Options"
48
+
49
+ project_options.each do |option_pair|
50
+ option_switch = option_pair[1].downcase.gsub(/\s|_/, "-").gsub(/--|---/, '-').gsub(/--/, '-')
51
+ opts.on("--#{option_switch}", String, "Set the #{option_pair[1]} for a card") do |option|
52
+ command_options[option_pair[0]] = option
53
+ end
54
+ end
55
+
56
+ opts.separator ""
57
+ end
58
+
59
+ opts.on_tail("-h", "--help", "Show this help message.") do |help|
60
+ puts opts
61
+ exit 0 unless MINGLR_ENV == "test"
62
+ end
63
+
64
+ opts.on_tail("--version", "Show version") do
65
+ version = YAML.load(File.read(File.join(File.dirname(__FILE__), "..", "..", "VERSION.yml")))
66
+ puts "#{version[:major]}.#{version[:minor]}.#{version[:patch]}"
67
+ exit 0 unless MINGLR_ENV == "test"
68
+ end
69
+
70
+ if args.empty?
71
+ puts opts
72
+ exit 0 unless MINGLR_ENV == "test"
73
+ end
74
+ end
75
+
76
+ parser.parse! args
77
+ command_options
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,51 @@
1
+ require 'httpclient'
2
+
3
+ module Resources
4
+
5
+ class Attachment < Base
6
+
7
+ def self.configure
8
+ self.prefix += "cards/:card_number/"
9
+ end
10
+
11
+ def self.curl(command)
12
+ `#{command}`
13
+ end
14
+
15
+ def self.fetch(card_number, username, password)
16
+ if card_to_update = Card.find(card_number)
17
+ attachments = find(:all, :params => { :card_number => card_number })
18
+ attachments.each do |attachment|
19
+ url = self.site + attachment.url
20
+ url.userinfo = nil, nil
21
+ puts "Downloading #{url.to_s}:"
22
+ command = "curl --insecure --progress-bar --output #{attachment.file_name} --user #{username}:#{password} #{url}"
23
+ curl(command)
24
+ end
25
+ end
26
+ end
27
+
28
+ def self.attach(card_number, file_name, username, password)
29
+ if card_to_update = Card.find(card_number)
30
+ url = (site.to_s.gsub(/#{site.path}$/, '')) + collection_path(:card_number => card_number)
31
+ if File.exist?(file_name)
32
+ File.open(file_name) do |file|
33
+ body = { 'file' => file, "filename" => file_name }
34
+ client = HTTPClient.new
35
+ client.set_auth(nil, username, password)
36
+ response = client.post(url, body)
37
+ if response.status_code == 201
38
+ puts "File '#{file_name}' attached to card #{card_number}"
39
+ else
40
+ puts "Error attaching file '#{file_name}' to card #{card_number} (Got back HTTP code #{response.status_code})"
41
+ end
42
+ end
43
+ else
44
+ warn "Unable to open file '#{file_name}'"
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,42 @@
1
+ require "uri"
2
+
3
+ module Resources
4
+
5
+ class Base < ActiveResource::Base
6
+
7
+ def self.configure(uri_options)
8
+ uri = URI.parse(uri_options[:url])
9
+ uri.user = uri_options[:username]
10
+ uri.password = uri_options[:password]
11
+ self.site = uri
12
+ end
13
+
14
+ def self.print_collection(collection, attributes, align = :left)
15
+ output = []
16
+ longest_attributes = Array.new(attributes.length, 0)
17
+ alignment = (align == :left ? :ljust : :rjust)
18
+ collection.each do |element|
19
+ entry = []
20
+ attributes.each_with_index do |attribute, index|
21
+ attribute_value = element.send(attribute).to_s
22
+ longest_attributes[index] = attribute_value.length if attribute_value.length > longest_attributes[index]
23
+ entry << attribute_value
24
+ end
25
+ output << entry
26
+ end
27
+ output.each do |entry|
28
+ row = []
29
+ entry.each_with_index do |part, index|
30
+ row << [part.send(alignment, longest_attributes[index])]
31
+ end
32
+ puts row.join(" - ")
33
+ end
34
+ end
35
+
36
+ def self.warn(message)
37
+ puts "Warning: #{message}"
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,117 @@
1
+ module Resources
2
+
3
+ class Card < Base
4
+
5
+ def self.create(options = {}, status_property = nil)
6
+ options.merge!({status_property.to_sym => "New"}) if status_property
7
+ card = self.new(options)
8
+ if card.save
9
+ card.reload
10
+ puts "Card ##{card.number} created"
11
+ else
12
+ warn "Unable to create card"
13
+ end
14
+ end
15
+
16
+ def self.move(card_number, options = {}, config = {})
17
+ if card_to_move = find(card_number)
18
+ transition_options = { :card => card_number }
19
+ transition_options.merge!({ :comment => options[:comment]}) if options[:comment]
20
+ if config[:status_property]
21
+ current_status = card_to_move.send(config[:status_property])
22
+ else
23
+ warn "No known status of card ##{card_number}, cannot move!"
24
+ return
25
+ end
26
+ next_transition = nil
27
+
28
+ card_type = card_to_move.card_type_name.downcase
29
+ case card_type
30
+ when /defect/
31
+ status_states = config.select do |key, value|
32
+ key.to_s =~ /^defect/
33
+ end
34
+ when /task/
35
+ status_states = config.select do |key, value|
36
+ key.to_s =~ /^task_state_/
37
+ end
38
+ when /story/
39
+ status_states = config.select do |key, value|
40
+ key.to_s =~ /^story_state/
41
+ end
42
+ else
43
+ warn "No transitions defined for card of type #{card_to_move.card_type_name}"
44
+ return
45
+ end
46
+ status_states = status_states.collect {|state| state.last }.collect {|state| state.split(">").collect { |value| value.strip } }
47
+ next_transition = status_states.select {|state| state.first.downcase == current_status.downcase }.first.last
48
+ transition_options.merge!({ :transition => next_transition })
49
+
50
+ if response = TransitionExecution.create(transition_options)
51
+ if response.attributes["status"] == "completed"
52
+ puts "Moved card from #{current_status} to #{next_transition}"
53
+ end
54
+ end
55
+ else
56
+ warn "No card ##{card_number} found to move"
57
+ end
58
+ end
59
+
60
+ def self.print_all(options = [], status_property = nil)
61
+ attributes = [:number, :card_type_name, status_property, :name].compact
62
+ cards = find(:all)
63
+ cards.send(:extend, Minglr::Extensions::Array)
64
+ cards = cards.filter(attributes, options)
65
+ if cards.any?
66
+ print_collection(cards, attributes)
67
+ else
68
+ warn "No cards found"
69
+ end
70
+ end
71
+
72
+ def self.print_card(card_number, status_property = nil)
73
+ if card = find(card_number.to_i)
74
+ puts card.to_s(status_property)
75
+ else
76
+ warn "No card ##{card_number} found"
77
+ end
78
+ end
79
+
80
+ def self.update(card_number, options = {})
81
+ if card_to_update = find(card_number)
82
+ options.each do |attribute, value|
83
+ card_to_update.send("#{attribute.to_s}=".to_sym, value)
84
+ end
85
+ card_to_update.save
86
+ puts "Card ##{card_to_update.number} updated\n\n"
87
+ puts card_to_update.to_s
88
+ else
89
+ warn "Unable to update card ##{card_number}"
90
+ end
91
+ end
92
+
93
+ def to_s(status_property = nil)
94
+ attachments = []
95
+ begin
96
+ attachments = Attachment.find(:all, :params => { :card_number => number })
97
+ attachments = attachments.collect do |attachment|
98
+ "* #{attachment.file_name}: #{Resources::Base.site + attachment.url}"
99
+ end
100
+ rescue ActiveResource::ResourceNotFound => error
101
+ attachments = ["N/A"]
102
+ end
103
+ output = <<-EOS
104
+ Number: #{number}
105
+ Name: #{name}
106
+ Type: #{card_type_name.nil? ? "N/A" : card_type_name}
107
+ Status: #{send(status_property) if status_property && self.respond_to?(status_property)}
108
+ Description: #{description.nil? ? "N/A" : description}
109
+
110
+ Attachments:
111
+ #{attachments.join("\n")}
112
+ EOS
113
+ output
114
+ end
115
+
116
+ end
117
+ end