Module: HelixVersioningEngine::Util

Defined in:
lib/helix_versioning_engine/util.rb

Overview

TODO most of the methods in this module should probably be removed

Namespace for p4ruby conventions used in the P4 Web API.

Constant Summary

PROPERTIES =
[
  :password, :port, :user, :api_level, :charset, :client,
  :host, :handler, :maxlocktime, :maxresults, :maxscanrows, :prog,
  :ticketfile
]
BRANCHES_MAP =

Data 'normalization'

A very annoying aspect of our tagged output is that it often is slightly different between 'plural' and spec forms. This logic is made available as an option in this API

{
  'branch' => 'Branch'
}
BRANCHES_DATES =
%w(Update Access)
CHANGES_MAP =
{
  'path' => 'Path',
  'change' => 'Change',
  'time' => 'Date',
  'client' => 'Client',
  'user' => 'User',
  'status' => 'Status',
  'type' => 'Type',
  'changeType' => 'Type',
  'desc' => 'Description'
}
CHANGES_DATES =
['Date']
CLIENTS_MAP =
{
  'client' => 'Client'
}
CLIENTS_DATES =
%w(Update Access)
DEPOTS_MAP =
{
  'name' => 'Depot',
  'time' => 'Date',
  'type' => 'Type',
  'map' => 'Map',
  'desc' => 'Description'
}
DEPOTS_DATES =
['Date']
DESCRIBE_MAP =

Note that individual files are handled differently, since the keys are prefixed

CHANGES_MAP
DESCRIBE_DATES =
CHANGES_DATES
DIRS_MAP =
{
  'dir' => 'Dir'
}
FILES_MAP =
{
  'depotFile' => 'DepotFile',
  'rev' => 'Revision',
  'change' => 'Change',
  'action' => 'Action',
  'type' => 'Type',
  'time' => 'Date'
}
FILES_DATES =
%w(Date)
GROUPS_MAP =
{
  'group' => 'Group',
  'maxResults' => 'MaxResults',
  'maxScanRows' => 'MaxScanRows',
  'maxLockTime' => 'MaxLockTime',
  'timeout' => 'Timeout',
  'desc' => 'Description',
  'user' => 'User',
  'isSubGroup' => 'IsSubGroup',
  'isOwner' => 'IsOwner',
  'isUser' => 'IsUser',
  'passTimeout' => 'PasswordTimeout'
}
JOBS_MAP =
{}
JOBS_DATES =
%w(Date)
LABELS_MAP =
{
  'label' => 'Label'
}
LABELS_DATES =
%w(Update Access)
STREAMS_MAP =

This isn't supported but kept here for reference. We might support this. OPENED_MAP = { 'change' => 'Change', 'client' => 'Client', 'user' => 'User' }

{
  'desc' => 'Description'
}
STREAMS_DATES =
%w(Update Access)
USERS_MAP =
{
  'passwordChange' => 'PasswordChange'
}
USERS_DATES =
%w(Update Access PasswordChange)
GROUP_PROPERTIES =

These are 'single values' of properties of Group objects we use while collating them.

