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.
data/lib/JiraMule.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'JiraMule/version'
2
+ require 'JiraMule/Config'
3
+ require 'JiraMule/jiraUtils'
4
+ require 'JiraMule/gitUtils'
5
+ require 'JiraMule/Tempo'
6
+
7
+ require 'JiraMule/commands'
@@ -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
+