jiraMule 0.1.1
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.
- 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
|
+
|