1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25import zipfile 26import zlib 27import ctypes 28import errno 29 30# support basestring in python3 31try: 32unicode=unicode 33exceptNameError: 34# 'unicode' is undefined, must be Python 3 35str=str 36unicode=str 37bytes=bytes 38 basestring = (str,bytes) 39else: 40# 'unicode' exists, must be Python 2 41str=str 42unicode=unicode 43bytes=str 44 basestring = basestring 45 46try: 47from subprocess import CalledProcessError 48exceptImportError: 49# from python2.7:subprocess.py 50# Exception classes used by this module. 51classCalledProcessError(Exception): 52"""This exception is raised when a process run by check_call() returns 53 a non-zero exit status. The exit status will be stored in the 54 returncode attribute.""" 55def__init__(self, returncode, cmd): 56 self.returncode = returncode 57 self.cmd = cmd 58def__str__(self): 59return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 60 61verbose =False 62 63# Only labels/tags matching this will be imported/exported 64defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 65 66# The block size is reduced automatically if required 67defaultBlockSize =1<<20 68 69p4_access_checked =False 70 71defp4_build_cmd(cmd): 72"""Build a suitable p4 command line. 73 74 This consolidates building and returning a p4 command line into one 75 location. It means that hooking into the environment, or other configuration 76 can be done more easily. 77 """ 78 real_cmd = ["p4"] 79 80 user =gitConfig("git-p4.user") 81iflen(user) >0: 82 real_cmd += ["-u",user] 83 84 password =gitConfig("git-p4.password") 85iflen(password) >0: 86 real_cmd += ["-P", password] 87 88 port =gitConfig("git-p4.port") 89iflen(port) >0: 90 real_cmd += ["-p", port] 91 92 host =gitConfig("git-p4.host") 93iflen(host) >0: 94 real_cmd += ["-H", host] 95 96 client =gitConfig("git-p4.client") 97iflen(client) >0: 98 real_cmd += ["-c", client] 99 100 retries =gitConfigInt("git-p4.retries") 101if retries is None: 102# Perform 3 retries by default 103 retries =3 104if retries >0: 105# Provide a way to not pass this option by setting git-p4.retries to 0 106 real_cmd += ["-r",str(retries)] 107 108ifisinstance(cmd,basestring): 109 real_cmd =' '.join(real_cmd) +' '+ cmd 110else: 111 real_cmd += cmd 112 113# now check that we can actually talk to the server 114global p4_access_checked 115if not p4_access_checked: 116 p4_access_checked =True# suppress access checks in p4_check_access itself 117p4_check_access() 118 119return real_cmd 120 121defgit_dir(path): 122""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 123 This won't automatically add ".git" to a directory. 124 """ 125 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 126if not d orlen(d) ==0: 127return None 128else: 129return d 130 131defchdir(path, is_client_path=False): 132"""Do chdir to the given path, and set the PWD environment 133 variable for use by P4. It does not look at getcwd() output. 134 Since we're not using the shell, it is necessary to set the 135 PWD environment variable explicitly. 136 137 Normally, expand the path to force it to be absolute. This 138 addresses the use of relative path names inside P4 settings, 139 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 140 as given; it looks for .p4config using PWD. 141 142 If is_client_path, the path was handed to us directly by p4, 143 and may be a symbolic link. Do not call os.getcwd() in this 144 case, because it will cause p4 to think that PWD is not inside 145 the client path. 146 """ 147 148 os.chdir(path) 149if not is_client_path: 150 path = os.getcwd() 151 os.environ['PWD'] = path 152 153defcalcDiskFree(): 154"""Return free space in bytes on the disk of the given dirname.""" 155if platform.system() =='Windows': 156 free_bytes = ctypes.c_ulonglong(0) 157 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 158return free_bytes.value 159else: 160 st = os.statvfs(os.getcwd()) 161return st.f_bavail * st.f_frsize 162 163defdie(msg): 164if verbose: 165raiseException(msg) 166else: 167 sys.stderr.write(msg +"\n") 168 sys.exit(1) 169 170defwrite_pipe(c, stdin): 171if verbose: 172 sys.stderr.write('Writing pipe:%s\n'%str(c)) 173 174 expand =isinstance(c,basestring) 175 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 176 pipe = p.stdin 177 val = pipe.write(stdin) 178 pipe.close() 179if p.wait(): 180die('Command failed:%s'%str(c)) 181 182return val 183 184defp4_write_pipe(c, stdin): 185 real_cmd =p4_build_cmd(c) 186returnwrite_pipe(real_cmd, stdin) 187 188defread_pipe_full(c): 189""" Read output from command. Returns a tuple 190 of the return status, stdout text and stderr 191 text. 192 """ 193if verbose: 194 sys.stderr.write('Reading pipe:%s\n'%str(c)) 195 196 expand =isinstance(c,basestring) 197 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 198(out, err) = p.communicate() 199return(p.returncode, out, err) 200 201defread_pipe(c, ignore_error=False): 202""" Read output from command. Returns the output text on 203 success. On failure, terminates execution, unless 204 ignore_error is True, when it returns an empty string. 205 """ 206(retcode, out, err) =read_pipe_full(c) 207if retcode !=0: 208if ignore_error: 209 out ="" 210else: 211die('Command failed:%s\nError:%s'% (str(c), err)) 212return out 213 214defread_pipe_text(c): 215""" Read output from a command with trailing whitespace stripped. 216 On error, returns None. 217 """ 218(retcode, out, err) =read_pipe_full(c) 219if retcode !=0: 220return None 221else: 222return out.rstrip() 223 224defp4_read_pipe(c, ignore_error=False): 225 real_cmd =p4_build_cmd(c) 226returnread_pipe(real_cmd, ignore_error) 227 228defread_pipe_lines(c): 229if verbose: 230 sys.stderr.write('Reading pipe:%s\n'%str(c)) 231 232 expand =isinstance(c, basestring) 233 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 234 pipe = p.stdout 235 val = pipe.readlines() 236if pipe.close()or p.wait(): 237die('Command failed:%s'%str(c)) 238 239return val 240 241defp4_read_pipe_lines(c): 242"""Specifically invoke p4 on the command supplied. """ 243 real_cmd =p4_build_cmd(c) 244returnread_pipe_lines(real_cmd) 245 246defp4_has_command(cmd): 247"""Ask p4 for help on this command. If it returns an error, the 248 command does not exist in this version of p4.""" 249 real_cmd =p4_build_cmd(["help", cmd]) 250 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 251 stderr=subprocess.PIPE) 252 p.communicate() 253return p.returncode ==0 254 255defp4_has_move_command(): 256"""See if the move command exists, that it supports -k, and that 257 it has not been administratively disabled. The arguments 258 must be correct, but the filenames do not have to exist. Use 259 ones with wildcards so even if they exist, it will fail.""" 260 261if notp4_has_command("move"): 262return False 263 cmd =p4_build_cmd(["move","-k","@from","@to"]) 264 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 265(out, err) = p.communicate() 266# return code will be 1 in either case 267if err.find("Invalid option") >=0: 268return False 269if err.find("disabled") >=0: 270return False 271# assume it failed because @... was invalid changelist 272return True 273 274defsystem(cmd, ignore_error=False): 275 expand =isinstance(cmd,basestring) 276if verbose: 277 sys.stderr.write("executing%s\n"%str(cmd)) 278 retcode = subprocess.call(cmd, shell=expand) 279if retcode and not ignore_error: 280raiseCalledProcessError(retcode, cmd) 281 282return retcode 283 284defp4_system(cmd): 285"""Specifically invoke p4 as the system command. """ 286 real_cmd =p4_build_cmd(cmd) 287 expand =isinstance(real_cmd, basestring) 288 retcode = subprocess.call(real_cmd, shell=expand) 289if retcode: 290raiseCalledProcessError(retcode, real_cmd) 291 292defdie_bad_access(s): 293die("failure accessing depot:{0}".format(s.rstrip())) 294 295defp4_check_access(min_expiration=1): 296""" Check if we can access Perforce - account still logged in 297 """ 298 results =p4CmdList(["login","-s"]) 299 300iflen(results) ==0: 301# should never get here: always get either some results, or a p4ExitCode 302assert("could not parse response from perforce") 303 304 result = results[0] 305 306if'p4ExitCode'in result: 307# p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path 308die_bad_access("could not run p4") 309 310 code = result.get("code") 311if not code: 312# we get here if we couldn't connect and there was nothing to unmarshal 313die_bad_access("could not connect") 314 315elif code =="stat": 316 expiry = result.get("TicketExpiration") 317if expiry: 318 expiry =int(expiry) 319if expiry > min_expiration: 320# ok to carry on 321return 322else: 323die_bad_access("perforce ticket expires in{0}seconds".format(expiry)) 324 325else: 326# account without a timeout - all ok 327return 328 329elif code =="error": 330 data = result.get("data") 331if data: 332die_bad_access("p4 error:{0}".format(data)) 333else: 334die_bad_access("unknown error") 335elif code =="info": 336return 337else: 338die_bad_access("unknown error code{0}".format(code)) 339 340_p4_version_string =None 341defp4_version_string(): 342"""Read the version string, showing just the last line, which 343 hopefully is the interesting version bit. 344 345 $ p4 -V 346 Perforce - The Fast Software Configuration Management System. 347 Copyright 1995-2011 Perforce Software. All rights reserved. 348 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 349 """ 350global _p4_version_string 351if not _p4_version_string: 352 a =p4_read_pipe_lines(["-V"]) 353 _p4_version_string = a[-1].rstrip() 354return _p4_version_string 355 356defp4_integrate(src, dest): 357p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 358 359defp4_sync(f, *options): 360p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 361 362defp4_add(f): 363# forcibly add file names with wildcards 364ifwildcard_present(f): 365p4_system(["add","-f", f]) 366else: 367p4_system(["add", f]) 368 369defp4_delete(f): 370p4_system(["delete",wildcard_encode(f)]) 371 372defp4_edit(f, *options): 373p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 374 375defp4_revert(f): 376p4_system(["revert",wildcard_encode(f)]) 377 378defp4_reopen(type, f): 379p4_system(["reopen","-t",type,wildcard_encode(f)]) 380 381defp4_reopen_in_change(changelist, files): 382 cmd = ["reopen","-c",str(changelist)] + files 383p4_system(cmd) 384 385defp4_move(src, dest): 386p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 387 388defp4_last_change(): 389 results =p4CmdList(["changes","-m","1"], skip_info=True) 390returnint(results[0]['change']) 391 392defp4_describe(change, shelved=False): 393"""Make sure it returns a valid result by checking for 394 the presence of field "time". Return a dict of the 395 results.""" 396 397 cmd = ["describe","-s"] 398if shelved: 399 cmd += ["-S"] 400 cmd += [str(change)] 401 402 ds =p4CmdList(cmd, skip_info=True) 403iflen(ds) !=1: 404die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 405 406 d = ds[0] 407 408if"p4ExitCode"in d: 409die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 410str(d))) 411if"code"in d: 412if d["code"] =="error": 413die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 414 415if"time"not in d: 416die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 417 418return d 419 420# 421# Canonicalize the p4 type and return a tuple of the 422# base type, plus any modifiers. See "p4 help filetypes" 423# for a list and explanation. 424# 425defsplit_p4_type(p4type): 426 427 p4_filetypes_historical = { 428"ctempobj":"binary+Sw", 429"ctext":"text+C", 430"cxtext":"text+Cx", 431"ktext":"text+k", 432"kxtext":"text+kx", 433"ltext":"text+F", 434"tempobj":"binary+FSw", 435"ubinary":"binary+F", 436"uresource":"resource+F", 437"uxbinary":"binary+Fx", 438"xbinary":"binary+x", 439"xltext":"text+Fx", 440"xtempobj":"binary+Swx", 441"xtext":"text+x", 442"xunicode":"unicode+x", 443"xutf16":"utf16+x", 444} 445if p4type in p4_filetypes_historical: 446 p4type = p4_filetypes_historical[p4type] 447 mods ="" 448 s = p4type.split("+") 449 base = s[0] 450 mods ="" 451iflen(s) >1: 452 mods = s[1] 453return(base, mods) 454 455# 456# return the raw p4 type of a file (text, text+ko, etc) 457# 458defp4_type(f): 459 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 460return results[0]['headType'] 461 462# 463# Given a type base and modifier, return a regexp matching 464# the keywords that can be expanded in the file 465# 466defp4_keywords_regexp_for_type(base, type_mods): 467if base in("text","unicode","binary"): 468 kwords =None 469if"ko"in type_mods: 470 kwords ='Id|Header' 471elif"k"in type_mods: 472 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 473else: 474return None 475 pattern = r""" 476 \$ # Starts with a dollar, followed by... 477 (%s) # one of the keywords, followed by... 478 (:[^$\n]+)? # possibly an old expansion, followed by... 479 \$ # another dollar 480 """% kwords 481return pattern 482else: 483return None 484 485# 486# Given a file, return a regexp matching the possible 487# RCS keywords that will be expanded, or None for files 488# with kw expansion turned off. 489# 490defp4_keywords_regexp_for_file(file): 491if not os.path.exists(file): 492return None 493else: 494(type_base, type_mods) =split_p4_type(p4_type(file)) 495returnp4_keywords_regexp_for_type(type_base, type_mods) 496 497defsetP4ExecBit(file, mode): 498# Reopens an already open file and changes the execute bit to match 499# the execute bit setting in the passed in mode. 500 501 p4Type ="+x" 502 503if notisModeExec(mode): 504 p4Type =getP4OpenedType(file) 505 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 506 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 507if p4Type[-1] =="+": 508 p4Type = p4Type[0:-1] 509 510p4_reopen(p4Type,file) 511 512defgetP4OpenedType(file): 513# Returns the perforce file type for the given file. 514 515 result =p4_read_pipe(["opened",wildcard_encode(file)]) 516 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 517if match: 518return match.group(1) 519else: 520die("Could not determine file type for%s(result: '%s')"% (file, result)) 521 522# Return the set of all p4 labels 523defgetP4Labels(depotPaths): 524 labels =set() 525ifisinstance(depotPaths,basestring): 526 depotPaths = [depotPaths] 527 528for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 529 label = l['label'] 530 labels.add(label) 531 532return labels 533 534# Return the set of all git tags 535defgetGitTags(): 536 gitTags =set() 537for line inread_pipe_lines(["git","tag"]): 538 tag = line.strip() 539 gitTags.add(tag) 540return gitTags 541 542defdiffTreePattern(): 543# This is a simple generator for the diff tree regex pattern. This could be 544# a class variable if this and parseDiffTreeEntry were a part of a class. 545 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 546while True: 547yield pattern 548 549defparseDiffTreeEntry(entry): 550"""Parses a single diff tree entry into its component elements. 551 552 See git-diff-tree(1) manpage for details about the format of the diff 553 output. This method returns a dictionary with the following elements: 554 555 src_mode - The mode of the source file 556 dst_mode - The mode of the destination file 557 src_sha1 - The sha1 for the source file 558 dst_sha1 - The sha1 fr the destination file 559 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 560 status_score - The score for the status (applicable for 'C' and 'R' 561 statuses). This is None if there is no score. 562 src - The path for the source file. 563 dst - The path for the destination file. This is only present for 564 copy or renames. If it is not present, this is None. 565 566 If the pattern is not matched, None is returned.""" 567 568 match =diffTreePattern().next().match(entry) 569if match: 570return{ 571'src_mode': match.group(1), 572'dst_mode': match.group(2), 573'src_sha1': match.group(3), 574'dst_sha1': match.group(4), 575'status': match.group(5), 576'status_score': match.group(6), 577'src': match.group(7), 578'dst': match.group(10) 579} 580return None 581 582defisModeExec(mode): 583# Returns True if the given git mode represents an executable file, 584# otherwise False. 585return mode[-3:] =="755" 586 587classP4Exception(Exception): 588""" Base class for exceptions from the p4 client """ 589def__init__(self, exit_code): 590 self.p4ExitCode = exit_code 591 592classP4ServerException(P4Exception): 593""" Base class for exceptions where we get some kind of marshalled up result from the server """ 594def__init__(self, exit_code, p4_result): 595super(P4ServerException, self).__init__(exit_code) 596 self.p4_result = p4_result 597 self.code = p4_result[0]['code'] 598 self.data = p4_result[0]['data'] 599 600classP4RequestSizeException(P4ServerException): 601""" One of the maxresults or maxscanrows errors """ 602def__init__(self, exit_code, p4_result, limit): 603super(P4RequestSizeException, self).__init__(exit_code, p4_result) 604 self.limit = limit 605 606defisModeExecChanged(src_mode, dst_mode): 607returnisModeExec(src_mode) !=isModeExec(dst_mode) 608 609defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False, 610 errors_as_exceptions=False): 611 612ifisinstance(cmd,basestring): 613 cmd ="-G "+ cmd 614 expand =True 615else: 616 cmd = ["-G"] + cmd 617 expand =False 618 619 cmd =p4_build_cmd(cmd) 620if verbose: 621 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 622 623# Use a temporary file to avoid deadlocks without 624# subprocess.communicate(), which would put another copy 625# of stdout into memory. 626 stdin_file =None 627if stdin is not None: 628 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 629ifisinstance(stdin,basestring): 630 stdin_file.write(stdin) 631else: 632for i in stdin: 633 stdin_file.write(i +'\n') 634 stdin_file.flush() 635 stdin_file.seek(0) 636 637 p4 = subprocess.Popen(cmd, 638 shell=expand, 639 stdin=stdin_file, 640 stdout=subprocess.PIPE) 641 642 result = [] 643try: 644while True: 645 entry = marshal.load(p4.stdout) 646if skip_info: 647if'code'in entry and entry['code'] =='info': 648continue 649if cb is not None: 650cb(entry) 651else: 652 result.append(entry) 653exceptEOFError: 654pass 655 exitCode = p4.wait() 656if exitCode !=0: 657if errors_as_exceptions: 658iflen(result) >0: 659 data = result[0].get('data') 660if data: 661 m = re.search('Too many rows scanned \(over (\d+)\)', data) 662if not m: 663 m = re.search('Request too large \(over (\d+)\)', data) 664 665if m: 666 limit =int(m.group(1)) 667raiseP4RequestSizeException(exitCode, result, limit) 668 669raiseP4ServerException(exitCode, result) 670else: 671raiseP4Exception(exitCode) 672else: 673 entry = {} 674 entry["p4ExitCode"] = exitCode 675 result.append(entry) 676 677return result 678 679defp4Cmd(cmd): 680list=p4CmdList(cmd) 681 result = {} 682for entry inlist: 683 result.update(entry) 684return result; 685 686defp4Where(depotPath): 687if not depotPath.endswith("/"): 688 depotPath +="/" 689 depotPathLong = depotPath +"..." 690 outputList =p4CmdList(["where", depotPathLong]) 691 output =None 692for entry in outputList: 693if"depotFile"in entry: 694# Search for the base client side depot path, as long as it starts with the branch's P4 path. 695# The base path always ends with "/...". 696if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 697 output = entry 698break 699elif"data"in entry: 700 data = entry.get("data") 701 space = data.find(" ") 702if data[:space] == depotPath: 703 output = entry 704break 705if output ==None: 706return"" 707if output["code"] =="error": 708return"" 709 clientPath ="" 710if"path"in output: 711 clientPath = output.get("path") 712elif"data"in output: 713 data = output.get("data") 714 lastSpace = data.rfind(" ") 715 clientPath = data[lastSpace +1:] 716 717if clientPath.endswith("..."): 718 clientPath = clientPath[:-3] 719return clientPath 720 721defcurrentGitBranch(): 722returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 723 724defisValidGitDir(path): 725returngit_dir(path) !=None 726 727defparseRevision(ref): 728returnread_pipe("git rev-parse%s"% ref).strip() 729 730defbranchExists(ref): 731 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 732 ignore_error=True) 733returnlen(rev) >0 734 735defextractLogMessageFromGitCommit(commit): 736 logMessage ="" 737 738## fixme: title is first line of commit, not 1st paragraph. 739 foundTitle =False 740for log inread_pipe_lines("git cat-file commit%s"% commit): 741if not foundTitle: 742iflen(log) ==1: 743 foundTitle =True 744continue 745 746 logMessage += log 747return logMessage 748 749defextractSettingsGitLog(log): 750 values = {} 751for line in log.split("\n"): 752 line = line.strip() 753 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 754if not m: 755continue 756 757 assignments = m.group(1).split(':') 758for a in assignments: 759 vals = a.split('=') 760 key = vals[0].strip() 761 val = ('='.join(vals[1:])).strip() 762if val.endswith('\"')and val.startswith('"'): 763 val = val[1:-1] 764 765 values[key] = val 766 767 paths = values.get("depot-paths") 768if not paths: 769 paths = values.get("depot-path") 770if paths: 771 values['depot-paths'] = paths.split(',') 772return values 773 774defgitBranchExists(branch): 775 proc = subprocess.Popen(["git","rev-parse", branch], 776 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 777return proc.wait() ==0; 778 779defgitUpdateRef(ref, newvalue): 780 subprocess.check_call(["git","update-ref", ref, newvalue]) 781 782defgitDeleteRef(ref): 783 subprocess.check_call(["git","update-ref","-d", ref]) 784 785_gitConfig = {} 786 787defgitConfig(key, typeSpecifier=None): 788if key not in _gitConfig: 789 cmd = ["git","config"] 790if typeSpecifier: 791 cmd += [ typeSpecifier ] 792 cmd += [ key ] 793 s =read_pipe(cmd, ignore_error=True) 794 _gitConfig[key] = s.strip() 795return _gitConfig[key] 796 797defgitConfigBool(key): 798"""Return a bool, using git config --bool. It is True only if the 799 variable is set to true, and False if set to false or not present 800 in the config.""" 801 802if key not in _gitConfig: 803 _gitConfig[key] =gitConfig(key,'--bool') =="true" 804return _gitConfig[key] 805 806defgitConfigInt(key): 807if key not in _gitConfig: 808 cmd = ["git","config","--int", key ] 809 s =read_pipe(cmd, ignore_error=True) 810 v = s.strip() 811try: 812 _gitConfig[key] =int(gitConfig(key,'--int')) 813exceptValueError: 814 _gitConfig[key] =None 815return _gitConfig[key] 816 817defgitConfigList(key): 818if key not in _gitConfig: 819 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 820 _gitConfig[key] = s.strip().splitlines() 821if _gitConfig[key] == ['']: 822 _gitConfig[key] = [] 823return _gitConfig[key] 824 825defp4BranchesInGit(branchesAreInRemotes=True): 826"""Find all the branches whose names start with "p4/", looking 827 in remotes or heads as specified by the argument. Return 828 a dictionary of{ branch: revision }for each one found. 829 The branch names are the short names, without any 830 "p4/" prefix.""" 831 832 branches = {} 833 834 cmdline ="git rev-parse --symbolic " 835if branchesAreInRemotes: 836 cmdline +="--remotes" 837else: 838 cmdline +="--branches" 839 840for line inread_pipe_lines(cmdline): 841 line = line.strip() 842 843# only import to p4/ 844if not line.startswith('p4/'): 845continue 846# special symbolic ref to p4/master 847if line =="p4/HEAD": 848continue 849 850# strip off p4/ prefix 851 branch = line[len("p4/"):] 852 853 branches[branch] =parseRevision(line) 854 855return branches 856 857defbranch_exists(branch): 858"""Make sure that the given ref name really exists.""" 859 860 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 861 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 862 out, _ = p.communicate() 863if p.returncode: 864return False 865# expect exactly one line of output: the branch name 866return out.rstrip() == branch 867 868deffindUpstreamBranchPoint(head ="HEAD"): 869 branches =p4BranchesInGit() 870# map from depot-path to branch name 871 branchByDepotPath = {} 872for branch in branches.keys(): 873 tip = branches[branch] 874 log =extractLogMessageFromGitCommit(tip) 875 settings =extractSettingsGitLog(log) 876if"depot-paths"in settings: 877 paths =",".join(settings["depot-paths"]) 878 branchByDepotPath[paths] ="remotes/p4/"+ branch 879 880 settings =None 881 parent =0 882while parent <65535: 883 commit = head +"~%s"% parent 884 log =extractLogMessageFromGitCommit(commit) 885 settings =extractSettingsGitLog(log) 886if"depot-paths"in settings: 887 paths =",".join(settings["depot-paths"]) 888if paths in branchByDepotPath: 889return[branchByDepotPath[paths], settings] 890 891 parent = parent +1 892 893return["", settings] 894 895defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 896if not silent: 897print("Creating/updating branch(es) in%sbased on origin branch(es)" 898% localRefPrefix) 899 900 originPrefix ="origin/p4/" 901 902for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 903 line = line.strip() 904if(not line.startswith(originPrefix))or line.endswith("HEAD"): 905continue 906 907 headName = line[len(originPrefix):] 908 remoteHead = localRefPrefix + headName 909 originHead = line 910 911 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 912if('depot-paths'not in original 913or'change'not in original): 914continue 915 916 update =False 917if notgitBranchExists(remoteHead): 918if verbose: 919print("creating%s"% remoteHead) 920 update =True 921else: 922 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 923if'change'in settings: 924if settings['depot-paths'] == original['depot-paths']: 925 originP4Change =int(original['change']) 926 p4Change =int(settings['change']) 927if originP4Change > p4Change: 928print("%s(%s) is newer than%s(%s). " 929"Updating p4 branch from origin." 930% (originHead, originP4Change, 931 remoteHead, p4Change)) 932 update =True 933else: 934print("Ignoring:%swas imported from%swhile " 935"%swas imported from%s" 936% (originHead,','.join(original['depot-paths']), 937 remoteHead,','.join(settings['depot-paths']))) 938 939if update: 940system("git update-ref%s %s"% (remoteHead, originHead)) 941 942deforiginP4BranchesExist(): 943returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 944 945 946defp4ParseNumericChangeRange(parts): 947 changeStart =int(parts[0][1:]) 948if parts[1] =='#head': 949 changeEnd =p4_last_change() 950else: 951 changeEnd =int(parts[1]) 952 953return(changeStart, changeEnd) 954 955defchooseBlockSize(blockSize): 956if blockSize: 957return blockSize 958else: 959return defaultBlockSize 960 961defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 962assert depotPaths 963 964# Parse the change range into start and end. Try to find integer 965# revision ranges as these can be broken up into blocks to avoid 966# hitting server-side limits (maxrows, maxscanresults). But if 967# that doesn't work, fall back to using the raw revision specifier 968# strings, without using block mode. 969 970if changeRange is None or changeRange =='': 971 changeStart =1 972 changeEnd =p4_last_change() 973 block_size =chooseBlockSize(requestedBlockSize) 974else: 975 parts = changeRange.split(',') 976assertlen(parts) ==2 977try: 978(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 979 block_size =chooseBlockSize(requestedBlockSize) 980exceptValueError: 981 changeStart = parts[0][1:] 982 changeEnd = parts[1] 983if requestedBlockSize: 984die("cannot use --changes-block-size with non-numeric revisions") 985 block_size =None 986 987 changes =set() 988 989# Retrieve changes a block at a time, to prevent running 990# into a MaxResults/MaxScanRows error from the server. If 991# we _do_ hit one of those errors, turn down the block size 992 993while True: 994 cmd = ['changes'] 995 996if block_size: 997 end =min(changeEnd, changeStart + block_size) 998 revisionRange ="%d,%d"% (changeStart, end) 999else:1000 revisionRange ="%s,%s"% (changeStart, changeEnd)10011002for p in depotPaths:1003 cmd += ["%s...@%s"% (p, revisionRange)]10041005# fetch the changes1006try:1007 result =p4CmdList(cmd, errors_as_exceptions=True)1008except P4RequestSizeException as e:1009if not block_size:1010 block_size = e.limit1011elif block_size > e.limit:1012 block_size = e.limit1013else:1014 block_size =max(2, block_size //2)10151016if verbose:print("block size error, retrying with block size{0}".format(block_size))1017continue1018except P4Exception as e:1019die('Error retrieving changes description ({0})'.format(e.p4ExitCode))10201021# Insert changes in chronological order1022for entry inreversed(result):1023if'change'not in entry:1024continue1025 changes.add(int(entry['change']))10261027if not block_size:1028break10291030if end >= changeEnd:1031break10321033 changeStart = end +110341035 changes =sorted(changes)1036return changes10371038defp4PathStartsWith(path, prefix):1039# This method tries to remedy a potential mixed-case issue:1040#1041# If UserA adds //depot/DirA/file11042# and UserB adds //depot/dira/file21043#1044# we may or may not have a problem. If you have core.ignorecase=true,1045# we treat DirA and dira as the same directory1046ifgitConfigBool("core.ignorecase"):1047return path.lower().startswith(prefix.lower())1048return path.startswith(prefix)10491050defgetClientSpec():1051"""Look at the p4 client spec, create a View() object that contains1052 all the mappings, and return it."""10531054 specList =p4CmdList("client -o")1055iflen(specList) !=1:1056die('Output from "client -o" is%dlines, expecting 1'%1057len(specList))10581059# dictionary of all client parameters1060 entry = specList[0]10611062# the //client/ name1063 client_name = entry["Client"]10641065# just the keys that start with "View"1066 view_keys = [ k for k in entry.keys()if k.startswith("View") ]10671068# hold this new View1069 view =View(client_name)10701071# append the lines, in order, to the view1072for view_num inrange(len(view_keys)):1073 k ="View%d"% view_num1074if k not in view_keys:1075die("Expected view key%smissing"% k)1076 view.append(entry[k])10771078return view10791080defgetClientRoot():1081"""Grab the client directory."""10821083 output =p4CmdList("client -o")1084iflen(output) !=1:1085die('Output from "client -o" is%dlines, expecting 1'%len(output))10861087 entry = output[0]1088if"Root"not in entry:1089die('Client has no "Root"')10901091return entry["Root"]10921093#1094# P4 wildcards are not allowed in filenames. P4 complains1095# if you simply add them, but you can force it with "-f", in1096# which case it translates them into %xx encoding internally.1097#1098defwildcard_decode(path):1099# Search for and fix just these four characters. Do % last so1100# that fixing it does not inadvertently create new %-escapes.1101# Cannot have * in a filename in windows; untested as to1102# what p4 would do in such a case.1103if not platform.system() =="Windows":1104 path = path.replace("%2A","*")1105 path = path.replace("%23","#") \1106.replace("%40","@") \1107.replace("%25","%")1108return path11091110defwildcard_encode(path):1111# do % first to avoid double-encoding the %s introduced here1112 path = path.replace("%","%25") \1113.replace("*","%2A") \1114.replace("#","%23") \1115.replace("@","%40")1116return path11171118defwildcard_present(path):1119 m = re.search("[*#@%]", path)1120return m is not None11211122classLargeFileSystem(object):1123"""Base class for large file system support."""11241125def__init__(self, writeToGitStream):1126 self.largeFiles =set()1127 self.writeToGitStream = writeToGitStream11281129defgeneratePointer(self, cloneDestination, contentFile):1130"""Return the content of a pointer file that is stored in Git instead of1131 the actual content."""1132assert False,"Method 'generatePointer' required in "+ self.__class__.__name__11331134defpushFile(self, localLargeFile):1135"""Push the actual content which is not stored in the Git repository to1136 a server."""1137assert False,"Method 'pushFile' required in "+ self.__class__.__name__11381139defhasLargeFileExtension(self, relPath):1140returnreduce(1141lambda a, b: a or b,1142[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1143False1144)11451146defgenerateTempFile(self, contents):1147 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1148for d in contents:1149 contentFile.write(d)1150 contentFile.close()1151return contentFile.name11521153defexceedsLargeFileThreshold(self, relPath, contents):1154ifgitConfigInt('git-p4.largeFileThreshold'):1155 contentsSize =sum(len(d)for d in contents)1156if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1157return True1158ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1159 contentsSize =sum(len(d)for d in contents)1160if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1161return False1162 contentTempFile = self.generateTempFile(contents)1163 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1164 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1165 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1166 zf.close()1167 compressedContentsSize = zf.infolist()[0].compress_size1168 os.remove(contentTempFile)1169 os.remove(compressedContentFile.name)1170if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1171return True1172return False11731174defaddLargeFile(self, relPath):1175 self.largeFiles.add(relPath)11761177defremoveLargeFile(self, relPath):1178 self.largeFiles.remove(relPath)11791180defisLargeFile(self, relPath):1181return relPath in self.largeFiles11821183defprocessContent(self, git_mode, relPath, contents):1184"""Processes the content of git fast import. This method decides if a1185 file is stored in the large file system and handles all necessary1186 steps."""1187if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1188 contentTempFile = self.generateTempFile(contents)1189(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1190if pointer_git_mode:1191 git_mode = pointer_git_mode1192if localLargeFile:1193# Move temp file to final location in large file system1194 largeFileDir = os.path.dirname(localLargeFile)1195if not os.path.isdir(largeFileDir):1196 os.makedirs(largeFileDir)1197 shutil.move(contentTempFile, localLargeFile)1198 self.addLargeFile(relPath)1199ifgitConfigBool('git-p4.largeFilePush'):1200 self.pushFile(localLargeFile)1201if verbose:1202 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1203return(git_mode, contents)12041205classMockLFS(LargeFileSystem):1206"""Mock large file system for testing."""12071208defgeneratePointer(self, contentFile):1209"""The pointer content is the original content prefixed with "pointer-".1210 The local filename of the large file storage is derived from the file content.1211 """1212withopen(contentFile,'r')as f:1213 content =next(f)1214 gitMode ='100644'1215 pointerContents ='pointer-'+ content1216 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1217return(gitMode, pointerContents, localLargeFile)12181219defpushFile(self, localLargeFile):1220"""The remote filename of the large file storage is the same as the local1221 one but in a different directory.1222 """1223 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1224if not os.path.exists(remotePath):1225 os.makedirs(remotePath)1226 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))12271228classGitLFS(LargeFileSystem):1229"""Git LFS as backend for the git-p4 large file system.1230 See https://git-lfs.github.com/ for details."""12311232def__init__(self, *args):1233 LargeFileSystem.__init__(self, *args)1234 self.baseGitAttributes = []12351236defgeneratePointer(self, contentFile):1237"""Generate a Git LFS pointer for the content. Return LFS Pointer file1238 mode and content which is stored in the Git repository instead of1239 the actual content. Return also the new location of the actual1240 content.1241 """1242if os.path.getsize(contentFile) ==0:1243return(None,'',None)12441245 pointerProcess = subprocess.Popen(1246['git','lfs','pointer','--file='+ contentFile],1247 stdout=subprocess.PIPE1248)1249 pointerFile = pointerProcess.stdout.read()1250if pointerProcess.wait():1251 os.remove(contentFile)1252die('git-lfs pointer command failed. Did you install the extension?')12531254# Git LFS removed the preamble in the output of the 'pointer' command1255# starting from version 1.2.0. Check for the preamble here to support1256# earlier versions.1257# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431258if pointerFile.startswith('Git LFS pointer for'):1259 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)12601261 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1262 localLargeFile = os.path.join(1263 os.getcwd(),1264'.git','lfs','objects', oid[:2], oid[2:4],1265 oid,1266)1267# LFS Spec states that pointer files should not have the executable bit set.1268 gitMode ='100644'1269return(gitMode, pointerFile, localLargeFile)12701271defpushFile(self, localLargeFile):1272 uploadProcess = subprocess.Popen(1273['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1274)1275if uploadProcess.wait():1276die('git-lfs push command failed. Did you define a remote?')12771278defgenerateGitAttributes(self):1279return(1280 self.baseGitAttributes +1281[1282'\n',1283'#\n',1284'# Git LFS (see https://git-lfs.github.com/)\n',1285'#\n',1286] +1287['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1288for f insorted(gitConfigList('git-p4.largeFileExtensions'))1289] +1290['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1291for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1292]1293)12941295defaddLargeFile(self, relPath):1296 LargeFileSystem.addLargeFile(self, relPath)1297 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12981299defremoveLargeFile(self, relPath):1300 LargeFileSystem.removeLargeFile(self, relPath)1301 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())13021303defprocessContent(self, git_mode, relPath, contents):1304if relPath =='.gitattributes':1305 self.baseGitAttributes = contents1306return(git_mode, self.generateGitAttributes())1307else:1308return LargeFileSystem.processContent(self, git_mode, relPath, contents)13091310class Command:1311 delete_actions = ("delete","move/delete","purge")1312 add_actions = ("add","move/add")13131314def__init__(self):1315 self.usage ="usage: %prog [options]"1316 self.needsGit =True1317 self.verbose =False13181319# This is required for the "append" update_shelve action1320defensure_value(self, attr, value):1321if nothasattr(self, attr)orgetattr(self, attr)is None:1322setattr(self, attr, value)1323returngetattr(self, attr)13241325class P4UserMap:1326def__init__(self):1327 self.userMapFromPerforceServer =False1328 self.myP4UserId =None13291330defp4UserId(self):1331if self.myP4UserId:1332return self.myP4UserId13331334 results =p4CmdList("user -o")1335for r in results:1336if'User'in r:1337 self.myP4UserId = r['User']1338return r['User']1339die("Could not find your p4 user id")13401341defp4UserIsMe(self, p4User):1342# return True if the given p4 user is actually me1343 me = self.p4UserId()1344if not p4User or p4User != me:1345return False1346else:1347return True13481349defgetUserCacheFilename(self):1350 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1351return home +"/.gitp4-usercache.txt"13521353defgetUserMapFromPerforceServer(self):1354if self.userMapFromPerforceServer:1355return1356 self.users = {}1357 self.emails = {}13581359for output inp4CmdList("users"):1360if"User"not in output:1361continue1362 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1363 self.emails[output["Email"]] = output["User"]13641365 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1366for mapUserConfig ingitConfigList("git-p4.mapUser"):1367 mapUser = mapUserConfigRegex.findall(mapUserConfig)1368if mapUser andlen(mapUser[0]) ==3:1369 user = mapUser[0][0]1370 fullname = mapUser[0][1]1371 email = mapUser[0][2]1372 self.users[user] = fullname +" <"+ email +">"1373 self.emails[email] = user13741375 s =''1376for(key, val)in self.users.items():1377 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))13781379open(self.getUserCacheFilename(),"wb").write(s)1380 self.userMapFromPerforceServer =True13811382defloadUserMapFromCache(self):1383 self.users = {}1384 self.userMapFromPerforceServer =False1385try:1386 cache =open(self.getUserCacheFilename(),"rb")1387 lines = cache.readlines()1388 cache.close()1389for line in lines:1390 entry = line.strip().split("\t")1391 self.users[entry[0]] = entry[1]1392exceptIOError:1393 self.getUserMapFromPerforceServer()13941395classP4Debug(Command):1396def__init__(self):1397 Command.__init__(self)1398 self.options = []1399 self.description ="A tool to debug the output of p4 -G."1400 self.needsGit =False14011402defrun(self, args):1403 j =01404for output inp4CmdList(args):1405print('Element:%d'% j)1406 j +=11407print(output)1408return True14091410classP4RollBack(Command):1411def__init__(self):1412 Command.__init__(self)1413 self.options = [1414 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1415]1416 self.description ="A tool to debug the multi-branch import. Don't use :)"1417 self.rollbackLocalBranches =False14181419defrun(self, args):1420iflen(args) !=1:1421return False1422 maxChange =int(args[0])14231424if"p4ExitCode"inp4Cmd("changes -m 1"):1425die("Problems executing p4");14261427if self.rollbackLocalBranches:1428 refPrefix ="refs/heads/"1429 lines =read_pipe_lines("git rev-parse --symbolic --branches")1430else:1431 refPrefix ="refs/remotes/"1432 lines =read_pipe_lines("git rev-parse --symbolic --remotes")14331434for line in lines:1435if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1436 line = line.strip()1437 ref = refPrefix + line1438 log =extractLogMessageFromGitCommit(ref)1439 settings =extractSettingsGitLog(log)14401441 depotPaths = settings['depot-paths']1442 change = settings['change']14431444 changed =False14451446iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1447for p in depotPaths]))) ==0:1448print("Branch%sdid not exist at change%s, deleting."% (ref, maxChange))1449system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1450continue14511452while change andint(change) > maxChange:1453 changed =True1454if self.verbose:1455print("%sis at%s; rewinding towards%s"% (ref, change, maxChange))1456system("git update-ref%s\"%s^\""% (ref, ref))1457 log =extractLogMessageFromGitCommit(ref)1458 settings =extractSettingsGitLog(log)145914601461 depotPaths = settings['depot-paths']1462 change = settings['change']14631464if changed:1465print("%srewound to%s"% (ref, change))14661467return True14681469classP4Submit(Command, P4UserMap):14701471 conflict_behavior_choices = ("ask","skip","quit")14721473def__init__(self):1474 Command.__init__(self)1475 P4UserMap.__init__(self)1476 self.options = [1477 optparse.make_option("--origin", dest="origin"),1478 optparse.make_option("-M", dest="detectRenames", action="store_true"),1479# preserve the user, requires relevant p4 permissions1480 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1481 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1482 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1483 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1484 optparse.make_option("--conflict", dest="conflict_behavior",1485 choices=self.conflict_behavior_choices),1486 optparse.make_option("--branch", dest="branch"),1487 optparse.make_option("--shelve", dest="shelve", action="store_true",1488help="Shelve instead of submit. Shelved files are reverted, "1489"restoring the workspace to the state before the shelve"),1490 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1491 metavar="CHANGELIST",1492help="update an existing shelved changelist, implies --shelve, "1493"repeat in-order for multiple shelved changelists"),1494 optparse.make_option("--commit", dest="commit", metavar="COMMIT",1495help="submit only the specified commit(s), one commit or xxx..xxx"),1496 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",1497help="Disable rebase after submit is completed. Can be useful if you "1498"work from a local git branch that is not master"),1499 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",1500help="Skip Perforce sync of p4/master after submit or shelve"),1501]1502 self.description ="""Submit changes from git to the perforce depot.\n1503 The `p4-pre-submit` hook is executed if it exists and is executable.1504 The hook takes no parameters and nothing from standard input. Exiting with1505 non-zero status from this script prevents `git-p4 submit` from launching.15061507 One usage scenario is to run unit tests in the hook."""15081509 self.usage +=" [name of git branch to submit into perforce depot]"1510 self.origin =""1511 self.detectRenames =False1512 self.preserveUser =gitConfigBool("git-p4.preserveUser")1513 self.dry_run =False1514 self.shelve =False1515 self.update_shelve =list()1516 self.commit =""1517 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1518 self.disable_p4sync =gitConfigBool("git-p4.disableP4Sync")1519 self.prepare_p4_only =False1520 self.conflict_behavior =None1521 self.isWindows = (platform.system() =="Windows")1522 self.exportLabels =False1523 self.p4HasMoveCommand =p4_has_move_command()1524 self.branch =None15251526ifgitConfig('git-p4.largeFileSystem'):1527die("Large file system not supported for git-p4 submit command. Please remove it from config.")15281529defcheck(self):1530iflen(p4CmdList("opened ...")) >0:1531die("You have files opened with perforce! Close them before starting the sync.")15321533defseparate_jobs_from_description(self, message):1534"""Extract and return a possible Jobs field in the commit1535 message. It goes into a separate section in the p4 change1536 specification.15371538 A jobs line starts with "Jobs:" and looks like a new field1539 in a form. Values are white-space separated on the same1540 line or on following lines that start with a tab.15411542 This does not parse and extract the full git commit message1543 like a p4 form. It just sees the Jobs: line as a marker1544 to pass everything from then on directly into the p4 form,1545 but outside the description section.15461547 Return a tuple (stripped log message, jobs string)."""15481549 m = re.search(r'^Jobs:', message, re.MULTILINE)1550if m is None:1551return(message,None)15521553 jobtext = message[m.start():]1554 stripped_message = message[:m.start()].rstrip()1555return(stripped_message, jobtext)15561557defprepareLogMessage(self, template, message, jobs):1558"""Edits the template returned from "p4 change -o" to insert1559 the message in the Description field, and the jobs text in1560 the Jobs field."""1561 result =""15621563 inDescriptionSection =False15641565for line in template.split("\n"):1566if line.startswith("#"):1567 result += line +"\n"1568continue15691570if inDescriptionSection:1571if line.startswith("Files:")or line.startswith("Jobs:"):1572 inDescriptionSection =False1573# insert Jobs section1574if jobs:1575 result += jobs +"\n"1576else:1577continue1578else:1579if line.startswith("Description:"):1580 inDescriptionSection =True1581 line +="\n"1582for messageLine in message.split("\n"):1583 line +="\t"+ messageLine +"\n"15841585 result += line +"\n"15861587return result15881589defpatchRCSKeywords(self,file, pattern):1590# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1591(handle, outFileName) = tempfile.mkstemp(dir='.')1592try:1593 outFile = os.fdopen(handle,"w+")1594 inFile =open(file,"r")1595 regexp = re.compile(pattern, re.VERBOSE)1596for line in inFile.readlines():1597 line = regexp.sub(r'$\1$', line)1598 outFile.write(line)1599 inFile.close()1600 outFile.close()1601# Forcibly overwrite the original file1602 os.unlink(file)1603 shutil.move(outFileName,file)1604except:1605# cleanup our temporary file1606 os.unlink(outFileName)1607print("Failed to strip RCS keywords in%s"%file)1608raise16091610print("Patched up RCS keywords in%s"%file)16111612defp4UserForCommit(self,id):1613# Return the tuple (perforce user,git email) for a given git commit id1614 self.getUserMapFromPerforceServer()1615 gitEmail =read_pipe(["git","log","--max-count=1",1616"--format=%ae",id])1617 gitEmail = gitEmail.strip()1618if gitEmail not in self.emails:1619return(None,gitEmail)1620else:1621return(self.emails[gitEmail],gitEmail)16221623defcheckValidP4Users(self,commits):1624# check if any git authors cannot be mapped to p4 users1625foridin commits:1626(user,email) = self.p4UserForCommit(id)1627if not user:1628 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1629ifgitConfigBool("git-p4.allowMissingP4Users"):1630print("%s"% msg)1631else:1632die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)16331634deflastP4Changelist(self):1635# Get back the last changelist number submitted in this client spec. This1636# then gets used to patch up the username in the change. If the same1637# client spec is being used by multiple processes then this might go1638# wrong.1639 results =p4CmdList("client -o")# find the current client1640 client =None1641for r in results:1642if'Client'in r:1643 client = r['Client']1644break1645if not client:1646die("could not get client spec")1647 results =p4CmdList(["changes","-c", client,"-m","1"])1648for r in results:1649if'change'in r:1650return r['change']1651die("Could not get changelist number for last submit - cannot patch up user details")16521653defmodifyChangelistUser(self, changelist, newUser):1654# fixup the user field of a changelist after it has been submitted.1655 changes =p4CmdList("change -o%s"% changelist)1656iflen(changes) !=1:1657die("Bad output from p4 change modifying%sto user%s"%1658(changelist, newUser))16591660 c = changes[0]1661if c['User'] == newUser:return# nothing to do1662 c['User'] = newUser1663input= marshal.dumps(c)16641665 result =p4CmdList("change -f -i", stdin=input)1666for r in result:1667if'code'in r:1668if r['code'] =='error':1669die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1670if'data'in r:1671print("Updated user field for changelist%sto%s"% (changelist, newUser))1672return1673die("Could not modify user field of changelist%sto%s"% (changelist, newUser))16741675defcanChangeChangelists(self):1676# check to see if we have p4 admin or super-user permissions, either of1677# which are required to modify changelists.1678 results =p4CmdList(["protects", self.depotPath])1679for r in results:1680if'perm'in r:1681if r['perm'] =='admin':1682return11683if r['perm'] =='super':1684return11685return016861687defprepareSubmitTemplate(self, changelist=None):1688"""Run "p4 change -o" to grab a change specification template.1689 This does not use "p4 -G", as it is nice to keep the submission1690 template in original order, since a human might edit it.16911692 Remove lines in the Files section that show changes to files1693 outside the depot path we're committing into."""16941695[upstream, settings] =findUpstreamBranchPoint()16961697 template ="""\1698# A Perforce Change Specification.1699#1700# Change: The change number. 'new' on a new changelist.1701# Date: The date this specification was last modified.1702# Client: The client on which the changelist was created. Read-only.1703# User: The user who created the changelist.1704# Status: Either 'pending' or 'submitted'. Read-only.1705# Type: Either 'public' or 'restricted'. Default is 'public'.1706# Description: Comments about the changelist. Required.1707# Jobs: What opened jobs are to be closed by this changelist.1708# You may delete jobs from this list. (New changelists only.)1709# Files: What opened files from the default changelist are to be added1710# to this changelist. You may delete files from this list.1711# (New changelists only.)1712"""1713 files_list = []1714 inFilesSection =False1715 change_entry =None1716 args = ['change','-o']1717if changelist:1718 args.append(str(changelist))1719for entry inp4CmdList(args):1720if'code'not in entry:1721continue1722if entry['code'] =='stat':1723 change_entry = entry1724break1725if not change_entry:1726die('Failed to decode output of p4 change -o')1727for key, value in change_entry.iteritems():1728if key.startswith('File'):1729if'depot-paths'in settings:1730if not[p for p in settings['depot-paths']1731ifp4PathStartsWith(value, p)]:1732continue1733else:1734if notp4PathStartsWith(value, self.depotPath):1735continue1736 files_list.append(value)1737continue1738# Output in the order expected by prepareLogMessage1739for key in['Change','Client','User','Status','Description','Jobs']:1740if key not in change_entry:1741continue1742 template +='\n'1743 template += key +':'1744if key =='Description':1745 template +='\n'1746for field_line in change_entry[key].splitlines():1747 template +='\t'+field_line+'\n'1748iflen(files_list) >0:1749 template +='\n'1750 template +='Files:\n'1751for path in files_list:1752 template +='\t'+path+'\n'1753return template17541755defedit_template(self, template_file):1756"""Invoke the editor to let the user change the submission1757 message. Return true if okay to continue with the submit."""17581759# if configured to skip the editing part, just submit1760ifgitConfigBool("git-p4.skipSubmitEdit"):1761return True17621763# look at the modification time, to check later if the user saved1764# the file1765 mtime = os.stat(template_file).st_mtime17661767# invoke the editor1768if"P4EDITOR"in os.environ and(os.environ.get("P4EDITOR") !=""):1769 editor = os.environ.get("P4EDITOR")1770else:1771 editor =read_pipe("git var GIT_EDITOR").strip()1772system(["sh","-c", ('%s"$@"'% editor), editor, template_file])17731774# If the file was not saved, prompt to see if this patch should1775# be skipped. But skip this verification step if configured so.1776ifgitConfigBool("git-p4.skipSubmitEditCheck"):1777return True17781779# modification time updated means user saved the file1780if os.stat(template_file).st_mtime > mtime:1781return True17821783while True:1784 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1785if response =='y':1786return True1787if response =='n':1788return False17891790defget_diff_description(self, editedFiles, filesToAdd, symlinks):1791# diff1792if"P4DIFF"in os.environ:1793del(os.environ["P4DIFF"])1794 diff =""1795for editedFile in editedFiles:1796 diff +=p4_read_pipe(['diff','-du',1797wildcard_encode(editedFile)])17981799# new file diff1800 newdiff =""1801for newFile in filesToAdd:1802 newdiff +="==== new file ====\n"1803 newdiff +="--- /dev/null\n"1804 newdiff +="+++%s\n"% newFile18051806 is_link = os.path.islink(newFile)1807 expect_link = newFile in symlinks18081809if is_link and expect_link:1810 newdiff +="+%s\n"% os.readlink(newFile)1811else:1812 f =open(newFile,"r")1813for line in f.readlines():1814 newdiff +="+"+ line1815 f.close()18161817return(diff + newdiff).replace('\r\n','\n')18181819defapplyCommit(self,id):1820"""Apply one commit, return True if it succeeded."""18211822print("Applying",read_pipe(["git","show","-s",1823"--format=format:%h%s",id]))18241825(p4User, gitEmail) = self.p4UserForCommit(id)18261827 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1828 filesToAdd =set()1829 filesToChangeType =set()1830 filesToDelete =set()1831 editedFiles =set()1832 pureRenameCopy =set()1833 symlinks =set()1834 filesToChangeExecBit = {}1835 all_files =list()18361837for line in diff:1838 diff =parseDiffTreeEntry(line)1839 modifier = diff['status']1840 path = diff['src']1841 all_files.append(path)18421843if modifier =="M":1844p4_edit(path)1845ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1846 filesToChangeExecBit[path] = diff['dst_mode']1847 editedFiles.add(path)1848elif modifier =="A":1849 filesToAdd.add(path)1850 filesToChangeExecBit[path] = diff['dst_mode']1851if path in filesToDelete:1852 filesToDelete.remove(path)18531854 dst_mode =int(diff['dst_mode'],8)1855if dst_mode ==0o120000:1856 symlinks.add(path)18571858elif modifier =="D":1859 filesToDelete.add(path)1860if path in filesToAdd:1861 filesToAdd.remove(path)1862elif modifier =="C":1863 src, dest = diff['src'], diff['dst']1864 all_files.append(dest)1865p4_integrate(src, dest)1866 pureRenameCopy.add(dest)1867if diff['src_sha1'] != diff['dst_sha1']:1868p4_edit(dest)1869 pureRenameCopy.discard(dest)1870ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1871p4_edit(dest)1872 pureRenameCopy.discard(dest)1873 filesToChangeExecBit[dest] = diff['dst_mode']1874if self.isWindows:1875# turn off read-only attribute1876 os.chmod(dest, stat.S_IWRITE)1877 os.unlink(dest)1878 editedFiles.add(dest)1879elif modifier =="R":1880 src, dest = diff['src'], diff['dst']1881 all_files.append(dest)1882if self.p4HasMoveCommand:1883p4_edit(src)# src must be open before move1884p4_move(src, dest)# opens for (move/delete, move/add)1885else:1886p4_integrate(src, dest)1887if diff['src_sha1'] != diff['dst_sha1']:1888p4_edit(dest)1889else:1890 pureRenameCopy.add(dest)1891ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1892if not self.p4HasMoveCommand:1893p4_edit(dest)# with move: already open, writable1894 filesToChangeExecBit[dest] = diff['dst_mode']1895if not self.p4HasMoveCommand:1896if self.isWindows:1897 os.chmod(dest, stat.S_IWRITE)1898 os.unlink(dest)1899 filesToDelete.add(src)1900 editedFiles.add(dest)1901elif modifier =="T":1902 filesToChangeType.add(path)1903else:1904die("unknown modifier%sfor%s"% (modifier, path))19051906 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1907 patchcmd = diffcmd +" | git apply "1908 tryPatchCmd = patchcmd +"--check -"1909 applyPatchCmd = patchcmd +"--check --apply -"1910 patch_succeeded =True19111912if os.system(tryPatchCmd) !=0:1913 fixed_rcs_keywords =False1914 patch_succeeded =False1915print("Unfortunately applying the change failed!")19161917# Patch failed, maybe it's just RCS keyword woes. Look through1918# the patch to see if that's possible.1919ifgitConfigBool("git-p4.attemptRCSCleanup"):1920file=None1921 pattern =None1922 kwfiles = {}1923forfilein editedFiles | filesToDelete:1924# did this file's delta contain RCS keywords?1925 pattern =p4_keywords_regexp_for_file(file)19261927if pattern:1928# this file is a possibility...look for RCS keywords.1929 regexp = re.compile(pattern, re.VERBOSE)1930for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1931if regexp.search(line):1932if verbose:1933print("got keyword match on%sin%sin%s"% (pattern, line,file))1934 kwfiles[file] = pattern1935break19361937forfilein kwfiles:1938if verbose:1939print("zapping%swith%s"% (line,pattern))1940# File is being deleted, so not open in p4. Must1941# disable the read-only bit on windows.1942if self.isWindows andfilenot in editedFiles:1943 os.chmod(file, stat.S_IWRITE)1944 self.patchRCSKeywords(file, kwfiles[file])1945 fixed_rcs_keywords =True19461947if fixed_rcs_keywords:1948print("Retrying the patch with RCS keywords cleaned up")1949if os.system(tryPatchCmd) ==0:1950 patch_succeeded =True19511952if not patch_succeeded:1953for f in editedFiles:1954p4_revert(f)1955return False19561957#1958# Apply the patch for real, and do add/delete/+x handling.1959#1960system(applyPatchCmd)19611962for f in filesToChangeType:1963p4_edit(f,"-t","auto")1964for f in filesToAdd:1965p4_add(f)1966for f in filesToDelete:1967p4_revert(f)1968p4_delete(f)19691970# Set/clear executable bits1971for f in filesToChangeExecBit.keys():1972 mode = filesToChangeExecBit[f]1973setP4ExecBit(f, mode)19741975 update_shelve =01976iflen(self.update_shelve) >0:1977 update_shelve = self.update_shelve.pop(0)1978p4_reopen_in_change(update_shelve, all_files)19791980#1981# Build p4 change description, starting with the contents1982# of the git commit message.1983#1984 logMessage =extractLogMessageFromGitCommit(id)1985 logMessage = logMessage.strip()1986(logMessage, jobs) = self.separate_jobs_from_description(logMessage)19871988 template = self.prepareSubmitTemplate(update_shelve)1989 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)19901991if self.preserveUser:1992 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User19931994if self.checkAuthorship and not self.p4UserIsMe(p4User):1995 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1996 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1997 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"19981999 separatorLine ="######## everything below this line is just the diff #######\n"2000if not self.prepare_p4_only:2001 submitTemplate += separatorLine2002 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)20032004(handle, fileName) = tempfile.mkstemp()2005 tmpFile = os.fdopen(handle,"w+b")2006if self.isWindows:2007 submitTemplate = submitTemplate.replace("\n","\r\n")2008 tmpFile.write(submitTemplate)2009 tmpFile.close()20102011if self.prepare_p4_only:2012#2013# Leave the p4 tree prepared, and the submit template around2014# and let the user decide what to do next2015#2016print()2017print("P4 workspace prepared for submission.")2018print("To submit or revert, go to client workspace")2019print(" "+ self.clientPath)2020print()2021print("To submit, use\"p4 submit\"to write a new description,")2022print("or\"p4 submit -i <%s\"to use the one prepared by" \2023"\"git p4\"."% fileName)2024print("You can delete the file\"%s\"when finished."% fileName)20252026if self.preserveUser and p4User and not self.p4UserIsMe(p4User):2027print("To preserve change ownership by user%s, you must\n" \2028"do\"p4 change -f <change>\"after submitting and\n" \2029"edit the User field.")2030if pureRenameCopy:2031print("After submitting, renamed files must be re-synced.")2032print("Invoke\"p4 sync -f\"on each of these files:")2033for f in pureRenameCopy:2034print(" "+ f)20352036print()2037print("To revert the changes, use\"p4 revert ...\", and delete")2038print("the submit template file\"%s\""% fileName)2039if filesToAdd:2040print("Since the commit adds new files, they must be deleted:")2041for f in filesToAdd:2042print(" "+ f)2043print()2044return True20452046#2047# Let the user edit the change description, then submit it.2048#2049 submitted =False20502051try:2052if self.edit_template(fileName):2053# read the edited message and submit2054 tmpFile =open(fileName,"rb")2055 message = tmpFile.read()2056 tmpFile.close()2057if self.isWindows:2058 message = message.replace("\r\n","\n")2059 submitTemplate = message[:message.index(separatorLine)]20602061if update_shelve:2062p4_write_pipe(['shelve','-r','-i'], submitTemplate)2063elif self.shelve:2064p4_write_pipe(['shelve','-i'], submitTemplate)2065else:2066p4_write_pipe(['submit','-i'], submitTemplate)2067# The rename/copy happened by applying a patch that created a2068# new file. This leaves it writable, which confuses p4.2069for f in pureRenameCopy:2070p4_sync(f,"-f")20712072if self.preserveUser:2073if p4User:2074# Get last changelist number. Cannot easily get it from2075# the submit command output as the output is2076# unmarshalled.2077 changelist = self.lastP4Changelist()2078 self.modifyChangelistUser(changelist, p4User)20792080 submitted =True20812082finally:2083# skip this patch2084if not submitted or self.shelve:2085if self.shelve:2086print("Reverting shelved files.")2087else:2088print("Submission cancelled, undoing p4 changes.")2089for f in editedFiles | filesToDelete:2090p4_revert(f)2091for f in filesToAdd:2092p4_revert(f)2093 os.remove(f)20942095 os.remove(fileName)2096return submitted20972098# Export git tags as p4 labels. Create a p4 label and then tag2099# with that.2100defexportGitTags(self, gitTags):2101 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")2102iflen(validLabelRegexp) ==0:2103 validLabelRegexp = defaultLabelRegexp2104 m = re.compile(validLabelRegexp)21052106for name in gitTags:21072108if not m.match(name):2109if verbose:2110print("tag%sdoes not match regexp%s"% (name, validLabelRegexp))2111continue21122113# Get the p4 commit this corresponds to2114 logMessage =extractLogMessageFromGitCommit(name)2115 values =extractSettingsGitLog(logMessage)21162117if'change'not in values:2118# a tag pointing to something not sent to p4; ignore2119if verbose:2120print("git tag%sdoes not give a p4 commit"% name)2121continue2122else:2123 changelist = values['change']21242125# Get the tag details.2126 inHeader =True2127 isAnnotated =False2128 body = []2129for l inread_pipe_lines(["git","cat-file","-p", name]):2130 l = l.strip()2131if inHeader:2132if re.match(r'tag\s+', l):2133 isAnnotated =True2134elif re.match(r'\s*$', l):2135 inHeader =False2136continue2137else:2138 body.append(l)21392140if not isAnnotated:2141 body = ["lightweight tag imported by git p4\n"]21422143# Create the label - use the same view as the client spec we are using2144 clientSpec =getClientSpec()21452146 labelTemplate ="Label:%s\n"% name2147 labelTemplate +="Description:\n"2148for b in body:2149 labelTemplate +="\t"+ b +"\n"2150 labelTemplate +="View:\n"2151for depot_side in clientSpec.mappings:2152 labelTemplate +="\t%s\n"% depot_side21532154if self.dry_run:2155print("Would create p4 label%sfor tag"% name)2156elif self.prepare_p4_only:2157print("Not creating p4 label%sfor tag due to option" \2158" --prepare-p4-only"% name)2159else:2160p4_write_pipe(["label","-i"], labelTemplate)21612162# Use the label2163p4_system(["tag","-l", name] +2164["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])21652166if verbose:2167print("created p4 label for tag%s"% name)21682169defrun(self, args):2170iflen(args) ==0:2171 self.master =currentGitBranch()2172eliflen(args) ==1:2173 self.master = args[0]2174if notbranchExists(self.master):2175die("Branch%sdoes not exist"% self.master)2176else:2177return False21782179for i in self.update_shelve:2180if i <=0:2181 sys.exit("invalid changelist%d"% i)21822183if self.master:2184 allowSubmit =gitConfig("git-p4.allowSubmit")2185iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2186die("%sis not in git-p4.allowSubmit"% self.master)21872188[upstream, settings] =findUpstreamBranchPoint()2189 self.depotPath = settings['depot-paths'][0]2190iflen(self.origin) ==0:2191 self.origin = upstream21922193iflen(self.update_shelve) >0:2194 self.shelve =True21952196if self.preserveUser:2197if not self.canChangeChangelists():2198die("Cannot preserve user names without p4 super-user or admin permissions")21992200# if not set from the command line, try the config file2201if self.conflict_behavior is None:2202 val =gitConfig("git-p4.conflict")2203if val:2204if val not in self.conflict_behavior_choices:2205die("Invalid value '%s' for config git-p4.conflict"% val)2206else:2207 val ="ask"2208 self.conflict_behavior = val22092210if self.verbose:2211print("Origin branch is "+ self.origin)22122213iflen(self.depotPath) ==0:2214print("Internal error: cannot locate perforce depot path from existing branches")2215 sys.exit(128)22162217 self.useClientSpec =False2218ifgitConfigBool("git-p4.useclientspec"):2219 self.useClientSpec =True2220if self.useClientSpec:2221 self.clientSpecDirs =getClientSpec()22222223# Check for the existence of P4 branches2224 branchesDetected = (len(p4BranchesInGit().keys()) >1)22252226if self.useClientSpec and not branchesDetected:2227# all files are relative to the client spec2228 self.clientPath =getClientRoot()2229else:2230 self.clientPath =p4Where(self.depotPath)22312232if self.clientPath =="":2233die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)22342235print("Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath))2236 self.oldWorkingDirectory = os.getcwd()22372238# ensure the clientPath exists2239 new_client_dir =False2240if not os.path.exists(self.clientPath):2241 new_client_dir =True2242 os.makedirs(self.clientPath)22432244chdir(self.clientPath, is_client_path=True)2245if self.dry_run:2246print("Would synchronize p4 checkout in%s"% self.clientPath)2247else:2248print("Synchronizing p4 checkout...")2249if new_client_dir:2250# old one was destroyed, and maybe nobody told p42251p4_sync("...","-f")2252else:2253p4_sync("...")2254 self.check()22552256 commits = []2257if self.master:2258 committish = self.master2259else:2260 committish ='HEAD'22612262if self.commit !="":2263if self.commit.find("..") != -1:2264 limits_ish = self.commit.split("..")2265for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2266 commits.append(line.strip())2267 commits.reverse()2268else:2269 commits.append(self.commit)2270else:2271for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, committish)]):2272 commits.append(line.strip())2273 commits.reverse()22742275if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2276 self.checkAuthorship =False2277else:2278 self.checkAuthorship =True22792280if self.preserveUser:2281 self.checkValidP4Users(commits)22822283#2284# Build up a set of options to be passed to diff when2285# submitting each commit to p4.2286#2287if self.detectRenames:2288# command-line -M arg2289 self.diffOpts ="-M"2290else:2291# If not explicitly set check the config variable2292 detectRenames =gitConfig("git-p4.detectRenames")22932294if detectRenames.lower() =="false"or detectRenames =="":2295 self.diffOpts =""2296elif detectRenames.lower() =="true":2297 self.diffOpts ="-M"2298else:2299 self.diffOpts ="-M%s"% detectRenames23002301# no command-line arg for -C or --find-copies-harder, just2302# config variables2303 detectCopies =gitConfig("git-p4.detectCopies")2304if detectCopies.lower() =="false"or detectCopies =="":2305pass2306elif detectCopies.lower() =="true":2307 self.diffOpts +=" -C"2308else:2309 self.diffOpts +=" -C%s"% detectCopies23102311ifgitConfigBool("git-p4.detectCopiesHarder"):2312 self.diffOpts +=" --find-copies-harder"23132314 num_shelves =len(self.update_shelve)2315if num_shelves >0and num_shelves !=len(commits):2316 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2317(len(commits), num_shelves))23182319 hooks_path =gitConfig("core.hooksPath")2320iflen(hooks_path) <=0:2321 hooks_path = os.path.join(os.environ.get("GIT_DIR",".git"),"hooks")23222323 hook_file = os.path.join(hooks_path,"p4-pre-submit")2324if os.path.isfile(hook_file)and os.access(hook_file, os.X_OK)and subprocess.call([hook_file]) !=0:2325 sys.exit(1)23262327#2328# Apply the commits, one at a time. On failure, ask if should2329# continue to try the rest of the patches, or quit.2330#2331if self.dry_run:2332print("Would apply")2333 applied = []2334 last =len(commits) -12335for i, commit inenumerate(commits):2336if self.dry_run:2337print(" ",read_pipe(["git","show","-s",2338"--format=format:%h%s", commit]))2339 ok =True2340else:2341 ok = self.applyCommit(commit)2342if ok:2343 applied.append(commit)2344else:2345if self.prepare_p4_only and i < last:2346print("Processing only the first commit due to option" \2347" --prepare-p4-only")2348break2349if i < last:2350 quit =False2351while True:2352# prompt for what to do, or use the option/variable2353if self.conflict_behavior =="ask":2354print("What do you want to do?")2355 response =raw_input("[s]kip this commit but apply"2356" the rest, or [q]uit? ")2357if not response:2358continue2359elif self.conflict_behavior =="skip":2360 response ="s"2361elif self.conflict_behavior =="quit":2362 response ="q"2363else:2364die("Unknown conflict_behavior '%s'"%2365 self.conflict_behavior)23662367if response[0] =="s":2368print("Skipping this commit, but applying the rest")2369break2370if response[0] =="q":2371print("Quitting")2372 quit =True2373break2374if quit:2375break23762377chdir(self.oldWorkingDirectory)2378 shelved_applied ="shelved"if self.shelve else"applied"2379if self.dry_run:2380pass2381elif self.prepare_p4_only:2382pass2383eliflen(commits) ==len(applied):2384print("All commits{0}!".format(shelved_applied))23852386 sync =P4Sync()2387if self.branch:2388 sync.branch = self.branch2389if self.disable_p4sync:2390 sync.sync_origin_only()2391else:2392 sync.run([])23932394if not self.disable_rebase:2395 rebase =P4Rebase()2396 rebase.rebase()23972398else:2399iflen(applied) ==0:2400print("No commits{0}.".format(shelved_applied))2401else:2402print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2403for c in commits:2404if c in applied:2405 star ="*"2406else:2407 star =" "2408print(star,read_pipe(["git","show","-s",2409"--format=format:%h%s", c]))2410print("You will have to do 'git p4 sync' and rebase.")24112412ifgitConfigBool("git-p4.exportLabels"):2413 self.exportLabels =True24142415if self.exportLabels:2416 p4Labels =getP4Labels(self.depotPath)2417 gitTags =getGitTags()24182419 missingGitTags = gitTags - p4Labels2420 self.exportGitTags(missingGitTags)24212422# exit with error unless everything applied perfectly2423iflen(commits) !=len(applied):2424 sys.exit(1)24252426return True24272428classView(object):2429"""Represent a p4 view ("p4 help views"), and map files in a2430 repo according to the view."""24312432def__init__(self, client_name):2433 self.mappings = []2434 self.client_prefix ="//%s/"% client_name2435# cache results of "p4 where" to lookup client file locations2436 self.client_spec_path_cache = {}24372438defappend(self, view_line):2439"""Parse a view line, splitting it into depot and client2440 sides. Append to self.mappings, preserving order. This2441 is only needed for tag creation."""24422443# Split the view line into exactly two words. P4 enforces2444# structure on these lines that simplifies this quite a bit.2445#2446# Either or both words may be double-quoted.2447# Single quotes do not matter.2448# Double-quote marks cannot occur inside the words.2449# A + or - prefix is also inside the quotes.2450# There are no quotes unless they contain a space.2451# The line is already white-space stripped.2452# The two words are separated by a single space.2453#2454if view_line[0] =='"':2455# First word is double quoted. Find its end.2456 close_quote_index = view_line.find('"',1)2457if close_quote_index <=0:2458die("No first-word closing quote found:%s"% view_line)2459 depot_side = view_line[1:close_quote_index]2460# skip closing quote and space2461 rhs_index = close_quote_index +1+12462else:2463 space_index = view_line.find(" ")2464if space_index <=0:2465die("No word-splitting space found:%s"% view_line)2466 depot_side = view_line[0:space_index]2467 rhs_index = space_index +124682469# prefix + means overlay on previous mapping2470if depot_side.startswith("+"):2471 depot_side = depot_side[1:]24722473# prefix - means exclude this path, leave out of mappings2474 exclude =False2475if depot_side.startswith("-"):2476 exclude =True2477 depot_side = depot_side[1:]24782479if not exclude:2480 self.mappings.append(depot_side)24812482defconvert_client_path(self, clientFile):2483# chop off //client/ part to make it relative2484if not clientFile.startswith(self.client_prefix):2485die("No prefix '%s' on clientFile '%s'"%2486(self.client_prefix, clientFile))2487return clientFile[len(self.client_prefix):]24882489defupdate_client_spec_path_cache(self, files):2490""" Caching file paths by "p4 where" batch query """24912492# List depot file paths exclude that already cached2493 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]24942495iflen(fileArgs) ==0:2496return# All files in cache24972498 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2499for res in where_result:2500if"code"in res and res["code"] =="error":2501# assume error is "... file(s) not in client view"2502continue2503if"clientFile"not in res:2504die("No clientFile in 'p4 where' output")2505if"unmap"in res:2506# it will list all of them, but only one not unmap-ped2507continue2508ifgitConfigBool("core.ignorecase"):2509 res['depotFile'] = res['depotFile'].lower()2510 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])25112512# not found files or unmap files set to ""2513for depotFile in fileArgs:2514ifgitConfigBool("core.ignorecase"):2515 depotFile = depotFile.lower()2516if depotFile not in self.client_spec_path_cache:2517 self.client_spec_path_cache[depotFile] =""25182519defmap_in_client(self, depot_path):2520"""Return the relative location in the client where this2521 depot file should live. Returns "" if the file should2522 not be mapped in the client."""25232524ifgitConfigBool("core.ignorecase"):2525 depot_path = depot_path.lower()25262527if depot_path in self.client_spec_path_cache:2528return self.client_spec_path_cache[depot_path]25292530die("Error:%sis not found in client spec path"% depot_path )2531return""25322533defcloneExcludeCallback(option, opt_str, value, parser):2534# prepend "/" because the first "/" was consumed as part of the option itself.2535# ("-//depot/A/..." becomes "/depot/A/..." after option parsing)2536 parser.values.cloneExclude += ["/"+ re.sub(r"\.\.\.$","", value)]25372538classP4Sync(Command, P4UserMap):25392540def__init__(self):2541 Command.__init__(self)2542 P4UserMap.__init__(self)2543 self.options = [2544 optparse.make_option("--branch", dest="branch"),2545 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2546 optparse.make_option("--changesfile", dest="changesFile"),2547 optparse.make_option("--silent", dest="silent", action="store_true"),2548 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2549 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2550 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2551help="Import into refs/heads/ , not refs/remotes"),2552 optparse.make_option("--max-changes", dest="maxChanges",2553help="Maximum number of changes to import"),2554 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2555help="Internal block size to use when iteratively calling p4 changes"),2556 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2557help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2558 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2559help="Only sync files that are included in the Perforce Client Spec"),2560 optparse.make_option("-/", dest="cloneExclude",2561 action="callback", callback=cloneExcludeCallback,type="string",2562help="exclude depot path"),2563]2564 self.description ="""Imports from Perforce into a git repository.\n2565 example:2566 //depot/my/project/ -- to import the current head2567 //depot/my/project/@all -- to import everything2568 //depot/my/project/@1,6 -- to import only from revision 1 to 625692570 (a ... is not needed in the path p4 specification, it's added implicitly)"""25712572 self.usage +=" //depot/path[@revRange]"2573 self.silent =False2574 self.createdBranches =set()2575 self.committedChanges =set()2576 self.branch =""2577 self.detectBranches =False2578 self.detectLabels =False2579 self.importLabels =False2580 self.changesFile =""2581 self.syncWithOrigin =True2582 self.importIntoRemotes =True2583 self.maxChanges =""2584 self.changes_block_size =None2585 self.keepRepoPath =False2586 self.depotPaths =None2587 self.p4BranchesInGit = []2588 self.cloneExclude = []2589 self.useClientSpec =False2590 self.useClientSpec_from_options =False2591 self.clientSpecDirs =None2592 self.tempBranches = []2593 self.tempBranchLocation ="refs/git-p4-tmp"2594 self.largeFileSystem =None2595 self.suppress_meta_comment =False25962597ifgitConfig('git-p4.largeFileSystem'):2598 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2599 self.largeFileSystem =largeFileSystemConstructor(2600lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2601)26022603ifgitConfig("git-p4.syncFromOrigin") =="false":2604 self.syncWithOrigin =False26052606 self.depotPaths = []2607 self.changeRange =""2608 self.previousDepotPaths = []2609 self.hasOrigin =False26102611# map from branch depot path to parent branch2612 self.knownBranches = {}2613 self.initialParents = {}26142615 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))2616 self.labels = {}26172618# Force a checkpoint in fast-import and wait for it to finish2619defcheckpoint(self):2620 self.gitStream.write("checkpoint\n\n")2621 self.gitStream.write("progress checkpoint\n\n")2622 out = self.gitOutput.readline()2623if self.verbose:2624print("checkpoint finished: "+ out)26252626defisPathWanted(self, path):2627for p in self.cloneExclude:2628if p.endswith("/"):2629ifp4PathStartsWith(path, p):2630return False2631# "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"2632elif path.lower() == p.lower():2633return False2634for p in self.depotPaths:2635ifp4PathStartsWith(path, p):2636return True2637return False26382639defextractFilesFromCommit(self, commit, shelved=False, shelved_cl =0):2640 files = []2641 fnum =02642while"depotFile%s"% fnum in commit:2643 path = commit["depotFile%s"% fnum]2644 found = self.isPathWanted(path)2645if not found:2646 fnum = fnum +12647continue26482649file= {}2650file["path"] = path2651file["rev"] = commit["rev%s"% fnum]2652file["action"] = commit["action%s"% fnum]2653file["type"] = commit["type%s"% fnum]2654if shelved:2655file["shelved_cl"] =int(shelved_cl)2656 files.append(file)2657 fnum = fnum +12658return files26592660defextractJobsFromCommit(self, commit):2661 jobs = []2662 jnum =02663while"job%s"% jnum in commit:2664 job = commit["job%s"% jnum]2665 jobs.append(job)2666 jnum = jnum +12667return jobs26682669defstripRepoPath(self, path, prefixes):2670"""When streaming files, this is called to map a p4 depot path2671 to where it should go in git. The prefixes are either2672 self.depotPaths, or self.branchPrefixes in the case of2673 branch detection."""26742675if self.useClientSpec:2676# branch detection moves files up a level (the branch name)2677# from what client spec interpretation gives2678 path = self.clientSpecDirs.map_in_client(path)2679if self.detectBranches:2680for b in self.knownBranches:2681ifp4PathStartsWith(path, b +"/"):2682 path = path[len(b)+1:]26832684elif self.keepRepoPath:2685# Preserve everything in relative path name except leading2686# //depot/; just look at first prefix as they all should2687# be in the same depot.2688 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2689ifp4PathStartsWith(path, depot):2690 path = path[len(depot):]26912692else:2693for p in prefixes:2694ifp4PathStartsWith(path, p):2695 path = path[len(p):]2696break26972698 path =wildcard_decode(path)2699return path27002701defsplitFilesIntoBranches(self, commit):2702"""Look at each depotFile in the commit to figure out to what2703 branch it belongs."""27042705if self.clientSpecDirs:2706 files = self.extractFilesFromCommit(commit)2707 self.clientSpecDirs.update_client_spec_path_cache(files)27082709 branches = {}2710 fnum =02711while"depotFile%s"% fnum in commit:2712 path = commit["depotFile%s"% fnum]2713 found = [p for p in self.depotPaths2714ifp4PathStartsWith(path, p)]2715if not found:2716 fnum = fnum +12717continue27182719file= {}2720file["path"] = path2721file["rev"] = commit["rev%s"% fnum]2722file["action"] = commit["action%s"% fnum]2723file["type"] = commit["type%s"% fnum]2724 fnum = fnum +127252726# start with the full relative path where this file would2727# go in a p4 client2728if self.useClientSpec:2729 relPath = self.clientSpecDirs.map_in_client(path)2730else:2731 relPath = self.stripRepoPath(path, self.depotPaths)27322733for branch in self.knownBranches.keys():2734# add a trailing slash so that a commit into qt/4.2foo2735# doesn't end up in qt/4.2, e.g.2736ifp4PathStartsWith(relPath, branch +"/"):2737if branch not in branches:2738 branches[branch] = []2739 branches[branch].append(file)2740break27412742return branches27432744defwriteToGitStream(self, gitMode, relPath, contents):2745 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2746 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2747for d in contents:2748 self.gitStream.write(d)2749 self.gitStream.write('\n')27502751defencodeWithUTF8(self, path):2752try:2753 path.decode('ascii')2754except:2755 encoding ='utf8'2756ifgitConfig('git-p4.pathEncoding'):2757 encoding =gitConfig('git-p4.pathEncoding')2758 path = path.decode(encoding,'replace').encode('utf8','replace')2759if self.verbose:2760print('Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path))2761return path27622763# output one file from the P4 stream2764# - helper for streamP4Files27652766defstreamOneP4File(self,file, contents):2767 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2768 relPath = self.encodeWithUTF8(relPath)2769if verbose:2770if'fileSize'in self.stream_file:2771 size =int(self.stream_file['fileSize'])2772else:2773 size =0# deleted files don't get a fileSize apparently2774 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2775 sys.stdout.flush()27762777(type_base, type_mods) =split_p4_type(file["type"])27782779 git_mode ="100644"2780if"x"in type_mods:2781 git_mode ="100755"2782if type_base =="symlink":2783 git_mode ="120000"2784# p4 print on a symlink sometimes contains "target\n";2785# if it does, remove the newline2786 data =''.join(contents)2787if not data:2788# Some version of p4 allowed creating a symlink that pointed2789# to nothing. This causes p4 errors when checking out such2790# a change, and errors here too. Work around it by ignoring2791# the bad symlink; hopefully a future change fixes it.2792print("\nIgnoring empty symlink in%s"%file['depotFile'])2793return2794elif data[-1] =='\n':2795 contents = [data[:-1]]2796else:2797 contents = [data]27982799if type_base =="utf16":2800# p4 delivers different text in the python output to -G2801# than it does when using "print -o", or normal p4 client2802# operations. utf16 is converted to ascii or utf8, perhaps.2803# But ascii text saved as -t utf16 is completely mangled.2804# Invoke print -o to get the real contents.2805#2806# On windows, the newlines will always be mangled by print, so put2807# them back too. This is not needed to the cygwin windows version,2808# just the native "NT" type.2809#2810try:2811 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2812exceptExceptionas e:2813if'Translation of file content failed'instr(e):2814 type_base ='binary'2815else:2816raise e2817else:2818ifp4_version_string().find('/NT') >=0:2819 text = text.replace('\r\n','\n')2820 contents = [ text ]28212822if type_base =="apple":2823# Apple filetype files will be streamed as a concatenation of2824# its appledouble header and the contents. This is useless2825# on both macs and non-macs. If using "print -q -o xx", it2826# will create "xx" with the data, and "%xx" with the header.2827# This is also not very useful.2828#2829# Ideally, someday, this script can learn how to generate2830# appledouble files directly and import those to git, but2831# non-mac machines can never find a use for apple filetype.2832print("\nIgnoring apple filetype file%s"%file['depotFile'])2833return28342835# Note that we do not try to de-mangle keywords on utf16 files,2836# even though in theory somebody may want that.2837 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2838if pattern:2839 regexp = re.compile(pattern, re.VERBOSE)2840 text =''.join(contents)2841 text = regexp.sub(r'$\1$', text)2842 contents = [ text ]28432844if self.largeFileSystem:2845(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)28462847 self.writeToGitStream(git_mode, relPath, contents)28482849defstreamOneP4Deletion(self,file):2850 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2851 relPath = self.encodeWithUTF8(relPath)2852if verbose:2853 sys.stdout.write("delete%s\n"% relPath)2854 sys.stdout.flush()2855 self.gitStream.write("D%s\n"% relPath)28562857if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2858 self.largeFileSystem.removeLargeFile(relPath)28592860# handle another chunk of streaming data2861defstreamP4FilesCb(self, marshalled):28622863# catch p4 errors and complain2864 err =None2865if"code"in marshalled:2866if marshalled["code"] =="error":2867if"data"in marshalled:2868 err = marshalled["data"].rstrip()28692870if not err and'fileSize'in self.stream_file:2871 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2872if required_bytes >0:2873 err ='Not enough space left on%s! Free at least%iMB.'% (2874 os.getcwd(), required_bytes/1024/10242875)28762877if err:2878 f =None2879if self.stream_have_file_info:2880if"depotFile"in self.stream_file:2881 f = self.stream_file["depotFile"]2882# force a failure in fast-import, else an empty2883# commit will be made2884 self.gitStream.write("\n")2885 self.gitStream.write("die-now\n")2886 self.gitStream.close()2887# ignore errors, but make sure it exits first2888 self.importProcess.wait()2889if f:2890die("Error from p4 print for%s:%s"% (f, err))2891else:2892die("Error from p4 print:%s"% err)28932894if'depotFile'in marshalled and self.stream_have_file_info:2895# start of a new file - output the old one first2896 self.streamOneP4File(self.stream_file, self.stream_contents)2897 self.stream_file = {}2898 self.stream_contents = []2899 self.stream_have_file_info =False29002901# pick up the new file information... for the2902# 'data' field we need to append to our array2903for k in marshalled.keys():2904if k =='data':2905if'streamContentSize'not in self.stream_file:2906 self.stream_file['streamContentSize'] =02907 self.stream_file['streamContentSize'] +=len(marshalled['data'])2908 self.stream_contents.append(marshalled['data'])2909else:2910 self.stream_file[k] = marshalled[k]29112912if(verbose and2913'streamContentSize'in self.stream_file and2914'fileSize'in self.stream_file and2915'depotFile'in self.stream_file):2916 size =int(self.stream_file["fileSize"])2917if size >0:2918 progress =100*self.stream_file['streamContentSize']/size2919 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2920 sys.stdout.flush()29212922 self.stream_have_file_info =True29232924# Stream directly from "p4 files" into "git fast-import"2925defstreamP4Files(self, files):2926 filesForCommit = []2927 filesToRead = []2928 filesToDelete = []29292930for f in files:2931 filesForCommit.append(f)2932if f['action']in self.delete_actions:2933 filesToDelete.append(f)2934else:2935 filesToRead.append(f)29362937# deleted files...2938for f in filesToDelete:2939 self.streamOneP4Deletion(f)29402941iflen(filesToRead) >0:2942 self.stream_file = {}2943 self.stream_contents = []2944 self.stream_have_file_info =False29452946# curry self argument2947defstreamP4FilesCbSelf(entry):2948 self.streamP4FilesCb(entry)29492950 fileArgs = []2951for f in filesToRead:2952if'shelved_cl'in f:2953# Handle shelved CLs using the "p4 print file@=N" syntax to print2954# the contents2955 fileArg ='%s@=%d'% (f['path'], f['shelved_cl'])2956else:2957 fileArg ='%s#%s'% (f['path'], f['rev'])29582959 fileArgs.append(fileArg)29602961p4CmdList(["-x","-","print"],2962 stdin=fileArgs,2963 cb=streamP4FilesCbSelf)29642965# do the last chunk2966if'depotFile'in self.stream_file:2967 self.streamOneP4File(self.stream_file, self.stream_contents)29682969defmake_email(self, userid):2970if userid in self.users:2971return self.users[userid]2972else:2973return"%s<a@b>"% userid29742975defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2976""" Stream a p4 tag.2977 commit is either a git commit, or a fast-import mark, ":<p4commit>"2978 """29792980if verbose:2981print("writing tag%sfor commit%s"% (labelName, commit))2982 gitStream.write("tag%s\n"% labelName)2983 gitStream.write("from%s\n"% commit)29842985if'Owner'in labelDetails:2986 owner = labelDetails["Owner"]2987else:2988 owner =None29892990# Try to use the owner of the p4 label, or failing that,2991# the current p4 user id.2992if owner:2993 email = self.make_email(owner)2994else:2995 email = self.make_email(self.p4UserId())2996 tagger ="%s %s %s"% (email, epoch, self.tz)29972998 gitStream.write("tagger%s\n"% tagger)29993000print("labelDetails=",labelDetails)3001if'Description'in labelDetails:3002 description = labelDetails['Description']3003else:3004 description ='Label from git p4'30053006 gitStream.write("data%d\n"%len(description))3007 gitStream.write(description)3008 gitStream.write("\n")30093010definClientSpec(self, path):3011if not self.clientSpecDirs:3012return True3013 inClientSpec = self.clientSpecDirs.map_in_client(path)3014if not inClientSpec and self.verbose:3015print('Ignoring file outside of client spec:{0}'.format(path))3016return inClientSpec30173018defhasBranchPrefix(self, path):3019if not self.branchPrefixes:3020return True3021 hasPrefix = [p for p in self.branchPrefixes3022ifp4PathStartsWith(path, p)]3023if not hasPrefix and self.verbose:3024print('Ignoring file outside of prefix:{0}'.format(path))3025return hasPrefix30263027defcommit(self, details, files, branch, parent ="", allow_empty=False):3028 epoch = details["time"]3029 author = details["user"]3030 jobs = self.extractJobsFromCommit(details)30313032if self.verbose:3033print('commit into{0}'.format(branch))30343035if self.clientSpecDirs:3036 self.clientSpecDirs.update_client_spec_path_cache(files)30373038 files = [f for f in files3039if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]30403041ifgitConfigBool('git-p4.keepEmptyCommits'):3042 allow_empty =True30433044if not files and not allow_empty:3045print('Ignoring revision{0}as it would produce an empty commit.'3046.format(details['change']))3047return30483049 self.gitStream.write("commit%s\n"% branch)3050 self.gitStream.write("mark :%s\n"% details["change"])3051 self.committedChanges.add(int(details["change"]))3052 committer =""3053if author not in self.users:3054 self.getUserMapFromPerforceServer()3055 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)30563057 self.gitStream.write("committer%s\n"% committer)30583059 self.gitStream.write("data <<EOT\n")3060 self.gitStream.write(details["desc"])3061iflen(jobs) >0:3062 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))30633064if not self.suppress_meta_comment:3065 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%3066(','.join(self.branchPrefixes), details["change"]))3067iflen(details['options']) >0:3068 self.gitStream.write(": options =%s"% details['options'])3069 self.gitStream.write("]\n")30703071 self.gitStream.write("EOT\n\n")30723073iflen(parent) >0:3074if self.verbose:3075print("parent%s"% parent)3076 self.gitStream.write("from%s\n"% parent)30773078 self.streamP4Files(files)3079 self.gitStream.write("\n")30803081 change =int(details["change"])30823083if change in self.labels:3084 label = self.labels[change]3085 labelDetails = label[0]3086 labelRevisions = label[1]3087if self.verbose:3088print("Change%sis labelled%s"% (change, labelDetails))30893090 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)3091for p in self.branchPrefixes])30923093iflen(files) ==len(labelRevisions):30943095 cleanedFiles = {}3096for info in files:3097if info["action"]in self.delete_actions:3098continue3099 cleanedFiles[info["depotFile"]] = info["rev"]31003101if cleanedFiles == labelRevisions:3102 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)31033104else:3105if not self.silent:3106print("Tag%sdoes not match with change%s: files do not match."3107% (labelDetails["label"], change))31083109else:3110if not self.silent:3111print("Tag%sdoes not match with change%s: file count is different."3112% (labelDetails["label"], change))31133114# Build a dictionary of changelists and labels, for "detect-labels" option.3115defgetLabels(self):3116 self.labels = {}31173118 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])3119iflen(l) >0and not self.silent:3120print("Finding files belonging to labels in%s"% self.depotPaths)31213122for output in l:3123 label = output["label"]3124 revisions = {}3125 newestChange =03126if self.verbose:3127print("Querying files for label%s"% label)3128forfileinp4CmdList(["files"] +3129["%s...@%s"% (p, label)3130for p in self.depotPaths]):3131 revisions[file["depotFile"]] =file["rev"]3132 change =int(file["change"])3133if change > newestChange:3134 newestChange = change31353136 self.labels[newestChange] = [output, revisions]31373138if self.verbose:3139print("Label changes:%s"% self.labels.keys())31403141# Import p4 labels as git tags. A direct mapping does not3142# exist, so assume that if all the files are at the same revision3143# then we can use that, or it's something more complicated we should3144# just ignore.3145defimportP4Labels(self, stream, p4Labels):3146if verbose:3147print("import p4 labels: "+' '.join(p4Labels))31483149 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")3150 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")3151iflen(validLabelRegexp) ==0:3152 validLabelRegexp = defaultLabelRegexp3153 m = re.compile(validLabelRegexp)31543155for name in p4Labels:3156 commitFound =False31573158if not m.match(name):3159if verbose:3160print("label%sdoes not match regexp%s"% (name,validLabelRegexp))3161continue31623163if name in ignoredP4Labels:3164continue31653166 labelDetails =p4CmdList(['label',"-o", name])[0]31673168# get the most recent changelist for each file in this label3169 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)3170for p in self.depotPaths])31713172if'change'in change:3173# find the corresponding git commit; take the oldest commit3174 changelist =int(change['change'])3175if changelist in self.committedChanges:3176 gitCommit =":%d"% changelist # use a fast-import mark3177 commitFound =True3178else:3179 gitCommit =read_pipe(["git","rev-list","--max-count=1",3180"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)3181iflen(gitCommit) ==0:3182print("importing label%s: could not find git commit for changelist%d"% (name, changelist))3183else:3184 commitFound =True3185 gitCommit = gitCommit.strip()31863187if commitFound:3188# Convert from p4 time format3189try:3190 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")3191exceptValueError:3192print("Could not convert label time%s"% labelDetails['Update'])3193 tmwhen =131943195 when =int(time.mktime(tmwhen))3196 self.streamTag(stream, name, labelDetails, gitCommit, when)3197if verbose:3198print("p4 label%smapped to git commit%s"% (name, gitCommit))3199else:3200if verbose:3201print("Label%shas no changelists - possibly deleted?"% name)32023203if not commitFound:3204# We can't import this label; don't try again as it will get very3205# expensive repeatedly fetching all the files for labels that will3206# never be imported. If the label is moved in the future, the3207# ignore will need to be removed manually.3208system(["git","config","--add","git-p4.ignoredP4Labels", name])32093210defguessProjectName(self):3211for p in self.depotPaths:3212if p.endswith("/"):3213 p = p[:-1]3214 p = p[p.strip().rfind("/") +1:]3215if not p.endswith("/"):3216 p +="/"3217return p32183219defgetBranchMapping(self):3220 lostAndFoundBranches =set()32213222 user =gitConfig("git-p4.branchUser")3223iflen(user) >0:3224 command ="branches -u%s"% user3225else:3226 command ="branches"32273228for info inp4CmdList(command):3229 details =p4Cmd(["branch","-o", info["branch"]])3230 viewIdx =03231while"View%s"% viewIdx in details:3232 paths = details["View%s"% viewIdx].split(" ")3233 viewIdx = viewIdx +13234# require standard //depot/foo/... //depot/bar/... mapping3235iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3236continue3237 source = paths[0]3238 destination = paths[1]3239## HACK3240ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3241 source = source[len(self.depotPaths[0]):-4]3242 destination = destination[len(self.depotPaths[0]):-4]32433244if destination in self.knownBranches:3245if not self.silent:3246print("p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination))3247print("but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination))3248continue32493250 self.knownBranches[destination] = source32513252 lostAndFoundBranches.discard(destination)32533254if source not in self.knownBranches:3255 lostAndFoundBranches.add(source)32563257# Perforce does not strictly require branches to be defined, so we also3258# check git config for a branch list.3259#3260# Example of branch definition in git config file:3261# [git-p4]3262# branchList=main:branchA3263# branchList=main:branchB3264# branchList=branchA:branchC3265 configBranches =gitConfigList("git-p4.branchList")3266for branch in configBranches:3267if branch:3268(source, destination) = branch.split(":")3269 self.knownBranches[destination] = source32703271 lostAndFoundBranches.discard(destination)32723273if source not in self.knownBranches:3274 lostAndFoundBranches.add(source)327532763277for branch in lostAndFoundBranches:3278 self.knownBranches[branch] = branch32793280defgetBranchMappingFromGitBranches(self):3281 branches =p4BranchesInGit(self.importIntoRemotes)3282for branch in branches.keys():3283if branch =="master":3284 branch ="main"3285else:3286 branch = branch[len(self.projectName):]3287 self.knownBranches[branch] = branch32883289defupdateOptionDict(self, d):3290 option_keys = {}3291if self.keepRepoPath:3292 option_keys['keepRepoPath'] =132933294 d["options"] =' '.join(sorted(option_keys.keys()))32953296defreadOptions(self, d):3297 self.keepRepoPath = ('options'in d3298and('keepRepoPath'in d['options']))32993300defgitRefForBranch(self, branch):3301if branch =="main":3302return self.refPrefix +"master"33033304iflen(branch) <=0:3305return branch33063307return self.refPrefix + self.projectName + branch33083309defgitCommitByP4Change(self, ref, change):3310if self.verbose:3311print("looking in ref "+ ref +" for change%susing bisect..."% change)33123313 earliestCommit =""3314 latestCommit =parseRevision(ref)33153316while True:3317if self.verbose:3318print("trying: earliest%slatest%s"% (earliestCommit, latestCommit))3319 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3320iflen(next) ==0:3321if self.verbose:3322print("argh")3323return""3324 log =extractLogMessageFromGitCommit(next)3325 settings =extractSettingsGitLog(log)3326 currentChange =int(settings['change'])3327if self.verbose:3328print("current change%s"% currentChange)33293330if currentChange == change:3331if self.verbose:3332print("found%s"% next)3333return next33343335if currentChange < change:3336 earliestCommit ="^%s"% next3337else:3338if next == latestCommit:3339die("Infinite loop while looking in ref%sfor change%s. Check your branch mappings"% (ref, change))3340 latestCommit ="%s^@"% next33413342return""33433344defimportNewBranch(self, branch, maxChange):3345# make fast-import flush all changes to disk and update the refs using the checkpoint3346# command so that we can try to find the branch parent in the git history3347 self.gitStream.write("checkpoint\n\n");3348 self.gitStream.flush();3349 branchPrefix = self.depotPaths[0] + branch +"/"3350range="@1,%s"% maxChange3351#print "prefix" + branchPrefix3352 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3353iflen(changes) <=0:3354return False3355 firstChange = changes[0]3356#print "first change in branch: %s" % firstChange3357 sourceBranch = self.knownBranches[branch]3358 sourceDepotPath = self.depotPaths[0] + sourceBranch3359 sourceRef = self.gitRefForBranch(sourceBranch)3360#print "source " + sourceBranch33613362 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3363#print "branch parent: %s" % branchParentChange3364 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3365iflen(gitParent) >0:3366 self.initialParents[self.gitRefForBranch(branch)] = gitParent3367#print "parent git commit: %s" % gitParent33683369 self.importChanges(changes)3370return True33713372defsearchParent(self, parent, branch, target):3373 parentFound =False3374for blob inread_pipe_lines(["git","rev-list","--reverse",3375"--no-merges", parent]):3376 blob = blob.strip()3377iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3378 parentFound =True3379if self.verbose:3380print("Found parent of%sin commit%s"% (branch, blob))3381break3382if parentFound:3383return blob3384else:3385return None33863387defimportChanges(self, changes, origin_revision=0):3388 cnt =13389for change in changes:3390 description =p4_describe(change)3391 self.updateOptionDict(description)33923393if not self.silent:3394 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3395 sys.stdout.flush()3396 cnt = cnt +133973398try:3399if self.detectBranches:3400 branches = self.splitFilesIntoBranches(description)3401for branch in branches.keys():3402## HACK --hwn3403 branchPrefix = self.depotPaths[0] + branch +"/"3404 self.branchPrefixes = [ branchPrefix ]34053406 parent =""34073408 filesForCommit = branches[branch]34093410if self.verbose:3411print("branch is%s"% branch)34123413 self.updatedBranches.add(branch)34143415if branch not in self.createdBranches:3416 self.createdBranches.add(branch)3417 parent = self.knownBranches[branch]3418if parent == branch:3419 parent =""3420else:3421 fullBranch = self.projectName + branch3422if fullBranch not in self.p4BranchesInGit:3423if not self.silent:3424print("\nImporting new branch%s"% fullBranch);3425if self.importNewBranch(branch, change -1):3426 parent =""3427 self.p4BranchesInGit.append(fullBranch)3428if not self.silent:3429print("\nResuming with change%s"% change);34303431if self.verbose:3432print("parent determined through known branches:%s"% parent)34333434 branch = self.gitRefForBranch(branch)3435 parent = self.gitRefForBranch(parent)34363437if self.verbose:3438print("looking for initial parent for%s; current parent is%s"% (branch, parent))34393440iflen(parent) ==0and branch in self.initialParents:3441 parent = self.initialParents[branch]3442del self.initialParents[branch]34433444 blob =None3445iflen(parent) >0:3446 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3447if self.verbose:3448print("Creating temporary branch: "+ tempBranch)3449 self.commit(description, filesForCommit, tempBranch)3450 self.tempBranches.append(tempBranch)3451 self.checkpoint()3452 blob = self.searchParent(parent, branch, tempBranch)3453if blob:3454 self.commit(description, filesForCommit, branch, blob)3455else:3456if self.verbose:3457print("Parent of%snot found. Committing into head of%s"% (branch, parent))3458 self.commit(description, filesForCommit, branch, parent)3459else:3460 files = self.extractFilesFromCommit(description)3461 self.commit(description, files, self.branch,3462 self.initialParent)3463# only needed once, to connect to the previous commit3464 self.initialParent =""3465exceptIOError:3466print(self.gitError.read())3467 sys.exit(1)34683469defsync_origin_only(self):3470if self.syncWithOrigin:3471 self.hasOrigin =originP4BranchesExist()3472if self.hasOrigin:3473if not self.silent:3474print('Syncing with origin first, using "git fetch origin"')3475system("git fetch origin")34763477defimportHeadRevision(self, revision):3478print("Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch))34793480 details = {}3481 details["user"] ="git perforce import user"3482 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3483% (' '.join(self.depotPaths), revision))3484 details["change"] = revision3485 newestRevision =034863487 fileCnt =03488 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]34893490for info inp4CmdList(["files"] + fileArgs):34913492if'code'in info and info['code'] =='error':3493 sys.stderr.write("p4 returned an error:%s\n"3494% info['data'])3495if info['data'].find("must refer to client") >=0:3496 sys.stderr.write("This particular p4 error is misleading.\n")3497 sys.stderr.write("Perhaps the depot path was misspelled.\n");3498 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3499 sys.exit(1)3500if'p4ExitCode'in info:3501 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3502 sys.exit(1)350335043505 change =int(info["change"])3506if change > newestRevision:3507 newestRevision = change35083509if info["action"]in self.delete_actions:3510# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3511#fileCnt = fileCnt + 13512continue35133514for prop in["depotFile","rev","action","type"]:3515 details["%s%s"% (prop, fileCnt)] = info[prop]35163517 fileCnt = fileCnt +135183519 details["change"] = newestRevision35203521# Use time from top-most change so that all git p4 clones of3522# the same p4 repo have the same commit SHA1s.3523 res =p4_describe(newestRevision)3524 details["time"] = res["time"]35253526 self.updateOptionDict(details)3527try:3528 self.commit(details, self.extractFilesFromCommit(details), self.branch)3529exceptIOError:3530print("IO error with git fast-import. Is your git version recent enough?")3531print(self.gitError.read())35323533defopenStreams(self):3534 self.importProcess = subprocess.Popen(["git","fast-import"],3535 stdin=subprocess.PIPE,3536 stdout=subprocess.PIPE,3537 stderr=subprocess.PIPE);3538 self.gitOutput = self.importProcess.stdout3539 self.gitStream = self.importProcess.stdin3540 self.gitError = self.importProcess.stderr35413542defcloseStreams(self):3543 self.gitStream.close()3544if self.importProcess.wait() !=0:3545die("fast-import failed:%s"% self.gitError.read())3546 self.gitOutput.close()3547 self.gitError.close()35483549defrun(self, args):3550if self.importIntoRemotes:3551 self.refPrefix ="refs/remotes/p4/"3552else:3553 self.refPrefix ="refs/heads/p4/"35543555 self.sync_origin_only()35563557 branch_arg_given =bool(self.branch)3558iflen(self.branch) ==0:3559 self.branch = self.refPrefix +"master"3560ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3561system("git update-ref%srefs/heads/p4"% self.branch)3562system("git branch -D p4")35633564# accept either the command-line option, or the configuration variable3565if self.useClientSpec:3566# will use this after clone to set the variable3567 self.useClientSpec_from_options =True3568else:3569ifgitConfigBool("git-p4.useclientspec"):3570 self.useClientSpec =True3571if self.useClientSpec:3572 self.clientSpecDirs =getClientSpec()35733574# TODO: should always look at previous commits,3575# merge with previous imports, if possible.3576if args == []:3577if self.hasOrigin:3578createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)35793580# branches holds mapping from branch name to sha13581 branches =p4BranchesInGit(self.importIntoRemotes)35823583# restrict to just this one, disabling detect-branches3584if branch_arg_given:3585 short = self.branch.split("/")[-1]3586if short in branches:3587 self.p4BranchesInGit = [ short ]3588else:3589 self.p4BranchesInGit = branches.keys()35903591iflen(self.p4BranchesInGit) >1:3592if not self.silent:3593print("Importing from/into multiple branches")3594 self.detectBranches =True3595for branch in branches.keys():3596 self.initialParents[self.refPrefix + branch] = \3597 branches[branch]35983599if self.verbose:3600print("branches:%s"% self.p4BranchesInGit)36013602 p4Change =03603for branch in self.p4BranchesInGit:3604 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)36053606 settings =extractSettingsGitLog(logMsg)36073608 self.readOptions(settings)3609if('depot-paths'in settings3610and'change'in settings):3611 change =int(settings['change']) +13612 p4Change =max(p4Change, change)36133614 depotPaths =sorted(settings['depot-paths'])3615if self.previousDepotPaths == []:3616 self.previousDepotPaths = depotPaths3617else:3618 paths = []3619for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3620 prev_list = prev.split("/")3621 cur_list = cur.split("/")3622for i inrange(0,min(len(cur_list),len(prev_list))):3623if cur_list[i] != prev_list[i]:3624 i = i -13625break36263627 paths.append("/".join(cur_list[:i +1]))36283629 self.previousDepotPaths = paths36303631if p4Change >0:3632 self.depotPaths =sorted(self.previousDepotPaths)3633 self.changeRange ="@%s,#head"% p4Change3634if not self.silent and not self.detectBranches:3635print("Performing incremental import into%sgit branch"% self.branch)36363637# accept multiple ref name abbreviations:3638# refs/foo/bar/branch -> use it exactly3639# p4/branch -> prepend refs/remotes/ or refs/heads/3640# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3641if not self.branch.startswith("refs/"):3642if self.importIntoRemotes:3643 prepend ="refs/remotes/"3644else:3645 prepend ="refs/heads/"3646if not self.branch.startswith("p4/"):3647 prepend +="p4/"3648 self.branch = prepend + self.branch36493650iflen(args) ==0and self.depotPaths:3651if not self.silent:3652print("Depot paths:%s"%' '.join(self.depotPaths))3653else:3654if self.depotPaths and self.depotPaths != args:3655print("previous import used depot path%sand now%swas specified. "3656"This doesn't work!"% (' '.join(self.depotPaths),3657' '.join(args)))3658 sys.exit(1)36593660 self.depotPaths =sorted(args)36613662 revision =""3663 self.users = {}36643665# Make sure no revision specifiers are used when --changesfile3666# is specified.3667 bad_changesfile =False3668iflen(self.changesFile) >0:3669for p in self.depotPaths:3670if p.find("@") >=0or p.find("#") >=0:3671 bad_changesfile =True3672break3673if bad_changesfile:3674die("Option --changesfile is incompatible with revision specifiers")36753676 newPaths = []3677for p in self.depotPaths:3678if p.find("@") != -1:3679 atIdx = p.index("@")3680 self.changeRange = p[atIdx:]3681if self.changeRange =="@all":3682 self.changeRange =""3683elif','not in self.changeRange:3684 revision = self.changeRange3685 self.changeRange =""3686 p = p[:atIdx]3687elif p.find("#") != -1:3688 hashIdx = p.index("#")3689 revision = p[hashIdx:]3690 p = p[:hashIdx]3691elif self.previousDepotPaths == []:3692# pay attention to changesfile, if given, else import3693# the entire p4 tree at the head revision3694iflen(self.changesFile) ==0:3695 revision ="#head"36963697 p = re.sub("\.\.\.$","", p)3698if not p.endswith("/"):3699 p +="/"37003701 newPaths.append(p)37023703 self.depotPaths = newPaths37043705# --detect-branches may change this for each branch3706 self.branchPrefixes = self.depotPaths37073708 self.loadUserMapFromCache()3709 self.labels = {}3710if self.detectLabels:3711 self.getLabels();37123713if self.detectBranches:3714## FIXME - what's a P4 projectName ?3715 self.projectName = self.guessProjectName()37163717if self.hasOrigin:3718 self.getBranchMappingFromGitBranches()3719else:3720 self.getBranchMapping()3721if self.verbose:3722print("p4-git branches:%s"% self.p4BranchesInGit)3723print("initial parents:%s"% self.initialParents)3724for b in self.p4BranchesInGit:3725if b !="master":37263727## FIXME3728 b = b[len(self.projectName):]3729 self.createdBranches.add(b)37303731 self.openStreams()37323733if revision:3734 self.importHeadRevision(revision)3735else:3736 changes = []37373738iflen(self.changesFile) >0:3739 output =open(self.changesFile).readlines()3740 changeSet =set()3741for line in output:3742 changeSet.add(int(line))37433744for change in changeSet:3745 changes.append(change)37463747 changes.sort()3748else:3749# catch "git p4 sync" with no new branches, in a repo that3750# does not have any existing p4 branches3751iflen(args) ==0:3752if not self.p4BranchesInGit:3753die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")37543755# The default branch is master, unless --branch is used to3756# specify something else. Make sure it exists, or complain3757# nicely about how to use --branch.3758if not self.detectBranches:3759if notbranch_exists(self.branch):3760if branch_arg_given:3761die("Error: branch%sdoes not exist."% self.branch)3762else:3763die("Error: no branch%s; perhaps specify one with --branch."%3764 self.branch)37653766if self.verbose:3767print("Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3768 self.changeRange))3769 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)37703771iflen(self.maxChanges) >0:3772 changes = changes[:min(int(self.maxChanges),len(changes))]37733774iflen(changes) ==0:3775if not self.silent:3776print("No changes to import!")3777else:3778if not self.silent and not self.detectBranches:3779print("Import destination:%s"% self.branch)37803781 self.updatedBranches =set()37823783if not self.detectBranches:3784if args:3785# start a new branch3786 self.initialParent =""3787else:3788# build on a previous revision3789 self.initialParent =parseRevision(self.branch)37903791 self.importChanges(changes)37923793if not self.silent:3794print("")3795iflen(self.updatedBranches) >0:3796 sys.stdout.write("Updated branches: ")3797for b in self.updatedBranches:3798 sys.stdout.write("%s"% b)3799 sys.stdout.write("\n")38003801ifgitConfigBool("git-p4.importLabels"):3802 self.importLabels =True38033804if self.importLabels:3805 p4Labels =getP4Labels(self.depotPaths)3806 gitTags =getGitTags()38073808 missingP4Labels = p4Labels - gitTags3809 self.importP4Labels(self.gitStream, missingP4Labels)38103811 self.closeStreams()38123813# Cleanup temporary branches created during import3814if self.tempBranches != []:3815for branch in self.tempBranches:3816read_pipe("git update-ref -d%s"% branch)3817 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))38183819# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3820# a convenient shortcut refname "p4".3821if self.importIntoRemotes:3822 head_ref = self.refPrefix +"HEAD"3823if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3824system(["git","symbolic-ref", head_ref, self.branch])38253826return True38273828classP4Rebase(Command):3829def__init__(self):3830 Command.__init__(self)3831 self.options = [3832 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3833]3834 self.importLabels =False3835 self.description = ("Fetches the latest revision from perforce and "3836+"rebases the current work (branch) against it")38373838defrun(self, args):3839 sync =P4Sync()3840 sync.importLabels = self.importLabels3841 sync.run([])38423843return self.rebase()38443845defrebase(self):3846if os.system("git update-index --refresh") !=0:3847die("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.");3848iflen(read_pipe("git diff-index HEAD --")) >0:3849die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");38503851[upstream, settings] =findUpstreamBranchPoint()3852iflen(upstream) ==0:3853die("Cannot find upstream branchpoint for rebase")38543855# the branchpoint may be p4/foo~3, so strip off the parent3856 upstream = re.sub("~[0-9]+$","", upstream)38573858print("Rebasing the current branch onto%s"% upstream)3859 oldHead =read_pipe("git rev-parse HEAD").strip()3860system("git rebase%s"% upstream)3861system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3862return True38633864classP4Clone(P4Sync):3865def__init__(self):3866 P4Sync.__init__(self)3867 self.description ="Creates a new git repository and imports from Perforce into it"3868 self.usage ="usage: %prog [options] //depot/path[@revRange]"3869 self.options += [3870 optparse.make_option("--destination", dest="cloneDestination",3871 action='store', default=None,3872help="where to leave result of the clone"),3873 optparse.make_option("--bare", dest="cloneBare",3874 action="store_true", default=False),3875]3876 self.cloneDestination =None3877 self.needsGit =False3878 self.cloneBare =False38793880defdefaultDestination(self, args):3881## TODO: use common prefix of args?3882 depotPath = args[0]3883 depotDir = re.sub("(@[^@]*)$","", depotPath)3884 depotDir = re.sub("(#[^#]*)$","", depotDir)3885 depotDir = re.sub(r"\.\.\.$","", depotDir)3886 depotDir = re.sub(r"/$","", depotDir)3887return os.path.split(depotDir)[1]38883889defrun(self, args):3890iflen(args) <1:3891return False38923893if self.keepRepoPath and not self.cloneDestination:3894 sys.stderr.write("Must specify destination for --keep-path\n")3895 sys.exit(1)38963897 depotPaths = args38983899if not self.cloneDestination andlen(depotPaths) >1:3900 self.cloneDestination = depotPaths[-1]3901 depotPaths = depotPaths[:-1]39023903for p in depotPaths:3904if not p.startswith("//"):3905 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3906return False39073908if not self.cloneDestination:3909 self.cloneDestination = self.defaultDestination(args)39103911print("Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination))39123913if not os.path.exists(self.cloneDestination):3914 os.makedirs(self.cloneDestination)3915chdir(self.cloneDestination)39163917 init_cmd = ["git","init"]3918if self.cloneBare:3919 init_cmd.append("--bare")3920 retcode = subprocess.call(init_cmd)3921if retcode:3922raiseCalledProcessError(retcode, init_cmd)39233924if not P4Sync.run(self, depotPaths):3925return False39263927# create a master branch and check out a work tree3928ifgitBranchExists(self.branch):3929system(["git","branch","master", self.branch ])3930if not self.cloneBare:3931system(["git","checkout","-f"])3932else:3933print('Not checking out any branch, use ' \3934'"git checkout -q -b master <branch>"')39353936# auto-set this variable if invoked with --use-client-spec3937if self.useClientSpec_from_options:3938system("git config --bool git-p4.useclientspec true")39393940return True39413942classP4Unshelve(Command):3943def__init__(self):3944 Command.__init__(self)3945 self.options = []3946 self.origin ="HEAD"3947 self.description ="Unshelve a P4 changelist into a git commit"3948 self.usage ="usage: %prog [options] changelist"3949 self.options += [3950 optparse.make_option("--origin", dest="origin",3951help="Use this base revision instead of the default (%s)"% self.origin),3952]3953 self.verbose =False3954 self.noCommit =False3955 self.destbranch ="refs/remotes/p4-unshelved"39563957defrenameBranch(self, branch_name):3958""" Rename the existing branch to branch_name.N3959 """39603961 found =True3962for i inrange(0,1000):3963 backup_branch_name ="{0}.{1}".format(branch_name, i)3964if notgitBranchExists(backup_branch_name):3965gitUpdateRef(backup_branch_name, branch_name)# copy ref to backup3966gitDeleteRef(branch_name)3967 found =True3968print("renamed old unshelve branch to{0}".format(backup_branch_name))3969break39703971if not found:3972 sys.exit("gave up trying to rename existing branch{0}".format(sync.branch))39733974deffindLastP4Revision(self, starting_point):3975""" Look back from starting_point for the first commit created by git-p43976 to find the P4 commit we are based on, and the depot-paths.3977 """39783979for parent in(range(65535)):3980 log =extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))3981 settings =extractSettingsGitLog(log)3982if'change'in settings:3983return settings39843985 sys.exit("could not find git-p4 commits in{0}".format(self.origin))39863987defcreateShelveParent(self, change, branch_name, sync, origin):3988""" Create a commit matching the parent of the shelved changelist 'change'3989 """3990 parent_description =p4_describe(change, shelved=True)3991 parent_description['desc'] ='parent for shelved changelist {}\n'.format(change)3992 files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)39933994 parent_files = []3995for f in files:3996# if it was added in the shelved changelist, it won't exist in the parent3997if f['action']in self.add_actions:3998continue39994000# if it was deleted in the shelved changelist it must not be deleted4001# in the parent - we might even need to create it if the origin branch4002# does not have it4003if f['action']in self.delete_actions:4004 f['action'] ='add'40054006 parent_files.append(f)40074008 sync.commit(parent_description, parent_files, branch_name,4009 parent=origin, allow_empty=True)4010print("created parent commit for{0}based on{1}in{2}".format(4011 change, self.origin, branch_name))40124013defrun(self, args):4014iflen(args) !=1:4015return False40164017if notgitBranchExists(self.origin):4018 sys.exit("origin branch{0}does not exist".format(self.origin))40194020 sync =P4Sync()4021 changes = args40224023# only one change at a time4024 change = changes[0]40254026# if the target branch already exists, rename it4027 branch_name ="{0}/{1}".format(self.destbranch, change)4028ifgitBranchExists(branch_name):4029 self.renameBranch(branch_name)4030 sync.branch = branch_name40314032 sync.verbose = self.verbose4033 sync.suppress_meta_comment =True40344035 settings = self.findLastP4Revision(self.origin)4036 sync.depotPaths = settings['depot-paths']4037 sync.branchPrefixes = sync.depotPaths40384039 sync.openStreams()4040 sync.loadUserMapFromCache()4041 sync.silent =True40424043# create a commit for the parent of the shelved changelist4044 self.createShelveParent(change, branch_name, sync, self.origin)40454046# create the commit for the shelved changelist itself4047 description =p4_describe(change,True)4048 files = sync.extractFilesFromCommit(description,True, change)40494050 sync.commit(description, files, branch_name,"")4051 sync.closeStreams()40524053print("unshelved changelist{0}into{1}".format(change, branch_name))40544055return True40564057classP4Branches(Command):4058def__init__(self):4059 Command.__init__(self)4060 self.options = [ ]4061 self.description = ("Shows the git branches that hold imports and their "4062+"corresponding perforce depot paths")4063 self.verbose =False40644065defrun(self, args):4066iforiginP4BranchesExist():4067createOrUpdateBranchesFromOrigin()40684069 cmdline ="git rev-parse --symbolic "4070 cmdline +=" --remotes"40714072for line inread_pipe_lines(cmdline):4073 line = line.strip()40744075if not line.startswith('p4/')or line =="p4/HEAD":4076continue4077 branch = line40784079 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)4080 settings =extractSettingsGitLog(log)40814082print("%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"]))4083return True40844085classHelpFormatter(optparse.IndentedHelpFormatter):4086def__init__(self):4087 optparse.IndentedHelpFormatter.__init__(self)40884089defformat_description(self, description):4090if description:4091return description +"\n"4092else:4093return""40944095defprintUsage(commands):4096print("usage:%s<command> [options]"% sys.argv[0])4097print("")4098print("valid commands:%s"%", ".join(commands))4099print("")4100print("Try%s<command> --help for command specific help."% sys.argv[0])4101print("")41024103commands = {4104"debug": P4Debug,4105"submit": P4Submit,4106"commit": P4Submit,4107"sync": P4Sync,4108"rebase": P4Rebase,4109"clone": P4Clone,4110"rollback": P4RollBack,4111"branches": P4Branches,4112"unshelve": P4Unshelve,4113}411441154116defmain():4117iflen(sys.argv[1:]) ==0:4118printUsage(commands.keys())4119 sys.exit(2)41204121 cmdName = sys.argv[1]4122try:4123 klass = commands[cmdName]4124 cmd =klass()4125exceptKeyError:4126print("unknown command%s"% cmdName)4127print("")4128printUsage(commands.keys())4129 sys.exit(2)41304131 options = cmd.options4132 cmd.gitdir = os.environ.get("GIT_DIR",None)41334134 args = sys.argv[2:]41354136 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))4137if cmd.needsGit:4138 options.append(optparse.make_option("--git-dir", dest="gitdir"))41394140 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),4141 options,4142 description = cmd.description,4143 formatter =HelpFormatter())41444145(cmd, args) = parser.parse_args(sys.argv[2:], cmd);4146global verbose4147 verbose = cmd.verbose4148if cmd.needsGit:4149if cmd.gitdir ==None:4150 cmd.gitdir = os.path.abspath(".git")4151if notisValidGitDir(cmd.gitdir):4152# "rev-parse --git-dir" without arguments will try $PWD/.git4153 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()4154if os.path.exists(cmd.gitdir):4155 cdup =read_pipe("git rev-parse --show-cdup").strip()4156iflen(cdup) >0:4157chdir(cdup);41584159if notisValidGitDir(cmd.gitdir):4160ifisValidGitDir(cmd.gitdir +"/.git"):4161 cmd.gitdir +="/.git"4162else:4163die("fatal: cannot locate git repository at%s"% cmd.gitdir)41644165# so git commands invoked from the P4 workspace will succeed4166 os.environ["GIT_DIR"] = cmd.gitdir41674168if not cmd.run(args):4169 parser.print_help()4170 sys.exit(2)417141724173if __name__ =='__main__':4174main()