vcseif 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/vcseif.rb +23 -0
- data/lib/vcseif/connection.rb +285 -0
- data/lib/vcseif/rally_vcs_connection.rb +925 -0
- data/lib/vcseif/utils/auxloader.rb +255 -0
- data/lib/vcseif/utils/exceptions.rb +101 -0
- data/lib/vcseif/utils/fuzzer.rb +71 -0
- data/lib/vcseif/utils/konfigger.rb +421 -0
- data/lib/vcseif/utils/lock_file.rb +90 -0
- data/lib/vcseif/utils/proctbl.rb +146 -0
- data/lib/vcseif/utils/rally_logger.rb +223 -0
- data/lib/vcseif/utils/time_file.rb +80 -0
- data/lib/vcseif/vcs_connector.rb +487 -0
- data/lib/vcseif/vcs_connector_driver.rb +227 -0
- data/lib/vcseif/vcs_connector_runner.rb +283 -0
- data/lib/version.rb +18 -0
- metadata +173 -0
data/lib/vcseif.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
|
2
|
+
|
3
|
+
minimum_ruby_version = "1.9.2"
|
4
|
+
if RUBY_VERSION < minimum_ruby_version
|
5
|
+
abort "\nVCSEIF does not work with Ruby versions below #{minimum_ruby_version}. Please upgrade.\n"
|
6
|
+
end
|
7
|
+
|
8
|
+
require_relative "version"
|
9
|
+
|
10
|
+
require_relative "vcseif/utils/rally_logger"
|
11
|
+
require_relative "vcseif/utils/exceptions"
|
12
|
+
require_relative "vcseif/utils/proctbl"
|
13
|
+
require_relative "vcseif/utils/lock_file"
|
14
|
+
require_relative "vcseif/utils/time_file"
|
15
|
+
require_relative "vcseif/utils/fuzzer"
|
16
|
+
require_relative "vcseif/utils/konfigger"
|
17
|
+
require_relative "vcseif/utils/auxloader"
|
18
|
+
|
19
|
+
require_relative "vcseif/connection"
|
20
|
+
require_relative "vcseif/rally_vcs_connection"
|
21
|
+
require_relative "vcseif/vcs_connector"
|
22
|
+
require_relative "vcseif/vcs_connector_runner"
|
23
|
+
require_relative "vcseif/vcs_connector_driver"
|
@@ -0,0 +1,285 @@
|
|
1
|
+
# Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
require "resolv"
|
5
|
+
|
6
|
+
#############################################################################################################
|
7
|
+
|
8
|
+
class ChangesetSelector
|
9
|
+
|
10
|
+
attr_accessor :attribute, :relation, :value
|
11
|
+
|
12
|
+
def initialize(condition)
|
13
|
+
"""
|
14
|
+
A ChangesetSelector instance is of the form:
|
15
|
+
attribute relational_operator value_or_expression
|
16
|
+
Prime our 3 instance attributes of interest in case the regex match fails
|
17
|
+
"""
|
18
|
+
@attribute = ""
|
19
|
+
@relation = ""
|
20
|
+
@value = ""
|
21
|
+
# define our extractor regex to handle a string with "attribute relation value_or_expression_that_may have spaces"
|
22
|
+
# examples: 'Revision = 4' or 'Author = NilsLofgren' or "CommitTimestamp >= '2012-09-12T17:32:49.000Z'"
|
23
|
+
# We only support these relational operators (=, !=, <, <=, >, >=)
|
24
|
+
attr_identifier_pattern = '[a-zA-Z@$%&*].*[a-zA-Z0-9_@$%&*]+'
|
25
|
+
relational_operators = ['=', '!=', '<', '<=', '>', '>=']
|
26
|
+
relations = "%s" % [relational_operators.join('|')]
|
27
|
+
selector_pattern_spec = "^(?<attribute>%s)\s+(?<relation>%s)\s+(?<value>.+)$" % [attr_identifier_pattern, relations]
|
28
|
+
|
29
|
+
selector_patt = Regexp.compile(selector_pattern_spec)
|
30
|
+
md = selector_patt.match(condition)
|
31
|
+
if not md.nil?
|
32
|
+
@attribute = md[:attribute].strip()
|
33
|
+
@relation = md[:relation]
|
34
|
+
@value = md[:value].strip()
|
35
|
+
end
|
36
|
+
if (@attribute.empty? or @relation.empty? or @value.empty?)
|
37
|
+
problem = "Invalid Changeset Selector specification: %s" % condition
|
38
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
39
|
+
raise confex, problem
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#############################################################################################################
|
45
|
+
|
46
|
+
class VCSConnection
|
47
|
+
|
48
|
+
attr_accessor :log, :config
|
49
|
+
attr_reader :username, :password, :username_required, :password_required, :rev_uri, :file_uri
|
50
|
+
attr_reader :connected
|
51
|
+
attr_accessor :changeset_selectors, :sshkey_path
|
52
|
+
|
53
|
+
REVNUM_TOKEN = "{revnumber}"
|
54
|
+
REVFILE_TOKEN = "{filepath}"
|
55
|
+
|
56
|
+
def initialize(logger)
|
57
|
+
@log = logger
|
58
|
+
@config = nil
|
59
|
+
@username = nil
|
60
|
+
@password = nil
|
61
|
+
@username_required = true
|
62
|
+
@password_required = true
|
63
|
+
@changeset_selectors = []
|
64
|
+
@connected = false
|
65
|
+
@log.info("Initializing %s connection version %s" % [name(), version()])
|
66
|
+
end
|
67
|
+
|
68
|
+
def name()
|
69
|
+
"""
|
70
|
+
abstract, provider should return a non-empty string with name of the specific connector
|
71
|
+
"""
|
72
|
+
raise NotImplementedError, problem
|
73
|
+
end
|
74
|
+
|
75
|
+
#Placeholder to put the version of the connector
|
76
|
+
def version()
|
77
|
+
"""
|
78
|
+
abstract, provider should return a non-empty string with version
|
79
|
+
identification of the specific connector
|
80
|
+
"""
|
81
|
+
problem = "All descendants of the VCSConnection class need to implement version()"
|
82
|
+
raise NotImplementedError, problem
|
83
|
+
end
|
84
|
+
|
85
|
+
def getBackendVersion()
|
86
|
+
problem = "All descendants of the VCSConnection class need to implement getBackendVersion()"
|
87
|
+
raise NotImplementedError, problem
|
88
|
+
end
|
89
|
+
|
90
|
+
def connect()
|
91
|
+
%{
|
92
|
+
Returns true or false depending on whether a connection was "established".
|
93
|
+
As many connectors are stateless, the "establishment of a connection" might
|
94
|
+
just mean that the target and credentials are adequate to post a request and
|
95
|
+
receive a non-error response.
|
96
|
+
}
|
97
|
+
problem = "All descendants of the VCSConnection class need to implement connect()"
|
98
|
+
raise NotImplementedError, problem
|
99
|
+
end
|
100
|
+
|
101
|
+
def disconnect()
|
102
|
+
"""
|
103
|
+
Returns true or false depending on whether an existing connection was disconnected
|
104
|
+
successfully.
|
105
|
+
As many connectors are stateless, the disconnection may be as easy as
|
106
|
+
resetting an instance variable to None
|
107
|
+
"""
|
108
|
+
problem = "All descendants of the VCSConnection class need to implement disconnect()"
|
109
|
+
raise NotImplementedError, problem
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
def validate()
|
114
|
+
satisfactory = true
|
115
|
+
|
116
|
+
if @username_required == true
|
117
|
+
if !username || @username.empty?
|
118
|
+
@log.error("<Username> is required in the config file")
|
119
|
+
satisfactory = false
|
120
|
+
else
|
121
|
+
@log.debug('%s - user entry "%s" detected in config file' % [self.class.name, @username])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
if @password_required == true
|
126
|
+
if !password || @password.empty?
|
127
|
+
@log.error("<Password> is required in the config file")
|
128
|
+
satisfactory = false
|
129
|
+
else
|
130
|
+
@log.debug('%s - password entry detected in config file' % self.class.name)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
satisfactory = hasValidChangesetSelectors() if satisfactory
|
135
|
+
|
136
|
+
return satisfactory
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
def getRecentChangesets(ref_time)
|
141
|
+
"""
|
142
|
+
Finds items that have been created since a reference time (ref_time is in UTC)
|
143
|
+
and applying all specified ChangesetSelector conditions.
|
144
|
+
|
145
|
+
Concrete subclasses must implement this method and return a list of qualified items.
|
146
|
+
"""
|
147
|
+
problem = "All descendants of the VCSConnection class need to implement getRecentChangesets(ref_time)"
|
148
|
+
raise NotImplementedError, problem
|
149
|
+
end
|
150
|
+
|
151
|
+
def is_windows?
|
152
|
+
RUBY_PLATFORM.downcase.include?("mswin") || RUBY_PLATFORM.downcase.include?("mingw")
|
153
|
+
end
|
154
|
+
|
155
|
+
#look for tokens {revnumber} and {filepath} and put in the correct values
|
156
|
+
#cgit here:
|
157
|
+
#commit: http://git.company.com/cgit/reponame.git/commit/?id={revnumber}
|
158
|
+
#file: http://git.company.com/cgit/reponame.git/diff/{filepath}?id={revnumber}
|
159
|
+
#github:
|
160
|
+
#commit: https://github.com/reponame/commit/{revnumber}
|
161
|
+
#file: https://github.com/reponame/blob/{revnumber}/{filepath}
|
162
|
+
#Mercurial (Hg):
|
163
|
+
#commit: http://hgweb:8000/rev/{revnumber}
|
164
|
+
#file: http://hgweb:8000/file/{revnumber}/{filepath}
|
165
|
+
def get_rev_uri(revision_number)
|
166
|
+
return nil if @rev_uri.nil?
|
167
|
+
@rev_uri.gsub(REVNUM_TOKEN, revision_number)
|
168
|
+
end
|
169
|
+
|
170
|
+
def get_file_rev_uri(revision_number, filepath, operation = nil)
|
171
|
+
return nil if @file_uri.nil?
|
172
|
+
file_uri = @file_uri.gsub(REVNUM_TOKEN, revision_number)
|
173
|
+
file_uri.gsub(REVFILE_TOKEN, URI::encode(filepath))
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
def internalizeConfig(config)
|
178
|
+
"""
|
179
|
+
config has already been read, it comes to us here as
|
180
|
+
a dict with information relevant to this connection
|
181
|
+
"""
|
182
|
+
@config = config
|
183
|
+
username = config['Username'] || false # Deprecate
|
184
|
+
user = config['User'] || false # Deprecate
|
185
|
+
@username = username || user
|
186
|
+
@password = config['Password'] || false # Deprecate
|
187
|
+
@sshkey_path = config["SSHKeyPath"]
|
188
|
+
@changeset_selectors = config['ChangesetSelectors'] || []
|
189
|
+
|
190
|
+
@rev_uri = config["RevURI"] unless config["RevURI"].nil?
|
191
|
+
@file_uri = config["FileURI"] unless config["FileURI"].nil?
|
192
|
+
|
193
|
+
# minimum valid selector spec is 6 chars, 'xy = z'
|
194
|
+
bad_chgset_selectors = @changeset_selectors.select {|sel| sel.length < 6}
|
195
|
+
if bad_chgset_selectors.length > 0
|
196
|
+
problem = "One or more ChangesetSelector specifications is structurally invalid"
|
197
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
198
|
+
raise confex, problem
|
199
|
+
end
|
200
|
+
|
201
|
+
if @changeset_selectors.length > 0
|
202
|
+
#transform our textual selector conditions to ChangesetSelector instances
|
203
|
+
@changeset_selectors = @changeset_selectors.collect {|cs| ChangesetSelector.new(cs)}
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
def field_exists?(field_name)
|
209
|
+
"""
|
210
|
+
Return a boolean truth value (true/false) depending on whether the targeted
|
211
|
+
field_name exists for the current connection on a Changeset
|
212
|
+
"""
|
213
|
+
problem = "All descendants of the VCSConnection class need to implement field_exists?(field_name)"
|
214
|
+
raise NotImplementedError, problem
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
def hasValidChangesetSelectors()
|
219
|
+
"""
|
220
|
+
This method should be overridden in the VCSConnection subclass for
|
221
|
+
connections whose target does not support standard relational
|
222
|
+
operators of (=, !=, <, >, <=, and >=) .
|
223
|
+
"""
|
224
|
+
if @changeset_selectors.length == 0
|
225
|
+
return true
|
226
|
+
end
|
227
|
+
status = true
|
228
|
+
@changeset_selectors.each do |cs|
|
229
|
+
if not field_exists?(cs.field)
|
230
|
+
@log.error("ChangesetSelector field_name %s not found" % cs.field)
|
231
|
+
status = false
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
return status
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
def _targetServerIsLocalhost(target)
|
240
|
+
localhost_short = Socket.gethostname
|
241
|
+
return true if target == localhost_short
|
242
|
+
return true if target.downcase == "localhost"
|
243
|
+
# is the IP of the target also the IP for resolv.getaddress(Socket.gethostname)?
|
244
|
+
target_ip = Resolv.getaddress(target)
|
245
|
+
localhost_ip = ''
|
246
|
+
begin
|
247
|
+
localhost_ip = Resolv.getaddress(localhost_short)
|
248
|
+
rescue => ex
|
249
|
+
ok = false
|
250
|
+
begin
|
251
|
+
ifc = getInterfaceInfo()
|
252
|
+
if ifc.include?('en0') and not ifc['en0'].nil?
|
253
|
+
localhost_ip = ifc['en0']
|
254
|
+
ok = true
|
255
|
+
elsif ifc.include?('en1') and not ifc['en1'].nil?
|
256
|
+
localhost_ip = ifc['en1']
|
257
|
+
ok = true
|
258
|
+
else
|
259
|
+
raise if not ok
|
260
|
+
end
|
261
|
+
rescue => ex
|
262
|
+
puts "#{ex.class.name} => #{ex.message}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
return target_ip == localhost_ip
|
266
|
+
end
|
267
|
+
|
268
|
+
def getInterfaceInfo()
|
269
|
+
# This seems to be only necessary on *nix when Resolv.getaddress(Socket.gethostname) fails
|
270
|
+
ifc_info = {}
|
271
|
+
ifc_out = %x[/sbin/ifconfig -a]
|
272
|
+
for line in ifc_out.split("\n") do
|
273
|
+
if line =~ /^(\w+\d): /
|
274
|
+
ifc = $1
|
275
|
+
ifc_info[ifc] = nil
|
276
|
+
elsif line =~ /\binet (\d+\.\d+\.\d+\.\d+) /
|
277
|
+
ip_addr = $1
|
278
|
+
ifc_info[ifc] = ip_addr
|
279
|
+
end
|
280
|
+
end
|
281
|
+
return ifc_info
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
|
@@ -0,0 +1,925 @@
|
|
1
|
+
# Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'uri' # for URI::encode of the filename attribute of Change
|
5
|
+
|
6
|
+
require 'vcseif/utils/exceptions'
|
7
|
+
require 'vcseif/connection'
|
8
|
+
require 'vcseif/utils/auxloader'
|
9
|
+
|
10
|
+
# from 'vcseif' we use utils/exceptions.UnrecoverableException,
|
11
|
+
# utils/exceptions.ConfigurationError
|
12
|
+
# utils/exceptions.OperationalError
|
13
|
+
# connection.VCSConnection
|
14
|
+
# utils/auxloader.ExtensionLoader
|
15
|
+
|
16
|
+
require 'rally_api'
|
17
|
+
|
18
|
+
############################################################################################
|
19
|
+
|
20
|
+
class RallyAPI::RallyObject
|
21
|
+
# Augment RallyAPI::RallyObject with the ability to set an attribute;
|
22
|
+
# We'll use it to set an 'ident' attribute
|
23
|
+
def []=(field, value)
|
24
|
+
@rally_object[field] = value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
############################################################################################
|
29
|
+
|
30
|
+
class RallyVCSConnection < VCSConnection
|
31
|
+
|
32
|
+
RALLY_VCS_CONNECTION_VERSION = "1.2.0"
|
33
|
+
WSAPI_VERSION = "1.43"
|
34
|
+
|
35
|
+
VALID_ARTIFACT_TYPES = ['HierarchicalRequirement', 'Task', 'Defect', 'DefectSuite']
|
36
|
+
|
37
|
+
ISO8601_TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
38
|
+
|
39
|
+
EXTENSION_SPEC_PATTERN = Regexp.compile('^(?<ext_class>[\w\.]+)\s*\((?<ext_parm>[^\)]+)\)$')
|
40
|
+
|
41
|
+
attr_reader :valid_artifact_pattern
|
42
|
+
attr_accessor :server
|
43
|
+
attr_accessor :rally, :operational_errors
|
44
|
+
attr_reader :extensions
|
45
|
+
attr_reader :rally_wsapi_version, :repo_ref
|
46
|
+
attr_reader :user_cache
|
47
|
+
|
48
|
+
def initialize(config, logger)
|
49
|
+
@valid_artifact_pattern = nil # set after config with artifact prefixes are known
|
50
|
+
super(logger)
|
51
|
+
internalizeConfig(config)
|
52
|
+
@log.info("Rally WSAPI Version %s" % rallyWSAPIVersion)
|
53
|
+
@integration_other_version = ""
|
54
|
+
@username_required = true
|
55
|
+
@password_required = true
|
56
|
+
@operational_errors = 0
|
57
|
+
@user_cache = {}
|
58
|
+
end
|
59
|
+
|
60
|
+
def name()
|
61
|
+
"Rally"
|
62
|
+
end
|
63
|
+
|
64
|
+
def version()
|
65
|
+
RALLY_VCS_CONNECTION_VERSION
|
66
|
+
end
|
67
|
+
|
68
|
+
def rallyWSAPIVersion()
|
69
|
+
@rally_wsapi_version
|
70
|
+
end
|
71
|
+
|
72
|
+
def getBackendVersion()
|
73
|
+
%{
|
74
|
+
Conform to Connection subclass protocol which requires the version of
|
75
|
+
the system this instance is "connected" to.
|
76
|
+
}
|
77
|
+
return "Rally WSAPI %s" % rallyWSAPIVersion
|
78
|
+
end
|
79
|
+
|
80
|
+
def internalizeConfig(config)
|
81
|
+
super(config)
|
82
|
+
|
83
|
+
@server = false
|
84
|
+
@server = config['Server'] if config.has_key?('Server')
|
85
|
+
if !@server || @server.nil?
|
86
|
+
problem = "RallyConnection spec missing a value for Server"
|
87
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
88
|
+
raise confex, problem
|
89
|
+
end
|
90
|
+
if server.downcase =~ /http:/ or server.downcase =~ /\/slm/
|
91
|
+
@log.error("Rally URL should be in the form 'rally1.rallydev.com'")
|
92
|
+
problem = "RallyConnection Server spec invalid format"
|
93
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
94
|
+
raise confex, problem
|
95
|
+
end
|
96
|
+
|
97
|
+
proxy = config['Proxy'] || nil
|
98
|
+
set_proxy_info(config) if not proxy.nil?
|
99
|
+
|
100
|
+
@rally_wsapi_version = config['WSAPIVersion'] || WSAPI_VERSION
|
101
|
+
@workspace_name = config['Workspace'] || nil
|
102
|
+
@repository_name = config['RepositoryName'] || nil
|
103
|
+
@repo_ref = nil
|
104
|
+
@restapi_debug = config['Debug'] || false
|
105
|
+
@restapi_logger = @log
|
106
|
+
#@restapi_logger = @log if restapi_debug || nil
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
def set_proxy_info(config)
|
111
|
+
"""
|
112
|
+
Given the config with any Proxy* related items, the simple case is to assemble the
|
113
|
+
final proxy to be stuffed in ENV['http_proxy'] as <protocol>://<proxy>.
|
114
|
+
If the config has config['ProxyUser'] and config['ProxyPassword'] values, then
|
115
|
+
the final proxy is assembled with <protocol>://<proxy_credentials>@<proxy>
|
116
|
+
and stuffed in ENV['http_proxy'].
|
117
|
+
"""
|
118
|
+
proxy = config['Proxy']
|
119
|
+
proxy_protocol = config['ProxyProtocol'] || "http" # protocol default is 'http'
|
120
|
+
if proxy.downcase =~ /^(https?):\/\/(.+)$/
|
121
|
+
proxy_protocol = $1
|
122
|
+
proxy = $2
|
123
|
+
end
|
124
|
+
|
125
|
+
proxy_user = config['ProxyUser'] || nil
|
126
|
+
proxy_password = config['ProxyPassword'] || nil
|
127
|
+
|
128
|
+
if proxy_user.nil? and proxy_password.nil?
|
129
|
+
proxy = "%s://%s" % [proxy_protocol, proxy]
|
130
|
+
elsif not proxy_user.nil? and not proxy_password.nil?
|
131
|
+
proxy_credentials = "%s:%s" % [proxy_user, proxy_password]
|
132
|
+
proxy = "%s://%s@%s" % [proxy_protocol, proxy_credentials, proxy]
|
133
|
+
else # one of proxy_user / proxy_password is nil
|
134
|
+
problem = "Proxy specified, insufficient proxy authentication information supplied"
|
135
|
+
@log.error(problem)
|
136
|
+
problem = "RallyConnection ProxyUser/ProxyPassword settings must both have values"
|
137
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
138
|
+
raise confex, problem
|
139
|
+
end
|
140
|
+
ENV['http_proxy'] = proxy
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
def get_custom_headers()
|
145
|
+
custom_headers = RallyAPI::CustomHttpHeader.new()
|
146
|
+
custom_headers.name = @integration_name || "Rally VCS Connector for UNKNOWN"
|
147
|
+
custom_headers.vendor = @integration_vendor || 'Rally'
|
148
|
+
custom_headers.version = @integration_version || "1.0"
|
149
|
+
|
150
|
+
if not @integration_other_version.empty?
|
151
|
+
conn_ver_target_ver = "%s - %s" % [custom_headers.version, @integration_other_version]
|
152
|
+
custom_headers.version = conn_ver_target_ver
|
153
|
+
end
|
154
|
+
return custom_headers
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def set_integration_header(header_info)
|
159
|
+
@integration_name = header_info['name']
|
160
|
+
@integration_vendor = header_info['vendor']
|
161
|
+
@integration_version = header_info['version']
|
162
|
+
if header_info.has_key?('other_version')
|
163
|
+
@integration_other_version = header_info['other_version']
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
def setSourceIdentification(vcs_type, vcs_uri)
|
169
|
+
@vcs_type = vcs_type
|
170
|
+
@vcs_uri = vcs_uri
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
private
|
175
|
+
def configureExtensionEnvironment()
|
176
|
+
%{
|
177
|
+
For now, the only extension to be considered is one that would provide a service
|
178
|
+
to pull artifact identifiers and actions from within a commit message that is going
|
179
|
+
to be used in creating a new Changeset in Rally. The enablement of that is controlled
|
180
|
+
by the truthiness of the UpdateArtifactState entry in the Rally section of the config.
|
181
|
+
If UpdateArtifactState is enabled, we'll want to load the extension that will provide
|
182
|
+
the service of artifact identifier/action extraction. But before that happens,
|
183
|
+
a list containing Hashes with information about the allowed values for the update field
|
184
|
+
of each artifact type must be constructed. This list is passed to the extension
|
185
|
+
at instantiation time so that it can accurately determine which actions to report
|
186
|
+
were present in the commit message.
|
187
|
+
|
188
|
+
To do that, we'll query Rally for the allowed values of each of the artifact types
|
189
|
+
in VALID_ARTIFACT_TYPES.
|
190
|
+
}
|
191
|
+
artifact_state_field = { 'US' => 'ScheduleState',
|
192
|
+
'S' => 'ScheduleState',
|
193
|
+
'DS' => 'ScheduleState',
|
194
|
+
'DE' => 'State',
|
195
|
+
'TA' => 'State'
|
196
|
+
}
|
197
|
+
|
198
|
+
# Do a Rally query to get allowed values for each actifact_type.artifact_state_field.
|
199
|
+
# so that we can have it to populate the artifact_specs list
|
200
|
+
wksp_oid = @workspace_ref.split('/')[-1][0...-3]
|
201
|
+
|
202
|
+
artifact_specs = []
|
203
|
+
@art_prefixes.each do |prefix|
|
204
|
+
art_type = @artifact_type[prefix]
|
205
|
+
rally_type = art_type
|
206
|
+
if ['Story', 'UserStory'].include?(rally_type)
|
207
|
+
rally_type = 'HierarchicalRequirement' # Peee-yuuuh
|
208
|
+
end
|
209
|
+
upd_field = artifact_state_field[prefix]
|
210
|
+
begin
|
211
|
+
allowed_values = @rally.allowed_values(rally_type, upd_field).keys()
|
212
|
+
next if allowed_values.nil?
|
213
|
+
rescue => ex
|
214
|
+
@log.debug("Unable to retrieve any allowed values for #{rally_type}.#{upd_field}")
|
215
|
+
next
|
216
|
+
end
|
217
|
+
candy = "allowed values for %s.%s: %s" % [art_type, upd_field, allowed_values.join(" | ")]
|
218
|
+
@log.debug(candy)
|
219
|
+
spec = { 'artifact' => art_type,
|
220
|
+
'abbrev' => prefix,
|
221
|
+
'update_field' => upd_field,
|
222
|
+
'valid_values' => allowed_values
|
223
|
+
}
|
224
|
+
artifact_specs << spec
|
225
|
+
end
|
226
|
+
|
227
|
+
@extensions = {}
|
228
|
+
extension = loadExtension(@config, 'UpdateArtifactState', 'StateExtractorClass')
|
229
|
+
# extension comes back as an assoc array, augment it with a live instance if anything is in it
|
230
|
+
if !extension.nil? and extension.length > 0
|
231
|
+
# TODO: see if some other means of supplying the initial argument can be done
|
232
|
+
# this really only works for *ArtifactsAndActionsExtractor related classes
|
233
|
+
extension['extender'] = extension['extension_class'].new(artifact_specs)
|
234
|
+
@extensions['StateExtractor'] = extension
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
def loadExtension(conf, enabler, extension_ident)
|
240
|
+
"""
|
241
|
+
alpha level implementation, doesn't allow for instantiation parms,
|
242
|
+
or method name(s) other than service.
|
243
|
+
Handling of service parm not really wonderful either (How about multiple parms...?)
|
244
|
+
|
245
|
+
Caller must provide a conf Hash with enabler and extension_ident keys in that Hash.
|
246
|
+
The enabler controls whether or not we look for, load and obtain an instance of
|
247
|
+
the extension_ident.
|
248
|
+
The extension_ident has to be in the form of [[subdir.]filename.]ClassName([parm])
|
249
|
+
"""
|
250
|
+
extension = {}
|
251
|
+
|
252
|
+
booleans = ["TrueClass", "FalseClass"]
|
253
|
+
enabled = conf[enabler] || false
|
254
|
+
if enabled and booleans.include?(enabled.class.name)
|
255
|
+
trues = ['true', '1', 'yes', 'y', 'ok', 'on', 'enable']
|
256
|
+
enabled = trues.include?(enabled.to_s.downcase) ? true : false
|
257
|
+
end
|
258
|
+
@log.info("%s enabled to use %s? %s" % [enabler, extension_ident, enabled])
|
259
|
+
if enabled
|
260
|
+
extension_spec = conf[extension_ident] || nil
|
261
|
+
md = EXTENSION_SPEC_PATTERN.match(extension_spec)
|
262
|
+
if not md
|
263
|
+
msg = "%s specified to enable %s via '%s' but that specification is in an unrecognized format" % \
|
264
|
+
[enabler, extension_ident, extension_spec]
|
265
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(msg)
|
266
|
+
raise confex, msg
|
267
|
+
end
|
268
|
+
|
269
|
+
extension_class_name, extension_parm = md[:ext_class], md[:ext_parm]
|
270
|
+
extension_class = ExtensionLoader.getExtension(extension_class_name)
|
271
|
+
@log.debug("Extension class %s loaded" % extension_class_name)
|
272
|
+
|
273
|
+
extension = {'extension_class_name'=> extension_class_name,
|
274
|
+
'extension_class' => extension_class,
|
275
|
+
'extension_parm' => extension_parm,
|
276
|
+
}
|
277
|
+
end
|
278
|
+
return extension
|
279
|
+
end
|
280
|
+
|
281
|
+
|
282
|
+
public
|
283
|
+
def connect()
|
284
|
+
@log.info("Connecting to Rally")
|
285
|
+
custom_headers = get_custom_headers()
|
286
|
+
rally_config = { :base_url => "https://%s/slm" % @config['Server'],
|
287
|
+
:username => @config['Username' ],
|
288
|
+
:password => @config['Password' ],
|
289
|
+
:version => @rally_wsapi_version,
|
290
|
+
:headers => custom_headers
|
291
|
+
}
|
292
|
+
# specifically exclude workspace from rally_config, so we can do a vanilla connect
|
293
|
+
# and then see if the workspace value in the @config actually exists
|
294
|
+
# in the workspaces available for the user
|
295
|
+
|
296
|
+
proxy = ENV['http_proxy'] || nil
|
297
|
+
if not proxy.nil? and not proxy.empty?
|
298
|
+
@log.info("Proxy for connection to Rally: %s" % proxy)
|
299
|
+
rally_config[:proxy => proxy]
|
300
|
+
end
|
301
|
+
|
302
|
+
begin
|
303
|
+
@rally = RallyAPI::RallyRestJson.new(rally_config)
|
304
|
+
info = @rally.user
|
305
|
+
rescue Exception => ex
|
306
|
+
@log.debug(ex.message)
|
307
|
+
problem = "Unable to connect to Rally at %s as user %s" % [@config['Server'], @config['Username']]
|
308
|
+
operr = VCSEIF_Exceptions::OperationalError.new(problem)
|
309
|
+
@operational_errors += 1
|
310
|
+
raise operr, problem
|
311
|
+
end
|
312
|
+
@log.info("Connected to Rally server: %s" % @config['Server'])
|
313
|
+
|
314
|
+
# verify the given workspace name exists
|
315
|
+
wksp = @rally.find_workspace(@config['Workspace'])
|
316
|
+
if wksp.nil? or (not wksp.nil? and wksp['Name'] != @config['Workspace'])
|
317
|
+
desc = "Specified Workspace: '%s' not in list of workspaces available "
|
318
|
+
desc << "for your credentials as user: %s"
|
319
|
+
problem = desc % [@config['Workspace'], @config['Username']]
|
320
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
321
|
+
raise confex, problem
|
322
|
+
end
|
323
|
+
|
324
|
+
# now that we know @config['Workspace'] is a valid workspace,
|
325
|
+
# ensure that all further interactions with Rally are confined to that workspace
|
326
|
+
# as embodied in the wksp object we just got back from the check
|
327
|
+
@rally.rally_default_workspace = wksp
|
328
|
+
@log.info(" Workspace: %s" % @config['Workspace'])
|
329
|
+
# and verify that the BuildandChangesetEnabled flag is set for this workspace
|
330
|
+
wksp.read()
|
331
|
+
wksp_conf = wksp.WorkspaceConfiguration
|
332
|
+
wksp_conf.read()
|
333
|
+
if wksp_conf.BuildandChangesetEnabled != true
|
334
|
+
problem = "The BuildandChangesetEnabled flag is not enabled for the '%s' workspace. " % @config['Workspace']
|
335
|
+
problem << "Contact your Rally workspace administrator to set this up."
|
336
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
337
|
+
raise confex, problem
|
338
|
+
end
|
339
|
+
@wksp = wksp
|
340
|
+
@workspace_ref = @wksp['_ref']
|
341
|
+
@workspace_oid = @workspace_ref.split('/')[-1][0...-3] # last part of url sans the '.js' suffix
|
342
|
+
getArtifactPrefixes()
|
343
|
+
@connected = true
|
344
|
+
return true
|
345
|
+
end
|
346
|
+
|
347
|
+
def getArtifactPrefixes()
|
348
|
+
@artifact_type = {}
|
349
|
+
@artifact_prefix = {}
|
350
|
+
@art_prefixes = []
|
351
|
+
|
352
|
+
query_info = { :type => :typedefinition, :fetch => "ElementName,IDPrefix",
|
353
|
+
:workspace => @wksp }
|
354
|
+
pfx_query = RallyAPI::RallyQuery.new(query_info)
|
355
|
+
results = @rally.find(pfx_query)
|
356
|
+
for typedef in results do
|
357
|
+
next if typedef['IDPrefix'].nil? or typedef['IDPrefix'].empty?
|
358
|
+
next if not VALID_ARTIFACT_TYPES.include?(typedef['ElementName'])
|
359
|
+
art_type = typedef['ElementName']
|
360
|
+
art_type = 'UserStory' if art_type == 'HierarchicalRequirement'
|
361
|
+
art_prefix = typedef['IDPrefix']
|
362
|
+
@artifact_type[art_prefix] = art_type
|
363
|
+
@artifact_prefix[art_type] = art_prefix
|
364
|
+
@art_prefixes << art_prefix
|
365
|
+
end
|
366
|
+
prefixes = @artifact_prefix.values.join('|')
|
367
|
+
@valid_artifact_pattern = Regexp.compile('^(%s)\d+$' % prefixes)
|
368
|
+
end
|
369
|
+
|
370
|
+
def validate()
|
371
|
+
# obtain a ref to the target SCMRepository
|
372
|
+
@log.info("SCMRepository: %s" % @config['RepositoryName'])
|
373
|
+
repo_name = @config['RepositoryName']
|
374
|
+
#todo should this get defaulted by the connector? it doesn't at the moment
|
375
|
+
if repo_name.nil?
|
376
|
+
problem = "A repo name must be specified in the Rally section of the config."
|
377
|
+
@log.error(problem)
|
378
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
379
|
+
raise confex, problem
|
380
|
+
end
|
381
|
+
|
382
|
+
repo = getSCMRepository(@wksp, repo_name)
|
383
|
+
|
384
|
+
if repo.nil?
|
385
|
+
repo = createSCMRepository(repo_name, @vcs_type, 'Created by VCSConnector', @vcs_uri)
|
386
|
+
action = "created SCMRepository: '%s' in the '%s' workspace"
|
387
|
+
@log.info(action % [repo_name, @workspace_name])
|
388
|
+
else
|
389
|
+
repo.read()
|
390
|
+
repo_oid = repo['_ref'].split('/')[-1][0...-3]
|
391
|
+
created = Time.parse(repo['CreationDate'])
|
392
|
+
found_repo = "Found SCMRepository '%s' with OID of %s created on %s in workspace '%s'"
|
393
|
+
@log.debug(found_repo % [repo_name, repo_oid, created.localtime, @wksp['Name']])
|
394
|
+
end
|
395
|
+
repo_oid = repo['_ref'].split('/')[-1][0...-3]
|
396
|
+
@repo_ref = 'scmrepository/%s' % repo_oid
|
397
|
+
|
398
|
+
configureExtensionEnvironment()
|
399
|
+
|
400
|
+
return true
|
401
|
+
end
|
402
|
+
|
403
|
+
|
404
|
+
def disconnect()
|
405
|
+
"""
|
406
|
+
Reset our rally instance variable to nil and toggle @connected indicator
|
407
|
+
"""
|
408
|
+
@rally = nil
|
409
|
+
@connected = false
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
private
|
414
|
+
def getSCMRepository(workspace, repo_name)
|
415
|
+
query = RallyAPI::RallyQuery.new(:type => :scmrepository,
|
416
|
+
:fetch => true,
|
417
|
+
:workspace => @wksp)
|
418
|
+
query.query_string = '(Name = "%s")' % repo_name
|
419
|
+
begin
|
420
|
+
results = @rally.find(query)
|
421
|
+
rescue Exception => ex
|
422
|
+
problem = "Unable to obtain Rally query results for SCMRepository named '%s'" % ex.message
|
423
|
+
boomex = VCSEIF_Exceptions::UnrecoverableException.new(problem)
|
424
|
+
raise boomex, problem
|
425
|
+
end
|
426
|
+
if results.length > 0
|
427
|
+
return results.first
|
428
|
+
else
|
429
|
+
@log.debug("No SCMRepository named |#{repo_name}| in Workspace: |#{workspace.Name}|")
|
430
|
+
return nil
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
|
435
|
+
def createSCMRepository(repo_name, scm_type, desc=nil, uri=nil)
|
436
|
+
"""
|
437
|
+
Create an SCMRepository using the given parms in the current workspace.
|
438
|
+
|
439
|
+
NOTA BENE: in order to get your SCMRepository created in a specific workspace
|
440
|
+
you _MUST_ provide the ref to that workspace, otherwise the
|
441
|
+
SCMRepository will be created in your default workspace.
|
442
|
+
|
443
|
+
ADDITIONAL: An SCMRepository is only indirectly associated with a Project
|
444
|
+
so setting it in the creation here would have no effect;
|
445
|
+
hence we dispense with attempting to do so.
|
446
|
+
"""
|
447
|
+
repo_spec = {
|
448
|
+
"Name" => repo_name,
|
449
|
+
"SCMType" => scm_type,
|
450
|
+
"Description" => desc || "",
|
451
|
+
"Uri" => uri || "",
|
452
|
+
"Workspace" => @workspace_ref,
|
453
|
+
}
|
454
|
+
activity = "Creating SCMRepository '%s' (%s) in the '%s' workspace..."
|
455
|
+
@log.info(activity % [repo_name, scm_type, @wksp.Name])
|
456
|
+
begin
|
457
|
+
repo = @rally.create(:scmrepository, repo_spec)
|
458
|
+
rescue Exception => ex
|
459
|
+
problem = 'Unable to create SCMRepository: %s' % ex.message
|
460
|
+
boomex = VCSEIF_Exceptions::UnrecoverableException.new(problem)
|
461
|
+
raise boomex, problem
|
462
|
+
end
|
463
|
+
@log.info('SCMRepository %s created, OID=%s' % [repo.Name, repo.ObjectID])
|
464
|
+
repo.read()
|
465
|
+
return repo
|
466
|
+
end
|
467
|
+
|
468
|
+
|
469
|
+
public
|
470
|
+
def getRecentChangesets(ref_time)
|
471
|
+
"""
|
472
|
+
Obtain all Changesets created in Rally at or after the ref_time parameter
|
473
|
+
(which is a Time instance)
|
474
|
+
"""
|
475
|
+
ref_time_readable = ref_time.to_s.sub('UTC', 'Z')
|
476
|
+
ref_time_iso = ref_time.iso8601
|
477
|
+
@log.info("Detecting recently added Rally Changesets")
|
478
|
+
selectors = ['SCMRepository.Name = "%s"' % @repository_name,
|
479
|
+
'CreationDate >= %s' % ref_time_iso
|
480
|
+
]
|
481
|
+
# TODO: See if having ChangesetSelectors is something that will be useful,
|
482
|
+
# but note that it will necessitate "binary'ing" the resultant query conditions
|
483
|
+
# to the Rally WSAPI requirements.
|
484
|
+
#for cs in changeset_selectors do
|
485
|
+
# sel = '%s %s "%s"' % [cs.field, ss.relation, cs.value]
|
486
|
+
# selectors << sel
|
487
|
+
#end
|
488
|
+
|
489
|
+
log_msg = ' recent Changesets query: %s' % selectors.join(' AND ')
|
490
|
+
@log.info(log_msg)
|
491
|
+
|
492
|
+
chgset_query = RallyAPI::RallyQuery.new(:type => :changeset,
|
493
|
+
:fetch => "Revision,CommitTimestamp",
|
494
|
+
:workspace => @workspace,
|
495
|
+
:project => nil,
|
496
|
+
:pagesize => 200,
|
497
|
+
:limit => 2000)
|
498
|
+
chgset_query.query_string = '((%s) AND (%s))' % selectors # P-eeee-yuuuhhhh (syntax)!
|
499
|
+
|
500
|
+
begin
|
501
|
+
results = @rally.find(chgset_query)
|
502
|
+
rescue => ex
|
503
|
+
bomex = VCSEIF_Exceptions::UnrecoverableException.new(ex.message)
|
504
|
+
raise boomex, ex.message.to_s
|
505
|
+
end
|
506
|
+
|
507
|
+
log_msg = " %d recently added Rally Changesets detected"
|
508
|
+
@log.info(log_msg % results.total_result_count)
|
509
|
+
changesets = []
|
510
|
+
results.each do |changeset|
|
511
|
+
changeset['ident'] = changeset.Revision
|
512
|
+
changesets << changeset
|
513
|
+
end
|
514
|
+
|
515
|
+
if not changesets.empty?
|
516
|
+
last_changeset = changesets.last
|
517
|
+
@log.info("date of last reflected Changeset in Rally: #{last_changeset.CommitTimestamp}")
|
518
|
+
end
|
519
|
+
|
520
|
+
return changesets
|
521
|
+
end
|
522
|
+
|
523
|
+
|
524
|
+
def createChangeset(int_work_item)
|
525
|
+
"""
|
526
|
+
This method should never be overridden (it might be called 'final' in another language).
|
527
|
+
Instead override any of the next three methods.
|
528
|
+
"""
|
529
|
+
modified_int_work_item = preCreate(int_work_item)
|
530
|
+
work_item = _createInternal(modified_int_work_item)
|
531
|
+
modified_artifact = postCreate(work_item)
|
532
|
+
return modified_artifact
|
533
|
+
end
|
534
|
+
|
535
|
+
|
536
|
+
private
|
537
|
+
|
538
|
+
def preCreate(int_work_item)
|
539
|
+
"""
|
540
|
+
Connection assumes int_work_item is giving us a timestamp already in iso8601 format
|
541
|
+
"""
|
542
|
+
drop_author = true # default to not including the Author in the Changeset
|
543
|
+
results = nil
|
544
|
+
# TODO: fix the following so we already have either User object or the username AND the user._ref ...
|
545
|
+
username = int_work_item['Author']
|
546
|
+
have_potential_author = !username.nil? and !username.empty?
|
547
|
+
if have_potential_author
|
548
|
+
user_query = RallyAPI::RallyQuery.new({:type => :user, :fetch => 'UserName'})
|
549
|
+
user_query.query_string = '(UserName = "%s")' % username
|
550
|
+
begin
|
551
|
+
results = @rally.find(user_query)
|
552
|
+
rescue => ex
|
553
|
+
@log.debug(ex.message)
|
554
|
+
problem = "Unable to complete Rally query for user information about '%s'" % username
|
555
|
+
operr = VCSEIF_Exceptions::OperationalError.new(problem)
|
556
|
+
@operational_errors += 1
|
557
|
+
raise operr, problem
|
558
|
+
end
|
559
|
+
end
|
560
|
+
if not results.nil?
|
561
|
+
user = results.first
|
562
|
+
begin
|
563
|
+
int_work_item['Author'] = user._ref
|
564
|
+
drop_author = false
|
565
|
+
rescue Exception => ex
|
566
|
+
verbiage = " No Rally User entry associated with '%s' was found, " % username
|
567
|
+
verbiage << "Author field for Changeset not set"
|
568
|
+
@log.warn(verbiage)
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
int_work_item.delete('Author') if drop_author
|
573
|
+
|
574
|
+
return int_work_item
|
575
|
+
end
|
576
|
+
|
577
|
+
|
578
|
+
def _createInternal(int_work_item)
|
579
|
+
# add Workspace and SCMRepository refs
|
580
|
+
int_work_item['Workspace'] = @workspace_ref
|
581
|
+
int_work_item['SCMRepository'] = @repo_ref
|
582
|
+
|
583
|
+
# add Artifacts from looking at Message and extracting the FormattedID values
|
584
|
+
artifactIDs = extractArtifactFormattedIDs(int_work_item['Message'])
|
585
|
+
if artifactIDs
|
586
|
+
@log.debug("artifactIDs from extractArtifactFormattedIDs: %s" % artifactIDs.join(", "))
|
587
|
+
end
|
588
|
+
artifacts = getArtifacts(artifactIDs)
|
589
|
+
artifacts.each { |art| @log.debug(" valid existing artifact: %s" % art.FormattedID) }
|
590
|
+
arts = artifacts.collect{ |art| art.FormattedID}.join(" ")
|
591
|
+
if artifacts.empty?
|
592
|
+
@log.debug("No valid artifacts were mentioned in message")
|
593
|
+
else
|
594
|
+
@log.debug("valid artifacts mentioned in message: %s" % arts)
|
595
|
+
end
|
596
|
+
artifact_refs = artifacts.collect {|art| {'_ref' => art._ref}}
|
597
|
+
int_work_item['Artifacts'] = artifact_refs
|
598
|
+
|
599
|
+
action_targets = {}
|
600
|
+
if @extensions.include?('StateExtractor')
|
601
|
+
extension = @extensions['StateExtractor']
|
602
|
+
if !extension.nil? and extension
|
603
|
+
extender = extension['extender']
|
604
|
+
# TODO: don't like hard-coding 'Message', what if we need others...?
|
605
|
+
action_targets = extender.service(int_work_item['Message'])
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
# save the action_targets and artifacts until after we create the Changeset/Changes
|
610
|
+
# then we can attempt to update those if so specified in the config (UpdateArtifactState : true)
|
611
|
+
|
612
|
+
# snarf the Additions, Modifications, Deletions to produce the Change list
|
613
|
+
# but then drop those out of the int_work_item
|
614
|
+
adds = int_work_item['Additions']
|
615
|
+
mods = int_work_item['Modifications']
|
616
|
+
dels = int_work_item['Deletions']
|
617
|
+
extraneous = ["Additions", "Modifications", "Deletions"]
|
618
|
+
for file_status in extraneous do # get rid of extraneous keys in int_work_item now
|
619
|
+
int_work_item.delete(file_status)
|
620
|
+
end
|
621
|
+
|
622
|
+
##
|
623
|
+
## puts ""
|
624
|
+
## puts "changeset raw data prior to attempted creation in Rally:"
|
625
|
+
## for field in ['Workspace', 'SCMRepository', 'Revision', 'CommitTimestamp',
|
626
|
+
## 'Author', 'Message', 'Uri', 'Artifacts'] do
|
627
|
+
## puts " %18s : %s" % [field, int_work_item[field]]
|
628
|
+
## end
|
629
|
+
## puts "valid artifacts mentioned in message: #{arts}"
|
630
|
+
## if config.get('UpdateArtifactState') and action_targets:
|
631
|
+
## showArtifacts(action_targets, artifacts)
|
632
|
+
## else
|
633
|
+
## puts "UpdateArtifactState setting: %s action_targets: %s" % \
|
634
|
+
## [config.get('UpdateArtifactState'), action_targets.inspect]
|
635
|
+
## end
|
636
|
+
## puts ""
|
637
|
+
## #NOTE: above commented out debugging/troubleshooting code last used June 2012
|
638
|
+
##
|
639
|
+
changeset = nil
|
640
|
+
begin
|
641
|
+
changeset = @rally.create(:changeset, int_work_item)
|
642
|
+
cset_info = [changeset.ObjectID, changeset.Revision[0...16], changeset["CommitTimestamp"]]
|
643
|
+
verbiage = "Created Changeset: OID: %s Revision: %s Timestamp:|%s|"
|
644
|
+
if artifacts.empty?
|
645
|
+
verbiage << " (not associated with any Artifacts)"
|
646
|
+
else
|
647
|
+
cset_info << arts
|
648
|
+
verbiage << " associated with Artifacts: %s"
|
649
|
+
end
|
650
|
+
@log.debug(verbiage % cset_info)
|
651
|
+
rescue Exception => ex
|
652
|
+
@log.debug(ex.message)
|
653
|
+
operr = VCSEIF_Exceptions::OperationalError.new(ex.message)
|
654
|
+
@operational_errors += 1
|
655
|
+
raise operr, ex.message.to_s
|
656
|
+
end
|
657
|
+
|
658
|
+
# add the Change records for the adds, mods and dels...
|
659
|
+
changes = {'A' => adds, 'M' => mods, 'D' => dels}
|
660
|
+
changes.each_pair do |action, files|
|
661
|
+
files.each do |fileinfo|
|
662
|
+
fn = fileinfo[:file] || ""
|
663
|
+
uri = fileinfo[:link] || ""
|
664
|
+
##
|
665
|
+
## ci = ["Changeset: #{changeset.ObjectID}",
|
666
|
+
## " Action: #{action} PathAndFilename: #{fn}",
|
667
|
+
## " Uri: #{uri}"
|
668
|
+
## ]
|
669
|
+
## puts "candidate Change info:\n %s" % ci.join("\n ")
|
670
|
+
##
|
671
|
+
chg_rec = {'Changeset' => changeset._ref,
|
672
|
+
'Action' => action,
|
673
|
+
'PathAndFilename' => fn,
|
674
|
+
'Uri' => uri
|
675
|
+
}
|
676
|
+
begin
|
677
|
+
change = @rally.create(:change, chg_rec)
|
678
|
+
info = [change.ObjectID, action, fn, uri]
|
679
|
+
blurb = " Change %s created for %s %s URI: %s"
|
680
|
+
@log.debug(blurb % info)
|
681
|
+
rescue Exception => ex
|
682
|
+
operr = VCSEIF_Exceptions::OperationalError.new(ex.message)
|
683
|
+
@operational_errors += 1
|
684
|
+
raise operr, ex.message.to_s
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
# call another method to attempt to update the artifacts with info in action_targets
|
690
|
+
if @config['UpdateArtifactState'] and action_targets
|
691
|
+
#showArtifacts(action_targets, artifacts)
|
692
|
+
updateArtifacts(action_targets, artifacts)
|
693
|
+
end
|
694
|
+
|
695
|
+
#we have to do the read since the changes are not on there with the original ref
|
696
|
+
return changeset.read
|
697
|
+
end
|
698
|
+
|
699
|
+
def postCreate(artifact)
|
700
|
+
"""
|
701
|
+
Either flesh this out here or override in a subclass to provide
|
702
|
+
processing after the Changeset has been created in Rally.
|
703
|
+
"""
|
704
|
+
return artifact
|
705
|
+
end
|
706
|
+
|
707
|
+
|
708
|
+
public
|
709
|
+
def changesetExists?(revision_ident)
|
710
|
+
"""
|
711
|
+
Issue a query against Changeset to obtain the Changeset identified by revision_ident.
|
712
|
+
Return a boolean indication of whether such an item exists in repository_name.
|
713
|
+
"""
|
714
|
+
criterion1 = 'SCMRepository.Name = "%s"' % @repository_name
|
715
|
+
criterion2 = 'Revision = %s' % revision_ident
|
716
|
+
criteria = '((%s) AND (%s))' % [criterion1, criterion2]
|
717
|
+
|
718
|
+
query = RallyAPI::RallyQuery.new({:type => :changeset,
|
719
|
+
:fetch => "Revision,CommitTimestamp,SCMRepository,Name",
|
720
|
+
:workspace => @workspace
|
721
|
+
})
|
722
|
+
query.query_string = criteria
|
723
|
+
begin
|
724
|
+
result = @rally.find(query)
|
725
|
+
rescue => ex
|
726
|
+
@log.debug(ex.message)
|
727
|
+
problem = "Unable to complete Rally query regarding existence of Changeset '%s'" % revision_ident
|
728
|
+
operr = VCSEIF_Exceptions::OperationalError.new(problem)
|
729
|
+
@operational_errors += 1
|
730
|
+
raise operr, problem
|
731
|
+
end
|
732
|
+
exists = false
|
733
|
+
exists = true if not result.nil? and result.total_result_count > 0
|
734
|
+
return exists
|
735
|
+
end
|
736
|
+
|
737
|
+
|
738
|
+
def getArtifacts(targets)
|
739
|
+
"""
|
740
|
+
For now this is brute force requiring a thump on the Rally endpoint for each artifact FormattedID.
|
741
|
+
TODO: consider caching for quicker turnaround and killing the duplicative queries.
|
742
|
+
"""
|
743
|
+
artifacts = []
|
744
|
+
targets.each do |target|
|
745
|
+
prefix = target.gsub(/\d/, '')
|
746
|
+
rally_entity_type = @artifact_type[prefix].downcase.to_sym
|
747
|
+
query = RallyAPI::RallyQuery.new(:type => rally_entity_type,
|
748
|
+
:fetch => "ObjectID,Name,FormattedID,ScheduleState,State",
|
749
|
+
:workspace => @wksp)
|
750
|
+
query.query_string = '(FormattedID = %s)' % target
|
751
|
+
begin
|
752
|
+
results = @rally.find(query)
|
753
|
+
rescue Exception => ex
|
754
|
+
# TODO: determine if setting missing to the text here is accurate,
|
755
|
+
# we should only hit this if the Rally WSAPI call fails,
|
756
|
+
# if there is no such Artifact, the query should return with empty results
|
757
|
+
missing = "No such Artifact: '%s' found in Rally for workspace: %s"
|
758
|
+
@log.warning(missing % [target, @workspace_name])
|
759
|
+
end
|
760
|
+
if !results.nil? and results.total_result_count > 0
|
761
|
+
artifact = results.first
|
762
|
+
@log.debug("artifact obtained: %s" % artifact.FormattedID)
|
763
|
+
artifacts << artifact
|
764
|
+
end
|
765
|
+
|
766
|
+
end
|
767
|
+
return artifacts
|
768
|
+
end
|
769
|
+
|
770
|
+
def getRallyUsers(attributes=nil)
|
771
|
+
"""
|
772
|
+
Run a REST get against Rally using @rally to get User information for all Rally
|
773
|
+
users in the currently scoped workspace and populate a cache keyed by the transform
|
774
|
+
lookup target with the UserName as the value for each key.
|
775
|
+
This makes it a snap to service the lookup method, we just have to consult the cache.
|
776
|
+
"""
|
777
|
+
fetch_fields = "_ref,UserName"
|
778
|
+
attributes.gsub(/UserName,?/, '') if !attributes.nil? and attributes =~ /UserName/
|
779
|
+
fetch_fields += "," + attributes if !attributes.nil? and !attributes.empty?
|
780
|
+
fetch_fields += ",DisplayName" if fetch_fields !~ /DisplayName/
|
781
|
+
query = RallyAPI::RallyQuery.new(:type => :user, :fetch => fetch_fields)
|
782
|
+
begin
|
783
|
+
results = @rally.find(query)
|
784
|
+
rescue Exception => ex
|
785
|
+
if @log and @log.respond_to?('error')
|
786
|
+
@log.error("Unable to retrieve User information from Rally")
|
787
|
+
end
|
788
|
+
raise
|
789
|
+
end
|
790
|
+
|
791
|
+
results.each do |user|
|
792
|
+
urec = {}
|
793
|
+
urec["ref"] = user.send("_ref")
|
794
|
+
if not attributes.nil?
|
795
|
+
for attr_name in attributes.split(',') do
|
796
|
+
urec[attr_name] = user.send(attr_name)
|
797
|
+
end
|
798
|
+
end
|
799
|
+
urec['UserName'] = user.send('UserName') if not urec.has_key?('UserName')
|
800
|
+
urec['DisplayName'] = user.send('DisplayName') if not urec.has_key?('DisplayName')
|
801
|
+
@user_cache[urec['UserName']] = urec
|
802
|
+
if @log and @log.respond_to?('debug')
|
803
|
+
@log.debug("user cache item |%s|" % urec['UserName'])
|
804
|
+
end
|
805
|
+
end
|
806
|
+
return @user_cache.dup # let caller have their own copy of the @user_cache
|
807
|
+
end
|
808
|
+
|
809
|
+
|
810
|
+
private
|
811
|
+
def isCompletedTask?(art_type, target_field, action)
|
812
|
+
"""
|
813
|
+
Convenience method
|
814
|
+
"""
|
815
|
+
return (art_type == 'Task' and target_field == 'State' and action == 'Completed')
|
816
|
+
end
|
817
|
+
|
818
|
+
|
819
|
+
def showArtifacts(action_targets, artifacts)
|
820
|
+
"""
|
821
|
+
Go through the action_targets Hash processing only those entries with a non-nil key.
|
822
|
+
|
823
|
+
"""
|
824
|
+
for action, targets in action_targets.each_pair do
|
825
|
+
next if action.nil?
|
826
|
+
targets = action_targets[action]
|
827
|
+
for target in targets do
|
828
|
+
artifact = artifacts.select { |art| art.FormattedID == target }
|
829
|
+
next if artifact.length == 0
|
830
|
+
|
831
|
+
prefix = target.gsub(/\d/, '')
|
832
|
+
begin
|
833
|
+
target_field = 'ScheduleState' if artifact.ScheduleState
|
834
|
+
rescue
|
835
|
+
target_field = 'State'
|
836
|
+
end
|
837
|
+
upd_rec = { 'FormattedID' => target, target_field => action }
|
838
|
+
if isCompletedTask?(@artifact_type[prefix], target_field, action)
|
839
|
+
upd_rec['ToDo'] = 0
|
840
|
+
end
|
841
|
+
end
|
842
|
+
end
|
843
|
+
end
|
844
|
+
|
845
|
+
|
846
|
+
def updateArtifacts(action_targets, artifacts)
|
847
|
+
"""
|
848
|
+
Go through the action_targets Hash processing only those entries with a non-nil key.
|
849
|
+
|
850
|
+
This may be the right point at which to qualify for update based on the action word.
|
851
|
+
However, as an action word can be associated with multiple targets that may not be
|
852
|
+
the same artifact type, this might be a bit dicey. For now, go with the shotgun...
|
853
|
+
|
854
|
+
"""
|
855
|
+
actions = action_targets.keys.select {|key| key != nil}
|
856
|
+
@log.debug( "updateArtifacts has %d actions" % actions.length)
|
857
|
+
for action in actions do
|
858
|
+
targets = action_targets[action]
|
859
|
+
@log.debug( "updateArtifacts has %d artifacts to %s" % [targets.length, action])
|
860
|
+
for target in targets do
|
861
|
+
@log.debug("updateArtifacts target: %s" % target)
|
862
|
+
prefix = target.gsub(/\d/, '')
|
863
|
+
target_artifacts = artifacts.select {|art| art.FormattedID == target}
|
864
|
+
if target_artifacts.empty?
|
865
|
+
@log.debug(" there are no target artifacts")
|
866
|
+
next
|
867
|
+
end
|
868
|
+
artifact = target_artifacts.first
|
869
|
+
target_field = 'State'
|
870
|
+
target_field = 'ScheduleState' if not ['defect', 'task'].include?(@artifact_type[prefix].downcase)
|
871
|
+
debug_info = "updateArtifacts to assign %s %s as |%s|"
|
872
|
+
@log.debug(debug_info % [artifact.FormattedID, target_field, action])
|
873
|
+
upd_rec = { 'FormattedID' => target, target_field => action }
|
874
|
+
if isCompletedTask?(@artifact_type[prefix], target_field, action)
|
875
|
+
upd_rec['ToDo'] = 0
|
876
|
+
end
|
877
|
+
@log.info("Updating %s %s field with value of %s" % [target, target_field, action])
|
878
|
+
debug_info = "attempting to update %s with %s" % [@artifact_type[prefix], upd_rec]
|
879
|
+
@log.debug(debug_info)
|
880
|
+
rally_entity_type = @artifact_type[prefix].downcase.to_sym
|
881
|
+
begin
|
882
|
+
updated = @rally.update(rally_entity_type, artifact.ObjectID, upd_rec)
|
883
|
+
rescue Exception => ex
|
884
|
+
##
|
885
|
+
## puts "Exception detected in updateArtifacts, line 874, %s" % ex.message
|
886
|
+
## puts " action value: #{actions}"
|
887
|
+
## puts " targets value: #{targets}"
|
888
|
+
## puts " target value triggering exception: #{target}"
|
889
|
+
## NOTE: last used in July 2012
|
890
|
+
##
|
891
|
+
operr = VCSEIF_Exceptions::OperationalError.new(ex.message)
|
892
|
+
@operational_errors += 1
|
893
|
+
raise operr, ex.message
|
894
|
+
end
|
895
|
+
debug_info = "After update, %s %s %s field has value of |%s|" % \
|
896
|
+
[@artifact_type[prefix], updated.FormattedID,
|
897
|
+
target_field, updated.send(target_field)]
|
898
|
+
@log.debug(debug_info)
|
899
|
+
end
|
900
|
+
end
|
901
|
+
end
|
902
|
+
|
903
|
+
public
|
904
|
+
def extractArtifactFormattedIDs(message)
|
905
|
+
%{
|
906
|
+
A brute force method to extract what look to be Rally Artifact FormattedID values
|
907
|
+
from within the message.
|
908
|
+
The algorithm is to replace any comma chars with a single space, any
|
909
|
+
colon with a single space, any semi-colon with a single space,
|
910
|
+
any '.' char with a single space, any '-' char with a single space and
|
911
|
+
then split the message on whitespace, resulting in a list of "word" tokens.
|
912
|
+
The token list is then subjected to conformance with a pattern for FormattedID
|
913
|
+
fof the types of artifacts that are listed in the configuration.
|
914
|
+
Returns the qualified list of tokens that are indeed validly constructed Rally FormattedIDs
|
915
|
+
(although this method makes no attempt to determine that an artifact actually exists for
|
916
|
+
each FormattedID candidate).
|
917
|
+
}
|
918
|
+
washed_message = message.gsub(',', ' ').gsub(':', ' ').gsub(';', ' ').gsub('.', ' ').gsub('-', ' ')
|
919
|
+
message_words = washed_message.split()
|
920
|
+
artifact_ids = message_words.select {|token| @valid_artifact_pattern.match(token)}
|
921
|
+
@log.debug("candidate FormattedIDs: %s" % artifact_ids.join(", "))
|
922
|
+
return artifact_ids.sort
|
923
|
+
end
|
924
|
+
|
925
|
+
end
|