%w(
Group MaxResults MaxScanRows MaxLockTime Timeout PasswordTimeout
FILE_KEYS =
%w(depotFile action type rev digest fileSize)

Class Method Summary (collapse)

Class Method Details

+ (Object) assert_no_special_paths(paths)

Assert that no relative directory or Perforce wildcard is in use for each string in the paths array.



67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/helix_versioning_engine/util.rb', line 67

def self.assert_no_special_paths(paths)
  paths.each do |path|
    if Util.wildcard?(path)
      fail P4Error.default_error("The path '#{path}' contains a Perforce "\
        'wildcard, which is not allowed')
    end
    if Util.relative_dir?(path)
      fail P4Error.default_error("The path '#{path}' is a relative " \
        'directory, which is not allowed')
    end
  end
end

+ (Object) collate_group_results(results)

Will apply the normalizer to set up a consistent field naming and date format, but then 'collates' the output into a single list of 'groups'.

The groups output is kind of funny, because it basically lists users, with group access fields. It's very non-obvious, and in the end, most clients will need to run logic like this anyway.



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/helix_versioning_engine/util.rb', line 334

def self.collate_group_results(results)
  collated = results.group_by { |x| x['Group'] }
  updated = collated.map do |_key, items|
    # The first item sets most of the values, we then figure out array
    # subvalues

    group = {}
    GROUP_PROPERTIES.each do |p|
      group[p] = items.first[p] if items.first.key?(p)
    end

    group['Users'] =
        items.find_all { |x| x['IsUser'] == '1' }.map { |x| x['User'] }

    group['Subgroups'] =
        items.find_all { |x| x['IsSubGroup'] == '1' }.map { |x| x['User'] }

    group['Owners'] =
        items.find_all { |x| x['IsOwner'] == '1' }.map { |x| x['User'] }

    group
  end

  results.replace(updated)
end

+ (Boolean) date_str?(str)

Returns true if we can parse the string as a date

Returns:

  • (Boolean)


418
419
420
# File 'lib/helix_versioning_engine/util.rb', line 418

def self.date_str?(str)
  Date.parse(str) rescue false
end

+ (Boolean) error?(p4)

Check for P4 errors

Returns:

  • (Boolean)


55
56
57
# File 'lib/helix_versioning_engine/util.rb', line 55

def self.error?(p4)
  !p4.errors.empty?
end

+ (Boolean) existing_path?(existing_results, depot_path)

Returns:

  • (Boolean)


117
118
119
120
121
# File 'lib/helix_versioning_engine/util.rb', line 117

def self.existing_path?(existing_results, depot_path)
  existing_results.any? do |result|
    result['depotFile'] == depot_path
  end
end

+ (Boolean) i?(str)

Returns true if .to_i will actually convert this string to an integer

Returns:

  • (Boolean)


413
414
415
# File 'lib/helix_versioning_engine/util.rb', line 413

def self.i?(str)
  (str =~ /\A[-+]?\d+\z/)
end

+ (Object) init_changelist(p4, description)



102
103
104
105
106
107
# File 'lib/helix_versioning_engine/util.rb', line 102

def self.init_changelist(p4, description)
  change_spec = p4.fetch_change
  change_spec._description = description
  results = p4.save_change(change_spec)
  results[0].gsub(/^Change (\d+) created./, '\1')
end

+ (Object) local_path(depot_path, root)



128
129
130
131
# File 'lib/helix_versioning_engine/util.rb', line 128

def self.local_path(depot_path, root)
  stripped = depot_path.gsub(/^\/+/, '')
  File.join(root, stripped)
end

+ (Object) make_normalizer(field_map, offset, date_fields = nil)



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/helix_versioning_engine/util.rb', line 298

def self.make_normalizer(field_map, offset, date_fields = nil)
  lambda do |results|
    return unless results

    # We need to ignore any instances of P4::Spec since that will 'validate'
    # fields on it's accessor methods
    results.map! do |result|
      if result.class <= P4::Spec
        spec = result
        result = Hash.new
        result.merge!(spec)
      else
        result
      end
    end

    results.each do |result|
      update_fields(field_map, result)
      update_dates(date_fields, result, offset)
    end
    results
  end
end

+ (Object) map_describe_file_key(key)



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/helix_versioning_engine/util.rb', line 395

def self.map_describe_file_key(key)
  case key
  when /^depotFile$/
    return 'DepotFile'
  when /^action$/
    return 'Action'
  when /^type$/
    return 'Type'
  when /^rev$/
    return 'Revision'
  when /^fileSize$/
    return 'FileSize'
  when /^digest$/
    return 'Digest'
  end
end

+ (Object) mark_change(type, p4, change_id, root, depot_path)



123
124
125
126
# File 'lib/helix_versioning_engine/util.rb', line 123

def self.mark_change(type, p4, change_id, root, depot_path)
  local_file = local_path(depot_path, root)
  results = p4.run(type, '-c', change_id, local_file)
end

+ (Object) normalize_describe_files(results)

Each file entry in the base describe tagged output contains several fields, suffixed with an index value.

P4 Ruby returns the individual 'index fields' already collated by the key type.



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/helix_versioning_engine/util.rb', line 373

def self.normalize_describe_files(results)
  results.each do |r|
    # We have to collect each index we discover as we iterate over the keys
    idx_2_file = {}

    FILE_KEYS.each do |key|
      if r.key?(key)
        r[key].each_index do |idx|
          mapped_key = Util.map_describe_file_key(key)

          idx_2_file[idx] = {} unless idx_2_file.key?(idx)
          idx_2_file[idx][mapped_key] = r[key][idx]
        end
        r.delete(key)
      end
    end

    r['Files'] = idx_2_file.values
  end
  results
end

+ (Object) normalizer(spec_type, offset)

For each 'spec type' returns a function that will ensure that: - case is consistent between different return calls, prefer 'spec' types - dates are returned

Parameters: - spec_type is the 'plural' form of spec class, e.g., 'users', 'clients' - offset is the current server offset, retrieve this via 'p4 info' command

rubocop:disable Metrics/CyclomaticComplexity



255
256
257
258
259
260
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
# File 'lib/helix_versioning_engine/util.rb', line 255

def self.normalizer(spec_type, offset)
  case spec_type
  when 'branches'
    make_normalizer(BRANCHES_MAP, offset, BRANCHES_DATES)
  when 'changes'
    make_normalizer(CHANGES_MAP, offset, CHANGES_DATES)
  when 'clients'
    make_normalizer(CLIENTS_MAP, offset, CLIENTS_DATES)
  when 'depots'
    make_normalizer(DEPOTS_MAP, offset, DEPOTS_DATES)
  when 'describe'
    # This only affects 'base' fields, not fields related to each file
    base_normalize = make_normalizer(DESCRIBE_MAP, offset, DESCRIBE_DATES)
    lambda do |results|
      results = base_normalize.call(results)
      Util.normalize_describe_files(results)
      results
    end
  when 'dirs'
    make_normalizer(DIRS_MAP, offset)
  when 'files'
    make_normalizer(FILES_MAP, offset, FILES_DATES)
  when 'groups'
    date_and_case = make_normalizer(GROUPS_MAP, offset)
    lambda do |results|
      results = date_and_case.call(results)
      Util.replace_unset_with_0(results)
      results
    end
  when 'jobs'
    make_normalizer(JOBS_MAP, offset, JOBS_DATES)
  when 'labels'
    make_normalizer(LABELS_MAP, offset, LABELS_DATES)
  when 'streams'
    make_normalizer(STREAMS_MAP, offset, STREAMS_DATES)
  when 'users'
    make_normalizer(USERS_MAP, offset, USERS_DATES)
  else
    # By default, do no translation
    return ->(x) { x }
  end
end

+ (Object) open(options = {})

Creates your p4 connection using some common forms.

If you call open with a block, this will call connect before your block executes, and disconnect afterwards.

If you do not call open with a block, it is up to the caller to connect and disconnect. (It's assumed you are calling w/o a block because you want to manage when the connection actually needs to happen.)



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/helix_versioning_engine/util.rb', line 34

def self.open(options = {})
  p4 = create_p4(options)

  # Again, if we're calling using the block, we'll connect and disconnect.
  # Otherwise, just return the created p4 object.
  if block_given?
    begin
      p4.connect
      yield p4
    rescue P4Exception => ex
      puts "Util.open ex #{ex}"
      raise make_p4_error(p4)
    end
  else
    return p4
  end
ensure
  p4.disconnect if block_given? && p4 && p4.connected?
end

+ (Object) p4_date_offset(str)



428
429
430
# File 'lib/helix_versioning_engine/util.rb', line 428

def self.p4_date_offset(str)
  DateTime.parse(str).zone
end

+ (Object) p4_date_to_i(offset, p4date)

In general we get dates without any offset information, which has to be retrieved via the serverDate field from 'p4 info'



424
425
426
# File 'lib/helix_versioning_engine/util.rb', line 424

def self.p4_date_to_i(offset, p4date)
  DateTime.parse("#{p4date} #{offset}").to_time.to_i
end

+ (Boolean) p4_ticket?(str)

Returns true if the string looks like a Perforce authentication ticket.

Returns:

  • (Boolean)


91
92
93
# File 'lib/helix_versioning_engine/util.rb', line 91

def self.p4_ticket?(str)
  /^[a-zA-Z0-9]{32,}$/.match(str) != nil
end

+ (Object) raise_error(p4)

Raise an exception if necessary on a P4 Error



60
61
62
63
# File 'lib/helix_versioning_engine/util.rb', line 60

def self.raise_error(p4)
  err = p4.messages.find { |m| m.severity > 2 }
  fail Perforce::P4Error.new(err.msgid, err.severity, err.to_s)
end

+ (Boolean) relative_dir?(str)

Returns true if str is '.' or '..'

Returns:

  • (Boolean)


86
87
88
# File 'lib/helix_versioning_engine/util.rb', line 86

def self.relative_dir?(str)
  str == '.' || str == '..'
end

+ (Object) replace_unset_with_0(results)



360
361
362
363
364
# File 'lib/helix_versioning_engine/util.rb', line 360

def self.replace_unset_with_0(results)
  results.each do |r|
    r.each_key { |k| r[k] = '0' if r[k] == 'unset' }
  end
end

+ (Object) resolve_charset(env, settings)



448
449
450
451
452
453
454
# File 'lib/helix_versioning_engine/util.rb', line 448

def self.resolve_charset(env, settings)
  if env.key?('P4_CHARSET') && settings.allow_env_p4_config
    env['P4_CHARSET']
  else
    settings.p4['charset'] if settings.p4.key?('charset')
  end
end

+ (Object) resolve_host(env, settings)



432
433
434
435
436
437
438
# File 'lib/helix_versioning_engine/util.rb', line 432

def self.resolve_host(env, settings)
  if env.key?('P4_HOST') && settings.allow_env_p4_config
    env['P4_HOST']
  else
    settings.p4['host']
  end
end

+ (Object) resolve_password(env, settings)



456
457
458
459
460
461
462
463
# File 'lib/helix_versioning_engine/util.rb', line 456

def self.resolve_password(env, settings)
  password = env['AUTH_CREDENTIALS'].last
  if !Util.uuid?(password)
    password
  else
    Auth.read_token(password, settings)['ticket']
  end
end

+ (Object) resolve_port(env, settings)



440
441
442
443
444
445
446
# File 'lib/helix_versioning_engine/util.rb', line 440

def self.resolve_port(env, settings)
  if env.key?('P4_PORT') && settings.allow_env_p4_config
    env['P4_PORT']
  else
    settings.p4['port']
  end
end

+ (Object) save_content(root, depot_path, content)



109
110
111
112
113
114
115
# File 'lib/helix_versioning_engine/util.rb', line 109

def self.save_content(root, depot_path, content)
  local_file = local_path(depot_path, root)
  dir = File.dirname(local_file)
  FileUtils.mkpath(dir) unless Dir.exist?(dir)

  IO.write(local_file, content)
end

+ (Object) singular(plural)



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/helix_versioning_engine/util.rb', line 133

def self.singular(plural)
  matches = {
    branches: 'branch',
    clients: 'client',
    depots: 'depot',
    groups: 'group',
    jobs: 'job',
    labels:  'label',
    protects: 'protect',
    servers: 'server',
    streams: 'stream',
    triggers: 'trigger',
    users: 'user'
  }
  matches[plural.to_sym]
end

+ (Boolean) uuid?(str)

We only count uuids that were returned via SecureRandom.uuid used to generate internal security tokens.

Returns:

  • (Boolean)


97
98
99
100
# File 'lib/helix_versioning_engine/util.rb', line 97

def self.uuid?(str)
  /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/
    .match(str)
end

+ (Boolean) wildcard?(str)

Returns true when str contains a Perforce wildcard

Returns:

  • (Boolean)


81
82
83
# File 'lib/helix_versioning_engine/util.rb', line 81

def self.wildcard?(str)
  (str =~ /\.\.\./ || str =~ /\*/) != nil
end