Class: VCSConnector

Inherits:
Object
  • Object
show all
Defined in:
lib/vcseif/vcs_connector.rb

Constant Summary collapse

VERSION =
VCSEIF::Version
PLUGIN_SPEC_PATTERN =
Regexp.compile('^(?<plugin_class>\w+)\s*\((?<plugin_config>[^\)]*)\)\s*$')
PLAIN_PLUGIN_SPEC_PATTERN =
Regexp.compile('(?<plugin_class>\w+)\s*$')
RALLY_TIMESTAMP =
"%Y-%m-%dT%H:%M:%S.%L %Z"
ISO8601_TIMESTAMP =
"%Y-%m-%dT%H:%M:%S"
@@transformable_attributes =
['Author']

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config, logger) ⇒ VCSConnector

Returns a new instance of VCSConnector.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/vcseif/vcs_connector.rb', line 40

def initialize(config, logger)
    @config = config
    @log    = logger

    conn_sections = []
    begin
        non_conn_sections = ['Services', 'Transforms']
        conn_sections = config.topLevels.select {|section| !non_conn_sections.include?(section)}
        if conn_sections.length != 2
            problem = 'Config does not contain two connection sections'
            confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
            raise confex, problem
        end    
    rescue Exception => ex
        problem = 'Config file lacks sufficient information for VCSConnector operation'
        confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
        raise confex, problem
    end

    @rally_conn = nil
    @vcs_conn   = nil

    @vcs_name = conn_sections.select {|section_name| section_name !~ /^Rally/}.first
    @log.info("Rally VCS Connector for %s, version %s" % [@vcs_name, VERSION])
    @log.info("Ruby platform: #{RUBY_PLATFORM}")
    @log.info("Ruby  version: #{RUBY_VERSION}")

    internalizeConfig(config)

    rally_conn_class_name = config.connectionClassName('Rally')
    vcs_conn_class_name   = config.connectionClassName(@vcs_name)

    @log.debug("Loading #{rally_conn_class_name} class")
    begin
        @rally_conn_class = ClassLoader.loadConnectionClass('lib', rally_conn_class_name, 'rally_vcs_connection')
    rescue Exception => ex
        problem = 'Unable to load RallyVCSConnection class, %s' % ex.message
        boomex = VCSEIF_Exceptions::UnrecoverableException.new(problem)
        raise boomex, problem
    end

    @log.debug("Loading #{vcs_conn_class_name} class")
    begin
        @vcs_conn_class = ClassLoader.loadConnectionClass('lib', vcs_conn_class_name)
    rescue Exception => ex
        problem = 'Unable to load %sConnection class, %s' % [@vcs_name, ex.message]
        boomex = VCSEIF_Exceptions::UnrecoverableException.new(problem)
        raise boomex, problem
    end

    @log.debug('Obtaining Rally and VCS connections...')
    establishConnections()
    @log.debug('Rally and VCS connections established')
    validate()  # basically just calls validate on both connection instances

    @log.debug("loading Transform class ...")
    @transforms = loadTransforms(@txfm_conf)
    @log.debug("Transform loaded")

    @log.info("Initialization complete: Delegate connections operational, " +\
              "aux facilities loaded, ready for scan/reflect ops")
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



34
35
36
# File 'lib/vcseif/vcs_connector.rb', line 34

def config
  @config
end

#logObject (readonly)

Returns the value of attribute log.



34
35
36
# File 'lib/vcseif/vcs_connector.rb', line 34

def log
  @log
end

#rally_confObject (readonly)

Returns the value of attribute rally_conf.



37
38
39
# File 'lib/vcseif/vcs_connector.rb', line 37

def rally_conf
  @rally_conf
end

#rally_connObject (readonly)

Returns the value of attribute rally_conn.



35
36
37
# File 'lib/vcseif/vcs_connector.rb', line 35

def rally_conn
  @rally_conn
end

