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_full(c): 164""" Read output from command. Returns a tuple 165 of the return status, stdout text and stderr 166 text. 167 """ 168if verbose: 169 sys.stderr.write('Reading pipe:%s\n'%str(c)) 170 171 expand =isinstance(c,basestring) 172 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 173(out, err) = p.communicate() 174return(p.returncode, out, err) 175 176defread_pipe(c, ignore_error=False): 177""" Read output from command. Returns the output text on 178 success. On failure, terminates execution, unless 179 ignore_error is True, when it returns an empty string. 180 """ 181(retcode, out, err) =read_pipe_full(c) 182if retcode !=0: 183if ignore_error: 184 out ="" 185else: 186die('Command failed:%s\nError:%s'% (str(c), err)) 187return out 188 189defread_pipe_text(c): 190""" Read output from a command with trailing whitespace stripped. 191 On error, returns None. 192 """ 193(retcode, out, err) =read_pipe_full(c) 194if retcode !=0: 195return None 196else: 197return out.rstrip() 198 199defp4_read_pipe(c, ignore_error=False): 200 real_cmd =p4_build_cmd(c) 201returnread_pipe(real_cmd, ignore_error) 202 203defread_pipe_lines(c): 204if verbose: 205 sys.stderr.write('Reading pipe:%s\n'%str(c)) 206 207 expand =isinstance(c, basestring) 208 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 209 pipe = p.stdout 210 val = pipe.readlines() 211if pipe.close()or p.wait(): 212die('Command failed:%s'%str(c)) 213 214return val 215 216defp4_read_pipe_lines(c): 217"""Specifically invoke p4 on the command supplied. """ 218 real_cmd =p4_build_cmd(c) 219returnread_pipe_lines(real_cmd) 220 221defp4_has_command(cmd): 222"""Ask p4 for help on this command. If it returns an error, the 223 command does not exist in this version of p4.""" 224 real_cmd =p4_build_cmd(["help", cmd]) 225 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 226 stderr=subprocess.PIPE) 227 p.communicate() 228return p.returncode ==0 229 230defp4_has_move_command(): 231"""See if the move command exists, that it supports -k, and that 232 it has not been administratively disabled. The arguments 233 must be correct, but the filenames do not have to exist. Use 234 ones with wildcards so even if they exist, it will fail.""" 235 236if notp4_has_command("move"): 237return False 238 cmd =p4_build_cmd(["move","-k","@from","@to"]) 239 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 240(out, err) = p.communicate() 241# return code will be 1 in either case 242if err.find("Invalid option") >=0: 243return False 244if err.find("disabled") >=0: 245return False 246# assume it failed because @... was invalid changelist 247return True 248 249defsystem(cmd, ignore_error=False): 250 expand =isinstance(cmd,basestring) 251if verbose: 252 sys.stderr.write("executing%s\n"%str(cmd)) 253 retcode = subprocess.call(cmd, shell=expand) 254if retcode and not ignore_error: 255raiseCalledProcessError(retcode, cmd) 256 257return retcode 258 259defp4_system(cmd): 260"""Specifically invoke p4 as the system command. """ 261 real_cmd =p4_build_cmd(cmd) 262 expand =isinstance(real_cmd, basestring) 263 retcode = subprocess.call(real_cmd, shell=expand) 264if retcode: 265raiseCalledProcessError(retcode, real_cmd) 266 267_p4_version_string =None 268defp4_version_string(): 269"""Read the version string, showing just the last line, which 270 hopefully is the interesting version bit. 271 272 $ p4 -V 273 Perforce - The Fast Software Configuration Management System. 274 Copyright 1995-2011 Perforce Software. All rights reserved. 275 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 276 """ 277global _p4_version_string 278if not _p4_version_string: 279 a =p4_read_pipe_lines(["-V"]) 280 _p4_version_string = a[-1].rstrip() 281return _p4_version_string 282 283defp4_integrate(src, dest): 284p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 285 286defp4_sync(f, *options): 287p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 288 289defp4_add(f): 290# forcibly add file names with wildcards 291ifwildcard_present(f): 292p4_system(["add","-f", f]) 293else: 294p4_system(["add", f]) 295 296defp4_delete(f): 297p4_system(["delete",wildcard_encode(f)]) 298 299defp4_edit(f, *options): 300p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 301 302defp4_revert(f): 303p4_system(["revert",wildcard_encode(f)]) 304 305defp4_reopen(type, f): 306p4_system(["reopen","-t",type,wildcard_encode(f)]) 307 308defp4_reopen_in_change(changelist, files): 309 cmd = ["reopen","-c",str(changelist)] + files 310p4_system(cmd) 311 312defp4_move(src, dest): 313p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 314 315defp4_last_change(): 316 results =p4CmdList(["changes","-m","1"]) 317returnint(results[0]['change']) 318 319defp4_describe(change): 320"""Make sure it returns a valid result by checking for 321 the presence of field "time". Return a dict of the 322 results.""" 323 324 ds =p4CmdList(["describe","-s",str(change)]) 325iflen(ds) !=1: 326die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 327 328 d = ds[0] 329 330if"p4ExitCode"in d: 331die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 332str(d))) 333if"code"in d: 334if d["code"] =="error": 335die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 336 337if"time"not in d: 338die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 339 340return d 341 342# 343# Canonicalize the p4 type and return a tuple of the 344# base type, plus any modifiers. See "p4 help filetypes" 345# for a list and explanation. 346# 347defsplit_p4_type(p4type): 348 349 p4_filetypes_historical = { 350"ctempobj":"binary+Sw", 351"ctext":"text+C", 352"cxtext":"text+Cx", 353"ktext":"text+k", 354"kxtext":"text+kx", 355"ltext":"text+F", 356"tempobj":"binary+FSw", 357"ubinary":"binary+F", 358"uresource":"resource+F", 359"uxbinary":"binary+Fx", 360"xbinary":"binary+x", 361"xltext":"text+Fx", 362"xtempobj":"binary+Swx", 363"xtext":"text+x", 364"xunicode":"unicode+x", 365"xutf16":"utf16+x", 366} 367if p4type in p4_filetypes_historical: 368 p4type = p4_filetypes_historical[p4type] 369 mods ="" 370 s = p4type.split("+") 371 base = s[0] 372 mods ="" 373iflen(s) >1: 374 mods = s[1] 375return(base, mods) 376 377# 378# return the raw p4 type of a file (text, text+ko, etc) 379# 380defp4_type(f): 381 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 382return results[0]['headType'] 383 384# 385# Given a type base and modifier, return a regexp matching 386# the keywords that can be expanded in the file 387# 388defp4_keywords_regexp_for_type(base, type_mods): 389if base in("text","unicode","binary"): 390 kwords =None 391if"ko"in type_mods: 392 kwords ='Id|Header' 393elif"k"in type_mods: 394 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 395else: 396return None 397 pattern = r""" 398 \$ # Starts with a dollar, followed by... 399 (%s) # one of the keywords, followed by... 400 (:[^$\n]+)? # possibly an old expansion, followed by... 401 \$ # another dollar 402 """% kwords 403return pattern 404else: 405return None 406 407# 408# Given a file, return a regexp matching the possible 409# RCS keywords that will be expanded, or None for files 410# with kw expansion turned off. 411# 412defp4_keywords_regexp_for_file(file): 413if not os.path.exists(file): 414return None 415else: 416(type_base, type_mods) =split_p4_type(p4_type(file)) 417returnp4_keywords_regexp_for_type(type_base, type_mods) 418 419defsetP4ExecBit(file, mode): 420# Reopens an already open file and changes the execute bit to match 421# the execute bit setting in the passed in mode. 422 423 p4Type ="+x" 424 425if notisModeExec(mode): 426 p4Type =getP4OpenedType(file) 427 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 428 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 429if p4Type[-1] =="+": 430 p4Type = p4Type[0:-1] 431 432p4_reopen(p4Type,file) 433 434defgetP4OpenedType(file): 435# Returns the perforce file type for the given file. 436 437 result =p4_read_pipe(["opened",wildcard_encode(file)]) 438 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 439if match: 440return match.group(1) 441else: 442die("Could not determine file type for%s(result: '%s')"% (file, result)) 443 444# Return the set of all p4 labels 445defgetP4Labels(depotPaths): 446 labels =set() 447ifisinstance(depotPaths,basestring): 448 depotPaths = [depotPaths] 449 450for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 451 label = l['label'] 452 labels.add(label) 453 454return labels 455 456# Return the set of all git tags 457defgetGitTags(): 458 gitTags =set() 459for line inread_pipe_lines(["git","tag"]): 460 tag = line.strip() 461 gitTags.add(tag) 462return gitTags 463 464defdiffTreePattern(): 465# This is a simple generator for the diff tree regex pattern. This could be 466# a class variable if this and parseDiffTreeEntry were a part of a class. 467 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 468while True: 469yield pattern 470 471defparseDiffTreeEntry(entry): 472"""Parses a single diff tree entry into its component elements. 473 474 See git-diff-tree(1) manpage for details about the format of the diff 475 output. This method returns a dictionary with the following elements: 476 477 src_mode - The mode of the source file 478 dst_mode - The mode of the destination file 479 src_sha1 - The sha1 for the source file 480 dst_sha1 - The sha1 fr the destination file 481 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 482 status_score - The score for the status (applicable for 'C' and 'R' 483 statuses). This is None if there is no score. 484 src - The path for the source file. 485 dst - The path for the destination file. This is only present for 486 copy or renames. If it is not present, this is None. 487 488 If the pattern is not matched, None is returned.""" 489 490 match =diffTreePattern().next().match(entry) 491if match: 492return{ 493'src_mode': match.group(1), 494'dst_mode': match.group(2), 495'src_sha1': match.group(3), 496'dst_sha1': match.group(4), 497'status': match.group(5), 498'status_score': match.group(6), 499'src': match.group(7), 500'dst': match.group(10) 501} 502return None 503 504defisModeExec(mode): 505# Returns True if the given git mode represents an executable file, 506# otherwise False. 507return mode[-3:] =="755" 508 509defisModeExecChanged(src_mode, dst_mode): 510returnisModeExec(src_mode) !=isModeExec(dst_mode) 511 512defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 513 514ifisinstance(cmd,basestring): 515 cmd ="-G "+ cmd 516 expand =True 517else: 518 cmd = ["-G"] + cmd 519 expand =False 520 521 cmd =p4_build_cmd(cmd) 522if verbose: 523 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 524 525# Use a temporary file to avoid deadlocks without 526# subprocess.communicate(), which would put another copy 527# of stdout into memory. 528 stdin_file =None 529if stdin is not None: 530 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 531ifisinstance(stdin,basestring): 532 stdin_file.write(stdin) 533else: 534for i in stdin: 535 stdin_file.write(i +'\n') 536 stdin_file.flush() 537 stdin_file.seek(0) 538 539 p4 = subprocess.Popen(cmd, 540 shell=expand, 541 stdin=stdin_file, 542 stdout=subprocess.PIPE) 543 544 result = [] 545try: 546while True: 547 entry = marshal.load(p4.stdout) 548if cb is not None: 549cb(entry) 550else: 551 result.append(entry) 552exceptEOFError: 553pass 554 exitCode = p4.wait() 555if exitCode !=0: 556 entry = {} 557 entry["p4ExitCode"] = exitCode 558 result.append(entry) 559 560return result 561 562defp4Cmd(cmd): 563list=p4CmdList(cmd) 564 result = {} 565for entry inlist: 566 result.update(entry) 567return result; 568 569defp4Where(depotPath): 570if not depotPath.endswith("/"): 571 depotPath +="/" 572 depotPathLong = depotPath +"..." 573 outputList =p4CmdList(["where", depotPathLong]) 574 output =None 575for entry in outputList: 576if"depotFile"in entry: 577# Search for the base client side depot path, as long as it starts with the branch's P4 path. 578# The base path always ends with "/...". 579if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 580 output = entry 581break 582elif"data"in entry: 583 data = entry.get("data") 584 space = data.find(" ") 585if data[:space] == depotPath: 586 output = entry 587break 588if output ==None: 589return"" 590if output["code"] =="error": 591return"" 592 clientPath ="" 593if"path"in output: 594 clientPath = output.get("path") 595elif"data"in output: 596 data = output.get("data") 597 lastSpace = data.rfind(" ") 598 clientPath = data[lastSpace +1:] 599 600if clientPath.endswith("..."): 601 clientPath = clientPath[:-3] 602return clientPath 603 604defcurrentGitBranch(): 605returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 606 607defisValidGitDir(path): 608returngit_dir(path) !=None 609 610defparseRevision(ref): 611returnread_pipe("git rev-parse%s"% ref).strip() 612 613defbranchExists(ref): 614 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 615 ignore_error=True) 616returnlen(rev) >0 617 618defextractLogMessageFromGitCommit(commit): 619 logMessage ="" 620 621## fixme: title is first line of commit, not 1st paragraph. 622 foundTitle =False 623for log inread_pipe_lines("git cat-file commit%s"% commit): 624if not foundTitle: 625iflen(log) ==1: 626 foundTitle =True 627continue 628 629 logMessage += log 630return logMessage 631 632defextractSettingsGitLog(log): 633 values = {} 634for line in log.split("\n"): 635 line = line.strip() 636 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 637if not m: 638continue 639 640 assignments = m.group(1).split(':') 641for a in assignments: 642 vals = a.split('=') 643 key = vals[0].strip() 644 val = ('='.join(vals[1:])).strip() 645if val.endswith('\"')and val.startswith('"'): 646 val = val[1:-1] 647 648 values[key] = val 649 650 paths = values.get("depot-paths") 651if not paths: 652 paths = values.get("depot-path") 653if paths: 654 values['depot-paths'] = paths.split(',') 655return values 656 657defgitBranchExists(branch): 658 proc = subprocess.Popen(["git","rev-parse", branch], 659 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 660return proc.wait() ==0; 661 662_gitConfig = {} 663 664defgitConfig(key, typeSpecifier=None): 665if not _gitConfig.has_key(key): 666 cmd = ["git","config"] 667if typeSpecifier: 668 cmd += [ typeSpecifier ] 669 cmd += [ key ] 670 s =read_pipe(cmd, ignore_error=True) 671 _gitConfig[key] = s.strip() 672return _gitConfig[key] 673 674defgitConfigBool(key): 675"""Return a bool, using git config --bool. It is True only if the 676 variable is set to true, and False if set to false or not present 677 in the config.""" 678 679if not _gitConfig.has_key(key): 680 _gitConfig[key] =gitConfig(key,'--bool') =="true" 681return _gitConfig[key] 682 683defgitConfigInt(key): 684if not _gitConfig.has_key(key): 685 cmd = ["git","config","--int", key ] 686 s =read_pipe(cmd, ignore_error=True) 687 v = s.strip() 688try: 689 _gitConfig[key] =int(gitConfig(key,'--int')) 690exceptValueError: 691 _gitConfig[key] =None 692return _gitConfig[key] 693 694defgitConfigList(key): 695if not _gitConfig.has_key(key): 696 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 697 _gitConfig[key] = s.strip().splitlines() 698if _gitConfig[key] == ['']: 699 _gitConfig[key] = [] 700return _gitConfig[key] 701 702defp4BranchesInGit(branchesAreInRemotes=True): 703"""Find all the branches whose names start with "p4/", looking 704 in remotes or heads as specified by the argument. Return 705 a dictionary of{ branch: revision }for each one found. 706 The branch names are the short names, without any 707 "p4/" prefix.""" 708 709 branches = {} 710 711 cmdline ="git rev-parse --symbolic " 712if branchesAreInRemotes: 713 cmdline +="--remotes" 714else: 715 cmdline +="--branches" 716 717for line inread_pipe_lines(cmdline): 718 line = line.strip() 719 720# only import to p4/ 721if not line.startswith('p4/'): 722continue 723# special symbolic ref to p4/master 724if line =="p4/HEAD": 725continue 726 727# strip off p4/ prefix 728 branch = line[len("p4/"):] 729 730 branches[branch] =parseRevision(line) 731 732return branches 733 734defbranch_exists(branch): 735"""Make sure that the given ref name really exists.""" 736 737 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 738 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 739 out, _ = p.communicate() 740if p.returncode: 741return False 742# expect exactly one line of output: the branch name 743return out.rstrip() == branch 744 745deffindUpstreamBranchPoint(head ="HEAD"): 746 branches =p4BranchesInGit() 747# map from depot-path to branch name 748 branchByDepotPath = {} 749for branch in branches.keys(): 750 tip = branches[branch] 751 log =extractLogMessageFromGitCommit(tip) 752 settings =extractSettingsGitLog(log) 753if settings.has_key("depot-paths"): 754 paths =",".join(settings["depot-paths"]) 755 branchByDepotPath[paths] ="remotes/p4/"+ branch 756 757 settings =None 758 parent =0 759while parent <65535: 760 commit = head +"~%s"% parent 761 log =extractLogMessageFromGitCommit(commit) 762 settings =extractSettingsGitLog(log) 763if settings.has_key("depot-paths"): 764 paths =",".join(settings["depot-paths"]) 765if branchByDepotPath.has_key(paths): 766return[branchByDepotPath[paths], settings] 767 768 parent = parent +1 769 770return["", settings] 771 772defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 773if not silent: 774print("Creating/updating branch(es) in%sbased on origin branch(es)" 775% localRefPrefix) 776 777 originPrefix ="origin/p4/" 778 779for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 780 line = line.strip() 781if(not line.startswith(originPrefix))or line.endswith("HEAD"): 782continue 783 784 headName = line[len(originPrefix):] 785 remoteHead = localRefPrefix + headName 786 originHead = line 787 788 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 789if(not original.has_key('depot-paths') 790or not original.has_key('change')): 791continue 792 793 update =False 794if notgitBranchExists(remoteHead): 795if verbose: 796print"creating%s"% remoteHead 797 update =True 798else: 799 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 800if settings.has_key('change') >0: 801if settings['depot-paths'] == original['depot-paths']: 802 originP4Change =int(original['change']) 803 p4Change =int(settings['change']) 804if originP4Change > p4Change: 805print("%s(%s) is newer than%s(%s). " 806"Updating p4 branch from origin." 807% (originHead, originP4Change, 808 remoteHead, p4Change)) 809 update =True 810else: 811print("Ignoring:%swas imported from%swhile " 812"%swas imported from%s" 813% (originHead,','.join(original['depot-paths']), 814 remoteHead,','.join(settings['depot-paths']))) 815 816if update: 817system("git update-ref%s %s"% (remoteHead, originHead)) 818 819deforiginP4BranchesExist(): 820returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 821 822 823defp4ParseNumericChangeRange(parts): 824 changeStart =int(parts[0][1:]) 825if parts[1] =='#head': 826 changeEnd =p4_last_change() 827else: 828 changeEnd =int(parts[1]) 829 830return(changeStart, changeEnd) 831 832defchooseBlockSize(blockSize): 833if blockSize: 834return blockSize 835else: 836return defaultBlockSize 837 838defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 839assert depotPaths 840 841# Parse the change range into start and end. Try to find integer 842# revision ranges as these can be broken up into blocks to avoid 843# hitting server-side limits (maxrows, maxscanresults). But if 844# that doesn't work, fall back to using the raw revision specifier 845# strings, without using block mode. 846 847if changeRange is None or changeRange =='': 848 changeStart =1 849 changeEnd =p4_last_change() 850 block_size =chooseBlockSize(requestedBlockSize) 851else: 852 parts = changeRange.split(',') 853assertlen(parts) ==2 854try: 855(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 856 block_size =chooseBlockSize(requestedBlockSize) 857except: 858 changeStart = parts[0][1:] 859 changeEnd = parts[1] 860if requestedBlockSize: 861die("cannot use --changes-block-size with non-numeric revisions") 862 block_size =None 863 864 changes =set() 865 866# Retrieve changes a block at a time, to prevent running 867# into a MaxResults/MaxScanRows error from the server. 868 869while True: 870 cmd = ['changes'] 871 872if block_size: 873 end =min(changeEnd, changeStart + block_size) 874 revisionRange ="%d,%d"% (changeStart, end) 875else: 876 revisionRange ="%s,%s"% (changeStart, changeEnd) 877 878for p in depotPaths: 879 cmd += ["%s...@%s"% (p, revisionRange)] 880 881# Insert changes in chronological order 882for line inreversed(p4_read_pipe_lines(cmd)): 883 changes.add(int(line.split(" ")[1])) 884 885if not block_size: 886break 887 888if end >= changeEnd: 889break 890 891 changeStart = end +1 892 893 changes =sorted(changes) 894return changes 895 896defp4PathStartsWith(path, prefix): 897# This method tries to remedy a potential mixed-case issue: 898# 899# If UserA adds //depot/DirA/file1 900# and UserB adds //depot/dira/file2 901# 902# we may or may not have a problem. If you have core.ignorecase=true, 903# we treat DirA and dira as the same directory 904ifgitConfigBool("core.ignorecase"): 905return path.lower().startswith(prefix.lower()) 906return path.startswith(prefix) 907 908defgetClientSpec(): 909"""Look at the p4 client spec, create a View() object that contains 910 all the mappings, and return it.""" 911 912 specList =p4CmdList("client -o") 913iflen(specList) !=1: 914die('Output from "client -o" is%dlines, expecting 1'% 915len(specList)) 916 917# dictionary of all client parameters 918 entry = specList[0] 919 920# the //client/ name 921 client_name = entry["Client"] 922 923# just the keys that start with "View" 924 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 925 926# hold this new View 927 view =View(client_name) 928 929# append the lines, in order, to the view 930for view_num inrange(len(view_keys)): 931 k ="View%d"% view_num 932if k not in view_keys: 933die("Expected view key%smissing"% k) 934 view.append(entry[k]) 935 936return view 937 938defgetClientRoot(): 939"""Grab the client directory.""" 940 941 output =p4CmdList("client -o") 942iflen(output) !=1: 943die('Output from "client -o" is%dlines, expecting 1'%len(output)) 944 945 entry = output[0] 946if"Root"not in entry: 947die('Client has no "Root"') 948 949return entry["Root"] 950 951# 952# P4 wildcards are not allowed in filenames. P4 complains 953# if you simply add them, but you can force it with "-f", in 954# which case it translates them into %xx encoding internally. 955# 956defwildcard_decode(path): 957# Search for and fix just these four characters. Do % last so 958# that fixing it does not inadvertently create new %-escapes. 959# Cannot have * in a filename in windows; untested as to 960# what p4 would do in such a case. 961if not platform.system() =="Windows": 962 path = path.replace("%2A","*") 963 path = path.replace("%23","#") \ 964.replace("%40","@") \ 965.replace("%25","%") 966return path 967 968defwildcard_encode(path): 969# do % first to avoid double-encoding the %s introduced here 970 path = path.replace("%","%25") \ 971.replace("*","%2A") \ 972.replace("#","%23") \ 973.replace("@","%40") 974return path 975 976defwildcard_present(path): 977 m = re.search("[*#@%]", path) 978return m is not None 979 980classLargeFileSystem(object): 981"""Base class for large file system support.""" 982 983def__init__(self, writeToGitStream): 984 self.largeFiles =set() 985 self.writeToGitStream = writeToGitStream 986 987defgeneratePointer(self, cloneDestination, contentFile): 988"""Return the content of a pointer file that is stored in Git instead of 989 the actual content.""" 990assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 991 992defpushFile(self, localLargeFile): 993"""Push the actual content which is not stored in the Git repository to 994 a server.""" 995assert False,"Method 'pushFile' required in "+ self.__class__.__name__ 996 997defhasLargeFileExtension(self, relPath): 998returnreduce( 999lambda a, b: a or b,1000[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1001False1002)10031004defgenerateTempFile(self, contents):1005 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1006for d in contents:1007 contentFile.write(d)1008 contentFile.close()1009return contentFile.name10101011defexceedsLargeFileThreshold(self, relPath, contents):1012ifgitConfigInt('git-p4.largeFileThreshold'):1013 contentsSize =sum(len(d)for d in contents)1014if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1015return True1016ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1017 contentsSize =sum(len(d)for d in contents)1018if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1019return False1020 contentTempFile = self.generateTempFile(contents)1021 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1022 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1023 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1024 zf.close()1025 compressedContentsSize = zf.infolist()[0].compress_size1026 os.remove(contentTempFile)1027 os.remove(compressedContentFile.name)1028if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1029return True1030return False10311032defaddLargeFile(self, relPath):1033 self.largeFiles.add(relPath)10341035defremoveLargeFile(self, relPath):1036 self.largeFiles.remove(relPath)10371038defisLargeFile(self, relPath):1039return relPath in self.largeFiles10401041defprocessContent(self, git_mode, relPath, contents):1042"""Processes the content of git fast import. This method decides if a1043 file is stored in the large file system and handles all necessary1044 steps."""1045if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1046 contentTempFile = self.generateTempFile(contents)1047(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1048if pointer_git_mode:1049 git_mode = pointer_git_mode1050if localLargeFile:1051# Move temp file to final location in large file system1052 largeFileDir = os.path.dirname(localLargeFile)1053if not os.path.isdir(largeFileDir):1054 os.makedirs(largeFileDir)1055 shutil.move(contentTempFile, localLargeFile)1056 self.addLargeFile(relPath)1057ifgitConfigBool('git-p4.largeFilePush'):1058 self.pushFile(localLargeFile)1059if verbose:1060 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1061return(git_mode, contents)10621063classMockLFS(LargeFileSystem):1064"""Mock large file system for testing."""10651066defgeneratePointer(self, contentFile):1067"""The pointer content is the original content prefixed with "pointer-".1068 The local filename of the large file storage is derived from the file content.1069 """1070withopen(contentFile,'r')as f:1071 content =next(f)1072 gitMode ='100644'1073 pointerContents ='pointer-'+ content1074 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1075return(gitMode, pointerContents, localLargeFile)10761077defpushFile(self, localLargeFile):1078"""The remote filename of the large file storage is the same as the local1079 one but in a different directory.1080 """1081 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1082if not os.path.exists(remotePath):1083 os.makedirs(remotePath)1084 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10851086classGitLFS(LargeFileSystem):1087"""Git LFS as backend for the git-p4 large file system.1088 See https://git-lfs.github.com/ for details."""10891090def__init__(self, *args):1091 LargeFileSystem.__init__(self, *args)1092 self.baseGitAttributes = []10931094defgeneratePointer(self, contentFile):1095"""Generate a Git LFS pointer for the content. Return LFS Pointer file1096 mode and content which is stored in the Git repository instead of1097 the actual content. Return also the new location of the actual1098 content.1099 """1100if os.path.getsize(contentFile) ==0:1101return(None,'',None)11021103 pointerProcess = subprocess.Popen(1104['git','lfs','pointer','--file='+ contentFile],1105 stdout=subprocess.PIPE1106)1107 pointerFile = pointerProcess.stdout.read()1108if pointerProcess.wait():1109 os.remove(contentFile)1110die('git-lfs pointer command failed. Did you install the extension?')11111112# Git LFS removed the preamble in the output of the 'pointer' command1113# starting from version 1.2.0. Check for the preamble here to support1114# earlier versions.1115# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431116if pointerFile.startswith('Git LFS pointer for'):1117 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)11181119 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1120 localLargeFile = os.path.join(1121 os.getcwd(),1122'.git','lfs','objects', oid[:2], oid[2:4],1123 oid,1124)1125# LFS Spec states that pointer files should not have the executable bit set.1126 gitMode ='100644'1127return(gitMode, pointerFile, localLargeFile)11281129defpushFile(self, localLargeFile):1130 uploadProcess = subprocess.Popen(1131['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1132)1133if uploadProcess.wait():1134die('git-lfs push command failed. Did you define a remote?')11351136defgenerateGitAttributes(self):1137return(1138 self.baseGitAttributes +1139[1140'\n',1141'#\n',1142'# Git LFS (see https://git-lfs.github.com/)\n',1143'#\n',1144] +1145['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1146for f insorted(gitConfigList('git-p4.largeFileExtensions'))1147] +1148['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1149for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1150]1151)11521153defaddLargeFile(self, relPath):1154 LargeFileSystem.addLargeFile(self, relPath)1155 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11561157defremoveLargeFile(self, relPath):1158 LargeFileSystem.removeLargeFile(self, relPath)1159 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11601161defprocessContent(self, git_mode, relPath, contents):1162if relPath =='.gitattributes':1163 self.baseGitAttributes = contents1164return(git_mode, self.generateGitAttributes())1165else:1166return LargeFileSystem.processContent(self, git_mode, relPath, contents)11671168class Command:1169def__init__(self):1170 self.usage ="usage: %prog [options]"1171 self.needsGit =True1172 self.verbose =False11731174class P4UserMap:1175def__init__(self):1176 self.userMapFromPerforceServer =False1177 self.myP4UserId =None11781179defp4UserId(self):1180if self.myP4UserId:1181return self.myP4UserId11821183 results =p4CmdList("user -o")1184for r in results:1185if r.has_key('User'):1186 self.myP4UserId = r['User']1187return r['User']1188die("Could not find your p4 user id")11891190defp4UserIsMe(self, p4User):1191# return True if the given p4 user is actually me1192 me = self.p4UserId()1193if not p4User or p4User != me:1194return False1195else:1196return True11971198defgetUserCacheFilename(self):1199 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1200return home +"/.gitp4-usercache.txt"12011202defgetUserMapFromPerforceServer(self):1203if self.userMapFromPerforceServer:1204return1205 self.users = {}1206 self.emails = {}12071208for output inp4CmdList("users"):1209if not output.has_key("User"):1210continue1211 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1212 self.emails[output["Email"]] = output["User"]12131214 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1215for mapUserConfig ingitConfigList("git-p4.mapUser"):1216 mapUser = mapUserConfigRegex.findall(mapUserConfig)1217if mapUser andlen(mapUser[0]) ==3:1218 user = mapUser[0][0]1219 fullname = mapUser[0][1]1220 email = mapUser[0][2]1221 self.users[user] = fullname +" <"+ email +">"1222 self.emails[email] = user12231224 s =''1225for(key, val)in self.users.items():1226 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12271228open(self.getUserCacheFilename(),"wb").write(s)1229 self.userMapFromPerforceServer =True12301231defloadUserMapFromCache(self):1232 self.users = {}1233 self.userMapFromPerforceServer =False1234try:1235 cache =open(self.getUserCacheFilename(),"rb")1236 lines = cache.readlines()1237 cache.close()1238for line in lines:1239 entry = line.strip().split("\t")1240 self.users[entry[0]] = entry[1]1241exceptIOError:1242 self.getUserMapFromPerforceServer()12431244classP4Debug(Command):1245def__init__(self):1246 Command.__init__(self)1247 self.options = []1248 self.description ="A tool to debug the output of p4 -G."1249 self.needsGit =False12501251defrun(self, args):1252 j =01253for output inp4CmdList(args):1254print'Element:%d'% j1255 j +=11256print output1257return True12581259classP4RollBack(Command):1260def__init__(self):1261 Command.__init__(self)1262 self.options = [1263 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1264]1265 self.description ="A tool to debug the multi-branch import. Don't use :)"1266 self.rollbackLocalBranches =False12671268defrun(self, args):1269iflen(args) !=1:1270return False1271 maxChange =int(args[0])12721273if"p4ExitCode"inp4Cmd("changes -m 1"):1274die("Problems executing p4");12751276if self.rollbackLocalBranches:1277 refPrefix ="refs/heads/"1278 lines =read_pipe_lines("git rev-parse --symbolic --branches")1279else:1280 refPrefix ="refs/remotes/"1281 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12821283for line in lines:1284if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1285 line = line.strip()1286 ref = refPrefix + line1287 log =extractLogMessageFromGitCommit(ref)1288 settings =extractSettingsGitLog(log)12891290 depotPaths = settings['depot-paths']1291 change = settings['change']12921293 changed =False12941295iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1296for p in depotPaths]))) ==0:1297print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1298system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1299continue13001301while change andint(change) > maxChange:1302 changed =True1303if self.verbose:1304print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1305system("git update-ref%s\"%s^\""% (ref, ref))1306 log =extractLogMessageFromGitCommit(ref)1307 settings =extractSettingsGitLog(log)130813091310 depotPaths = settings['depot-paths']1311 change = settings['change']13121313if changed:1314print"%srewound to%s"% (ref, change)13151316return True13171318classP4Submit(Command, P4UserMap):13191320 conflict_behavior_choices = ("ask","skip","quit")13211322def__init__(self):1323 Command.__init__(self)1324 P4UserMap.__init__(self)1325 self.options = [1326 optparse.make_option("--origin", dest="origin"),1327 optparse.make_option("-M", dest="detectRenames", action="store_true"),1328# preserve the user, requires relevant p4 permissions1329 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1330 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1331 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1332 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1333 optparse.make_option("--conflict", dest="conflict_behavior",1334 choices=self.conflict_behavior_choices),1335 optparse.make_option("--branch", dest="branch"),1336 optparse.make_option("--shelve", dest="shelve", action="store_true",1337help="Shelve instead of submit. Shelved files are reverted, "1338"restoring the workspace to the state before the shelve"),1339 optparse.make_option("--update-shelve", dest="update_shelve", action="store",type="int",1340 metavar="CHANGELIST",1341help="update an existing shelved changelist, implies --shelve")1342]1343 self.description ="Submit changes from git to the perforce depot."1344 self.usage +=" [name of git branch to submit into perforce depot]"1345 self.origin =""1346 self.detectRenames =False1347 self.preserveUser =gitConfigBool("git-p4.preserveUser")1348 self.dry_run =False1349 self.shelve =False1350 self.update_shelve =None1351 self.prepare_p4_only =False1352 self.conflict_behavior =None1353 self.isWindows = (platform.system() =="Windows")1354 self.exportLabels =False1355 self.p4HasMoveCommand =p4_has_move_command()1356 self.branch =None13571358ifgitConfig('git-p4.largeFileSystem'):1359die("Large file system not supported for git-p4 submit command. Please remove it from config.")13601361defcheck(self):1362iflen(p4CmdList("opened ...")) >0:1363die("You have files opened with perforce! Close them before starting the sync.")13641365defseparate_jobs_from_description(self, message):1366"""Extract and return a possible Jobs field in the commit1367 message. It goes into a separate section in the p4 change1368 specification.13691370 A jobs line starts with "Jobs:" and looks like a new field1371 in a form. Values are white-space separated on the same1372 line or on following lines that start with a tab.13731374 This does not parse and extract the full git commit message1375 like a p4 form. It just sees the Jobs: line as a marker1376 to pass everything from then on directly into the p4 form,1377 but outside the description section.13781379 Return a tuple (stripped log message, jobs string)."""13801381 m = re.search(r'^Jobs:', message, re.MULTILINE)1382if m is None:1383return(message,None)13841385 jobtext = message[m.start():]1386 stripped_message = message[:m.start()].rstrip()1387return(stripped_message, jobtext)13881389defprepareLogMessage(self, template, message, jobs):1390"""Edits the template returned from "p4 change -o" to insert1391 the message in the Description field, and the jobs text in1392 the Jobs field."""1393 result =""13941395 inDescriptionSection =False13961397for line in template.split("\n"):1398if line.startswith("#"):1399 result += line +"\n"1400continue14011402if inDescriptionSection:1403if line.startswith("Files:")or line.startswith("Jobs:"):1404 inDescriptionSection =False1405# insert Jobs section1406if jobs:1407 result += jobs +"\n"1408else:1409continue1410else:1411if line.startswith("Description:"):1412 inDescriptionSection =True1413 line +="\n"1414for messageLine in message.split("\n"):1415 line +="\t"+ messageLine +"\n"14161417 result += line +"\n"14181419return result14201421defpatchRCSKeywords(self,file, pattern):1422# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1423(handle, outFileName) = tempfile.mkstemp(dir='.')1424try:1425 outFile = os.fdopen(handle,"w+")1426 inFile =open(file,"r")1427 regexp = re.compile(pattern, re.VERBOSE)1428for line in inFile.readlines():1429 line = regexp.sub(r'$\1$', line)1430 outFile.write(line)1431 inFile.close()1432 outFile.close()1433# Forcibly overwrite the original file1434 os.unlink(file)1435 shutil.move(outFileName,file)1436except:1437# cleanup our temporary file1438 os.unlink(outFileName)1439print"Failed to strip RCS keywords in%s"%file1440raise14411442print"Patched up RCS keywords in%s"%file14431444defp4UserForCommit(self,id):1445# Return the tuple (perforce user,git email) for a given git commit id1446 self.getUserMapFromPerforceServer()1447 gitEmail =read_pipe(["git","log","--max-count=1",1448"--format=%ae",id])1449 gitEmail = gitEmail.strip()1450if not self.emails.has_key(gitEmail):1451return(None,gitEmail)1452else:1453return(self.emails[gitEmail],gitEmail)14541455defcheckValidP4Users(self,commits):1456# check if any git authors cannot be mapped to p4 users1457foridin commits:1458(user,email) = self.p4UserForCommit(id)1459if not user:1460 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1461ifgitConfigBool("git-p4.allowMissingP4Users"):1462print"%s"% msg1463else:1464die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14651466deflastP4Changelist(self):1467# Get back the last changelist number submitted in this client spec. This1468# then gets used to patch up the username in the change. If the same1469# client spec is being used by multiple processes then this might go1470# wrong.1471 results =p4CmdList("client -o")# find the current client1472 client =None1473for r in results:1474if r.has_key('Client'):1475 client = r['Client']1476break1477if not client:1478die("could not get client spec")1479 results =p4CmdList(["changes","-c", client,"-m","1"])1480for r in results:1481if r.has_key('change'):1482return r['change']1483die("Could not get changelist number for last submit - cannot patch up user details")14841485defmodifyChangelistUser(self, changelist, newUser):1486# fixup the user field of a changelist after it has been submitted.1487 changes =p4CmdList("change -o%s"% changelist)1488iflen(changes) !=1:1489die("Bad output from p4 change modifying%sto user%s"%1490(changelist, newUser))14911492 c = changes[0]1493if c['User'] == newUser:return# nothing to do1494 c['User'] = newUser1495input= marshal.dumps(c)14961497 result =p4CmdList("change -f -i", stdin=input)1498for r in result:1499if r.has_key('code'):1500if r['code'] =='error':1501die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1502if r.has_key('data'):1503print("Updated user field for changelist%sto%s"% (changelist, newUser))1504return1505die("Could not modify user field of changelist%sto%s"% (changelist, newUser))15061507defcanChangeChangelists(self):1508# check to see if we have p4 admin or super-user permissions, either of1509# which are required to modify changelists.1510 results =p4CmdList(["protects", self.depotPath])1511for r in results:1512if r.has_key('perm'):1513if r['perm'] =='admin':1514return11515if r['perm'] =='super':1516return11517return015181519defprepareSubmitTemplate(self, changelist=None):1520"""Run "p4 change -o" to grab a change specification template.1521 This does not use "p4 -G", as it is nice to keep the submission1522 template in original order, since a human might edit it.15231524 Remove lines in the Files section that show changes to files1525 outside the depot path we're committing into."""15261527[upstream, settings] =findUpstreamBranchPoint()15281529 template =""1530 inFilesSection =False1531 args = ['change','-o']1532if changelist:1533 args.append(str(changelist))15341535for line inp4_read_pipe_lines(args):1536if line.endswith("\r\n"):1537 line = line[:-2] +"\n"1538if inFilesSection:1539if line.startswith("\t"):1540# path starts and ends with a tab1541 path = line[1:]1542 lastTab = path.rfind("\t")1543if lastTab != -1:1544 path = path[:lastTab]1545if settings.has_key('depot-paths'):1546if not[p for p in settings['depot-paths']1547ifp4PathStartsWith(path, p)]:1548continue1549else:1550if notp4PathStartsWith(path, self.depotPath):1551continue1552else:1553 inFilesSection =False1554else:1555if line.startswith("Files:"):1556 inFilesSection =True15571558 template += line15591560return template15611562defedit_template(self, template_file):1563"""Invoke the editor to let the user change the submission1564 message. Return true if okay to continue with the submit."""15651566# if configured to skip the editing part, just submit1567ifgitConfigBool("git-p4.skipSubmitEdit"):1568return True15691570# look at the modification time, to check later if the user saved1571# the file1572 mtime = os.stat(template_file).st_mtime15731574# invoke the editor1575if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1576 editor = os.environ.get("P4EDITOR")1577else:1578 editor =read_pipe("git var GIT_EDITOR").strip()1579system(["sh","-c", ('%s"$@"'% editor), editor, template_file])15801581# If the file was not saved, prompt to see if this patch should1582# be skipped. But skip this verification step if configured so.1583ifgitConfigBool("git-p4.skipSubmitEditCheck"):1584return True15851586# modification time updated means user saved the file1587if os.stat(template_file).st_mtime > mtime:1588return True15891590while True:1591 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1592if response =='y':1593return True1594if response =='n':1595return False15961597defget_diff_description(self, editedFiles, filesToAdd, symlinks):1598# diff1599if os.environ.has_key("P4DIFF"):1600del(os.environ["P4DIFF"])1601 diff =""1602for editedFile in editedFiles:1603 diff +=p4_read_pipe(['diff','-du',1604wildcard_encode(editedFile)])16051606# new file diff1607 newdiff =""1608for newFile in filesToAdd:1609 newdiff +="==== new file ====\n"1610 newdiff +="--- /dev/null\n"1611 newdiff +="+++%s\n"% newFile16121613 is_link = os.path.islink(newFile)1614 expect_link = newFile in symlinks16151616if is_link and expect_link:1617 newdiff +="+%s\n"% os.readlink(newFile)1618else:1619 f =open(newFile,"r")1620for line in f.readlines():1621 newdiff +="+"+ line1622 f.close()16231624return(diff + newdiff).replace('\r\n','\n')16251626defapplyCommit(self,id):1627"""Apply one commit, return True if it succeeded."""16281629print"Applying",read_pipe(["git","show","-s",1630"--format=format:%h%s",id])16311632(p4User, gitEmail) = self.p4UserForCommit(id)16331634 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1635 filesToAdd =set()1636 filesToChangeType =set()1637 filesToDelete =set()1638 editedFiles =set()1639 pureRenameCopy =set()1640 symlinks =set()1641 filesToChangeExecBit = {}1642 all_files =list()16431644for line in diff:1645 diff =parseDiffTreeEntry(line)1646 modifier = diff['status']1647 path = diff['src']1648 all_files.append(path)16491650if modifier =="M":1651p4_edit(path)1652ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1653 filesToChangeExecBit[path] = diff['dst_mode']1654 editedFiles.add(path)1655elif modifier =="A":1656 filesToAdd.add(path)1657 filesToChangeExecBit[path] = diff['dst_mode']1658if path in filesToDelete:1659 filesToDelete.remove(path)16601661 dst_mode =int(diff['dst_mode'],8)1662if dst_mode ==0120000:1663 symlinks.add(path)16641665elif modifier =="D":1666 filesToDelete.add(path)1667if path in filesToAdd:1668 filesToAdd.remove(path)1669elif modifier =="C":1670 src, dest = diff['src'], diff['dst']1671p4_integrate(src, dest)1672 pureRenameCopy.add(dest)1673if diff['src_sha1'] != diff['dst_sha1']:1674p4_edit(dest)1675 pureRenameCopy.discard(dest)1676ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1677p4_edit(dest)1678 pureRenameCopy.discard(dest)1679 filesToChangeExecBit[dest] = diff['dst_mode']1680if self.isWindows:1681# turn off read-only attribute1682 os.chmod(dest, stat.S_IWRITE)1683 os.unlink(dest)1684 editedFiles.add(dest)1685elif modifier =="R":1686 src, dest = diff['src'], diff['dst']1687if self.p4HasMoveCommand:1688p4_edit(src)# src must be open before move1689p4_move(src, dest)# opens for (move/delete, move/add)1690else:1691p4_integrate(src, dest)1692if diff['src_sha1'] != diff['dst_sha1']:1693p4_edit(dest)1694else:1695 pureRenameCopy.add(dest)1696ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1697if not self.p4HasMoveCommand:1698p4_edit(dest)# with move: already open, writable1699 filesToChangeExecBit[dest] = diff['dst_mode']1700if not self.p4HasMoveCommand:1701if self.isWindows:1702 os.chmod(dest, stat.S_IWRITE)1703 os.unlink(dest)1704 filesToDelete.add(src)1705 editedFiles.add(dest)1706elif modifier =="T":1707 filesToChangeType.add(path)1708else:1709die("unknown modifier%sfor%s"% (modifier, path))17101711 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1712 patchcmd = diffcmd +" | git apply "1713 tryPatchCmd = patchcmd +"--check -"1714 applyPatchCmd = patchcmd +"--check --apply -"1715 patch_succeeded =True17161717if os.system(tryPatchCmd) !=0:1718 fixed_rcs_keywords =False1719 patch_succeeded =False1720print"Unfortunately applying the change failed!"17211722# Patch failed, maybe it's just RCS keyword woes. Look through1723# the patch to see if that's possible.1724ifgitConfigBool("git-p4.attemptRCSCleanup"):1725file=None1726 pattern =None1727 kwfiles = {}1728forfilein editedFiles | filesToDelete:1729# did this file's delta contain RCS keywords?1730 pattern =p4_keywords_regexp_for_file(file)17311732if pattern:1733# this file is a possibility...look for RCS keywords.1734 regexp = re.compile(pattern, re.VERBOSE)1735for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1736if regexp.search(line):1737if verbose:1738print"got keyword match on%sin%sin%s"% (pattern, line,file)1739 kwfiles[file] = pattern1740break17411742forfilein kwfiles:1743if verbose:1744print"zapping%swith%s"% (line,pattern)1745# File is being deleted, so not open in p4. Must1746# disable the read-only bit on windows.1747if self.isWindows andfilenot in editedFiles:1748 os.chmod(file, stat.S_IWRITE)1749 self.patchRCSKeywords(file, kwfiles[file])1750 fixed_rcs_keywords =True17511752if fixed_rcs_keywords:1753print"Retrying the patch with RCS keywords cleaned up"1754if os.system(tryPatchCmd) ==0:1755 patch_succeeded =True17561757if not patch_succeeded:1758for f in editedFiles:1759p4_revert(f)1760return False17611762#1763# Apply the patch for real, and do add/delete/+x handling.1764#1765system(applyPatchCmd)17661767for f in filesToChangeType:1768p4_edit(f,"-t","auto")1769for f in filesToAdd:1770p4_add(f)1771for f in filesToDelete:1772p4_revert(f)1773p4_delete(f)17741775# Set/clear executable bits1776for f in filesToChangeExecBit.keys():1777 mode = filesToChangeExecBit[f]1778setP4ExecBit(f, mode)17791780if self.update_shelve:1781print("all_files =%s"%str(all_files))1782p4_reopen_in_change(self.update_shelve, all_files)17831784#1785# Build p4 change description, starting with the contents1786# of the git commit message.1787#1788 logMessage =extractLogMessageFromGitCommit(id)1789 logMessage = logMessage.strip()1790(logMessage, jobs) = self.separate_jobs_from_description(logMessage)17911792 template = self.prepareSubmitTemplate(self.update_shelve)1793 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)17941795if self.preserveUser:1796 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User17971798if self.checkAuthorship and not self.p4UserIsMe(p4User):1799 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1800 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1801 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"18021803 separatorLine ="######## everything below this line is just the diff #######\n"1804if not self.prepare_p4_only:1805 submitTemplate += separatorLine1806 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)18071808(handle, fileName) = tempfile.mkstemp()1809 tmpFile = os.fdopen(handle,"w+b")1810if self.isWindows:1811 submitTemplate = submitTemplate.replace("\n","\r\n")1812 tmpFile.write(submitTemplate)1813 tmpFile.close()18141815if self.prepare_p4_only:1816#1817# Leave the p4 tree prepared, and the submit template around1818# and let the user decide what to do next1819#1820print1821print"P4 workspace prepared for submission."1822print"To submit or revert, go to client workspace"1823print" "+ self.clientPath1824print1825print"To submit, use\"p4 submit\"to write a new description,"1826print"or\"p4 submit -i <%s\"to use the one prepared by" \1827"\"git p4\"."% fileName1828print"You can delete the file\"%s\"when finished."% fileName18291830if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1831print"To preserve change ownership by user%s, you must\n" \1832"do\"p4 change -f <change>\"after submitting and\n" \1833"edit the User field."1834if pureRenameCopy:1835print"After submitting, renamed files must be re-synced."1836print"Invoke\"p4 sync -f\"on each of these files:"1837for f in pureRenameCopy:1838print" "+ f18391840print1841print"To revert the changes, use\"p4 revert ...\", and delete"1842print"the submit template file\"%s\""% fileName1843if filesToAdd:1844print"Since the commit adds new files, they must be deleted:"1845for f in filesToAdd:1846print" "+ f1847print1848return True18491850#1851# Let the user edit the change description, then submit it.1852#1853 submitted =False18541855try:1856if self.edit_template(fileName):1857# read the edited message and submit1858 tmpFile =open(fileName,"rb")1859 message = tmpFile.read()1860 tmpFile.close()1861if self.isWindows:1862 message = message.replace("\r\n","\n")1863 submitTemplate = message[:message.index(separatorLine)]18641865if self.update_shelve:1866p4_write_pipe(['shelve','-r','-i'], submitTemplate)1867elif self.shelve:1868p4_write_pipe(['shelve','-i'], submitTemplate)1869else:1870p4_write_pipe(['submit','-i'], submitTemplate)1871# The rename/copy happened by applying a patch that created a1872# new file. This leaves it writable, which confuses p4.1873for f in pureRenameCopy:1874p4_sync(f,"-f")18751876if self.preserveUser:1877if p4User:1878# Get last changelist number. Cannot easily get it from1879# the submit command output as the output is1880# unmarshalled.1881 changelist = self.lastP4Changelist()1882 self.modifyChangelistUser(changelist, p4User)18831884 submitted =True18851886finally:1887# skip this patch1888if not submitted or self.shelve:1889if self.shelve:1890print("Reverting shelved files.")1891else:1892print("Submission cancelled, undoing p4 changes.")1893for f in editedFiles | filesToDelete:1894p4_revert(f)1895for f in filesToAdd:1896p4_revert(f)1897 os.remove(f)18981899 os.remove(fileName)1900return submitted19011902# Export git tags as p4 labels. Create a p4 label and then tag1903# with that.1904defexportGitTags(self, gitTags):1905 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1906iflen(validLabelRegexp) ==0:1907 validLabelRegexp = defaultLabelRegexp1908 m = re.compile(validLabelRegexp)19091910for name in gitTags:19111912if not m.match(name):1913if verbose:1914print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1915continue19161917# Get the p4 commit this corresponds to1918 logMessage =extractLogMessageFromGitCommit(name)1919 values =extractSettingsGitLog(logMessage)19201921if not values.has_key('change'):1922# a tag pointing to something not sent to p4; ignore1923if verbose:1924print"git tag%sdoes not give a p4 commit"% name1925continue1926else:1927 changelist = values['change']19281929# Get the tag details.1930 inHeader =True1931 isAnnotated =False1932 body = []1933for l inread_pipe_lines(["git","cat-file","-p", name]):1934 l = l.strip()1935if inHeader:1936if re.match(r'tag\s+', l):1937 isAnnotated =True1938elif re.match(r'\s*$', l):1939 inHeader =False1940continue1941else:1942 body.append(l)19431944if not isAnnotated:1945 body = ["lightweight tag imported by git p4\n"]19461947# Create the label - use the same view as the client spec we are using1948 clientSpec =getClientSpec()19491950 labelTemplate ="Label:%s\n"% name1951 labelTemplate +="Description:\n"1952for b in body:1953 labelTemplate +="\t"+ b +"\n"1954 labelTemplate +="View:\n"1955for depot_side in clientSpec.mappings:1956 labelTemplate +="\t%s\n"% depot_side19571958if self.dry_run:1959print"Would create p4 label%sfor tag"% name1960elif self.prepare_p4_only:1961print"Not creating p4 label%sfor tag due to option" \1962" --prepare-p4-only"% name1963else:1964p4_write_pipe(["label","-i"], labelTemplate)19651966# Use the label1967p4_system(["tag","-l", name] +1968["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])19691970if verbose:1971print"created p4 label for tag%s"% name19721973defrun(self, args):1974iflen(args) ==0:1975 self.master =currentGitBranch()1976eliflen(args) ==1:1977 self.master = args[0]1978if notbranchExists(self.master):1979die("Branch%sdoes not exist"% self.master)1980else:1981return False19821983if self.master:1984 allowSubmit =gitConfig("git-p4.allowSubmit")1985iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1986die("%sis not in git-p4.allowSubmit"% self.master)19871988[upstream, settings] =findUpstreamBranchPoint()1989 self.depotPath = settings['depot-paths'][0]1990iflen(self.origin) ==0:1991 self.origin = upstream19921993if self.update_shelve:1994 self.shelve =True19951996if self.preserveUser:1997if not self.canChangeChangelists():1998die("Cannot preserve user names without p4 super-user or admin permissions")19992000# if not set from the command line, try the config file2001if self.conflict_behavior is None:2002 val =gitConfig("git-p4.conflict")2003if val:2004if val not in self.conflict_behavior_choices:2005die("Invalid value '%s' for config git-p4.conflict"% val)2006else:2007 val ="ask"2008 self.conflict_behavior = val20092010if self.verbose:2011print"Origin branch is "+ self.origin20122013iflen(self.depotPath) ==0:2014print"Internal error: cannot locate perforce depot path from existing branches"2015 sys.exit(128)20162017 self.useClientSpec =False2018ifgitConfigBool("git-p4.useclientspec"):2019 self.useClientSpec =True2020if self.useClientSpec:2021 self.clientSpecDirs =getClientSpec()20222023# Check for the existence of P4 branches2024 branchesDetected = (len(p4BranchesInGit().keys()) >1)20252026if self.useClientSpec and not branchesDetected:2027# all files are relative to the client spec2028 self.clientPath =getClientRoot()2029else:2030 self.clientPath =p4Where(self.depotPath)20312032if self.clientPath =="":2033die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)20342035print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2036 self.oldWorkingDirectory = os.getcwd()20372038# ensure the clientPath exists2039 new_client_dir =False2040if not os.path.exists(self.clientPath):2041 new_client_dir =True2042 os.makedirs(self.clientPath)20432044chdir(self.clientPath, is_client_path=True)2045if self.dry_run:2046print"Would synchronize p4 checkout in%s"% self.clientPath2047else:2048print"Synchronizing p4 checkout..."2049if new_client_dir:2050# old one was destroyed, and maybe nobody told p42051p4_sync("...","-f")2052else:2053p4_sync("...")2054 self.check()20552056 commits = []2057if self.master:2058 commitish = self.master2059else:2060 commitish ='HEAD'20612062for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2063 commits.append(line.strip())2064 commits.reverse()20652066if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2067 self.checkAuthorship =False2068else:2069 self.checkAuthorship =True20702071if self.preserveUser:2072 self.checkValidP4Users(commits)20732074#2075# Build up a set of options to be passed to diff when2076# submitting each commit to p4.2077#2078if self.detectRenames:2079# command-line -M arg2080 self.diffOpts ="-M"2081else:2082# If not explicitly set check the config variable2083 detectRenames =gitConfig("git-p4.detectRenames")20842085if detectRenames.lower() =="false"or detectRenames =="":2086 self.diffOpts =""2087elif detectRenames.lower() =="true":2088 self.diffOpts ="-M"2089else:2090 self.diffOpts ="-M%s"% detectRenames20912092# no command-line arg for -C or --find-copies-harder, just2093# config variables2094 detectCopies =gitConfig("git-p4.detectCopies")2095if detectCopies.lower() =="false"or detectCopies =="":2096pass2097elif detectCopies.lower() =="true":2098 self.diffOpts +=" -C"2099else:2100 self.diffOpts +=" -C%s"% detectCopies21012102ifgitConfigBool("git-p4.detectCopiesHarder"):2103 self.diffOpts +=" --find-copies-harder"21042105#2106# Apply the commits, one at a time. On failure, ask if should2107# continue to try the rest of the patches, or quit.2108#2109if self.dry_run:2110print"Would apply"2111 applied = []2112 last =len(commits) -12113for i, commit inenumerate(commits):2114if self.dry_run:2115print" ",read_pipe(["git","show","-s",2116"--format=format:%h%s", commit])2117 ok =True2118else:2119 ok = self.applyCommit(commit)2120if ok:2121 applied.append(commit)2122else:2123if self.prepare_p4_only and i < last:2124print"Processing only the first commit due to option" \2125" --prepare-p4-only"2126break2127if i < last:2128 quit =False2129while True:2130# prompt for what to do, or use the option/variable2131if self.conflict_behavior =="ask":2132print"What do you want to do?"2133 response =raw_input("[s]kip this commit but apply"2134" the rest, or [q]uit? ")2135if not response:2136continue2137elif self.conflict_behavior =="skip":2138 response ="s"2139elif self.conflict_behavior =="quit":2140 response ="q"2141else:2142die("Unknown conflict_behavior '%s'"%2143 self.conflict_behavior)21442145if response[0] =="s":2146print"Skipping this commit, but applying the rest"2147break2148if response[0] =="q":2149print"Quitting"2150 quit =True2151break2152if quit:2153break21542155chdir(self.oldWorkingDirectory)2156 shelved_applied ="shelved"if self.shelve else"applied"2157if self.dry_run:2158pass2159elif self.prepare_p4_only:2160pass2161eliflen(commits) ==len(applied):2162print("All commits{0}!".format(shelved_applied))21632164 sync =P4Sync()2165if self.branch:2166 sync.branch = self.branch2167 sync.run([])21682169 rebase =P4Rebase()2170 rebase.rebase()21712172else:2173iflen(applied) ==0:2174print("No commits{0}.".format(shelved_applied))2175else:2176print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2177for c in commits:2178if c in applied:2179 star ="*"2180else:2181 star =" "2182print star,read_pipe(["git","show","-s",2183"--format=format:%h%s", c])2184print"You will have to do 'git p4 sync' and rebase."21852186ifgitConfigBool("git-p4.exportLabels"):2187 self.exportLabels =True21882189if self.exportLabels:2190 p4Labels =getP4Labels(self.depotPath)2191 gitTags =getGitTags()21922193 missingGitTags = gitTags - p4Labels2194 self.exportGitTags(missingGitTags)21952196# exit with error unless everything applied perfectly2197iflen(commits) !=len(applied):2198 sys.exit(1)21992200return True22012202classView(object):2203"""Represent a p4 view ("p4 help views"), and map files in a2204 repo according to the view."""22052206def__init__(self, client_name):2207 self.mappings = []2208 self.client_prefix ="//%s/"% client_name2209# cache results of "p4 where" to lookup client file locations2210 self.client_spec_path_cache = {}22112212defappend(self, view_line):2213"""Parse a view line, splitting it into depot and client2214 sides. Append to self.mappings, preserving order. This2215 is only needed for tag creation."""22162217# Split the view line into exactly two words. P4 enforces2218# structure on these lines that simplifies this quite a bit.2219#2220# Either or both words may be double-quoted.2221# Single quotes do not matter.2222# Double-quote marks cannot occur inside the words.2223# A + or - prefix is also inside the quotes.2224# There are no quotes unless they contain a space.2225# The line is already white-space stripped.2226# The two words are separated by a single space.2227#2228if view_line[0] =='"':2229# First word is double quoted. Find its end.2230 close_quote_index = view_line.find('"',1)2231if close_quote_index <=0:2232die("No first-word closing quote found:%s"% view_line)2233 depot_side = view_line[1:close_quote_index]2234# skip closing quote and space2235 rhs_index = close_quote_index +1+12236else:2237 space_index = view_line.find(" ")2238if space_index <=0:2239die("No word-splitting space found:%s"% view_line)2240 depot_side = view_line[0:space_index]2241 rhs_index = space_index +122422243# prefix + means overlay on previous mapping2244if depot_side.startswith("+"):2245 depot_side = depot_side[1:]22462247# prefix - means exclude this path, leave out of mappings2248 exclude =False2249if depot_side.startswith("-"):2250 exclude =True2251 depot_side = depot_side[1:]22522253if not exclude:2254 self.mappings.append(depot_side)22552256defconvert_client_path(self, clientFile):2257# chop off //client/ part to make it relative2258if not clientFile.startswith(self.client_prefix):2259die("No prefix '%s' on clientFile '%s'"%2260(self.client_prefix, clientFile))2261return clientFile[len(self.client_prefix):]22622263defupdate_client_spec_path_cache(self, files):2264""" Caching file paths by "p4 where" batch query """22652266# List depot file paths exclude that already cached2267 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]22682269iflen(fileArgs) ==0:2270return# All files in cache22712272 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2273for res in where_result:2274if"code"in res and res["code"] =="error":2275# assume error is "... file(s) not in client view"2276continue2277if"clientFile"not in res:2278die("No clientFile in 'p4 where' output")2279if"unmap"in res:2280# it will list all of them, but only one not unmap-ped2281continue2282ifgitConfigBool("core.ignorecase"):2283 res['depotFile'] = res['depotFile'].lower()2284 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])22852286# not found files or unmap files set to ""2287for depotFile in fileArgs:2288ifgitConfigBool("core.ignorecase"):2289 depotFile = depotFile.lower()2290if depotFile not in self.client_spec_path_cache:2291 self.client_spec_path_cache[depotFile] =""22922293defmap_in_client(self, depot_path):2294"""Return the relative location in the client where this2295 depot file should live. Returns "" if the file should2296 not be mapped in the client."""22972298ifgitConfigBool("core.ignorecase"):2299 depot_path = depot_path.lower()23002301if depot_path in self.client_spec_path_cache:2302return self.client_spec_path_cache[depot_path]23032304die("Error:%sis not found in client spec path"% depot_path )2305return""23062307classP4Sync(Command, P4UserMap):2308 delete_actions = ("delete","move/delete","purge")23092310def__init__(self):2311 Command.__init__(self)2312 P4UserMap.__init__(self)2313 self.options = [2314 optparse.make_option("--branch", dest="branch"),2315 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2316 optparse.make_option("--changesfile", dest="changesFile"),2317 optparse.make_option("--silent", dest="silent", action="store_true"),2318 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2319 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2320 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2321help="Import into refs/heads/ , not refs/remotes"),2322 optparse.make_option("--max-changes", dest="maxChanges",2323help="Maximum number of changes to import"),2324 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2325help="Internal block size to use when iteratively calling p4 changes"),2326 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2327help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2328 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2329help="Only sync files that are included in the Perforce Client Spec"),2330 optparse.make_option("-/", dest="cloneExclude",2331 action="append",type="string",2332help="exclude depot path"),2333]2334 self.description ="""Imports from Perforce into a git repository.\n2335 example:2336 //depot/my/project/ -- to import the current head2337 //depot/my/project/@all -- to import everything2338 //depot/my/project/@1,6 -- to import only from revision 1 to 623392340 (a ... is not needed in the path p4 specification, it's added implicitly)"""23412342 self.usage +=" //depot/path[@revRange]"2343 self.silent =False2344 self.createdBranches =set()2345 self.committedChanges =set()2346 self.branch =""2347 self.detectBranches =False2348 self.detectLabels =False2349 self.importLabels =False2350 self.changesFile =""2351 self.syncWithOrigin =True2352 self.importIntoRemotes =True2353 self.maxChanges =""2354 self.changes_block_size =None2355 self.keepRepoPath =False2356 self.depotPaths =None2357 self.p4BranchesInGit = []2358 self.cloneExclude = []2359 self.useClientSpec =False2360 self.useClientSpec_from_options =False2361 self.clientSpecDirs =None2362 self.tempBranches = []2363 self.tempBranchLocation ="refs/git-p4-tmp"2364 self.largeFileSystem =None23652366ifgitConfig('git-p4.largeFileSystem'):2367 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2368 self.largeFileSystem =largeFileSystemConstructor(2369lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2370)23712372ifgitConfig("git-p4.syncFromOrigin") =="false":2373 self.syncWithOrigin =False23742375# This is required for the "append" cloneExclude action2376defensure_value(self, attr, value):2377if nothasattr(self, attr)orgetattr(self, attr)is None:2378setattr(self, attr, value)2379returngetattr(self, attr)23802381# Force a checkpoint in fast-import and wait for it to finish2382defcheckpoint(self):2383 self.gitStream.write("checkpoint\n\n")2384 self.gitStream.write("progress checkpoint\n\n")2385 out = self.gitOutput.readline()2386if self.verbose:2387print"checkpoint finished: "+ out23882389defextractFilesFromCommit(self, commit):2390 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2391for path in self.cloneExclude]2392 files = []2393 fnum =02394while commit.has_key("depotFile%s"% fnum):2395 path = commit["depotFile%s"% fnum]23962397if[p for p in self.cloneExclude2398ifp4PathStartsWith(path, p)]:2399 found =False2400else:2401 found = [p for p in self.depotPaths2402ifp4PathStartsWith(path, p)]2403if not found:2404 fnum = fnum +12405continue24062407file= {}2408file["path"] = path2409file["rev"] = commit["rev%s"% fnum]2410file["action"] = commit["action%s"% fnum]2411file["type"] = commit["type%s"% fnum]2412 files.append(file)2413 fnum = fnum +12414return files24152416defextractJobsFromCommit(self, commit):2417 jobs = []2418 jnum =02419while commit.has_key("job%s"% jnum):2420 job = commit["job%s"% jnum]2421 jobs.append(job)2422 jnum = jnum +12423return jobs24242425defstripRepoPath(self, path, prefixes):2426"""When streaming files, this is called to map a p4 depot path2427 to where it should go in git. The prefixes are either2428 self.depotPaths, or self.branchPrefixes in the case of2429 branch detection."""24302431if self.useClientSpec:2432# branch detection moves files up a level (the branch name)2433# from what client spec interpretation gives2434 path = self.clientSpecDirs.map_in_client(path)2435if self.detectBranches:2436for b in self.knownBranches:2437if path.startswith(b +"/"):2438 path = path[len(b)+1:]24392440elif self.keepRepoPath:2441# Preserve everything in relative path name except leading2442# //depot/; just look at first prefix as they all should2443# be in the same depot.2444 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2445ifp4PathStartsWith(path, depot):2446 path = path[len(depot):]24472448else:2449for p in prefixes:2450ifp4PathStartsWith(path, p):2451 path = path[len(p):]2452break24532454 path =wildcard_decode(path)2455return path24562457defsplitFilesIntoBranches(self, commit):2458"""Look at each depotFile in the commit to figure out to what2459 branch it belongs."""24602461if self.clientSpecDirs:2462 files = self.extractFilesFromCommit(commit)2463 self.clientSpecDirs.update_client_spec_path_cache(files)24642465 branches = {}2466 fnum =02467while commit.has_key("depotFile%s"% fnum):2468 path = commit["depotFile%s"% fnum]2469 found = [p for p in self.depotPaths2470ifp4PathStartsWith(path, p)]2471if not found:2472 fnum = fnum +12473continue24742475file= {}2476file["path"] = path2477file["rev"] = commit["rev%s"% fnum]2478file["action"] = commit["action%s"% fnum]2479file["type"] = commit["type%s"% fnum]2480 fnum = fnum +124812482# start with the full relative path where this file would2483# go in a p4 client2484if self.useClientSpec:2485 relPath = self.clientSpecDirs.map_in_client(path)2486else:2487 relPath = self.stripRepoPath(path, self.depotPaths)24882489for branch in self.knownBranches.keys():2490# add a trailing slash so that a commit into qt/4.2foo2491# doesn't end up in qt/4.2, e.g.2492if relPath.startswith(branch +"/"):2493if branch not in branches:2494 branches[branch] = []2495 branches[branch].append(file)2496break24972498return branches24992500defwriteToGitStream(self, gitMode, relPath, contents):2501 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2502 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2503for d in contents:2504 self.gitStream.write(d)2505 self.gitStream.write('\n')25062507defencodeWithUTF8(self, path):2508try:2509 path.decode('ascii')2510except:2511 encoding ='utf8'2512ifgitConfig('git-p4.pathEncoding'):2513 encoding =gitConfig('git-p4.pathEncoding')2514 path = path.decode(encoding,'replace').encode('utf8','replace')2515if self.verbose:2516print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2517return path25182519# output one file from the P4 stream2520# - helper for streamP4Files25212522defstreamOneP4File(self,file, contents):2523 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2524 relPath = self.encodeWithUTF8(relPath)2525if verbose:2526 size =int(self.stream_file['fileSize'])2527 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2528 sys.stdout.flush()25292530(type_base, type_mods) =split_p4_type(file["type"])25312532 git_mode ="100644"2533if"x"in type_mods:2534 git_mode ="100755"2535if type_base =="symlink":2536 git_mode ="120000"2537# p4 print on a symlink sometimes contains "target\n";2538# if it does, remove the newline2539 data =''.join(contents)2540if not data:2541# Some version of p4 allowed creating a symlink that pointed2542# to nothing. This causes p4 errors when checking out such2543# a change, and errors here too. Work around it by ignoring2544# the bad symlink; hopefully a future change fixes it.2545print"\nIgnoring empty symlink in%s"%file['depotFile']2546return2547elif data[-1] =='\n':2548 contents = [data[:-1]]2549else:2550 contents = [data]25512552if type_base =="utf16":2553# p4 delivers different text in the python output to -G2554# than it does when using "print -o", or normal p4 client2555# operations. utf16 is converted to ascii or utf8, perhaps.2556# But ascii text saved as -t utf16 is completely mangled.2557# Invoke print -o to get the real contents.2558#2559# On windows, the newlines will always be mangled by print, so put2560# them back too. This is not needed to the cygwin windows version,2561# just the native "NT" type.2562#2563try:2564 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2565exceptExceptionas e:2566if'Translation of file content failed'instr(e):2567 type_base ='binary'2568else:2569raise e2570else:2571ifp4_version_string().find('/NT') >=0:2572 text = text.replace('\r\n','\n')2573 contents = [ text ]25742575if type_base =="apple":2576# Apple filetype files will be streamed as a concatenation of2577# its appledouble header and the contents. This is useless2578# on both macs and non-macs. If using "print -q -o xx", it2579# will create "xx" with the data, and "%xx" with the header.2580# This is also not very useful.2581#2582# Ideally, someday, this script can learn how to generate2583# appledouble files directly and import those to git, but2584# non-mac machines can never find a use for apple filetype.2585print"\nIgnoring apple filetype file%s"%file['depotFile']2586return25872588# Note that we do not try to de-mangle keywords on utf16 files,2589# even though in theory somebody may want that.2590 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2591if pattern:2592 regexp = re.compile(pattern, re.VERBOSE)2593 text =''.join(contents)2594 text = regexp.sub(r'$\1$', text)2595 contents = [ text ]25962597if self.largeFileSystem:2598(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)25992600 self.writeToGitStream(git_mode, relPath, contents)26012602defstreamOneP4Deletion(self,file):2603 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2604 relPath = self.encodeWithUTF8(relPath)2605if verbose:2606 sys.stdout.write("delete%s\n"% relPath)2607 sys.stdout.flush()2608 self.gitStream.write("D%s\n"% relPath)26092610if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2611 self.largeFileSystem.removeLargeFile(relPath)26122613# handle another chunk of streaming data2614defstreamP4FilesCb(self, marshalled):26152616# catch p4 errors and complain2617 err =None2618if"code"in marshalled:2619if marshalled["code"] =="error":2620if"data"in marshalled:2621 err = marshalled["data"].rstrip()26222623if not err and'fileSize'in self.stream_file:2624 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2625if required_bytes >0:2626 err ='Not enough space left on%s! Free at least%iMB.'% (2627 os.getcwd(), required_bytes/1024/10242628)26292630if err:2631 f =None2632if self.stream_have_file_info:2633if"depotFile"in self.stream_file:2634 f = self.stream_file["depotFile"]2635# force a failure in fast-import, else an empty2636# commit will be made2637 self.gitStream.write("\n")2638 self.gitStream.write("die-now\n")2639 self.gitStream.close()2640# ignore errors, but make sure it exits first2641 self.importProcess.wait()2642if f:2643die("Error from p4 print for%s:%s"% (f, err))2644else:2645die("Error from p4 print:%s"% err)26462647if marshalled.has_key('depotFile')and self.stream_have_file_info:2648# start of a new file - output the old one first2649 self.streamOneP4File(self.stream_file, self.stream_contents)2650 self.stream_file = {}2651 self.stream_contents = []2652 self.stream_have_file_info =False26532654# pick up the new file information... for the2655# 'data' field we need to append to our array2656for k in marshalled.keys():2657if k =='data':2658if'streamContentSize'not in self.stream_file:2659 self.stream_file['streamContentSize'] =02660 self.stream_file['streamContentSize'] +=len(marshalled['data'])2661 self.stream_contents.append(marshalled['data'])2662else:2663 self.stream_file[k] = marshalled[k]26642665if(verbose and2666'streamContentSize'in self.stream_file and2667'fileSize'in self.stream_file and2668'depotFile'in self.stream_file):2669 size =int(self.stream_file["fileSize"])2670if size >0:2671 progress =100*self.stream_file['streamContentSize']/size2672 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2673 sys.stdout.flush()26742675 self.stream_have_file_info =True26762677# Stream directly from "p4 files" into "git fast-import"2678defstreamP4Files(self, files):2679 filesForCommit = []2680 filesToRead = []2681 filesToDelete = []26822683for f in files:2684 filesForCommit.append(f)2685if f['action']in self.delete_actions:2686 filesToDelete.append(f)2687else:2688 filesToRead.append(f)26892690# deleted files...2691for f in filesToDelete:2692 self.streamOneP4Deletion(f)26932694iflen(filesToRead) >0:2695 self.stream_file = {}2696 self.stream_contents = []2697 self.stream_have_file_info =False26982699# curry self argument2700defstreamP4FilesCbSelf(entry):2701 self.streamP4FilesCb(entry)27022703 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]27042705p4CmdList(["-x","-","print"],2706 stdin=fileArgs,2707 cb=streamP4FilesCbSelf)27082709# do the last chunk2710if self.stream_file.has_key('depotFile'):2711 self.streamOneP4File(self.stream_file, self.stream_contents)27122713defmake_email(self, userid):2714if userid in self.users:2715return self.users[userid]2716else:2717return"%s<a@b>"% userid27182719defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2720""" Stream a p4 tag.2721 commit is either a git commit, or a fast-import mark, ":<p4commit>"2722 """27232724if verbose:2725print"writing tag%sfor commit%s"% (labelName, commit)2726 gitStream.write("tag%s\n"% labelName)2727 gitStream.write("from%s\n"% commit)27282729if labelDetails.has_key('Owner'):2730 owner = labelDetails["Owner"]2731else:2732 owner =None27332734# Try to use the owner of the p4 label, or failing that,2735# the current p4 user id.2736if owner:2737 email = self.make_email(owner)2738else:2739 email = self.make_email(self.p4UserId())2740 tagger ="%s %s %s"% (email, epoch, self.tz)27412742 gitStream.write("tagger%s\n"% tagger)27432744print"labelDetails=",labelDetails2745if labelDetails.has_key('Description'):2746 description = labelDetails['Description']2747else:2748 description ='Label from git p4'27492750 gitStream.write("data%d\n"%len(description))2751 gitStream.write(description)2752 gitStream.write("\n")27532754definClientSpec(self, path):2755if not self.clientSpecDirs:2756return True2757 inClientSpec = self.clientSpecDirs.map_in_client(path)2758if not inClientSpec and self.verbose:2759print('Ignoring file outside of client spec:{0}'.format(path))2760return inClientSpec27612762defhasBranchPrefix(self, path):2763if not self.branchPrefixes:2764return True2765 hasPrefix = [p for p in self.branchPrefixes2766ifp4PathStartsWith(path, p)]2767if not hasPrefix and self.verbose:2768print('Ignoring file outside of prefix:{0}'.format(path))2769return hasPrefix27702771defcommit(self, details, files, branch, parent =""):2772 epoch = details["time"]2773 author = details["user"]2774 jobs = self.extractJobsFromCommit(details)27752776if self.verbose:2777print('commit into{0}'.format(branch))27782779if self.clientSpecDirs:2780 self.clientSpecDirs.update_client_spec_path_cache(files)27812782 files = [f for f in files2783if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]27842785if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2786print('Ignoring revision{0}as it would produce an empty commit.'2787.format(details['change']))2788return27892790 self.gitStream.write("commit%s\n"% branch)2791 self.gitStream.write("mark :%s\n"% details["change"])2792 self.committedChanges.add(int(details["change"]))2793 committer =""2794if author not in self.users:2795 self.getUserMapFromPerforceServer()2796 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)27972798 self.gitStream.write("committer%s\n"% committer)27992800 self.gitStream.write("data <<EOT\n")2801 self.gitStream.write(details["desc"])2802iflen(jobs) >0:2803 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2804 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2805(','.join(self.branchPrefixes), details["change"]))2806iflen(details['options']) >0:2807 self.gitStream.write(": options =%s"% details['options'])2808 self.gitStream.write("]\nEOT\n\n")28092810iflen(parent) >0:2811if self.verbose:2812print"parent%s"% parent2813 self.gitStream.write("from%s\n"% parent)28142815 self.streamP4Files(files)2816 self.gitStream.write("\n")28172818 change =int(details["change"])28192820if self.labels.has_key(change):2821 label = self.labels[change]2822 labelDetails = label[0]2823 labelRevisions = label[1]2824if self.verbose:2825print"Change%sis labelled%s"% (change, labelDetails)28262827 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2828for p in self.branchPrefixes])28292830iflen(files) ==len(labelRevisions):28312832 cleanedFiles = {}2833for info in files:2834if info["action"]in self.delete_actions:2835continue2836 cleanedFiles[info["depotFile"]] = info["rev"]28372838if cleanedFiles == labelRevisions:2839 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)28402841else:2842if not self.silent:2843print("Tag%sdoes not match with change%s: files do not match."2844% (labelDetails["label"], change))28452846else:2847if not self.silent:2848print("Tag%sdoes not match with change%s: file count is different."2849% (labelDetails["label"], change))28502851# Build a dictionary of changelists and labels, for "detect-labels" option.2852defgetLabels(self):2853 self.labels = {}28542855 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2856iflen(l) >0and not self.silent:2857print"Finding files belonging to labels in%s"% `self.depotPaths`28582859for output in l:2860 label = output["label"]2861 revisions = {}2862 newestChange =02863if self.verbose:2864print"Querying files for label%s"% label2865forfileinp4CmdList(["files"] +2866["%s...@%s"% (p, label)2867for p in self.depotPaths]):2868 revisions[file["depotFile"]] =file["rev"]2869 change =int(file["change"])2870if change > newestChange:2871 newestChange = change28722873 self.labels[newestChange] = [output, revisions]28742875if self.verbose:2876print"Label changes:%s"% self.labels.keys()28772878# Import p4 labels as git tags. A direct mapping does not2879# exist, so assume that if all the files are at the same revision2880# then we can use that, or it's something more complicated we should2881# just ignore.2882defimportP4Labels(self, stream, p4Labels):2883if verbose:2884print"import p4 labels: "+' '.join(p4Labels)28852886 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2887 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2888iflen(validLabelRegexp) ==0:2889 validLabelRegexp = defaultLabelRegexp2890 m = re.compile(validLabelRegexp)28912892for name in p4Labels:2893 commitFound =False28942895if not m.match(name):2896if verbose:2897print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2898continue28992900if name in ignoredP4Labels:2901continue29022903 labelDetails =p4CmdList(['label',"-o", name])[0]29042905# get the most recent changelist for each file in this label2906 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2907for p in self.depotPaths])29082909if change.has_key('change'):2910# find the corresponding git commit; take the oldest commit2911 changelist =int(change['change'])2912if changelist in self.committedChanges:2913 gitCommit =":%d"% changelist # use a fast-import mark2914 commitFound =True2915else:2916 gitCommit =read_pipe(["git","rev-list","--max-count=1",2917"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2918iflen(gitCommit) ==0:2919print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2920else:2921 commitFound =True2922 gitCommit = gitCommit.strip()29232924if commitFound:2925# Convert from p4 time format2926try:2927 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2928exceptValueError:2929print"Could not convert label time%s"% labelDetails['Update']2930 tmwhen =129312932 when =int(time.mktime(tmwhen))2933 self.streamTag(stream, name, labelDetails, gitCommit, when)2934if verbose:2935print"p4 label%smapped to git commit%s"% (name, gitCommit)2936else:2937if verbose:2938print"Label%shas no changelists - possibly deleted?"% name29392940if not commitFound:2941# We can't import this label; don't try again as it will get very2942# expensive repeatedly fetching all the files for labels that will2943# never be imported. If the label is moved in the future, the2944# ignore will need to be removed manually.2945system(["git","config","--add","git-p4.ignoredP4Labels", name])29462947defguessProjectName(self):2948for p in self.depotPaths:2949if p.endswith("/"):2950 p = p[:-1]2951 p = p[p.strip().rfind("/") +1:]2952if not p.endswith("/"):2953 p +="/"2954return p29552956defgetBranchMapping(self):2957 lostAndFoundBranches =set()29582959 user =gitConfig("git-p4.branchUser")2960iflen(user) >0:2961 command ="branches -u%s"% user2962else:2963 command ="branches"29642965for info inp4CmdList(command):2966 details =p4Cmd(["branch","-o", info["branch"]])2967 viewIdx =02968while details.has_key("View%s"% viewIdx):2969 paths = details["View%s"% viewIdx].split(" ")2970 viewIdx = viewIdx +12971# require standard //depot/foo/... //depot/bar/... mapping2972iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2973continue2974 source = paths[0]2975 destination = paths[1]2976## HACK2977ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2978 source = source[len(self.depotPaths[0]):-4]2979 destination = destination[len(self.depotPaths[0]):-4]29802981if destination in self.knownBranches:2982if not self.silent:2983print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2984print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2985continue29862987 self.knownBranches[destination] = source29882989 lostAndFoundBranches.discard(destination)29902991if source not in self.knownBranches:2992 lostAndFoundBranches.add(source)29932994# Perforce does not strictly require branches to be defined, so we also2995# check git config for a branch list.2996#2997# Example of branch definition in git config file:2998# [git-p4]2999# branchList=main:branchA3000# branchList=main:branchB3001# branchList=branchA:branchC3002 configBranches =gitConfigList("git-p4.branchList")3003for branch in configBranches:3004if branch:3005(source, destination) = branch.split(":")3006 self.knownBranches[destination] = source30073008 lostAndFoundBranches.discard(destination)30093010if source not in self.knownBranches:3011 lostAndFoundBranches.add(source)301230133014for branch in lostAndFoundBranches:3015 self.knownBranches[branch] = branch30163017defgetBranchMappingFromGitBranches(self):3018 branches =p4BranchesInGit(self.importIntoRemotes)3019for branch in branches.keys():3020if branch =="master":3021 branch ="main"3022else:3023 branch = branch[len(self.projectName):]3024 self.knownBranches[branch] = branch30253026defupdateOptionDict(self, d):3027 option_keys = {}3028if self.keepRepoPath:3029 option_keys['keepRepoPath'] =130303031 d["options"] =' '.join(sorted(option_keys.keys()))30323033defreadOptions(self, d):3034 self.keepRepoPath = (d.has_key('options')3035and('keepRepoPath'in d['options']))30363037defgitRefForBranch(self, branch):3038if branch =="main":3039return self.refPrefix +"master"30403041iflen(branch) <=0:3042return branch30433044return self.refPrefix + self.projectName + branch30453046defgitCommitByP4Change(self, ref, change):3047if self.verbose:3048print"looking in ref "+ ref +" for change%susing bisect..."% change30493050 earliestCommit =""3051 latestCommit =parseRevision(ref)30523053while True:3054if self.verbose:3055print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3056 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3057iflen(next) ==0:3058if self.verbose:3059print"argh"3060return""3061 log =extractLogMessageFromGitCommit(next)3062 settings =extractSettingsGitLog(log)3063 currentChange =int(settings['change'])3064if self.verbose:3065print"current change%s"% currentChange30663067if currentChange == change:3068if self.verbose:3069print"found%s"% next3070return next30713072if currentChange < change:3073 earliestCommit ="^%s"% next3074else:3075 latestCommit ="%s"% next30763077return""30783079defimportNewBranch(self, branch, maxChange):3080# make fast-import flush all changes to disk and update the refs using the checkpoint3081# command so that we can try to find the branch parent in the git history3082 self.gitStream.write("checkpoint\n\n");3083 self.gitStream.flush();3084 branchPrefix = self.depotPaths[0] + branch +"/"3085range="@1,%s"% maxChange3086#print "prefix" + branchPrefix3087 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3088iflen(changes) <=0:3089return False3090 firstChange = changes[0]3091#print "first change in branch: %s" % firstChange3092 sourceBranch = self.knownBranches[branch]3093 sourceDepotPath = self.depotPaths[0] + sourceBranch3094 sourceRef = self.gitRefForBranch(sourceBranch)3095#print "source " + sourceBranch30963097 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3098#print "branch parent: %s" % branchParentChange3099 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3100iflen(gitParent) >0:3101 self.initialParents[self.gitRefForBranch(branch)] = gitParent3102#print "parent git commit: %s" % gitParent31033104 self.importChanges(changes)3105return True31063107defsearchParent(self, parent, branch, target):3108 parentFound =False3109for blob inread_pipe_lines(["git","rev-list","--reverse",3110"--no-merges", parent]):3111 blob = blob.strip()3112iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3113 parentFound =True3114if self.verbose:3115print"Found parent of%sin commit%s"% (branch, blob)3116break3117if parentFound:3118return blob3119else:3120return None31213122defimportChanges(self, changes):3123 cnt =13124for change in changes:3125 description =p4_describe(change)3126 self.updateOptionDict(description)31273128if not self.silent:3129 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3130 sys.stdout.flush()3131 cnt = cnt +131323133try:3134if self.detectBranches:3135 branches = self.splitFilesIntoBranches(description)3136for branch in branches.keys():3137## HACK --hwn3138 branchPrefix = self.depotPaths[0] + branch +"/"3139 self.branchPrefixes = [ branchPrefix ]31403141 parent =""31423143 filesForCommit = branches[branch]31443145if self.verbose:3146print"branch is%s"% branch31473148 self.updatedBranches.add(branch)31493150if branch not in self.createdBranches:3151 self.createdBranches.add(branch)3152 parent = self.knownBranches[branch]3153if parent == branch:3154 parent =""3155else:3156 fullBranch = self.projectName + branch3157if fullBranch not in self.p4BranchesInGit:3158if not self.silent:3159print("\nImporting new branch%s"% fullBranch);3160if self.importNewBranch(branch, change -1):3161 parent =""3162 self.p4BranchesInGit.append(fullBranch)3163if not self.silent:3164print("\nResuming with change%s"% change);31653166if self.verbose:3167print"parent determined through known branches:%s"% parent31683169 branch = self.gitRefForBranch(branch)3170 parent = self.gitRefForBranch(parent)31713172if self.verbose:3173print"looking for initial parent for%s; current parent is%s"% (branch, parent)31743175iflen(parent) ==0and branch in self.initialParents:3176 parent = self.initialParents[branch]3177del self.initialParents[branch]31783179 blob =None3180iflen(parent) >0:3181 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3182if self.verbose:3183print"Creating temporary branch: "+ tempBranch3184 self.commit(description, filesForCommit, tempBranch)3185 self.tempBranches.append(tempBranch)3186 self.checkpoint()3187 blob = self.searchParent(parent, branch, tempBranch)3188if blob:3189 self.commit(description, filesForCommit, branch, blob)3190else:3191if self.verbose:3192print"Parent of%snot found. Committing into head of%s"% (branch, parent)3193 self.commit(description, filesForCommit, branch, parent)3194else:3195 files = self.extractFilesFromCommit(description)3196 self.commit(description, files, self.branch,3197 self.initialParent)3198# only needed once, to connect to the previous commit3199 self.initialParent =""3200exceptIOError:3201print self.gitError.read()3202 sys.exit(1)32033204defimportHeadRevision(self, revision):3205print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)32063207 details = {}3208 details["user"] ="git perforce import user"3209 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3210% (' '.join(self.depotPaths), revision))3211 details["change"] = revision3212 newestRevision =032133214 fileCnt =03215 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]32163217for info inp4CmdList(["files"] + fileArgs):32183219if'code'in info and info['code'] =='error':3220 sys.stderr.write("p4 returned an error:%s\n"3221% info['data'])3222if info['data'].find("must refer to client") >=0:3223 sys.stderr.write("This particular p4 error is misleading.\n")3224 sys.stderr.write("Perhaps the depot path was misspelled.\n");3225 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3226 sys.exit(1)3227if'p4ExitCode'in info:3228 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3229 sys.exit(1)323032313232 change =int(info["change"])3233if change > newestRevision:3234 newestRevision = change32353236if info["action"]in self.delete_actions:3237# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3238#fileCnt = fileCnt + 13239continue32403241for prop in["depotFile","rev","action","type"]:3242 details["%s%s"% (prop, fileCnt)] = info[prop]32433244 fileCnt = fileCnt +132453246 details["change"] = newestRevision32473248# Use time from top-most change so that all git p4 clones of3249# the same p4 repo have the same commit SHA1s.3250 res =p4_describe(newestRevision)3251 details["time"] = res["time"]32523253 self.updateOptionDict(details)3254try:3255 self.commit(details, self.extractFilesFromCommit(details), self.branch)3256exceptIOError:3257print"IO error with git fast-import. Is your git version recent enough?"3258print self.gitError.read()325932603261defrun(self, args):3262 self.depotPaths = []3263 self.changeRange =""3264 self.previousDepotPaths = []3265 self.hasOrigin =False32663267# map from branch depot path to parent branch3268 self.knownBranches = {}3269 self.initialParents = {}32703271if self.importIntoRemotes:3272 self.refPrefix ="refs/remotes/p4/"3273else:3274 self.refPrefix ="refs/heads/p4/"32753276if self.syncWithOrigin:3277 self.hasOrigin =originP4BranchesExist()3278if self.hasOrigin:3279if not self.silent:3280print'Syncing with origin first, using "git fetch origin"'3281system("git fetch origin")32823283 branch_arg_given =bool(self.branch)3284iflen(self.branch) ==0:3285 self.branch = self.refPrefix +"master"3286ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3287system("git update-ref%srefs/heads/p4"% self.branch)3288system("git branch -D p4")32893290# accept either the command-line option, or the configuration variable3291if self.useClientSpec:3292# will use this after clone to set the variable3293 self.useClientSpec_from_options =True3294else:3295ifgitConfigBool("git-p4.useclientspec"):3296 self.useClientSpec =True3297if self.useClientSpec:3298 self.clientSpecDirs =getClientSpec()32993300# TODO: should always look at previous commits,3301# merge with previous imports, if possible.3302if args == []:3303if self.hasOrigin:3304createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)33053306# branches holds mapping from branch name to sha13307 branches =p4BranchesInGit(self.importIntoRemotes)33083309# restrict to just this one, disabling detect-branches3310if branch_arg_given:3311 short = self.branch.split("/")[-1]3312if short in branches:3313 self.p4BranchesInGit = [ short ]3314else:3315 self.p4BranchesInGit = branches.keys()33163317iflen(self.p4BranchesInGit) >1:3318if not self.silent:3319print"Importing from/into multiple branches"3320 self.detectBranches =True3321for branch in branches.keys():3322 self.initialParents[self.refPrefix + branch] = \3323 branches[branch]33243325if self.verbose:3326print"branches:%s"% self.p4BranchesInGit33273328 p4Change =03329for branch in self.p4BranchesInGit:3330 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)33313332 settings =extractSettingsGitLog(logMsg)33333334 self.readOptions(settings)3335if(settings.has_key('depot-paths')3336and settings.has_key('change')):3337 change =int(settings['change']) +13338 p4Change =max(p4Change, change)33393340 depotPaths =sorted(settings['depot-paths'])3341if self.previousDepotPaths == []:3342 self.previousDepotPaths = depotPaths3343else:3344 paths = []3345for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3346 prev_list = prev.split("/")3347 cur_list = cur.split("/")3348for i inrange(0,min(len(cur_list),len(prev_list))):3349if cur_list[i] <> prev_list[i]:3350 i = i -13351break33523353 paths.append("/".join(cur_list[:i +1]))33543355 self.previousDepotPaths = paths33563357if p4Change >0:3358 self.depotPaths =sorted(self.previousDepotPaths)3359 self.changeRange ="@%s,#head"% p4Change3360if not self.silent and not self.detectBranches:3361print"Performing incremental import into%sgit branch"% self.branch33623363# accept multiple ref name abbreviations:3364# refs/foo/bar/branch -> use it exactly3365# p4/branch -> prepend refs/remotes/ or refs/heads/3366# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3367if not self.branch.startswith("refs/"):3368if self.importIntoRemotes:3369 prepend ="refs/remotes/"3370else:3371 prepend ="refs/heads/"3372if not self.branch.startswith("p4/"):3373 prepend +="p4/"3374 self.branch = prepend + self.branch33753376iflen(args) ==0and self.depotPaths:3377if not self.silent:3378print"Depot paths:%s"%' '.join(self.depotPaths)3379else:3380if self.depotPaths and self.depotPaths != args:3381print("previous import used depot path%sand now%swas specified. "3382"This doesn't work!"% (' '.join(self.depotPaths),3383' '.join(args)))3384 sys.exit(1)33853386 self.depotPaths =sorted(args)33873388 revision =""3389 self.users = {}33903391# Make sure no revision specifiers are used when --changesfile3392# is specified.3393 bad_changesfile =False3394iflen(self.changesFile) >0:3395for p in self.depotPaths:3396if p.find("@") >=0or p.find("#") >=0:3397 bad_changesfile =True3398break3399if bad_changesfile:3400die("Option --changesfile is incompatible with revision specifiers")34013402 newPaths = []3403for p in self.depotPaths:3404if p.find("@") != -1:3405 atIdx = p.index("@")3406 self.changeRange = p[atIdx:]3407if self.changeRange =="@all":3408 self.changeRange =""3409elif','not in self.changeRange:3410 revision = self.changeRange3411 self.changeRange =""3412 p = p[:atIdx]3413elif p.find("#") != -1:3414 hashIdx = p.index("#")3415 revision = p[hashIdx:]3416 p = p[:hashIdx]3417elif self.previousDepotPaths == []:3418# pay attention to changesfile, if given, else import3419# the entire p4 tree at the head revision3420iflen(self.changesFile) ==0:3421 revision ="#head"34223423 p = re.sub("\.\.\.$","", p)3424if not p.endswith("/"):3425 p +="/"34263427 newPaths.append(p)34283429 self.depotPaths = newPaths34303431# --detect-branches may change this for each branch3432 self.branchPrefixes = self.depotPaths34333434 self.loadUserMapFromCache()3435 self.labels = {}3436if self.detectLabels:3437 self.getLabels();34383439if self.detectBranches:3440## FIXME - what's a P4 projectName ?3441 self.projectName = self.guessProjectName()34423443if self.hasOrigin:3444 self.getBranchMappingFromGitBranches()3445else:3446 self.getBranchMapping()3447if self.verbose:3448print"p4-git branches:%s"% self.p4BranchesInGit3449print"initial parents:%s"% self.initialParents3450for b in self.p4BranchesInGit:3451if b !="master":34523453## FIXME3454 b = b[len(self.projectName):]3455 self.createdBranches.add(b)34563457 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))34583459 self.importProcess = subprocess.Popen(["git","fast-import"],3460 stdin=subprocess.PIPE,3461 stdout=subprocess.PIPE,3462 stderr=subprocess.PIPE);3463 self.gitOutput = self.importProcess.stdout3464 self.gitStream = self.importProcess.stdin3465 self.gitError = self.importProcess.stderr34663467if revision:3468 self.importHeadRevision(revision)3469else:3470 changes = []34713472iflen(self.changesFile) >0:3473 output =open(self.changesFile).readlines()3474 changeSet =set()3475for line in output:3476 changeSet.add(int(line))34773478for change in changeSet:3479 changes.append(change)34803481 changes.sort()3482else:3483# catch "git p4 sync" with no new branches, in a repo that3484# does not have any existing p4 branches3485iflen(args) ==0:3486if not self.p4BranchesInGit:3487die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")34883489# The default branch is master, unless --branch is used to3490# specify something else. Make sure it exists, or complain3491# nicely about how to use --branch.3492if not self.detectBranches:3493if notbranch_exists(self.branch):3494if branch_arg_given:3495die("Error: branch%sdoes not exist."% self.branch)3496else:3497die("Error: no branch%s; perhaps specify one with --branch."%3498 self.branch)34993500if self.verbose:3501print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3502 self.changeRange)3503 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)35043505iflen(self.maxChanges) >0:3506 changes = changes[:min(int(self.maxChanges),len(changes))]35073508iflen(changes) ==0:3509if not self.silent:3510print"No changes to import!"3511else:3512if not self.silent and not self.detectBranches:3513print"Import destination:%s"% self.branch35143515 self.updatedBranches =set()35163517if not self.detectBranches:3518if args:3519# start a new branch3520 self.initialParent =""3521else:3522# build on a previous revision3523 self.initialParent =parseRevision(self.branch)35243525 self.importChanges(changes)35263527if not self.silent:3528print""3529iflen(self.updatedBranches) >0:3530 sys.stdout.write("Updated branches: ")3531for b in self.updatedBranches:3532 sys.stdout.write("%s"% b)3533 sys.stdout.write("\n")35343535ifgitConfigBool("git-p4.importLabels"):3536 self.importLabels =True35373538if self.importLabels:3539 p4Labels =getP4Labels(self.depotPaths)3540 gitTags =getGitTags()35413542 missingP4Labels = p4Labels - gitTags3543 self.importP4Labels(self.gitStream, missingP4Labels)35443545 self.gitStream.close()3546if self.importProcess.wait() !=0:3547die("fast-import failed:%s"% self.gitError.read())3548 self.gitOutput.close()3549 self.gitError.close()35503551# Cleanup temporary branches created during import3552if self.tempBranches != []:3553for branch in self.tempBranches:3554read_pipe("git update-ref -d%s"% branch)3555 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))35563557# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3558# a convenient shortcut refname "p4".3559if self.importIntoRemotes:3560 head_ref = self.refPrefix +"HEAD"3561if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3562system(["git","symbolic-ref", head_ref, self.branch])35633564return True35653566classP4Rebase(Command):3567def__init__(self):3568 Command.__init__(self)3569 self.options = [3570 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3571]3572 self.importLabels =False3573 self.description = ("Fetches the latest revision from perforce and "3574+"rebases the current work (branch) against it")35753576defrun(self, args):3577 sync =P4Sync()3578 sync.importLabels = self.importLabels3579 sync.run([])35803581return self.rebase()35823583defrebase(self):3584if os.system("git update-index --refresh") !=0:3585die("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.");3586iflen(read_pipe("git diff-index HEAD --")) >0:3587die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");35883589[upstream, settings] =findUpstreamBranchPoint()3590iflen(upstream) ==0:3591die("Cannot find upstream branchpoint for rebase")35923593# the branchpoint may be p4/foo~3, so strip off the parent3594 upstream = re.sub("~[0-9]+$","", upstream)35953596print"Rebasing the current branch onto%s"% upstream3597 oldHead =read_pipe("git rev-parse HEAD").strip()3598system("git rebase%s"% upstream)3599system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3600return True36013602classP4Clone(P4Sync):3603def__init__(self):3604 P4Sync.__init__(self)3605 self.description ="Creates a new git repository and imports from Perforce into it"3606 self.usage ="usage: %prog [options] //depot/path[@revRange]"3607 self.options += [3608 optparse.make_option("--destination", dest="cloneDestination",3609 action='store', default=None,3610help="where to leave result of the clone"),3611 optparse.make_option("--bare", dest="cloneBare",3612 action="store_true", default=False),3613]3614 self.cloneDestination =None3615 self.needsGit =False3616 self.cloneBare =False36173618defdefaultDestination(self, args):3619## TODO: use common prefix of args?3620 depotPath = args[0]3621 depotDir = re.sub("(@[^@]*)$","", depotPath)3622 depotDir = re.sub("(#[^#]*)$","", depotDir)3623 depotDir = re.sub(r"\.\.\.$","", depotDir)3624 depotDir = re.sub(r"/$","", depotDir)3625return os.path.split(depotDir)[1]36263627defrun(self, args):3628iflen(args) <1:3629return False36303631if self.keepRepoPath and not self.cloneDestination:3632 sys.stderr.write("Must specify destination for --keep-path\n")3633 sys.exit(1)36343635 depotPaths = args36363637if not self.cloneDestination andlen(depotPaths) >1:3638 self.cloneDestination = depotPaths[-1]3639 depotPaths = depotPaths[:-1]36403641 self.cloneExclude = ["/"+p for p in self.cloneExclude]3642for p in depotPaths:3643if not p.startswith("//"):3644 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3645return False36463647if not self.cloneDestination:3648 self.cloneDestination = self.defaultDestination(args)36493650print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)36513652if not os.path.exists(self.cloneDestination):3653 os.makedirs(self.cloneDestination)3654chdir(self.cloneDestination)36553656 init_cmd = ["git","init"]3657if self.cloneBare:3658 init_cmd.append("--bare")3659 retcode = subprocess.call(init_cmd)3660if retcode:3661raiseCalledProcessError(retcode, init_cmd)36623663if not P4Sync.run(self, depotPaths):3664return False36653666# create a master branch and check out a work tree3667ifgitBranchExists(self.branch):3668system(["git","branch","master", self.branch ])3669if not self.cloneBare:3670system(["git","checkout","-f"])3671else:3672print'Not checking out any branch, use ' \3673'"git checkout -q -b master <branch>"'36743675# auto-set this variable if invoked with --use-client-spec3676if self.useClientSpec_from_options:3677system("git config --bool git-p4.useclientspec true")36783679return True36803681classP4Branches(Command):3682def__init__(self):3683 Command.__init__(self)3684 self.options = [ ]3685 self.description = ("Shows the git branches that hold imports and their "3686+"corresponding perforce depot paths")3687 self.verbose =False36883689defrun(self, args):3690iforiginP4BranchesExist():3691createOrUpdateBranchesFromOrigin()36923693 cmdline ="git rev-parse --symbolic "3694 cmdline +=" --remotes"36953696for line inread_pipe_lines(cmdline):3697 line = line.strip()36983699if not line.startswith('p4/')or line =="p4/HEAD":3700continue3701 branch = line37023703 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3704 settings =extractSettingsGitLog(log)37053706print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3707return True37083709classHelpFormatter(optparse.IndentedHelpFormatter):3710def__init__(self):3711 optparse.IndentedHelpFormatter.__init__(self)37123713defformat_description(self, description):3714if description:3715return description +"\n"3716else:3717return""37183719defprintUsage(commands):3720print"usage:%s<command> [options]"% sys.argv[0]3721print""3722print"valid commands:%s"%", ".join(commands)3723print""3724print"Try%s<command> --help for command specific help."% sys.argv[0]3725print""37263727commands = {3728"debug": P4Debug,3729"submit": P4Submit,3730"commit": P4Submit,3731"sync": P4Sync,3732"rebase": P4Rebase,3733"clone": P4Clone,3734"rollback": P4RollBack,3735"branches": P4Branches3736}373737383739defmain():3740iflen(sys.argv[1:]) ==0:3741printUsage(commands.keys())3742 sys.exit(2)37433744 cmdName = sys.argv[1]3745try:3746 klass = commands[cmdName]3747 cmd =klass()3748exceptKeyError:3749print"unknown command%s"% cmdName3750print""3751printUsage(commands.keys())3752 sys.exit(2)37533754 options = cmd.options3755 cmd.gitdir = os.environ.get("GIT_DIR",None)37563757 args = sys.argv[2:]37583759 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3760if cmd.needsGit:3761 options.append(optparse.make_option("--git-dir", dest="gitdir"))37623763 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3764 options,3765 description = cmd.description,3766 formatter =HelpFormatter())37673768(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3769global verbose3770 verbose = cmd.verbose3771if cmd.needsGit:3772if cmd.gitdir ==None:3773 cmd.gitdir = os.path.abspath(".git")3774if notisValidGitDir(cmd.gitdir):3775# "rev-parse --git-dir" without arguments will try $PWD/.git3776 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3777if os.path.exists(cmd.gitdir):3778 cdup =read_pipe("git rev-parse --show-cdup").strip()3779iflen(cdup) >0:3780chdir(cdup);37813782if notisValidGitDir(cmd.gitdir):3783ifisValidGitDir(cmd.gitdir +"/.git"):3784 cmd.gitdir +="/.git"3785else:3786die("fatal: cannot locate git repository at%s"% cmd.gitdir)37873788# so git commands invoked from the P4 workspace will succeed3789 os.environ["GIT_DIR"] = cmd.gitdir37903791if not cmd.run(args):3792 parser.print_help()3793 sys.exit(2)379437953796if __name__ =='__main__':3797main()