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 86if retries >0: 87# Provide a way to not pass this option by setting git-p4.retries to 0 88 real_cmd += ["-r",str(retries)] 89 90ifisinstance(cmd,basestring): 91 real_cmd =' '.join(real_cmd) +' '+ cmd 92else: 93 real_cmd += cmd 94return real_cmd 95 96defgit_dir(path): 97""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 98 This won't automatically add ".git" to a directory. 99 """ 100 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 101if not d orlen(d) ==0: 102return None 103else: 104return d 105 106defchdir(path, is_client_path=False): 107"""Do chdir to the given path, and set the PWD environment 108 variable for use by P4. It does not look at getcwd() output. 109 Since we're not using the shell, it is necessary to set the 110 PWD environment variable explicitly. 111 112 Normally, expand the path to force it to be absolute. This 113 addresses the use of relative path names inside P4 settings, 114 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 115 as given; it looks for .p4config using PWD. 116 117 If is_client_path, the path was handed to us directly by p4, 118 and may be a symbolic link. Do not call os.getcwd() in this 119 case, because it will cause p4 to think that PWD is not inside 120 the client path. 121 """ 122 123 os.chdir(path) 124if not is_client_path: 125 path = os.getcwd() 126 os.environ['PWD'] = path 127 128defcalcDiskFree(): 129"""Return free space in bytes on the disk of the given dirname.""" 130if platform.system() =='Windows': 131 free_bytes = ctypes.c_ulonglong(0) 132 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 133return free_bytes.value 134else: 135 st = os.statvfs(os.getcwd()) 136return st.f_bavail * st.f_frsize 137 138defdie(msg): 139if verbose: 140raiseException(msg) 141else: 142 sys.stderr.write(msg +"\n") 143 sys.exit(1) 144 145defwrite_pipe(c, stdin): 146if verbose: 147 sys.stderr.write('Writing pipe:%s\n'%str(c)) 148 149 expand =isinstance(c,basestring) 150 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 151 pipe = p.stdin 152 val = pipe.write(stdin) 153 pipe.close() 154if p.wait(): 155die('Command failed:%s'%str(c)) 156 157return val 158 159defp4_write_pipe(c, stdin): 160 real_cmd =p4_build_cmd(c) 161returnwrite_pipe(real_cmd, stdin) 162 163defread_pipe(c, ignore_error=False): 164if verbose: 165 sys.stderr.write('Reading pipe:%s\n'%str(c)) 166 167 expand =isinstance(c,basestring) 168 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 169(out, err) = p.communicate() 170if p.returncode !=0and not ignore_error: 171die('Command failed:%s\nError:%s'% (str(c), err)) 172return out 173 174defp4_read_pipe(c, ignore_error=False): 175 real_cmd =p4_build_cmd(c) 176returnread_pipe(real_cmd, ignore_error) 177 178defread_pipe_lines(c): 179if verbose: 180 sys.stderr.write('Reading pipe:%s\n'%str(c)) 181 182 expand =isinstance(c, basestring) 183 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 184 pipe = p.stdout 185 val = pipe.readlines() 186if pipe.close()or p.wait(): 187die('Command failed:%s'%str(c)) 188 189return val 190 191defp4_read_pipe_lines(c): 192"""Specifically invoke p4 on the command supplied. """ 193 real_cmd =p4_build_cmd(c) 194returnread_pipe_lines(real_cmd) 195 196defp4_has_command(cmd): 197"""Ask p4 for help on this command. If it returns an error, the 198 command does not exist in this version of p4.""" 199 real_cmd =p4_build_cmd(["help", cmd]) 200 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 201 stderr=subprocess.PIPE) 202 p.communicate() 203return p.returncode ==0 204 205defp4_has_move_command(): 206"""See if the move command exists, that it supports -k, and that 207 it has not been administratively disabled. The arguments 208 must be correct, but the filenames do not have to exist. Use 209 ones with wildcards so even if they exist, it will fail.""" 210 211if notp4_has_command("move"): 212return False 213 cmd =p4_build_cmd(["move","-k","@from","@to"]) 214 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 215(out, err) = p.communicate() 216# return code will be 1 in either case 217if err.find("Invalid option") >=0: 218return False 219if err.find("disabled") >=0: 220return False 221# assume it failed because @... was invalid changelist 222return True 223 224defsystem(cmd, ignore_error=False): 225 expand =isinstance(cmd,basestring) 226if verbose: 227 sys.stderr.write("executing%s\n"%str(cmd)) 228 retcode = subprocess.call(cmd, shell=expand) 229if retcode and not ignore_error: 230raiseCalledProcessError(retcode, cmd) 231 232return retcode 233 234defp4_system(cmd): 235"""Specifically invoke p4 as the system command. """ 236 real_cmd =p4_build_cmd(cmd) 237 expand =isinstance(real_cmd, basestring) 238 retcode = subprocess.call(real_cmd, shell=expand) 239if retcode: 240raiseCalledProcessError(retcode, real_cmd) 241 242_p4_version_string =None 243defp4_version_string(): 244"""Read the version string, showing just the last line, which 245 hopefully is the interesting version bit. 246 247 $ p4 -V 248 Perforce - The Fast Software Configuration Management System. 249 Copyright 1995-2011 Perforce Software. All rights reserved. 250 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 251 """ 252global _p4_version_string 253if not _p4_version_string: 254 a =p4_read_pipe_lines(["-V"]) 255 _p4_version_string = a[-1].rstrip() 256return _p4_version_string 257 258defp4_integrate(src, dest): 259p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 260 261defp4_sync(f, *options): 262p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 263 264defp4_add(f): 265# forcibly add file names with wildcards 266ifwildcard_present(f): 267p4_system(["add","-f", f]) 268else: 269p4_system(["add", f]) 270 271defp4_delete(f): 272p4_system(["delete",wildcard_encode(f)]) 273 274defp4_edit(f, *options): 275p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 276 277defp4_revert(f): 278p4_system(["revert",wildcard_encode(f)]) 279 280defp4_reopen(type, f): 281p4_system(["reopen","-t",type,wildcard_encode(f)]) 282 283defp4_reopen_in_change(changelist, files): 284 cmd = ["reopen","-c",str(changelist)] + files 285p4_system(cmd) 286 287defp4_move(src, dest): 288p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 289 290defp4_last_change(): 291 results =p4CmdList(["changes","-m","1"]) 292returnint(results[0]['change']) 293 294defp4_describe(change): 295"""Make sure it returns a valid result by checking for 296 the presence of field "time". Return a dict of the 297 results.""" 298 299 ds =p4CmdList(["describe","-s",str(change)]) 300iflen(ds) !=1: 301die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 302 303 d = ds[0] 304 305if"p4ExitCode"in d: 306die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 307str(d))) 308if"code"in d: 309if d["code"] =="error": 310die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 311 312if"time"not in d: 313die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 314 315return d 316 317# 318# Canonicalize the p4 type and return a tuple of the 319# base type, plus any modifiers. See "p4 help filetypes" 320# for a list and explanation. 321# 322defsplit_p4_type(p4type): 323 324 p4_filetypes_historical = { 325"ctempobj":"binary+Sw", 326"ctext":"text+C", 327"cxtext":"text+Cx", 328"ktext":"text+k", 329"kxtext":"text+kx", 330"ltext":"text+F", 331"tempobj":"binary+FSw", 332"ubinary":"binary+F", 333"uresource":"resource+F", 334"uxbinary":"binary+Fx", 335"xbinary":"binary+x", 336"xltext":"text+Fx", 337"xtempobj":"binary+Swx", 338"xtext":"text+x", 339"xunicode":"unicode+x", 340"xutf16":"utf16+x", 341} 342if p4type in p4_filetypes_historical: 343 p4type = p4_filetypes_historical[p4type] 344 mods ="" 345 s = p4type.split("+") 346 base = s[0] 347 mods ="" 348iflen(s) >1: 349 mods = s[1] 350return(base, mods) 351 352# 353# return the raw p4 type of a file (text, text+ko, etc) 354# 355defp4_type(f): 356 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 357return results[0]['headType'] 358 359# 360# Given a type base and modifier, return a regexp matching 361# the keywords that can be expanded in the file 362# 363defp4_keywords_regexp_for_type(base, type_mods): 364if base in("text","unicode","binary"): 365 kwords =None 366if"ko"in type_mods: 367 kwords ='Id|Header' 368elif"k"in type_mods: 369 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 370else: 371return None 372 pattern = r""" 373 \$ # Starts with a dollar, followed by... 374 (%s) # one of the keywords, followed by... 375 (:[^$\n]+)? # possibly an old expansion, followed by... 376 \$ # another dollar 377 """% kwords 378return pattern 379else: 380return None 381 382# 383# Given a file, return a regexp matching the possible 384# RCS keywords that will be expanded, or None for files 385# with kw expansion turned off. 386# 387defp4_keywords_regexp_for_file(file): 388if not os.path.exists(file): 389return None 390else: 391(type_base, type_mods) =split_p4_type(p4_type(file)) 392returnp4_keywords_regexp_for_type(type_base, type_mods) 393 394defsetP4ExecBit(file, mode): 395# Reopens an already open file and changes the execute bit to match 396# the execute bit setting in the passed in mode. 397 398 p4Type ="+x" 399 400if notisModeExec(mode): 401 p4Type =getP4OpenedType(file) 402 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 403 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 404if p4Type[-1] =="+": 405 p4Type = p4Type[0:-1] 406 407p4_reopen(p4Type,file) 408 409defgetP4OpenedType(file): 410# Returns the perforce file type for the given file. 411 412 result =p4_read_pipe(["opened",wildcard_encode(file)]) 413 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 414if match: 415return match.group(1) 416else: 417die("Could not determine file type for%s(result: '%s')"% (file, result)) 418 419# Return the set of all p4 labels 420defgetP4Labels(depotPaths): 421 labels =set() 422ifisinstance(depotPaths,basestring): 423 depotPaths = [depotPaths] 424 425for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 426 label = l['label'] 427 labels.add(label) 428 429return labels 430 431# Return the set of all git tags 432defgetGitTags(): 433 gitTags =set() 434for line inread_pipe_lines(["git","tag"]): 435 tag = line.strip() 436 gitTags.add(tag) 437return gitTags 438 439defdiffTreePattern(): 440# This is a simple generator for the diff tree regex pattern. This could be 441# a class variable if this and parseDiffTreeEntry were a part of a class. 442 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 443while True: 444yield pattern 445 446defparseDiffTreeEntry(entry): 447"""Parses a single diff tree entry into its component elements. 448 449 See git-diff-tree(1) manpage for details about the format of the diff 450 output. This method returns a dictionary with the following elements: 451 452 src_mode - The mode of the source file 453 dst_mode - The mode of the destination file 454 src_sha1 - The sha1 for the source file 455 dst_sha1 - The sha1 fr the destination file 456 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 457 status_score - The score for the status (applicable for 'C' and 'R' 458 statuses). This is None if there is no score. 459 src - The path for the source file. 460 dst - The path for the destination file. This is only present for 461 copy or renames. If it is not present, this is None. 462 463 If the pattern is not matched, None is returned.""" 464 465 match =diffTreePattern().next().match(entry) 466if match: 467return{ 468'src_mode': match.group(1), 469'dst_mode': match.group(2), 470'src_sha1': match.group(3), 471'dst_sha1': match.group(4), 472'status': match.group(5), 473'status_score': match.group(6), 474'src': match.group(7), 475'dst': match.group(10) 476} 477return None 478 479defisModeExec(mode): 480# Returns True if the given git mode represents an executable file, 481# otherwise False. 482return mode[-3:] =="755" 483 484defisModeExecChanged(src_mode, dst_mode): 485returnisModeExec(src_mode) !=isModeExec(dst_mode) 486 487defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 488 489ifisinstance(cmd,basestring): 490 cmd ="-G "+ cmd 491 expand =True 492else: 493 cmd = ["-G"] + cmd 494 expand =False 495 496 cmd =p4_build_cmd(cmd) 497if verbose: 498 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 499 500# Use a temporary file to avoid deadlocks without 501# subprocess.communicate(), which would put another copy 502# of stdout into memory. 503 stdin_file =None 504if stdin is not None: 505 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 506ifisinstance(stdin,basestring): 507 stdin_file.write(stdin) 508else: 509for i in stdin: 510 stdin_file.write(i +'\n') 511 stdin_file.flush() 512 stdin_file.seek(0) 513 514 p4 = subprocess.Popen(cmd, 515 shell=expand, 516 stdin=stdin_file, 517 stdout=subprocess.PIPE) 518 519 result = [] 520try: 521while True: 522 entry = marshal.load(p4.stdout) 523if cb is not None: 524cb(entry) 525else: 526 result.append(entry) 527exceptEOFError: 528pass 529 exitCode = p4.wait() 530if exitCode !=0: 531 entry = {} 532 entry["p4ExitCode"] = exitCode 533 result.append(entry) 534 535return result 536 537defp4Cmd(cmd): 538list=p4CmdList(cmd) 539 result = {} 540for entry inlist: 541 result.update(entry) 542return result; 543 544defp4Where(depotPath): 545if not depotPath.endswith("/"): 546 depotPath +="/" 547 depotPathLong = depotPath +"..." 548 outputList =p4CmdList(["where", depotPathLong]) 549 output =None 550for entry in outputList: 551if"depotFile"in entry: 552# Search for the base client side depot path, as long as it starts with the branch's P4 path. 553# The base path always ends with "/...". 554if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 555 output = entry 556break 557elif"data"in entry: 558 data = entry.get("data") 559 space = data.find(" ") 560if data[:space] == depotPath: 561 output = entry 562break 563if output ==None: 564return"" 565if output["code"] =="error": 566return"" 567 clientPath ="" 568if"path"in output: 569 clientPath = output.get("path") 570elif"data"in output: 571 data = output.get("data") 572 lastSpace = data.rfind(" ") 573 clientPath = data[lastSpace +1:] 574 575if clientPath.endswith("..."): 576 clientPath = clientPath[:-3] 577return clientPath 578 579defcurrentGitBranch(): 580 retcode =system(["git","symbolic-ref","-q","HEAD"], ignore_error=True) 581if retcode !=0: 582# on a detached head 583return None 584else: 585returnread_pipe(["git","name-rev","HEAD"]).split(" ")[1].strip() 586 587defisValidGitDir(path): 588returngit_dir(path) !=None 589 590defparseRevision(ref): 591returnread_pipe("git rev-parse%s"% ref).strip() 592 593defbranchExists(ref): 594 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 595 ignore_error=True) 596returnlen(rev) >0 597 598defextractLogMessageFromGitCommit(commit): 599 logMessage ="" 600 601## fixme: title is first line of commit, not 1st paragraph. 602 foundTitle =False 603for log inread_pipe_lines("git cat-file commit%s"% commit): 604if not foundTitle: 605iflen(log) ==1: 606 foundTitle =True 607continue 608 609 logMessage += log 610return logMessage 611 612defextractSettingsGitLog(log): 613 values = {} 614for line in log.split("\n"): 615 line = line.strip() 616 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 617if not m: 618continue 619 620 assignments = m.group(1).split(':') 621for a in assignments: 622 vals = a.split('=') 623 key = vals[0].strip() 624 val = ('='.join(vals[1:])).strip() 625if val.endswith('\"')and val.startswith('"'): 626 val = val[1:-1] 627 628 values[key] = val 629 630 paths = values.get("depot-paths") 631if not paths: 632 paths = values.get("depot-path") 633if paths: 634 values['depot-paths'] = paths.split(',') 635return values 636 637defgitBranchExists(branch): 638 proc = subprocess.Popen(["git","rev-parse", branch], 639 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 640return proc.wait() ==0; 641 642_gitConfig = {} 643 644defgitConfig(key, typeSpecifier=None): 645if not _gitConfig.has_key(key): 646 cmd = ["git","config"] 647if typeSpecifier: 648 cmd += [ typeSpecifier ] 649 cmd += [ key ] 650 s =read_pipe(cmd, ignore_error=True) 651 _gitConfig[key] = s.strip() 652return _gitConfig[key] 653 654defgitConfigBool(key): 655"""Return a bool, using git config --bool. It is True only if the 656 variable is set to true, and False if set to false or not present 657 in the config.""" 658 659if not _gitConfig.has_key(key): 660 _gitConfig[key] =gitConfig(key,'--bool') =="true" 661return _gitConfig[key] 662 663defgitConfigInt(key): 664if not _gitConfig.has_key(key): 665 cmd = ["git","config","--int", key ] 666 s =read_pipe(cmd, ignore_error=True) 667 v = s.strip() 668try: 669 _gitConfig[key] =int(gitConfig(key,'--int')) 670exceptValueError: 671 _gitConfig[key] =None 672return _gitConfig[key] 673 674defgitConfigList(key): 675if not _gitConfig.has_key(key): 676 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 677 _gitConfig[key] = s.strip().splitlines() 678if _gitConfig[key] == ['']: 679 _gitConfig[key] = [] 680return _gitConfig[key] 681 682defp4BranchesInGit(branchesAreInRemotes=True): 683"""Find all the branches whose names start with "p4/", looking 684 in remotes or heads as specified by the argument. Return 685 a dictionary of{ branch: revision }for each one found. 686 The branch names are the short names, without any 687 "p4/" prefix.""" 688 689 branches = {} 690 691 cmdline ="git rev-parse --symbolic " 692if branchesAreInRemotes: 693 cmdline +="--remotes" 694else: 695 cmdline +="--branches" 696 697for line inread_pipe_lines(cmdline): 698 line = line.strip() 699 700# only import to p4/ 701if not line.startswith('p4/'): 702continue 703# special symbolic ref to p4/master 704if line =="p4/HEAD": 705continue 706 707# strip off p4/ prefix 708 branch = line[len("p4/"):] 709 710 branches[branch] =parseRevision(line) 711 712return branches 713 714defbranch_exists(branch): 715"""Make sure that the given ref name really exists.""" 716 717 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 718 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 719 out, _ = p.communicate() 720if p.returncode: 721return False 722# expect exactly one line of output: the branch name 723return out.rstrip() == branch 724 725deffindUpstreamBranchPoint(head ="HEAD"): 726 branches =p4BranchesInGit() 727# map from depot-path to branch name 728 branchByDepotPath = {} 729for branch in branches.keys(): 730 tip = branches[branch] 731 log =extractLogMessageFromGitCommit(tip) 732 settings =extractSettingsGitLog(log) 733if settings.has_key("depot-paths"): 734 paths =",".join(settings["depot-paths"]) 735 branchByDepotPath[paths] ="remotes/p4/"+ branch 736 737 settings =None 738 parent =0 739while parent <65535: 740 commit = head +"~%s"% parent 741 log =extractLogMessageFromGitCommit(commit) 742 settings =extractSettingsGitLog(log) 743if settings.has_key("depot-paths"): 744 paths =",".join(settings["depot-paths"]) 745if branchByDepotPath.has_key(paths): 746return[branchByDepotPath[paths], settings] 747 748 parent = parent +1 749 750return["", settings] 751 752defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 753if not silent: 754print("Creating/updating branch(es) in%sbased on origin branch(es)" 755% localRefPrefix) 756 757 originPrefix ="origin/p4/" 758 759for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 760 line = line.strip() 761if(not line.startswith(originPrefix))or line.endswith("HEAD"): 762continue 763 764 headName = line[len(originPrefix):] 765 remoteHead = localRefPrefix + headName 766 originHead = line 767 768 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 769if(not original.has_key('depot-paths') 770or not original.has_key('change')): 771continue 772 773 update =False 774if notgitBranchExists(remoteHead): 775if verbose: 776print"creating%s"% remoteHead 777 update =True 778else: 779 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 780if settings.has_key('change') >0: 781if settings['depot-paths'] == original['depot-paths']: 782 originP4Change =int(original['change']) 783 p4Change =int(settings['change']) 784if originP4Change > p4Change: 785print("%s(%s) is newer than%s(%s). " 786"Updating p4 branch from origin." 787% (originHead, originP4Change, 788 remoteHead, p4Change)) 789 update =True 790else: 791print("Ignoring:%swas imported from%swhile " 792"%swas imported from%s" 793% (originHead,','.join(original['depot-paths']), 794 remoteHead,','.join(settings['depot-paths']))) 795 796if update: 797system("git update-ref%s %s"% (remoteHead, originHead)) 798 799deforiginP4BranchesExist(): 800returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 801 802 803defp4ParseNumericChangeRange(parts): 804 changeStart =int(parts[0][1:]) 805if parts[1] =='#head': 806 changeEnd =p4_last_change() 807else: 808 changeEnd =int(parts[1]) 809 810return(changeStart, changeEnd) 811 812defchooseBlockSize(blockSize): 813if blockSize: 814return blockSize 815else: 816return defaultBlockSize 817 818defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 819assert depotPaths 820 821# Parse the change range into start and end. Try to find integer 822# revision ranges as these can be broken up into blocks to avoid 823# hitting server-side limits (maxrows, maxscanresults). But if 824# that doesn't work, fall back to using the raw revision specifier 825# strings, without using block mode. 826 827if changeRange is None or changeRange =='': 828 changeStart =1 829 changeEnd =p4_last_change() 830 block_size =chooseBlockSize(requestedBlockSize) 831else: 832 parts = changeRange.split(',') 833assertlen(parts) ==2 834try: 835(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 836 block_size =chooseBlockSize(requestedBlockSize) 837except: 838 changeStart = parts[0][1:] 839 changeEnd = parts[1] 840if requestedBlockSize: 841die("cannot use --changes-block-size with non-numeric revisions") 842 block_size =None 843 844 changes =set() 845 846# Retrieve changes a block at a time, to prevent running 847# into a MaxResults/MaxScanRows error from the server. 848 849while True: 850 cmd = ['changes'] 851 852if block_size: 853 end =min(changeEnd, changeStart + block_size) 854 revisionRange ="%d,%d"% (changeStart, end) 855else: 856 revisionRange ="%s,%s"% (changeStart, changeEnd) 857 858for p in depotPaths: 859 cmd += ["%s...@%s"% (p, revisionRange)] 860 861# Insert changes in chronological order 862for line inreversed(p4_read_pipe_lines(cmd)): 863 changes.add(int(line.split(" ")[1])) 864 865if not block_size: 866break 867 868if end >= changeEnd: 869break 870 871 changeStart = end +1 872 873 changes =sorted(changes) 874return changes 875 876defp4PathStartsWith(path, prefix): 877# This method tries to remedy a potential mixed-case issue: 878# 879# If UserA adds //depot/DirA/file1 880# and UserB adds //depot/dira/file2 881# 882# we may or may not have a problem. If you have core.ignorecase=true, 883# we treat DirA and dira as the same directory 884ifgitConfigBool("core.ignorecase"): 885return path.lower().startswith(prefix.lower()) 886return path.startswith(prefix) 887 888defgetClientSpec(): 889"""Look at the p4 client spec, create a View() object that contains 890 all the mappings, and return it.""" 891 892 specList =p4CmdList("client -o") 893iflen(specList) !=1: 894die('Output from "client -o" is%dlines, expecting 1'% 895len(specList)) 896 897# dictionary of all client parameters 898 entry = specList[0] 899 900# the //client/ name 901 client_name = entry["Client"] 902 903# just the keys that start with "View" 904 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 905 906# hold this new View 907 view =View(client_name) 908 909# append the lines, in order, to the view 910for view_num inrange(len(view_keys)): 911 k ="View%d"% view_num 912if k not in view_keys: 913die("Expected view key%smissing"% k) 914 view.append(entry[k]) 915 916return view 917 918defgetClientRoot(): 919"""Grab the client directory.""" 920 921 output =p4CmdList("client -o") 922iflen(output) !=1: 923die('Output from "client -o" is%dlines, expecting 1'%len(output)) 924 925 entry = output[0] 926if"Root"not in entry: 927die('Client has no "Root"') 928 929return entry["Root"] 930 931# 932# P4 wildcards are not allowed in filenames. P4 complains 933# if you simply add them, but you can force it with "-f", in 934# which case it translates them into %xx encoding internally. 935# 936defwildcard_decode(path): 937# Search for and fix just these four characters. Do % last so 938# that fixing it does not inadvertently create new %-escapes. 939# Cannot have * in a filename in windows; untested as to 940# what p4 would do in such a case. 941if not platform.system() =="Windows": 942 path = path.replace("%2A","*") 943 path = path.replace("%23","#") \ 944.replace("%40","@") \ 945.replace("%25","%") 946return path 947 948defwildcard_encode(path): 949# do % first to avoid double-encoding the %s introduced here 950 path = path.replace("%","%25") \ 951.replace("*","%2A") \ 952.replace("#","%23") \ 953.replace("@","%40") 954return path 955 956defwildcard_present(path): 957 m = re.search("[*#@%]", path) 958return m is not None 959 960classLargeFileSystem(object): 961"""Base class for large file system support.""" 962 963def__init__(self, writeToGitStream): 964 self.largeFiles =set() 965 self.writeToGitStream = writeToGitStream 966 967defgeneratePointer(self, cloneDestination, contentFile): 968"""Return the content of a pointer file that is stored in Git instead of 969 the actual content.""" 970assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 971 972defpushFile(self, localLargeFile): 973"""Push the actual content which is not stored in the Git repository to 974 a server.""" 975assert False,"Method 'pushFile' required in "+ self.__class__.__name__ 976 977defhasLargeFileExtension(self, relPath): 978returnreduce( 979lambda a, b: a or b, 980[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')], 981False 982) 983 984defgenerateTempFile(self, contents): 985 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 986for d in contents: 987 contentFile.write(d) 988 contentFile.close() 989return contentFile.name 990 991defexceedsLargeFileThreshold(self, relPath, contents): 992ifgitConfigInt('git-p4.largeFileThreshold'): 993 contentsSize =sum(len(d)for d in contents) 994if contentsSize >gitConfigInt('git-p4.largeFileThreshold'): 995return True 996ifgitConfigInt('git-p4.largeFileCompressedThreshold'): 997 contentsSize =sum(len(d)for d in contents) 998if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'): 999return False1000 contentTempFile = self.generateTempFile(contents)1001 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1002 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1003 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1004 zf.close()1005 compressedContentsSize = zf.infolist()[0].compress_size1006 os.remove(contentTempFile)1007 os.remove(compressedContentFile.name)1008if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1009return True1010return False10111012defaddLargeFile(self, relPath):1013 self.largeFiles.add(relPath)10141015defremoveLargeFile(self, relPath):1016 self.largeFiles.remove(relPath)10171018defisLargeFile(self, relPath):1019return relPath in self.largeFiles10201021defprocessContent(self, git_mode, relPath, contents):1022"""Processes the content of git fast import. This method decides if a1023 file is stored in the large file system and handles all necessary1024 steps."""1025if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1026 contentTempFile = self.generateTempFile(contents)1027(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1028if pointer_git_mode:1029 git_mode = pointer_git_mode1030if localLargeFile:1031# Move temp file to final location in large file system1032 largeFileDir = os.path.dirname(localLargeFile)1033if not os.path.isdir(largeFileDir):1034 os.makedirs(largeFileDir)1035 shutil.move(contentTempFile, localLargeFile)1036 self.addLargeFile(relPath)1037ifgitConfigBool('git-p4.largeFilePush'):1038 self.pushFile(localLargeFile)1039if verbose:1040 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1041return(git_mode, contents)10421043classMockLFS(LargeFileSystem):1044"""Mock large file system for testing."""10451046defgeneratePointer(self, contentFile):1047"""The pointer content is the original content prefixed with "pointer-".1048 The local filename of the large file storage is derived from the file content.1049 """1050withopen(contentFile,'r')as f:1051 content =next(f)1052 gitMode ='100644'1053 pointerContents ='pointer-'+ content1054 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1055return(gitMode, pointerContents, localLargeFile)10561057defpushFile(self, localLargeFile):1058"""The remote filename of the large file storage is the same as the local1059 one but in a different directory.1060 """1061 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1062if not os.path.exists(remotePath):1063 os.makedirs(remotePath)1064 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10651066classGitLFS(LargeFileSystem):1067"""Git LFS as backend for the git-p4 large file system.1068 See https://git-lfs.github.com/ for details."""10691070def__init__(self, *args):1071 LargeFileSystem.__init__(self, *args)1072 self.baseGitAttributes = []10731074defgeneratePointer(self, contentFile):1075"""Generate a Git LFS pointer for the content. Return LFS Pointer file1076 mode and content which is stored in the Git repository instead of1077 the actual content. Return also the new location of the actual1078 content.1079 """1080if os.path.getsize(contentFile) ==0:1081return(None,'',None)10821083 pointerProcess = subprocess.Popen(1084['git','lfs','pointer','--file='+ contentFile],1085 stdout=subprocess.PIPE1086)1087 pointerFile = pointerProcess.stdout.read()1088if pointerProcess.wait():1089 os.remove(contentFile)1090die('git-lfs pointer command failed. Did you install the extension?')10911092# Git LFS removed the preamble in the output of the 'pointer' command1093# starting from version 1.2.0. Check for the preamble here to support1094# earlier versions.1095# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431096if pointerFile.startswith('Git LFS pointer for'):1097 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)10981099 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1100 localLargeFile = os.path.join(1101 os.getcwd(),1102'.git','lfs','objects', oid[:2], oid[2:4],1103 oid,1104)1105# LFS Spec states that pointer files should not have the executable bit set.1106 gitMode ='100644'1107return(gitMode, pointerFile, localLargeFile)11081109defpushFile(self, localLargeFile):1110 uploadProcess = subprocess.Popen(1111['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1112)1113if uploadProcess.wait():1114die('git-lfs push command failed. Did you define a remote?')11151116defgenerateGitAttributes(self):1117return(1118 self.baseGitAttributes +1119[1120'\n',1121'#\n',1122'# Git LFS (see https://git-lfs.github.com/)\n',1123'#\n',1124] +1125['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1126for f insorted(gitConfigList('git-p4.largeFileExtensions'))1127] +1128['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1129for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1130]1131)11321133defaddLargeFile(self, relPath):1134 LargeFileSystem.addLargeFile(self, relPath)1135 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11361137defremoveLargeFile(self, relPath):1138 LargeFileSystem.removeLargeFile(self, relPath)1139 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11401141defprocessContent(self, git_mode, relPath, contents):1142if relPath =='.gitattributes':1143 self.baseGitAttributes = contents1144return(git_mode, self.generateGitAttributes())1145else:1146return LargeFileSystem.processContent(self, git_mode, relPath, contents)11471148class Command:1149def__init__(self):1150 self.usage ="usage: %prog [options]"1151 self.needsGit =True1152 self.verbose =False11531154class P4UserMap:1155def__init__(self):1156 self.userMapFromPerforceServer =False1157 self.myP4UserId =None11581159defp4UserId(self):1160if self.myP4UserId:1161return self.myP4UserId11621163 results =p4CmdList("user -o")1164for r in results:1165if r.has_key('User'):1166 self.myP4UserId = r['User']1167return r['User']1168die("Could not find your p4 user id")11691170defp4UserIsMe(self, p4User):1171# return True if the given p4 user is actually me1172 me = self.p4UserId()1173if not p4User or p4User != me:1174return False1175else:1176return True11771178defgetUserCacheFilename(self):1179 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1180return home +"/.gitp4-usercache.txt"11811182defgetUserMapFromPerforceServer(self):1183if self.userMapFromPerforceServer:1184return1185 self.users = {}1186 self.emails = {}11871188for output inp4CmdList("users"):1189if not output.has_key("User"):1190continue1191 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1192 self.emails[output["Email"]] = output["User"]11931194 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1195for mapUserConfig ingitConfigList("git-p4.mapUser"):1196 mapUser = mapUserConfigRegex.findall(mapUserConfig)1197if mapUser andlen(mapUser[0]) ==3:1198 user = mapUser[0][0]1199 fullname = mapUser[0][1]1200 email = mapUser[0][2]1201 self.users[user] = fullname +" <"+ email +">"1202 self.emails[email] = user12031204 s =''1205for(key, val)in self.users.items():1206 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12071208open(self.getUserCacheFilename(),"wb").write(s)1209 self.userMapFromPerforceServer =True12101211defloadUserMapFromCache(self):1212 self.users = {}1213 self.userMapFromPerforceServer =False1214try:1215 cache =open(self.getUserCacheFilename(),"rb")1216 lines = cache.readlines()1217 cache.close()1218for line in lines:1219 entry = line.strip().split("\t")1220 self.users[entry[0]] = entry[1]1221exceptIOError:1222 self.getUserMapFromPerforceServer()12231224classP4Debug(Command):1225def__init__(self):1226 Command.__init__(self)1227 self.options = []1228 self.description ="A tool to debug the output of p4 -G."1229 self.needsGit =False12301231defrun(self, args):1232 j =01233for output inp4CmdList(args):1234print'Element:%d'% j1235 j +=11236print output1237return True12381239classP4RollBack(Command):1240def__init__(self):1241 Command.__init__(self)1242 self.options = [1243 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1244]1245 self.description ="A tool to debug the multi-branch import. Don't use :)"1246 self.rollbackLocalBranches =False12471248defrun(self, args):1249iflen(args) !=1:1250return False1251 maxChange =int(args[0])12521253if"p4ExitCode"inp4Cmd("changes -m 1"):1254die("Problems executing p4");12551256if self.rollbackLocalBranches:1257 refPrefix ="refs/heads/"1258 lines =read_pipe_lines("git rev-parse --symbolic --branches")1259else:1260 refPrefix ="refs/remotes/"1261 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12621263for line in lines:1264if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1265 line = line.strip()1266 ref = refPrefix + line1267 log =extractLogMessageFromGitCommit(ref)1268 settings =extractSettingsGitLog(log)12691270 depotPaths = settings['depot-paths']1271 change = settings['change']12721273 changed =False12741275iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1276for p in depotPaths]))) ==0:1277print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1278system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1279continue12801281while change andint(change) > maxChange:1282 changed =True1283if self.verbose:1284print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1285system("git update-ref%s\"%s^\""% (ref, ref))1286 log =extractLogMessageFromGitCommit(ref)1287 settings =extractSettingsGitLog(log)128812891290 depotPaths = settings['depot-paths']1291 change = settings['change']12921293if changed:1294print"%srewound to%s"% (ref, change)12951296return True12971298classP4Submit(Command, P4UserMap):12991300 conflict_behavior_choices = ("ask","skip","quit")13011302def__init__(self):1303 Command.__init__(self)1304 P4UserMap.__init__(self)1305 self.options = [1306 optparse.make_option("--origin", dest="origin"),1307 optparse.make_option("-M", dest="detectRenames", action="store_true"),1308# preserve the user, requires relevant p4 permissions1309 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1310 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1311 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1312 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1313 optparse.make_option("--conflict", dest="conflict_behavior",1314 choices=self.conflict_behavior_choices),1315 optparse.make_option("--branch", dest="branch"),1316 optparse.make_option("--shelve", dest="shelve", action="store_true",1317help="Shelve instead of submit. Shelved files are reverted, "1318"restoring the workspace to the state before the shelve"),1319 optparse.make_option("--update-shelve", dest="update_shelve", action="store",type="int",1320 metavar="CHANGELIST",1321help="update an existing shelved changelist, implies --shelve")1322]1323 self.description ="Submit changes from git to the perforce depot."1324 self.usage +=" [name of git branch to submit into perforce depot]"1325 self.origin =""1326 self.detectRenames =False1327 self.preserveUser =gitConfigBool("git-p4.preserveUser")1328 self.dry_run =False1329 self.shelve =False1330 self.update_shelve =None1331 self.prepare_p4_only =False1332 self.conflict_behavior =None1333 self.isWindows = (platform.system() =="Windows")1334 self.exportLabels =False1335 self.p4HasMoveCommand =p4_has_move_command()1336 self.branch =None13371338ifgitConfig('git-p4.largeFileSystem'):1339die("Large file system not supported for git-p4 submit command. Please remove it from config.")13401341defcheck(self):1342iflen(p4CmdList("opened ...")) >0:1343die("You have files opened with perforce! Close them before starting the sync.")13441345defseparate_jobs_from_description(self, message):1346"""Extract and return a possible Jobs field in the commit1347 message. It goes into a separate section in the p4 change1348 specification.13491350 A jobs line starts with "Jobs:" and looks like a new field1351 in a form. Values are white-space separated on the same1352 line or on following lines that start with a tab.13531354 This does not parse and extract the full git commit message1355 like a p4 form. It just sees the Jobs: line as a marker1356 to pass everything from then on directly into the p4 form,1357 but outside the description section.13581359 Return a tuple (stripped log message, jobs string)."""13601361 m = re.search(r'^Jobs:', message, re.MULTILINE)1362if m is None:1363return(message,None)13641365 jobtext = message[m.start():]1366 stripped_message = message[:m.start()].rstrip()1367return(stripped_message, jobtext)13681369defprepareLogMessage(self, template, message, jobs):1370"""Edits the template returned from "p4 change -o" to insert1371 the message in the Description field, and the jobs text in1372 the Jobs field."""1373 result =""13741375 inDescriptionSection =False13761377for line in template.split("\n"):1378if line.startswith("#"):1379 result += line +"\n"1380continue13811382if inDescriptionSection:1383if line.startswith("Files:")or line.startswith("Jobs:"):1384 inDescriptionSection =False1385# insert Jobs section1386if jobs:1387 result += jobs +"\n"1388else:1389continue1390else:1391if line.startswith("Description:"):1392 inDescriptionSection =True1393 line +="\n"1394for messageLine in message.split("\n"):1395 line +="\t"+ messageLine +"\n"13961397 result += line +"\n"13981399return result14001401defpatchRCSKeywords(self,file, pattern):1402# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1403(handle, outFileName) = tempfile.mkstemp(dir='.')1404try:1405 outFile = os.fdopen(handle,"w+")1406 inFile =open(file,"r")1407 regexp = re.compile(pattern, re.VERBOSE)1408for line in inFile.readlines():1409 line = regexp.sub(r'$\1$', line)1410 outFile.write(line)1411 inFile.close()1412 outFile.close()1413# Forcibly overwrite the original file1414 os.unlink(file)1415 shutil.move(outFileName,file)1416except:1417# cleanup our temporary file1418 os.unlink(outFileName)1419print"Failed to strip RCS keywords in%s"%file1420raise14211422print"Patched up RCS keywords in%s"%file14231424defp4UserForCommit(self,id):1425# Return the tuple (perforce user,git email) for a given git commit id1426 self.getUserMapFromPerforceServer()1427 gitEmail =read_pipe(["git","log","--max-count=1",1428"--format=%ae",id])1429 gitEmail = gitEmail.strip()1430if not self.emails.has_key(gitEmail):1431return(None,gitEmail)1432else:1433return(self.emails[gitEmail],gitEmail)14341435defcheckValidP4Users(self,commits):1436# check if any git authors cannot be mapped to p4 users1437foridin commits:1438(user,email) = self.p4UserForCommit(id)1439if not user:1440 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1441ifgitConfigBool("git-p4.allowMissingP4Users"):1442print"%s"% msg1443else:1444die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14451446deflastP4Changelist(self):1447# Get back the last changelist number submitted in this client spec. This1448# then gets used to patch up the username in the change. If the same1449# client spec is being used by multiple processes then this might go1450# wrong.1451 results =p4CmdList("client -o")# find the current client1452 client =None1453for r in results:1454if r.has_key('Client'):1455 client = r['Client']1456break1457if not client:1458die("could not get client spec")1459 results =p4CmdList(["changes","-c", client,"-m","1"])1460for r in results:1461if r.has_key('change'):1462return r['change']1463die("Could not get changelist number for last submit - cannot patch up user details")14641465defmodifyChangelistUser(self, changelist, newUser):1466# fixup the user field of a changelist after it has been submitted.1467 changes =p4CmdList("change -o%s"% changelist)1468iflen(changes) !=1:1469die("Bad output from p4 change modifying%sto user%s"%1470(changelist, newUser))14711472 c = changes[0]1473if c['User'] == newUser:return# nothing to do1474 c['User'] = newUser1475input= marshal.dumps(c)14761477 result =p4CmdList("change -f -i", stdin=input)1478for r in result:1479if r.has_key('code'):1480if r['code'] =='error':1481die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1482if r.has_key('data'):1483print("Updated user field for changelist%sto%s"% (changelist, newUser))1484return1485die("Could not modify user field of changelist%sto%s"% (changelist, newUser))14861487defcanChangeChangelists(self):1488# check to see if we have p4 admin or super-user permissions, either of1489# which are required to modify changelists.1490 results =p4CmdList(["protects", self.depotPath])1491for r in results:1492if r.has_key('perm'):1493if r['perm'] =='admin':1494return11495if r['perm'] =='super':1496return11497return014981499defprepareSubmitTemplate(self, changelist=None):1500"""Run "p4 change -o" to grab a change specification template.1501 This does not use "p4 -G", as it is nice to keep the submission1502 template in original order, since a human might edit it.15031504 Remove lines in the Files section that show changes to files1505 outside the depot path we're committing into."""15061507[upstream, settings] =findUpstreamBranchPoint()15081509 template =""1510 inFilesSection =False1511 args = ['change','-o']1512if changelist:1513 args.append(str(changelist))15141515for line inp4_read_pipe_lines(args):1516if line.endswith("\r\n"):1517 line = line[:-2] +"\n"1518if inFilesSection:1519if line.startswith("\t"):1520# path starts and ends with a tab1521 path = line[1:]1522 lastTab = path.rfind("\t")1523if lastTab != -1:1524 path = path[:lastTab]1525if settings.has_key('depot-paths'):1526if not[p for p in settings['depot-paths']1527ifp4PathStartsWith(path, p)]:1528continue1529else:1530if notp4PathStartsWith(path, self.depotPath):1531continue1532else:1533 inFilesSection =False1534else:1535if line.startswith("Files:"):1536 inFilesSection =True15371538 template += line15391540return template15411542defedit_template(self, template_file):1543"""Invoke the editor to let the user change the submission1544 message. Return true if okay to continue with the submit."""15451546# if configured to skip the editing part, just submit1547ifgitConfigBool("git-p4.skipSubmitEdit"):1548return True15491550# look at the modification time, to check later if the user saved1551# the file1552 mtime = os.stat(template_file).st_mtime15531554# invoke the editor1555if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1556 editor = os.environ.get("P4EDITOR")1557else:1558 editor =read_pipe("git var GIT_EDITOR").strip()1559system(["sh","-c", ('%s"$@"'% editor), editor, template_file])15601561# If the file was not saved, prompt to see if this patch should1562# be skipped. But skip this verification step if configured so.1563ifgitConfigBool("git-p4.skipSubmitEditCheck"):1564return True15651566# modification time updated means user saved the file1567if os.stat(template_file).st_mtime > mtime:1568return True15691570while True:1571 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1572if response =='y':1573return True1574if response =='n':1575return False15761577defget_diff_description(self, editedFiles, filesToAdd, symlinks):1578# diff1579if os.environ.has_key("P4DIFF"):1580del(os.environ["P4DIFF"])1581 diff =""1582for editedFile in editedFiles:1583 diff +=p4_read_pipe(['diff','-du',1584wildcard_encode(editedFile)])15851586# new file diff1587 newdiff =""1588for newFile in filesToAdd:1589 newdiff +="==== new file ====\n"1590 newdiff +="--- /dev/null\n"1591 newdiff +="+++%s\n"% newFile15921593 is_link = os.path.islink(newFile)1594 expect_link = newFile in symlinks15951596if is_link and expect_link:1597 newdiff +="+%s\n"% os.readlink(newFile)1598else:1599 f =open(newFile,"r")1600for line in f.readlines():1601 newdiff +="+"+ line1602 f.close()16031604return(diff + newdiff).replace('\r\n','\n')16051606defapplyCommit(self,id):1607"""Apply one commit, return True if it succeeded."""16081609print"Applying",read_pipe(["git","show","-s",1610"--format=format:%h%s",id])16111612(p4User, gitEmail) = self.p4UserForCommit(id)16131614 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1615 filesToAdd =set()1616 filesToChangeType =set()1617 filesToDelete =set()1618 editedFiles =set()1619 pureRenameCopy =set()1620 symlinks =set()1621 filesToChangeExecBit = {}1622 all_files =list()16231624for line in diff:1625 diff =parseDiffTreeEntry(line)1626 modifier = diff['status']1627 path = diff['src']1628 all_files.append(path)16291630if modifier =="M":1631p4_edit(path)1632ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1633 filesToChangeExecBit[path] = diff['dst_mode']1634 editedFiles.add(path)1635elif modifier =="A":1636 filesToAdd.add(path)1637 filesToChangeExecBit[path] = diff['dst_mode']1638if path in filesToDelete:1639 filesToDelete.remove(path)16401641 dst_mode =int(diff['dst_mode'],8)1642if dst_mode ==0120000:1643 symlinks.add(path)16441645elif modifier =="D":1646 filesToDelete.add(path)1647if path in filesToAdd:1648 filesToAdd.remove(path)1649elif modifier =="C":1650 src, dest = diff['src'], diff['dst']1651p4_integrate(src, dest)1652 pureRenameCopy.add(dest)1653if diff['src_sha1'] != diff['dst_sha1']:1654p4_edit(dest)1655 pureRenameCopy.discard(dest)1656ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1657p4_edit(dest)1658 pureRenameCopy.discard(dest)1659 filesToChangeExecBit[dest] = diff['dst_mode']1660if self.isWindows:1661# turn off read-only attribute1662 os.chmod(dest, stat.S_IWRITE)1663 os.unlink(dest)1664 editedFiles.add(dest)1665elif modifier =="R":1666 src, dest = diff['src'], diff['dst']1667if self.p4HasMoveCommand:1668p4_edit(src)# src must be open before move1669p4_move(src, dest)# opens for (move/delete, move/add)1670else:1671p4_integrate(src, dest)1672if diff['src_sha1'] != diff['dst_sha1']:1673p4_edit(dest)1674else:1675 pureRenameCopy.add(dest)1676ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1677if not self.p4HasMoveCommand:1678p4_edit(dest)# with move: already open, writable1679 filesToChangeExecBit[dest] = diff['dst_mode']1680if not self.p4HasMoveCommand:1681if self.isWindows:1682 os.chmod(dest, stat.S_IWRITE)1683 os.unlink(dest)1684 filesToDelete.add(src)1685 editedFiles.add(dest)1686elif modifier =="T":1687 filesToChangeType.add(path)1688else:1689die("unknown modifier%sfor%s"% (modifier, path))16901691 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1692 patchcmd = diffcmd +" | git apply "1693 tryPatchCmd = patchcmd +"--check -"1694 applyPatchCmd = patchcmd +"--check --apply -"1695 patch_succeeded =True16961697if os.system(tryPatchCmd) !=0:1698 fixed_rcs_keywords =False1699 patch_succeeded =False1700print"Unfortunately applying the change failed!"17011702# Patch failed, maybe it's just RCS keyword woes. Look through1703# the patch to see if that's possible.1704ifgitConfigBool("git-p4.attemptRCSCleanup"):1705file=None1706 pattern =None1707 kwfiles = {}1708forfilein editedFiles | filesToDelete:1709# did this file's delta contain RCS keywords?1710 pattern =p4_keywords_regexp_for_file(file)17111712if pattern:1713# this file is a possibility...look for RCS keywords.1714 regexp = re.compile(pattern, re.VERBOSE)1715for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1716if regexp.search(line):1717if verbose:1718print"got keyword match on%sin%sin%s"% (pattern, line,file)1719 kwfiles[file] = pattern1720break17211722forfilein kwfiles:1723if verbose:1724print"zapping%swith%s"% (line,pattern)1725# File is being deleted, so not open in p4. Must1726# disable the read-only bit on windows.1727if self.isWindows andfilenot in editedFiles:1728 os.chmod(file, stat.S_IWRITE)1729 self.patchRCSKeywords(file, kwfiles[file])1730 fixed_rcs_keywords =True17311732if fixed_rcs_keywords:1733print"Retrying the patch with RCS keywords cleaned up"1734if os.system(tryPatchCmd) ==0:1735 patch_succeeded =True17361737if not patch_succeeded:1738for f in editedFiles:1739p4_revert(f)1740return False17411742#1743# Apply the patch for real, and do add/delete/+x handling.1744#1745system(applyPatchCmd)17461747for f in filesToChangeType:1748p4_edit(f,"-t","auto")1749for f in filesToAdd:1750p4_add(f)1751for f in filesToDelete:1752p4_revert(f)1753p4_delete(f)17541755# Set/clear executable bits1756for f in filesToChangeExecBit.keys():1757 mode = filesToChangeExecBit[f]1758setP4ExecBit(f, mode)17591760if self.update_shelve:1761print("all_files =%s"%str(all_files))1762p4_reopen_in_change(self.update_shelve, all_files)17631764#1765# Build p4 change description, starting with the contents1766# of the git commit message.1767#1768 logMessage =extractLogMessageFromGitCommit(id)1769 logMessage = logMessage.strip()1770(logMessage, jobs) = self.separate_jobs_from_description(logMessage)17711772 template = self.prepareSubmitTemplate(self.update_shelve)1773 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)17741775if self.preserveUser:1776 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User17771778if self.checkAuthorship and not self.p4UserIsMe(p4User):1779 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1780 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1781 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"17821783 separatorLine ="######## everything below this line is just the diff #######\n"1784if not self.prepare_p4_only:1785 submitTemplate += separatorLine1786 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)17871788(handle, fileName) = tempfile.mkstemp()1789 tmpFile = os.fdopen(handle,"w+b")1790if self.isWindows:1791 submitTemplate = submitTemplate.replace("\n","\r\n")1792 tmpFile.write(submitTemplate)1793 tmpFile.close()17941795if self.prepare_p4_only:1796#1797# Leave the p4 tree prepared, and the submit template around1798# and let the user decide what to do next1799#1800print1801print"P4 workspace prepared for submission."1802print"To submit or revert, go to client workspace"1803print" "+ self.clientPath1804print1805print"To submit, use\"p4 submit\"to write a new description,"1806print"or\"p4 submit -i <%s\"to use the one prepared by" \1807"\"git p4\"."% fileName1808print"You can delete the file\"%s\"when finished."% fileName18091810if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1811print"To preserve change ownership by user%s, you must\n" \1812"do\"p4 change -f <change>\"after submitting and\n" \1813"edit the User field."1814if pureRenameCopy:1815print"After submitting, renamed files must be re-synced."1816print"Invoke\"p4 sync -f\"on each of these files:"1817for f in pureRenameCopy:1818print" "+ f18191820print1821print"To revert the changes, use\"p4 revert ...\", and delete"1822print"the submit template file\"%s\""% fileName1823if filesToAdd:1824print"Since the commit adds new files, they must be deleted:"1825for f in filesToAdd:1826print" "+ f1827print1828return True18291830#1831# Let the user edit the change description, then submit it.1832#1833 submitted =False18341835try:1836if self.edit_template(fileName):1837# read the edited message and submit1838 tmpFile =open(fileName,"rb")1839 message = tmpFile.read()1840 tmpFile.close()1841if self.isWindows:1842 message = message.replace("\r\n","\n")1843 submitTemplate = message[:message.index(separatorLine)]18441845if self.update_shelve:1846p4_write_pipe(['shelve','-r','-i'], submitTemplate)1847elif self.shelve:1848p4_write_pipe(['shelve','-i'], submitTemplate)1849else:1850p4_write_pipe(['submit','-i'], submitTemplate)1851# The rename/copy happened by applying a patch that created a1852# new file. This leaves it writable, which confuses p4.1853for f in pureRenameCopy:1854p4_sync(f,"-f")18551856if self.preserveUser:1857if p4User:1858# Get last changelist number. Cannot easily get it from1859# the submit command output as the output is1860# unmarshalled.1861 changelist = self.lastP4Changelist()1862 self.modifyChangelistUser(changelist, p4User)18631864 submitted =True18651866finally:1867# skip this patch1868if not submitted or self.shelve:1869if self.shelve:1870print("Reverting shelved files.")1871else:1872print("Submission cancelled, undoing p4 changes.")1873for f in editedFiles | filesToDelete:1874p4_revert(f)1875for f in filesToAdd:1876p4_revert(f)1877 os.remove(f)18781879 os.remove(fileName)1880return submitted18811882# Export git tags as p4 labels. Create a p4 label and then tag1883# with that.1884defexportGitTags(self, gitTags):1885 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1886iflen(validLabelRegexp) ==0:1887 validLabelRegexp = defaultLabelRegexp1888 m = re.compile(validLabelRegexp)18891890for name in gitTags:18911892if not m.match(name):1893if verbose:1894print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1895continue18961897# Get the p4 commit this corresponds to1898 logMessage =extractLogMessageFromGitCommit(name)1899 values =extractSettingsGitLog(logMessage)19001901if not values.has_key('change'):1902# a tag pointing to something not sent to p4; ignore1903if verbose:1904print"git tag%sdoes not give a p4 commit"% name1905continue1906else:1907 changelist = values['change']19081909# Get the tag details.1910 inHeader =True1911 isAnnotated =False1912 body = []1913for l inread_pipe_lines(["git","cat-file","-p", name]):1914 l = l.strip()1915if inHeader:1916if re.match(r'tag\s+', l):1917 isAnnotated =True1918elif re.match(r'\s*$', l):1919 inHeader =False1920continue1921else:1922 body.append(l)19231924if not isAnnotated:1925 body = ["lightweight tag imported by git p4\n"]19261927# Create the label - use the same view as the client spec we are using1928 clientSpec =getClientSpec()19291930 labelTemplate ="Label:%s\n"% name1931 labelTemplate +="Description:\n"1932for b in body:1933 labelTemplate +="\t"+ b +"\n"1934 labelTemplate +="View:\n"1935for depot_side in clientSpec.mappings:1936 labelTemplate +="\t%s\n"% depot_side19371938if self.dry_run:1939print"Would create p4 label%sfor tag"% name1940elif self.prepare_p4_only:1941print"Not creating p4 label%sfor tag due to option" \1942" --prepare-p4-only"% name1943else:1944p4_write_pipe(["label","-i"], labelTemplate)19451946# Use the label1947p4_system(["tag","-l", name] +1948["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])19491950if verbose:1951print"created p4 label for tag%s"% name19521953defrun(self, args):1954iflen(args) ==0:1955 self.master =currentGitBranch()1956eliflen(args) ==1:1957 self.master = args[0]1958if notbranchExists(self.master):1959die("Branch%sdoes not exist"% self.master)1960else:1961return False19621963if self.master:1964 allowSubmit =gitConfig("git-p4.allowSubmit")1965iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1966die("%sis not in git-p4.allowSubmit"% self.master)19671968[upstream, settings] =findUpstreamBranchPoint()1969 self.depotPath = settings['depot-paths'][0]1970iflen(self.origin) ==0:1971 self.origin = upstream19721973if self.update_shelve:1974 self.shelve =True19751976if self.preserveUser:1977if not self.canChangeChangelists():1978die("Cannot preserve user names without p4 super-user or admin permissions")19791980# if not set from the command line, try the config file1981if self.conflict_behavior is None:1982 val =gitConfig("git-p4.conflict")1983if val:1984if val not in self.conflict_behavior_choices:1985die("Invalid value '%s' for config git-p4.conflict"% val)1986else:1987 val ="ask"1988 self.conflict_behavior = val19891990if self.verbose:1991print"Origin branch is "+ self.origin19921993iflen(self.depotPath) ==0:1994print"Internal error: cannot locate perforce depot path from existing branches"1995 sys.exit(128)19961997 self.useClientSpec =False1998ifgitConfigBool("git-p4.useclientspec"):1999 self.useClientSpec =True2000if self.useClientSpec:2001 self.clientSpecDirs =getClientSpec()20022003# Check for the existence of P4 branches2004 branchesDetected = (len(p4BranchesInGit().keys()) >1)20052006if self.useClientSpec and not branchesDetected:2007# all files are relative to the client spec2008 self.clientPath =getClientRoot()2009else:2010 self.clientPath =p4Where(self.depotPath)20112012if self.clientPath =="":2013die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)20142015print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2016 self.oldWorkingDirectory = os.getcwd()20172018# ensure the clientPath exists2019 new_client_dir =False2020if not os.path.exists(self.clientPath):2021 new_client_dir =True2022 os.makedirs(self.clientPath)20232024chdir(self.clientPath, is_client_path=True)2025if self.dry_run:2026print"Would synchronize p4 checkout in%s"% self.clientPath2027else:2028print"Synchronizing p4 checkout..."2029if new_client_dir:2030# old one was destroyed, and maybe nobody told p42031p4_sync("...","-f")2032else:2033p4_sync("...")2034 self.check()20352036 commits = []2037if self.master:2038 commitish = self.master2039else:2040 commitish ='HEAD'20412042for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2043 commits.append(line.strip())2044 commits.reverse()20452046if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2047 self.checkAuthorship =False2048else:2049 self.checkAuthorship =True20502051if self.preserveUser:2052 self.checkValidP4Users(commits)20532054#2055# Build up a set of options to be passed to diff when2056# submitting each commit to p4.2057#2058if self.detectRenames:2059# command-line -M arg2060 self.diffOpts ="-M"2061else:2062# If not explicitly set check the config variable2063 detectRenames =gitConfig("git-p4.detectRenames")20642065if detectRenames.lower() =="false"or detectRenames =="":2066 self.diffOpts =""2067elif detectRenames.lower() =="true":2068 self.diffOpts ="-M"2069else:2070 self.diffOpts ="-M%s"% detectRenames20712072# no command-line arg for -C or --find-copies-harder, just2073# config variables2074 detectCopies =gitConfig("git-p4.detectCopies")2075if detectCopies.lower() =="false"or detectCopies =="":2076pass2077elif detectCopies.lower() =="true":2078 self.diffOpts +=" -C"2079else:2080 self.diffOpts +=" -C%s"% detectCopies20812082ifgitConfigBool("git-p4.detectCopiesHarder"):2083 self.diffOpts +=" --find-copies-harder"20842085#2086# Apply the commits, one at a time. On failure, ask if should2087# continue to try the rest of the patches, or quit.2088#2089if self.dry_run:2090print"Would apply"2091 applied = []2092 last =len(commits) -12093for i, commit inenumerate(commits):2094if self.dry_run:2095print" ",read_pipe(["git","show","-s",2096"--format=format:%h%s", commit])2097 ok =True2098else:2099 ok = self.applyCommit(commit)2100if ok:2101 applied.append(commit)2102else:2103if self.prepare_p4_only and i < last:2104print"Processing only the first commit due to option" \2105" --prepare-p4-only"2106break2107if i < last:2108 quit =False2109while True:2110# prompt for what to do, or use the option/variable2111if self.conflict_behavior =="ask":2112print"What do you want to do?"2113 response =raw_input("[s]kip this commit but apply"2114" the rest, or [q]uit? ")2115if not response:2116continue2117elif self.conflict_behavior =="skip":2118 response ="s"2119elif self.conflict_behavior =="quit":2120 response ="q"2121else:2122die("Unknown conflict_behavior '%s'"%2123 self.conflict_behavior)21242125if response[0] =="s":2126print"Skipping this commit, but applying the rest"2127break2128if response[0] =="q":2129print"Quitting"2130 quit =True2131break2132if quit:2133break21342135chdir(self.oldWorkingDirectory)2136 shelved_applied ="shelved"if self.shelve else"applied"2137if self.dry_run:2138pass2139elif self.prepare_p4_only:2140pass2141eliflen(commits) ==len(applied):2142print("All commits{0}!".format(shelved_applied))21432144 sync =P4Sync()2145if self.branch:2146 sync.branch = self.branch2147 sync.run([])21482149 rebase =P4Rebase()2150 rebase.rebase()21512152else:2153iflen(applied) ==0:2154print("No commits{0}.".format(shelved_applied))2155else:2156print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2157for c in commits:2158if c in applied:2159 star ="*"2160else:2161 star =" "2162print star,read_pipe(["git","show","-s",2163"--format=format:%h%s", c])2164print"You will have to do 'git p4 sync' and rebase."21652166ifgitConfigBool("git-p4.exportLabels"):2167 self.exportLabels =True21682169if self.exportLabels:2170 p4Labels =getP4Labels(self.depotPath)2171 gitTags =getGitTags()21722173 missingGitTags = gitTags - p4Labels2174 self.exportGitTags(missingGitTags)21752176# exit with error unless everything applied perfectly2177iflen(commits) !=len(applied):2178 sys.exit(1)21792180return True21812182classView(object):2183"""Represent a p4 view ("p4 help views"), and map files in a2184 repo according to the view."""21852186def__init__(self, client_name):2187 self.mappings = []2188 self.client_prefix ="//%s/"% client_name2189# cache results of "p4 where" to lookup client file locations2190 self.client_spec_path_cache = {}21912192defappend(self, view_line):2193"""Parse a view line, splitting it into depot and client2194 sides. Append to self.mappings, preserving order. This2195 is only needed for tag creation."""21962197# Split the view line into exactly two words. P4 enforces2198# structure on these lines that simplifies this quite a bit.2199#2200# Either or both words may be double-quoted.2201# Single quotes do not matter.2202# Double-quote marks cannot occur inside the words.2203# A + or - prefix is also inside the quotes.2204# There are no quotes unless they contain a space.2205# The line is already white-space stripped.2206# The two words are separated by a single space.2207#2208if view_line[0] =='"':2209# First word is double quoted. Find its end.2210 close_quote_index = view_line.find('"',1)2211if close_quote_index <=0:2212die("No first-word closing quote found:%s"% view_line)2213 depot_side = view_line[1:close_quote_index]2214# skip closing quote and space2215 rhs_index = close_quote_index +1+12216else:2217 space_index = view_line.find(" ")2218if space_index <=0:2219die("No word-splitting space found:%s"% view_line)2220 depot_side = view_line[0:space_index]2221 rhs_index = space_index +122222223# prefix + means overlay on previous mapping2224if depot_side.startswith("+"):2225 depot_side = depot_side[1:]22262227# prefix - means exclude this path, leave out of mappings2228 exclude =False2229if depot_side.startswith("-"):2230 exclude =True2231 depot_side = depot_side[1:]22322233if not exclude:2234 self.mappings.append(depot_side)22352236defconvert_client_path(self, clientFile):2237# chop off //client/ part to make it relative2238if not clientFile.startswith(self.client_prefix):2239die("No prefix '%s' on clientFile '%s'"%2240(self.client_prefix, clientFile))2241return clientFile[len(self.client_prefix):]22422243defupdate_client_spec_path_cache(self, files):2244""" Caching file paths by "p4 where" batch query """22452246# List depot file paths exclude that already cached2247 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]22482249iflen(fileArgs) ==0:2250return# All files in cache22512252 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2253for res in where_result:2254if"code"in res and res["code"] =="error":2255# assume error is "... file(s) not in client view"2256continue2257if"clientFile"not in res:2258die("No clientFile in 'p4 where' output")2259if"unmap"in res:2260# it will list all of them, but only one not unmap-ped2261continue2262ifgitConfigBool("core.ignorecase"):2263 res['depotFile'] = res['depotFile'].lower()2264 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])22652266# not found files or unmap files set to ""2267for depotFile in fileArgs:2268ifgitConfigBool("core.ignorecase"):2269 depotFile = depotFile.lower()2270if depotFile not in self.client_spec_path_cache:2271 self.client_spec_path_cache[depotFile] =""22722273defmap_in_client(self, depot_path):2274"""Return the relative location in the client where this2275 depot file should live. Returns "" if the file should2276 not be mapped in the client."""22772278ifgitConfigBool("core.ignorecase"):2279 depot_path = depot_path.lower()22802281if depot_path in self.client_spec_path_cache:2282return self.client_spec_path_cache[depot_path]22832284die("Error:%sis not found in client spec path"% depot_path )2285return""22862287classP4Sync(Command, P4UserMap):2288 delete_actions = ("delete","move/delete","purge")22892290def__init__(self):2291 Command.__init__(self)2292 P4UserMap.__init__(self)2293 self.options = [2294 optparse.make_option("--branch", dest="branch"),2295 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2296 optparse.make_option("--changesfile", dest="changesFile"),2297 optparse.make_option("--silent", dest="silent", action="store_true"),2298 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2299 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2300 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2301help="Import into refs/heads/ , not refs/remotes"),2302 optparse.make_option("--max-changes", dest="maxChanges",2303help="Maximum number of changes to import"),2304 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2305help="Internal block size to use when iteratively calling p4 changes"),2306 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2307help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2308 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2309help="Only sync files that are included in the Perforce Client Spec"),2310 optparse.make_option("-/", dest="cloneExclude",2311 action="append",type="string",2312help="exclude depot path"),2313]2314 self.description ="""Imports from Perforce into a git repository.\n2315 example:2316 //depot/my/project/ -- to import the current head2317 //depot/my/project/@all -- to import everything2318 //depot/my/project/@1,6 -- to import only from revision 1 to 623192320 (a ... is not needed in the path p4 specification, it's added implicitly)"""23212322 self.usage +=" //depot/path[@revRange]"2323 self.silent =False2324 self.createdBranches =set()2325 self.committedChanges =set()2326 self.branch =""2327 self.detectBranches =False2328 self.detectLabels =False2329 self.importLabels =False2330 self.changesFile =""2331 self.syncWithOrigin =True2332 self.importIntoRemotes =True2333 self.maxChanges =""2334 self.changes_block_size =None2335 self.keepRepoPath =False2336 self.depotPaths =None2337 self.p4BranchesInGit = []2338 self.cloneExclude = []2339 self.useClientSpec =False2340 self.useClientSpec_from_options =False2341 self.clientSpecDirs =None2342 self.tempBranches = []2343 self.tempBranchLocation ="refs/git-p4-tmp"2344 self.largeFileSystem =None23452346ifgitConfig('git-p4.largeFileSystem'):2347 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2348 self.largeFileSystem =largeFileSystemConstructor(2349lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2350)23512352ifgitConfig("git-p4.syncFromOrigin") =="false":2353 self.syncWithOrigin =False23542355# This is required for the "append" cloneExclude action2356defensure_value(self, attr, value):2357if nothasattr(self, attr)orgetattr(self, attr)is None:2358setattr(self, attr, value)2359returngetattr(self, attr)23602361# Force a checkpoint in fast-import and wait for it to finish2362defcheckpoint(self):2363 self.gitStream.write("checkpoint\n\n")2364 self.gitStream.write("progress checkpoint\n\n")2365 out = self.gitOutput.readline()2366if self.verbose:2367print"checkpoint finished: "+ out23682369defextractFilesFromCommit(self, commit):2370 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2371for path in self.cloneExclude]2372 files = []2373 fnum =02374while commit.has_key("depotFile%s"% fnum):2375 path = commit["depotFile%s"% fnum]23762377if[p for p in self.cloneExclude2378ifp4PathStartsWith(path, p)]:2379 found =False2380else:2381 found = [p for p in self.depotPaths2382ifp4PathStartsWith(path, p)]2383if not found:2384 fnum = fnum +12385continue23862387file= {}2388file["path"] = path2389file["rev"] = commit["rev%s"% fnum]2390file["action"] = commit["action%s"% fnum]2391file["type"] = commit["type%s"% fnum]2392 files.append(file)2393 fnum = fnum +12394return files23952396defextractJobsFromCommit(self, commit):2397 jobs = []2398 jnum =02399while commit.has_key("job%s"% jnum):2400 job = commit["job%s"% jnum]2401 jobs.append(job)2402 jnum = jnum +12403return jobs24042405defstripRepoPath(self, path, prefixes):2406"""When streaming files, this is called to map a p4 depot path2407 to where it should go in git. The prefixes are either2408 self.depotPaths, or self.branchPrefixes in the case of2409 branch detection."""24102411if self.useClientSpec:2412# branch detection moves files up a level (the branch name)2413# from what client spec interpretation gives2414 path = self.clientSpecDirs.map_in_client(path)2415if self.detectBranches:2416for b in self.knownBranches:2417if path.startswith(b +"/"):2418 path = path[len(b)+1:]24192420elif self.keepRepoPath:2421# Preserve everything in relative path name except leading2422# //depot/; just look at first prefix as they all should2423# be in the same depot.2424 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2425ifp4PathStartsWith(path, depot):2426 path = path[len(depot):]24272428else:2429for p in prefixes:2430ifp4PathStartsWith(path, p):2431 path = path[len(p):]2432break24332434 path =wildcard_decode(path)2435return path24362437defsplitFilesIntoBranches(self, commit):2438"""Look at each depotFile in the commit to figure out to what2439 branch it belongs."""24402441if self.clientSpecDirs:2442 files = self.extractFilesFromCommit(commit)2443 self.clientSpecDirs.update_client_spec_path_cache(files)24442445 branches = {}2446 fnum =02447while commit.has_key("depotFile%s"% fnum):2448 path = commit["depotFile%s"% fnum]2449 found = [p for p in self.depotPaths2450ifp4PathStartsWith(path, p)]2451if not found:2452 fnum = fnum +12453continue24542455file= {}2456file["path"] = path2457file["rev"] = commit["rev%s"% fnum]2458file["action"] = commit["action%s"% fnum]2459file["type"] = commit["type%s"% fnum]2460 fnum = fnum +124612462# start with the full relative path where this file would2463# go in a p4 client2464if self.useClientSpec:2465 relPath = self.clientSpecDirs.map_in_client(path)2466else:2467 relPath = self.stripRepoPath(path, self.depotPaths)24682469for branch in self.knownBranches.keys():2470# add a trailing slash so that a commit into qt/4.2foo2471# doesn't end up in qt/4.2, e.g.2472if relPath.startswith(branch +"/"):2473if branch not in branches:2474 branches[branch] = []2475 branches[branch].append(file)2476break24772478return branches24792480defwriteToGitStream(self, gitMode, relPath, contents):2481 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2482 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2483for d in contents:2484 self.gitStream.write(d)2485 self.gitStream.write('\n')24862487# output one file from the P4 stream2488# - helper for streamP4Files24892490defstreamOneP4File(self,file, contents):2491 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2492if verbose:2493 size =int(self.stream_file['fileSize'])2494 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2495 sys.stdout.flush()24962497(type_base, type_mods) =split_p4_type(file["type"])24982499 git_mode ="100644"2500if"x"in type_mods:2501 git_mode ="100755"2502if type_base =="symlink":2503 git_mode ="120000"2504# p4 print on a symlink sometimes contains "target\n";2505# if it does, remove the newline2506 data =''.join(contents)2507if not data:2508# Some version of p4 allowed creating a symlink that pointed2509# to nothing. This causes p4 errors when checking out such2510# a change, and errors here too. Work around it by ignoring2511# the bad symlink; hopefully a future change fixes it.2512print"\nIgnoring empty symlink in%s"%file['depotFile']2513return2514elif data[-1] =='\n':2515 contents = [data[:-1]]2516else:2517 contents = [data]25182519if type_base =="utf16":2520# p4 delivers different text in the python output to -G2521# than it does when using "print -o", or normal p4 client2522# operations. utf16 is converted to ascii or utf8, perhaps.2523# But ascii text saved as -t utf16 is completely mangled.2524# Invoke print -o to get the real contents.2525#2526# On windows, the newlines will always be mangled by print, so put2527# them back too. This is not needed to the cygwin windows version,2528# just the native "NT" type.2529#2530try:2531 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2532exceptExceptionas e:2533if'Translation of file content failed'instr(e):2534 type_base ='binary'2535else:2536raise e2537else:2538ifp4_version_string().find('/NT') >=0:2539 text = text.replace('\r\n','\n')2540 contents = [ text ]25412542if type_base =="apple":2543# Apple filetype files will be streamed as a concatenation of2544# its appledouble header and the contents. This is useless2545# on both macs and non-macs. If using "print -q -o xx", it2546# will create "xx" with the data, and "%xx" with the header.2547# This is also not very useful.2548#2549# Ideally, someday, this script can learn how to generate2550# appledouble files directly and import those to git, but2551# non-mac machines can never find a use for apple filetype.2552print"\nIgnoring apple filetype file%s"%file['depotFile']2553return25542555# Note that we do not try to de-mangle keywords on utf16 files,2556# even though in theory somebody may want that.2557 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2558if pattern:2559 regexp = re.compile(pattern, re.VERBOSE)2560 text =''.join(contents)2561 text = regexp.sub(r'$\1$', text)2562 contents = [ text ]25632564try:2565 relPath.decode('ascii')2566except:2567 encoding ='utf8'2568ifgitConfig('git-p4.pathEncoding'):2569 encoding =gitConfig('git-p4.pathEncoding')2570 relPath = relPath.decode(encoding,'replace').encode('utf8','replace')2571if self.verbose:2572print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, relPath)25732574if self.largeFileSystem:2575(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)25762577 self.writeToGitStream(git_mode, relPath, contents)25782579defstreamOneP4Deletion(self,file):2580 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2581if verbose:2582 sys.stdout.write("delete%s\n"% relPath)2583 sys.stdout.flush()2584 self.gitStream.write("D%s\n"% relPath)25852586if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2587 self.largeFileSystem.removeLargeFile(relPath)25882589# handle another chunk of streaming data2590defstreamP4FilesCb(self, marshalled):25912592# catch p4 errors and complain2593 err =None2594if"code"in marshalled:2595if marshalled["code"] =="error":2596if"data"in marshalled:2597 err = marshalled["data"].rstrip()25982599if not err and'fileSize'in self.stream_file:2600 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2601if required_bytes >0:2602 err ='Not enough space left on%s! Free at least%iMB.'% (2603 os.getcwd(), required_bytes/1024/10242604)26052606if err:2607 f =None2608if self.stream_have_file_info:2609if"depotFile"in self.stream_file:2610 f = self.stream_file["depotFile"]2611# force a failure in fast-import, else an empty2612# commit will be made2613 self.gitStream.write("\n")2614 self.gitStream.write("die-now\n")2615 self.gitStream.close()2616# ignore errors, but make sure it exits first2617 self.importProcess.wait()2618if f:2619die("Error from p4 print for%s:%s"% (f, err))2620else:2621die("Error from p4 print:%s"% err)26222623if marshalled.has_key('depotFile')and self.stream_have_file_info:2624# start of a new file - output the old one first2625 self.streamOneP4File(self.stream_file, self.stream_contents)2626 self.stream_file = {}2627 self.stream_contents = []2628 self.stream_have_file_info =False26292630# pick up the new file information... for the2631# 'data' field we need to append to our array2632for k in marshalled.keys():2633if k =='data':2634if'streamContentSize'not in self.stream_file:2635 self.stream_file['streamContentSize'] =02636 self.stream_file['streamContentSize'] +=len(marshalled['data'])2637 self.stream_contents.append(marshalled['data'])2638else:2639 self.stream_file[k] = marshalled[k]26402641if(verbose and2642'streamContentSize'in self.stream_file and2643'fileSize'in self.stream_file and2644'depotFile'in self.stream_file):2645 size =int(self.stream_file["fileSize"])2646if size >0:2647 progress =100*self.stream_file['streamContentSize']/size2648 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2649 sys.stdout.flush()26502651 self.stream_have_file_info =True26522653# Stream directly from "p4 files" into "git fast-import"2654defstreamP4Files(self, files):2655 filesForCommit = []2656 filesToRead = []2657 filesToDelete = []26582659for f in files:2660 filesForCommit.append(f)2661if f['action']in self.delete_actions:2662 filesToDelete.append(f)2663else:2664 filesToRead.append(f)26652666# deleted files...2667for f in filesToDelete:2668 self.streamOneP4Deletion(f)26692670iflen(filesToRead) >0:2671 self.stream_file = {}2672 self.stream_contents = []2673 self.stream_have_file_info =False26742675# curry self argument2676defstreamP4FilesCbSelf(entry):2677 self.streamP4FilesCb(entry)26782679 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]26802681p4CmdList(["-x","-","print"],2682 stdin=fileArgs,2683 cb=streamP4FilesCbSelf)26842685# do the last chunk2686if self.stream_file.has_key('depotFile'):2687 self.streamOneP4File(self.stream_file, self.stream_contents)26882689defmake_email(self, userid):2690if userid in self.users:2691return self.users[userid]2692else:2693return"%s<a@b>"% userid26942695defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2696""" Stream a p4 tag.2697 commit is either a git commit, or a fast-import mark, ":<p4commit>"2698 """26992700if verbose:2701print"writing tag%sfor commit%s"% (labelName, commit)2702 gitStream.write("tag%s\n"% labelName)2703 gitStream.write("from%s\n"% commit)27042705if labelDetails.has_key('Owner'):2706 owner = labelDetails["Owner"]2707else:2708 owner =None27092710# Try to use the owner of the p4 label, or failing that,2711# the current p4 user id.2712if owner:2713 email = self.make_email(owner)2714else:2715 email = self.make_email(self.p4UserId())2716 tagger ="%s %s %s"% (email, epoch, self.tz)27172718 gitStream.write("tagger%s\n"% tagger)27192720print"labelDetails=",labelDetails2721if labelDetails.has_key('Description'):2722 description = labelDetails['Description']2723else:2724 description ='Label from git p4'27252726 gitStream.write("data%d\n"%len(description))2727 gitStream.write(description)2728 gitStream.write("\n")27292730definClientSpec(self, path):2731if not self.clientSpecDirs:2732return True2733 inClientSpec = self.clientSpecDirs.map_in_client(path)2734if not inClientSpec and self.verbose:2735print('Ignoring file outside of client spec:{0}'.format(path))2736return inClientSpec27372738defhasBranchPrefix(self, path):2739if not self.branchPrefixes:2740return True2741 hasPrefix = [p for p in self.branchPrefixes2742ifp4PathStartsWith(path, p)]2743if not hasPrefix and self.verbose:2744print('Ignoring file outside of prefix:{0}'.format(path))2745return hasPrefix27462747defcommit(self, details, files, branch, parent =""):2748 epoch = details["time"]2749 author = details["user"]2750 jobs = self.extractJobsFromCommit(details)27512752if self.verbose:2753print('commit into{0}'.format(branch))27542755if self.clientSpecDirs:2756 self.clientSpecDirs.update_client_spec_path_cache(files)27572758 files = [f for f in files2759if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]27602761if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2762print('Ignoring revision{0}as it would produce an empty commit.'2763.format(details['change']))2764return27652766 self.gitStream.write("commit%s\n"% branch)2767 self.gitStream.write("mark :%s\n"% details["change"])2768 self.committedChanges.add(int(details["change"]))2769 committer =""2770if author not in self.users:2771 self.getUserMapFromPerforceServer()2772 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)27732774 self.gitStream.write("committer%s\n"% committer)27752776 self.gitStream.write("data <<EOT\n")2777 self.gitStream.write(details["desc"])2778iflen(jobs) >0:2779 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2780 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2781(','.join(self.branchPrefixes), details["change"]))2782iflen(details['options']) >0:2783 self.gitStream.write(": options =%s"% details['options'])2784 self.gitStream.write("]\nEOT\n\n")27852786iflen(parent) >0:2787if self.verbose:2788print"parent%s"% parent2789 self.gitStream.write("from%s\n"% parent)27902791 self.streamP4Files(files)2792 self.gitStream.write("\n")27932794 change =int(details["change"])27952796if self.labels.has_key(change):2797 label = self.labels[change]2798 labelDetails = label[0]2799 labelRevisions = label[1]2800if self.verbose:2801print"Change%sis labelled%s"% (change, labelDetails)28022803 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2804for p in self.branchPrefixes])28052806iflen(files) ==len(labelRevisions):28072808 cleanedFiles = {}2809for info in files:2810if info["action"]in self.delete_actions:2811continue2812 cleanedFiles[info["depotFile"]] = info["rev"]28132814if cleanedFiles == labelRevisions:2815 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)28162817else:2818if not self.silent:2819print("Tag%sdoes not match with change%s: files do not match."2820% (labelDetails["label"], change))28212822else:2823if not self.silent:2824print("Tag%sdoes not match with change%s: file count is different."2825% (labelDetails["label"], change))28262827# Build a dictionary of changelists and labels, for "detect-labels" option.2828defgetLabels(self):2829 self.labels = {}28302831 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2832iflen(l) >0and not self.silent:2833print"Finding files belonging to labels in%s"% `self.depotPaths`28342835for output in l:2836 label = output["label"]2837 revisions = {}2838 newestChange =02839if self.verbose:2840print"Querying files for label%s"% label2841forfileinp4CmdList(["files"] +2842["%s...@%s"% (p, label)2843for p in self.depotPaths]):2844 revisions[file["depotFile"]] =file["rev"]2845 change =int(file["change"])2846if change > newestChange:2847 newestChange = change28482849 self.labels[newestChange] = [output, revisions]28502851if self.verbose:2852print"Label changes:%s"% self.labels.keys()28532854# Import p4 labels as git tags. A direct mapping does not2855# exist, so assume that if all the files are at the same revision2856# then we can use that, or it's something more complicated we should2857# just ignore.2858defimportP4Labels(self, stream, p4Labels):2859if verbose:2860print"import p4 labels: "+' '.join(p4Labels)28612862 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2863 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2864iflen(validLabelRegexp) ==0:2865 validLabelRegexp = defaultLabelRegexp2866 m = re.compile(validLabelRegexp)28672868for name in p4Labels:2869 commitFound =False28702871if not m.match(name):2872if verbose:2873print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2874continue28752876if name in ignoredP4Labels:2877continue28782879 labelDetails =p4CmdList(['label',"-o", name])[0]28802881# get the most recent changelist for each file in this label2882 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2883for p in self.depotPaths])28842885if change.has_key('change'):2886# find the corresponding git commit; take the oldest commit2887 changelist =int(change['change'])2888if changelist in self.committedChanges:2889 gitCommit =":%d"% changelist # use a fast-import mark2890 commitFound =True2891else:2892 gitCommit =read_pipe(["git","rev-list","--max-count=1",2893"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2894iflen(gitCommit) ==0:2895print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2896else:2897 commitFound =True2898 gitCommit = gitCommit.strip()28992900if commitFound:2901# Convert from p4 time format2902try:2903 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2904exceptValueError:2905print"Could not convert label time%s"% labelDetails['Update']2906 tmwhen =129072908 when =int(time.mktime(tmwhen))2909 self.streamTag(stream, name, labelDetails, gitCommit, when)2910if verbose:2911print"p4 label%smapped to git commit%s"% (name, gitCommit)2912else:2913if verbose:2914print"Label%shas no changelists - possibly deleted?"% name29152916if not commitFound:2917# We can't import this label; don't try again as it will get very2918# expensive repeatedly fetching all the files for labels that will2919# never be imported. If the label is moved in the future, the2920# ignore will need to be removed manually.2921system(["git","config","--add","git-p4.ignoredP4Labels", name])29222923defguessProjectName(self):2924for p in self.depotPaths:2925if p.endswith("/"):2926 p = p[:-1]2927 p = p[p.strip().rfind("/") +1:]2928if not p.endswith("/"):2929 p +="/"2930return p29312932defgetBranchMapping(self):2933 lostAndFoundBranches =set()29342935 user =gitConfig("git-p4.branchUser")2936iflen(user) >0:2937 command ="branches -u%s"% user2938else:2939 command ="branches"29402941for info inp4CmdList(command):2942 details =p4Cmd(["branch","-o", info["branch"]])2943 viewIdx =02944while details.has_key("View%s"% viewIdx):2945 paths = details["View%s"% viewIdx].split(" ")2946 viewIdx = viewIdx +12947# require standard //depot/foo/... //depot/bar/... mapping2948iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2949continue2950 source = paths[0]2951 destination = paths[1]2952## HACK2953ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2954 source = source[len(self.depotPaths[0]):-4]2955 destination = destination[len(self.depotPaths[0]):-4]29562957if destination in self.knownBranches:2958if not self.silent:2959print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2960print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2961continue29622963 self.knownBranches[destination] = source29642965 lostAndFoundBranches.discard(destination)29662967if source not in self.knownBranches:2968 lostAndFoundBranches.add(source)29692970# Perforce does not strictly require branches to be defined, so we also2971# check git config for a branch list.2972#2973# Example of branch definition in git config file:2974# [git-p4]2975# branchList=main:branchA2976# branchList=main:branchB2977# branchList=branchA:branchC2978 configBranches =gitConfigList("git-p4.branchList")2979for branch in configBranches:2980if branch:2981(source, destination) = branch.split(":")2982 self.knownBranches[destination] = source29832984 lostAndFoundBranches.discard(destination)29852986if source not in self.knownBranches:2987 lostAndFoundBranches.add(source)298829892990for branch in lostAndFoundBranches:2991 self.knownBranches[branch] = branch29922993defgetBranchMappingFromGitBranches(self):2994 branches =p4BranchesInGit(self.importIntoRemotes)2995for branch in branches.keys():2996if branch =="master":2997 branch ="main"2998else:2999 branch = branch[len(self.projectName):]3000 self.knownBranches[branch] = branch30013002defupdateOptionDict(self, d):3003 option_keys = {}3004if self.keepRepoPath:3005 option_keys['keepRepoPath'] =130063007 d["options"] =' '.join(sorted(option_keys.keys()))30083009defreadOptions(self, d):3010 self.keepRepoPath = (d.has_key('options')3011and('keepRepoPath'in d['options']))30123013defgitRefForBranch(self, branch):3014if branch =="main":3015return self.refPrefix +"master"30163017iflen(branch) <=0:3018return branch30193020return self.refPrefix + self.projectName + branch30213022defgitCommitByP4Change(self, ref, change):3023if self.verbose:3024print"looking in ref "+ ref +" for change%susing bisect..."% change30253026 earliestCommit =""3027 latestCommit =parseRevision(ref)30283029while True:3030if self.verbose:3031print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3032 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3033iflen(next) ==0:3034if self.verbose:3035print"argh"3036return""3037 log =extractLogMessageFromGitCommit(next)3038 settings =extractSettingsGitLog(log)3039 currentChange =int(settings['change'])3040if self.verbose:3041print"current change%s"% currentChange30423043if currentChange == change:3044if self.verbose:3045print"found%s"% next3046return next30473048if currentChange < change:3049 earliestCommit ="^%s"% next3050else:3051 latestCommit ="%s"% next30523053return""30543055defimportNewBranch(self, branch, maxChange):3056# make fast-import flush all changes to disk and update the refs using the checkpoint3057# command so that we can try to find the branch parent in the git history3058 self.gitStream.write("checkpoint\n\n");3059 self.gitStream.flush();3060 branchPrefix = self.depotPaths[0] + branch +"/"3061range="@1,%s"% maxChange3062#print "prefix" + branchPrefix3063 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3064iflen(changes) <=0:3065return False3066 firstChange = changes[0]3067#print "first change in branch: %s" % firstChange3068 sourceBranch = self.knownBranches[branch]3069 sourceDepotPath = self.depotPaths[0] + sourceBranch3070 sourceRef = self.gitRefForBranch(sourceBranch)3071#print "source " + sourceBranch30723073 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3074#print "branch parent: %s" % branchParentChange3075 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3076iflen(gitParent) >0:3077 self.initialParents[self.gitRefForBranch(branch)] = gitParent3078#print "parent git commit: %s" % gitParent30793080 self.importChanges(changes)3081return True30823083defsearchParent(self, parent, branch, target):3084 parentFound =False3085for blob inread_pipe_lines(["git","rev-list","--reverse",3086"--no-merges", parent]):3087 blob = blob.strip()3088iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3089 parentFound =True3090if self.verbose:3091print"Found parent of%sin commit%s"% (branch, blob)3092break3093if parentFound:3094return blob3095else:3096return None30973098defimportChanges(self, changes):3099 cnt =13100for change in changes:3101 description =p4_describe(change)3102 self.updateOptionDict(description)31033104if not self.silent:3105 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3106 sys.stdout.flush()3107 cnt = cnt +131083109try:3110if self.detectBranches:3111 branches = self.splitFilesIntoBranches(description)3112for branch in branches.keys():3113## HACK --hwn3114 branchPrefix = self.depotPaths[0] + branch +"/"3115 self.branchPrefixes = [ branchPrefix ]31163117 parent =""31183119 filesForCommit = branches[branch]31203121if self.verbose:3122print"branch is%s"% branch31233124 self.updatedBranches.add(branch)31253126if branch not in self.createdBranches:3127 self.createdBranches.add(branch)3128 parent = self.knownBranches[branch]3129if parent == branch:3130 parent =""3131else:3132 fullBranch = self.projectName + branch3133if fullBranch not in self.p4BranchesInGit:3134if not self.silent:3135print("\nImporting new branch%s"% fullBranch);3136if self.importNewBranch(branch, change -1):3137 parent =""3138 self.p4BranchesInGit.append(fullBranch)3139if not self.silent:3140print("\nResuming with change%s"% change);31413142if self.verbose:3143print"parent determined through known branches:%s"% parent31443145 branch = self.gitRefForBranch(branch)3146 parent = self.gitRefForBranch(parent)31473148if self.verbose:3149print"looking for initial parent for%s; current parent is%s"% (branch, parent)31503151iflen(parent) ==0and branch in self.initialParents:3152 parent = self.initialParents[branch]3153del self.initialParents[branch]31543155 blob =None3156iflen(parent) >0:3157 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3158if self.verbose:3159print"Creating temporary branch: "+ tempBranch3160 self.commit(description, filesForCommit, tempBranch)3161 self.tempBranches.append(tempBranch)3162 self.checkpoint()3163 blob = self.searchParent(parent, branch, tempBranch)3164if blob:3165 self.commit(description, filesForCommit, branch, blob)3166else:3167if self.verbose:3168print"Parent of%snot found. Committing into head of%s"% (branch, parent)3169 self.commit(description, filesForCommit, branch, parent)3170else:3171 files = self.extractFilesFromCommit(description)3172 self.commit(description, files, self.branch,3173 self.initialParent)3174# only needed once, to connect to the previous commit3175 self.initialParent =""3176exceptIOError:3177print self.gitError.read()3178 sys.exit(1)31793180defimportHeadRevision(self, revision):3181print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)31823183 details = {}3184 details["user"] ="git perforce import user"3185 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3186% (' '.join(self.depotPaths), revision))3187 details["change"] = revision3188 newestRevision =031893190 fileCnt =03191 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]31923193for info inp4CmdList(["files"] + fileArgs):31943195if'code'in info and info['code'] =='error':3196 sys.stderr.write("p4 returned an error:%s\n"3197% info['data'])3198if info['data'].find("must refer to client") >=0:3199 sys.stderr.write("This particular p4 error is misleading.\n")3200 sys.stderr.write("Perhaps the depot path was misspelled.\n");3201 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3202 sys.exit(1)3203if'p4ExitCode'in info:3204 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3205 sys.exit(1)320632073208 change =int(info["change"])3209if change > newestRevision:3210 newestRevision = change32113212if info["action"]in self.delete_actions:3213# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3214#fileCnt = fileCnt + 13215continue32163217for prop in["depotFile","rev","action","type"]:3218 details["%s%s"% (prop, fileCnt)] = info[prop]32193220 fileCnt = fileCnt +132213222 details["change"] = newestRevision32233224# Use time from top-most change so that all git p4 clones of3225# the same p4 repo have the same commit SHA1s.3226 res =p4_describe(newestRevision)3227 details["time"] = res["time"]32283229 self.updateOptionDict(details)3230try:3231 self.commit(details, self.extractFilesFromCommit(details), self.branch)3232exceptIOError:3233print"IO error with git fast-import. Is your git version recent enough?"3234print self.gitError.read()323532363237defrun(self, args):3238 self.depotPaths = []3239 self.changeRange =""3240 self.previousDepotPaths = []3241 self.hasOrigin =False32423243# map from branch depot path to parent branch3244 self.knownBranches = {}3245 self.initialParents = {}32463247if self.importIntoRemotes:3248 self.refPrefix ="refs/remotes/p4/"3249else:3250 self.refPrefix ="refs/heads/p4/"32513252if self.syncWithOrigin:3253 self.hasOrigin =originP4BranchesExist()3254if self.hasOrigin:3255if not self.silent:3256print'Syncing with origin first, using "git fetch origin"'3257system("git fetch origin")32583259 branch_arg_given =bool(self.branch)3260iflen(self.branch) ==0:3261 self.branch = self.refPrefix +"master"3262ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3263system("git update-ref%srefs/heads/p4"% self.branch)3264system("git branch -D p4")32653266# accept either the command-line option, or the configuration variable3267if self.useClientSpec:3268# will use this after clone to set the variable3269 self.useClientSpec_from_options =True3270else:3271ifgitConfigBool("git-p4.useclientspec"):3272 self.useClientSpec =True3273if self.useClientSpec:3274 self.clientSpecDirs =getClientSpec()32753276# TODO: should always look at previous commits,3277# merge with previous imports, if possible.3278if args == []:3279if self.hasOrigin:3280createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)32813282# branches holds mapping from branch name to sha13283 branches =p4BranchesInGit(self.importIntoRemotes)32843285# restrict to just this one, disabling detect-branches3286if branch_arg_given:3287 short = self.branch.split("/")[-1]3288if short in branches:3289 self.p4BranchesInGit = [ short ]3290else:3291 self.p4BranchesInGit = branches.keys()32923293iflen(self.p4BranchesInGit) >1:3294if not self.silent:3295print"Importing from/into multiple branches"3296 self.detectBranches =True3297for branch in branches.keys():3298 self.initialParents[self.refPrefix + branch] = \3299 branches[branch]33003301if self.verbose:3302print"branches:%s"% self.p4BranchesInGit33033304 p4Change =03305for branch in self.p4BranchesInGit:3306 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)33073308 settings =extractSettingsGitLog(logMsg)33093310 self.readOptions(settings)3311if(settings.has_key('depot-paths')3312and settings.has_key('change')):3313 change =int(settings['change']) +13314 p4Change =max(p4Change, change)33153316 depotPaths =sorted(settings['depot-paths'])3317if self.previousDepotPaths == []:3318 self.previousDepotPaths = depotPaths3319else:3320 paths = []3321for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3322 prev_list = prev.split("/")3323 cur_list = cur.split("/")3324for i inrange(0,min(len(cur_list),len(prev_list))):3325if cur_list[i] <> prev_list[i]:3326 i = i -13327break33283329 paths.append("/".join(cur_list[:i +1]))33303331 self.previousDepotPaths = paths33323333if p4Change >0:3334 self.depotPaths =sorted(self.previousDepotPaths)3335 self.changeRange ="@%s,#head"% p4Change3336if not self.silent and not self.detectBranches:3337print"Performing incremental import into%sgit branch"% self.branch33383339# accept multiple ref name abbreviations:3340# refs/foo/bar/branch -> use it exactly3341# p4/branch -> prepend refs/remotes/ or refs/heads/3342# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3343if not self.branch.startswith("refs/"):3344if self.importIntoRemotes:3345 prepend ="refs/remotes/"3346else:3347 prepend ="refs/heads/"3348if not self.branch.startswith("p4/"):3349 prepend +="p4/"3350 self.branch = prepend + self.branch33513352iflen(args) ==0and self.depotPaths:3353if not self.silent:3354print"Depot paths:%s"%' '.join(self.depotPaths)3355else:3356if self.depotPaths and self.depotPaths != args:3357print("previous import used depot path%sand now%swas specified. "3358"This doesn't work!"% (' '.join(self.depotPaths),3359' '.join(args)))3360 sys.exit(1)33613362 self.depotPaths =sorted(args)33633364 revision =""3365 self.users = {}33663367# Make sure no revision specifiers are used when --changesfile3368# is specified.3369 bad_changesfile =False3370iflen(self.changesFile) >0:3371for p in self.depotPaths:3372if p.find("@") >=0or p.find("#") >=0:3373 bad_changesfile =True3374break3375if bad_changesfile:3376die("Option --changesfile is incompatible with revision specifiers")33773378 newPaths = []3379for p in self.depotPaths:3380if p.find("@") != -1:3381 atIdx = p.index("@")3382 self.changeRange = p[atIdx:]3383if self.changeRange =="@all":3384 self.changeRange =""3385elif','not in self.changeRange:3386 revision = self.changeRange3387 self.changeRange =""3388 p = p[:atIdx]3389elif p.find("#") != -1:3390 hashIdx = p.index("#")3391 revision = p[hashIdx:]3392 p = p[:hashIdx]3393elif self.previousDepotPaths == []:3394# pay attention to changesfile, if given, else import3395# the entire p4 tree at the head revision3396iflen(self.changesFile) ==0:3397 revision ="#head"33983399 p = re.sub("\.\.\.$","", p)3400if not p.endswith("/"):3401 p +="/"34023403 newPaths.append(p)34043405 self.depotPaths = newPaths34063407# --detect-branches may change this for each branch3408 self.branchPrefixes = self.depotPaths34093410 self.loadUserMapFromCache()3411 self.labels = {}3412if self.detectLabels:3413 self.getLabels();34143415if self.detectBranches:3416## FIXME - what's a P4 projectName ?3417 self.projectName = self.guessProjectName()34183419if self.hasOrigin:3420 self.getBranchMappingFromGitBranches()3421else:3422 self.getBranchMapping()3423if self.verbose:3424print"p4-git branches:%s"% self.p4BranchesInGit3425print"initial parents:%s"% self.initialParents3426for b in self.p4BranchesInGit:3427if b !="master":34283429## FIXME3430 b = b[len(self.projectName):]3431 self.createdBranches.add(b)34323433 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))34343435 self.importProcess = subprocess.Popen(["git","fast-import"],3436 stdin=subprocess.PIPE,3437 stdout=subprocess.PIPE,3438 stderr=subprocess.PIPE);3439 self.gitOutput = self.importProcess.stdout3440 self.gitStream = self.importProcess.stdin3441 self.gitError = self.importProcess.stderr34423443if revision:3444 self.importHeadRevision(revision)3445else:3446 changes = []34473448iflen(self.changesFile) >0:3449 output =open(self.changesFile).readlines()3450 changeSet =set()3451for line in output:3452 changeSet.add(int(line))34533454for change in changeSet:3455 changes.append(change)34563457 changes.sort()3458else:3459# catch "git p4 sync" with no new branches, in a repo that3460# does not have any existing p4 branches3461iflen(args) ==0:3462if not self.p4BranchesInGit:3463die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")34643465# The default branch is master, unless --branch is used to3466# specify something else. Make sure it exists, or complain3467# nicely about how to use --branch.3468if not self.detectBranches:3469if notbranch_exists(self.branch):3470if branch_arg_given:3471die("Error: branch%sdoes not exist."% self.branch)3472else:3473die("Error: no branch%s; perhaps specify one with --branch."%3474 self.branch)34753476if self.verbose:3477print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3478 self.changeRange)3479 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)34803481iflen(self.maxChanges) >0:3482 changes = changes[:min(int(self.maxChanges),len(changes))]34833484iflen(changes) ==0:3485if not self.silent:3486print"No changes to import!"3487else:3488if not self.silent and not self.detectBranches:3489print"Import destination:%s"% self.branch34903491 self.updatedBranches =set()34923493if not self.detectBranches:3494if args:3495# start a new branch3496 self.initialParent =""3497else:3498# build on a previous revision3499 self.initialParent =parseRevision(self.branch)35003501 self.importChanges(changes)35023503if not self.silent:3504print""3505iflen(self.updatedBranches) >0:3506 sys.stdout.write("Updated branches: ")3507for b in self.updatedBranches:3508 sys.stdout.write("%s"% b)3509 sys.stdout.write("\n")35103511ifgitConfigBool("git-p4.importLabels"):3512 self.importLabels =True35133514if self.importLabels:3515 p4Labels =getP4Labels(self.depotPaths)3516 gitTags =getGitTags()35173518 missingP4Labels = p4Labels - gitTags3519 self.importP4Labels(self.gitStream, missingP4Labels)35203521 self.gitStream.close()3522if self.importProcess.wait() !=0:3523die("fast-import failed:%s"% self.gitError.read())3524 self.gitOutput.close()3525 self.gitError.close()35263527# Cleanup temporary branches created during import3528if self.tempBranches != []:3529for branch in self.tempBranches:3530read_pipe("git update-ref -d%s"% branch)3531 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))35323533# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3534# a convenient shortcut refname "p4".3535if self.importIntoRemotes:3536 head_ref = self.refPrefix +"HEAD"3537if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3538system(["git","symbolic-ref", head_ref, self.branch])35393540return True35413542classP4Rebase(Command):3543def__init__(self):3544 Command.__init__(self)3545 self.options = [3546 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3547]3548 self.importLabels =False3549 self.description = ("Fetches the latest revision from perforce and "3550+"rebases the current work (branch) against it")35513552defrun(self, args):3553 sync =P4Sync()3554 sync.importLabels = self.importLabels3555 sync.run([])35563557return self.rebase()35583559defrebase(self):3560if os.system("git update-index --refresh") !=0:3561die("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.");3562iflen(read_pipe("git diff-index HEAD --")) >0:3563die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");35643565[upstream, settings] =findUpstreamBranchPoint()3566iflen(upstream) ==0:3567die("Cannot find upstream branchpoint for rebase")35683569# the branchpoint may be p4/foo~3, so strip off the parent3570 upstream = re.sub("~[0-9]+$","", upstream)35713572print"Rebasing the current branch onto%s"% upstream3573 oldHead =read_pipe("git rev-parse HEAD").strip()3574system("git rebase%s"% upstream)3575system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3576return True35773578classP4Clone(P4Sync):3579def__init__(self):3580 P4Sync.__init__(self)3581 self.description ="Creates a new git repository and imports from Perforce into it"3582 self.usage ="usage: %prog [options] //depot/path[@revRange]"3583 self.options += [3584 optparse.make_option("--destination", dest="cloneDestination",3585 action='store', default=None,3586help="where to leave result of the clone"),3587 optparse.make_option("--bare", dest="cloneBare",3588 action="store_true", default=False),3589]3590 self.cloneDestination =None3591 self.needsGit =False3592 self.cloneBare =False35933594defdefaultDestination(self, args):3595## TODO: use common prefix of args?3596 depotPath = args[0]3597 depotDir = re.sub("(@[^@]*)$","", depotPath)3598 depotDir = re.sub("(#[^#]*)$","", depotDir)3599 depotDir = re.sub(r"\.\.\.$","", depotDir)3600 depotDir = re.sub(r"/$","", depotDir)3601return os.path.split(depotDir)[1]36023603defrun(self, args):3604iflen(args) <1:3605return False36063607if self.keepRepoPath and not self.cloneDestination:3608 sys.stderr.write("Must specify destination for --keep-path\n")3609 sys.exit(1)36103611 depotPaths = args36123613if not self.cloneDestination andlen(depotPaths) >1:3614 self.cloneDestination = depotPaths[-1]3615 depotPaths = depotPaths[:-1]36163617 self.cloneExclude = ["/"+p for p in self.cloneExclude]3618for p in depotPaths:3619if not p.startswith("//"):3620 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3621return False36223623if not self.cloneDestination:3624 self.cloneDestination = self.defaultDestination(args)36253626print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)36273628if not os.path.exists(self.cloneDestination):3629 os.makedirs(self.cloneDestination)3630chdir(self.cloneDestination)36313632 init_cmd = ["git","init"]3633if self.cloneBare:3634 init_cmd.append("--bare")3635 retcode = subprocess.call(init_cmd)3636if retcode:3637raiseCalledProcessError(retcode, init_cmd)36383639if not P4Sync.run(self, depotPaths):3640return False36413642# create a master branch and check out a work tree3643ifgitBranchExists(self.branch):3644system(["git","branch","master", self.branch ])3645if not self.cloneBare:3646system(["git","checkout","-f"])3647else:3648print'Not checking out any branch, use ' \3649'"git checkout -q -b master <branch>"'36503651# auto-set this variable if invoked with --use-client-spec3652if self.useClientSpec_from_options:3653system("git config --bool git-p4.useclientspec true")36543655return True36563657classP4Branches(Command):3658def__init__(self):3659 Command.__init__(self)3660 self.options = [ ]3661 self.description = ("Shows the git branches that hold imports and their "3662+"corresponding perforce depot paths")3663 self.verbose =False36643665defrun(self, args):3666iforiginP4BranchesExist():3667createOrUpdateBranchesFromOrigin()36683669 cmdline ="git rev-parse --symbolic "3670 cmdline +=" --remotes"36713672for line inread_pipe_lines(cmdline):3673 line = line.strip()36743675if not line.startswith('p4/')or line =="p4/HEAD":3676continue3677 branch = line36783679 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3680 settings =extractSettingsGitLog(log)36813682print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3683return True36843685classHelpFormatter(optparse.IndentedHelpFormatter):3686def__init__(self):3687 optparse.IndentedHelpFormatter.__init__(self)36883689defformat_description(self, description):3690if description:3691return description +"\n"3692else:3693return""36943695defprintUsage(commands):3696print"usage:%s<command> [options]"% sys.argv[0]3697print""3698print"valid commands:%s"%", ".join(commands)3699print""3700print"Try%s<command> --help for command specific help."% sys.argv[0]3701print""37023703commands = {3704"debug": P4Debug,3705"submit": P4Submit,3706"commit": P4Submit,3707"sync": P4Sync,3708"rebase": P4Rebase,3709"clone": P4Clone,3710"rollback": P4RollBack,3711"branches": P4Branches3712}371337143715defmain():3716iflen(sys.argv[1:]) ==0:3717printUsage(commands.keys())3718 sys.exit(2)37193720 cmdName = sys.argv[1]3721try:3722 klass = commands[cmdName]3723 cmd =klass()3724exceptKeyError:3725print"unknown command%s"% cmdName3726print""3727printUsage(commands.keys())3728 sys.exit(2)37293730 options = cmd.options3731 cmd.gitdir = os.environ.get("GIT_DIR",None)37323733 args = sys.argv[2:]37343735 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3736if cmd.needsGit:3737 options.append(optparse.make_option("--git-dir", dest="gitdir"))37383739 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3740 options,3741 description = cmd.description,3742 formatter =HelpFormatter())37433744(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3745global verbose3746 verbose = cmd.verbose3747if cmd.needsGit:3748if cmd.gitdir ==None:3749 cmd.gitdir = os.path.abspath(".git")3750if notisValidGitDir(cmd.gitdir):3751# "rev-parse --git-dir" without arguments will try $PWD/.git3752 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3753if os.path.exists(cmd.gitdir):3754 cdup =read_pipe("git rev-parse --show-cdup").strip()3755iflen(cdup) >0:3756chdir(cdup);37573758if notisValidGitDir(cmd.gitdir):3759ifisValidGitDir(cmd.gitdir +"/.git"):3760 cmd.gitdir +="/.git"3761else:3762die("fatal: cannot locate git repository at%s"% cmd.gitdir)37633764# so git commands invoked from the P4 workspace will succeed3765 os.environ["GIT_DIR"] = cmd.gitdir37663767if not cmd.run(args):3768 parser.print_help()3769 sys.exit(2)377037713772if __name__ =='__main__':3773main()