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") 335else: 336die_bad_access("unknown error code{0}".format(code)) 337 338_p4_version_string =None 339defp4_version_string(): 340"""Read the version string, showing just the last line, which 341 hopefully is the interesting version bit. 342 343 $ p4 -V 344 Perforce - The Fast Software Configuration Management System. 345 Copyright 1995-2011 Perforce Software. All rights reserved. 346 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 347 """ 348global _p4_version_string 349if not _p4_version_string: 350 a =p4_read_pipe_lines(["-V"]) 351 _p4_version_string = a[-1].rstrip() 352return _p4_version_string 353 354defp4_integrate(src, dest): 355p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 356 357defp4_sync(f, *options): 358p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 359 360defp4_add(f): 361# forcibly add file names with wildcards 362ifwildcard_present(f): 363p4_system(["add","-f", f]) 364else: 365p4_system(["add", f]) 366 367defp4_delete(f): 368p4_system(["delete",wildcard_encode(f)]) 369 370defp4_edit(f, *options): 371p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 372 373defp4_revert(f): 374p4_system(["revert",wildcard_encode(f)]) 375 376defp4_reopen(type, f): 377p4_system(["reopen","-t",type,wildcard_encode(f)]) 378 379defp4_reopen_in_change(changelist, files): 380 cmd = ["reopen","-c",str(changelist)] + files 381p4_system(cmd) 382 383defp4_move(src, dest): 384p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 385 386defp4_last_change(): 387 results =p4CmdList(["changes","-m","1"], skip_info=True) 388returnint(results[0]['change']) 389 390defp4_describe(change, shelved=False): 391"""Make sure it returns a valid result by checking for 392 the presence of field "time". Return a dict of the 393 results.""" 394 395 cmd = ["describe","-s"] 396if shelved: 397 cmd += ["-S"] 398 cmd += [str(change)] 399 400 ds =p4CmdList(cmd, skip_info=True) 401iflen(ds) !=1: 402die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 403 404 d = ds[0] 405 406if"p4ExitCode"in d: 407die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 408str(d))) 409if"code"in d: 410if d["code"] =="error": 411die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 412 413if"time"not in d: 414die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 415 416return d 417 418# 419# Canonicalize the p4 type and return a tuple of the 420# base type, plus any modifiers. See "p4 help filetypes" 421# for a list and explanation. 422# 423defsplit_p4_type(p4type): 424 425 p4_filetypes_historical = { 426"ctempobj":"binary+Sw", 427"ctext":"text+C", 428"cxtext":"text+Cx", 429"ktext":"text+k", 430"kxtext":"text+kx", 431"ltext":"text+F", 432"tempobj":"binary+FSw", 433"ubinary":"binary+F", 434"uresource":"resource+F", 435"uxbinary":"binary+Fx", 436"xbinary":"binary+x", 437"xltext":"text+Fx", 438"xtempobj":"binary+Swx", 439"xtext":"text+x", 440"xunicode":"unicode+x", 441"xutf16":"utf16+x", 442} 443if p4type in p4_filetypes_historical: 444 p4type = p4_filetypes_historical[p4type] 445 mods ="" 446 s = p4type.split("+") 447 base = s[0] 448 mods ="" 449iflen(s) >1: 450 mods = s[1] 451return(base, mods) 452 453# 454# return the raw p4 type of a file (text, text+ko, etc) 455# 456defp4_type(f): 457 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 458return results[0]['headType'] 459 460# 461# Given a type base and modifier, return a regexp matching 462# the keywords that can be expanded in the file 463# 464defp4_keywords_regexp_for_type(base, type_mods): 465if base in("text","unicode","binary"): 466 kwords =None 467if"ko"in type_mods: 468 kwords ='Id|Header' 469elif"k"in type_mods: 470 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 471else: 472return None 473 pattern = r""" 474 \$ # Starts with a dollar, followed by... 475 (%s) # one of the keywords, followed by... 476 (:[^$\n]+)? # possibly an old expansion, followed by... 477 \$ # another dollar 478 """% kwords 479return pattern 480else: 481return None 482 483# 484# Given a file, return a regexp matching the possible 485# RCS keywords that will be expanded, or None for files 486# with kw expansion turned off. 487# 488defp4_keywords_regexp_for_file(file): 489if not os.path.exists(file): 490return None 491else: 492(type_base, type_mods) =split_p4_type(p4_type(file)) 493returnp4_keywords_regexp_for_type(type_base, type_mods) 494 495defsetP4ExecBit(file, mode): 496# Reopens an already open file and changes the execute bit to match 497# the execute bit setting in the passed in mode. 498 499 p4Type ="+x" 500 501if notisModeExec(mode): 502 p4Type =getP4OpenedType(file) 503 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 504 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 505if p4Type[-1] =="+": 506 p4Type = p4Type[0:-1] 507 508p4_reopen(p4Type,file) 509 510defgetP4OpenedType(file): 511# Returns the perforce file type for the given file. 512 513 result =p4_read_pipe(["opened",wildcard_encode(file)]) 514 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 515if match: 516return match.group(1) 517else: 518die("Could not determine file type for%s(result: '%s')"% (file, result)) 519 520# Return the set of all p4 labels 521defgetP4Labels(depotPaths): 522 labels =set() 523ifisinstance(depotPaths,basestring): 524 depotPaths = [depotPaths] 525 526for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 527 label = l['label'] 528 labels.add(label) 529 530return labels 531 532# Return the set of all git tags 533defgetGitTags(): 534 gitTags =set() 535for line inread_pipe_lines(["git","tag"]): 536 tag = line.strip() 537 gitTags.add(tag) 538return gitTags 539 540defdiffTreePattern(): 541# This is a simple generator for the diff tree regex pattern. This could be 542# a class variable if this and parseDiffTreeEntry were a part of a class. 543 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 544while True: 545yield pattern 546 547defparseDiffTreeEntry(entry): 548"""Parses a single diff tree entry into its component elements. 549 550 See git-diff-tree(1) manpage for details about the format of the diff 551 output. This method returns a dictionary with the following elements: 552 553 src_mode - The mode of the source file 554 dst_mode - The mode of the destination file 555 src_sha1 - The sha1 for the source file 556 dst_sha1 - The sha1 fr the destination file 557 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 558 status_score - The score for the status (applicable for 'C' and 'R' 559 statuses). This is None if there is no score. 560 src - The path for the source file. 561 dst - The path for the destination file. This is only present for 562 copy or renames. If it is not present, this is None. 563 564 If the pattern is not matched, None is returned.""" 565 566 match =diffTreePattern().next().match(entry) 567if match: 568return{ 569'src_mode': match.group(1), 570'dst_mode': match.group(2), 571'src_sha1': match.group(3), 572'dst_sha1': match.group(4), 573'status': match.group(5), 574'status_score': match.group(6), 575'src': match.group(7), 576'dst': match.group(10) 577} 578return None 579 580defisModeExec(mode): 581# Returns True if the given git mode represents an executable file, 582# otherwise False. 583return mode[-3:] =="755" 584 585classP4Exception(Exception): 586""" Base class for exceptions from the p4 client """ 587def__init__(self, exit_code): 588 self.p4ExitCode = exit_code 589 590classP4ServerException(P4Exception): 591""" Base class for exceptions where we get some kind of marshalled up result from the server """ 592def__init__(self, exit_code, p4_result): 593super(P4ServerException, self).__init__(exit_code) 594 self.p4_result = p4_result 595 self.code = p4_result[0]['code'] 596 self.data = p4_result[0]['data'] 597 598classP4RequestSizeException(P4ServerException): 599""" One of the maxresults or maxscanrows errors """ 600def__init__(self, exit_code, p4_result, limit): 601super(P4RequestSizeException, self).__init__(exit_code, p4_result) 602 self.limit = limit 603 604defisModeExecChanged(src_mode, dst_mode): 605returnisModeExec(src_mode) !=isModeExec(dst_mode) 606 607defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False, 608 errors_as_exceptions=False): 609 610ifisinstance(cmd,basestring): 611 cmd ="-G "+ cmd 612 expand =True 613else: 614 cmd = ["-G"] + cmd 615 expand =False 616 617 cmd =p4_build_cmd(cmd) 618if verbose: 619 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 620 621# Use a temporary file to avoid deadlocks without 622# subprocess.communicate(), which would put another copy 623# of stdout into memory. 624 stdin_file =None 625if stdin is not None: 626 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 627ifisinstance(stdin,basestring): 628 stdin_file.write(stdin) 629else: 630for i in stdin: 631 stdin_file.write(i +'\n') 632 stdin_file.flush() 633 stdin_file.seek(0) 634 635 p4 = subprocess.Popen(cmd, 636 shell=expand, 637 stdin=stdin_file, 638 stdout=subprocess.PIPE) 639 640 result = [] 641try: 642while True: 643 entry = marshal.load(p4.stdout) 644if skip_info: 645if'code'in entry and entry['code'] =='info': 646continue 647if cb is not None: 648cb(entry) 649else: 650 result.append(entry) 651exceptEOFError: 652pass 653 exitCode = p4.wait() 654if exitCode !=0: 655if errors_as_exceptions: 656iflen(result) >0: 657 data = result[0].get('data') 658if data: 659 m = re.search('Too many rows scanned \(over (\d+)\)', data) 660if not m: 661 m = re.search('Request too large \(over (\d+)\)', data) 662 663if m: 664 limit =int(m.group(1)) 665raiseP4RequestSizeException(exitCode, result, limit) 666 667raiseP4ServerException(exitCode, result) 668else: 669raiseP4Exception(exitCode) 670else: 671 entry = {} 672 entry["p4ExitCode"] = exitCode 673 result.append(entry) 674 675return result 676 677defp4Cmd(cmd): 678list=p4CmdList(cmd) 679 result = {} 680for entry inlist: 681 result.update(entry) 682return result; 683 684defp4Where(depotPath): 685if not depotPath.endswith("/"): 686 depotPath +="/" 687 depotPathLong = depotPath +"..." 688 outputList =p4CmdList(["where", depotPathLong]) 689 output =None 690for entry in outputList: 691if"depotFile"in entry: 692# Search for the base client side depot path, as long as it starts with the branch's P4 path. 693# The base path always ends with "/...". 694if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 695 output = entry 696break 697elif"data"in entry: 698 data = entry.get("data") 699 space = data.find(" ") 700if data[:space] == depotPath: 701 output = entry 702break 703if output ==None: 704return"" 705if output["code"] =="error": 706return"" 707 clientPath ="" 708if"path"in output: 709 clientPath = output.get("path") 710elif"data"in output: 711 data = output.get("data") 712 lastSpace = data.rfind(" ") 713 clientPath = data[lastSpace +1:] 714 715if clientPath.endswith("..."): 716 clientPath = clientPath[:-3] 717return clientPath 718 719defcurrentGitBranch(): 720returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 721 722defisValidGitDir(path): 723returngit_dir(path) !=None 724 725defparseRevision(ref): 726returnread_pipe("git rev-parse%s"% ref).strip() 727 728defbranchExists(ref): 729 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 730 ignore_error=True) 731returnlen(rev) >0 732 733defextractLogMessageFromGitCommit(commit): 734 logMessage ="" 735 736## fixme: title is first line of commit, not 1st paragraph. 737 foundTitle =False 738for log inread_pipe_lines("git cat-file commit%s"% commit): 739if not foundTitle: 740iflen(log) ==1: 741 foundTitle =True 742continue 743 744 logMessage += log 745return logMessage 746 747defextractSettingsGitLog(log): 748 values = {} 749for line in log.split("\n"): 750 line = line.strip() 751 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 752if not m: 753continue 754 755 assignments = m.group(1).split(':') 756for a in assignments: 757 vals = a.split('=') 758 key = vals[0].strip() 759 val = ('='.join(vals[1:])).strip() 760if val.endswith('\"')and val.startswith('"'): 761 val = val[1:-1] 762 763 values[key] = val 764 765 paths = values.get("depot-paths") 766if not paths: 767 paths = values.get("depot-path") 768if paths: 769 values['depot-paths'] = paths.split(',') 770return values 771 772defgitBranchExists(branch): 773 proc = subprocess.Popen(["git","rev-parse", branch], 774 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 775return proc.wait() ==0; 776 777defgitUpdateRef(ref, newvalue): 778 subprocess.check_call(["git","update-ref", ref, newvalue]) 779 780defgitDeleteRef(ref): 781 subprocess.check_call(["git","update-ref","-d", ref]) 782 783_gitConfig = {} 784 785defgitConfig(key, typeSpecifier=None): 786if key not in _gitConfig: 787 cmd = ["git","config"] 788if typeSpecifier: 789 cmd += [ typeSpecifier ] 790 cmd += [ key ] 791 s =read_pipe(cmd, ignore_error=True) 792 _gitConfig[key] = s.strip() 793return _gitConfig[key] 794 795defgitConfigBool(key): 796"""Return a bool, using git config --bool. It is True only if the 797 variable is set to true, and False if set to false or not present 798 in the config.""" 799 800if key not in _gitConfig: 801 _gitConfig[key] =gitConfig(key,'--bool') =="true" 802return _gitConfig[key] 803 804defgitConfigInt(key): 805if key not in _gitConfig: 806 cmd = ["git","config","--int", key ] 807 s =read_pipe(cmd, ignore_error=True) 808 v = s.strip() 809try: 810 _gitConfig[key] =int(gitConfig(key,'--int')) 811exceptValueError: 812 _gitConfig[key] =None 813return _gitConfig[key] 814 815defgitConfigList(key): 816if key not in _gitConfig: 817 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 818 _gitConfig[key] = s.strip().splitlines() 819if _gitConfig[key] == ['']: 820 _gitConfig[key] = [] 821return _gitConfig[key] 822 823defp4BranchesInGit(branchesAreInRemotes=True): 824"""Find all the branches whose names start with "p4/", looking 825 in remotes or heads as specified by the argument. Return 826 a dictionary of{ branch: revision }for each one found. 827 The branch names are the short names, without any 828 "p4/" prefix.""" 829 830 branches = {} 831 832 cmdline ="git rev-parse --symbolic " 833if branchesAreInRemotes: 834 cmdline +="--remotes" 835else: 836 cmdline +="--branches" 837 838for line inread_pipe_lines(cmdline): 839 line = line.strip() 840 841# only import to p4/ 842if not line.startswith('p4/'): 843continue 844# special symbolic ref to p4/master 845if line =="p4/HEAD": 846continue 847 848# strip off p4/ prefix 849 branch = line[len("p4/"):] 850 851 branches[branch] =parseRevision(line) 852 853return branches 854 855defbranch_exists(branch): 856"""Make sure that the given ref name really exists.""" 857 858 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 859 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 860 out, _ = p.communicate() 861if p.returncode: 862return False 863# expect exactly one line of output: the branch name 864return out.rstrip() == branch 865 866deffindUpstreamBranchPoint(head ="HEAD"): 867 branches =p4BranchesInGit() 868# map from depot-path to branch name 869 branchByDepotPath = {} 870for branch in branches.keys(): 871 tip = branches[branch] 872 log =extractLogMessageFromGitCommit(tip) 873 settings =extractSettingsGitLog(log) 874if"depot-paths"in settings: 875 paths =",".join(settings["depot-paths"]) 876 branchByDepotPath[paths] ="remotes/p4/"+ branch 877 878 settings =None 879 parent =0 880while parent <65535: 881 commit = head +"~%s"% parent 882 log =extractLogMessageFromGitCommit(commit) 883 settings =extractSettingsGitLog(log) 884if"depot-paths"in settings: 885 paths =",".join(settings["depot-paths"]) 886if paths in branchByDepotPath: 887return[branchByDepotPath[paths], settings] 888 889 parent = parent +1 890 891return["", settings] 892 893defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 894if not silent: 895print("Creating/updating branch(es) in%sbased on origin branch(es)" 896% localRefPrefix) 897 898 originPrefix ="origin/p4/" 899 900for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 901 line = line.strip() 902if(not line.startswith(originPrefix))or line.endswith("HEAD"): 903continue 904 905 headName = line[len(originPrefix):] 906 remoteHead = localRefPrefix + headName 907 originHead = line 908 909 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 910if('depot-paths'not in original 911or'change'not in original): 912continue 913 914 update =False 915if notgitBranchExists(remoteHead): 916if verbose: 917print("creating%s"% remoteHead) 918 update =True 919else: 920 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 921if'change'in settings: 922if settings['depot-paths'] == original['depot-paths']: 923 originP4Change =int(original['change']) 924 p4Change =int(settings['change']) 925if originP4Change > p4Change: 926print("%s(%s) is newer than%s(%s). " 927"Updating p4 branch from origin." 928% (originHead, originP4Change, 929 remoteHead, p4Change)) 930 update =True 931else: 932print("Ignoring:%swas imported from%swhile " 933"%swas imported from%s" 934% (originHead,','.join(original['depot-paths']), 935 remoteHead,','.join(settings['depot-paths']))) 936 937if update: 938system("git update-ref%s %s"% (remoteHead, originHead)) 939 940deforiginP4BranchesExist(): 941returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 942 943 944defp4ParseNumericChangeRange(parts): 945 changeStart =int(parts[0][1:]) 946if parts[1] =='#head': 947 changeEnd =p4_last_change() 948else: 949 changeEnd =int(parts[1]) 950 951return(changeStart, changeEnd) 952 953defchooseBlockSize(blockSize): 954if blockSize: 955return blockSize 956else: 957return defaultBlockSize 958 959defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 960assert depotPaths 961 962# Parse the change range into start and end. Try to find integer 963# revision ranges as these can be broken up into blocks to avoid 964# hitting server-side limits (maxrows, maxscanresults). But if 965# that doesn't work, fall back to using the raw revision specifier 966# strings, without using block mode. 967 968if changeRange is None or changeRange =='': 969 changeStart =1 970 changeEnd =p4_last_change() 971 block_size =chooseBlockSize(requestedBlockSize) 972else: 973 parts = changeRange.split(',') 974assertlen(parts) ==2 975try: 976(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 977 block_size =chooseBlockSize(requestedBlockSize) 978exceptValueError: 979 changeStart = parts[0][1:] 980 changeEnd = parts[1] 981if requestedBlockSize: 982die("cannot use --changes-block-size with non-numeric revisions") 983 block_size =None 984 985 changes =set() 986 987# Retrieve changes a block at a time, to prevent running 988# into a MaxResults/MaxScanRows error from the server. If 989# we _do_ hit one of those errors, turn down the block size 990 991while True: 992 cmd = ['changes'] 993 994if block_size: 995 end =min(changeEnd, changeStart + block_size) 996 revisionRange ="%d,%d"% (changeStart, end) 997else: 998 revisionRange ="%s,%s"% (changeStart, changeEnd) 9991000for p in depotPaths:1001 cmd += ["%s...@%s"% (p, revisionRange)]10021003# fetch the changes1004try:1005 result =p4CmdList(cmd, errors_as_exceptions=True)1006except P4RequestSizeException as e:1007if not block_size:1008 block_size = e.limit1009elif block_size > e.limit:1010 block_size = e.limit1011else:1012 block_size =max(2, block_size //2)10131014if verbose:print("block size error, retrying with block size{0}".format(block_size))1015continue1016except P4Exception as e:1017die('Error retrieving changes description ({0})'.format(e.p4ExitCode))10181019# Insert changes in chronological order1020for entry inreversed(result):1021if'change'not in entry:1022continue1023 changes.add(int(entry['change']))10241025if not block_size:1026break10271028if end >= changeEnd:1029break10301031 changeStart = end +110321033 changes =sorted(changes)1034return changes10351036defp4PathStartsWith(path, prefix):1037# This method tries to remedy a potential mixed-case issue:1038#1039# If UserA adds //depot/DirA/file11040# and UserB adds //depot/dira/file21041#1042# we may or may not have a problem. If you have core.ignorecase=true,1043# we treat DirA and dira as the same directory1044ifgitConfigBool("core.ignorecase"):1045return path.lower().startswith(prefix.lower())1046return path.startswith(prefix)10471048defgetClientSpec():1049"""Look at the p4 client spec, create a View() object that contains1050 all the mappings, and return it."""10511052 specList =p4CmdList("client -o")1053iflen(specList) !=1:1054die('Output from "client -o" is%dlines, expecting 1'%1055len(specList))10561057# dictionary of all client parameters1058 entry = specList[0]10591060# the //client/ name1061 client_name = entry["Client"]10621063# just the keys that start with "View"1064 view_keys = [ k for k in entry.keys()if k.startswith("View") ]10651066# hold this new View1067 view =View(client_name)10681069# append the lines, in order, to the view1070for view_num inrange(len(view_keys)):1071 k ="View%d"% view_num1072if k not in view_keys:1073die("Expected view key%smissing"% k)1074 view.append(entry[k])10751076return view10771078defgetClientRoot():1079"""Grab the client directory."""10801081 output =p4CmdList("client -o")1082iflen(output) !=1:1083die('Output from "client -o" is%dlines, expecting 1'%len(output))10841085 entry = output[0]1086if"Root"not in entry:1087die('Client has no "Root"')10881089return entry["Root"]10901091#1092# P4 wildcards are not allowed in filenames. P4 complains1093# if you simply add them, but you can force it with "-f", in1094# which case it translates them into %xx encoding internally.1095#1096defwildcard_decode(path):1097# Search for and fix just these four characters. Do % last so1098# that fixing it does not inadvertently create new %-escapes.1099# Cannot have * in a filename in windows; untested as to1100# what p4 would do in such a case.1101if not platform.system() =="Windows":1102 path = path.replace("%2A","*")1103 path = path.replace("%23","#") \1104.replace("%40","@") \1105.replace("%25","%")1106return path11071108defwildcard_encode(path):1109# do % first to avoid double-encoding the %s introduced here1110 path = path.replace("%","%25") \1111.replace("*","%2A") \1112.replace("#","%23") \1113.replace("@","%40")1114return path11151116defwildcard_present(path):1117 m = re.search("[*#@%]", path)1118return m is not None11191120classLargeFileSystem(object):1121"""Base class for large file system support."""11221123def__init__(self, writeToGitStream):1124 self.largeFiles =set()1125 self.writeToGitStream = writeToGitStream11261127defgeneratePointer(self, cloneDestination, contentFile):1128"""Return the content of a pointer file that is stored in Git instead of1129 the actual content."""1130assert False,"Method 'generatePointer' required in "+ self.__class__.__name__11311132defpushFile(self, localLargeFile):1133"""Push the actual content which is not stored in the Git repository to1134 a server."""1135assert False,"Method 'pushFile' required in "+ self.__class__.__name__11361137defhasLargeFileExtension(self, relPath):1138returnreduce(1139lambda a, b: a or b,1140[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1141False1142)11431144defgenerateTempFile(self, contents):1145 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1146for d in contents:1147 contentFile.write(d)1148 contentFile.close()1149return contentFile.name11501151defexceedsLargeFileThreshold(self, relPath, contents):1152ifgitConfigInt('git-p4.largeFileThreshold'):1153 contentsSize =sum(len(d)for d in contents)1154if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1155return True1156ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1157 contentsSize =sum(len(d)for d in contents)1158if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1159return False1160 contentTempFile = self.generateTempFile(contents)1161 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1162 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1163 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1164 zf.close()1165 compressedContentsSize = zf.infolist()[0].compress_size1166 os.remove(contentTempFile)1167 os.remove(compressedContentFile.name)1168if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1169return True1170return False11711172defaddLargeFile(self, relPath):1173 self.largeFiles.add(relPath)11741175defremoveLargeFile(self, relPath):1176 self.largeFiles.remove(relPath)11771178defisLargeFile(self, relPath):1179return relPath in self.largeFiles11801181defprocessContent(self, git_mode, relPath, contents):1182"""Processes the content of git fast import. This method decides if a1183 file is stored in the large file system and handles all necessary1184 steps."""1185if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1186 contentTempFile = self.generateTempFile(contents)1187(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1188if pointer_git_mode:1189 git_mode = pointer_git_mode1190if localLargeFile:1191# Move temp file to final location in large file system1192 largeFileDir = os.path.dirname(localLargeFile)1193if not os.path.isdir(largeFileDir):1194 os.makedirs(largeFileDir)1195 shutil.move(contentTempFile, localLargeFile)1196 self.addLargeFile(relPath)1197ifgitConfigBool('git-p4.largeFilePush'):1198 self.pushFile(localLargeFile)1199if verbose:1200 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1201return(git_mode, contents)12021203classMockLFS(LargeFileSystem):1204"""Mock large file system for testing."""12051206defgeneratePointer(self, contentFile):1207"""The pointer content is the original content prefixed with "pointer-".1208 The local filename of the large file storage is derived from the file content.1209 """1210withopen(contentFile,'r')as f:1211 content =next(f)1212 gitMode ='100644'1213 pointerContents ='pointer-'+ content1214 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1215return(gitMode, pointerContents, localLargeFile)12161217defpushFile(self, localLargeFile):1218"""The remote filename of the large file storage is the same as the local1219 one but in a different directory.1220 """1221 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1222if not os.path.exists(remotePath):1223 os.makedirs(remotePath)1224 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))12251226classGitLFS(LargeFileSystem):1227"""Git LFS as backend for the git-p4 large file system.1228 See https://git-lfs.github.com/ for details."""12291230def__init__(self, *args):1231 LargeFileSystem.__init__(self, *args)1232 self.baseGitAttributes = []12331234defgeneratePointer(self, contentFile):1235"""Generate a Git LFS pointer for the content. Return LFS Pointer file1236 mode and content which is stored in the Git repository instead of1237 the actual content. Return also the new location of the actual1238 content.1239 """1240if os.path.getsize(contentFile) ==0:1241return(None,'',None)12421243 pointerProcess = subprocess.Popen(1244['git','lfs','pointer','--file='+ contentFile],1245 stdout=subprocess.PIPE1246)1247 pointerFile = pointerProcess.stdout.read()1248if pointerProcess.wait():1249 os.remove(contentFile)1250die('git-lfs pointer command failed. Did you install the extension?')12511252# Git LFS removed the preamble in the output of the 'pointer' command1253# starting from version 1.2.0. Check for the preamble here to support1254# earlier versions.1255# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431256if pointerFile.startswith('Git LFS pointer for'):1257 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)12581259 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1260 localLargeFile = os.path.join(1261 os.getcwd(),1262'.git','lfs','objects', oid[:2], oid[2:4],1263 oid,1264)1265# LFS Spec states that pointer files should not have the executable bit set.1266 gitMode ='100644'1267return(gitMode, pointerFile, localLargeFile)12681269defpushFile(self, localLargeFile):1270 uploadProcess = subprocess.Popen(1271['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1272)1273if uploadProcess.wait():1274die('git-lfs push command failed. Did you define a remote?')12751276defgenerateGitAttributes(self):1277return(1278 self.baseGitAttributes +1279[1280'\n',1281'#\n',1282'# Git LFS (see https://git-lfs.github.com/)\n',1283'#\n',1284] +1285['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1286for f insorted(gitConfigList('git-p4.largeFileExtensions'))1287] +1288['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1289for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1290]1291)12921293defaddLargeFile(self, relPath):1294 LargeFileSystem.addLargeFile(self, relPath)1295 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12961297defremoveLargeFile(self, relPath):1298 LargeFileSystem.removeLargeFile(self, relPath)1299 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())13001301defprocessContent(self, git_mode, relPath, contents):1302if relPath =='.gitattributes':1303 self.baseGitAttributes = contents1304return(git_mode, self.generateGitAttributes())1305else:1306return LargeFileSystem.processContent(self, git_mode, relPath, contents)13071308class Command:1309def__init__(self):1310 self.usage ="usage: %prog [options]"1311 self.needsGit =True1312 self.verbose =False13131314# This is required for the "append" cloneExclude action1315defensure_value(self, attr, value):1316if nothasattr(self, attr)orgetattr(self, attr)is None:1317setattr(self, attr, value)1318returngetattr(self, attr)13191320class P4UserMap:1321def__init__(self):1322 self.userMapFromPerforceServer =False1323 self.myP4UserId =None13241325defp4UserId(self):1326if self.myP4UserId:1327return self.myP4UserId13281329 results =p4CmdList("user -o")1330for r in results:1331if'User'in r:1332 self.myP4UserId = r['User']1333return r['User']1334die("Could not find your p4 user id")13351336defp4UserIsMe(self, p4User):1337# return True if the given p4 user is actually me1338 me = self.p4UserId()1339if not p4User or p4User != me:1340return False1341else:1342return True13431344defgetUserCacheFilename(self):1345 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1346return home +"/.gitp4-usercache.txt"13471348defgetUserMapFromPerforceServer(self):1349if self.userMapFromPerforceServer:1350return1351 self.users = {}1352 self.emails = {}13531354for output inp4CmdList("users"):1355if"User"not in output:1356continue1357 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1358 self.emails[output["Email"]] = output["User"]13591360 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1361for mapUserConfig ingitConfigList("git-p4.mapUser"):1362 mapUser = mapUserConfigRegex.findall(mapUserConfig)1363if mapUser andlen(mapUser[0]) ==3:1364 user = mapUser[0][0]1365 fullname = mapUser[0][1]1366 email = mapUser[0][2]1367 self.users[user] = fullname +" <"+ email +">"1368 self.emails[email] = user13691370 s =''1371for(key, val)in self.users.items():1372 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))13731374open(self.getUserCacheFilename(),"wb").write(s)1375 self.userMapFromPerforceServer =True13761377defloadUserMapFromCache(self):1378 self.users = {}1379 self.userMapFromPerforceServer =False1380try:1381 cache =open(self.getUserCacheFilename(),"rb")1382 lines = cache.readlines()1383 cache.close()1384for line in lines:1385 entry = line.strip().split("\t")1386 self.users[entry[0]] = entry[1]1387exceptIOError:1388 self.getUserMapFromPerforceServer()13891390classP4Debug(Command):1391def__init__(self):1392 Command.__init__(self)1393 self.options = []1394 self.description ="A tool to debug the output of p4 -G."1395 self.needsGit =False13961397defrun(self, args):1398 j =01399for output inp4CmdList(args):1400print('Element:%d'% j)1401 j +=11402print(output)1403return True14041405classP4RollBack(Command):1406def__init__(self):1407 Command.__init__(self)1408 self.options = [1409 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1410]1411 self.description ="A tool to debug the multi-branch import. Don't use :)"1412 self.rollbackLocalBranches =False14131414defrun(self, args):1415iflen(args) !=1:1416return False1417 maxChange =int(args[0])14181419if"p4ExitCode"inp4Cmd("changes -m 1"):1420die("Problems executing p4");14211422if self.rollbackLocalBranches:1423 refPrefix ="refs/heads/"1424 lines =read_pipe_lines("git rev-parse --symbolic --branches")1425else:1426 refPrefix ="refs/remotes/"1427 lines =read_pipe_lines("git rev-parse --symbolic --remotes")14281429for line in lines:1430if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1431 line = line.strip()1432 ref = refPrefix + line1433 log =extractLogMessageFromGitCommit(ref)1434 settings =extractSettingsGitLog(log)14351436 depotPaths = settings['depot-paths']1437 change = settings['change']14381439 changed =False14401441iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1442for p in depotPaths]))) ==0:1443print("Branch%sdid not exist at change%s, deleting."% (ref, maxChange))1444system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1445continue14461447while change andint(change) > maxChange:1448 changed =True1449if self.verbose:1450print("%sis at%s; rewinding towards%s"% (ref, change, maxChange))1451system("git update-ref%s\"%s^\""% (ref, ref))1452 log =extractLogMessageFromGitCommit(ref)1453 settings =extractSettingsGitLog(log)145414551456 depotPaths = settings['depot-paths']1457 change = settings['change']14581459if changed:1460print("%srewound to%s"% (ref, change))14611462return True14631464classP4Submit(Command, P4UserMap):14651466 conflict_behavior_choices = ("ask","skip","quit")14671468def__init__(self):1469 Command.__init__(self)1470 P4UserMap.__init__(self)1471 self.options = [1472 optparse.make_option("--origin", dest="origin"),1473 optparse.make_option("-M", dest="detectRenames", action="store_true"),1474# preserve the user, requires relevant p4 permissions1475 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1476 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1477 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1478 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1479 optparse.make_option("--conflict", dest="conflict_behavior",1480 choices=self.conflict_behavior_choices),1481 optparse.make_option("--branch", dest="branch"),1482 optparse.make_option("--shelve", dest="shelve", action="store_true",1483help="Shelve instead of submit. Shelved files are reverted, "1484"restoring the workspace to the state before the shelve"),1485 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1486 metavar="CHANGELIST",1487help="update an existing shelved changelist, implies --shelve, "1488"repeat in-order for multiple shelved changelists"),1489 optparse.make_option("--commit", dest="commit", metavar="COMMIT",1490help="submit only the specified commit(s), one commit or xxx..xxx"),1491 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",1492help="Disable rebase after submit is completed. Can be useful if you "1493"work from a local git branch that is not master"),1494 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",1495help="Skip Perforce sync of p4/master after submit or shelve"),1496]1497 self.description ="""Submit changes from git to the perforce depot.\n1498 The `p4-pre-submit` hook is executed if it exists and is executable.1499 The hook takes no parameters and nothing from standard input. Exiting with1500 non-zero status from this script prevents `git-p4 submit` from launching.15011502 One usage scenario is to run unit tests in the hook."""15031504 self.usage +=" [name of git branch to submit into perforce depot]"1505 self.origin =""1506 self.detectRenames =False1507 self.preserveUser =gitConfigBool("git-p4.preserveUser")1508 self.dry_run =False1509 self.shelve =False1510 self.update_shelve =list()1511 self.commit =""1512 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1513 self.disable_p4sync =gitConfigBool("git-p4.disableP4Sync")1514 self.prepare_p4_only =False1515 self.conflict_behavior =None1516 self.isWindows = (platform.system() =="Windows")1517 self.exportLabels =False1518 self.p4HasMoveCommand =p4_has_move_command()1519 self.branch =None15201521ifgitConfig('git-p4.largeFileSystem'):1522die("Large file system not supported for git-p4 submit command. Please remove it from config.")15231524defcheck(self):1525iflen(p4CmdList("opened ...")) >0:1526die("You have files opened with perforce! Close them before starting the sync.")15271528defseparate_jobs_from_description(self, message):1529"""Extract and return a possible Jobs field in the commit1530 message. It goes into a separate section in the p4 change1531 specification.15321533 A jobs line starts with "Jobs:" and looks like a new field1534 in a form. Values are white-space separated on the same1535 line or on following lines that start with a tab.15361537 This does not parse and extract the full git commit message1538 like a p4 form. It just sees the Jobs: line as a marker1539 to pass everything from then on directly into the p4 form,1540 but outside the description section.15411542 Return a tuple (stripped log message, jobs string)."""15431544 m = re.search(r'^Jobs:', message, re.MULTILINE)1545if m is None:1546return(message,None)15471548 jobtext = message[m.start():]1549 stripped_message = message[:m.start()].rstrip()1550return(stripped_message, jobtext)15511552defprepareLogMessage(self, template, message, jobs):1553"""Edits the template returned from "p4 change -o" to insert1554 the message in the Description field, and the jobs text in1555 the Jobs field."""1556 result =""15571558 inDescriptionSection =False15591560for line in template.split("\n"):1561if line.startswith("#"):1562 result += line +"\n"1563continue15641565if inDescriptionSection:1566if line.startswith("Files:")or line.startswith("Jobs:"):1567 inDescriptionSection =False1568# insert Jobs section1569if jobs:1570 result += jobs +"\n"1571else:1572continue1573else:1574if line.startswith("Description:"):1575 inDescriptionSection =True1576 line +="\n"1577for messageLine in message.split("\n"):1578 line +="\t"+ messageLine +"\n"15791580 result += line +"\n"15811582return result15831584defpatchRCSKeywords(self,file, pattern):1585# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1586(handle, outFileName) = tempfile.mkstemp(dir='.')1587try:1588 outFile = os.fdopen(handle,"w+")1589 inFile =open(file,"r")1590 regexp = re.compile(pattern, re.VERBOSE)1591for line in inFile.readlines():1592 line = regexp.sub(r'$\1$', line)1593 outFile.write(line)1594 inFile.close()1595 outFile.close()1596# Forcibly overwrite the original file1597 os.unlink(file)1598 shutil.move(outFileName,file)1599except:1600# cleanup our temporary file1601 os.unlink(outFileName)1602print("Failed to strip RCS keywords in%s"%file)1603raise16041605print("Patched up RCS keywords in%s"%file)16061607defp4UserForCommit(self,id):1608# Return the tuple (perforce user,git email) for a given git commit id1609 self.getUserMapFromPerforceServer()1610 gitEmail =read_pipe(["git","log","--max-count=1",1611"--format=%ae",id])1612 gitEmail = gitEmail.strip()1613if gitEmail not in self.emails:1614return(None,gitEmail)1615else:1616return(self.emails[gitEmail],gitEmail)16171618defcheckValidP4Users(self,commits):1619# check if any git authors cannot be mapped to p4 users1620foridin commits:1621(user,email) = self.p4UserForCommit(id)1622if not user:1623 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1624ifgitConfigBool("git-p4.allowMissingP4Users"):1625print("%s"% msg)1626else:1627die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)16281629deflastP4Changelist(self):1630# Get back the last changelist number submitted in this client spec. This1631# then gets used to patch up the username in the change. If the same1632# client spec is being used by multiple processes then this might go1633# wrong.1634 results =p4CmdList("client -o")# find the current client1635 client =None1636for r in results:1637if'Client'in r:1638 client = r['Client']1639break1640if not client:1641die("could not get client spec")1642 results =p4CmdList(["changes","-c", client,"-m","1"])1643for r in results:1644if'change'in r:1645return r['change']1646die("Could not get changelist number for last submit - cannot patch up user details")16471648defmodifyChangelistUser(self, changelist, newUser):1649# fixup the user field of a changelist after it has been submitted.1650 changes =p4CmdList("change -o%s"% changelist)1651iflen(changes) !=1:1652die("Bad output from p4 change modifying%sto user%s"%1653(changelist, newUser))16541655 c = changes[0]1656if c['User'] == newUser:return# nothing to do1657 c['User'] = newUser1658input= marshal.dumps(c)16591660 result =p4CmdList("change -f -i", stdin=input)1661for r in result:1662if'code'in r:1663if r['code'] =='error':1664die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1665if'data'in r:1666print("Updated user field for changelist%sto%s"% (changelist, newUser))1667return1668die("Could not modify user field of changelist%sto%s"% (changelist, newUser))16691670defcanChangeChangelists(self):1671# check to see if we have p4 admin or super-user permissions, either of1672# which are required to modify changelists.1673 results =p4CmdList(["protects", self.depotPath])1674for r in results:1675if'perm'in r:1676if r['perm'] =='admin':1677return11678if r['perm'] =='super':1679return11680return016811682defprepareSubmitTemplate(self, changelist=None):1683"""Run "p4 change -o" to grab a change specification template.1684 This does not use "p4 -G", as it is nice to keep the submission1685 template in original order, since a human might edit it.16861687 Remove lines in the Files section that show changes to files1688 outside the depot path we're committing into."""16891690[upstream, settings] =findUpstreamBranchPoint()16911692 template ="""\1693# A Perforce Change Specification.1694#1695# Change: The change number. 'new' on a new changelist.1696# Date: The date this specification was last modified.1697# Client: The client on which the changelist was created. Read-only.1698# User: The user who created the changelist.1699# Status: Either 'pending' or 'submitted'. Read-only.1700# Type: Either 'public' or 'restricted'. Default is 'public'.1701# Description: Comments about the changelist. Required.1702# Jobs: What opened jobs are to be closed by this changelist.1703# You may delete jobs from this list. (New changelists only.)1704# Files: What opened files from the default changelist are to be added1705# to this changelist. You may delete files from this list.1706# (New changelists only.)1707"""1708 files_list = []1709 inFilesSection =False1710 change_entry =None1711 args = ['change','-o']1712if changelist:1713 args.append(str(changelist))1714for entry inp4CmdList(args):1715if'code'not in entry:1716continue1717if entry['code'] =='stat':1718 change_entry = entry1719break1720if not change_entry:1721die('Failed to decode output of p4 change -o')1722for key, value in change_entry.iteritems():1723if key.startswith('File'):1724if'depot-paths'in settings:1725if not[p for p in settings['depot-paths']1726ifp4PathStartsWith(value, p)]:1727continue1728else:1729if notp4PathStartsWith(value, self.depotPath):1730continue1731 files_list.append(value)1732continue1733# Output in the order expected by prepareLogMessage1734for key in['Change','Client','User','Status','Description','Jobs']:1735if key not in change_entry:1736continue1737 template +='\n'1738 template += key +':'1739if key =='Description':1740 template +='\n'1741for field_line in change_entry[key].splitlines():1742 template +='\t'+field_line+'\n'1743iflen(files_list) >0:1744 template +='\n'1745 template +='Files:\n'1746for path in files_list:1747 template +='\t'+path+'\n'1748return template17491750defedit_template(self, template_file):1751"""Invoke the editor to let the user change the submission1752 message. Return true if okay to continue with the submit."""17531754# if configured to skip the editing part, just submit1755ifgitConfigBool("git-p4.skipSubmitEdit"):1756return True17571758# look at the modification time, to check later if the user saved1759# the file1760 mtime = os.stat(template_file).st_mtime17611762# invoke the editor1763if"P4EDITOR"in os.environ and(os.environ.get("P4EDITOR") !=""):1764 editor = os.environ.get("P4EDITOR")1765else:1766 editor =read_pipe("git var GIT_EDITOR").strip()1767system(["sh","-c", ('%s"$@"'% editor), editor, template_file])17681769# If the file was not saved, prompt to see if this patch should1770# be skipped. But skip this verification step if configured so.1771ifgitConfigBool("git-p4.skipSubmitEditCheck"):1772return True17731774# modification time updated means user saved the file1775if os.stat(template_file).st_mtime > mtime:1776return True17771778while True:1779 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1780if response =='y':1781return True1782if response =='n':1783return False17841785defget_diff_description(self, editedFiles, filesToAdd, symlinks):1786# diff1787if"P4DIFF"in os.environ:1788del(os.environ["P4DIFF"])1789 diff =""1790for editedFile in editedFiles:1791 diff +=p4_read_pipe(['diff','-du',1792wildcard_encode(editedFile)])17931794# new file diff1795 newdiff =""1796for newFile in filesToAdd:1797 newdiff +="==== new file ====\n"1798 newdiff +="--- /dev/null\n"1799 newdiff +="+++%s\n"% newFile18001801 is_link = os.path.islink(newFile)1802 expect_link = newFile in symlinks18031804if is_link and expect_link:1805 newdiff +="+%s\n"% os.readlink(newFile)1806else:1807 f =open(newFile,"r")1808for line in f.readlines():1809 newdiff +="+"+ line1810 f.close()18111812return(diff + newdiff).replace('\r\n','\n')18131814defapplyCommit(self,id):1815"""Apply one commit, return True if it succeeded."""18161817print("Applying",read_pipe(["git","show","-s",1818"--format=format:%h%s",id]))18191820(p4User, gitEmail) = self.p4UserForCommit(id)18211822 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1823 filesToAdd =set()1824 filesToChangeType =set()1825 filesToDelete =set()1826 editedFiles =set()1827 pureRenameCopy =set()1828 symlinks =set()1829 filesToChangeExecBit = {}1830 all_files =list()18311832for line in diff:1833 diff =parseDiffTreeEntry(line)1834 modifier = diff['status']1835 path = diff['src']1836 all_files.append(path)18371838if modifier =="M":1839p4_edit(path)1840ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1841 filesToChangeExecBit[path] = diff['dst_mode']1842 editedFiles.add(path)1843elif modifier =="A":1844 filesToAdd.add(path)1845 filesToChangeExecBit[path] = diff['dst_mode']1846if path in filesToDelete:1847 filesToDelete.remove(path)18481849 dst_mode =int(diff['dst_mode'],8)1850if dst_mode ==0o120000:1851 symlinks.add(path)18521853elif modifier =="D":1854 filesToDelete.add(path)1855if path in filesToAdd:1856 filesToAdd.remove(path)1857elif modifier =="C":1858 src, dest = diff['src'], diff['dst']1859p4_integrate(src, dest)1860 pureRenameCopy.add(dest)1861if diff['src_sha1'] != diff['dst_sha1']:1862p4_edit(dest)1863 pureRenameCopy.discard(dest)1864ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1865p4_edit(dest)1866 pureRenameCopy.discard(dest)1867 filesToChangeExecBit[dest] = diff['dst_mode']1868if self.isWindows:1869# turn off read-only attribute1870 os.chmod(dest, stat.S_IWRITE)1871 os.unlink(dest)1872 editedFiles.add(dest)1873elif modifier =="R":1874 src, dest = diff['src'], diff['dst']1875if self.p4HasMoveCommand:1876p4_edit(src)# src must be open before move1877p4_move(src, dest)# opens for (move/delete, move/add)1878else:1879p4_integrate(src, dest)1880if diff['src_sha1'] != diff['dst_sha1']:1881p4_edit(dest)1882else:1883 pureRenameCopy.add(dest)1884ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1885if not self.p4HasMoveCommand:1886p4_edit(dest)# with move: already open, writable1887 filesToChangeExecBit[dest] = diff['dst_mode']1888if not self.p4HasMoveCommand:1889if self.isWindows:1890 os.chmod(dest, stat.S_IWRITE)1891 os.unlink(dest)1892 filesToDelete.add(src)1893 editedFiles.add(dest)1894elif modifier =="T":1895 filesToChangeType.add(path)1896else:1897die("unknown modifier%sfor%s"% (modifier, path))18981899 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1900 patchcmd = diffcmd +" | git apply "1901 tryPatchCmd = patchcmd +"--check -"1902 applyPatchCmd = patchcmd +"--check --apply -"1903 patch_succeeded =True19041905if os.system(tryPatchCmd) !=0:1906 fixed_rcs_keywords =False1907 patch_succeeded =False1908print("Unfortunately applying the change failed!")19091910# Patch failed, maybe it's just RCS keyword woes. Look through1911# the patch to see if that's possible.1912ifgitConfigBool("git-p4.attemptRCSCleanup"):1913file=None1914 pattern =None1915 kwfiles = {}1916forfilein editedFiles | filesToDelete:1917# did this file's delta contain RCS keywords?1918 pattern =p4_keywords_regexp_for_file(file)19191920if pattern:1921# this file is a possibility...look for RCS keywords.1922 regexp = re.compile(pattern, re.VERBOSE)1923for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1924if regexp.search(line):1925if verbose:1926print("got keyword match on%sin%sin%s"% (pattern, line,file))1927 kwfiles[file] = pattern1928break19291930forfilein kwfiles:1931if verbose:1932print("zapping%swith%s"% (line,pattern))1933# File is being deleted, so not open in p4. Must1934# disable the read-only bit on windows.1935if self.isWindows andfilenot in editedFiles:1936 os.chmod(file, stat.S_IWRITE)1937 self.patchRCSKeywords(file, kwfiles[file])1938 fixed_rcs_keywords =True19391940if fixed_rcs_keywords:1941print("Retrying the patch with RCS keywords cleaned up")1942if os.system(tryPatchCmd) ==0:1943 patch_succeeded =True19441945if not patch_succeeded:1946for f in editedFiles:1947p4_revert(f)1948return False19491950#1951# Apply the patch for real, and do add/delete/+x handling.1952#1953system(applyPatchCmd)19541955for f in filesToChangeType:1956p4_edit(f,"-t","auto")1957for f in filesToAdd:1958p4_add(f)1959for f in filesToDelete:1960p4_revert(f)1961p4_delete(f)19621963# Set/clear executable bits1964for f in filesToChangeExecBit.keys():1965 mode = filesToChangeExecBit[f]1966setP4ExecBit(f, mode)19671968 update_shelve =01969iflen(self.update_shelve) >0:1970 update_shelve = self.update_shelve.pop(0)1971p4_reopen_in_change(update_shelve, all_files)19721973#1974# Build p4 change description, starting with the contents1975# of the git commit message.1976#1977 logMessage =extractLogMessageFromGitCommit(id)1978 logMessage = logMessage.strip()1979(logMessage, jobs) = self.separate_jobs_from_description(logMessage)19801981 template = self.prepareSubmitTemplate(update_shelve)1982 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)19831984if self.preserveUser:1985 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User19861987if self.checkAuthorship and not self.p4UserIsMe(p4User):1988 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1989 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1990 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"19911992 separatorLine ="######## everything below this line is just the diff #######\n"1993if not self.prepare_p4_only:1994 submitTemplate += separatorLine1995 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)19961997(handle, fileName) = tempfile.mkstemp()1998 tmpFile = os.fdopen(handle,"w+b")1999if self.isWindows:2000 submitTemplate = submitTemplate.replace("\n","\r\n")2001 tmpFile.write(submitTemplate)2002 tmpFile.close()20032004if self.prepare_p4_only:2005#2006# Leave the p4 tree prepared, and the submit template around2007# and let the user decide what to do next2008#2009print()2010print("P4 workspace prepared for submission.")2011print("To submit or revert, go to client workspace")2012print(" "+ self.clientPath)2013print()2014print("To submit, use\"p4 submit\"to write a new description,")2015print("or\"p4 submit -i <%s\"to use the one prepared by" \2016"\"git p4\"."% fileName)2017print("You can delete the file\"%s\"when finished."% fileName)20182019if self.preserveUser and p4User and not self.p4UserIsMe(p4User):2020print("To preserve change ownership by user%s, you must\n" \2021"do\"p4 change -f <change>\"after submitting and\n" \2022"edit the User field.")2023if pureRenameCopy:2024print("After submitting, renamed files must be re-synced.")2025print("Invoke\"p4 sync -f\"on each of these files:")2026for f in pureRenameCopy:2027print(" "+ f)20282029print()2030print("To revert the changes, use\"p4 revert ...\", and delete")2031print("the submit template file\"%s\""% fileName)2032if filesToAdd:2033print("Since the commit adds new files, they must be deleted:")2034for f in filesToAdd:2035print(" "+ f)2036print()2037return True20382039#2040# Let the user edit the change description, then submit it.2041#2042 submitted =False20432044try:2045if self.edit_template(fileName):2046# read the edited message and submit2047 tmpFile =open(fileName,"rb")2048 message = tmpFile.read()2049 tmpFile.close()2050if self.isWindows:2051 message = message.replace("\r\n","\n")2052 submitTemplate = message[:message.index(separatorLine)]20532054if update_shelve:2055p4_write_pipe(['shelve','-r','-i'], submitTemplate)2056elif self.shelve:2057p4_write_pipe(['shelve','-i'], submitTemplate)2058else:2059p4_write_pipe(['submit','-i'], submitTemplate)2060# The rename/copy happened by applying a patch that created a2061# new file. This leaves it writable, which confuses p4.2062for f in pureRenameCopy:2063p4_sync(f,"-f")20642065if self.preserveUser:2066if p4User:2067# Get last changelist number. Cannot easily get it from2068# the submit command output as the output is2069# unmarshalled.2070 changelist = self.lastP4Changelist()2071 self.modifyChangelistUser(changelist, p4User)20722073 submitted =True20742075finally:2076# skip this patch2077if not submitted or self.shelve:2078if self.shelve:2079print("Reverting shelved files.")2080else:2081print("Submission cancelled, undoing p4 changes.")2082for f in editedFiles | filesToDelete:2083p4_revert(f)2084for f in filesToAdd:2085p4_revert(f)2086 os.remove(f)20872088 os.remove(fileName)2089return submitted20902091# Export git tags as p4 labels. Create a p4 label and then tag2092# with that.2093defexportGitTags(self, gitTags):2094 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")2095iflen(validLabelRegexp) ==0:2096 validLabelRegexp = defaultLabelRegexp2097 m = re.compile(validLabelRegexp)20982099for name in gitTags:21002101if not m.match(name):2102if verbose:2103print("tag%sdoes not match regexp%s"% (name, validLabelRegexp))2104continue21052106# Get the p4 commit this corresponds to2107 logMessage =extractLogMessageFromGitCommit(name)2108 values =extractSettingsGitLog(logMessage)21092110if'change'not in values:2111# a tag pointing to something not sent to p4; ignore2112if verbose:2113print("git tag%sdoes not give a p4 commit"% name)2114continue2115else:2116 changelist = values['change']21172118# Get the tag details.2119 inHeader =True2120 isAnnotated =False2121 body = []2122for l inread_pipe_lines(["git","cat-file","-p", name]):2123 l = l.strip()2124if inHeader:2125if re.match(r'tag\s+', l):2126 isAnnotated =True2127elif re.match(r'\s*$', l):2128 inHeader =False2129continue2130else:2131 body.append(l)21322133if not isAnnotated:2134 body = ["lightweight tag imported by git p4\n"]21352136# Create the label - use the same view as the client spec we are using2137 clientSpec =getClientSpec()21382139 labelTemplate ="Label:%s\n"% name2140 labelTemplate +="Description:\n"2141for b in body:2142 labelTemplate +="\t"+ b +"\n"2143 labelTemplate +="View:\n"2144for depot_side in clientSpec.mappings:2145 labelTemplate +="\t%s\n"% depot_side21462147if self.dry_run:2148print("Would create p4 label%sfor tag"% name)2149elif self.prepare_p4_only:2150print("Not creating p4 label%sfor tag due to option" \2151" --prepare-p4-only"% name)2152else:2153p4_write_pipe(["label","-i"], labelTemplate)21542155# Use the label2156p4_system(["tag","-l", name] +2157["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])21582159if verbose:2160print("created p4 label for tag%s"% name)21612162defrun(self, args):2163iflen(args) ==0:2164 self.master =currentGitBranch()2165eliflen(args) ==1:2166 self.master = args[0]2167if notbranchExists(self.master):2168die("Branch%sdoes not exist"% self.master)2169else:2170return False21712172for i in self.update_shelve:2173if i <=0:2174 sys.exit("invalid changelist%d"% i)21752176if self.master:2177 allowSubmit =gitConfig("git-p4.allowSubmit")2178iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2179die("%sis not in git-p4.allowSubmit"% self.master)21802181[upstream, settings] =findUpstreamBranchPoint()2182 self.depotPath = settings['depot-paths'][0]2183iflen(self.origin) ==0:2184 self.origin = upstream21852186iflen(self.update_shelve) >0:2187 self.shelve =True21882189if self.preserveUser:2190if not self.canChangeChangelists():2191die("Cannot preserve user names without p4 super-user or admin permissions")21922193# if not set from the command line, try the config file2194if self.conflict_behavior is None:2195 val =gitConfig("git-p4.conflict")2196if val:2197if val not in self.conflict_behavior_choices:2198die("Invalid value '%s' for config git-p4.conflict"% val)2199else:2200 val ="ask"2201 self.conflict_behavior = val22022203if self.verbose:2204print("Origin branch is "+ self.origin)22052206iflen(self.depotPath) ==0:2207print("Internal error: cannot locate perforce depot path from existing branches")2208 sys.exit(128)22092210 self.useClientSpec =False2211ifgitConfigBool("git-p4.useclientspec"):2212 self.useClientSpec =True2213if self.useClientSpec:2214 self.clientSpecDirs =getClientSpec()22152216# Check for the existence of P4 branches2217 branchesDetected = (len(p4BranchesInGit().keys()) >1)22182219if self.useClientSpec and not branchesDetected:2220# all files are relative to the client spec2221 self.clientPath =getClientRoot()2222else:2223 self.clientPath =p4Where(self.depotPath)22242225if self.clientPath =="":2226die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)22272228print("Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath))2229 self.oldWorkingDirectory = os.getcwd()22302231# ensure the clientPath exists2232 new_client_dir =False2233if not os.path.exists(self.clientPath):2234 new_client_dir =True2235 os.makedirs(self.clientPath)22362237chdir(self.clientPath, is_client_path=True)2238if self.dry_run:2239print("Would synchronize p4 checkout in%s"% self.clientPath)2240else:2241print("Synchronizing p4 checkout...")2242if new_client_dir:2243# old one was destroyed, and maybe nobody told p42244p4_sync("...","-f")2245else:2246p4_sync("...")2247 self.check()22482249 commits = []2250if self.master:2251 committish = self.master2252else:2253 committish ='HEAD'22542255if self.commit !="":2256if self.commit.find("..") != -1:2257 limits_ish = self.commit.split("..")2258for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2259 commits.append(line.strip())2260 commits.reverse()2261else:2262 commits.append(self.commit)2263else:2264for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, committish)]):2265 commits.append(line.strip())2266 commits.reverse()22672268if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2269 self.checkAuthorship =False2270else:2271 self.checkAuthorship =True22722273if self.preserveUser:2274 self.checkValidP4Users(commits)22752276#2277# Build up a set of options to be passed to diff when2278# submitting each commit to p4.2279#2280if self.detectRenames:2281# command-line -M arg2282 self.diffOpts ="-M"2283else:2284# If not explicitly set check the config variable2285 detectRenames =gitConfig("git-p4.detectRenames")22862287if detectRenames.lower() =="false"or detectRenames =="":2288 self.diffOpts =""2289elif detectRenames.lower() =="true":2290 self.diffOpts ="-M"2291else:2292 self.diffOpts ="-M%s"% detectRenames22932294# no command-line arg for -C or --find-copies-harder, just2295# config variables2296 detectCopies =gitConfig("git-p4.detectCopies")2297if detectCopies.lower() =="false"or detectCopies =="":2298pass2299elif detectCopies.lower() =="true":2300 self.diffOpts +=" -C"2301else:2302 self.diffOpts +=" -C%s"% detectCopies23032304ifgitConfigBool("git-p4.detectCopiesHarder"):2305 self.diffOpts +=" --find-copies-harder"23062307 num_shelves =len(self.update_shelve)2308if num_shelves >0and num_shelves !=len(commits):2309 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2310(len(commits), num_shelves))23112312 hooks_path =gitConfig("core.hooksPath")2313iflen(hooks_path) <=0:2314 hooks_path = os.path.join(os.environ.get("GIT_DIR",".git"),"hooks")23152316 hook_file = os.path.join(hooks_path,"p4-pre-submit")2317if os.path.isfile(hook_file)and os.access(hook_file, os.X_OK)and subprocess.call([hook_file]) !=0:2318 sys.exit(1)23192320#2321# Apply the commits, one at a time. On failure, ask if should2322# continue to try the rest of the patches, or quit.2323#2324if self.dry_run:2325print("Would apply")2326 applied = []2327 last =len(commits) -12328for i, commit inenumerate(commits):2329if self.dry_run:2330print(" ",read_pipe(["git","show","-s",2331"--format=format:%h%s", commit]))2332 ok =True2333else:2334 ok = self.applyCommit(commit)2335if ok:2336 applied.append(commit)2337else:2338if self.prepare_p4_only and i < last:2339print("Processing only the first commit due to option" \2340" --prepare-p4-only")2341break2342if i < last:2343 quit =False2344while True:2345# prompt for what to do, or use the option/variable2346if self.conflict_behavior =="ask":2347print("What do you want to do?")2348 response =raw_input("[s]kip this commit but apply"2349" the rest, or [q]uit? ")2350if not response:2351continue2352elif self.conflict_behavior =="skip":2353 response ="s"2354elif self.conflict_behavior =="quit":2355 response ="q"2356else:2357die("Unknown conflict_behavior '%s'"%2358 self.conflict_behavior)23592360if response[0] =="s":2361print("Skipping this commit, but applying the rest")2362break2363if response[0] =="q":2364print("Quitting")2365 quit =True2366break2367if quit:2368break23692370chdir(self.oldWorkingDirectory)2371 shelved_applied ="shelved"if self.shelve else"applied"2372if self.dry_run:2373pass2374elif self.prepare_p4_only:2375pass2376eliflen(commits) ==len(applied):2377print("All commits{0}!".format(shelved_applied))23782379 sync =P4Sync()2380if self.branch:2381 sync.branch = self.branch2382if self.disable_p4sync:2383 sync.sync_origin_only()2384else:2385 sync.run([])23862387if not self.disable_rebase:2388 rebase =P4Rebase()2389 rebase.rebase()23902391else:2392iflen(applied) ==0:2393print("No commits{0}.".format(shelved_applied))2394else:2395print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2396for c in commits:2397if c in applied:2398 star ="*"2399else:2400 star =" "2401print(star,read_pipe(["git","show","-s",2402"--format=format:%h%s", c]))2403print("You will have to do 'git p4 sync' and rebase.")24042405ifgitConfigBool("git-p4.exportLabels"):2406 self.exportLabels =True24072408if self.exportLabels:2409 p4Labels =getP4Labels(self.depotPath)2410 gitTags =getGitTags()24112412 missingGitTags = gitTags - p4Labels2413 self.exportGitTags(missingGitTags)24142415# exit with error unless everything applied perfectly2416iflen(commits) !=len(applied):2417 sys.exit(1)24182419return True24202421classView(object):2422"""Represent a p4 view ("p4 help views"), and map files in a2423 repo according to the view."""24242425def__init__(self, client_name):2426 self.mappings = []2427 self.client_prefix ="//%s/"% client_name2428# cache results of "p4 where" to lookup client file locations2429 self.client_spec_path_cache = {}24302431defappend(self, view_line):2432"""Parse a view line, splitting it into depot and client2433 sides. Append to self.mappings, preserving order. This2434 is only needed for tag creation."""24352436# Split the view line into exactly two words. P4 enforces2437# structure on these lines that simplifies this quite a bit.2438#2439# Either or both words may be double-quoted.2440# Single quotes do not matter.2441# Double-quote marks cannot occur inside the words.2442# A + or - prefix is also inside the quotes.2443# There are no quotes unless they contain a space.2444# The line is already white-space stripped.2445# The two words are separated by a single space.2446#2447if view_line[0] =='"':2448# First word is double quoted. Find its end.2449 close_quote_index = view_line.find('"',1)2450if close_quote_index <=0:2451die("No first-word closing quote found:%s"% view_line)2452 depot_side = view_line[1:close_quote_index]2453# skip closing quote and space2454 rhs_index = close_quote_index +1+12455else:2456 space_index = view_line.find(" ")2457if space_index <=0:2458die("No word-splitting space found:%s"% view_line)2459 depot_side = view_line[0:space_index]2460 rhs_index = space_index +124612462# prefix + means overlay on previous mapping2463if depot_side.startswith("+"):2464 depot_side = depot_side[1:]24652466# prefix - means exclude this path, leave out of mappings2467 exclude =False2468if depot_side.startswith("-"):2469 exclude =True2470 depot_side = depot_side[1:]24712472if not exclude:2473 self.mappings.append(depot_side)24742475defconvert_client_path(self, clientFile):2476# chop off //client/ part to make it relative2477if not clientFile.startswith(self.client_prefix):2478die("No prefix '%s' on clientFile '%s'"%2479(self.client_prefix, clientFile))2480return clientFile[len(self.client_prefix):]24812482defupdate_client_spec_path_cache(self, files):2483""" Caching file paths by "p4 where" batch query """24842485# List depot file paths exclude that already cached2486 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]24872488iflen(fileArgs) ==0:2489return# All files in cache24902491 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2492for res in where_result:2493if"code"in res and res["code"] =="error":2494# assume error is "... file(s) not in client view"2495continue2496if"clientFile"not in res:2497die("No clientFile in 'p4 where' output")2498if"unmap"in res:2499# it will list all of them, but only one not unmap-ped2500continue2501ifgitConfigBool("core.ignorecase"):2502 res['depotFile'] = res['depotFile'].lower()2503 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])25042505# not found files or unmap files set to ""2506for depotFile in fileArgs:2507ifgitConfigBool("core.ignorecase"):2508 depotFile = depotFile.lower()2509if depotFile not in self.client_spec_path_cache:2510 self.client_spec_path_cache[depotFile] =""25112512defmap_in_client(self, depot_path):2513"""Return the relative location in the client where this2514 depot file should live. Returns "" if the file should2515 not be mapped in the client."""25162517ifgitConfigBool("core.ignorecase"):2518 depot_path = depot_path.lower()25192520if depot_path in self.client_spec_path_cache:2521return self.client_spec_path_cache[depot_path]25222523die("Error:%sis not found in client spec path"% depot_path )2524return""25252526classP4Sync(Command, P4UserMap):2527 delete_actions = ("delete","move/delete","purge")25282529def__init__(self):2530 Command.__init__(self)2531 P4UserMap.__init__(self)2532 self.options = [2533 optparse.make_option("--branch", dest="branch"),2534 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2535 optparse.make_option("--changesfile", dest="changesFile"),2536 optparse.make_option("--silent", dest="silent", action="store_true"),2537 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2538 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2539 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2540help="Import into refs/heads/ , not refs/remotes"),2541 optparse.make_option("--max-changes", dest="maxChanges",2542help="Maximum number of changes to import"),2543 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2544help="Internal block size to use when iteratively calling p4 changes"),2545 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2546help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2547 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2548help="Only sync files that are included in the Perforce Client Spec"),2549 optparse.make_option("-/", dest="cloneExclude",2550 action="append",type="string",2551help="exclude depot path"),2552]2553 self.description ="""Imports from Perforce into a git repository.\n2554 example:2555 //depot/my/project/ -- to import the current head2556 //depot/my/project/@all -- to import everything2557 //depot/my/project/@1,6 -- to import only from revision 1 to 625582559 (a ... is not needed in the path p4 specification, it's added implicitly)"""25602561 self.usage +=" //depot/path[@revRange]"2562 self.silent =False2563 self.createdBranches =set()2564 self.committedChanges =set()2565 self.branch =""2566 self.detectBranches =False2567 self.detectLabels =False2568 self.importLabels =False2569 self.changesFile =""2570 self.syncWithOrigin =True2571 self.importIntoRemotes =True2572 self.maxChanges =""2573 self.changes_block_size =None2574 self.keepRepoPath =False2575 self.depotPaths =None2576 self.p4BranchesInGit = []2577 self.cloneExclude = []2578 self.useClientSpec =False2579 self.useClientSpec_from_options =False2580 self.clientSpecDirs =None2581 self.tempBranches = []2582 self.tempBranchLocation ="refs/git-p4-tmp"2583 self.largeFileSystem =None2584 self.suppress_meta_comment =False25852586ifgitConfig('git-p4.largeFileSystem'):2587 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2588 self.largeFileSystem =largeFileSystemConstructor(2589lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2590)25912592ifgitConfig("git-p4.syncFromOrigin") =="false":2593 self.syncWithOrigin =False25942595 self.depotPaths = []2596 self.changeRange =""2597 self.previousDepotPaths = []2598 self.hasOrigin =False25992600# map from branch depot path to parent branch2601 self.knownBranches = {}2602 self.initialParents = {}26032604 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))2605 self.labels = {}26062607# Force a checkpoint in fast-import and wait for it to finish2608defcheckpoint(self):2609 self.gitStream.write("checkpoint\n\n")2610 self.gitStream.write("progress checkpoint\n\n")2611 out = self.gitOutput.readline()2612if self.verbose:2613print("checkpoint finished: "+ out)26142615defcmp_shelved(self, path, filerev, revision):2616""" Determine if a path at revision #filerev is the same as the file2617 at revision @revision for a shelved changelist. If they don't match,2618 unshelving won't be safe (we will get other changes mixed in).26192620 This is comparing the revision that the shelved changelist is *based* on, not2621 the shelved changelist itself.2622 """2623 ret =p4Cmd(["diff2","{0}#{1}".format(path, filerev),"{0}@{1}".format(path, revision)])2624if verbose:2625print("p4 diff2 path%sfilerev%srevision%s=>%s"% (path, filerev, revision, ret))2626return ret["status"] =="identical"26272628defextractFilesFromCommit(self, commit, shelved=False, shelved_cl =0, origin_revision =0):2629 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2630for path in self.cloneExclude]2631 files = []2632 fnum =02633while"depotFile%s"% fnum in commit:2634 path = commit["depotFile%s"% fnum]26352636if[p for p in self.cloneExclude2637ifp4PathStartsWith(path, p)]:2638 found =False2639else:2640 found = [p for p in self.depotPaths2641ifp4PathStartsWith(path, p)]2642if not found:2643 fnum = fnum +12644continue26452646file= {}2647file["path"] = path2648file["rev"] = commit["rev%s"% fnum]2649file["action"] = commit["action%s"% fnum]2650file["type"] = commit["type%s"% fnum]2651if shelved:2652file["shelved_cl"] =int(shelved_cl)26532654# For shelved changelists, check that the revision of each file that the2655# shelve was based on matches the revision that we are using for the2656# starting point for git-fast-import (self.initialParent). Otherwise2657# the resulting diff will contain deltas from multiple commits.26582659iffile["action"] !="add"and \2660not self.cmp_shelved(path,file["rev"], origin_revision):2661 sys.exit("change{0}not based on{1}for{2}, cannot unshelve".format(2662 commit["change"], self.initialParent, path))26632664 files.append(file)2665 fnum = fnum +12666return files26672668defextractJobsFromCommit(self, commit):2669 jobs = []2670 jnum =02671while"job%s"% jnum in commit:2672 job = commit["job%s"% jnum]2673 jobs.append(job)2674 jnum = jnum +12675return jobs26762677defstripRepoPath(self, path, prefixes):2678"""When streaming files, this is called to map a p4 depot path2679 to where it should go in git. The prefixes are either2680 self.depotPaths, or self.branchPrefixes in the case of2681 branch detection."""26822683if self.useClientSpec:2684# branch detection moves files up a level (the branch name)2685# from what client spec interpretation gives2686 path = self.clientSpecDirs.map_in_client(path)2687if self.detectBranches:2688for b in self.knownBranches:2689if path.startswith(b +"/"):2690 path = path[len(b)+1:]26912692elif self.keepRepoPath:2693# Preserve everything in relative path name except leading2694# //depot/; just look at first prefix as they all should2695# be in the same depot.2696 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2697ifp4PathStartsWith(path, depot):2698 path = path[len(depot):]26992700else:2701for p in prefixes:2702ifp4PathStartsWith(path, p):2703 path = path[len(p):]2704break27052706 path =wildcard_decode(path)2707return path27082709defsplitFilesIntoBranches(self, commit):2710"""Look at each depotFile in the commit to figure out to what2711 branch it belongs."""27122713if self.clientSpecDirs:2714 files = self.extractFilesFromCommit(commit)2715 self.clientSpecDirs.update_client_spec_path_cache(files)27162717 branches = {}2718 fnum =02719while"depotFile%s"% fnum in commit:2720 path = commit["depotFile%s"% fnum]2721 found = [p for p in self.depotPaths2722ifp4PathStartsWith(path, p)]2723if not found:2724 fnum = fnum +12725continue27262727file= {}2728file["path"] = path2729file["rev"] = commit["rev%s"% fnum]2730file["action"] = commit["action%s"% fnum]2731file["type"] = commit["type%s"% fnum]2732 fnum = fnum +127332734# start with the full relative path where this file would2735# go in a p4 client2736if self.useClientSpec:2737 relPath = self.clientSpecDirs.map_in_client(path)2738else:2739 relPath = self.stripRepoPath(path, self.depotPaths)27402741for branch in self.knownBranches.keys():2742# add a trailing slash so that a commit into qt/4.2foo2743# doesn't end up in qt/4.2, e.g.2744if relPath.startswith(branch +"/"):2745if branch not in branches:2746 branches[branch] = []2747 branches[branch].append(file)2748break27492750return branches27512752defwriteToGitStream(self, gitMode, relPath, contents):2753 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2754 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2755for d in contents:2756 self.gitStream.write(d)2757 self.gitStream.write('\n')27582759defencodeWithUTF8(self, path):2760try:2761 path.decode('ascii')2762except:2763 encoding ='utf8'2764ifgitConfig('git-p4.pathEncoding'):2765 encoding =gitConfig('git-p4.pathEncoding')2766 path = path.decode(encoding,'replace').encode('utf8','replace')2767if self.verbose:2768print('Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path))2769return path27702771# output one file from the P4 stream2772# - helper for streamP4Files27732774defstreamOneP4File(self,file, contents):2775 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2776 relPath = self.encodeWithUTF8(relPath)2777if verbose:2778if'fileSize'in self.stream_file:2779 size =int(self.stream_file['fileSize'])2780else:2781 size =0# deleted files don't get a fileSize apparently2782 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2783 sys.stdout.flush()27842785(type_base, type_mods) =split_p4_type(file["type"])27862787 git_mode ="100644"2788if"x"in type_mods:2789 git_mode ="100755"2790if type_base =="symlink":2791 git_mode ="120000"2792# p4 print on a symlink sometimes contains "target\n";2793# if it does, remove the newline2794 data =''.join(contents)2795if not data:2796# Some version of p4 allowed creating a symlink that pointed2797# to nothing. This causes p4 errors when checking out such2798# a change, and errors here too. Work around it by ignoring2799# the bad symlink; hopefully a future change fixes it.2800print("\nIgnoring empty symlink in%s"%file['depotFile'])2801return2802elif data[-1] =='\n':2803 contents = [data[:-1]]2804else:2805 contents = [data]28062807if type_base =="utf16":2808# p4 delivers different text in the python output to -G2809# than it does when using "print -o", or normal p4 client2810# operations. utf16 is converted to ascii or utf8, perhaps.2811# But ascii text saved as -t utf16 is completely mangled.2812# Invoke print -o to get the real contents.2813#2814# On windows, the newlines will always be mangled by print, so put2815# them back too. This is not needed to the cygwin windows version,2816# just the native "NT" type.2817#2818try:2819 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2820exceptExceptionas e:2821if'Translation of file content failed'instr(e):2822 type_base ='binary'2823else:2824raise e2825else:2826ifp4_version_string().find('/NT') >=0:2827 text = text.replace('\r\n','\n')2828 contents = [ text ]28292830if type_base =="apple":2831# Apple filetype files will be streamed as a concatenation of2832# its appledouble header and the contents. This is useless2833# on both macs and non-macs. If using "print -q -o xx", it2834# will create "xx" with the data, and "%xx" with the header.2835# This is also not very useful.2836#2837# Ideally, someday, this script can learn how to generate2838# appledouble files directly and import those to git, but2839# non-mac machines can never find a use for apple filetype.2840print("\nIgnoring apple filetype file%s"%file['depotFile'])2841return28422843# Note that we do not try to de-mangle keywords on utf16 files,2844# even though in theory somebody may want that.2845 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2846if pattern:2847 regexp = re.compile(pattern, re.VERBOSE)2848 text =''.join(contents)2849 text = regexp.sub(r'$\1$', text)2850 contents = [ text ]28512852if self.largeFileSystem:2853(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)28542855 self.writeToGitStream(git_mode, relPath, contents)28562857defstreamOneP4Deletion(self,file):2858 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2859 relPath = self.encodeWithUTF8(relPath)2860if verbose:2861 sys.stdout.write("delete%s\n"% relPath)2862 sys.stdout.flush()2863 self.gitStream.write("D%s\n"% relPath)28642865if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2866 self.largeFileSystem.removeLargeFile(relPath)28672868# handle another chunk of streaming data2869defstreamP4FilesCb(self, marshalled):28702871# catch p4 errors and complain2872 err =None2873if"code"in marshalled:2874if marshalled["code"] =="error":2875if"data"in marshalled:2876 err = marshalled["data"].rstrip()28772878if not err and'fileSize'in self.stream_file:2879 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2880if required_bytes >0:2881 err ='Not enough space left on%s! Free at least%iMB.'% (2882 os.getcwd(), required_bytes/1024/10242883)28842885if err:2886 f =None2887if self.stream_have_file_info:2888if"depotFile"in self.stream_file:2889 f = self.stream_file["depotFile"]2890# force a failure in fast-import, else an empty2891# commit will be made2892 self.gitStream.write("\n")2893 self.gitStream.write("die-now\n")2894 self.gitStream.close()2895# ignore errors, but make sure it exits first2896 self.importProcess.wait()2897if f:2898die("Error from p4 print for%s:%s"% (f, err))2899else:2900die("Error from p4 print:%s"% err)29012902if'depotFile'in marshalled and self.stream_have_file_info:2903# start of a new file - output the old one first2904 self.streamOneP4File(self.stream_file, self.stream_contents)2905 self.stream_file = {}2906 self.stream_contents = []2907 self.stream_have_file_info =False29082909# pick up the new file information... for the2910# 'data' field we need to append to our array2911for k in marshalled.keys():2912if k =='data':2913if'streamContentSize'not in self.stream_file:2914 self.stream_file['streamContentSize'] =02915 self.stream_file['streamContentSize'] +=len(marshalled['data'])2916 self.stream_contents.append(marshalled['data'])2917else:2918 self.stream_file[k] = marshalled[k]29192920if(verbose and2921'streamContentSize'in self.stream_file and2922'fileSize'in self.stream_file and2923'depotFile'in self.stream_file):2924 size =int(self.stream_file["fileSize"])2925if size >0:2926 progress =100*self.stream_file['streamContentSize']/size2927 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2928 sys.stdout.flush()29292930 self.stream_have_file_info =True29312932# Stream directly from "p4 files" into "git fast-import"2933defstreamP4Files(self, files):2934 filesForCommit = []2935 filesToRead = []2936 filesToDelete = []29372938for f in files:2939 filesForCommit.append(f)2940if f['action']in self.delete_actions:2941 filesToDelete.append(f)2942else:2943 filesToRead.append(f)29442945# deleted files...2946for f in filesToDelete:2947 self.streamOneP4Deletion(f)29482949iflen(filesToRead) >0:2950 self.stream_file = {}2951 self.stream_contents = []2952 self.stream_have_file_info =False29532954# curry self argument2955defstreamP4FilesCbSelf(entry):2956 self.streamP4FilesCb(entry)29572958 fileArgs = []2959for f in filesToRead:2960if'shelved_cl'in f:2961# Handle shelved CLs using the "p4 print file@=N" syntax to print2962# the contents2963 fileArg ='%s@=%d'% (f['path'], f['shelved_cl'])2964else:2965 fileArg ='%s#%s'% (f['path'], f['rev'])29662967 fileArgs.append(fileArg)29682969p4CmdList(["-x","-","print"],2970 stdin=fileArgs,2971 cb=streamP4FilesCbSelf)29722973# do the last chunk2974if'depotFile'in self.stream_file:2975 self.streamOneP4File(self.stream_file, self.stream_contents)29762977defmake_email(self, userid):2978if userid in self.users:2979return self.users[userid]2980else:2981return"%s<a@b>"% userid29822983defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2984""" Stream a p4 tag.2985 commit is either a git commit, or a fast-import mark, ":<p4commit>"2986 """29872988if verbose:2989print("writing tag%sfor commit%s"% (labelName, commit))2990 gitStream.write("tag%s\n"% labelName)2991 gitStream.write("from%s\n"% commit)29922993if'Owner'in labelDetails:2994 owner = labelDetails["Owner"]2995else:2996 owner =None29972998# Try to use the owner of the p4 label, or failing that,2999# the current p4 user id.3000if owner:3001 email = self.make_email(owner)3002else:3003 email = self.make_email(self.p4UserId())3004 tagger ="%s %s %s"% (email, epoch, self.tz)30053006 gitStream.write("tagger%s\n"% tagger)30073008print("labelDetails=",labelDetails)3009if'Description'in labelDetails:3010 description = labelDetails['Description']3011else:3012 description ='Label from git p4'30133014 gitStream.write("data%d\n"%len(description))3015 gitStream.write(description)3016 gitStream.write("\n")30173018definClientSpec(self, path):3019if not self.clientSpecDirs:3020return True3021 inClientSpec = self.clientSpecDirs.map_in_client(path)3022if not inClientSpec and self.verbose:3023print('Ignoring file outside of client spec:{0}'.format(path))3024return inClientSpec30253026defhasBranchPrefix(self, path):3027if not self.branchPrefixes:3028return True3029 hasPrefix = [p for p in self.branchPrefixes3030ifp4PathStartsWith(path, p)]3031if not hasPrefix and self.verbose:3032print('Ignoring file outside of prefix:{0}'.format(path))3033return hasPrefix30343035defcommit(self, details, files, branch, parent =""):3036 epoch = details["time"]3037 author = details["user"]3038 jobs = self.extractJobsFromCommit(details)30393040if self.verbose:3041print('commit into{0}'.format(branch))30423043if self.clientSpecDirs:3044 self.clientSpecDirs.update_client_spec_path_cache(files)30453046 files = [f for f in files3047if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]30483049if not files and notgitConfigBool('git-p4.keepEmptyCommits'):3050print('Ignoring revision{0}as it would produce an empty commit.'3051.format(details['change']))3052return30533054 self.gitStream.write("commit%s\n"% branch)3055 self.gitStream.write("mark :%s\n"% details["change"])3056 self.committedChanges.add(int(details["change"]))3057 committer =""3058if author not in self.users:3059 self.getUserMapFromPerforceServer()3060 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)30613062 self.gitStream.write("committer%s\n"% committer)30633064 self.gitStream.write("data <<EOT\n")3065 self.gitStream.write(details["desc"])3066iflen(jobs) >0:3067 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))30683069if not self.suppress_meta_comment:3070 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%3071(','.join(self.branchPrefixes), details["change"]))3072iflen(details['options']) >0:3073 self.gitStream.write(": options =%s"% details['options'])3074 self.gitStream.write("]\n")30753076 self.gitStream.write("EOT\n\n")30773078iflen(parent) >0:3079if self.verbose:3080print("parent%s"% parent)3081 self.gitStream.write("from%s\n"% parent)30823083 self.streamP4Files(files)3084 self.gitStream.write("\n")30853086 change =int(details["change"])30873088if change in self.labels:3089 label = self.labels[change]3090 labelDetails = label[0]3091 labelRevisions = label[1]3092if self.verbose:3093print("Change%sis labelled%s"% (change, labelDetails))30943095 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)3096for p in self.branchPrefixes])30973098iflen(files) ==len(labelRevisions):30993100 cleanedFiles = {}3101for info in files:3102if info["action"]in self.delete_actions:3103continue3104 cleanedFiles[info["depotFile"]] = info["rev"]31053106if cleanedFiles == labelRevisions:3107 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)31083109else:3110if not self.silent:3111print("Tag%sdoes not match with change%s: files do not match."3112% (labelDetails["label"], change))31133114else:3115if not self.silent:3116print("Tag%sdoes not match with change%s: file count is different."3117% (labelDetails["label"], change))31183119# Build a dictionary of changelists and labels, for "detect-labels" option.3120defgetLabels(self):3121 self.labels = {}31223123 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])3124iflen(l) >0and not self.silent:3125print("Finding files belonging to labels in%s"% self.depotPaths)31263127for output in l:3128 label = output["label"]3129 revisions = {}3130 newestChange =03131if self.verbose:3132print("Querying files for label%s"% label)3133forfileinp4CmdList(["files"] +3134["%s...@%s"% (p, label)3135for p in self.depotPaths]):3136 revisions[file["depotFile"]] =file["rev"]3137 change =int(file["change"])3138if change > newestChange:3139 newestChange = change31403141 self.labels[newestChange] = [output, revisions]31423143if self.verbose:3144print("Label changes:%s"% self.labels.keys())31453146# Import p4 labels as git tags. A direct mapping does not3147# exist, so assume that if all the files are at the same revision3148# then we can use that, or it's something more complicated we should3149# just ignore.3150defimportP4Labels(self, stream, p4Labels):3151if verbose:3152print("import p4 labels: "+' '.join(p4Labels))31533154 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")3155 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")3156iflen(validLabelRegexp) ==0:3157 validLabelRegexp = defaultLabelRegexp3158 m = re.compile(validLabelRegexp)31593160for name in p4Labels:3161 commitFound =False31623163if not m.match(name):3164if verbose:3165print("label%sdoes not match regexp%s"% (name,validLabelRegexp))3166continue31673168if name in ignoredP4Labels:3169continue31703171 labelDetails =p4CmdList(['label',"-o", name])[0]31723173# get the most recent changelist for each file in this label3174 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)3175for p in self.depotPaths])31763177if'change'in change:3178# find the corresponding git commit; take the oldest commit3179 changelist =int(change['change'])3180if changelist in self.committedChanges:3181 gitCommit =":%d"% changelist # use a fast-import mark3182 commitFound =True3183else:3184 gitCommit =read_pipe(["git","rev-list","--max-count=1",3185"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)3186iflen(gitCommit) ==0:3187print("importing label%s: could not find git commit for changelist%d"% (name, changelist))3188else:3189 commitFound =True3190 gitCommit = gitCommit.strip()31913192if commitFound:3193# Convert from p4 time format3194try:3195 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")3196exceptValueError:3197print("Could not convert label time%s"% labelDetails['Update'])3198 tmwhen =131993200 when =int(time.mktime(tmwhen))3201 self.streamTag(stream, name, labelDetails, gitCommit, when)3202if verbose:3203print("p4 label%smapped to git commit%s"% (name, gitCommit))3204else:3205if verbose:3206print("Label%shas no changelists - possibly deleted?"% name)32073208if not commitFound:3209# We can't import this label; don't try again as it will get very3210# expensive repeatedly fetching all the files for labels that will3211# never be imported. If the label is moved in the future, the3212# ignore will need to be removed manually.3213system(["git","config","--add","git-p4.ignoredP4Labels", name])32143215defguessProjectName(self):3216for p in self.depotPaths:3217if p.endswith("/"):3218 p = p[:-1]3219 p = p[p.strip().rfind("/") +1:]3220if not p.endswith("/"):3221 p +="/"3222return p32233224defgetBranchMapping(self):3225 lostAndFoundBranches =set()32263227 user =gitConfig("git-p4.branchUser")3228iflen(user) >0:3229 command ="branches -u%s"% user3230else:3231 command ="branches"32323233for info inp4CmdList(command):3234 details =p4Cmd(["branch","-o", info["branch"]])3235 viewIdx =03236while"View%s"% viewIdx in details:3237 paths = details["View%s"% viewIdx].split(" ")3238 viewIdx = viewIdx +13239# require standard //depot/foo/... //depot/bar/... mapping3240iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3241continue3242 source = paths[0]3243 destination = paths[1]3244## HACK3245ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3246 source = source[len(self.depotPaths[0]):-4]3247 destination = destination[len(self.depotPaths[0]):-4]32483249if destination in self.knownBranches:3250if not self.silent:3251print("p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination))3252print("but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination))3253continue32543255 self.knownBranches[destination] = source32563257 lostAndFoundBranches.discard(destination)32583259if source not in self.knownBranches:3260 lostAndFoundBranches.add(source)32613262# Perforce does not strictly require branches to be defined, so we also3263# check git config for a branch list.3264#3265# Example of branch definition in git config file:3266# [git-p4]3267# branchList=main:branchA3268# branchList=main:branchB3269# branchList=branchA:branchC3270 configBranches =gitConfigList("git-p4.branchList")3271for branch in configBranches:3272if branch:3273(source, destination) = branch.split(":")3274 self.knownBranches[destination] = source32753276 lostAndFoundBranches.discard(destination)32773278if source not in self.knownBranches:3279 lostAndFoundBranches.add(source)328032813282for branch in lostAndFoundBranches:3283 self.knownBranches[branch] = branch32843285defgetBranchMappingFromGitBranches(self):3286 branches =p4BranchesInGit(self.importIntoRemotes)3287for branch in branches.keys():3288if branch =="master":3289 branch ="main"3290else:3291 branch = branch[len(self.projectName):]3292 self.knownBranches[branch] = branch32933294defupdateOptionDict(self, d):3295 option_keys = {}3296if self.keepRepoPath:3297 option_keys['keepRepoPath'] =132983299 d["options"] =' '.join(sorted(option_keys.keys()))33003301defreadOptions(self, d):3302 self.keepRepoPath = ('options'in d3303and('keepRepoPath'in d['options']))33043305defgitRefForBranch(self, branch):3306if branch =="main":3307return self.refPrefix +"master"33083309iflen(branch) <=0:3310return branch33113312return self.refPrefix + self.projectName + branch33133314defgitCommitByP4Change(self, ref, change):3315if self.verbose:3316print("looking in ref "+ ref +" for change%susing bisect..."% change)33173318 earliestCommit =""3319 latestCommit =parseRevision(ref)33203321while True:3322if self.verbose:3323print("trying: earliest%slatest%s"% (earliestCommit, latestCommit))3324 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3325iflen(next) ==0:3326if self.verbose:3327print("argh")3328return""3329 log =extractLogMessageFromGitCommit(next)3330 settings =extractSettingsGitLog(log)3331 currentChange =int(settings['change'])3332if self.verbose:3333print("current change%s"% currentChange)33343335if currentChange == change:3336if self.verbose:3337print("found%s"% next)3338return next33393340if currentChange < change:3341 earliestCommit ="^%s"% next3342else:3343 latestCommit ="%s"% next33443345return""33463347defimportNewBranch(self, branch, maxChange):3348# make fast-import flush all changes to disk and update the refs using the checkpoint3349# command so that we can try to find the branch parent in the git history3350 self.gitStream.write("checkpoint\n\n");3351 self.gitStream.flush();3352 branchPrefix = self.depotPaths[0] + branch +"/"3353range="@1,%s"% maxChange3354#print "prefix" + branchPrefix3355 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3356iflen(changes) <=0:3357return False3358 firstChange = changes[0]3359#print "first change in branch: %s" % firstChange3360 sourceBranch = self.knownBranches[branch]3361 sourceDepotPath = self.depotPaths[0] + sourceBranch3362 sourceRef = self.gitRefForBranch(sourceBranch)3363#print "source " + sourceBranch33643365 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3366#print "branch parent: %s" % branchParentChange3367 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3368iflen(gitParent) >0:3369 self.initialParents[self.gitRefForBranch(branch)] = gitParent3370#print "parent git commit: %s" % gitParent33713372 self.importChanges(changes)3373return True33743375defsearchParent(self, parent, branch, target):3376 parentFound =False3377for blob inread_pipe_lines(["git","rev-list","--reverse",3378"--no-merges", parent]):3379 blob = blob.strip()3380iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3381 parentFound =True3382if self.verbose:3383print("Found parent of%sin commit%s"% (branch, blob))3384break3385if parentFound:3386return blob3387else:3388return None33893390defimportChanges(self, changes, shelved=False, origin_revision=0):3391 cnt =13392for change in changes:3393 description =p4_describe(change, shelved)3394 self.updateOptionDict(description)33953396if not self.silent:3397 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3398 sys.stdout.flush()3399 cnt = cnt +134003401try:3402if self.detectBranches:3403 branches = self.splitFilesIntoBranches(description)3404for branch in branches.keys():3405## HACK --hwn3406 branchPrefix = self.depotPaths[0] + branch +"/"3407 self.branchPrefixes = [ branchPrefix ]34083409 parent =""34103411 filesForCommit = branches[branch]34123413if self.verbose:3414print("branch is%s"% branch)34153416 self.updatedBranches.add(branch)34173418if branch not in self.createdBranches:3419 self.createdBranches.add(branch)3420 parent = self.knownBranches[branch]3421if parent == branch:3422 parent =""3423else:3424 fullBranch = self.projectName + branch3425if fullBranch not in self.p4BranchesInGit:3426if not self.silent:3427print("\nImporting new branch%s"% fullBranch);3428if self.importNewBranch(branch, change -1):3429 parent =""3430 self.p4BranchesInGit.append(fullBranch)3431if not self.silent:3432print("\nResuming with change%s"% change);34333434if self.verbose:3435print("parent determined through known branches:%s"% parent)34363437 branch = self.gitRefForBranch(branch)3438 parent = self.gitRefForBranch(parent)34393440if self.verbose:3441print("looking for initial parent for%s; current parent is%s"% (branch, parent))34423443iflen(parent) ==0and branch in self.initialParents:3444 parent = self.initialParents[branch]3445del self.initialParents[branch]34463447 blob =None3448iflen(parent) >0:3449 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3450if self.verbose:3451print("Creating temporary branch: "+ tempBranch)3452 self.commit(description, filesForCommit, tempBranch)3453 self.tempBranches.append(tempBranch)3454 self.checkpoint()3455 blob = self.searchParent(parent, branch, tempBranch)3456if blob:3457 self.commit(description, filesForCommit, branch, blob)3458else:3459if self.verbose:3460print("Parent of%snot found. Committing into head of%s"% (branch, parent))3461 self.commit(description, filesForCommit, branch, parent)3462else:3463 files = self.extractFilesFromCommit(description, shelved, change, origin_revision)3464 self.commit(description, files, self.branch,3465 self.initialParent)3466# only needed once, to connect to the previous commit3467 self.initialParent =""3468exceptIOError:3469print(self.gitError.read())3470 sys.exit(1)34713472defsync_origin_only(self):3473if self.syncWithOrigin:3474 self.hasOrigin =originP4BranchesExist()3475if self.hasOrigin:3476if not self.silent:3477print('Syncing with origin first, using "git fetch origin"')3478system("git fetch origin")34793480defimportHeadRevision(self, revision):3481print("Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch))34823483 details = {}3484 details["user"] ="git perforce import user"3485 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3486% (' '.join(self.depotPaths), revision))3487 details["change"] = revision3488 newestRevision =034893490 fileCnt =03491 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]34923493for info inp4CmdList(["files"] + fileArgs):34943495if'code'in info and info['code'] =='error':3496 sys.stderr.write("p4 returned an error:%s\n"3497% info['data'])3498if info['data'].find("must refer to client") >=0:3499 sys.stderr.write("This particular p4 error is misleading.\n")3500 sys.stderr.write("Perhaps the depot path was misspelled.\n");3501 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3502 sys.exit(1)3503if'p4ExitCode'in info:3504 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3505 sys.exit(1)350635073508 change =int(info["change"])3509if change > newestRevision:3510 newestRevision = change35113512if info["action"]in self.delete_actions:3513# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3514#fileCnt = fileCnt + 13515continue35163517for prop in["depotFile","rev","action","type"]:3518 details["%s%s"% (prop, fileCnt)] = info[prop]35193520 fileCnt = fileCnt +135213522 details["change"] = newestRevision35233524# Use time from top-most change so that all git p4 clones of3525# the same p4 repo have the same commit SHA1s.3526 res =p4_describe(newestRevision)3527 details["time"] = res["time"]35283529 self.updateOptionDict(details)3530try:3531 self.commit(details, self.extractFilesFromCommit(details), self.branch)3532exceptIOError:3533print("IO error with git fast-import. Is your git version recent enough?")3534print(self.gitError.read())35353536defopenStreams(self):3537 self.importProcess = subprocess.Popen(["git","fast-import"],3538 stdin=subprocess.PIPE,3539 stdout=subprocess.PIPE,3540 stderr=subprocess.PIPE);3541 self.gitOutput = self.importProcess.stdout3542 self.gitStream = self.importProcess.stdin3543 self.gitError = self.importProcess.stderr35443545defcloseStreams(self):3546 self.gitStream.close()3547if self.importProcess.wait() !=0:3548die("fast-import failed:%s"% self.gitError.read())3549 self.gitOutput.close()3550 self.gitError.close()35513552defrun(self, args):3553if self.importIntoRemotes:3554 self.refPrefix ="refs/remotes/p4/"3555else:3556 self.refPrefix ="refs/heads/p4/"35573558 self.sync_origin_only()35593560 branch_arg_given =bool(self.branch)3561iflen(self.branch) ==0:3562 self.branch = self.refPrefix +"master"3563ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3564system("git update-ref%srefs/heads/p4"% self.branch)3565system("git branch -D p4")35663567# accept either the command-line option, or the configuration variable3568if self.useClientSpec:3569# will use this after clone to set the variable3570 self.useClientSpec_from_options =True3571else:3572ifgitConfigBool("git-p4.useclientspec"):3573 self.useClientSpec =True3574if self.useClientSpec:3575 self.clientSpecDirs =getClientSpec()35763577# TODO: should always look at previous commits,3578# merge with previous imports, if possible.3579if args == []:3580if self.hasOrigin:3581createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)35823583# branches holds mapping from branch name to sha13584 branches =p4BranchesInGit(self.importIntoRemotes)35853586# restrict to just this one, disabling detect-branches3587if branch_arg_given:3588 short = self.branch.split("/")[-1]3589if short in branches:3590 self.p4BranchesInGit = [ short ]3591else:3592 self.p4BranchesInGit = branches.keys()35933594iflen(self.p4BranchesInGit) >1:3595if not self.silent:3596print("Importing from/into multiple branches")3597 self.detectBranches =True3598for branch in branches.keys():3599 self.initialParents[self.refPrefix + branch] = \3600 branches[branch]36013602if self.verbose:3603print("branches:%s"% self.p4BranchesInGit)36043605 p4Change =03606for branch in self.p4BranchesInGit:3607 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)36083609 settings =extractSettingsGitLog(logMsg)36103611 self.readOptions(settings)3612if('depot-paths'in settings3613and'change'in settings):3614 change =int(settings['change']) +13615 p4Change =max(p4Change, change)36163617 depotPaths =sorted(settings['depot-paths'])3618if self.previousDepotPaths == []:3619 self.previousDepotPaths = depotPaths3620else:3621 paths = []3622for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3623 prev_list = prev.split("/")3624 cur_list = cur.split("/")3625for i inrange(0,min(len(cur_list),len(prev_list))):3626if cur_list[i] != prev_list[i]:3627 i = i -13628break36293630 paths.append("/".join(cur_list[:i +1]))36313632 self.previousDepotPaths = paths36333634if p4Change >0:3635 self.depotPaths =sorted(self.previousDepotPaths)3636 self.changeRange ="@%s,#head"% p4Change3637if not self.silent and not self.detectBranches:3638print("Performing incremental import into%sgit branch"% self.branch)36393640# accept multiple ref name abbreviations:3641# refs/foo/bar/branch -> use it exactly3642# p4/branch -> prepend refs/remotes/ or refs/heads/3643# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3644if not self.branch.startswith("refs/"):3645if self.importIntoRemotes:3646 prepend ="refs/remotes/"3647else:3648 prepend ="refs/heads/"3649if not self.branch.startswith("p4/"):3650 prepend +="p4/"3651 self.branch = prepend + self.branch36523653iflen(args) ==0and self.depotPaths:3654if not self.silent:3655print("Depot paths:%s"%' '.join(self.depotPaths))3656else:3657if self.depotPaths and self.depotPaths != args:3658print("previous import used depot path%sand now%swas specified. "3659"This doesn't work!"% (' '.join(self.depotPaths),3660' '.join(args)))3661 sys.exit(1)36623663 self.depotPaths =sorted(args)36643665 revision =""3666 self.users = {}36673668# Make sure no revision specifiers are used when --changesfile3669# is specified.3670 bad_changesfile =False3671iflen(self.changesFile) >0:3672for p in self.depotPaths:3673if p.find("@") >=0or p.find("#") >=0:3674 bad_changesfile =True3675break3676if bad_changesfile:3677die("Option --changesfile is incompatible with revision specifiers")36783679 newPaths = []3680for p in self.depotPaths:3681if p.find("@") != -1:3682 atIdx = p.index("@")3683 self.changeRange = p[atIdx:]3684if self.changeRange =="@all":3685 self.changeRange =""3686elif','not in self.changeRange:3687 revision = self.changeRange3688 self.changeRange =""3689 p = p[:atIdx]3690elif p.find("#") != -1:3691 hashIdx = p.index("#")3692 revision = p[hashIdx:]3693 p = p[:hashIdx]3694elif self.previousDepotPaths == []:3695# pay attention to changesfile, if given, else import3696# the entire p4 tree at the head revision3697iflen(self.changesFile) ==0:3698 revision ="#head"36993700 p = re.sub("\.\.\.$","", p)3701if not p.endswith("/"):3702 p +="/"37033704 newPaths.append(p)37053706 self.depotPaths = newPaths37073708# --detect-branches may change this for each branch3709 self.branchPrefixes = self.depotPaths37103711 self.loadUserMapFromCache()3712 self.labels = {}3713if self.detectLabels:3714 self.getLabels();37153716if self.detectBranches:3717## FIXME - what's a P4 projectName ?3718 self.projectName = self.guessProjectName()37193720if self.hasOrigin:3721 self.getBranchMappingFromGitBranches()3722else:3723 self.getBranchMapping()3724if self.verbose:3725print("p4-git branches:%s"% self.p4BranchesInGit)3726print("initial parents:%s"% self.initialParents)3727for b in self.p4BranchesInGit:3728if b !="master":37293730## FIXME3731 b = b[len(self.projectName):]3732 self.createdBranches.add(b)37333734 self.openStreams()37353736if revision:3737 self.importHeadRevision(revision)3738else:3739 changes = []37403741iflen(self.changesFile) >0:3742 output =open(self.changesFile).readlines()3743 changeSet =set()3744for line in output:3745 changeSet.add(int(line))37463747for change in changeSet:3748 changes.append(change)37493750 changes.sort()3751else:3752# catch "git p4 sync" with no new branches, in a repo that3753# does not have any existing p4 branches3754iflen(args) ==0:3755if not self.p4BranchesInGit:3756die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")37573758# The default branch is master, unless --branch is used to3759# specify something else. Make sure it exists, or complain3760# nicely about how to use --branch.3761if not self.detectBranches:3762if notbranch_exists(self.branch):3763if branch_arg_given:3764die("Error: branch%sdoes not exist."% self.branch)3765else:3766die("Error: no branch%s; perhaps specify one with --branch."%3767 self.branch)37683769if self.verbose:3770print("Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3771 self.changeRange))3772 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)37733774iflen(self.maxChanges) >0:3775 changes = changes[:min(int(self.maxChanges),len(changes))]37763777iflen(changes) ==0:3778if not self.silent:3779print("No changes to import!")3780else:3781if not self.silent and not self.detectBranches:3782print("Import destination:%s"% self.branch)37833784 self.updatedBranches =set()37853786if not self.detectBranches:3787if args:3788# start a new branch3789 self.initialParent =""3790else:3791# build on a previous revision3792 self.initialParent =parseRevision(self.branch)37933794 self.importChanges(changes)37953796if not self.silent:3797print("")3798iflen(self.updatedBranches) >0:3799 sys.stdout.write("Updated branches: ")3800for b in self.updatedBranches:3801 sys.stdout.write("%s"% b)3802 sys.stdout.write("\n")38033804ifgitConfigBool("git-p4.importLabels"):3805 self.importLabels =True38063807if self.importLabels:3808 p4Labels =getP4Labels(self.depotPaths)3809 gitTags =getGitTags()38103811 missingP4Labels = p4Labels - gitTags3812 self.importP4Labels(self.gitStream, missingP4Labels)38133814 self.closeStreams()38153816# Cleanup temporary branches created during import3817if self.tempBranches != []:3818for branch in self.tempBranches:3819read_pipe("git update-ref -d%s"% branch)3820 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))38213822# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3823# a convenient shortcut refname "p4".3824if self.importIntoRemotes:3825 head_ref = self.refPrefix +"HEAD"3826if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3827system(["git","symbolic-ref", head_ref, self.branch])38283829return True38303831classP4Rebase(Command):3832def__init__(self):3833 Command.__init__(self)3834 self.options = [3835 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3836]3837 self.importLabels =False3838 self.description = ("Fetches the latest revision from perforce and "3839+"rebases the current work (branch) against it")38403841defrun(self, args):3842 sync =P4Sync()3843 sync.importLabels = self.importLabels3844 sync.run([])38453846return self.rebase()38473848defrebase(self):3849if os.system("git update-index --refresh") !=0:3850die("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.");3851iflen(read_pipe("git diff-index HEAD --")) >0:3852die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");38533854[upstream, settings] =findUpstreamBranchPoint()3855iflen(upstream) ==0:3856die("Cannot find upstream branchpoint for rebase")38573858# the branchpoint may be p4/foo~3, so strip off the parent3859 upstream = re.sub("~[0-9]+$","", upstream)38603861print("Rebasing the current branch onto%s"% upstream)3862 oldHead =read_pipe("git rev-parse HEAD").strip()3863system("git rebase%s"% upstream)3864system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3865return True38663867classP4Clone(P4Sync):3868def__init__(self):3869 P4Sync.__init__(self)3870 self.description ="Creates a new git repository and imports from Perforce into it"3871 self.usage ="usage: %prog [options] //depot/path[@revRange]"3872 self.options += [3873 optparse.make_option("--destination", dest="cloneDestination",3874 action='store', default=None,3875help="where to leave result of the clone"),3876 optparse.make_option("--bare", dest="cloneBare",3877 action="store_true", default=False),3878]3879 self.cloneDestination =None3880 self.needsGit =False3881 self.cloneBare =False38823883defdefaultDestination(self, args):3884## TODO: use common prefix of args?3885 depotPath = args[0]3886 depotDir = re.sub("(@[^@]*)$","", depotPath)3887 depotDir = re.sub("(#[^#]*)$","", depotDir)3888 depotDir = re.sub(r"\.\.\.$","", depotDir)3889 depotDir = re.sub(r"/$","", depotDir)3890return os.path.split(depotDir)[1]38913892defrun(self, args):3893iflen(args) <1:3894return False38953896if self.keepRepoPath and not self.cloneDestination:3897 sys.stderr.write("Must specify destination for --keep-path\n")3898 sys.exit(1)38993900 depotPaths = args39013902if not self.cloneDestination andlen(depotPaths) >1:3903 self.cloneDestination = depotPaths[-1]3904 depotPaths = depotPaths[:-1]39053906 self.cloneExclude = ["/"+p for p in self.cloneExclude]3907for p in depotPaths:3908if not p.startswith("//"):3909 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3910return False39113912if not self.cloneDestination:3913 self.cloneDestination = self.defaultDestination(args)39143915print("Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination))39163917if not os.path.exists(self.cloneDestination):3918 os.makedirs(self.cloneDestination)3919chdir(self.cloneDestination)39203921 init_cmd = ["git","init"]3922if self.cloneBare:3923 init_cmd.append("--bare")3924 retcode = subprocess.call(init_cmd)3925if retcode:3926raiseCalledProcessError(retcode, init_cmd)39273928if not P4Sync.run(self, depotPaths):3929return False39303931# create a master branch and check out a work tree3932ifgitBranchExists(self.branch):3933system(["git","branch","master", self.branch ])3934if not self.cloneBare:3935system(["git","checkout","-f"])3936else:3937print('Not checking out any branch, use ' \3938'"git checkout -q -b master <branch>"')39393940# auto-set this variable if invoked with --use-client-spec3941if self.useClientSpec_from_options:3942system("git config --bool git-p4.useclientspec true")39433944return True39453946classP4Unshelve(Command):3947def__init__(self):3948 Command.__init__(self)3949 self.options = []3950 self.origin ="HEAD"3951 self.description ="Unshelve a P4 changelist into a git commit"3952 self.usage ="usage: %prog [options] changelist"3953 self.options += [3954 optparse.make_option("--origin", dest="origin",3955help="Use this base revision instead of the default (%s)"% self.origin),3956]3957 self.verbose =False3958 self.noCommit =False3959 self.destbranch ="refs/remotes/p4/unshelved"39603961defrenameBranch(self, branch_name):3962""" Rename the existing branch to branch_name.N3963 """39643965 found =True3966for i inrange(0,1000):3967 backup_branch_name ="{0}.{1}".format(branch_name, i)3968if notgitBranchExists(backup_branch_name):3969gitUpdateRef(backup_branch_name, branch_name)# copy ref to backup3970gitDeleteRef(branch_name)3971 found =True3972print("renamed old unshelve branch to{0}".format(backup_branch_name))3973break39743975if not found:3976 sys.exit("gave up trying to rename existing branch{0}".format(sync.branch))39773978deffindLastP4Revision(self, starting_point):3979""" Look back from starting_point for the first commit created by git-p43980 to find the P4 commit we are based on, and the depot-paths.3981 """39823983for parent in(range(65535)):3984 log =extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))3985 settings =extractSettingsGitLog(log)3986if'change'in settings:3987return settings39883989 sys.exit("could not find git-p4 commits in{0}".format(self.origin))39903991defrun(self, args):3992iflen(args) !=1:3993return False39943995if notgitBranchExists(self.origin):3996 sys.exit("origin branch{0}does not exist".format(self.origin))39973998 sync =P4Sync()3999 changes = args4000 sync.initialParent = self.origin40014002# use the first change in the list to construct the branch to unshelve into4003 change = changes[0]40044005# if the target branch already exists, rename it4006 branch_name ="{0}/{1}".format(self.destbranch, change)4007ifgitBranchExists(branch_name):4008 self.renameBranch(branch_name)4009 sync.branch = branch_name40104011 sync.verbose = self.verbose4012 sync.suppress_meta_comment =True40134014 settings = self.findLastP4Revision(self.origin)4015 origin_revision = settings['change']4016 sync.depotPaths = settings['depot-paths']4017 sync.branchPrefixes = sync.depotPaths40184019 sync.openStreams()4020 sync.loadUserMapFromCache()4021 sync.silent =True4022 sync.importChanges(changes, shelved=True, origin_revision=origin_revision)4023 sync.closeStreams()40244025print("unshelved changelist{0}into{1}".format(change, branch_name))40264027return True40284029classP4Branches(Command):4030def__init__(self):4031 Command.__init__(self)4032 self.options = [ ]4033 self.description = ("Shows the git branches that hold imports and their "4034+"corresponding perforce depot paths")4035 self.verbose =False40364037defrun(self, args):4038iforiginP4BranchesExist():4039createOrUpdateBranchesFromOrigin()40404041 cmdline ="git rev-parse --symbolic "4042 cmdline +=" --remotes"40434044for line inread_pipe_lines(cmdline):4045 line = line.strip()40464047if not line.startswith('p4/')or line =="p4/HEAD":4048continue4049 branch = line40504051 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)4052 settings =extractSettingsGitLog(log)40534054print("%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"]))4055return True40564057classHelpFormatter(optparse.IndentedHelpFormatter):4058def__init__(self):4059 optparse.IndentedHelpFormatter.__init__(self)40604061defformat_description(self, description):4062if description:4063return description +"\n"4064else:4065return""40664067defprintUsage(commands):4068print("usage:%s<command> [options]"% sys.argv[0])4069print("")4070print("valid commands:%s"%", ".join(commands))4071print("")4072print("Try%s<command> --help for command specific help."% sys.argv[0])4073print("")40744075commands = {4076"debug": P4Debug,4077"submit": P4Submit,4078"commit": P4Submit,4079"sync": P4Sync,4080"rebase": P4Rebase,4081"clone": P4Clone,4082"rollback": P4RollBack,4083"branches": P4Branches,4084"unshelve": P4Unshelve,4085}408640874088defmain():4089iflen(sys.argv[1:]) ==0:4090printUsage(commands.keys())4091 sys.exit(2)40924093 cmdName = sys.argv[1]4094try:4095 klass = commands[cmdName]4096 cmd =klass()4097exceptKeyError:4098print("unknown command%s"% cmdName)4099print("")4100printUsage(commands.keys())4101 sys.exit(2)41024103 options = cmd.options4104 cmd.gitdir = os.environ.get("GIT_DIR",None)41054106 args = sys.argv[2:]41074108 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))4109if cmd.needsGit:4110 options.append(optparse.make_option("--git-dir", dest="gitdir"))41114112 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),4113 options,4114 description = cmd.description,4115 formatter =HelpFormatter())41164117(cmd, args) = parser.parse_args(sys.argv[2:], cmd);4118global verbose4119 verbose = cmd.verbose4120if cmd.needsGit:4121if cmd.gitdir ==None:4122 cmd.gitdir = os.path.abspath(".git")4123if notisValidGitDir(cmd.gitdir):4124# "rev-parse --git-dir" without arguments will try $PWD/.git4125 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()4126if os.path.exists(cmd.gitdir):4127 cdup =read_pipe("git rev-parse --show-cdup").strip()4128iflen(cdup) >0:4129chdir(cdup);41304131if notisValidGitDir(cmd.gitdir):4132ifisValidGitDir(cmd.gitdir +"/.git"):4133 cmd.gitdir +="/.git"4134else:4135die("fatal: cannot locate git repository at%s"% cmd.gitdir)41364137# so git commands invoked from the P4 workspace will succeed4138 os.environ["GIT_DIR"] = cmd.gitdir41394140if not cmd.run(args):4141 parser.print_help()4142 sys.exit(2)414341444145if __name__ =='__main__':4146main()