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 28import errno 29 30try: 31from subprocess import CalledProcessError 32exceptImportError: 33# from python2.7:subprocess.py 34# Exception classes used by this module. 35classCalledProcessError(Exception): 36"""This exception is raised when a process run by check_call() returns 37 a non-zero exit status. The exit status will be stored in the 38 returncode attribute.""" 39def__init__(self, returncode, cmd): 40 self.returncode = returncode 41 self.cmd = cmd 42def__str__(self): 43return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 44 45verbose =False 46 47# Only labels/tags matching this will be imported/exported 48defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 49 50# Grab changes in blocks of this many revisions, unless otherwise requested 51defaultBlockSize =512 52 53defp4_build_cmd(cmd): 54"""Build a suitable p4 command line. 55 56 This consolidates building and returning a p4 command line into one 57 location. It means that hooking into the environment, or other configuration 58 can be done more easily. 59 """ 60 real_cmd = ["p4"] 61 62 user =gitConfig("git-p4.user") 63iflen(user) >0: 64 real_cmd += ["-u",user] 65 66 password =gitConfig("git-p4.password") 67iflen(password) >0: 68 real_cmd += ["-P", password] 69 70 port =gitConfig("git-p4.port") 71iflen(port) >0: 72 real_cmd += ["-p", port] 73 74 host =gitConfig("git-p4.host") 75iflen(host) >0: 76 real_cmd += ["-H", host] 77 78 client =gitConfig("git-p4.client") 79iflen(client) >0: 80 real_cmd += ["-c", client] 81 82 retries =gitConfigInt("git-p4.retries") 83if retries is None: 84# Perform 3 retries by default 85 retries =3 86 real_cmd += ["-r",str(retries)] 87 88ifisinstance(cmd,basestring): 89 real_cmd =' '.join(real_cmd) +' '+ cmd 90else: 91 real_cmd += cmd 92return real_cmd 93 94defgit_dir(path): 95""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 96 This won't automatically add ".git" to a directory. 97 """ 98 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 99if not d orlen(d) ==0: 100return None 101else: 102return d 103 104defchdir(path, is_client_path=False): 105"""Do chdir to the given path, and set the PWD environment 106 variable for use by P4. It does not look at getcwd() output. 107 Since we're not using the shell, it is necessary to set the 108 PWD environment variable explicitly. 109 110 Normally, expand the path to force it to be absolute. This 111 addresses the use of relative path names inside P4 settings, 112 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 113 as given; it looks for .p4config using PWD. 114 115 If is_client_path, the path was handed to us directly by p4, 116 and may be a symbolic link. Do not call os.getcwd() in this 117 case, because it will cause p4 to think that PWD is not inside 118 the client path. 119 """ 120 121 os.chdir(path) 122if not is_client_path: 123 path = os.getcwd() 124 os.environ['PWD'] = path 125 126defcalcDiskFree(): 127"""Return free space in bytes on the disk of the given dirname.""" 128if platform.system() =='Windows': 129 free_bytes = ctypes.c_ulonglong(0) 130 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 131return free_bytes.value 132else: 133 st = os.statvfs(os.getcwd()) 134return st.f_bavail * st.f_frsize 135 136defdie(msg): 137if verbose: 138raiseException(msg) 139else: 140 sys.stderr.write(msg +"\n") 141 sys.exit(1) 142 143defwrite_pipe(c, stdin): 144if verbose: 145 sys.stderr.write('Writing pipe:%s\n'%str(c)) 146 147 expand =isinstance(c,basestring) 148 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 149 pipe = p.stdin 150 val = pipe.write(stdin) 151 pipe.close() 152if p.wait(): 153die('Command failed:%s'%str(c)) 154 155return val 156 157defp4_write_pipe(c, stdin): 158 real_cmd =p4_build_cmd(c) 159returnwrite_pipe(real_cmd, stdin) 160 161defread_pipe(c, ignore_error=False): 162if verbose: 163 sys.stderr.write('Reading pipe:%s\n'%str(c)) 164 165 expand =isinstance(c,basestring) 166 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 167(out, err) = p.communicate() 168if p.returncode !=0and not ignore_error: 169die('Command failed:%s\nError:%s'% (str(c), err)) 170return out 171 172defp4_read_pipe(c, ignore_error=False): 173 real_cmd =p4_build_cmd(c) 174returnread_pipe(real_cmd, ignore_error) 175 176defread_pipe_lines(c): 177if verbose: 178 sys.stderr.write('Reading pipe:%s\n'%str(c)) 179 180 expand =isinstance(c, basestring) 181 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 182 pipe = p.stdout 183 val = pipe.readlines() 184if pipe.close()or p.wait(): 185die('Command failed:%s'%str(c)) 186 187return val 188 189defp4_read_pipe_lines(c): 190"""Specifically invoke p4 on the command supplied. """ 191 real_cmd =p4_build_cmd(c) 192returnread_pipe_lines(real_cmd) 193 194defp4_has_command(cmd): 195"""Ask p4 for help on this command. If it returns an error, the 196 command does not exist in this version of p4.""" 197 real_cmd =p4_build_cmd(["help", cmd]) 198 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 199 stderr=subprocess.PIPE) 200 p.communicate() 201return p.returncode ==0 202 203defp4_has_move_command(): 204"""See if the move command exists, that it supports -k, and that 205 it has not been administratively disabled. The arguments 206 must be correct, but the filenames do not have to exist. Use 207 ones with wildcards so even if they exist, it will fail.""" 208 209if notp4_has_command("move"): 210return False 211 cmd =p4_build_cmd(["move","-k","@from","@to"]) 212 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 213(out, err) = p.communicate() 214# return code will be 1 in either case 215if err.find("Invalid option") >=0: 216return False 217if err.find("disabled") >=0: 218return False 219# assume it failed because @... was invalid changelist 220return True 221 222defsystem(cmd, ignore_error=False): 223 expand =isinstance(cmd,basestring) 224if verbose: 225 sys.stderr.write("executing%s\n"%str(cmd)) 226 retcode = subprocess.call(cmd, shell=expand) 227if retcode and not ignore_error: 228raiseCalledProcessError(retcode, cmd) 229 230return retcode 231 232defp4_system(cmd): 233"""Specifically invoke p4 as the system command. """ 234 real_cmd =p4_build_cmd(cmd) 235 expand =isinstance(real_cmd, basestring) 236 retcode = subprocess.call(real_cmd, shell=expand) 237if retcode: 238raiseCalledProcessError(retcode, real_cmd) 239 240_p4_version_string =None 241defp4_version_string(): 242"""Read the version string, showing just the last line, which 243 hopefully is the interesting version bit. 244 245 $ p4 -V 246 Perforce - The Fast Software Configuration Management System. 247 Copyright 1995-2011 Perforce Software. All rights reserved. 248 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 249 """ 250global _p4_version_string 251if not _p4_version_string: 252 a =p4_read_pipe_lines(["-V"]) 253 _p4_version_string = a[-1].rstrip() 254return _p4_version_string 255 256defp4_integrate(src, dest): 257p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 258 259defp4_sync(f, *options): 260p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 261 262defp4_add(f): 263# forcibly add file names with wildcards 264ifwildcard_present(f): 265p4_system(["add","-f", f]) 266else: 267p4_system(["add", f]) 268 269defp4_delete(f): 270p4_system(["delete",wildcard_encode(f)]) 271 272defp4_edit(f, *options): 273p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 274 275defp4_revert(f): 276p4_system(["revert",wildcard_encode(f)]) 277 278defp4_reopen(type, f): 279p4_system(["reopen","-t",type,wildcard_encode(f)]) 280 281defp4_reopen_in_change(changelist, files): 282 cmd = ["reopen","-c",str(changelist)] + files 283p4_system(cmd) 284 285defp4_move(src, dest): 286p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 287 288defp4_last_change(): 289 results =p4CmdList(["changes","-m","1"]) 290returnint(results[0]['change']) 291 292defp4_describe(change): 293"""Make sure it returns a valid result by checking for 294 the presence of field "time". Return a dict of the 295 results.""" 296 297 ds =p4CmdList(["describe","-s",str(change)]) 298iflen(ds) !=1: 299die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 300 301 d = ds[0] 302 303if"p4ExitCode"in d: 304die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 305str(d))) 306if"code"in d: 307if d["code"] =="error": 308die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 309 310if"time"not in d: 311die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 312 313return d 314 315# 316# Canonicalize the p4 type and return a tuple of the 317# base type, plus any modifiers. See "p4 help filetypes" 318# for a list and explanation. 319# 320defsplit_p4_type(p4type): 321 322 p4_filetypes_historical = { 323"ctempobj":"binary+Sw", 324"ctext":"text+C", 325"cxtext":"text+Cx", 326"ktext":"text+k", 327"kxtext":"text+kx", 328"ltext":"text+F", 329"tempobj":"binary+FSw", 330"ubinary":"binary+F", 331"uresource":"resource+F", 332"uxbinary":"binary+Fx", 333"xbinary":"binary+x", 334"xltext":"text+Fx", 335"xtempobj":"binary+Swx", 336"xtext":"text+x", 337"xunicode":"unicode+x", 338"xutf16":"utf16+x", 339} 340if p4type in p4_filetypes_historical: 341 p4type = p4_filetypes_historical[p4type] 342 mods ="" 343 s = p4type.split("+") 344 base = s[0] 345 mods ="" 346iflen(s) >1: 347 mods = s[1] 348return(base, mods) 349 350# 351# return the raw p4 type of a file (text, text+ko, etc) 352# 353defp4_type(f): 354 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 355return results[0]['headType'] 356 357# 358# Given a type base and modifier, return a regexp matching 359# the keywords that can be expanded in the file 360# 361defp4_keywords_regexp_for_type(base, type_mods): 362if base in("text","unicode","binary"): 363 kwords =None 364if"ko"in type_mods: 365 kwords ='Id|Header' 366elif"k"in type_mods: 367 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 368else: 369return None 370 pattern = r""" 371 \$ # Starts with a dollar, followed by... 372 (%s) # one of the keywords, followed by... 373 (:[^$\n]+)? # possibly an old expansion, followed by... 374 \$ # another dollar 375 """% kwords 376return pattern 377else: 378return None 379 380# 381# Given a file, return a regexp matching the possible 382# RCS keywords that will be expanded, or None for files 383# with kw expansion turned off. 384# 385defp4_keywords_regexp_for_file(file): 386if not os.path.exists(file): 387return None 388else: 389(type_base, type_mods) =split_p4_type(p4_type(file)) 390returnp4_keywords_regexp_for_type(type_base, type_mods) 391 392defsetP4ExecBit(file, mode): 393# Reopens an already open file and changes the execute bit to match 394# the execute bit setting in the passed in mode. 395 396 p4Type ="+x" 397 398if notisModeExec(mode): 399 p4Type =getP4OpenedType(file) 400 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 401 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 402if p4Type[-1] =="+": 403 p4Type = p4Type[0:-1] 404 405p4_reopen(p4Type,file) 406 407defgetP4OpenedType(file): 408# Returns the perforce file type for the given file. 409 410 result =p4_read_pipe(["opened",wildcard_encode(file)]) 411 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 412if match: 413return match.group(1) 414else: 415die("Could not determine file type for%s(result: '%s')"% (file, result)) 416 417# Return the set of all p4 labels 418defgetP4Labels(depotPaths): 419 labels =set() 420ifisinstance(depotPaths,basestring): 421 depotPaths = [depotPaths] 422 423for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 424 label = l['label'] 425 labels.add(label) 426 427return labels 428 429# Return the set of all git tags 430defgetGitTags(): 431 gitTags =set() 432for line inread_pipe_lines(["git","tag"]): 433 tag = line.strip() 434 gitTags.add(tag) 435return gitTags 436 437defdiffTreePattern(): 438# This is a simple generator for the diff tree regex pattern. This could be 439# a class variable if this and parseDiffTreeEntry were a part of a class. 440 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 441while True: 442yield pattern 443 444defparseDiffTreeEntry(entry): 445"""Parses a single diff tree entry into its component elements. 446 447 See git-diff-tree(1) manpage for details about the format of the diff 448 output. This method returns a dictionary with the following elements: 449 450 src_mode - The mode of the source file 451 dst_mode - The mode of the destination file 452 src_sha1 - The sha1 for the source file 453 dst_sha1 - The sha1 fr the destination file 454 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 455 status_score - The score for the status (applicable for 'C' and 'R' 456 statuses). This is None if there is no score. 457 src - The path for the source file. 458 dst - The path for the destination file. This is only present for 459 copy or renames. If it is not present, this is None. 460 461 If the pattern is not matched, None is returned.""" 462 463 match =diffTreePattern().next().match(entry) 464if match: 465return{ 466'src_mode': match.group(1), 467'dst_mode': match.group(2), 468'src_sha1': match.group(3), 469'dst_sha1': match.group(4), 470'status': match.group(5), 471'status_score': match.group(6), 472'src': match.group(7), 473'dst': match.group(10) 474} 475return None 476 477defisModeExec(mode): 478# Returns True if the given git mode represents an executable file, 479# otherwise False. 480return mode[-3:] =="755" 481 482defisModeExecChanged(src_mode, dst_mode): 483returnisModeExec(src_mode) !=isModeExec(dst_mode) 484 485defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 486 487ifisinstance(cmd,basestring): 488 cmd ="-G "+ cmd 489 expand =True 490else: 491 cmd = ["-G"] + cmd 492 expand =False 493 494 cmd =p4_build_cmd(cmd) 495if verbose: 496 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 497 498# Use a temporary file to avoid deadlocks without 499# subprocess.communicate(), which would put another copy 500# of stdout into memory. 501 stdin_file =None 502if stdin is not None: 503 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 504ifisinstance(stdin,basestring): 505 stdin_file.write(stdin) 506else: 507for i in stdin: 508 stdin_file.write(i +'\n') 509 stdin_file.flush() 510 stdin_file.seek(0) 511 512 p4 = subprocess.Popen(cmd, 513 shell=expand, 514 stdin=stdin_file, 515 stdout=subprocess.PIPE) 516 517 result = [] 518try: 519while True: 520 entry = marshal.load(p4.stdout) 521if cb is not None: 522cb(entry) 523else: 524 result.append(entry) 525exceptEOFError: 526pass 527 exitCode = p4.wait() 528if exitCode !=0: 529 entry = {} 530 entry["p4ExitCode"] = exitCode 531 result.append(entry) 532 533return result 534 535defp4Cmd(cmd): 536list=p4CmdList(cmd) 537 result = {} 538for entry inlist: 539 result.update(entry) 540return result; 541 542defp4Where(depotPath): 543if not depotPath.endswith("/"): 544 depotPath +="/" 545 depotPathLong = depotPath +"..." 546 outputList =p4CmdList(["where", depotPathLong]) 547 output =None 548for entry in outputList: 549if"depotFile"in entry: 550# Search for the base client side depot path, as long as it starts with the branch's P4 path. 551# The base path always ends with "/...". 552if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 553 output = entry 554break 555elif"data"in entry: 556 data = entry.get("data") 557 space = data.find(" ") 558if data[:space] == depotPath: 559 output = entry 560break 561if output ==None: 562return"" 563if output["code"] =="error": 564return"" 565 clientPath ="" 566if"path"in output: 567 clientPath = output.get("path") 568elif"data"in output: 569 data = output.get("data") 570 lastSpace = data.rfind(" ") 571 clientPath = data[lastSpace +1:] 572 573if clientPath.endswith("..."): 574 clientPath = clientPath[:-3] 575return clientPath 576 577defcurrentGitBranch(): 578 retcode =system(["git","symbolic-ref","-q","HEAD"], ignore_error=True) 579if retcode !=0: 580# on a detached head 581return None 582else: 583returnread_pipe(["git","name-rev","HEAD"]).split(" ")[1].strip() 584 585defisValidGitDir(path): 586returngit_dir(path) !=None 587 588defparseRevision(ref): 589returnread_pipe("git rev-parse%s"% ref).strip() 590 591defbranchExists(ref): 592 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 593 ignore_error=True) 594returnlen(rev) >0 595 596defextractLogMessageFromGitCommit(commit): 597 logMessage ="" 598 599## fixme: title is first line of commit, not 1st paragraph. 600 foundTitle =False 601for log inread_pipe_lines("git cat-file commit%s"% commit): 602if not foundTitle: 603iflen(log) ==1: 604 foundTitle =True 605continue 606 607 logMessage += log 608return logMessage 609 610defextractSettingsGitLog(log): 611 values = {} 612for line in log.split("\n"): 613 line = line.strip() 614 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 615if not m: 616continue 617 618 assignments = m.group(1).split(':') 619for a in assignments: 620 vals = a.split('=') 621 key = vals[0].strip() 622 val = ('='.join(vals[1:])).strip() 623if val.endswith('\"')and val.startswith('"'): 624 val = val[1:-1] 625 626 values[key] = val 627 628 paths = values.get("depot-paths") 629if not paths: 630 paths = values.get("depot-path") 631if paths: 632 values['depot-paths'] = paths.split(',') 633return values 634 635defgitBranchExists(branch): 636 proc = subprocess.Popen(["git","rev-parse", branch], 637 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 638return proc.wait() ==0; 639 640_gitConfig = {} 641 642defgitConfig(key, typeSpecifier=None): 643if not _gitConfig.has_key(key): 644 cmd = ["git","config"] 645if typeSpecifier: 646 cmd += [ typeSpecifier ] 647 cmd += [ key ] 648 s =read_pipe(cmd, ignore_error=True) 649 _gitConfig[key] = s.strip() 650return _gitConfig[key] 651 652defgitConfigBool(key): 653"""Return a bool, using git config --bool. It is True only if the 654 variable is set to true, and False if set to false or not present 655 in the config.""" 656 657if not _gitConfig.has_key(key): 658 _gitConfig[key] =gitConfig(key,'--bool') =="true" 659return _gitConfig[key] 660 661defgitConfigInt(key): 662if not _gitConfig.has_key(key): 663 cmd = ["git","config","--int", key ] 664 s =read_pipe(cmd, ignore_error=True) 665 v = s.strip() 666try: 667 _gitConfig[key] =int(gitConfig(key,'--int')) 668exceptValueError: 669 _gitConfig[key] =None 670return _gitConfig[key] 671 672defgitConfigList(key): 673if not _gitConfig.has_key(key): 674 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 675 _gitConfig[key] = s.strip().split(os.linesep) 676if _gitConfig[key] == ['']: 677 _gitConfig[key] = [] 678return _gitConfig[key] 679 680defp4BranchesInGit(branchesAreInRemotes=True): 681"""Find all the branches whose names start with "p4/", looking 682 in remotes or heads as specified by the argument. Return 683 a dictionary of{ branch: revision }for each one found. 684 The branch names are the short names, without any 685 "p4/" prefix.""" 686 687 branches = {} 688 689 cmdline ="git rev-parse --symbolic " 690if branchesAreInRemotes: 691 cmdline +="--remotes" 692else: 693 cmdline +="--branches" 694 695for line inread_pipe_lines(cmdline): 696 line = line.strip() 697 698# only import to p4/ 699if not line.startswith('p4/'): 700continue 701# special symbolic ref to p4/master 702if line =="p4/HEAD": 703continue 704 705# strip off p4/ prefix 706 branch = line[len("p4/"):] 707 708 branches[branch] =parseRevision(line) 709 710return branches 711 712defbranch_exists(branch): 713"""Make sure that the given ref name really exists.""" 714 715 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 716 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 717 out, _ = p.communicate() 718if p.returncode: 719return False 720# expect exactly one line of output: the branch name 721return out.rstrip() == branch 722 723deffindUpstreamBranchPoint(head ="HEAD"): 724 branches =p4BranchesInGit() 725# map from depot-path to branch name 726 branchByDepotPath = {} 727for branch in branches.keys(): 728 tip = branches[branch] 729 log =extractLogMessageFromGitCommit(tip) 730 settings =extractSettingsGitLog(log) 731if settings.has_key("depot-paths"): 732 paths =",".join(settings["depot-paths"]) 733 branchByDepotPath[paths] ="remotes/p4/"+ branch 734 735 settings =None 736 parent =0 737while parent <65535: 738 commit = head +"~%s"% parent 739 log =extractLogMessageFromGitCommit(commit) 740 settings =extractSettingsGitLog(log) 741if settings.has_key("depot-paths"): 742 paths =",".join(settings["depot-paths"]) 743if branchByDepotPath.has_key(paths): 744return[branchByDepotPath[paths], settings] 745 746 parent = parent +1 747 748return["", settings] 749 750defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 751if not silent: 752print("Creating/updating branch(es) in%sbased on origin branch(es)" 753% localRefPrefix) 754 755 originPrefix ="origin/p4/" 756 757for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 758 line = line.strip() 759if(not line.startswith(originPrefix))or line.endswith("HEAD"): 760continue 761 762 headName = line[len(originPrefix):] 763 remoteHead = localRefPrefix + headName 764 originHead = line 765 766 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 767if(not original.has_key('depot-paths') 768or not original.has_key('change')): 769continue 770 771 update =False 772if notgitBranchExists(remoteHead): 773if verbose: 774print"creating%s"% remoteHead 775 update =True 776else: 777 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 778if settings.has_key('change') >0: 779if settings['depot-paths'] == original['depot-paths']: 780 originP4Change =int(original['change']) 781 p4Change =int(settings['change']) 782if originP4Change > p4Change: 783print("%s(%s) is newer than%s(%s). " 784"Updating p4 branch from origin." 785% (originHead, originP4Change, 786 remoteHead, p4Change)) 787 update =True 788else: 789print("Ignoring:%swas imported from%swhile " 790"%swas imported from%s" 791% (originHead,','.join(original['depot-paths']), 792 remoteHead,','.join(settings['depot-paths']))) 793 794if update: 795system("git update-ref%s %s"% (remoteHead, originHead)) 796 797deforiginP4BranchesExist(): 798returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 799 800 801defp4ParseNumericChangeRange(parts): 802 changeStart =int(parts[0][1:]) 803if parts[1] =='#head': 804 changeEnd =p4_last_change() 805else: 806 changeEnd =int(parts[1]) 807 808return(changeStart, changeEnd) 809 810defchooseBlockSize(blockSize): 811if blockSize: 812return blockSize 813else: 814return defaultBlockSize 815 816defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 817assert depotPaths 818 819# Parse the change range into start and end. Try to find integer 820# revision ranges as these can be broken up into blocks to avoid 821# hitting server-side limits (maxrows, maxscanresults). But if 822# that doesn't work, fall back to using the raw revision specifier 823# strings, without using block mode. 824 825if changeRange is None or changeRange =='': 826 changeStart =1 827 changeEnd =p4_last_change() 828 block_size =chooseBlockSize(requestedBlockSize) 829else: 830 parts = changeRange.split(',') 831assertlen(parts) ==2 832try: 833(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 834 block_size =chooseBlockSize(requestedBlockSize) 835except: 836 changeStart = parts[0][1:] 837 changeEnd = parts[1] 838if requestedBlockSize: 839die("cannot use --changes-block-size with non-numeric revisions") 840 block_size =None 841 842 changes =set() 843 844# Retrieve changes a block at a time, to prevent running 845# into a MaxResults/MaxScanRows error from the server. 846 847while True: 848 cmd = ['changes'] 849 850if block_size: 851 end =min(changeEnd, changeStart + block_size) 852 revisionRange ="%d,%d"% (changeStart, end) 853else: 854 revisionRange ="%s,%s"% (changeStart, changeEnd) 855 856for p in depotPaths: 857 cmd += ["%s...@%s"% (p, revisionRange)] 858 859# Insert changes in chronological order 860for line inreversed(p4_read_pipe_lines(cmd)): 861 changes.add(int(line.split(" ")[1])) 862 863if not block_size: 864break 865 866if end >= changeEnd: 867break 868 869 changeStart = end +1 870 871 changes =sorted(changes) 872return changes 873 874defp4PathStartsWith(path, prefix): 875# This method tries to remedy a potential mixed-case issue: 876# 877# If UserA adds //depot/DirA/file1 878# and UserB adds //depot/dira/file2 879# 880# we may or may not have a problem. If you have core.ignorecase=true, 881# we treat DirA and dira as the same directory 882ifgitConfigBool("core.ignorecase"): 883return path.lower().startswith(prefix.lower()) 884return path.startswith(prefix) 885 886defgetClientSpec(): 887"""Look at the p4 client spec, create a View() object that contains 888 all the mappings, and return it.""" 889 890 specList =p4CmdList("client -o") 891iflen(specList) !=1: 892die('Output from "client -o" is%dlines, expecting 1'% 893len(specList)) 894 895# dictionary of all client parameters 896 entry = specList[0] 897 898# the //client/ name 899 client_name = entry["Client"] 900 901# just the keys that start with "View" 902 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 903 904# hold this new View 905 view =View(client_name) 906 907# append the lines, in order, to the view 908for view_num inrange(len(view_keys)): 909 k ="View%d"% view_num 910if k not in view_keys: 911die("Expected view key%smissing"% k) 912 view.append(entry[k]) 913 914return view 915 916defgetClientRoot(): 917"""Grab the client directory.""" 918 919 output =p4CmdList("client -o") 920iflen(output) !=1: 921die('Output from "client -o" is%dlines, expecting 1'%len(output)) 922 923 entry = output[0] 924if"Root"not in entry: 925die('Client has no "Root"') 926 927return entry["Root"] 928 929# 930# P4 wildcards are not allowed in filenames. P4 complains 931# if you simply add them, but you can force it with "-f", in 932# which case it translates them into %xx encoding internally. 933# 934defwildcard_decode(path): 935# Search for and fix just these four characters. Do % last so 936# that fixing it does not inadvertently create new %-escapes. 937# Cannot have * in a filename in windows; untested as to 938# what p4 would do in such a case. 939if not platform.system() =="Windows": 940 path = path.replace("%2A","*") 941 path = path.replace("%23","#") \ 942.replace("%40","@") \ 943.replace("%25","%") 944return path 945 946defwildcard_encode(path): 947# do % first to avoid double-encoding the %s introduced here 948 path = path.replace("%","%25") \ 949.replace("*","%2A") \ 950.replace("#","%23") \ 951.replace("@","%40") 952return path 953 954defwildcard_present(path): 955 m = re.search("[*#@%]", path) 956return m is not None 957 958classLargeFileSystem(object): 959"""Base class for large file system support.""" 960 961def__init__(self, writeToGitStream): 962 self.largeFiles =set() 963 self.writeToGitStream = writeToGitStream 964 965defgeneratePointer(self, cloneDestination, contentFile): 966"""Return the content of a pointer file that is stored in Git instead of 967 the actual content.""" 968assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 969 970defpushFile(self, localLargeFile): 971"""Push the actual content which is not stored in the Git repository to 972 a server.""" 973assert False,"Method 'pushFile' required in "+ self.__class__.__name__ 974 975defhasLargeFileExtension(self, relPath): 976returnreduce( 977lambda a, b: a or b, 978[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')], 979False 980) 981 982defgenerateTempFile(self, contents): 983 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 984for d in contents: 985 contentFile.write(d) 986 contentFile.close() 987return contentFile.name 988 989defexceedsLargeFileThreshold(self, relPath, contents): 990ifgitConfigInt('git-p4.largeFileThreshold'): 991 contentsSize =sum(len(d)for d in contents) 992if contentsSize >gitConfigInt('git-p4.largeFileThreshold'): 993return True 994ifgitConfigInt('git-p4.largeFileCompressedThreshold'): 995 contentsSize =sum(len(d)for d in contents) 996if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'): 997return False 998 contentTempFile = self.generateTempFile(contents) 999 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1000 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1001 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1002 zf.close()1003 compressedContentsSize = zf.infolist()[0].compress_size1004 os.remove(contentTempFile)1005 os.remove(compressedContentFile.name)1006if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1007return True1008return False10091010defaddLargeFile(self, relPath):1011 self.largeFiles.add(relPath)10121013defremoveLargeFile(self, relPath):1014 self.largeFiles.remove(relPath)10151016defisLargeFile(self, relPath):1017return relPath in self.largeFiles10181019defprocessContent(self, git_mode, relPath, contents):1020"""Processes the content of git fast import. This method decides if a1021 file is stored in the large file system and handles all necessary1022 steps."""1023if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1024 contentTempFile = self.generateTempFile(contents)1025(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1026if pointer_git_mode:1027 git_mode = pointer_git_mode1028if localLargeFile:1029# Move temp file to final location in large file system1030 largeFileDir = os.path.dirname(localLargeFile)1031if not os.path.isdir(largeFileDir):1032 os.makedirs(largeFileDir)1033 shutil.move(contentTempFile, localLargeFile)1034 self.addLargeFile(relPath)1035ifgitConfigBool('git-p4.largeFilePush'):1036 self.pushFile(localLargeFile)1037if verbose:1038 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1039return(git_mode, contents)10401041classMockLFS(LargeFileSystem):1042"""Mock large file system for testing."""10431044defgeneratePointer(self, contentFile):1045"""The pointer content is the original content prefixed with "pointer-".1046 The local filename of the large file storage is derived from the file content.1047 """1048withopen(contentFile,'r')as f:1049 content =next(f)1050 gitMode ='100644'1051 pointerContents ='pointer-'+ content1052 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1053return(gitMode, pointerContents, localLargeFile)10541055defpushFile(self, localLargeFile):1056"""The remote filename of the large file storage is the same as the local1057 one but in a different directory.1058 """1059 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1060if not os.path.exists(remotePath):1061 os.makedirs(remotePath)1062 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10631064classGitLFS(LargeFileSystem):1065"""Git LFS as backend for the git-p4 large file system.1066 See https://git-lfs.github.com/ for details."""10671068def__init__(self, *args):1069 LargeFileSystem.__init__(self, *args)1070 self.baseGitAttributes = []10711072defgeneratePointer(self, contentFile):1073"""Generate a Git LFS pointer for the content. Return LFS Pointer file1074 mode and content which is stored in the Git repository instead of1075 the actual content. Return also the new location of the actual1076 content.1077 """1078if os.path.getsize(contentFile) ==0:1079return(None,'',None)10801081 pointerProcess = subprocess.Popen(1082['git','lfs','pointer','--file='+ contentFile],1083 stdout=subprocess.PIPE1084)1085 pointerFile = pointerProcess.stdout.read()1086if pointerProcess.wait():1087 os.remove(contentFile)1088die('git-lfs pointer command failed. Did you install the extension?')10891090# Git LFS removed the preamble in the output of the 'pointer' command1091# starting from version 1.2.0. Check for the preamble here to support1092# earlier versions.1093# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431094if pointerFile.startswith('Git LFS pointer for'):1095 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)10961097 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1098 localLargeFile = os.path.join(1099 os.getcwd(),1100'.git','lfs','objects', oid[:2], oid[2:4],1101 oid,1102)1103# LFS Spec states that pointer files should not have the executable bit set.1104 gitMode ='100644'1105return(gitMode, pointerFile, localLargeFile)11061107defpushFile(self, localLargeFile):1108 uploadProcess = subprocess.Popen(1109['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1110)1111if uploadProcess.wait():1112die('git-lfs push command failed. Did you define a remote?')11131114defgenerateGitAttributes(self):1115return(1116 self.baseGitAttributes +1117[1118'\n',1119'#\n',1120'# Git LFS (see https://git-lfs.github.com/)\n',1121'#\n',1122] +1123['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1124for f insorted(gitConfigList('git-p4.largeFileExtensions'))1125] +1126['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1127for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1128]1129)11301131defaddLargeFile(self, relPath):1132 LargeFileSystem.addLargeFile(self, relPath)1133 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11341135defremoveLargeFile(self, relPath):1136 LargeFileSystem.removeLargeFile(self, relPath)1137 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11381139defprocessContent(self, git_mode, relPath, contents):1140if relPath =='.gitattributes':1141 self.baseGitAttributes = contents1142return(git_mode, self.generateGitAttributes())1143else:1144return LargeFileSystem.processContent(self, git_mode, relPath, contents)11451146class Command:1147def__init__(self):1148 self.usage ="usage: %prog [options]"1149 self.needsGit =True1150 self.verbose =False11511152class P4UserMap:1153def__init__(self):1154 self.userMapFromPerforceServer =False1155 self.myP4UserId =None11561157defp4UserId(self):1158if self.myP4UserId:1159return self.myP4UserId11601161 results =p4CmdList("user -o")1162for r in results:1163if r.has_key('User'):1164 self.myP4UserId = r['User']1165return r['User']1166die("Could not find your p4 user id")11671168defp4UserIsMe(self, p4User):1169# return True if the given p4 user is actually me1170 me = self.p4UserId()1171if not p4User or p4User != me:1172return False1173else:1174return True11751176defgetUserCacheFilename(self):1177 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1178return home +"/.gitp4-usercache.txt"11791180defgetUserMapFromPerforceServer(self):1181if self.userMapFromPerforceServer:1182return1183 self.users = {}1184 self.emails = {}11851186for output inp4CmdList("users"):1187if not output.has_key("User"):1188continue1189 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1190 self.emails[output["Email"]] = output["User"]11911192 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1193for mapUserConfig ingitConfigList("git-p4.mapUser"):1194 mapUser = mapUserConfigRegex.findall(mapUserConfig)1195if mapUser andlen(mapUser[0]) ==3:1196 user = mapUser[0][0]1197 fullname = mapUser[0][1]1198 email = mapUser[0][2]1199 self.users[user] = fullname +" <"+ email +">"1200 self.emails[email] = user12011202 s =''1203for(key, val)in self.users.items():1204 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12051206open(self.getUserCacheFilename(),"wb").write(s)1207 self.userMapFromPerforceServer =True12081209defloadUserMapFromCache(self):1210 self.users = {}1211 self.userMapFromPerforceServer =False1212try:1213 cache =open(self.getUserCacheFilename(),"rb")1214 lines = cache.readlines()1215 cache.close()1216for line in lines:1217 entry = line.strip().split("\t")1218 self.users[entry[0]] = entry[1]1219exceptIOError:1220 self.getUserMapFromPerforceServer()12211222classP4Debug(Command):1223def__init__(self):1224 Command.__init__(self)1225 self.options = []1226 self.description ="A tool to debug the output of p4 -G."1227 self.needsGit =False12281229defrun(self, args):1230 j =01231for output inp4CmdList(args):1232print'Element:%d'% j1233 j +=11234print output1235return True12361237classP4RollBack(Command):1238def__init__(self):1239 Command.__init__(self)1240 self.options = [1241 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1242]1243 self.description ="A tool to debug the multi-branch import. Don't use :)"1244 self.rollbackLocalBranches =False12451246defrun(self, args):1247iflen(args) !=1:1248return False1249 maxChange =int(args[0])12501251if"p4ExitCode"inp4Cmd("changes -m 1"):1252die("Problems executing p4");12531254if self.rollbackLocalBranches:1255 refPrefix ="refs/heads/"1256 lines =read_pipe_lines("git rev-parse --symbolic --branches")1257else:1258 refPrefix ="refs/remotes/"1259 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12601261for line in lines:1262if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1263 line = line.strip()1264 ref = refPrefix + line1265 log =extractLogMessageFromGitCommit(ref)1266 settings =extractSettingsGitLog(log)12671268 depotPaths = settings['depot-paths']1269 change = settings['change']12701271 changed =False12721273iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1274for p in depotPaths]))) ==0:1275print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1276system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1277continue12781279while change andint(change) > maxChange:1280 changed =True1281if self.verbose:1282print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1283system("git update-ref%s\"%s^\""% (ref, ref))1284 log =extractLogMessageFromGitCommit(ref)1285 settings =extractSettingsGitLog(log)128612871288 depotPaths = settings['depot-paths']1289 change = settings['change']12901291if changed:1292print"%srewound to%s"% (ref, change)12931294return True12951296classP4Submit(Command, P4UserMap):12971298 conflict_behavior_choices = ("ask","skip","quit")12991300def__init__(self):1301 Command.__init__(self)1302 P4UserMap.__init__(self)1303 self.options = [1304 optparse.make_option("--origin", dest="origin"),1305 optparse.make_option("-M", dest="detectRenames", action="store_true"),1306# preserve the user, requires relevant p4 permissions1307 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1308 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1309 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1310 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1311 optparse.make_option("--conflict", dest="conflict_behavior",1312 choices=self.conflict_behavior_choices),1313 optparse.make_option("--branch", dest="branch"),1314 optparse.make_option("--shelve", dest="shelve", action="store_true",1315help="Shelve instead of submit. Shelved files are reverted, "1316"restoring the workspace to the state before the shelve"),1317 optparse.make_option("--update-shelve", dest="update_shelve", action="store",type="int",1318 metavar="CHANGELIST",1319help="update an existing shelved changelist, implies --shelve")1320]1321 self.description ="Submit changes from git to the perforce depot."1322 self.usage +=" [name of git branch to submit into perforce depot]"1323 self.origin =""1324 self.detectRenames =False1325 self.preserveUser =gitConfigBool("git-p4.preserveUser")1326 self.dry_run =False1327 self.shelve =False1328 self.update_shelve =None1329 self.prepare_p4_only =False1330 self.conflict_behavior =None1331 self.isWindows = (platform.system() =="Windows")1332 self.exportLabels =False1333 self.p4HasMoveCommand =p4_has_move_command()1334 self.branch =None13351336ifgitConfig('git-p4.largeFileSystem'):1337die("Large file system not supported for git-p4 submit command. Please remove it from config.")13381339defcheck(self):1340iflen(p4CmdList("opened ...")) >0:1341die("You have files opened with perforce! Close them before starting the sync.")13421343defseparate_jobs_from_description(self, message):1344"""Extract and return a possible Jobs field in the commit1345 message. It goes into a separate section in the p4 change1346 specification.13471348 A jobs line starts with "Jobs:" and looks like a new field1349 in a form. Values are white-space separated on the same1350 line or on following lines that start with a tab.13511352 This does not parse and extract the full git commit message1353 like a p4 form. It just sees the Jobs: line as a marker1354 to pass everything from then on directly into the p4 form,1355 but outside the description section.13561357 Return a tuple (stripped log message, jobs string)."""13581359 m = re.search(r'^Jobs:', message, re.MULTILINE)1360if m is None:1361return(message,None)13621363 jobtext = message[m.start():]1364 stripped_message = message[:m.start()].rstrip()1365return(stripped_message, jobtext)13661367defprepareLogMessage(self, template, message, jobs):1368"""Edits the template returned from "p4 change -o" to insert1369 the message in the Description field, and the jobs text in1370 the Jobs field."""1371 result =""13721373 inDescriptionSection =False13741375for line in template.split("\n"):1376if line.startswith("#"):1377 result += line +"\n"1378continue13791380if inDescriptionSection:1381if line.startswith("Files:")or line.startswith("Jobs:"):1382 inDescriptionSection =False1383# insert Jobs section1384if jobs:1385 result += jobs +"\n"1386else:1387continue1388else:1389if line.startswith("Description:"):1390 inDescriptionSection =True1391 line +="\n"1392for messageLine in message.split("\n"):1393 line +="\t"+ messageLine +"\n"13941395 result += line +"\n"13961397return result13981399defpatchRCSKeywords(self,file, pattern):1400# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1401(handle, outFileName) = tempfile.mkstemp(dir='.')1402try:1403 outFile = os.fdopen(handle,"w+")1404 inFile =open(file,"r")1405 regexp = re.compile(pattern, re.VERBOSE)1406for line in inFile.readlines():1407 line = regexp.sub(r'$\1$', line)1408 outFile.write(line)1409 inFile.close()1410 outFile.close()1411# Forcibly overwrite the original file1412 os.unlink(file)1413 shutil.move(outFileName,file)1414except:1415# cleanup our temporary file1416 os.unlink(outFileName)1417print"Failed to strip RCS keywords in%s"%file1418raise14191420print"Patched up RCS keywords in%s"%file14211422defp4UserForCommit(self,id):1423# Return the tuple (perforce user,git email) for a given git commit id1424 self.getUserMapFromPerforceServer()1425 gitEmail =read_pipe(["git","log","--max-count=1",1426"--format=%ae",id])1427 gitEmail = gitEmail.strip()1428if not self.emails.has_key(gitEmail):1429return(None,gitEmail)1430else:1431return(self.emails[gitEmail],gitEmail)14321433defcheckValidP4Users(self,commits):1434# check if any git authors cannot be mapped to p4 users1435foridin commits:1436(user,email) = self.p4UserForCommit(id)1437if not user:1438 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1439ifgitConfigBool("git-p4.allowMissingP4Users"):1440print"%s"% msg1441else:1442die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14431444deflastP4Changelist(self):1445# Get back the last changelist number submitted in this client spec. This1446# then gets used to patch up the username in the change. If the same1447# client spec is being used by multiple processes then this might go1448# wrong.1449 results =p4CmdList("client -o")# find the current client1450 client =None1451for r in results:1452if r.has_key('Client'):1453 client = r['Client']1454break1455if not client:1456die("could not get client spec")1457 results =p4CmdList(["changes","-c", client,"-m","1"])1458for r in results:1459if r.has_key('change'):1460return r['change']1461die("Could not get changelist number for last submit - cannot patch up user details")14621463defmodifyChangelistUser(self, changelist, newUser):1464# fixup the user field of a changelist after it has been submitted.1465 changes =p4CmdList("change -o%s"% changelist)1466iflen(changes) !=1:1467die("Bad output from p4 change modifying%sto user%s"%1468(changelist, newUser))14691470 c = changes[0]1471if c['User'] == newUser:return# nothing to do1472 c['User'] = newUser1473input= marshal.dumps(c)14741475 result =p4CmdList("change -f -i", stdin=input)1476for r in result:1477if r.has_key('code'):1478if r['code'] =='error':1479die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1480if r.has_key('data'):1481print("Updated user field for changelist%sto%s"% (changelist, newUser))1482return1483die("Could not modify user field of changelist%sto%s"% (changelist, newUser))14841485defcanChangeChangelists(self):1486# check to see if we have p4 admin or super-user permissions, either of1487# which are required to modify changelists.1488 results =p4CmdList(["protects", self.depotPath])1489for r in results:1490if r.has_key('perm'):1491if r['perm'] =='admin':1492return11493if r['perm'] =='super':1494return11495return014961497defprepareSubmitTemplate(self, changelist=None):1498"""Run "p4 change -o" to grab a change specification template.1499 This does not use "p4 -G", as it is nice to keep the submission1500 template in original order, since a human might edit it.15011502 Remove lines in the Files section that show changes to files1503 outside the depot path we're committing into."""15041505[upstream, settings] =findUpstreamBranchPoint()15061507 template =""1508 inFilesSection =False1509 args = ['change','-o']1510if changelist:1511 args.append(str(changelist))15121513for line inp4_read_pipe_lines(args):1514if line.endswith("\r\n"):1515 line = line[:-2] +"\n"1516if inFilesSection:1517if line.startswith("\t"):1518# path starts and ends with a tab1519 path = line[1:]1520 lastTab = path.rfind("\t")1521if lastTab != -1:1522 path = path[:lastTab]1523if settings.has_key('depot-paths'):1524if not[p for p in settings['depot-paths']1525ifp4PathStartsWith(path, p)]:1526continue1527else:1528if notp4PathStartsWith(path, self.depotPath):1529continue1530else:1531 inFilesSection =False1532else:1533if line.startswith("Files:"):1534 inFilesSection =True15351536 template += line15371538return template15391540defedit_template(self, template_file):1541"""Invoke the editor to let the user change the submission1542 message. Return true if okay to continue with the submit."""15431544# if configured to skip the editing part, just submit1545ifgitConfigBool("git-p4.skipSubmitEdit"):1546return True15471548# look at the modification time, to check later if the user saved1549# the file1550 mtime = os.stat(template_file).st_mtime15511552# invoke the editor1553if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1554 editor = os.environ.get("P4EDITOR")1555else:1556 editor =read_pipe("git var GIT_EDITOR").strip()1557system(["sh","-c", ('%s"$@"'% editor), editor, template_file])15581559# If the file was not saved, prompt to see if this patch should1560# be skipped. But skip this verification step if configured so.1561ifgitConfigBool("git-p4.skipSubmitEditCheck"):1562return True15631564# modification time updated means user saved the file1565if os.stat(template_file).st_mtime > mtime:1566return True15671568while True:1569 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1570if response =='y':1571return True1572if response =='n':1573return False15741575defget_diff_description(self, editedFiles, filesToAdd, symlinks):1576# diff1577if os.environ.has_key("P4DIFF"):1578del(os.environ["P4DIFF"])1579 diff =""1580for editedFile in editedFiles:1581 diff +=p4_read_pipe(['diff','-du',1582wildcard_encode(editedFile)])15831584# new file diff1585 newdiff =""1586for newFile in filesToAdd:1587 newdiff +="==== new file ====\n"1588 newdiff +="--- /dev/null\n"1589 newdiff +="+++%s\n"% newFile15901591 is_link = os.path.islink(newFile)1592 expect_link = newFile in symlinks15931594if is_link and expect_link:1595 newdiff +="+%s\n"% os.readlink(newFile)1596else:1597 f =open(newFile,"r")1598for line in f.readlines():1599 newdiff +="+"+ line1600 f.close()16011602return(diff + newdiff).replace('\r\n','\n')16031604defapplyCommit(self,id):1605"""Apply one commit, return True if it succeeded."""16061607print"Applying",read_pipe(["git","show","-s",1608"--format=format:%h%s",id])16091610(p4User, gitEmail) = self.p4UserForCommit(id)16111612 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1613 filesToAdd =set()1614 filesToChangeType =set()1615 filesToDelete =set()1616 editedFiles =set()1617 pureRenameCopy =set()1618 symlinks =set()1619 filesToChangeExecBit = {}1620 all_files =list()16211622for line in diff:1623 diff =parseDiffTreeEntry(line)1624 modifier = diff['status']1625 path = diff['src']1626 all_files.append(path)16271628if modifier =="M":1629p4_edit(path)1630ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1631 filesToChangeExecBit[path] = diff['dst_mode']1632 editedFiles.add(path)1633elif modifier =="A":1634 filesToAdd.add(path)1635 filesToChangeExecBit[path] = diff['dst_mode']1636if path in filesToDelete:1637 filesToDelete.remove(path)16381639 dst_mode =int(diff['dst_mode'],8)1640if dst_mode ==0120000:1641 symlinks.add(path)16421643elif modifier =="D":1644 filesToDelete.add(path)1645if path in filesToAdd:1646 filesToAdd.remove(path)1647elif modifier =="C":1648 src, dest = diff['src'], diff['dst']1649p4_integrate(src, dest)1650 pureRenameCopy.add(dest)1651if diff['src_sha1'] != diff['dst_sha1']:1652p4_edit(dest)1653 pureRenameCopy.discard(dest)1654ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1655p4_edit(dest)1656 pureRenameCopy.discard(dest)1657 filesToChangeExecBit[dest] = diff['dst_mode']1658if self.isWindows:1659# turn off read-only attribute1660 os.chmod(dest, stat.S_IWRITE)1661 os.unlink(dest)1662 editedFiles.add(dest)1663elif modifier =="R":1664 src, dest = diff['src'], diff['dst']1665if self.p4HasMoveCommand:1666p4_edit(src)# src must be open before move1667p4_move(src, dest)# opens for (move/delete, move/add)1668else:1669p4_integrate(src, dest)1670if diff['src_sha1'] != diff['dst_sha1']:1671p4_edit(dest)1672else:1673 pureRenameCopy.add(dest)1674ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1675if not self.p4HasMoveCommand:1676p4_edit(dest)# with move: already open, writable1677 filesToChangeExecBit[dest] = diff['dst_mode']1678if not self.p4HasMoveCommand:1679if self.isWindows:1680 os.chmod(dest, stat.S_IWRITE)1681 os.unlink(dest)1682 filesToDelete.add(src)1683 editedFiles.add(dest)1684elif modifier =="T":1685 filesToChangeType.add(path)1686else:1687die("unknown modifier%sfor%s"% (modifier, path))16881689 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1690 patchcmd = diffcmd +" | git apply "1691 tryPatchCmd = patchcmd +"--check -"1692 applyPatchCmd = patchcmd +"--check --apply -"1693 patch_succeeded =True16941695if os.system(tryPatchCmd) !=0:1696 fixed_rcs_keywords =False1697 patch_succeeded =False1698print"Unfortunately applying the change failed!"16991700# Patch failed, maybe it's just RCS keyword woes. Look through1701# the patch to see if that's possible.1702ifgitConfigBool("git-p4.attemptRCSCleanup"):1703file=None1704 pattern =None1705 kwfiles = {}1706forfilein editedFiles | filesToDelete:1707# did this file's delta contain RCS keywords?1708 pattern =p4_keywords_regexp_for_file(file)17091710if pattern:1711# this file is a possibility...look for RCS keywords.1712 regexp = re.compile(pattern, re.VERBOSE)1713for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1714if regexp.search(line):1715if verbose:1716print"got keyword match on%sin%sin%s"% (pattern, line,file)1717 kwfiles[file] = pattern1718break17191720forfilein kwfiles:1721if verbose:1722print"zapping%swith%s"% (line,pattern)1723# File is being deleted, so not open in p4. Must1724# disable the read-only bit on windows.1725if self.isWindows andfilenot in editedFiles:1726 os.chmod(file, stat.S_IWRITE)1727 self.patchRCSKeywords(file, kwfiles[file])1728 fixed_rcs_keywords =True17291730if fixed_rcs_keywords:1731print"Retrying the patch with RCS keywords cleaned up"1732if os.system(tryPatchCmd) ==0:1733 patch_succeeded =True17341735if not patch_succeeded:1736for f in editedFiles:1737p4_revert(f)1738return False17391740#1741# Apply the patch for real, and do add/delete/+x handling.1742#1743system(applyPatchCmd)17441745for f in filesToChangeType:1746p4_edit(f,"-t","auto")1747for f in filesToAdd:1748p4_add(f)1749for f in filesToDelete:1750p4_revert(f)1751p4_delete(f)17521753# Set/clear executable bits1754for f in filesToChangeExecBit.keys():1755 mode = filesToChangeExecBit[f]1756setP4ExecBit(f, mode)17571758if self.update_shelve:1759print("all_files =%s"%str(all_files))1760p4_reopen_in_change(self.update_shelve, all_files)17611762#1763# Build p4 change description, starting with the contents1764# of the git commit message.1765#1766 logMessage =extractLogMessageFromGitCommit(id)1767 logMessage = logMessage.strip()1768(logMessage, jobs) = self.separate_jobs_from_description(logMessage)17691770 template = self.prepareSubmitTemplate(self.update_shelve)1771 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)17721773if self.preserveUser:1774 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User17751776if self.checkAuthorship and not self.p4UserIsMe(p4User):1777 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1778 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1779 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"17801781 separatorLine ="######## everything below this line is just the diff #######\n"1782if not self.prepare_p4_only:1783 submitTemplate += separatorLine1784 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)17851786(handle, fileName) = tempfile.mkstemp()1787 tmpFile = os.fdopen(handle,"w+b")1788if self.isWindows:1789 submitTemplate = submitTemplate.replace("\n","\r\n")1790 tmpFile.write(submitTemplate)1791 tmpFile.close()17921793if self.prepare_p4_only:1794#1795# Leave the p4 tree prepared, and the submit template around1796# and let the user decide what to do next1797#1798print1799print"P4 workspace prepared for submission."1800print"To submit or revert, go to client workspace"1801print" "+ self.clientPath1802print1803print"To submit, use\"p4 submit\"to write a new description,"1804print"or\"p4 submit -i <%s\"to use the one prepared by" \1805"\"git p4\"."% fileName1806print"You can delete the file\"%s\"when finished."% fileName18071808if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1809print"To preserve change ownership by user%s, you must\n" \1810"do\"p4 change -f <change>\"after submitting and\n" \1811"edit the User field."1812if pureRenameCopy:1813print"After submitting, renamed files must be re-synced."1814print"Invoke\"p4 sync -f\"on each of these files:"1815for f in pureRenameCopy:1816print" "+ f18171818print1819print"To revert the changes, use\"p4 revert ...\", and delete"1820print"the submit template file\"%s\""% fileName1821if filesToAdd:1822print"Since the commit adds new files, they must be deleted:"1823for f in filesToAdd:1824print" "+ f1825print1826return True18271828#1829# Let the user edit the change description, then submit it.1830#1831 submitted =False18321833try:1834if self.edit_template(fileName):1835# read the edited message and submit1836 tmpFile =open(fileName,"rb")1837 message = tmpFile.read()1838 tmpFile.close()1839if self.isWindows:1840 message = message.replace("\r\n","\n")1841 submitTemplate = message[:message.index(separatorLine)]18421843if self.update_shelve:1844p4_write_pipe(['shelve','-r','-i'], submitTemplate)1845elif self.shelve:1846p4_write_pipe(['shelve','-i'], submitTemplate)1847else:1848p4_write_pipe(['submit','-i'], submitTemplate)1849# The rename/copy happened by applying a patch that created a1850# new file. This leaves it writable, which confuses p4.1851for f in pureRenameCopy:1852p4_sync(f,"-f")18531854if self.preserveUser:1855if p4User:1856# Get last changelist number. Cannot easily get it from1857# the submit command output as the output is1858# unmarshalled.1859 changelist = self.lastP4Changelist()1860 self.modifyChangelistUser(changelist, p4User)18611862 submitted =True18631864finally:1865# skip this patch1866if not submitted or self.shelve:1867if self.shelve:1868print("Reverting shelved files.")1869else:1870print("Submission cancelled, undoing p4 changes.")1871for f in editedFiles | filesToDelete:1872p4_revert(f)1873for f in filesToAdd:1874p4_revert(f)1875 os.remove(f)18761877 os.remove(fileName)1878return submitted18791880# Export git tags as p4 labels. Create a p4 label and then tag1881# with that.1882defexportGitTags(self, gitTags):1883 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1884iflen(validLabelRegexp) ==0:1885 validLabelRegexp = defaultLabelRegexp1886 m = re.compile(validLabelRegexp)18871888for name in gitTags:18891890if not m.match(name):1891if verbose:1892print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1893continue18941895# Get the p4 commit this corresponds to1896 logMessage =extractLogMessageFromGitCommit(name)1897 values =extractSettingsGitLog(logMessage)18981899if not values.has_key('change'):1900# a tag pointing to something not sent to p4; ignore1901if verbose:1902print"git tag%sdoes not give a p4 commit"% name1903continue1904else:1905 changelist = values['change']19061907# Get the tag details.1908 inHeader =True1909 isAnnotated =False1910 body = []1911for l inread_pipe_lines(["git","cat-file","-p", name]):1912 l = l.strip()1913if inHeader:1914if re.match(r'tag\s+', l):1915 isAnnotated =True1916elif re.match(r'\s*$', l):1917 inHeader =False1918continue1919else:1920 body.append(l)19211922if not isAnnotated:1923 body = ["lightweight tag imported by git p4\n"]19241925# Create the label - use the same view as the client spec we are using1926 clientSpec =getClientSpec()19271928 labelTemplate ="Label:%s\n"% name1929 labelTemplate +="Description:\n"1930for b in body:1931 labelTemplate +="\t"+ b +"\n"1932 labelTemplate +="View:\n"1933for depot_side in clientSpec.mappings:1934 labelTemplate +="\t%s\n"% depot_side19351936if self.dry_run:1937print"Would create p4 label%sfor tag"% name1938elif self.prepare_p4_only:1939print"Not creating p4 label%sfor tag due to option" \1940" --prepare-p4-only"% name1941else:1942p4_write_pipe(["label","-i"], labelTemplate)19431944# Use the label1945p4_system(["tag","-l", name] +1946["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])19471948if verbose:1949print"created p4 label for tag%s"% name19501951defrun(self, args):1952iflen(args) ==0:1953 self.master =currentGitBranch()1954eliflen(args) ==1:1955 self.master = args[0]1956if notbranchExists(self.master):1957die("Branch%sdoes not exist"% self.master)1958else:1959return False19601961if self.master:1962 allowSubmit =gitConfig("git-p4.allowSubmit")1963iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1964die("%sis not in git-p4.allowSubmit"% self.master)19651966[upstream, settings] =findUpstreamBranchPoint()1967 self.depotPath = settings['depot-paths'][0]1968iflen(self.origin) ==0:1969 self.origin = upstream19701971if self.update_shelve:1972 self.shelve =True19731974if self.preserveUser:1975if not self.canChangeChangelists():1976die("Cannot preserve user names without p4 super-user or admin permissions")19771978# if not set from the command line, try the config file1979if self.conflict_behavior is None:1980 val =gitConfig("git-p4.conflict")1981if val:1982if val not in self.conflict_behavior_choices:1983die("Invalid value '%s' for config git-p4.conflict"% val)1984else:1985 val ="ask"1986 self.conflict_behavior = val19871988if self.verbose:1989print"Origin branch is "+ self.origin19901991iflen(self.depotPath) ==0:1992print"Internal error: cannot locate perforce depot path from existing branches"1993 sys.exit(128)19941995 self.useClientSpec =False1996ifgitConfigBool("git-p4.useclientspec"):1997 self.useClientSpec =True1998if self.useClientSpec:1999 self.clientSpecDirs =getClientSpec()20002001# Check for the existence of P4 branches2002 branchesDetected = (len(p4BranchesInGit().keys()) >1)20032004if self.useClientSpec and not branchesDetected:2005# all files are relative to the client spec2006 self.clientPath =getClientRoot()2007else:2008 self.clientPath =p4Where(self.depotPath)20092010if self.clientPath =="":2011die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)20122013print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2014 self.oldWorkingDirectory = os.getcwd()20152016# ensure the clientPath exists2017 new_client_dir =False2018if not os.path.exists(self.clientPath):2019 new_client_dir =True2020 os.makedirs(self.clientPath)20212022chdir(self.clientPath, is_client_path=True)2023if self.dry_run:2024print"Would synchronize p4 checkout in%s"% self.clientPath2025else:2026print"Synchronizing p4 checkout..."2027if new_client_dir:2028# old one was destroyed, and maybe nobody told p42029p4_sync("...","-f")2030else:2031p4_sync("...")2032 self.check()20332034 commits = []2035if self.master:2036 commitish = self.master2037else:2038 commitish ='HEAD'20392040for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2041 commits.append(line.strip())2042 commits.reverse()20432044if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2045 self.checkAuthorship =False2046else:2047 self.checkAuthorship =True20482049if self.preserveUser:2050 self.checkValidP4Users(commits)20512052#2053# Build up a set of options to be passed to diff when2054# submitting each commit to p4.2055#2056if self.detectRenames:2057# command-line -M arg2058 self.diffOpts ="-M"2059else:2060# If not explicitly set check the config variable2061 detectRenames =gitConfig("git-p4.detectRenames")20622063if detectRenames.lower() =="false"or detectRenames =="":2064 self.diffOpts =""2065elif detectRenames.lower() =="true":2066 self.diffOpts ="-M"2067else:2068 self.diffOpts ="-M%s"% detectRenames20692070# no command-line arg for -C or --find-copies-harder, just2071# config variables2072 detectCopies =gitConfig("git-p4.detectCopies")2073if detectCopies.lower() =="false"or detectCopies =="":2074pass2075elif detectCopies.lower() =="true":2076 self.diffOpts +=" -C"2077else:2078 self.diffOpts +=" -C%s"% detectCopies20792080ifgitConfigBool("git-p4.detectCopiesHarder"):2081 self.diffOpts +=" --find-copies-harder"20822083#2084# Apply the commits, one at a time. On failure, ask if should2085# continue to try the rest of the patches, or quit.2086#2087if self.dry_run:2088print"Would apply"2089 applied = []2090 last =len(commits) -12091for i, commit inenumerate(commits):2092if self.dry_run:2093print" ",read_pipe(["git","show","-s",2094"--format=format:%h%s", commit])2095 ok =True2096else:2097 ok = self.applyCommit(commit)2098if ok:2099 applied.append(commit)2100else:2101if self.prepare_p4_only and i < last:2102print"Processing only the first commit due to option" \2103" --prepare-p4-only"2104break2105if i < last:2106 quit =False2107while True:2108# prompt for what to do, or use the option/variable2109if self.conflict_behavior =="ask":2110print"What do you want to do?"2111 response =raw_input("[s]kip this commit but apply"2112" the rest, or [q]uit? ")2113if not response:2114continue2115elif self.conflict_behavior =="skip":2116 response ="s"2117elif self.conflict_behavior =="quit":2118 response ="q"2119else:2120die("Unknown conflict_behavior '%s'"%2121 self.conflict_behavior)21222123if response[0] =="s":2124print"Skipping this commit, but applying the rest"2125break2126if response[0] =="q":2127print"Quitting"2128 quit =True2129break2130if quit:2131break21322133chdir(self.oldWorkingDirectory)2134 shelved_applied ="shelved"if self.shelve else"applied"2135if self.dry_run:2136pass2137elif self.prepare_p4_only:2138pass2139eliflen(commits) ==len(applied):2140print("All commits{0}!".format(shelved_applied))21412142 sync =P4Sync()2143if self.branch:2144 sync.branch = self.branch2145 sync.run([])21462147 rebase =P4Rebase()2148 rebase.rebase()21492150else:2151iflen(applied) ==0:2152print("No commits{0}.".format(shelved_applied))2153else:2154print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2155for c in commits:2156if c in applied:2157 star ="*"2158else:2159 star =" "2160print star,read_pipe(["git","show","-s",2161"--format=format:%h%s", c])2162print"You will have to do 'git p4 sync' and rebase."21632164ifgitConfigBool("git-p4.exportLabels"):2165 self.exportLabels =True21662167if self.exportLabels:2168 p4Labels =getP4Labels(self.depotPath)2169 gitTags =getGitTags()21702171 missingGitTags = gitTags - p4Labels2172 self.exportGitTags(missingGitTags)21732174# exit with error unless everything applied perfectly2175iflen(commits) !=len(applied):2176 sys.exit(1)21772178return True21792180classView(object):2181"""Represent a p4 view ("p4 help views"), and map files in a2182 repo according to the view."""21832184def__init__(self, client_name):2185 self.mappings = []2186 self.client_prefix ="//%s/"% client_name2187# cache results of "p4 where" to lookup client file locations2188 self.client_spec_path_cache = {}21892190defappend(self, view_line):2191"""Parse a view line, splitting it into depot and client2192 sides. Append to self.mappings, preserving order. This2193 is only needed for tag creation."""21942195# Split the view line into exactly two words. P4 enforces2196# structure on these lines that simplifies this quite a bit.2197#2198# Either or both words may be double-quoted.2199# Single quotes do not matter.2200# Double-quote marks cannot occur inside the words.2201# A + or - prefix is also inside the quotes.2202# There are no quotes unless they contain a space.2203# The line is already white-space stripped.2204# The two words are separated by a single space.2205#2206if view_line[0] =='"':2207# First word is double quoted. Find its end.2208 close_quote_index = view_line.find('"',1)2209if close_quote_index <=0:2210die("No first-word closing quote found:%s"% view_line)2211 depot_side = view_line[1:close_quote_index]2212# skip closing quote and space2213 rhs_index = close_quote_index +1+12214else:2215 space_index = view_line.find(" ")2216if space_index <=0:2217die("No word-splitting space found:%s"% view_line)2218 depot_side = view_line[0:space_index]2219 rhs_index = space_index +122202221# prefix + means overlay on previous mapping2222if depot_side.startswith("+"):2223 depot_side = depot_side[1:]22242225# prefix - means exclude this path, leave out of mappings2226 exclude =False2227if depot_side.startswith("-"):2228 exclude =True2229 depot_side = depot_side[1:]22302231if not exclude:2232 self.mappings.append(depot_side)22332234defconvert_client_path(self, clientFile):2235# chop off //client/ part to make it relative2236if not clientFile.startswith(self.client_prefix):2237die("No prefix '%s' on clientFile '%s'"%2238(self.client_prefix, clientFile))2239return clientFile[len(self.client_prefix):]22402241defupdate_client_spec_path_cache(self, files):2242""" Caching file paths by "p4 where" batch query """22432244# List depot file paths exclude that already cached2245 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]22462247iflen(fileArgs) ==0:2248return# All files in cache22492250 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2251for res in where_result:2252if"code"in res and res["code"] =="error":2253# assume error is "... file(s) not in client view"2254continue2255if"clientFile"not in res:2256die("No clientFile in 'p4 where' output")2257if"unmap"in res:2258# it will list all of them, but only one not unmap-ped2259continue2260ifgitConfigBool("core.ignorecase"):2261 res['depotFile'] = res['depotFile'].lower()2262 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])22632264# not found files or unmap files set to ""2265for depotFile in fileArgs:2266ifgitConfigBool("core.ignorecase"):2267 depotFile = depotFile.lower()2268if depotFile not in self.client_spec_path_cache:2269 self.client_spec_path_cache[depotFile] =""22702271defmap_in_client(self, depot_path):2272"""Return the relative location in the client where this2273 depot file should live. Returns "" if the file should2274 not be mapped in the client."""22752276ifgitConfigBool("core.ignorecase"):2277 depot_path = depot_path.lower()22782279if depot_path in self.client_spec_path_cache:2280return self.client_spec_path_cache[depot_path]22812282die("Error:%sis not found in client spec path"% depot_path )2283return""22842285classP4Sync(Command, P4UserMap):2286 delete_actions = ("delete","move/delete","purge")22872288def__init__(self):2289 Command.__init__(self)2290 P4UserMap.__init__(self)2291 self.options = [2292 optparse.make_option("--branch", dest="branch"),2293 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2294 optparse.make_option("--changesfile", dest="changesFile"),2295 optparse.make_option("--silent", dest="silent", action="store_true"),2296 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2297 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2298 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2299help="Import into refs/heads/ , not refs/remotes"),2300 optparse.make_option("--max-changes", dest="maxChanges",2301help="Maximum number of changes to import"),2302 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2303help="Internal block size to use when iteratively calling p4 changes"),2304 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2305help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2306 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2307help="Only sync files that are included in the Perforce Client Spec"),2308 optparse.make_option("-/", dest="cloneExclude",2309 action="append",type="string",2310help="exclude depot path"),2311]2312 self.description ="""Imports from Perforce into a git repository.\n2313 example:2314 //depot/my/project/ -- to import the current head2315 //depot/my/project/@all -- to import everything2316 //depot/my/project/@1,6 -- to import only from revision 1 to 623172318 (a ... is not needed in the path p4 specification, it's added implicitly)"""23192320 self.usage +=" //depot/path[@revRange]"2321 self.silent =False2322 self.createdBranches =set()2323 self.committedChanges =set()2324 self.branch =""2325 self.detectBranches =False2326 self.detectLabels =False2327 self.importLabels =False2328 self.changesFile =""2329 self.syncWithOrigin =True2330 self.importIntoRemotes =True2331 self.maxChanges =""2332 self.changes_block_size =None2333 self.keepRepoPath =False2334 self.depotPaths =None2335 self.p4BranchesInGit = []2336 self.cloneExclude = []2337 self.useClientSpec =False2338 self.useClientSpec_from_options =False2339 self.clientSpecDirs =None2340 self.tempBranches = []2341 self.tempBranchLocation ="refs/git-p4-tmp"2342 self.largeFileSystem =None23432344ifgitConfig('git-p4.largeFileSystem'):2345 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2346 self.largeFileSystem =largeFileSystemConstructor(2347lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2348)23492350ifgitConfig("git-p4.syncFromOrigin") =="false":2351 self.syncWithOrigin =False23522353# This is required for the "append" cloneExclude action2354defensure_value(self, attr, value):2355if nothasattr(self, attr)orgetattr(self, attr)is None:2356setattr(self, attr, value)2357returngetattr(self, attr)23582359# Force a checkpoint in fast-import and wait for it to finish2360defcheckpoint(self):2361 self.gitStream.write("checkpoint\n\n")2362 self.gitStream.write("progress checkpoint\n\n")2363 out = self.gitOutput.readline()2364if self.verbose:2365print"checkpoint finished: "+ out23662367defextractFilesFromCommit(self, commit):2368 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2369for path in self.cloneExclude]2370 files = []2371 fnum =02372while commit.has_key("depotFile%s"% fnum):2373 path = commit["depotFile%s"% fnum]23742375if[p for p in self.cloneExclude2376ifp4PathStartsWith(path, p)]:2377 found =False2378else:2379 found = [p for p in self.depotPaths2380ifp4PathStartsWith(path, p)]2381if not found:2382 fnum = fnum +12383continue23842385file= {}2386file["path"] = path2387file["rev"] = commit["rev%s"% fnum]2388file["action"] = commit["action%s"% fnum]2389file["type"] = commit["type%s"% fnum]2390 files.append(file)2391 fnum = fnum +12392return files23932394defextractJobsFromCommit(self, commit):2395 jobs = []2396 jnum =02397while commit.has_key("job%s"% jnum):2398 job = commit["job%s"% jnum]2399 jobs.append(job)2400 jnum = jnum +12401return jobs24022403defstripRepoPath(self, path, prefixes):2404"""When streaming files, this is called to map a p4 depot path2405 to where it should go in git. The prefixes are either2406 self.depotPaths, or self.branchPrefixes in the case of2407 branch detection."""24082409if self.useClientSpec:2410# branch detection moves files up a level (the branch name)2411# from what client spec interpretation gives2412 path = self.clientSpecDirs.map_in_client(path)2413if self.detectBranches:2414for b in self.knownBranches:2415if path.startswith(b +"/"):2416 path = path[len(b)+1:]24172418elif self.keepRepoPath:2419# Preserve everything in relative path name except leading2420# //depot/; just look at first prefix as they all should2421# be in the same depot.2422 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2423ifp4PathStartsWith(path, depot):2424 path = path[len(depot):]24252426else:2427for p in prefixes:2428ifp4PathStartsWith(path, p):2429 path = path[len(p):]2430break24312432 path =wildcard_decode(path)2433return path24342435defsplitFilesIntoBranches(self, commit):2436"""Look at each depotFile in the commit to figure out to what2437 branch it belongs."""24382439if self.clientSpecDirs:2440 files = self.extractFilesFromCommit(commit)2441 self.clientSpecDirs.update_client_spec_path_cache(files)24422443 branches = {}2444 fnum =02445while commit.has_key("depotFile%s"% fnum):2446 path = commit["depotFile%s"% fnum]2447 found = [p for p in self.depotPaths2448ifp4PathStartsWith(path, p)]2449if not found:2450 fnum = fnum +12451continue24522453file= {}2454file["path"] = path2455file["rev"] = commit["rev%s"% fnum]2456file["action"] = commit["action%s"% fnum]2457file["type"] = commit["type%s"% fnum]2458 fnum = fnum +124592460# start with the full relative path where this file would2461# go in a p4 client2462if self.useClientSpec:2463 relPath = self.clientSpecDirs.map_in_client(path)2464else:2465 relPath = self.stripRepoPath(path, self.depotPaths)24662467for branch in self.knownBranches.keys():2468# add a trailing slash so that a commit into qt/4.2foo2469# doesn't end up in qt/4.2, e.g.2470if relPath.startswith(branch +"/"):2471if branch not in branches:2472 branches[branch] = []2473 branches[branch].append(file)2474break24752476return branches24772478defwriteToGitStream(self, gitMode, relPath, contents):2479 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2480 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2481for d in contents:2482 self.gitStream.write(d)2483 self.gitStream.write('\n')24842485# output one file from the P4 stream2486# - helper for streamP4Files24872488defstreamOneP4File(self,file, contents):2489 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2490if verbose:2491 size =int(self.stream_file['fileSize'])2492 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2493 sys.stdout.flush()24942495(type_base, type_mods) =split_p4_type(file["type"])24962497 git_mode ="100644"2498if"x"in type_mods:2499 git_mode ="100755"2500if type_base =="symlink":2501 git_mode ="120000"2502# p4 print on a symlink sometimes contains "target\n";2503# if it does, remove the newline2504 data =''.join(contents)2505if not data:2506# Some version of p4 allowed creating a symlink that pointed2507# to nothing. This causes p4 errors when checking out such2508# a change, and errors here too. Work around it by ignoring2509# the bad symlink; hopefully a future change fixes it.2510print"\nIgnoring empty symlink in%s"%file['depotFile']2511return2512elif data[-1] =='\n':2513 contents = [data[:-1]]2514else:2515 contents = [data]25162517if type_base =="utf16":2518# p4 delivers different text in the python output to -G2519# than it does when using "print -o", or normal p4 client2520# operations. utf16 is converted to ascii or utf8, perhaps.2521# But ascii text saved as -t utf16 is completely mangled.2522# Invoke print -o to get the real contents.2523#2524# On windows, the newlines will always be mangled by print, so put2525# them back too. This is not needed to the cygwin windows version,2526# just the native "NT" type.2527#2528try:2529 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2530exceptExceptionas e:2531if'Translation of file content failed'instr(e):2532 type_base ='binary'2533else:2534raise e2535else:2536ifp4_version_string().find('/NT') >=0:2537 text = text.replace('\r\n','\n')2538 contents = [ text ]25392540if type_base =="apple":2541# Apple filetype files will be streamed as a concatenation of2542# its appledouble header and the contents. This is useless2543# on both macs and non-macs. If using "print -q -o xx", it2544# will create "xx" with the data, and "%xx" with the header.2545# This is also not very useful.2546#2547# Ideally, someday, this script can learn how to generate2548# appledouble files directly and import those to git, but2549# non-mac machines can never find a use for apple filetype.2550print"\nIgnoring apple filetype file%s"%file['depotFile']2551return25522553# Note that we do not try to de-mangle keywords on utf16 files,2554# even though in theory somebody may want that.2555 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2556if pattern:2557 regexp = re.compile(pattern, re.VERBOSE)2558 text =''.join(contents)2559 text = regexp.sub(r'$\1$', text)2560 contents = [ text ]25612562try:2563 relPath.decode('ascii')2564except:2565 encoding ='utf8'2566ifgitConfig('git-p4.pathEncoding'):2567 encoding =gitConfig('git-p4.pathEncoding')2568 relPath = relPath.decode(encoding,'replace').encode('utf8','replace')2569if self.verbose:2570print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, relPath)25712572if self.largeFileSystem:2573(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)25742575 self.writeToGitStream(git_mode, relPath, contents)25762577defstreamOneP4Deletion(self,file):2578 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2579if verbose:2580 sys.stdout.write("delete%s\n"% relPath)2581 sys.stdout.flush()2582 self.gitStream.write("D%s\n"% relPath)25832584if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2585 self.largeFileSystem.removeLargeFile(relPath)25862587# handle another chunk of streaming data2588defstreamP4FilesCb(self, marshalled):25892590# catch p4 errors and complain2591 err =None2592if"code"in marshalled:2593if marshalled["code"] =="error":2594if"data"in marshalled:2595 err = marshalled["data"].rstrip()25962597if not err and'fileSize'in self.stream_file:2598 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2599if required_bytes >0:2600 err ='Not enough space left on%s! Free at least%iMB.'% (2601 os.getcwd(), required_bytes/1024/10242602)26032604if err:2605 f =None2606if self.stream_have_file_info:2607if"depotFile"in self.stream_file:2608 f = self.stream_file["depotFile"]2609# force a failure in fast-import, else an empty2610# commit will be made2611 self.gitStream.write("\n")2612 self.gitStream.write("die-now\n")2613 self.gitStream.close()2614# ignore errors, but make sure it exits first2615 self.importProcess.wait()2616if f:2617die("Error from p4 print for%s:%s"% (f, err))2618else:2619die("Error from p4 print:%s"% err)26202621if marshalled.has_key('depotFile')and self.stream_have_file_info:2622# start of a new file - output the old one first2623 self.streamOneP4File(self.stream_file, self.stream_contents)2624 self.stream_file = {}2625 self.stream_contents = []2626 self.stream_have_file_info =False26272628# pick up the new file information... for the2629# 'data' field we need to append to our array2630for k in marshalled.keys():2631if k =='data':2632if'streamContentSize'not in self.stream_file:2633 self.stream_file['streamContentSize'] =02634 self.stream_file['streamContentSize'] +=len(marshalled['data'])2635 self.stream_contents.append(marshalled['data'])2636else:2637 self.stream_file[k] = marshalled[k]26382639if(verbose and2640'streamContentSize'in self.stream_file and2641'fileSize'in self.stream_file and2642'depotFile'in self.stream_file):2643 size =int(self.stream_file["fileSize"])2644if size >0:2645 progress =100*self.stream_file['streamContentSize']/size2646 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2647 sys.stdout.flush()26482649 self.stream_have_file_info =True26502651# Stream directly from "p4 files" into "git fast-import"2652defstreamP4Files(self, files):2653 filesForCommit = []2654 filesToRead = []2655 filesToDelete = []26562657for f in files:2658 filesForCommit.append(f)2659if f['action']in self.delete_actions:2660 filesToDelete.append(f)2661else:2662 filesToRead.append(f)26632664# deleted files...2665for f in filesToDelete:2666 self.streamOneP4Deletion(f)26672668iflen(filesToRead) >0:2669 self.stream_file = {}2670 self.stream_contents = []2671 self.stream_have_file_info =False26722673# curry self argument2674defstreamP4FilesCbSelf(entry):2675 self.streamP4FilesCb(entry)26762677 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]26782679p4CmdList(["-x","-","print"],2680 stdin=fileArgs,2681 cb=streamP4FilesCbSelf)26822683# do the last chunk2684if self.stream_file.has_key('depotFile'):2685 self.streamOneP4File(self.stream_file, self.stream_contents)26862687defmake_email(self, userid):2688if userid in self.users:2689return self.users[userid]2690else:2691return"%s<a@b>"% userid26922693defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2694""" Stream a p4 tag.2695 commit is either a git commit, or a fast-import mark, ":<p4commit>"2696 """26972698if verbose:2699print"writing tag%sfor commit%s"% (labelName, commit)2700 gitStream.write("tag%s\n"% labelName)2701 gitStream.write("from%s\n"% commit)27022703if labelDetails.has_key('Owner'):2704 owner = labelDetails["Owner"]2705else:2706 owner =None27072708# Try to use the owner of the p4 label, or failing that,2709# the current p4 user id.2710if owner:2711 email = self.make_email(owner)2712else:2713 email = self.make_email(self.p4UserId())2714 tagger ="%s %s %s"% (email, epoch, self.tz)27152716 gitStream.write("tagger%s\n"% tagger)27172718print"labelDetails=",labelDetails2719if labelDetails.has_key('Description'):2720 description = labelDetails['Description']2721else:2722 description ='Label from git p4'27232724 gitStream.write("data%d\n"%len(description))2725 gitStream.write(description)2726 gitStream.write("\n")27272728definClientSpec(self, path):2729if not self.clientSpecDirs:2730return True2731 inClientSpec = self.clientSpecDirs.map_in_client(path)2732if not inClientSpec and self.verbose:2733print('Ignoring file outside of client spec:{0}'.format(path))2734return inClientSpec27352736defhasBranchPrefix(self, path):2737if not self.branchPrefixes:2738return True2739 hasPrefix = [p for p in self.branchPrefixes2740ifp4PathStartsWith(path, p)]2741if not hasPrefix and self.verbose:2742print('Ignoring file outside of prefix:{0}'.format(path))2743return hasPrefix27442745defcommit(self, details, files, branch, parent =""):2746 epoch = details["time"]2747 author = details["user"]2748 jobs = self.extractJobsFromCommit(details)27492750if self.verbose:2751print('commit into{0}'.format(branch))27522753if self.clientSpecDirs:2754 self.clientSpecDirs.update_client_spec_path_cache(files)27552756 files = [f for f in files2757if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]27582759if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2760print('Ignoring revision{0}as it would produce an empty commit.'2761.format(details['change']))2762return27632764 self.gitStream.write("commit%s\n"% branch)2765 self.gitStream.write("mark :%s\n"% details["change"])2766 self.committedChanges.add(int(details["change"]))2767 committer =""2768if author not in self.users:2769 self.getUserMapFromPerforceServer()2770 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)27712772 self.gitStream.write("committer%s\n"% committer)27732774 self.gitStream.write("data <<EOT\n")2775 self.gitStream.write(details["desc"])2776iflen(jobs) >0:2777 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2778 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2779(','.join(self.branchPrefixes), details["change"]))2780iflen(details['options']) >0:2781 self.gitStream.write(": options =%s"% details['options'])2782 self.gitStream.write("]\nEOT\n\n")27832784iflen(parent) >0:2785if self.verbose:2786print"parent%s"% parent2787 self.gitStream.write("from%s\n"% parent)27882789 self.streamP4Files(files)2790 self.gitStream.write("\n")27912792 change =int(details["change"])27932794if self.labels.has_key(change):2795 label = self.labels[change]2796 labelDetails = label[0]2797 labelRevisions = label[1]2798if self.verbose:2799print"Change%sis labelled%s"% (change, labelDetails)28002801 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2802for p in self.branchPrefixes])28032804iflen(files) ==len(labelRevisions):28052806 cleanedFiles = {}2807for info in files:2808if info["action"]in self.delete_actions:2809continue2810 cleanedFiles[info["depotFile"]] = info["rev"]28112812if cleanedFiles == labelRevisions:2813 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)28142815else:2816if not self.silent:2817print("Tag%sdoes not match with change%s: files do not match."2818% (labelDetails["label"], change))28192820else:2821if not self.silent:2822print("Tag%sdoes not match with change%s: file count is different."2823% (labelDetails["label"], change))28242825# Build a dictionary of changelists and labels, for "detect-labels" option.2826defgetLabels(self):2827 self.labels = {}28282829 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2830iflen(l) >0and not self.silent:2831print"Finding files belonging to labels in%s"% `self.depotPaths`28322833for output in l:2834 label = output["label"]2835 revisions = {}2836 newestChange =02837if self.verbose:2838print"Querying files for label%s"% label2839forfileinp4CmdList(["files"] +2840["%s...@%s"% (p, label)2841for p in self.depotPaths]):2842 revisions[file["depotFile"]] =file["rev"]2843 change =int(file["change"])2844if change > newestChange:2845 newestChange = change28462847 self.labels[newestChange] = [output, revisions]28482849if self.verbose:2850print"Label changes:%s"% self.labels.keys()28512852# Import p4 labels as git tags. A direct mapping does not2853# exist, so assume that if all the files are at the same revision2854# then we can use that, or it's something more complicated we should2855# just ignore.2856defimportP4Labels(self, stream, p4Labels):2857if verbose:2858print"import p4 labels: "+' '.join(p4Labels)28592860 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2861 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2862iflen(validLabelRegexp) ==0:2863 validLabelRegexp = defaultLabelRegexp2864 m = re.compile(validLabelRegexp)28652866for name in p4Labels:2867 commitFound =False28682869if not m.match(name):2870if verbose:2871print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2872continue28732874if name in ignoredP4Labels:2875continue28762877 labelDetails =p4CmdList(['label',"-o", name])[0]28782879# get the most recent changelist for each file in this label2880 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2881for p in self.depotPaths])28822883if change.has_key('change'):2884# find the corresponding git commit; take the oldest commit2885 changelist =int(change['change'])2886if changelist in self.committedChanges:2887 gitCommit =":%d"% changelist # use a fast-import mark2888 commitFound =True2889else:2890 gitCommit =read_pipe(["git","rev-list","--max-count=1",2891"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2892iflen(gitCommit) ==0:2893print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2894else:2895 commitFound =True2896 gitCommit = gitCommit.strip()28972898if commitFound:2899# Convert from p4 time format2900try:2901 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2902exceptValueError:2903print"Could not convert label time%s"% labelDetails['Update']2904 tmwhen =129052906 when =int(time.mktime(tmwhen))2907 self.streamTag(stream, name, labelDetails, gitCommit, when)2908if verbose:2909print"p4 label%smapped to git commit%s"% (name, gitCommit)2910else:2911if verbose:2912print"Label%shas no changelists - possibly deleted?"% name29132914if not commitFound:2915# We can't import this label; don't try again as it will get very2916# expensive repeatedly fetching all the files for labels that will2917# never be imported. If the label is moved in the future, the2918# ignore will need to be removed manually.2919system(["git","config","--add","git-p4.ignoredP4Labels", name])29202921defguessProjectName(self):2922for p in self.depotPaths:2923if p.endswith("/"):2924 p = p[:-1]2925 p = p[p.strip().rfind("/") +1:]2926if not p.endswith("/"):2927 p +="/"2928return p29292930defgetBranchMapping(self):2931 lostAndFoundBranches =set()29322933 user =gitConfig("git-p4.branchUser")2934iflen(user) >0:2935 command ="branches -u%s"% user2936else:2937 command ="branches"29382939for info inp4CmdList(command):2940 details =p4Cmd(["branch","-o", info["branch"]])2941 viewIdx =02942while details.has_key("View%s"% viewIdx):2943 paths = details["View%s"% viewIdx].split(" ")2944 viewIdx = viewIdx +12945# require standard //depot/foo/... //depot/bar/... mapping2946iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2947continue2948 source = paths[0]2949 destination = paths[1]2950## HACK2951ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2952 source = source[len(self.depotPaths[0]):-4]2953 destination = destination[len(self.depotPaths[0]):-4]29542955if destination in self.knownBranches:2956if not self.silent:2957print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2958print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2959continue29602961 self.knownBranches[destination] = source29622963 lostAndFoundBranches.discard(destination)29642965if source not in self.knownBranches:2966 lostAndFoundBranches.add(source)29672968# Perforce does not strictly require branches to be defined, so we also2969# check git config for a branch list.2970#2971# Example of branch definition in git config file:2972# [git-p4]2973# branchList=main:branchA2974# branchList=main:branchB2975# branchList=branchA:branchC2976 configBranches =gitConfigList("git-p4.branchList")2977for branch in configBranches:2978if branch:2979(source, destination) = branch.split(":")2980 self.knownBranches[destination] = source29812982 lostAndFoundBranches.discard(destination)29832984if source not in self.knownBranches:2985 lostAndFoundBranches.add(source)298629872988for branch in lostAndFoundBranches:2989 self.knownBranches[branch] = branch29902991defgetBranchMappingFromGitBranches(self):2992 branches =p4BranchesInGit(self.importIntoRemotes)2993for branch in branches.keys():2994if branch =="master":2995 branch ="main"2996else:2997 branch = branch[len(self.projectName):]2998 self.knownBranches[branch] = branch29993000defupdateOptionDict(self, d):3001 option_keys = {}3002if self.keepRepoPath:3003 option_keys['keepRepoPath'] =130043005 d["options"] =' '.join(sorted(option_keys.keys()))30063007defreadOptions(self, d):3008 self.keepRepoPath = (d.has_key('options')3009and('keepRepoPath'in d['options']))30103011defgitRefForBranch(self, branch):3012if branch =="main":3013return self.refPrefix +"master"30143015iflen(branch) <=0:3016return branch30173018return self.refPrefix + self.projectName + branch30193020defgitCommitByP4Change(self, ref, change):3021if self.verbose:3022print"looking in ref "+ ref +" for change%susing bisect..."% change30233024 earliestCommit =""3025 latestCommit =parseRevision(ref)30263027while True:3028if self.verbose:3029print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3030 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3031iflen(next) ==0:3032if self.verbose:3033print"argh"3034return""3035 log =extractLogMessageFromGitCommit(next)3036 settings =extractSettingsGitLog(log)3037 currentChange =int(settings['change'])3038if self.verbose:3039print"current change%s"% currentChange30403041if currentChange == change:3042if self.verbose:3043print"found%s"% next3044return next30453046if currentChange < change:3047 earliestCommit ="^%s"% next3048else:3049 latestCommit ="%s"% next30503051return""30523053defimportNewBranch(self, branch, maxChange):3054# make fast-import flush all changes to disk and update the refs using the checkpoint3055# command so that we can try to find the branch parent in the git history3056 self.gitStream.write("checkpoint\n\n");3057 self.gitStream.flush();3058 branchPrefix = self.depotPaths[0] + branch +"/"3059range="@1,%s"% maxChange3060#print "prefix" + branchPrefix3061 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3062iflen(changes) <=0:3063return False3064 firstChange = changes[0]3065#print "first change in branch: %s" % firstChange3066 sourceBranch = self.knownBranches[branch]3067 sourceDepotPath = self.depotPaths[0] + sourceBranch3068 sourceRef = self.gitRefForBranch(sourceBranch)3069#print "source " + sourceBranch30703071 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3072#print "branch parent: %s" % branchParentChange3073 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3074iflen(gitParent) >0:3075 self.initialParents[self.gitRefForBranch(branch)] = gitParent3076#print "parent git commit: %s" % gitParent30773078 self.importChanges(changes)3079return True30803081defsearchParent(self, parent, branch, target):3082 parentFound =False3083for blob inread_pipe_lines(["git","rev-list","--reverse",3084"--no-merges", parent]):3085 blob = blob.strip()3086iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3087 parentFound =True3088if self.verbose:3089print"Found parent of%sin commit%s"% (branch, blob)3090break3091if parentFound:3092return blob3093else:3094return None30953096defimportChanges(self, changes):3097 cnt =13098for change in changes:3099 description =p4_describe(change)3100 self.updateOptionDict(description)31013102if not self.silent:3103 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3104 sys.stdout.flush()3105 cnt = cnt +131063107try:3108if self.detectBranches:3109 branches = self.splitFilesIntoBranches(description)3110for branch in branches.keys():3111## HACK --hwn3112 branchPrefix = self.depotPaths[0] + branch +"/"3113 self.branchPrefixes = [ branchPrefix ]31143115 parent =""31163117 filesForCommit = branches[branch]31183119if self.verbose:3120print"branch is%s"% branch31213122 self.updatedBranches.add(branch)31233124if branch not in self.createdBranches:3125 self.createdBranches.add(branch)3126 parent = self.knownBranches[branch]3127if parent == branch:3128 parent =""3129else:3130 fullBranch = self.projectName + branch3131if fullBranch not in self.p4BranchesInGit:3132if not self.silent:3133print("\nImporting new branch%s"% fullBranch);3134if self.importNewBranch(branch, change -1):3135 parent =""3136 self.p4BranchesInGit.append(fullBranch)3137if not self.silent:3138print("\nResuming with change%s"% change);31393140if self.verbose:3141print"parent determined through known branches:%s"% parent31423143 branch = self.gitRefForBranch(branch)3144 parent = self.gitRefForBranch(parent)31453146if self.verbose:3147print"looking for initial parent for%s; current parent is%s"% (branch, parent)31483149iflen(parent) ==0and branch in self.initialParents:3150 parent = self.initialParents[branch]3151del self.initialParents[branch]31523153 blob =None3154iflen(parent) >0:3155 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3156if self.verbose:3157print"Creating temporary branch: "+ tempBranch3158 self.commit(description, filesForCommit, tempBranch)3159 self.tempBranches.append(tempBranch)3160 self.checkpoint()3161 blob = self.searchParent(parent, branch, tempBranch)3162if blob:3163 self.commit(description, filesForCommit, branch, blob)3164else:3165if self.verbose:3166print"Parent of%snot found. Committing into head of%s"% (branch, parent)3167 self.commit(description, filesForCommit, branch, parent)3168else:3169 files = self.extractFilesFromCommit(description)3170 self.commit(description, files, self.branch,3171 self.initialParent)3172# only needed once, to connect to the previous commit3173 self.initialParent =""3174exceptIOError:3175print self.gitError.read()3176 sys.exit(1)31773178defimportHeadRevision(self, revision):3179print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)31803181 details = {}3182 details["user"] ="git perforce import user"3183 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3184% (' '.join(self.depotPaths), revision))3185 details["change"] = revision3186 newestRevision =031873188 fileCnt =03189 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]31903191for info inp4CmdList(["files"] + fileArgs):31923193if'code'in info and info['code'] =='error':3194 sys.stderr.write("p4 returned an error:%s\n"3195% info['data'])3196if info['data'].find("must refer to client") >=0:3197 sys.stderr.write("This particular p4 error is misleading.\n")3198 sys.stderr.write("Perhaps the depot path was misspelled.\n");3199 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3200 sys.exit(1)3201if'p4ExitCode'in info:3202 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3203 sys.exit(1)320432053206 change =int(info["change"])3207if change > newestRevision:3208 newestRevision = change32093210if info["action"]in self.delete_actions:3211# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3212#fileCnt = fileCnt + 13213continue32143215for prop in["depotFile","rev","action","type"]:3216 details["%s%s"% (prop, fileCnt)] = info[prop]32173218 fileCnt = fileCnt +132193220 details["change"] = newestRevision32213222# Use time from top-most change so that all git p4 clones of3223# the same p4 repo have the same commit SHA1s.3224 res =p4_describe(newestRevision)3225 details["time"] = res["time"]32263227 self.updateOptionDict(details)3228try:3229 self.commit(details, self.extractFilesFromCommit(details), self.branch)3230exceptIOError:3231print"IO error with git fast-import. Is your git version recent enough?"3232print self.gitError.read()323332343235defrun(self, args):3236 self.depotPaths = []3237 self.changeRange =""3238 self.previousDepotPaths = []3239 self.hasOrigin =False32403241# map from branch depot path to parent branch3242 self.knownBranches = {}3243 self.initialParents = {}32443245if self.importIntoRemotes:3246 self.refPrefix ="refs/remotes/p4/"3247else:3248 self.refPrefix ="refs/heads/p4/"32493250if self.syncWithOrigin:3251 self.hasOrigin =originP4BranchesExist()3252if self.hasOrigin:3253if not self.silent:3254print'Syncing with origin first, using "git fetch origin"'3255system("git fetch origin")32563257 branch_arg_given =bool(self.branch)3258iflen(self.branch) ==0:3259 self.branch = self.refPrefix +"master"3260ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3261system("git update-ref%srefs/heads/p4"% self.branch)3262system("git branch -D p4")32633264# accept either the command-line option, or the configuration variable3265if self.useClientSpec:3266# will use this after clone to set the variable3267 self.useClientSpec_from_options =True3268else:3269ifgitConfigBool("git-p4.useclientspec"):3270 self.useClientSpec =True3271if self.useClientSpec:3272 self.clientSpecDirs =getClientSpec()32733274# TODO: should always look at previous commits,3275# merge with previous imports, if possible.3276if args == []:3277if self.hasOrigin:3278createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)32793280# branches holds mapping from branch name to sha13281 branches =p4BranchesInGit(self.importIntoRemotes)32823283# restrict to just this one, disabling detect-branches3284if branch_arg_given:3285 short = self.branch.split("/")[-1]3286if short in branches:3287 self.p4BranchesInGit = [ short ]3288else:3289 self.p4BranchesInGit = branches.keys()32903291iflen(self.p4BranchesInGit) >1:3292if not self.silent:3293print"Importing from/into multiple branches"3294 self.detectBranches =True3295for branch in branches.keys():3296 self.initialParents[self.refPrefix + branch] = \3297 branches[branch]32983299if self.verbose:3300print"branches:%s"% self.p4BranchesInGit33013302 p4Change =03303for branch in self.p4BranchesInGit:3304 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)33053306 settings =extractSettingsGitLog(logMsg)33073308 self.readOptions(settings)3309if(settings.has_key('depot-paths')3310and settings.has_key('change')):3311 change =int(settings['change']) +13312 p4Change =max(p4Change, change)33133314 depotPaths =sorted(settings['depot-paths'])3315if self.previousDepotPaths == []:3316 self.previousDepotPaths = depotPaths3317else:3318 paths = []3319for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3320 prev_list = prev.split("/")3321 cur_list = cur.split("/")3322for i inrange(0,min(len(cur_list),len(prev_list))):3323if cur_list[i] <> prev_list[i]:3324 i = i -13325break33263327 paths.append("/".join(cur_list[:i +1]))33283329 self.previousDepotPaths = paths33303331if p4Change >0:3332 self.depotPaths =sorted(self.previousDepotPaths)3333 self.changeRange ="@%s,#head"% p4Change3334if not self.silent and not self.detectBranches:3335print"Performing incremental import into%sgit branch"% self.branch33363337# accept multiple ref name abbreviations:3338# refs/foo/bar/branch -> use it exactly3339# p4/branch -> prepend refs/remotes/ or refs/heads/3340# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3341if not self.branch.startswith("refs/"):3342if self.importIntoRemotes:3343 prepend ="refs/remotes/"3344else:3345 prepend ="refs/heads/"3346if not self.branch.startswith("p4/"):3347 prepend +="p4/"3348 self.branch = prepend + self.branch33493350iflen(args) ==0and self.depotPaths:3351if not self.silent:3352print"Depot paths:%s"%' '.join(self.depotPaths)3353else:3354if self.depotPaths and self.depotPaths != args:3355print("previous import used depot path%sand now%swas specified. "3356"This doesn't work!"% (' '.join(self.depotPaths),3357' '.join(args)))3358 sys.exit(1)33593360 self.depotPaths =sorted(args)33613362 revision =""3363 self.users = {}33643365# Make sure no revision specifiers are used when --changesfile3366# is specified.3367 bad_changesfile =False3368iflen(self.changesFile) >0:3369for p in self.depotPaths:3370if p.find("@") >=0or p.find("#") >=0:3371 bad_changesfile =True3372break3373if bad_changesfile:3374die("Option --changesfile is incompatible with revision specifiers")33753376 newPaths = []3377for p in self.depotPaths:3378if p.find("@") != -1:3379 atIdx = p.index("@")3380 self.changeRange = p[atIdx:]3381if self.changeRange =="@all":3382 self.changeRange =""3383elif','not in self.changeRange:3384 revision = self.changeRange3385 self.changeRange =""3386 p = p[:atIdx]3387elif p.find("#") != -1:3388 hashIdx = p.index("#")3389 revision = p[hashIdx:]3390 p = p[:hashIdx]3391elif self.previousDepotPaths == []:3392# pay attention to changesfile, if given, else import3393# the entire p4 tree at the head revision3394iflen(self.changesFile) ==0:3395 revision ="#head"33963397 p = re.sub("\.\.\.$","", p)3398if not p.endswith("/"):3399 p +="/"34003401 newPaths.append(p)34023403 self.depotPaths = newPaths34043405# --detect-branches may change this for each branch3406 self.branchPrefixes = self.depotPaths34073408 self.loadUserMapFromCache()3409 self.labels = {}3410if self.detectLabels:3411 self.getLabels();34123413if self.detectBranches:3414## FIXME - what's a P4 projectName ?3415 self.projectName = self.guessProjectName()34163417if self.hasOrigin:3418 self.getBranchMappingFromGitBranches()3419else:3420 self.getBranchMapping()3421if self.verbose:3422print"p4-git branches:%s"% self.p4BranchesInGit3423print"initial parents:%s"% self.initialParents3424for b in self.p4BranchesInGit:3425if b !="master":34263427## FIXME3428 b = b[len(self.projectName):]3429 self.createdBranches.add(b)34303431 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))34323433 self.importProcess = subprocess.Popen(["git","fast-import"],3434 stdin=subprocess.PIPE,3435 stdout=subprocess.PIPE,3436 stderr=subprocess.PIPE);3437 self.gitOutput = self.importProcess.stdout3438 self.gitStream = self.importProcess.stdin3439 self.gitError = self.importProcess.stderr34403441if revision:3442 self.importHeadRevision(revision)3443else:3444 changes = []34453446iflen(self.changesFile) >0:3447 output =open(self.changesFile).readlines()3448 changeSet =set()3449for line in output:3450 changeSet.add(int(line))34513452for change in changeSet:3453 changes.append(change)34543455 changes.sort()3456else:3457# catch "git p4 sync" with no new branches, in a repo that3458# does not have any existing p4 branches3459iflen(args) ==0:3460if not self.p4BranchesInGit:3461die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")34623463# The default branch is master, unless --branch is used to3464# specify something else. Make sure it exists, or complain3465# nicely about how to use --branch.3466if not self.detectBranches:3467if notbranch_exists(self.branch):3468if branch_arg_given:3469die("Error: branch%sdoes not exist."% self.branch)3470else:3471die("Error: no branch%s; perhaps specify one with --branch."%3472 self.branch)34733474if self.verbose:3475print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3476 self.changeRange)3477 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)34783479iflen(self.maxChanges) >0:3480 changes = changes[:min(int(self.maxChanges),len(changes))]34813482iflen(changes) ==0:3483if not self.silent:3484print"No changes to import!"3485else:3486if not self.silent and not self.detectBranches:3487print"Import destination:%s"% self.branch34883489 self.updatedBranches =set()34903491if not self.detectBranches:3492if args:3493# start a new branch3494 self.initialParent =""3495else:3496# build on a previous revision3497 self.initialParent =parseRevision(self.branch)34983499 self.importChanges(changes)35003501if not self.silent:3502print""3503iflen(self.updatedBranches) >0:3504 sys.stdout.write("Updated branches: ")3505for b in self.updatedBranches:3506 sys.stdout.write("%s"% b)3507 sys.stdout.write("\n")35083509ifgitConfigBool("git-p4.importLabels"):3510 self.importLabels =True35113512if self.importLabels:3513 p4Labels =getP4Labels(self.depotPaths)3514 gitTags =getGitTags()35153516 missingP4Labels = p4Labels - gitTags3517 self.importP4Labels(self.gitStream, missingP4Labels)35183519 self.gitStream.close()3520if self.importProcess.wait() !=0:3521die("fast-import failed:%s"% self.gitError.read())3522 self.gitOutput.close()3523 self.gitError.close()35243525# Cleanup temporary branches created during import3526if self.tempBranches != []:3527for branch in self.tempBranches:3528read_pipe("git update-ref -d%s"% branch)3529 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))35303531# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3532# a convenient shortcut refname "p4".3533if self.importIntoRemotes:3534 head_ref = self.refPrefix +"HEAD"3535if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3536system(["git","symbolic-ref", head_ref, self.branch])35373538return True35393540classP4Rebase(Command):3541def__init__(self):3542 Command.__init__(self)3543 self.options = [3544 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3545]3546 self.importLabels =False3547 self.description = ("Fetches the latest revision from perforce and "3548+"rebases the current work (branch) against it")35493550defrun(self, args):3551 sync =P4Sync()3552 sync.importLabels = self.importLabels3553 sync.run([])35543555return self.rebase()35563557defrebase(self):3558if os.system("git update-index --refresh") !=0:3559die("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.");3560iflen(read_pipe("git diff-index HEAD --")) >0:3561die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");35623563[upstream, settings] =findUpstreamBranchPoint()3564iflen(upstream) ==0:3565die("Cannot find upstream branchpoint for rebase")35663567# the branchpoint may be p4/foo~3, so strip off the parent3568 upstream = re.sub("~[0-9]+$","", upstream)35693570print"Rebasing the current branch onto%s"% upstream3571 oldHead =read_pipe("git rev-parse HEAD").strip()3572system("git rebase%s"% upstream)3573system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3574return True35753576classP4Clone(P4Sync):3577def__init__(self):3578 P4Sync.__init__(self)3579 self.description ="Creates a new git repository and imports from Perforce into it"3580 self.usage ="usage: %prog [options] //depot/path[@revRange]"3581 self.options += [3582 optparse.make_option("--destination", dest="cloneDestination",3583 action='store', default=None,3584help="where to leave result of the clone"),3585 optparse.make_option("--bare", dest="cloneBare",3586 action="store_true", default=False),3587]3588 self.cloneDestination =None3589 self.needsGit =False3590 self.cloneBare =False35913592defdefaultDestination(self, args):3593## TODO: use common prefix of args?3594 depotPath = args[0]3595 depotDir = re.sub("(@[^@]*)$","", depotPath)3596 depotDir = re.sub("(#[^#]*)$","", depotDir)3597 depotDir = re.sub(r"\.\.\.$","", depotDir)3598 depotDir = re.sub(r"/$","", depotDir)3599return os.path.split(depotDir)[1]36003601defrun(self, args):3602iflen(args) <1:3603return False36043605if self.keepRepoPath and not self.cloneDestination:3606 sys.stderr.write("Must specify destination for --keep-path\n")3607 sys.exit(1)36083609 depotPaths = args36103611if not self.cloneDestination andlen(depotPaths) >1:3612 self.cloneDestination = depotPaths[-1]3613 depotPaths = depotPaths[:-1]36143615 self.cloneExclude = ["/"+p for p in self.cloneExclude]3616for p in depotPaths:3617if not p.startswith("//"):3618 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3619return False36203621if not self.cloneDestination:3622 self.cloneDestination = self.defaultDestination(args)36233624print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)36253626if not os.path.exists(self.cloneDestination):3627 os.makedirs(self.cloneDestination)3628chdir(self.cloneDestination)36293630 init_cmd = ["git","init"]3631if self.cloneBare:3632 init_cmd.append("--bare")3633 retcode = subprocess.call(init_cmd)3634if retcode:3635raiseCalledProcessError(retcode, init_cmd)36363637if not P4Sync.run(self, depotPaths):3638return False36393640# create a master branch and check out a work tree3641ifgitBranchExists(self.branch):3642system(["git","branch","master", self.branch ])3643if not self.cloneBare:3644system(["git","checkout","-f"])3645else:3646print'Not checking out any branch, use ' \3647'"git checkout -q -b master <branch>"'36483649# auto-set this variable if invoked with --use-client-spec3650if self.useClientSpec_from_options:3651system("git config --bool git-p4.useclientspec true")36523653return True36543655classP4Branches(Command):3656def__init__(self):3657 Command.__init__(self)3658 self.options = [ ]3659 self.description = ("Shows the git branches that hold imports and their "3660+"corresponding perforce depot paths")3661 self.verbose =False36623663defrun(self, args):3664iforiginP4BranchesExist():3665createOrUpdateBranchesFromOrigin()36663667 cmdline ="git rev-parse --symbolic "3668 cmdline +=" --remotes"36693670for line inread_pipe_lines(cmdline):3671 line = line.strip()36723673if not line.startswith('p4/')or line =="p4/HEAD":3674continue3675 branch = line36763677 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3678 settings =extractSettingsGitLog(log)36793680print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3681return True36823683classHelpFormatter(optparse.IndentedHelpFormatter):3684def__init__(self):3685 optparse.IndentedHelpFormatter.__init__(self)36863687defformat_description(self, description):3688if description:3689return description +"\n"3690else:3691return""36923693defprintUsage(commands):3694print"usage:%s<command> [options]"% sys.argv[0]3695print""3696print"valid commands:%s"%", ".join(commands)3697print""3698print"Try%s<command> --help for command specific help."% sys.argv[0]3699print""37003701commands = {3702"debug": P4Debug,3703"submit": P4Submit,3704"commit": P4Submit,3705"sync": P4Sync,3706"rebase": P4Rebase,3707"clone": P4Clone,3708"rollback": P4RollBack,3709"branches": P4Branches3710}371137123713defmain():3714iflen(sys.argv[1:]) ==0:3715printUsage(commands.keys())3716 sys.exit(2)37173718 cmdName = sys.argv[1]3719try:3720 klass = commands[cmdName]3721 cmd =klass()3722exceptKeyError:3723print"unknown command%s"% cmdName3724print""3725printUsage(commands.keys())3726 sys.exit(2)37273728 options = cmd.options3729 cmd.gitdir = os.environ.get("GIT_DIR",None)37303731 args = sys.argv[2:]37323733 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3734if cmd.needsGit:3735 options.append(optparse.make_option("--git-dir", dest="gitdir"))37363737 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3738 options,3739 description = cmd.description,3740 formatter =HelpFormatter())37413742(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3743global verbose3744 verbose = cmd.verbose3745if cmd.needsGit:3746if cmd.gitdir ==None:3747 cmd.gitdir = os.path.abspath(".git")3748if notisValidGitDir(cmd.gitdir):3749# "rev-parse --git-dir" without arguments will try $PWD/.git3750 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3751if os.path.exists(cmd.gitdir):3752 cdup =read_pipe("git rev-parse --show-cdup").strip()3753iflen(cdup) >0:3754chdir(cdup);37553756if notisValidGitDir(cmd.gitdir):3757ifisValidGitDir(cmd.gitdir +"/.git"):3758 cmd.gitdir +="/.git"3759else:3760die("fatal: cannot locate git repository at%s"% cmd.gitdir)37613762# so git commands invoked from the P4 workspace will succeed3763 os.environ["GIT_DIR"] = cmd.gitdir37643765if not cmd.run(args):3766 parser.print_help()3767 sys.exit(2)376837693770if __name__ =='__main__':3771main()