require 'base64' require_relative 'util' module HelixVersioningEngine # This class assists in creating changelists based on an array of file # changes, some of which may be file uploads. # # This is the implementation for file uploads and change creation. class ChangeService # The P4 connection we're using, that should already be logged in for a # particular user. attr_reader :p4 # The name of the temporary client attr_reader :client_name # The root directory of the temporary client attr_reader :client_root def initialize(p4: nil, client_name: nil, client_root: nil) @p4 = p4 @client_name = client_name @client_root = client_root end def submit(files: [], description: 'Updating files') change_id = Util.init_changelist(p4, description) begin files = files.map { |f| f.is_a?(ChangeService::File) ? f : ChangeService::File.new(f) } collect_file_results(files) files.each { |f| f.call(p4, change_id, client_root) } p4.run_submit('-c', change_id) rescue StandardError => ex # Delete the changelist p4.at_exception_level(P4::RAISE_NONE) do p4.run('revert', '-c', change_id, '//...') p4.run_change('-d', change_id) if Util.error?(p4) puts "possible issues deleting change #{change_id}: #{p4.messages}" end end raise ex end end # Handles tracking 'file modification' operations on a single file for a # change. # # One important aspect of initializing these files is assigning a 'files' # result. The ChangeHelper pretty much grabs the output of 'p4 files' for # all file changes, which lets us know if a file exists or not in the # system, which changes downstream behavior during an upload process. class File attr_accessor :depot_file attr_accessor :action attr_accessor :content # For *some* actions, like a move, we need this indicated by the user. attr_accessor :from_depot_file # The output of p4 files on this depot_file path, if it exists. attr_accessor :file_result # If set, this should be the version number we require before running. # If you want to require a new file, this should be set to 0. Otherwise # specify the version directly. attr_accessor :require_version # Notice how the file *must* be specified using our 'external normalized' # data style. def initialize(depot_file: nil, action: nil, content: nil, from_depot_file: nil, require_version: nil) @depot_file = depot_file @action = action @content = content @from_depot_file = from_depot_file @require_version = require_version end # The "external" JSON representation uses a CamelCase style string. def self.from_json(obj) args = {} args[:depot_file] = obj['DepotFile'] if obj.key?('DepotFile') args[:action] = obj['Action'] if obj.key?('Action') args[:from_depot_file] = obj['FromDepotFile'] if obj.key?('FromDepotFile') args[:require_version] = obj['RequireVersion'] if obj.key?('RequireVersion') content_base64 = obj['Content'] if obj.key?('Content') args[:content] = Base64.decode64(content_base64) if content_base64 self.new(args) end def exists? !file_result.nil? && file_result['action'] != 'delete' end # # This really should only be called by ChangeHelper::call to do work # once we have associated any file_result and from_file properties. def call(p4, change_id, client_root) case action when 'upload' upload_file(p4, change_id, client_root) when 'branch' integrate_file(p4, change_id) when 'move' move_file(p4, change_id) end end def upload_file(p4, change_id, client_root) Util.assert_no_special_paths(@depot_file.split('/')) if !require_version.nil? if require_version == 0 && exists? msg = "assertion failed: file #{@depot_file} exists" raise P4Error.default_error(msg) end cur_version = 0 cur_version = file_result['rev'].to_i if exists? if cur_version != require_version.to_i msg = "assertion failed: file #{@depot_file} not at required " \ "version #{require_version}, " \ "current version is #{cur_version}" raise P4Error.default_error(msg) end end if exists? p4.run_sync(depot_file) Util.mark_change('edit', p4, change_id, client_root, depot_file) Util.save_content(client_root, depot_file, content) else Util.save_content(client_root, depot_file, content) Util.mark_change('add', p4, change_id, client_root, depot_file) end end def integrate_file(p4, change_id) p4.run_integrate('-c', change_id, from_depot_file, depot_file) end def move_file(p4, change_id) p4.run_move('-c', change_id, from_depot_file, depot_file) end end private # Runs p4 files on all our depot paths, then updates each file model # that exists. def collect_file_results(files) file_results = p4.run_files(depot_files(files)) file_results.each do |file_result| file = file_for_depot_file(files, file_result['depotFile']) file.file_result = file_result if file end end def depot_files(files) files.map(&:depot_file) end def file_for_depot_file(files, depot_file) files.find { |f| f.depot_file == depot_file } end end end