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')24862487defencodeWithUTF8(self, path):2488try:2489 path.decode('ascii')2490except:2491 encoding ='utf8'2492ifgitConfig('git-p4.pathEncoding'):2493 encoding =gitConfig('git-p4.pathEncoding')2494 path = path.decode(encoding,'replace').encode('utf8','replace')2495if self.verbose:2496print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2497return path24982499# output one file from the P4 stream2500# - helper for streamP4Files25012502defstreamOneP4File(self,file, contents):2503 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2504 relPath = self.encodeWithUTF8(relPath)2505if verbose:2506 size =int(self.stream_file['fileSize'])2507 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2508 sys.stdout.flush()25092510(type_base, type_mods) =split_p4_type(file["type"])25112512 git_mode ="100644"2513if"x"in type_mods:2514 git_mode ="100755"2515if type_base =="symlink":2516 git_mode ="120000"2517# p4 print on a symlink sometimes contains "target\n";2518# if it does, remove the newline2519 data =''.join(contents)2520if not data:2521# Some version of p4 allowed creating a symlink that pointed2522# to nothing. This causes p4 errors when checking out such2523# a change, and errors here too. Work around it by ignoring2524# the bad symlink; hopefully a future change fixes it.2525print"\nIgnoring empty symlink in%s"%file['depotFile']2526return2527elif data[-1] =='\n':2528 contents = [data[:-1]]2529else:2530 contents = [data]25312532if type_base =="utf16":2533# p4 delivers different text in the python output to -G2534# than it does when using "print -o", or normal p4 client2535# operations. utf16 is converted to ascii or utf8, perhaps.2536# But ascii text saved as -t utf16 is completely mangled.2537# Invoke print -o to get the real contents.2538#2539# On windows, the newlines will always be mangled by print, so put2540# them back too. This is not needed to the cygwin windows version,2541# just the native "NT" type.2542#2543try:2544 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2545exceptExceptionas e:2546if'Translation of file content failed'instr(e):2547 type_base ='binary'2548else:2549raise e2550else:2551ifp4_version_string().find('/NT') >=0:2552 text = text.replace('\r\n','\n')2553 contents = [ text ]25542555if type_base =="apple":2556# Apple filetype files will be streamed as a concatenation of2557# its appledouble header and the contents. This is useless2558# on both macs and non-macs. If using "print -q -o xx", it2559# will create "xx" with the data, and "%xx" with the header.2560# This is also not very useful.2561#2562# Ideally, someday, this script can learn how to generate2563# appledouble files directly and import those to git, but2564# non-mac machines can never find a use for apple filetype.2565print"\nIgnoring apple filetype file%s"%file['depotFile']2566return25672568# Note that we do not try to de-mangle keywords on utf16 files,2569# even though in theory somebody may want that.2570 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2571if pattern:2572 regexp = re.compile(pattern, re.VERBOSE)2573 text =''.join(contents)2574 text = regexp.sub(r'$\1$', text)2575 contents = [ text ]25762577if self.largeFileSystem:2578(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)25792580 self.writeToGitStream(git_mode, relPath, contents)25812582defstreamOneP4Deletion(self,file):2583 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2584 relPath = self.encodeWithUTF8(relPath)2585if verbose:2586 sys.stdout.write("delete%s\n"% relPath)2587 sys.stdout.flush()2588 self.gitStream.write("D%s\n"% relPath)25892590if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2591 self.largeFileSystem.removeLargeFile(relPath)25922593# handle another chunk of streaming data2594defstreamP4FilesCb(self, marshalled):25952596# catch p4 errors and complain2597 err =None2598if"code"in marshalled:2599if marshalled["code"] =="error":2600if"data"in marshalled:2601 err = marshalled["data"].rstrip()26022603if not err and'fileSize'in self.stream_file:2604 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2605if required_bytes >0:2606 err ='Not enough space left on%s! Free at least%iMB.'% (2607 os.getcwd(), required_bytes/1024/10242608)26092610if err:2611 f =None2612if self.stream_have_file_info:2613if"depotFile"in self.stream_file:2614 f = self.stream_file["depotFile"]2615# force a failure in fast-import, else an empty2616# commit will be made2617 self.gitStream.write("\n")2618 self.gitStream.write("die-now\n")2619 self.gitStream.close()2620# ignore errors, but make sure it exits first2621 self.importProcess.wait()2622if f:2623die("Error from p4 print for%s:%s"% (f, err))2624else:2625die("Error from p4 print:%s"% err)26262627if marshalled.has_key('depotFile')and self.stream_have_file_info:2628# start of a new file - output the old one first2629 self.streamOneP4File(self.stream_file, self.stream_contents)2630 self.stream_file = {}2631 self.stream_contents = []2632 self.stream_have_file_info =False26332634# pick up the new file information... for the2635# 'data' field we need to append to our array2636for k in marshalled.keys():2637if k =='data':2638if'streamContentSize'not in self.stream_file:2639 self.stream_file['streamContentSize'] =02640 self.stream_file['streamContentSize'] +=len(marshalled['data'])2641 self.stream_contents.append(marshalled['data'])2642else:2643 self.stream_file[k] = marshalled[k]26442645if(verbose and2646'streamContentSize'in self.stream_file and2647'fileSize'in self.stream_file and2648'depotFile'in self.stream_file):2649 size =int(self.stream_file["fileSize"])2650if size >0:2651 progress =100*self.stream_file['streamContentSize']/size2652 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2653 sys.stdout.flush()26542655 self.stream_have_file_info =True26562657# Stream directly from "p4 files" into "git fast-import"2658defstreamP4Files(self, files):2659 filesForCommit = []2660 filesToRead = []2661 filesToDelete = []26622663for f in files:2664 filesForCommit.append(f)2665if f['action']in self.delete_actions:2666 filesToDelete.append(f)2667else:2668 filesToRead.append(f)26692670# deleted files...2671for f in filesToDelete:2672 self.streamOneP4Deletion(f)26732674iflen(filesToRead) >0:2675 self.stream_file = {}2676 self.stream_contents = []2677 self.stream_have_file_info =False26782679# curry self argument2680defstreamP4FilesCbSelf(entry):2681 self.streamP4FilesCb(entry)26822683 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]26842685p4CmdList(["-x","-","print"],2686 stdin=fileArgs,2687 cb=streamP4FilesCbSelf)26882689# do the last chunk2690if self.stream_file.has_key('depotFile'):2691 self.streamOneP4File(self.stream_file, self.stream_contents)26922693defmake_email(self, userid):2694if userid in self.users:2695return self.users[userid]2696else:2697return"%s<a@b>"% userid26982699defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2700""" Stream a p4 tag.2701 commit is either a git commit, or a fast-import mark, ":<p4commit>"2702 """27032704if verbose:2705print"writing tag%sfor commit%s"% (labelName, commit)2706 gitStream.write("tag%s\n"% labelName)2707 gitStream.write("from%s\n"% commit)27082709if labelDetails.has_key('Owner'):2710 owner = labelDetails["Owner"]2711else:2712 owner =None27132714# Try to use the owner of the p4 label, or failing that,2715# the current p4 user id.2716if owner:2717 email = self.make_email(owner)2718else:2719 email = self.make_email(self.p4UserId())2720 tagger ="%s %s %s"% (email, epoch, self.tz)27212722 gitStream.write("tagger%s\n"% tagger)27232724print"labelDetails=",labelDetails2725if labelDetails.has_key('Description'):2726 description = labelDetails['Description']2727else:2728 description ='Label from git p4'27292730 gitStream.write("data%d\n"%len(description))2731 gitStream.write(description)2732 gitStream.write("\n")27332734definClientSpec(self, path):2735if not self.clientSpecDirs:2736return True2737 inClientSpec = self.clientSpecDirs.map_in_client(path)2738if not inClientSpec and self.verbose:2739print('Ignoring file outside of client spec:{0}'.format(path))2740return inClientSpec27412742defhasBranchPrefix(self, path):2743if not self.branchPrefixes:2744return True2745 hasPrefix = [p for p in self.branchPrefixes2746ifp4PathStartsWith(path, p)]2747if not hasPrefix and self.verbose:2748print('Ignoring file outside of prefix:{0}'.format(path))2749return hasPrefix27502751defcommit(self, details, files, branch, parent =""):2752 epoch = details["time"]2753 author = details["user"]2754 jobs = self.extractJobsFromCommit(details)27552756if self.verbose:2757print('commit into{0}'.format(branch))27582759if self.clientSpecDirs:2760 self.clientSpecDirs.update_client_spec_path_cache(files)27612762 files = [f for f in files2763if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]27642765if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2766print('Ignoring revision{0}as it would produce an empty commit.'2767.format(details['change']))2768return27692770 self.gitStream.write("commit%s\n"% branch)2771 self.gitStream.write("mark :%s\n"% details["change"])2772 self.committedChanges.add(int(details["change"]))2773 committer =""2774if author not in self.users:2775 self.getUserMapFromPerforceServer()2776 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)27772778 self.gitStream.write("committer%s\n"% committer)27792780 self.gitStream.write("data <<EOT\n")2781 self.gitStream.write(details["desc"])2782iflen(jobs) >0:2783 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2784 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2785(','.join(self.branchPrefixes), details["change"]))2786iflen(details['options']) >0:2787 self.gitStream.write(": options =%s"% details['options'])2788 self.gitStream.write("]\nEOT\n\n")27892790iflen(parent) >0:2791if self.verbose:2792print"parent%s"% parent2793 self.gitStream.write("from%s\n"% parent)27942795 self.streamP4Files(files)2796 self.gitStream.write("\n")27972798 change =int(details["change"])27992800if self.labels.has_key(change):2801 label = self.labels[change]2802 labelDetails = label[0]2803 labelRevisions = label[1]2804if self.verbose:2805print"Change%sis labelled%s"% (change, labelDetails)28062807 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2808for p in self.branchPrefixes])28092810iflen(files) ==len(labelRevisions):28112812 cleanedFiles = {}2813for info in files:2814if info["action"]in self.delete_actions:2815continue2816 cleanedFiles[info["depotFile"]] = info["rev"]28172818if cleanedFiles == labelRevisions:2819 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)28202821else:2822if not self.silent:2823print("Tag%sdoes not match with change%s: files do not match."2824% (labelDetails["label"], change))28252826else:2827if not self.silent:2828print("Tag%sdoes not match with change%s: file count is different."2829% (labelDetails["label"], change))28302831# Build a dictionary of changelists and labels, for "detect-labels" option.2832defgetLabels(self):2833 self.labels = {}28342835 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2836iflen(l) >0and not self.silent:2837print"Finding files belonging to labels in%s"% `self.depotPaths`28382839for output in l:2840 label = output["label"]2841 revisions = {}2842 newestChange =02843if self.verbose:2844print"Querying files for label%s"% label2845forfileinp4CmdList(["files"] +2846["%s...@%s"% (p, label)2847for p in self.depotPaths]):2848 revisions[file["depotFile"]] =file["rev"]2849 change =int(file["change"])2850if change > newestChange:2851 newestChange = change28522853 self.labels[newestChange] = [output, revisions]28542855if self.verbose:2856print"Label changes:%s"% self.labels.keys()28572858# Import p4 labels as git tags. A direct mapping does not2859# exist, so assume that if all the files are at the same revision2860# then we can use that, or it's something more complicated we should2861# just ignore.2862defimportP4Labels(self, stream, p4Labels):2863if verbose:2864print"import p4 labels: "+' '.join(p4Labels)28652866 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2867 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2868iflen(validLabelRegexp) ==0:2869 validLabelRegexp = defaultLabelRegexp2870 m = re.compile(validLabelRegexp)28712872for name in p4Labels:2873 commitFound =False28742875if not m.match(name):2876if verbose:2877print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2878continue28792880if name in ignoredP4Labels:2881continue28822883 labelDetails =p4CmdList(['label',"-o", name])[0]28842885# get the most recent changelist for each file in this label2886 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2887for p in self.depotPaths])28882889if change.has_key('change'):2890# find the corresponding git commit; take the oldest commit2891 changelist =int(change['change'])2892if changelist in self.committedChanges:2893 gitCommit =":%d"% changelist # use a fast-import mark2894 commitFound =True2895else:2896 gitCommit =read_pipe(["git","rev-list","--max-count=1",2897"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2898iflen(gitCommit) ==0:2899print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2900else:2901 commitFound =True2902 gitCommit = gitCommit.strip()29032904if commitFound:2905# Convert from p4 time format2906try:2907 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2908exceptValueError:2909print"Could not convert label time%s"% labelDetails['Update']2910 tmwhen =129112912 when =int(time.mktime(tmwhen))2913 self.streamTag(stream, name, labelDetails, gitCommit, when)2914if verbose:2915print"p4 label%smapped to git commit%s"% (name, gitCommit)2916else:2917if verbose:2918print"Label%shas no changelists - possibly deleted?"% name29192920if not commitFound:2921# We can't import this label; don't try again as it will get very2922# expensive repeatedly fetching all the files for labels that will2923# never be imported. If the label is moved in the future, the2924# ignore will need to be removed manually.2925system(["git","config","--add","git-p4.ignoredP4Labels", name])29262927defguessProjectName(self):2928for p in self.depotPaths:2929if p.endswith("/"):2930 p = p[:-1]2931 p = p[p.strip().rfind("/") +1:]2932if not p.endswith("/"):2933 p +="/"2934return p29352936defgetBranchMapping(self):2937 lostAndFoundBranches =set()29382939 user =gitConfig("git-p4.branchUser")2940iflen(user) >0:2941 command ="branches -u%s"% user2942else:2943 command ="branches"29442945for info inp4CmdList(command):2946 details =p4Cmd(["branch","-o", info["branch"]])2947 viewIdx =02948while details.has_key("View%s"% viewIdx):2949 paths = details["View%s"% viewIdx].split(" ")2950 viewIdx = viewIdx +12951# require standard //depot/foo/... //depot/bar/... mapping2952iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2953continue2954 source = paths[0]2955 destination = paths[1]2956## HACK2957ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2958 source = source[len(self.depotPaths[0]):-4]2959 destination = destination[len(self.depotPaths[0]):-4]29602961if destination in self.knownBranches:2962if not self.silent:2963print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2964print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2965continue29662967 self.knownBranches[destination] = source29682969 lostAndFoundBranches.discard(destination)29702971if source not in self.knownBranches:2972 lostAndFoundBranches.add(source)29732974# Perforce does not strictly require branches to be defined, so we also2975# check git config for a branch list.2976#2977# Example of branch definition in git config file:2978# [git-p4]2979# branchList=main:branchA2980# branchList=main:branchB2981# branchList=branchA:branchC2982 configBranches =gitConfigList("git-p4.branchList")2983for branch in configBranches:2984if branch:2985(source, destination) = branch.split(":")2986 self.knownBranches[destination] = source29872988 lostAndFoundBranches.discard(destination)29892990if source not in self.knownBranches:2991 lostAndFoundBranches.add(source)299229932994for branch in lostAndFoundBranches:2995 self.knownBranches[branch] = branch29962997defgetBranchMappingFromGitBranches(self):2998 branches =p4BranchesInGit(self.importIntoRemotes)2999for branch in branches.keys():3000if branch =="master":3001 branch ="main"3002else:3003 branch = branch[len(self.projectName):]3004 self.knownBranches[branch] = branch30053006defupdateOptionDict(self, d):3007 option_keys = {}3008if self.keepRepoPath:3009 option_keys['keepRepoPath'] =130103011 d["options"] =' '.join(sorted(option_keys.keys()))30123013defreadOptions(self, d):3014 self.keepRepoPath = (d.has_key('options')3015and('keepRepoPath'in d['options']))30163017defgitRefForBranch(self, branch):3018if branch =="main":3019return self.refPrefix +"master"30203021iflen(branch) <=0:3022return branch30233024return self.refPrefix + self.projectName + branch30253026defgitCommitByP4Change(self, ref, change):3027if self.verbose:3028print"looking in ref "+ ref +" for change%susing bisect..."% change30293030 earliestCommit =""3031 latestCommit =parseRevision(ref)30323033while True:3034if self.verbose:3035print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3036 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3037iflen(next) ==0:3038if self.verbose:3039print"argh"3040return""3041 log =extractLogMessageFromGitCommit(next)3042 settings =extractSettingsGitLog(log)3043 currentChange =int(settings['change'])3044if self.verbose:3045print"current change%s"% currentChange30463047if currentChange == change:3048if self.verbose:3049print"found%s"% next3050return next30513052if currentChange < change:3053 earliestCommit ="^%s"% next3054else:3055 latestCommit ="%s"% next30563057return""30583059defimportNewBranch(self, branch, maxChange):3060# make fast-import flush all changes to disk and update the refs using the checkpoint3061# command so that we can try to find the branch parent in the git history3062 self.gitStream.write("checkpoint\n\n");3063 self.gitStream.flush();3064 branchPrefix = self.depotPaths[0] + branch +"/"3065range="@1,%s"% maxChange3066#print "prefix" + branchPrefix3067 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3068iflen(changes) <=0:3069return False3070 firstChange = changes[0]3071#print "first change in branch: %s" % firstChange3072 sourceBranch = self.knownBranches[branch]3073 sourceDepotPath = self.depotPaths[0] + sourceBranch3074 sourceRef = self.gitRefForBranch(sourceBranch)3075#print "source " + sourceBranch30763077 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3078#print "branch parent: %s" % branchParentChange3079 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3080iflen(gitParent) >0:3081 self.initialParents[self.gitRefForBranch(branch)] = gitParent3082#print "parent git commit: %s" % gitParent30833084 self.importChanges(changes)3085return True30863087defsearchParent(self, parent, branch, target):3088 parentFound =False3089for blob inread_pipe_lines(["git","rev-list","--reverse",3090"--no-merges", parent]):3091 blob = blob.strip()3092iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3093 parentFound =True3094if self.verbose:3095print"Found parent of%sin commit%s"% (branch, blob)3096break3097if parentFound:3098return blob3099else:3100return None31013102defimportChanges(self, changes):3103 cnt =13104for change in changes:3105 description =p4_describe(change)3106 self.updateOptionDict(description)31073108if not self.silent:3109 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3110 sys.stdout.flush()3111 cnt = cnt +131123113try:3114if self.detectBranches:3115 branches = self.splitFilesIntoBranches(description)3116for branch in branches.keys():3117## HACK --hwn3118 branchPrefix = self.depotPaths[0] + branch +"/"3119 self.branchPrefixes = [ branchPrefix ]31203121 parent =""31223123 filesForCommit = branches[branch]31243125if self.verbose:3126print"branch is%s"% branch31273128 self.updatedBranches.add(branch)31293130if branch not in self.createdBranches:3131 self.createdBranches.add(branch)3132 parent = self.knownBranches[branch]3133if parent == branch:3134 parent =""3135else:3136 fullBranch = self.projectName + branch3137if fullBranch not in self.p4BranchesInGit:3138if not self.silent:3139print("\nImporting new branch%s"% fullBranch);3140if self.importNewBranch(branch, change -1):3141 parent =""3142 self.p4BranchesInGit.append(fullBranch)3143if not self.silent:3144print("\nResuming with change%s"% change);31453146if self.verbose:3147print"parent determined through known branches:%s"% parent31483149 branch = self.gitRefForBranch(branch)3150 parent = self.gitRefForBranch(parent)31513152if self.verbose:3153print"looking for initial parent for%s; current parent is%s"% (branch, parent)31543155iflen(parent) ==0and branch in self.initialParents:3156 parent = self.initialParents[branch]3157del self.initialParents[branch]31583159 blob =None3160iflen(parent) >0:3161 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3162if self.verbose:3163print"Creating temporary branch: "+ tempBranch3164 self.commit(description, filesForCommit, tempBranch)3165 self.tempBranches.append(tempBranch)3166 self.checkpoint()3167 blob = self.searchParent(parent, branch, tempBranch)3168if blob:3169 self.commit(description, filesForCommit, branch, blob)3170else:3171if self.verbose:3172print"Parent of%snot found. Committing into head of%s"% (branch, parent)3173 self.commit(description, filesForCommit, branch, parent)3174else:3175 files = self.extractFilesFromCommit(description)3176 self.commit(description, files, self.branch,3177 self.initialParent)3178# only needed once, to connect to the previous commit3179 self.initialParent =""3180exceptIOError:3181print self.gitError.read()3182 sys.exit(1)31833184defimportHeadRevision(self, revision):3185print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)31863187 details = {}3188 details["user"] ="git perforce import user"3189 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3190% (' '.join(self.depotPaths), revision))3191 details["change"] = revision3192 newestRevision =031933194 fileCnt =03195 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]31963197for info inp4CmdList(["files"] + fileArgs):31983199if'code'in info and info['code'] =='error':3200 sys.stderr.write("p4 returned an error:%s\n"3201% info['data'])3202if info['data'].find("must refer to client") >=0:3203 sys.stderr.write("This particular p4 error is misleading.\n")3204 sys.stderr.write("Perhaps the depot path was misspelled.\n");3205 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3206 sys.exit(1)3207if'p4ExitCode'in info:3208 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3209 sys.exit(1)321032113212 change =int(info["change"])3213if change > newestRevision:3214 newestRevision = change32153216if info["action"]in self.delete_actions:3217# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3218#fileCnt = fileCnt + 13219continue32203221for prop in["depotFile","rev","action","type"]:3222 details["%s%s"% (prop, fileCnt)] = info[prop]32233224 fileCnt = fileCnt +132253226 details["change"] = newestRevision32273228# Use time from top-most change so that all git p4 clones of3229# the same p4 repo have the same commit SHA1s.3230 res =p4_describe(newestRevision)3231 details["time"] = res["time"]32323233 self.updateOptionDict(details)3234try:3235 self.commit(details, self.extractFilesFromCommit(details), self.branch)3236exceptIOError:3237print"IO error with git fast-import. Is your git version recent enough?"3238print self.gitError.read()323932403241defrun(self, args):3242 self.depotPaths = []3243 self.changeRange =""3244 self.previousDepotPaths = []3245 self.hasOrigin =False32463247# map from branch depot path to parent branch3248 self.knownBranches = {}3249 self.initialParents = {}32503251if self.importIntoRemotes:3252 self.refPrefix ="refs/remotes/p4/"3253else:3254 self.refPrefix ="refs/heads/p4/"32553256if self.syncWithOrigin:3257 self.hasOrigin =originP4BranchesExist()3258if self.hasOrigin:3259if not self.silent:3260print'Syncing with origin first, using "git fetch origin"'3261system("git fetch origin")32623263 branch_arg_given =bool(self.branch)3264iflen(self.branch) ==0:3265 self.branch = self.refPrefix +"master"3266ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3267system("git update-ref%srefs/heads/p4"% self.branch)3268system("git branch -D p4")32693270# accept either the command-line option, or the configuration variable3271if self.useClientSpec:3272# will use this after clone to set the variable3273 self.useClientSpec_from_options =True3274else:3275ifgitConfigBool("git-p4.useclientspec"):3276 self.useClientSpec =True3277if self.useClientSpec:3278 self.clientSpecDirs =getClientSpec()32793280# TODO: should always look at previous commits,3281# merge with previous imports, if possible.3282if args == []:3283if self.hasOrigin:3284createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)32853286# branches holds mapping from branch name to sha13287 branches =p4BranchesInGit(self.importIntoRemotes)32883289# restrict to just this one, disabling detect-branches3290if branch_arg_given:3291 short = self.branch.split("/")[-1]3292if short in branches:3293 self.p4BranchesInGit = [ short ]3294else:3295 self.p4BranchesInGit = branches.keys()32963297iflen(self.p4BranchesInGit) >1:3298if not self.silent:3299print"Importing from/into multiple branches"3300 self.detectBranches =True3301for branch in branches.keys():3302 self.initialParents[self.refPrefix + branch] = \3303 branches[branch]33043305if self.verbose:3306print"branches:%s"% self.p4BranchesInGit33073308 p4Change =03309for branch in self.p4BranchesInGit:3310 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)33113312 settings =extractSettingsGitLog(logMsg)33133314 self.readOptions(settings)3315if(settings.has_key('depot-paths')3316and settings.has_key('change')):3317 change =int(settings['change']) +13318 p4Change =max(p4Change, change)33193320 depotPaths =sorted(settings['depot-paths'])3321if self.previousDepotPaths == []:3322 self.previousDepotPaths = depotPaths3323else:3324 paths = []3325for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3326 prev_list = prev.split("/")3327 cur_list = cur.split("/")3328for i inrange(0,min(len(cur_list),len(prev_list))):3329if cur_list[i] <> prev_list[i]:3330 i = i -13331break33323333 paths.append("/".join(cur_list[:i +1]))33343335 self.previousDepotPaths = paths33363337if p4Change >0:3338 self.depotPaths =sorted(self.previousDepotPaths)3339 self.changeRange ="@%s,#head"% p4Change3340if not self.silent and not self.detectBranches:3341print"Performing incremental import into%sgit branch"% self.branch33423343# accept multiple ref name abbreviations:3344# refs/foo/bar/branch -> use it exactly3345# p4/branch -> prepend refs/remotes/ or refs/heads/3346# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3347if not self.branch.startswith("refs/"):3348if self.importIntoRemotes:3349 prepend ="refs/remotes/"3350else:3351 prepend ="refs/heads/"3352if not self.branch.startswith("p4/"):3353 prepend +="p4/"3354 self.branch = prepend + self.branch33553356iflen(args) ==0and self.depotPaths:3357if not self.silent:3358print"Depot paths:%s"%' '.join(self.depotPaths)3359else:3360if self.depotPaths and self.depotPaths != args:3361print("previous import used depot path%sand now%swas specified. "3362"This doesn't work!"% (' '.join(self.depotPaths),3363' '.join(args)))3364 sys.exit(1)33653366 self.depotPaths =sorted(args)33673368 revision =""3369 self.users = {}33703371# Make sure no revision specifiers are used when --changesfile3372# is specified.3373 bad_changesfile =False3374iflen(self.changesFile) >0:3375for p in self.depotPaths:3376if p.find("@") >=0or p.find("#") >=0:3377 bad_changesfile =True3378break3379if bad_changesfile:3380die("Option --changesfile is incompatible with revision specifiers")33813382 newPaths = []3383for p in self.depotPaths:3384if p.find("@") != -1:3385 atIdx = p.index("@")3386 self.changeRange = p[atIdx:]3387if self.changeRange =="@all":3388 self.changeRange =""3389elif','not in self.changeRange:3390 revision = self.changeRange3391 self.changeRange =""3392 p = p[:atIdx]3393elif p.find("#") != -1:3394 hashIdx = p.index("#")3395 revision = p[hashIdx:]3396 p = p[:hashIdx]3397elif self.previousDepotPaths == []:3398# pay attention to changesfile, if given, else import3399# the entire p4 tree at the head revision3400iflen(self.changesFile) ==0:3401 revision ="#head"34023403 p = re.sub("\.\.\.$","", p)3404if not p.endswith("/"):3405 p +="/"34063407 newPaths.append(p)34083409 self.depotPaths = newPaths34103411# --detect-branches may change this for each branch3412 self.branchPrefixes = self.depotPaths34133414 self.loadUserMapFromCache()3415 self.labels = {}3416if self.detectLabels:3417 self.getLabels();34183419if self.detectBranches:3420## FIXME - what's a P4 projectName ?3421 self.projectName = self.guessProjectName()34223423if self.hasOrigin:3424 self.getBranchMappingFromGitBranches()3425else:3426 self.getBranchMapping()3427if self.verbose:3428print"p4-git branches:%s"% self.p4BranchesInGit3429print"initial parents:%s"% self.initialParents3430for b in self.p4BranchesInGit:3431if b !="master":34323433## FIXME3434 b = b[len(self.projectName):]3435 self.createdBranches.add(b)34363437 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))34383439 self.importProcess = subprocess.Popen(["git","fast-import"],3440 stdin=subprocess.PIPE,3441 stdout=subprocess.PIPE,3442 stderr=subprocess.PIPE);3443 self.gitOutput = self.importProcess.stdout3444 self.gitStream = self.importProcess.stdin3445 self.gitError = self.importProcess.stderr34463447if revision:3448 self.importHeadRevision(revision)3449else:3450 changes = []34513452iflen(self.changesFile) >0:3453 output =open(self.changesFile).readlines()3454 changeSet =set()3455for line in output:3456 changeSet.add(int(line))34573458for change in changeSet:3459 changes.append(change)34603461 changes.sort()3462else:3463# catch "git p4 sync" with no new branches, in a repo that3464# does not have any existing p4 branches3465iflen(args) ==0:3466if not self.p4BranchesInGit:3467die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")34683469# The default branch is master, unless --branch is used to3470# specify something else. Make sure it exists, or complain3471# nicely about how to use --branch.3472if not self.detectBranches:3473if notbranch_exists(self.branch):3474if branch_arg_given:3475die("Error: branch%sdoes not exist."% self.branch)3476else:3477die("Error: no branch%s; perhaps specify one with --branch."%3478 self.branch)34793480if self.verbose:3481print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3482 self.changeRange)3483 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)34843485iflen(self.maxChanges) >0:3486 changes = changes[:min(int(self.maxChanges),len(changes))]34873488iflen(changes) ==0:3489if not self.silent:3490print"No changes to import!"3491else:3492if not self.silent and not self.detectBranches:3493print"Import destination:%s"% self.branch34943495 self.updatedBranches =set()34963497if not self.detectBranches:3498if args:3499# start a new branch3500 self.initialParent =""3501else:3502# build on a previous revision3503 self.initialParent =parseRevision(self.branch)35043505 self.importChanges(changes)35063507if not self.silent:3508print""3509iflen(self.updatedBranches) >0:3510 sys.stdout.write("Updated branches: ")3511for b in self.updatedBranches:3512 sys.stdout.write("%s"% b)3513 sys.stdout.write("\n")35143515ifgitConfigBool("git-p4.importLabels"):3516 self.importLabels =True35173518if self.importLabels:3519 p4Labels =getP4Labels(self.depotPaths)3520 gitTags =getGitTags()35213522 missingP4Labels = p4Labels - gitTags3523 self.importP4Labels(self.gitStream, missingP4Labels)35243525 self.gitStream.close()3526if self.importProcess.wait() !=0:3527die("fast-import failed:%s"% self.gitError.read())3528 self.gitOutput.close()3529 self.gitError.close()35303531# Cleanup temporary branches created during import3532if self.tempBranches != []:3533for branch in self.tempBranches:3534read_pipe("git update-ref -d%s"% branch)3535 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))35363537# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3538# a convenient shortcut refname "p4".3539if self.importIntoRemotes:3540 head_ref = self.refPrefix +"HEAD"3541if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3542system(["git","symbolic-ref", head_ref, self.branch])35433544return True35453546classP4Rebase(Command):3547def__init__(self):3548 Command.__init__(self)3549 self.options = [3550 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3551]3552 self.importLabels =False3553 self.description = ("Fetches the latest revision from perforce and "3554+"rebases the current work (branch) against it")35553556defrun(self, args):3557 sync =P4Sync()3558 sync.importLabels = self.importLabels3559 sync.run([])35603561return self.rebase()35623563defrebase(self):3564if os.system("git update-index --refresh") !=0:3565die("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.");3566iflen(read_pipe("git diff-index HEAD --")) >0:3567die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");35683569[upstream, settings] =findUpstreamBranchPoint()3570iflen(upstream) ==0:3571die("Cannot find upstream branchpoint for rebase")35723573# the branchpoint may be p4/foo~3, so strip off the parent3574 upstream = re.sub("~[0-9]+$","", upstream)35753576print"Rebasing the current branch onto%s"% upstream3577 oldHead =read_pipe("git rev-parse HEAD").strip()3578system("git rebase%s"% upstream)3579system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3580return True35813582classP4Clone(P4Sync):3583def__init__(self):3584 P4Sync.__init__(self)3585 self.description ="Creates a new git repository and imports from Perforce into it"3586 self.usage ="usage: %prog [options] //depot/path[@revRange]"3587 self.options += [3588 optparse.make_option("--destination", dest="cloneDestination",3589 action='store', default=None,3590help="where to leave result of the clone"),3591 optparse.make_option("--bare", dest="cloneBare",3592 action="store_true", default=False),3593]3594 self.cloneDestination =None3595 self.needsGit =False3596 self.cloneBare =False35973598defdefaultDestination(self, args):3599## TODO: use common prefix of args?3600 depotPath = args[0]3601 depotDir = re.sub("(@[^@]*)$","", depotPath)3602 depotDir = re.sub("(#[^#]*)$","", depotDir)3603 depotDir = re.sub(r"\.\.\.$","", depotDir)3604 depotDir = re.sub(r"/$","", depotDir)3605return os.path.split(depotDir)[1]36063607defrun(self, args):3608iflen(args) <1:3609return False36103611if self.keepRepoPath and not self.cloneDestination:3612 sys.stderr.write("Must specify destination for --keep-path\n")3613 sys.exit(1)36143615 depotPaths = args36163617if not self.cloneDestination andlen(depotPaths) >1:3618 self.cloneDestination = depotPaths[-1]3619 depotPaths = depotPaths[:-1]36203621 self.cloneExclude = ["/"+p for p in self.cloneExclude]3622for p in depotPaths:3623if not p.startswith("//"):3624 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3625return False36263627if not self.cloneDestination:3628 self.cloneDestination = self.defaultDestination(args)36293630print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)36313632if not os.path.exists(self.cloneDestination):3633 os.makedirs(self.cloneDestination)3634chdir(self.cloneDestination)36353636 init_cmd = ["git","init"]3637if self.cloneBare:3638 init_cmd.append("--bare")3639 retcode = subprocess.call(init_cmd)3640if retcode:3641raiseCalledProcessError(retcode, init_cmd)36423643if not P4Sync.run(self, depotPaths):3644return False36453646# create a master branch and check out a work tree3647ifgitBranchExists(self.branch):3648system(["git","branch","master", self.branch ])3649if not self.cloneBare:3650system(["git","checkout","-f"])3651else:3652print'Not checking out any branch, use ' \3653'"git checkout -q -b master <branch>"'36543655# auto-set this variable if invoked with --use-client-spec3656if self.useClientSpec_from_options:3657system("git config --bool git-p4.useclientspec true")36583659return True36603661classP4Branches(Command):3662def__init__(self):3663 Command.__init__(self)3664 self.options = [ ]3665 self.description = ("Shows the git branches that hold imports and their "3666+"corresponding perforce depot paths")3667 self.verbose =False36683669defrun(self, args):3670iforiginP4BranchesExist():3671createOrUpdateBranchesFromOrigin()36723673 cmdline ="git rev-parse --symbolic "3674 cmdline +=" --remotes"36753676for line inread_pipe_lines(cmdline):3677 line = line.strip()36783679if not line.startswith('p4/')or line =="p4/HEAD":3680continue3681 branch = line36823683 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3684 settings =extractSettingsGitLog(log)36853686print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3687return True36883689classHelpFormatter(optparse.IndentedHelpFormatter):3690def__init__(self):3691 optparse.IndentedHelpFormatter.__init__(self)36923693defformat_description(self, description):3694if description:3695return description +"\n"3696else:3697return""36983699defprintUsage(commands):3700print"usage:%s<command> [options]"% sys.argv[0]3701print""3702print"valid commands:%s"%", ".join(commands)3703print""3704print"Try%s<command> --help for command specific help."% sys.argv[0]3705print""37063707commands = {3708"debug": P4Debug,3709"submit": P4Submit,3710"commit": P4Submit,3711"sync": P4Sync,3712"rebase": P4Rebase,3713"clone": P4Clone,3714"rollback": P4RollBack,3715"branches": P4Branches3716}371737183719defmain():3720iflen(sys.argv[1:]) ==0:3721printUsage(commands.keys())3722 sys.exit(2)37233724 cmdName = sys.argv[1]3725try:3726 klass = commands[cmdName]3727 cmd =klass()3728exceptKeyError:3729print"unknown command%s"% cmdName3730print""3731printUsage(commands.keys())3732 sys.exit(2)37333734 options = cmd.options3735 cmd.gitdir = os.environ.get("GIT_DIR",None)37363737 args = sys.argv[2:]37383739 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3740if cmd.needsGit:3741 options.append(optparse.make_option("--git-dir", dest="gitdir"))37423743 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3744 options,3745 description = cmd.description,3746 formatter =HelpFormatter())37473748(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3749global verbose3750 verbose = cmd.verbose3751if cmd.needsGit:3752if cmd.gitdir ==None:3753 cmd.gitdir = os.path.abspath(".git")3754if notisValidGitDir(cmd.gitdir):3755# "rev-parse --git-dir" without arguments will try $PWD/.git3756 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3757if os.path.exists(cmd.gitdir):3758 cdup =read_pipe("git rev-parse --show-cdup").strip()3759iflen(cdup) >0:3760chdir(cdup);37613762if notisValidGitDir(cmd.gitdir):3763ifisValidGitDir(cmd.gitdir +"/.git"):3764 cmd.gitdir +="/.git"3765else:3766die("fatal: cannot locate git repository at%s"% cmd.gitdir)37673768# so git commands invoked from the P4 workspace will succeed3769 os.environ["GIT_DIR"] = cmd.gitdir37703771if not cmd.run(args):3772 parser.print_help()3773 sys.exit(2)377437753776if __name__ =='__main__':3777main()