#rally_conn_classObject (readonly)

Returns the value of attribute rally_conn_class.



35
36
37
# File 'lib/vcseif/vcs_connector.rb', line 35

def rally_conn_class
  @rally_conn_class
end

#svc_confObject (readonly)

Returns the value of attribute svc_conf.



37
38
39
# File 'lib/vcseif/vcs_connector.rb', line 37

def svc_conf
  @svc_conf
end

#transformsObject (readonly)

Returns the value of attribute transforms.



38
39
40
# File 'lib/vcseif/vcs_connector.rb', line 38

def transforms
  @transforms
end

#txfm_confObject (readonly)

Returns the value of attribute txfm_conf.



37
38
39
# File 'lib/vcseif/vcs_connector.rb', line 37

def txfm_conf
  @txfm_conf
end

#vcs_confObject (readonly)

Returns the value of attribute vcs_conf.



37
38
39
# File 'lib/vcseif/vcs_connector.rb', line 37

def vcs_conf
  @vcs_conf
end

#vcs_connObject (readonly)

Returns the value of attribute vcs_conn.



35
36
37
# File 'lib/vcseif/vcs_connector.rb', line 35

def vcs_conn
  @vcs_conn
end

#vcs_conn_classObject (readonly)

Returns the value of attribute vcs_conn_class.



35
36
37
# File 'lib/vcseif/vcs_connector.rb', line 35

def vcs_conn_class
  @vcs_conn_class
end

#vcs_nameObject (readonly)

Returns the value of attribute vcs_name.



36
37
38
# File 'lib/vcseif/vcs_connector.rb', line 36

def vcs_name
  @vcs_name
end

Instance Method Details



349
350
351
352
353
354
355
356
357
# File 'lib/vcseif/vcs_connector.rb', line 349

def collect_files_and_links(revnum, file_list, operation)
  return_list = []
  file_list.each do |rev_file|
    file_uri =  @vcs_conn.get_file_rev_uri(revnum, rev_file, operation)
    log.debug(" Change file URI: #{file_uri}")
    return_list << {:file => rev_file, :link => file_uri}
  end
  return_list
end

#establishConnectionsObject



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/vcseif/vcs_connector.rb', line 119

def establishConnections()
    @rally_conn = @rally_conn_class.new(@rally_conf, @log)
    @vcs_conn   =   @vcs_conn_class.new(  @vcs_conf, @log)
    @vcs_conn.connect()  # we do this before rally_conn to be able to get the vcs backend version
    vcs_backend_version = @vcs_conn.getBackendVersion()

    if @rally_conn != nil and @vcs_conn != nil and @rally_conn.respond_to?('set_integration_header')
        # so we can use it in our X-Rally-Integrations header items here
        rally_headers = {'name'    => 'Rally VCSConnector for %s' % @vcs_name,
                         'version' => VERSION,
                         'vendor'  => 'Rally Software',
                         'other_version' => vcs_backend_version
                        }
        @rally_conn.set_integration_header(rally_headers)
    end
    @rally_conn.setSourceIdentification(@vcs_conn.name, @vcs_conn.uri)
    @rally_conn.connect()  
end

#get_config_lookback(config) ⇒ Object



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/vcseif/vcs_connector.rb', line 373

def get_config_lookback(config)
  default_value = 3600  # 1 hour in seconds
  lookback_val = config['Lookback']
  return default_value if lookback_val.nil? || lookback_val.to_i <= 0

  #default assumption is in minutes for lookback setting
  lookback_val  = lookback_val.to_s.downcase.strip
  converted_val = lookback_val.to_i  #rubyism:  "5days".to_i = 5   "number".to_i = 0
  return_val    = (converted_val <= 0) ? default_value : converted_val * 60  #seconds per minute

  if lookback_val.include?("d")     #5 days, 5days, 5d, 5 d
    return_val = (converted_val <= 0) ? default_value : converted_val * 86400  #seconds per day
  elsif lookback_val.include?("h")  #24 hours, 24 hours, 24h, 24 h
    return_val = (converted_val <= 0) ? default_value : converted_val * 3600  #seconds per hour
  end
  return_val
