1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25import zipfile 26import zlib 27import ctypes 28import errno 29 30try: 31from subprocess import CalledProcessError 32exceptImportError: 33# from python2.7:subprocess.py 34# Exception classes used by this module. 35classCalledProcessError(Exception): 36"""This exception is raised when a process run by check_call() returns 37 a non-zero exit status. The exit status will be stored in the 38 returncode attribute.""" 39def__init__(self, returncode, cmd): 40 self.returncode = returncode 41 self.cmd = cmd 42def__str__(self): 43return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 44 45verbose =False 46 47# Only labels/tags matching this will be imported/exported 48defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 49 50# Grab changes in blocks of this many revisions, unless otherwise requested 51defaultBlockSize =512 52 53defp4_build_cmd(cmd): 54"""Build a suitable p4 command line. 55 56 This consolidates building and returning a p4 command line into one 57 location. It means that hooking into the environment, or other configuration 58 can be done more easily. 59 """ 60 real_cmd = ["p4"] 61 62 user =gitConfig("git-p4.user") 63iflen(user) >0: 64 real_cmd += ["-u",user] 65 66 password =gitConfig("git-p4.password") 67iflen(password) >0: 68 real_cmd += ["-P", password] 69 70 port =gitConfig("git-p4.port") 71iflen(port) >0: 72 real_cmd += ["-p", port] 73 74 host =gitConfig("git-p4.host") 75iflen(host) >0: 76 real_cmd += ["-H", host] 77 78 client =gitConfig("git-p4.client") 79iflen(client) >0: 80 real_cmd += ["-c", client] 81 82 retries =gitConfigInt("git-p4.retries") 83if retries is None: 84# Perform 3 retries by default 85 retries =3 86if retries >0: 87# Provide a way to not pass this option by setting git-p4.retries to 0 88 real_cmd += ["-r",str(retries)] 89 90ifisinstance(cmd,basestring): 91 real_cmd =' '.join(real_cmd) +' '+ cmd 92else: 93 real_cmd += cmd 94return real_cmd 95 96defgit_dir(path): 97""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 98 This won't automatically add ".git" to a directory. 99 """ 100 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 101if not d orlen(d) ==0: 102return None 103else: 104return d 105 106defchdir(path, is_client_path=False): 107"""Do chdir to the given path, and set the PWD environment 108 variable for use by P4. It does not look at getcwd() output. 109 Since we're not using the shell, it is necessary to set the 110 PWD environment variable explicitly. 111 112 Normally, expand the path to force it to be absolute. This 113 addresses the use of relative path names inside P4 settings, 114 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 115 as given; it looks for .p4config using PWD. 116 117 If is_client_path, the path was handed to us directly by p4, 118 and may be a symbolic link. Do not call os.getcwd() in this 119 case, because it will cause p4 to think that PWD is not inside 120 the client path. 121 """ 122 123 os.chdir(path) 124if not is_client_path: 125 path = os.getcwd() 126 os.environ['PWD'] = path 127 128defcalcDiskFree(): 129"""Return free space in bytes on the disk of the given dirname.""" 130if platform.system() =='Windows': 131 free_bytes = ctypes.c_ulonglong(0) 132 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 133return free_bytes.value 134else: 135 st = os.statvfs(os.getcwd()) 136return st.f_bavail * st.f_frsize 137 138defdie(msg): 139if verbose: 140raiseException(msg) 141else: 142 sys.stderr.write(msg +"\n") 143 sys.exit(1) 144 145defwrite_pipe(c, stdin): 146if verbose: 147 sys.stderr.write('Writing pipe:%s\n'%str(c)) 148 149 expand =isinstance(c,basestring) 150 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 151 pipe = p.stdin 152 val = pipe.write(stdin) 153 pipe.close() 154if p.wait(): 155die('Command failed:%s'%str(c)) 156 157return val 158 159defp4_write_pipe(c, stdin): 160 real_cmd =p4_build_cmd(c) 161returnwrite_pipe(real_cmd, stdin) 162 163defread_pipe_full(c): 164""" Read output from command. Returns a tuple 165 of the return status, stdout text and stderr 166 text. 167 """ 168if verbose: 169 sys.stderr.write('Reading pipe:%s\n'%str(c)) 170 171 expand =isinstance(c,basestring) 172 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 173(out, err) = p.communicate() 174return(p.returncode, out, err) 175 176defread_pipe(c, ignore_error=False): 177""" Read output from command. Returns the output text on 178 success. On failure, terminates execution, unless 179 ignore_error is True, when it returns an empty string. 180 """ 181(retcode, out, err) =read_pipe_full(c) 182if retcode !=0: 183if ignore_error: 184 out ="" 185else: 186die('Command failed:%s\nError:%s'% (str(c), err)) 187return out 188 189defread_pipe_text(c): 190""" Read output from a command with trailing whitespace stripped. 191 On error, returns None. 192 """ 193(retcode, out, err) =read_pipe_full(c) 194if retcode !=0: 195return None 196else: 197return out.rstrip() 198 199defp4_read_pipe(c, ignore_error=False): 200 real_cmd =p4_build_cmd(c) 201returnread_pipe(real_cmd, ignore_error) 202 203defread_pipe_lines(c): 204if verbose: 205 sys.stderr.write('Reading pipe:%s\n'%str(c)) 206 207 expand =isinstance(c, basestring) 208 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 209 pipe = p.stdout 210 val = pipe.readlines() 211if pipe.close()or p.wait(): 212die('Command failed:%s'%str(c)) 213 214return val 215 216defp4_read_pipe_lines(c): 217"""Specifically invoke p4 on the command supplied. """ 218 real_cmd =p4_build_cmd(c) 219returnread_pipe_lines(real_cmd) 220 221defp4_has_command(cmd): 222"""Ask p4 for help on this command. If it returns an error, the 223 command does not exist in this version of p4.""" 224 real_cmd =p4_build_cmd(["help", cmd]) 225 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 226 stderr=subprocess.PIPE) 227 p.communicate() 228return p.returncode ==0 229 230defp4_has_move_command(): 231"""See if the move command exists, that it supports -k, and that 232 it has not been administratively disabled. The arguments 233 must be correct, but the filenames do not have to exist. Use 234 ones with wildcards so even if they exist, it will fail.""" 235 236if notp4_has_command("move"): 237return False 238 cmd =p4_build_cmd(["move","-k","@from","@to"]) 239 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 240(out, err) = p.communicate() 241# return code will be 1 in either case 242if err.find("Invalid option") >=0: 243return False 244if err.find("disabled") >=0: 245return False 246# assume it failed because @... was invalid changelist 247return True 248 249defsystem(cmd, ignore_error=False): 250 expand =isinstance(cmd,basestring) 251if verbose: 252 sys.stderr.write("executing%s\n"%str(cmd)) 253 retcode = subprocess.call(cmd, shell=expand) 254if retcode and not ignore_error: 255raiseCalledProcessError(retcode, cmd) 256 257return retcode 258 259defp4_system(cmd): 260"""Specifically invoke p4 as the system command. """ 261 real_cmd =p4_build_cmd(cmd) 262 expand =isinstance(real_cmd, basestring) 263 retcode = subprocess.call(real_cmd, shell=expand) 264if retcode: 265raiseCalledProcessError(retcode, real_cmd) 266 267_p4_version_string =None 268defp4_version_string(): 269"""Read the version string, showing just the last line, which 270 hopefully is the interesting version bit. 271 272 $ p4 -V 273 Perforce - The Fast Software Configuration Management System. 274 Copyright 1995-2011 Perforce Software. All rights reserved. 275 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 276 """ 277global _p4_version_string 278if not _p4_version_string: 279 a =p4_read_pipe_lines(["-V"]) 280 _p4_version_string = a[-1].rstrip() 281return _p4_version_string 282 283defp4_integrate(src, dest): 284p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 285 286defp4_sync(f, *options): 287p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 288 289defp4_add(f): 290# forcibly add file names with wildcards 291ifwildcard_present(f): 292p4_system(["add","-f", f]) 293else: 294p4_system(["add", f]) 295 296defp4_delete(f): 297p4_system(["delete",wildcard_encode(f)]) 298 299defp4_edit(f, *options): 300p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 301 302defp4_revert(f): 303p4_system(["revert",wildcard_encode(f)]) 304 305defp4_reopen(type, f): 306p4_system(["reopen","-t",type,wildcard_encode(f)]) 307 308defp4_reopen_in_change(changelist, files): 309 cmd = ["reopen","-c",str(changelist)] + files 310p4_system(cmd) 311 312defp4_move(src, dest): 313p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 314 315defp4_last_change(): 316 results =p4CmdList(["changes","-m","1"]) 317returnint(results[0]['change']) 318 319defp4_describe(change): 320"""Make sure it returns a valid result by checking for 321 the presence of field "time". Return a dict of the 322 results.""" 323 324 ds =p4CmdList(["describe","-s",str(change)]) 325iflen(ds) !=1: 326die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 327 328 d = ds[0] 329 330if"p4ExitCode"in d: 331die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 332str(d))) 333if"code"in d: 334if d["code"] =="error": 335die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 336 337if"time"not in d: 338die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 339 340return d 341 342# 343# Canonicalize the p4 type and return a tuple of the 344# base type, plus any modifiers. See "p4 help filetypes" 345# for a list and explanation. 346# 347defsplit_p4_type(p4type): 348 349 p4_filetypes_historical = { 350"ctempobj":"binary+Sw", 351"ctext":"text+C", 352"cxtext":"text+Cx", 353"ktext":"text+k", 354"kxtext":"text+kx", 355"ltext":"text+F", 356"tempobj":"binary+FSw", 357"ubinary":"binary+F", 358"uresource":"resource+F", 359"uxbinary":"binary+Fx", 360"xbinary":"binary+x", 361"xltext":"text+Fx", 362"xtempobj":"binary+Swx", 363"xtext":"text+x", 364"xunicode":"unicode+x", 365"xutf16":"utf16+x", 366} 367if p4type in p4_filetypes_historical: 368 p4type = p4_filetypes_historical[p4type] 369 mods ="" 370 s = p4type.split("+") 371 base = s[0] 372 mods ="" 373iflen(s) >1: 374 mods = s[1] 375return(base, mods) 376 377# 378# return the raw p4 type of a file (text, text+ko, etc) 379# 380defp4_type(f): 381 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 382return results[0]['headType'] 383 384# 385# Given a type base and modifier, return a regexp matching 386# the keywords that can be expanded in the file 387# 388defp4_keywords_regexp_for_type(base, type_mods): 389if base in("text","unicode","binary"): 390 kwords =None 391if"ko"in type_mods: 392 kwords ='Id|Header' 393elif"k"in type_mods: 394 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 395else: 396return None 397 pattern = r""" 398 \$ # Starts with a dollar, followed by... 399 (%s) # one of the keywords, followed by... 400 (:[^$\n]+)? # possibly an old expansion, followed by... 401 \$ # another dollar 402 """% kwords 403return pattern 404else: 405return None 406 407# 408# Given a file, return a regexp matching the possible 409# RCS keywords that will be expanded, or None for files 410# with kw expansion turned off. 411# 412defp4_keywords_regexp_for_file(file): 413if not os.path.exists(file): 414return None 415else: 416(type_base, type_mods) =split_p4_type(p4_type(file)) 417returnp4_keywords_regexp_for_type(type_base, type_mods) 418 419defsetP4ExecBit(file, mode): 420# Reopens an already open file and changes the execute bit to match 421# the execute bit setting in the passed in mode. 422 423 p4Type ="+x" 424 425if notisModeExec(mode): 426 p4Type =getP4OpenedType(file) 427 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 428 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 429if p4Type[-1] =="+": 430 p4Type = p4Type[0:-1] 431 432p4_reopen(p4Type,file) 433 434defgetP4OpenedType(file): 435# Returns the perforce file type for the given file. 436 437 result =p4_read_pipe(["opened",wildcard_encode(file)]) 438 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 439if match: 440return match.group(1) 441else: 442die("Could not determine file type for%s(result: '%s')"% (file, result)) 443 444# Return the set of all p4 labels 445defgetP4Labels(depotPaths): 446 labels =set() 447ifisinstance(depotPaths,basestring): 448 depotPaths = [depotPaths] 449 450for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 451 label = l['label'] 452 labels.add(label) 453 454return labels 455 456# Return the set of all git tags 457defgetGitTags(): 458 gitTags =set() 459for line inread_pipe_lines(["git","tag"]): 460 tag = line.strip() 461 gitTags.add(tag) 462return gitTags 463 464defdiffTreePattern(): 465# This is a simple generator for the diff tree regex pattern. This could be 466# a class variable if this and parseDiffTreeEntry were a part of a class. 467 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 468while True: 469yield pattern 470 471defparseDiffTreeEntry(entry): 472"""Parses a single diff tree entry into its component elements. 473 474 See git-diff-tree(1) manpage for details about the format of the diff 475 output. This method returns a dictionary with the following elements: 476 477 src_mode - The mode of the source file 478 dst_mode - The mode of the destination file 479 src_sha1 - The sha1 for the source file 480 dst_sha1 - The sha1 fr the destination file 481 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 482 status_score - The score for the status (applicable for 'C' and 'R' 483 statuses). This is None if there is no score. 484 src - The path for the source file. 485 dst - The path for the destination file. This is only present for 486 copy or renames. If it is not present, this is None. 487 488 If the pattern is not matched, None is returned.""" 489 490 match =diffTreePattern().next().match(entry) 491if match: 492return{ 493'src_mode': match.group(1), 494'dst_mode': match.group(2), 495'src_sha1': match.group(3), 496'dst_sha1': match.group(4), 497'status': match.group(5), 498'status_score': match.group(6), 499'src': match.group(7), 500'dst': match.group(10) 501} 502return None 503 504defisModeExec(mode): 505# Returns True if the given git mode represents an executable file, 506# otherwise False. 507return mode[-3:] =="755" 508 509defisModeExecChanged(src_mode, dst_mode): 510returnisModeExec(src_mode) !=isModeExec(dst_mode) 511 512defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 513 514ifisinstance(cmd,basestring): 515 cmd ="-G "+ cmd 516 expand =True 517else: 518 cmd = ["-G"] + cmd 519 expand =False 520 521 cmd =p4_build_cmd(cmd) 522if verbose: 523 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 524 525# Use a temporary file to avoid deadlocks without 526# subprocess.communicate(), which would put another copy 527# of stdout into memory. 528 stdin_file =None 529if stdin is not None: 530 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 531ifisinstance(stdin,basestring): 532 stdin_file.write(stdin) 533else: 534for i in stdin: 535 stdin_file.write(i +'\n') 536 stdin_file.flush() 537 stdin_file.seek(0) 538 539 p4 = subprocess.Popen(cmd, 540 shell=expand, 541 stdin=stdin_file, 542 stdout=subprocess.PIPE) 543 544 result = [] 545try: 546while True: 547 entry = marshal.load(p4.stdout) 548if cb is not None: 549cb(entry) 550else: 551 result.append(entry) 552exceptEOFError: 553pass 554 exitCode = p4.wait() 555if exitCode !=0: 556 entry = {} 557 entry["p4ExitCode"] = exitCode 558 result.append(entry) 559 560return result 561 562defp4Cmd(cmd): 563list=p4CmdList(cmd) 564 result = {} 565for entry inlist: 566 result.update(entry) 567return result; 568 569defp4Where(depotPath): 570if not depotPath.endswith("/"): 571 depotPath +="/" 572 depotPathLong = depotPath +"..." 573 outputList =p4CmdList(["where", depotPathLong]) 574 output =None 575for entry in outputList: 576if"depotFile"in entry: 577# Search for the base client side depot path, as long as it starts with the branch's P4 path. 578# The base path always ends with "/...". 579if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 580 output = entry 581break 582elif"data"in entry: 583 data = entry.get("data") 584 space = data.find(" ") 585if data[:space] == depotPath: 586 output = entry 587break 588if output ==None: 589return"" 590if output["code"] =="error": 591return"" 592 clientPath ="" 593if"path"in output: 594 clientPath = output.get("path") 595elif"data"in output: 596 data = output.get("data") 597 lastSpace = data.rfind(" ") 598 clientPath = data[lastSpace +1:] 599 600if clientPath.endswith("..."): 601 clientPath = clientPath[:-3] 602return clientPath 603 604defcurrentGitBranch(): 605returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 606 607defisValidGitDir(path): 608returngit_dir(path) !=None 609 610defparseRevision(ref): 611returnread_pipe("git rev-parse%s"% ref).strip() 612 613defbranchExists(ref): 614 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 615 ignore_error=True) 616returnlen(rev) >0 617 618defextractLogMessageFromGitCommit(commit): 619 logMessage ="" 620 621## fixme: title is first line of commit, not 1st paragraph. 622 foundTitle =False 623for log inread_pipe_lines("git cat-file commit%s"% commit): 624if not foundTitle: 625iflen(log) ==1: 626 foundTitle =True 627continue 628 629 logMessage += log 630return logMessage 631 632defextractSettingsGitLog(log): 633 values = {} 634for line in log.split("\n"): 635 line = line.strip() 636 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 637if not m: 638continue 639 640 assignments = m.group(1).split(':') 641for a in assignments: 642 vals = a.split('=') 643 key = vals[0].strip() 644 val = ('='.join(vals[1:])).strip() 645if val.endswith('\"')and val.startswith('"'): 646 val = val[1:-1] 647 648 values[key] = val 649 650 paths = values.get("depot-paths") 651if not paths: 652 paths = values.get("depot-path") 653if paths: 654 values['depot-paths'] = paths.split(',') 655return values 656 657defgitBranchExists(branch): 658 proc = subprocess.Popen(["git","rev-parse", branch], 659 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 660return proc.wait() ==0; 661 662_gitConfig = {} 663 664defgitConfig(key, typeSpecifier=None): 665if not _gitConfig.has_key(key): 666 cmd = ["git","config"] 667if typeSpecifier: 668 cmd += [ typeSpecifier ] 669 cmd += [ key ] 670 s =read_pipe(cmd, ignore_error=True) 671 _gitConfig[key] = s.strip() 672return _gitConfig[key] 673 674defgitConfigBool(key): 675"""Return a bool, using git config --bool. It is True only if the 676 variable is set to true, and False if set to false or not present 677 in the config.""" 678 679if not _gitConfig.has_key(key): 680 _gitConfig[key] =gitConfig(key,'--bool') =="true" 681return _gitConfig[key] 682 683defgitConfigInt(key): 684if not _gitConfig.has_key(key): 685 cmd = ["git","config","--int", key ] 686 s =read_pipe(cmd, ignore_error=True) 687 v = s.strip() 688try: 689 _gitConfig[key] =int(gitConfig(key,'--int')) 690exceptValueError: 691 _gitConfig[key] =None 692return _gitConfig[key] 693 694defgitConfigList(key): 695if not _gitConfig.has_key(key): 696 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 697 _gitConfig[key] = s.strip().splitlines() 698if _gitConfig[key] == ['']: 699 _gitConfig[key] = [] 700return _gitConfig[key] 701 702defp4BranchesInGit(branchesAreInRemotes=True): 703"""Find all the branches whose names start with "p4/", looking 704 in remotes or heads as specified by the argument. Return 705 a dictionary of{ branch: revision }for each one found. 706 The branch names are the short names, without any 707 "p4/" prefix.""" 708 709 branches = {} 710 711 cmdline ="git rev-parse --symbolic " 712if branchesAreInRemotes: 713 cmdline +="--remotes" 714else: 715 cmdline +="--branches" 716 717for line inread_pipe_lines(cmdline): 718 line = line.strip() 719 720# only import to p4/ 721if not line.startswith('p4/'): 722continue 723# special symbolic ref to p4/master 724if line =="p4/HEAD": 725continue 726 727# strip off p4/ prefix 728 branch = line[len("p4/"):] 729 730 branches[branch] =parseRevision(line) 731 732return branches 733 734defbranch_exists(branch): 735"""Make sure that the given ref name really exists.""" 736 737 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 738 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 739 out, _ = p.communicate() 740if p.returncode: 741return False 742# expect exactly one line of output: the branch name 743return out.rstrip() == branch 744 745deffindUpstreamBranchPoint(head ="HEAD"): 746 branches =p4BranchesInGit() 747# map from depot-path to branch name 748 branchByDepotPath = {} 749for branch in branches.keys(): 750 tip = branches[branch] 751 log =extractLogMessageFromGitCommit(tip) 752 settings =extractSettingsGitLog(log) 753if settings.has_key("depot-paths"): 754 paths =",".join(settings["depot-paths"]) 755 branchByDepotPath[paths] ="remotes/p4/"+ branch 756 757 settings =None 758 parent =0 759while parent <65535: 760 commit = head +"~%s"% parent 761 log =extractLogMessageFromGitCommit(commit) 762 settings =extractSettingsGitLog(log) 763if settings.has_key("depot-paths"): 764 paths =",".join(settings["depot-paths"]) 765if branchByDepotPath.has_key(paths): 766return[branchByDepotPath[paths], settings] 767 768 parent = parent +1 769 770return["", settings] 771 772defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 773if not silent: 774print("Creating/updating branch(es) in%sbased on origin branch(es)" 775% localRefPrefix) 776 777 originPrefix ="origin/p4/" 778 779for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 780 line = line.strip() 781if(not line.startswith(originPrefix))or line.endswith("HEAD"): 782continue 783 784 headName = line[len(originPrefix):] 785 remoteHead = localRefPrefix + headName 786 originHead = line 787 788 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 789if(not original.has_key('depot-paths') 790or not original.has_key('change')): 791continue 792 793 update =False 794if notgitBranchExists(remoteHead): 795if verbose: 796print"creating%s"% remoteHead 797 update =True 798else: 799 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 800if settings.has_key('change') >0: 801if settings['depot-paths'] == original['depot-paths']: 802 originP4Change =int(original['change']) 803 p4Change =int(settings['change']) 804if originP4Change > p4Change: 805print("%s(%s) is newer than%s(%s). " 806"Updating p4 branch from origin." 807% (originHead, originP4Change, 808 remoteHead, p4Change)) 809 update =True 810else: 811print("Ignoring:%swas imported from%swhile " 812"%swas imported from%s" 813% (originHead,','.join(original['depot-paths']), 814 remoteHead,','.join(settings['depot-paths']))) 815 816if update: 817system("git update-ref%s %s"% (remoteHead, originHead)) 818 819deforiginP4BranchesExist(): 820returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 821 822 823defp4ParseNumericChangeRange(parts): 824 changeStart =int(parts[0][1:]) 825if parts[1] =='#head': 826 changeEnd =p4_last_change() 827else: 828 changeEnd =int(parts[1]) 829 830return(changeStart, changeEnd) 831 832defchooseBlockSize(blockSize): 833if blockSize: 834return blockSize 835else: 836return defaultBlockSize 837 838defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 839assert depotPaths 840 841# Parse the change range into start and end. Try to find integer 842# revision ranges as these can be broken up into blocks to avoid 843# hitting server-side limits (maxrows, maxscanresults). But if 844# that doesn't work, fall back to using the raw revision specifier 845# strings, without using block mode. 846 847if changeRange is None or changeRange =='': 848 changeStart =1 849 changeEnd =p4_last_change() 850 block_size =chooseBlockSize(requestedBlockSize) 851else: 852 parts = changeRange.split(',') 853assertlen(parts) ==2 854try: 855(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 856 block_size =chooseBlockSize(requestedBlockSize) 857except: 858 changeStart = parts[0][1:] 859 changeEnd = parts[1] 860if requestedBlockSize: 861die("cannot use --changes-block-size with non-numeric revisions") 862 block_size =None 863 864 changes =set() 865 866# Retrieve changes a block at a time, to prevent running 867# into a MaxResults/MaxScanRows error from the server. 868 869while True: 870 cmd = ['changes'] 871 872if block_size: 873 end =min(changeEnd, changeStart + block_size) 874 revisionRange ="%d,%d"% (changeStart, end) 875else: 876 revisionRange ="%s,%s"% (changeStart, changeEnd) 877 878for p in depotPaths: 879 cmd += ["%s...@%s"% (p, revisionRange)] 880 881# Insert changes in chronological order 882for entry inreversed(p4CmdList(cmd)): 883if entry.has_key('p4ExitCode'): 884die('Error retrieving changes descriptions ({})'.format(entry['p4ExitCode'])) 885if not entry.has_key('change'): 886continue 887 changes.add(int(entry['change'])) 888 889if not block_size: 890break 891 892if end >= changeEnd: 893break 894 895 changeStart = end +1 896 897 changes =sorted(changes) 898return changes 899 900defp4PathStartsWith(path, prefix): 901# This method tries to remedy a potential mixed-case issue: 902# 903# If UserA adds //depot/DirA/file1 904# and UserB adds //depot/dira/file2 905# 906# we may or may not have a problem. If you have core.ignorecase=true, 907# we treat DirA and dira as the same directory 908ifgitConfigBool("core.ignorecase"): 909return path.lower().startswith(prefix.lower()) 910return path.startswith(prefix) 911 912defgetClientSpec(): 913"""Look at the p4 client spec, create a View() object that contains 914 all the mappings, and return it.""" 915 916 specList =p4CmdList("client -o") 917iflen(specList) !=1: 918die('Output from "client -o" is%dlines, expecting 1'% 919len(specList)) 920 921# dictionary of all client parameters 922 entry = specList[0] 923 924# the //client/ name 925 client_name = entry["Client"] 926 927# just the keys that start with "View" 928 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 929 930# hold this new View 931 view =View(client_name) 932 933# append the lines, in order, to the view 934for view_num inrange(len(view_keys)): 935 k ="View%d"% view_num 936if k not in view_keys: 937die("Expected view key%smissing"% k) 938 view.append(entry[k]) 939 940return view 941 942defgetClientRoot(): 943"""Grab the client directory.""" 944 945 output =p4CmdList("client -o") 946iflen(output) !=1: 947die('Output from "client -o" is%dlines, expecting 1'%len(output)) 948 949 entry = output[0] 950if"Root"not in entry: 951die('Client has no "Root"') 952 953return entry["Root"] 954 955# 956# P4 wildcards are not allowed in filenames. P4 complains 957# if you simply add them, but you can force it with "-f", in 958# which case it translates them into %xx encoding internally. 959# 960defwildcard_decode(path): 961# Search for and fix just these four characters. Do % last so 962# that fixing it does not inadvertently create new %-escapes. 963# Cannot have * in a filename in windows; untested as to 964# what p4 would do in such a case. 965if not platform.system() =="Windows": 966 path = path.replace("%2A","*") 967 path = path.replace("%23","#") \ 968.replace("%40","@") \ 969.replace("%25","%") 970return path 971 972defwildcard_encode(path): 973# do % first to avoid double-encoding the %s introduced here 974 path = path.replace("%","%25") \ 975.replace("*","%2A") \ 976.replace("#","%23") \ 977.replace("@","%40") 978return path 979 980defwildcard_present(path): 981 m = re.search("[*#@%]", path) 982return m is not None 983 984classLargeFileSystem(object): 985"""Base class for large file system support.""" 986 987def__init__(self, writeToGitStream): 988 self.largeFiles =set() 989 self.writeToGitStream = writeToGitStream 990 991defgeneratePointer(self, cloneDestination, contentFile): 992"""Return the content of a pointer file that is stored in Git instead of 993 the actual content.""" 994assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 995 996defpushFile(self, localLargeFile): 997"""Push the actual content which is not stored in the Git repository to 998 a server.""" 999assert False,"Method 'pushFile' required in "+ self.__class__.__name__10001001defhasLargeFileExtension(self, relPath):1002returnreduce(1003lambda a, b: a or b,1004[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1005False1006)10071008defgenerateTempFile(self, contents):1009 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1010for d in contents:1011 contentFile.write(d)1012 contentFile.close()1013return contentFile.name10141015defexceedsLargeFileThreshold(self, relPath, contents):1016ifgitConfigInt('git-p4.largeFileThreshold'):1017 contentsSize =sum(len(d)for d in contents)1018if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1019return True1020ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1021 contentsSize =sum(len(d)for d in contents)1022if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1023return False1024 contentTempFile = self.generateTempFile(contents)1025 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1026 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1027 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1028 zf.close()1029 compressedContentsSize = zf.infolist()[0].compress_size1030 os.remove(contentTempFile)1031 os.remove(compressedContentFile.name)1032if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1033return True1034return False10351036defaddLargeFile(self, relPath):1037 self.largeFiles.add(relPath)10381039defremoveLargeFile(self, relPath):1040 self.largeFiles.remove(relPath)10411042defisLargeFile(self, relPath):1043return relPath in self.largeFiles10441045defprocessContent(self, git_mode, relPath, contents):1046"""Processes the content of git fast import. This method decides if a1047 file is stored in the large file system and handles all necessary1048 steps."""1049if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1050 contentTempFile = self.generateTempFile(contents)1051(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1052if pointer_git_mode:1053 git_mode = pointer_git_mode1054if localLargeFile:1055# Move temp file to final location in large file system1056 largeFileDir = os.path.dirname(localLargeFile)1057if not os.path.isdir(largeFileDir):1058 os.makedirs(largeFileDir)1059 shutil.move(contentTempFile, localLargeFile)1060 self.addLargeFile(relPath)1061ifgitConfigBool('git-p4.largeFilePush'):1062 self.pushFile(localLargeFile)1063if verbose:1064 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1065return(git_mode, contents)10661067classMockLFS(LargeFileSystem):1068"""Mock large file system for testing."""10691070defgeneratePointer(self, contentFile):1071"""The pointer content is the original content prefixed with "pointer-".1072 The local filename of the large file storage is derived from the file content.1073 """1074withopen(contentFile,'r')as f:1075 content =next(f)1076 gitMode ='100644'1077 pointerContents ='pointer-'+ content1078 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1079return(gitMode, pointerContents, localLargeFile)10801081defpushFile(self, localLargeFile):1082"""The remote filename of the large file storage is the same as the local1083 one but in a different directory.1084 """1085 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1086if not os.path.exists(remotePath):1087 os.makedirs(remotePath)1088 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10891090classGitLFS(LargeFileSystem):1091"""Git LFS as backend for the git-p4 large file system.1092 See https://git-lfs.github.com/ for details."""10931094def__init__(self, *args):1095 LargeFileSystem.__init__(self, *args)1096 self.baseGitAttributes = []10971098defgeneratePointer(self, contentFile):1099"""Generate a Git LFS pointer for the content. Return LFS Pointer file1100 mode and content which is stored in the Git repository instead of1101 the actual content. Return also the new location of the actual1102 content.1103 """1104if os.path.getsize(contentFile) ==0:1105return(None,'',None)11061107 pointerProcess = subprocess.Popen(1108['git','lfs','pointer','--file='+ contentFile],1109 stdout=subprocess.PIPE1110)1111 pointerFile = pointerProcess.stdout.read()1112if pointerProcess.wait():1113 os.remove(contentFile)1114die('git-lfs pointer command failed. Did you install the extension?')11151116# Git LFS removed the preamble in the output of the 'pointer' command1117# starting from version 1.2.0. Check for the preamble here to support1118# earlier versions.1119# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431120if pointerFile.startswith('Git LFS pointer for'):1121 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)11221123 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1124 localLargeFile = os.path.join(1125 os.getcwd(),1126'.git','lfs','objects', oid[:2], oid[2:4],1127 oid,1128)1129# LFS Spec states that pointer files should not have the executable bit set.1130 gitMode ='100644'1131return(gitMode, pointerFile, localLargeFile)11321133defpushFile(self, localLargeFile):1134 uploadProcess = subprocess.Popen(1135['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1136)1137if uploadProcess.wait():1138die('git-lfs push command failed. Did you define a remote?')11391140defgenerateGitAttributes(self):1141return(1142 self.baseGitAttributes +1143[1144'\n',1145'#\n',1146'# Git LFS (see https://git-lfs.github.com/)\n',1147'#\n',1148] +1149['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1150for f insorted(gitConfigList('git-p4.largeFileExtensions'))1151] +1152['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1153for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1154]1155)11561157defaddLargeFile(self, relPath):1158 LargeFileSystem.addLargeFile(self, relPath)1159 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11601161defremoveLargeFile(self, relPath):1162 LargeFileSystem.removeLargeFile(self, relPath)1163 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11641165defprocessContent(self, git_mode, relPath, contents):1166if relPath =='.gitattributes':1167 self.baseGitAttributes = contents1168return(git_mode, self.generateGitAttributes())1169else:1170return LargeFileSystem.processContent(self, git_mode, relPath, contents)11711172class Command:1173def__init__(self):1174 self.usage ="usage: %prog [options]"1175 self.needsGit =True1176 self.verbose =False11771178class P4UserMap:1179def__init__(self):1180 self.userMapFromPerforceServer =False1181 self.myP4UserId =None11821183defp4UserId(self):1184if self.myP4UserId:1185return self.myP4UserId11861187 results =p4CmdList("user -o")1188for r in results:1189if r.has_key('User'):1190 self.myP4UserId = r['User']1191return r['User']1192die("Could not find your p4 user id")11931194defp4UserIsMe(self, p4User):1195# return True if the given p4 user is actually me1196 me = self.p4UserId()1197if not p4User or p4User != me:1198return False1199else:1200return True12011202defgetUserCacheFilename(self):1203 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1204return home +"/.gitp4-usercache.txt"12051206defgetUserMapFromPerforceServer(self):1207if self.userMapFromPerforceServer:1208return1209 self.users = {}1210 self.emails = {}12111212for output inp4CmdList("users"):1213if not output.has_key("User"):1214continue1215 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1216 self.emails[output["Email"]] = output["User"]12171218 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1219for mapUserConfig ingitConfigList("git-p4.mapUser"):1220 mapUser = mapUserConfigRegex.findall(mapUserConfig)1221if mapUser andlen(mapUser[0]) ==3:1222 user = mapUser[0][0]1223 fullname = mapUser[0][1]1224 email = mapUser[0][2]1225 self.users[user] = fullname +" <"+ email +">"1226 self.emails[email] = user12271228 s =''1229for(key, val)in self.users.items():1230 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12311232open(self.getUserCacheFilename(),"wb").write(s)1233 self.userMapFromPerforceServer =True12341235defloadUserMapFromCache(self):1236 self.users = {}1237 self.userMapFromPerforceServer =False1238try:1239 cache =open(self.getUserCacheFilename(),"rb")1240 lines = cache.readlines()1241 cache.close()1242for line in lines:1243 entry = line.strip().split("\t")1244 self.users[entry[0]] = entry[1]1245exceptIOError:1246 self.getUserMapFromPerforceServer()12471248classP4Debug(Command):1249def__init__(self):1250 Command.__init__(self)1251 self.options = []1252 self.description ="A tool to debug the output of p4 -G."1253 self.needsGit =False12541255defrun(self, args):1256 j =01257for output inp4CmdList(args):1258print'Element:%d'% j1259 j +=11260print output1261return True12621263classP4RollBack(Command):1264def__init__(self):1265 Command.__init__(self)1266 self.options = [1267 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1268]1269 self.description ="A tool to debug the multi-branch import. Don't use :)"1270 self.rollbackLocalBranches =False12711272defrun(self, args):1273iflen(args) !=1:1274return False1275 maxChange =int(args[0])12761277if"p4ExitCode"inp4Cmd("changes -m 1"):1278die("Problems executing p4");12791280if self.rollbackLocalBranches:1281 refPrefix ="refs/heads/"1282 lines =read_pipe_lines("git rev-parse --symbolic --branches")1283else:1284 refPrefix ="refs/remotes/"1285 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12861287for line in lines:1288if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1289 line = line.strip()1290 ref = refPrefix + line1291 log =extractLogMessageFromGitCommit(ref)1292 settings =extractSettingsGitLog(log)12931294 depotPaths = settings['depot-paths']1295 change = settings['change']12961297 changed =False12981299iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1300for p in depotPaths]))) ==0:1301print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1302system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1303continue13041305while change andint(change) > maxChange:1306 changed =True1307if self.verbose:1308print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1309system("git update-ref%s\"%s^\""% (ref, ref))1310 log =extractLogMessageFromGitCommit(ref)1311 settings =extractSettingsGitLog(log)131213131314 depotPaths = settings['depot-paths']1315 change = settings['change']13161317if changed:1318print"%srewound to%s"% (ref, change)13191320return True13211322classP4Submit(Command, P4UserMap):13231324 conflict_behavior_choices = ("ask","skip","quit")13251326def__init__(self):1327 Command.__init__(self)1328 P4UserMap.__init__(self)1329 self.options = [1330 optparse.make_option("--origin", dest="origin"),1331 optparse.make_option("-M", dest="detectRenames", action="store_true"),1332# preserve the user, requires relevant p4 permissions1333 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1334 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1335 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1336 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1337 optparse.make_option("--conflict", dest="conflict_behavior",1338 choices=self.conflict_behavior_choices),1339 optparse.make_option("--branch", dest="branch"),1340 optparse.make_option("--shelve", dest="shelve", action="store_true",1341help="Shelve instead of submit. Shelved files are reverted, "1342"restoring the workspace to the state before the shelve"),1343 optparse.make_option("--update-shelve", dest="update_shelve", action="store",type="int",1344 metavar="CHANGELIST",1345help="update an existing shelved changelist, implies --shelve")1346]1347 self.description ="Submit changes from git to the perforce depot."1348 self.usage +=" [name of git branch to submit into perforce depot]"1349 self.origin =""1350 self.detectRenames =False1351 self.preserveUser =gitConfigBool("git-p4.preserveUser")1352 self.dry_run =False1353 self.shelve =False1354 self.update_shelve =None1355 self.prepare_p4_only =False1356 self.conflict_behavior =None1357 self.isWindows = (platform.system() =="Windows")1358 self.exportLabels =False1359 self.p4HasMoveCommand =p4_has_move_command()1360 self.branch =None13611362ifgitConfig('git-p4.largeFileSystem'):1363die("Large file system not supported for git-p4 submit command. Please remove it from config.")13641365defcheck(self):1366iflen(p4CmdList("opened ...")) >0:1367die("You have files opened with perforce! Close them before starting the sync.")13681369defseparate_jobs_from_description(self, message):1370"""Extract and return a possible Jobs field in the commit1371 message. It goes into a separate section in the p4 change1372 specification.13731374 A jobs line starts with "Jobs:" and looks like a new field1375 in a form. Values are white-space separated on the same1376 line or on following lines that start with a tab.13771378 This does not parse and extract the full git commit message1379 like a p4 form. It just sees the Jobs: line as a marker1380 to pass everything from then on directly into the p4 form,1381 but outside the description section.13821383 Return a tuple (stripped log message, jobs string)."""13841385 m = re.search(r'^Jobs:', message, re.MULTILINE)1386if m is None:1387return(message,None)13881389 jobtext = message[m.start():]1390 stripped_message = message[:m.start()].rstrip()1391return(stripped_message, jobtext)13921393defprepareLogMessage(self, template, message, jobs):1394"""Edits the template returned from "p4 change -o" to insert1395 the message in the Description field, and the jobs text in1396 the Jobs field."""1397 result =""13981399 inDescriptionSection =False14001401for line in template.split("\n"):1402if line.startswith("#"):1403 result += line +"\n"1404continue14051406if inDescriptionSection:1407if line.startswith("Files:")or line.startswith("Jobs:"):1408 inDescriptionSection =False1409# insert Jobs section1410if jobs:1411 result += jobs +"\n"1412else:1413continue1414else:1415if line.startswith("Description:"):1416 inDescriptionSection =True1417 line +="\n"1418for messageLine in message.split("\n"):1419 line +="\t"+ messageLine +"\n"14201421 result += line +"\n"14221423return result14241425defpatchRCSKeywords(self,file, pattern):1426# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1427(handle, outFileName) = tempfile.mkstemp(dir='.')1428try:1429 outFile = os.fdopen(handle,"w+")1430 inFile =open(file,"r")1431 regexp = re.compile(pattern, re.VERBOSE)1432for line in inFile.readlines():1433 line = regexp.sub(r'$\1$', line)1434 outFile.write(line)1435 inFile.close()1436 outFile.close()1437# Forcibly overwrite the original file1438 os.unlink(file)1439 shutil.move(outFileName,file)1440except:1441# cleanup our temporary file1442 os.unlink(outFileName)1443print"Failed to strip RCS keywords in%s"%file1444raise14451446print"Patched up RCS keywords in%s"%file14471448defp4UserForCommit(self,id):1449# Return the tuple (perforce user,git email) for a given git commit id1450 self.getUserMapFromPerforceServer()1451 gitEmail =read_pipe(["git","log","--max-count=1",1452"--format=%ae",id])1453 gitEmail = gitEmail.strip()1454if not self.emails.has_key(gitEmail):1455return(None,gitEmail)1456else:1457return(self.emails[gitEmail],gitEmail)14581459defcheckValidP4Users(self,commits):1460# check if any git authors cannot be mapped to p4 users1461foridin commits:1462(user,email) = self.p4UserForCommit(id)1463if not user:1464 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1465ifgitConfigBool("git-p4.allowMissingP4Users"):1466print"%s"% msg1467else:1468die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14691470deflastP4Changelist(self):1471# Get back the last changelist number submitted in this client spec. This1472# then gets used to patch up the username in the change. If the same1473# client spec is being used by multiple processes then this might go1474# wrong.1475 results =p4CmdList("client -o")# find the current client1476 client =None1477for r in results:1478if r.has_key('Client'):1479 client = r['Client']1480break1481if not client:1482die("could not get client spec")1483 results =p4CmdList(["changes","-c", client,"-m","1"])1484for r in results:1485if r.has_key('change'):1486return r['change']1487die("Could not get changelist number for last submit - cannot patch up user details")14881489defmodifyChangelistUser(self, changelist, newUser):1490# fixup the user field of a changelist after it has been submitted.1491 changes =p4CmdList("change -o%s"% changelist)1492iflen(changes) !=1:1493die("Bad output from p4 change modifying%sto user%s"%1494(changelist, newUser))14951496 c = changes[0]1497if c['User'] == newUser:return# nothing to do1498 c['User'] = newUser1499input= marshal.dumps(c)15001501 result =p4CmdList("change -f -i", stdin=input)1502for r in result:1503if r.has_key('code'):1504if r['code'] =='error':1505die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1506if r.has_key('data'):1507print("Updated user field for changelist%sto%s"% (changelist, newUser))1508return1509die("Could not modify user field of changelist%sto%s"% (changelist, newUser))15101511defcanChangeChangelists(self):1512# check to see if we have p4 admin or super-user permissions, either of1513# which are required to modify changelists.1514 results =p4CmdList(["protects", self.depotPath])1515for r in results:1516if r.has_key('perm'):1517if r['perm'] =='admin':1518return11519if r['perm'] =='super':1520return11521return015221523defprepareSubmitTemplate(self, changelist=None):1524"""Run "p4 change -o" to grab a change specification template.1525 This does not use "p4 -G", as it is nice to keep the submission1526 template in original order, since a human might edit it.15271528 Remove lines in the Files section that show changes to files1529 outside the depot path we're committing into."""15301531[upstream, settings] =findUpstreamBranchPoint()15321533 template ="""\1534# A Perforce Change Specification.1535#1536# Change: The change number. 'new' on a new changelist.1537# Date: The date this specification was last modified.1538# Client: The client on which the changelist was created. Read-only.1539# User: The user who created the changelist.1540# Status: Either 'pending' or 'submitted'. Read-only.1541# Type: Either 'public' or 'restricted'. Default is 'public'.1542# Description: Comments about the changelist. Required.1543# Jobs: What opened jobs are to be closed by this changelist.1544# You may delete jobs from this list. (New changelists only.)1545# Files: What opened files from the default changelist are to be added1546# to this changelist. You may delete files from this list.1547# (New changelists only.)1548"""1549 files_list = []1550 inFilesSection =False1551 change_entry =None1552 args = ['change','-o']1553if changelist:1554 args.append(str(changelist))1555for entry inp4CmdList(args):1556if not entry.has_key('code'):1557continue1558if entry['code'] =='stat':1559 change_entry = entry1560break1561if not change_entry:1562die('Failed to decode output of p4 change -o')1563for key, value in change_entry.iteritems():1564if key.startswith('File'):1565if settings.has_key('depot-paths'):1566if not[p for p in settings['depot-paths']1567ifp4PathStartsWith(value, p)]:1568continue1569else:1570if notp4PathStartsWith(value, self.depotPath):1571continue1572 files_list.append(value)1573continue1574# Output in the order expected by prepareLogMessage1575for key in['Change','Client','User','Status','Description','Jobs']:1576if not change_entry.has_key(key):1577continue1578 template +='\n'1579 template += key +':'1580if key =='Description':1581 template +='\n'1582for field_line in change_entry[key].splitlines():1583 template +='\t'+field_line+'\n'1584iflen(files_list) >0:1585 template +='\n'1586 template +='Files:\n'1587for path in files_list:1588 template +='\t'+path+'\n'1589return template15901591defedit_template(self, template_file):1592"""Invoke the editor to let the user change the submission1593 message. Return true if okay to continue with the submit."""15941595# if configured to skip the editing part, just submit1596ifgitConfigBool("git-p4.skipSubmitEdit"):1597return True15981599# look at the modification time, to check later if the user saved1600# the file1601 mtime = os.stat(template_file).st_mtime16021603# invoke the editor1604if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1605 editor = os.environ.get("P4EDITOR")1606else:1607 editor =read_pipe("git var GIT_EDITOR").strip()1608system(["sh","-c", ('%s"$@"'% editor), editor, template_file])16091610# If the file was not saved, prompt to see if this patch should1611# be skipped. But skip this verification step if configured so.1612ifgitConfigBool("git-p4.skipSubmitEditCheck"):1613return True16141615# modification time updated means user saved the file1616if os.stat(template_file).st_mtime > mtime:1617return True16181619while True:1620 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1621if response =='y':1622return True1623if response =='n':1624return False16251626defget_diff_description(self, editedFiles, filesToAdd, symlinks):1627# diff1628if os.environ.has_key("P4DIFF"):1629del(os.environ["P4DIFF"])1630 diff =""1631for editedFile in editedFiles:1632 diff +=p4_read_pipe(['diff','-du',1633wildcard_encode(editedFile)])16341635# new file diff1636 newdiff =""1637for newFile in filesToAdd:1638 newdiff +="==== new file ====\n"1639 newdiff +="--- /dev/null\n"1640 newdiff +="+++%s\n"% newFile16411642 is_link = os.path.islink(newFile)1643 expect_link = newFile in symlinks16441645if is_link and expect_link:1646 newdiff +="+%s\n"% os.readlink(newFile)1647else:1648 f =open(newFile,"r")1649for line in f.readlines():1650 newdiff +="+"+ line1651 f.close()16521653return(diff + newdiff).replace('\r\n','\n')16541655defapplyCommit(self,id):1656"""Apply one commit, return True if it succeeded."""16571658print"Applying",read_pipe(["git","show","-s",1659"--format=format:%h%s",id])16601661(p4User, gitEmail) = self.p4UserForCommit(id)16621663 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1664 filesToAdd =set()1665 filesToChangeType =set()1666 filesToDelete =set()1667 editedFiles =set()1668 pureRenameCopy =set()1669 symlinks =set()1670 filesToChangeExecBit = {}1671 all_files =list()16721673for line in diff:1674 diff =parseDiffTreeEntry(line)1675 modifier = diff['status']1676 path = diff['src']1677 all_files.append(path)16781679if modifier =="M":1680p4_edit(path)1681ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1682 filesToChangeExecBit[path] = diff['dst_mode']1683 editedFiles.add(path)1684elif modifier =="A":1685 filesToAdd.add(path)1686 filesToChangeExecBit[path] = diff['dst_mode']1687if path in filesToDelete:1688 filesToDelete.remove(path)16891690 dst_mode =int(diff['dst_mode'],8)1691if dst_mode ==0120000:1692 symlinks.add(path)16931694elif modifier =="D":1695 filesToDelete.add(path)1696if path in filesToAdd:1697 filesToAdd.remove(path)1698elif modifier =="C":1699 src, dest = diff['src'], diff['dst']1700p4_integrate(src, dest)1701 pureRenameCopy.add(dest)1702if diff['src_sha1'] != diff['dst_sha1']:1703p4_edit(dest)1704 pureRenameCopy.discard(dest)1705ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1706p4_edit(dest)1707 pureRenameCopy.discard(dest)1708 filesToChangeExecBit[dest] = diff['dst_mode']1709if self.isWindows:1710# turn off read-only attribute1711 os.chmod(dest, stat.S_IWRITE)1712 os.unlink(dest)1713 editedFiles.add(dest)1714elif modifier =="R":1715 src, dest = diff['src'], diff['dst']1716if self.p4HasMoveCommand:1717p4_edit(src)# src must be open before move1718p4_move(src, dest)# opens for (move/delete, move/add)1719else:1720p4_integrate(src, dest)1721if diff['src_sha1'] != diff['dst_sha1']:1722p4_edit(dest)1723else:1724 pureRenameCopy.add(dest)1725ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1726if not self.p4HasMoveCommand:1727p4_edit(dest)# with move: already open, writable1728 filesToChangeExecBit[dest] = diff['dst_mode']1729if not self.p4HasMoveCommand:1730if self.isWindows:1731 os.chmod(dest, stat.S_IWRITE)1732 os.unlink(dest)1733 filesToDelete.add(src)1734 editedFiles.add(dest)1735elif modifier =="T":1736 filesToChangeType.add(path)1737else:1738die("unknown modifier%sfor%s"% (modifier, path))17391740 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1741 patchcmd = diffcmd +" | git apply "1742 tryPatchCmd = patchcmd +"--check -"1743 applyPatchCmd = patchcmd +"--check --apply -"1744 patch_succeeded =True17451746if os.system(tryPatchCmd) !=0:1747 fixed_rcs_keywords =False1748 patch_succeeded =False1749print"Unfortunately applying the change failed!"17501751# Patch failed, maybe it's just RCS keyword woes. Look through1752# the patch to see if that's possible.1753ifgitConfigBool("git-p4.attemptRCSCleanup"):1754file=None1755 pattern =None1756 kwfiles = {}1757forfilein editedFiles | filesToDelete:1758# did this file's delta contain RCS keywords?1759 pattern =p4_keywords_regexp_for_file(file)17601761if pattern:1762# this file is a possibility...look for RCS keywords.1763 regexp = re.compile(pattern, re.VERBOSE)1764for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1765if regexp.search(line):1766if verbose:1767print"got keyword match on%sin%sin%s"% (pattern, line,file)1768 kwfiles[file] = pattern1769break17701771forfilein kwfiles:1772if verbose:1773print"zapping%swith%s"% (line,pattern)1774# File is being deleted, so not open in p4. Must1775# disable the read-only bit on windows.1776if self.isWindows andfilenot in editedFiles:1777 os.chmod(file, stat.S_IWRITE)1778 self.patchRCSKeywords(file, kwfiles[file])1779 fixed_rcs_keywords =True17801781if fixed_rcs_keywords:1782print"Retrying the patch with RCS keywords cleaned up"1783if os.system(tryPatchCmd) ==0:1784 patch_succeeded =True17851786if not patch_succeeded:1787for f in editedFiles:1788p4_revert(f)1789return False17901791#1792# Apply the patch for real, and do add/delete/+x handling.1793#1794system(applyPatchCmd)17951796for f in filesToChangeType:1797p4_edit(f,"-t","auto")1798for f in filesToAdd:1799p4_add(f)1800for f in filesToDelete:1801p4_revert(f)1802p4_delete(f)18031804# Set/clear executable bits1805for f in filesToChangeExecBit.keys():1806 mode = filesToChangeExecBit[f]1807setP4ExecBit(f, mode)18081809if self.update_shelve:1810print("all_files =%s"%str(all_files))1811p4_reopen_in_change(self.update_shelve, all_files)18121813#1814# Build p4 change description, starting with the contents1815# of the git commit message.1816#1817 logMessage =extractLogMessageFromGitCommit(id)1818 logMessage = logMessage.strip()1819(logMessage, jobs) = self.separate_jobs_from_description(logMessage)18201821 template = self.prepareSubmitTemplate(self.update_shelve)1822 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)18231824if self.preserveUser:1825 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User18261827if self.checkAuthorship and not self.p4UserIsMe(p4User):1828 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1829 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1830 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"18311832 separatorLine ="######## everything below this line is just the diff #######\n"1833if not self.prepare_p4_only:1834 submitTemplate += separatorLine1835 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)18361837(handle, fileName) = tempfile.mkstemp()1838 tmpFile = os.fdopen(handle,"w+b")1839if self.isWindows:1840 submitTemplate = submitTemplate.replace("\n","\r\n")1841 tmpFile.write(submitTemplate)1842 tmpFile.close()18431844if self.prepare_p4_only:1845#1846# Leave the p4 tree prepared, and the submit template around1847# and let the user decide what to do next1848#1849print1850print"P4 workspace prepared for submission."1851print"To submit or revert, go to client workspace"1852print" "+ self.clientPath1853print1854print"To submit, use\"p4 submit\"to write a new description,"1855print"or\"p4 submit -i <%s\"to use the one prepared by" \1856"\"git p4\"."% fileName1857print"You can delete the file\"%s\"when finished."% fileName18581859if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1860print"To preserve change ownership by user%s, you must\n" \1861"do\"p4 change -f <change>\"after submitting and\n" \1862"edit the User field."1863if pureRenameCopy:1864print"After submitting, renamed files must be re-synced."1865print"Invoke\"p4 sync -f\"on each of these files:"1866for f in pureRenameCopy:1867print" "+ f18681869print1870print"To revert the changes, use\"p4 revert ...\", and delete"1871print"the submit template file\"%s\""% fileName1872if filesToAdd:1873print"Since the commit adds new files, they must be deleted:"1874for f in filesToAdd:1875print" "+ f1876print1877return True18781879#1880# Let the user edit the change description, then submit it.1881#1882 submitted =False18831884try:1885if self.edit_template(fileName):1886# read the edited message and submit1887 tmpFile =open(fileName,"rb")1888 message = tmpFile.read()1889 tmpFile.close()1890if self.isWindows:1891 message = message.replace("\r\n","\n")1892 submitTemplate = message[:message.index(separatorLine)]18931894if self.update_shelve:1895p4_write_pipe(['shelve','-r','-i'], submitTemplate)1896elif self.shelve:1897p4_write_pipe(['shelve','-i'], submitTemplate)1898else:1899p4_write_pipe(['submit','-i'], submitTemplate)1900# The rename/copy happened by applying a patch that created a1901# new file. This leaves it writable, which confuses p4.1902for f in pureRenameCopy:1903p4_sync(f,"-f")19041905if self.preserveUser:1906if p4User:1907# Get last changelist number. Cannot easily get it from1908# the submit command output as the output is1909# unmarshalled.1910 changelist = self.lastP4Changelist()1911 self.modifyChangelistUser(changelist, p4User)19121913 submitted =True19141915finally:1916# skip this patch1917if not submitted or self.shelve:1918if self.shelve:1919print("Reverting shelved files.")1920else:1921print("Submission cancelled, undoing p4 changes.")1922for f in editedFiles | filesToDelete:1923p4_revert(f)1924for f in filesToAdd:1925p4_revert(f)1926 os.remove(f)19271928 os.remove(fileName)1929return submitted19301931# Export git tags as p4 labels. Create a p4 label and then tag1932# with that.1933defexportGitTags(self, gitTags):1934 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1935iflen(validLabelRegexp) ==0:1936 validLabelRegexp = defaultLabelRegexp1937 m = re.compile(validLabelRegexp)19381939for name in gitTags:19401941if not m.match(name):1942if verbose:1943print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1944continue19451946# Get the p4 commit this corresponds to1947 logMessage =extractLogMessageFromGitCommit(name)1948 values =extractSettingsGitLog(logMessage)19491950if not values.has_key('change'):1951# a tag pointing to something not sent to p4; ignore1952if verbose:1953print"git tag%sdoes not give a p4 commit"% name1954continue1955else:1956 changelist = values['change']19571958# Get the tag details.1959 inHeader =True1960 isAnnotated =False1961 body = []1962for l inread_pipe_lines(["git","cat-file","-p", name]):1963 l = l.strip()1964if inHeader:1965if re.match(r'tag\s+', l):1966 isAnnotated =True1967elif re.match(r'\s*$', l):1968 inHeader =False1969continue1970else:1971 body.append(l)19721973if not isAnnotated:1974 body = ["lightweight tag imported by git p4\n"]19751976# Create the label - use the same view as the client spec we are using1977 clientSpec =getClientSpec()19781979 labelTemplate ="Label:%s\n"% name1980 labelTemplate +="Description:\n"1981for b in body:1982 labelTemplate +="\t"+ b +"\n"1983 labelTemplate +="View:\n"1984for depot_side in clientSpec.mappings:1985 labelTemplate +="\t%s\n"% depot_side19861987if self.dry_run:1988print"Would create p4 label%sfor tag"% name1989elif self.prepare_p4_only:1990print"Not creating p4 label%sfor tag due to option" \1991" --prepare-p4-only"% name1992else:1993p4_write_pipe(["label","-i"], labelTemplate)19941995# Use the label1996p4_system(["tag","-l", name] +1997["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])19981999if verbose:2000print"created p4 label for tag%s"% name20012002defrun(self, args):2003iflen(args) ==0:2004 self.master =currentGitBranch()2005eliflen(args) ==1:2006 self.master = args[0]2007if notbranchExists(self.master):2008die("Branch%sdoes not exist"% self.master)2009else:2010return False20112012if self.master:2013 allowSubmit =gitConfig("git-p4.allowSubmit")2014iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2015die("%sis not in git-p4.allowSubmit"% self.master)20162017[upstream, settings] =findUpstreamBranchPoint()2018 self.depotPath = settings['depot-paths'][0]2019iflen(self.origin) ==0:2020 self.origin = upstream20212022if self.update_shelve:2023 self.shelve =True20242025if self.preserveUser:2026if not self.canChangeChangelists():2027die("Cannot preserve user names without p4 super-user or admin permissions")20282029# if not set from the command line, try the config file2030if self.conflict_behavior is None:2031 val =gitConfig("git-p4.conflict")2032if val:2033if val not in self.conflict_behavior_choices:2034die("Invalid value '%s' for config git-p4.conflict"% val)2035else:2036 val ="ask"2037 self.conflict_behavior = val20382039if self.verbose:2040print"Origin branch is "+ self.origin20412042iflen(self.depotPath) ==0:2043print"Internal error: cannot locate perforce depot path from existing branches"2044 sys.exit(128)20452046 self.useClientSpec =False2047ifgitConfigBool("git-p4.useclientspec"):2048 self.useClientSpec =True2049if self.useClientSpec:2050 self.clientSpecDirs =getClientSpec()20512052# Check for the existence of P4 branches2053 branchesDetected = (len(p4BranchesInGit().keys()) >1)20542055if self.useClientSpec and not branchesDetected:2056# all files are relative to the client spec2057 self.clientPath =getClientRoot()2058else:2059 self.clientPath =p4Where(self.depotPath)20602061if self.clientPath =="":2062die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)20632064print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2065 self.oldWorkingDirectory = os.getcwd()20662067# ensure the clientPath exists2068 new_client_dir =False2069if not os.path.exists(self.clientPath):2070 new_client_dir =True2071 os.makedirs(self.clientPath)20722073chdir(self.clientPath, is_client_path=True)2074if self.dry_run:2075print"Would synchronize p4 checkout in%s"% self.clientPath2076else:2077print"Synchronizing p4 checkout..."2078if new_client_dir:2079# old one was destroyed, and maybe nobody told p42080p4_sync("...","-f")2081else:2082p4_sync("...")2083 self.check()20842085 commits = []2086if self.master:2087 commitish = self.master2088else:2089 commitish ='HEAD'20902091for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2092 commits.append(line.strip())2093 commits.reverse()20942095if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2096 self.checkAuthorship =False2097else:2098 self.checkAuthorship =True20992100if self.preserveUser:2101 self.checkValidP4Users(commits)21022103#2104# Build up a set of options to be passed to diff when2105# submitting each commit to p4.2106#2107if self.detectRenames:2108# command-line -M arg2109 self.diffOpts ="-M"2110else:2111# If not explicitly set check the config variable2112 detectRenames =gitConfig("git-p4.detectRenames")21132114if detectRenames.lower() =="false"or detectRenames =="":2115 self.diffOpts =""2116elif detectRenames.lower() =="true":2117 self.diffOpts ="-M"2118else:2119 self.diffOpts ="-M%s"% detectRenames21202121# no command-line arg for -C or --find-copies-harder, just2122# config variables2123 detectCopies =gitConfig("git-p4.detectCopies")2124if detectCopies.lower() =="false"or detectCopies =="":2125pass2126elif detectCopies.lower() =="true":2127 self.diffOpts +=" -C"2128else:2129 self.diffOpts +=" -C%s"% detectCopies21302131ifgitConfigBool("git-p4.detectCopiesHarder"):2132 self.diffOpts +=" --find-copies-harder"21332134#2135# Apply the commits, one at a time. On failure, ask if should2136# continue to try the rest of the patches, or quit.2137#2138if self.dry_run:2139print"Would apply"2140 applied = []2141 last =len(commits) -12142for i, commit inenumerate(commits):2143if self.dry_run:2144print" ",read_pipe(["git","show","-s",2145"--format=format:%h%s", commit])2146 ok =True2147else:2148 ok = self.applyCommit(commit)2149if ok:2150 applied.append(commit)2151else:2152if self.prepare_p4_only and i < last:2153print"Processing only the first commit due to option" \2154" --prepare-p4-only"2155break2156if i < last:2157 quit =False2158while True:2159# prompt for what to do, or use the option/variable2160if self.conflict_behavior =="ask":2161print"What do you want to do?"2162 response =raw_input("[s]kip this commit but apply"2163" the rest, or [q]uit? ")2164if not response:2165continue2166elif self.conflict_behavior =="skip":2167 response ="s"2168elif self.conflict_behavior =="quit":2169 response ="q"2170else:2171die("Unknown conflict_behavior '%s'"%2172 self.conflict_behavior)21732174if response[0] =="s":2175print"Skipping this commit, but applying the rest"2176break2177if response[0] =="q":2178print"Quitting"2179 quit =True2180break2181if quit:2182break21832184chdir(self.oldWorkingDirectory)2185 shelved_applied ="shelved"if self.shelve else"applied"2186if self.dry_run:2187pass2188elif self.prepare_p4_only:2189pass2190eliflen(commits) ==len(applied):2191print("All commits{0}!".format(shelved_applied))21922193 sync =P4Sync()2194if self.branch:2195 sync.branch = self.branch2196 sync.run([])21972198 rebase =P4Rebase()2199 rebase.rebase()22002201else:2202iflen(applied) ==0:2203print("No commits{0}.".format(shelved_applied))2204else:2205print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2206for c in commits:2207if c in applied:2208 star ="*"2209else:2210 star =" "2211print star,read_pipe(["git","show","-s",2212"--format=format:%h%s", c])2213print"You will have to do 'git p4 sync' and rebase."22142215ifgitConfigBool("git-p4.exportLabels"):2216 self.exportLabels =True22172218if self.exportLabels:2219 p4Labels =getP4Labels(self.depotPath)2220 gitTags =getGitTags()22212222 missingGitTags = gitTags - p4Labels2223 self.exportGitTags(missingGitTags)22242225# exit with error unless everything applied perfectly2226iflen(commits) !=len(applied):2227 sys.exit(1)22282229return True22302231classView(object):2232"""Represent a p4 view ("p4 help views"), and map files in a2233 repo according to the view."""22342235def__init__(self, client_name):2236 self.mappings = []2237 self.client_prefix ="//%s/"% client_name2238# cache results of "p4 where" to lookup client file locations2239 self.client_spec_path_cache = {}22402241defappend(self, view_line):2242"""Parse a view line, splitting it into depot and client2243 sides. Append to self.mappings, preserving order. This2244 is only needed for tag creation."""22452246# Split the view line into exactly two words. P4 enforces2247# structure on these lines that simplifies this quite a bit.2248#2249# Either or both words may be double-quoted.2250# Single quotes do not matter.2251# Double-quote marks cannot occur inside the words.2252# A + or - prefix is also inside the quotes.2253# There are no quotes unless they contain a space.2254# The line is already white-space stripped.2255# The two words are separated by a single space.2256#2257if view_line[0] =='"':2258# First word is double quoted. Find its end.2259 close_quote_index = view_line.find('"',1)2260if close_quote_index <=0:2261die("No first-word closing quote found:%s"% view_line)2262 depot_side = view_line[1:close_quote_index]2263# skip closing quote and space2264 rhs_index = close_quote_index +1+12265else:2266 space_index = view_line.find(" ")2267if space_index <=0:2268die("No word-splitting space found:%s"% view_line)2269 depot_side = view_line[0:space_index]2270 rhs_index = space_index +122712272# prefix + means overlay on previous mapping2273if depot_side.startswith("+"):2274 depot_side = depot_side[1:]22752276# prefix - means exclude this path, leave out of mappings2277 exclude =False2278if depot_side.startswith("-"):2279 exclude =True2280 depot_side = depot_side[1:]22812282if not exclude:2283 self.mappings.append(depot_side)22842285defconvert_client_path(self, clientFile):2286# chop off //client/ part to make it relative2287if not clientFile.startswith(self.client_prefix):2288die("No prefix '%s' on clientFile '%s'"%2289(self.client_prefix, clientFile))2290return clientFile[len(self.client_prefix):]22912292defupdate_client_spec_path_cache(self, files):2293""" Caching file paths by "p4 where" batch query """22942295# List depot file paths exclude that already cached2296 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]22972298iflen(fileArgs) ==0:2299return# All files in cache23002301 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2302for res in where_result:2303if"code"in res and res["code"] =="error":2304# assume error is "... file(s) not in client view"2305continue2306if"clientFile"not in res:2307die("No clientFile in 'p4 where' output")2308if"unmap"in res:2309# it will list all of them, but only one not unmap-ped2310continue2311ifgitConfigBool("core.ignorecase"):2312 res['depotFile'] = res['depotFile'].lower()2313 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])23142315# not found files or unmap files set to ""2316for depotFile in fileArgs:2317ifgitConfigBool("core.ignorecase"):2318 depotFile = depotFile.lower()2319if depotFile not in self.client_spec_path_cache:2320 self.client_spec_path_cache[depotFile] =""23212322defmap_in_client(self, depot_path):2323"""Return the relative location in the client where this2324 depot file should live. Returns "" if the file should2325 not be mapped in the client."""23262327ifgitConfigBool("core.ignorecase"):2328 depot_path = depot_path.lower()23292330if depot_path in self.client_spec_path_cache:2331return self.client_spec_path_cache[depot_path]23322333die("Error:%sis not found in client spec path"% depot_path )2334return""23352336classP4Sync(Command, P4UserMap):2337 delete_actions = ("delete","move/delete","purge")23382339def__init__(self):2340 Command.__init__(self)2341 P4UserMap.__init__(self)2342 self.options = [2343 optparse.make_option("--branch", dest="branch"),2344 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2345 optparse.make_option("--changesfile", dest="changesFile"),2346 optparse.make_option("--silent", dest="silent", action="store_true"),2347 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2348 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2349 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2350help="Import into refs/heads/ , not refs/remotes"),2351 optparse.make_option("--max-changes", dest="maxChanges",2352help="Maximum number of changes to import"),2353 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2354help="Internal block size to use when iteratively calling p4 changes"),2355 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2356help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2357 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2358help="Only sync files that are included in the Perforce Client Spec"),2359 optparse.make_option("-/", dest="cloneExclude",2360 action="append",type="string",2361help="exclude depot path"),2362]2363 self.description ="""Imports from Perforce into a git repository.\n2364 example:2365 //depot/my/project/ -- to import the current head2366 //depot/my/project/@all -- to import everything2367 //depot/my/project/@1,6 -- to import only from revision 1 to 623682369 (a ... is not needed in the path p4 specification, it's added implicitly)"""23702371 self.usage +=" //depot/path[@revRange]"2372 self.silent =False2373 self.createdBranches =set()2374 self.committedChanges =set()2375 self.branch =""2376 self.detectBranches =False2377 self.detectLabels =False2378 self.importLabels =False2379 self.changesFile =""2380 self.syncWithOrigin =True2381 self.importIntoRemotes =True2382 self.maxChanges =""2383 self.changes_block_size =None2384 self.keepRepoPath =False2385 self.depotPaths =None2386 self.p4BranchesInGit = []2387 self.cloneExclude = []2388 self.useClientSpec =False2389 self.useClientSpec_from_options =False2390 self.clientSpecDirs =None2391 self.tempBranches = []2392 self.tempBranchLocation ="refs/git-p4-tmp"2393 self.largeFileSystem =None23942395ifgitConfig('git-p4.largeFileSystem'):2396 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2397 self.largeFileSystem =largeFileSystemConstructor(2398lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2399)24002401ifgitConfig("git-p4.syncFromOrigin") =="false":2402 self.syncWithOrigin =False24032404# This is required for the "append" cloneExclude action2405defensure_value(self, attr, value):2406if nothasattr(self, attr)orgetattr(self, attr)is None:2407setattr(self, attr, value)2408returngetattr(self, attr)24092410# Force a checkpoint in fast-import and wait for it to finish2411defcheckpoint(self):2412 self.gitStream.write("checkpoint\n\n")2413 self.gitStream.write("progress checkpoint\n\n")2414 out = self.gitOutput.readline()2415if self.verbose:2416print"checkpoint finished: "+ out24172418defextractFilesFromCommit(self, commit):2419 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2420for path in self.cloneExclude]2421 files = []2422 fnum =02423while commit.has_key("depotFile%s"% fnum):2424 path = commit["depotFile%s"% fnum]24252426if[p for p in self.cloneExclude2427ifp4PathStartsWith(path, p)]:2428 found =False2429else:2430 found = [p for p in self.depotPaths2431ifp4PathStartsWith(path, p)]2432if not found:2433 fnum = fnum +12434continue24352436file= {}2437file["path"] = path2438file["rev"] = commit["rev%s"% fnum]2439file["action"] = commit["action%s"% fnum]2440file["type"] = commit["type%s"% fnum]2441 files.append(file)2442 fnum = fnum +12443return files24442445defextractJobsFromCommit(self, commit):2446 jobs = []2447 jnum =02448while commit.has_key("job%s"% jnum):2449 job = commit["job%s"% jnum]2450 jobs.append(job)2451 jnum = jnum +12452return jobs24532454defstripRepoPath(self, path, prefixes):2455"""When streaming files, this is called to map a p4 depot path2456 to where it should go in git. The prefixes are either2457 self.depotPaths, or self.branchPrefixes in the case of2458 branch detection."""24592460if self.useClientSpec:2461# branch detection moves files up a level (the branch name)2462# from what client spec interpretation gives2463 path = self.clientSpecDirs.map_in_client(path)2464if self.detectBranches:2465for b in self.knownBranches:2466if path.startswith(b +"/"):2467 path = path[len(b)+1:]24682469elif self.keepRepoPath:2470# Preserve everything in relative path name except leading2471# //depot/; just look at first prefix as they all should2472# be in the same depot.2473 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2474ifp4PathStartsWith(path, depot):2475 path = path[len(depot):]24762477else:2478for p in prefixes:2479ifp4PathStartsWith(path, p):2480 path = path[len(p):]2481break24822483 path =wildcard_decode(path)2484return path24852486defsplitFilesIntoBranches(self, commit):2487"""Look at each depotFile in the commit to figure out to what2488 branch it belongs."""24892490if self.clientSpecDirs:2491 files = self.extractFilesFromCommit(commit)2492 self.clientSpecDirs.update_client_spec_path_cache(files)24932494 branches = {}2495 fnum =02496while commit.has_key("depotFile%s"% fnum):2497 path = commit["depotFile%s"% fnum]2498 found = [p for p in self.depotPaths2499ifp4PathStartsWith(path, p)]2500if not found:2501 fnum = fnum +12502continue25032504file= {}2505file["path"] = path2506file["rev"] = commit["rev%s"% fnum]2507file["action"] = commit["action%s"% fnum]2508file["type"] = commit["type%s"% fnum]2509 fnum = fnum +125102511# start with the full relative path where this file would2512# go in a p4 client2513if self.useClientSpec:2514 relPath = self.clientSpecDirs.map_in_client(path)2515else:2516 relPath = self.stripRepoPath(path, self.depotPaths)25172518for branch in self.knownBranches.keys():2519# add a trailing slash so that a commit into qt/4.2foo2520# doesn't end up in qt/4.2, e.g.2521if relPath.startswith(branch +"/"):2522if branch not in branches:2523 branches[branch] = []2524 branches[branch].append(file)2525break25262527return branches25282529defwriteToGitStream(self, gitMode, relPath, contents):2530 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2531 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2532for d in contents:2533 self.gitStream.write(d)2534 self.gitStream.write('\n')25352536defencodeWithUTF8(self, path):2537try:2538 path.decode('ascii')2539except:2540 encoding ='utf8'2541ifgitConfig('git-p4.pathEncoding'):2542 encoding =gitConfig('git-p4.pathEncoding')2543 path = path.decode(encoding,'replace').encode('utf8','replace')2544if self.verbose:2545print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2546return path25472548# output one file from the P4 stream2549# - helper for streamP4Files25502551defstreamOneP4File(self,file, contents):2552 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2553 relPath = self.encodeWithUTF8(relPath)2554if verbose:2555 size =int(self.stream_file['fileSize'])2556 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2557 sys.stdout.flush()25582559(type_base, type_mods) =split_p4_type(file["type"])25602561 git_mode ="100644"2562if"x"in type_mods:2563 git_mode ="100755"2564if type_base =="symlink":2565 git_mode ="120000"2566# p4 print on a symlink sometimes contains "target\n";2567# if it does, remove the newline2568 data =''.join(contents)2569if not data:2570# Some version of p4 allowed creating a symlink that pointed2571# to nothing. This causes p4 errors when checking out such2572# a change, and errors here too. Work around it by ignoring2573# the bad symlink; hopefully a future change fixes it.2574print"\nIgnoring empty symlink in%s"%file['depotFile']2575return2576elif data[-1] =='\n':2577 contents = [data[:-1]]2578else:2579 contents = [data]25802581if type_base =="utf16":2582# p4 delivers different text in the python output to -G2583# than it does when using "print -o", or normal p4 client2584# operations. utf16 is converted to ascii or utf8, perhaps.2585# But ascii text saved as -t utf16 is completely mangled.2586# Invoke print -o to get the real contents.2587#2588# On windows, the newlines will always be mangled by print, so put2589# them back too. This is not needed to the cygwin windows version,2590# just the native "NT" type.2591#2592try:2593 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2594exceptExceptionas e:2595if'Translation of file content failed'instr(e):2596 type_base ='binary'2597else:2598raise e2599else:2600ifp4_version_string().find('/NT') >=0:2601 text = text.replace('\r\n','\n')2602 contents = [ text ]26032604if type_base =="apple":2605# Apple filetype files will be streamed as a concatenation of2606# its appledouble header and the contents. This is useless2607# on both macs and non-macs. If using "print -q -o xx", it2608# will create "xx" with the data, and "%xx" with the header.2609# This is also not very useful.2610#2611# Ideally, someday, this script can learn how to generate2612# appledouble files directly and import those to git, but2613# non-mac machines can never find a use for apple filetype.2614print"\nIgnoring apple filetype file%s"%file['depotFile']2615return26162617# Note that we do not try to de-mangle keywords on utf16 files,2618# even though in theory somebody may want that.2619 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2620if pattern:2621 regexp = re.compile(pattern, re.VERBOSE)2622 text =''.join(contents)2623 text = regexp.sub(r'$\1$', text)2624 contents = [ text ]26252626if self.largeFileSystem:2627(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)26282629 self.writeToGitStream(git_mode, relPath, contents)26302631defstreamOneP4Deletion(self,file):2632 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2633 relPath = self.encodeWithUTF8(relPath)2634if verbose:2635 sys.stdout.write("delete%s\n"% relPath)2636 sys.stdout.flush()2637 self.gitStream.write("D%s\n"% relPath)26382639if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2640 self.largeFileSystem.removeLargeFile(relPath)26412642# handle another chunk of streaming data2643defstreamP4FilesCb(self, marshalled):26442645# catch p4 errors and complain2646 err =None2647if"code"in marshalled:2648if marshalled["code"] =="error":2649if"data"in marshalled:2650 err = marshalled["data"].rstrip()26512652if not err and'fileSize'in self.stream_file:2653 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2654if required_bytes >0:2655 err ='Not enough space left on%s! Free at least%iMB.'% (2656 os.getcwd(), required_bytes/1024/10242657)26582659if err:2660 f =None2661if self.stream_have_file_info:2662if"depotFile"in self.stream_file:2663 f = self.stream_file["depotFile"]2664# force a failure in fast-import, else an empty2665# commit will be made2666 self.gitStream.write("\n")2667 self.gitStream.write("die-now\n")2668 self.gitStream.close()2669# ignore errors, but make sure it exits first2670 self.importProcess.wait()2671if f:2672die("Error from p4 print for%s:%s"% (f, err))2673else:2674die("Error from p4 print:%s"% err)26752676if marshalled.has_key('depotFile')and self.stream_have_file_info:2677# start of a new file - output the old one first2678 self.streamOneP4File(self.stream_file, self.stream_contents)2679 self.stream_file = {}2680 self.stream_contents = []2681 self.stream_have_file_info =False26822683# pick up the new file information... for the2684# 'data' field we need to append to our array2685for k in marshalled.keys():2686if k =='data':2687if'streamContentSize'not in self.stream_file:2688 self.stream_file['streamContentSize'] =02689 self.stream_file['streamContentSize'] +=len(marshalled['data'])2690 self.stream_contents.append(marshalled['data'])2691else:2692 self.stream_file[k] = marshalled[k]26932694if(verbose and2695'streamContentSize'in self.stream_file and2696'fileSize'in self.stream_file and2697'depotFile'in self.stream_file):2698 size =int(self.stream_file["fileSize"])2699if size >0:2700 progress =100*self.stream_file['streamContentSize']/size2701 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2702 sys.stdout.flush()27032704 self.stream_have_file_info =True27052706# Stream directly from "p4 files" into "git fast-import"2707defstreamP4Files(self, files):2708 filesForCommit = []2709 filesToRead = []2710 filesToDelete = []27112712for f in files:2713 filesForCommit.append(f)2714if f['action']in self.delete_actions:2715 filesToDelete.append(f)2716else:2717 filesToRead.append(f)27182719# deleted files...2720for f in filesToDelete:2721 self.streamOneP4Deletion(f)27222723iflen(filesToRead) >0:2724 self.stream_file = {}2725 self.stream_contents = []2726 self.stream_have_file_info =False27272728# curry self argument2729defstreamP4FilesCbSelf(entry):2730 self.streamP4FilesCb(entry)27312732 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]27332734p4CmdList(["-x","-","print"],2735 stdin=fileArgs,2736 cb=streamP4FilesCbSelf)27372738# do the last chunk2739if self.stream_file.has_key('depotFile'):2740 self.streamOneP4File(self.stream_file, self.stream_contents)27412742defmake_email(self, userid):2743if userid in self.users:2744return self.users[userid]2745else:2746return"%s<a@b>"% userid27472748defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2749""" Stream a p4 tag.2750 commit is either a git commit, or a fast-import mark, ":<p4commit>"2751 """27522753if verbose:2754print"writing tag%sfor commit%s"% (labelName, commit)2755 gitStream.write("tag%s\n"% labelName)2756 gitStream.write("from%s\n"% commit)27572758if labelDetails.has_key('Owner'):2759 owner = labelDetails["Owner"]2760else:2761 owner =None27622763# Try to use the owner of the p4 label, or failing that,2764# the current p4 user id.2765if owner:2766 email = self.make_email(owner)2767else:2768 email = self.make_email(self.p4UserId())2769 tagger ="%s %s %s"% (email, epoch, self.tz)27702771 gitStream.write("tagger%s\n"% tagger)27722773print"labelDetails=",labelDetails2774if labelDetails.has_key('Description'):2775 description = labelDetails['Description']2776else:2777 description ='Label from git p4'27782779 gitStream.write("data%d\n"%len(description))2780 gitStream.write(description)2781 gitStream.write("\n")27822783definClientSpec(self, path):2784if not self.clientSpecDirs:2785return True2786 inClientSpec = self.clientSpecDirs.map_in_client(path)2787if not inClientSpec and self.verbose:2788print('Ignoring file outside of client spec:{0}'.format(path))2789return inClientSpec27902791defhasBranchPrefix(self, path):2792if not self.branchPrefixes:2793return True2794 hasPrefix = [p for p in self.branchPrefixes2795ifp4PathStartsWith(path, p)]2796if not hasPrefix and self.verbose:2797print('Ignoring file outside of prefix:{0}'.format(path))2798return hasPrefix27992800defcommit(self, details, files, branch, parent =""):2801 epoch = details["time"]2802 author = details["user"]2803 jobs = self.extractJobsFromCommit(details)28042805if self.verbose:2806print('commit into{0}'.format(branch))28072808if self.clientSpecDirs:2809 self.clientSpecDirs.update_client_spec_path_cache(files)28102811 files = [f for f in files2812if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]28132814if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2815print('Ignoring revision{0}as it would produce an empty commit.'2816.format(details['change']))2817return28182819 self.gitStream.write("commit%s\n"% branch)2820 self.gitStream.write("mark :%s\n"% details["change"])2821 self.committedChanges.add(int(details["change"]))2822 committer =""2823if author not in self.users:2824 self.getUserMapFromPerforceServer()2825 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)28262827 self.gitStream.write("committer%s\n"% committer)28282829 self.gitStream.write("data <<EOT\n")2830 self.gitStream.write(details["desc"])2831iflen(jobs) >0:2832 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2833 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2834(','.join(self.branchPrefixes), details["change"]))2835iflen(details['options']) >0:2836 self.gitStream.write(": options =%s"% details['options'])2837 self.gitStream.write("]\nEOT\n\n")28382839iflen(parent) >0:2840if self.verbose:2841print"parent%s"% parent2842 self.gitStream.write("from%s\n"% parent)28432844 self.streamP4Files(files)2845 self.gitStream.write("\n")28462847 change =int(details["change"])28482849if self.labels.has_key(change):2850 label = self.labels[change]2851 labelDetails = label[0]2852 labelRevisions = label[1]2853if self.verbose:2854print"Change%sis labelled%s"% (change, labelDetails)28552856 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2857for p in self.branchPrefixes])28582859iflen(files) ==len(labelRevisions):28602861 cleanedFiles = {}2862for info in files:2863if info["action"]in self.delete_actions:2864continue2865 cleanedFiles[info["depotFile"]] = info["rev"]28662867if cleanedFiles == labelRevisions:2868 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)28692870else:2871if not self.silent:2872print("Tag%sdoes not match with change%s: files do not match."2873% (labelDetails["label"], change))28742875else:2876if not self.silent:2877print("Tag%sdoes not match with change%s: file count is different."2878% (labelDetails["label"], change))28792880# Build a dictionary of changelists and labels, for "detect-labels" option.2881defgetLabels(self):2882 self.labels = {}28832884 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2885iflen(l) >0and not self.silent:2886print"Finding files belonging to labels in%s"% `self.depotPaths`28872888for output in l:2889 label = output["label"]2890 revisions = {}2891 newestChange =02892if self.verbose:2893print"Querying files for label%s"% label2894forfileinp4CmdList(["files"] +2895["%s...@%s"% (p, label)2896for p in self.depotPaths]):2897 revisions[file["depotFile"]] =file["rev"]2898 change =int(file["change"])2899if change > newestChange:2900 newestChange = change29012902 self.labels[newestChange] = [output, revisions]29032904if self.verbose:2905print"Label changes:%s"% self.labels.keys()29062907# Import p4 labels as git tags. A direct mapping does not2908# exist, so assume that if all the files are at the same revision2909# then we can use that, or it's something more complicated we should2910# just ignore.2911defimportP4Labels(self, stream, p4Labels):2912if verbose:2913print"import p4 labels: "+' '.join(p4Labels)29142915 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2916 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2917iflen(validLabelRegexp) ==0:2918 validLabelRegexp = defaultLabelRegexp2919 m = re.compile(validLabelRegexp)29202921for name in p4Labels:2922 commitFound =False29232924if not m.match(name):2925if verbose:2926print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2927continue29282929if name in ignoredP4Labels:2930continue29312932 labelDetails =p4CmdList(['label',"-o", name])[0]29332934# get the most recent changelist for each file in this label2935 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2936for p in self.depotPaths])29372938if change.has_key('change'):2939# find the corresponding git commit; take the oldest commit2940 changelist =int(change['change'])2941if changelist in self.committedChanges:2942 gitCommit =":%d"% changelist # use a fast-import mark2943 commitFound =True2944else:2945 gitCommit =read_pipe(["git","rev-list","--max-count=1",2946"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2947iflen(gitCommit) ==0:2948print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2949else:2950 commitFound =True2951 gitCommit = gitCommit.strip()29522953if commitFound:2954# Convert from p4 time format2955try:2956 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2957exceptValueError:2958print"Could not convert label time%s"% labelDetails['Update']2959 tmwhen =129602961 when =int(time.mktime(tmwhen))2962 self.streamTag(stream, name, labelDetails, gitCommit, when)2963if verbose:2964print"p4 label%smapped to git commit%s"% (name, gitCommit)2965else:2966if verbose:2967print"Label%shas no changelists - possibly deleted?"% name29682969if not commitFound:2970# We can't import this label; don't try again as it will get very2971# expensive repeatedly fetching all the files for labels that will2972# never be imported. If the label is moved in the future, the2973# ignore will need to be removed manually.2974system(["git","config","--add","git-p4.ignoredP4Labels", name])29752976defguessProjectName(self):2977for p in self.depotPaths:2978if p.endswith("/"):2979 p = p[:-1]2980 p = p[p.strip().rfind("/") +1:]2981if not p.endswith("/"):2982 p +="/"2983return p29842985defgetBranchMapping(self):2986 lostAndFoundBranches =set()29872988 user =gitConfig("git-p4.branchUser")2989iflen(user) >0:2990 command ="branches -u%s"% user2991else:2992 command ="branches"29932994for info inp4CmdList(command):2995 details =p4Cmd(["branch","-o", info["branch"]])2996 viewIdx =02997while details.has_key("View%s"% viewIdx):2998 paths = details["View%s"% viewIdx].split(" ")2999 viewIdx = viewIdx +13000# require standard //depot/foo/... //depot/bar/... mapping3001iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3002continue3003 source = paths[0]3004 destination = paths[1]3005## HACK3006ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3007 source = source[len(self.depotPaths[0]):-4]3008 destination = destination[len(self.depotPaths[0]):-4]30093010if destination in self.knownBranches:3011if not self.silent:3012print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)3013print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)3014continue30153016 self.knownBranches[destination] = source30173018 lostAndFoundBranches.discard(destination)30193020if source not in self.knownBranches:3021 lostAndFoundBranches.add(source)30223023# Perforce does not strictly require branches to be defined, so we also3024# check git config for a branch list.3025#3026# Example of branch definition in git config file:3027# [git-p4]3028# branchList=main:branchA3029# branchList=main:branchB3030# branchList=branchA:branchC3031 configBranches =gitConfigList("git-p4.branchList")3032for branch in configBranches:3033if branch:3034(source, destination) = branch.split(":")3035 self.knownBranches[destination] = source30363037 lostAndFoundBranches.discard(destination)30383039if source not in self.knownBranches:3040 lostAndFoundBranches.add(source)304130423043for branch in lostAndFoundBranches:3044 self.knownBranches[branch] = branch30453046defgetBranchMappingFromGitBranches(self):3047 branches =p4BranchesInGit(self.importIntoRemotes)3048for branch in branches.keys():3049if branch =="master":3050 branch ="main"3051else:3052 branch = branch[len(self.projectName):]3053 self.knownBranches[branch] = branch30543055defupdateOptionDict(self, d):3056 option_keys = {}3057if self.keepRepoPath:3058 option_keys['keepRepoPath'] =130593060 d["options"] =' '.join(sorted(option_keys.keys()))30613062defreadOptions(self, d):3063 self.keepRepoPath = (d.has_key('options')3064and('keepRepoPath'in d['options']))30653066defgitRefForBranch(self, branch):3067if branch =="main":3068return self.refPrefix +"master"30693070iflen(branch) <=0:3071return branch30723073return self.refPrefix + self.projectName + branch30743075defgitCommitByP4Change(self, ref, change):3076if self.verbose:3077print"looking in ref "+ ref +" for change%susing bisect..."% change30783079 earliestCommit =""3080 latestCommit =parseRevision(ref)30813082while True:3083if self.verbose:3084print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3085 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3086iflen(next) ==0:3087if self.verbose:3088print"argh"3089return""3090 log =extractLogMessageFromGitCommit(next)3091 settings =extractSettingsGitLog(log)3092 currentChange =int(settings['change'])3093if self.verbose:3094print"current change%s"% currentChange30953096if currentChange == change:3097if self.verbose:3098print"found%s"% next3099return next31003101if currentChange < change:3102 earliestCommit ="^%s"% next3103else:3104 latestCommit ="%s"% next31053106return""31073108defimportNewBranch(self, branch, maxChange):3109# make fast-import flush all changes to disk and update the refs using the checkpoint3110# command so that we can try to find the branch parent in the git history3111 self.gitStream.write("checkpoint\n\n");3112 self.gitStream.flush();3113 branchPrefix = self.depotPaths[0] + branch +"/"3114range="@1,%s"% maxChange3115#print "prefix" + branchPrefix3116 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3117iflen(changes) <=0:3118return False3119 firstChange = changes[0]3120#print "first change in branch: %s" % firstChange3121 sourceBranch = self.knownBranches[branch]3122 sourceDepotPath = self.depotPaths[0] + sourceBranch3123 sourceRef = self.gitRefForBranch(sourceBranch)3124#print "source " + sourceBranch31253126 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3127#print "branch parent: %s" % branchParentChange3128 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3129iflen(gitParent) >0:3130 self.initialParents[self.gitRefForBranch(branch)] = gitParent3131#print "parent git commit: %s" % gitParent31323133 self.importChanges(changes)3134return True31353136defsearchParent(self, parent, branch, target):3137 parentFound =False3138for blob inread_pipe_lines(["git","rev-list","--reverse",3139"--no-merges", parent]):3140 blob = blob.strip()3141iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3142 parentFound =True3143if self.verbose:3144print"Found parent of%sin commit%s"% (branch, blob)3145break3146if parentFound:3147return blob3148else:3149return None31503151defimportChanges(self, changes):3152 cnt =13153for change in changes:3154 description =p4_describe(change)3155 self.updateOptionDict(description)31563157if not self.silent:3158 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3159 sys.stdout.flush()3160 cnt = cnt +131613162try:3163if self.detectBranches:3164 branches = self.splitFilesIntoBranches(description)3165for branch in branches.keys():3166## HACK --hwn3167 branchPrefix = self.depotPaths[0] + branch +"/"3168 self.branchPrefixes = [ branchPrefix ]31693170 parent =""31713172 filesForCommit = branches[branch]31733174if self.verbose:3175print"branch is%s"% branch31763177 self.updatedBranches.add(branch)31783179if branch not in self.createdBranches:3180 self.createdBranches.add(branch)3181 parent = self.knownBranches[branch]3182if parent == branch:3183 parent =""3184else:3185 fullBranch = self.projectName + branch3186if fullBranch not in self.p4BranchesInGit:3187if not self.silent:3188print("\nImporting new branch%s"% fullBranch);3189if self.importNewBranch(branch, change -1):3190 parent =""3191 self.p4BranchesInGit.append(fullBranch)3192if not self.silent:3193print("\nResuming with change%s"% change);31943195if self.verbose:3196print"parent determined through known branches:%s"% parent31973198 branch = self.gitRefForBranch(branch)3199 parent = self.gitRefForBranch(parent)32003201if self.verbose:3202print"looking for initial parent for%s; current parent is%s"% (branch, parent)32033204iflen(parent) ==0and branch in self.initialParents:3205 parent = self.initialParents[branch]3206del self.initialParents[branch]32073208 blob =None3209iflen(parent) >0:3210 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3211if self.verbose:3212print"Creating temporary branch: "+ tempBranch3213 self.commit(description, filesForCommit, tempBranch)3214 self.tempBranches.append(tempBranch)3215 self.checkpoint()3216 blob = self.searchParent(parent, branch, tempBranch)3217if blob:3218 self.commit(description, filesForCommit, branch, blob)3219else:3220if self.verbose:3221print"Parent of%snot found. Committing into head of%s"% (branch, parent)3222 self.commit(description, filesForCommit, branch, parent)3223else:3224 files = self.extractFilesFromCommit(description)3225 self.commit(description, files, self.branch,3226 self.initialParent)3227# only needed once, to connect to the previous commit3228 self.initialParent =""3229exceptIOError:3230print self.gitError.read()3231 sys.exit(1)32323233defimportHeadRevision(self, revision):3234print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)32353236 details = {}3237 details["user"] ="git perforce import user"3238 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3239% (' '.join(self.depotPaths), revision))3240 details["change"] = revision3241 newestRevision =032423243 fileCnt =03244 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]32453246for info inp4CmdList(["files"] + fileArgs):32473248if'code'in info and info['code'] =='error':3249 sys.stderr.write("p4 returned an error:%s\n"3250% info['data'])3251if info['data'].find("must refer to client") >=0:3252 sys.stderr.write("This particular p4 error is misleading.\n")3253 sys.stderr.write("Perhaps the depot path was misspelled.\n");3254 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3255 sys.exit(1)3256if'p4ExitCode'in info:3257 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3258 sys.exit(1)325932603261 change =int(info["change"])3262if change > newestRevision:3263 newestRevision = change32643265if info["action"]in self.delete_actions:3266# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3267#fileCnt = fileCnt + 13268continue32693270for prop in["depotFile","rev","action","type"]:3271 details["%s%s"% (prop, fileCnt)] = info[prop]32723273 fileCnt = fileCnt +132743275 details["change"] = newestRevision32763277# Use time from top-most change so that all git p4 clones of3278# the same p4 repo have the same commit SHA1s.3279 res =p4_describe(newestRevision)3280 details["time"] = res["time"]32813282 self.updateOptionDict(details)3283try:3284 self.commit(details, self.extractFilesFromCommit(details), self.branch)3285exceptIOError:3286print"IO error with git fast-import. Is your git version recent enough?"3287print self.gitError.read()328832893290defrun(self, args):3291 self.depotPaths = []3292 self.changeRange =""3293 self.previousDepotPaths = []3294 self.hasOrigin =False32953296# map from branch depot path to parent branch3297 self.knownBranches = {}3298 self.initialParents = {}32993300if self.importIntoRemotes:3301 self.refPrefix ="refs/remotes/p4/"3302else:3303 self.refPrefix ="refs/heads/p4/"33043305if self.syncWithOrigin:3306 self.hasOrigin =originP4BranchesExist()3307if self.hasOrigin:3308if not self.silent:3309print'Syncing with origin first, using "git fetch origin"'3310system("git fetch origin")33113312 branch_arg_given =bool(self.branch)3313iflen(self.branch) ==0:3314 self.branch = self.refPrefix +"master"3315ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3316system("git update-ref%srefs/heads/p4"% self.branch)3317system("git branch -D p4")33183319# accept either the command-line option, or the configuration variable3320if self.useClientSpec:3321# will use this after clone to set the variable3322 self.useClientSpec_from_options =True3323else:3324ifgitConfigBool("git-p4.useclientspec"):3325 self.useClientSpec =True3326if self.useClientSpec:3327 self.clientSpecDirs =getClientSpec()33283329# TODO: should always look at previous commits,3330# merge with previous imports, if possible.3331if args == []:3332if self.hasOrigin:3333createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)33343335# branches holds mapping from branch name to sha13336 branches =p4BranchesInGit(self.importIntoRemotes)33373338# restrict to just this one, disabling detect-branches3339if branch_arg_given:3340 short = self.branch.split("/")[-1]3341if short in branches:3342 self.p4BranchesInGit = [ short ]3343else:3344 self.p4BranchesInGit = branches.keys()33453346iflen(self.p4BranchesInGit) >1:3347if not self.silent:3348print"Importing from/into multiple branches"3349 self.detectBranches =True3350for branch in branches.keys():3351 self.initialParents[self.refPrefix + branch] = \3352 branches[branch]33533354if self.verbose:3355print"branches:%s"% self.p4BranchesInGit33563357 p4Change =03358for branch in self.p4BranchesInGit:3359 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)33603361 settings =extractSettingsGitLog(logMsg)33623363 self.readOptions(settings)3364if(settings.has_key('depot-paths')3365and settings.has_key('change')):3366 change =int(settings['change']) +13367 p4Change =max(p4Change, change)33683369 depotPaths =sorted(settings['depot-paths'])3370if self.previousDepotPaths == []:3371 self.previousDepotPaths = depotPaths3372else:3373 paths = []3374for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3375 prev_list = prev.split("/")3376 cur_list = cur.split("/")3377for i inrange(0,min(len(cur_list),len(prev_list))):3378if cur_list[i] <> prev_list[i]:3379 i = i -13380break33813382 paths.append("/".join(cur_list[:i +1]))33833384 self.previousDepotPaths = paths33853386if p4Change >0:3387 self.depotPaths =sorted(self.previousDepotPaths)3388 self.changeRange ="@%s,#head"% p4Change3389if not self.silent and not self.detectBranches:3390print"Performing incremental import into%sgit branch"% self.branch33913392# accept multiple ref name abbreviations:3393# refs/foo/bar/branch -> use it exactly3394# p4/branch -> prepend refs/remotes/ or refs/heads/3395# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3396if not self.branch.startswith("refs/"):3397if self.importIntoRemotes:3398 prepend ="refs/remotes/"3399else:3400 prepend ="refs/heads/"3401if not self.branch.startswith("p4/"):3402 prepend +="p4/"3403 self.branch = prepend + self.branch34043405iflen(args) ==0and self.depotPaths:3406if not self.silent:3407print"Depot paths:%s"%' '.join(self.depotPaths)3408else:3409if self.depotPaths and self.depotPaths != args:3410print("previous import used depot path%sand now%swas specified. "3411"This doesn't work!"% (' '.join(self.depotPaths),3412' '.join(args)))3413 sys.exit(1)34143415 self.depotPaths =sorted(args)34163417 revision =""3418 self.users = {}34193420# Make sure no revision specifiers are used when --changesfile3421# is specified.3422 bad_changesfile =False3423iflen(self.changesFile) >0:3424for p in self.depotPaths:3425if p.find("@") >=0or p.find("#") >=0:3426 bad_changesfile =True3427break3428if bad_changesfile:3429die("Option --changesfile is incompatible with revision specifiers")34303431 newPaths = []3432for p in self.depotPaths:3433if p.find("@") != -1:3434 atIdx = p.index("@")3435 self.changeRange = p[atIdx:]3436if self.changeRange =="@all":3437 self.changeRange =""3438elif','not in self.changeRange:3439 revision = self.changeRange3440 self.changeRange =""3441 p = p[:atIdx]3442elif p.find("#") != -1:3443 hashIdx = p.index("#")3444 revision = p[hashIdx:]3445 p = p[:hashIdx]3446elif self.previousDepotPaths == []:3447# pay attention to changesfile, if given, else import3448# the entire p4 tree at the head revision3449iflen(self.changesFile) ==0:3450 revision ="#head"34513452 p = re.sub("\.\.\.$","", p)3453if not p.endswith("/"):3454 p +="/"34553456 newPaths.append(p)34573458 self.depotPaths = newPaths34593460# --detect-branches may change this for each branch3461 self.branchPrefixes = self.depotPaths34623463 self.loadUserMapFromCache()3464 self.labels = {}3465if self.detectLabels:3466 self.getLabels();34673468if self.detectBranches:3469## FIXME - what's a P4 projectName ?3470 self.projectName = self.guessProjectName()34713472if self.hasOrigin:3473 self.getBranchMappingFromGitBranches()3474else:3475 self.getBranchMapping()3476if self.verbose:3477print"p4-git branches:%s"% self.p4BranchesInGit3478print"initial parents:%s"% self.initialParents3479for b in self.p4BranchesInGit:3480if b !="master":34813482## FIXME3483 b = b[len(self.projectName):]3484 self.createdBranches.add(b)34853486 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))34873488 self.importProcess = subprocess.Popen(["git","fast-import"],3489 stdin=subprocess.PIPE,3490 stdout=subprocess.PIPE,3491 stderr=subprocess.PIPE);3492 self.gitOutput = self.importProcess.stdout3493 self.gitStream = self.importProcess.stdin3494 self.gitError = self.importProcess.stderr34953496if revision:3497 self.importHeadRevision(revision)3498else:3499 changes = []35003501iflen(self.changesFile) >0:3502 output =open(self.changesFile).readlines()3503 changeSet =set()3504for line in output:3505 changeSet.add(int(line))35063507for change in changeSet:3508 changes.append(change)35093510 changes.sort()3511else:3512# catch "git p4 sync" with no new branches, in a repo that3513# does not have any existing p4 branches3514iflen(args) ==0:3515if not self.p4BranchesInGit:3516die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")35173518# The default branch is master, unless --branch is used to3519# specify something else. Make sure it exists, or complain3520# nicely about how to use --branch.3521if not self.detectBranches:3522if notbranch_exists(self.branch):3523if branch_arg_given:3524die("Error: branch%sdoes not exist."% self.branch)3525else:3526die("Error: no branch%s; perhaps specify one with --branch."%3527 self.branch)35283529if self.verbose:3530print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3531 self.changeRange)3532 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)35333534iflen(self.maxChanges) >0:3535 changes = changes[:min(int(self.maxChanges),len(changes))]35363537iflen(changes) ==0:3538if not self.silent:3539print"No changes to import!"3540else:3541if not self.silent and not self.detectBranches:3542print"Import destination:%s"% self.branch35433544 self.updatedBranches =set()35453546if not self.detectBranches:3547if args:3548# start a new branch3549 self.initialParent =""3550else:3551# build on a previous revision3552 self.initialParent =parseRevision(self.branch)35533554 self.importChanges(changes)35553556if not self.silent:3557print""3558iflen(self.updatedBranches) >0:3559 sys.stdout.write("Updated branches: ")3560for b in self.updatedBranches:3561 sys.stdout.write("%s"% b)3562 sys.stdout.write("\n")35633564ifgitConfigBool("git-p4.importLabels"):3565 self.importLabels =True35663567if self.importLabels:3568 p4Labels =getP4Labels(self.depotPaths)3569 gitTags =getGitTags()35703571 missingP4Labels = p4Labels - gitTags3572 self.importP4Labels(self.gitStream, missingP4Labels)35733574 self.gitStream.close()3575if self.importProcess.wait() !=0:3576die("fast-import failed:%s"% self.gitError.read())3577 self.gitOutput.close()3578 self.gitError.close()35793580# Cleanup temporary branches created during import3581if self.tempBranches != []:3582for branch in self.tempBranches:3583read_pipe("git update-ref -d%s"% branch)3584 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))35853586# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3587# a convenient shortcut refname "p4".3588if self.importIntoRemotes:3589 head_ref = self.refPrefix +"HEAD"3590if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3591system(["git","symbolic-ref", head_ref, self.branch])35923593return True35943595classP4Rebase(Command):3596def__init__(self):3597 Command.__init__(self)3598 self.options = [3599 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3600]3601 self.importLabels =False3602 self.description = ("Fetches the latest revision from perforce and "3603+"rebases the current work (branch) against it")36043605defrun(self, args):3606 sync =P4Sync()3607 sync.importLabels = self.importLabels3608 sync.run([])36093610return self.rebase()36113612defrebase(self):3613if os.system("git update-index --refresh") !=0:3614die("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.");3615iflen(read_pipe("git diff-index HEAD --")) >0:3616die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");36173618[upstream, settings] =findUpstreamBranchPoint()3619iflen(upstream) ==0:3620die("Cannot find upstream branchpoint for rebase")36213622# the branchpoint may be p4/foo~3, so strip off the parent3623 upstream = re.sub("~[0-9]+$","", upstream)36243625print"Rebasing the current branch onto%s"% upstream3626 oldHead =read_pipe("git rev-parse HEAD").strip()3627system("git rebase%s"% upstream)3628system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3629return True36303631classP4Clone(P4Sync):3632def__init__(self):3633 P4Sync.__init__(self)3634 self.description ="Creates a new git repository and imports from Perforce into it"3635 self.usage ="usage: %prog [options] //depot/path[@revRange]"3636 self.options += [3637 optparse.make_option("--destination", dest="cloneDestination",3638 action='store', default=None,3639help="where to leave result of the clone"),3640 optparse.make_option("--bare", dest="cloneBare",3641 action="store_true", default=False),3642]3643 self.cloneDestination =None3644 self.needsGit =False3645 self.cloneBare =False36463647defdefaultDestination(self, args):3648## TODO: use common prefix of args?3649 depotPath = args[0]3650 depotDir = re.sub("(@[^@]*)$","", depotPath)3651 depotDir = re.sub("(#[^#]*)$","", depotDir)3652 depotDir = re.sub(r"\.\.\.$","", depotDir)3653 depotDir = re.sub(r"/$","", depotDir)3654return os.path.split(depotDir)[1]36553656defrun(self, args):3657iflen(args) <1:3658return False36593660if self.keepRepoPath and not self.cloneDestination:3661 sys.stderr.write("Must specify destination for --keep-path\n")3662 sys.exit(1)36633664 depotPaths = args36653666if not self.cloneDestination andlen(depotPaths) >1:3667 self.cloneDestination = depotPaths[-1]3668 depotPaths = depotPaths[:-1]36693670 self.cloneExclude = ["/"+p for p in self.cloneExclude]3671for p in depotPaths:3672if not p.startswith("//"):3673 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3674return False36753676if not self.cloneDestination:3677 self.cloneDestination = self.defaultDestination(args)36783679print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)36803681if not os.path.exists(self.cloneDestination):3682 os.makedirs(self.cloneDestination)3683chdir(self.cloneDestination)36843685 init_cmd = ["git","init"]3686if self.cloneBare:3687 init_cmd.append("--bare")3688 retcode = subprocess.call(init_cmd)3689if retcode:3690raiseCalledProcessError(retcode, init_cmd)36913692if not P4Sync.run(self, depotPaths):3693return False36943695# create a master branch and check out a work tree3696ifgitBranchExists(self.branch):3697system(["git","branch","master", self.branch ])3698if not self.cloneBare:3699system(["git","checkout","-f"])3700else:3701print'Not checking out any branch, use ' \3702'"git checkout -q -b master <branch>"'37033704# auto-set this variable if invoked with --use-client-spec3705if self.useClientSpec_from_options:3706system("git config --bool git-p4.useclientspec true")37073708return True37093710classP4Branches(Command):3711def__init__(self):3712 Command.__init__(self)3713 self.options = [ ]3714 self.description = ("Shows the git branches that hold imports and their "3715+"corresponding perforce depot paths")3716 self.verbose =False37173718defrun(self, args):3719iforiginP4BranchesExist():3720createOrUpdateBranchesFromOrigin()37213722 cmdline ="git rev-parse --symbolic "3723 cmdline +=" --remotes"37243725for line inread_pipe_lines(cmdline):3726 line = line.strip()37273728if not line.startswith('p4/')or line =="p4/HEAD":3729continue3730 branch = line37313732 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3733 settings =extractSettingsGitLog(log)37343735print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3736return True37373738classHelpFormatter(optparse.IndentedHelpFormatter):3739def__init__(self):3740 optparse.IndentedHelpFormatter.__init__(self)37413742defformat_description(self, description):3743if description:3744return description +"\n"3745else:3746return""37473748defprintUsage(commands):3749print"usage:%s<command> [options]"% sys.argv[0]3750print""3751print"valid commands:%s"%", ".join(commands)3752print""3753print"Try%s<command> --help for command specific help."% sys.argv[0]3754print""37553756commands = {3757"debug": P4Debug,3758"submit": P4Submit,3759"commit": P4Submit,3760"sync": P4Sync,3761"rebase": P4Rebase,3762"clone": P4Clone,3763"rollback": P4RollBack,3764"branches": P4Branches3765}376637673768defmain():3769iflen(sys.argv[1:]) ==0:3770printUsage(commands.keys())3771 sys.exit(2)37723773 cmdName = sys.argv[1]3774try:3775 klass = commands[cmdName]3776 cmd =klass()3777exceptKeyError:3778print"unknown command%s"% cmdName3779print""3780printUsage(commands.keys())3781 sys.exit(2)37823783 options = cmd.options3784 cmd.gitdir = os.environ.get("GIT_DIR",None)37853786 args = sys.argv[2:]37873788 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3789if cmd.needsGit:3790 options.append(optparse.make_option("--git-dir", dest="gitdir"))37913792 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3793 options,3794 description = cmd.description,3795 formatter =HelpFormatter())37963797(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3798global verbose3799 verbose = cmd.verbose3800if cmd.needsGit:3801if cmd.gitdir ==None:3802 cmd.gitdir = os.path.abspath(".git")3803if notisValidGitDir(cmd.gitdir):3804# "rev-parse --git-dir" without arguments will try $PWD/.git3805 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3806if os.path.exists(cmd.gitdir):3807 cdup =read_pipe("git rev-parse --show-cdup").strip()3808iflen(cdup) >0:3809chdir(cdup);38103811if notisValidGitDir(cmd.gitdir):3812ifisValidGitDir(cmd.gitdir +"/.git"):3813 cmd.gitdir +="/.git"3814else:3815die("fatal: cannot locate git repository at%s"% cmd.gitdir)38163817# so git commands invoked from the P4 workspace will succeed3818 os.environ["GIT_DIR"] = cmd.gitdir38193820if not cmd.run(args):3821 parser.print_help()3822 sys.exit(2)382338243825if __name__ =='__main__':3826main()