#!/usr/bin/env python3 # # This is really similar to "p4 reconcile", except that it # reconciles from an external directory, "source_dir", to # the Perforce repo, "perforce_dir". # # Eg., to preview the changes that would be made: # # ./same_as_p4_reconcile.py -n -c 42 source_dir perforce_dir # # To perform the actual reconciliation, drop the "-n": # # ./same_as_p4_reconcile.py -c 42 source_dir perforce_dir import os from argparse import ArgumentParser import subprocess import shutil import filecmp from typing import Optional, List def call(cmd_array: List[str], changelist: str, preview: bool) -> int: """ If not preview, makes the call.""" ret = 0 if not preview: ret = subprocess.call(add_changelist_to_cmd(cmd_array, changelist)) if ret != 0: print("ERROR: Couldn't %s, ret = %d" % (str(cmd_array), ret)) else: print('#', ' '.join(cmd_array)) return ret def add_changelist_to_cmd(cmd: List[str], changelist: Optional[str]) -> List[str]: """Injects "-c changelist" to the p4 command.""" if changelist is None: return cmd cmd.insert(2, '-c') cmd.insert(3, changelist) return cmd def do_diff_by_name(preview: bool, changelist: str, source_dir: str, p4_dir: str) -> None: do_diff(preview, changelist, filecmp.dircmp(p4_dir, source_dir)) delete_empty_directories(p4_dir) def delete_empty_directories(top: str) -> None: """ p4 deletes could leave us with empty hierarchies. """ for root, dirs, files in os.walk(top, topdown=False): for name in dirs: try: os.rmdir(os.path.join(root,name)) except OSError as ex: pass def do_diff(preview: bool, changelist:str, d: filecmp.dircmp) -> None: for f in d.right_only: if os.path.isfile(os.path.join(d.right, f)): if not preview: shutil.copy(os.path.join(d.right, f), os.path.join(d.left, f)) call(['p4', 'add', os.path.join(d.left, f)], changelist, preview) else: # Can't add directory in one step. Do so by walking dir. if not preview: shutil.copytree(os.path.join(d.right, f), os.path.join(d.left, f)) for root, dirs, files in os.walk(os.path.join(d.left, f)): for f in files: call(['p4', 'add', os.path.join(root, f)], changelist, preview) else: print('# Adding hierarchy', os.path.join(d.right, f)) for f in d.left_only: if os.path.isfile(os.path.join(d.left, f)): call(['p4', 'delete', os.path.join(d.left, f)], changelist, preview) else: for root, dirs, files in os.walk(os.path.join(d.left, f)): for f in files: call(['p4', 'delete', os.path.join(root, f)], changelist, preview) for f in d.diff_files: if not call(['p4', 'edit', os.path.join(d.left, f)], changelist, preview): # call() returning 0 is a successful call if not preview: shutil.copy(os.path.join(d.right, f), os.path.join(d.left, f)) for sub in d.subdirs.values(): do_diff(preview, changelist, sub) if __name__ == '__main__': parser = ArgumentParser(description='Similar to "p4 reconcile"') parser.add_argument('-c', '--changelist', help='Changelist to use') parser.add_argument('-n', '--preview', action='store_true', help="Preview only, don't change any files") parser.add_argument('source_dir', help='The newer directory outside perforce') parser.add_argument('p4_dir', help='The perforce directory to be modified') args = parser.parse_args() do_diff_by_name(args.preview, args.changelist, args.source_dir, args.p4_dir)