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