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:1309 delete_actions = ("delete","move/delete","purge")1310 add_actions = ("add","move/add")13111312def__init__(self):1313 self.usage ="usage: %prog [options]"1314 self.needsGit =True1315 self.verbose =False13161317# This is required for the "append" cloneExclude action1318defensure_value(self, attr, value):1319if nothasattr(self, attr)orgetattr(self, attr)is None:1320setattr(self, attr, value)1321returngetattr(self, attr)13221323class P4UserMap:1324def__init__(self):1325 self.userMapFromPerforceServer =False1326 self.myP4UserId =None13271328defp4UserId(self):1329if self.myP4UserId:1330return self.myP4UserId13311332 results =p4CmdList("user -o")1333for r in results:1334if'User'in r:1335 self.myP4UserId = r['User']1336return r['User']1337die("Could not find your p4 user id")13381339defp4UserIsMe(self, p4User):1340# return True if the given p4 user is actually me1341 me = self.p4UserId()1342if not p4User or p4User != me:1343return False1344else:1345return True13461347defgetUserCacheFilename(self):1348 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1349return home +"/.gitp4-usercache.txt"13501351defgetUserMapFromPerforceServer(self):1352if self.userMapFromPerforceServer:1353return1354 self.users = {}1355 self.emails = {}13561357for output inp4CmdList("users"):1358if"User"not in output:1359continue1360 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1361 self.emails[output["Email"]] = output["User"]13621363 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1364for mapUserConfig ingitConfigList("git-p4.mapUser"):1365 mapUser = mapUserConfigRegex.findall(mapUserConfig)1366if mapUser andlen(mapUser[0]) ==3:1367 user = mapUser[0][0]1368 fullname = mapUser[0][1]1369 email = mapUser[0][2]1370 self.users[user] = fullname +" <"+ email +">"1371 self.emails[email] = user13721373 s =''1374for(key, val)in self.users.items():1375 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))13761377open(self.getUserCacheFilename(),"wb").write(s)1378 self.userMapFromPerforceServer =True13791380defloadUserMapFromCache(self):1381 self.users = {}1382 self.userMapFromPerforceServer =False1383try:1384 cache =open(self.getUserCacheFilename(),"rb")1385 lines = cache.readlines()1386 cache.close()1387for line in lines:1388 entry = line.strip().split("\t")1389 self.users[entry[0]] = entry[1]1390exceptIOError:1391 self.getUserMapFromPerforceServer()13921393classP4Debug(Command):1394def__init__(self):1395 Command.__init__(self)1396 self.options = []1397 self.description ="A tool to debug the output of p4 -G."1398 self.needsGit =False13991400defrun(self, args):1401 j =01402for output inp4CmdList(args):1403print('Element:%d'% j)1404 j +=11405print(output)1406return True14071408classP4RollBack(Command):1409def__init__(self):1410 Command.__init__(self)1411 self.options = [1412 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1413]1414 self.description ="A tool to debug the multi-branch import. Don't use :)"1415 self.rollbackLocalBranches =False14161417defrun(self, args):1418iflen(args) !=1:1419return False1420 maxChange =int(args[0])14211422if"p4ExitCode"inp4Cmd("changes -m 1"):1423die("Problems executing p4");14241425if self.rollbackLocalBranches:1426 refPrefix ="refs/heads/"1427 lines =read_pipe_lines("git rev-parse --symbolic --branches")1428else:1429 refPrefix ="refs/remotes/"1430 lines =read_pipe_lines("git rev-parse --symbolic --remotes")14311432for line in lines:1433if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1434 line = line.strip()1435 ref = refPrefix + line1436 log =extractLogMessageFromGitCommit(ref)1437 settings =extractSettingsGitLog(log)14381439 depotPaths = settings['depot-paths']1440 change = settings['change']14411442 changed =False14431444iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1445for p in depotPaths]))) ==0:1446print("Branch%sdid not exist at change%s, deleting."% (ref, maxChange))1447system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1448continue14491450while change andint(change) > maxChange:1451 changed =True1452if self.verbose:1453print("%sis at%s; rewinding towards%s"% (ref, change, maxChange))1454system("git update-ref%s\"%s^\""% (ref, ref))1455 log =extractLogMessageFromGitCommit(ref)1456 settings =extractSettingsGitLog(log)145714581459 depotPaths = settings['depot-paths']1460 change = settings['change']14611462if changed:1463print("%srewound to%s"% (ref, change))14641465return True14661467classP4Submit(Command, P4UserMap):14681469 conflict_behavior_choices = ("ask","skip","quit")14701471def__init__(self):1472 Command.__init__(self)1473 P4UserMap.__init__(self)1474 self.options = [1475 optparse.make_option("--origin", dest="origin"),1476 optparse.make_option("-M", dest="detectRenames", action="store_true"),1477# preserve the user, requires relevant p4 permissions1478 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1479 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1480 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1481 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1482 optparse.make_option("--conflict", dest="conflict_behavior",1483 choices=self.conflict_behavior_choices),1484 optparse.make_option("--branch", dest="branch"),1485 optparse.make_option("--shelve", dest="shelve", action="store_true",1486help="Shelve instead of submit. Shelved files are reverted, "1487"restoring the workspace to the state before the shelve"),1488 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1489 metavar="CHANGELIST",1490help="update an existing shelved changelist, implies --shelve, "1491"repeat in-order for multiple shelved changelists"),1492 optparse.make_option("--commit", dest="commit", metavar="COMMIT",1493help="submit only the specified commit(s), one commit or xxx..xxx"),1494 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",1495help="Disable rebase after submit is completed. Can be useful if you "1496"work from a local git branch that is not master"),1497 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",1498help="Skip Perforce sync of p4/master after submit or shelve"),1499]1500 self.description ="""Submit changes from git to the perforce depot.\n1501 The `p4-pre-submit` hook is executed if it exists and is executable.1502 The hook takes no parameters and nothing from standard input. Exiting with1503 non-zero status from this script prevents `git-p4 submit` from launching.15041505 One usage scenario is to run unit tests in the hook."""15061507 self.usage +=" [name of git branch to submit into perforce depot]"1508 self.origin =""1509 self.detectRenames =False1510 self.preserveUser =gitConfigBool("git-p4.preserveUser")1511 self.dry_run =False1512 self.shelve =False1513 self.update_shelve =list()1514 self.commit =""1515 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1516 self.disable_p4sync =gitConfigBool("git-p4.disableP4Sync")1517 self.prepare_p4_only =False1518 self.conflict_behavior =None1519 self.isWindows = (platform.system() =="Windows")1520 self.exportLabels =False1521 self.p4HasMoveCommand =p4_has_move_command()1522 self.branch =None15231524ifgitConfig('git-p4.largeFileSystem'):1525die("Large file system not supported for git-p4 submit command. Please remove it from config.")15261527defcheck(self):1528iflen(p4CmdList("opened ...")) >0:1529die("You have files opened with perforce! Close them before starting the sync.")15301531defseparate_jobs_from_description(self, message):1532"""Extract and return a possible Jobs field in the commit1533 message. It goes into a separate section in the p4 change1534 specification.15351536 A jobs line starts with "Jobs:" and looks like a new field1537 in a form. Values are white-space separated on the same1538 line or on following lines that start with a tab.15391540 This does not parse and extract the full git commit message1541 like a p4 form. It just sees the Jobs: line as a marker1542 to pass everything from then on directly into the p4 form,1543 but outside the description section.15441545 Return a tuple (stripped log message, jobs string)."""15461547 m = re.search(r'^Jobs:', message, re.MULTILINE)1548if m is None:1549return(message,None)15501551 jobtext = message[m.start():]1552 stripped_message = message[:m.start()].rstrip()1553return(stripped_message, jobtext)15541555defprepareLogMessage(self, template, message, jobs):1556"""Edits the template returned from "p4 change -o" to insert1557 the message in the Description field, and the jobs text in1558 the Jobs field."""1559 result =""15601561 inDescriptionSection =False15621563for line in template.split("\n"):1564if line.startswith("#"):1565 result += line +"\n"1566continue15671568if inDescriptionSection:1569if line.startswith("Files:")or line.startswith("Jobs:"):1570 inDescriptionSection =False1571# insert Jobs section1572if jobs:1573 result += jobs +"\n"1574else:1575continue1576else:1577if line.startswith("Description:"):1578 inDescriptionSection =True1579 line +="\n"1580for messageLine in message.split("\n"):1581 line +="\t"+ messageLine +"\n"15821583 result += line +"\n"15841585return result15861587defpatchRCSKeywords(self,file, pattern):1588# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1589(handle, outFileName) = tempfile.mkstemp(dir='.')1590try:1591 outFile = os.fdopen(handle,"w+")1592 inFile =open(file,"r")1593 regexp = re.compile(pattern, re.VERBOSE)1594for line in inFile.readlines():1595 line = regexp.sub(r'$\1$', line)1596 outFile.write(line)1597 inFile.close()1598 outFile.close()1599# Forcibly overwrite the original file1600 os.unlink(file)1601 shutil.move(outFileName,file)1602except:1603# cleanup our temporary file1604 os.unlink(outFileName)1605print("Failed to strip RCS keywords in%s"%file)1606raise16071608print("Patched up RCS keywords in%s"%file)16091610defp4UserForCommit(self,id):1611# Return the tuple (perforce user,git email) for a given git commit id1612 self.getUserMapFromPerforceServer()1613 gitEmail =read_pipe(["git","log","--max-count=1",1614"--format=%ae",id])1615 gitEmail = gitEmail.strip()1616if gitEmail not in self.emails:1617return(None,gitEmail)1618else:1619return(self.emails[gitEmail],gitEmail)16201621defcheckValidP4Users(self,commits):1622# check if any git authors cannot be mapped to p4 users1623foridin commits:1624(user,email) = self.p4UserForCommit(id)1625if not user:1626 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1627ifgitConfigBool("git-p4.allowMissingP4Users"):1628print("%s"% msg)1629else:1630die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)16311632deflastP4Changelist(self):1633# Get back the last changelist number submitted in this client spec. This1634# then gets used to patch up the username in the change. If the same1635# client spec is being used by multiple processes then this might go1636# wrong.1637 results =p4CmdList("client -o")# find the current client1638 client =None1639for r in results:1640if'Client'in r:1641 client = r['Client']1642break1643if not client:1644die("could not get client spec")1645 results =p4CmdList(["changes","-c", client,"-m","1"])1646for r in results:1647if'change'in r:1648return r['change']1649die("Could not get changelist number for last submit - cannot patch up user details")16501651defmodifyChangelistUser(self, changelist, newUser):1652# fixup the user field of a changelist after it has been submitted.1653 changes =p4CmdList("change -o%s"% changelist)1654iflen(changes) !=1:1655die("Bad output from p4 change modifying%sto user%s"%1656(changelist, newUser))16571658 c = changes[0]1659if c['User'] == newUser:return# nothing to do1660 c['User'] = newUser1661input= marshal.dumps(c)16621663 result =p4CmdList("change -f -i", stdin=input)1664for r in result:1665if'code'in r:1666if r['code'] =='error':1667die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1668if'data'in r:1669print("Updated user field for changelist%sto%s"% (changelist, newUser))1670return1671die("Could not modify user field of changelist%sto%s"% (changelist, newUser))16721673defcanChangeChangelists(self):1674# check to see if we have p4 admin or super-user permissions, either of1675# which are required to modify changelists.1676 results =p4CmdList(["protects", self.depotPath])1677for r in results:1678if'perm'in r:1679if r['perm'] =='admin':1680return11681if r['perm'] =='super':1682return11683return016841685defprepareSubmitTemplate(self, changelist=None):1686"""Run "p4 change -o" to grab a change specification template.1687 This does not use "p4 -G", as it is nice to keep the submission1688 template in original order, since a human might edit it.16891690 Remove lines in the Files section that show changes to files1691 outside the depot path we're committing into."""16921693[upstream, settings] =findUpstreamBranchPoint()16941695 template ="""\1696# A Perforce Change Specification.1697#1698# Change: The change number. 'new' on a new changelist.1699# Date: The date this specification was last modified.1700# Client: The client on which the changelist was created. Read-only.1701# User: The user who created the changelist.1702# Status: Either 'pending' or 'submitted'. Read-only.1703# Type: Either 'public' or 'restricted'. Default is 'public'.1704# Description: Comments about the changelist. Required.1705# Jobs: What opened jobs are to be closed by this changelist.1706# You may delete jobs from this list. (New changelists only.)1707# Files: What opened files from the default changelist are to be added1708# to this changelist. You may delete files from this list.1709# (New changelists only.)1710"""1711 files_list = []1712 inFilesSection =False1713 change_entry =None1714 args = ['change','-o']1715if changelist:1716 args.append(str(changelist))1717for entry inp4CmdList(args):1718if'code'not in entry:1719continue1720if entry['code'] =='stat':1721 change_entry = entry1722break1723if not change_entry:1724die('Failed to decode output of p4 change -o')1725for key, value in change_entry.iteritems():1726if key.startswith('File'):1727if'depot-paths'in settings:1728if not[p for p in settings['depot-paths']1729ifp4PathStartsWith(value, p)]:1730continue1731else:1732if notp4PathStartsWith(value, self.depotPath):1733continue1734 files_list.append(value)1735continue1736# Output in the order expected by prepareLogMessage1737for key in['Change','Client','User','Status','Description','Jobs']:1738if key not in change_entry:1739continue1740 template +='\n'1741 template += key +':'1742if key =='Description':1743 template +='\n'1744for field_line in change_entry[key].splitlines():1745 template +='\t'+field_line+'\n'1746iflen(files_list) >0:1747 template +='\n'1748 template +='Files:\n'1749for path in files_list:1750 template +='\t'+path+'\n'1751return template17521753defedit_template(self, template_file):1754"""Invoke the editor to let the user change the submission1755 message. Return true if okay to continue with the submit."""17561757# if configured to skip the editing part, just submit1758ifgitConfigBool("git-p4.skipSubmitEdit"):1759return True17601761# look at the modification time, to check later if the user saved1762# the file1763 mtime = os.stat(template_file).st_mtime17641765# invoke the editor1766if"P4EDITOR"in os.environ and(os.environ.get("P4EDITOR") !=""):1767 editor = os.environ.get("P4EDITOR")1768else:1769 editor =read_pipe("git var GIT_EDITOR").strip()1770system(["sh","-c", ('%s"$@"'% editor), editor, template_file])17711772# If the file was not saved, prompt to see if this patch should1773# be skipped. But skip this verification step if configured so.1774ifgitConfigBool("git-p4.skipSubmitEditCheck"):1775return True17761777# modification time updated means user saved the file1778if os.stat(template_file).st_mtime > mtime:1779return True17801781while True:1782 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1783if response =='y':1784return True1785if response =='n':1786return False17871788defget_diff_description(self, editedFiles, filesToAdd, symlinks):1789# diff1790if"P4DIFF"in os.environ:1791del(os.environ["P4DIFF"])1792 diff =""1793for editedFile in editedFiles:1794 diff +=p4_read_pipe(['diff','-du',1795wildcard_encode(editedFile)])17961797# new file diff1798 newdiff =""1799for newFile in filesToAdd:1800 newdiff +="==== new file ====\n"1801 newdiff +="--- /dev/null\n"1802 newdiff +="+++%s\n"% newFile18031804 is_link = os.path.islink(newFile)1805 expect_link = newFile in symlinks18061807if is_link and expect_link:1808 newdiff +="+%s\n"% os.readlink(newFile)1809else:1810 f =open(newFile,"r")1811for line in f.readlines():1812 newdiff +="+"+ line1813 f.close()18141815return(diff + newdiff).replace('\r\n','\n')18161817defapplyCommit(self,id):1818"""Apply one commit, return True if it succeeded."""18191820print("Applying",read_pipe(["git","show","-s",1821"--format=format:%h%s",id]))18221823(p4User, gitEmail) = self.p4UserForCommit(id)18241825 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1826 filesToAdd =set()1827 filesToChangeType =set()1828 filesToDelete =set()1829 editedFiles =set()1830 pureRenameCopy =set()1831 symlinks =set()1832 filesToChangeExecBit = {}1833 all_files =list()18341835for line in diff:1836 diff =parseDiffTreeEntry(line)1837 modifier = diff['status']1838 path = diff['src']1839 all_files.append(path)18401841if modifier =="M":1842p4_edit(path)1843ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1844 filesToChangeExecBit[path] = diff['dst_mode']1845 editedFiles.add(path)1846elif modifier =="A":1847 filesToAdd.add(path)1848 filesToChangeExecBit[path] = diff['dst_mode']1849if path in filesToDelete:1850 filesToDelete.remove(path)18511852 dst_mode =int(diff['dst_mode'],8)1853if dst_mode ==0o120000:1854 symlinks.add(path)18551856elif modifier =="D":1857 filesToDelete.add(path)1858if path in filesToAdd:1859 filesToAdd.remove(path)1860elif modifier =="C":1861 src, dest = diff['src'], diff['dst']1862p4_integrate(src, dest)1863 pureRenameCopy.add(dest)1864if diff['src_sha1'] != diff['dst_sha1']:1865p4_edit(dest)1866 pureRenameCopy.discard(dest)1867ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1868p4_edit(dest)1869 pureRenameCopy.discard(dest)1870 filesToChangeExecBit[dest] = diff['dst_mode']1871if self.isWindows:1872# turn off read-only attribute1873 os.chmod(dest, stat.S_IWRITE)1874 os.unlink(dest)1875 editedFiles.add(dest)1876elif modifier =="R":1877 src, dest = diff['src'], diff['dst']1878if self.p4HasMoveCommand:1879p4_edit(src)# src must be open before move1880p4_move(src, dest)# opens for (move/delete, move/add)1881else:1882p4_integrate(src, dest)1883if diff['src_sha1'] != diff['dst_sha1']:1884p4_edit(dest)1885else:1886 pureRenameCopy.add(dest)1887ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1888if not self.p4HasMoveCommand:1889p4_edit(dest)# with move: already open, writable1890 filesToChangeExecBit[dest] = diff['dst_mode']1891if not self.p4HasMoveCommand:1892if self.isWindows:1893 os.chmod(dest, stat.S_IWRITE)1894 os.unlink(dest)1895 filesToDelete.add(src)1896 editedFiles.add(dest)1897elif modifier =="T":1898 filesToChangeType.add(path)1899else:1900die("unknown modifier%sfor%s"% (modifier, path))19011902 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1903 patchcmd = diffcmd +" | git apply "1904 tryPatchCmd = patchcmd +"--check -"1905 applyPatchCmd = patchcmd +"--check --apply -"1906 patch_succeeded =True19071908if os.system(tryPatchCmd) !=0:1909 fixed_rcs_keywords =False1910 patch_succeeded =False1911print("Unfortunately applying the change failed!")19121913# Patch failed, maybe it's just RCS keyword woes. Look through1914# the patch to see if that's possible.1915ifgitConfigBool("git-p4.attemptRCSCleanup"):1916file=None1917 pattern =None1918 kwfiles = {}1919forfilein editedFiles | filesToDelete:1920# did this file's delta contain RCS keywords?1921 pattern =p4_keywords_regexp_for_file(file)19221923if pattern:1924# this file is a possibility...look for RCS keywords.1925 regexp = re.compile(pattern, re.VERBOSE)1926for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1927if regexp.search(line):1928if verbose:1929print("got keyword match on%sin%sin%s"% (pattern, line,file))1930 kwfiles[file] = pattern1931break19321933forfilein kwfiles:1934if verbose:1935print("zapping%swith%s"% (line,pattern))1936# File is being deleted, so not open in p4. Must1937# disable the read-only bit on windows.1938if self.isWindows andfilenot in editedFiles:1939 os.chmod(file, stat.S_IWRITE)1940 self.patchRCSKeywords(file, kwfiles[file])1941 fixed_rcs_keywords =True19421943if fixed_rcs_keywords:1944print("Retrying the patch with RCS keywords cleaned up")1945if os.system(tryPatchCmd) ==0:1946 patch_succeeded =True19471948if not patch_succeeded:1949for f in editedFiles:1950p4_revert(f)1951return False19521953#1954# Apply the patch for real, and do add/delete/+x handling.1955#1956system(applyPatchCmd)19571958for f in filesToChangeType:1959p4_edit(f,"-t","auto")1960for f in filesToAdd:1961p4_add(f)1962for f in filesToDelete:1963p4_revert(f)1964p4_delete(f)19651966# Set/clear executable bits1967for f in filesToChangeExecBit.keys():1968 mode = filesToChangeExecBit[f]1969setP4ExecBit(f, mode)19701971 update_shelve =01972iflen(self.update_shelve) >0:1973 update_shelve = self.update_shelve.pop(0)1974p4_reopen_in_change(update_shelve, all_files)19751976#1977# Build p4 change description, starting with the contents1978# of the git commit message.1979#1980 logMessage =extractLogMessageFromGitCommit(id)1981 logMessage = logMessage.strip()1982(logMessage, jobs) = self.separate_jobs_from_description(logMessage)19831984 template = self.prepareSubmitTemplate(update_shelve)1985 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)19861987if self.preserveUser:1988 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User19891990if self.checkAuthorship and not self.p4UserIsMe(p4User):1991 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1992 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1993 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"19941995 separatorLine ="######## everything below this line is just the diff #######\n"1996if not self.prepare_p4_only:1997 submitTemplate += separatorLine1998 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)19992000(handle, fileName) = tempfile.mkstemp()2001 tmpFile = os.fdopen(handle,"w+b")2002if self.isWindows:2003 submitTemplate = submitTemplate.replace("\n","\r\n")2004 tmpFile.write(submitTemplate)2005 tmpFile.close()20062007if self.prepare_p4_only:2008#2009# Leave the p4 tree prepared, and the submit template around2010# and let the user decide what to do next2011#2012print()2013print("P4 workspace prepared for submission.")2014print("To submit or revert, go to client workspace")2015print(" "+ self.clientPath)2016print()2017print("To submit, use\"p4 submit\"to write a new description,")2018print("or\"p4 submit -i <%s\"to use the one prepared by" \2019"\"git p4\"."% fileName)2020print("You can delete the file\"%s\"when finished."% fileName)20212022if self.preserveUser and p4User and not self.p4UserIsMe(p4User):2023print("To preserve change ownership by user%s, you must\n" \2024"do\"p4 change -f <change>\"after submitting and\n" \2025"edit the User field.")2026if pureRenameCopy:2027print("After submitting, renamed files must be re-synced.")2028print("Invoke\"p4 sync -f\"on each of these files:")2029for f in pureRenameCopy:2030print(" "+ f)20312032print()2033print("To revert the changes, use\"p4 revert ...\", and delete")2034print("the submit template file\"%s\""% fileName)2035if filesToAdd:2036print("Since the commit adds new files, they must be deleted:")2037for f in filesToAdd:2038print(" "+ f)2039print()2040return True20412042#2043# Let the user edit the change description, then submit it.2044#2045 submitted =False20462047try:2048if self.edit_template(fileName):2049# read the edited message and submit2050 tmpFile =open(fileName,"rb")2051 message = tmpFile.read()2052 tmpFile.close()2053if self.isWindows:2054 message = message.replace("\r\n","\n")2055 submitTemplate = message[:message.index(separatorLine)]20562057if update_shelve:2058p4_write_pipe(['shelve','-r','-i'], submitTemplate)2059elif self.shelve:2060p4_write_pipe(['shelve','-i'], submitTemplate)2061else:2062p4_write_pipe(['submit','-i'], submitTemplate)2063# The rename/copy happened by applying a patch that created a2064# new file. This leaves it writable, which confuses p4.2065for f in pureRenameCopy:2066p4_sync(f,"-f")20672068if self.preserveUser:2069if p4User:2070# Get last changelist number. Cannot easily get it from2071# the submit command output as the output is2072# unmarshalled.2073 changelist = self.lastP4Changelist()2074 self.modifyChangelistUser(changelist, p4User)20752076 submitted =True20772078finally:2079# skip this patch2080if not submitted or self.shelve:2081if self.shelve:2082print("Reverting shelved files.")2083else:2084print("Submission cancelled, undoing p4 changes.")2085for f in editedFiles | filesToDelete:2086p4_revert(f)2087for f in filesToAdd:2088p4_revert(f)2089 os.remove(f)20902091 os.remove(fileName)2092return submitted20932094# Export git tags as p4 labels. Create a p4 label and then tag2095# with that.2096defexportGitTags(self, gitTags):2097 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")2098iflen(validLabelRegexp) ==0:2099 validLabelRegexp = defaultLabelRegexp2100 m = re.compile(validLabelRegexp)21012102for name in gitTags:21032104if not m.match(name):2105if verbose:2106print("tag%sdoes not match regexp%s"% (name, validLabelRegexp))2107continue21082109# Get the p4 commit this corresponds to2110 logMessage =extractLogMessageFromGitCommit(name)2111 values =extractSettingsGitLog(logMessage)21122113if'change'not in values:2114# a tag pointing to something not sent to p4; ignore2115if verbose:2116print("git tag%sdoes not give a p4 commit"% name)2117continue2118else:2119 changelist = values['change']21202121# Get the tag details.2122 inHeader =True2123 isAnnotated =False2124 body = []2125for l inread_pipe_lines(["git","cat-file","-p", name]):2126 l = l.strip()2127if inHeader:2128if re.match(r'tag\s+', l):2129 isAnnotated =True2130elif re.match(r'\s*$', l):2131 inHeader =False2132continue2133else:2134 body.append(l)21352136if not isAnnotated:2137 body = ["lightweight tag imported by git p4\n"]21382139# Create the label - use the same view as the client spec we are using2140 clientSpec =getClientSpec()21412142 labelTemplate ="Label:%s\n"% name2143 labelTemplate +="Description:\n"2144for b in body:2145 labelTemplate +="\t"+ b +"\n"2146 labelTemplate +="View:\n"2147for depot_side in clientSpec.mappings:2148 labelTemplate +="\t%s\n"% depot_side21492150if self.dry_run:2151print("Would create p4 label%sfor tag"% name)2152elif self.prepare_p4_only:2153print("Not creating p4 label%sfor tag due to option" \2154" --prepare-p4-only"% name)2155else:2156p4_write_pipe(["label","-i"], labelTemplate)21572158# Use the label2159p4_system(["tag","-l", name] +2160["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])21612162if verbose:2163print("created p4 label for tag%s"% name)21642165defrun(self, args):2166iflen(args) ==0:2167 self.master =currentGitBranch()2168eliflen(args) ==1:2169 self.master = args[0]2170if notbranchExists(self.master):2171die("Branch%sdoes not exist"% self.master)2172else:2173return False21742175for i in self.update_shelve:2176if i <=0:2177 sys.exit("invalid changelist%d"% i)21782179if self.master:2180 allowSubmit =gitConfig("git-p4.allowSubmit")2181iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2182die("%sis not in git-p4.allowSubmit"% self.master)21832184[upstream, settings] =findUpstreamBranchPoint()2185 self.depotPath = settings['depot-paths'][0]2186iflen(self.origin) ==0:2187 self.origin = upstream21882189iflen(self.update_shelve) >0:2190 self.shelve =True21912192if self.preserveUser:2193if not self.canChangeChangelists():2194die("Cannot preserve user names without p4 super-user or admin permissions")21952196# if not set from the command line, try the config file2197if self.conflict_behavior is None:2198 val =gitConfig("git-p4.conflict")2199if val:2200if val not in self.conflict_behavior_choices:2201die("Invalid value '%s' for config git-p4.conflict"% val)2202else:2203 val ="ask"2204 self.conflict_behavior = val22052206if self.verbose:2207print("Origin branch is "+ self.origin)22082209iflen(self.depotPath) ==0:2210print("Internal error: cannot locate perforce depot path from existing branches")2211 sys.exit(128)22122213 self.useClientSpec =False2214ifgitConfigBool("git-p4.useclientspec"):2215 self.useClientSpec =True2216if self.useClientSpec:2217 self.clientSpecDirs =getClientSpec()22182219# Check for the existence of P4 branches2220 branchesDetected = (len(p4BranchesInGit().keys()) >1)22212222if self.useClientSpec and not branchesDetected:2223# all files are relative to the client spec2224 self.clientPath =getClientRoot()2225else:2226 self.clientPath =p4Where(self.depotPath)22272228if self.clientPath =="":2229die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)22302231print("Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath))2232 self.oldWorkingDirectory = os.getcwd()22332234# ensure the clientPath exists2235 new_client_dir =False2236if not os.path.exists(self.clientPath):2237 new_client_dir =True2238 os.makedirs(self.clientPath)22392240chdir(self.clientPath, is_client_path=True)2241if self.dry_run:2242print("Would synchronize p4 checkout in%s"% self.clientPath)2243else:2244print("Synchronizing p4 checkout...")2245if new_client_dir:2246# old one was destroyed, and maybe nobody told p42247p4_sync("...","-f")2248else:2249p4_sync("...")2250 self.check()22512252 commits = []2253if self.master:2254 committish = self.master2255else:2256 committish ='HEAD'22572258if self.commit !="":2259if self.commit.find("..") != -1:2260 limits_ish = self.commit.split("..")2261for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2262 commits.append(line.strip())2263 commits.reverse()2264else:2265 commits.append(self.commit)2266else:2267for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, committish)]):2268 commits.append(line.strip())2269 commits.reverse()22702271if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2272 self.checkAuthorship =False2273else:2274 self.checkAuthorship =True22752276if self.preserveUser:2277 self.checkValidP4Users(commits)22782279#2280# Build up a set of options to be passed to diff when2281# submitting each commit to p4.2282#2283if self.detectRenames:2284# command-line -M arg2285 self.diffOpts ="-M"2286else:2287# If not explicitly set check the config variable2288 detectRenames =gitConfig("git-p4.detectRenames")22892290if detectRenames.lower() =="false"or detectRenames =="":2291 self.diffOpts =""2292elif detectRenames.lower() =="true":2293 self.diffOpts ="-M"2294else:2295 self.diffOpts ="-M%s"% detectRenames22962297# no command-line arg for -C or --find-copies-harder, just2298# config variables2299 detectCopies =gitConfig("git-p4.detectCopies")2300if detectCopies.lower() =="false"or detectCopies =="":2301pass2302elif detectCopies.lower() =="true":2303 self.diffOpts +=" -C"2304else:2305 self.diffOpts +=" -C%s"% detectCopies23062307ifgitConfigBool("git-p4.detectCopiesHarder"):2308 self.diffOpts +=" --find-copies-harder"23092310 num_shelves =len(self.update_shelve)2311if num_shelves >0and num_shelves !=len(commits):2312 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2313(len(commits), num_shelves))23142315 hooks_path =gitConfig("core.hooksPath")2316iflen(hooks_path) <=0:2317 hooks_path = os.path.join(os.environ.get("GIT_DIR",".git"),"hooks")23182319 hook_file = os.path.join(hooks_path,"p4-pre-submit")2320if os.path.isfile(hook_file)and os.access(hook_file, os.X_OK)and subprocess.call([hook_file]) !=0:2321 sys.exit(1)23222323#2324# Apply the commits, one at a time. On failure, ask if should2325# continue to try the rest of the patches, or quit.2326#2327if self.dry_run:2328print("Would apply")2329 applied = []2330 last =len(commits) -12331for i, commit inenumerate(commits):2332if self.dry_run:2333print(" ",read_pipe(["git","show","-s",2334"--format=format:%h%s", commit]))2335 ok =True2336else:2337 ok = self.applyCommit(commit)2338if ok:2339 applied.append(commit)2340else:2341if self.prepare_p4_only and i < last:2342print("Processing only the first commit due to option" \2343" --prepare-p4-only")2344break2345if i < last:2346 quit =False2347while True:2348# prompt for what to do, or use the option/variable2349if self.conflict_behavior =="ask":2350print("What do you want to do?")2351 response =raw_input("[s]kip this commit but apply"2352" the rest, or [q]uit? ")2353if not response:2354continue2355elif self.conflict_behavior =="skip":2356 response ="s"2357elif self.conflict_behavior =="quit":2358 response ="q"2359else:2360die("Unknown conflict_behavior '%s'"%2361 self.conflict_behavior)23622363if response[0] =="s":2364print("Skipping this commit, but applying the rest")2365break2366if response[0] =="q":2367print("Quitting")2368 quit =True2369break2370if quit:2371break23722373chdir(self.oldWorkingDirectory)2374 shelved_applied ="shelved"if self.shelve else"applied"2375if self.dry_run:2376pass2377elif self.prepare_p4_only:2378pass2379eliflen(commits) ==len(applied):2380print("All commits{0}!".format(shelved_applied))23812382 sync =P4Sync()2383if self.branch:2384 sync.branch = self.branch2385if self.disable_p4sync:2386 sync.sync_origin_only()2387else:2388 sync.run([])23892390if not self.disable_rebase:2391 rebase =P4Rebase()2392 rebase.rebase()23932394else:2395iflen(applied) ==0:2396print("No commits{0}.".format(shelved_applied))2397else:2398print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2399for c in commits:2400if c in applied:2401 star ="*"2402else:2403 star =" "2404print(star,read_pipe(["git","show","-s",2405"--format=format:%h%s", c]))2406print("You will have to do 'git p4 sync' and rebase.")24072408ifgitConfigBool("git-p4.exportLabels"):2409 self.exportLabels =True24102411if self.exportLabels:2412 p4Labels =getP4Labels(self.depotPath)2413 gitTags =getGitTags()24142415 missingGitTags = gitTags - p4Labels2416 self.exportGitTags(missingGitTags)24172418# exit with error unless everything applied perfectly2419iflen(commits) !=len(applied):2420 sys.exit(1)24212422return True24232424classView(object):2425"""Represent a p4 view ("p4 help views"), and map files in a2426 repo according to the view."""24272428def__init__(self, client_name):2429 self.mappings = []2430 self.client_prefix ="//%s/"% client_name2431# cache results of "p4 where" to lookup client file locations2432 self.client_spec_path_cache = {}24332434defappend(self, view_line):2435"""Parse a view line, splitting it into depot and client2436 sides. Append to self.mappings, preserving order. This2437 is only needed for tag creation."""24382439# Split the view line into exactly two words. P4 enforces2440# structure on these lines that simplifies this quite a bit.2441#2442# Either or both words may be double-quoted.2443# Single quotes do not matter.2444# Double-quote marks cannot occur inside the words.2445# A + or - prefix is also inside the quotes.2446# There are no quotes unless they contain a space.2447# The line is already white-space stripped.2448# The two words are separated by a single space.2449#2450if view_line[0] =='"':2451# First word is double quoted. Find its end.2452 close_quote_index = view_line.find('"',1)2453if close_quote_index <=0:2454die("No first-word closing quote found:%s"% view_line)2455 depot_side = view_line[1:close_quote_index]2456# skip closing quote and space2457 rhs_index = close_quote_index +1+12458else:2459 space_index = view_line.find(" ")2460if space_index <=0:2461die("No word-splitting space found:%s"% view_line)2462 depot_side = view_line[0:space_index]2463 rhs_index = space_index +124642465# prefix + means overlay on previous mapping2466if depot_side.startswith("+"):2467 depot_side = depot_side[1:]24682469# prefix - means exclude this path, leave out of mappings2470 exclude =False2471if depot_side.startswith("-"):2472 exclude =True2473 depot_side = depot_side[1:]24742475if not exclude:2476 self.mappings.append(depot_side)24772478defconvert_client_path(self, clientFile):2479# chop off //client/ part to make it relative2480if not clientFile.startswith(self.client_prefix):2481die("No prefix '%s' on clientFile '%s'"%2482(self.client_prefix, clientFile))2483return clientFile[len(self.client_prefix):]24842485defupdate_client_spec_path_cache(self, files):2486""" Caching file paths by "p4 where" batch query """24872488# List depot file paths exclude that already cached2489 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]24902491iflen(fileArgs) ==0:2492return# All files in cache24932494 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2495for res in where_result:2496if"code"in res and res["code"] =="error":2497# assume error is "... file(s) not in client view"2498continue2499if"clientFile"not in res:2500die("No clientFile in 'p4 where' output")2501if"unmap"in res:2502# it will list all of them, but only one not unmap-ped2503continue2504ifgitConfigBool("core.ignorecase"):2505 res['depotFile'] = res['depotFile'].lower()2506 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])25072508# not found files or unmap files set to ""2509for depotFile in fileArgs:2510ifgitConfigBool("core.ignorecase"):2511 depotFile = depotFile.lower()2512if depotFile not in self.client_spec_path_cache:2513 self.client_spec_path_cache[depotFile] =""25142515defmap_in_client(self, depot_path):2516"""Return the relative location in the client where this2517 depot file should live. Returns "" if the file should2518 not be mapped in the client."""25192520ifgitConfigBool("core.ignorecase"):2521 depot_path = depot_path.lower()25222523if depot_path in self.client_spec_path_cache:2524return self.client_spec_path_cache[depot_path]25252526die("Error:%sis not found in client spec path"% depot_path )2527return""25282529classP4Sync(Command, P4UserMap):25302531def__init__(self):2532 Command.__init__(self)2533 P4UserMap.__init__(self)2534 self.options = [2535 optparse.make_option("--branch", dest="branch"),2536 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2537 optparse.make_option("--changesfile", dest="changesFile"),2538 optparse.make_option("--silent", dest="silent", action="store_true"),2539 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2540 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2541 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2542help="Import into refs/heads/ , not refs/remotes"),2543 optparse.make_option("--max-changes", dest="maxChanges",2544help="Maximum number of changes to import"),2545 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2546help="Internal block size to use when iteratively calling p4 changes"),2547 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2548help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2549 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2550help="Only sync files that are included in the Perforce Client Spec"),2551 optparse.make_option("-/", dest="cloneExclude",2552 action="append",type="string",2553help="exclude depot path"),2554]2555 self.description ="""Imports from Perforce into a git repository.\n2556 example:2557 //depot/my/project/ -- to import the current head2558 //depot/my/project/@all -- to import everything2559 //depot/my/project/@1,6 -- to import only from revision 1 to 625602561 (a ... is not needed in the path p4 specification, it's added implicitly)"""25622563 self.usage +=" //depot/path[@revRange]"2564 self.silent =False2565 self.createdBranches =set()2566 self.committedChanges =set()2567 self.branch =""2568 self.detectBranches =False2569 self.detectLabels =False2570 self.importLabels =False2571 self.changesFile =""2572 self.syncWithOrigin =True2573 self.importIntoRemotes =True2574 self.maxChanges =""2575 self.changes_block_size =None2576 self.keepRepoPath =False2577 self.depotPaths =None2578 self.p4BranchesInGit = []2579 self.cloneExclude = []2580 self.useClientSpec =False2581 self.useClientSpec_from_options =False2582 self.clientSpecDirs =None2583 self.tempBranches = []2584 self.tempBranchLocation ="refs/git-p4-tmp"2585 self.largeFileSystem =None2586 self.suppress_meta_comment =False25872588ifgitConfig('git-p4.largeFileSystem'):2589 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2590 self.largeFileSystem =largeFileSystemConstructor(2591lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2592)25932594ifgitConfig("git-p4.syncFromOrigin") =="false":2595 self.syncWithOrigin =False25962597 self.depotPaths = []2598 self.changeRange =""2599 self.previousDepotPaths = []2600 self.hasOrigin =False26012602# map from branch depot path to parent branch2603 self.knownBranches = {}2604 self.initialParents = {}26052606 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))2607 self.labels = {}26082609# Force a checkpoint in fast-import and wait for it to finish2610defcheckpoint(self):2611 self.gitStream.write("checkpoint\n\n")2612 self.gitStream.write("progress checkpoint\n\n")2613 out = self.gitOutput.readline()2614if self.verbose:2615print("checkpoint finished: "+ out)26162617defextractFilesFromCommit(self, commit, shelved=False, shelved_cl =0):2618 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2619for path in self.cloneExclude]2620 files = []2621 fnum =02622while"depotFile%s"% fnum in commit:2623 path = commit["depotFile%s"% fnum]26242625if[p for p in self.cloneExclude2626ifp4PathStartsWith(path, p)]:2627 found =False2628else:2629 found = [p for p in self.depotPaths2630ifp4PathStartsWith(path, p)]2631if not found:2632 fnum = fnum +12633continue26342635file= {}2636file["path"] = path2637file["rev"] = commit["rev%s"% fnum]2638file["action"] = commit["action%s"% fnum]2639file["type"] = commit["type%s"% fnum]2640if shelved:2641file["shelved_cl"] =int(shelved_cl)2642 files.append(file)2643 fnum = fnum +12644return files26452646defextractJobsFromCommit(self, commit):2647 jobs = []2648 jnum =02649while"job%s"% jnum in commit:2650 job = commit["job%s"% jnum]2651 jobs.append(job)2652 jnum = jnum +12653return jobs26542655defstripRepoPath(self, path, prefixes):2656"""When streaming files, this is called to map a p4 depot path2657 to where it should go in git. The prefixes are either2658 self.depotPaths, or self.branchPrefixes in the case of2659 branch detection."""26602661if self.useClientSpec:2662# branch detection moves files up a level (the branch name)2663# from what client spec interpretation gives2664 path = self.clientSpecDirs.map_in_client(path)2665if self.detectBranches:2666for b in self.knownBranches:2667if path.startswith(b +"/"):2668 path = path[len(b)+1:]26692670elif self.keepRepoPath:2671# Preserve everything in relative path name except leading2672# //depot/; just look at first prefix as they all should2673# be in the same depot.2674 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2675ifp4PathStartsWith(path, depot):2676 path = path[len(depot):]26772678else:2679for p in prefixes:2680ifp4PathStartsWith(path, p):2681 path = path[len(p):]2682break26832684 path =wildcard_decode(path)2685return path26862687defsplitFilesIntoBranches(self, commit):2688"""Look at each depotFile in the commit to figure out to what2689 branch it belongs."""26902691if self.clientSpecDirs:2692 files = self.extractFilesFromCommit(commit)2693 self.clientSpecDirs.update_client_spec_path_cache(files)26942695 branches = {}2696 fnum =02697while"depotFile%s"% fnum in commit:2698 path = commit["depotFile%s"% fnum]2699 found = [p for p in self.depotPaths2700ifp4PathStartsWith(path, p)]2701if not found:2702 fnum = fnum +12703continue27042705file= {}2706file["path"] = path2707file["rev"] = commit["rev%s"% fnum]2708file["action"] = commit["action%s"% fnum]2709file["type"] = commit["type%s"% fnum]2710 fnum = fnum +127112712# start with the full relative path where this file would2713# go in a p4 client2714if self.useClientSpec:2715 relPath = self.clientSpecDirs.map_in_client(path)2716else:2717 relPath = self.stripRepoPath(path, self.depotPaths)27182719for branch in self.knownBranches.keys():2720# add a trailing slash so that a commit into qt/4.2foo2721# doesn't end up in qt/4.2, e.g.2722if relPath.startswith(branch +"/"):2723if branch not in branches:2724 branches[branch] = []2725 branches[branch].append(file)2726break27272728return branches27292730defwriteToGitStream(self, gitMode, relPath, contents):2731 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2732 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2733for d in contents:2734 self.gitStream.write(d)2735 self.gitStream.write('\n')27362737defencodeWithUTF8(self, path):2738try:2739 path.decode('ascii')2740except:2741 encoding ='utf8'2742ifgitConfig('git-p4.pathEncoding'):2743 encoding =gitConfig('git-p4.pathEncoding')2744 path = path.decode(encoding,'replace').encode('utf8','replace')2745if self.verbose:2746print('Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path))2747return path27482749# output one file from the P4 stream2750# - helper for streamP4Files27512752defstreamOneP4File(self,file, contents):2753 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2754 relPath = self.encodeWithUTF8(relPath)2755if verbose:2756if'fileSize'in self.stream_file:2757 size =int(self.stream_file['fileSize'])2758else:2759 size =0# deleted files don't get a fileSize apparently2760 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2761 sys.stdout.flush()27622763(type_base, type_mods) =split_p4_type(file["type"])27642765 git_mode ="100644"2766if"x"in type_mods:2767 git_mode ="100755"2768if type_base =="symlink":2769 git_mode ="120000"2770# p4 print on a symlink sometimes contains "target\n";2771# if it does, remove the newline2772 data =''.join(contents)2773if not data:2774# Some version of p4 allowed creating a symlink that pointed2775# to nothing. This causes p4 errors when checking out such2776# a change, and errors here too. Work around it by ignoring2777# the bad symlink; hopefully a future change fixes it.2778print("\nIgnoring empty symlink in%s"%file['depotFile'])2779return2780elif data[-1] =='\n':2781 contents = [data[:-1]]2782else:2783 contents = [data]27842785if type_base =="utf16":2786# p4 delivers different text in the python output to -G2787# than it does when using "print -o", or normal p4 client2788# operations. utf16 is converted to ascii or utf8, perhaps.2789# But ascii text saved as -t utf16 is completely mangled.2790# Invoke print -o to get the real contents.2791#2792# On windows, the newlines will always be mangled by print, so put2793# them back too. This is not needed to the cygwin windows version,2794# just the native "NT" type.2795#2796try:2797 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2798exceptExceptionas e:2799if'Translation of file content failed'instr(e):2800 type_base ='binary'2801else:2802raise e2803else:2804ifp4_version_string().find('/NT') >=0:2805 text = text.replace('\r\n','\n')2806 contents = [ text ]28072808if type_base =="apple":2809# Apple filetype files will be streamed as a concatenation of2810# its appledouble header and the contents. This is useless2811# on both macs and non-macs. If using "print -q -o xx", it2812# will create "xx" with the data, and "%xx" with the header.2813# This is also not very useful.2814#2815# Ideally, someday, this script can learn how to generate2816# appledouble files directly and import those to git, but2817# non-mac machines can never find a use for apple filetype.2818print("\nIgnoring apple filetype file%s"%file['depotFile'])2819return28202821# Note that we do not try to de-mangle keywords on utf16 files,2822# even though in theory somebody may want that.2823 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2824if pattern:2825 regexp = re.compile(pattern, re.VERBOSE)2826 text =''.join(contents)2827 text = regexp.sub(r'$\1$', text)2828 contents = [ text ]28292830if self.largeFileSystem:2831(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)28322833 self.writeToGitStream(git_mode, relPath, contents)28342835defstreamOneP4Deletion(self,file):2836 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2837 relPath = self.encodeWithUTF8(relPath)2838if verbose:2839 sys.stdout.write("delete%s\n"% relPath)2840 sys.stdout.flush()2841 self.gitStream.write("D%s\n"% relPath)28422843if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2844 self.largeFileSystem.removeLargeFile(relPath)28452846# handle another chunk of streaming data2847defstreamP4FilesCb(self, marshalled):28482849# catch p4 errors and complain2850 err =None2851if"code"in marshalled:2852if marshalled["code"] =="error":2853if"data"in marshalled:2854 err = marshalled["data"].rstrip()28552856if not err and'fileSize'in self.stream_file:2857 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2858if required_bytes >0:2859 err ='Not enough space left on%s! Free at least%iMB.'% (2860 os.getcwd(), required_bytes/1024/10242861)28622863if err:2864 f =None2865if self.stream_have_file_info:2866if"depotFile"in self.stream_file:2867 f = self.stream_file["depotFile"]2868# force a failure in fast-import, else an empty2869# commit will be made2870 self.gitStream.write("\n")2871 self.gitStream.write("die-now\n")2872 self.gitStream.close()2873# ignore errors, but make sure it exits first2874 self.importProcess.wait()2875if f:2876die("Error from p4 print for%s:%s"% (f, err))2877else:2878die("Error from p4 print:%s"% err)28792880if'depotFile'in marshalled and self.stream_have_file_info:2881# start of a new file - output the old one first2882 self.streamOneP4File(self.stream_file, self.stream_contents)2883 self.stream_file = {}2884 self.stream_contents = []2885 self.stream_have_file_info =False28862887# pick up the new file information... for the2888# 'data' field we need to append to our array2889for k in marshalled.keys():2890if k =='data':2891if'streamContentSize'not in self.stream_file:2892 self.stream_file['streamContentSize'] =02893 self.stream_file['streamContentSize'] +=len(marshalled['data'])2894 self.stream_contents.append(marshalled['data'])2895else:2896 self.stream_file[k] = marshalled[k]28972898if(verbose and2899'streamContentSize'in self.stream_file and2900'fileSize'in self.stream_file and2901'depotFile'in self.stream_file):2902 size =int(self.stream_file["fileSize"])2903if size >0:2904 progress =100*self.stream_file['streamContentSize']/size2905 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2906 sys.stdout.flush()29072908 self.stream_have_file_info =True29092910# Stream directly from "p4 files" into "git fast-import"2911defstreamP4Files(self, files):2912 filesForCommit = []2913 filesToRead = []2914 filesToDelete = []29152916for f in files:2917 filesForCommit.append(f)2918if f['action']in self.delete_actions:2919 filesToDelete.append(f)2920else:2921 filesToRead.append(f)29222923# deleted files...2924for f in filesToDelete:2925 self.streamOneP4Deletion(f)29262927iflen(filesToRead) >0:2928 self.stream_file = {}2929 self.stream_contents = []2930 self.stream_have_file_info =False29312932# curry self argument2933defstreamP4FilesCbSelf(entry):2934 self.streamP4FilesCb(entry)29352936 fileArgs = []2937for f in filesToRead:2938if'shelved_cl'in f:2939# Handle shelved CLs using the "p4 print file@=N" syntax to print2940# the contents2941 fileArg ='%s@=%d'% (f['path'], f['shelved_cl'])2942else:2943 fileArg ='%s#%s'% (f['path'], f['rev'])29442945 fileArgs.append(fileArg)29462947p4CmdList(["-x","-","print"],2948 stdin=fileArgs,2949 cb=streamP4FilesCbSelf)29502951# do the last chunk2952if'depotFile'in self.stream_file:2953 self.streamOneP4File(self.stream_file, self.stream_contents)29542955defmake_email(self, userid):2956if userid in self.users:2957return self.users[userid]2958else:2959return"%s<a@b>"% userid29602961defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2962""" Stream a p4 tag.2963 commit is either a git commit, or a fast-import mark, ":<p4commit>"2964 """29652966if verbose:2967print("writing tag%sfor commit%s"% (labelName, commit))2968 gitStream.write("tag%s\n"% labelName)2969 gitStream.write("from%s\n"% commit)29702971if'Owner'in labelDetails:2972 owner = labelDetails["Owner"]2973else:2974 owner =None29752976# Try to use the owner of the p4 label, or failing that,2977# the current p4 user id.2978if owner:2979 email = self.make_email(owner)2980else:2981 email = self.make_email(self.p4UserId())2982 tagger ="%s %s %s"% (email, epoch, self.tz)29832984 gitStream.write("tagger%s\n"% tagger)29852986print("labelDetails=",labelDetails)2987if'Description'in labelDetails:2988 description = labelDetails['Description']2989else:2990 description ='Label from git p4'29912992 gitStream.write("data%d\n"%len(description))2993 gitStream.write(description)2994 gitStream.write("\n")29952996definClientSpec(self, path):2997if not self.clientSpecDirs:2998return True2999 inClientSpec = self.clientSpecDirs.map_in_client(path)3000if not inClientSpec and self.verbose:3001print('Ignoring file outside of client spec:{0}'.format(path))3002return inClientSpec30033004defhasBranchPrefix(self, path):3005if not self.branchPrefixes:3006return True3007 hasPrefix = [p for p in self.branchPrefixes3008ifp4PathStartsWith(path, p)]3009if not hasPrefix and self.verbose:3010print('Ignoring file outside of prefix:{0}'.format(path))3011return hasPrefix30123013defcommit(self, details, files, branch, parent ="", allow_empty=False):3014 epoch = details["time"]3015 author = details["user"]3016 jobs = self.extractJobsFromCommit(details)30173018if self.verbose:3019print('commit into{0}'.format(branch))30203021if self.clientSpecDirs:3022 self.clientSpecDirs.update_client_spec_path_cache(files)30233024 files = [f for f in files3025if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]30263027ifgitConfigBool('git-p4.keepEmptyCommits'):3028 allow_empty =True30293030if not files and not allow_empty:3031print('Ignoring revision{0}as it would produce an empty commit.'3032.format(details['change']))3033return30343035 self.gitStream.write("commit%s\n"% branch)3036 self.gitStream.write("mark :%s\n"% details["change"])3037 self.committedChanges.add(int(details["change"]))3038 committer =""3039if author not in self.users:3040 self.getUserMapFromPerforceServer()3041 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)30423043 self.gitStream.write("committer%s\n"% committer)30443045 self.gitStream.write("data <<EOT\n")3046 self.gitStream.write(details["desc"])3047iflen(jobs) >0:3048 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))30493050if not self.suppress_meta_comment:3051 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%3052(','.join(self.branchPrefixes), details["change"]))3053iflen(details['options']) >0:3054 self.gitStream.write(": options =%s"% details['options'])3055 self.gitStream.write("]\n")30563057 self.gitStream.write("EOT\n\n")30583059iflen(parent) >0:3060if self.verbose:3061print("parent%s"% parent)3062 self.gitStream.write("from%s\n"% parent)30633064 self.streamP4Files(files)3065 self.gitStream.write("\n")30663067 change =int(details["change"])30683069if change in self.labels:3070 label = self.labels[change]3071 labelDetails = label[0]3072 labelRevisions = label[1]3073if self.verbose:3074print("Change%sis labelled%s"% (change, labelDetails))30753076 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)3077for p in self.branchPrefixes])30783079iflen(files) ==len(labelRevisions):30803081 cleanedFiles = {}3082for info in files:3083if info["action"]in self.delete_actions:3084continue3085 cleanedFiles[info["depotFile"]] = info["rev"]30863087if cleanedFiles == labelRevisions:3088 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)30893090else:3091if not self.silent:3092print("Tag%sdoes not match with change%s: files do not match."3093% (labelDetails["label"], change))30943095else:3096if not self.silent:3097print("Tag%sdoes not match with change%s: file count is different."3098% (labelDetails["label"], change))30993100# Build a dictionary of changelists and labels, for "detect-labels" option.3101defgetLabels(self):3102 self.labels = {}31033104 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])3105iflen(l) >0and not self.silent:3106print("Finding files belonging to labels in%s"% self.depotPaths)31073108for output in l:3109 label = output["label"]3110 revisions = {}3111 newestChange =03112if self.verbose:3113print("Querying files for label%s"% label)3114forfileinp4CmdList(["files"] +3115["%s...@%s"% (p, label)3116for p in self.depotPaths]):3117 revisions[file["depotFile"]] =file["rev"]3118 change =int(file["change"])3119if change > newestChange:3120 newestChange = change31213122 self.labels[newestChange] = [output, revisions]31233124if self.verbose:3125print("Label changes:%s"% self.labels.keys())31263127# Import p4 labels as git tags. A direct mapping does not3128# exist, so assume that if all the files are at the same revision3129# then we can use that, or it's something more complicated we should3130# just ignore.3131defimportP4Labels(self, stream, p4Labels):3132if verbose:3133print("import p4 labels: "+' '.join(p4Labels))31343135 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")3136 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")3137iflen(validLabelRegexp) ==0:3138 validLabelRegexp = defaultLabelRegexp3139 m = re.compile(validLabelRegexp)31403141for name in p4Labels:3142 commitFound =False31433144if not m.match(name):3145if verbose:3146print("label%sdoes not match regexp%s"% (name,validLabelRegexp))3147continue31483149if name in ignoredP4Labels:3150continue31513152 labelDetails =p4CmdList(['label',"-o", name])[0]31533154# get the most recent changelist for each file in this label3155 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)3156for p in self.depotPaths])31573158if'change'in change:3159# find the corresponding git commit; take the oldest commit3160 changelist =int(change['change'])3161if changelist in self.committedChanges:3162 gitCommit =":%d"% changelist # use a fast-import mark3163 commitFound =True3164else:3165 gitCommit =read_pipe(["git","rev-list","--max-count=1",3166"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)3167iflen(gitCommit) ==0:3168print("importing label%s: could not find git commit for changelist%d"% (name, changelist))3169else:3170 commitFound =True3171 gitCommit = gitCommit.strip()31723173if commitFound:3174# Convert from p4 time format3175try:3176 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")3177exceptValueError:3178print("Could not convert label time%s"% labelDetails['Update'])3179 tmwhen =131803181 when =int(time.mktime(tmwhen))3182 self.streamTag(stream, name, labelDetails, gitCommit, when)3183if verbose:3184print("p4 label%smapped to git commit%s"% (name, gitCommit))3185else:3186if verbose:3187print("Label%shas no changelists - possibly deleted?"% name)31883189if not commitFound:3190# We can't import this label; don't try again as it will get very3191# expensive repeatedly fetching all the files for labels that will3192# never be imported. If the label is moved in the future, the3193# ignore will need to be removed manually.3194system(["git","config","--add","git-p4.ignoredP4Labels", name])31953196defguessProjectName(self):3197for p in self.depotPaths:3198if p.endswith("/"):3199 p = p[:-1]3200 p = p[p.strip().rfind("/") +1:]3201if not p.endswith("/"):3202 p +="/"3203return p32043205defgetBranchMapping(self):3206 lostAndFoundBranches =set()32073208 user =gitConfig("git-p4.branchUser")3209iflen(user) >0:3210 command ="branches -u%s"% user3211else:3212 command ="branches"32133214for info inp4CmdList(command):3215 details =p4Cmd(["branch","-o", info["branch"]])3216 viewIdx =03217while"View%s"% viewIdx in details:3218 paths = details["View%s"% viewIdx].split(" ")3219 viewIdx = viewIdx +13220# require standard //depot/foo/... //depot/bar/... mapping3221iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3222continue3223 source = paths[0]3224 destination = paths[1]3225## HACK3226ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3227 source = source[len(self.depotPaths[0]):-4]3228 destination = destination[len(self.depotPaths[0]):-4]32293230if destination in self.knownBranches:3231if not self.silent:3232print("p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination))3233print("but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination))3234continue32353236 self.knownBranches[destination] = source32373238 lostAndFoundBranches.discard(destination)32393240if source not in self.knownBranches:3241 lostAndFoundBranches.add(source)32423243# Perforce does not strictly require branches to be defined, so we also3244# check git config for a branch list.3245#3246# Example of branch definition in git config file:3247# [git-p4]3248# branchList=main:branchA3249# branchList=main:branchB3250# branchList=branchA:branchC3251 configBranches =gitConfigList("git-p4.branchList")3252for branch in configBranches:3253if branch:3254(source, destination) = branch.split(":")3255 self.knownBranches[destination] = source32563257 lostAndFoundBranches.discard(destination)32583259if source not in self.knownBranches:3260 lostAndFoundBranches.add(source)326132623263for branch in lostAndFoundBranches:3264 self.knownBranches[branch] = branch32653266defgetBranchMappingFromGitBranches(self):3267 branches =p4BranchesInGit(self.importIntoRemotes)3268for branch in branches.keys():3269if branch =="master":3270 branch ="main"3271else:3272 branch = branch[len(self.projectName):]3273 self.knownBranches[branch] = branch32743275defupdateOptionDict(self, d):3276 option_keys = {}3277if self.keepRepoPath:3278 option_keys['keepRepoPath'] =132793280 d["options"] =' '.join(sorted(option_keys.keys()))32813282defreadOptions(self, d):3283 self.keepRepoPath = ('options'in d3284and('keepRepoPath'in d['options']))32853286defgitRefForBranch(self, branch):3287if branch =="main":3288return self.refPrefix +"master"32893290iflen(branch) <=0:3291return branch32923293return self.refPrefix + self.projectName + branch32943295defgitCommitByP4Change(self, ref, change):3296if self.verbose:3297print("looking in ref "+ ref +" for change%susing bisect..."% change)32983299 earliestCommit =""3300 latestCommit =parseRevision(ref)33013302while True:3303if self.verbose:3304print("trying: earliest%slatest%s"% (earliestCommit, latestCommit))3305 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3306iflen(next) ==0:3307if self.verbose:3308print("argh")3309return""3310 log =extractLogMessageFromGitCommit(next)3311 settings =extractSettingsGitLog(log)3312 currentChange =int(settings['change'])3313if self.verbose:3314print("current change%s"% currentChange)33153316if currentChange == change:3317if self.verbose:3318print("found%s"% next)3319return next33203321if currentChange < change:3322 earliestCommit ="^%s"% next3323else:3324 latestCommit ="%s"% next33253326return""33273328defimportNewBranch(self, branch, maxChange):3329# make fast-import flush all changes to disk and update the refs using the checkpoint3330# command so that we can try to find the branch parent in the git history3331 self.gitStream.write("checkpoint\n\n");3332 self.gitStream.flush();3333 branchPrefix = self.depotPaths[0] + branch +"/"3334range="@1,%s"% maxChange3335#print "prefix" + branchPrefix3336 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3337iflen(changes) <=0:3338return False3339 firstChange = changes[0]3340#print "first change in branch: %s" % firstChange3341 sourceBranch = self.knownBranches[branch]3342 sourceDepotPath = self.depotPaths[0] + sourceBranch3343 sourceRef = self.gitRefForBranch(sourceBranch)3344#print "source " + sourceBranch33453346 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3347#print "branch parent: %s" % branchParentChange3348 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3349iflen(gitParent) >0:3350 self.initialParents[self.gitRefForBranch(branch)] = gitParent3351#print "parent git commit: %s" % gitParent33523353 self.importChanges(changes)3354return True33553356defsearchParent(self, parent, branch, target):3357 parentFound =False3358for blob inread_pipe_lines(["git","rev-list","--reverse",3359"--no-merges", parent]):3360 blob = blob.strip()3361iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3362 parentFound =True3363if self.verbose:3364print("Found parent of%sin commit%s"% (branch, blob))3365break3366if parentFound:3367return blob3368else:3369return None33703371defimportChanges(self, changes, origin_revision=0):3372 cnt =13373for change in changes:3374 description =p4_describe(change)3375 self.updateOptionDict(description)33763377if not self.silent:3378 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3379 sys.stdout.flush()3380 cnt = cnt +133813382try:3383if self.detectBranches:3384 branches = self.splitFilesIntoBranches(description)3385for branch in branches.keys():3386## HACK --hwn3387 branchPrefix = self.depotPaths[0] + branch +"/"3388 self.branchPrefixes = [ branchPrefix ]33893390 parent =""33913392 filesForCommit = branches[branch]33933394if self.verbose:3395print("branch is%s"% branch)33963397 self.updatedBranches.add(branch)33983399if branch not in self.createdBranches:3400 self.createdBranches.add(branch)3401 parent = self.knownBranches[branch]3402if parent == branch:3403 parent =""3404else:3405 fullBranch = self.projectName + branch3406if fullBranch not in self.p4BranchesInGit:3407if not self.silent:3408print("\nImporting new branch%s"% fullBranch);3409if self.importNewBranch(branch, change -1):3410 parent =""3411 self.p4BranchesInGit.append(fullBranch)3412if not self.silent:3413print("\nResuming with change%s"% change);34143415if self.verbose:3416print("parent determined through known branches:%s"% parent)34173418 branch = self.gitRefForBranch(branch)3419 parent = self.gitRefForBranch(parent)34203421if self.verbose:3422print("looking for initial parent for%s; current parent is%s"% (branch, parent))34233424iflen(parent) ==0and branch in self.initialParents:3425 parent = self.initialParents[branch]3426del self.initialParents[branch]34273428 blob =None3429iflen(parent) >0:3430 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3431if self.verbose:3432print("Creating temporary branch: "+ tempBranch)3433 self.commit(description, filesForCommit, tempBranch)3434 self.tempBranches.append(tempBranch)3435 self.checkpoint()3436 blob = self.searchParent(parent, branch, tempBranch)3437if blob:3438 self.commit(description, filesForCommit, branch, blob)3439else:3440if self.verbose:3441print("Parent of%snot found. Committing into head of%s"% (branch, parent))3442 self.commit(description, filesForCommit, branch, parent)3443else:3444 files = self.extractFilesFromCommit(description)3445 self.commit(description, files, self.branch,3446 self.initialParent)3447# only needed once, to connect to the previous commit3448 self.initialParent =""3449exceptIOError:3450print(self.gitError.read())3451 sys.exit(1)34523453defsync_origin_only(self):3454if self.syncWithOrigin:3455 self.hasOrigin =originP4BranchesExist()3456if self.hasOrigin:3457if not self.silent:3458print('Syncing with origin first, using "git fetch origin"')3459system("git fetch origin")34603461defimportHeadRevision(self, revision):3462print("Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch))34633464 details = {}3465 details["user"] ="git perforce import user"3466 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3467% (' '.join(self.depotPaths), revision))3468 details["change"] = revision3469 newestRevision =034703471 fileCnt =03472 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]34733474for info inp4CmdList(["files"] + fileArgs):34753476if'code'in info and info['code'] =='error':3477 sys.stderr.write("p4 returned an error:%s\n"3478% info['data'])3479if info['data'].find("must refer to client") >=0:3480 sys.stderr.write("This particular p4 error is misleading.\n")3481 sys.stderr.write("Perhaps the depot path was misspelled.\n");3482 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3483 sys.exit(1)3484if'p4ExitCode'in info:3485 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3486 sys.exit(1)348734883489 change =int(info["change"])3490if change > newestRevision:3491 newestRevision = change34923493if info["action"]in self.delete_actions:3494# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3495#fileCnt = fileCnt + 13496continue34973498for prop in["depotFile","rev","action","type"]:3499 details["%s%s"% (prop, fileCnt)] = info[prop]35003501 fileCnt = fileCnt +135023503 details["change"] = newestRevision35043505# Use time from top-most change so that all git p4 clones of3506# the same p4 repo have the same commit SHA1s.3507 res =p4_describe(newestRevision)3508 details["time"] = res["time"]35093510 self.updateOptionDict(details)3511try:3512 self.commit(details, self.extractFilesFromCommit(details), self.branch)3513exceptIOError:3514print("IO error with git fast-import. Is your git version recent enough?")3515print(self.gitError.read())35163517defopenStreams(self):3518 self.importProcess = subprocess.Popen(["git","fast-import"],3519 stdin=subprocess.PIPE,3520 stdout=subprocess.PIPE,3521 stderr=subprocess.PIPE);3522 self.gitOutput = self.importProcess.stdout3523 self.gitStream = self.importProcess.stdin3524 self.gitError = self.importProcess.stderr35253526defcloseStreams(self):3527 self.gitStream.close()3528if self.importProcess.wait() !=0:3529die("fast-import failed:%s"% self.gitError.read())3530 self.gitOutput.close()3531 self.gitError.close()35323533defrun(self, args):3534if self.importIntoRemotes:3535 self.refPrefix ="refs/remotes/p4/"3536else:3537 self.refPrefix ="refs/heads/p4/"35383539 self.sync_origin_only()35403541 branch_arg_given =bool(self.branch)3542iflen(self.branch) ==0:3543 self.branch = self.refPrefix +"master"3544ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3545system("git update-ref%srefs/heads/p4"% self.branch)3546system("git branch -D p4")35473548# accept either the command-line option, or the configuration variable3549if self.useClientSpec:3550# will use this after clone to set the variable3551 self.useClientSpec_from_options =True3552else:3553ifgitConfigBool("git-p4.useclientspec"):3554 self.useClientSpec =True3555if self.useClientSpec:3556 self.clientSpecDirs =getClientSpec()35573558# TODO: should always look at previous commits,3559# merge with previous imports, if possible.3560if args == []:3561if self.hasOrigin:3562createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)35633564# branches holds mapping from branch name to sha13565 branches =p4BranchesInGit(self.importIntoRemotes)35663567# restrict to just this one, disabling detect-branches3568if branch_arg_given:3569 short = self.branch.split("/")[-1]3570if short in branches:3571 self.p4BranchesInGit = [ short ]3572else:3573 self.p4BranchesInGit = branches.keys()35743575iflen(self.p4BranchesInGit) >1:3576if not self.silent:3577print("Importing from/into multiple branches")3578 self.detectBranches =True3579for branch in branches.keys():3580 self.initialParents[self.refPrefix + branch] = \3581 branches[branch]35823583if self.verbose:3584print("branches:%s"% self.p4BranchesInGit)35853586 p4Change =03587for branch in self.p4BranchesInGit:3588 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)35893590 settings =extractSettingsGitLog(logMsg)35913592 self.readOptions(settings)3593if('depot-paths'in settings3594and'change'in settings):3595 change =int(settings['change']) +13596 p4Change =max(p4Change, change)35973598 depotPaths =sorted(settings['depot-paths'])3599if self.previousDepotPaths == []:3600 self.previousDepotPaths = depotPaths3601else:3602 paths = []3603for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3604 prev_list = prev.split("/")3605 cur_list = cur.split("/")3606for i inrange(0,min(len(cur_list),len(prev_list))):3607if cur_list[i] != prev_list[i]:3608 i = i -13609break36103611 paths.append("/".join(cur_list[:i +1]))36123613 self.previousDepotPaths = paths36143615if p4Change >0:3616 self.depotPaths =sorted(self.previousDepotPaths)3617 self.changeRange ="@%s,#head"% p4Change3618if not self.silent and not self.detectBranches:3619print("Performing incremental import into%sgit branch"% self.branch)36203621# accept multiple ref name abbreviations:3622# refs/foo/bar/branch -> use it exactly3623# p4/branch -> prepend refs/remotes/ or refs/heads/3624# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3625if not self.branch.startswith("refs/"):3626if self.importIntoRemotes:3627 prepend ="refs/remotes/"3628else:3629 prepend ="refs/heads/"3630if not self.branch.startswith("p4/"):3631 prepend +="p4/"3632 self.branch = prepend + self.branch36333634iflen(args) ==0and self.depotPaths:3635if not self.silent:3636print("Depot paths:%s"%' '.join(self.depotPaths))3637else:3638if self.depotPaths and self.depotPaths != args:3639print("previous import used depot path%sand now%swas specified. "3640"This doesn't work!"% (' '.join(self.depotPaths),3641' '.join(args)))3642 sys.exit(1)36433644 self.depotPaths =sorted(args)36453646 revision =""3647 self.users = {}36483649# Make sure no revision specifiers are used when --changesfile3650# is specified.3651 bad_changesfile =False3652iflen(self.changesFile) >0:3653for p in self.depotPaths:3654if p.find("@") >=0or p.find("#") >=0:3655 bad_changesfile =True3656break3657if bad_changesfile:3658die("Option --changesfile is incompatible with revision specifiers")36593660 newPaths = []3661for p in self.depotPaths:3662if p.find("@") != -1:3663 atIdx = p.index("@")3664 self.changeRange = p[atIdx:]3665if self.changeRange =="@all":3666 self.changeRange =""3667elif','not in self.changeRange:3668 revision = self.changeRange3669 self.changeRange =""3670 p = p[:atIdx]3671elif p.find("#") != -1:3672 hashIdx = p.index("#")3673 revision = p[hashIdx:]3674 p = p[:hashIdx]3675elif self.previousDepotPaths == []:3676# pay attention to changesfile, if given, else import3677# the entire p4 tree at the head revision3678iflen(self.changesFile) ==0:3679 revision ="#head"36803681 p = re.sub("\.\.\.$","", p)3682if not p.endswith("/"):3683 p +="/"36843685 newPaths.append(p)36863687 self.depotPaths = newPaths36883689# --detect-branches may change this for each branch3690 self.branchPrefixes = self.depotPaths36913692 self.loadUserMapFromCache()3693 self.labels = {}3694if self.detectLabels:3695 self.getLabels();36963697if self.detectBranches:3698## FIXME - what's a P4 projectName ?3699 self.projectName = self.guessProjectName()37003701if self.hasOrigin:3702 self.getBranchMappingFromGitBranches()3703else:3704 self.getBranchMapping()3705if self.verbose:3706print("p4-git branches:%s"% self.p4BranchesInGit)3707print("initial parents:%s"% self.initialParents)3708for b in self.p4BranchesInGit:3709if b !="master":37103711## FIXME3712 b = b[len(self.projectName):]3713 self.createdBranches.add(b)37143715 self.openStreams()37163717if revision:3718 self.importHeadRevision(revision)3719else:3720 changes = []37213722iflen(self.changesFile) >0:3723 output =open(self.changesFile).readlines()3724 changeSet =set()3725for line in output:3726 changeSet.add(int(line))37273728for change in changeSet:3729 changes.append(change)37303731 changes.sort()3732else:3733# catch "git p4 sync" with no new branches, in a repo that3734# does not have any existing p4 branches3735iflen(args) ==0:3736if not self.p4BranchesInGit:3737die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")37383739# The default branch is master, unless --branch is used to3740# specify something else. Make sure it exists, or complain3741# nicely about how to use --branch.3742if not self.detectBranches:3743if notbranch_exists(self.branch):3744if branch_arg_given:3745die("Error: branch%sdoes not exist."% self.branch)3746else:3747die("Error: no branch%s; perhaps specify one with --branch."%3748 self.branch)37493750if self.verbose:3751print("Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3752 self.changeRange))3753 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)37543755iflen(self.maxChanges) >0:3756 changes = changes[:min(int(self.maxChanges),len(changes))]37573758iflen(changes) ==0:3759if not self.silent:3760print("No changes to import!")3761else:3762if not self.silent and not self.detectBranches:3763print("Import destination:%s"% self.branch)37643765 self.updatedBranches =set()37663767if not self.detectBranches:3768if args:3769# start a new branch3770 self.initialParent =""3771else:3772# build on a previous revision3773 self.initialParent =parseRevision(self.branch)37743775 self.importChanges(changes)37763777if not self.silent:3778print("")3779iflen(self.updatedBranches) >0:3780 sys.stdout.write("Updated branches: ")3781for b in self.updatedBranches:3782 sys.stdout.write("%s"% b)3783 sys.stdout.write("\n")37843785ifgitConfigBool("git-p4.importLabels"):3786 self.importLabels =True37873788if self.importLabels:3789 p4Labels =getP4Labels(self.depotPaths)3790 gitTags =getGitTags()37913792 missingP4Labels = p4Labels - gitTags3793 self.importP4Labels(self.gitStream, missingP4Labels)37943795 self.closeStreams()37963797# Cleanup temporary branches created during import3798if self.tempBranches != []:3799for branch in self.tempBranches:3800read_pipe("git update-ref -d%s"% branch)3801 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))38023803# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3804# a convenient shortcut refname "p4".3805if self.importIntoRemotes:3806 head_ref = self.refPrefix +"HEAD"3807if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3808system(["git","symbolic-ref", head_ref, self.branch])38093810return True38113812classP4Rebase(Command):3813def__init__(self):3814 Command.__init__(self)3815 self.options = [3816 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3817]3818 self.importLabels =False3819 self.description = ("Fetches the latest revision from perforce and "3820+"rebases the current work (branch) against it")38213822defrun(self, args):3823 sync =P4Sync()3824 sync.importLabels = self.importLabels3825 sync.run([])38263827return self.rebase()38283829defrebase(self):3830if os.system("git update-index --refresh") !=0:3831die("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.");3832iflen(read_pipe("git diff-index HEAD --")) >0:3833die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");38343835[upstream, settings] =findUpstreamBranchPoint()3836iflen(upstream) ==0:3837die("Cannot find upstream branchpoint for rebase")38383839# the branchpoint may be p4/foo~3, so strip off the parent3840 upstream = re.sub("~[0-9]+$","", upstream)38413842print("Rebasing the current branch onto%s"% upstream)3843 oldHead =read_pipe("git rev-parse HEAD").strip()3844system("git rebase%s"% upstream)3845system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3846return True38473848classP4Clone(P4Sync):3849def__init__(self):3850 P4Sync.__init__(self)3851 self.description ="Creates a new git repository and imports from Perforce into it"3852 self.usage ="usage: %prog [options] //depot/path[@revRange]"3853 self.options += [3854 optparse.make_option("--destination", dest="cloneDestination",3855 action='store', default=None,3856help="where to leave result of the clone"),3857 optparse.make_option("--bare", dest="cloneBare",3858 action="store_true", default=False),3859]3860 self.cloneDestination =None3861 self.needsGit =False3862 self.cloneBare =False38633864defdefaultDestination(self, args):3865## TODO: use common prefix of args?3866 depotPath = args[0]3867 depotDir = re.sub("(@[^@]*)$","", depotPath)3868 depotDir = re.sub("(#[^#]*)$","", depotDir)3869 depotDir = re.sub(r"\.\.\.$","", depotDir)3870 depotDir = re.sub(r"/$","", depotDir)3871return os.path.split(depotDir)[1]38723873defrun(self, args):3874iflen(args) <1:3875return False38763877if self.keepRepoPath and not self.cloneDestination:3878 sys.stderr.write("Must specify destination for --keep-path\n")3879 sys.exit(1)38803881 depotPaths = args38823883if not self.cloneDestination andlen(depotPaths) >1:3884 self.cloneDestination = depotPaths[-1]3885 depotPaths = depotPaths[:-1]38863887 self.cloneExclude = ["/"+p for p in self.cloneExclude]3888for p in depotPaths:3889if not p.startswith("//"):3890 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3891return False38923893if not self.cloneDestination:3894 self.cloneDestination = self.defaultDestination(args)38953896print("Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination))38973898if not os.path.exists(self.cloneDestination):3899 os.makedirs(self.cloneDestination)3900chdir(self.cloneDestination)39013902 init_cmd = ["git","init"]3903if self.cloneBare:3904 init_cmd.append("--bare")3905 retcode = subprocess.call(init_cmd)3906if retcode:3907raiseCalledProcessError(retcode, init_cmd)39083909if not P4Sync.run(self, depotPaths):3910return False39113912# create a master branch and check out a work tree3913ifgitBranchExists(self.branch):3914system(["git","branch","master", self.branch ])3915if not self.cloneBare:3916system(["git","checkout","-f"])3917else:3918print('Not checking out any branch, use ' \3919'"git checkout -q -b master <branch>"')39203921# auto-set this variable if invoked with --use-client-spec3922if self.useClientSpec_from_options:3923system("git config --bool git-p4.useclientspec true")39243925return True39263927classP4Unshelve(Command):3928def__init__(self):3929 Command.__init__(self)3930 self.options = []3931 self.origin ="HEAD"3932 self.description ="Unshelve a P4 changelist into a git commit"3933 self.usage ="usage: %prog [options] changelist"3934 self.options += [3935 optparse.make_option("--origin", dest="origin",3936help="Use this base revision instead of the default (%s)"% self.origin),3937]3938 self.verbose =False3939 self.noCommit =False3940 self.destbranch ="refs/remotes/p4-unshelved"39413942defrenameBranch(self, branch_name):3943""" Rename the existing branch to branch_name.N3944 """39453946 found =True3947for i inrange(0,1000):3948 backup_branch_name ="{0}.{1}".format(branch_name, i)3949if notgitBranchExists(backup_branch_name):3950gitUpdateRef(backup_branch_name, branch_name)# copy ref to backup3951gitDeleteRef(branch_name)3952 found =True3953print("renamed old unshelve branch to{0}".format(backup_branch_name))3954break39553956if not found:3957 sys.exit("gave up trying to rename existing branch{0}".format(sync.branch))39583959deffindLastP4Revision(self, starting_point):3960""" Look back from starting_point for the first commit created by git-p43961 to find the P4 commit we are based on, and the depot-paths.3962 """39633964for parent in(range(65535)):3965 log =extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))3966 settings =extractSettingsGitLog(log)3967if'change'in settings:3968return settings39693970 sys.exit("could not find git-p4 commits in{0}".format(self.origin))39713972defcreateShelveParent(self, change, branch_name, sync, origin):3973""" Create a commit matching the parent of the shelved changelist 'change'3974 """3975 parent_description =p4_describe(change, shelved=True)3976 parent_description['desc'] ='parent for shelved changelist {}\n'.format(change)3977 files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)39783979 parent_files = []3980for f in files:3981# if it was added in the shelved changelist, it won't exist in the parent3982if f['action']in self.add_actions:3983continue39843985# if it was deleted in the shelved changelist it must not be deleted3986# in the parent - we might even need to create it if the origin branch3987# does not have it3988if f['action']in self.delete_actions:3989 f['action'] ='add'39903991 parent_files.append(f)39923993 sync.commit(parent_description, parent_files, branch_name,3994 parent=origin, allow_empty=True)3995print("created parent commit for{0}based on{1}in{2}".format(3996 change, self.origin, branch_name))39973998defrun(self, args):3999iflen(args) !=1:4000return False40014002if notgitBranchExists(self.origin):4003 sys.exit("origin branch{0}does not exist".format(self.origin))40044005 sync =P4Sync()4006 changes = args40074008# only one change at a time4009 change = changes[0]40104011# if the target branch already exists, rename it4012 branch_name ="{0}/{1}".format(self.destbranch, change)4013ifgitBranchExists(branch_name):4014 self.renameBranch(branch_name)4015 sync.branch = branch_name40164017 sync.verbose = self.verbose4018 sync.suppress_meta_comment =True40194020 settings = self.findLastP4Revision(self.origin)4021 sync.depotPaths = settings['depot-paths']4022 sync.branchPrefixes = sync.depotPaths40234024 sync.openStreams()4025 sync.loadUserMapFromCache()4026 sync.silent =True40274028# create a commit for the parent of the shelved changelist4029 self.createShelveParent(change, branch_name, sync, self.origin)40304031# create the commit for the shelved changelist itself4032 description =p4_describe(change,True)4033 files = sync.extractFilesFromCommit(description,True, change)40344035 sync.commit(description, files, branch_name,"")4036 sync.closeStreams()40374038print("unshelved changelist{0}into{1}".format(change, branch_name))40394040return True40414042classP4Branches(Command):4043def__init__(self):4044 Command.__init__(self)4045 self.options = [ ]4046 self.description = ("Shows the git branches that hold imports and their "4047+"corresponding perforce depot paths")4048 self.verbose =False40494050defrun(self, args):4051iforiginP4BranchesExist():4052createOrUpdateBranchesFromOrigin()40534054 cmdline ="git rev-parse --symbolic "4055 cmdline +=" --remotes"40564057for line inread_pipe_lines(cmdline):4058 line = line.strip()40594060if not line.startswith('p4/')or line =="p4/HEAD":4061continue4062 branch = line40634064 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)4065 settings =extractSettingsGitLog(log)40664067print("%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"]))4068return True40694070classHelpFormatter(optparse.IndentedHelpFormatter):4071def__init__(self):4072 optparse.IndentedHelpFormatter.__init__(self)40734074defformat_description(self, description):4075if description:4076return description +"\n"4077else:4078return""40794080defprintUsage(commands):4081print("usage:%s<command> [options]"% sys.argv[0])4082print("")4083print("valid commands:%s"%", ".join(commands))4084print("")4085print("Try%s<command> --help for command specific help."% sys.argv[0])4086print("")40874088commands = {4089"debug": P4Debug,4090"submit": P4Submit,4091"commit": P4Submit,4092"sync": P4Sync,4093"rebase": P4Rebase,4094"clone": P4Clone,4095"rollback": P4RollBack,4096"branches": P4Branches,4097"unshelve": P4Unshelve,4098}409941004101defmain():4102iflen(sys.argv[1:]) ==0:4103printUsage(commands.keys())4104 sys.exit(2)41054106 cmdName = sys.argv[1]4107try:4108 klass = commands[cmdName]4109 cmd =klass()4110exceptKeyError:4111print("unknown command%s"% cmdName)4112print("")4113printUsage(commands.keys())4114 sys.exit(2)41154116 options = cmd.options4117 cmd.gitdir = os.environ.get("GIT_DIR",None)41184119 args = sys.argv[2:]41204121 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))4122if cmd.needsGit:4123 options.append(optparse.make_option("--git-dir", dest="gitdir"))41244125 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),4126 options,4127 description = cmd.description,4128 formatter =HelpFormatter())41294130(cmd, args) = parser.parse_args(sys.argv[2:], cmd);4131global verbose4132 verbose = cmd.verbose4133if cmd.needsGit:4134if cmd.gitdir ==None:4135 cmd.gitdir = os.path.abspath(".git")4136if notisValidGitDir(cmd.gitdir):4137# "rev-parse --git-dir" without arguments will try $PWD/.git4138 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()4139if os.path.exists(cmd.gitdir):4140 cdup =read_pipe("git rev-parse --show-cdup").strip()4141iflen(cdup) >0:4142chdir(cdup);41434144if notisValidGitDir(cmd.gitdir):4145ifisValidGitDir(cmd.gitdir +"/.git"):4146 cmd.gitdir +="/.git"4147else:4148die("fatal: cannot locate git repository at%s"% cmd.gitdir)41494150# so git commands invoked from the P4 workspace will succeed4151 os.environ["GIT_DIR"] = cmd.gitdir41524153if not cmd.run(args):4154 parser.print_help()4155 sys.exit(2)415641574158if __name__ =='__main__':4159main()