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.
@@ -0,0 +1,147 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module JiraMule
6
+ module Http
7
+
8
+ def json_opts
9
+ return @json_opts unless not defined?(@json_opts) or @json_opts.nil?
10
+ @json_opts = {
11
+ :allow_nan => true,
12
+ :symbolize_names => true,
13
+ :create_additions => false
14
+ }
15
+ end
16
+
17
+ def curldebug(request)
18
+ if $cfg['tool.curldebug'] then
19
+ a = []
20
+ a << %{curl -s }
21
+ if request.key?('Authorization') then
22
+ a << %{-H 'Authorization: #{request['Authorization']}'}
23
+ end
24
+ a << %{-H 'User-Agent: #{request['User-Agent']}'}
25
+ a << %{-H 'Content-Type: #{request.content_type}'}
26
+ a << %{-X #{request.method}}
27
+ a << %{'#{request.uri.to_s}'}
28
+ a << %{-d '#{request.body}'} unless request.body.nil?
29
+ puts a.join(' ')
30
+ end
31
+ end
32
+
33
+ def http
34
+ uri = URI($cfg['net.url'])
35
+ if not defined?(@http) or @http.nil? then
36
+ @http = Net::HTTP.new(uri.host, uri.port)
37
+ @http.use_ssl = true
38
+ @http.start
39
+ end
40
+ @http
41
+ end
42
+ def http_reset
43
+ @http = nil
44
+ end
45
+
46
+ def set_def_headers(request)
47
+ request.content_type = 'application/json'
48
+ request.basic_auth(username(), password())
49
+ request['User-Agent'] = "JiraMule/#{JiraMule::VERSION}"
50
+ request
51
+ end
52
+
53
+ def isJSON(data)
54
+ begin
55
+ return true, JSON.parse(data, json_opts)
56
+ rescue
57
+ return false, data
58
+ end
59
+ end
60
+
61
+ def showHttpError(request, response)
62
+ if $cfg['tool.debug'] then
63
+ puts "Sent #{request.method} #{request.uri.to_s}"
64
+ request.each_capitalized{|k,v| puts "> #{k}: #{v}"}
65
+ if request.body.nil? then
66
+ else
67
+ puts " > #{request.body[0..156]}"
68
+ end
69
+ puts "Got #{response.code} #{response.message}"
70
+ response.each_capitalized{|k,v| puts "< #{k}: #{v}"}
71
+ end
72
+ isj, jsn = isJSON(response.body)
73
+ resp = "Request Failed: #{response.code}: "
74
+ if isj then
75
+ if $cfg['tool.fullerror'] then
76
+ resp << JSON.pretty_generate(jsn)
77
+ else
78
+ resp << "[#{jsn[:statusCode]}] " if jsn.has_key? :statusCode
79
+ resp << jsn[:message] if jsn.has_key? :message
80
+ end
81
+ else
82
+ resp << jsn
83
+ end
84
+ say_error resp
85
+ end
86
+
87
+ def workit(request, &block)
88
+ curldebug(request)
89
+ if block_given? then
90
+ return yield request, http()
91
+ else
92
+ response = http().request(request)
93
+ case response
94
+ when Net::HTTPSuccess
95
+ return {} if response.body.nil?
96
+ begin
97
+ return JSON.parse(response.body, json_opts)
98
+ rescue
99
+ return response.body
100
+ end
101
+ else
102
+ showHttpError(request, response)
103
+ raise response
104
+ end
105
+ end
106
+ end
107
+
108
+ def get(path='', query=nil, &block)
109
+ uri = endPoint(path)
110
+ uri.query = URI.encode_www_form(query) unless query.nil?
111
+ req = Net::HTTP::Get.new(uri)
112
+ workit(set_def_headers(req), &block)
113
+ end
114
+
115
+ def post(path='', body={}, &block)
116
+ uri = endPoint(path)
117
+ req = Net::HTTP::Post.new(uri)
118
+ set_def_headers(req)
119
+ req.body = JSON.generate(body)
120
+ workit(req, &block)
121
+ end
122
+
123
+ def postf(path='', form={}, &block)
124
+ uri = endPoint(path)
125
+ req = Net::HTTP::Post.new(uri)
126
+ set_def_headers(req)
127
+ req.content_type = 'application/x-www-form-urlencoded; charset=utf-8'
128
+ req.form_data = form
129
+ workit(req, &block)
130
+ end
131
+
132
+ def put(path='', body={}, &block)
133
+ uri = endPoint(path)
134
+ req = Net::HTTP::Put.new(uri)
135
+ set_def_headers(req)
136
+ req.body = JSON.generate(body)
137
+ workit(req, &block)
138
+ end
139
+
140
+ def delete(path='', &block)
141
+ uri = endPoint(path)
142
+ workit(set_def_headers(Net::HTTP::Delete.new(uri)), &block)
143
+ end
144
+
145
+ end
146
+ end
147
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,288 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/http/post/multipart'
4
+ require 'json'
5
+ require 'date'
6
+ require 'pp'
7
+ require 'mime/types'
8
+ require 'JiraMule/Config'
9
+ require 'JiraMule/Passwords'
10
+ require 'JiraMule/http'
11
+ require 'JiraMule/verbosing'
12
+
13
+ module JiraMule
14
+ class JiraUtilsException < Exception
15
+ attr_accessor :request, :response
16
+ end
17
+
18
+ class JiraUtils
19
+ include Verbose
20
+ include Http
21
+
22
+ # TODO: all params are now optional.
23
+ def initialize(args=nil, options=nil, cfg=nil)
24
+ acc = Account.new
25
+ up = acc.loginInfo
26
+ @username = up[:email]
27
+ @password = up[:password]
28
+ end
29
+ attr_reader :username, :password
30
+
31
+ def jiraEndPoint
32
+ endPoint()
33
+ end
34
+
35
+ def endPoint(path='')
36
+ URI($cfg['net.url'] + '/rest/api/2/' + path.to_s)
37
+ end
38
+
39
+ def project
40
+ return @project unless @project.nil?
41
+ @project = $cfg['jira.project']
42
+ return @project
43
+ end
44
+
45
+ ##
46
+ # Given an array of issues keys that may or may not have the project prefixed
47
+ # Return an array with the project prefixed.
48
+ #
49
+ # So on project APP, from %w{1 56 BUG-78} you get %w{APP-1 APP-56 BUG-78}
50
+ def expandKeys(keys)
51
+ return keys.map do |k|
52
+ k.match(/([a-zA-Z]+-)?(\d+)/) do |m|
53
+ if m[1].nil? then
54
+ "#{project}-#{m[2]}"
55
+ else
56
+ m[0]
57
+ end
58
+ end
59
+ end.compact
60
+ end
61
+
62
+ ##
63
+ # Allow for some sloppy matching.
64
+ # +transition+:: The transition hash to match against
65
+ # +couldBe+:: The string from a human to match for
66
+ def fuzzyMatchStatus(transition, couldBe)
67
+ return transition[:id] == couldBe if couldBe =~ /^\d+$/
68
+
69
+ # Build a regexp for all sorts of variations.
70
+
71
+ # Replace whitespace with a rex for dashes, whitespace, or nospace.
72
+ cb = couldBe.gsub(/\s+/, '[-_\s]*')
73
+
74
+ matcher = Regexp.new(cb, Regexp::IGNORECASE)
75
+ debug "Fuzzing: #{transition[:name]} =~ #{cb}"
76
+ return transition[:name] =~ matcher
77
+ end
78
+
79
+ ##
80
+ # Lookup a path from one state to another in a map
81
+ # +at+:: The starting state
82
+ # +to+:: The stopping state
83
+ def getPath(at, to)
84
+ verbose "Getting path from '#{at}' to '#{to}'"
85
+
86
+ # [goto-map]
87
+ # at-to: each, step, to, end
88
+ tr = $cfg["goto-maps.#{at.gsub(/\W+/,'_')}-#{to.gsub(/\W+/,'_')}"]
89
+ (tr or "").split(/,/).map{|p| p.strip}
90
+ end
91
+
92
+ ##
93
+ # Run a JQL query and get issues with the selected fields
94
+ def getIssues(query, fields=[ 'key', 'summary' ])
95
+ verbose "Get keys: #{query}"
96
+ data = post('search', {:jql=>query, :fields=>fields})
97
+ data[:issues]
98
+ end
99
+
100
+ ##
101
+ # make sure #user is an actual user in the system.
102
+ def checkUser(user, keyMatch=true)
103
+ verbose "Get user: #{user}"
104
+ users = get("user/search", {:username=>user})
105
+ return [] if users.empty?
106
+ userKeys = users.map{|i| i[:key]}
107
+ return [user] if keyMatch and userKeys.index(user)
108
+ return userKeys
109
+ end
110
+
111
+ ##
112
+ # Create a new version for release.
113
+ # TODO: test this.
114
+ def createVersion(project, version)
115
+ verbose "Creating #{request.body}"
116
+ unless $cfg['tool.dry'] then
117
+ data = post('version', {
118
+ 'name' => version,
119
+ 'archived' => false,
120
+ 'released' => true,
121
+ 'releaseDate' => DateTime.now.strftime('%Y-%m-%d'),
122
+ 'project' => project,
123
+ })
124
+ unless data[:released] then
125
+ # Sometimes setting released on create doesn't work.
126
+ # So modify it.
127
+ put("version/#{data[:id]}", {:released=>true})
128
+ end
129
+ end
130
+ end
131
+
132
+ ##
133
+ # Create a new issue
134
+ # +type+:: The type of issue this is
135
+ # +summary+:: Short title text
136
+ # +description+:: Full details.
137
+ def createIssue(type, summary, description)
138
+ verbose "Creating #{type} issue for #{summary}"
139
+ unless $cfg['tool.dry'] then
140
+ post('issue', {
141
+ :fields=>{
142
+ :issuetype=>{:name=>type},
143
+ :project=>{:key=>project},
144
+ :summary=>summary,
145
+ :description=>description,
146
+ :labels=>['auto-imported'],
147
+ }
148
+ })
149
+ else
150
+ {:key=>'_'}
151
+ end
152
+ end
153
+
154
+ ##
155
+ # Check this issue type in current project
156
+ def checkIssueType(type='bug')
157
+ rt = Regexp.new(type.gsub(/\s+/, '[-_\s]*'), Regexp::IGNORECASE)
158
+ cmeta = get('issue/createmeta')
159
+ prj = cmeta[:projects].select{|p| p[:key] == project}.first
160
+ prj[:issuetypes].select{|it| it[:name] =~ rt}
161
+ end
162
+
163
+ # Update fields on a key
164
+ # +keys+:: Array of keys to update
165
+ # +update+:: Hash of fields to update. (see https://docs.atlassian.com/jira/REST/6.4.7/#d2e261)
166
+ def updateKeys(keys, update)
167
+ keys = [keys] unless keys.kind_of? Array
168
+ keys.each do |key|
169
+ verbose "Updating key #{key} with #{update}"
170
+ put("issue/#{key}", {:update=>update}) unless $cfg['tool.dry']
171
+ end
172
+ end
173
+
174
+ # Transition key into a new status
175
+ # +key+:: The key to transition
176
+ # +toID+:: The ID of the transition to make
177
+ def transition(key, toID)
178
+ verbose "Transitioning key #{key} to #{toID}"
179
+ post('issue/' + key + '/transitions', {:transition=>{:id=>toID}}) unless $cfg['tool.dry']
180
+ end
181
+
182
+ # Get the transitions that a key can move to.
183
+ # +key+:: The issue
184
+ def transitionsFor(key)
185
+ verbose "Fetching transitions for #{key}"
186
+ data = get('issue/' + key + '/transitions')
187
+ data[:transitions]
188
+ end
189
+
190
+ # Get the status for a project
191
+ # +project+:: The project to fetch status from
192
+ def statusesFor(project)
193
+ verbose "Fetching statuses for #{project}"
194
+ get('project/' + project + '/statuses')
195
+ end
196
+
197
+ # Assign issues to a user
198
+ # +keys+:: Array of keys to assign
199
+ # +to+:: The user to assign to (or -1 for default)
200
+ def assignTo(keys, to="-1")
201
+ keys = [keys] unless keys.kind_of? Array
202
+ r=[]
203
+ keys.each do |key|
204
+ verbose "Assigning #{key} to #{to}"
205
+ r << put("issue/#{key}/assignee", {:name=>to}) unless $cfg['tool.dry']
206
+ end
207
+ r
208
+ end
209
+
210
+ # Log a work entry to Jira
211
+ # +key+:: The issue to log work on
212
+ # +timespend+:: The time spent in seconds
213
+ # +notes+:: Any notes to add.
214
+ # +on+:: When this work happened. (default is now)
215
+ def logWork(key, timespent, notes="", on=nil)
216
+ body = {
217
+ :comment => notes,
218
+ :timeSpentSeconds => timespent,
219
+ }
220
+ body[:started] = on.to_time.strftime('%FT%T.%3N%z') unless on.nil?
221
+
222
+ verbose "Logging #{timespent} of work to #{key} with note \"#{notes}\""
223
+ post('issue/' + key + '/worklog', body) unless $cfg['tool.dry']
224
+ end
225
+
226
+ # Get the work log for an Issue
227
+ # +key+:: The issue to retrive the work log for
228
+ def workLogs(key)
229
+ verbose "Fetching work logs for #{key}"
230
+ get('issue/' + key + '/worklog')
231
+ end
232
+
233
+ ## Get links on an issue
234
+ # +key+:: The issue to retrive the remote links for
235
+ def remote_links(key)
236
+ get("issue/#{key}/remotelink")
237
+ end
238
+
239
+ ## Attach a URL to an issue
240
+ # +key+:: The issue to set a remote link on
241
+ # +url+:: The URL to link to
242
+ # +title+:: The title of the link
243
+ def linkTo(key, url, title)
244
+ verbose "Attaching [#{title}](#{url}) to #{key}"
245
+ unless $cfg['tool.dry'] then
246
+ post("issue/#{key}/remotelink", {
247
+ :object=>{
248
+ :url=>url,
249
+ :title=>title,
250
+ }
251
+ })
252
+ # XXX Lookup favicon for site and include?
253
+ end
254
+ end
255
+
256
+ def linkKeys(key, toKey, relation='related to')
257
+ end
258
+
259
+ # Attach a file to an issue.
260
+ # +key+:: The issue to attach to
261
+ # +file+:: Full path to the file to be attached
262
+ # +type+:: MIME type of the fiel data
263
+ # +name+:: Aternate name of file being uploaded
264
+ def attach(key, file, type=nil, name=nil)
265
+ file = Pathname.new(file) unless file.kind_of? Pathname
266
+
267
+ name = file.basename if name.nil?
268
+
269
+ if type.nil? then
270
+ mime = MIME::Types.type_for(file.to_s)[0] || MIME::Types["application/octet-stream"][0]
271
+ type = mime.simplified
272
+ end
273
+
274
+ verbose "Going to upload #{file} [#{type}] to #{key}"
275
+
276
+ uri = endPoint('issue/' + key + '/attachments')
277
+ fuio = UploadIO.new(file.open, type, name)
278
+ req = Net::HTTP::Post::Multipart.new(uri, 'file'=> fuio )
279
+ req.basic_auth(username(), password())
280
+ req['User-Agent'] = "JiraMule/#{JiraMule::VERSION}"
281
+ #set_def_headers(req)
282
+ req['X-Atlassian-Token'] = 'nocheck'
283
+ workit(req) unless $cfg['tool.dry']
284
+ end
285
+
286
+ end
287
+ end
288
+ # vim: set sw=4 ts=4 :
@@ -0,0 +1,28 @@
1
+ require 'pp'
2
+
3
+ module JiraMule
4
+ module Verbose
5
+
6
+ def verbose(msg)
7
+ if $cfg['tool.verbose'] then
8
+ say "\033[1m=#\033[0m #{msg}"
9
+ end
10
+ end
11
+
12
+ def debug(msg)
13
+ if $cfg['tool.debug'] then
14
+ say "\033[1m=#\033[0m #{msg}"
15
+ end
16
+ end
17
+
18
+ def printVars(map)
19
+ $stdout.print("\033[1m=:\033[0m ")
20
+ map.each {|k,v|
21
+ $stdout.print("\033[1m#{k}:\033[0m #{v} ")
22
+ }
23
+ $stdout.print("\n")
24
+ end
25
+
26
+ end
27
+ end
28
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,3 @@
1
+ module JiraMule
2
+ VERSION = '0.1.1'
3
+ end