git-pivotal-tracker 0.9.0

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 (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