git-pivotal-tracker 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/.gitignore +5 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +3 -0
  5. data/CHANGELOG +45 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +74 -0
  8. data/LICENSE +21 -0
  9. data/Rakefile +24 -0
  10. data/VERSION +1 -0
  11. data/bin/git-accept +7 -0
  12. data/bin/git-block +7 -0
  13. data/bin/git-finish +7 -0
  14. data/bin/git-info +7 -0
  15. data/bin/git-start +8 -0
  16. data/bin/git-unblock +7 -0
  17. data/features/accept.feature +140 -0
  18. data/features/block.feature +94 -0
  19. data/features/finish.feature +119 -0
  20. data/features/info.feature +99 -0
  21. data/features/start.feature +113 -0
  22. data/features/step_definitions/steps.rb +114 -0
  23. data/features/support/dsl/assertions.rb +11 -0
  24. data/features/support/dsl/data.rb +11 -0
  25. data/features/support/dsl/pivotal.rb +76 -0
  26. data/features/support/env.rb +6 -0
  27. data/features/support/git-pivotal.rb +69 -0
  28. data/features/test_repo/origin.git/COMMIT_EDITMSG +1 -0
  29. data/features/test_repo/origin.git/HEAD +1 -0
  30. data/features/test_repo/origin.git/config +8 -0
  31. data/features/test_repo/origin.git/description +1 -0
  32. data/features/test_repo/origin.git/hooks/applypatch-msg.sample +15 -0
  33. data/features/test_repo/origin.git/hooks/commit-msg.sample +24 -0
  34. data/features/test_repo/origin.git/hooks/post-commit.sample +8 -0
  35. data/features/test_repo/origin.git/hooks/post-receive.sample +15 -0
  36. data/features/test_repo/origin.git/hooks/post-update.sample +8 -0
  37. data/features/test_repo/origin.git/hooks/pre-applypatch.sample +14 -0
  38. data/features/test_repo/origin.git/hooks/pre-commit.sample +46 -0
  39. data/features/test_repo/origin.git/hooks/pre-rebase.sample +169 -0
  40. data/features/test_repo/origin.git/hooks/prepare-commit-msg.sample +36 -0
  41. data/features/test_repo/origin.git/hooks/update.sample +128 -0
  42. data/features/test_repo/origin.git/index +0 -0
  43. data/features/test_repo/origin.git/info/exclude +6 -0
  44. data/features/test_repo/origin.git/logs/HEAD +1 -0
  45. data/features/test_repo/origin.git/logs/refs/heads/master +1 -0
  46. data/features/test_repo/origin.git/objects/0c/6f7b1384910d1a2f137590095f008a06c7e00c +0 -0
  47. data/features/test_repo/origin.git/objects/10/ecf2b7ce989f01f3f7266e712b48d9275f2635 +0 -0
  48. data/features/test_repo/origin.git/objects/a5/71d56305df09fb060f6ccb730b46080d305beb +0 -0
  49. data/features/test_repo/origin.git/refs/heads/master +1 -0
  50. data/features/test_repo/readme +1 -0
  51. data/features/unblock.feature +68 -0
  52. data/git-pivotal-tracker.gemspec +27 -0
  53. data/lib/commands/accept.rb +76 -0
  54. data/lib/commands/base.rb +128 -0
  55. data/lib/commands/block.rb +59 -0
  56. data/lib/commands/bug.rb +19 -0
  57. data/lib/commands/card.rb +32 -0
  58. data/lib/commands/chore.rb +19 -0
  59. data/lib/commands/feature.rb +19 -0
  60. data/lib/commands/finish.rb +59 -0
  61. data/lib/commands/info.rb +58 -0
  62. data/lib/commands/map.rb +10 -0
  63. data/lib/commands/pick.rb +76 -0
  64. data/lib/commands/start.rb +35 -0
  65. data/lib/commands/unblock.rb +55 -0
  66. data/lib/git-pivotal-tracker.rb +11 -0
  67. data/readme.markdown +95 -0
  68. data/spec/commands/base_spec.rb +151 -0
  69. data/spec/commands/bug_spec.rb +24 -0
  70. data/spec/commands/chore_spec.rb +24 -0
  71. data/spec/commands/feature_spec.rb +24 -0
  72. data/spec/commands/finish_spec.rb +125 -0
  73. data/spec/commands/map_spec.rb +14 -0
  74. data/spec/commands/start_spec.rb +29 -0
  75. data/spec/factories.rb +13 -0
  76. data/spec/factory.rb +26 -0
  77. data/spec/spec_helper.rb +24 -0
  78. metadata +251 -0
