git-jira 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/bin/git-jira +199 -0
  3. metadata +46 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6aca75559bbe16627be69d2d9179d46bca592050
4
+ data.tar.gz: 2e7ffedc35bb5820718deb03e85996763262a185
5
+ SHA512:
6
+ metadata.gz: 3828ada68d80f931bf27650a3e1f559f6d95904c400299f49c56f4aba294113ff1645cbfc142535fbf35a9493602a7015bdb76f2d098f46533ac0d4047e799fc
7
+ data.tar.gz: 9fba0f88c01611c966ebb1b2a5ef11dcabc1faa9aa89458295156624d8131880ec1a4d79a62c88a192e94006a287f44eab55909ae525108fca5fa78389e5e84e
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'tempfile'
4
+ class StoryNotFound < StandardError; end
5
+
6
+ begin
7
+ config = YAML.load_file(File.expand_path('~/.jira.d/config.yml'))
8
+ config['user'] ||= ENV['USER']
9
+ rescue
10
+ puts 'Could not load config file ~/.jira.d/config.yml'
11
+ puts 'Please make sure it is as described in these instructions:'
12
+ puts ' https://github.com/Netflix-Skunkworks/go-jira'
13
+ end
14
+
15
+ command = ARGV.shift
16
+
17
+ def commit_footer(story)
18
+ tag = case story['issuetype']
19
+ when 'Bug'
20
+ "[Fixes #{story['issue']}]"
21
+ else
22
+ "[Finishes #{story['issue']}]"
23
+ end
24
+ tag += ' -'
25
+ tag_indent = tag.length + 1
26
+ tag_line_length = tag.length
27
+
28
+ story['summary'].split(' ').each do |summary_word|
29
+ if (tag_line_length + summary_word.length) >= 72
30
+ tag += "\n#{' ' * tag_indent}#{summary_word}"
31
+ tag_line_length = tag_indent + summary_word.length + 1
32
+ else
33
+ tag += " #{summary_word}"
34
+ tag_line_length += summary_word.length + 1
35
+ end
36
+ end
37
+
38
+ tag
39
+ end
40
+
41
+ def load_story(story)
42
+ body = `jira view #{story} 2>&1`
43
+
44
+ if body =~ /404 Not Found/
45
+ raise StoryNotFound, "Story #{story} not found"
46
+ end
47
+
48
+ begin
49
+ YAML.load(body)
50
+ rescue Psych::SyntaxError
51
+ # go-jira's YAML is not quite equal to ruby's YAML - if the body of the
52
+ # description or comments fields has a " foo:" line, then ruby won't be
53
+ # able to parse it
54
+ # HACK: replace those fields with a double-quoted value.
55
+ body = body.each_line.map do |line|
56
+ # if the line contains colons or brackets or quotes, escape them
57
+ match = line.match(/^([a-z]+): .*(: |[<>'"\[\]])/)
58
+ if match
59
+ "#{match[1]}: #{line[(match[1].length + 2)..-1].strip.inspect}"
60
+ else
61
+ line
62
+ end
63
+ end.join("\n")
64
+
65
+ return YAML.load(body)
66
+ end
67
+ end
68
+
69
+ def story_branches
70
+ `git branch`
71
+ .strip.each_line.map(&:split).map(&:last)
72
+ .delete_if { |b| !matches_story?(b) }
73
+ .map(&:downcase)
74
+ end
75
+
76
+ def matches_story?(string)
77
+ string =~ /[a-z]{2,3}-[0-9]+/i
78
+ end
79
+
80
+ def current_branch
81
+ `git rev-parse --abbrev-ref HEAD`.strip
82
+ end
83
+
84
+ def confirm?
85
+ $stdout.write 'Go? [y/N]: '
86
+ exit unless $stdin.gets.chomp =~ /y/i
87
+ $stdout.puts
88
+ end
89
+
90
+ def pick_story_of_mine
91
+ my_stories = `jira ls -q 'assignee = currentUser() AND (status = "To Do" OR status = Backlog OR status = "In Progress")'`.strip.split("\n")
92
+ branches = story_branches
93
+ my_stories = my_stories
94
+ .map { |s| s.split(':', 2) }
95
+ .find_all { |s| !branches.include?(s[0].downcase) }
96
+ puts "which story to start?"
97
+ my_stories.each_with_index do |story, i|
98
+ puts "#{i}: #{story.join(' ')}"
99
+ end
100
+ my_stories[gets.strip.to_i][0]
101
+ end
102
+
103
+ def time_ago_in_words(distance_in_seconds)
104
+ distance_in_minutes = (distance_in_seconds / 60.0).round
105
+ distance_in_hours = (distance_in_minutes / 60.0).round
106
+ distance_in_days = (distance_in_hours / 24.0).floor
107
+
108
+ case distance_in_seconds
109
+ when 0..60
110
+ 'less than a minute ago'
111
+ when 60..2700
112
+ "about #{distance_in_minutes} minutes ago"
113
+ when 2700..86400
114
+ "about #{(distance_in_minutes / 60).round} hours ago"
115
+ else
116
+ "#{distance_in_days} days ago"
117
+ end
118
+ end
119
+
120
+ case command
121
+ when 'start'
122
+ story_id = ARGV.shift || pick_story_of_mine
123
+ story = load_story(story_id)
124
+ puts "Starting story #{story_id}:"
125
+ puts " summary: #{story['summary']}"
126
+ puts " reporter: #{story['reporter']}"
127
+ puts " created: #{time_ago_in_words(Time.now - story['created'])}"
128
+ puts
129
+
130
+ if story_branches.include?(story_id.downcase)
131
+ `git checkout #{story_id}`
132
+ else
133
+ `git checkout -b #{story_id} origin/master` unless current_branch == story_id
134
+ end
135
+
136
+ `jira start #{story_id}` unless story['status'] == 'In Progress'
137
+ `jira take #{story_id}` unless story['assignee'] == config['user']
138
+ when 'cleanup'
139
+ branches_to_remove = story_branches.find_all do |b|
140
+ begin
141
+ load_story(b)['status'] == 'Closed'
142
+ rescue StoryNotFound
143
+ # perhaps dangerous because of other branches in the format abc-123, but
144
+ # if a story has been deleted on JIRA then it won't be shown, and since we
145
+ # ask the user to confirm all removals, let's just assume that if the
146
+ # story doesn't exist on JIRA we should delete the branch
147
+ true
148
+ end
149
+ end
150
+
151
+ if branches_to_remove.none?
152
+ puts 'No branches to remove!'
153
+ exit
154
+ end
155
+
156
+ puts "Will remove branches: #{branches_to_remove.join(', ')}"
157
+ confirm?
158
+
159
+ `git checkout master` if branches_to_remove.include?(current_branch)
160
+ `git branch -D #{branches_to_remove.join(' ')}`
161
+ when 'ci'
162
+ unless matches_story?(current_branch)
163
+ puts "You don't appear to be on a story branch!"
164
+ exit 1
165
+ end
166
+
167
+ story = load_story(current_branch)
168
+
169
+ if story['description']
170
+ Tempfile.open('git-j-commit-msg') do |f|
171
+ f.puts story['summary']
172
+ f.puts
173
+ f.puts story['description'].strip.gsub("\n\n", "\n") # TODO: <- normalize helper
174
+ f.puts
175
+ f.puts commit_footer(story)
176
+ f.close
177
+
178
+ system("git commit -v -t #{f.path}")
179
+ end
180
+ else
181
+ puts 'Failed to load description for story!'
182
+ end
183
+ when 'ls'
184
+ branches = `git branch`.strip.each_line.map(&:split).map(&:last)
185
+ branches.delete_if { |b| !matches_story?(b) }
186
+ branches.map do |branch|
187
+ puts "#{branch.ljust(8)} - #{load_story(branch)['summary']}"
188
+ end
189
+ when 'show'
190
+ branch = current_branch
191
+ if story_branches.include?(branch)
192
+ puts load_story(branch).inspect
193
+ else
194
+ echo 'Could not parse story branch!'
195
+ end
196
+ else
197
+ puts 'unknown command!'
198
+ exit 1
199
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git-jira
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Dooner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: tom.dooner@brigade.com
15
+ executables:
16
+ - git-jira
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/git-jira
21
+ homepage: https://github.com/tdooner/git-jira
22
+ licenses:
23
+ - MIT
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib/git-jira.rb
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.4.5.1
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Workflow tools for integrating git and jira
45
+ test_files: []
46
+ has_rdoc: