jiraMule 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/Gemfile +17 -0
- data/LICENSE +22 -0
- data/README.md +37 -0
- data/Rakefile +95 -0
- data/TODO.taskpaper +6 -0
- data/bin/jm +87 -0
- data/jiraMule.gemspec +43 -0
- data/lib/JiraMule/Tempo.rb +75 -0
- data/lib/JiraMule/commands/assign.rb +38 -0
- data/lib/JiraMule/commands/githubImport.rb +46 -0
- data/lib/JiraMule/commands/link.rb +41 -0
- data/lib/JiraMule/commands/timesheet.rb +113 -0
- data/lib/JiraMule/gb.rb +33 -0
- data/lib/JiraMule.rb +7 -0
- data/lib/jiraMule/Config.rb +224 -0
- data/lib/jiraMule/Passwords.rb +81 -0
- data/lib/jiraMule/commands/attach.rb +69 -0
- data/lib/jiraMule/commands/config.rb +68 -0
- data/lib/jiraMule/commands/goto.rb +166 -0
- data/lib/jiraMule/commands/kanban.rb +243 -0
- data/lib/jiraMule/commands/logWork.rb +39 -0
- data/lib/jiraMule/commands/next.rb +89 -0
- data/lib/jiraMule/commands/progress.rb +68 -0
- data/lib/jiraMule/commands/query.rb +80 -0
- data/lib/jiraMule/commands/release.rb +44 -0
- data/lib/jiraMule/commands/testReady.rb +68 -0
- data/lib/jiraMule/commands.rb +20 -0
- data/lib/jiraMule/gitUtils.rb +11 -0
- data/lib/jiraMule/http.rb +147 -0
- data/lib/jiraMule/jiraUtils.rb +288 -0
- data/lib/jiraMule/verbosing.rb +28 -0
- data/lib/jiraMule/version.rb +3 -0
- metadata +268 -0
data/lib/JiraMule.rb
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'inifile'
|
3
|
+
|
4
|
+
module JiraMule
|
5
|
+
class Config
|
6
|
+
#
|
7
|
+
# internal transient this-run-only things (also -c options)
|
8
|
+
# specified from --configfile
|
9
|
+
# env from ENV['JM_CONFIGFILE']
|
10
|
+
# project .jiramulerc at project dir
|
11
|
+
# user .jiramulerc at $HOME
|
12
|
+
# system .jiramulerc at /etc
|
13
|
+
# defaults Internal hardcoded defaults
|
14
|
+
#
|
15
|
+
ConfigFile = Struct.new(:kind, :path, :data) do
|
16
|
+
def load()
|
17
|
+
return if kind == :internal
|
18
|
+
return if kind == :defaults
|
19
|
+
self[:path] = Pathname.new(path) unless path.kind_of? Pathname
|
20
|
+
self[:data] = IniFile.new(:filename=>path.to_s) if self[:data].nil?
|
21
|
+
self[:data].restore
|
22
|
+
end
|
23
|
+
|
24
|
+
def write()
|
25
|
+
return if kind == :internal
|
26
|
+
return if kind == :defaults
|
27
|
+
self[:path] = Pathname.new(path) unless path.kind_of? Pathname
|
28
|
+
self[:data] = IniFile.new(:filename=>path.to_s) if self[:data].nil?
|
29
|
+
self[:data].save
|
30
|
+
path.chmod(0600)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr :paths
|
35
|
+
attr_reader :projectDir
|
36
|
+
|
37
|
+
CFG_SCOPES=%w{internal specified env project private user system defaults}.map{|i| i.to_sym}.freeze
|
38
|
+
CFG_FILE_NAME = '.jiramulerc'.freeze
|
39
|
+
CFG_DIR_NAME = '.jiramule'.freeze
|
40
|
+
CFG_ALTRC_NAME = '.jiramule/config'.freeze
|
41
|
+
CFG_SYS_NAME = '/etc/jiramulerc'.freeze
|
42
|
+
|
43
|
+
def initialize
|
44
|
+
@paths = []
|
45
|
+
@paths << ConfigFile.new(:internal, nil, IniFile.new())
|
46
|
+
# :specified --configfile FILE goes here. (see load_specific)
|
47
|
+
unless ENV['JM_CONFIGFILE'].nil? then
|
48
|
+
# if it exists, must be a file
|
49
|
+
# if it doesn't exist, that's ok
|
50
|
+
ep = Pathname.new(ENV['JM_CONFIGFILE'])
|
51
|
+
if ep.file? or not ep.exist? then
|
52
|
+
@paths << ConfigFile.new(:env, ep)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
@projectDir = findProjectDir()
|
56
|
+
unless @projectDir.nil? then
|
57
|
+
@paths << ConfigFile.new(:project, @projectDir + CFG_FILE_NAME)
|
58
|
+
fixModes(@projectDir + CFG_DIR_NAME)
|
59
|
+
end
|
60
|
+
@paths << ConfigFile.new(:user, Pathname.new(Dir.home) + CFG_FILE_NAME)
|
61
|
+
fixModes(Pathname.new(Dir.home) + CFG_DIR_NAME)
|
62
|
+
@paths << ConfigFile.new(:system, Pathname.new(CFG_SYS_NAME))
|
63
|
+
@paths << ConfigFile.new(:defaults, nil, IniFile.new())
|
64
|
+
|
65
|
+
|
66
|
+
set('tool.verbose', false, :defaults)
|
67
|
+
set('tool.debug', false, :defaults)
|
68
|
+
set('tool.dry', false, :defaults)
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
## Find the root of this project Directory.
|
73
|
+
#
|
74
|
+
# The Project dir is the directory between PWD and HOME that has one of (in
|
75
|
+
# order of preference):
|
76
|
+
# - .jiramulerc
|
77
|
+
# - .jiramule/config
|
78
|
+
# - .jiramule/
|
79
|
+
# - .git/
|
80
|
+
def findProjectDir()
|
81
|
+
result=nil
|
82
|
+
fileNames=[CFG_FILE_NAME, CFG_ALTRC_NAME]
|
83
|
+
dirNames=[CFG_DIR_NAME]
|
84
|
+
home = Pathname.new(Dir.home)
|
85
|
+
pwd = Pathname.new(Dir.pwd)
|
86
|
+
return nil if home == pwd
|
87
|
+
pwd.dirname.ascend do |i|
|
88
|
+
break unless result.nil?
|
89
|
+
break if i == home
|
90
|
+
fileNames.each do |f|
|
91
|
+
if (i + f).exist? then
|
92
|
+
result = i
|
93
|
+
end
|
94
|
+
end
|
95
|
+
dirNames.each do |f|
|
96
|
+
if (i + f).directory? then
|
97
|
+
result = i
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# If nothing found, do a last ditch try by looking for .git/
|
103
|
+
if result.nil? then
|
104
|
+
pwd.dirname.ascend do |i|
|
105
|
+
break unless result.nil?
|
106
|
+
break if i == home
|
107
|
+
if (i + '.git').directory? then
|
108
|
+
result = i
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Now if nothing found, assume it will live in pwd.
|
114
|
+
result = Pathname.new(Dir.pwd) if result.nil?
|
115
|
+
return result
|
116
|
+
end
|
117
|
+
private :findProjectDir
|
118
|
+
|
119
|
+
def fixModes(path)
|
120
|
+
if path.directory? then
|
121
|
+
path.chmod(0700)
|
122
|
+
elsif path.file? then
|
123
|
+
path.chmod(0600)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def file_at(name, scope=:project)
|
128
|
+
case scope
|
129
|
+
when :internal
|
130
|
+
root = nil
|
131
|
+
when :specified
|
132
|
+
root = nil
|
133
|
+
when :project
|
134
|
+
root = @projectDir + CFG_DIR_NAME
|
135
|
+
when :user
|
136
|
+
root = Pathname.new(Dir.home) + CFG_DIR_NAME
|
137
|
+
when :system
|
138
|
+
root = nil
|
139
|
+
when :defaults
|
140
|
+
root = nil
|
141
|
+
end
|
142
|
+
return nil if root.nil?
|
143
|
+
root.mkpath
|
144
|
+
root + name
|
145
|
+
end
|
146
|
+
|
147
|
+
## Load all of the potential config files
|
148
|
+
def load()
|
149
|
+
# - read/write config file in [Project, User, System] (all are optional)
|
150
|
+
@paths.each { |cfg| cfg.load }
|
151
|
+
end
|
152
|
+
|
153
|
+
## Load specified file into the config stack
|
154
|
+
# This can be called multiple times and each will get loaded into the config
|
155
|
+
def load_specific(file)
|
156
|
+
spc = ConfigFile.new(:specified, Pathname.new(file))
|
157
|
+
spc.load
|
158
|
+
@paths.insert(1, spc)
|
159
|
+
end
|
160
|
+
|
161
|
+
## Get a value for key, looking at the specificed scopes
|
162
|
+
# key is <section>.<key>
|
163
|
+
def get(key, scope=CFG_SCOPES)
|
164
|
+
scope = [scope] unless scope.kind_of? Array
|
165
|
+
paths = @paths.select{|p| scope.include? p.kind}
|
166
|
+
|
167
|
+
section, ikey = key.split('.')
|
168
|
+
paths.each do |path|
|
169
|
+
if path.data.has_section?(section) then
|
170
|
+
sec = path.data[section]
|
171
|
+
return sec if ikey.nil?
|
172
|
+
if sec.has_key?(ikey) then
|
173
|
+
return sec[ikey]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
return nil
|
178
|
+
end
|
179
|
+
|
180
|
+
## Dump out a combined config
|
181
|
+
def dump()
|
182
|
+
# have a fake, merge all into it, then dump it.
|
183
|
+
base = IniFile.new()
|
184
|
+
@paths.reverse.each do |ini|
|
185
|
+
base.merge! ini.data
|
186
|
+
end
|
187
|
+
base.to_s
|
188
|
+
end
|
189
|
+
|
190
|
+
def set(key, value, scope=:project)
|
191
|
+
section, ikey = key.split('.', 2)
|
192
|
+
raise "Invalid key" if section.nil?
|
193
|
+
if not section.nil? and ikey.nil? then
|
194
|
+
# If key isn't dotted, then assume the tool section.
|
195
|
+
ikey = section
|
196
|
+
section = 'tool'
|
197
|
+
end
|
198
|
+
|
199
|
+
paths = @paths.select{|p| scope == p.kind}
|
200
|
+
raise "Unknown scope" if paths.empty?
|
201
|
+
cfg = paths.first
|
202
|
+
data = cfg.data
|
203
|
+
tomod = data[section]
|
204
|
+
tomod[ikey] = value unless value.nil?
|
205
|
+
tomod.delete(ikey) if value.nil?
|
206
|
+
data[section] = tomod
|
207
|
+
cfg.write
|
208
|
+
end
|
209
|
+
|
210
|
+
# key is <section>.<key>
|
211
|
+
def [](key)
|
212
|
+
get(key)
|
213
|
+
end
|
214
|
+
|
215
|
+
# For setting internal, this-run-only values
|
216
|
+
def []=(key, value)
|
217
|
+
set(key, value, :internal)
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
# vim: set ai et sw=2 ts=2 :
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'date'
|
5
|
+
require 'pathname'
|
6
|
+
require 'yaml'
|
7
|
+
require 'JiraMule/Config'
|
8
|
+
require 'JiraMule/http'
|
9
|
+
|
10
|
+
module JiraMule
|
11
|
+
class Passwords
|
12
|
+
def initialize(path)
|
13
|
+
path = Pathname.new(path) unless path.kind_of? Pathname
|
14
|
+
@path = path
|
15
|
+
@data = nil
|
16
|
+
end
|
17
|
+
def load()
|
18
|
+
if @path.exist? then
|
19
|
+
@path.chmod(0600)
|
20
|
+
@path.open('rb') do |io|
|
21
|
+
@data = YAML.load(io)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
def save()
|
26
|
+
@path.dirname.mkpath unless @path.dirname.exist?
|
27
|
+
@path.open('wb') do |io|
|
28
|
+
io << @data.to_yaml
|
29
|
+
end
|
30
|
+
@path.chmod(0600)
|
31
|
+
end
|
32
|
+
def set(host, user, pass)
|
33
|
+
unless @data.kind_of? Hash then
|
34
|
+
@data = {host=>{user=>pass}}
|
35
|
+
return
|
36
|
+
end
|
37
|
+
hd = @data[host]
|
38
|
+
if hd.nil? or not hd.kind_of?(Hash) then
|
39
|
+
@data[host] = {user=>pass}
|
40
|
+
return
|
41
|
+
end
|
42
|
+
@data[host][user] = pass
|
43
|
+
return
|
44
|
+
end
|
45
|
+
def get(host, user)
|
46
|
+
return nil unless @data.kind_of? Hash
|
47
|
+
return nil unless @data.has_key? host
|
48
|
+
return nil unless @data[host].kind_of? Hash
|
49
|
+
return nil unless @data[host].has_key? user
|
50
|
+
return @data[host][user]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Account
|
55
|
+
def loginInfo
|
56
|
+
host = $cfg['net.url']
|
57
|
+
user = $cfg['user.name']
|
58
|
+
if user.nil? then
|
59
|
+
say_error("No Jira user account found; please login")
|
60
|
+
user = ask("User name: ")
|
61
|
+
$cfg.set('user.name', user, :user)
|
62
|
+
end
|
63
|
+
pff = $cfg.file_at('passwords', :user)
|
64
|
+
pf = Passwords.new(pff)
|
65
|
+
pf.load
|
66
|
+
pws = pf.get(host, user)
|
67
|
+
if pws.nil? then
|
68
|
+
say_error("Couldn't find password for #{user}")
|
69
|
+
pws = ask("Password: ") { |q| q.echo = "*" }
|
70
|
+
pf.set(host, user, pws)
|
71
|
+
pf.save
|
72
|
+
end
|
73
|
+
{
|
74
|
+
:email => $cfg['user.name'],
|
75
|
+
:password => pws
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# vim: set ai et sw=2 ts=2 :
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'zip'
|
3
|
+
require 'JiraMule/jiraUtils'
|
4
|
+
|
5
|
+
command :attach do |c|
|
6
|
+
c.syntax = 'jm attach [options] [key] [file...]'
|
7
|
+
c.summary = 'Attach file to an Issue'
|
8
|
+
c.description = 'Attach a file to an Issue'
|
9
|
+
c.example 'Attach a file', %{jm attach BUG-1 foo.log}
|
10
|
+
c.option '-z', '--zip', 'Zip the file[s] first'
|
11
|
+
|
12
|
+
c.action do |args, options|
|
13
|
+
options.default :zip => false
|
14
|
+
|
15
|
+
jira = JiraMule::JiraUtils.new(args, options)
|
16
|
+
key = args.shift
|
17
|
+
|
18
|
+
# keys can be with or without the project prefix.
|
19
|
+
key = jira.expandKeys([key]).first
|
20
|
+
|
21
|
+
jira.printVars(:key=>key, :files=>args)
|
22
|
+
|
23
|
+
begin
|
24
|
+
if options.zip then
|
25
|
+
tf = Tempfile.new('zipped')
|
26
|
+
begin
|
27
|
+
tf.close
|
28
|
+
Zip::File.open(tf.path, Zip::File::CREATE) do |zipfile|
|
29
|
+
args.each do |file|
|
30
|
+
if File.directory?(file) then
|
31
|
+
Dir[File.join(file, '**', '**')].each do |dfile|
|
32
|
+
zipfile.add(dfile, dfile)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
zipfile.add(file, file)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
jira.attach(key, tf.path, 'application/zip', "#{Time.new.to_i}.zip")
|
41
|
+
|
42
|
+
ensure
|
43
|
+
tf.unlink
|
44
|
+
end
|
45
|
+
|
46
|
+
else
|
47
|
+
args.each do |file|
|
48
|
+
raise "Cannot send directories! #{file}" if File.directory?(file)
|
49
|
+
raise "No such file! #{file}" unless File.exists? file
|
50
|
+
mime=`file -I -b #{file}`
|
51
|
+
# if mime.nil? use ruby built in.
|
52
|
+
jira.attach(key, file, mime)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
rescue JiraMule::JiraUtilsException => e
|
57
|
+
puts "= #{e}"
|
58
|
+
puts "= #{e.request}"
|
59
|
+
puts "= #{e.response}"
|
60
|
+
puts "= #{e.response.inspect}"
|
61
|
+
puts "= #{e.response.body}"
|
62
|
+
rescue Exception => e
|
63
|
+
puts e
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# vim: set sw=2 ts=2 :
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'JiraMule/Config'
|
2
|
+
|
3
|
+
command :config do |c|
|
4
|
+
c.syntax = %{jm config [options] <key> [<new value>]}
|
5
|
+
c.summary = %{Get and set options}
|
6
|
+
c.description = %{
|
7
|
+
You can get, set, or query config options with this command. All config
|
8
|
+
options are in a 'section.key' format. There is also a layer of scopes
|
9
|
+
that the keys can be saved in.
|
10
|
+
}
|
11
|
+
|
12
|
+
c.example %{See what the current combined config is}, 'jm config --dump'
|
13
|
+
c.example %{Query a value}, 'jm config jira.project'
|
14
|
+
c.example %{Set a new value; writing to the project config file}, 'jm config jira.project XXXXXXXX'
|
15
|
+
c.example %{Set a new value; writing to the user config file}, 'jm config --user user.name my@email.address'
|
16
|
+
c.example %{Unset a value in a configfile. (lower scopes will become visible if set)},
|
17
|
+
'jm config diff.cmd --unset'
|
18
|
+
|
19
|
+
|
20
|
+
c.option '--system', 'Use only the system config file. (/etc/jiramulerc)'
|
21
|
+
c.option '--user', 'Use only the config file in $HOME (.jiramulerc)'
|
22
|
+
c.option '--project', 'Use only the config file in the project (.jiramulerc)'
|
23
|
+
c.option '--env', 'Use only the config file from $JM_CONFIGFILE'
|
24
|
+
c.option '--specified', 'Use only the config file from the --config option.'
|
25
|
+
|
26
|
+
c.option '--unset', 'Remove key from config file.'
|
27
|
+
c.option '--dump', 'Dump the current combined view of the config'
|
28
|
+
|
29
|
+
c.action do |args, options|
|
30
|
+
|
31
|
+
if options.dump then
|
32
|
+
puts $cfg.dump()
|
33
|
+
elsif args.count == 0 then
|
34
|
+
say_error "Need a config key"
|
35
|
+
elsif args.count == 1 and not options.unset then
|
36
|
+
options.defaults :system=>false, :user=>false, :project=>false,
|
37
|
+
:specified=>false, :env=>false
|
38
|
+
|
39
|
+
# For read, if no scopes, than all. Otherwise just those specified
|
40
|
+
scopes = []
|
41
|
+
scopes << :system if options.system
|
42
|
+
scopes << :user if options.user
|
43
|
+
scopes << :project if options.project
|
44
|
+
scopes << :env if options.env
|
45
|
+
scopes << :specified if options.specified
|
46
|
+
scopes = JiraMule::Config::CFG_SCOPES if scopes.empty?
|
47
|
+
|
48
|
+
say $cfg.get(args[0], scopes)
|
49
|
+
else
|
50
|
+
|
51
|
+
options.defaults :system=>false, :user=>false, :project=>true,
|
52
|
+
:specified=>false, :env=>false
|
53
|
+
# For write, if scope is specified, only write to that scope.
|
54
|
+
scope = :project
|
55
|
+
scope = :system if options.system
|
56
|
+
scope = :user if options.user
|
57
|
+
scope = :project if options.project
|
58
|
+
scope = :env if options.env
|
59
|
+
scope = :specified if options.specified
|
60
|
+
|
61
|
+
args[1] = nil if options.unset
|
62
|
+
$cfg.set(args[0], args[1], scope)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
# vim: set ai et sw=2 ts=2 :
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'vine'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
command :goto do |c|
|
5
|
+
c.syntax = 'jm goto [options] [status] [keys]'
|
6
|
+
c.summary = 'Move issue to a status; making multiple transitions if needed'
|
7
|
+
c.description = %{
|
8
|
+
Named for the bad command that sometimes there is nothing better to use.
|
9
|
+
|
10
|
+
Your issue has a status X, and you need it in Y, and there are multiple steps from
|
11
|
+
X to Y. Why would you do something a computer can do better? Hence goto.
|
12
|
+
|
13
|
+
The down side is there is no good way to automatically get mutli-step transitions.
|
14
|
+
So these need to be added to your config.
|
15
|
+
}
|
16
|
+
c.example 'Move BUG-4 into the In Progress state.', %{jm goto 'In Progress' BUG-4}
|
17
|
+
|
18
|
+
c.action do |args, options|
|
19
|
+
jira = JiraMule::JiraUtils.new(args, options)
|
20
|
+
to = args.shift
|
21
|
+
|
22
|
+
# keys can be with or without the project prefix.
|
23
|
+
keys = jira.expandKeys(args)
|
24
|
+
jira.printVars(:to=>to, :keys=>keys)
|
25
|
+
raise "No keys to transition" if keys.empty?
|
26
|
+
|
27
|
+
keys.each do |key|
|
28
|
+
# First see if we can just go there.
|
29
|
+
trans = jira.transitionsFor(key)
|
30
|
+
direct = trans.select {|item| jira.fuzzyMatchStatus(item, to) }
|
31
|
+
if not direct.empty? then
|
32
|
+
# We can just go right there.
|
33
|
+
id = direct.first[:id]
|
34
|
+
jira.transition(key, id)
|
35
|
+
# TODO: deal with required field.
|
36
|
+
else
|
37
|
+
|
38
|
+
# where we are.
|
39
|
+
#query = "assignee = #{jira.username} AND project = #{jira.project} AND "
|
40
|
+
query = "key = #{key}"
|
41
|
+
issues = jira.getIssues(query, ["status"])
|
42
|
+
#type = issues.first.access('fields.issuetype.name')
|
43
|
+
at = issues.first.access('fields.status.name')
|
44
|
+
|
45
|
+
if at == to then
|
46
|
+
say "All ready at '#{to}'"
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get the
|
51
|
+
transMap = jira.getPath(at, to)
|
52
|
+
if transMap.nil? or transMap.empty? then
|
53
|
+
say "No transision map found between '#{at}' and '#{to}'"
|
54
|
+
y=ask("Would you like to build one? [Yn]")
|
55
|
+
exit if y =~ /^n/i
|
56
|
+
say_warning "This will make changes to the issue as the map is built."
|
57
|
+
|
58
|
+
start_at = at
|
59
|
+
transMap = []
|
60
|
+
loop do
|
61
|
+
issues = jira.getIssues("key = #{key}", ["status"])
|
62
|
+
at = issues.first.access('fields.status.name')
|
63
|
+
break if at == to
|
64
|
+
trans = jira.transitionsFor(key)
|
65
|
+
if trans.length == 1 then
|
66
|
+
id = trans.first[:id]
|
67
|
+
say "Taking single exit: '#{trans.first[:name]}' (#{trans.first[:id]})"
|
68
|
+
transMap << trans.first[:name]
|
69
|
+
jira.transition(key, id)
|
70
|
+
else
|
71
|
+
choose do |menu|
|
72
|
+
menu.prompt = "Follow which transition?"
|
73
|
+
trans.each do |tr|
|
74
|
+
menu.choice(tr[:name]) do
|
75
|
+
say "Transitioning #{key} to '#{tr[:name]}' (#{tr[:id]})"
|
76
|
+
transMap << tr[:name]
|
77
|
+
jira.transition(key, tr[:id])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
say "Found the end."
|
84
|
+
say " '#{start_at}' to '#{to}' via: #{transMap.join(', ')}"
|
85
|
+
y=ask("Record this? [Yn]")
|
86
|
+
unless y =~ /^n/ then
|
87
|
+
cfgkey = "goto-maps.#{start_at.gsub(/\W+/,'_')}-#{to.gsub(/\W+/,'_')}"
|
88
|
+
$cfg.set(cfgkey, transMap.join(', '), :project)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
|
92
|
+
# Now move thru
|
93
|
+
jira.printVars(:key=>key, :tm=>transMap)
|
94
|
+
transMap.each do |step|
|
95
|
+
trans = jira.transitionsFor(key)
|
96
|
+
direct = trans.select {|item| jira.fuzzyMatchStatus(item, step) }
|
97
|
+
raise "Broken transition step on #{key} to #{step}" if direct.empty?
|
98
|
+
id = direct.first[:id]
|
99
|
+
jira.transition(key, id)
|
100
|
+
# TODO: deal with required field.
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
alias_command :move, :goto
|
109
|
+
|
110
|
+
command :mapGoto do |c|
|
111
|
+
c.syntax = 'jm mapGoto [options]'
|
112
|
+
c.summary = 'Attempt to build a goto map'
|
113
|
+
c.option '--dot', %{Output a dot graph}
|
114
|
+
c.description = %{
|
115
|
+
This command is incomplete. The goal here is to auto-build the transision maps
|
116
|
+
for multi-step gotos.
|
117
|
+
|
118
|
+
Right now it is just dumping stuff.
|
119
|
+
|
120
|
+
}
|
121
|
+
c.action do |args, options|
|
122
|
+
options.defaults :dot => true
|
123
|
+
jira = JiraMule::JiraUtils.new(args, options)
|
124
|
+
|
125
|
+
# Get all of the states that issues can be in.
|
126
|
+
# Try to find an actual issue in each state, and load the next transitions from
|
127
|
+
# it.
|
128
|
+
#
|
129
|
+
types = jira.statusesFor(jira.project)
|
130
|
+
|
131
|
+
# There is only one workflow for all types it seems.
|
132
|
+
|
133
|
+
# We just need the names, so we'll merge down.
|
134
|
+
statusNames = {}
|
135
|
+
|
136
|
+
types.each do |type|
|
137
|
+
statuses = type[:statuses]
|
138
|
+
next if statuses.nil?
|
139
|
+
next if statuses.empty?
|
140
|
+
statuses.each {|status| statusNames[ status[:name] ] = 1}
|
141
|
+
end
|
142
|
+
|
143
|
+
puts "digraph #{jira.project} {"
|
144
|
+
statusNames.each_key do |status|
|
145
|
+
#puts " #{status}"
|
146
|
+
query = %{project = #{jira.project} AND status = "#{status}"}
|
147
|
+
issues = jira.getIssues(query, ["key"])
|
148
|
+
if issues.empty? then
|
149
|
+
#?
|
150
|
+
puts %{/* Cannot find transitions for #{status} */}
|
151
|
+
else
|
152
|
+
key = issues.first[:key]
|
153
|
+
# get transisitons.
|
154
|
+
trans = jira.transitionsFor(key)
|
155
|
+
trans.each do |tr|
|
156
|
+
puts %{ "#{status}" -> "#{tr[:to][:name]}" [label="#{tr[:name]}"]} # [#{tr[:id]}]"]}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
puts "}"
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# vim: set sw=2 ts=2 :
|
166
|
+
|