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 28 29try: 30from subprocess import CalledProcessError 31exceptImportError: 32# from python2.7:subprocess.py 33# Exception classes used by this module. 34classCalledProcessError(Exception): 35"""This exception is raised when a process run by check_call() returns 36 a non-zero exit status. The exit status will be stored in the 37 returncode attribute.""" 38def__init__(self, returncode, cmd): 39 self.returncode = returncode 40 self.cmd = cmd 41def__str__(self): 42return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 43 44verbose =False 45 46# Only labels/tags matching this will be imported/exported 47defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 48 49# Grab changes in blocks of this many revisions, unless otherwise requested 50defaultBlockSize =512 51 52defp4_build_cmd(cmd): 53"""Build a suitable p4 command line. 54 55 This consolidates building and returning a p4 command line into one 56 location. It means that hooking into the environment, or other configuration 57 can be done more easily. 58 """ 59 real_cmd = ["p4"] 60 61 user =gitConfig("git-p4.user") 62iflen(user) >0: 63 real_cmd += ["-u",user] 64 65 password =gitConfig("git-p4.password") 66iflen(password) >0: 67 real_cmd += ["-P", password] 68 69 port =gitConfig("git-p4.port") 70iflen(port) >0: 71 real_cmd += ["-p", port] 72 73 host =gitConfig("git-p4.host") 74iflen(host) >0: 75 real_cmd += ["-H", host] 76 77 client =gitConfig("git-p4.client") 78iflen(client) >0: 79 real_cmd += ["-c", client] 80 81 retries =gitConfigInt("git-p4.retries") 82if retries is None: 83# Perform 3 retries by default 84 retries =3 85 real_cmd += ["-r",str(retries)] 86 87ifisinstance(cmd,basestring): 88 real_cmd =' '.join(real_cmd) +' '+ cmd 89else: 90 real_cmd += cmd 91return real_cmd 92 93defgit_dir(path): 94""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 95 This won't automatically add ".git" to a directory. 96 """ 97 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 98if not d orlen(d) ==0: 99return None 100else: 101return d 102 103defchdir(path, is_client_path=False): 104"""Do chdir to the given path, and set the PWD environment 105 variable for use by P4. It does not look at getcwd() output. 106 Since we're not using the shell, it is necessary to set the 107 PWD environment variable explicitly. 108 109 Normally, expand the path to force it to be absolute. This 110 addresses the use of relative path names inside P4 settings, 111 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 112 as given; it looks for .p4config using PWD. 113 114 If is_client_path, the path was handed to us directly by p4, 115 and may be a symbolic link. Do not call os.getcwd() in this 116 case, because it will cause p4 to think that PWD is not inside 117 the client path. 118 """ 119 120 os.chdir(path) 121if not is_client_path: 122 path = os.getcwd() 123 os.environ['PWD'] = path 124 125defcalcDiskFree(): 126"""Return free space in bytes on the disk of the given dirname.""" 127if platform.system() =='Windows': 128 free_bytes = ctypes.c_ulonglong(0) 129 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 130return free_bytes.value 131else: 132 st = os.statvfs(os.getcwd()) 133return st.f_bavail * st.f_frsize 134 135defdie(msg): 136if verbose: 137raiseException(msg) 138else: 139 sys.stderr.write(msg +"\n") 140 sys.exit(1) 141 142defwrite_pipe(c, stdin): 143if verbose: 144 sys.stderr.write('Writing pipe:%s\n'%str(c)) 145 146 expand =isinstance(c,basestring) 147 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 148 pipe = p.stdin 149 val = pipe.write(stdin) 150 pipe.close() 151if p.wait(): 152die('Command failed:%s'%str(c)) 153 154return val 155 156defp4_write_pipe(c, stdin): 157 real_cmd =p4_build_cmd(c) 158returnwrite_pipe(real_cmd, stdin) 159 160defread_pipe(c, ignore_error=False): 161if verbose: 162 sys.stderr.write('Reading pipe:%s\n'%str(c)) 163 164 expand =isinstance(c,basestring) 165 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 166(out, err) = p.communicate() 167if p.returncode !=0and not ignore_error: 168die('Command failed:%s\nError:%s'% (str(c), err)) 169return out 170 171defp4_read_pipe(c, ignore_error=False): 172 real_cmd =p4_build_cmd(c) 173returnread_pipe(real_cmd, ignore_error) 174 175defread_pipe_lines(c): 176if verbose: 177 sys.stderr.write('Reading pipe:%s\n'%str(c)) 178 179 expand =isinstance(c, basestring) 180 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 181 pipe = p.stdout 182 val = pipe.readlines() 183if pipe.close()or p.wait(): 184die('Command failed:%s'%str(c)) 185 186return val 187 188defp4_read_pipe_lines(c): 189"""Specifically invoke p4 on the command supplied. """ 190 real_cmd =p4_build_cmd(c) 191returnread_pipe_lines(real_cmd) 192 193defp4_has_command(cmd): 194"""Ask p4 for help on this command. If it returns an error, the 195 command does not exist in this version of p4.""" 196 real_cmd =p4_build_cmd(["help", cmd]) 197 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 198 stderr=subprocess.PIPE) 199 p.communicate() 200return p.returncode ==0 201 202defp4_has_move_command(): 203"""See if the move command exists, that it supports -k, and that 204 it has not been administratively disabled. The arguments 205 must be correct, but the filenames do not have to exist. Use 206 ones with wildcards so even if they exist, it will fail.""" 207 208if notp4_has_command("move"): 209return False 210 cmd =p4_build_cmd(["move","-k","@from","@to"]) 211 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 212(out, err) = p.communicate() 213# return code will be 1 in either case 214if err.find("Invalid option") >=0: 215return False 216if err.find("disabled") >=0: 217return False 218# assume it failed because @... was invalid changelist 219return True 220 221defsystem(cmd, ignore_error=False): 222 expand =isinstance(cmd,basestring) 223if verbose: 224 sys.stderr.write("executing%s\n"%str(cmd)) 225 retcode = subprocess.call(cmd, shell=expand) 226if retcode and not ignore_error: 227raiseCalledProcessError(retcode, cmd) 228 229return retcode 230 231defp4_system(cmd): 232"""Specifically invoke p4 as the system command. """ 233 real_cmd =p4_build_cmd(cmd) 234 expand =isinstance(real_cmd, basestring) 235 retcode = subprocess.call(real_cmd, shell=expand) 236if retcode: 237raiseCalledProcessError(retcode, real_cmd) 238 239_p4_version_string =None 240defp4_version_string(): 241"""Read the version string, showing just the last line, which 242 hopefully is the interesting version bit. 243 244 $ p4 -V 245 Perforce - The Fast Software Configuration Management System. 246 Copyright 1995-2011 Perforce Software. All rights reserved. 247 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 248 """ 249global _p4_version_string 250if not _p4_version_string: 251 a =p4_read_pipe_lines(["-V"]) 252 _p4_version_string = a[-1].rstrip() 253return _p4_version_string 254 255defp4_integrate(src, dest): 256p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 257 258defp4_sync(f, *options): 259p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 260 261defp4_add(f): 262# forcibly add file names with wildcards 263ifwildcard_present(f): 264p4_system(["add","-f", f]) 265else: 266p4_system(["add", f]) 267 268defp4_delete(f): 269p4_system(["delete",wildcard_encode(f)]) 270 271defp4_edit(f, *options): 272p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 273 274defp4_revert(f): 275p4_system(["revert",wildcard_encode(f)]) 276 277defp4_reopen(type, f): 278p4_system(["reopen","-t",type,wildcard_encode(f)]) 279 280defp4_reopen_in_change(changelist, files): 281 cmd = ["reopen","-c",str(changelist)] + files 282p4_system(cmd) 283 284defp4_move(src, dest): 285p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 286 287defp4_last_change(): 288 results =p4CmdList(["changes","-m","1"]) 289returnint(results[0]['change']) 290 291defp4_describe(change): 292"""Make sure it returns a valid result by checking for 293 the presence of field "time". Return a dict of the 294 results.""" 295 296 ds =p4CmdList(["describe","-s",str(change)]) 297iflen(ds) !=1: 298die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 299 300 d = ds[0] 301 302if"p4ExitCode"in d: 303die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 304str(d))) 305if"code"in d: 306if d["code"] =="error": 307die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 308 309if"time"not in d: 310die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 311 312return d 313 314# 315# Canonicalize the p4 type and return a tuple of the 316# base type, plus any modifiers. See "p4 help filetypes" 317# for a list and explanation. 318# 319defsplit_p4_type(p4type): 320 321 p4_filetypes_historical = { 322"ctempobj":"binary+Sw", 323"ctext":"text+C", 324"cxtext":"text+Cx", 325"ktext":"text+k", 326"kxtext":"text+kx", 327"ltext":"text+F", 328"tempobj":"binary+FSw", 329"ubinary":"binary+F", 330"uresource":"resource+F", 331"uxbinary":"binary+Fx", 332"xbinary":"binary+x", 333"xltext":"text+Fx", 334"xtempobj":"binary+Swx", 335"xtext":"text+x", 336"xunicode":"unicode+x", 337"xutf16":"utf16+x", 338} 339if p4type in p4_filetypes_historical: 340 p4type = p4_filetypes_historical[p4type] 341 mods ="" 342 s = p4type.split("+") 343 base = s[0] 344 mods ="" 345iflen(s) >1: 346 mods = s[1] 347return(base, mods) 348 349# 350# return the raw p4 type of a file (text, text+ko, etc) 351# 352defp4_type(f): 353 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 354return results[0]['headType'] 355 356# 357# Given a type base and modifier, return a regexp matching 358# the keywords that can be expanded in the file 359# 360defp4_keywords_regexp_for_type(base, type_mods): 361if base in("text","unicode","binary"): 362 kwords =None 363if"ko"in type_mods: 364 kwords ='Id|Header' 365elif"k"in type_mods: 366 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 367else: 368return None 369 pattern = r""" 370 \$ # Starts with a dollar, followed by... 371 (%s) # one of the keywords, followed by... 372 (:[^$\n]+)? # possibly an old expansion, followed by... 373 \$ # another dollar 374 """% kwords 375return pattern 376else: 377return None 378 379# 380# Given a file, return a regexp matching the possible 381# RCS keywords that will be expanded, or None for files 382# with kw expansion turned off. 383# 384defp4_keywords_regexp_for_file(file): 385if not os.path.exists(file): 386return None 387else: 388(type_base, type_mods) =split_p4_type(p4_type(file)) 389returnp4_keywords_regexp_for_type(type_base, type_mods) 390 391defsetP4ExecBit(file, mode): 392# Reopens an already open file and changes the execute bit to match 393# the execute bit setting in the passed in mode. 394 395 p4Type ="+x" 396 397if notisModeExec(mode): 398 p4Type =getP4OpenedType(file) 399 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 400 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 401if p4Type[-1] =="+": 402 p4Type = p4Type[0:-1] 403 404p4_reopen(p4Type,file) 405 406defgetP4OpenedType(file): 407# Returns the perforce file type for the given file. 408 409 result =p4_read_pipe(["opened",wildcard_encode(file)]) 410 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 411if match: 412return match.group(1) 413else: 414die("Could not determine file type for%s(result: '%s')"% (file, result)) 415 416# Return the set of all p4 labels 417defgetP4Labels(depotPaths): 418 labels =set() 419ifisinstance(depotPaths,basestring): 420 depotPaths = [depotPaths] 421 422for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 423 label = l['label'] 424 labels.add(label) 425 426return labels 427 428# Return the set of all git tags 429defgetGitTags(): 430 gitTags =set() 431for line inread_pipe_lines(["git","tag"]): 432 tag = line.strip() 433 gitTags.add(tag) 434return gitTags 435 436defdiffTreePattern(): 437# This is a simple generator for the diff tree regex pattern. This could be 438# a class variable if this and parseDiffTreeEntry were a part of a class. 439 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 440while True: 441yield pattern 442 443defparseDiffTreeEntry(entry): 444"""Parses a single diff tree entry into its component elements. 445 446 See git-diff-tree(1) manpage for details about the format of the diff 447 output. This method returns a dictionary with the following elements: 448 449 src_mode - The mode of the source file 450 dst_mode - The mode of the destination file 451 src_sha1 - The sha1 for the source file 452 dst_sha1 - The sha1 fr the destination file 453 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 454 status_score - The score for the status (applicable for 'C' and 'R' 455 statuses). This is None if there is no score. 456 src - The path for the source file. 457 dst - The path for the destination file. This is only present for 458 copy or renames. If it is not present, this is None. 459 460 If the pattern is not matched, None is returned.""" 461 462 match =diffTreePattern().next().match(entry) 463if match: 464return{ 465'src_mode': match.group(1), 466'dst_mode': match.group(2), 467'src_sha1': match.group(3), 468'dst_sha1': match.group(4), 469'status': match.group(5), 470'status_score': match.group(6), 471'src': match.group(7), 472'dst': match.group(10) 473} 474return None 475 476defisModeExec(mode): 477# Returns True if the given git mode represents an executable file, 478# otherwise False. 479return mode[-3:] =="755" 480 481defisModeExecChanged(src_mode, dst_mode): 482returnisModeExec(src_mode) !=isModeExec(dst_mode) 483 484defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 485 486ifisinstance(cmd,basestring): 487 cmd ="-G "+ cmd 488 expand =True 489else: 490 cmd = ["-G"] + cmd 491 expand =False 492 493 cmd =p4_build_cmd(cmd) 494if verbose: 495 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 496 497# Use a temporary file to avoid deadlocks without 498# subprocess.communicate(), which would put another copy 499# of stdout into memory. 500 stdin_file =None 501if stdin is not None: 502 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 503ifisinstance(stdin,basestring): 504 stdin_file.write(stdin) 505else: 506for i in stdin: 507 stdin_file.write(i +'\n') 508 stdin_file.flush() 509 stdin_file.seek(0) 510 511 p4 = subprocess.Popen(cmd, 512 shell=expand, 513 stdin=stdin_file, 514 stdout=subprocess.PIPE) 515 516 result = [] 517try: 518while True: 519 entry = marshal.load(p4.stdout) 520if cb is not None: 521cb(entry) 522else: 523 result.append(entry) 524exceptEOFError: 525pass 526 exitCode = p4.wait() 527if exitCode !=0: 528 entry = {} 529 entry["p4ExitCode"] = exitCode 530 result.append(entry) 531 532return result 533 534defp4Cmd(cmd): 535list=p4CmdList(cmd) 536 result = {} 537for entry inlist: 538 result.update(entry) 539return result; 540 541defp4Where(depotPath): 542if not depotPath.endswith("/"): 543 depotPath +="/" 544 depotPathLong = depotPath +"..." 545 outputList =p4CmdList(["where", depotPathLong]) 546 output =None 547for entry in outputList: 548if"depotFile"in entry: 549# Search for the base client side depot path, as long as it starts with the branch's P4 path. 550# The base path always ends with "/...". 551if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 552 output = entry 553break 554elif"data"in entry: 555 data = entry.get("data") 556 space = data.find(" ") 557if data[:space] == depotPath: 558 output = entry 559break 560if output ==None: 561return"" 562if output["code"] =="error": 563return"" 564 clientPath ="" 565if"path"in output: 566 clientPath = output.get("path") 567elif"data"in output: 568 data = output.get("data") 569 lastSpace = data.rfind(" ") 570 clientPath = data[lastSpace +1:] 571 572if clientPath.endswith("..."): 573 clientPath = clientPath[:-3] 574return clientPath 575 576defcurrentGitBranch(): 577 retcode =system(["git","symbolic-ref","-q","HEAD"], ignore_error=True) 578if retcode !=0: 579# on a detached head 580return None 581else: 582returnread_pipe(["git","name-rev","HEAD"]).split(" ")[1].strip() 583 584defisValidGitDir(path): 585returngit_dir(path) !=None 586 587defparseRevision(ref): 588returnread_pipe("git rev-parse%s"% ref).strip() 589 590defbranchExists(ref): 591 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 592 ignore_error=True) 593returnlen(rev) >0 594 595defextractLogMessageFromGitCommit(commit): 596 logMessage ="" 597 598## fixme: title is first line of commit, not 1st paragraph. 599 foundTitle =False 600for log inread_pipe_lines("git cat-file commit%s"% commit): 601if not foundTitle: 602iflen(log) ==1: 603 foundTitle =True 604continue 605 606 logMessage += log 607return logMessage 608 609defextractSettingsGitLog(log): 610 values = {} 611for line in log.split("\n"): 612 line = line.strip() 613 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 614if not m: 615continue 616 617 assignments = m.group(1).split(':') 618for a in assignments: 619 vals = a.split('=') 620 key = vals[0].strip() 621 val = ('='.join(vals[1:])).strip() 622if val.endswith('\"')and val.startswith('"'): 623 val = val[1:-1] 624 625 values[key] = val 626 627 paths = values.get("depot-paths") 628if not paths: 629 paths = values.get("depot-path") 630if paths: 631 values['depot-paths'] = paths.split(',') 632return values 633 634defgitBranchExists(branch): 635 proc = subprocess.Popen(["git","rev-parse", branch], 636 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 637return proc.wait() ==0; 638 639_gitConfig = {} 640 641defgitConfig(key, typeSpecifier=None): 642if not _gitConfig.has_key(key): 643 cmd = ["git","config"] 644if typeSpecifier: 645 cmd += [ typeSpecifier ] 646 cmd += [ key ] 647 s =read_pipe(cmd, ignore_error=True) 648 _gitConfig[key] = s.strip() 649return _gitConfig[key] 650 651defgitConfigBool(key): 652"""Return a bool, using git config --bool. It is True only if the 653 variable is set to true, and False if set to false or not present 654 in the config.""" 655 656if not _gitConfig.has_key(key): 657 _gitConfig[key] =gitConfig(key,'--bool') =="true" 658return _gitConfig[key] 659 660defgitConfigInt(key): 661if not _gitConfig.has_key(key): 662 cmd = ["git","config","--int", key ] 663 s =read_pipe(cmd, ignore_error=True) 664 v = s.strip() 665try: 666 _gitConfig[key] =int(gitConfig(key,'--int')) 667exceptValueError: 668 _gitConfig[key] =None 669return _gitConfig[key] 670 671defgitConfigList(key): 672if not _gitConfig.has_key(key): 673 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 674 _gitConfig[key] = s.strip().split(os.linesep) 675if _gitConfig[key] == ['']: 676 _gitConfig[key] = [] 677return _gitConfig[key] 678 679defp4BranchesInGit(branchesAreInRemotes=True): 680"""Find all the branches whose names start with "p4/", looking 681 in remotes or heads as specified by the argument. Return 682 a dictionary of{ branch: revision }for each one found. 683 The branch names are the short names, without any 684 "p4/" prefix.""" 685 686 branches = {} 687 688 cmdline ="git rev-parse --symbolic " 689if branchesAreInRemotes: 690 cmdline +="--remotes" 691else: 692 cmdline +="--branches" 693 694for line inread_pipe_lines(cmdline): 695 line = line.strip() 696 697# only import to p4/ 698if not line.startswith('p4/'): 699continue 700# special symbolic ref to p4/master 701if line =="p4/HEAD": 702continue 703 704# strip off p4/ prefix 705 branch = line[len("p4/"):] 706 707 branches[branch] =parseRevision(line) 708 709return branches 710 711defbranch_exists(branch): 712"""Make sure that the given ref name really exists.""" 713 714 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 715 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 716 out, _ = p.communicate() 717if p.returncode: 718return False 719# expect exactly one line of output: the branch name 720return out.rstrip() == branch 721 722deffindUpstreamBranchPoint(head ="HEAD"): 723 branches =p4BranchesInGit() 724# map from depot-path to branch name 725 branchByDepotPath = {} 726for branch in branches.keys(): 727 tip = branches[branch] 728 log =extractLogMessageFromGitCommit(tip) 729 settings =extractSettingsGitLog(log) 730if settings.has_key("depot-paths"): 731 paths =",".join(settings["depot-paths"]) 732 branchByDepotPath[paths] ="remotes/p4/"+ branch 733 734 settings =None 735 parent =0 736while parent <65535: 737 commit = head +"~%s"% parent 738 log =extractLogMessageFromGitCommit(commit) 739 settings =extractSettingsGitLog(log) 740if settings.has_key("depot-paths"): 741 paths =",".join(settings["depot-paths"]) 742if branchByDepotPath.has_key(paths): 743return[branchByDepotPath[paths], settings] 744 745 parent = parent +1 746 747return["", settings] 748 749defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 750if not silent: 751print("Creating/updating branch(es) in%sbased on origin branch(es)" 752% localRefPrefix) 753 754 originPrefix ="origin/p4/" 755 756for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 757 line = line.strip() 758if(not line.startswith(originPrefix))or line.endswith("HEAD"): 759continue 760 761 headName = line[len(originPrefix):] 762 remoteHead = localRefPrefix + headName 763 originHead = line 764 765 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 766if(not original.has_key('depot-paths') 767or not original.has_key('change')): 768continue 769 770 update =False 771if notgitBranchExists(remoteHead): 772if verbose: 773print"creating%s"% remoteHead 774 update =True 775else: 776 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 777if settings.has_key('change') >0: 778if settings['depot-paths'] == original['depot-paths']: 779 originP4Change =int(original['change']) 780 p4Change =int(settings['change']) 781if originP4Change > p4Change: 782print("%s(%s) is newer than%s(%s). " 783"Updating p4 branch from origin." 784% (originHead, originP4Change, 785 remoteHead, p4Change)) 786 update =True 787else: 788print("Ignoring:%swas imported from%swhile " 789"%swas imported from%s" 790% (originHead,','.join(original['depot-paths']), 791 remoteHead,','.join(settings['depot-paths']))) 792 793if update: 794system("git update-ref%s %s"% (remoteHead, originHead)) 795 796deforiginP4BranchesExist(): 797returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 798 799 800defp4ParseNumericChangeRange(parts): 801 changeStart =int(parts[0][1:]) 802if parts[1] =='#head': 803 changeEnd =p4_last_change() 804else: 805 changeEnd =int(parts[1]) 806 807return(changeStart, changeEnd) 808 809defchooseBlockSize(blockSize): 810if blockSize: 811return blockSize 812else: 813return defaultBlockSize 814 815defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 816assert depotPaths 817 818# Parse the change range into start and end. Try to find integer 819# revision ranges as these can be broken up into blocks to avoid 820# hitting server-side limits (maxrows, maxscanresults). But if 821# that doesn't work, fall back to using the raw revision specifier 822# strings, without using block mode. 823 824if changeRange is None or changeRange =='': 825 changeStart =1 826 changeEnd =p4_last_change() 827 block_size =chooseBlockSize(requestedBlockSize) 828else: 829 parts = changeRange.split(',') 830assertlen(parts) ==2 831try: 832(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 833 block_size =chooseBlockSize(requestedBlockSize) 834except: 835 changeStart = parts[0][1:] 836 changeEnd = parts[1] 837if requestedBlockSize: 838die("cannot use --changes-block-size with non-numeric revisions") 839 block_size =None 840 841 changes = [] 842 843# Retrieve changes a block at a time, to prevent running 844# into a MaxResults/MaxScanRows error from the server. 845 846while True: 847 cmd = ['changes'] 848 849if block_size: 850 end =min(changeEnd, changeStart + block_size) 851 revisionRange ="%d,%d"% (changeStart, end) 852else: 853 revisionRange ="%s,%s"% (changeStart, changeEnd) 854 855for p in depotPaths: 856 cmd += ["%s...@%s"% (p, revisionRange)] 857 858# Insert changes in chronological order 859for line inreversed(p4_read_pipe_lines(cmd)): 860 changes.append(int(line.split(" ")[1])) 861 862if not block_size: 863break 864 865if end >= changeEnd: 866break 867 868 changeStart = end +1 869 870 changes =sorted(changes) 871return changes 872 873defp4PathStartsWith(path, prefix): 874# This method tries to remedy a potential mixed-case issue: 875# 876# If UserA adds //depot/DirA/file1 877# and UserB adds //depot/dira/file2 878# 879# we may or may not have a problem. If you have core.ignorecase=true, 880# we treat DirA and dira as the same directory 881ifgitConfigBool("core.ignorecase"): 882return path.lower().startswith(prefix.lower()) 883return path.startswith(prefix) 884 885defgetClientSpec(): 886"""Look at the p4 client spec, create a View() object that contains 887 all the mappings, and return it.""" 888 889 specList =p4CmdList("client -o") 890iflen(specList) !=1: 891die('Output from "client -o" is%dlines, expecting 1'% 892len(specList)) 893 894# dictionary of all client parameters 895 entry = specList[0] 896 897# the //client/ name 898 client_name = entry["Client"] 899 900# just the keys that start with "View" 901 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 902 903# hold this new View 904 view =View(client_name) 905 906# append the lines, in order, to the view 907for view_num inrange(len(view_keys)): 908 k ="View%d"% view_num 909if k not in view_keys: 910die("Expected view key%smissing"% k) 911 view.append(entry[k]) 912 913return view 914 915defgetClientRoot(): 916"""Grab the client directory.""" 917 918 output =p4CmdList("client -o") 919iflen(output) !=1: 920die('Output from "client -o" is%dlines, expecting 1'%len(output)) 921 922 entry = output[0] 923if"Root"not in entry: 924die('Client has no "Root"') 925 926return entry["Root"] 927 928# 929# P4 wildcards are not allowed in filenames. P4 complains 930# if you simply add them, but you can force it with "-f", in 931# which case it translates them into %xx encoding internally. 932# 933defwildcard_decode(path): 934# Search for and fix just these four characters. Do % last so 935# that fixing it does not inadvertently create new %-escapes. 936# Cannot have * in a filename in windows; untested as to 937# what p4 would do in such a case. 938if not platform.system() =="Windows": 939 path = path.replace("%2A","*") 940 path = path.replace("%23","#") \ 941.replace("%40","@") \ 942.replace("%25","%") 943return path 944 945defwildcard_encode(path): 946# do % first to avoid double-encoding the %s introduced here 947 path = path.replace("%","%25") \ 948.replace("*","%2A") \ 949.replace("#","%23") \ 950.replace("@","%40") 951return path 952 953defwildcard_present(path): 954 m = re.search("[*#@%]", path) 955return m is not None 956 957classLargeFileSystem(object): 958"""Base class for large file system support.""" 959 960def__init__(self, writeToGitStream): 961 self.largeFiles =set() 962 self.writeToGitStream = writeToGitStream 963 964defgeneratePointer(self, cloneDestination, contentFile): 965"""Return the content of a pointer file that is stored in Git instead of 966 the actual content.""" 967assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 968 969defpushFile(self, localLargeFile): 970"""Push the actual content which is not stored in the Git repository to 971 a server.""" 972assert False,"Method 'pushFile' required in "+ self.__class__.__name__ 973 974defhasLargeFileExtension(self, relPath): 975returnreduce( 976lambda a, b: a or b, 977[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')], 978False 979) 980 981defgenerateTempFile(self, contents): 982 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 983for d in contents: 984 contentFile.write(d) 985 contentFile.close() 986return contentFile.name 987 988defexceedsLargeFileThreshold(self, relPath, contents): 989ifgitConfigInt('git-p4.largeFileThreshold'): 990 contentsSize =sum(len(d)for d in contents) 991if contentsSize >gitConfigInt('git-p4.largeFileThreshold'): 992return True 993ifgitConfigInt('git-p4.largeFileCompressedThreshold'): 994 contentsSize =sum(len(d)for d in contents) 995if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'): 996return False 997 contentTempFile = self.generateTempFile(contents) 998 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 999 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1000 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1001 zf.close()1002 compressedContentsSize = zf.infolist()[0].compress_size1003 os.remove(contentTempFile)1004 os.remove(compressedContentFile.name)1005if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1006return True1007return False10081009defaddLargeFile(self, relPath):1010 self.largeFiles.add(relPath)10111012defremoveLargeFile(self, relPath):1013 self.largeFiles.remove(relPath)10141015defisLargeFile(self, relPath):1016return relPath in self.largeFiles10171018defprocessContent(self, git_mode, relPath, contents):1019"""Processes the content of git fast import. This method decides if a1020 file is stored in the large file system and handles all necessary1021 steps."""1022if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1023 contentTempFile = self.generateTempFile(contents)1024(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1025if pointer_git_mode:1026 git_mode = pointer_git_mode1027if localLargeFile:1028# Move temp file to final location in large file system1029 largeFileDir = os.path.dirname(localLargeFile)1030if not os.path.isdir(largeFileDir):1031 os.makedirs(largeFileDir)1032 shutil.move(contentTempFile, localLargeFile)1033 self.addLargeFile(relPath)1034ifgitConfigBool('git-p4.largeFilePush'):1035 self.pushFile(localLargeFile)1036if verbose:1037 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1038return(git_mode, contents)10391040classMockLFS(LargeFileSystem):1041"""Mock large file system for testing."""10421043defgeneratePointer(self, contentFile):1044"""The pointer content is the original content prefixed with "pointer-".1045 The local filename of the large file storage is derived from the file content.1046 """1047withopen(contentFile,'r')as f:1048 content =next(f)1049 gitMode ='100644'1050 pointerContents ='pointer-'+ content1051 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1052return(gitMode, pointerContents, localLargeFile)10531054defpushFile(self, localLargeFile):1055"""The remote filename of the large file storage is the same as the local1056 one but in a different directory.1057 """1058 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1059if not os.path.exists(remotePath):1060 os.makedirs(remotePath)1061 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10621063classGitLFS(LargeFileSystem):1064"""Git LFS as backend for the git-p4 large file system.1065 See https://git-lfs.github.com/ for details."""10661067def__init__(self, *args):1068 LargeFileSystem.__init__(self, *args)1069 self.baseGitAttributes = []10701071defgeneratePointer(self, contentFile):1072"""Generate a Git LFS pointer for the content. Return LFS Pointer file1073 mode and content which is stored in the Git repository instead of1074 the actual content. Return also the new location of the actual1075 content.1076 """1077if os.path.getsize(contentFile) ==0:1078return(None,'',None)10791080 pointerProcess = subprocess.Popen(1081['git','lfs','pointer','--file='+ contentFile],1082 stdout=subprocess.PIPE1083)1084 pointerFile = pointerProcess.stdout.read()1085if pointerProcess.wait():1086 os.remove(contentFile)1087die('git-lfs pointer command failed. Did you install the extension?')10881089# Git LFS removed the preamble in the output of the 'pointer' command1090# starting from version 1.2.0. Check for the preamble here to support1091# earlier versions.1092# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431093if pointerFile.startswith('Git LFS pointer for'):1094 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)10951096 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1097 localLargeFile = os.path.join(1098 os.getcwd(),1099'.git','lfs','objects', oid[:2], oid[2:4],1100 oid,1101)1102# LFS Spec states that pointer files should not have the executable bit set.1103 gitMode ='100644'1104return(gitMode, pointerFile, localLargeFile)11051106defpushFile(self, localLargeFile):1107 uploadProcess = subprocess.Popen(1108['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1109)1110if uploadProcess.wait():1111die('git-lfs push command failed. Did you define a remote?')11121113defgenerateGitAttributes(self):1114return(1115 self.baseGitAttributes +1116[1117'\n',1118'#\n',1119'# Git LFS (see https://git-lfs.github.com/)\n',1120'#\n',1121] +1122['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1123for f insorted(gitConfigList('git-p4.largeFileExtensions'))1124] +1125['/'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1126for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1127]1128)11291130defaddLargeFile(self, relPath):1131 LargeFileSystem.addLargeFile(self, relPath)1132 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11331134defremoveLargeFile(self, relPath):1135 LargeFileSystem.removeLargeFile(self, relPath)1136 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11371138defprocessContent(self, git_mode, relPath, contents):1139if relPath =='.gitattributes':1140 self.baseGitAttributes = contents1141return(git_mode, self.generateGitAttributes())1142else:1143return LargeFileSystem.processContent(self, git_mode, relPath, contents)11441145class Command:1146def__init__(self):1147 self.usage ="usage: %prog [options]"1148 self.needsGit =True1149 self.verbose =False11501151class P4UserMap:1152def__init__(self):1153 self.userMapFromPerforceServer =False1154 self.myP4UserId =None11551156defp4UserId(self):1157if self.myP4UserId:1158return self.myP4UserId11591160 results =p4CmdList("user -o")1161for r in results:1162if r.has_key('User'):1163 self.myP4UserId = r['User']1164return r['User']1165die("Could not find your p4 user id")11661167defp4UserIsMe(self, p4User):1168# return True if the given p4 user is actually me1169 me = self.p4UserId()1170if not p4User or p4User != me:1171return False1172else:1173return True11741175defgetUserCacheFilename(self):1176 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1177return home +"/.gitp4-usercache.txt"11781179defgetUserMapFromPerforceServer(self):1180if self.userMapFromPerforceServer:1181return1182 self.users = {}1183 self.emails = {}11841185for output inp4CmdList("users"):1186if not output.has_key("User"):1187continue1188 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1189 self.emails[output["Email"]] = output["User"]11901191 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1192for mapUserConfig ingitConfigList("git-p4.mapUser"):1193 mapUser = mapUserConfigRegex.findall(mapUserConfig)1194if mapUser andlen(mapUser[0]) ==3:1195 user = mapUser[0][0]1196 fullname = mapUser[0][1]1197 email = mapUser[0][2]1198 self.users[user] = fullname +" <"+ email +">"1199 self.emails[email] = user12001201 s =''1202for(key, val)in self.users.items():1203 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12041205open(self.getUserCacheFilename(),"wb").write(s)1206 self.userMapFromPerforceServer =True12071208defloadUserMapFromCache(self):1209 self.users = {}1210 self.userMapFromPerforceServer =False1211try:1212 cache =open(self.getUserCacheFilename(),"rb")1213 lines = cache.readlines()1214 cache.close()1215for line in lines:1216 entry = line.strip().split("\t")1217 self.users[entry[0]] = entry[1]1218exceptIOError:1219 self.getUserMapFromPerforceServer()12201221classP4Debug(Command):1222def__init__(self):1223 Command.__init__(self)1224 self.options = []1225 self.description ="A tool to debug the output of p4 -G."1226 self.needsGit =False12271228defrun(self, args):1229 j =01230for output inp4CmdList(args):1231print'Element:%d'% j1232 j +=11233print output1234return True12351236classP4RollBack(Command):1237def__init__(self):1238 Command.__init__(self)1239 self.options = [1240 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1241]1242 self.description ="A tool to debug the multi-branch import. Don't use :)"1243 self.rollbackLocalBranches =False12441245defrun(self, args):1246iflen(args) !=1:1247return False1248 maxChange =int(args[0])12491250if"p4ExitCode"inp4Cmd("changes -m 1"):1251die("Problems executing p4");12521253if self.rollbackLocalBranches:1254 refPrefix ="refs/heads/"1255 lines =read_pipe_lines("git rev-parse --symbolic --branches")1256else:1257 refPrefix ="refs/remotes/"1258 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12591260for line in lines:1261if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1262 line = line.strip()1263 ref = refPrefix + line1264 log =extractLogMessageFromGitCommit(ref)1265 settings =extractSettingsGitLog(log)12661267 depotPaths = settings['depot-paths']1268 change = settings['change']12691270 changed =False12711272iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1273for p in depotPaths]))) ==0:1274print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1275system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1276continue12771278while change andint(change) > maxChange:1279 changed =True1280if self.verbose:1281print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1282system("git update-ref%s\"%s^\""% (ref, ref))1283 log =extractLogMessageFromGitCommit(ref)1284 settings =extractSettingsGitLog(log)128512861287 depotPaths = settings['depot-paths']1288 change = settings['change']12891290if changed:1291print"%srewound to%s"% (ref, change)12921293return True12941295classP4Submit(Command, P4UserMap):12961297 conflict_behavior_choices = ("ask","skip","quit")12981299def__init__(self):1300 Command.__init__(self)1301 P4UserMap.__init__(self)1302 self.options = [1303 optparse.make_option("--origin", dest="origin"),1304 optparse.make_option("-M", dest="detectRenames", action="store_true"),1305# preserve the user, requires relevant p4 permissions1306 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1307 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1308 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1309 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1310 optparse.make_option("--conflict", dest="conflict_behavior",1311 choices=self.conflict_behavior_choices),1312 optparse.make_option("--branch", dest="branch"),1313 optparse.make_option("--shelve", dest="shelve", action="store_true",1314help="Shelve instead of submit. Shelved files are reverted, "1315"restoring the workspace to the state before the shelve"),1316 optparse.make_option("--update-shelve", dest="update_shelve", action="store",type="int",1317 metavar="CHANGELIST",1318help="update an existing shelved changelist, implies --shelve")1319]1320 self.description ="Submit changes from git to the perforce depot."1321 self.usage +=" [name of git branch to submit into perforce depot]"1322 self.origin =""1323 self.detectRenames =False1324 self.preserveUser =gitConfigBool("git-p4.preserveUser")1325 self.dry_run =False1326 self.shelve =False1327 self.update_shelve =None1328 self.prepare_p4_only =False1329 self.conflict_behavior =None1330 self.isWindows = (platform.system() =="Windows")1331 self.exportLabels =False1332 self.p4HasMoveCommand =p4_has_move_command()1333 self.branch =None13341335ifgitConfig('git-p4.largeFileSystem'):1336die("Large file system not supported for git-p4 submit command. Please remove it from config.")13371338defcheck(self):1339iflen(p4CmdList("opened ...")) >0:1340die("You have files opened with perforce! Close them before starting the sync.")13411342defseparate_jobs_from_description(self, message):1343"""Extract and return a possible Jobs field in the commit1344 message. It goes into a separate section in the p4 change1345 specification.13461347 A jobs line starts with "Jobs:" and looks like a new field1348 in a form. Values are white-space separated on the same1349 line or on following lines that start with a tab.13501351 This does not parse and extract the full git commit message1352 like a p4 form. It just sees the Jobs: line as a marker1353 to pass everything from then on directly into the p4 form,1354 but outside the description section.13551356 Return a tuple (stripped log message, jobs string)."""13571358 m = re.search(r'^Jobs:', message, re.MULTILINE)1359if m is None:1360return(message,None)13611362 jobtext = message[m.start():]1363 stripped_message = message[:m.start()].rstrip()1364return(stripped_message, jobtext)13651366defprepareLogMessage(self, template, message, jobs):1367"""Edits the template returned from "p4 change -o" to insert1368 the message in the Description field, and the jobs text in1369 the Jobs field."""1370 result =""13711372 inDescriptionSection =False13731374for line in template.split("\n"):1375if line.startswith("#"):1376 result += line +"\n"1377continue13781379if inDescriptionSection:1380if line.startswith("Files:")or line.startswith("Jobs:"):1381 inDescriptionSection =False1382# insert Jobs section1383if jobs:1384 result += jobs +"\n"1385else:1386continue1387else:1388if line.startswith("Description:"):1389 inDescriptionSection =True1390 line +="\n"1391for messageLine in message.split("\n"):1392 line +="\t"+ messageLine +"\n"13931394 result += line +"\n"13951396return result13971398defpatchRCSKeywords(self,file, pattern):1399# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1400(handle, outFileName) = tempfile.mkstemp(dir='.')1401try:1402 outFile = os.fdopen(handle,"w+")1403 inFile =open(file,"r")1404 regexp = re.compile(pattern, re.VERBOSE)1405for line in inFile.readlines():1406 line = regexp.sub(r'$\1$', line)1407 outFile.write(line)1408 inFile.close()1409 outFile.close()1410# Forcibly overwrite the original file1411 os.unlink(file)1412 shutil.move(outFileName,file)1413except:1414# cleanup our temporary file1415 os.unlink(outFileName)1416print"Failed to strip RCS keywords in%s"%file1417raise14181419print"Patched up RCS keywords in%s"%file14201421defp4UserForCommit(self,id):1422# Return the tuple (perforce user,git email) for a given git commit id1423 self.getUserMapFromPerforceServer()1424 gitEmail =read_pipe(["git","log","--max-count=1",1425"--format=%ae",id])1426 gitEmail = gitEmail.strip()1427if not self.emails.has_key(gitEmail):1428return(None,gitEmail)1429else:1430return(self.emails[gitEmail],gitEmail)14311432defcheckValidP4Users(self,commits):1433# check if any git authors cannot be mapped to p4 users1434foridin commits:1435(user,email) = self.p4UserForCommit(id)1436if not user:1437 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1438ifgitConfigBool("git-p4.allowMissingP4Users"):1439print"%s"% msg1440else:1441die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14421443deflastP4Changelist(self):1444# Get back the last changelist number submitted in this client spec. This1445# then gets used to patch up the username in the change. If the same1446# client spec is being used by multiple processes then this might go1447# wrong.1448 results =p4CmdList("client -o")# find the current client1449 client =None1450for r in results:1451if r.has_key('Client'):1452 client = r['Client']1453break1454if not client:1455die("could not get client spec")1456 results =p4CmdList(["changes","-c", client,"-m","1"])1457for r in results:1458if r.has_key('change'):1459return r['change']1460die("Could not get changelist number for last submit - cannot patch up user details")14611462defmodifyChangelistUser(self, changelist, newUser):1463# fixup the user field of a changelist after it has been submitted.1464 changes =p4CmdList("change -o%s"% changelist)1465iflen(changes) !=1:1466die("Bad output from p4 change modifying%sto user%s"%1467(changelist, newUser))14681469 c = changes[0]1470if c['User'] == newUser:return# nothing to do1471 c['User'] = newUser1472input= marshal.dumps(c)14731474 result =p4CmdList("change -f -i", stdin=input)1475for r in result:1476if r.has_key('code'):1477if r['code'] =='error':1478die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1479if r.has_key('data'):1480print("Updated user field for changelist%sto%s"% (changelist, newUser))1481return1482die("Could not modify user field of changelist%sto%s"% (changelist, newUser))14831484defcanChangeChangelists(self):1485# check to see if we have p4 admin or super-user permissions, either of1486# which are required to modify changelists.1487 results =p4CmdList(["protects", self.depotPath])1488for r in results:1489if r.has_key('perm'):1490if r['perm'] =='admin':1491return11492if r['perm'] =='super':1493return11494return014951496defprepareSubmitTemplate(self, changelist=None):1497"""Run "p4 change -o" to grab a change specification template.1498 This does not use "p4 -G", as it is nice to keep the submission1499 template in original order, since a human might edit it.15001501 Remove lines in the Files section that show changes to files1502 outside the depot path we're committing into."""15031504[upstream, settings] =findUpstreamBranchPoint()15051506 template =""1507 inFilesSection =False1508 args = ['change','-o']1509if changelist:1510 args.append(str(changelist))15111512for line inp4_read_pipe_lines(args):1513if line.endswith("\r\n"):1514 line = line[:-2] +"\n"1515if inFilesSection:1516if line.startswith("\t"):1517# path starts and ends with a tab1518 path = line[1:]1519 lastTab = path.rfind("\t")1520if lastTab != -1:1521 path = path[:lastTab]1522if settings.has_key('depot-paths'):1523if not[p for p in settings['depot-paths']1524ifp4PathStartsWith(path, p)]:1525continue1526else:1527if notp4PathStartsWith(path, self.depotPath):1528continue1529else:1530 inFilesSection =False1531else:1532if line.startswith("Files:"):1533 inFilesSection =True15341535 template += line15361537return template15381539defedit_template(self, template_file):1540"""Invoke the editor to let the user change the submission1541 message. Return true if okay to continue with the submit."""15421543# if configured to skip the editing part, just submit1544ifgitConfigBool("git-p4.skipSubmitEdit"):1545return True15461547# look at the modification time, to check later if the user saved1548# the file1549 mtime = os.stat(template_file).st_mtime15501551# invoke the editor1552if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1553 editor = os.environ.get("P4EDITOR")1554else:1555 editor =read_pipe("git var GIT_EDITOR").strip()1556system(["sh","-c", ('%s"$@"'% editor), editor, template_file])15571558# If the file was not saved, prompt to see if this patch should1559# be skipped. But skip this verification step if configured so.1560ifgitConfigBool("git-p4.skipSubmitEditCheck"):1561return True15621563# modification time updated means user saved the file1564if os.stat(template_file).st_mtime > mtime:1565return True15661567while True:1568 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1569if response =='y':1570return True1571if response =='n':1572return False15731574defget_diff_description(self, editedFiles, filesToAdd):1575# diff1576if os.environ.has_key("P4DIFF"):1577del(os.environ["P4DIFF"])1578 diff =""1579for editedFile in editedFiles:1580 diff +=p4_read_pipe(['diff','-du',1581wildcard_encode(editedFile)])15821583# new file diff1584 newdiff =""1585for newFile in filesToAdd:1586 newdiff +="==== new file ====\n"1587 newdiff +="--- /dev/null\n"1588 newdiff +="+++%s\n"% newFile1589 f =open(newFile,"r")1590for line in f.readlines():1591 newdiff +="+"+ line1592 f.close()15931594return(diff + newdiff).replace('\r\n','\n')15951596defapplyCommit(self,id):1597"""Apply one commit, return True if it succeeded."""15981599print"Applying",read_pipe(["git","show","-s",1600"--format=format:%h%s",id])16011602(p4User, gitEmail) = self.p4UserForCommit(id)16031604 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1605 filesToAdd =set()1606 filesToChangeType =set()1607 filesToDelete =set()1608 editedFiles =set()1609 pureRenameCopy =set()1610 filesToChangeExecBit = {}1611 all_files =list()16121613for line in diff:1614 diff =parseDiffTreeEntry(line)1615 modifier = diff['status']1616 path = diff['src']1617 all_files.append(path)16181619if modifier =="M":1620p4_edit(path)1621ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1622 filesToChangeExecBit[path] = diff['dst_mode']1623 editedFiles.add(path)1624elif modifier =="A":1625 filesToAdd.add(path)1626 filesToChangeExecBit[path] = diff['dst_mode']1627if path in filesToDelete:1628 filesToDelete.remove(path)1629elif modifier =="D":1630 filesToDelete.add(path)1631if path in filesToAdd:1632 filesToAdd.remove(path)1633elif modifier =="C":1634 src, dest = diff['src'], diff['dst']1635p4_integrate(src, dest)1636 pureRenameCopy.add(dest)1637if diff['src_sha1'] != diff['dst_sha1']:1638p4_edit(dest)1639 pureRenameCopy.discard(dest)1640ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1641p4_edit(dest)1642 pureRenameCopy.discard(dest)1643 filesToChangeExecBit[dest] = diff['dst_mode']1644if self.isWindows:1645# turn off read-only attribute1646 os.chmod(dest, stat.S_IWRITE)1647 os.unlink(dest)1648 editedFiles.add(dest)1649elif modifier =="R":1650 src, dest = diff['src'], diff['dst']1651if self.p4HasMoveCommand:1652p4_edit(src)# src must be open before move1653p4_move(src, dest)# opens for (move/delete, move/add)1654else:1655p4_integrate(src, dest)1656if diff['src_sha1'] != diff['dst_sha1']:1657p4_edit(dest)1658else:1659 pureRenameCopy.add(dest)1660ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1661if not self.p4HasMoveCommand:1662p4_edit(dest)# with move: already open, writable1663 filesToChangeExecBit[dest] = diff['dst_mode']1664if not self.p4HasMoveCommand:1665if self.isWindows:1666 os.chmod(dest, stat.S_IWRITE)1667 os.unlink(dest)1668 filesToDelete.add(src)1669 editedFiles.add(dest)1670elif modifier =="T":1671 filesToChangeType.add(path)1672else:1673die("unknown modifier%sfor%s"% (modifier, path))16741675 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1676 patchcmd = diffcmd +" | git apply "1677 tryPatchCmd = patchcmd +"--check -"1678 applyPatchCmd = patchcmd +"--check --apply -"1679 patch_succeeded =True16801681if os.system(tryPatchCmd) !=0:1682 fixed_rcs_keywords =False1683 patch_succeeded =False1684print"Unfortunately applying the change failed!"16851686# Patch failed, maybe it's just RCS keyword woes. Look through1687# the patch to see if that's possible.1688ifgitConfigBool("git-p4.attemptRCSCleanup"):1689file=None1690 pattern =None1691 kwfiles = {}1692forfilein editedFiles | filesToDelete:1693# did this file's delta contain RCS keywords?1694 pattern =p4_keywords_regexp_for_file(file)16951696if pattern:1697# this file is a possibility...look for RCS keywords.1698 regexp = re.compile(pattern, re.VERBOSE)1699for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1700if regexp.search(line):1701if verbose:1702print"got keyword match on%sin%sin%s"% (pattern, line,file)1703 kwfiles[file] = pattern1704break17051706forfilein kwfiles:1707if verbose:1708print"zapping%swith%s"% (line,pattern)1709# File is being deleted, so not open in p4. Must1710# disable the read-only bit on windows.1711if self.isWindows andfilenot in editedFiles:1712 os.chmod(file, stat.S_IWRITE)1713 self.patchRCSKeywords(file, kwfiles[file])1714 fixed_rcs_keywords =True17151716if fixed_rcs_keywords:1717print"Retrying the patch with RCS keywords cleaned up"1718if os.system(tryPatchCmd) ==0:1719 patch_succeeded =True17201721if not patch_succeeded:1722for f in editedFiles:1723p4_revert(f)1724return False17251726#1727# Apply the patch for real, and do add/delete/+x handling.1728#1729system(applyPatchCmd)17301731for f in filesToChangeType:1732p4_edit(f,"-t","auto")1733for f in filesToAdd:1734p4_add(f)1735for f in filesToDelete:1736p4_revert(f)1737p4_delete(f)17381739# Set/clear executable bits1740for f in filesToChangeExecBit.keys():1741 mode = filesToChangeExecBit[f]1742setP4ExecBit(f, mode)17431744if self.update_shelve:1745print("all_files =%s"%str(all_files))1746p4_reopen_in_change(self.update_shelve, all_files)17471748#1749# Build p4 change description, starting with the contents1750# of the git commit message.1751#1752 logMessage =extractLogMessageFromGitCommit(id)1753 logMessage = logMessage.strip()1754(logMessage, jobs) = self.separate_jobs_from_description(logMessage)17551756 template = self.prepareSubmitTemplate(self.update_shelve)1757 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)17581759if self.preserveUser:1760 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User17611762if self.checkAuthorship and not self.p4UserIsMe(p4User):1763 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1764 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1765 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"17661767 separatorLine ="######## everything below this line is just the diff #######\n"1768if not self.prepare_p4_only:1769 submitTemplate += separatorLine1770 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)17711772(handle, fileName) = tempfile.mkstemp()1773 tmpFile = os.fdopen(handle,"w+b")1774if self.isWindows:1775 submitTemplate = submitTemplate.replace("\n","\r\n")1776 tmpFile.write(submitTemplate)1777 tmpFile.close()17781779if self.prepare_p4_only:1780#1781# Leave the p4 tree prepared, and the submit template around1782# and let the user decide what to do next1783#1784print1785print"P4 workspace prepared for submission."1786print"To submit or revert, go to client workspace"1787print" "+ self.clientPath1788print1789print"To submit, use\"p4 submit\"to write a new description,"1790print"or\"p4 submit -i <%s\"to use the one prepared by" \1791"\"git p4\"."% fileName1792print"You can delete the file\"%s\"when finished."% fileName17931794if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1795print"To preserve change ownership by user%s, you must\n" \1796"do\"p4 change -f <change>\"after submitting and\n" \1797"edit the User field."1798if pureRenameCopy:1799print"After submitting, renamed files must be re-synced."1800print"Invoke\"p4 sync -f\"on each of these files:"1801for f in pureRenameCopy:1802print" "+ f18031804print1805print"To revert the changes, use\"p4 revert ...\", and delete"1806print"the submit template file\"%s\""% fileName1807if filesToAdd:1808print"Since the commit adds new files, they must be deleted:"1809for f in filesToAdd:1810print" "+ f1811print1812return True18131814#1815# Let the user edit the change description, then submit it.1816#1817 submitted =False18181819try:1820if self.edit_template(fileName):1821# read the edited message and submit1822 tmpFile =open(fileName,"rb")1823 message = tmpFile.read()1824 tmpFile.close()1825if self.isWindows:1826 message = message.replace("\r\n","\n")1827 submitTemplate = message[:message.index(separatorLine)]18281829if self.update_shelve:1830p4_write_pipe(['shelve','-r','-i'], submitTemplate)1831elif self.shelve:1832p4_write_pipe(['shelve','-i'], submitTemplate)1833else:1834p4_write_pipe(['submit','-i'], submitTemplate)1835# The rename/copy happened by applying a patch that created a1836# new file. This leaves it writable, which confuses p4.1837for f in pureRenameCopy:1838p4_sync(f,"-f")18391840if self.preserveUser:1841if p4User:1842# Get last changelist number. Cannot easily get it from1843# the submit command output as the output is1844# unmarshalled.1845 changelist = self.lastP4Changelist()1846 self.modifyChangelistUser(changelist, p4User)18471848 submitted =True18491850finally:1851# skip this patch1852if not submitted or self.shelve:1853if self.shelve:1854print("Reverting shelved files.")1855else:1856print("Submission cancelled, undoing p4 changes.")1857for f in editedFiles | filesToDelete:1858p4_revert(f)1859for f in filesToAdd:1860p4_revert(f)1861 os.remove(f)18621863 os.remove(fileName)1864return submitted18651866# Export git tags as p4 labels. Create a p4 label and then tag1867# with that.1868defexportGitTags(self, gitTags):1869 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1870iflen(validLabelRegexp) ==0:1871 validLabelRegexp = defaultLabelRegexp1872 m = re.compile(validLabelRegexp)18731874for name in gitTags:18751876if not m.match(name):1877if verbose:1878print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1879continue18801881# Get the p4 commit this corresponds to1882 logMessage =extractLogMessageFromGitCommit(name)1883 values =extractSettingsGitLog(logMessage)18841885if not values.has_key('change'):1886# a tag pointing to something not sent to p4; ignore1887if verbose:1888print"git tag%sdoes not give a p4 commit"% name1889continue1890else:1891 changelist = values['change']18921893# Get the tag details.1894 inHeader =True1895 isAnnotated =False1896 body = []1897for l inread_pipe_lines(["git","cat-file","-p", name]):1898 l = l.strip()1899if inHeader:1900if re.match(r'tag\s+', l):1901 isAnnotated =True1902elif re.match(r'\s*$', l):1903 inHeader =False1904continue1905else:1906 body.append(l)19071908if not isAnnotated:1909 body = ["lightweight tag imported by git p4\n"]19101911# Create the label - use the same view as the client spec we are using1912 clientSpec =getClientSpec()19131914 labelTemplate ="Label:%s\n"% name1915 labelTemplate +="Description:\n"1916for b in body:1917 labelTemplate +="\t"+ b +"\n"1918 labelTemplate +="View:\n"1919for depot_side in clientSpec.mappings:1920 labelTemplate +="\t%s\n"% depot_side19211922if self.dry_run:1923print"Would create p4 label%sfor tag"% name1924elif self.prepare_p4_only:1925print"Not creating p4 label%sfor tag due to option" \1926" --prepare-p4-only"% name1927else:1928p4_write_pipe(["label","-i"], labelTemplate)19291930# Use the label1931p4_system(["tag","-l", name] +1932["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])19331934if verbose:1935print"created p4 label for tag%s"% name19361937defrun(self, args):1938iflen(args) ==0:1939 self.master =currentGitBranch()1940eliflen(args) ==1:1941 self.master = args[0]1942if notbranchExists(self.master):1943die("Branch%sdoes not exist"% self.master)1944else:1945return False19461947if self.master:1948 allowSubmit =gitConfig("git-p4.allowSubmit")1949iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1950die("%sis not in git-p4.allowSubmit"% self.master)19511952[upstream, settings] =findUpstreamBranchPoint()1953 self.depotPath = settings['depot-paths'][0]1954iflen(self.origin) ==0:1955 self.origin = upstream19561957if self.update_shelve:1958 self.shelve =True19591960if self.preserveUser:1961if not self.canChangeChangelists():1962die("Cannot preserve user names without p4 super-user or admin permissions")19631964# if not set from the command line, try the config file1965if self.conflict_behavior is None:1966 val =gitConfig("git-p4.conflict")1967if val:1968if val not in self.conflict_behavior_choices:1969die("Invalid value '%s' for config git-p4.conflict"% val)1970else:1971 val ="ask"1972 self.conflict_behavior = val19731974if self.verbose:1975print"Origin branch is "+ self.origin19761977iflen(self.depotPath) ==0:1978print"Internal error: cannot locate perforce depot path from existing branches"1979 sys.exit(128)19801981 self.useClientSpec =False1982ifgitConfigBool("git-p4.useclientspec"):1983 self.useClientSpec =True1984if self.useClientSpec:1985 self.clientSpecDirs =getClientSpec()19861987# Check for the existence of P4 branches1988 branchesDetected = (len(p4BranchesInGit().keys()) >1)19891990if self.useClientSpec and not branchesDetected:1991# all files are relative to the client spec1992 self.clientPath =getClientRoot()1993else:1994 self.clientPath =p4Where(self.depotPath)19951996if self.clientPath =="":1997die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)19981999print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2000 self.oldWorkingDirectory = os.getcwd()20012002# ensure the clientPath exists2003 new_client_dir =False2004if not os.path.exists(self.clientPath):2005 new_client_dir =True2006 os.makedirs(self.clientPath)20072008chdir(self.clientPath, is_client_path=True)2009if self.dry_run:2010print"Would synchronize p4 checkout in%s"% self.clientPath2011else:2012print"Synchronizing p4 checkout..."2013if new_client_dir:2014# old one was destroyed, and maybe nobody told p42015p4_sync("...","-f")2016else:2017p4_sync("...")2018 self.check()20192020 commits = []2021if self.master:2022 commitish = self.master2023else:2024 commitish ='HEAD'20252026for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2027 commits.append(line.strip())2028 commits.reverse()20292030if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2031 self.checkAuthorship =False2032else:2033 self.checkAuthorship =True20342035if self.preserveUser:2036 self.checkValidP4Users(commits)20372038#2039# Build up a set of options to be passed to diff when2040# submitting each commit to p4.2041#2042if self.detectRenames:2043# command-line -M arg2044 self.diffOpts ="-M"2045else:2046# If not explicitly set check the config variable2047 detectRenames =gitConfig("git-p4.detectRenames")20482049if detectRenames.lower() =="false"or detectRenames =="":2050 self.diffOpts =""2051elif detectRenames.lower() =="true":2052 self.diffOpts ="-M"2053else:2054 self.diffOpts ="-M%s"% detectRenames20552056# no command-line arg for -C or --find-copies-harder, just2057# config variables2058 detectCopies =gitConfig("git-p4.detectCopies")2059if detectCopies.lower() =="false"or detectCopies =="":2060pass2061elif detectCopies.lower() =="true":2062 self.diffOpts +=" -C"2063else:2064 self.diffOpts +=" -C%s"% detectCopies20652066ifgitConfigBool("git-p4.detectCopiesHarder"):2067 self.diffOpts +=" --find-copies-harder"20682069#2070# Apply the commits, one at a time. On failure, ask if should2071# continue to try the rest of the patches, or quit.2072#2073if self.dry_run:2074print"Would apply"2075 applied = []2076 last =len(commits) -12077for i, commit inenumerate(commits):2078if self.dry_run:2079print" ",read_pipe(["git","show","-s",2080"--format=format:%h%s", commit])2081 ok =True2082else:2083 ok = self.applyCommit(commit)2084if ok:2085 applied.append(commit)2086else:2087if self.prepare_p4_only and i < last:2088print"Processing only the first commit due to option" \2089" --prepare-p4-only"2090break2091if i < last:2092 quit =False2093while True:2094# prompt for what to do, or use the option/variable2095if self.conflict_behavior =="ask":2096print"What do you want to do?"2097 response =raw_input("[s]kip this commit but apply"2098" the rest, or [q]uit? ")2099if not response:2100continue2101elif self.conflict_behavior =="skip":2102 response ="s"2103elif self.conflict_behavior =="quit":2104 response ="q"2105else:2106die("Unknown conflict_behavior '%s'"%2107 self.conflict_behavior)21082109if response[0] =="s":2110print"Skipping this commit, but applying the rest"2111break2112if response[0] =="q":2113print"Quitting"2114 quit =True2115break2116if quit:2117break21182119chdir(self.oldWorkingDirectory)2120 shelved_applied ="shelved"if self.shelve else"applied"2121if self.dry_run:2122pass2123elif self.prepare_p4_only:2124pass2125eliflen(commits) ==len(applied):2126print("All commits{0}!".format(shelved_applied))21272128 sync =P4Sync()2129if self.branch:2130 sync.branch = self.branch2131 sync.run([])21322133 rebase =P4Rebase()2134 rebase.rebase()21352136else:2137iflen(applied) ==0:2138print("No commits{0}.".format(shelved_applied))2139else:2140print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2141for c in commits:2142if c in applied:2143 star ="*"2144else:2145 star =" "2146print star,read_pipe(["git","show","-s",2147"--format=format:%h%s", c])2148print"You will have to do 'git p4 sync' and rebase."21492150ifgitConfigBool("git-p4.exportLabels"):2151 self.exportLabels =True21522153if self.exportLabels:2154 p4Labels =getP4Labels(self.depotPath)2155 gitTags =getGitTags()21562157 missingGitTags = gitTags - p4Labels2158 self.exportGitTags(missingGitTags)21592160# exit with error unless everything applied perfectly2161iflen(commits) !=len(applied):2162 sys.exit(1)21632164return True21652166classView(object):2167"""Represent a p4 view ("p4 help views"), and map files in a2168 repo according to the view."""21692170def__init__(self, client_name):2171 self.mappings = []2172 self.client_prefix ="//%s/"% client_name2173# cache results of "p4 where" to lookup client file locations2174 self.client_spec_path_cache = {}21752176defappend(self, view_line):2177"""Parse a view line, splitting it into depot and client2178 sides. Append to self.mappings, preserving order. This2179 is only needed for tag creation."""21802181# Split the view line into exactly two words. P4 enforces2182# structure on these lines that simplifies this quite a bit.2183#2184# Either or both words may be double-quoted.2185# Single quotes do not matter.2186# Double-quote marks cannot occur inside the words.2187# A + or - prefix is also inside the quotes.2188# There are no quotes unless they contain a space.2189# The line is already white-space stripped.2190# The two words are separated by a single space.2191#2192if view_line[0] =='"':2193# First word is double quoted. Find its end.2194 close_quote_index = view_line.find('"',1)2195if close_quote_index <=0:2196die("No first-word closing quote found:%s"% view_line)2197 depot_side = view_line[1:close_quote_index]2198# skip closing quote and space2199 rhs_index = close_quote_index +1+12200else:2201 space_index = view_line.find(" ")2202if space_index <=0:2203die("No word-splitting space found:%s"% view_line)2204 depot_side = view_line[0:space_index]2205 rhs_index = space_index +122062207# prefix + means overlay on previous mapping2208if depot_side.startswith("+"):2209 depot_side = depot_side[1:]22102211# prefix - means exclude this path, leave out of mappings2212 exclude =False2213if depot_side.startswith("-"):2214 exclude =True2215 depot_side = depot_side[1:]22162217if not exclude:2218 self.mappings.append(depot_side)22192220defconvert_client_path(self, clientFile):2221# chop off //client/ part to make it relative2222if not clientFile.startswith(self.client_prefix):2223die("No prefix '%s' on clientFile '%s'"%2224(self.client_prefix, clientFile))2225return clientFile[len(self.client_prefix):]22262227defupdate_client_spec_path_cache(self, files):2228""" Caching file paths by "p4 where" batch query """22292230# List depot file paths exclude that already cached2231 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]22322233iflen(fileArgs) ==0:2234return# All files in cache22352236 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2237for res in where_result:2238if"code"in res and res["code"] =="error":2239# assume error is "... file(s) not in client view"2240continue2241if"clientFile"not in res:2242die("No clientFile in 'p4 where' output")2243if"unmap"in res:2244# it will list all of them, but only one not unmap-ped2245continue2246ifgitConfigBool("core.ignorecase"):2247 res['depotFile'] = res['depotFile'].lower()2248 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])22492250# not found files or unmap files set to ""2251for depotFile in fileArgs:2252ifgitConfigBool("core.ignorecase"):2253 depotFile = depotFile.lower()2254if depotFile not in self.client_spec_path_cache:2255 self.client_spec_path_cache[depotFile] =""22562257defmap_in_client(self, depot_path):2258"""Return the relative location in the client where this2259 depot file should live. Returns "" if the file should2260 not be mapped in the client."""22612262ifgitConfigBool("core.ignorecase"):2263 depot_path = depot_path.lower()22642265if depot_path in self.client_spec_path_cache:2266return self.client_spec_path_cache[depot_path]22672268die("Error:%sis not found in client spec path"% depot_path )2269return""22702271classP4Sync(Command, P4UserMap):2272 delete_actions = ("delete","move/delete","purge")22732274def__init__(self):2275 Command.__init__(self)2276 P4UserMap.__init__(self)2277 self.options = [2278 optparse.make_option("--branch", dest="branch"),2279 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2280 optparse.make_option("--changesfile", dest="changesFile"),2281 optparse.make_option("--silent", dest="silent", action="store_true"),2282 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2283 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2284 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2285help="Import into refs/heads/ , not refs/remotes"),2286 optparse.make_option("--max-changes", dest="maxChanges",2287help="Maximum number of changes to import"),2288 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2289help="Internal block size to use when iteratively calling p4 changes"),2290 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2291help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2292 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2293help="Only sync files that are included in the Perforce Client Spec"),2294 optparse.make_option("-/", dest="cloneExclude",2295 action="append",type="string",2296help="exclude depot path"),2297]2298 self.description ="""Imports from Perforce into a git repository.\n2299 example:2300 //depot/my/project/ -- to import the current head2301 //depot/my/project/@all -- to import everything2302 //depot/my/project/@1,6 -- to import only from revision 1 to 623032304 (a ... is not needed in the path p4 specification, it's added implicitly)"""23052306 self.usage +=" //depot/path[@revRange]"2307 self.silent =False2308 self.createdBranches =set()2309 self.committedChanges =set()2310 self.branch =""2311 self.detectBranches =False2312 self.detectLabels =False2313 self.importLabels =False2314 self.changesFile =""2315 self.syncWithOrigin =True2316 self.importIntoRemotes =True2317 self.maxChanges =""2318 self.changes_block_size =None2319 self.keepRepoPath =False2320 self.depotPaths =None2321 self.p4BranchesInGit = []2322 self.cloneExclude = []2323 self.useClientSpec =False2324 self.useClientSpec_from_options =False2325 self.clientSpecDirs =None2326 self.tempBranches = []2327 self.tempBranchLocation ="refs/git-p4-tmp"2328 self.largeFileSystem =None23292330ifgitConfig('git-p4.largeFileSystem'):2331 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2332 self.largeFileSystem =largeFileSystemConstructor(2333lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2334)23352336ifgitConfig("git-p4.syncFromOrigin") =="false":2337 self.syncWithOrigin =False23382339# This is required for the "append" cloneExclude action2340defensure_value(self, attr, value):2341if nothasattr(self, attr)orgetattr(self, attr)is None:2342setattr(self, attr, value)2343returngetattr(self, attr)23442345# Force a checkpoint in fast-import and wait for it to finish2346defcheckpoint(self):2347 self.gitStream.write("checkpoint\n\n")2348 self.gitStream.write("progress checkpoint\n\n")2349 out = self.gitOutput.readline()2350if self.verbose:2351print"checkpoint finished: "+ out23522353defextractFilesFromCommit(self, commit):2354 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2355for path in self.cloneExclude]2356 files = []2357 fnum =02358while commit.has_key("depotFile%s"% fnum):2359 path = commit["depotFile%s"% fnum]23602361if[p for p in self.cloneExclude2362ifp4PathStartsWith(path, p)]:2363 found =False2364else:2365 found = [p for p in self.depotPaths2366ifp4PathStartsWith(path, p)]2367if not found:2368 fnum = fnum +12369continue23702371file= {}2372file["path"] = path2373file["rev"] = commit["rev%s"% fnum]2374file["action"] = commit["action%s"% fnum]2375file["type"] = commit["type%s"% fnum]2376 files.append(file)2377 fnum = fnum +12378return files23792380defextractJobsFromCommit(self, commit):2381 jobs = []2382 jnum =02383while commit.has_key("job%s"% jnum):2384 job = commit["job%s"% jnum]2385 jobs.append(job)2386 jnum = jnum +12387return jobs23882389defstripRepoPath(self, path, prefixes):2390"""When streaming files, this is called to map a p4 depot path2391 to where it should go in git. The prefixes are either2392 self.depotPaths, or self.branchPrefixes in the case of2393 branch detection."""23942395if self.useClientSpec:2396# branch detection moves files up a level (the branch name)2397# from what client spec interpretation gives2398 path = self.clientSpecDirs.map_in_client(path)2399if self.detectBranches:2400for b in self.knownBranches:2401if path.startswith(b +"/"):2402 path = path[len(b)+1:]24032404elif self.keepRepoPath:2405# Preserve everything in relative path name except leading2406# //depot/; just look at first prefix as they all should2407# be in the same depot.2408 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2409ifp4PathStartsWith(path, depot):2410 path = path[len(depot):]24112412else:2413for p in prefixes:2414ifp4PathStartsWith(path, p):2415 path = path[len(p):]2416break24172418 path =wildcard_decode(path)2419return path24202421defsplitFilesIntoBranches(self, commit):2422"""Look at each depotFile in the commit to figure out to what2423 branch it belongs."""24242425if self.clientSpecDirs:2426 files = self.extractFilesFromCommit(commit)2427 self.clientSpecDirs.update_client_spec_path_cache(files)24282429 branches = {}2430 fnum =02431while commit.has_key("depotFile%s"% fnum):2432 path = commit["depotFile%s"% fnum]2433 found = [p for p in self.depotPaths2434ifp4PathStartsWith(path, p)]2435if not found:2436 fnum = fnum +12437continue24382439file= {}2440file["path"] = path2441file["rev"] = commit["rev%s"% fnum]2442file["action"] = commit["action%s"% fnum]2443file["type"] = commit["type%s"% fnum]2444 fnum = fnum +124452446# start with the full relative path where this file would2447# go in a p4 client2448if self.useClientSpec:2449 relPath = self.clientSpecDirs.map_in_client(path)2450else:2451 relPath = self.stripRepoPath(path, self.depotPaths)24522453for branch in self.knownBranches.keys():2454# add a trailing slash so that a commit into qt/4.2foo2455# doesn't end up in qt/4.2, e.g.2456if relPath.startswith(branch +"/"):2457if branch not in branches:2458 branches[branch] = []2459 branches[branch].append(file)2460break24612462return branches24632464defwriteToGitStream(self, gitMode, relPath, contents):2465 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2466 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2467for d in contents:2468 self.gitStream.write(d)2469 self.gitStream.write('\n')24702471# output one file from the P4 stream2472# - helper for streamP4Files24732474defstreamOneP4File(self,file, contents):2475 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2476if verbose:2477 size =int(self.stream_file['fileSize'])2478 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2479 sys.stdout.flush()24802481(type_base, type_mods) =split_p4_type(file["type"])24822483 git_mode ="100644"2484if"x"in type_mods:2485 git_mode ="100755"2486if type_base =="symlink":2487 git_mode ="120000"2488# p4 print on a symlink sometimes contains "target\n";2489# if it does, remove the newline2490 data =''.join(contents)2491if not data:2492# Some version of p4 allowed creating a symlink that pointed2493# to nothing. This causes p4 errors when checking out such2494# a change, and errors here too. Work around it by ignoring2495# the bad symlink; hopefully a future change fixes it.2496print"\nIgnoring empty symlink in%s"%file['depotFile']2497return2498elif data[-1] =='\n':2499 contents = [data[:-1]]2500else:2501 contents = [data]25022503if type_base =="utf16":2504# p4 delivers different text in the python output to -G2505# than it does when using "print -o", or normal p4 client2506# operations. utf16 is converted to ascii or utf8, perhaps.2507# But ascii text saved as -t utf16 is completely mangled.2508# Invoke print -o to get the real contents.2509#2510# On windows, the newlines will always be mangled by print, so put2511# them back too. This is not needed to the cygwin windows version,2512# just the native "NT" type.2513#2514try:2515 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2516exceptExceptionas e:2517if'Translation of file content failed'instr(e):2518 type_base ='binary'2519else:2520raise e2521else:2522ifp4_version_string().find('/NT') >=0:2523 text = text.replace('\r\n','\n')2524 contents = [ text ]25252526if type_base =="apple":2527# Apple filetype files will be streamed as a concatenation of2528# its appledouble header and the contents. This is useless2529# on both macs and non-macs. If using "print -q -o xx", it2530# will create "xx" with the data, and "%xx" with the header.2531# This is also not very useful.2532#2533# Ideally, someday, this script can learn how to generate2534# appledouble files directly and import those to git, but2535# non-mac machines can never find a use for apple filetype.2536print"\nIgnoring apple filetype file%s"%file['depotFile']2537return25382539# Note that we do not try to de-mangle keywords on utf16 files,2540# even though in theory somebody may want that.2541 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2542if pattern:2543 regexp = re.compile(pattern, re.VERBOSE)2544 text =''.join(contents)2545 text = regexp.sub(r'$\1$', text)2546 contents = [ text ]25472548try:2549 relPath.decode('ascii')2550except:2551 encoding ='utf8'2552ifgitConfig('git-p4.pathEncoding'):2553 encoding =gitConfig('git-p4.pathEncoding')2554 relPath = relPath.decode(encoding,'replace').encode('utf8','replace')2555if self.verbose:2556print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, relPath)25572558if self.largeFileSystem:2559(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)25602561 self.writeToGitStream(git_mode, relPath, contents)25622563defstreamOneP4Deletion(self,file):2564 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2565if verbose:2566 sys.stdout.write("delete%s\n"% relPath)2567 sys.stdout.flush()2568 self.gitStream.write("D%s\n"% relPath)25692570if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2571 self.largeFileSystem.removeLargeFile(relPath)25722573# handle another chunk of streaming data2574defstreamP4FilesCb(self, marshalled):25752576# catch p4 errors and complain2577 err =None2578if"code"in marshalled:2579if marshalled["code"] =="error":2580if"data"in marshalled:2581 err = marshalled["data"].rstrip()25822583if not err and'fileSize'in self.stream_file:2584 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2585if required_bytes >0:2586 err ='Not enough space left on%s! Free at least%iMB.'% (2587 os.getcwd(), required_bytes/1024/10242588)25892590if err:2591 f =None2592if self.stream_have_file_info:2593if"depotFile"in self.stream_file:2594 f = self.stream_file["depotFile"]2595# force a failure in fast-import, else an empty2596# commit will be made2597 self.gitStream.write("\n")2598 self.gitStream.write("die-now\n")2599 self.gitStream.close()2600# ignore errors, but make sure it exits first2601 self.importProcess.wait()2602if f:2603die("Error from p4 print for%s:%s"% (f, err))2604else:2605die("Error from p4 print:%s"% err)26062607if marshalled.has_key('depotFile')and self.stream_have_file_info:2608# start of a new file - output the old one first2609 self.streamOneP4File(self.stream_file, self.stream_contents)2610 self.stream_file = {}2611 self.stream_contents = []2612 self.stream_have_file_info =False26132614# pick up the new file information... for the2615# 'data' field we need to append to our array2616for k in marshalled.keys():2617if k =='data':2618if'streamContentSize'not in self.stream_file:2619 self.stream_file['streamContentSize'] =02620 self.stream_file['streamContentSize'] +=len(marshalled['data'])2621 self.stream_contents.append(marshalled['data'])2622else:2623 self.stream_file[k] = marshalled[k]26242625if(verbose and2626'streamContentSize'in self.stream_file and2627'fileSize'in self.stream_file and2628'depotFile'in self.stream_file):2629 size =int(self.stream_file["fileSize"])2630if size >0:2631 progress =100*self.stream_file['streamContentSize']/size2632 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2633 sys.stdout.flush()26342635 self.stream_have_file_info =True26362637# Stream directly from "p4 files" into "git fast-import"2638defstreamP4Files(self, files):2639 filesForCommit = []2640 filesToRead = []2641 filesToDelete = []26422643for f in files:2644 filesForCommit.append(f)2645if f['action']in self.delete_actions:2646 filesToDelete.append(f)2647else:2648 filesToRead.append(f)26492650# deleted files...2651for f in filesToDelete:2652 self.streamOneP4Deletion(f)26532654iflen(filesToRead) >0:2655 self.stream_file = {}2656 self.stream_contents = []2657 self.stream_have_file_info =False26582659# curry self argument2660defstreamP4FilesCbSelf(entry):2661 self.streamP4FilesCb(entry)26622663 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]26642665p4CmdList(["-x","-","print"],2666 stdin=fileArgs,2667 cb=streamP4FilesCbSelf)26682669# do the last chunk2670if self.stream_file.has_key('depotFile'):2671 self.streamOneP4File(self.stream_file, self.stream_contents)26722673defmake_email(self, userid):2674if userid in self.users:2675return self.users[userid]2676else:2677return"%s<a@b>"% userid26782679defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2680""" Stream a p4 tag.2681 commit is either a git commit, or a fast-import mark, ":<p4commit>"2682 """26832684if verbose:2685print"writing tag%sfor commit%s"% (labelName, commit)2686 gitStream.write("tag%s\n"% labelName)2687 gitStream.write("from%s\n"% commit)26882689if labelDetails.has_key('Owner'):2690 owner = labelDetails["Owner"]2691else:2692 owner =None26932694# Try to use the owner of the p4 label, or failing that,2695# the current p4 user id.2696if owner:2697 email = self.make_email(owner)2698else:2699 email = self.make_email(self.p4UserId())2700 tagger ="%s %s %s"% (email, epoch, self.tz)27012702 gitStream.write("tagger%s\n"% tagger)27032704print"labelDetails=",labelDetails2705if labelDetails.has_key('Description'):2706 description = labelDetails['Description']2707else:2708 description ='Label from git p4'27092710 gitStream.write("data%d\n"%len(description))2711 gitStream.write(description)2712 gitStream.write("\n")27132714definClientSpec(self, path):2715if not self.clientSpecDirs:2716return True2717 inClientSpec = self.clientSpecDirs.map_in_client(path)2718if not inClientSpec and self.verbose:2719print('Ignoring file outside of client spec:{0}'.format(path))2720return inClientSpec27212722defhasBranchPrefix(self, path):2723if not self.branchPrefixes:2724return True2725 hasPrefix = [p for p in self.branchPrefixes2726ifp4PathStartsWith(path, p)]2727if not hasPrefix and self.verbose:2728print('Ignoring file outside of prefix:{0}'.format(path))2729return hasPrefix27302731defcommit(self, details, files, branch, parent =""):2732 epoch = details["time"]2733 author = details["user"]2734 jobs = self.extractJobsFromCommit(details)27352736if self.verbose:2737print('commit into{0}'.format(branch))27382739if self.clientSpecDirs:2740 self.clientSpecDirs.update_client_spec_path_cache(files)27412742 files = [f for f in files2743if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]27442745if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2746print('Ignoring revision{0}as it would produce an empty commit.'2747.format(details['change']))2748return27492750 self.gitStream.write("commit%s\n"% branch)2751 self.gitStream.write("mark :%s\n"% details["change"])2752 self.committedChanges.add(int(details["change"]))2753 committer =""2754if author not in self.users:2755 self.getUserMapFromPerforceServer()2756 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)27572758 self.gitStream.write("committer%s\n"% committer)27592760 self.gitStream.write("data <<EOT\n")2761 self.gitStream.write(details["desc"])2762iflen(jobs) >0:2763 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2764 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2765(','.join(self.branchPrefixes), details["change"]))2766iflen(details['options']) >0:2767 self.gitStream.write(": options =%s"% details['options'])2768 self.gitStream.write("]\nEOT\n\n")27692770iflen(parent) >0:2771if self.verbose:2772print"parent%s"% parent2773 self.gitStream.write("from%s\n"% parent)27742775 self.streamP4Files(files)2776 self.gitStream.write("\n")27772778 change =int(details["change"])27792780if self.labels.has_key(change):2781 label = self.labels[change]2782 labelDetails = label[0]2783 labelRevisions = label[1]2784if self.verbose:2785print"Change%sis labelled%s"% (change, labelDetails)27862787 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2788for p in self.branchPrefixes])27892790iflen(files) ==len(labelRevisions):27912792 cleanedFiles = {}2793for info in files:2794if info["action"]in self.delete_actions:2795continue2796 cleanedFiles[info["depotFile"]] = info["rev"]27972798if cleanedFiles == labelRevisions:2799 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)28002801else:2802if not self.silent:2803print("Tag%sdoes not match with change%s: files do not match."2804% (labelDetails["label"], change))28052806else:2807if not self.silent:2808print("Tag%sdoes not match with change%s: file count is different."2809% (labelDetails["label"], change))28102811# Build a dictionary of changelists and labels, for "detect-labels" option.2812defgetLabels(self):2813 self.labels = {}28142815 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2816iflen(l) >0and not self.silent:2817print"Finding files belonging to labels in%s"% `self.depotPaths`28182819for output in l:2820 label = output["label"]2821 revisions = {}2822 newestChange =02823if self.verbose:2824print"Querying files for label%s"% label2825forfileinp4CmdList(["files"] +2826["%s...@%s"% (p, label)2827for p in self.depotPaths]):2828 revisions[file["depotFile"]] =file["rev"]2829 change =int(file["change"])2830if change > newestChange:2831 newestChange = change28322833 self.labels[newestChange] = [output, revisions]28342835if self.verbose:2836print"Label changes:%s"% self.labels.keys()28372838# Import p4 labels as git tags. A direct mapping does not2839# exist, so assume that if all the files are at the same revision2840# then we can use that, or it's something more complicated we should2841# just ignore.2842defimportP4Labels(self, stream, p4Labels):2843if verbose:2844print"import p4 labels: "+' '.join(p4Labels)28452846 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2847 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2848iflen(validLabelRegexp) ==0:2849 validLabelRegexp = defaultLabelRegexp2850 m = re.compile(validLabelRegexp)28512852for name in p4Labels:2853 commitFound =False28542855if not m.match(name):2856if verbose:2857print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2858continue28592860if name in ignoredP4Labels:2861continue28622863 labelDetails =p4CmdList(['label',"-o", name])[0]28642865# get the most recent changelist for each file in this label2866 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2867for p in self.depotPaths])28682869if change.has_key('change'):2870# find the corresponding git commit; take the oldest commit2871 changelist =int(change['change'])2872if changelist in self.committedChanges:2873 gitCommit =":%d"% changelist # use a fast-import mark2874 commitFound =True2875else:2876 gitCommit =read_pipe(["git","rev-list","--max-count=1",2877"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2878iflen(gitCommit) ==0:2879print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2880else:2881 commitFound =True2882 gitCommit = gitCommit.strip()28832884if commitFound:2885# Convert from p4 time format2886try:2887 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2888exceptValueError:2889print"Could not convert label time%s"% labelDetails['Update']2890 tmwhen =128912892 when =int(time.mktime(tmwhen))2893 self.streamTag(stream, name, labelDetails, gitCommit, when)2894if verbose:2895print"p4 label%smapped to git commit%s"% (name, gitCommit)2896else:2897if verbose:2898print"Label%shas no changelists - possibly deleted?"% name28992900if not commitFound:2901# We can't import this label; don't try again as it will get very2902# expensive repeatedly fetching all the files for labels that will2903# never be imported. If the label is moved in the future, the2904# ignore will need to be removed manually.2905system(["git","config","--add","git-p4.ignoredP4Labels", name])29062907defguessProjectName(self):2908for p in self.depotPaths:2909if p.endswith("/"):2910 p = p[:-1]2911 p = p[p.strip().rfind("/") +1:]2912if not p.endswith("/"):2913 p +="/"2914return p29152916defgetBranchMapping(self):2917 lostAndFoundBranches =set()29182919 user =gitConfig("git-p4.branchUser")2920iflen(user) >0:2921 command ="branches -u%s"% user2922else:2923 command ="branches"29242925for info inp4CmdList(command):2926 details =p4Cmd(["branch","-o", info["branch"]])2927 viewIdx =02928while details.has_key("View%s"% viewIdx):2929 paths = details["View%s"% viewIdx].split(" ")2930 viewIdx = viewIdx +12931# require standard //depot/foo/... //depot/bar/... mapping2932iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2933continue2934 source = paths[0]2935 destination = paths[1]2936## HACK2937ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2938 source = source[len(self.depotPaths[0]):-4]2939 destination = destination[len(self.depotPaths[0]):-4]29402941if destination in self.knownBranches:2942if not self.silent:2943print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2944print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2945continue29462947 self.knownBranches[destination] = source29482949 lostAndFoundBranches.discard(destination)29502951if source not in self.knownBranches:2952 lostAndFoundBranches.add(source)29532954# Perforce does not strictly require branches to be defined, so we also2955# check git config for a branch list.2956#2957# Example of branch definition in git config file:2958# [git-p4]2959# branchList=main:branchA2960# branchList=main:branchB2961# branchList=branchA:branchC2962 configBranches =gitConfigList("git-p4.branchList")2963for branch in configBranches:2964if branch:2965(source, destination) = branch.split(":")2966 self.knownBranches[destination] = source29672968 lostAndFoundBranches.discard(destination)29692970if source not in self.knownBranches:2971 lostAndFoundBranches.add(source)297229732974for branch in lostAndFoundBranches:2975 self.knownBranches[branch] = branch29762977defgetBranchMappingFromGitBranches(self):2978 branches =p4BranchesInGit(self.importIntoRemotes)2979for branch in branches.keys():2980if branch =="master":2981 branch ="main"2982else:2983 branch = branch[len(self.projectName):]2984 self.knownBranches[branch] = branch29852986defupdateOptionDict(self, d):2987 option_keys = {}2988if self.keepRepoPath:2989 option_keys['keepRepoPath'] =129902991 d["options"] =' '.join(sorted(option_keys.keys()))29922993defreadOptions(self, d):2994 self.keepRepoPath = (d.has_key('options')2995and('keepRepoPath'in d['options']))29962997defgitRefForBranch(self, branch):2998if branch =="main":2999return self.refPrefix +"master"30003001iflen(branch) <=0:3002return branch30033004return self.refPrefix + self.projectName + branch30053006defgitCommitByP4Change(self, ref, change):3007if self.verbose:3008print"looking in ref "+ ref +" for change%susing bisect..."% change30093010 earliestCommit =""3011 latestCommit =parseRevision(ref)30123013while True:3014if self.verbose:3015print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3016 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3017iflen(next) ==0:3018if self.verbose:3019print"argh"3020return""3021 log =extractLogMessageFromGitCommit(next)3022 settings =extractSettingsGitLog(log)3023 currentChange =int(settings['change'])3024if self.verbose:3025print"current change%s"% currentChange30263027if currentChange == change:3028if self.verbose:3029print"found%s"% next3030return next30313032if currentChange < change:3033 earliestCommit ="^%s"% next3034else:3035 latestCommit ="%s"% next30363037return""30383039defimportNewBranch(self, branch, maxChange):3040# make fast-import flush all changes to disk and update the refs using the checkpoint3041# command so that we can try to find the branch parent in the git history3042 self.gitStream.write("checkpoint\n\n");3043 self.gitStream.flush();3044 branchPrefix = self.depotPaths[0] + branch +"/"3045range="@1,%s"% maxChange3046#print "prefix" + branchPrefix3047 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3048iflen(changes) <=0:3049return False3050 firstChange = changes[0]3051#print "first change in branch: %s" % firstChange3052 sourceBranch = self.knownBranches[branch]3053 sourceDepotPath = self.depotPaths[0] + sourceBranch3054 sourceRef = self.gitRefForBranch(sourceBranch)3055#print "source " + sourceBranch30563057 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3058#print "branch parent: %s" % branchParentChange3059 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3060iflen(gitParent) >0:3061 self.initialParents[self.gitRefForBranch(branch)] = gitParent3062#print "parent git commit: %s" % gitParent30633064 self.importChanges(changes)3065return True30663067defsearchParent(self, parent, branch, target):3068 parentFound =False3069for blob inread_pipe_lines(["git","rev-list","--reverse",3070"--no-merges", parent]):3071 blob = blob.strip()3072iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3073 parentFound =True3074if self.verbose:3075print"Found parent of%sin commit%s"% (branch, blob)3076break3077if parentFound:3078return blob3079else:3080return None30813082defimportChanges(self, changes):3083 cnt =13084for change in changes:3085 description =p4_describe(change)3086 self.updateOptionDict(description)30873088if not self.silent:3089 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3090 sys.stdout.flush()3091 cnt = cnt +130923093try:3094if self.detectBranches:3095 branches = self.splitFilesIntoBranches(description)3096for branch in branches.keys():3097## HACK --hwn3098 branchPrefix = self.depotPaths[0] + branch +"/"3099 self.branchPrefixes = [ branchPrefix ]31003101 parent =""31023103 filesForCommit = branches[branch]31043105if self.verbose:3106print"branch is%s"% branch31073108 self.updatedBranches.add(branch)31093110if branch not in self.createdBranches:3111 self.createdBranches.add(branch)3112 parent = self.knownBranches[branch]3113if parent == branch:3114 parent =""3115else:3116 fullBranch = self.projectName + branch3117if fullBranch not in self.p4BranchesInGit:3118if not self.silent:3119print("\nImporting new branch%s"% fullBranch);3120if self.importNewBranch(branch, change -1):3121 parent =""3122 self.p4BranchesInGit.append(fullBranch)3123if not self.silent:3124print("\nResuming with change%s"% change);31253126if self.verbose:3127print"parent determined through known branches:%s"% parent31283129 branch = self.gitRefForBranch(branch)3130 parent = self.gitRefForBranch(parent)31313132if self.verbose:3133print"looking for initial parent for%s; current parent is%s"% (branch, parent)31343135iflen(parent) ==0and branch in self.initialParents:3136 parent = self.initialParents[branch]3137del self.initialParents[branch]31383139 blob =None3140iflen(parent) >0:3141 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3142if self.verbose:3143print"Creating temporary branch: "+ tempBranch3144 self.commit(description, filesForCommit, tempBranch)3145 self.tempBranches.append(tempBranch)3146 self.checkpoint()3147 blob = self.searchParent(parent, branch, tempBranch)3148if blob:3149 self.commit(description, filesForCommit, branch, blob)3150else:3151if self.verbose:3152print"Parent of%snot found. Committing into head of%s"% (branch, parent)3153 self.commit(description, filesForCommit, branch, parent)3154else:3155 files = self.extractFilesFromCommit(description)3156 self.commit(description, files, self.branch,3157 self.initialParent)3158# only needed once, to connect to the previous commit3159 self.initialParent =""3160exceptIOError:3161print self.gitError.read()3162 sys.exit(1)31633164defimportHeadRevision(self, revision):3165print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)31663167 details = {}3168 details["user"] ="git perforce import user"3169 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3170% (' '.join(self.depotPaths), revision))3171 details["change"] = revision3172 newestRevision =031733174 fileCnt =03175 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]31763177for info inp4CmdList(["files"] + fileArgs):31783179if'code'in info and info['code'] =='error':3180 sys.stderr.write("p4 returned an error:%s\n"3181% info['data'])3182if info['data'].find("must refer to client") >=0:3183 sys.stderr.write("This particular p4 error is misleading.\n")3184 sys.stderr.write("Perhaps the depot path was misspelled.\n");3185 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3186 sys.exit(1)3187if'p4ExitCode'in info:3188 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3189 sys.exit(1)319031913192 change =int(info["change"])3193if change > newestRevision:3194 newestRevision = change31953196if info["action"]in self.delete_actions:3197# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3198#fileCnt = fileCnt + 13199continue32003201for prop in["depotFile","rev","action","type"]:3202 details["%s%s"% (prop, fileCnt)] = info[prop]32033204 fileCnt = fileCnt +132053206 details["change"] = newestRevision32073208# Use time from top-most change so that all git p4 clones of3209# the same p4 repo have the same commit SHA1s.3210 res =p4_describe(newestRevision)3211 details["time"] = res["time"]32123213 self.updateOptionDict(details)3214try:3215 self.commit(details, self.extractFilesFromCommit(details), self.branch)3216exceptIOError:3217print"IO error with git fast-import. Is your git version recent enough?"3218print self.gitError.read()321932203221defrun(self, args):3222 self.depotPaths = []3223 self.changeRange =""3224 self.previousDepotPaths = []3225 self.hasOrigin =False32263227# map from branch depot path to parent branch3228 self.knownBranches = {}3229 self.initialParents = {}32303231if self.importIntoRemotes:3232 self.refPrefix ="refs/remotes/p4/"3233else:3234 self.refPrefix ="refs/heads/p4/"32353236if self.syncWithOrigin:3237 self.hasOrigin =originP4BranchesExist()3238if self.hasOrigin:3239if not self.silent:3240print'Syncing with origin first, using "git fetch origin"'3241system("git fetch origin")32423243 branch_arg_given =bool(self.branch)3244iflen(self.branch) ==0:3245 self.branch = self.refPrefix +"master"3246ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3247system("git update-ref%srefs/heads/p4"% self.branch)3248system("git branch -D p4")32493250# accept either the command-line option, or the configuration variable3251if self.useClientSpec:3252# will use this after clone to set the variable3253 self.useClientSpec_from_options =True3254else:3255ifgitConfigBool("git-p4.useclientspec"):3256 self.useClientSpec =True3257if self.useClientSpec:3258 self.clientSpecDirs =getClientSpec()32593260# TODO: should always look at previous commits,3261# merge with previous imports, if possible.3262if args == []:3263if self.hasOrigin:3264createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)32653266# branches holds mapping from branch name to sha13267 branches =p4BranchesInGit(self.importIntoRemotes)32683269# restrict to just this one, disabling detect-branches3270if branch_arg_given:3271 short = self.branch.split("/")[-1]3272if short in branches:3273 self.p4BranchesInGit = [ short ]3274else:3275 self.p4BranchesInGit = branches.keys()32763277iflen(self.p4BranchesInGit) >1:3278if not self.silent:3279print"Importing from/into multiple branches"3280 self.detectBranches =True3281for branch in branches.keys():3282 self.initialParents[self.refPrefix + branch] = \3283 branches[branch]32843285if self.verbose:3286print"branches:%s"% self.p4BranchesInGit32873288 p4Change =03289for branch in self.p4BranchesInGit:3290 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)32913292 settings =extractSettingsGitLog(logMsg)32933294 self.readOptions(settings)3295if(settings.has_key('depot-paths')3296and settings.has_key('change')):3297 change =int(settings['change']) +13298 p4Change =max(p4Change, change)32993300 depotPaths =sorted(settings['depot-paths'])3301if self.previousDepotPaths == []:3302 self.previousDepotPaths = depotPaths3303else:3304 paths = []3305for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3306 prev_list = prev.split("/")3307 cur_list = cur.split("/")3308for i inrange(0,min(len(cur_list),len(prev_list))):3309if cur_list[i] <> prev_list[i]:3310 i = i -13311break33123313 paths.append("/".join(cur_list[:i +1]))33143315 self.previousDepotPaths = paths33163317if p4Change >0:3318 self.depotPaths =sorted(self.previousDepotPaths)3319 self.changeRange ="@%s,#head"% p4Change3320if not self.silent and not self.detectBranches:3321print"Performing incremental import into%sgit branch"% self.branch33223323# accept multiple ref name abbreviations:3324# refs/foo/bar/branch -> use it exactly3325# p4/branch -> prepend refs/remotes/ or refs/heads/3326# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3327if not self.branch.startswith("refs/"):3328if self.importIntoRemotes:3329 prepend ="refs/remotes/"3330else:3331 prepend ="refs/heads/"3332if not self.branch.startswith("p4/"):3333 prepend +="p4/"3334 self.branch = prepend + self.branch33353336iflen(args) ==0and self.depotPaths:3337if not self.silent:3338print"Depot paths:%s"%' '.join(self.depotPaths)3339else:3340if self.depotPaths and self.depotPaths != args:3341print("previous import used depot path%sand now%swas specified. "3342"This doesn't work!"% (' '.join(self.depotPaths),3343' '.join(args)))3344 sys.exit(1)33453346 self.depotPaths =sorted(args)33473348 revision =""3349 self.users = {}33503351# Make sure no revision specifiers are used when --changesfile3352# is specified.3353 bad_changesfile =False3354iflen(self.changesFile) >0:3355for p in self.depotPaths:3356if p.find("@") >=0or p.find("#") >=0:3357 bad_changesfile =True3358break3359if bad_changesfile:3360die("Option --changesfile is incompatible with revision specifiers")33613362 newPaths = []3363for p in self.depotPaths:3364if p.find("@") != -1:3365 atIdx = p.index("@")3366 self.changeRange = p[atIdx:]3367if self.changeRange =="@all":3368 self.changeRange =""3369elif','not in self.changeRange:3370 revision = self.changeRange3371 self.changeRange =""3372 p = p[:atIdx]3373elif p.find("#") != -1:3374 hashIdx = p.index("#")3375 revision = p[hashIdx:]3376 p = p[:hashIdx]3377elif self.previousDepotPaths == []:3378# pay attention to changesfile, if given, else import3379# the entire p4 tree at the head revision3380iflen(self.changesFile) ==0:3381 revision ="#head"33823383 p = re.sub("\.\.\.$","", p)3384if not p.endswith("/"):3385 p +="/"33863387 newPaths.append(p)33883389 self.depotPaths = newPaths33903391# --detect-branches may change this for each branch3392 self.branchPrefixes = self.depotPaths33933394 self.loadUserMapFromCache()3395 self.labels = {}3396if self.detectLabels:3397 self.getLabels();33983399if self.detectBranches:3400## FIXME - what's a P4 projectName ?3401 self.projectName = self.guessProjectName()34023403if self.hasOrigin:3404 self.getBranchMappingFromGitBranches()3405else:3406 self.getBranchMapping()3407if self.verbose:3408print"p4-git branches:%s"% self.p4BranchesInGit3409print"initial parents:%s"% self.initialParents3410for b in self.p4BranchesInGit:3411if b !="master":34123413## FIXME3414 b = b[len(self.projectName):]3415 self.createdBranches.add(b)34163417 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))34183419 self.importProcess = subprocess.Popen(["git","fast-import"],3420 stdin=subprocess.PIPE,3421 stdout=subprocess.PIPE,3422 stderr=subprocess.PIPE);3423 self.gitOutput = self.importProcess.stdout3424 self.gitStream = self.importProcess.stdin3425 self.gitError = self.importProcess.stderr34263427if revision:3428 self.importHeadRevision(revision)3429else:3430 changes = []34313432iflen(self.changesFile) >0:3433 output =open(self.changesFile).readlines()3434 changeSet =set()3435for line in output:3436 changeSet.add(int(line))34373438for change in changeSet:3439 changes.append(change)34403441 changes.sort()3442else:3443# catch "git p4 sync" with no new branches, in a repo that3444# does not have any existing p4 branches3445iflen(args) ==0:3446if not self.p4BranchesInGit:3447die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")34483449# The default branch is master, unless --branch is used to3450# specify something else. Make sure it exists, or complain3451# nicely about how to use --branch.3452if not self.detectBranches:3453if notbranch_exists(self.branch):3454if branch_arg_given:3455die("Error: branch%sdoes not exist."% self.branch)3456else:3457die("Error: no branch%s; perhaps specify one with --branch."%3458 self.branch)34593460if self.verbose:3461print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3462 self.changeRange)3463 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)34643465iflen(self.maxChanges) >0:3466 changes = changes[:min(int(self.maxChanges),len(changes))]34673468iflen(changes) ==0:3469if not self.silent:3470print"No changes to import!"3471else:3472if not self.silent and not self.detectBranches:3473print"Import destination:%s"% self.branch34743475 self.updatedBranches =set()34763477if not self.detectBranches:3478if args:3479# start a new branch3480 self.initialParent =""3481else:3482# build on a previous revision3483 self.initialParent =parseRevision(self.branch)34843485 self.importChanges(changes)34863487if not self.silent:3488print""3489iflen(self.updatedBranches) >0:3490 sys.stdout.write("Updated branches: ")3491for b in self.updatedBranches:3492 sys.stdout.write("%s"% b)3493 sys.stdout.write("\n")34943495ifgitConfigBool("git-p4.importLabels"):3496 self.importLabels =True34973498if self.importLabels:3499 p4Labels =getP4Labels(self.depotPaths)3500 gitTags =getGitTags()35013502 missingP4Labels = p4Labels - gitTags3503 self.importP4Labels(self.gitStream, missingP4Labels)35043505 self.gitStream.close()3506if self.importProcess.wait() !=0:3507die("fast-import failed:%s"% self.gitError.read())3508 self.gitOutput.close()3509 self.gitError.close()35103511# Cleanup temporary branches created during import3512if self.tempBranches != []:3513for branch in self.tempBranches:3514read_pipe("git update-ref -d%s"% branch)3515 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))35163517# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3518# a convenient shortcut refname "p4".3519if self.importIntoRemotes:3520 head_ref = self.refPrefix +"HEAD"3521if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3522system(["git","symbolic-ref", head_ref, self.branch])35233524return True35253526classP4Rebase(Command):3527def__init__(self):3528 Command.__init__(self)3529 self.options = [3530 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3531]3532 self.importLabels =False3533 self.description = ("Fetches the latest revision from perforce and "3534+"rebases the current work (branch) against it")35353536defrun(self, args):3537 sync =P4Sync()3538 sync.importLabels = self.importLabels3539 sync.run([])35403541return self.rebase()35423543defrebase(self):3544if os.system("git update-index --refresh") !=0:3545die("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.");3546iflen(read_pipe("git diff-index HEAD --")) >0:3547die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");35483549[upstream, settings] =findUpstreamBranchPoint()3550iflen(upstream) ==0:3551die("Cannot find upstream branchpoint for rebase")35523553# the branchpoint may be p4/foo~3, so strip off the parent3554 upstream = re.sub("~[0-9]+$","", upstream)35553556print"Rebasing the current branch onto%s"% upstream3557 oldHead =read_pipe("git rev-parse HEAD").strip()3558system("git rebase%s"% upstream)3559system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3560return True35613562classP4Clone(P4Sync):3563def__init__(self):3564 P4Sync.__init__(self)3565 self.description ="Creates a new git repository and imports from Perforce into it"3566 self.usage ="usage: %prog [options] //depot/path[@revRange]"3567 self.options += [3568 optparse.make_option("--destination", dest="cloneDestination",3569 action='store', default=None,3570help="where to leave result of the clone"),3571 optparse.make_option("--bare", dest="cloneBare",3572 action="store_true", default=False),3573]3574 self.cloneDestination =None3575 self.needsGit =False3576 self.cloneBare =False35773578defdefaultDestination(self, args):3579## TODO: use common prefix of args?3580 depotPath = args[0]3581 depotDir = re.sub("(@[^@]*)$","", depotPath)3582 depotDir = re.sub("(#[^#]*)$","", depotDir)3583 depotDir = re.sub(r"\.\.\.$","", depotDir)3584 depotDir = re.sub(r"/$","", depotDir)3585return os.path.split(depotDir)[1]35863587defrun(self, args):3588iflen(args) <1:3589return False35903591if self.keepRepoPath and not self.cloneDestination:3592 sys.stderr.write("Must specify destination for --keep-path\n")3593 sys.exit(1)35943595 depotPaths = args35963597if not self.cloneDestination andlen(depotPaths) >1:3598 self.cloneDestination = depotPaths[-1]3599 depotPaths = depotPaths[:-1]36003601 self.cloneExclude = ["/"+p for p in self.cloneExclude]3602for p in depotPaths:3603if not p.startswith("//"):3604 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3605return False36063607if not self.cloneDestination:3608 self.cloneDestination = self.defaultDestination(args)36093610print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)36113612if not os.path.exists(self.cloneDestination):3613 os.makedirs(self.cloneDestination)3614chdir(self.cloneDestination)36153616 init_cmd = ["git","init"]3617if self.cloneBare:3618 init_cmd.append("--bare")3619 retcode = subprocess.call(init_cmd)3620if retcode:3621raiseCalledProcessError(retcode, init_cmd)36223623if not P4Sync.run(self, depotPaths):3624return False36253626# create a master branch and check out a work tree3627ifgitBranchExists(self.branch):3628system(["git","branch","master", self.branch ])3629if not self.cloneBare:3630system(["git","checkout","-f"])3631else:3632print'Not checking out any branch, use ' \3633'"git checkout -q -b master <branch>"'36343635# auto-set this variable if invoked with --use-client-spec3636if self.useClientSpec_from_options:3637system("git config --bool git-p4.useclientspec true")36383639return True36403641classP4Branches(Command):3642def__init__(self):3643 Command.__init__(self)3644 self.options = [ ]3645 self.description = ("Shows the git branches that hold imports and their "3646+"corresponding perforce depot paths")3647 self.verbose =False36483649defrun(self, args):3650iforiginP4BranchesExist():3651createOrUpdateBranchesFromOrigin()36523653 cmdline ="git rev-parse --symbolic "3654 cmdline +=" --remotes"36553656for line inread_pipe_lines(cmdline):3657 line = line.strip()36583659if not line.startswith('p4/')or line =="p4/HEAD":3660continue3661 branch = line36623663 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3664 settings =extractSettingsGitLog(log)36653666print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3667return True36683669classHelpFormatter(optparse.IndentedHelpFormatter):3670def__init__(self):3671 optparse.IndentedHelpFormatter.__init__(self)36723673defformat_description(self, description):3674if description:3675return description +"\n"3676else:3677return""36783679defprintUsage(commands):3680print"usage:%s<command> [options]"% sys.argv[0]3681print""3682print"valid commands:%s"%", ".join(commands)3683print""3684print"Try%s<command> --help for command specific help."% sys.argv[0]3685print""36863687commands = {3688"debug": P4Debug,3689"submit": P4Submit,3690"commit": P4Submit,3691"sync": P4Sync,3692"rebase": P4Rebase,3693"clone": P4Clone,3694"rollback": P4RollBack,3695"branches": P4Branches3696}369736983699defmain():3700iflen(sys.argv[1:]) ==0:3701printUsage(commands.keys())3702 sys.exit(2)37033704 cmdName = sys.argv[1]3705try:3706 klass = commands[cmdName]3707 cmd =klass()3708exceptKeyError:3709print"unknown command%s"% cmdName3710print""3711printUsage(commands.keys())3712 sys.exit(2)37133714 options = cmd.options3715 cmd.gitdir = os.environ.get("GIT_DIR",None)37163717 args = sys.argv[2:]37183719 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3720if cmd.needsGit:3721 options.append(optparse.make_option("--git-dir", dest="gitdir"))37223723 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3724 options,3725 description = cmd.description,3726 formatter =HelpFormatter())37273728(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3729global verbose3730 verbose = cmd.verbose3731if cmd.needsGit:3732if cmd.gitdir ==None:3733 cmd.gitdir = os.path.abspath(".git")3734if notisValidGitDir(cmd.gitdir):3735# "rev-parse --git-dir" without arguments will try $PWD/.git3736 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3737if os.path.exists(cmd.gitdir):3738 cdup =read_pipe("git rev-parse --show-cdup").strip()3739iflen(cdup) >0:3740chdir(cdup);37413742if notisValidGitDir(cmd.gitdir):3743ifisValidGitDir(cmd.gitdir +"/.git"):3744 cmd.gitdir +="/.git"3745else:3746die("fatal: cannot locate git repository at%s"% cmd.gitdir)37473748# so git commands invoked from the P4 workspace will succeed3749 os.environ["GIT_DIR"] = cmd.gitdir37503751if not cmd.run(args):3752 parser.print_help()3753 sys.exit(2)375437553756if __name__ =='__main__':3757main()