1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25import zipfile 26import zlib 27import ctypes 28 29try: 30from subprocess import CalledProcessError 31exceptImportError: 32# from python2.7:subprocess.py 33# Exception classes used by this module. 34classCalledProcessError(Exception): 35"""This exception is raised when a process run by check_call() returns 36 a non-zero exit status. The exit status will be stored in the 37 returncode attribute.""" 38def__init__(self, returncode, cmd): 39 self.returncode = returncode 40 self.cmd = cmd 41def__str__(self): 42return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 43 44verbose =False 45 46# Only labels/tags matching this will be imported/exported 47defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 48 49# Grab changes in blocks of this many revisions, unless otherwise requested 50defaultBlockSize =512 51 52defp4_build_cmd(cmd): 53"""Build a suitable p4 command line. 54 55 This consolidates building and returning a p4 command line into one 56 location. It means that hooking into the environment, or other configuration 57 can be done more easily. 58 """ 59 real_cmd = ["p4"] 60 61 user =gitConfig("git-p4.user") 62iflen(user) >0: 63 real_cmd += ["-u",user] 64 65 password =gitConfig("git-p4.password") 66iflen(password) >0: 67 real_cmd += ["-P", password] 68 69 port =gitConfig("git-p4.port") 70iflen(port) >0: 71 real_cmd += ["-p", port] 72 73 host =gitConfig("git-p4.host") 74iflen(host) >0: 75 real_cmd += ["-H", host] 76 77 client =gitConfig("git-p4.client") 78iflen(client) >0: 79 real_cmd += ["-c", client] 80 81 retries =gitConfigInt("git-p4.retries") 82if retries is None: 83# Perform 3 retries by default 84 retries =3 85 real_cmd += ["-r",str(retries)] 86 87ifisinstance(cmd,basestring): 88 real_cmd =' '.join(real_cmd) +' '+ cmd 89else: 90 real_cmd += cmd 91return real_cmd 92 93defchdir(path, is_client_path=False): 94"""Do chdir to the given path, and set the PWD environment 95 variable for use by P4. It does not look at getcwd() output. 96 Since we're not using the shell, it is necessary to set the 97 PWD environment variable explicitly. 98 99 Normally, expand the path to force it to be absolute. This 100 addresses the use of relative path names inside P4 settings, 101 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 102 as given; it looks for .p4config using PWD. 103 104 If is_client_path, the path was handed to us directly by p4, 105 and may be a symbolic link. Do not call os.getcwd() in this 106 case, because it will cause p4 to think that PWD is not inside 107 the client path. 108 """ 109 110 os.chdir(path) 111if not is_client_path: 112 path = os.getcwd() 113 os.environ['PWD'] = path 114 115defcalcDiskFree(): 116"""Return free space in bytes on the disk of the given dirname.""" 117if platform.system() =='Windows': 118 free_bytes = ctypes.c_ulonglong(0) 119 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 120return free_bytes.value 121else: 122 st = os.statvfs(os.getcwd()) 123return st.f_bavail * st.f_frsize 124 125defdie(msg): 126if verbose: 127raiseException(msg) 128else: 129 sys.stderr.write(msg +"\n") 130 sys.exit(1) 131 132defwrite_pipe(c, stdin): 133if verbose: 134 sys.stderr.write('Writing pipe:%s\n'%str(c)) 135 136 expand =isinstance(c,basestring) 137 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 138 pipe = p.stdin 139 val = pipe.write(stdin) 140 pipe.close() 141if p.wait(): 142die('Command failed:%s'%str(c)) 143 144return val 145 146defp4_write_pipe(c, stdin): 147 real_cmd =p4_build_cmd(c) 148returnwrite_pipe(real_cmd, stdin) 149 150defread_pipe(c, ignore_error=False): 151if verbose: 152 sys.stderr.write('Reading pipe:%s\n'%str(c)) 153 154 expand =isinstance(c,basestring) 155 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 156(out, err) = p.communicate() 157if p.returncode !=0and not ignore_error: 158die('Command failed:%s\nError:%s'% (str(c), err)) 159return out 160 161defp4_read_pipe(c, ignore_error=False): 162 real_cmd =p4_build_cmd(c) 163returnread_pipe(real_cmd, ignore_error) 164 165defread_pipe_lines(c): 166if verbose: 167 sys.stderr.write('Reading pipe:%s\n'%str(c)) 168 169 expand =isinstance(c, basestring) 170 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 171 pipe = p.stdout 172 val = pipe.readlines() 173if pipe.close()or p.wait(): 174die('Command failed:%s'%str(c)) 175 176return val 177 178defp4_read_pipe_lines(c): 179"""Specifically invoke p4 on the command supplied. """ 180 real_cmd =p4_build_cmd(c) 181returnread_pipe_lines(real_cmd) 182 183defp4_has_command(cmd): 184"""Ask p4 for help on this command. If it returns an error, the 185 command does not exist in this version of p4.""" 186 real_cmd =p4_build_cmd(["help", cmd]) 187 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 188 stderr=subprocess.PIPE) 189 p.communicate() 190return p.returncode ==0 191 192defp4_has_move_command(): 193"""See if the move command exists, that it supports -k, and that 194 it has not been administratively disabled. The arguments 195 must be correct, but the filenames do not have to exist. Use 196 ones with wildcards so even if they exist, it will fail.""" 197 198if notp4_has_command("move"): 199return False 200 cmd =p4_build_cmd(["move","-k","@from","@to"]) 201 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 202(out, err) = p.communicate() 203# return code will be 1 in either case 204if err.find("Invalid option") >=0: 205return False 206if err.find("disabled") >=0: 207return False 208# assume it failed because @... was invalid changelist 209return True 210 211defsystem(cmd, ignore_error=False): 212 expand =isinstance(cmd,basestring) 213if verbose: 214 sys.stderr.write("executing%s\n"%str(cmd)) 215 retcode = subprocess.call(cmd, shell=expand) 216if retcode and not ignore_error: 217raiseCalledProcessError(retcode, cmd) 218 219return retcode 220 221defp4_system(cmd): 222"""Specifically invoke p4 as the system command. """ 223 real_cmd =p4_build_cmd(cmd) 224 expand =isinstance(real_cmd, basestring) 225 retcode = subprocess.call(real_cmd, shell=expand) 226if retcode: 227raiseCalledProcessError(retcode, real_cmd) 228 229_p4_version_string =None 230defp4_version_string(): 231"""Read the version string, showing just the last line, which 232 hopefully is the interesting version bit. 233 234 $ p4 -V 235 Perforce - The Fast Software Configuration Management System. 236 Copyright 1995-2011 Perforce Software. All rights reserved. 237 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 238 """ 239global _p4_version_string 240if not _p4_version_string: 241 a =p4_read_pipe_lines(["-V"]) 242 _p4_version_string = a[-1].rstrip() 243return _p4_version_string 244 245defp4_integrate(src, dest): 246p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 247 248defp4_sync(f, *options): 249p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 250 251defp4_add(f): 252# forcibly add file names with wildcards 253ifwildcard_present(f): 254p4_system(["add","-f", f]) 255else: 256p4_system(["add", f]) 257 258defp4_delete(f): 259p4_system(["delete",wildcard_encode(f)]) 260 261defp4_edit(f, *options): 262p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 263 264defp4_revert(f): 265p4_system(["revert",wildcard_encode(f)]) 266 267defp4_reopen(type, f): 268p4_system(["reopen","-t",type,wildcard_encode(f)]) 269 270defp4_reopen_in_change(changelist, files): 271 cmd = ["reopen","-c",str(changelist)] + files 272p4_system(cmd) 273 274defp4_move(src, dest): 275p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 276 277defp4_last_change(): 278 results =p4CmdList(["changes","-m","1"]) 279returnint(results[0]['change']) 280 281defp4_describe(change): 282"""Make sure it returns a valid result by checking for 283 the presence of field "time". Return a dict of the 284 results.""" 285 286 ds =p4CmdList(["describe","-s",str(change)]) 287iflen(ds) !=1: 288die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 289 290 d = ds[0] 291 292if"p4ExitCode"in d: 293die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 294str(d))) 295if"code"in d: 296if d["code"] =="error": 297die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 298 299if"time"not in d: 300die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 301 302return d 303 304# 305# Canonicalize the p4 type and return a tuple of the 306# base type, plus any modifiers. See "p4 help filetypes" 307# for a list and explanation. 308# 309defsplit_p4_type(p4type): 310 311 p4_filetypes_historical = { 312"ctempobj":"binary+Sw", 313"ctext":"text+C", 314"cxtext":"text+Cx", 315"ktext":"text+k", 316"kxtext":"text+kx", 317"ltext":"text+F", 318"tempobj":"binary+FSw", 319"ubinary":"binary+F", 320"uresource":"resource+F", 321"uxbinary":"binary+Fx", 322"xbinary":"binary+x", 323"xltext":"text+Fx", 324"xtempobj":"binary+Swx", 325"xtext":"text+x", 326"xunicode":"unicode+x", 327"xutf16":"utf16+x", 328} 329if p4type in p4_filetypes_historical: 330 p4type = p4_filetypes_historical[p4type] 331 mods ="" 332 s = p4type.split("+") 333 base = s[0] 334 mods ="" 335iflen(s) >1: 336 mods = s[1] 337return(base, mods) 338 339# 340# return the raw p4 type of a file (text, text+ko, etc) 341# 342defp4_type(f): 343 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 344return results[0]['headType'] 345 346# 347# Given a type base and modifier, return a regexp matching 348# the keywords that can be expanded in the file 349# 350defp4_keywords_regexp_for_type(base, type_mods): 351if base in("text","unicode","binary"): 352 kwords =None 353if"ko"in type_mods: 354 kwords ='Id|Header' 355elif"k"in type_mods: 356 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 357else: 358return None 359 pattern = r""" 360 \$ # Starts with a dollar, followed by... 361 (%s) # one of the keywords, followed by... 362 (:[^$\n]+)? # possibly an old expansion, followed by... 363 \$ # another dollar 364 """% kwords 365return pattern 366else: 367return None 368 369# 370# Given a file, return a regexp matching the possible 371# RCS keywords that will be expanded, or None for files 372# with kw expansion turned off. 373# 374defp4_keywords_regexp_for_file(file): 375if not os.path.exists(file): 376return None 377else: 378(type_base, type_mods) =split_p4_type(p4_type(file)) 379returnp4_keywords_regexp_for_type(type_base, type_mods) 380 381defsetP4ExecBit(file, mode): 382# Reopens an already open file and changes the execute bit to match 383# the execute bit setting in the passed in mode. 384 385 p4Type ="+x" 386 387if notisModeExec(mode): 388 p4Type =getP4OpenedType(file) 389 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 390 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 391if p4Type[-1] =="+": 392 p4Type = p4Type[0:-1] 393 394p4_reopen(p4Type,file) 395 396defgetP4OpenedType(file): 397# Returns the perforce file type for the given file. 398 399 result =p4_read_pipe(["opened",wildcard_encode(file)]) 400 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 401if match: 402return match.group(1) 403else: 404die("Could not determine file type for%s(result: '%s')"% (file, result)) 405 406# Return the set of all p4 labels 407defgetP4Labels(depotPaths): 408 labels =set() 409ifisinstance(depotPaths,basestring): 410 depotPaths = [depotPaths] 411 412for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 413 label = l['label'] 414 labels.add(label) 415 416return labels 417 418# Return the set of all git tags 419defgetGitTags(): 420 gitTags =set() 421for line inread_pipe_lines(["git","tag"]): 422 tag = line.strip() 423 gitTags.add(tag) 424return gitTags 425 426defdiffTreePattern(): 427# This is a simple generator for the diff tree regex pattern. This could be 428# a class variable if this and parseDiffTreeEntry were a part of a class. 429 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 430while True: 431yield pattern 432 433defparseDiffTreeEntry(entry): 434"""Parses a single diff tree entry into its component elements. 435 436 See git-diff-tree(1) manpage for details about the format of the diff 437 output. This method returns a dictionary with the following elements: 438 439 src_mode - The mode of the source file 440 dst_mode - The mode of the destination file 441 src_sha1 - The sha1 for the source file 442 dst_sha1 - The sha1 fr the destination file 443 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 444 status_score - The score for the status (applicable for 'C' and 'R' 445 statuses). This is None if there is no score. 446 src - The path for the source file. 447 dst - The path for the destination file. This is only present for 448 copy or renames. If it is not present, this is None. 449 450 If the pattern is not matched, None is returned.""" 451 452 match =diffTreePattern().next().match(entry) 453if match: 454return{ 455'src_mode': match.group(1), 456'dst_mode': match.group(2), 457'src_sha1': match.group(3), 458'dst_sha1': match.group(4), 459'status': match.group(5), 460'status_score': match.group(6), 461'src': match.group(7), 462'dst': match.group(10) 463} 464return None 465 466defisModeExec(mode): 467# Returns True if the given git mode represents an executable file, 468# otherwise False. 469return mode[-3:] =="755" 470 471defisModeExecChanged(src_mode, dst_mode): 472returnisModeExec(src_mode) !=isModeExec(dst_mode) 473 474defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 475 476ifisinstance(cmd,basestring): 477 cmd ="-G "+ cmd 478 expand =True 479else: 480 cmd = ["-G"] + cmd 481 expand =False 482 483 cmd =p4_build_cmd(cmd) 484if verbose: 485 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 486 487# Use a temporary file to avoid deadlocks without 488# subprocess.communicate(), which would put another copy 489# of stdout into memory. 490 stdin_file =None 491if stdin is not None: 492 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 493ifisinstance(stdin,basestring): 494 stdin_file.write(stdin) 495else: 496for i in stdin: 497 stdin_file.write(i +'\n') 498 stdin_file.flush() 499 stdin_file.seek(0) 500 501 p4 = subprocess.Popen(cmd, 502 shell=expand, 503 stdin=stdin_file, 504 stdout=subprocess.PIPE) 505 506 result = [] 507try: 508while True: 509 entry = marshal.load(p4.stdout) 510if cb is not None: 511cb(entry) 512else: 513 result.append(entry) 514exceptEOFError: 515pass 516 exitCode = p4.wait() 517if exitCode !=0: 518 entry = {} 519 entry["p4ExitCode"] = exitCode 520 result.append(entry) 521 522return result 523 524defp4Cmd(cmd): 525list=p4CmdList(cmd) 526 result = {} 527for entry inlist: 528 result.update(entry) 529return result; 530 531defp4Where(depotPath): 532if not depotPath.endswith("/"): 533 depotPath +="/" 534 depotPathLong = depotPath +"..." 535 outputList =p4CmdList(["where", depotPathLong]) 536 output =None 537for entry in outputList: 538if"depotFile"in entry: 539# Search for the base client side depot path, as long as it starts with the branch's P4 path. 540# The base path always ends with "/...". 541if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 542 output = entry 543break 544elif"data"in entry: 545 data = entry.get("data") 546 space = data.find(" ") 547if data[:space] == depotPath: 548 output = entry 549break 550if output ==None: 551return"" 552if output["code"] =="error": 553return"" 554 clientPath ="" 555if"path"in output: 556 clientPath = output.get("path") 557elif"data"in output: 558 data = output.get("data") 559 lastSpace = data.rfind(" ") 560 clientPath = data[lastSpace +1:] 561 562if clientPath.endswith("..."): 563 clientPath = clientPath[:-3] 564return clientPath 565 566defcurrentGitBranch(): 567 retcode =system(["git","symbolic-ref","-q","HEAD"], ignore_error=True) 568if retcode !=0: 569# on a detached head 570return None 571else: 572returnread_pipe(["git","name-rev","HEAD"]).split(" ")[1].strip() 573 574defisValidGitDir(path): 575if(os.path.exists(path +"/HEAD") 576and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 577return True; 578return False 579 580defparseRevision(ref): 581returnread_pipe("git rev-parse%s"% ref).strip() 582 583defbranchExists(ref): 584 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 585 ignore_error=True) 586returnlen(rev) >0 587 588defextractLogMessageFromGitCommit(commit): 589 logMessage ="" 590 591## fixme: title is first line of commit, not 1st paragraph. 592 foundTitle =False 593for log inread_pipe_lines("git cat-file commit%s"% commit): 594if not foundTitle: 595iflen(log) ==1: 596 foundTitle =True 597continue 598 599 logMessage += log 600return logMessage 601 602defextractSettingsGitLog(log): 603 values = {} 604for line in log.split("\n"): 605 line = line.strip() 606 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 607if not m: 608continue 609 610 assignments = m.group(1).split(':') 611for a in assignments: 612 vals = a.split('=') 613 key = vals[0].strip() 614 val = ('='.join(vals[1:])).strip() 615if val.endswith('\"')and val.startswith('"'): 616 val = val[1:-1] 617 618 values[key] = val 619 620 paths = values.get("depot-paths") 621if not paths: 622 paths = values.get("depot-path") 623if paths: 624 values['depot-paths'] = paths.split(',') 625return values 626 627defgitBranchExists(branch): 628 proc = subprocess.Popen(["git","rev-parse", branch], 629 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 630return proc.wait() ==0; 631 632_gitConfig = {} 633 634defgitConfig(key, typeSpecifier=None): 635if not _gitConfig.has_key(key): 636 cmd = ["git","config"] 637if typeSpecifier: 638 cmd += [ typeSpecifier ] 639 cmd += [ key ] 640 s =read_pipe(cmd, ignore_error=True) 641 _gitConfig[key] = s.strip() 642return _gitConfig[key] 643 644defgitConfigBool(key): 645"""Return a bool, using git config --bool. It is True only if the 646 variable is set to true, and False if set to false or not present 647 in the config.""" 648 649if not _gitConfig.has_key(key): 650 _gitConfig[key] =gitConfig(key,'--bool') =="true" 651return _gitConfig[key] 652 653defgitConfigInt(key): 654if not _gitConfig.has_key(key): 655 cmd = ["git","config","--int", key ] 656 s =read_pipe(cmd, ignore_error=True) 657 v = s.strip() 658try: 659 _gitConfig[key] =int(gitConfig(key,'--int')) 660exceptValueError: 661 _gitConfig[key] =None 662return _gitConfig[key] 663 664defgitConfigList(key): 665if not _gitConfig.has_key(key): 666 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 667 _gitConfig[key] = s.strip().split(os.linesep) 668if _gitConfig[key] == ['']: 669 _gitConfig[key] = [] 670return _gitConfig[key] 671 672defp4BranchesInGit(branchesAreInRemotes=True): 673"""Find all the branches whose names start with "p4/", looking 674 in remotes or heads as specified by the argument. Return 675 a dictionary of{ branch: revision }for each one found. 676 The branch names are the short names, without any 677 "p4/" prefix.""" 678 679 branches = {} 680 681 cmdline ="git rev-parse --symbolic " 682if branchesAreInRemotes: 683 cmdline +="--remotes" 684else: 685 cmdline +="--branches" 686 687for line inread_pipe_lines(cmdline): 688 line = line.strip() 689 690# only import to p4/ 691if not line.startswith('p4/'): 692continue 693# special symbolic ref to p4/master 694if line =="p4/HEAD": 695continue 696 697# strip off p4/ prefix 698 branch = line[len("p4/"):] 699 700 branches[branch] =parseRevision(line) 701 702return branches 703 704defbranch_exists(branch): 705"""Make sure that the given ref name really exists.""" 706 707 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 708 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 709 out, _ = p.communicate() 710if p.returncode: 711return False 712# expect exactly one line of output: the branch name 713return out.rstrip() == branch 714 715deffindUpstreamBranchPoint(head ="HEAD"): 716 branches =p4BranchesInGit() 717# map from depot-path to branch name 718 branchByDepotPath = {} 719for branch in branches.keys(): 720 tip = branches[branch] 721 log =extractLogMessageFromGitCommit(tip) 722 settings =extractSettingsGitLog(log) 723if settings.has_key("depot-paths"): 724 paths =",".join(settings["depot-paths"]) 725 branchByDepotPath[paths] ="remotes/p4/"+ branch 726 727 settings =None 728 parent =0 729while parent <65535: 730 commit = head +"~%s"% parent 731 log =extractLogMessageFromGitCommit(commit) 732 settings =extractSettingsGitLog(log) 733if settings.has_key("depot-paths"): 734 paths =",".join(settings["depot-paths"]) 735if branchByDepotPath.has_key(paths): 736return[branchByDepotPath[paths], settings] 737 738 parent = parent +1 739 740return["", settings] 741 742defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 743if not silent: 744print("Creating/updating branch(es) in%sbased on origin branch(es)" 745% localRefPrefix) 746 747 originPrefix ="origin/p4/" 748 749for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 750 line = line.strip() 751if(not line.startswith(originPrefix))or line.endswith("HEAD"): 752continue 753 754 headName = line[len(originPrefix):] 755 remoteHead = localRefPrefix + headName 756 originHead = line 757 758 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 759if(not original.has_key('depot-paths') 760or not original.has_key('change')): 761continue 762 763 update =False 764if notgitBranchExists(remoteHead): 765if verbose: 766print"creating%s"% remoteHead 767 update =True 768else: 769 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 770if settings.has_key('change') >0: 771if settings['depot-paths'] == original['depot-paths']: 772 originP4Change =int(original['change']) 773 p4Change =int(settings['change']) 774if originP4Change > p4Change: 775print("%s(%s) is newer than%s(%s). " 776"Updating p4 branch from origin." 777% (originHead, originP4Change, 778 remoteHead, p4Change)) 779 update =True 780else: 781print("Ignoring:%swas imported from%swhile " 782"%swas imported from%s" 783% (originHead,','.join(original['depot-paths']), 784 remoteHead,','.join(settings['depot-paths']))) 785 786if update: 787system("git update-ref%s %s"% (remoteHead, originHead)) 788 789deforiginP4BranchesExist(): 790returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 791 792 793defp4ParseNumericChangeRange(parts): 794 changeStart =int(parts[0][1:]) 795if parts[1] =='#head': 796 changeEnd =p4_last_change() 797else: 798 changeEnd =int(parts[1]) 799 800return(changeStart, changeEnd) 801 802defchooseBlockSize(blockSize): 803if blockSize: 804return blockSize 805else: 806return defaultBlockSize 807 808defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 809assert depotPaths 810 811# Parse the change range into start and end. Try to find integer 812# revision ranges as these can be broken up into blocks to avoid 813# hitting server-side limits (maxrows, maxscanresults). But if 814# that doesn't work, fall back to using the raw revision specifier 815# strings, without using block mode. 816 817if changeRange is None or changeRange =='': 818 changeStart =1 819 changeEnd =p4_last_change() 820 block_size =chooseBlockSize(requestedBlockSize) 821else: 822 parts = changeRange.split(',') 823assertlen(parts) ==2 824try: 825(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 826 block_size =chooseBlockSize(requestedBlockSize) 827except: 828 changeStart = parts[0][1:] 829 changeEnd = parts[1] 830if requestedBlockSize: 831die("cannot use --changes-block-size with non-numeric revisions") 832 block_size =None 833 834 changes = [] 835 836# Retrieve changes a block at a time, to prevent running 837# into a MaxResults/MaxScanRows error from the server. 838 839while True: 840 cmd = ['changes'] 841 842if block_size: 843 end =min(changeEnd, changeStart + block_size) 844 revisionRange ="%d,%d"% (changeStart, end) 845else: 846 revisionRange ="%s,%s"% (changeStart, changeEnd) 847 848for p in depotPaths: 849 cmd += ["%s...@%s"% (p, revisionRange)] 850 851# Insert changes in chronological order 852for line inreversed(p4_read_pipe_lines(cmd)): 853 changes.append(int(line.split(" ")[1])) 854 855if not block_size: 856break 857 858if end >= changeEnd: 859break 860 861 changeStart = end +1 862 863 changes =sorted(changes) 864return changes 865 866defp4PathStartsWith(path, prefix): 867# This method tries to remedy a potential mixed-case issue: 868# 869# If UserA adds //depot/DirA/file1 870# and UserB adds //depot/dira/file2 871# 872# we may or may not have a problem. If you have core.ignorecase=true, 873# we treat DirA and dira as the same directory 874ifgitConfigBool("core.ignorecase"): 875return path.lower().startswith(prefix.lower()) 876return path.startswith(prefix) 877 878defgetClientSpec(): 879"""Look at the p4 client spec, create a View() object that contains 880 all the mappings, and return it.""" 881 882 specList =p4CmdList("client -o") 883iflen(specList) !=1: 884die('Output from "client -o" is%dlines, expecting 1'% 885len(specList)) 886 887# dictionary of all client parameters 888 entry = specList[0] 889 890# the //client/ name 891 client_name = entry["Client"] 892 893# just the keys that start with "View" 894 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 895 896# hold this new View 897 view =View(client_name) 898 899# append the lines, in order, to the view 900for view_num inrange(len(view_keys)): 901 k ="View%d"% view_num 902if k not in view_keys: 903die("Expected view key%smissing"% k) 904 view.append(entry[k]) 905 906return view 907 908defgetClientRoot(): 909"""Grab the client directory.""" 910 911 output =p4CmdList("client -o") 912iflen(output) !=1: 913die('Output from "client -o" is%dlines, expecting 1'%len(output)) 914 915 entry = output[0] 916if"Root"not in entry: 917die('Client has no "Root"') 918 919return entry["Root"] 920 921# 922# P4 wildcards are not allowed in filenames. P4 complains 923# if you simply add them, but you can force it with "-f", in 924# which case it translates them into %xx encoding internally. 925# 926defwildcard_decode(path): 927# Search for and fix just these four characters. Do % last so 928# that fixing it does not inadvertently create new %-escapes. 929# Cannot have * in a filename in windows; untested as to 930# what p4 would do in such a case. 931if not platform.system() =="Windows": 932 path = path.replace("%2A","*") 933 path = path.replace("%23","#") \ 934.replace("%40","@") \ 935.replace("%25","%") 936return path 937 938defwildcard_encode(path): 939# do % first to avoid double-encoding the %s introduced here 940 path = path.replace("%","%25") \ 941.replace("*","%2A") \ 942.replace("#","%23") \ 943.replace("@","%40") 944return path 945 946defwildcard_present(path): 947 m = re.search("[*#@%]", path) 948return m is not None 949 950classLargeFileSystem(object): 951"""Base class for large file system support.""" 952 953def__init__(self, writeToGitStream): 954 self.largeFiles =set() 955 self.writeToGitStream = writeToGitStream 956 957defgeneratePointer(self, cloneDestination, contentFile): 958"""Return the content of a pointer file that is stored in Git instead of 959 the actual content.""" 960assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 961 962defpushFile(self, localLargeFile): 963"""Push the actual content which is not stored in the Git repository to 964 a server.""" 965assert False,"Method 'pushFile' required in "+ self.__class__.__name__ 966 967defhasLargeFileExtension(self, relPath): 968returnreduce( 969lambda a, b: a or b, 970[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')], 971False 972) 973 974defgenerateTempFile(self, contents): 975 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 976for d in contents: 977 contentFile.write(d) 978 contentFile.close() 979return contentFile.name 980 981defexceedsLargeFileThreshold(self, relPath, contents): 982ifgitConfigInt('git-p4.largeFileThreshold'): 983 contentsSize =sum(len(d)for d in contents) 984if contentsSize >gitConfigInt('git-p4.largeFileThreshold'): 985return True 986ifgitConfigInt('git-p4.largeFileCompressedThreshold'): 987 contentsSize =sum(len(d)for d in contents) 988if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'): 989return False 990 contentTempFile = self.generateTempFile(contents) 991 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 992 zf = zipfile.ZipFile(compressedContentFile.name, mode='w') 993 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED) 994 zf.close() 995 compressedContentsSize = zf.infolist()[0].compress_size 996 os.remove(contentTempFile) 997 os.remove(compressedContentFile.name) 998if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'): 999return True1000return False10011002defaddLargeFile(self, relPath):1003 self.largeFiles.add(relPath)10041005defremoveLargeFile(self, relPath):1006 self.largeFiles.remove(relPath)10071008defisLargeFile(self, relPath):1009return relPath in self.largeFiles10101011defprocessContent(self, git_mode, relPath, contents):1012"""Processes the content of git fast import. This method decides if a1013 file is stored in the large file system and handles all necessary1014 steps."""1015if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1016 contentTempFile = self.generateTempFile(contents)1017(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1018if pointer_git_mode:1019 git_mode = pointer_git_mode1020if localLargeFile:1021# Move temp file to final location in large file system1022 largeFileDir = os.path.dirname(localLargeFile)1023if not os.path.isdir(largeFileDir):1024 os.makedirs(largeFileDir)1025 shutil.move(contentTempFile, localLargeFile)1026 self.addLargeFile(relPath)1027ifgitConfigBool('git-p4.largeFilePush'):1028 self.pushFile(localLargeFile)1029if verbose:1030 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1031return(git_mode, contents)10321033classMockLFS(LargeFileSystem):1034"""Mock large file system for testing."""10351036defgeneratePointer(self, contentFile):1037"""The pointer content is the original content prefixed with "pointer-".1038 The local filename of the large file storage is derived from the file content.1039 """1040withopen(contentFile,'r')as f:1041 content =next(f)1042 gitMode ='100644'1043 pointerContents ='pointer-'+ content1044 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1045return(gitMode, pointerContents, localLargeFile)10461047defpushFile(self, localLargeFile):1048"""The remote filename of the large file storage is the same as the local1049 one but in a different directory.1050 """1051 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1052if not os.path.exists(remotePath):1053 os.makedirs(remotePath)1054 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10551056classGitLFS(LargeFileSystem):1057"""Git LFS as backend for the git-p4 large file system.1058 See https://git-lfs.github.com/ for details."""10591060def__init__(self, *args):1061 LargeFileSystem.__init__(self, *args)1062 self.baseGitAttributes = []10631064defgeneratePointer(self, contentFile):1065"""Generate a Git LFS pointer for the content. Return LFS Pointer file1066 mode and content which is stored in the Git repository instead of1067 the actual content. Return also the new location of the actual1068 content.1069 """1070if os.path.getsize(contentFile) ==0:1071return(None,'',None)10721073 pointerProcess = subprocess.Popen(1074['git','lfs','pointer','--file='+ contentFile],1075 stdout=subprocess.PIPE1076)1077 pointerFile = pointerProcess.stdout.read()1078if pointerProcess.wait():1079 os.remove(contentFile)1080die('git-lfs pointer command failed. Did you install the extension?')10811082# Git LFS removed the preamble in the output of the 'pointer' command1083# starting from version 1.2.0. Check for the preamble here to support1084# earlier versions.1085# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431086if pointerFile.startswith('Git LFS pointer for'):1087 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)10881089 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1090 localLargeFile = os.path.join(1091 os.getcwd(),1092'.git','lfs','objects', oid[:2], oid[2:4],1093 oid,1094)1095# LFS Spec states that pointer files should not have the executable bit set.1096 gitMode ='100644'1097return(gitMode, pointerFile, localLargeFile)10981099defpushFile(self, localLargeFile):1100 uploadProcess = subprocess.Popen(1101['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1102)1103if uploadProcess.wait():1104die('git-lfs push command failed. Did you define a remote?')11051106defgenerateGitAttributes(self):1107return(1108 self.baseGitAttributes +1109[1110'\n',1111'#\n',1112'# Git LFS (see https://git-lfs.github.com/)\n',1113'#\n',1114] +1115['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1116for f insorted(gitConfigList('git-p4.largeFileExtensions'))1117] +1118['/'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1119for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1120]1121)11221123defaddLargeFile(self, relPath):1124 LargeFileSystem.addLargeFile(self, relPath)1125 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11261127defremoveLargeFile(self, relPath):1128 LargeFileSystem.removeLargeFile(self, relPath)1129 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11301131defprocessContent(self, git_mode, relPath, contents):1132if relPath =='.gitattributes':1133 self.baseGitAttributes = contents1134return(git_mode, self.generateGitAttributes())1135else:1136return LargeFileSystem.processContent(self, git_mode, relPath, contents)11371138class Command:1139def__init__(self):1140 self.usage ="usage: %prog [options]"1141 self.needsGit =True1142 self.verbose =False11431144class P4UserMap:1145def__init__(self):1146 self.userMapFromPerforceServer =False1147 self.myP4UserId =None11481149defp4UserId(self):1150if self.myP4UserId:1151return self.myP4UserId11521153 results =p4CmdList("user -o")1154for r in results:1155if r.has_key('User'):1156 self.myP4UserId = r['User']1157return r['User']1158die("Could not find your p4 user id")11591160defp4UserIsMe(self, p4User):1161# return True if the given p4 user is actually me1162 me = self.p4UserId()1163if not p4User or p4User != me:1164return False1165else:1166return True11671168defgetUserCacheFilename(self):1169 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1170return home +"/.gitp4-usercache.txt"11711172defgetUserMapFromPerforceServer(self):1173if self.userMapFromPerforceServer:1174return1175 self.users = {}1176 self.emails = {}11771178for output inp4CmdList("users"):1179if not output.has_key("User"):1180continue1181 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1182 self.emails[output["Email"]] = output["User"]11831184 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1185for mapUserConfig ingitConfigList("git-p4.mapUser"):1186 mapUser = mapUserConfigRegex.findall(mapUserConfig)1187if mapUser andlen(mapUser[0]) ==3:1188 user = mapUser[0][0]1189 fullname = mapUser[0][1]1190 email = mapUser[0][2]1191 self.users[user] = fullname +" <"+ email +">"1192 self.emails[email] = user11931194 s =''1195for(key, val)in self.users.items():1196 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))11971198open(self.getUserCacheFilename(),"wb").write(s)1199 self.userMapFromPerforceServer =True12001201defloadUserMapFromCache(self):1202 self.users = {}1203 self.userMapFromPerforceServer =False1204try:1205 cache =open(self.getUserCacheFilename(),"rb")1206 lines = cache.readlines()1207 cache.close()1208for line in lines:1209 entry = line.strip().split("\t")1210 self.users[entry[0]] = entry[1]1211exceptIOError:1212 self.getUserMapFromPerforceServer()12131214classP4Debug(Command):1215def__init__(self):1216 Command.__init__(self)1217 self.options = []1218 self.description ="A tool to debug the output of p4 -G."1219 self.needsGit =False12201221defrun(self, args):1222 j =01223for output inp4CmdList(args):1224print'Element:%d'% j1225 j +=11226print output1227return True12281229classP4RollBack(Command):1230def__init__(self):1231 Command.__init__(self)1232 self.options = [1233 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1234]1235 self.description ="A tool to debug the multi-branch import. Don't use :)"1236 self.rollbackLocalBranches =False12371238defrun(self, args):1239iflen(args) !=1:1240return False1241 maxChange =int(args[0])12421243if"p4ExitCode"inp4Cmd("changes -m 1"):1244die("Problems executing p4");12451246if self.rollbackLocalBranches:1247 refPrefix ="refs/heads/"1248 lines =read_pipe_lines("git rev-parse --symbolic --branches")1249else:1250 refPrefix ="refs/remotes/"1251 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12521253for line in lines:1254if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1255 line = line.strip()1256 ref = refPrefix + line1257 log =extractLogMessageFromGitCommit(ref)1258 settings =extractSettingsGitLog(log)12591260 depotPaths = settings['depot-paths']1261 change = settings['change']12621263 changed =False12641265iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1266for p in depotPaths]))) ==0:1267print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1268system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1269continue12701271while change andint(change) > maxChange:1272 changed =True1273if self.verbose:1274print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1275system("git update-ref%s\"%s^\""% (ref, ref))1276 log =extractLogMessageFromGitCommit(ref)1277 settings =extractSettingsGitLog(log)127812791280 depotPaths = settings['depot-paths']1281 change = settings['change']12821283if changed:1284print"%srewound to%s"% (ref, change)12851286return True12871288classP4Submit(Command, P4UserMap):12891290 conflict_behavior_choices = ("ask","skip","quit")12911292def__init__(self):1293 Command.__init__(self)1294 P4UserMap.__init__(self)1295 self.options = [1296 optparse.make_option("--origin", dest="origin"),1297 optparse.make_option("-M", dest="detectRenames", action="store_true"),1298# preserve the user, requires relevant p4 permissions1299 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1300 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1301 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1302 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1303 optparse.make_option("--conflict", dest="conflict_behavior",1304 choices=self.conflict_behavior_choices),1305 optparse.make_option("--branch", dest="branch"),1306 optparse.make_option("--shelve", dest="shelve", action="store_true",1307help="Shelve instead of submit. Shelved files are reverted, "1308"restoring the workspace to the state before the shelve"),1309 optparse.make_option("--update-shelve", dest="update_shelve", action="store",type="int",1310 metavar="CHANGELIST",1311help="update an existing shelved changelist, implies --shelve")1312]1313 self.description ="Submit changes from git to the perforce depot."1314 self.usage +=" [name of git branch to submit into perforce depot]"1315 self.origin =""1316 self.detectRenames =False1317 self.preserveUser =gitConfigBool("git-p4.preserveUser")1318 self.dry_run =False1319 self.shelve =False1320 self.update_shelve =None1321 self.prepare_p4_only =False1322 self.conflict_behavior =None1323 self.isWindows = (platform.system() =="Windows")1324 self.exportLabels =False1325 self.p4HasMoveCommand =p4_has_move_command()1326 self.branch =None13271328ifgitConfig('git-p4.largeFileSystem'):1329die("Large file system not supported for git-p4 submit command. Please remove it from config.")13301331defcheck(self):1332iflen(p4CmdList("opened ...")) >0:1333die("You have files opened with perforce! Close them before starting the sync.")13341335defseparate_jobs_from_description(self, message):1336"""Extract and return a possible Jobs field in the commit1337 message. It goes into a separate section in the p4 change1338 specification.13391340 A jobs line starts with "Jobs:" and looks like a new field1341 in a form. Values are white-space separated on the same1342 line or on following lines that start with a tab.13431344 This does not parse and extract the full git commit message1345 like a p4 form. It just sees the Jobs: line as a marker1346 to pass everything from then on directly into the p4 form,1347 but outside the description section.13481349 Return a tuple (stripped log message, jobs string)."""13501351 m = re.search(r'^Jobs:', message, re.MULTILINE)1352if m is None:1353return(message,None)13541355 jobtext = message[m.start():]1356 stripped_message = message[:m.start()].rstrip()1357return(stripped_message, jobtext)13581359defprepareLogMessage(self, template, message, jobs):1360"""Edits the template returned from "p4 change -o" to insert1361 the message in the Description field, and the jobs text in1362 the Jobs field."""1363 result =""13641365 inDescriptionSection =False13661367for line in template.split("\n"):1368if line.startswith("#"):1369 result += line +"\n"1370continue13711372if inDescriptionSection:1373if line.startswith("Files:")or line.startswith("Jobs:"):1374 inDescriptionSection =False1375# insert Jobs section1376if jobs:1377 result += jobs +"\n"1378else:1379continue1380else:1381if line.startswith("Description:"):1382 inDescriptionSection =True1383 line +="\n"1384for messageLine in message.split("\n"):1385 line +="\t"+ messageLine +"\n"13861387 result += line +"\n"13881389return result13901391defpatchRCSKeywords(self,file, pattern):1392# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1393(handle, outFileName) = tempfile.mkstemp(dir='.')1394try:1395 outFile = os.fdopen(handle,"w+")1396 inFile =open(file,"r")1397 regexp = re.compile(pattern, re.VERBOSE)1398for line in inFile.readlines():1399 line = regexp.sub(r'$\1$', line)1400 outFile.write(line)1401 inFile.close()1402 outFile.close()1403# Forcibly overwrite the original file1404 os.unlink(file)1405 shutil.move(outFileName,file)1406except:1407# cleanup our temporary file1408 os.unlink(outFileName)1409print"Failed to strip RCS keywords in%s"%file1410raise14111412print"Patched up RCS keywords in%s"%file14131414defp4UserForCommit(self,id):1415# Return the tuple (perforce user,git email) for a given git commit id1416 self.getUserMapFromPerforceServer()1417 gitEmail =read_pipe(["git","log","--max-count=1",1418"--format=%ae",id])1419 gitEmail = gitEmail.strip()1420if not self.emails.has_key(gitEmail):1421return(None,gitEmail)1422else:1423return(self.emails[gitEmail],gitEmail)14241425defcheckValidP4Users(self,commits):1426# check if any git authors cannot be mapped to p4 users1427foridin commits:1428(user,email) = self.p4UserForCommit(id)1429if not user:1430 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1431ifgitConfigBool("git-p4.allowMissingP4Users"):1432print"%s"% msg1433else:1434die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14351436deflastP4Changelist(self):1437# Get back the last changelist number submitted in this client spec. This1438# then gets used to patch up the username in the change. If the same1439# client spec is being used by multiple processes then this might go1440# wrong.1441 results =p4CmdList("client -o")# find the current client1442 client =None1443for r in results:1444if r.has_key('Client'):1445 client = r['Client']1446break1447if not client:1448die("could not get client spec")1449 results =p4CmdList(["changes","-c", client,"-m","1"])1450for r in results:1451if r.has_key('change'):1452return r['change']1453die("Could not get changelist number for last submit - cannot patch up user details")14541455defmodifyChangelistUser(self, changelist, newUser):1456# fixup the user field of a changelist after it has been submitted.1457 changes =p4CmdList("change -o%s"% changelist)1458iflen(changes) !=1:1459die("Bad output from p4 change modifying%sto user%s"%1460(changelist, newUser))14611462 c = changes[0]1463if c['User'] == newUser:return# nothing to do1464 c['User'] = newUser1465input= marshal.dumps(c)14661467 result =p4CmdList("change -f -i", stdin=input)1468for r in result:1469if r.has_key('code'):1470if r['code'] =='error':1471die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1472if r.has_key('data'):1473print("Updated user field for changelist%sto%s"% (changelist, newUser))1474return1475die("Could not modify user field of changelist%sto%s"% (changelist, newUser))14761477defcanChangeChangelists(self):1478# check to see if we have p4 admin or super-user permissions, either of1479# which are required to modify changelists.1480 results =p4CmdList(["protects", self.depotPath])1481for r in results:1482if r.has_key('perm'):1483if r['perm'] =='admin':1484return11485if r['perm'] =='super':1486return11487return014881489defprepareSubmitTemplate(self, changelist=None):1490"""Run "p4 change -o" to grab a change specification template.1491 This does not use "p4 -G", as it is nice to keep the submission1492 template in original order, since a human might edit it.14931494 Remove lines in the Files section that show changes to files1495 outside the depot path we're committing into."""14961497[upstream, settings] =findUpstreamBranchPoint()14981499 template =""1500 inFilesSection =False1501 args = ['change','-o']1502if changelist:1503 args.append(str(changelist))15041505for line inp4_read_pipe_lines(args):1506if line.endswith("\r\n"):1507 line = line[:-2] +"\n"1508if inFilesSection:1509if line.startswith("\t"):1510# path starts and ends with a tab1511 path = line[1:]1512 lastTab = path.rfind("\t")1513if lastTab != -1:1514 path = path[:lastTab]1515if settings.has_key('depot-paths'):1516if not[p for p in settings['depot-paths']1517ifp4PathStartsWith(path, p)]:1518continue1519else:1520if notp4PathStartsWith(path, self.depotPath):1521continue1522else:1523 inFilesSection =False1524else:1525if line.startswith("Files:"):1526 inFilesSection =True15271528 template += line15291530return template15311532defedit_template(self, template_file):1533"""Invoke the editor to let the user change the submission1534 message. Return true if okay to continue with the submit."""15351536# if configured to skip the editing part, just submit1537ifgitConfigBool("git-p4.skipSubmitEdit"):1538return True15391540# look at the modification time, to check later if the user saved1541# the file1542 mtime = os.stat(template_file).st_mtime15431544# invoke the editor1545if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1546 editor = os.environ.get("P4EDITOR")1547else:1548 editor =read_pipe("git var GIT_EDITOR").strip()1549system(["sh","-c", ('%s"$@"'% editor), editor, template_file])15501551# If the file was not saved, prompt to see if this patch should1552# be skipped. But skip this verification step if configured so.1553ifgitConfigBool("git-p4.skipSubmitEditCheck"):1554return True15551556# modification time updated means user saved the file1557if os.stat(template_file).st_mtime > mtime:1558return True15591560while True:1561 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1562if response =='y':1563return True1564if response =='n':1565return False15661567defget_diff_description(self, editedFiles, filesToAdd):1568# diff1569if os.environ.has_key("P4DIFF"):1570del(os.environ["P4DIFF"])1571 diff =""1572for editedFile in editedFiles:1573 diff +=p4_read_pipe(['diff','-du',1574wildcard_encode(editedFile)])15751576# new file diff1577 newdiff =""1578for newFile in filesToAdd:1579 newdiff +="==== new file ====\n"1580 newdiff +="--- /dev/null\n"1581 newdiff +="+++%s\n"% newFile1582 f =open(newFile,"r")1583for line in f.readlines():1584 newdiff +="+"+ line1585 f.close()15861587return(diff + newdiff).replace('\r\n','\n')15881589defapplyCommit(self,id):1590"""Apply one commit, return True if it succeeded."""15911592print"Applying",read_pipe(["git","show","-s",1593"--format=format:%h%s",id])15941595(p4User, gitEmail) = self.p4UserForCommit(id)15961597 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1598 filesToAdd =set()1599 filesToChangeType =set()1600 filesToDelete =set()1601 editedFiles =set()1602 pureRenameCopy =set()1603 filesToChangeExecBit = {}1604 all_files =list()16051606for line in diff:1607 diff =parseDiffTreeEntry(line)1608 modifier = diff['status']1609 path = diff['src']1610 all_files.append(path)16111612if modifier =="M":1613p4_edit(path)1614ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1615 filesToChangeExecBit[path] = diff['dst_mode']1616 editedFiles.add(path)1617elif modifier =="A":1618 filesToAdd.add(path)1619 filesToChangeExecBit[path] = diff['dst_mode']1620if path in filesToDelete:1621 filesToDelete.remove(path)1622elif modifier =="D":1623 filesToDelete.add(path)1624if path in filesToAdd:1625 filesToAdd.remove(path)1626elif modifier =="C":1627 src, dest = diff['src'], diff['dst']1628p4_integrate(src, dest)1629 pureRenameCopy.add(dest)1630if diff['src_sha1'] != diff['dst_sha1']:1631p4_edit(dest)1632 pureRenameCopy.discard(dest)1633ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1634p4_edit(dest)1635 pureRenameCopy.discard(dest)1636 filesToChangeExecBit[dest] = diff['dst_mode']1637if self.isWindows:1638# turn off read-only attribute1639 os.chmod(dest, stat.S_IWRITE)1640 os.unlink(dest)1641 editedFiles.add(dest)1642elif modifier =="R":1643 src, dest = diff['src'], diff['dst']1644if self.p4HasMoveCommand:1645p4_edit(src)# src must be open before move1646p4_move(src, dest)# opens for (move/delete, move/add)1647else:1648p4_integrate(src, dest)1649if diff['src_sha1'] != diff['dst_sha1']:1650p4_edit(dest)1651else:1652 pureRenameCopy.add(dest)1653ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1654if not self.p4HasMoveCommand:1655p4_edit(dest)# with move: already open, writable1656 filesToChangeExecBit[dest] = diff['dst_mode']1657if not self.p4HasMoveCommand:1658if self.isWindows:1659 os.chmod(dest, stat.S_IWRITE)1660 os.unlink(dest)1661 filesToDelete.add(src)1662 editedFiles.add(dest)1663elif modifier =="T":1664 filesToChangeType.add(path)1665else:1666die("unknown modifier%sfor%s"% (modifier, path))16671668 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1669 patchcmd = diffcmd +" | git apply "1670 tryPatchCmd = patchcmd +"--check -"1671 applyPatchCmd = patchcmd +"--check --apply -"1672 patch_succeeded =True16731674if os.system(tryPatchCmd) !=0:1675 fixed_rcs_keywords =False1676 patch_succeeded =False1677print"Unfortunately applying the change failed!"16781679# Patch failed, maybe it's just RCS keyword woes. Look through1680# the patch to see if that's possible.1681ifgitConfigBool("git-p4.attemptRCSCleanup"):1682file=None1683 pattern =None1684 kwfiles = {}1685forfilein editedFiles | filesToDelete:1686# did this file's delta contain RCS keywords?1687 pattern =p4_keywords_regexp_for_file(file)16881689if pattern:1690# this file is a possibility...look for RCS keywords.1691 regexp = re.compile(pattern, re.VERBOSE)1692for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1693if regexp.search(line):1694if verbose:1695print"got keyword match on%sin%sin%s"% (pattern, line,file)1696 kwfiles[file] = pattern1697break16981699forfilein kwfiles:1700if verbose:1701print"zapping%swith%s"% (line,pattern)1702# File is being deleted, so not open in p4. Must1703# disable the read-only bit on windows.1704if self.isWindows andfilenot in editedFiles:1705 os.chmod(file, stat.S_IWRITE)1706 self.patchRCSKeywords(file, kwfiles[file])1707 fixed_rcs_keywords =True17081709if fixed_rcs_keywords:1710print"Retrying the patch with RCS keywords cleaned up"1711if os.system(tryPatchCmd) ==0:1712 patch_succeeded =True17131714if not patch_succeeded:1715for f in editedFiles:1716p4_revert(f)1717return False17181719#1720# Apply the patch for real, and do add/delete/+x handling.1721#1722system(applyPatchCmd)17231724for f in filesToChangeType:1725p4_edit(f,"-t","auto")1726for f in filesToAdd:1727p4_add(f)1728for f in filesToDelete:1729p4_revert(f)1730p4_delete(f)17311732# Set/clear executable bits1733for f in filesToChangeExecBit.keys():1734 mode = filesToChangeExecBit[f]1735setP4ExecBit(f, mode)17361737if self.update_shelve:1738print("all_files =%s"%str(all_files))1739p4_reopen_in_change(self.update_shelve, all_files)17401741#1742# Build p4 change description, starting with the contents1743# of the git commit message.1744#1745 logMessage =extractLogMessageFromGitCommit(id)1746 logMessage = logMessage.strip()1747(logMessage, jobs) = self.separate_jobs_from_description(logMessage)17481749 template = self.prepareSubmitTemplate(self.update_shelve)1750 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)17511752if self.preserveUser:1753 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User17541755if self.checkAuthorship and not self.p4UserIsMe(p4User):1756 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1757 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1758 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"17591760 separatorLine ="######## everything below this line is just the diff #######\n"1761if not self.prepare_p4_only:1762 submitTemplate += separatorLine1763 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)17641765(handle, fileName) = tempfile.mkstemp()1766 tmpFile = os.fdopen(handle,"w+b")1767if self.isWindows:1768 submitTemplate = submitTemplate.replace("\n","\r\n")1769 tmpFile.write(submitTemplate)1770 tmpFile.close()17711772if self.prepare_p4_only:1773#1774# Leave the p4 tree prepared, and the submit template around1775# and let the user decide what to do next1776#1777print1778print"P4 workspace prepared for submission."1779print"To submit or revert, go to client workspace"1780print" "+ self.clientPath1781print1782print"To submit, use\"p4 submit\"to write a new description,"1783print"or\"p4 submit -i <%s\"to use the one prepared by" \1784"\"git p4\"."% fileName1785print"You can delete the file\"%s\"when finished."% fileName17861787if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1788print"To preserve change ownership by user%s, you must\n" \1789"do\"p4 change -f <change>\"after submitting and\n" \1790"edit the User field."1791if pureRenameCopy:1792print"After submitting, renamed files must be re-synced."1793print"Invoke\"p4 sync -f\"on each of these files:"1794for f in pureRenameCopy:1795print" "+ f17961797print1798print"To revert the changes, use\"p4 revert ...\", and delete"1799print"the submit template file\"%s\""% fileName1800if filesToAdd:1801print"Since the commit adds new files, they must be deleted:"1802for f in filesToAdd:1803print" "+ f1804print1805return True18061807#1808# Let the user edit the change description, then submit it.1809#1810 submitted =False18111812try:1813if self.edit_template(fileName):1814# read the edited message and submit1815 tmpFile =open(fileName,"rb")1816 message = tmpFile.read()1817 tmpFile.close()1818if self.isWindows:1819 message = message.replace("\r\n","\n")1820 submitTemplate = message[:message.index(separatorLine)]18211822if self.update_shelve:1823p4_write_pipe(['shelve','-r','-i'], submitTemplate)1824elif self.shelve:1825p4_write_pipe(['shelve','-i'], submitTemplate)1826else:1827p4_write_pipe(['submit','-i'], submitTemplate)1828# The rename/copy happened by applying a patch that created a1829# new file. This leaves it writable, which confuses p4.1830for f in pureRenameCopy:1831p4_sync(f,"-f")18321833if self.preserveUser:1834if p4User:1835# Get last changelist number. Cannot easily get it from1836# the submit command output as the output is1837# unmarshalled.1838 changelist = self.lastP4Changelist()1839 self.modifyChangelistUser(changelist, p4User)18401841 submitted =True18421843finally:1844# skip this patch1845if not submitted or self.shelve:1846if self.shelve:1847print("Reverting shelved files.")1848else:1849print("Submission cancelled, undoing p4 changes.")1850for f in editedFiles | filesToDelete:1851p4_revert(f)1852for f in filesToAdd:1853p4_revert(f)1854 os.remove(f)18551856 os.remove(fileName)1857return submitted18581859# Export git tags as p4 labels. Create a p4 label and then tag1860# with that.1861defexportGitTags(self, gitTags):1862 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1863iflen(validLabelRegexp) ==0:1864 validLabelRegexp = defaultLabelRegexp1865 m = re.compile(validLabelRegexp)18661867for name in gitTags:18681869if not m.match(name):1870if verbose:1871print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1872continue18731874# Get the p4 commit this corresponds to1875 logMessage =extractLogMessageFromGitCommit(name)1876 values =extractSettingsGitLog(logMessage)18771878if not values.has_key('change'):1879# a tag pointing to something not sent to p4; ignore1880if verbose:1881print"git tag%sdoes not give a p4 commit"% name1882continue1883else:1884 changelist = values['change']18851886# Get the tag details.1887 inHeader =True1888 isAnnotated =False1889 body = []1890for l inread_pipe_lines(["git","cat-file","-p", name]):1891 l = l.strip()1892if inHeader:1893if re.match(r'tag\s+', l):1894 isAnnotated =True1895elif re.match(r'\s*$', l):1896 inHeader =False1897continue1898else:1899 body.append(l)19001901if not isAnnotated:1902 body = ["lightweight tag imported by git p4\n"]19031904# Create the label - use the same view as the client spec we are using1905 clientSpec =getClientSpec()19061907 labelTemplate ="Label:%s\n"% name1908 labelTemplate +="Description:\n"1909for b in body:1910 labelTemplate +="\t"+ b +"\n"1911 labelTemplate +="View:\n"1912for depot_side in clientSpec.mappings:1913 labelTemplate +="\t%s\n"% depot_side19141915if self.dry_run:1916print"Would create p4 label%sfor tag"% name1917elif self.prepare_p4_only:1918print"Not creating p4 label%sfor tag due to option" \1919" --prepare-p4-only"% name1920else:1921p4_write_pipe(["label","-i"], labelTemplate)19221923# Use the label1924p4_system(["tag","-l", name] +1925["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])19261927if verbose:1928print"created p4 label for tag%s"% name19291930defrun(self, args):1931iflen(args) ==0:1932 self.master =currentGitBranch()1933eliflen(args) ==1:1934 self.master = args[0]1935if notbranchExists(self.master):1936die("Branch%sdoes not exist"% self.master)1937else:1938return False19391940if self.master:1941 allowSubmit =gitConfig("git-p4.allowSubmit")1942iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1943die("%sis not in git-p4.allowSubmit"% self.master)19441945[upstream, settings] =findUpstreamBranchPoint()1946 self.depotPath = settings['depot-paths'][0]1947iflen(self.origin) ==0:1948 self.origin = upstream19491950if self.update_shelve:1951 self.shelve =True19521953if self.preserveUser:1954if not self.canChangeChangelists():1955die("Cannot preserve user names without p4 super-user or admin permissions")19561957# if not set from the command line, try the config file1958if self.conflict_behavior is None:1959 val =gitConfig("git-p4.conflict")1960if val:1961if val not in self.conflict_behavior_choices:1962die("Invalid value '%s' for config git-p4.conflict"% val)1963else:1964 val ="ask"1965 self.conflict_behavior = val19661967if self.verbose:1968print"Origin branch is "+ self.origin19691970iflen(self.depotPath) ==0:1971print"Internal error: cannot locate perforce depot path from existing branches"1972 sys.exit(128)19731974 self.useClientSpec =False1975ifgitConfigBool("git-p4.useclientspec"):1976 self.useClientSpec =True1977if self.useClientSpec:1978 self.clientSpecDirs =getClientSpec()19791980# Check for the existence of P4 branches1981 branchesDetected = (len(p4BranchesInGit().keys()) >1)19821983if self.useClientSpec and not branchesDetected:1984# all files are relative to the client spec1985 self.clientPath =getClientRoot()1986else:1987 self.clientPath =p4Where(self.depotPath)19881989if self.clientPath =="":1990die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)19911992print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1993 self.oldWorkingDirectory = os.getcwd()19941995# ensure the clientPath exists1996 new_client_dir =False1997if not os.path.exists(self.clientPath):1998 new_client_dir =True1999 os.makedirs(self.clientPath)20002001chdir(self.clientPath, is_client_path=True)2002if self.dry_run:2003print"Would synchronize p4 checkout in%s"% self.clientPath2004else:2005print"Synchronizing p4 checkout..."2006if new_client_dir:2007# old one was destroyed, and maybe nobody told p42008p4_sync("...","-f")2009else:2010p4_sync("...")2011 self.check()20122013 commits = []2014if self.master:2015 commitish = self.master2016else:2017 commitish ='HEAD'20182019for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2020 commits.append(line.strip())2021 commits.reverse()20222023if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2024 self.checkAuthorship =False2025else:2026 self.checkAuthorship =True20272028if self.preserveUser:2029 self.checkValidP4Users(commits)20302031#2032# Build up a set of options to be passed to diff when2033# submitting each commit to p4.2034#2035if self.detectRenames:2036# command-line -M arg2037 self.diffOpts ="-M"2038else:2039# If not explicitly set check the config variable2040 detectRenames =gitConfig("git-p4.detectRenames")20412042if detectRenames.lower() =="false"or detectRenames =="":2043 self.diffOpts =""2044elif detectRenames.lower() =="true":2045 self.diffOpts ="-M"2046else:2047 self.diffOpts ="-M%s"% detectRenames20482049# no command-line arg for -C or --find-copies-harder, just2050# config variables2051 detectCopies =gitConfig("git-p4.detectCopies")2052if detectCopies.lower() =="false"or detectCopies =="":2053pass2054elif detectCopies.lower() =="true":2055 self.diffOpts +=" -C"2056else:2057 self.diffOpts +=" -C%s"% detectCopies20582059ifgitConfigBool("git-p4.detectCopiesHarder"):2060 self.diffOpts +=" --find-copies-harder"20612062#2063# Apply the commits, one at a time. On failure, ask if should2064# continue to try the rest of the patches, or quit.2065#2066if self.dry_run:2067print"Would apply"2068 applied = []2069 last =len(commits) -12070for i, commit inenumerate(commits):2071if self.dry_run:2072print" ",read_pipe(["git","show","-s",2073"--format=format:%h%s", commit])2074 ok =True2075else:2076 ok = self.applyCommit(commit)2077if ok:2078 applied.append(commit)2079else:2080if self.prepare_p4_only and i < last:2081print"Processing only the first commit due to option" \2082" --prepare-p4-only"2083break2084if i < last:2085 quit =False2086while True:2087# prompt for what to do, or use the option/variable2088if self.conflict_behavior =="ask":2089print"What do you want to do?"2090 response =raw_input("[s]kip this commit but apply"2091" the rest, or [q]uit? ")2092if not response:2093continue2094elif self.conflict_behavior =="skip":2095 response ="s"2096elif self.conflict_behavior =="quit":2097 response ="q"2098else:2099die("Unknown conflict_behavior '%s'"%2100 self.conflict_behavior)21012102if response[0] =="s":2103print"Skipping this commit, but applying the rest"2104break2105if response[0] =="q":2106print"Quitting"2107 quit =True2108break2109if quit:2110break21112112chdir(self.oldWorkingDirectory)2113 shelved_applied ="shelved"if self.shelve else"applied"2114if self.dry_run:2115pass2116elif self.prepare_p4_only:2117pass2118eliflen(commits) ==len(applied):2119print("All commits{0}!".format(shelved_applied))21202121 sync =P4Sync()2122if self.branch:2123 sync.branch = self.branch2124 sync.run([])21252126 rebase =P4Rebase()2127 rebase.rebase()21282129else:2130iflen(applied) ==0:2131print("No commits{0}.".format(shelved_applied))2132else:2133print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2134for c in commits:2135if c in applied:2136 star ="*"2137else:2138 star =" "2139print star,read_pipe(["git","show","-s",2140"--format=format:%h%s", c])2141print"You will have to do 'git p4 sync' and rebase."21422143ifgitConfigBool("git-p4.exportLabels"):2144 self.exportLabels =True21452146if self.exportLabels:2147 p4Labels =getP4Labels(self.depotPath)2148 gitTags =getGitTags()21492150 missingGitTags = gitTags - p4Labels2151 self.exportGitTags(missingGitTags)21522153# exit with error unless everything applied perfectly2154iflen(commits) !=len(applied):2155 sys.exit(1)21562157return True21582159classView(object):2160"""Represent a p4 view ("p4 help views"), and map files in a2161 repo according to the view."""21622163def__init__(self, client_name):2164 self.mappings = []2165 self.client_prefix ="//%s/"% client_name2166# cache results of "p4 where" to lookup client file locations2167 self.client_spec_path_cache = {}21682169defappend(self, view_line):2170"""Parse a view line, splitting it into depot and client2171 sides. Append to self.mappings, preserving order. This2172 is only needed for tag creation."""21732174# Split the view line into exactly two words. P4 enforces2175# structure on these lines that simplifies this quite a bit.2176#2177# Either or both words may be double-quoted.2178# Single quotes do not matter.2179# Double-quote marks cannot occur inside the words.2180# A + or - prefix is also inside the quotes.2181# There are no quotes unless they contain a space.2182# The line is already white-space stripped.2183# The two words are separated by a single space.2184#2185if view_line[0] =='"':2186# First word is double quoted. Find its end.2187 close_quote_index = view_line.find('"',1)2188if close_quote_index <=0:2189die("No first-word closing quote found:%s"% view_line)2190 depot_side = view_line[1:close_quote_index]2191# skip closing quote and space2192 rhs_index = close_quote_index +1+12193else:2194 space_index = view_line.find(" ")2195if space_index <=0:2196die("No word-splitting space found:%s"% view_line)2197 depot_side = view_line[0:space_index]2198 rhs_index = space_index +121992200# prefix + means overlay on previous mapping2201if depot_side.startswith("+"):2202 depot_side = depot_side[1:]22032204# prefix - means exclude this path, leave out of mappings2205 exclude =False2206if depot_side.startswith("-"):2207 exclude =True2208 depot_side = depot_side[1:]22092210if not exclude:2211 self.mappings.append(depot_side)22122213defconvert_client_path(self, clientFile):2214# chop off //client/ part to make it relative2215if not clientFile.startswith(self.client_prefix):2216die("No prefix '%s' on clientFile '%s'"%2217(self.client_prefix, clientFile))2218return clientFile[len(self.client_prefix):]22192220defupdate_client_spec_path_cache(self, files):2221""" Caching file paths by "p4 where" batch query """22222223# List depot file paths exclude that already cached2224 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]22252226iflen(fileArgs) ==0:2227return# All files in cache22282229 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2230for res in where_result:2231if"code"in res and res["code"] =="error":2232# assume error is "... file(s) not in client view"2233continue2234if"clientFile"not in res:2235die("No clientFile in 'p4 where' output")2236if"unmap"in res:2237# it will list all of them, but only one not unmap-ped2238continue2239ifgitConfigBool("core.ignorecase"):2240 res['depotFile'] = res['depotFile'].lower()2241 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])22422243# not found files or unmap files set to ""2244for depotFile in fileArgs:2245ifgitConfigBool("core.ignorecase"):2246 depotFile = depotFile.lower()2247if depotFile not in self.client_spec_path_cache:2248 self.client_spec_path_cache[depotFile] =""22492250defmap_in_client(self, depot_path):2251"""Return the relative location in the client where this2252 depot file should live. Returns "" if the file should2253 not be mapped in the client."""22542255ifgitConfigBool("core.ignorecase"):2256 depot_path = depot_path.lower()22572258if depot_path in self.client_spec_path_cache:2259return self.client_spec_path_cache[depot_path]22602261die("Error:%sis not found in client spec path"% depot_path )2262return""22632264classP4Sync(Command, P4UserMap):2265 delete_actions = ("delete","move/delete","purge")22662267def__init__(self):2268 Command.__init__(self)2269 P4UserMap.__init__(self)2270 self.options = [2271 optparse.make_option("--branch", dest="branch"),2272 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2273 optparse.make_option("--changesfile", dest="changesFile"),2274 optparse.make_option("--silent", dest="silent", action="store_true"),2275 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2276 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2277 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2278help="Import into refs/heads/ , not refs/remotes"),2279 optparse.make_option("--max-changes", dest="maxChanges",2280help="Maximum number of changes to import"),2281 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2282help="Internal block size to use when iteratively calling p4 changes"),2283 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2284help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2285 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2286help="Only sync files that are included in the Perforce Client Spec"),2287 optparse.make_option("-/", dest="cloneExclude",2288 action="append",type="string",2289help="exclude depot path"),2290]2291 self.description ="""Imports from Perforce into a git repository.\n2292 example:2293 //depot/my/project/ -- to import the current head2294 //depot/my/project/@all -- to import everything2295 //depot/my/project/@1,6 -- to import only from revision 1 to 622962297 (a ... is not needed in the path p4 specification, it's added implicitly)"""22982299 self.usage +=" //depot/path[@revRange]"2300 self.silent =False2301 self.createdBranches =set()2302 self.committedChanges =set()2303 self.branch =""2304 self.detectBranches =False2305 self.detectLabels =False2306 self.importLabels =False2307 self.changesFile =""2308 self.syncWithOrigin =True2309 self.importIntoRemotes =True2310 self.maxChanges =""2311 self.changes_block_size =None2312 self.keepRepoPath =False2313 self.depotPaths =None2314 self.p4BranchesInGit = []2315 self.cloneExclude = []2316 self.useClientSpec =False2317 self.useClientSpec_from_options =False2318 self.clientSpecDirs =None2319 self.tempBranches = []2320 self.tempBranchLocation ="refs/git-p4-tmp"2321 self.largeFileSystem =None23222323ifgitConfig('git-p4.largeFileSystem'):2324 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2325 self.largeFileSystem =largeFileSystemConstructor(2326lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2327)23282329ifgitConfig("git-p4.syncFromOrigin") =="false":2330 self.syncWithOrigin =False23312332# This is required for the "append" cloneExclude action2333defensure_value(self, attr, value):2334if nothasattr(self, attr)orgetattr(self, attr)is None:2335setattr(self, attr, value)2336returngetattr(self, attr)23372338# Force a checkpoint in fast-import and wait for it to finish2339defcheckpoint(self):2340 self.gitStream.write("checkpoint\n\n")2341 self.gitStream.write("progress checkpoint\n\n")2342 out = self.gitOutput.readline()2343if self.verbose:2344print"checkpoint finished: "+ out23452346defextractFilesFromCommit(self, commit):2347 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2348for path in self.cloneExclude]2349 files = []2350 fnum =02351while commit.has_key("depotFile%s"% fnum):2352 path = commit["depotFile%s"% fnum]23532354if[p for p in self.cloneExclude2355ifp4PathStartsWith(path, p)]:2356 found =False2357else:2358 found = [p for p in self.depotPaths2359ifp4PathStartsWith(path, p)]2360if not found:2361 fnum = fnum +12362continue23632364file= {}2365file["path"] = path2366file["rev"] = commit["rev%s"% fnum]2367file["action"] = commit["action%s"% fnum]2368file["type"] = commit["type%s"% fnum]2369 files.append(file)2370 fnum = fnum +12371return files23722373defextractJobsFromCommit(self, commit):2374 jobs = []2375 jnum =02376while commit.has_key("job%s"% jnum):2377 job = commit["job%s"% jnum]2378 jobs.append(job)2379 jnum = jnum +12380return jobs23812382defstripRepoPath(self, path, prefixes):2383"""When streaming files, this is called to map a p4 depot path2384 to where it should go in git. The prefixes are either2385 self.depotPaths, or self.branchPrefixes in the case of2386 branch detection."""23872388if self.useClientSpec:2389# branch detection moves files up a level (the branch name)2390# from what client spec interpretation gives2391 path = self.clientSpecDirs.map_in_client(path)2392if self.detectBranches:2393for b in self.knownBranches:2394if path.startswith(b +"/"):2395 path = path[len(b)+1:]23962397elif self.keepRepoPath:2398# Preserve everything in relative path name except leading2399# //depot/; just look at first prefix as they all should2400# be in the same depot.2401 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2402ifp4PathStartsWith(path, depot):2403 path = path[len(depot):]24042405else:2406for p in prefixes:2407ifp4PathStartsWith(path, p):2408 path = path[len(p):]2409break24102411 path =wildcard_decode(path)2412return path24132414defsplitFilesIntoBranches(self, commit):2415"""Look at each depotFile in the commit to figure out to what2416 branch it belongs."""24172418if self.clientSpecDirs:2419 files = self.extractFilesFromCommit(commit)2420 self.clientSpecDirs.update_client_spec_path_cache(files)24212422 branches = {}2423 fnum =02424while commit.has_key("depotFile%s"% fnum):2425 path = commit["depotFile%s"% fnum]2426 found = [p for p in self.depotPaths2427ifp4PathStartsWith(path, p)]2428if not found:2429 fnum = fnum +12430continue24312432file= {}2433file["path"] = path2434file["rev"] = commit["rev%s"% fnum]2435file["action"] = commit["action%s"% fnum]2436file["type"] = commit["type%s"% fnum]2437 fnum = fnum +124382439# start with the full relative path where this file would2440# go in a p4 client2441if self.useClientSpec:2442 relPath = self.clientSpecDirs.map_in_client(path)2443else:2444 relPath = self.stripRepoPath(path, self.depotPaths)24452446for branch in self.knownBranches.keys():2447# add a trailing slash so that a commit into qt/4.2foo2448# doesn't end up in qt/4.2, e.g.2449if relPath.startswith(branch +"/"):2450if branch not in branches:2451 branches[branch] = []2452 branches[branch].append(file)2453break24542455return branches24562457defwriteToGitStream(self, gitMode, relPath, contents):2458 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2459 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2460for d in contents:2461 self.gitStream.write(d)2462 self.gitStream.write('\n')24632464# output one file from the P4 stream2465# - helper for streamP4Files24662467defstreamOneP4File(self,file, contents):2468 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2469if verbose:2470 size =int(self.stream_file['fileSize'])2471 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2472 sys.stdout.flush()24732474(type_base, type_mods) =split_p4_type(file["type"])24752476 git_mode ="100644"2477if"x"in type_mods:2478 git_mode ="100755"2479if type_base =="symlink":2480 git_mode ="120000"2481# p4 print on a symlink sometimes contains "target\n";2482# if it does, remove the newline2483 data =''.join(contents)2484if not data:2485# Some version of p4 allowed creating a symlink that pointed2486# to nothing. This causes p4 errors when checking out such2487# a change, and errors here too. Work around it by ignoring2488# the bad symlink; hopefully a future change fixes it.2489print"\nIgnoring empty symlink in%s"%file['depotFile']2490return2491elif data[-1] =='\n':2492 contents = [data[:-1]]2493else:2494 contents = [data]24952496if type_base =="utf16":2497# p4 delivers different text in the python output to -G2498# than it does when using "print -o", or normal p4 client2499# operations. utf16 is converted to ascii or utf8, perhaps.2500# But ascii text saved as -t utf16 is completely mangled.2501# Invoke print -o to get the real contents.2502#2503# On windows, the newlines will always be mangled by print, so put2504# them back too. This is not needed to the cygwin windows version,2505# just the native "NT" type.2506#2507try:2508 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2509exceptExceptionas e:2510if'Translation of file content failed'instr(e):2511 type_base ='binary'2512else:2513raise e2514else:2515ifp4_version_string().find('/NT') >=0:2516 text = text.replace('\r\n','\n')2517 contents = [ text ]25182519if type_base =="apple":2520# Apple filetype files will be streamed as a concatenation of2521# its appledouble header and the contents. This is useless2522# on both macs and non-macs. If using "print -q -o xx", it2523# will create "xx" with the data, and "%xx" with the header.2524# This is also not very useful.2525#2526# Ideally, someday, this script can learn how to generate2527# appledouble files directly and import those to git, but2528# non-mac machines can never find a use for apple filetype.2529print"\nIgnoring apple filetype file%s"%file['depotFile']2530return25312532# Note that we do not try to de-mangle keywords on utf16 files,2533# even though in theory somebody may want that.2534 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2535if pattern:2536 regexp = re.compile(pattern, re.VERBOSE)2537 text =''.join(contents)2538 text = regexp.sub(r'$\1$', text)2539 contents = [ text ]25402541try:2542 relPath.decode('ascii')2543except:2544 encoding ='utf8'2545ifgitConfig('git-p4.pathEncoding'):2546 encoding =gitConfig('git-p4.pathEncoding')2547 relPath = relPath.decode(encoding,'replace').encode('utf8','replace')2548if self.verbose:2549print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, relPath)25502551if self.largeFileSystem:2552(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)25532554 self.writeToGitStream(git_mode, relPath, contents)25552556defstreamOneP4Deletion(self,file):2557 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2558if verbose:2559 sys.stdout.write("delete%s\n"% relPath)2560 sys.stdout.flush()2561 self.gitStream.write("D%s\n"% relPath)25622563if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2564 self.largeFileSystem.removeLargeFile(relPath)25652566# handle another chunk of streaming data2567defstreamP4FilesCb(self, marshalled):25682569# catch p4 errors and complain2570 err =None2571if"code"in marshalled:2572if marshalled["code"] =="error":2573if"data"in marshalled:2574 err = marshalled["data"].rstrip()25752576if not err and'fileSize'in self.stream_file:2577 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2578if required_bytes >0:2579 err ='Not enough space left on%s! Free at least%iMB.'% (2580 os.getcwd(), required_bytes/1024/10242581)25822583if err:2584 f =None2585if self.stream_have_file_info:2586if"depotFile"in self.stream_file:2587 f = self.stream_file["depotFile"]2588# force a failure in fast-import, else an empty2589# commit will be made2590 self.gitStream.write("\n")2591 self.gitStream.write("die-now\n")2592 self.gitStream.close()2593# ignore errors, but make sure it exits first2594 self.importProcess.wait()2595if f:2596die("Error from p4 print for%s:%s"% (f, err))2597else:2598die("Error from p4 print:%s"% err)25992600if marshalled.has_key('depotFile')and self.stream_have_file_info:2601# start of a new file - output the old one first2602 self.streamOneP4File(self.stream_file, self.stream_contents)2603 self.stream_file = {}2604 self.stream_contents = []2605 self.stream_have_file_info =False26062607# pick up the new file information... for the2608# 'data' field we need to append to our array2609for k in marshalled.keys():2610if k =='data':2611if'streamContentSize'not in self.stream_file:2612 self.stream_file['streamContentSize'] =02613 self.stream_file['streamContentSize'] +=len(marshalled['data'])2614 self.stream_contents.append(marshalled['data'])2615else:2616 self.stream_file[k] = marshalled[k]26172618if(verbose and2619'streamContentSize'in self.stream_file and2620'fileSize'in self.stream_file and2621'depotFile'in self.stream_file):2622 size =int(self.stream_file["fileSize"])2623if size >0:2624 progress =100*self.stream_file['streamContentSize']/size2625 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2626 sys.stdout.flush()26272628 self.stream_have_file_info =True26292630# Stream directly from "p4 files" into "git fast-import"2631defstreamP4Files(self, files):2632 filesForCommit = []2633 filesToRead = []2634 filesToDelete = []26352636for f in files:2637 filesForCommit.append(f)2638if f['action']in self.delete_actions:2639 filesToDelete.append(f)2640else:2641 filesToRead.append(f)26422643# deleted files...2644for f in filesToDelete:2645 self.streamOneP4Deletion(f)26462647iflen(filesToRead) >0:2648 self.stream_file = {}2649 self.stream_contents = []2650 self.stream_have_file_info =False26512652# curry self argument2653defstreamP4FilesCbSelf(entry):2654 self.streamP4FilesCb(entry)26552656 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]26572658p4CmdList(["-x","-","print"],2659 stdin=fileArgs,2660 cb=streamP4FilesCbSelf)26612662# do the last chunk2663if self.stream_file.has_key('depotFile'):2664 self.streamOneP4File(self.stream_file, self.stream_contents)26652666defmake_email(self, userid):2667if userid in self.users:2668return self.users[userid]2669else:2670return"%s<a@b>"% userid26712672defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2673""" Stream a p4 tag.2674 commit is either a git commit, or a fast-import mark, ":<p4commit>"2675 """26762677if verbose:2678print"writing tag%sfor commit%s"% (labelName, commit)2679 gitStream.write("tag%s\n"% labelName)2680 gitStream.write("from%s\n"% commit)26812682if labelDetails.has_key('Owner'):2683 owner = labelDetails["Owner"]2684else:2685 owner =None26862687# Try to use the owner of the p4 label, or failing that,2688# the current p4 user id.2689if owner:2690 email = self.make_email(owner)2691else:2692 email = self.make_email(self.p4UserId())2693 tagger ="%s %s %s"% (email, epoch, self.tz)26942695 gitStream.write("tagger%s\n"% tagger)26962697print"labelDetails=",labelDetails2698if labelDetails.has_key('Description'):2699 description = labelDetails['Description']2700else:2701 description ='Label from git p4'27022703 gitStream.write("data%d\n"%len(description))2704 gitStream.write(description)2705 gitStream.write("\n")27062707definClientSpec(self, path):2708if not self.clientSpecDirs:2709return True2710 inClientSpec = self.clientSpecDirs.map_in_client(path)2711if not inClientSpec and self.verbose:2712print('Ignoring file outside of client spec:{0}'.format(path))2713return inClientSpec27142715defhasBranchPrefix(self, path):2716if not self.branchPrefixes:2717return True2718 hasPrefix = [p for p in self.branchPrefixes2719ifp4PathStartsWith(path, p)]2720if not hasPrefix and self.verbose:2721print('Ignoring file outside of prefix:{0}'.format(path))2722return hasPrefix27232724defcommit(self, details, files, branch, parent =""):2725 epoch = details["time"]2726 author = details["user"]2727 jobs = self.extractJobsFromCommit(details)27282729if self.verbose:2730print('commit into{0}'.format(branch))27312732if self.clientSpecDirs:2733 self.clientSpecDirs.update_client_spec_path_cache(files)27342735 files = [f for f in files2736if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]27372738if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2739print('Ignoring revision{0}as it would produce an empty commit.'2740.format(details['change']))2741return27422743 self.gitStream.write("commit%s\n"% branch)2744 self.gitStream.write("mark :%s\n"% details["change"])2745 self.committedChanges.add(int(details["change"]))2746 committer =""2747if author not in self.users:2748 self.getUserMapFromPerforceServer()2749 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)27502751 self.gitStream.write("committer%s\n"% committer)27522753 self.gitStream.write("data <<EOT\n")2754 self.gitStream.write(details["desc"])2755iflen(jobs) >0:2756 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2757 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2758(','.join(self.branchPrefixes), details["change"]))2759iflen(details['options']) >0:2760 self.gitStream.write(": options =%s"% details['options'])2761 self.gitStream.write("]\nEOT\n\n")27622763iflen(parent) >0:2764if self.verbose:2765print"parent%s"% parent2766 self.gitStream.write("from%s\n"% parent)27672768 self.streamP4Files(files)2769 self.gitStream.write("\n")27702771 change =int(details["change"])27722773if self.labels.has_key(change):2774 label = self.labels[change]2775 labelDetails = label[0]2776 labelRevisions = label[1]2777if self.verbose:2778print"Change%sis labelled%s"% (change, labelDetails)27792780 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2781for p in self.branchPrefixes])27822783iflen(files) ==len(labelRevisions):27842785 cleanedFiles = {}2786for info in files:2787if info["action"]in self.delete_actions:2788continue2789 cleanedFiles[info["depotFile"]] = info["rev"]27902791if cleanedFiles == labelRevisions:2792 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)27932794else:2795if not self.silent:2796print("Tag%sdoes not match with change%s: files do not match."2797% (labelDetails["label"], change))27982799else:2800if not self.silent:2801print("Tag%sdoes not match with change%s: file count is different."2802% (labelDetails["label"], change))28032804# Build a dictionary of changelists and labels, for "detect-labels" option.2805defgetLabels(self):2806 self.labels = {}28072808 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2809iflen(l) >0and not self.silent:2810print"Finding files belonging to labels in%s"% `self.depotPaths`28112812for output in l:2813 label = output["label"]2814 revisions = {}2815 newestChange =02816if self.verbose:2817print"Querying files for label%s"% label2818forfileinp4CmdList(["files"] +2819["%s...@%s"% (p, label)2820for p in self.depotPaths]):2821 revisions[file["depotFile"]] =file["rev"]2822 change =int(file["change"])2823if change > newestChange:2824 newestChange = change28252826 self.labels[newestChange] = [output, revisions]28272828if self.verbose:2829print"Label changes:%s"% self.labels.keys()28302831# Import p4 labels as git tags. A direct mapping does not2832# exist, so assume that if all the files are at the same revision2833# then we can use that, or it's something more complicated we should2834# just ignore.2835defimportP4Labels(self, stream, p4Labels):2836if verbose:2837print"import p4 labels: "+' '.join(p4Labels)28382839 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2840 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2841iflen(validLabelRegexp) ==0:2842 validLabelRegexp = defaultLabelRegexp2843 m = re.compile(validLabelRegexp)28442845for name in p4Labels:2846 commitFound =False28472848if not m.match(name):2849if verbose:2850print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2851continue28522853if name in ignoredP4Labels:2854continue28552856 labelDetails =p4CmdList(['label',"-o", name])[0]28572858# get the most recent changelist for each file in this label2859 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2860for p in self.depotPaths])28612862if change.has_key('change'):2863# find the corresponding git commit; take the oldest commit2864 changelist =int(change['change'])2865if changelist in self.committedChanges:2866 gitCommit =":%d"% changelist # use a fast-import mark2867 commitFound =True2868else:2869 gitCommit =read_pipe(["git","rev-list","--max-count=1",2870"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2871iflen(gitCommit) ==0:2872print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2873else:2874 commitFound =True2875 gitCommit = gitCommit.strip()28762877if commitFound:2878# Convert from p4 time format2879try:2880 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2881exceptValueError:2882print"Could not convert label time%s"% labelDetails['Update']2883 tmwhen =128842885 when =int(time.mktime(tmwhen))2886 self.streamTag(stream, name, labelDetails, gitCommit, when)2887if verbose:2888print"p4 label%smapped to git commit%s"% (name, gitCommit)2889else:2890if verbose:2891print"Label%shas no changelists - possibly deleted?"% name28922893if not commitFound:2894# We can't import this label; don't try again as it will get very2895# expensive repeatedly fetching all the files for labels that will2896# never be imported. If the label is moved in the future, the2897# ignore will need to be removed manually.2898system(["git","config","--add","git-p4.ignoredP4Labels", name])28992900defguessProjectName(self):2901for p in self.depotPaths:2902if p.endswith("/"):2903 p = p[:-1]2904 p = p[p.strip().rfind("/") +1:]2905if not p.endswith("/"):2906 p +="/"2907return p29082909defgetBranchMapping(self):2910 lostAndFoundBranches =set()29112912 user =gitConfig("git-p4.branchUser")2913iflen(user) >0:2914 command ="branches -u%s"% user2915else:2916 command ="branches"29172918for info inp4CmdList(command):2919 details =p4Cmd(["branch","-o", info["branch"]])2920 viewIdx =02921while details.has_key("View%s"% viewIdx):2922 paths = details["View%s"% viewIdx].split(" ")2923 viewIdx = viewIdx +12924# require standard //depot/foo/... //depot/bar/... mapping2925iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2926continue2927 source = paths[0]2928 destination = paths[1]2929## HACK2930ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2931 source = source[len(self.depotPaths[0]):-4]2932 destination = destination[len(self.depotPaths[0]):-4]29332934if destination in self.knownBranches:2935if not self.silent:2936print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2937print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2938continue29392940 self.knownBranches[destination] = source29412942 lostAndFoundBranches.discard(destination)29432944if source not in self.knownBranches:2945 lostAndFoundBranches.add(source)29462947# Perforce does not strictly require branches to be defined, so we also2948# check git config for a branch list.2949#2950# Example of branch definition in git config file:2951# [git-p4]2952# branchList=main:branchA2953# branchList=main:branchB2954# branchList=branchA:branchC2955 configBranches =gitConfigList("git-p4.branchList")2956for branch in configBranches:2957if branch:2958(source, destination) = branch.split(":")2959 self.knownBranches[destination] = source29602961 lostAndFoundBranches.discard(destination)29622963if source not in self.knownBranches:2964 lostAndFoundBranches.add(source)296529662967for branch in lostAndFoundBranches:2968 self.knownBranches[branch] = branch29692970defgetBranchMappingFromGitBranches(self):2971 branches =p4BranchesInGit(self.importIntoRemotes)2972for branch in branches.keys():2973if branch =="master":2974 branch ="main"2975else:2976 branch = branch[len(self.projectName):]2977 self.knownBranches[branch] = branch29782979defupdateOptionDict(self, d):2980 option_keys = {}2981if self.keepRepoPath:2982 option_keys['keepRepoPath'] =129832984 d["options"] =' '.join(sorted(option_keys.keys()))29852986defreadOptions(self, d):2987 self.keepRepoPath = (d.has_key('options')2988and('keepRepoPath'in d['options']))29892990defgitRefForBranch(self, branch):2991if branch =="main":2992return self.refPrefix +"master"29932994iflen(branch) <=0:2995return branch29962997return self.refPrefix + self.projectName + branch29982999defgitCommitByP4Change(self, ref, change):3000if self.verbose:3001print"looking in ref "+ ref +" for change%susing bisect..."% change30023003 earliestCommit =""3004 latestCommit =parseRevision(ref)30053006while True:3007if self.verbose:3008print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3009 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3010iflen(next) ==0:3011if self.verbose:3012print"argh"3013return""3014 log =extractLogMessageFromGitCommit(next)3015 settings =extractSettingsGitLog(log)3016 currentChange =int(settings['change'])3017if self.verbose:3018print"current change%s"% currentChange30193020if currentChange == change:3021if self.verbose:3022print"found%s"% next3023return next30243025if currentChange < change:3026 earliestCommit ="^%s"% next3027else:3028 latestCommit ="%s"% next30293030return""30313032defimportNewBranch(self, branch, maxChange):3033# make fast-import flush all changes to disk and update the refs using the checkpoint3034# command so that we can try to find the branch parent in the git history3035 self.gitStream.write("checkpoint\n\n");3036 self.gitStream.flush();3037 branchPrefix = self.depotPaths[0] + branch +"/"3038range="@1,%s"% maxChange3039#print "prefix" + branchPrefix3040 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3041iflen(changes) <=0:3042return False3043 firstChange = changes[0]3044#print "first change in branch: %s" % firstChange3045 sourceBranch = self.knownBranches[branch]3046 sourceDepotPath = self.depotPaths[0] + sourceBranch3047 sourceRef = self.gitRefForBranch(sourceBranch)3048#print "source " + sourceBranch30493050 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3051#print "branch parent: %s" % branchParentChange3052 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3053iflen(gitParent) >0:3054 self.initialParents[self.gitRefForBranch(branch)] = gitParent3055#print "parent git commit: %s" % gitParent30563057 self.importChanges(changes)3058return True30593060defsearchParent(self, parent, branch, target):3061 parentFound =False3062for blob inread_pipe_lines(["git","rev-list","--reverse",3063"--no-merges", parent]):3064 blob = blob.strip()3065iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3066 parentFound =True3067if self.verbose:3068print"Found parent of%sin commit%s"% (branch, blob)3069break3070if parentFound:3071return blob3072else:3073return None30743075defimportChanges(self, changes):3076 cnt =13077for change in changes:3078 description =p4_describe(change)3079 self.updateOptionDict(description)30803081if not self.silent:3082 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3083 sys.stdout.flush()3084 cnt = cnt +130853086try:3087if self.detectBranches:3088 branches = self.splitFilesIntoBranches(description)3089for branch in branches.keys():3090## HACK --hwn3091 branchPrefix = self.depotPaths[0] + branch +"/"3092 self.branchPrefixes = [ branchPrefix ]30933094 parent =""30953096 filesForCommit = branches[branch]30973098if self.verbose:3099print"branch is%s"% branch31003101 self.updatedBranches.add(branch)31023103if branch not in self.createdBranches:3104 self.createdBranches.add(branch)3105 parent = self.knownBranches[branch]3106if parent == branch:3107 parent =""3108else:3109 fullBranch = self.projectName + branch3110if fullBranch not in self.p4BranchesInGit:3111if not self.silent:3112print("\nImporting new branch%s"% fullBranch);3113if self.importNewBranch(branch, change -1):3114 parent =""3115 self.p4BranchesInGit.append(fullBranch)3116if not self.silent:3117print("\nResuming with change%s"% change);31183119if self.verbose:3120print"parent determined through known branches:%s"% parent31213122 branch = self.gitRefForBranch(branch)3123 parent = self.gitRefForBranch(parent)31243125if self.verbose:3126print"looking for initial parent for%s; current parent is%s"% (branch, parent)31273128iflen(parent) ==0and branch in self.initialParents:3129 parent = self.initialParents[branch]3130del self.initialParents[branch]31313132 blob =None3133iflen(parent) >0:3134 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3135if self.verbose:3136print"Creating temporary branch: "+ tempBranch3137 self.commit(description, filesForCommit, tempBranch)3138 self.tempBranches.append(tempBranch)3139 self.checkpoint()3140 blob = self.searchParent(parent, branch, tempBranch)3141if blob:3142 self.commit(description, filesForCommit, branch, blob)3143else:3144if self.verbose:3145print"Parent of%snot found. Committing into head of%s"% (branch, parent)3146 self.commit(description, filesForCommit, branch, parent)3147else:3148 files = self.extractFilesFromCommit(description)3149 self.commit(description, files, self.branch,3150 self.initialParent)3151# only needed once, to connect to the previous commit3152 self.initialParent =""3153exceptIOError:3154print self.gitError.read()3155 sys.exit(1)31563157defimportHeadRevision(self, revision):3158print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)31593160 details = {}3161 details["user"] ="git perforce import user"3162 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3163% (' '.join(self.depotPaths), revision))3164 details["change"] = revision3165 newestRevision =031663167 fileCnt =03168 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]31693170for info inp4CmdList(["files"] + fileArgs):31713172if'code'in info and info['code'] =='error':3173 sys.stderr.write("p4 returned an error:%s\n"3174% info['data'])3175if info['data'].find("must refer to client") >=0:3176 sys.stderr.write("This particular p4 error is misleading.\n")3177 sys.stderr.write("Perhaps the depot path was misspelled.\n");3178 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3179 sys.exit(1)3180if'p4ExitCode'in info:3181 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3182 sys.exit(1)318331843185 change =int(info["change"])3186if change > newestRevision:3187 newestRevision = change31883189if info["action"]in self.delete_actions:3190# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3191#fileCnt = fileCnt + 13192continue31933194for prop in["depotFile","rev","action","type"]:3195 details["%s%s"% (prop, fileCnt)] = info[prop]31963197 fileCnt = fileCnt +131983199 details["change"] = newestRevision32003201# Use time from top-most change so that all git p4 clones of3202# the same p4 repo have the same commit SHA1s.3203 res =p4_describe(newestRevision)3204 details["time"] = res["time"]32053206 self.updateOptionDict(details)3207try:3208 self.commit(details, self.extractFilesFromCommit(details), self.branch)3209exceptIOError:3210print"IO error with git fast-import. Is your git version recent enough?"3211print self.gitError.read()321232133214defrun(self, args):3215 self.depotPaths = []3216 self.changeRange =""3217 self.previousDepotPaths = []3218 self.hasOrigin =False32193220# map from branch depot path to parent branch3221 self.knownBranches = {}3222 self.initialParents = {}32233224if self.importIntoRemotes:3225 self.refPrefix ="refs/remotes/p4/"3226else:3227 self.refPrefix ="refs/heads/p4/"32283229if self.syncWithOrigin:3230 self.hasOrigin =originP4BranchesExist()3231if self.hasOrigin:3232if not self.silent:3233print'Syncing with origin first, using "git fetch origin"'3234system("git fetch origin")32353236 branch_arg_given =bool(self.branch)3237iflen(self.branch) ==0:3238 self.branch = self.refPrefix +"master"3239ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3240system("git update-ref%srefs/heads/p4"% self.branch)3241system("git branch -D p4")32423243# accept either the command-line option, or the configuration variable3244if self.useClientSpec:3245# will use this after clone to set the variable3246 self.useClientSpec_from_options =True3247else:3248ifgitConfigBool("git-p4.useclientspec"):3249 self.useClientSpec =True3250if self.useClientSpec:3251 self.clientSpecDirs =getClientSpec()32523253# TODO: should always look at previous commits,3254# merge with previous imports, if possible.3255if args == []:3256if self.hasOrigin:3257createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)32583259# branches holds mapping from branch name to sha13260 branches =p4BranchesInGit(self.importIntoRemotes)32613262# restrict to just this one, disabling detect-branches3263if branch_arg_given:3264 short = self.branch.split("/")[-1]3265if short in branches:3266 self.p4BranchesInGit = [ short ]3267else:3268 self.p4BranchesInGit = branches.keys()32693270iflen(self.p4BranchesInGit) >1:3271if not self.silent:3272print"Importing from/into multiple branches"3273 self.detectBranches =True3274for branch in branches.keys():3275 self.initialParents[self.refPrefix + branch] = \3276 branches[branch]32773278if self.verbose:3279print"branches:%s"% self.p4BranchesInGit32803281 p4Change =03282for branch in self.p4BranchesInGit:3283 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)32843285 settings =extractSettingsGitLog(logMsg)32863287 self.readOptions(settings)3288if(settings.has_key('depot-paths')3289and settings.has_key('change')):3290 change =int(settings['change']) +13291 p4Change =max(p4Change, change)32923293 depotPaths =sorted(settings['depot-paths'])3294if self.previousDepotPaths == []:3295 self.previousDepotPaths = depotPaths3296else:3297 paths = []3298for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3299 prev_list = prev.split("/")3300 cur_list = cur.split("/")3301for i inrange(0,min(len(cur_list),len(prev_list))):3302if cur_list[i] <> prev_list[i]:3303 i = i -13304break33053306 paths.append("/".join(cur_list[:i +1]))33073308 self.previousDepotPaths = paths33093310if p4Change >0:3311 self.depotPaths =sorted(self.previousDepotPaths)3312 self.changeRange ="@%s,#head"% p4Change3313if not self.silent and not self.detectBranches:3314print"Performing incremental import into%sgit branch"% self.branch33153316# accept multiple ref name abbreviations:3317# refs/foo/bar/branch -> use it exactly3318# p4/branch -> prepend refs/remotes/ or refs/heads/3319# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3320if not self.branch.startswith("refs/"):3321if self.importIntoRemotes:3322 prepend ="refs/remotes/"3323else:3324 prepend ="refs/heads/"3325if not self.branch.startswith("p4/"):3326 prepend +="p4/"3327 self.branch = prepend + self.branch33283329iflen(args) ==0and self.depotPaths:3330if not self.silent:3331print"Depot paths:%s"%' '.join(self.depotPaths)3332else:3333if self.depotPaths and self.depotPaths != args:3334print("previous import used depot path%sand now%swas specified. "3335"This doesn't work!"% (' '.join(self.depotPaths),3336' '.join(args)))3337 sys.exit(1)33383339 self.depotPaths =sorted(args)33403341 revision =""3342 self.users = {}33433344# Make sure no revision specifiers are used when --changesfile3345# is specified.3346 bad_changesfile =False3347iflen(self.changesFile) >0:3348for p in self.depotPaths:3349if p.find("@") >=0or p.find("#") >=0:3350 bad_changesfile =True3351break3352if bad_changesfile:3353die("Option --changesfile is incompatible with revision specifiers")33543355 newPaths = []3356for p in self.depotPaths:3357if p.find("@") != -1:3358 atIdx = p.index("@")3359 self.changeRange = p[atIdx:]3360if self.changeRange =="@all":3361 self.changeRange =""3362elif','not in self.changeRange:3363 revision = self.changeRange3364 self.changeRange =""3365 p = p[:atIdx]3366elif p.find("#") != -1:3367 hashIdx = p.index("#")3368 revision = p[hashIdx:]3369 p = p[:hashIdx]3370elif self.previousDepotPaths == []:3371# pay attention to changesfile, if given, else import3372# the entire p4 tree at the head revision3373iflen(self.changesFile) ==0:3374 revision ="#head"33753376 p = re.sub("\.\.\.$","", p)3377if not p.endswith("/"):3378 p +="/"33793380 newPaths.append(p)33813382 self.depotPaths = newPaths33833384# --detect-branches may change this for each branch3385 self.branchPrefixes = self.depotPaths33863387 self.loadUserMapFromCache()3388 self.labels = {}3389if self.detectLabels:3390 self.getLabels();33913392if self.detectBranches:3393## FIXME - what's a P4 projectName ?3394 self.projectName = self.guessProjectName()33953396if self.hasOrigin:3397 self.getBranchMappingFromGitBranches()3398else:3399 self.getBranchMapping()3400if self.verbose:3401print"p4-git branches:%s"% self.p4BranchesInGit3402print"initial parents:%s"% self.initialParents3403for b in self.p4BranchesInGit:3404if b !="master":34053406## FIXME3407 b = b[len(self.projectName):]3408 self.createdBranches.add(b)34093410 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))34113412 self.importProcess = subprocess.Popen(["git","fast-import"],3413 stdin=subprocess.PIPE,3414 stdout=subprocess.PIPE,3415 stderr=subprocess.PIPE);3416 self.gitOutput = self.importProcess.stdout3417 self.gitStream = self.importProcess.stdin3418 self.gitError = self.importProcess.stderr34193420if revision:3421 self.importHeadRevision(revision)3422else:3423 changes = []34243425iflen(self.changesFile) >0:3426 output =open(self.changesFile).readlines()3427 changeSet =set()3428for line in output:3429 changeSet.add(int(line))34303431for change in changeSet:3432 changes.append(change)34333434 changes.sort()3435else:3436# catch "git p4 sync" with no new branches, in a repo that3437# does not have any existing p4 branches3438iflen(args) ==0:3439if not self.p4BranchesInGit:3440die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")34413442# The default branch is master, unless --branch is used to3443# specify something else. Make sure it exists, or complain3444# nicely about how to use --branch.3445if not self.detectBranches:3446if notbranch_exists(self.branch):3447if branch_arg_given:3448die("Error: branch%sdoes not exist."% self.branch)3449else:3450die("Error: no branch%s; perhaps specify one with --branch."%3451 self.branch)34523453if self.verbose:3454print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3455 self.changeRange)3456 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)34573458iflen(self.maxChanges) >0:3459 changes = changes[:min(int(self.maxChanges),len(changes))]34603461iflen(changes) ==0:3462if not self.silent:3463print"No changes to import!"3464else:3465if not self.silent and not self.detectBranches:3466print"Import destination:%s"% self.branch34673468 self.updatedBranches =set()34693470if not self.detectBranches:3471if args:3472# start a new branch3473 self.initialParent =""3474else:3475# build on a previous revision3476 self.initialParent =parseRevision(self.branch)34773478 self.importChanges(changes)34793480if not self.silent:3481print""3482iflen(self.updatedBranches) >0:3483 sys.stdout.write("Updated branches: ")3484for b in self.updatedBranches:3485 sys.stdout.write("%s"% b)3486 sys.stdout.write("\n")34873488ifgitConfigBool("git-p4.importLabels"):3489 self.importLabels =True34903491if self.importLabels:3492 p4Labels =getP4Labels(self.depotPaths)3493 gitTags =getGitTags()34943495 missingP4Labels = p4Labels - gitTags3496 self.importP4Labels(self.gitStream, missingP4Labels)34973498 self.gitStream.close()3499if self.importProcess.wait() !=0:3500die("fast-import failed:%s"% self.gitError.read())3501 self.gitOutput.close()3502 self.gitError.close()35033504# Cleanup temporary branches created during import3505if self.tempBranches != []:3506for branch in self.tempBranches:3507read_pipe("git update-ref -d%s"% branch)3508 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))35093510# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3511# a convenient shortcut refname "p4".3512if self.importIntoRemotes:3513 head_ref = self.refPrefix +"HEAD"3514if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3515system(["git","symbolic-ref", head_ref, self.branch])35163517return True35183519classP4Rebase(Command):3520def__init__(self):3521 Command.__init__(self)3522 self.options = [3523 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3524]3525 self.importLabels =False3526 self.description = ("Fetches the latest revision from perforce and "3527+"rebases the current work (branch) against it")35283529defrun(self, args):3530 sync =P4Sync()3531 sync.importLabels = self.importLabels3532 sync.run([])35333534return self.rebase()35353536defrebase(self):3537if os.system("git update-index --refresh") !=0:3538die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up-to-date or stash away all your changes with git stash.");3539iflen(read_pipe("git diff-index HEAD --")) >0:3540die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");35413542[upstream, settings] =findUpstreamBranchPoint()3543iflen(upstream) ==0:3544die("Cannot find upstream branchpoint for rebase")35453546# the branchpoint may be p4/foo~3, so strip off the parent3547 upstream = re.sub("~[0-9]+$","", upstream)35483549print"Rebasing the current branch onto%s"% upstream3550 oldHead =read_pipe("git rev-parse HEAD").strip()3551system("git rebase%s"% upstream)3552system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3553return True35543555classP4Clone(P4Sync):3556def__init__(self):3557 P4Sync.__init__(self)3558 self.description ="Creates a new git repository and imports from Perforce into it"3559 self.usage ="usage: %prog [options] //depot/path[@revRange]"3560 self.options += [3561 optparse.make_option("--destination", dest="cloneDestination",3562 action='store', default=None,3563help="where to leave result of the clone"),3564 optparse.make_option("--bare", dest="cloneBare",3565 action="store_true", default=False),3566]3567 self.cloneDestination =None3568 self.needsGit =False3569 self.cloneBare =False35703571defdefaultDestination(self, args):3572## TODO: use common prefix of args?3573 depotPath = args[0]3574 depotDir = re.sub("(@[^@]*)$","", depotPath)3575 depotDir = re.sub("(#[^#]*)$","", depotDir)3576 depotDir = re.sub(r"\.\.\.$","", depotDir)3577 depotDir = re.sub(r"/$","", depotDir)3578return os.path.split(depotDir)[1]35793580defrun(self, args):3581iflen(args) <1:3582return False35833584if self.keepRepoPath and not self.cloneDestination:3585 sys.stderr.write("Must specify destination for --keep-path\n")3586 sys.exit(1)35873588 depotPaths = args35893590if not self.cloneDestination andlen(depotPaths) >1:3591 self.cloneDestination = depotPaths[-1]3592 depotPaths = depotPaths[:-1]35933594 self.cloneExclude = ["/"+p for p in self.cloneExclude]3595for p in depotPaths:3596if not p.startswith("//"):3597 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3598return False35993600if not self.cloneDestination:3601 self.cloneDestination = self.defaultDestination(args)36023603print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)36043605if not os.path.exists(self.cloneDestination):3606 os.makedirs(self.cloneDestination)3607chdir(self.cloneDestination)36083609 init_cmd = ["git","init"]3610if self.cloneBare:3611 init_cmd.append("--bare")3612 retcode = subprocess.call(init_cmd)3613if retcode:3614raiseCalledProcessError(retcode, init_cmd)36153616if not P4Sync.run(self, depotPaths):3617return False36183619# create a master branch and check out a work tree3620ifgitBranchExists(self.branch):3621system(["git","branch","master", self.branch ])3622if not self.cloneBare:3623system(["git","checkout","-f"])3624else:3625print'Not checking out any branch, use ' \3626'"git checkout -q -b master <branch>"'36273628# auto-set this variable if invoked with --use-client-spec3629if self.useClientSpec_from_options:3630system("git config --bool git-p4.useclientspec true")36313632return True36333634classP4Branches(Command):3635def__init__(self):3636 Command.__init__(self)3637 self.options = [ ]3638 self.description = ("Shows the git branches that hold imports and their "3639+"corresponding perforce depot paths")3640 self.verbose =False36413642defrun(self, args):3643iforiginP4BranchesExist():3644createOrUpdateBranchesFromOrigin()36453646 cmdline ="git rev-parse --symbolic "3647 cmdline +=" --remotes"36483649for line inread_pipe_lines(cmdline):3650 line = line.strip()36513652if not line.startswith('p4/')or line =="p4/HEAD":3653continue3654 branch = line36553656 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3657 settings =extractSettingsGitLog(log)36583659print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3660return True36613662classHelpFormatter(optparse.IndentedHelpFormatter):3663def__init__(self):3664 optparse.IndentedHelpFormatter.__init__(self)36653666defformat_description(self, description):3667if description:3668return description +"\n"3669else:3670return""36713672defprintUsage(commands):3673print"usage:%s<command> [options]"% sys.argv[0]3674print""3675print"valid commands:%s"%", ".join(commands)3676print""3677print"Try%s<command> --help for command specific help."% sys.argv[0]3678print""36793680commands = {3681"debug": P4Debug,3682"submit": P4Submit,3683"commit": P4Submit,3684"sync": P4Sync,3685"rebase": P4Rebase,3686"clone": P4Clone,3687"rollback": P4RollBack,3688"branches": P4Branches3689}369036913692defmain():3693iflen(sys.argv[1:]) ==0:3694printUsage(commands.keys())3695 sys.exit(2)36963697 cmdName = sys.argv[1]3698try:3699 klass = commands[cmdName]3700 cmd =klass()3701exceptKeyError:3702print"unknown command%s"% cmdName3703print""3704printUsage(commands.keys())3705 sys.exit(2)37063707 options = cmd.options3708 cmd.gitdir = os.environ.get("GIT_DIR",None)37093710 args = sys.argv[2:]37113712 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3713if cmd.needsGit:3714 options.append(optparse.make_option("--git-dir", dest="gitdir"))37153716 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3717 options,3718 description = cmd.description,3719 formatter =HelpFormatter())37203721(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3722global verbose3723 verbose = cmd.verbose3724if cmd.needsGit:3725if cmd.gitdir ==None:3726 cmd.gitdir = os.path.abspath(".git")3727if notisValidGitDir(cmd.gitdir):3728 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3729if os.path.exists(cmd.gitdir):3730 cdup =read_pipe("git rev-parse --show-cdup").strip()3731iflen(cdup) >0:3732chdir(cdup);37333734if notisValidGitDir(cmd.gitdir):3735ifisValidGitDir(cmd.gitdir +"/.git"):3736 cmd.gitdir +="/.git"3737else:3738die("fatal: cannot locate git repository at%s"% cmd.gitdir)37393740 os.environ["GIT_DIR"] = cmd.gitdir37413742if not cmd.run(args):3743 parser.print_help()3744 sys.exit(2)374537463747if __name__ =='__main__':3748main()