end

#getRefTimes(last_commit_timestamp) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/vcseif/vcs_connector.rb', line 359

def getRefTimes(last_commit_timestamp)
    """
        last_commit_timestamp is provided as an epoch seconds value. 
        Return a two-tuple of the reference time to be used for obtaining the 
        recent changesets in Rally and the reference time to be used for 
        obtaining the recent changesets in the target VCS system.
    """
    rally_lookback = get_config_lookback(@rally_conf)
    vcs_lookback   = get_config_lookback(@vcs_conf)
    rally_ref_time = Time.at(last_commit_timestamp - rally_lookback).gmtime
    vcs_ref_time   = Time.at(last_commit_timestamp -   vcs_lookback).gmtime 
    return [rally_ref_time, vcs_ref_time]
end

#identifyUnrecordedChangesets(rally_changesets, vcs_changesets, ref_time) ⇒ Object



436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/vcseif/vcs_connector.rb', line 436

def identifyUnrecordedChangesets(rally_changesets, vcs_changesets, ref_time)
    """
        Assume that for purposes of matching that the changeset identity in either system
        can be determined by the rev_ident (as opposed to the rev_num).
        If there are items in the rally_changesets for which there is  a counterpart in 
        the vcs_changesets, the information has already been reflected in Rally.  --> NOOP
        If there are items in the rally_changesets for which there is no counterpart in 
        the vcs_changesets, information has been lost,  dat be berry berry bad... --> ERROR
        If there are items in the vcs_changesets   for which there is no counterpart in
        the rally_changesets, those items are candidates to be reflected in Rally --> REFLECT
    """
    rally_tank = {}
    # requirement:  rc.ident <-- rc.Revision
    rally_changesets.each {|rally_chgset| rally_tank[rally_chgset.ident] = rally_chgset}

    vcs_tank   = {}
    # requirement:  vc.ident <-- {node} or {hash} or {number}, etc .
##
##        puts "vcs_changesets ..."
##        vcs_changesets.each { |cset| { puts cset.details }
##
    vcs_changesets.each {|vcs_chgset| vcs_tank[vcs_chgset.ident] = vcs_chgset}

    reflected_changesets  = rally_changesets.select {|rcs|   vcs_tank.has_key?(rcs.ident) == true }
    unpaired_changesets   = rally_changesets.select {|rcs|   vcs_tank.has_key?(rcs.ident) == false}
    unrecorded_changesets =   vcs_changesets.select {|vcs| rally_tank.has_key?(vcs.ident) == false}

    if unpaired_changesets.length > 0
        #lost_changesets = unpaired_changesets.select {|rcs| rcs.CommitTimestamp > ref_time}
        lost_changesets = []
        unpaired_changesets.each do |rcs|
            rally_commit_time = Time.strptime(rcs.CommitTimestamp[0..18], ISO8601_TIMESTAMP)
            lost_changesets << rcs if rally_commit_time > ref_time
        end
        # Hold off on blurting this information in the log. It may just lead to confusion.
        # There _could_ be other systems throwing info into the Rally Change/Changeset pool
        # (although we wouldn't expect that to happen very often).
        #if not lost_changesets.empty?
        #  problem =  "Changesets exist in Rally for which there is no "
        #  problem << "longer a counterpart changeset in the target VCS\n    "
        #  cs_idents = lost_changesets.collect {|cs| cs.ident}
        #  @log.error(problem + cs_idents.join("\n    "))
        #end
    end

    return unrecorded_changesets
end

#internalizeConfig(config) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/vcseif/vcs_connector.rb', line 104

