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(): 605 retcode =system(["git","symbolic-ref","-q","HEAD"], ignore_error=True) 606if retcode !=0: 607# on a detached head 608return None 609else: 610returnread_pipe(["git","name-rev","HEAD"]).split(" ")[1].strip() 611 612defisValidGitDir(path): 613returngit_dir(path) !=None 614 615defparseRevision(ref): 616returnread_pipe("git rev-parse%s"% ref).strip() 617 618defbranchExists(ref): 619 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 620 ignore_error=True) 621returnlen(rev) >0 622 623defextractLogMessageFromGitCommit(commit): 624 logMessage ="" 625 626## fixme: title is first line of commit, not 1st paragraph. 627 foundTitle =False 628for log inread_pipe_lines("git cat-file commit%s"% commit): 629if not foundTitle: 630iflen(log) ==1: 631 foundTitle =True 632continue 633 634 logMessage += log 635return logMessage 636 637defextractSettingsGitLog(log): 638 values = {} 639for line in log.split("\n"): 640 line = line.strip() 641 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 642if not m: 643continue 644 645 assignments = m.group(1).split(':') 646for a in assignments: 647 vals = a.split('=') 648 key = vals[0].strip() 649 val = ('='.join(vals[1:])).strip() 650if val.endswith('\"')and val.startswith('"'): 651 val = val[1:-1] 652 653 values[key] = val 654 655 paths = values.get("depot-paths") 656if not paths: 657 paths = values.get("depot-path") 658if paths: 659 values['depot-paths'] = paths.split(',') 660return values 661 662defgitBranchExists(branch): 663 proc = subprocess.Popen(["git","rev-parse", branch], 664 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 665return proc.wait() ==0; 666 667_gitConfig = {} 668 669defgitConfig(key, typeSpecifier=None): 670if not _gitConfig.has_key(key): 671 cmd = ["git","config"] 672if typeSpecifier: 673 cmd += [ typeSpecifier ] 674 cmd += [ key ] 675 s =read_pipe(cmd, ignore_error=True) 676 _gitConfig[key] = s.strip() 677return _gitConfig[key] 678 679defgitConfigBool(key): 680"""Return a bool, using git config --bool. It is True only if the 681 variable is set to true, and False if set to false or not present 682 in the config.""" 683 684if not _gitConfig.has_key(key): 685 _gitConfig[key] =gitConfig(key,'--bool') =="true" 686return _gitConfig[key] 687 688defgitConfigInt(key): 689if not _gitConfig.has_key(key): 690 cmd = ["git","config","--int", key ] 691 s =read_pipe(cmd, ignore_error=True) 692 v = s.strip() 693try: 694 _gitConfig[key] =int(gitConfig(key,'--int')) 695exceptValueError: 696 _gitConfig[key] =None 697return _gitConfig[key] 698 699defgitConfigList(key): 700if not _gitConfig.has_key(key): 701 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 702 _gitConfig[key] = s.strip().splitlines() 703if _gitConfig[key] == ['']: 704 _gitConfig[key] = [] 705return _gitConfig[key] 706 707defp4BranchesInGit(branchesAreInRemotes=True): 708"""Find all the branches whose names start with "p4/", looking 709 in remotes or heads as specified by the argument. Return 710 a dictionary of{ branch: revision }for each one found. 711 The branch names are the short names, without any 712 "p4/" prefix.""" 713 714 branches = {} 715 716 cmdline ="git rev-parse --symbolic " 717if branchesAreInRemotes: 718 cmdline +="--remotes" 719else: 720 cmdline +="--branches" 721 722for line inread_pipe_lines(cmdline): 723 line = line.strip() 724 725# only import to p4/ 726if not line.startswith('p4/'): 727continue 728# special symbolic ref to p4/master 729if line =="p4/HEAD": 730continue 731 732# strip off p4/ prefix 733 branch = line[len("p4/"):] 734 735 branches[branch] =parseRevision(line) 736 737return branches 738 739defbranch_exists(branch): 740"""Make sure that the given ref name really exists.""" 741 742 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 743 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 744 out, _ = p.communicate() 745if p.returncode: 746return False 747# expect exactly one line of output: the branch name 748return out.rstrip() == branch 749 750deffindUpstreamBranchPoint(head ="HEAD"): 751 branches =p4BranchesInGit() 752# map from depot-path to branch name 753 branchByDepotPath = {} 754for branch in branches.keys(): 755 tip = branches[branch] 756 log =extractLogMessageFromGitCommit(tip) 757 settings =extractSettingsGitLog(log) 758if settings.has_key("depot-paths"): 759 paths =",".join(settings["depot-paths"]) 760 branchByDepotPath[paths] ="remotes/p4/"+ branch 761 762 settings =None 763 parent =0 764while parent <65535: 765 commit = head +"~%s"% parent 766 log =extractLogMessageFromGitCommit(commit) 767 settings =extractSettingsGitLog(log) 768if settings.has_key("depot-paths"): 769 paths =",".join(settings["depot-paths"]) 770if branchByDepotPath.has_key(paths): 771return[branchByDepotPath[paths], settings] 772 773 parent = parent +1 774 775return["", settings] 776 777defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 778if not silent: 779print("Creating/updating branch(es) in%sbased on origin branch(es)" 780% localRefPrefix) 781 782 originPrefix ="origin/p4/" 783 784for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 785 line = line.strip() 786if(not line.startswith(originPrefix))or line.endswith("HEAD"): 787continue 788 789 headName = line[len(originPrefix):] 790 remoteHead = localRefPrefix + headName 791 originHead = line 792 793 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 794if(not original.has_key('depot-paths') 795or not original.has_key('change')): 796continue 797 798 update =False 799if notgitBranchExists(remoteHead): 800if verbose: 801print"creating%s"% remoteHead 802 update =True 803else: 804 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 805if settings.has_key('change') >0: 806if settings['depot-paths'] == original['depot-paths']: 807 originP4Change =int(original['change']) 808 p4Change =int(settings['change']) 809if originP4Change > p4Change: 810print("%s(%s) is newer than%s(%s). " 811"Updating p4 branch from origin." 812% (originHead, originP4Change, 813 remoteHead, p4Change)) 814 update =True 815else: 816print("Ignoring:%swas imported from%swhile " 817"%swas imported from%s" 818% (originHead,','.join(original['depot-paths']), 819 remoteHead,','.join(settings['depot-paths']))) 820 821if update: 822system("git update-ref%s %s"% (remoteHead, originHead)) 823 824deforiginP4BranchesExist(): 825returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 826 827 828defp4ParseNumericChangeRange(parts): 829 changeStart =int(parts[0][1:]) 830if parts[1] =='#head': 831 changeEnd =p4_last_change() 832else: 833 changeEnd =int(parts[1]) 834 835return(changeStart, changeEnd) 836 837defchooseBlockSize(blockSize): 838if blockSize: 839return blockSize 840else: 841return defaultBlockSize 842 843defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 844assert depotPaths 845 846# Parse the change range into start and end. Try to find integer 847# revision ranges as these can be broken up into blocks to avoid 848# hitting server-side limits (maxrows, maxscanresults). But if 849# that doesn't work, fall back to using the raw revision specifier 850# strings, without using block mode. 851 852if changeRange is None or changeRange =='': 853 changeStart =1 854 changeEnd =p4_last_change() 855 block_size =chooseBlockSize(requestedBlockSize) 856else: 857 parts = changeRange.split(',') 858assertlen(parts) ==2 859try: 860(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 861 block_size =chooseBlockSize(requestedBlockSize) 862except: 863 changeStart = parts[0][1:] 864 changeEnd = parts[1] 865if requestedBlockSize: 866die("cannot use --changes-block-size with non-numeric revisions") 867 block_size =None 868 869 changes =set() 870 871# Retrieve changes a block at a time, to prevent running 872# into a MaxResults/MaxScanRows error from the server. 873 874while True: 875 cmd = ['changes'] 876 877if block_size: 878 end =min(changeEnd, changeStart + block_size) 879 revisionRange ="%d,%d"% (changeStart, end) 880else: 881 revisionRange ="%s,%s"% (changeStart, changeEnd) 882 883for p in depotPaths: 884 cmd += ["%s...@%s"% (p, revisionRange)] 885 886# Insert changes in chronological order 887for line inreversed(p4_read_pipe_lines(cmd)): 888 changes.add(int(line.split(" ")[1])) 889 890if not block_size: 891break 892 893if end >= changeEnd: 894break 895 896 changeStart = end +1 897 898 changes =sorted(changes) 899return changes 900 901defp4PathStartsWith(path, prefix): 902# This method tries to remedy a potential mixed-case issue: 903# 904# If UserA adds //depot/DirA/file1 905# and UserB adds //depot/dira/file2 906# 907# we may or may not have a problem. If you have core.ignorecase=true, 908# we treat DirA and dira as the same directory 909ifgitConfigBool("core.ignorecase"): 910return path.lower().startswith(prefix.lower()) 911return path.startswith(prefix) 912 913defgetClientSpec(): 914"""Look at the p4 client spec, create a View() object that contains 915 all the mappings, and return it.""" 916 917 specList =p4CmdList("client -o") 918iflen(specList) !=1: 919die('Output from "client -o" is%dlines, expecting 1'% 920len(specList)) 921 922# dictionary of all client parameters 923 entry = specList[0] 924 925# the //client/ name 926 client_name = entry["Client"] 927 928# just the keys that start with "View" 929 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 930 931# hold this new View 932 view =View(client_name) 933 934# append the lines, in order, to the view 935for view_num inrange(len(view_keys)): 936 k ="View%d"% view_num 937if k not in view_keys: 938die("Expected view key%smissing"% k) 939 view.append(entry[k]) 940 941return view 942 943defgetClientRoot(): 944"""Grab the client directory.""" 945 946 output =p4CmdList("client -o") 947iflen(output) !=1: 948die('Output from "client -o" is%dlines, expecting 1'%len(output)) 949 950 entry = output[0] 951if"Root"not in entry: 952die('Client has no "Root"') 953 954return entry["Root"] 955 956# 957# P4 wildcards are not allowed in filenames. P4 complains 958# if you simply add them, but you can force it with "-f", in 959# which case it translates them into %xx encoding internally. 960# 961defwildcard_decode(path): 962# Search for and fix just these four characters. Do % last so 963# that fixing it does not inadvertently create new %-escapes. 964# Cannot have * in a filename in windows; untested as to 965# what p4 would do in such a case. 966if not platform.system() =="Windows": 967 path = path.replace("%2A","*") 968 path = path.replace("%23","#") \ 969.replace("%40","@") \ 970.replace("%25","%") 971return path 972 973defwildcard_encode(path): 974# do % first to avoid double-encoding the %s introduced here 975 path = path.replace("%","%25") \ 976.replace("*","%2A") \ 977.replace("#","%23") \ 978.replace("@","%40") 979return path 980 981defwildcard_present(path): 982 m = re.search("[*#@%]", path) 983return m is not None 984 985classLargeFileSystem(object): 986"""Base class for large file system support.""" 987 988def__init__(self, writeToGitStream): 989 self.largeFiles =set() 990 self.writeToGitStream = writeToGitStream 991 992defgeneratePointer(self, cloneDestination, contentFile): 993"""Return the content of a pointer file that is stored in Git instead of 994 the actual content.""" 995assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 996 997defpushFile(self, localLargeFile): 998"""Push the actual content which is not stored in the Git repository to 999 a server."""1000assert False,"Method 'pushFile' required in "+ self.__class__.__name__10011002defhasLargeFileExtension(self, relPath):1003returnreduce(1004lambda a, b: a or b,1005[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1006False1007)10081009defgenerateTempFile(self, contents):1010 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1011for d in contents:1012 contentFile.write(d)1013 contentFile.close()1014return contentFile.name10151016defexceedsLargeFileThreshold(self, relPath, contents):1017ifgitConfigInt('git-p4.largeFileThreshold'):1018 contentsSize =sum(len(d)for d in contents)1019if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1020return True1021ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1022 contentsSize =sum(len(d)for d in contents)1023if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1024return False1025 contentTempFile = self.generateTempFile(contents)1026 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1027 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1028 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1029 zf.close()1030 compressedContentsSize = zf.infolist()[0].compress_size1031 os.remove(contentTempFile)1032 os.remove(compressedContentFile.name)1033if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1034return True1035return False10361037defaddLargeFile(self, relPath):1038 self.largeFiles.add(relPath)10391040defremoveLargeFile(self, relPath):1041 self.largeFiles.remove(relPath)10421043defisLargeFile(self, relPath):1044return relPath in self.largeFiles10451046defprocessContent(self, git_mode, relPath, contents):1047"""Processes the content of git fast import. This method decides if a1048 file is stored in the large file system and handles all necessary1049 steps."""1050if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1051 contentTempFile = self.generateTempFile(contents)1052(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1053if pointer_git_mode:1054 git_mode = pointer_git_mode1055if localLargeFile:1056# Move temp file to final location in large file system1057 largeFileDir = os.path.dirname(localLargeFile)1058if not os.path.isdir(largeFileDir):1059 os.makedirs(largeFileDir)1060 shutil.move(contentTempFile, localLargeFile)1061 self.addLargeFile(relPath)1062ifgitConfigBool('git-p4.largeFilePush'):1063 self.pushFile(localLargeFile)1064if verbose:1065 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1066return(git_mode, contents)10671068classMockLFS(LargeFileSystem):1069"""Mock large file system for testing."""10701071defgeneratePointer(self, contentFile):1072"""The pointer content is the original content prefixed with "pointer-".1073 The local filename of the large file storage is derived from the file content.1074 """1075withopen(contentFile,'r')as f:1076 content =next(f)1077 gitMode ='100644'1078 pointerContents ='pointer-'+ content1079 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1080return(gitMode, pointerContents, localLargeFile)10811082defpushFile(self, localLargeFile):1083"""The remote filename of the large file storage is the same as the local1084 one but in a different directory.1085 """1086 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1087if not os.path.exists(remotePath):1088 os.makedirs(remotePath)1089 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10901091classGitLFS(LargeFileSystem):1092"""Git LFS as backend for the git-p4 large file system.1093 See https://git-lfs.github.com/ for details."""10941095def__init__(self, *args):1096 LargeFileSystem.__init__(self, *args)1097 self.baseGitAttributes = []10981099defgeneratePointer(self, contentFile):1100"""Generate a Git LFS pointer for the content. Return LFS Pointer file1101 mode and content which is stored in the Git repository instead of1102 the actual content. Return also the new location of the actual1103 content.1104 """1105if os.path.getsize(contentFile) ==0:1106return(None,'',None)11071108 pointerProcess = subprocess.Popen(1109['git','lfs','pointer','--file='+ contentFile],1110 stdout=subprocess.PIPE1111)1112 pointerFile = pointerProcess.stdout.read()1113if pointerProcess.wait():1114 os.remove(contentFile)1115die('git-lfs pointer command failed. Did you install the extension?')11161117# Git LFS removed the preamble in the output of the 'pointer' command1118# starting from version 1.2.0. Check for the preamble here to support1119# earlier versions.1120# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431121if pointerFile.startswith('Git LFS pointer for'):1122 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)11231124 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1125 localLargeFile = os.path.join(1126 os.getcwd(),1127'.git','lfs','objects', oid[:2], oid[2:4],1128 oid,1129)1130# LFS Spec states that pointer files should not have the executable bit set.1131 gitMode ='100644'1132return(gitMode, pointerFile, localLargeFile)11331134defpushFile(self, localLargeFile):1135 uploadProcess = subprocess.Popen(1136['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1137)1138if uploadProcess.wait():1139die('git-lfs push command failed. Did you define a remote?')11401141defgenerateGitAttributes(self):1142return(1143 self.baseGitAttributes +1144[1145'\n',1146'#\n',1147'# Git LFS (see https://git-lfs.github.com/)\n',1148'#\n',1149] +1150['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1151for f insorted(gitConfigList('git-p4.largeFileExtensions'))1152] +1153['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1154for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1155]1156)11571158defaddLargeFile(self, relPath):1159 LargeFileSystem.addLargeFile(self, relPath)1160 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11611162defremoveLargeFile(self, relPath):1163 LargeFileSystem.removeLargeFile(self, relPath)1164 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11651166defprocessContent(self, git_mode, relPath, contents):1167if relPath =='.gitattributes':1168 self.baseGitAttributes = contents1169return(git_mode, self.generateGitAttributes())1170else:1171return LargeFileSystem.processContent(self, git_mode, relPath, contents)11721173class Command:1174def__init__(self):1175 self.usage ="usage: %prog [options]"1176 self.needsGit =True1177 self.verbose =False11781179class P4UserMap:1180def__init__(self):1181 self.userMapFromPerforceServer =False1182 self.myP4UserId =None11831184defp4UserId(self):1185if self.myP4UserId:1186return self.myP4UserId11871188 results =p4CmdList("user -o")1189for r in results:1190if r.has_key('User'):1191 self.myP4UserId = r['User']1192return r['User']1193die("Could not find your p4 user id")11941195defp4UserIsMe(self, p4User):1196# return True if the given p4 user is actually me1197 me = self.p4UserId()1198if not p4User or p4User != me:1199return False1200else:1201return True12021203defgetUserCacheFilename(self):1204 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1205return home +"/.gitp4-usercache.txt"12061207defgetUserMapFromPerforceServer(self):1208if self.userMapFromPerforceServer:1209return1210 self.users = {}1211 self.emails = {}12121213for output inp4CmdList("users"):1214if not output.has_key("User"):1215continue1216 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1217 self.emails[output["Email"]] = output["User"]12181219 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1220for mapUserConfig ingitConfigList("git-p4.mapUser"):1221 mapUser = mapUserConfigRegex.findall(mapUserConfig)1222if mapUser andlen(mapUser[0]) ==3:1223 user = mapUser[0][0]1224 fullname = mapUser[0][1]1225 email = mapUser[0][2]1226 self.users[user] = fullname +" <"+ email +">"1227 self.emails[email] = user12281229 s =''1230for(key, val)in self.users.items():1231 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12321233open(self.getUserCacheFilename(),"wb").write(s)1234 self.userMapFromPerforceServer =True12351236defloadUserMapFromCache(self):1237 self.users = {}1238 self.userMapFromPerforceServer =False1239try:1240 cache =open(self.getUserCacheFilename(),"rb")1241 lines = cache.readlines()1242 cache.close()1243for line in lines:1244 entry = line.strip().split("\t")1245 self.users[entry[0]] = entry[1]1246exceptIOError:1247 self.getUserMapFromPerforceServer()12481249classP4Debug(Command):1250def__init__(self):1251 Command.__init__(self)1252 self.options = []1253 self.description ="A tool to debug the output of p4 -G."1254 self.needsGit =False12551256defrun(self, args):1257 j =01258for output inp4CmdList(args):1259print'Element:%d'% j1260 j +=11261print output1262return True12631264classP4RollBack(Command):1265def__init__(self):1266 Command.__init__(self)1267 self.options = [1268 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1269]1270 self.description ="A tool to debug the multi-branch import. Don't use :)"1271 self.rollbackLocalBranches =False12721273defrun(self, args):1274iflen(args) !=1:1275return False1276 maxChange =int(args[0])12771278if"p4ExitCode"inp4Cmd("changes -m 1"):1279die("Problems executing p4");12801281if self.rollbackLocalBranches:1282 refPrefix ="refs/heads/"1283 lines =read_pipe_lines("git rev-parse --symbolic --branches")1284else:1285 refPrefix ="refs/remotes/"1286 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12871288for line in lines:1289if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1290 line = line.strip()1291 ref = refPrefix + line1292 log =extractLogMessageFromGitCommit(ref)1293 settings =extractSettingsGitLog(log)12941295 depotPaths = settings['depot-paths']1296 change = settings['change']12971298 changed =False12991300iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1301for p in depotPaths]))) ==0:1302print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1303system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1304continue13051306while change andint(change) > maxChange:1307 changed =True1308if self.verbose:1309print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1310system("git update-ref%s\"%s^\""% (ref, ref))1311 log =extractLogMessageFromGitCommit(ref)1312 settings =extractSettingsGitLog(log)131313141315 depotPaths = settings['depot-paths']1316 change = settings['change']13171318if changed:1319print"%srewound to%s"% (ref, change)13201321return True13221323classP4Submit(Command, P4UserMap):13241325 conflict_behavior_choices = ("ask","skip","quit")13261327def__init__(self):1328 Command.__init__(self)1329 P4UserMap.__init__(self)1330 self.options = [1331 optparse.make_option("--origin", dest="origin"),1332 optparse.make_option("-M", dest="detectRenames", action="store_true"),1333# preserve the user, requires relevant p4 permissions1334 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1335 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1336 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1337 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1338 optparse.make_option("--conflict", dest="conflict_behavior",1339 choices=self.conflict_behavior_choices),1340 optparse.make_option("--branch", dest="branch"),1341 optparse.make_option("--shelve", dest="shelve", action="store_true",1342help="Shelve instead of submit. Shelved files are reverted, "1343"restoring the workspace to the state before the shelve"),1344 optparse.make_option("--update-shelve", dest="update_shelve", action="store",type="int",1345 metavar="CHANGELIST",1346help="update an existing shelved changelist, implies --shelve")1347]1348 self.description ="Submit changes from git to the perforce depot."1349 self.usage +=" [name of git branch to submit into perforce depot]"1350 self.origin =""1351 self.detectRenames =False1352 self.preserveUser =gitConfigBool("git-p4.preserveUser")1353 self.dry_run =False1354 self.shelve =False1355 self.update_shelve =None1356 self.prepare_p4_only =False1357 self.conflict_behavior =None1358 self.isWindows = (platform.system() =="Windows")1359 self.exportLabels =False1360 self.p4HasMoveCommand =p4_has_move_command()1361 self.branch =None13621363ifgitConfig('git-p4.largeFileSystem'):1364die("Large file system not supported for git-p4 submit command. Please remove it from config.")13651366defcheck(self):1367iflen(p4CmdList("opened ...")) >0:1368die("You have files opened with perforce! Close them before starting the sync.")13691370defseparate_jobs_from_description(self, message):1371"""Extract and return a possible Jobs field in the commit1372 message. It goes into a separate section in the p4 change1373 specification.13741375 A jobs line starts with "Jobs:" and looks like a new field1376 in a form. Values are white-space separated on the same1377 line or on following lines that start with a tab.13781379 This does not parse and extract the full git commit message1380 like a p4 form. It just sees the Jobs: line as a marker1381 to pass everything from then on directly into the p4 form,1382 but outside the description section.13831384 Return a tuple (stripped log message, jobs string)."""13851386 m = re.search(r'^Jobs:', message, re.MULTILINE)1387if m is None:1388return(message,None)13891390 jobtext = message[m.start():]1391 stripped_message = message[:m.start()].rstrip()1392return(stripped_message, jobtext)13931394defprepareLogMessage(self, template, message, jobs):1395"""Edits the template returned from "p4 change -o" to insert1396 the message in the Description field, and the jobs text in1397 the Jobs field."""1398 result =""13991400 inDescriptionSection =False14011402for line in template.split("\n"):1403if line.startswith("#"):1404 result += line +"\n"1405continue14061407if inDescriptionSection:1408if line.startswith("Files:")or line.startswith("Jobs:"):1409 inDescriptionSection =False1410# insert Jobs section1411if jobs:1412 result += jobs +"\n"1413else:1414continue1415else:1416if line.startswith("Description:"):1417 inDescriptionSection =True1418 line +="\n"1419for messageLine in message.split("\n"):1420 line +="\t"+ messageLine +"\n"14211422 result += line +"\n"14231424return result14251426defpatchRCSKeywords(self,file, pattern):1427# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1428(handle, outFileName) = tempfile.mkstemp(dir='.')1429try:1430 outFile = os.fdopen(handle,"w+")1431 inFile =open(file,"r")1432 regexp = re.compile(pattern, re.VERBOSE)1433for line in inFile.readlines():1434 line = regexp.sub(r'$\1$', line)1435 outFile.write(line)1436 inFile.close()1437 outFile.close()1438# Forcibly overwrite the original file1439 os.unlink(file)1440 shutil.move(outFileName,file)1441except:1442# cleanup our temporary file1443 os.unlink(outFileName)1444print"Failed to strip RCS keywords in%s"%file1445raise14461447print"Patched up RCS keywords in%s"%file14481449defp4UserForCommit(self,id):1450# Return the tuple (perforce user,git email) for a given git commit id1451 self.getUserMapFromPerforceServer()1452 gitEmail =read_pipe(["git","log","--max-count=1",1453"--format=%ae",id])1454 gitEmail = gitEmail.strip()1455if not self.emails.has_key(gitEmail):1456return(None,gitEmail)1457else:1458return(self.emails[gitEmail],gitEmail)14591460defcheckValidP4Users(self,commits):1461# check if any git authors cannot be mapped to p4 users1462foridin commits:1463(user,email) = self.p4UserForCommit(id)1464if not user:1465 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1466ifgitConfigBool("git-p4.allowMissingP4Users"):1467print"%s"% msg1468else:1469die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14701471deflastP4Changelist(self):1472# Get back the last changelist number submitted in this client spec. This1473# then gets used to patch up the username in the change. If the same1474# client spec is being used by multiple processes then this might go1475# wrong.1476 results =p4CmdList("client -o")# find the current client1477 client =None1478for r in results:1479if r.has_key('Client'):1480 client = r['Client']1481break1482if not client:1483die("could not get client spec")1484 results =p4CmdList(["changes","-c", client,"-m","1"])1485for r in results:1486if r.has_key('change'):1487return r['change']1488die("Could not get changelist number for last submit - cannot patch up user details")14891490defmodifyChangelistUser(self, changelist, newUser):1491# fixup the user field of a changelist after it has been submitted.1492 changes =p4CmdList("change -o%s"% changelist)1493iflen(changes) !=1:1494die("Bad output from p4 change modifying%sto user%s"%1495(changelist, newUser))14961497 c = changes[0]1498if c['User'] == newUser:return# nothing to do1499 c['User'] = newUser1500input= marshal.dumps(c)15011502 result =p4CmdList("change -f -i", stdin=input)1503for r in result:1504if r.has_key('code'):1505if r['code'] =='error':1506die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1507if r.has_key('data'):1508print("Updated user field for changelist%sto%s"% (changelist, newUser))1509return1510die("Could not modify user field of changelist%sto%s"% (changelist, newUser))15111512defcanChangeChangelists(self):1513# check to see if we have p4 admin or super-user permissions, either of1514# which are required to modify changelists.1515 results =p4CmdList(["protects", self.depotPath])1516for r in results:1517if r.has_key('perm'):1518if r['perm'] =='admin':1519return11520if r['perm'] =='super':1521return11522return015231524defprepareSubmitTemplate(self, changelist=None):1525"""Run "p4 change -o" to grab a change specification template.1526 This does not use "p4 -G", as it is nice to keep the submission1527 template in original order, since a human might edit it.15281529 Remove lines in the Files section that show changes to files1530 outside the depot path we're committing into."""15311532[upstream, settings] =findUpstreamBranchPoint()15331534 template =""1535 inFilesSection =False1536 args = ['change','-o']1537if changelist:1538 args.append(str(changelist))15391540for line inp4_read_pipe_lines(args):1541if line.endswith("\r\n"):1542 line = line[:-2] +"\n"1543if inFilesSection:1544if line.startswith("\t"):1545# path starts and ends with a tab1546 path = line[1:]1547 lastTab = path.rfind("\t")1548if lastTab != -1:1549 path = path[:lastTab]1550if settings.has_key('depot-paths'):1551if not[p for p in settings['depot-paths']1552ifp4PathStartsWith(path, p)]:1553continue1554else:1555if notp4PathStartsWith(path, self.depotPath):1556continue1557else:1558 inFilesSection =False1559else:1560if line.startswith("Files:"):1561 inFilesSection =True15621563 template += line15641565return template15661567defedit_template(self, template_file):1568"""Invoke the editor to let the user change the submission1569 message. Return true if okay to continue with the submit."""15701571# if configured to skip the editing part, just submit1572ifgitConfigBool("git-p4.skipSubmitEdit"):1573return True15741575# look at the modification time, to check later if the user saved1576# the file1577 mtime = os.stat(template_file).st_mtime15781579# invoke the editor1580if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1581 editor = os.environ.get("P4EDITOR")1582else:1583 editor =read_pipe("git var GIT_EDITOR").strip()1584system(["sh","-c", ('%s"$@"'% editor), editor, template_file])15851586# If the file was not saved, prompt to see if this patch should1587# be skipped. But skip this verification step if configured so.1588ifgitConfigBool("git-p4.skipSubmitEditCheck"):1589return True15901591# modification time updated means user saved the file1592if os.stat(template_file).st_mtime > mtime:1593return True15941595while True:1596 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1597if response =='y':1598return True1599if response =='n':1600return False16011602defget_diff_description(self, editedFiles, filesToAdd, symlinks):1603# diff1604if os.environ.has_key("P4DIFF"):1605del(os.environ["P4DIFF"])1606 diff =""1607for editedFile in editedFiles:1608 diff +=p4_read_pipe(['diff','-du',1609wildcard_encode(editedFile)])16101611# new file diff1612 newdiff =""1613for newFile in filesToAdd:1614 newdiff +="==== new file ====\n"1615 newdiff +="--- /dev/null\n"1616 newdiff +="+++%s\n"% newFile16171618 is_link = os.path.islink(newFile)1619 expect_link = newFile in symlinks16201621if is_link and expect_link:1622 newdiff +="+%s\n"% os.readlink(newFile)1623else:1624 f =open(newFile,"r")1625for line in f.readlines():1626 newdiff +="+"+ line1627 f.close()16281629return(diff + newdiff).replace('\r\n','\n')16301631defapplyCommit(self,id):1632"""Apply one commit, return True if it succeeded."""16331634print"Applying",read_pipe(["git","show","-s",1635"--format=format:%h%s",id])16361637(p4User, gitEmail) = self.p4UserForCommit(id)16381639 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1640 filesToAdd =set()1641 filesToChangeType =set()1642 filesToDelete =set()1643 editedFiles =set()1644 pureRenameCopy =set()1645 symlinks =set()1646 filesToChangeExecBit = {}1647 all_files =list()16481649for line in diff:1650 diff =parseDiffTreeEntry(line)1651 modifier = diff['status']1652 path = diff['src']1653 all_files.append(path)16541655if modifier =="M":1656p4_edit(path)1657ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1658 filesToChangeExecBit[path] = diff['dst_mode']1659 editedFiles.add(path)1660elif modifier =="A":1661 filesToAdd.add(path)1662 filesToChangeExecBit[path] = diff['dst_mode']1663if path in filesToDelete:1664 filesToDelete.remove(path)16651666 dst_mode =int(diff['dst_mode'],8)1667if dst_mode ==0120000:1668 symlinks.add(path)16691670elif modifier =="D":1671 filesToDelete.add(path)1672if path in filesToAdd:1673 filesToAdd.remove(path)1674elif modifier =="C":1675 src, dest = diff['src'], diff['dst']1676p4_integrate(src, dest)1677 pureRenameCopy.add(dest)1678if diff['src_sha1'] != diff['dst_sha1']:1679p4_edit(dest)1680 pureRenameCopy.discard(dest)1681ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1682p4_edit(dest)1683 pureRenameCopy.discard(dest)1684 filesToChangeExecBit[dest] = diff['dst_mode']1685if self.isWindows:1686# turn off read-only attribute1687 os.chmod(dest, stat.S_IWRITE)1688 os.unlink(dest)1689 editedFiles.add(dest)1690elif modifier =="R":1691 src, dest = diff['src'], diff['dst']1692if self.p4HasMoveCommand:1693p4_edit(src)# src must be open before move1694p4_move(src, dest)# opens for (move/delete, move/add)1695else:1696p4_integrate(src, dest)1697if diff['src_sha1'] != diff['dst_sha1']:1698p4_edit(dest)1699else:1700 pureRenameCopy.add(dest)1701ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1702if not self.p4HasMoveCommand:1703p4_edit(dest)# with move: already open, writable1704 filesToChangeExecBit[dest] = diff['dst_mode']1705if not self.p4HasMoveCommand:1706if self.isWindows:1707 os.chmod(dest, stat.S_IWRITE)1708 os.unlink(dest)1709 filesToDelete.add(src)1710 editedFiles.add(dest)1711elif modifier =="T":1712 filesToChangeType.add(path)1713else:1714die("unknown modifier%sfor%s"% (modifier, path))17151716 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1717 patchcmd = diffcmd +" | git apply "1718 tryPatchCmd = patchcmd +"--check -"1719 applyPatchCmd = patchcmd +"--check --apply -"1720 patch_succeeded =True17211722if os.system(tryPatchCmd) !=0:1723 fixed_rcs_keywords =False1724 patch_succeeded =False1725print"Unfortunately applying the change failed!"17261727# Patch failed, maybe it's just RCS keyword woes. Look through1728# the patch to see if that's possible.1729ifgitConfigBool("git-p4.attemptRCSCleanup"):1730file=None1731 pattern =None1732 kwfiles = {}1733forfilein editedFiles | filesToDelete:1734# did this file's delta contain RCS keywords?1735 pattern =p4_keywords_regexp_for_file(file)17361737if pattern:1738# this file is a possibility...look for RCS keywords.1739 regexp = re.compile(pattern, re.VERBOSE)1740for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1741if regexp.search(line):1742if verbose:1743print"got keyword match on%sin%sin%s"% (pattern, line,file)1744 kwfiles[file] = pattern1745break17461747forfilein kwfiles:1748if verbose:1749print"zapping%swith%s"% (line,pattern)1750# File is being deleted, so not open in p4. Must1751# disable the read-only bit on windows.1752if self.isWindows andfilenot in editedFiles:1753 os.chmod(file, stat.S_IWRITE)1754 self.patchRCSKeywords(file, kwfiles[file])1755 fixed_rcs_keywords =True17561757if fixed_rcs_keywords:1758print"Retrying the patch with RCS keywords cleaned up"1759if os.system(tryPatchCmd) ==0:1760 patch_succeeded =True17611762if not patch_succeeded:1763for f in editedFiles:1764p4_revert(f)1765return False17661767#1768# Apply the patch for real, and do add/delete/+x handling.1769#1770system(applyPatchCmd)17711772for f in filesToChangeType:1773p4_edit(f,"-t","auto")1774for f in filesToAdd:1775p4_add(f)1776for f in filesToDelete:1777p4_revert(f)1778p4_delete(f)17791780# Set/clear executable bits1781for f in filesToChangeExecBit.keys():1782 mode = filesToChangeExecBit[f]1783setP4ExecBit(f, mode)17841785if self.update_shelve:1786print("all_files =%s"%str(all_files))1787p4_reopen_in_change(self.update_shelve, all_files)17881789#1790# Build p4 change description, starting with the contents1791# of the git commit message.1792#1793 logMessage =extractLogMessageFromGitCommit(id)1794 logMessage = logMessage.strip()1795(logMessage, jobs) = self.separate_jobs_from_description(logMessage)17961797 template = self.prepareSubmitTemplate(self.update_shelve)1798 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)17991800if self.preserveUser:1801 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User18021803if self.checkAuthorship and not self.p4UserIsMe(p4User):1804 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1805 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1806 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"18071808 separatorLine ="######## everything below this line is just the diff #######\n"1809if not self.prepare_p4_only:1810 submitTemplate += separatorLine1811 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)18121813(handle, fileName) = tempfile.mkstemp()1814 tmpFile = os.fdopen(handle,"w+b")1815if self.isWindows:1816 submitTemplate = submitTemplate.replace("\n","\r\n")1817 tmpFile.write(submitTemplate)1818 tmpFile.close()18191820if self.prepare_p4_only:1821#1822# Leave the p4 tree prepared, and the submit template around1823# and let the user decide what to do next1824#1825print1826print"P4 workspace prepared for submission."1827print"To submit or revert, go to client workspace"1828print" "+ self.clientPath1829print1830print"To submit, use\"p4 submit\"to write a new description,"1831print"or\"p4 submit -i <%s\"to use the one prepared by" \1832"\"git p4\"."% fileName1833print"You can delete the file\"%s\"when finished."% fileName18341835if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1836print"To preserve change ownership by user%s, you must\n" \1837"do\"p4 change -f <change>\"after submitting and\n" \1838"edit the User field."1839if pureRenameCopy:1840print"After submitting, renamed files must be re-synced."1841print"Invoke\"p4 sync -f\"on each of these files:"1842for f in pureRenameCopy:1843print" "+ f18441845print1846print"To revert the changes, use\"p4 revert ...\", and delete"1847print"the submit template file\"%s\""% fileName1848if filesToAdd:1849print"Since the commit adds new files, they must be deleted:"1850for f in filesToAdd:1851print" "+ f1852print1853return True18541855#1856# Let the user edit the change description, then submit it.1857#1858 submitted =False18591860try:1861if self.edit_template(fileName):1862# read the edited message and submit1863 tmpFile =open(fileName,"rb")1864 message = tmpFile.read()1865 tmpFile.close()1866if self.isWindows:1867 message = message.replace("\r\n","\n")1868 submitTemplate = message[:message.index(separatorLine)]18691870if self.update_shelve:1871p4_write_pipe(['shelve','-r','-i'], submitTemplate)1872elif self.shelve:1873p4_write_pipe(['shelve','-i'], submitTemplate)1874else:1875p4_write_pipe(['submit','-i'], submitTemplate)1876# The rename/copy happened by applying a patch that created a1877# new file. This leaves it writable, which confuses p4.1878for f in pureRenameCopy:1879p4_sync(f,"-f")18801881if self.preserveUser:1882if p4User:1883# Get last changelist number. Cannot easily get it from1884# the submit command output as the output is1885# unmarshalled.1886 changelist = self.lastP4Changelist()1887 self.modifyChangelistUser(changelist, p4User)18881889 submitted =True18901891finally:1892# skip this patch1893if not submitted or self.shelve:1894if self.shelve:1895print("Reverting shelved files.")1896else:1897print("Submission cancelled, undoing p4 changes.")1898for f in editedFiles | filesToDelete:1899p4_revert(f)1900for f in filesToAdd:1901p4_revert(f)1902 os.remove(f)19031904 os.remove(fileName)1905return submitted19061907# Export git tags as p4 labels. Create a p4 label and then tag1908# with that.1909defexportGitTags(self, gitTags):1910 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1911iflen(validLabelRegexp) ==0:1912 validLabelRegexp = defaultLabelRegexp1913 m = re.compile(validLabelRegexp)19141915for name in gitTags:19161917if not m.match(name):1918if verbose:1919print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1920continue19211922# Get the p4 commit this corresponds to1923 logMessage =extractLogMessageFromGitCommit(name)1924 values =extractSettingsGitLog(logMessage)19251926if not values.has_key('change'):1927# a tag pointing to something not sent to p4; ignore1928if verbose:1929print"git tag%sdoes not give a p4 commit"% name1930continue1931else:1932 changelist = values['change']19331934# Get the tag details.1935 inHeader =True1936 isAnnotated =False1937 body = []1938for l inread_pipe_lines(["git","cat-file","-p", name]):1939 l = l.strip()1940if inHeader:1941if re.match(r'tag\s+', l):1942 isAnnotated =True1943elif re.match(r'\s*$', l):1944 inHeader =False1945continue1946else:1947 body.append(l)19481949if not isAnnotated:1950 body = ["lightweight tag imported by git p4\n"]19511952# Create the label - use the same view as the client spec we are using1953 clientSpec =getClientSpec()19541955 labelTemplate ="Label:%s\n"% name1956 labelTemplate +="Description:\n"1957for b in body:1958 labelTemplate +="\t"+ b +"\n"1959 labelTemplate +="View:\n"1960for depot_side in clientSpec.mappings:1961 labelTemplate +="\t%s\n"% depot_side19621963if self.dry_run:1964print"Would create p4 label%sfor tag"% name1965elif self.prepare_p4_only:1966print"Not creating p4 label%sfor tag due to option" \1967" --prepare-p4-only"% name1968else:1969p4_write_pipe(["label","-i"], labelTemplate)19701971# Use the label1972p4_system(["tag","-l", name] +1973["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])19741975if verbose:1976print"created p4 label for tag%s"% name19771978defrun(self, args):1979iflen(args) ==0:1980 self.master =currentGitBranch()1981eliflen(args) ==1:1982 self.master = args[0]1983if notbranchExists(self.master):1984die("Branch%sdoes not exist"% self.master)1985else:1986return False19871988if self.master:1989 allowSubmit =gitConfig("git-p4.allowSubmit")1990iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1991die("%sis not in git-p4.allowSubmit"% self.master)19921993[upstream, settings] =findUpstreamBranchPoint()1994 self.depotPath = settings['depot-paths'][0]1995iflen(self.origin) ==0:1996 self.origin = upstream19971998if self.update_shelve:1999 self.shelve =True20002001if self.preserveUser:2002if not self.canChangeChangelists():2003die("Cannot preserve user names without p4 super-user or admin permissions")20042005# if not set from the command line, try the config file2006if self.conflict_behavior is None:2007 val =gitConfig("git-p4.conflict")2008if val:2009if val not in self.conflict_behavior_choices:2010die("Invalid value '%s' for config git-p4.conflict"% val)2011else:2012 val ="ask"2013 self.conflict_behavior = val20142015if self.verbose:2016print"Origin branch is "+ self.origin20172018iflen(self.depotPath) ==0:2019print"Internal error: cannot locate perforce depot path from existing branches"2020 sys.exit(128)20212022 self.useClientSpec =False2023ifgitConfigBool("git-p4.useclientspec"):2024 self.useClientSpec =True2025if self.useClientSpec:2026 self.clientSpecDirs =getClientSpec()20272028# Check for the existence of P4 branches2029 branchesDetected = (len(p4BranchesInGit().keys()) >1)20302031if self.useClientSpec and not branchesDetected:2032# all files are relative to the client spec2033 self.clientPath =getClientRoot()2034else:2035 self.clientPath =p4Where(self.depotPath)20362037if self.clientPath =="":2038die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)20392040print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2041 self.oldWorkingDirectory = os.getcwd()20422043# ensure the clientPath exists2044 new_client_dir =False2045if not os.path.exists(self.clientPath):2046 new_client_dir =True2047 os.makedirs(self.clientPath)20482049chdir(self.clientPath, is_client_path=True)2050if self.dry_run:2051print"Would synchronize p4 checkout in%s"% self.clientPath2052else:2053print"Synchronizing p4 checkout..."2054if new_client_dir:2055# old one was destroyed, and maybe nobody told p42056p4_sync("...","-f")2057else:2058p4_sync("...")2059 self.check()20602061 commits = []2062if self.master:2063 commitish = self.master2064else:2065 commitish ='HEAD'20662067for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2068 commits.append(line.strip())2069 commits.reverse()20702071if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2072 self.checkAuthorship =False2073else:2074 self.checkAuthorship =True20752076if self.preserveUser:2077 self.checkValidP4Users(commits)20782079#2080# Build up a set of options to be passed to diff when2081# submitting each commit to p4.2082#2083if self.detectRenames:2084# command-line -M arg2085 self.diffOpts ="-M"2086else:2087# If not explicitly set check the config variable2088 detectRenames =gitConfig("git-p4.detectRenames")20892090if detectRenames.lower() =="false"or detectRenames =="":2091 self.diffOpts =""2092elif detectRenames.lower() =="true":2093 self.diffOpts ="-M"2094else:2095 self.diffOpts ="-M%s"% detectRenames20962097# no command-line arg for -C or --find-copies-harder, just2098# config variables2099 detectCopies =gitConfig("git-p4.detectCopies")2100if detectCopies.lower() =="false"or detectCopies =="":2101pass2102elif detectCopies.lower() =="true":2103 self.diffOpts +=" -C"2104else:2105 self.diffOpts +=" -C%s"% detectCopies21062107ifgitConfigBool("git-p4.detectCopiesHarder"):2108 self.diffOpts +=" --find-copies-harder"21092110#2111# Apply the commits, one at a time. On failure, ask if should2112# continue to try the rest of the patches, or quit.2113#2114if self.dry_run:2115print"Would apply"2116 applied = []2117 last =len(commits) -12118for i, commit inenumerate(commits):2119if self.dry_run:2120print" ",read_pipe(["git","show","-s",2121"--format=format:%h%s", commit])2122 ok =True2123else:2124 ok = self.applyCommit(commit)2125if ok:2126 applied.append(commit)2127else:2128if self.prepare_p4_only and i < last:2129print"Processing only the first commit due to option" \2130" --prepare-p4-only"2131break2132if i < last:2133 quit =False2134while True:2135# prompt for what to do, or use the option/variable2136if self.conflict_behavior =="ask":2137print"What do you want to do?"2138 response =raw_input("[s]kip this commit but apply"2139" the rest, or [q]uit? ")2140if not response:2141continue2142elif self.conflict_behavior =="skip":2143 response ="s"2144elif self.conflict_behavior =="quit":2145 response ="q"2146else:2147die("Unknown conflict_behavior '%s'"%2148 self.conflict_behavior)21492150if response[0] =="s":2151print"Skipping this commit, but applying the rest"2152break2153if response[0] =="q":2154print"Quitting"2155 quit =True2156break2157if quit:2158break21592160chdir(self.oldWorkingDirectory)2161 shelved_applied ="shelved"if self.shelve else"applied"2162if self.dry_run:2163pass2164elif self.prepare_p4_only:2165pass2166eliflen(commits) ==len(applied):2167print("All commits{0}!".format(shelved_applied))21682169 sync =P4Sync()2170if self.branch:2171 sync.branch = self.branch2172 sync.run([])21732174 rebase =P4Rebase()2175 rebase.rebase()21762177else:2178iflen(applied) ==0:2179print("No commits{0}.".format(shelved_applied))2180else:2181print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2182for c in commits:2183if c in applied:2184 star ="*"2185else:2186 star =" "2187print star,read_pipe(["git","show","-s",2188"--format=format:%h%s", c])2189print"You will have to do 'git p4 sync' and rebase."21902191ifgitConfigBool("git-p4.exportLabels"):2192 self.exportLabels =True21932194if self.exportLabels:2195 p4Labels =getP4Labels(self.depotPath)2196 gitTags =getGitTags()21972198 missingGitTags = gitTags - p4Labels2199 self.exportGitTags(missingGitTags)22002201# exit with error unless everything applied perfectly2202iflen(commits) !=len(applied):2203 sys.exit(1)22042205return True22062207classView(object):2208"""Represent a p4 view ("p4 help views"), and map files in a2209 repo according to the view."""22102211def__init__(self, client_name):2212 self.mappings = []2213 self.client_prefix ="//%s/"% client_name2214# cache results of "p4 where" to lookup client file locations2215 self.client_spec_path_cache = {}22162217defappend(self, view_line):2218"""Parse a view line, splitting it into depot and client2219 sides. Append to self.mappings, preserving order. This2220 is only needed for tag creation."""22212222# Split the view line into exactly two words. P4 enforces2223# structure on these lines that simplifies this quite a bit.2224#2225# Either or both words may be double-quoted.2226# Single quotes do not matter.2227# Double-quote marks cannot occur inside the words.2228# A + or - prefix is also inside the quotes.2229# There are no quotes unless they contain a space.2230# The line is already white-space stripped.2231# The two words are separated by a single space.2232#2233if view_line[0] =='"':2234# First word is double quoted. Find its end.2235 close_quote_index = view_line.find('"',1)2236if close_quote_index <=0:2237die("No first-word closing quote found:%s"% view_line)2238 depot_side = view_line[1:close_quote_index]2239# skip closing quote and space2240 rhs_index = close_quote_index +1+12241else:2242 space_index = view_line.find(" ")2243if space_index <=0:2244die("No word-splitting space found:%s"% view_line)2245 depot_side = view_line[0:space_index]2246 rhs_index = space_index +122472248# prefix + means overlay on previous mapping2249if depot_side.startswith("+"):2250 depot_side = depot_side[1:]22512252# prefix - means exclude this path, leave out of mappings2253 exclude =False2254if depot_side.startswith("-"):2255 exclude =True2256 depot_side = depot_side[1:]22572258if not exclude:2259 self.mappings.append(depot_side)22602261defconvert_client_path(self, clientFile):2262# chop off //client/ part to make it relative2263if not clientFile.startswith(self.client_prefix):2264die("No prefix '%s' on clientFile '%s'"%2265(self.client_prefix, clientFile))2266return clientFile[len(self.client_prefix):]22672268defupdate_client_spec_path_cache(self, files):2269""" Caching file paths by "p4 where" batch query """22702271# List depot file paths exclude that already cached2272 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]22732274iflen(fileArgs) ==0:2275return# All files in cache22762277 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2278for res in where_result:2279if"code"in res and res["code"] =="error":2280# assume error is "... file(s) not in client view"2281continue2282if"clientFile"not in res:2283die("No clientFile in 'p4 where' output")2284if"unmap"in res:2285# it will list all of them, but only one not unmap-ped2286continue2287ifgitConfigBool("core.ignorecase"):2288 res['depotFile'] = res['depotFile'].lower()2289 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])22902291# not found files or unmap files set to ""2292for depotFile in fileArgs:2293ifgitConfigBool("core.ignorecase"):2294 depotFile = depotFile.lower()2295if depotFile not in self.client_spec_path_cache:2296 self.client_spec_path_cache[depotFile] =""22972298defmap_in_client(self, depot_path):2299"""Return the relative location in the client where this2300 depot file should live. Returns "" if the file should2301 not be mapped in the client."""23022303ifgitConfigBool("core.ignorecase"):2304 depot_path = depot_path.lower()23052306if depot_path in self.client_spec_path_cache:2307return self.client_spec_path_cache[depot_path]23082309die("Error:%sis not found in client spec path"% depot_path )2310return""23112312classP4Sync(Command, P4UserMap):2313 delete_actions = ("delete","move/delete","purge")23142315def__init__(self):2316 Command.__init__(self)2317 P4UserMap.__init__(self)2318 self.options = [2319 optparse.make_option("--branch", dest="branch"),2320 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2321 optparse.make_option("--changesfile", dest="changesFile"),2322 optparse.make_option("--silent", dest="silent", action="store_true"),2323 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2324 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2325 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2326help="Import into refs/heads/ , not refs/remotes"),2327 optparse.make_option("--max-changes", dest="maxChanges",2328help="Maximum number of changes to import"),2329 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2330help="Internal block size to use when iteratively calling p4 changes"),2331 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2332help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2333 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2334help="Only sync files that are included in the Perforce Client Spec"),2335 optparse.make_option("-/", dest="cloneExclude",2336 action="append",type="string",2337help="exclude depot path"),2338]2339 self.description ="""Imports from Perforce into a git repository.\n2340 example:2341 //depot/my/project/ -- to import the current head2342 //depot/my/project/@all -- to import everything2343 //depot/my/project/@1,6 -- to import only from revision 1 to 623442345 (a ... is not needed in the path p4 specification, it's added implicitly)"""23462347 self.usage +=" //depot/path[@revRange]"2348 self.silent =False2349 self.createdBranches =set()2350 self.committedChanges =set()2351 self.branch =""2352 self.detectBranches =False2353 self.detectLabels =False2354 self.importLabels =False2355 self.changesFile =""2356 self.syncWithOrigin =True2357 self.importIntoRemotes =True2358 self.maxChanges =""2359 self.changes_block_size =None2360 self.keepRepoPath =False2361 self.depotPaths =None2362 self.p4BranchesInGit = []2363 self.cloneExclude = []2364 self.useClientSpec =False2365 self.useClientSpec_from_options =False2366 self.clientSpecDirs =None2367 self.tempBranches = []2368 self.tempBranchLocation ="refs/git-p4-tmp"2369 self.largeFileSystem =None23702371ifgitConfig('git-p4.largeFileSystem'):2372 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2373 self.largeFileSystem =largeFileSystemConstructor(2374lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2375)23762377ifgitConfig("git-p4.syncFromOrigin") =="false":2378 self.syncWithOrigin =False23792380# This is required for the "append" cloneExclude action2381defensure_value(self, attr, value):2382if nothasattr(self, attr)orgetattr(self, attr)is None:2383setattr(self, attr, value)2384returngetattr(self, attr)23852386# Force a checkpoint in fast-import and wait for it to finish2387defcheckpoint(self):2388 self.gitStream.write("checkpoint\n\n")2389 self.gitStream.write("progress checkpoint\n\n")2390 out = self.gitOutput.readline()2391if self.verbose:2392print"checkpoint finished: "+ out23932394defextractFilesFromCommit(self, commit):2395 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2396for path in self.cloneExclude]2397 files = []2398 fnum =02399while commit.has_key("depotFile%s"% fnum):2400 path = commit["depotFile%s"% fnum]24012402if[p for p in self.cloneExclude2403ifp4PathStartsWith(path, p)]:2404 found =False2405else:2406 found = [p for p in self.depotPaths2407ifp4PathStartsWith(path, p)]2408if not found:2409 fnum = fnum +12410continue24112412file= {}2413file["path"] = path2414file["rev"] = commit["rev%s"% fnum]2415file["action"] = commit["action%s"% fnum]2416file["type"] = commit["type%s"% fnum]2417 files.append(file)2418 fnum = fnum +12419return files24202421defextractJobsFromCommit(self, commit):2422 jobs = []2423 jnum =02424while commit.has_key("job%s"% jnum):2425 job = commit["job%s"% jnum]2426 jobs.append(job)2427 jnum = jnum +12428return jobs24292430defstripRepoPath(self, path, prefixes):2431"""When streaming files, this is called to map a p4 depot path2432 to where it should go in git. The prefixes are either2433 self.depotPaths, or self.branchPrefixes in the case of2434 branch detection."""24352436if self.useClientSpec:2437# branch detection moves files up a level (the branch name)2438# from what client spec interpretation gives2439 path = self.clientSpecDirs.map_in_client(path)2440if self.detectBranches:2441for b in self.knownBranches:2442if path.startswith(b +"/"):2443 path = path[len(b)+1:]24442445elif self.keepRepoPath:2446# Preserve everything in relative path name except leading2447# //depot/; just look at first prefix as they all should2448# be in the same depot.2449 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2450ifp4PathStartsWith(path, depot):2451 path = path[len(depot):]24522453else:2454for p in prefixes:2455ifp4PathStartsWith(path, p):2456 path = path[len(p):]2457break24582459 path =wildcard_decode(path)2460return path24612462defsplitFilesIntoBranches(self, commit):2463"""Look at each depotFile in the commit to figure out to what2464 branch it belongs."""24652466if self.clientSpecDirs:2467 files = self.extractFilesFromCommit(commit)2468 self.clientSpecDirs.update_client_spec_path_cache(files)24692470 branches = {}2471 fnum =02472while commit.has_key("depotFile%s"% fnum):2473 path = commit["depotFile%s"% fnum]2474 found = [p for p in self.depotPaths2475ifp4PathStartsWith(path, p)]2476if not found:2477 fnum = fnum +12478continue24792480file= {}2481file["path"] = path2482file["rev"] = commit["rev%s"% fnum]2483file["action"] = commit["action%s"% fnum]2484file["type"] = commit["type%s"% fnum]2485 fnum = fnum +124862487# start with the full relative path where this file would2488# go in a p4 client2489if self.useClientSpec:2490 relPath = self.clientSpecDirs.map_in_client(path)2491else:2492 relPath = self.stripRepoPath(path, self.depotPaths)24932494for branch in self.knownBranches.keys():2495# add a trailing slash so that a commit into qt/4.2foo2496# doesn't end up in qt/4.2, e.g.2497if relPath.startswith(branch +"/"):2498if branch not in branches:2499 branches[branch] = []2500 branches[branch].append(file)2501break25022503return branches25042505defwriteToGitStream(self, gitMode, relPath, contents):2506 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2507 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2508for d in contents:2509 self.gitStream.write(d)2510 self.gitStream.write('\n')25112512defencodeWithUTF8(self, path):2513try:2514 path.decode('ascii')2515except:2516 encoding ='utf8'2517ifgitConfig('git-p4.pathEncoding'):2518 encoding =gitConfig('git-p4.pathEncoding')2519 path = path.decode(encoding,'replace').encode('utf8','replace')2520if self.verbose:2521print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2522return path25232524# output one file from the P4 stream2525# - helper for streamP4Files25262527defstreamOneP4File(self,file, contents):2528 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2529 relPath = self.encodeWithUTF8(relPath)2530if verbose:2531 size =int(self.stream_file['fileSize'])2532 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2533 sys.stdout.flush()25342535(type_base, type_mods) =split_p4_type(file["type"])25362537 git_mode ="100644"2538if"x"in type_mods:2539 git_mode ="100755"2540if type_base =="symlink":2541 git_mode ="120000"2542# p4 print on a symlink sometimes contains "target\n";2543# if it does, remove the newline2544 data =''.join(contents)2545if not data:2546# Some version of p4 allowed creating a symlink that pointed2547# to nothing. This causes p4 errors when checking out such2548# a change, and errors here too. Work around it by ignoring2549# the bad symlink; hopefully a future change fixes it.2550print"\nIgnoring empty symlink in%s"%file['depotFile']2551return2552elif data[-1] =='\n':2553 contents = [data[:-1]]2554else:2555 contents = [data]25562557if type_base =="utf16":2558# p4 delivers different text in the python output to -G2559# than it does when using "print -o", or normal p4 client2560# operations. utf16 is converted to ascii or utf8, perhaps.2561# But ascii text saved as -t utf16 is completely mangled.2562# Invoke print -o to get the real contents.2563#2564# On windows, the newlines will always be mangled by print, so put2565# them back too. This is not needed to the cygwin windows version,2566# just the native "NT" type.2567#2568try:2569 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2570exceptExceptionas e:2571if'Translation of file content failed'instr(e):2572 type_base ='binary'2573else:2574raise e2575else:2576ifp4_version_string().find('/NT') >=0:2577 text = text.replace('\r\n','\n')2578 contents = [ text ]25792580if type_base =="apple":2581# Apple filetype files will be streamed as a concatenation of2582# its appledouble header and the contents. This is useless2583# on both macs and non-macs. If using "print -q -o xx", it2584# will create "xx" with the data, and "%xx" with the header.2585# This is also not very useful.2586#2587# Ideally, someday, this script can learn how to generate2588# appledouble files directly and import those to git, but2589# non-mac machines can never find a use for apple filetype.2590print"\nIgnoring apple filetype file%s"%file['depotFile']2591return25922593# Note that we do not try to de-mangle keywords on utf16 files,2594# even though in theory somebody may want that.2595 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2596if pattern:2597 regexp = re.compile(pattern, re.VERBOSE)2598 text =''.join(contents)2599 text = regexp.sub(r'$\1$', text)2600 contents = [ text ]26012602if self.largeFileSystem:2603(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)26042605 self.writeToGitStream(git_mode, relPath, contents)26062607defstreamOneP4Deletion(self,file):2608 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2609 relPath = self.encodeWithUTF8(relPath)2610if verbose:2611 sys.stdout.write("delete%s\n"% relPath)2612 sys.stdout.flush()2613 self.gitStream.write("D%s\n"% relPath)26142615if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2616 self.largeFileSystem.removeLargeFile(relPath)26172618# handle another chunk of streaming data2619defstreamP4FilesCb(self, marshalled):26202621# catch p4 errors and complain2622 err =None2623if"code"in marshalled:2624if marshalled["code"] =="error":2625if"data"in marshalled:2626 err = marshalled["data"].rstrip()26272628if not err and'fileSize'in self.stream_file:2629 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2630if required_bytes >0:2631 err ='Not enough space left on%s! Free at least%iMB.'% (2632 os.getcwd(), required_bytes/1024/10242633)26342635if err:2636 f =None2637if self.stream_have_file_info:2638if"depotFile"in self.stream_file:2639 f = self.stream_file["depotFile"]2640# force a failure in fast-import, else an empty2641# commit will be made2642 self.gitStream.write("\n")2643 self.gitStream.write("die-now\n")2644 self.gitStream.close()2645# ignore errors, but make sure it exits first2646 self.importProcess.wait()2647if f:2648die("Error from p4 print for%s:%s"% (f, err))2649else:2650die("Error from p4 print:%s"% err)26512652if marshalled.has_key('depotFile')and self.stream_have_file_info:2653# start of a new file - output the old one first2654 self.streamOneP4File(self.stream_file, self.stream_contents)2655 self.stream_file = {}2656 self.stream_contents = []2657 self.stream_have_file_info =False26582659# pick up the new file information... for the2660# 'data' field we need to append to our array2661for k in marshalled.keys():2662if k =='data':2663if'streamContentSize'not in self.stream_file:2664 self.stream_file['streamContentSize'] =02665 self.stream_file['streamContentSize'] +=len(marshalled['data'])2666 self.stream_contents.append(marshalled['data'])2667else:2668 self.stream_file[k] = marshalled[k]26692670if(verbose and2671'streamContentSize'in self.stream_file and2672'fileSize'in self.stream_file and2673'depotFile'in self.stream_file):2674 size =int(self.stream_file["fileSize"])2675if size >0:2676 progress =100*self.stream_file['streamContentSize']/size2677 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2678 sys.stdout.flush()26792680 self.stream_have_file_info =True26812682# Stream directly from "p4 files" into "git fast-import"2683defstreamP4Files(self, files):2684 filesForCommit = []2685 filesToRead = []2686 filesToDelete = []26872688for f in files:2689 filesForCommit.append(f)2690if f['action']in self.delete_actions:2691 filesToDelete.append(f)2692else:2693 filesToRead.append(f)26942695# deleted files...2696for f in filesToDelete:2697 self.streamOneP4Deletion(f)26982699iflen(filesToRead) >0:2700 self.stream_file = {}2701 self.stream_contents = []2702 self.stream_have_file_info =False27032704# curry self argument2705defstreamP4FilesCbSelf(entry):2706 self.streamP4FilesCb(entry)27072708 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]27092710p4CmdList(["-x","-","print"],2711 stdin=fileArgs,2712 cb=streamP4FilesCbSelf)27132714# do the last chunk2715if self.stream_file.has_key('depotFile'):2716 self.streamOneP4File(self.stream_file, self.stream_contents)27172718defmake_email(self, userid):2719if userid in self.users:2720return self.users[userid]2721else:2722return"%s<a@b>"% userid27232724defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2725""" Stream a p4 tag.2726 commit is either a git commit, or a fast-import mark, ":<p4commit>"2727 """27282729if verbose:2730print"writing tag%sfor commit%s"% (labelName, commit)2731 gitStream.write("tag%s\n"% labelName)2732 gitStream.write("from%s\n"% commit)27332734if labelDetails.has_key('Owner'):2735 owner = labelDetails["Owner"]2736else:2737 owner =None27382739# Try to use the owner of the p4 label, or failing that,2740# the current p4 user id.2741if owner:2742 email = self.make_email(owner)2743else:2744 email = self.make_email(self.p4UserId())2745 tagger ="%s %s %s"% (email, epoch, self.tz)27462747 gitStream.write("tagger%s\n"% tagger)27482749print"labelDetails=",labelDetails2750if labelDetails.has_key('Description'):2751 description = labelDetails['Description']2752else:2753 description ='Label from git p4'27542755 gitStream.write("data%d\n"%len(description))2756 gitStream.write(description)2757 gitStream.write("\n")27582759definClientSpec(self, path):2760if not self.clientSpecDirs:2761return True2762 inClientSpec = self.clientSpecDirs.map_in_client(path)2763if not inClientSpec and self.verbose:2764print('Ignoring file outside of client spec:{0}'.format(path))2765return inClientSpec27662767defhasBranchPrefix(self, path):2768if not self.branchPrefixes:2769return True2770 hasPrefix = [p for p in self.branchPrefixes2771ifp4PathStartsWith(path, p)]2772if not hasPrefix and self.verbose:2773print('Ignoring file outside of prefix:{0}'.format(path))2774return hasPrefix27752776defcommit(self, details, files, branch, parent =""):2777 epoch = details["time"]2778 author = details["user"]2779 jobs = self.extractJobsFromCommit(details)27802781if self.verbose:2782print('commit into{0}'.format(branch))27832784if self.clientSpecDirs:2785 self.clientSpecDirs.update_client_spec_path_cache(files)27862787 files = [f for f in files2788if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]27892790if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2791print('Ignoring revision{0}as it would produce an empty commit.'2792.format(details['change']))2793return27942795 self.gitStream.write("commit%s\n"% branch)2796 self.gitStream.write("mark :%s\n"% details["change"])2797 self.committedChanges.add(int(details["change"]))2798 committer =""2799if author not in self.users:2800 self.getUserMapFromPerforceServer()2801 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)28022803 self.gitStream.write("committer%s\n"% committer)28042805 self.gitStream.write("data <<EOT\n")2806 self.gitStream.write(details["desc"])2807iflen(jobs) >0:2808 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2809 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2810(','.join(self.branchPrefixes), details["change"]))2811iflen(details['options']) >0:2812 self.gitStream.write(": options =%s"% details['options'])2813 self.gitStream.write("]\nEOT\n\n")28142815iflen(parent) >0:2816if self.verbose:2817print"parent%s"% parent2818 self.gitStream.write("from%s\n"% parent)28192820 self.streamP4Files(files)2821 self.gitStream.write("\n")28222823 change =int(details["change"])28242825if self.labels.has_key(change):2826 label = self.labels[change]2827 labelDetails = label[0]2828 labelRevisions = label[1]2829if self.verbose:2830print"Change%sis labelled%s"% (change, labelDetails)28312832 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2833for p in self.branchPrefixes])28342835iflen(files) ==len(labelRevisions):28362837 cleanedFiles = {}2838for info in files:2839if info["action"]in self.delete_actions:2840continue2841 cleanedFiles[info["depotFile"]] = info["rev"]28422843if cleanedFiles == labelRevisions:2844 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)28452846else:2847if not self.silent:2848print("Tag%sdoes not match with change%s: files do not match."2849% (labelDetails["label"], change))28502851else:2852if not self.silent:2853print("Tag%sdoes not match with change%s: file count is different."2854% (labelDetails["label"], change))28552856# Build a dictionary of changelists and labels, for "detect-labels" option.2857defgetLabels(self):2858 self.labels = {}28592860 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2861iflen(l) >0and not self.silent:2862print"Finding files belonging to labels in%s"% `self.depotPaths`28632864for output in l:2865 label = output["label"]2866 revisions = {}2867 newestChange =02868if self.verbose:2869print"Querying files for label%s"% label2870forfileinp4CmdList(["files"] +2871["%s...@%s"% (p, label)2872for p in self.depotPaths]):2873 revisions[file["depotFile"]] =file["rev"]2874 change =int(file["change"])2875if change > newestChange:2876 newestChange = change28772878 self.labels[newestChange] = [output, revisions]28792880if self.verbose:2881print"Label changes:%s"% self.labels.keys()28822883# Import p4 labels as git tags. A direct mapping does not2884# exist, so assume that if all the files are at the same revision2885# then we can use that, or it's something more complicated we should2886# just ignore.2887defimportP4Labels(self, stream, p4Labels):2888if verbose:2889print"import p4 labels: "+' '.join(p4Labels)28902891 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2892 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2893iflen(validLabelRegexp) ==0:2894 validLabelRegexp = defaultLabelRegexp2895 m = re.compile(validLabelRegexp)28962897for name in p4Labels:2898 commitFound =False28992900if not m.match(name):2901if verbose:2902print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2903continue29042905if name in ignoredP4Labels:2906continue29072908 labelDetails =p4CmdList(['label',"-o", name])[0]29092910# get the most recent changelist for each file in this label2911 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2912for p in self.depotPaths])29132914if change.has_key('change'):2915# find the corresponding git commit; take the oldest commit2916 changelist =int(change['change'])2917if changelist in self.committedChanges:2918 gitCommit =":%d"% changelist # use a fast-import mark2919 commitFound =True2920else:2921 gitCommit =read_pipe(["git","rev-list","--max-count=1",2922"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2923iflen(gitCommit) ==0:2924print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2925else:2926 commitFound =True2927 gitCommit = gitCommit.strip()29282929if commitFound:2930# Convert from p4 time format2931try:2932 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2933exceptValueError:2934print"Could not convert label time%s"% labelDetails['Update']2935 tmwhen =129362937 when =int(time.mktime(tmwhen))2938 self.streamTag(stream, name, labelDetails, gitCommit, when)2939if verbose:2940print"p4 label%smapped to git commit%s"% (name, gitCommit)2941else:2942if verbose:2943print"Label%shas no changelists - possibly deleted?"% name29442945if not commitFound:2946# We can't import this label; don't try again as it will get very2947# expensive repeatedly fetching all the files for labels that will2948# never be imported. If the label is moved in the future, the2949# ignore will need to be removed manually.2950system(["git","config","--add","git-p4.ignoredP4Labels", name])29512952defguessProjectName(self):2953for p in self.depotPaths:2954if p.endswith("/"):2955 p = p[:-1]2956 p = p[p.strip().rfind("/") +1:]2957if not p.endswith("/"):2958 p +="/"2959return p29602961defgetBranchMapping(self):2962 lostAndFoundBranches =set()29632964 user =gitConfig("git-p4.branchUser")2965iflen(user) >0:2966 command ="branches -u%s"% user2967else:2968 command ="branches"29692970for info inp4CmdList(command):2971 details =p4Cmd(["branch","-o", info["branch"]])2972 viewIdx =02973while details.has_key("View%s"% viewIdx):2974 paths = details["View%s"% viewIdx].split(" ")2975 viewIdx = viewIdx +12976# require standard //depot/foo/... //depot/bar/... mapping2977iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2978continue2979 source = paths[0]2980 destination = paths[1]2981## HACK2982ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2983 source = source[len(self.depotPaths[0]):-4]2984 destination = destination[len(self.depotPaths[0]):-4]29852986if destination in self.knownBranches:2987if not self.silent:2988print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2989print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2990continue29912992 self.knownBranches[destination] = source29932994 lostAndFoundBranches.discard(destination)29952996if source not in self.knownBranches:2997 lostAndFoundBranches.add(source)29982999# Perforce does not strictly require branches to be defined, so we also3000# check git config for a branch list.3001#3002# Example of branch definition in git config file:3003# [git-p4]3004# branchList=main:branchA3005# branchList=main:branchB3006# branchList=branchA:branchC3007 configBranches =gitConfigList("git-p4.branchList")3008for branch in configBranches:3009if branch:3010(source, destination) = branch.split(":")3011 self.knownBranches[destination] = source30123013 lostAndFoundBranches.discard(destination)30143015if source not in self.knownBranches:3016 lostAndFoundBranches.add(source)301730183019for branch in lostAndFoundBranches:3020 self.knownBranches[branch] = branch30213022defgetBranchMappingFromGitBranches(self):3023 branches =p4BranchesInGit(self.importIntoRemotes)3024for branch in branches.keys():3025if branch =="master":3026 branch ="main"3027else:3028 branch = branch[len(self.projectName):]3029 self.knownBranches[branch] = branch30303031defupdateOptionDict(self, d):3032 option_keys = {}3033if self.keepRepoPath:3034 option_keys['keepRepoPath'] =130353036 d["options"] =' '.join(sorted(option_keys.keys()))30373038defreadOptions(self, d):3039 self.keepRepoPath = (d.has_key('options')3040and('keepRepoPath'in d['options']))30413042defgitRefForBranch(self, branch):3043if branch =="main":3044return self.refPrefix +"master"30453046iflen(branch) <=0:3047return branch30483049return self.refPrefix + self.projectName + branch30503051defgitCommitByP4Change(self, ref, change):3052if self.verbose:3053print"looking in ref "+ ref +" for change%susing bisect..."% change30543055 earliestCommit =""3056 latestCommit =parseRevision(ref)30573058while True:3059if self.verbose:3060print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3061 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3062iflen(next) ==0:3063if self.verbose:3064print"argh"3065return""3066 log =extractLogMessageFromGitCommit(next)3067 settings =extractSettingsGitLog(log)3068 currentChange =int(settings['change'])3069if self.verbose:3070print"current change%s"% currentChange30713072if currentChange == change:3073if self.verbose:3074print"found%s"% next3075return next30763077if currentChange < change:3078 earliestCommit ="^%s"% next3079else:3080 latestCommit ="%s"% next30813082return""30833084defimportNewBranch(self, branch, maxChange):3085# make fast-import flush all changes to disk and update the refs using the checkpoint3086# command so that we can try to find the branch parent in the git history3087 self.gitStream.write("checkpoint\n\n");3088 self.gitStream.flush();3089 branchPrefix = self.depotPaths[0] + branch +"/"3090range="@1,%s"% maxChange3091#print "prefix" + branchPrefix3092 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3093iflen(changes) <=0:3094return False3095 firstChange = changes[0]3096#print "first change in branch: %s" % firstChange3097 sourceBranch = self.knownBranches[branch]3098 sourceDepotPath = self.depotPaths[0] + sourceBranch3099 sourceRef = self.gitRefForBranch(sourceBranch)3100#print "source " + sourceBranch31013102 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3103#print "branch parent: %s" % branchParentChange3104 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3105iflen(gitParent) >0:3106 self.initialParents[self.gitRefForBranch(branch)] = gitParent3107#print "parent git commit: %s" % gitParent31083109 self.importChanges(changes)3110return True31113112defsearchParent(self, parent, branch, target):3113 parentFound =False3114for blob inread_pipe_lines(["git","rev-list","--reverse",3115"--no-merges", parent]):3116 blob = blob.strip()3117iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3118 parentFound =True3119if self.verbose:3120print"Found parent of%sin commit%s"% (branch, blob)3121break3122if parentFound:3123return blob3124else:3125return None31263127defimportChanges(self, changes):3128 cnt =13129for change in changes:3130 description =p4_describe(change)3131 self.updateOptionDict(description)31323133if not self.silent:3134 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3135 sys.stdout.flush()3136 cnt = cnt +131373138try:3139if self.detectBranches:3140 branches = self.splitFilesIntoBranches(description)3141for branch in branches.keys():3142## HACK --hwn3143 branchPrefix = self.depotPaths[0] + branch +"/"3144 self.branchPrefixes = [ branchPrefix ]31453146 parent =""31473148 filesForCommit = branches[branch]31493150if self.verbose:3151print"branch is%s"% branch31523153 self.updatedBranches.add(branch)31543155if branch not in self.createdBranches:3156 self.createdBranches.add(branch)3157 parent = self.knownBranches[branch]3158if parent == branch:3159 parent =""3160else:3161 fullBranch = self.projectName + branch3162if fullBranch not in self.p4BranchesInGit:3163if not self.silent:3164print("\nImporting new branch%s"% fullBranch);3165if self.importNewBranch(branch, change -1):3166 parent =""3167 self.p4BranchesInGit.append(fullBranch)3168if not self.silent:3169print("\nResuming with change%s"% change);31703171if self.verbose:3172print"parent determined through known branches:%s"% parent31733174 branch = self.gitRefForBranch(branch)3175 parent = self.gitRefForBranch(parent)31763177if self.verbose:3178print"looking for initial parent for%s; current parent is%s"% (branch, parent)31793180iflen(parent) ==0and branch in self.initialParents:3181 parent = self.initialParents[branch]3182del self.initialParents[branch]31833184 blob =None3185iflen(parent) >0:3186 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3187if self.verbose:3188print"Creating temporary branch: "+ tempBranch3189 self.commit(description, filesForCommit, tempBranch)3190 self.tempBranches.append(tempBranch)3191 self.checkpoint()3192 blob = self.searchParent(parent, branch, tempBranch)3193if blob:3194 self.commit(description, filesForCommit, branch, blob)3195else:3196if self.verbose:3197print"Parent of%snot found. Committing into head of%s"% (branch, parent)3198 self.commit(description, filesForCommit, branch, parent)3199else:3200 files = self.extractFilesFromCommit(description)3201 self.commit(description, files, self.branch,3202 self.initialParent)3203# only needed once, to connect to the previous commit3204 self.initialParent =""3205exceptIOError:3206print self.gitError.read()3207 sys.exit(1)32083209defimportHeadRevision(self, revision):3210print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)32113212 details = {}3213 details["user"] ="git perforce import user"3214 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3215% (' '.join(self.depotPaths), revision))3216 details["change"] = revision3217 newestRevision =032183219 fileCnt =03220 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]32213222for info inp4CmdList(["files"] + fileArgs):32233224if'code'in info and info['code'] =='error':3225 sys.stderr.write("p4 returned an error:%s\n"3226% info['data'])3227if info['data'].find("must refer to client") >=0:3228 sys.stderr.write("This particular p4 error is misleading.\n")3229 sys.stderr.write("Perhaps the depot path was misspelled.\n");3230 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3231 sys.exit(1)3232if'p4ExitCode'in info:3233 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3234 sys.exit(1)323532363237 change =int(info["change"])3238if change > newestRevision:3239 newestRevision = change32403241if info["action"]in self.delete_actions:3242# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3243#fileCnt = fileCnt + 13244continue32453246for prop in["depotFile","rev","action","type"]:3247 details["%s%s"% (prop, fileCnt)] = info[prop]32483249 fileCnt = fileCnt +132503251 details["change"] = newestRevision32523253# Use time from top-most change so that all git p4 clones of3254# the same p4 repo have the same commit SHA1s.3255 res =p4_describe(newestRevision)3256 details["time"] = res["time"]32573258 self.updateOptionDict(details)3259try:3260 self.commit(details, self.extractFilesFromCommit(details), self.branch)3261exceptIOError:3262print"IO error with git fast-import. Is your git version recent enough?"3263print self.gitError.read()326432653266defrun(self, args):3267 self.depotPaths = []3268 self.changeRange =""3269 self.previousDepotPaths = []3270 self.hasOrigin =False32713272# map from branch depot path to parent branch3273 self.knownBranches = {}3274 self.initialParents = {}32753276if self.importIntoRemotes:3277 self.refPrefix ="refs/remotes/p4/"3278else:3279 self.refPrefix ="refs/heads/p4/"32803281if self.syncWithOrigin:3282 self.hasOrigin =originP4BranchesExist()3283if self.hasOrigin:3284if not self.silent:3285print'Syncing with origin first, using "git fetch origin"'3286system("git fetch origin")32873288 branch_arg_given =bool(self.branch)3289iflen(self.branch) ==0:3290 self.branch = self.refPrefix +"master"3291ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3292system("git update-ref%srefs/heads/p4"% self.branch)3293system("git branch -D p4")32943295# accept either the command-line option, or the configuration variable3296if self.useClientSpec:3297# will use this after clone to set the variable3298 self.useClientSpec_from_options =True3299else:3300ifgitConfigBool("git-p4.useclientspec"):3301 self.useClientSpec =True3302if self.useClientSpec:3303 self.clientSpecDirs =getClientSpec()33043305# TODO: should always look at previous commits,3306# merge with previous imports, if possible.3307if args == []:3308if self.hasOrigin:3309createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)33103311# branches holds mapping from branch name to sha13312 branches =p4BranchesInGit(self.importIntoRemotes)33133314# restrict to just this one, disabling detect-branches3315if branch_arg_given:3316 short = self.branch.split("/")[-1]3317if short in branches:3318 self.p4BranchesInGit = [ short ]3319else:3320 self.p4BranchesInGit = branches.keys()33213322iflen(self.p4BranchesInGit) >1:3323if not self.silent:3324print"Importing from/into multiple branches"3325 self.detectBranches =True3326for branch in branches.keys():3327 self.initialParents[self.refPrefix + branch] = \3328 branches[branch]33293330if self.verbose:3331print"branches:%s"% self.p4BranchesInGit33323333 p4Change =03334for branch in self.p4BranchesInGit:3335 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)33363337 settings =extractSettingsGitLog(logMsg)33383339 self.readOptions(settings)3340if(settings.has_key('depot-paths')3341and settings.has_key('change')):3342 change =int(settings['change']) +13343 p4Change =max(p4Change, change)33443345 depotPaths =sorted(settings['depot-paths'])3346if self.previousDepotPaths == []:3347 self.previousDepotPaths = depotPaths3348else:3349 paths = []3350for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3351 prev_list = prev.split("/")3352 cur_list = cur.split("/")3353for i inrange(0,min(len(cur_list),len(prev_list))):3354if cur_list[i] <> prev_list[i]:3355 i = i -13356break33573358 paths.append("/".join(cur_list[:i +1]))33593360 self.previousDepotPaths = paths33613362if p4Change >0:3363 self.depotPaths =sorted(self.previousDepotPaths)3364 self.changeRange ="@%s,#head"% p4Change3365if not self.silent and not self.detectBranches:3366print"Performing incremental import into%sgit branch"% self.branch33673368# accept multiple ref name abbreviations:3369# refs/foo/bar/branch -> use it exactly3370# p4/branch -> prepend refs/remotes/ or refs/heads/3371# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3372if not self.branch.startswith("refs/"):3373if self.importIntoRemotes:3374 prepend ="refs/remotes/"3375else:3376 prepend ="refs/heads/"3377if not self.branch.startswith("p4/"):3378 prepend +="p4/"3379 self.branch = prepend + self.branch33803381iflen(args) ==0and self.depotPaths:3382if not self.silent:3383print"Depot paths:%s"%' '.join(self.depotPaths)3384else:3385if self.depotPaths and self.depotPaths != args:3386print("previous import used depot path%sand now%swas specified. "3387"This doesn't work!"% (' '.join(self.depotPaths),3388' '.join(args)))3389 sys.exit(1)33903391 self.depotPaths =sorted(args)33923393 revision =""3394 self.users = {}33953396# Make sure no revision specifiers are used when --changesfile3397# is specified.3398 bad_changesfile =False3399iflen(self.changesFile) >0:3400for p in self.depotPaths:3401if p.find("@") >=0or p.find("#") >=0:3402 bad_changesfile =True3403break3404if bad_changesfile:3405die("Option --changesfile is incompatible with revision specifiers")34063407 newPaths = []3408for p in self.depotPaths:3409if p.find("@") != -1:3410 atIdx = p.index("@")3411 self.changeRange = p[atIdx:]3412if self.changeRange =="@all":3413 self.changeRange =""3414elif','not in self.changeRange:3415 revision = self.changeRange3416 self.changeRange =""3417 p = p[:atIdx]3418elif p.find("#") != -1:3419 hashIdx = p.index("#")3420 revision = p[hashIdx:]3421 p = p[:hashIdx]3422elif self.previousDepotPaths == []:3423# pay attention to changesfile, if given, else import3424# the entire p4 tree at the head revision3425iflen(self.changesFile) ==0:3426 revision ="#head"34273428 p = re.sub("\.\.\.$","", p)3429if not p.endswith("/"):3430 p +="/"34313432 newPaths.append(p)34333434 self.depotPaths = newPaths34353436# --detect-branches may change this for each branch3437 self.branchPrefixes = self.depotPaths34383439 self.loadUserMapFromCache()3440 self.labels = {}3441if self.detectLabels:3442 self.getLabels();34433444if self.detectBranches:3445## FIXME - what's a P4 projectName ?3446 self.projectName = self.guessProjectName()34473448if self.hasOrigin:3449 self.getBranchMappingFromGitBranches()3450else:3451 self.getBranchMapping()3452if self.verbose:3453print"p4-git branches:%s"% self.p4BranchesInGit3454print"initial parents:%s"% self.initialParents3455for b in self.p4BranchesInGit:3456if b !="master":34573458## FIXME3459 b = b[len(self.projectName):]3460 self.createdBranches.add(b)34613462 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))34633464 self.importProcess = subprocess.Popen(["git","fast-import"],3465 stdin=subprocess.PIPE,3466 stdout=subprocess.PIPE,3467 stderr=subprocess.PIPE);3468 self.gitOutput = self.importProcess.stdout3469 self.gitStream = self.importProcess.stdin3470 self.gitError = self.importProcess.stderr34713472if revision:3473 self.importHeadRevision(revision)3474else:3475 changes = []34763477iflen(self.changesFile) >0:3478 output =open(self.changesFile).readlines()3479 changeSet =set()3480for line in output:3481 changeSet.add(int(line))34823483for change in changeSet:3484 changes.append(change)34853486 changes.sort()3487else:3488# catch "git p4 sync" with no new branches, in a repo that3489# does not have any existing p4 branches3490iflen(args) ==0:3491if not self.p4BranchesInGit:3492die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")34933494# The default branch is master, unless --branch is used to3495# specify something else. Make sure it exists, or complain3496# nicely about how to use --branch.3497if not self.detectBranches:3498if notbranch_exists(self.branch):3499if branch_arg_given:3500die("Error: branch%sdoes not exist."% self.branch)3501else:3502die("Error: no branch%s; perhaps specify one with --branch."%3503 self.branch)35043505if self.verbose:3506print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3507 self.changeRange)3508 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)35093510iflen(self.maxChanges) >0:3511 changes = changes[:min(int(self.maxChanges),len(changes))]35123513iflen(changes) ==0:3514if not self.silent:3515print"No changes to import!"3516else:3517if not self.silent and not self.detectBranches:3518print"Import destination:%s"% self.branch35193520 self.updatedBranches =set()35213522if not self.detectBranches:3523if args:3524# start a new branch3525 self.initialParent =""3526else:3527# build on a previous revision3528 self.initialParent =parseRevision(self.branch)35293530 self.importChanges(changes)35313532if not self.silent:3533print""3534iflen(self.updatedBranches) >0:3535 sys.stdout.write("Updated branches: ")3536for b in self.updatedBranches:3537 sys.stdout.write("%s"% b)3538 sys.stdout.write("\n")35393540ifgitConfigBool("git-p4.importLabels"):3541 self.importLabels =True35423543if self.importLabels:3544 p4Labels =getP4Labels(self.depotPaths)3545 gitTags =getGitTags()35463547 missingP4Labels = p4Labels - gitTags3548 self.importP4Labels(self.gitStream, missingP4Labels)35493550 self.gitStream.close()3551if self.importProcess.wait() !=0:3552die("fast-import failed:%s"% self.gitError.read())3553 self.gitOutput.close()3554 self.gitError.close()35553556# Cleanup temporary branches created during import3557if self.tempBranches != []:3558for branch in self.tempBranches:3559read_pipe("git update-ref -d%s"% branch)3560 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))35613562# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3563# a convenient shortcut refname "p4".3564if self.importIntoRemotes:3565 head_ref = self.refPrefix +"HEAD"3566if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3567system(["git","symbolic-ref", head_ref, self.branch])35683569return True35703571classP4Rebase(Command):3572def__init__(self):3573 Command.__init__(self)3574 self.options = [3575 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3576]3577 self.importLabels =False3578 self.description = ("Fetches the latest revision from perforce and "3579+"rebases the current work (branch) against it")35803581defrun(self, args):3582 sync =P4Sync()3583 sync.importLabels = self.importLabels3584 sync.run([])35853586return self.rebase()35873588defrebase(self):3589if os.system("git update-index --refresh") !=0:3590die("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.");3591iflen(read_pipe("git diff-index HEAD --")) >0:3592die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");35933594[upstream, settings] =findUpstreamBranchPoint()3595iflen(upstream) ==0:3596die("Cannot find upstream branchpoint for rebase")35973598# the branchpoint may be p4/foo~3, so strip off the parent3599 upstream = re.sub("~[0-9]+$","", upstream)36003601print"Rebasing the current branch onto%s"% upstream3602 oldHead =read_pipe("git rev-parse HEAD").strip()3603system("git rebase%s"% upstream)3604system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3605return True36063607classP4Clone(P4Sync):3608def__init__(self):3609 P4Sync.__init__(self)3610 self.description ="Creates a new git repository and imports from Perforce into it"3611 self.usage ="usage: %prog [options] //depot/path[@revRange]"3612 self.options += [3613 optparse.make_option("--destination", dest="cloneDestination",3614 action='store', default=None,3615help="where to leave result of the clone"),3616 optparse.make_option("--bare", dest="cloneBare",3617 action="store_true", default=False),3618]3619 self.cloneDestination =None3620 self.needsGit =False3621 self.cloneBare =False36223623defdefaultDestination(self, args):3624## TODO: use common prefix of args?3625 depotPath = args[0]3626 depotDir = re.sub("(@[^@]*)$","", depotPath)3627 depotDir = re.sub("(#[^#]*)$","", depotDir)3628 depotDir = re.sub(r"\.\.\.$","", depotDir)3629 depotDir = re.sub(r"/$","", depotDir)3630return os.path.split(depotDir)[1]36313632defrun(self, args):3633iflen(args) <1:3634return False36353636if self.keepRepoPath and not self.cloneDestination:3637 sys.stderr.write("Must specify destination for --keep-path\n")3638 sys.exit(1)36393640 depotPaths = args36413642if not self.cloneDestination andlen(depotPaths) >1:3643 self.cloneDestination = depotPaths[-1]3644 depotPaths = depotPaths[:-1]36453646 self.cloneExclude = ["/"+p for p in self.cloneExclude]3647for p in depotPaths:3648if not p.startswith("//"):3649 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3650return False36513652if not self.cloneDestination:3653 self.cloneDestination = self.defaultDestination(args)36543655print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)36563657if not os.path.exists(self.cloneDestination):3658 os.makedirs(self.cloneDestination)3659chdir(self.cloneDestination)36603661 init_cmd = ["git","init"]3662if self.cloneBare:3663 init_cmd.append("--bare")3664 retcode = subprocess.call(init_cmd)3665if retcode:3666raiseCalledProcessError(retcode, init_cmd)36673668if not P4Sync.run(self, depotPaths):3669return False36703671# create a master branch and check out a work tree3672ifgitBranchExists(self.branch):3673system(["git","branch","master", self.branch ])3674if not self.cloneBare:3675system(["git","checkout","-f"])3676else:3677print'Not checking out any branch, use ' \3678'"git checkout -q -b master <branch>"'36793680# auto-set this variable if invoked with --use-client-spec3681if self.useClientSpec_from_options:3682system("git config --bool git-p4.useclientspec true")36833684return True36853686classP4Branches(Command):3687def__init__(self):3688 Command.__init__(self)3689 self.options = [ ]3690 self.description = ("Shows the git branches that hold imports and their "3691+"corresponding perforce depot paths")3692 self.verbose =False36933694defrun(self, args):3695iforiginP4BranchesExist():3696createOrUpdateBranchesFromOrigin()36973698 cmdline ="git rev-parse --symbolic "3699 cmdline +=" --remotes"37003701for line inread_pipe_lines(cmdline):3702 line = line.strip()37033704if not line.startswith('p4/')or line =="p4/HEAD":3705continue3706 branch = line37073708 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3709 settings =extractSettingsGitLog(log)37103711print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3712return True37133714classHelpFormatter(optparse.IndentedHelpFormatter):3715def__init__(self):3716 optparse.IndentedHelpFormatter.__init__(self)37173718defformat_description(self, description):3719if description:3720return description +"\n"3721else:3722return""37233724defprintUsage(commands):3725print"usage:%s<command> [options]"% sys.argv[0]3726print""3727print"valid commands:%s"%", ".join(commands)3728print""3729print"Try%s<command> --help for command specific help."% sys.argv[0]3730print""37313732commands = {3733"debug": P4Debug,3734"submit": P4Submit,3735"commit": P4Submit,3736"sync": P4Sync,3737"rebase": P4Rebase,3738"clone": P4Clone,3739"rollback": P4RollBack,3740"branches": P4Branches3741}374237433744defmain():3745iflen(sys.argv[1:]) ==0:3746printUsage(commands.keys())3747 sys.exit(2)37483749 cmdName = sys.argv[1]3750try:3751 klass = commands[cmdName]3752 cmd =klass()3753exceptKeyError:3754print"unknown command%s"% cmdName3755print""3756printUsage(commands.keys())3757 sys.exit(2)37583759 options = cmd.options3760 cmd.gitdir = os.environ.get("GIT_DIR",None)37613762 args = sys.argv[2:]37633764 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3765if cmd.needsGit:3766 options.append(optparse.make_option("--git-dir", dest="gitdir"))37673768 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3769 options,3770 description = cmd.description,3771 formatter =HelpFormatter())37723773(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3774global verbose3775 verbose = cmd.verbose3776if cmd.needsGit:3777if cmd.gitdir ==None:3778 cmd.gitdir = os.path.abspath(".git")3779if notisValidGitDir(cmd.gitdir):3780# "rev-parse --git-dir" without arguments will try $PWD/.git3781 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3782if os.path.exists(cmd.gitdir):3783 cdup =read_pipe("git rev-parse --show-cdup").strip()3784iflen(cdup) >0:3785chdir(cdup);37863787if notisValidGitDir(cmd.gitdir):3788ifisValidGitDir(cmd.gitdir +"/.git"):3789 cmd.gitdir +="/.git"3790else:3791die("fatal: cannot locate git repository at%s"% cmd.gitdir)37923793# so git commands invoked from the P4 workspace will succeed3794 os.environ["GIT_DIR"] = cmd.gitdir37953796if not cmd.run(args):3797 parser.print_help()3798 sys.exit(2)379938003801if __name__ =='__main__':3802main()