vcseif 1.2.0

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