jiraMule 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
+