def internalizeConfig(config)
    @rally_conf = config.topLevel('Rally')
    @vcs_conf   = config.topLevel(@vcs_name)
    @svc_conf   = config.topLevel('Services')
    @txfm_conf  = config.topLevel('Transforms') || {}
    # default the Author transform to Passthru if not specified in @txfm_conf
    if not @txfm_conf.keys.include?('Author')
        @txfm_conf['Author'] = 'Passthru'
    end
##
##        @log.debug("config.Services has |#{@svc_conf.inspect}|")
##
end

#postBatch(extension, status, repo_changesets) ⇒ Object



251
252
253
254
255
256
257
258
# File 'lib/vcseif/vcs_connector.rb', line 251

def postBatch(extension, status, repo_changesets)
    """
    """
    if extension != nil and extension =~ /PostBatchAction/
        postba = extension['PostBatchAction']
        postba.service(status, repo_changesets)
    end
end

#preBatch(extension) ⇒ Object



241
242
243
244
245
246
247
248
# File 'lib/vcseif/vcs_connector.rb', line 241

def preBatch(extension)
    """
    """
    if extension != nil and extension =~ /PreBatchAction/
        preba = extension['PreBatchAction']
        preba.service()
    end
end

#reflectChangesetsInRally(last_commit_timestamp) ⇒ Object

last_commit_timestamp is an instance of Time



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/vcseif/vcs_connector.rb', line 261

def reflectChangesetsInRally(last_commit_timestamp)
    """
        The last commit timestamm is passed to Connection objects in UTC;
        they are responsible for converting if necessary. 
        Time in log messages is always reported in UTC (aka Z or Zulu time).
    """
    status = false
    rally  = @rally_conn
    vcs    = @vcs_conn

    rally_repo   = @rally_conf['RepositoryName']
    preview_mode = false
    preview_item = @svc_conf['Preview'].to_s.downcase
    preview_mode = true if ['true', 'yes', '1', 'ok', 'on'].include?(preview_item)
    pm_tag = ''
    action = 'adding'
    if preview_mode == true
        pm_tag = "Preview: "
        action = "would add"
        @log.info('***** Preview Mode *****   (no Changesets will be created in Rally)')
    end

    begin
        rally_ref_time, vcs_ref_time = getRefTimes(last_commit_timestamp)
        time_info = "    ref times -->  rally_ref_time: |%s|  vcs_ref_time: |%s|"
        @log.debug(time_info % [rally_ref_time, vcs_ref_time])
        recent_rally_changesets = rally.getRecentChangesets(rally_ref_time)
        @log.debug("Obtained Rally recent changesets info")
        recent_vcs_changesets   =   vcs.getRecentChangesets(  vcs_ref_time)
        @log.debug("Obtained VCS recent changesets info")
        unrecorded_changesets = identifyUnrecordedChangesets(recent_rally_changesets, 
                                                             recent_vcs_changesets, 
                                                             vcs_ref_time)
        @log.info("%d unrecorded Changesets" % unrecorded_changesets.length)
        
        recorded_changesets = []
        unrecorded_changesets.each do |changeset|
            cts = changeset.commit_timestamp.sub('Z', ' Z').sub('T', ' ')
            desc = '%sChangeset %16.16s | %s | %s  not yet reflected in Rally'
            @log.debug(desc % [pm_tag, changeset.ident, cts, changeset.author])
            committer = changeset.author
            transformEligibleAttributes(changeset)

            adds = collect_files_and_links(changeset.ident, changeset.adds, 'add')
            mods = collect_files_and_links(changeset.ident, changeset.mods, 'mod')
            dels = collect_files_and_links(changeset.ident, changeset.dels, 'del')

            info = { 
                     'Revision'        => changeset.ident,
                     'CommitTimestamp' => changeset.commit_timestamp,
                     'Committer'       => committer,
                     'Author'          => changeset.author,
                     'Message'         => changeset.message,
                     'Additions'       => adds,
                     'Modifications'   => mods,
                     'Deletions'       => dels,
                     'Uri'             => @vcs_conn.get_rev_uri(changeset.ident)
                   }

            desc = '%sChangeset %16.16s | %s | %s  %s to Rally...'
            @log.info(desc % [pm_tag, changeset.ident, cts, changeset.author, action])
            @log.debug("  Rev URI: #{info['Uri']}")

            if not preview_mode
                begin
                    if not rally.changesetExists?(changeset.ident)
                        rally_changeset = rally.createChangeset(info)
                        recorded_changesets << rally_changeset
                    end
                rescue Exception => ex
                    raise VCSEIF_Exceptions::OperationalError.new(ex.message)
                end
            end
        end
        status = true
    rescue VCSEIF_Exceptions::OperationalError => ex
        # already resulted in a log message
    rescue VCSEIF_Exceptions::UnrecoverableException => ex
        raise
    rescue Exception => ex
        raise
    end    
    status = false if rally.operational_errors > 0

    repo_changesets = {rally_repo => recorded_changesets} 
    return [status, repo_changesets]
