git-jira 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/git-jira +199 -0
- metadata +46 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/git-jira
ADDED
@@ -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:
|