vcseif 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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