end

#run(last_commit_timestamp, extension) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/vcseif/vcs_connector.rb', line 227

def run(last_commit_timestamp, extension)
    """
        Where the rubber meets the road, or rather, controlling 
        the machinery that puts the rubber on the road...
    """
    preBatch(extension)
    status, repo_changesets = reflectChangesetsInRally(last_commit_timestamp)
    postBatch(extension, status, repo_changesets)
    @rally_conn.disconnect()
    @vcs_conn.disconnect()
    return [status, repo_changesets]
end

#transformEligibleAttributes(changeset) ⇒ Object



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/vcseif/vcs_connector.rb', line 391

def transformEligibleAttributes(changeset)
    """
        Check for a transformer for each transformable attribute, 
        if no such transformer, leave the changeset.<attribute> value unchanged.
        Otherwise, get the transformer for the transformable attribute,
        call it's service method with:
           getattr(changeset, transformable_attribute.lower())
        and use the result as the value for transformable_attribute,
        modifying the changeset item.
    """
    @@transformable_attributes.each do |transformable_attribute|
    @log.debug("tranformable attribute: |#{transformable_attribute}|")
        if @transforms.include?(transformable_attribute)
            @log.debug("tranforms has a transformer class for #{transformable_attribute}")
            transformer = @transforms[transformable_attribute] 
            target_attr = transformable_attribute.downcase()
            @log.debug("transformable target attribute: |#{target_attr}|")
            target_value = changeset.send(target_attr)
            @log.debug("transform target_value: |#{target_value}|")
            transformed = transformer.service(target_value)
            @log.debug("transformed to value: |#{transformed}|")
            if not transformed.nil?
                begin
##
##                        puts "#{changeset.class.name} attributes: #{changeset.instance_variables}"
##
                    changeset.send("#{target_attr}=", transformed)
                rescue Exception => ex
                    puts "#{ex.message}"
                    for bt_line in ex.backtrace do
                        puts bt_line.sub(Dir.pwd, '${CWD}')
                    end
                    puts "#{ex.inspect}"
                end
##
##                puts "Changeset.#{target_attr} new value: |#{changeset.send(target_attr)}| should be set to |#{transformed}| ?"
##
            else
                # TODO: should we log this?
            end
        end
    end
end

#validateObject



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/vcseif/vcs_connector.rb', line 202

def validate()
    """
        This calls the validate method on both the Rally and the VCS connections
    """
    valid = true

    @log.info("Connector validation starting")

    if not @rally_conn.validate()
        @log.debug("RallyConnection validation failed")
        return false
    end
    @log.debug("RallyConnection validation succeeded")

    if not @vcs_conn.validate()
        @log.debug("%sConnection validation failed" % @vcs_name)
        return false
    end
    @log.debug("%sConnection validation succeeded" % @vcs_name)
    @log.info("Connector validation completed")

    return valid
end