jiraMule 0.1.1

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