@@ -0,0 +1,59 @@
1
+ require 'commands/base'
2
+
3
+ module Commands
4
+ class Block < Base
5
+ Label = "blocked"
6
+ MessagePrefix = "Blocked:"
7
+
8
+ def initialize(*args)
9
+ @story_id = args.shift if args.first =~ /^(\d+)$/
10
+ super(*args)
11
+ end
12
+
13
+ def run!
14
+ super
15
+
16
+ unless story_id
17
+ put "No story id was supplied and you aren't on a topic branch!"
18
+ return 1
19
+ end
20
+
21
+ if story.labels.to_s.include?(Label)
22
+ put "Story #{story_id} is already blocked."
23
+ return 0
24
+ end
25
+
26
+ message = options[:message].to_s
27
+
28
+ if message.empty?
29
+ loop do
30
+ put "What's the reason for blocking this story?"
31
+ message = input.gets.chomp
32
+ break unless message.empty?
33
+ put ""
34
+ end
35
+ end
36
+
37
+ labels = story.labels.to_s.split(",").concat([Label]).join(",")
38
+ story.update :labels => labels
39
+ story.notes.create :author => full_name, :text => "#{MessagePrefix} #{message}"
40
+ put "Story #{story_id} has been blocked."
41
+
42
+ return 0
43
+ end
44
+
45
+ protected
46
+
47
+ def on_parse(opts)
48
+ opts.on("-m [String]", "--message [String]", "The message to provide when blocking a story."){ |m| options[:message] = m }
49
+ end
50
+
51
+ def story_id
52
+ @story_id || current_branch[/\d+/]
53
+ end
54
+
55
+ def story
56
+ @story ||= project.stories.find(story_id)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,19 @@
1
+ require 'commands/pick'
2
+
3
+ module Commands
4
+ class Bug < Pick
5
+
6
+ def type
7
+ "bug"
8
+ end
9
+
10
+ def plural_type
11
+ "bugs"
12
+ end
13
+
14
+ def branch_suffix
15
+ "bugfix"
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ require 'commands/pick'
2
+
3
+ module Commands
4
+ class Card < Pick
5
+ attr_accessor :story_id
6
+
7
+ def type
8
+ "story"
9
+ end
10
+
11
+ def plural_type
12
+ "cards"
13
+ end
14
+
15
+ def branch_suffix
16
+ if story.story_type == "bug"
17
+ "bugfix"
18
+ else
19
+ story.story_type
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ def story
26
+ return @story if @story
27
+ raise ArgumentError, "No story id was given!" unless story_id
28
+ project.stories.find(story_id)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ require 'commands/pick'
2
+
3
+ module Commands
4
+ class Chore < Pick
5
+
6
+ def type
7
+ "chore"
8
+ end
9
+
10
+ def plural_type
11
+ "chores"
12
+ end
13
+
14
+ def branch_suffix
15
+ "chore"
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'commands/pick'
2
+
3
+ module Commands
4
+ class Feature < Pick
5
+
6
+ def type
7
+ "feature"
8
+ end
9
+
10
+ def plural_type
11
+ "features"
12
+ end
13
+
14
+ def branch_suffix
15
+ "feature"
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ require 'commands/base'
2
+
3
+ module Commands
4
+ class Finish < Base
5
+
6
+ def run!
7
+ super
8
+
9
+ unless story_id
10
+ put "Branch name must contain a Pivotal Tracker story id"
11
+ return 1
12
+ end
13
+
14
+ put "Marking Story #{story_id} as finished..."
15
+ if story.update(:current_state => finished_state)
16
+ topic_branch = current_branch
17
+
18
+ put "Pushing #{topic_branch} to #{remote}"
19
+ sys "git push --set-upstream #{remote} #{topic_branch}"
20
+
21
+ put "Pulling #{acceptance_branch}..."
22
+ sys "git checkout #{acceptance_branch}"
23
+ sys "git pull"
24
+
25
+ put "Merging #{topic_branch} into #{acceptance_branch}"
26
+ sys "git merge --no-ff #{topic_branch}"
27
+
28
+ put "Pushing #{acceptance_branch} to #{remote}"
29
+ sys "git push"
30
+
31
+ put "Now on #{acceptance_branch}."
32
+
33
+ return 0
34
+ else
35
+ put "Unable to mark Story #{story_id} as finished"
36
+
37
+ return 1
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ def finished_state
44
+ if story.story_type == "chore"
45
+ "accepted"
46
+ else
47
+ "finished"
48
+ end
49
+ end
50
+
51
+ def story_id
52
+ match = current_branch[/\d+/] and match.to_i
53
+ end
54
+
55
+ def story
56
+ @story ||= project.stories.find(story_id)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,58 @@
1
+ require 'commands/base'
2
+
3
+ module Commands
4
+ class Info < Base
5
+ def initialize(*args)
6
+ @story_id = args.shift if args.first =~ /^(\d+)$/
7
+ super(*args)
8
+ end
9
+
10
+ def run!
11
+ super
12
+
13
+ unless story_id
14
+ put "No story id was supplied and you aren't on a topic branch!"
15
+ return 1
16
+ end
17
+
18
+ put "Story: #{story.name}"
19
+ put "URL: #{story.url}"
20
+ put "Labels: #{story.labels.split(',').join(', ')}" if story.labels
21
+ put "State: #{story.accepted_at ? 'accepted' : 'not accepted'}"
22
+
23
+ colwidth = 74
24
+
25
+ put "\nDescription:\n"
26
+ put wrap_text("#{story.description}\n", colwidth).gsub(/^/, ' ').chomp
27
+
28
+ if options[:comments]
29
+ put "\nComments:\n"
30
+ story.notes.all.each do |note|
31
+ @output.printf " %-37s%37s\n\n", note.author, note.noted_at.strftime("%b %e, %Y %k:%M%p")
32
+ put wrap_text(note.text, colwidth - 2).gsub(/^/, ' ')
33
+ end
34
+ end
35
+
36
+ return 0
37
+ end
38
+
39
+ protected
40
+
41
+ def wrap_text(txt, col = 80)
42
+ txt.gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/,
43
+ "\\1\\3\n")
44
+ end
45
+
46
+ def on_parse(opts)
47
+ opts.on("-c", "--comments", "Display comments"){ |v| options[:comments] = v }
48
+ end
49
+
50
+ def story_id
51
+ @story_id || current_branch[/\d+/]
52
+ end
53
+
54
+ def story
55
+ @story ||= project.stories.find(story_id)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,10 @@
1
+ module Commands
2
+ class Map < Hash
3
+ def [](arg)
4
+ super || (
5
+ key = keys.detect{ |e| e.respond_to?(:match) && e.match(arg) }
6
+ super key
7
+ )
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,76 @@
1
+ require 'commands/base'
2
+
3
+ module Commands
4
+ class Pick < Base
5
+
6
+ def type
7
+ raise Error("must define in subclass")
8
+ end
9
+
10
+ def plural_type
11
+ raise Error("must define in subclass")
12
+ end
13
+
14
+ def branch_suffix
15
+ raise Error("must define in subclass")
16
+ end
17
+
18
+ def run!
19
+ response = super
20
+ return response if response > 0
21
+
22
+ msg = "Retrieving latest #{plural_type} from Pivotal Tracker"
23
+ if options[:only_mine]
24
+ msg += " for #{options[:full_name]}"
25
+ end
26
+ put "#{msg}..."
27
+
28
+ unless story
29
+ put "No #{plural_type} available!"
30
+ return 0
31
+ end
32
+
33
+ put "Story: #{story.name}"
34
+ put "URL: #{story.url}"
35
+
36
+ put "Updating #{type} status in Pivotal Tracker..."
37
+ story.update(:owned_by => options[:full_name], :current_state => :started)
38
+
39
+ if story.errors.empty?
40
+ suffix_or_prefix = ""
41
+ unless options[:quiet] || options[:defaults]
42
+ put "Enter branch name (will be #{options[:append_name] ? 'appended' : 'prepended'} by #{story.id}) [#{suffix_or_prefix}]: ", false
43
+ suffix_or_prefix = input.gets.chomp
44
+ end
45
+ suffix_or_prefix = branch_suffix if suffix_or_prefix == ""
46
+
47
+ if options[:append_name]
48
+ branch = "#{suffix_or_prefix}-#{story.id}"
49
+ else
50
+ branch = "#{story.id}-#{suffix_or_prefix}"
51
+ end
52
+ if get("git branch").match(branch).nil?
53
+ put "Switched to a new branch '#{branch}'"
54
+ sys "git checkout -b #{branch}"
55
+ end
56
+
57
+ return 0
58
+ else
59
+ put "Unable to mark #{type} as started"
60
+ put "\t" + story.errors.to_a.join("\n\t")
61
+
62
+ return 1
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ def story
69
+ return @story if @story
70
+
71
+ conditions = { :story_type => type, :current_state => "unstarted", :limit => 1, :offset => 0 }
72
+ conditions[:owned_by] = options[:full_name] if options[:only_mine]
73
+ @story = project.stories.all(conditions).first
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,35 @@
1
+ require 'commands/map'
2
+ require 'commands/bug'
3
+ require 'commands/card'
4
+ require 'commands/chore'
5
+ require 'commands/feature'
6
+
7
+ module Commands
8
+ class Start
9
+ COMMAND_MAP = Map.new.merge({
10
+ "bug" => Commands::Bug,
11
+ "chore" => Commands::Chore,
12
+ "feature" => Commands::Feature,
13
+ /^\d+$/ => Commands::Card
14
+ })
15
+
16
+ class << self
17
+ def for(*args)
18
+ identifier = args.shift
19
+ construct_instance_for(identifier, args) ||
20
+ raise(ArgumentError, "Unknown card identifier given: #{identifier}")
21
+ end
22
+
23
+ private
24
+
25
+ def construct_instance_for(identifier, args)
26
+ if klass=COMMAND_MAP[identifier]
27
+ instance = klass.new(*args)
28
+ instance.story_id = identifier if instance.respond_to?(:story_id=)
29
+ instance
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,55 @@
1
+ require 'commands/base'
2
+ require 'commands/block'
3
+
4
+ module Commands
5
+ class Unblock < Base
6
+ PlaceholderLabel = "."
7
+
8
+ def initialize(*args)
9
+ @story_id = args.shift if args.first =~ /^(\d+)$/
10
+ super(*args)
11
+ end
12
+
13
+ def run!
14
+ super
15
+
16
+ unless story_id
17
+ put "No story id was supplied and you aren't on a topic branch!"
18
+ return 1
19
+ end
20
+
21
+ unless story.labels.to_s.include?(Block::Label)
22
+ put "Story #{story_id} is already unblocked."
23
+ return 0
24
+ end
25
+
26
+ labels = story.labels.to_s.split(",") - [Block::Label]
27
+
28
+ # this line is to work aroudn Pivotal Tracker's broken API for removing the last
29
+ # label on a card. http://community.pivotaltracker.com/pivotal/topics/api_v3_cannot_remove_labels_from_a_story_anymore
30
+ if labels.empty?
31
+ labels << PlaceholderLabel
32
+ put "Note: a '.' label will be placed on this card due to a bug in the v3 API of Pivotal Tracker."
33
+ end
34
+
35
+ story.update :labels => labels.join(",")
36
+ put "Story #{story_id} has been unblocked."
37
+
38
+ return 0
39
+ end
40
+
41
+ protected
42
+
43
+ def on_parse(opts)
44
+ opts.on("-m [String]", "--message [String]", "The message to provide when blocking a story."){ |m| options[:message] = m }
45
+ end
46
+
47
+ def story_id
48
+ @story_id || current_branch[/\d+/]
49
+ end
50
+
51
+ def story
52
+ @story ||= project.stories.find(story_id)
53
+ end
54
+ end
55
+ end