1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25import zipfile 26import zlib 27import ctypes 28import errno 29 30# support basestring in python3 31try: 32unicode=unicode 33exceptNameError: 34# 'unicode' is undefined, must be Python 3 35str=str 36unicode=str 37bytes=bytes 38 basestring = (str,bytes) 39else: 40# 'unicode' exists, must be Python 2 41str=str 42unicode=unicode 43bytes=str 44 basestring = basestring 45 46try: 47from subprocess import CalledProcessError 48exceptImportError: 49# from python2.7:subprocess.py 50# Exception classes used by this module. 51classCalledProcessError(Exception): 52"""This exception is raised when a process run by check_call() returns 53 a non-zero exit status. The exit status will be stored in the 54 returncode attribute.""" 55def__init__(self, returncode, cmd): 56 self.returncode = returncode 57 self.cmd = cmd 58def__str__(self): 59return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 60 61verbose =False 62 63# Only labels/tags matching this will be imported/exported 64defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 65 66# The block size is reduced automatically if required 67defaultBlockSize =1<<20 68 69p4_access_checked =False 70 71defp4_build_cmd(cmd): 72"""Build a suitable p4 command line. 73 74 This consolidates building and returning a p4 command line into one 75 location. It means that hooking into the environment, or other configuration 76 can be done more easily. 77 """ 78 real_cmd = ["p4"] 79 80 user =gitConfig("git-p4.user") 81iflen(user) >0: 82 real_cmd += ["-u",user] 83 84 password =gitConfig("git-p4.password") 85iflen(password) >0: 86 real_cmd += ["-P", password] 87 88 port =gitConfig("git-p4.port") 89iflen(port) >0: 90 real_cmd += ["-p", port] 91 92 host =gitConfig("git-p4.host") 93iflen(host) >0: 94 real_cmd += ["-H", host] 95 96 client =gitConfig("git-p4.client") 97iflen(client) >0: 98 real_cmd += ["-c", client] 99 100 retries =gitConfigInt("git-p4.retries") 101if retries is None: 102# Perform 3 retries by default 103 retries =3 104if retries >0: 105# Provide a way to not pass this option by setting git-p4.retries to 0 106 real_cmd += ["-r",str(retries)] 107 108ifisinstance(cmd,basestring): 109 real_cmd =' '.join(real_cmd) +' '+ cmd 110else: 111 real_cmd += cmd 112 113# now check that we can actually talk to the server 114global p4_access_checked 115if not p4_access_checked: 116 p4_access_checked =True# suppress access checks in p4_check_access itself 117p4_check_access() 118 119return real_cmd 120 121defgit_dir(path): 122""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 123 This won't automatically add ".git" to a directory. 124 """ 125 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 126if not d orlen(d) ==0: 127return None 128else: 129return d 130 131defchdir(path, is_client_path=False): 132"""Do chdir to the given path, and set the PWD environment 133 variable for use by P4. It does not look at getcwd() output. 134 Since we're not using the shell, it is necessary to set the 135 PWD environment variable explicitly. 136 137 Normally, expand the path to force it to be absolute. This 138 addresses the use of relative path names inside P4 settings, 139 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 140 as given; it looks for .p4config using PWD. 141 142 If is_client_path, the path was handed to us directly by p4, 143 and may be a symbolic link. Do not call os.getcwd() in this 144 case, because it will cause p4 to think that PWD is not inside 145 the client path. 146 """ 147 148 os.chdir(path) 149if not is_client_path: 150 path = os.getcwd() 151 os.environ['PWD'] = path 152 153defcalcDiskFree(): 154"""Return free space in bytes on the disk of the given dirname.""" 155if platform.system() =='Windows': 156 free_bytes = ctypes.c_ulonglong(0) 157 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 158return free_bytes.value 159else: 160 st = os.statvfs(os.getcwd()) 161return st.f_bavail * st.f_frsize 162 163defdie(msg): 164if verbose: 165raiseException(msg) 166else: 167 sys.stderr.write(msg +"\n") 168 sys.exit(1) 169 170defwrite_pipe(c, stdin): 171if verbose: 172 sys.stderr.write('Writing pipe:%s\n'%str(c)) 173 174 expand =isinstance(c,basestring) 175 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 176 pipe = p.stdin 177 val = pipe.write(stdin) 178 pipe.close() 179if p.wait(): 180die('Command failed:%s'%str(c)) 181 182return val 183 184defp4_write_pipe(c, stdin): 185 real_cmd =p4_build_cmd(c) 186returnwrite_pipe(real_cmd, stdin) 187 188defread_pipe_full(c): 189""" Read output from command. Returns a tuple 190 of the return status, stdout text and stderr 191 text. 192 """ 193if verbose: 194 sys.stderr.write('Reading pipe:%s\n'%str(c)) 195 196 expand =isinstance(c,basestring) 197 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 198(out, err) = p.communicate() 199return(p.returncode, out, err) 200 201defread_pipe(c, ignore_error=False): 202""" Read output from command. Returns the output text on 203 success. On failure, terminates execution, unless 204 ignore_error is True, when it returns an empty string. 205 """ 206(retcode, out, err) =read_pipe_full(c) 207if retcode !=0: 208if ignore_error: 209 out ="" 210else: 211die('Command failed:%s\nError:%s'% (str(c), err)) 212return out 213 214defread_pipe_text(c): 215""" Read output from a command with trailing whitespace stripped. 216 On error, returns None. 217 """ 218(retcode, out, err) =read_pipe_full(c) 219if retcode !=0: 220return None 221else: 222return out.rstrip() 223 224defp4_read_pipe(c, ignore_error=False): 225 real_cmd =p4_build_cmd(c) 226returnread_pipe(real_cmd, ignore_error) 227 228defread_pipe_lines(c): 229if verbose: 230 sys.stderr.write('Reading pipe:%s\n'%str(c)) 231 232 expand =isinstance(c, basestring) 233 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 234 pipe = p.stdout 235 val = pipe.readlines() 236if pipe.close()or p.wait(): 237die('Command failed:%s'%str(c)) 238 239return val 240 241defp4_read_pipe_lines(c): 242"""Specifically invoke p4 on the command supplied. """ 243 real_cmd =p4_build_cmd(c) 244returnread_pipe_lines(real_cmd) 245 246defp4_has_command(cmd): 247"""Ask p4 for help on this command. If it returns an error, the 248 command does not exist in this version of p4.""" 249 real_cmd =p4_build_cmd(["help", cmd]) 250 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 251 stderr=subprocess.PIPE) 252 p.communicate() 253return p.returncode ==0 254 255defp4_has_move_command(): 256"""See if the move command exists, that it supports -k, and that 257 it has not been administratively disabled. The arguments 258 must be correct, but the filenames do not have to exist. Use 259 ones with wildcards so even if they exist, it will fail.""" 260 261if notp4_has_command("move"): 262return False 263 cmd =p4_build_cmd(["move","-k","@from","@to"]) 264 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 265(out, err) = p.communicate() 266# return code will be 1 in either case 267if err.find("Invalid option") >=0: 268return False 269if err.find("disabled") >=0: 270return False 271# assume it failed because @... was invalid changelist 272return True 273 274defsystem(cmd, ignore_error=False): 275 expand =isinstance(cmd,basestring) 276if verbose: 277 sys.stderr.write("executing%s\n"%str(cmd)) 278 retcode = subprocess.call(cmd, shell=expand) 279if retcode and not ignore_error: 280raiseCalledProcessError(retcode, cmd) 281 282return retcode 283 284defp4_system(cmd): 285"""Specifically invoke p4 as the system command. """ 286 real_cmd =p4_build_cmd(cmd) 287 expand =isinstance(real_cmd, basestring) 288 retcode = subprocess.call(real_cmd, shell=expand) 289if retcode: 290raiseCalledProcessError(retcode, real_cmd) 291 292defdie_bad_access(s): 293die("failure accessing depot:{0}".format(s.rstrip())) 294 295defp4_check_access(min_expiration=1): 296""" Check if we can access Perforce - account still logged in 297 """ 298 results =p4CmdList(["login","-s"]) 299 300iflen(results) ==0: 301# should never get here: always get either some results, or a p4ExitCode 302assert("could not parse response from perforce") 303 304 result = results[0] 305 306if'p4ExitCode'in result: 307# p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path 308die_bad_access("could not run p4") 309 310 code = result.get("code") 311if not code: 312# we get here if we couldn't connect and there was nothing to unmarshal 313die_bad_access("could not connect") 314 315elif code =="stat": 316 expiry = result.get("TicketExpiration") 317if expiry: 318 expiry =int(expiry) 319if expiry > min_expiration: 320# ok to carry on 321return 322else: 323die_bad_access("perforce ticket expires in{0}seconds".format(expiry)) 324 325else: 326# account without a timeout - all ok 327return 328 329elif code =="error": 330 data = result.get("data") 331if data: 332die_bad_access("p4 error:{0}".format(data)) 333else: 334die_bad_access("unknown error") 335else: 336die_bad_access("unknown error code{0}".format(code)) 337 338_p4_version_string =None 339defp4_version_string(): 340"""Read the version string, showing just the last line, which 341 hopefully is the interesting version bit. 342 343 $ p4 -V 344 Perforce - The Fast Software Configuration Management System. 345 Copyright 1995-2011 Perforce Software. All rights reserved. 346 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 347 """ 348global _p4_version_string 349if not _p4_version_string: 350 a =p4_read_pipe_lines(["-V"]) 351 _p4_version_string = a[-1].rstrip() 352return _p4_version_string 353 354defp4_integrate(src, dest): 355p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 356 357defp4_sync(f, *options): 358p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 359 360defp4_add(f): 361# forcibly add file names with wildcards 362ifwildcard_present(f): 363p4_system(["add","-f", f]) 364else: 365p4_system(["add", f]) 366 367defp4_delete(f): 368p4_system(["delete",wildcard_encode(f)]) 369 370defp4_edit(f, *options): 371p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 372 373defp4_revert(f): 374p4_system(["revert",wildcard_encode(f)]) 375 376defp4_reopen(type, f): 377p4_system(["reopen","-t",type,wildcard_encode(f)]) 378 379defp4_reopen_in_change(changelist, files): 380 cmd = ["reopen","-c",str(changelist)] + files 381p4_system(cmd) 382 383defp4_move(src, dest): 384p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 385 386defp4_last_change(): 387 results =p4CmdList(["changes","-m","1"], skip_info=True) 388returnint(results[0]['change']) 389 390defp4_describe(change, shelved=False): 391"""Make sure it returns a valid result by checking for 392 the presence of field "time". Return a dict of the 393 results.""" 394 395 cmd = ["describe","-s"] 396if shelved: 397 cmd += ["-S"] 398 cmd += [str(change)] 399 400 ds =p4CmdList(cmd, skip_info=True) 401iflen(ds) !=1: 402die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 403 404 d = ds[0] 405 406if"p4ExitCode"in d: 407die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 408str(d))) 409if"code"in d: 410if d["code"] =="error": 411die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 412 413if"time"not in d: 414die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 415 416return d 417 418# 419# Canonicalize the p4 type and return a tuple of the 420# base type, plus any modifiers. See "p4 help filetypes" 421# for a list and explanation. 422# 423defsplit_p4_type(p4type): 424 425 p4_filetypes_historical = { 426"ctempobj":"binary+Sw", 427"ctext":"text+C", 428"cxtext":"text+Cx", 429"ktext":"text+k", 430"kxtext":"text+kx", 431"ltext":"text+F", 432"tempobj":"binary+FSw", 433"ubinary":"binary+F", 434"uresource":"resource+F", 435"uxbinary":"binary+Fx", 436"xbinary":"binary+x", 437"xltext":"text+Fx", 438"xtempobj":"binary+Swx", 439"xtext":"text+x", 440"xunicode":"unicode+x", 441"xutf16":"utf16+x", 442} 443if p4type in p4_filetypes_historical: 444 p4type = p4_filetypes_historical[p4type] 445 mods ="" 446 s = p4type.split("+") 447 base = s[0] 448 mods ="" 449iflen(s) >1: 450 mods = s[1] 451return(base, mods) 452 453# 454# return the raw p4 type of a file (text, text+ko, etc) 455# 456defp4_type(f): 457 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 458return results[0]['headType'] 459 460# 461# Given a type base and modifier, return a regexp matching 462# the keywords that can be expanded in the file 463# 464defp4_keywords_regexp_for_type(base, type_mods): 465if base in("text","unicode","binary"): 466 kwords =None 467if"ko"in type_mods: 468 kwords ='Id|Header' 469elif"k"in type_mods: 470 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 471else: 472return None 473 pattern = r""" 474 \$ # Starts with a dollar, followed by... 475 (%s) # one of the keywords, followed by... 476 (:[^$\n]+)? # possibly an old expansion, followed by... 477 \$ # another dollar 478 """% kwords 479return pattern 480else: 481return None 482 483# 484# Given a file, return a regexp matching the possible 485# RCS keywords that will be expanded, or None for files 486# with kw expansion turned off. 487# 488defp4_keywords_regexp_for_file(file): 489if not os.path.exists(file): 490return None 491else: 492(type_base, type_mods) =split_p4_type(p4_type(file)) 493returnp4_keywords_regexp_for_type(type_base, type_mods) 494 495defsetP4ExecBit(file, mode): 496# Reopens an already open file and changes the execute bit to match 497# the execute bit setting in the passed in mode. 498 499 p4Type ="+x" 500 501if notisModeExec(mode): 502 p4Type =getP4OpenedType(file) 503 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 504 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 505if p4Type[-1] =="+": 506 p4Type = p4Type[0:-1] 507 508p4_reopen(p4Type,file) 509 510defgetP4OpenedType(file): 511# Returns the perforce file type for the given file. 512 513 result =p4_read_pipe(["opened",wildcard_encode(file)]) 514 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 515if match: 516return match.group(1) 517else: 518die("Could not determine file type for%s(result: '%s')"% (file, result)) 519 520# Return the set of all p4 labels 521defgetP4Labels(depotPaths): 522 labels =set() 523ifisinstance(depotPaths,basestring): 524 depotPaths = [depotPaths] 525 526for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 527 label = l['label'] 528 labels.add(label) 529 530return labels 531 532# Return the set of all git tags 533defgetGitTags(): 534 gitTags =set() 535for line inread_pipe_lines(["git","tag"]): 536 tag = line.strip() 537 gitTags.add(tag) 538return gitTags 539 540defdiffTreePattern(): 541# This is a simple generator for the diff tree regex pattern. This could be 542# a class variable if this and parseDiffTreeEntry were a part of a class. 543 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 544while True: 545yield pattern 546 547defparseDiffTreeEntry(entry): 548"""Parses a single diff tree entry into its component elements. 549 550 See git-diff-tree(1) manpage for details about the format of the diff 551 output. This method returns a dictionary with the following elements: 552 553 src_mode - The mode of the source file 554 dst_mode - The mode of the destination file 555 src_sha1 - The sha1 for the source file 556 dst_sha1 - The sha1 fr the destination file 557 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 558 status_score - The score for the status (applicable for 'C' and 'R' 559 statuses). This is None if there is no score. 560 src - The path for the source file. 561 dst - The path for the destination file. This is only present for 562 copy or renames. If it is not present, this is None. 563 564 If the pattern is not matched, None is returned.""" 565 566 match =diffTreePattern().next().match(entry) 567if match: 568return{ 569'src_mode': match.group(1), 570'dst_mode': match.group(2), 571'src_sha1': match.group(3), 572'dst_sha1': match.group(4), 573'status': match.group(5), 574'status_score': match.group(6), 575'src': match.group(7), 576'dst': match.group(10) 577} 578return None 579 580defisModeExec(mode): 581# Returns True if the given git mode represents an executable file, 582# otherwise False. 583return mode[-3:] =="755" 584 585classP4Exception(Exception): 586""" Base class for exceptions from the p4 client """ 587def__init__(self, exit_code): 588 self.p4ExitCode = exit_code 589 590classP4ServerException(P4Exception): 591""" Base class for exceptions where we get some kind of marshalled up result from the server """ 592def__init__(self, exit_code, p4_result): 593super(P4ServerException, self).__init__(exit_code) 594 self.p4_result = p4_result 595 self.code = p4_result[0]['code'] 596 self.data = p4_result[0]['data'] 597 598classP4RequestSizeException(P4ServerException): 599""" One of the maxresults or maxscanrows errors """ 600def__init__(self, exit_code, p4_result, limit): 601super(P4RequestSizeException, self).__init__(exit_code, p4_result) 602 self.limit = limit 603 604defisModeExecChanged(src_mode, dst_mode): 605returnisModeExec(src_mode) !=isModeExec(dst_mode) 606 607defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False, 608 errors_as_exceptions=False): 609 610ifisinstance(cmd,basestring): 611 cmd ="-G "+ cmd 612 expand =True 613else: 614 cmd = ["-G"] + cmd 615 expand =False 616 617 cmd =p4_build_cmd(cmd) 618if verbose: 619 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 620 621# Use a temporary file to avoid deadlocks without 622# subprocess.communicate(), which would put another copy 623# of stdout into memory. 624 stdin_file =None 625if stdin is not None: 626 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 627ifisinstance(stdin,basestring): 628 stdin_file.write(stdin) 629else: 630for i in stdin: 631 stdin_file.write(i +'\n') 632 stdin_file.flush() 633 stdin_file.seek(0) 634 635 p4 = subprocess.Popen(cmd, 636 shell=expand, 637 stdin=stdin_file, 638 stdout=subprocess.PIPE) 639 640 result = [] 641try: 642while True: 643 entry = marshal.load(p4.stdout) 644if skip_info: 645if'code'in entry and entry['code'] =='info': 646continue 647if cb is not None: 648cb(entry) 649else: 650 result.append(entry) 651exceptEOFError: 652pass 653 exitCode = p4.wait() 654if exitCode !=0: 655if errors_as_exceptions: 656iflen(result) >0: 657 data = result[0].get('data') 658if data: 659 m = re.search('Too many rows scanned \(over (\d+)\)', data) 660if not m: 661 m = re.search('Request too large \(over (\d+)\)', data) 662 663if m: 664 limit =int(m.group(1)) 665raiseP4RequestSizeException(exitCode, result, limit) 666 667raiseP4ServerException(exitCode, result) 668else: 669raiseP4Exception(exitCode) 670else: 671 entry = {} 672 entry["p4ExitCode"] = exitCode 673 result.append(entry) 674 675return result 676 677defp4Cmd(cmd): 678list=p4CmdList(cmd) 679 result = {} 680for entry inlist: 681 result.update(entry) 682return result; 683 684defp4Where(depotPath): 685if not depotPath.endswith("/"): 686 depotPath +="/" 687 depotPathLong = depotPath +"..." 688 outputList =p4CmdList(["where", depotPathLong]) 689 output =None 690for entry in outputList: 691if"depotFile"in entry: 692# Search for the base client side depot path, as long as it starts with the branch's P4 path. 693# The base path always ends with "/...". 694if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 695 output = entry 696break 697elif"data"in entry: 698 data = entry.get("data") 699 space = data.find(" ") 700if data[:space] == depotPath: 701 output = entry 702break 703if output ==None: 704return"" 705if output["code"] =="error": 706return"" 707 clientPath ="" 708if"path"in output: 709 clientPath = output.get("path") 710elif"data"in output: 711 data = output.get("data") 712 lastSpace = data.rfind(" ") 713 clientPath = data[lastSpace +1:] 714 715if clientPath.endswith("..."): 716 clientPath = clientPath[:-3] 717return clientPath 718 719defcurrentGitBranch(): 720returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 721 722defisValidGitDir(path): 723returngit_dir(path) !=None 724 725defparseRevision(ref): 726returnread_pipe("git rev-parse%s"% ref).strip() 727 728defbranchExists(ref): 729 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 730 ignore_error=True) 731returnlen(rev) >0 732 733defextractLogMessageFromGitCommit(commit): 734 logMessage ="" 735 736## fixme: title is first line of commit, not 1st paragraph. 737 foundTitle =False 738for log inread_pipe_lines("git cat-file commit%s"% commit): 739if not foundTitle: 740iflen(log) ==1: 741 foundTitle =True 742continue 743 744 logMessage += log 745return logMessage 746 747defextractSettingsGitLog(log): 748 values = {} 749for line in log.split("\n"): 750 line = line.strip() 751 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 752if not m: 753continue 754 755 assignments = m.group(1).split(':') 756for a in assignments: 757 vals = a.split('=') 758 key = vals[0].strip() 759 val = ('='.join(vals[1:])).strip() 760if val.endswith('\"')and val.startswith('"'): 761 val = val[1:-1] 762 763 values[key] = val 764 765 paths = values.get("depot-paths") 766if not paths: 767 paths = values.get("depot-path") 768if paths: 769 values['depot-paths'] = paths.split(',') 770return values 771 772defgitBranchExists(branch): 773 proc = subprocess.Popen(["git","rev-parse", branch], 774 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 775return proc.wait() ==0; 776 777defgitUpdateRef(ref, newvalue): 778 subprocess.check_call(["git","update-ref", ref, newvalue]) 779 780defgitDeleteRef(ref): 781 subprocess.check_call(["git","update-ref","-d", ref]) 782 783_gitConfig = {} 784 785defgitConfig(key, typeSpecifier=None): 786if key not in _gitConfig: 787 cmd = ["git","config"] 788if typeSpecifier: 789 cmd += [ typeSpecifier ] 790 cmd += [ key ] 791 s =read_pipe(cmd, ignore_error=True) 792 _gitConfig[key] = s.strip() 793return _gitConfig[key] 794 795defgitConfigBool(key): 796"""Return a bool, using git config --bool. It is True only if the 797 variable is set to true, and False if set to false or not present 798 in the config.""" 799 800if key not in _gitConfig: 801 _gitConfig[key] =gitConfig(key,'--bool') =="true" 802return _gitConfig[key] 803 804defgitConfigInt(key): 805if key not in _gitConfig: 806 cmd = ["git","config","--int", key ] 807 s =read_pipe(cmd, ignore_error=True) 808 v = s.strip() 809try: 810 _gitConfig[key] =int(gitConfig(key,'--int')) 811exceptValueError: 812 _gitConfig[key] =None 813return _gitConfig[key] 814 815defgitConfigList(key): 816if key not in _gitConfig: 817 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 818 _gitConfig[key] = s.strip().splitlines() 819if _gitConfig[key] == ['']: 820 _gitConfig[key] = [] 821return _gitConfig[key] 822 823defp4BranchesInGit(branchesAreInRemotes=True): 824"""Find all the branches whose names start with "p4/", looking 825 in remotes or heads as specified by the argument. Return 826 a dictionary of{ branch: revision }for each one found. 827 The branch names are the short names, without any 828 "p4/" prefix.""" 829 830 branches = {} 831 832 cmdline ="git rev-parse --symbolic " 833if branchesAreInRemotes: 834 cmdline +="--remotes" 835else: 836 cmdline +="--branches" 837 838for line inread_pipe_lines(cmdline): 839 line = line.strip() 840 841# only import to p4/ 842if not line.startswith('p4/'): 843continue 844# special symbolic ref to p4/master 845if line =="p4/HEAD": 846continue 847 848# strip off p4/ prefix 849 branch = line[len("p4/"):] 850 851 branches[branch] =parseRevision(line) 852 853return branches 854 855defbranch_exists(branch): 856"""Make sure that the given ref name really exists.""" 857 858 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 859 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 860 out, _ = p.communicate() 861if p.returncode: 862return False 863# expect exactly one line of output: the branch name 864return out.rstrip() == branch 865 866deffindUpstreamBranchPoint(head ="HEAD"): 867 branches =p4BranchesInGit() 868# map from depot-path to branch name 869 branchByDepotPath = {} 870for branch in branches.keys(): 871 tip = branches[branch] 872 log =extractLogMessageFromGitCommit(tip) 873 settings =extractSettingsGitLog(log) 874if"depot-paths"in settings: 875 paths =",".join(settings["depot-paths"]) 876 branchByDepotPath[paths] ="remotes/p4/"+ branch 877 878 settings =None 879 parent =0 880while parent <65535: 881 commit = head +"~%s"% parent 882 log =extractLogMessageFromGitCommit(commit) 883 settings =extractSettingsGitLog(log) 884if"depot-paths"in settings: 885 paths =",".join(settings["depot-paths"]) 886if paths in branchByDepotPath: 887return[branchByDepotPath[paths], settings] 888 889 parent = parent +1 890 891return["", settings] 892 893defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 894if not silent: 895print("Creating/updating branch(es) in%sbased on origin branch(es)" 896% localRefPrefix) 897 898 originPrefix ="origin/p4/" 899 900for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 901 line = line.strip() 902if(not line.startswith(originPrefix))or line.endswith("HEAD"): 903continue 904 905 headName = line[len(originPrefix):] 906 remoteHead = localRefPrefix + headName 907 originHead = line 908 909 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 910if('depot-paths'not in original 911or'change'not in original): 912continue 913 914 update =False 915if notgitBranchExists(remoteHead): 916if verbose: 917print("creating%s"% remoteHead) 918 update =True 919else: 920 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 921if'change'in settings: 922if settings['depot-paths'] == original['depot-paths']: 923 originP4Change =int(original['change']) 924 p4Change =int(settings['change']) 925if originP4Change > p4Change: 926print("%s(%s) is newer than%s(%s). " 927"Updating p4 branch from origin." 928% (originHead, originP4Change, 929 remoteHead, p4Change)) 930 update =True 931else: 932print("Ignoring:%swas imported from%swhile " 933"%swas imported from%s" 934% (originHead,','.join(original['depot-paths']), 935 remoteHead,','.join(settings['depot-paths']))) 936 937if update: 938system("git update-ref%s %s"% (remoteHead, originHead)) 939 940deforiginP4BranchesExist(): 941returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 942 943 944defp4ParseNumericChangeRange(parts): 945 changeStart =int(parts[0][1:]) 946if parts[1] =='#head': 947 changeEnd =p4_last_change() 948else: 949 changeEnd =int(parts[1]) 950 951return(changeStart, changeEnd) 952 953defchooseBlockSize(blockSize): 954if blockSize: 955return blockSize 956else: 957return defaultBlockSize 958 959defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 960assert depotPaths 961 962# Parse the change range into start and end. Try to find integer 963# revision ranges as these can be broken up into blocks to avoid 964# hitting server-side limits (maxrows, maxscanresults). But if 965# that doesn't work, fall back to using the raw revision specifier 966# strings, without using block mode. 967 968if changeRange is None or changeRange =='': 969 changeStart =1 970 changeEnd =p4_last_change() 971 block_size =chooseBlockSize(requestedBlockSize) 972else: 973 parts = changeRange.split(',') 974assertlen(parts) ==2 975try: 976(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 977 block_size =chooseBlockSize(requestedBlockSize) 978exceptValueError: 979 changeStart = parts[0][1:] 980 changeEnd = parts[1] 981if requestedBlockSize: 982die("cannot use --changes-block-size with non-numeric revisions") 983 block_size =None 984 985 changes =set() 986 987# Retrieve changes a block at a time, to prevent running 988# into a MaxResults/MaxScanRows error from the server. If 989# we _do_ hit one of those errors, turn down the block size 990 991while True: 992 cmd = ['changes'] 993 994if block_size: 995 end =min(changeEnd, changeStart + block_size) 996 revisionRange ="%d,%d"% (changeStart, end) 997else: 998 revisionRange ="%s,%s"% (changeStart, changeEnd) 9991000for p in depotPaths:1001 cmd += ["%s...@%s"% (p, revisionRange)]10021003# fetch the changes1004try:1005 result =p4CmdList(cmd, errors_as_exceptions=True)1006except P4RequestSizeException as e:1007if not block_size:1008 block_size = e.limit1009elif block_size > e.limit:1010 block_size = e.limit1011else:1012 block_size =max(2, block_size //2)10131014if verbose:print("block size error, retrying with block size{0}".format(block_size))1015continue1016except P4Exception as e:1017die('Error retrieving changes description ({0})'.format(e.p4ExitCode))10181019# Insert changes in chronological order1020for entry inreversed(result):1021if'change'not in entry:1022continue1023 changes.add(int(entry['change']))10241025if not block_size:1026break10271028if end >= changeEnd:1029break10301031 changeStart = end +110321033 changes =sorted(changes)1034return changes10351036defp4PathStartsWith(path, prefix):1037# This method tries to remedy a potential mixed-case issue:1038#1039# If UserA adds //depot/DirA/file11040# and UserB adds //depot/dira/file21041#1042# we may or may not have a problem. If you have core.ignorecase=true,1043# we treat DirA and dira as the same directory1044ifgitConfigBool("core.ignorecase"):1045return path.lower().startswith(prefix.lower())1046return path.startswith(prefix)10471048defgetClientSpec():1049"""Look at the p4 client spec, create a View() object that contains1050 all the mappings, and return it."""10511052 specList =p4CmdList("client -o")1053iflen(specList) !=1:1054die('Output from "client -o" is%dlines, expecting 1'%1055len(specList))10561057# dictionary of all client parameters1058 entry = specList[0]10591060# the //client/ name1061 client_name = entry["Client"]10621063# just the keys that start with "View"1064 view_keys = [ k for k in entry.keys()if k.startswith("View") ]10651066# hold this new View1067 view =View(client_name)10681069# append the lines, in order, to the view1070for view_num inrange(len(view_keys)):1071 k ="View%d"% view_num1072if k not in view_keys:1073die("Expected view key%smissing"% k)1074 view.append(entry[k])10751076return view10771078defgetClientRoot():1079"""Grab the client directory."""10801081 output =p4CmdList("client -o")1082iflen(output) !=1:1083die('Output from "client -o" is%dlines, expecting 1'%len(output))10841085 entry = output[0]1086if"Root"not in entry:1087die('Client has no "Root"')10881089return entry["Root"]10901091#1092# P4 wildcards are not allowed in filenames. P4 complains1093# if you simply add them, but you can force it with "-f", in1094# which case it translates them into %xx encoding internally.1095#1096defwildcard_decode(path):1097# Search for and fix just these four characters. Do % last so1098# that fixing it does not inadvertently create new %-escapes.1099# Cannot have * in a filename in windows; untested as to1100# what p4 would do in such a case.1101if not platform.system() =="Windows":1102 path = path.replace("%2A","*")1103 path = path.replace("%23","#") \1104.replace("%40","@") \1105.replace("%25","%")1106return path11071108defwildcard_encode(path):1109# do % first to avoid double-encoding the %s introduced here1110 path = path.replace("%","%25") \1111.replace("*","%2A") \1112.replace("#","%23") \1113.replace("@","%40")1114return path11151116defwildcard_present(path):1117 m = re.search("[*#@%]", path)1118return m is not None11191120classLargeFileSystem(object):1121"""Base class for large file system support."""11221123def__init__(self, writeToGitStream):1124 self.largeFiles =set()1125 self.writeToGitStream = writeToGitStream11261127defgeneratePointer(self, cloneDestination, contentFile):1128"""Return the content of a pointer file that is stored in Git instead of1129 the actual content."""1130assert False,"Method 'generatePointer' required in "+ self.__class__.__name__11311132defpushFile(self, localLargeFile):1133"""Push the actual content which is not stored in the Git repository to1134 a server."""1135assert False,"Method 'pushFile' required in "+ self.__class__.__name__11361137defhasLargeFileExtension(self, relPath):1138returnreduce(1139lambda a, b: a or b,1140[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1141False1142)11431144defgenerateTempFile(self, contents):1145 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1146for d in contents:1147 contentFile.write(d)1148 contentFile.close()1149return contentFile.name11501151defexceedsLargeFileThreshold(self, relPath, contents):1152ifgitConfigInt('git-p4.largeFileThreshold'):1153 contentsSize =sum(len(d)for d in contents)1154if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1155return True1156ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1157 contentsSize =sum(len(d)for d in contents)1158if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1159return False1160 contentTempFile = self.generateTempFile(contents)1161 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1162 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1163 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1164 zf.close()1165 compressedContentsSize = zf.infolist()[0].compress_size1166 os.remove(contentTempFile)1167 os.remove(compressedContentFile.name)1168if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1169return True1170return False11711172defaddLargeFile(self, relPath):1173 self.largeFiles.add(relPath)11741175defremoveLargeFile(self, relPath):1176 self.largeFiles.remove(relPath)11771178defisLargeFile(self, relPath):1179return relPath in self.largeFiles11801181defprocessContent(self, git_mode, relPath, contents):1182"""Processes the content of git fast import. This method decides if a1183 file is stored in the large file system and handles all necessary1184 steps."""1185if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1186 contentTempFile = self.generateTempFile(contents)1187(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1188if pointer_git_mode:1189 git_mode = pointer_git_mode1190if localLargeFile:1191# Move temp file to final location in large file system1192 largeFileDir = os.path.dirname(localLargeFile)1193if not os.path.isdir(largeFileDir):1194 os.makedirs(largeFileDir)1195 shutil.move(contentTempFile, localLargeFile)1196 self.addLargeFile(relPath)1197ifgitConfigBool('git-p4.largeFilePush'):1198 self.pushFile(localLargeFile)1199if verbose:1200 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1201return(git_mode, contents)12021203classMockLFS(LargeFileSystem):1204"""Mock large file system for testing."""12051206defgeneratePointer(self, contentFile):1207"""The pointer content is the original content prefixed with "pointer-".1208 The local filename of the large file storage is derived from the file content.1209 """1210withopen(contentFile,'r')as f:1211 content =next(f)1212 gitMode ='100644'1213 pointerContents ='pointer-'+ content1214 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1215return(gitMode, pointerContents, localLargeFile)12161217defpushFile(self, localLargeFile):1218"""The remote filename of the large file storage is the same as the local1219 one but in a different directory.1220 """1221 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1222if not os.path.exists(remotePath):1223 os.makedirs(remotePath)1224 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))12251226classGitLFS(LargeFileSystem):1227"""Git LFS as backend for the git-p4 large file system.1228 See https://git-lfs.github.com/ for details."""12291230def__init__(self, *args):1231 LargeFileSystem.__init__(self, *args)1232 self.baseGitAttributes = []12331234defgeneratePointer(self, contentFile):1235"""Generate a Git LFS pointer for the content. Return LFS Pointer file1236 mode and content which is stored in the Git repository instead of1237 the actual content. Return also the new location of the actual1238 content.1239 """1240if os.path.getsize(contentFile) ==0:1241return(None,'',None)12421243 pointerProcess = subprocess.Popen(1244['git','lfs','pointer','--file='+ contentFile],1245 stdout=subprocess.PIPE1246)1247 pointerFile = pointerProcess.stdout.read()1248if pointerProcess.wait():1249 os.remove(contentFile)1250die('git-lfs pointer command failed. Did you install the extension?')12511252# Git LFS removed the preamble in the output of the 'pointer' command1253# starting from version 1.2.0. Check for the preamble here to support1254# earlier versions.1255# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431256if pointerFile.startswith('Git LFS pointer for'):1257 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)12581259 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1260 localLargeFile = os.path.join(1261 os.getcwd(),1262'.git','lfs','objects', oid[:2], oid[2:4],1263 oid,1264)1265# LFS Spec states that pointer files should not have the executable bit set.1266 gitMode ='100644'1267return(gitMode, pointerFile, localLargeFile)12681269defpushFile(self, localLargeFile):1270 uploadProcess = subprocess.Popen(1271['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1272)1273if uploadProcess.wait():1274die('git-lfs push command failed. Did you define a remote?')12751276defgenerateGitAttributes(self):1277return(1278 self.baseGitAttributes +1279[1280'\n',1281'#\n',1282'# Git LFS (see https://git-lfs.github.com/)\n',1283'#\n',1284] +1285['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1286for f insorted(gitConfigList('git-p4.largeFileExtensions'))1287] +1288['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1289for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1290]1291)12921293defaddLargeFile(self, relPath):1294 LargeFileSystem.addLargeFile(self, relPath)1295 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12961297defremoveLargeFile(self, relPath):1298 LargeFileSystem.removeLargeFile(self, relPath)1299 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())13001301defprocessContent(self, git_mode, relPath, contents):1302if relPath =='.gitattributes':1303 self.baseGitAttributes = contents1304return(git_mode, self.generateGitAttributes())1305else:1306return LargeFileSystem.processContent(self, git_mode, relPath, contents)13071308class Command:1309def__init__(self):1310 self.usage ="usage: %prog [options]"1311 self.needsGit =True1312 self.verbose =False13131314# This is required for the "append" cloneExclude action1315defensure_value(self, attr, value):1316if nothasattr(self, attr)orgetattr(self, attr)is None:1317setattr(self, attr, value)1318returngetattr(self, attr)13191320class P4UserMap:1321def__init__(self):1322 self.userMapFromPerforceServer =False1323 self.myP4UserId =None13241325defp4UserId(self):1326if self.myP4UserId:1327return self.myP4UserId13281329 results =p4CmdList("user -o")1330for r in results:1331if'User'in r:1332 self.myP4UserId = r['User']1333return r['User']1334die("Could not find your p4 user id")13351336defp4UserIsMe(self, p4User):1337# return True if the given p4 user is actually me1338 me = self.p4UserId()1339if not p4User or p4User != me:1340return False1341else:1342return True13431344defgetUserCacheFilename(self):1345 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1346return home +"/.gitp4-usercache.txt"13471348defgetUserMapFromPerforceServer(self):1349if self.userMapFromPerforceServer:1350return1351 self.users = {}1352 self.emails = {}13531354for output inp4CmdList("users"):1355if"User"not in output:1356continue1357 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1358 self.emails[output["Email"]] = output["User"]13591360 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1361for mapUserConfig ingitConfigList("git-p4.mapUser"):1362 mapUser = mapUserConfigRegex.findall(mapUserConfig)1363if mapUser andlen(mapUser[0]) ==3:1364 user = mapUser[0][0]1365 fullname = mapUser[0][1]1366 email = mapUser[0][2]1367 self.users[user] = fullname +" <"+ email +">"1368 self.emails[email] = user13691370 s =''1371for(key, val)in self.users.items():1372 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))13731374open(self.getUserCacheFilename(),"wb").write(s)1375 self.userMapFromPerforceServer =True13761377defloadUserMapFromCache(self):1378 self.users = {}1379 self.userMapFromPerforceServer =False1380try:1381 cache =open(self.getUserCacheFilename(),"rb")1382 lines = cache.readlines()1383 cache.close()1384for line in lines:1385 entry = line.strip().split("\t")1386 self.users[entry[0]] = entry[1]1387exceptIOError:1388 self.getUserMapFromPerforceServer()13891390classP4Debug(Command):1391def__init__(self):1392 Command.__init__(self)1393 self.options = []1394 self.description ="A tool to debug the output of p4 -G."1395 self.needsGit =False13961397defrun(self, args):1398 j =01399for output inp4CmdList(args):1400print('Element:%d'% j)1401 j +=11402print(output)1403return True14041405classP4RollBack(Command):1406def__init__(self):1407 Command.__init__(self)1408 self.options = [1409 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1410]1411 self.description ="A tool to debug the multi-branch import. Don't use :)"1412 self.rollbackLocalBranches =False14131414defrun(self, args):1415iflen(args) !=1:1416return False1417 maxChange =int(args[0])14181419if"p4ExitCode"inp4Cmd("changes -m 1"):1420die("Problems executing p4");14211422if self.rollbackLocalBranches:1423 refPrefix ="refs/heads/"1424 lines =read_pipe_lines("git rev-parse --symbolic --branches")1425else:1426 refPrefix ="refs/remotes/"1427 lines =read_pipe_lines("git rev-parse --symbolic --remotes")14281429for line in lines:1430if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1431 line = line.strip()1432 ref = refPrefix + line1433 log =extractLogMessageFromGitCommit(ref)1434 settings =extractSettingsGitLog(log)14351436 depotPaths = settings['depot-paths']1437 change = settings['change']14381439 changed =False14401441iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1442for p in depotPaths]))) ==0:1443print("Branch%sdid not exist at change%s, deleting."% (ref, maxChange))1444system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1445continue14461447while change andint(change) > maxChange:1448 changed =True1449if self.verbose:1450print("%sis at%s; rewinding towards%s"% (ref, change, maxChange))1451system("git update-ref%s\"%s^\""% (ref, ref))1452 log =extractLogMessageFromGitCommit(ref)1453 settings =extractSettingsGitLog(log)145414551456 depotPaths = settings['depot-paths']1457 change = settings['change']14581459if changed:1460print("%srewound to%s"% (ref, change))14611462return True14631464classP4Submit(Command, P4UserMap):14651466 conflict_behavior_choices = ("ask","skip","quit")14671468def__init__(self):1469 Command.__init__(self)1470 P4UserMap.__init__(self)1471 self.options = [1472 optparse.make_option("--origin", dest="origin"),1473 optparse.make_option("-M", dest="detectRenames", action="store_true"),1474# preserve the user, requires relevant p4 permissions1475 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1476 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1477 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1478 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1479 optparse.make_option("--conflict", dest="conflict_behavior",1480 choices=self.conflict_behavior_choices),1481 optparse.make_option("--branch", dest="branch"),1482 optparse.make_option("--shelve", dest="shelve", action="store_true",1483help="Shelve instead of submit. Shelved files are reverted, "1484"restoring the workspace to the state before the shelve"),1485 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1486 metavar="CHANGELIST",1487help="update an existing shelved changelist, implies --shelve, "1488"repeat in-order for multiple shelved changelists"),1489 optparse.make_option("--commit", dest="commit", metavar="COMMIT",1490help="submit only the specified commit(s), one commit or xxx..xxx"),1491 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",1492help="Disable rebase after submit is completed. Can be useful if you "1493"work from a local git branch that is not master"),1494 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",1495help="Skip Perforce sync of p4/master after submit or shelve"),1496]1497 self.description ="Submit changes from git to the perforce depot."1498 self.usage +=" [name of git branch to submit into perforce depot]"1499 self.origin =""1500 self.detectRenames =False1501 self.preserveUser =gitConfigBool("git-p4.preserveUser")1502 self.dry_run =False1503 self.shelve =False1504 self.update_shelve =list()1505 self.commit =""1506 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1507 self.disable_p4sync =gitConfigBool("git-p4.disableP4Sync")1508 self.prepare_p4_only =False1509 self.conflict_behavior =None1510 self.isWindows = (platform.system() =="Windows")1511 self.exportLabels =False1512 self.p4HasMoveCommand =p4_has_move_command()1513 self.branch =None15141515ifgitConfig('git-p4.largeFileSystem'):1516die("Large file system not supported for git-p4 submit command. Please remove it from config.")15171518defcheck(self):1519iflen(p4CmdList("opened ...")) >0:1520die("You have files opened with perforce! Close them before starting the sync.")15211522defseparate_jobs_from_description(self, message):1523"""Extract and return a possible Jobs field in the commit1524 message. It goes into a separate section in the p4 change1525 specification.15261527 A jobs line starts with "Jobs:" and looks like a new field1528 in a form. Values are white-space separated on the same1529 line or on following lines that start with a tab.15301531 This does not parse and extract the full git commit message1532 like a p4 form. It just sees the Jobs: line as a marker1533 to pass everything from then on directly into the p4 form,1534 but outside the description section.15351536 Return a tuple (stripped log message, jobs string)."""15371538 m = re.search(r'^Jobs:', message, re.MULTILINE)1539if m is None:1540return(message,None)15411542 jobtext = message[m.start():]1543 stripped_message = message[:m.start()].rstrip()1544return(stripped_message, jobtext)15451546defprepareLogMessage(self, template, message, jobs):1547"""Edits the template returned from "p4 change -o" to insert1548 the message in the Description field, and the jobs text in1549 the Jobs field."""1550 result =""15511552 inDescriptionSection =False15531554for line in template.split("\n"):1555if line.startswith("#"):1556 result += line +"\n"1557continue15581559if inDescriptionSection:1560if line.startswith("Files:")or line.startswith("Jobs:"):1561 inDescriptionSection =False1562# insert Jobs section1563if jobs:1564 result += jobs +"\n"1565else:1566continue1567else:1568if line.startswith("Description:"):1569 inDescriptionSection =True1570 line +="\n"1571for messageLine in message.split("\n"):1572 line +="\t"+ messageLine +"\n"15731574 result += line +"\n"15751576return result15771578defpatchRCSKeywords(self,file, pattern):1579# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1580(handle, outFileName) = tempfile.mkstemp(dir='.')1581try:1582 outFile = os.fdopen(handle,"w+")1583 inFile =open(file,"r")1584 regexp = re.compile(pattern, re.VERBOSE)1585for line in inFile.readlines():1586 line = regexp.sub(r'$\1$', line)1587 outFile.write(line)1588 inFile.close()1589 outFile.close()1590# Forcibly overwrite the original file1591 os.unlink(file)1592 shutil.move(outFileName,file)1593except:1594# cleanup our temporary file1595 os.unlink(outFileName)1596print("Failed to strip RCS keywords in%s"%file)1597raise15981599print("Patched up RCS keywords in%s"%file)16001601defp4UserForCommit(self,id):1602# Return the tuple (perforce user,git email) for a given git commit id1603 self.getUserMapFromPerforceServer()1604 gitEmail =read_pipe(["git","log","--max-count=1",1605"--format=%ae",id])1606 gitEmail = gitEmail.strip()1607if gitEmail not in self.emails:1608return(None,gitEmail)1609else:1610return(self.emails[gitEmail],gitEmail)16111612defcheckValidP4Users(self,commits):1613# check if any git authors cannot be mapped to p4 users1614foridin commits:1615(user,email) = self.p4UserForCommit(id)1616if not user:1617 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1618ifgitConfigBool("git-p4.allowMissingP4Users"):1619print("%s"% msg)1620else:1621die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)16221623deflastP4Changelist(self):1624# Get back the last changelist number submitted in this client spec. This1625# then gets used to patch up the username in the change. If the same1626# client spec is being used by multiple processes then this might go1627# wrong.1628 results =p4CmdList("client -o")# find the current client1629 client =None1630for r in results:1631if'Client'in r:1632 client = r['Client']1633break1634if not client:1635die("could not get client spec")1636 results =p4CmdList(["changes","-c", client,"-m","1"])1637for r in results:1638if'change'in r:1639return r['change']1640die("Could not get changelist number for last submit - cannot patch up user details")16411642defmodifyChangelistUser(self, changelist, newUser):1643# fixup the user field of a changelist after it has been submitted.1644 changes =p4CmdList("change -o%s"% changelist)1645iflen(changes) !=1:1646die("Bad output from p4 change modifying%sto user%s"%1647(changelist, newUser))16481649 c = changes[0]1650if c['User'] == newUser:return# nothing to do1651 c['User'] = newUser1652input= marshal.dumps(c)16531654 result =p4CmdList("change -f -i", stdin=input)1655for r in result:1656if'code'in r:1657if r['code'] =='error':1658die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1659if'data'in r:1660print("Updated user field for changelist%sto%s"% (changelist, newUser))1661return1662die("Could not modify user field of changelist%sto%s"% (changelist, newUser))16631664defcanChangeChangelists(self):1665# check to see if we have p4 admin or super-user permissions, either of1666# which are required to modify changelists.1667 results =p4CmdList(["protects", self.depotPath])1668for r in results:1669if'perm'in r:1670if r['perm'] =='admin':1671return11672if r['perm'] =='super':1673return11674return016751676defprepareSubmitTemplate(self, changelist=None):1677"""Run "p4 change -o" to grab a change specification template.1678 This does not use "p4 -G", as it is nice to keep the submission1679 template in original order, since a human might edit it.16801681 Remove lines in the Files section that show changes to files1682 outside the depot path we're committing into."""16831684[upstream, settings] =findUpstreamBranchPoint()16851686 template ="""\1687# A Perforce Change Specification.1688#1689# Change: The change number. 'new' on a new changelist.1690# Date: The date this specification was last modified.1691# Client: The client on which the changelist was created. Read-only.1692# User: The user who created the changelist.1693# Status: Either 'pending' or 'submitted'. Read-only.1694# Type: Either 'public' or 'restricted'. Default is 'public'.1695# Description: Comments about the changelist. Required.1696# Jobs: What opened jobs are to be closed by this changelist.1697# You may delete jobs from this list. (New changelists only.)1698# Files: What opened files from the default changelist are to be added1699# to this changelist. You may delete files from this list.1700# (New changelists only.)1701"""1702 files_list = []1703 inFilesSection =False1704 change_entry =None1705 args = ['change','-o']1706if changelist:1707 args.append(str(changelist))1708for entry inp4CmdList(args):1709if'code'not in entry:1710continue1711if entry['code'] =='stat':1712 change_entry = entry1713break1714if not change_entry:1715die('Failed to decode output of p4 change -o')1716for key, value in change_entry.iteritems():1717if key.startswith('File'):1718if'depot-paths'in settings:1719if not[p for p in settings['depot-paths']1720ifp4PathStartsWith(value, p)]:1721continue1722else:1723if notp4PathStartsWith(value, self.depotPath):1724continue1725 files_list.append(value)1726continue1727# Output in the order expected by prepareLogMessage1728for key in['Change','Client','User','Status','Description','Jobs']:1729if key not in change_entry:1730continue1731 template +='\n'1732 template += key +':'1733if key =='Description':1734 template +='\n'1735for field_line in change_entry[key].splitlines():1736 template +='\t'+field_line+'\n'1737iflen(files_list) >0:1738 template +='\n'1739 template +='Files:\n'1740for path in files_list:1741 template +='\t'+path+'\n'1742return template17431744defedit_template(self, template_file):1745"""Invoke the editor to let the user change the submission1746 message. Return true if okay to continue with the submit."""17471748# if configured to skip the editing part, just submit1749ifgitConfigBool("git-p4.skipSubmitEdit"):1750return True17511752# look at the modification time, to check later if the user saved1753# the file1754 mtime = os.stat(template_file).st_mtime17551756# invoke the editor1757if"P4EDITOR"in os.environ and(os.environ.get("P4EDITOR") !=""):1758 editor = os.environ.get("P4EDITOR")1759else:1760 editor =read_pipe("git var GIT_EDITOR").strip()1761system(["sh","-c", ('%s"$@"'% editor), editor, template_file])17621763# If the file was not saved, prompt to see if this patch should1764# be skipped. But skip this verification step if configured so.1765ifgitConfigBool("git-p4.skipSubmitEditCheck"):1766return True17671768# modification time updated means user saved the file1769if os.stat(template_file).st_mtime > mtime:1770return True17711772while True:1773 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1774if response =='y':1775return True1776if response =='n':1777return False17781779defget_diff_description(self, editedFiles, filesToAdd, symlinks):1780# diff1781if"P4DIFF"in os.environ:1782del(os.environ["P4DIFF"])1783 diff =""1784for editedFile in editedFiles:1785 diff +=p4_read_pipe(['diff','-du',1786wildcard_encode(editedFile)])17871788# new file diff1789 newdiff =""1790for newFile in filesToAdd:1791 newdiff +="==== new file ====\n"1792 newdiff +="--- /dev/null\n"1793 newdiff +="+++%s\n"% newFile17941795 is_link = os.path.islink(newFile)1796 expect_link = newFile in symlinks17971798if is_link and expect_link:1799 newdiff +="+%s\n"% os.readlink(newFile)1800else:1801 f =open(newFile,"r")1802for line in f.readlines():1803 newdiff +="+"+ line1804 f.close()18051806return(diff + newdiff).replace('\r\n','\n')18071808defapplyCommit(self,id):1809"""Apply one commit, return True if it succeeded."""18101811print("Applying",read_pipe(["git","show","-s",1812"--format=format:%h%s",id]))18131814(p4User, gitEmail) = self.p4UserForCommit(id)18151816 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1817 filesToAdd =set()1818 filesToChangeType =set()1819 filesToDelete =set()1820 editedFiles =set()1821 pureRenameCopy =set()1822 symlinks =set()1823 filesToChangeExecBit = {}1824 all_files =list()18251826for line in diff:1827 diff =parseDiffTreeEntry(line)1828 modifier = diff['status']1829 path = diff['src']1830 all_files.append(path)18311832if modifier =="M":1833p4_edit(path)1834ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1835 filesToChangeExecBit[path] = diff['dst_mode']1836 editedFiles.add(path)1837elif modifier =="A":1838 filesToAdd.add(path)1839 filesToChangeExecBit[path] = diff['dst_mode']1840if path in filesToDelete:1841 filesToDelete.remove(path)18421843 dst_mode =int(diff['dst_mode'],8)1844if dst_mode ==0o120000:1845 symlinks.add(path)18461847elif modifier =="D":1848 filesToDelete.add(path)1849if path in filesToAdd:1850 filesToAdd.remove(path)1851elif modifier =="C":1852 src, dest = diff['src'], diff['dst']1853p4_integrate(src, dest)1854 pureRenameCopy.add(dest)1855if diff['src_sha1'] != diff['dst_sha1']:1856p4_edit(dest)1857 pureRenameCopy.discard(dest)1858ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1859p4_edit(dest)1860 pureRenameCopy.discard(dest)1861 filesToChangeExecBit[dest] = diff['dst_mode']1862if self.isWindows:1863# turn off read-only attribute1864 os.chmod(dest, stat.S_IWRITE)1865 os.unlink(dest)1866 editedFiles.add(dest)1867elif modifier =="R":1868 src, dest = diff['src'], diff['dst']1869if self.p4HasMoveCommand:1870p4_edit(src)# src must be open before move1871p4_move(src, dest)# opens for (move/delete, move/add)1872else:1873p4_integrate(src, dest)1874if diff['src_sha1'] != diff['dst_sha1']:1875p4_edit(dest)1876else:1877 pureRenameCopy.add(dest)1878ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1879if not self.p4HasMoveCommand:1880p4_edit(dest)# with move: already open, writable1881 filesToChangeExecBit[dest] = diff['dst_mode']1882if not self.p4HasMoveCommand:1883if self.isWindows:1884 os.chmod(dest, stat.S_IWRITE)1885 os.unlink(dest)1886 filesToDelete.add(src)1887 editedFiles.add(dest)1888elif modifier =="T":1889 filesToChangeType.add(path)1890else:1891die("unknown modifier%sfor%s"% (modifier, path))18921893 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1894 patchcmd = diffcmd +" | git apply "1895 tryPatchCmd = patchcmd +"--check -"1896 applyPatchCmd = patchcmd +"--check --apply -"1897 patch_succeeded =True18981899if os.system(tryPatchCmd) !=0:1900 fixed_rcs_keywords =False1901 patch_succeeded =False1902print("Unfortunately applying the change failed!")19031904# Patch failed, maybe it's just RCS keyword woes. Look through1905# the patch to see if that's possible.1906ifgitConfigBool("git-p4.attemptRCSCleanup"):1907file=None1908 pattern =None1909 kwfiles = {}1910forfilein editedFiles | filesToDelete:1911# did this file's delta contain RCS keywords?1912 pattern =p4_keywords_regexp_for_file(file)19131914if pattern:1915# this file is a possibility...look for RCS keywords.1916 regexp = re.compile(pattern, re.VERBOSE)1917for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1918if regexp.search(line):1919if verbose:1920print("got keyword match on%sin%sin%s"% (pattern, line,file))1921 kwfiles[file] = pattern1922break19231924forfilein kwfiles:1925if verbose:1926print("zapping%swith%s"% (line,pattern))1927# File is being deleted, so not open in p4. Must1928# disable the read-only bit on windows.1929if self.isWindows andfilenot in editedFiles:1930 os.chmod(file, stat.S_IWRITE)1931 self.patchRCSKeywords(file, kwfiles[file])1932 fixed_rcs_keywords =True19331934if fixed_rcs_keywords:1935print("Retrying the patch with RCS keywords cleaned up")1936if os.system(tryPatchCmd) ==0:1937 patch_succeeded =True19381939if not patch_succeeded:1940for f in editedFiles:1941p4_revert(f)1942return False19431944#1945# Apply the patch for real, and do add/delete/+x handling.1946#1947system(applyPatchCmd)19481949for f in filesToChangeType:1950p4_edit(f,"-t","auto")1951for f in filesToAdd:1952p4_add(f)1953for f in filesToDelete:1954p4_revert(f)1955p4_delete(f)19561957# Set/clear executable bits1958for f in filesToChangeExecBit.keys():1959 mode = filesToChangeExecBit[f]1960setP4ExecBit(f, mode)19611962 update_shelve =01963iflen(self.update_shelve) >0:1964 update_shelve = self.update_shelve.pop(0)1965p4_reopen_in_change(update_shelve, all_files)19661967#1968# Build p4 change description, starting with the contents1969# of the git commit message.1970#1971 logMessage =extractLogMessageFromGitCommit(id)1972 logMessage = logMessage.strip()1973(logMessage, jobs) = self.separate_jobs_from_description(logMessage)19741975 template = self.prepareSubmitTemplate(update_shelve)1976 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)19771978if self.preserveUser:1979 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User19801981if self.checkAuthorship and not self.p4UserIsMe(p4User):1982 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1983 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1984 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"19851986 separatorLine ="######## everything below this line is just the diff #######\n"1987if not self.prepare_p4_only:1988 submitTemplate += separatorLine1989 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)19901991(handle, fileName) = tempfile.mkstemp()1992 tmpFile = os.fdopen(handle,"w+b")1993if self.isWindows:1994 submitTemplate = submitTemplate.replace("\n","\r\n")1995 tmpFile.write(submitTemplate)1996 tmpFile.close()19971998if self.prepare_p4_only:1999#2000# Leave the p4 tree prepared, and the submit template around2001# and let the user decide what to do next2002#2003print()2004print("P4 workspace prepared for submission.")2005print("To submit or revert, go to client workspace")2006print(" "+ self.clientPath)2007print()2008print("To submit, use\"p4 submit\"to write a new description,")2009print("or\"p4 submit -i <%s\"to use the one prepared by" \2010"\"git p4\"."% fileName)2011print("You can delete the file\"%s\"when finished."% fileName)20122013if self.preserveUser and p4User and not self.p4UserIsMe(p4User):2014print("To preserve change ownership by user%s, you must\n" \2015"do\"p4 change -f <change>\"after submitting and\n" \2016"edit the User field.")2017if pureRenameCopy:2018print("After submitting, renamed files must be re-synced.")2019print("Invoke\"p4 sync -f\"on each of these files:")2020for f in pureRenameCopy:2021print(" "+ f)20222023print()2024print("To revert the changes, use\"p4 revert ...\", and delete")2025print("the submit template file\"%s\""% fileName)2026if filesToAdd:2027print("Since the commit adds new files, they must be deleted:")2028for f in filesToAdd:2029print(" "+ f)2030print()2031return True20322033#2034# Let the user edit the change description, then submit it.2035#2036 submitted =False20372038try:2039if self.edit_template(fileName):2040# read the edited message and submit2041 tmpFile =open(fileName,"rb")2042 message = tmpFile.read()2043 tmpFile.close()2044if self.isWindows:2045 message = message.replace("\r\n","\n")2046 submitTemplate = message[:message.index(separatorLine)]20472048if update_shelve:2049p4_write_pipe(['shelve','-r','-i'], submitTemplate)2050elif self.shelve:2051p4_write_pipe(['shelve','-i'], submitTemplate)2052else:2053p4_write_pipe(['submit','-i'], submitTemplate)2054# The rename/copy happened by applying a patch that created a2055# new file. This leaves it writable, which confuses p4.2056for f in pureRenameCopy:2057p4_sync(f,"-f")20582059if self.preserveUser:2060if p4User:2061# Get last changelist number. Cannot easily get it from2062# the submit command output as the output is2063# unmarshalled.2064 changelist = self.lastP4Changelist()2065 self.modifyChangelistUser(changelist, p4User)20662067 submitted =True20682069finally:2070# skip this patch2071if not submitted or self.shelve:2072if self.shelve:2073print("Reverting shelved files.")2074else:2075print("Submission cancelled, undoing p4 changes.")2076for f in editedFiles | filesToDelete:2077p4_revert(f)2078for f in filesToAdd:2079p4_revert(f)2080 os.remove(f)20812082 os.remove(fileName)2083return submitted20842085# Export git tags as p4 labels. Create a p4 label and then tag2086# with that.2087defexportGitTags(self, gitTags):2088 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")2089iflen(validLabelRegexp) ==0:2090 validLabelRegexp = defaultLabelRegexp2091 m = re.compile(validLabelRegexp)20922093for name in gitTags:20942095if not m.match(name):2096if verbose:2097print("tag%sdoes not match regexp%s"% (name, validLabelRegexp))2098continue20992100# Get the p4 commit this corresponds to2101 logMessage =extractLogMessageFromGitCommit(name)2102 values =extractSettingsGitLog(logMessage)21032104if'change'not in values:2105# a tag pointing to something not sent to p4; ignore2106if verbose:2107print("git tag%sdoes not give a p4 commit"% name)2108continue2109else:2110 changelist = values['change']21112112# Get the tag details.2113 inHeader =True2114 isAnnotated =False2115 body = []2116for l inread_pipe_lines(["git","cat-file","-p", name]):2117 l = l.strip()2118if inHeader:2119if re.match(r'tag\s+', l):2120 isAnnotated =True2121elif re.match(r'\s*$', l):2122 inHeader =False2123continue2124else:2125 body.append(l)21262127if not isAnnotated:2128 body = ["lightweight tag imported by git p4\n"]21292130# Create the label - use the same view as the client spec we are using2131 clientSpec =getClientSpec()21322133 labelTemplate ="Label:%s\n"% name2134 labelTemplate +="Description:\n"2135for b in body:2136 labelTemplate +="\t"+ b +"\n"2137 labelTemplate +="View:\n"2138for depot_side in clientSpec.mappings:2139 labelTemplate +="\t%s\n"% depot_side21402141if self.dry_run:2142print("Would create p4 label%sfor tag"% name)2143elif self.prepare_p4_only:2144print("Not creating p4 label%sfor tag due to option" \2145" --prepare-p4-only"% name)2146else:2147p4_write_pipe(["label","-i"], labelTemplate)21482149# Use the label2150p4_system(["tag","-l", name] +2151["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])21522153if verbose:2154print("created p4 label for tag%s"% name)21552156defrun(self, args):2157iflen(args) ==0:2158 self.master =currentGitBranch()2159eliflen(args) ==1:2160 self.master = args[0]2161if notbranchExists(self.master):2162die("Branch%sdoes not exist"% self.master)2163else:2164return False21652166for i in self.update_shelve:2167if i <=0:2168 sys.exit("invalid changelist%d"% i)21692170if self.master:2171 allowSubmit =gitConfig("git-p4.allowSubmit")2172iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2173die("%sis not in git-p4.allowSubmit"% self.master)21742175[upstream, settings] =findUpstreamBranchPoint()2176 self.depotPath = settings['depot-paths'][0]2177iflen(self.origin) ==0:2178 self.origin = upstream21792180iflen(self.update_shelve) >0:2181 self.shelve =True21822183if self.preserveUser:2184if not self.canChangeChangelists():2185die("Cannot preserve user names without p4 super-user or admin permissions")21862187# if not set from the command line, try the config file2188if self.conflict_behavior is None:2189 val =gitConfig("git-p4.conflict")2190if val:2191if val not in self.conflict_behavior_choices:2192die("Invalid value '%s' for config git-p4.conflict"% val)2193else:2194 val ="ask"2195 self.conflict_behavior = val21962197if self.verbose:2198print("Origin branch is "+ self.origin)21992200iflen(self.depotPath) ==0:2201print("Internal error: cannot locate perforce depot path from existing branches")2202 sys.exit(128)22032204 self.useClientSpec =False2205ifgitConfigBool("git-p4.useclientspec"):2206 self.useClientSpec =True2207if self.useClientSpec:2208 self.clientSpecDirs =getClientSpec()22092210# Check for the existence of P4 branches2211 branchesDetected = (len(p4BranchesInGit().keys()) >1)22122213if self.useClientSpec and not branchesDetected:2214# all files are relative to the client spec2215 self.clientPath =getClientRoot()2216else:2217 self.clientPath =p4Where(self.depotPath)22182219if self.clientPath =="":2220die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)22212222print("Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath))2223 self.oldWorkingDirectory = os.getcwd()22242225# ensure the clientPath exists2226 new_client_dir =False2227if not os.path.exists(self.clientPath):2228 new_client_dir =True2229 os.makedirs(self.clientPath)22302231chdir(self.clientPath, is_client_path=True)2232if self.dry_run:2233print("Would synchronize p4 checkout in%s"% self.clientPath)2234else:2235print("Synchronizing p4 checkout...")2236if new_client_dir:2237# old one was destroyed, and maybe nobody told p42238p4_sync("...","-f")2239else:2240p4_sync("...")2241 self.check()22422243 commits = []2244if self.master:2245 committish = self.master2246else:2247 committish ='HEAD'22482249if self.commit !="":2250if self.commit.find("..") != -1:2251 limits_ish = self.commit.split("..")2252for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2253 commits.append(line.strip())2254 commits.reverse()2255else:2256 commits.append(self.commit)2257else:2258for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, committish)]):2259 commits.append(line.strip())2260 commits.reverse()22612262if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2263 self.checkAuthorship =False2264else:2265 self.checkAuthorship =True22662267if self.preserveUser:2268 self.checkValidP4Users(commits)22692270#2271# Build up a set of options to be passed to diff when2272# submitting each commit to p4.2273#2274if self.detectRenames:2275# command-line -M arg2276 self.diffOpts ="-M"2277else:2278# If not explicitly set check the config variable2279 detectRenames =gitConfig("git-p4.detectRenames")22802281if detectRenames.lower() =="false"or detectRenames =="":2282 self.diffOpts =""2283elif detectRenames.lower() =="true":2284 self.diffOpts ="-M"2285else:2286 self.diffOpts ="-M%s"% detectRenames22872288# no command-line arg for -C or --find-copies-harder, just2289# config variables2290 detectCopies =gitConfig("git-p4.detectCopies")2291if detectCopies.lower() =="false"or detectCopies =="":2292pass2293elif detectCopies.lower() =="true":2294 self.diffOpts +=" -C"2295else:2296 self.diffOpts +=" -C%s"% detectCopies22972298ifgitConfigBool("git-p4.detectCopiesHarder"):2299 self.diffOpts +=" --find-copies-harder"23002301 num_shelves =len(self.update_shelve)2302if num_shelves >0and num_shelves !=len(commits):2303 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2304(len(commits), num_shelves))23052306#2307# Apply the commits, one at a time. On failure, ask if should2308# continue to try the rest of the patches, or quit.2309#2310if self.dry_run:2311print("Would apply")2312 applied = []2313 last =len(commits) -12314for i, commit inenumerate(commits):2315if self.dry_run:2316print(" ",read_pipe(["git","show","-s",2317"--format=format:%h%s", commit]))2318 ok =True2319else:2320 ok = self.applyCommit(commit)2321if ok:2322 applied.append(commit)2323else:2324if self.prepare_p4_only and i < last:2325print("Processing only the first commit due to option" \2326" --prepare-p4-only")2327break2328if i < last:2329 quit =False2330while True:2331# prompt for what to do, or use the option/variable2332if self.conflict_behavior =="ask":2333print("What do you want to do?")2334 response =raw_input("[s]kip this commit but apply"2335" the rest, or [q]uit? ")2336if not response:2337continue2338elif self.conflict_behavior =="skip":2339 response ="s"2340elif self.conflict_behavior =="quit":2341 response ="q"2342else:2343die("Unknown conflict_behavior '%s'"%2344 self.conflict_behavior)23452346if response[0] =="s":2347print("Skipping this commit, but applying the rest")2348break2349if response[0] =="q":2350print("Quitting")2351 quit =True2352break2353if quit:2354break23552356chdir(self.oldWorkingDirectory)2357 shelved_applied ="shelved"if self.shelve else"applied"2358if self.dry_run:2359pass2360elif self.prepare_p4_only:2361pass2362eliflen(commits) ==len(applied):2363print("All commits{0}!".format(shelved_applied))23642365 sync =P4Sync()2366if self.branch:2367 sync.branch = self.branch2368if self.disable_p4sync:2369 sync.sync_origin_only()2370else:2371 sync.run([])23722373if not self.disable_rebase:2374 rebase =P4Rebase()2375 rebase.rebase()23762377else:2378iflen(applied) ==0:2379print("No commits{0}.".format(shelved_applied))2380else:2381print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2382for c in commits:2383if c in applied:2384 star ="*"2385else:2386 star =" "2387print(star,read_pipe(["git","show","-s",2388"--format=format:%h%s", c]))2389print("You will have to do 'git p4 sync' and rebase.")23902391ifgitConfigBool("git-p4.exportLabels"):2392 self.exportLabels =True23932394if self.exportLabels:2395 p4Labels =getP4Labels(self.depotPath)2396 gitTags =getGitTags()23972398 missingGitTags = gitTags - p4Labels2399 self.exportGitTags(missingGitTags)24002401# exit with error unless everything applied perfectly2402iflen(commits) !=len(applied):2403 sys.exit(1)24042405return True24062407classView(object):2408"""Represent a p4 view ("p4 help views"), and map files in a2409 repo according to the view."""24102411def__init__(self, client_name):2412 self.mappings = []2413 self.client_prefix ="//%s/"% client_name2414# cache results of "p4 where" to lookup client file locations2415 self.client_spec_path_cache = {}24162417defappend(self, view_line):2418"""Parse a view line, splitting it into depot and client2419 sides. Append to self.mappings, preserving order. This2420 is only needed for tag creation."""24212422# Split the view line into exactly two words. P4 enforces2423# structure on these lines that simplifies this quite a bit.2424#2425# Either or both words may be double-quoted.2426# Single quotes do not matter.2427# Double-quote marks cannot occur inside the words.2428# A + or - prefix is also inside the quotes.2429# There are no quotes unless they contain a space.2430# The line is already white-space stripped.2431# The two words are separated by a single space.2432#2433if view_line[0] =='"':2434# First word is double quoted. Find its end.2435 close_quote_index = view_line.find('"',1)2436if close_quote_index <=0:2437die("No first-word closing quote found:%s"% view_line)2438 depot_side = view_line[1:close_quote_index]2439# skip closing quote and space2440 rhs_index = close_quote_index +1+12441else:2442 space_index = view_line.find(" ")2443if space_index <=0:2444die("No word-splitting space found:%s"% view_line)2445 depot_side = view_line[0:space_index]2446 rhs_index = space_index +124472448# prefix + means overlay on previous mapping2449if depot_side.startswith("+"):2450 depot_side = depot_side[1:]24512452# prefix - means exclude this path, leave out of mappings2453 exclude =False2454if depot_side.startswith("-"):2455 exclude =True2456 depot_side = depot_side[1:]24572458if not exclude:2459 self.mappings.append(depot_side)24602461defconvert_client_path(self, clientFile):2462# chop off //client/ part to make it relative2463if not clientFile.startswith(self.client_prefix):2464die("No prefix '%s' on clientFile '%s'"%2465(self.client_prefix, clientFile))2466return clientFile[len(self.client_prefix):]24672468defupdate_client_spec_path_cache(self, files):2469""" Caching file paths by "p4 where" batch query """24702471# List depot file paths exclude that already cached2472 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]24732474iflen(fileArgs) ==0:2475return# All files in cache24762477 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2478for res in where_result:2479if"code"in res and res["code"] =="error":2480# assume error is "... file(s) not in client view"2481continue2482if"clientFile"not in res:2483die("No clientFile in 'p4 where' output")2484if"unmap"in res:2485# it will list all of them, but only one not unmap-ped2486continue2487ifgitConfigBool("core.ignorecase"):2488 res['depotFile'] = res['depotFile'].lower()2489 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])24902491# not found files or unmap files set to ""2492for depotFile in fileArgs:2493ifgitConfigBool("core.ignorecase"):2494 depotFile = depotFile.lower()2495if depotFile not in self.client_spec_path_cache:2496 self.client_spec_path_cache[depotFile] =""24972498defmap_in_client(self, depot_path):2499"""Return the relative location in the client where this2500 depot file should live. Returns "" if the file should2501 not be mapped in the client."""25022503ifgitConfigBool("core.ignorecase"):2504 depot_path = depot_path.lower()25052506if depot_path in self.client_spec_path_cache:2507return self.client_spec_path_cache[depot_path]25082509die("Error:%sis not found in client spec path"% depot_path )2510return""25112512classP4Sync(Command, P4UserMap):2513 delete_actions = ("delete","move/delete","purge")25142515def__init__(self):2516 Command.__init__(self)2517 P4UserMap.__init__(self)2518 self.options = [2519 optparse.make_option("--branch", dest="branch"),2520 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2521 optparse.make_option("--changesfile", dest="changesFile"),2522 optparse.make_option("--silent", dest="silent", action="store_true"),2523 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2524 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2525 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2526help="Import into refs/heads/ , not refs/remotes"),2527 optparse.make_option("--max-changes", dest="maxChanges",2528help="Maximum number of changes to import"),2529 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2530help="Internal block size to use when iteratively calling p4 changes"),2531 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2532help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2533 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2534help="Only sync files that are included in the Perforce Client Spec"),2535 optparse.make_option("-/", dest="cloneExclude",2536 action="append",type="string",2537help="exclude depot path"),2538]2539 self.description ="""Imports from Perforce into a git repository.\n2540 example:2541 //depot/my/project/ -- to import the current head2542 //depot/my/project/@all -- to import everything2543 //depot/my/project/@1,6 -- to import only from revision 1 to 625442545 (a ... is not needed in the path p4 specification, it's added implicitly)"""25462547 self.usage +=" //depot/path[@revRange]"2548 self.silent =False2549 self.createdBranches =set()2550 self.committedChanges =set()2551 self.branch =""2552 self.detectBranches =False2553 self.detectLabels =False2554 self.importLabels =False2555 self.changesFile =""2556 self.syncWithOrigin =True2557 self.importIntoRemotes =True2558 self.maxChanges =""2559 self.changes_block_size =None2560 self.keepRepoPath =False2561 self.depotPaths =None2562 self.p4BranchesInGit = []2563 self.cloneExclude = []2564 self.useClientSpec =False2565 self.useClientSpec_from_options =False2566 self.clientSpecDirs =None2567 self.tempBranches = []2568 self.tempBranchLocation ="refs/git-p4-tmp"2569 self.largeFileSystem =None2570 self.suppress_meta_comment =False25712572ifgitConfig('git-p4.largeFileSystem'):2573 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2574 self.largeFileSystem =largeFileSystemConstructor(2575lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2576)25772578ifgitConfig("git-p4.syncFromOrigin") =="false":2579 self.syncWithOrigin =False25802581 self.depotPaths = []2582 self.changeRange =""2583 self.previousDepotPaths = []2584 self.hasOrigin =False25852586# map from branch depot path to parent branch2587 self.knownBranches = {}2588 self.initialParents = {}25892590 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))2591 self.labels = {}25922593# Force a checkpoint in fast-import and wait for it to finish2594defcheckpoint(self):2595 self.gitStream.write("checkpoint\n\n")2596 self.gitStream.write("progress checkpoint\n\n")2597 out = self.gitOutput.readline()2598if self.verbose:2599print("checkpoint finished: "+ out)26002601defcmp_shelved(self, path, filerev, revision):2602""" Determine if a path at revision #filerev is the same as the file2603 at revision @revision for a shelved changelist. If they don't match,2604 unshelving won't be safe (we will get other changes mixed in).26052606 This is comparing the revision that the shelved changelist is *based* on, not2607 the shelved changelist itself.2608 """2609 ret =p4Cmd(["diff2","{0}#{1}".format(path, filerev),"{0}@{1}".format(path, revision)])2610if verbose:2611print("p4 diff2 path%sfilerev%srevision%s=>%s"% (path, filerev, revision, ret))2612return ret["status"] =="identical"26132614defextractFilesFromCommit(self, commit, shelved=False, shelved_cl =0, origin_revision =0):2615 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2616for path in self.cloneExclude]2617 files = []2618 fnum =02619while"depotFile%s"% fnum in commit:2620 path = commit["depotFile%s"% fnum]26212622if[p for p in self.cloneExclude2623ifp4PathStartsWith(path, p)]:2624 found =False2625else:2626 found = [p for p in self.depotPaths2627ifp4PathStartsWith(path, p)]2628if not found:2629 fnum = fnum +12630continue26312632file= {}2633file["path"] = path2634file["rev"] = commit["rev%s"% fnum]2635file["action"] = commit["action%s"% fnum]2636file["type"] = commit["type%s"% fnum]2637if shelved:2638file["shelved_cl"] =int(shelved_cl)26392640# For shelved changelists, check that the revision of each file that the2641# shelve was based on matches the revision that we are using for the2642# starting point for git-fast-import (self.initialParent). Otherwise2643# the resulting diff will contain deltas from multiple commits.26442645iffile["action"] !="add"and \2646not self.cmp_shelved(path,file["rev"], origin_revision):2647 sys.exit("change{0}not based on{1}for{2}, cannot unshelve".format(2648 commit["change"], self.initialParent, path))26492650 files.append(file)2651 fnum = fnum +12652return files26532654defextractJobsFromCommit(self, commit):2655 jobs = []2656 jnum =02657while"job%s"% jnum in commit:2658 job = commit["job%s"% jnum]2659 jobs.append(job)2660 jnum = jnum +12661return jobs26622663defstripRepoPath(self, path, prefixes):2664"""When streaming files, this is called to map a p4 depot path2665 to where it should go in git. The prefixes are either2666 self.depotPaths, or self.branchPrefixes in the case of2667 branch detection."""26682669if self.useClientSpec:2670# branch detection moves files up a level (the branch name)2671# from what client spec interpretation gives2672 path = self.clientSpecDirs.map_in_client(path)2673if self.detectBranches:2674for b in self.knownBranches:2675if path.startswith(b +"/"):2676 path = path[len(b)+1:]26772678elif self.keepRepoPath:2679# Preserve everything in relative path name except leading2680# //depot/; just look at first prefix as they all should2681# be in the same depot.2682 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2683ifp4PathStartsWith(path, depot):2684 path = path[len(depot):]26852686else:2687for p in prefixes:2688ifp4PathStartsWith(path, p):2689 path = path[len(p):]2690break26912692 path =wildcard_decode(path)2693return path26942695defsplitFilesIntoBranches(self, commit):2696"""Look at each depotFile in the commit to figure out to what2697 branch it belongs."""26982699if self.clientSpecDirs:2700 files = self.extractFilesFromCommit(commit)2701 self.clientSpecDirs.update_client_spec_path_cache(files)27022703 branches = {}2704 fnum =02705while"depotFile%s"% fnum in commit:2706 path = commit["depotFile%s"% fnum]2707 found = [p for p in self.depotPaths2708ifp4PathStartsWith(path, p)]2709if not found:2710 fnum = fnum +12711continue27122713file= {}2714file["path"] = path2715file["rev"] = commit["rev%s"% fnum]2716file["action"] = commit["action%s"% fnum]2717file["type"] = commit["type%s"% fnum]2718 fnum = fnum +127192720# start with the full relative path where this file would2721# go in a p4 client2722if self.useClientSpec:2723 relPath = self.clientSpecDirs.map_in_client(path)2724else:2725 relPath = self.stripRepoPath(path, self.depotPaths)27262727for branch in self.knownBranches.keys():2728# add a trailing slash so that a commit into qt/4.2foo2729# doesn't end up in qt/4.2, e.g.2730if relPath.startswith(branch +"/"):2731if branch not in branches:2732 branches[branch] = []2733 branches[branch].append(file)2734break27352736return branches27372738defwriteToGitStream(self, gitMode, relPath, contents):2739 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2740 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2741for d in contents:2742 self.gitStream.write(d)2743 self.gitStream.write('\n')27442745defencodeWithUTF8(self, path):2746try:2747 path.decode('ascii')2748except:2749 encoding ='utf8'2750ifgitConfig('git-p4.pathEncoding'):2751 encoding =gitConfig('git-p4.pathEncoding')2752 path = path.decode(encoding,'replace').encode('utf8','replace')2753if self.verbose:2754print('Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path))2755return path27562757# output one file from the P4 stream2758# - helper for streamP4Files27592760defstreamOneP4File(self,file, contents):2761 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2762 relPath = self.encodeWithUTF8(relPath)2763if verbose:2764 size =int(self.stream_file['fileSize'])2765 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2766 sys.stdout.flush()27672768(type_base, type_mods) =split_p4_type(file["type"])27692770 git_mode ="100644"2771if"x"in type_mods:2772 git_mode ="100755"2773if type_base =="symlink":2774 git_mode ="120000"2775# p4 print on a symlink sometimes contains "target\n";2776# if it does, remove the newline2777 data =''.join(contents)2778if not data:2779# Some version of p4 allowed creating a symlink that pointed2780# to nothing. This causes p4 errors when checking out such2781# a change, and errors here too. Work around it by ignoring2782# the bad symlink; hopefully a future change fixes it.2783print("\nIgnoring empty symlink in%s"%file['depotFile'])2784return2785elif data[-1] =='\n':2786 contents = [data[:-1]]2787else:2788 contents = [data]27892790if type_base =="utf16":2791# p4 delivers different text in the python output to -G2792# than it does when using "print -o", or normal p4 client2793# operations. utf16 is converted to ascii or utf8, perhaps.2794# But ascii text saved as -t utf16 is completely mangled.2795# Invoke print -o to get the real contents.2796#2797# On windows, the newlines will always be mangled by print, so put2798# them back too. This is not needed to the cygwin windows version,2799# just the native "NT" type.2800#2801try:2802 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2803exceptExceptionas e:2804if'Translation of file content failed'instr(e):2805 type_base ='binary'2806else:2807raise e2808else:2809ifp4_version_string().find('/NT') >=0:2810 text = text.replace('\r\n','\n')2811 contents = [ text ]28122813if type_base =="apple":2814# Apple filetype files will be streamed as a concatenation of2815# its appledouble header and the contents. This is useless2816# on both macs and non-macs. If using "print -q -o xx", it2817# will create "xx" with the data, and "%xx" with the header.2818# This is also not very useful.2819#2820# Ideally, someday, this script can learn how to generate2821# appledouble files directly and import those to git, but2822# non-mac machines can never find a use for apple filetype.2823print("\nIgnoring apple filetype file%s"%file['depotFile'])2824return28252826# Note that we do not try to de-mangle keywords on utf16 files,2827# even though in theory somebody may want that.2828 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2829if pattern:2830 regexp = re.compile(pattern, re.VERBOSE)2831 text =''.join(contents)2832 text = regexp.sub(r'$\1$', text)2833 contents = [ text ]28342835if self.largeFileSystem:2836(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)28372838 self.writeToGitStream(git_mode, relPath, contents)28392840defstreamOneP4Deletion(self,file):2841 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2842 relPath = self.encodeWithUTF8(relPath)2843if verbose:2844 sys.stdout.write("delete%s\n"% relPath)2845 sys.stdout.flush()2846 self.gitStream.write("D%s\n"% relPath)28472848if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2849 self.largeFileSystem.removeLargeFile(relPath)28502851# handle another chunk of streaming data2852defstreamP4FilesCb(self, marshalled):28532854# catch p4 errors and complain2855 err =None2856if"code"in marshalled:2857if marshalled["code"] =="error":2858if"data"in marshalled:2859 err = marshalled["data"].rstrip()28602861if not err and'fileSize'in self.stream_file:2862 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2863if required_bytes >0:2864 err ='Not enough space left on%s! Free at least%iMB.'% (2865 os.getcwd(), required_bytes/1024/10242866)28672868if err:2869 f =None2870if self.stream_have_file_info:2871if"depotFile"in self.stream_file:2872 f = self.stream_file["depotFile"]2873# force a failure in fast-import, else an empty2874# commit will be made2875 self.gitStream.write("\n")2876 self.gitStream.write("die-now\n")2877 self.gitStream.close()2878# ignore errors, but make sure it exits first2879 self.importProcess.wait()2880if f:2881die("Error from p4 print for%s:%s"% (f, err))2882else:2883die("Error from p4 print:%s"% err)28842885if'depotFile'in marshalled and self.stream_have_file_info:2886# start of a new file - output the old one first2887 self.streamOneP4File(self.stream_file, self.stream_contents)2888 self.stream_file = {}2889 self.stream_contents = []2890 self.stream_have_file_info =False28912892# pick up the new file information... for the2893# 'data' field we need to append to our array2894for k in marshalled.keys():2895if k =='data':2896if'streamContentSize'not in self.stream_file:2897 self.stream_file['streamContentSize'] =02898 self.stream_file['streamContentSize'] +=len(marshalled['data'])2899 self.stream_contents.append(marshalled['data'])2900else:2901 self.stream_file[k] = marshalled[k]29022903if(verbose and2904'streamContentSize'in self.stream_file and2905'fileSize'in self.stream_file and2906'depotFile'in self.stream_file):2907 size =int(self.stream_file["fileSize"])2908if size >0:2909 progress =100*self.stream_file['streamContentSize']/size2910 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2911 sys.stdout.flush()29122913 self.stream_have_file_info =True29142915# Stream directly from "p4 files" into "git fast-import"2916defstreamP4Files(self, files):2917 filesForCommit = []2918 filesToRead = []2919 filesToDelete = []29202921for f in files:2922 filesForCommit.append(f)2923if f['action']in self.delete_actions:2924 filesToDelete.append(f)2925else:2926 filesToRead.append(f)29272928# deleted files...2929for f in filesToDelete:2930 self.streamOneP4Deletion(f)29312932iflen(filesToRead) >0:2933 self.stream_file = {}2934 self.stream_contents = []2935 self.stream_have_file_info =False29362937# curry self argument2938defstreamP4FilesCbSelf(entry):2939 self.streamP4FilesCb(entry)29402941 fileArgs = []2942for f in filesToRead:2943if'shelved_cl'in f:2944# Handle shelved CLs using the "p4 print file@=N" syntax to print2945# the contents2946 fileArg ='%s@=%d'% (f['path'], f['shelved_cl'])2947else:2948 fileArg ='%s#%s'% (f['path'], f['rev'])29492950 fileArgs.append(fileArg)29512952p4CmdList(["-x","-","print"],2953 stdin=fileArgs,2954 cb=streamP4FilesCbSelf)29552956# do the last chunk2957if'depotFile'in self.stream_file:2958 self.streamOneP4File(self.stream_file, self.stream_contents)29592960defmake_email(self, userid):2961if userid in self.users:2962return self.users[userid]2963else:2964return"%s<a@b>"% userid29652966defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2967""" Stream a p4 tag.2968 commit is either a git commit, or a fast-import mark, ":<p4commit>"2969 """29702971if verbose:2972print("writing tag%sfor commit%s"% (labelName, commit))2973 gitStream.write("tag%s\n"% labelName)2974 gitStream.write("from%s\n"% commit)29752976if'Owner'in labelDetails:2977 owner = labelDetails["Owner"]2978else:2979 owner =None29802981# Try to use the owner of the p4 label, or failing that,2982# the current p4 user id.2983if owner:2984 email = self.make_email(owner)2985else:2986 email = self.make_email(self.p4UserId())2987 tagger ="%s %s %s"% (email, epoch, self.tz)29882989 gitStream.write("tagger%s\n"% tagger)29902991print("labelDetails=",labelDetails)2992if'Description'in labelDetails:2993 description = labelDetails['Description']2994else:2995 description ='Label from git p4'29962997 gitStream.write("data%d\n"%len(description))2998 gitStream.write(description)2999 gitStream.write("\n")30003001definClientSpec(self, path):3002if not self.clientSpecDirs:3003return True3004 inClientSpec = self.clientSpecDirs.map_in_client(path)3005if not inClientSpec and self.verbose:3006print('Ignoring file outside of client spec:{0}'.format(path))3007return inClientSpec30083009defhasBranchPrefix(self, path):3010if not self.branchPrefixes:3011return True3012 hasPrefix = [p for p in self.branchPrefixes3013ifp4PathStartsWith(path, p)]3014if not hasPrefix and self.verbose:3015print('Ignoring file outside of prefix:{0}'.format(path))3016return hasPrefix30173018defcommit(self, details, files, branch, parent =""):3019 epoch = details["time"]3020 author = details["user"]3021 jobs = self.extractJobsFromCommit(details)30223023if self.verbose:3024print('commit into{0}'.format(branch))30253026if self.clientSpecDirs:3027 self.clientSpecDirs.update_client_spec_path_cache(files)30283029 files = [f for f in files3030if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]30313032if not files and notgitConfigBool('git-p4.keepEmptyCommits'):3033print('Ignoring revision{0}as it would produce an empty commit.'3034.format(details['change']))3035return30363037 self.gitStream.write("commit%s\n"% branch)3038 self.gitStream.write("mark :%s\n"% details["change"])3039 self.committedChanges.add(int(details["change"]))3040 committer =""3041if author not in self.users:3042 self.getUserMapFromPerforceServer()3043 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)30443045 self.gitStream.write("committer%s\n"% committer)30463047 self.gitStream.write("data <<EOT\n")3048 self.gitStream.write(details["desc"])3049iflen(jobs) >0:3050 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))30513052if not self.suppress_meta_comment:3053 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%3054(','.join(self.branchPrefixes), details["change"]))3055iflen(details['options']) >0:3056 self.gitStream.write(": options =%s"% details['options'])3057 self.gitStream.write("]\n")30583059 self.gitStream.write("EOT\n\n")30603061iflen(parent) >0:3062if self.verbose:3063print("parent%s"% parent)3064 self.gitStream.write("from%s\n"% parent)30653066 self.streamP4Files(files)3067 self.gitStream.write("\n")30683069 change =int(details["change"])30703071if change in self.labels:3072 label = self.labels[change]3073 labelDetails = label[0]3074 labelRevisions = label[1]3075if self.verbose:3076print("Change%sis labelled%s"% (change, labelDetails))30773078 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)3079for p in self.branchPrefixes])30803081iflen(files) ==len(labelRevisions):30823083 cleanedFiles = {}3084for info in files:3085if info["action"]in self.delete_actions:3086continue3087 cleanedFiles[info["depotFile"]] = info["rev"]30883089if cleanedFiles == labelRevisions:3090 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)30913092else:3093if not self.silent:3094print("Tag%sdoes not match with change%s: files do not match."3095% (labelDetails["label"], change))30963097else:3098if not self.silent:3099print("Tag%sdoes not match with change%s: file count is different."3100% (labelDetails["label"], change))31013102# Build a dictionary of changelists and labels, for "detect-labels" option.3103defgetLabels(self):3104 self.labels = {}31053106 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])3107iflen(l) >0and not self.silent:3108print("Finding files belonging to labels in%s"% self.depotPaths)31093110for output in l:3111 label = output["label"]3112 revisions = {}3113 newestChange =03114if self.verbose:3115print("Querying files for label%s"% label)3116forfileinp4CmdList(["files"] +3117["%s...@%s"% (p, label)3118for p in self.depotPaths]):3119 revisions[file["depotFile"]] =file["rev"]3120 change =int(file["change"])3121if change > newestChange:3122 newestChange = change31233124 self.labels[newestChange] = [output, revisions]31253126if self.verbose:3127print("Label changes:%s"% self.labels.keys())31283129# Import p4 labels as git tags. A direct mapping does not3130# exist, so assume that if all the files are at the same revision3131# then we can use that, or it's something more complicated we should3132# just ignore.3133defimportP4Labels(self, stream, p4Labels):3134if verbose:3135print("import p4 labels: "+' '.join(p4Labels))31363137 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")3138 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")3139iflen(validLabelRegexp) ==0:3140 validLabelRegexp = defaultLabelRegexp3141 m = re.compile(validLabelRegexp)31423143for name in p4Labels:3144 commitFound =False31453146if not m.match(name):3147if verbose:3148print("label%sdoes not match regexp%s"% (name,validLabelRegexp))3149continue31503151if name in ignoredP4Labels:3152continue31533154 labelDetails =p4CmdList(['label',"-o", name])[0]31553156# get the most recent changelist for each file in this label3157 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)3158for p in self.depotPaths])31593160if'change'in change:3161# find the corresponding git commit; take the oldest commit3162 changelist =int(change['change'])3163if changelist in self.committedChanges:3164 gitCommit =":%d"% changelist # use a fast-import mark3165 commitFound =True3166else:3167 gitCommit =read_pipe(["git","rev-list","--max-count=1",3168"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)3169iflen(gitCommit) ==0:3170print("importing label%s: could not find git commit for changelist%d"% (name, changelist))3171else:3172 commitFound =True3173 gitCommit = gitCommit.strip()31743175if commitFound:3176# Convert from p4 time format3177try:3178 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")3179exceptValueError:3180print("Could not convert label time%s"% labelDetails['Update'])3181 tmwhen =131823183 when =int(time.mktime(tmwhen))3184 self.streamTag(stream, name, labelDetails, gitCommit, when)3185if verbose:3186print("p4 label%smapped to git commit%s"% (name, gitCommit))3187else:3188if verbose:3189print("Label%shas no changelists - possibly deleted?"% name)31903191if not commitFound:3192# We can't import this label; don't try again as it will get very3193# expensive repeatedly fetching all the files for labels that will3194# never be imported. If the label is moved in the future, the3195# ignore will need to be removed manually.3196system(["git","config","--add","git-p4.ignoredP4Labels", name])31973198defguessProjectName(self):3199for p in self.depotPaths:3200if p.endswith("/"):3201 p = p[:-1]3202 p = p[p.strip().rfind("/") +1:]3203if not p.endswith("/"):3204 p +="/"3205return p32063207defgetBranchMapping(self):3208 lostAndFoundBranches =set()32093210 user =gitConfig("git-p4.branchUser")3211iflen(user) >0:3212 command ="branches -u%s"% user3213else:3214 command ="branches"32153216for info inp4CmdList(command):3217 details =p4Cmd(["branch","-o", info["branch"]])3218 viewIdx =03219while"View%s"% viewIdx in details:3220 paths = details["View%s"% viewIdx].split(" ")3221 viewIdx = viewIdx +13222# require standard //depot/foo/... //depot/bar/... mapping3223iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3224continue3225 source = paths[0]3226 destination = paths[1]3227## HACK3228ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3229 source = source[len(self.depotPaths[0]):-4]3230 destination = destination[len(self.depotPaths[0]):-4]32313232if destination in self.knownBranches:3233if not self.silent:3234print("p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination))3235print("but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination))3236continue32373238 self.knownBranches[destination] = source32393240 lostAndFoundBranches.discard(destination)32413242if source not in self.knownBranches:3243 lostAndFoundBranches.add(source)32443245# Perforce does not strictly require branches to be defined, so we also3246# check git config for a branch list.3247#3248# Example of branch definition in git config file:3249# [git-p4]3250# branchList=main:branchA3251# branchList=main:branchB3252# branchList=branchA:branchC3253 configBranches =gitConfigList("git-p4.branchList")3254for branch in configBranches:3255if branch:3256(source, destination) = branch.split(":")3257 self.knownBranches[destination] = source32583259 lostAndFoundBranches.discard(destination)32603261if source not in self.knownBranches:3262 lostAndFoundBranches.add(source)326332643265for branch in lostAndFoundBranches:3266 self.knownBranches[branch] = branch32673268defgetBranchMappingFromGitBranches(self):3269 branches =p4BranchesInGit(self.importIntoRemotes)3270for branch in branches.keys():3271if branch =="master":3272 branch ="main"3273else:3274 branch = branch[len(self.projectName):]3275 self.knownBranches[branch] = branch32763277defupdateOptionDict(self, d):3278 option_keys = {}3279if self.keepRepoPath:3280 option_keys['keepRepoPath'] =132813282 d["options"] =' '.join(sorted(option_keys.keys()))32833284defreadOptions(self, d):3285 self.keepRepoPath = ('options'in d3286and('keepRepoPath'in d['options']))32873288defgitRefForBranch(self, branch):3289if branch =="main":3290return self.refPrefix +"master"32913292iflen(branch) <=0:3293return branch32943295return self.refPrefix + self.projectName + branch32963297defgitCommitByP4Change(self, ref, change):3298if self.verbose:3299print("looking in ref "+ ref +" for change%susing bisect..."% change)33003301 earliestCommit =""3302 latestCommit =parseRevision(ref)33033304while True:3305if self.verbose:3306print("trying: earliest%slatest%s"% (earliestCommit, latestCommit))3307 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3308iflen(next) ==0:3309if self.verbose:3310print("argh")3311return""3312 log =extractLogMessageFromGitCommit(next)3313 settings =extractSettingsGitLog(log)3314 currentChange =int(settings['change'])3315if self.verbose:3316print("current change%s"% currentChange)33173318if currentChange == change:3319if self.verbose:3320print("found%s"% next)3321return next33223323if currentChange < change:3324 earliestCommit ="^%s"% next3325else:3326 latestCommit ="%s"% next33273328return""33293330defimportNewBranch(self, branch, maxChange):3331# make fast-import flush all changes to disk and update the refs using the checkpoint3332# command so that we can try to find the branch parent in the git history3333 self.gitStream.write("checkpoint\n\n");3334 self.gitStream.flush();3335 branchPrefix = self.depotPaths[0] + branch +"/"3336range="@1,%s"% maxChange3337#print "prefix" + branchPrefix3338 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3339iflen(changes) <=0:3340return False3341 firstChange = changes[0]3342#print "first change in branch: %s" % firstChange3343 sourceBranch = self.knownBranches[branch]3344 sourceDepotPath = self.depotPaths[0] + sourceBranch3345 sourceRef = self.gitRefForBranch(sourceBranch)3346#print "source " + sourceBranch33473348 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3349#print "branch parent: %s" % branchParentChange3350 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3351iflen(gitParent) >0:3352 self.initialParents[self.gitRefForBranch(branch)] = gitParent3353#print "parent git commit: %s" % gitParent33543355 self.importChanges(changes)3356return True33573358defsearchParent(self, parent, branch, target):3359 parentFound =False3360for blob inread_pipe_lines(["git","rev-list","--reverse",3361"--no-merges", parent]):3362 blob = blob.strip()3363iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3364 parentFound =True3365if self.verbose:3366print("Found parent of%sin commit%s"% (branch, blob))3367break3368if parentFound:3369return blob3370else:3371return None33723373defimportChanges(self, changes, shelved=False, origin_revision=0):3374 cnt =13375for change in changes:3376 description =p4_describe(change, shelved)3377 self.updateOptionDict(description)33783379if not self.silent:3380 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3381 sys.stdout.flush()3382 cnt = cnt +133833384try:3385if self.detectBranches:3386 branches = self.splitFilesIntoBranches(description)3387for branch in branches.keys():3388## HACK --hwn3389 branchPrefix = self.depotPaths[0] + branch +"/"3390 self.branchPrefixes = [ branchPrefix ]33913392 parent =""33933394 filesForCommit = branches[branch]33953396if self.verbose:3397print("branch is%s"% branch)33983399 self.updatedBranches.add(branch)34003401if branch not in self.createdBranches:3402 self.createdBranches.add(branch)3403 parent = self.knownBranches[branch]3404if parent == branch:3405 parent =""3406else:3407 fullBranch = self.projectName + branch3408if fullBranch not in self.p4BranchesInGit:3409if not self.silent:3410print("\nImporting new branch%s"% fullBranch);3411if self.importNewBranch(branch, change -1):3412 parent =""3413 self.p4BranchesInGit.append(fullBranch)3414if not self.silent:3415print("\nResuming with change%s"% change);34163417if self.verbose:3418print("parent determined through known branches:%s"% parent)34193420 branch = self.gitRefForBranch(branch)3421 parent = self.gitRefForBranch(parent)34223423if self.verbose:3424print("looking for initial parent for%s; current parent is%s"% (branch, parent))34253426iflen(parent) ==0and branch in self.initialParents:3427 parent = self.initialParents[branch]3428del self.initialParents[branch]34293430 blob =None3431iflen(parent) >0:3432 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3433if self.verbose:3434print("Creating temporary branch: "+ tempBranch)3435 self.commit(description, filesForCommit, tempBranch)3436 self.tempBranches.append(tempBranch)3437 self.checkpoint()3438 blob = self.searchParent(parent, branch, tempBranch)3439if blob:3440 self.commit(description, filesForCommit, branch, blob)3441else:3442if self.verbose:3443print("Parent of%snot found. Committing into head of%s"% (branch, parent))3444 self.commit(description, filesForCommit, branch, parent)3445else:3446 files = self.extractFilesFromCommit(description, shelved, change, origin_revision)3447 self.commit(description, files, self.branch,3448 self.initialParent)3449# only needed once, to connect to the previous commit3450 self.initialParent =""3451exceptIOError:3452print(self.gitError.read())3453 sys.exit(1)34543455defsync_origin_only(self):3456if self.syncWithOrigin:3457 self.hasOrigin =originP4BranchesExist()3458if self.hasOrigin:3459if not self.silent:3460print('Syncing with origin first, using "git fetch origin"')3461system("git fetch origin")34623463defimportHeadRevision(self, revision):3464print("Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch))34653466 details = {}3467 details["user"] ="git perforce import user"3468 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3469% (' '.join(self.depotPaths), revision))3470 details["change"] = revision3471 newestRevision =034723473 fileCnt =03474 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]34753476for info inp4CmdList(["files"] + fileArgs):34773478if'code'in info and info['code'] =='error':3479 sys.stderr.write("p4 returned an error:%s\n"3480% info['data'])3481if info['data'].find("must refer to client") >=0:3482 sys.stderr.write("This particular p4 error is misleading.\n")3483 sys.stderr.write("Perhaps the depot path was misspelled.\n");3484 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3485 sys.exit(1)3486if'p4ExitCode'in info:3487 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3488 sys.exit(1)348934903491 change =int(info["change"])3492if change > newestRevision:3493 newestRevision = change34943495if info["action"]in self.delete_actions:3496# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3497#fileCnt = fileCnt + 13498continue34993500for prop in["depotFile","rev","action","type"]:3501 details["%s%s"% (prop, fileCnt)] = info[prop]35023503 fileCnt = fileCnt +135043505 details["change"] = newestRevision35063507# Use time from top-most change so that all git p4 clones of3508# the same p4 repo have the same commit SHA1s.3509 res =p4_describe(newestRevision)3510 details["time"] = res["time"]35113512 self.updateOptionDict(details)3513try:3514 self.commit(details, self.extractFilesFromCommit(details), self.branch)3515exceptIOError:3516print("IO error with git fast-import. Is your git version recent enough?")3517print(self.gitError.read())35183519defopenStreams(self):3520 self.importProcess = subprocess.Popen(["git","fast-import"],3521 stdin=subprocess.PIPE,3522 stdout=subprocess.PIPE,3523 stderr=subprocess.PIPE);3524 self.gitOutput = self.importProcess.stdout3525 self.gitStream = self.importProcess.stdin3526 self.gitError = self.importProcess.stderr35273528defcloseStreams(self):3529 self.gitStream.close()3530if self.importProcess.wait() !=0:3531die("fast-import failed:%s"% self.gitError.read())3532 self.gitOutput.close()3533 self.gitError.close()35343535defrun(self, args):3536if self.importIntoRemotes:3537 self.refPrefix ="refs/remotes/p4/"3538else:3539 self.refPrefix ="refs/heads/p4/"35403541 self.sync_origin_only()35423543 branch_arg_given =bool(self.branch)3544iflen(self.branch) ==0:3545 self.branch = self.refPrefix +"master"3546ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3547system("git update-ref%srefs/heads/p4"% self.branch)3548system("git branch -D p4")35493550# accept either the command-line option, or the configuration variable3551if self.useClientSpec:3552# will use this after clone to set the variable3553 self.useClientSpec_from_options =True3554else:3555ifgitConfigBool("git-p4.useclientspec"):3556 self.useClientSpec =True3557if self.useClientSpec:3558 self.clientSpecDirs =getClientSpec()35593560# TODO: should always look at previous commits,3561# merge with previous imports, if possible.3562if args == []:3563if self.hasOrigin:3564createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)35653566# branches holds mapping from branch name to sha13567 branches =p4BranchesInGit(self.importIntoRemotes)35683569# restrict to just this one, disabling detect-branches3570if branch_arg_given:3571 short = self.branch.split("/")[-1]3572if short in branches:3573 self.p4BranchesInGit = [ short ]3574else:3575 self.p4BranchesInGit = branches.keys()35763577iflen(self.p4BranchesInGit) >1:3578if not self.silent:3579print("Importing from/into multiple branches")3580 self.detectBranches =True3581for branch in branches.keys():3582 self.initialParents[self.refPrefix + branch] = \3583 branches[branch]35843585if self.verbose:3586print("branches:%s"% self.p4BranchesInGit)35873588 p4Change =03589for branch in self.p4BranchesInGit:3590 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)35913592 settings =extractSettingsGitLog(logMsg)35933594 self.readOptions(settings)3595if('depot-paths'in settings3596and'change'in settings):3597 change =int(settings['change']) +13598 p4Change =max(p4Change, change)35993600 depotPaths =sorted(settings['depot-paths'])3601if self.previousDepotPaths == []:3602 self.previousDepotPaths = depotPaths3603else:3604 paths = []3605for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3606 prev_list = prev.split("/")3607 cur_list = cur.split("/")3608for i inrange(0,min(len(cur_list),len(prev_list))):3609if cur_list[i] != prev_list[i]:3610 i = i -13611break36123613 paths.append("/".join(cur_list[:i +1]))36143615 self.previousDepotPaths = paths36163617if p4Change >0:3618 self.depotPaths =sorted(self.previousDepotPaths)3619 self.changeRange ="@%s,#head"% p4Change3620if not self.silent and not self.detectBranches:3621print("Performing incremental import into%sgit branch"% self.branch)36223623# accept multiple ref name abbreviations:3624# refs/foo/bar/branch -> use it exactly3625# p4/branch -> prepend refs/remotes/ or refs/heads/3626# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3627if not self.branch.startswith("refs/"):3628if self.importIntoRemotes:3629 prepend ="refs/remotes/"3630else:3631 prepend ="refs/heads/"3632if not self.branch.startswith("p4/"):3633 prepend +="p4/"3634 self.branch = prepend + self.branch36353636iflen(args) ==0and self.depotPaths:3637if not self.silent:3638print("Depot paths:%s"%' '.join(self.depotPaths))3639else:3640if self.depotPaths and self.depotPaths != args:3641print("previous import used depot path%sand now%swas specified. "3642"This doesn't work!"% (' '.join(self.depotPaths),3643' '.join(args)))3644 sys.exit(1)36453646 self.depotPaths =sorted(args)36473648 revision =""3649 self.users = {}36503651# Make sure no revision specifiers are used when --changesfile3652# is specified.3653 bad_changesfile =False3654iflen(self.changesFile) >0:3655for p in self.depotPaths:3656if p.find("@") >=0or p.find("#") >=0:3657 bad_changesfile =True3658break3659if bad_changesfile:3660die("Option --changesfile is incompatible with revision specifiers")36613662 newPaths = []3663for p in self.depotPaths:3664if p.find("@") != -1:3665 atIdx = p.index("@")3666 self.changeRange = p[atIdx:]3667if self.changeRange =="@all":3668 self.changeRange =""3669elif','not in self.changeRange:3670 revision = self.changeRange3671 self.changeRange =""3672 p = p[:atIdx]3673elif p.find("#") != -1:3674 hashIdx = p.index("#")3675 revision = p[hashIdx:]3676 p = p[:hashIdx]3677elif self.previousDepotPaths == []:3678# pay attention to changesfile, if given, else import3679# the entire p4 tree at the head revision3680iflen(self.changesFile) ==0:3681 revision ="#head"36823683 p = re.sub("\.\.\.$","", p)3684if not p.endswith("/"):3685 p +="/"36863687 newPaths.append(p)36883689 self.depotPaths = newPaths36903691# --detect-branches may change this for each branch3692 self.branchPrefixes = self.depotPaths36933694 self.loadUserMapFromCache()3695 self.labels = {}3696if self.detectLabels:3697 self.getLabels();36983699if self.detectBranches:3700## FIXME - what's a P4 projectName ?3701 self.projectName = self.guessProjectName()37023703if self.hasOrigin:3704 self.getBranchMappingFromGitBranches()3705else:3706 self.getBranchMapping()3707if self.verbose:3708print("p4-git branches:%s"% self.p4BranchesInGit)3709print("initial parents:%s"% self.initialParents)3710for b in self.p4BranchesInGit:3711if b !="master":37123713## FIXME3714 b = b[len(self.projectName):]3715 self.createdBranches.add(b)37163717 self.openStreams()37183719if revision:3720 self.importHeadRevision(revision)3721else:3722 changes = []37233724iflen(self.changesFile) >0:3725 output =open(self.changesFile).readlines()3726 changeSet =set()3727for line in output:3728 changeSet.add(int(line))37293730for change in changeSet:3731 changes.append(change)37323733 changes.sort()3734else:3735# catch "git p4 sync" with no new branches, in a repo that3736# does not have any existing p4 branches3737iflen(args) ==0:3738if not self.p4BranchesInGit:3739die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")37403741# The default branch is master, unless --branch is used to3742# specify something else. Make sure it exists, or complain3743# nicely about how to use --branch.3744if not self.detectBranches:3745if notbranch_exists(self.branch):3746if branch_arg_given:3747die("Error: branch%sdoes not exist."% self.branch)3748else:3749die("Error: no branch%s; perhaps specify one with --branch."%3750 self.branch)37513752if self.verbose:3753print("Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3754 self.changeRange))3755 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)37563757iflen(self.maxChanges) >0:3758 changes = changes[:min(int(self.maxChanges),len(changes))]37593760iflen(changes) ==0:3761if not self.silent:3762print("No changes to import!")3763else:3764if not self.silent and not self.detectBranches:3765print("Import destination:%s"% self.branch)37663767 self.updatedBranches =set()37683769if not self.detectBranches:3770if args:3771# start a new branch3772 self.initialParent =""3773else:3774# build on a previous revision3775 self.initialParent =parseRevision(self.branch)37763777 self.importChanges(changes)37783779if not self.silent:3780print("")3781iflen(self.updatedBranches) >0:3782 sys.stdout.write("Updated branches: ")3783for b in self.updatedBranches:3784 sys.stdout.write("%s"% b)3785 sys.stdout.write("\n")37863787ifgitConfigBool("git-p4.importLabels"):3788 self.importLabels =True37893790if self.importLabels:3791 p4Labels =getP4Labels(self.depotPaths)3792 gitTags =getGitTags()37933794 missingP4Labels = p4Labels - gitTags3795 self.importP4Labels(self.gitStream, missingP4Labels)37963797 self.closeStreams()37983799# Cleanup temporary branches created during import3800if self.tempBranches != []:3801for branch in self.tempBranches:3802read_pipe("git update-ref -d%s"% branch)3803 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))38043805# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3806# a convenient shortcut refname "p4".3807if self.importIntoRemotes:3808 head_ref = self.refPrefix +"HEAD"3809if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3810system(["git","symbolic-ref", head_ref, self.branch])38113812return True38133814classP4Rebase(Command):3815def__init__(self):3816 Command.__init__(self)3817 self.options = [3818 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3819]3820 self.importLabels =False3821 self.description = ("Fetches the latest revision from perforce and "3822+"rebases the current work (branch) against it")38233824defrun(self, args):3825 sync =P4Sync()3826 sync.importLabels = self.importLabels3827 sync.run([])38283829return self.rebase()38303831defrebase(self):3832if os.system("git update-index --refresh") !=0:3833die("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.");3834iflen(read_pipe("git diff-index HEAD --")) >0:3835die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");38363837[upstream, settings] =findUpstreamBranchPoint()3838iflen(upstream) ==0:3839die("Cannot find upstream branchpoint for rebase")38403841# the branchpoint may be p4/foo~3, so strip off the parent3842 upstream = re.sub("~[0-9]+$","", upstream)38433844print("Rebasing the current branch onto%s"% upstream)3845 oldHead =read_pipe("git rev-parse HEAD").strip()3846system("git rebase%s"% upstream)3847system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3848return True38493850classP4Clone(P4Sync):3851def__init__(self):3852 P4Sync.__init__(self)3853 self.description ="Creates a new git repository and imports from Perforce into it"3854 self.usage ="usage: %prog [options] //depot/path[@revRange]"3855 self.options += [3856 optparse.make_option("--destination", dest="cloneDestination",3857 action='store', default=None,3858help="where to leave result of the clone"),3859 optparse.make_option("--bare", dest="cloneBare",3860 action="store_true", default=False),3861]3862 self.cloneDestination =None3863 self.needsGit =False3864 self.cloneBare =False38653866defdefaultDestination(self, args):3867## TODO: use common prefix of args?3868 depotPath = args[0]3869 depotDir = re.sub("(@[^@]*)$","", depotPath)3870 depotDir = re.sub("(#[^#]*)$","", depotDir)3871 depotDir = re.sub(r"\.\.\.$","", depotDir)3872 depotDir = re.sub(r"/$","", depotDir)3873return os.path.split(depotDir)[1]38743875defrun(self, args):3876iflen(args) <1:3877return False38783879if self.keepRepoPath and not self.cloneDestination:3880 sys.stderr.write("Must specify destination for --keep-path\n")3881 sys.exit(1)38823883 depotPaths = args38843885if not self.cloneDestination andlen(depotPaths) >1:3886 self.cloneDestination = depotPaths[-1]3887 depotPaths = depotPaths[:-1]38883889 self.cloneExclude = ["/"+p for p in self.cloneExclude]3890for p in depotPaths:3891if not p.startswith("//"):3892 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3893return False38943895if not self.cloneDestination:3896 self.cloneDestination = self.defaultDestination(args)38973898print("Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination))38993900if not os.path.exists(self.cloneDestination):3901 os.makedirs(self.cloneDestination)3902chdir(self.cloneDestination)39033904 init_cmd = ["git","init"]3905if self.cloneBare:3906 init_cmd.append("--bare")3907 retcode = subprocess.call(init_cmd)3908if retcode:3909raiseCalledProcessError(retcode, init_cmd)39103911if not P4Sync.run(self, depotPaths):3912return False39133914# create a master branch and check out a work tree3915ifgitBranchExists(self.branch):3916system(["git","branch","master", self.branch ])3917if not self.cloneBare:3918system(["git","checkout","-f"])3919else:3920print('Not checking out any branch, use ' \3921'"git checkout -q -b master <branch>"')39223923# auto-set this variable if invoked with --use-client-spec3924if self.useClientSpec_from_options:3925system("git config --bool git-p4.useclientspec true")39263927return True39283929classP4Unshelve(Command):3930def__init__(self):3931 Command.__init__(self)3932 self.options = []3933 self.origin ="HEAD"3934 self.description ="Unshelve a P4 changelist into a git commit"3935 self.usage ="usage: %prog [options] changelist"3936 self.options += [3937 optparse.make_option("--origin", dest="origin",3938help="Use this base revision instead of the default (%s)"% self.origin),3939]3940 self.verbose =False3941 self.noCommit =False3942 self.destbranch ="refs/remotes/p4/unshelved"39433944defrenameBranch(self, branch_name):3945""" Rename the existing branch to branch_name.N3946 """39473948 found =True3949for i inrange(0,1000):3950 backup_branch_name ="{0}.{1}".format(branch_name, i)3951if notgitBranchExists(backup_branch_name):3952gitUpdateRef(backup_branch_name, branch_name)# copy ref to backup3953gitDeleteRef(branch_name)3954 found =True3955print("renamed old unshelve branch to{0}".format(backup_branch_name))3956break39573958if not found:3959 sys.exit("gave up trying to rename existing branch{0}".format(sync.branch))39603961deffindLastP4Revision(self, starting_point):3962""" Look back from starting_point for the first commit created by git-p43963 to find the P4 commit we are based on, and the depot-paths.3964 """39653966for parent in(range(65535)):3967 log =extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))3968 settings =extractSettingsGitLog(log)3969if'change'in settings:3970return settings39713972 sys.exit("could not find git-p4 commits in{0}".format(self.origin))39733974defrun(self, args):3975iflen(args) !=1:3976return False39773978if notgitBranchExists(self.origin):3979 sys.exit("origin branch{0}does not exist".format(self.origin))39803981 sync =P4Sync()3982 changes = args3983 sync.initialParent = self.origin39843985# use the first change in the list to construct the branch to unshelve into3986 change = changes[0]39873988# if the target branch already exists, rename it3989 branch_name ="{0}/{1}".format(self.destbranch, change)3990ifgitBranchExists(branch_name):3991 self.renameBranch(branch_name)3992 sync.branch = branch_name39933994 sync.verbose = self.verbose3995 sync.suppress_meta_comment =True39963997 settings = self.findLastP4Revision(self.origin)3998 origin_revision = settings['change']3999 sync.depotPaths = settings['depot-paths']4000 sync.branchPrefixes = sync.depotPaths40014002 sync.openStreams()4003 sync.loadUserMapFromCache()4004 sync.silent =True4005 sync.importChanges(changes, shelved=True, origin_revision=origin_revision)4006 sync.closeStreams()40074008print("unshelved changelist{0}into{1}".format(change, branch_name))40094010return True40114012classP4Branches(Command):4013def__init__(self):4014 Command.__init__(self)4015 self.options = [ ]4016 self.description = ("Shows the git branches that hold imports and their "4017+"corresponding perforce depot paths")4018 self.verbose =False40194020defrun(self, args):4021iforiginP4BranchesExist():4022createOrUpdateBranchesFromOrigin()40234024 cmdline ="git rev-parse --symbolic "4025 cmdline +=" --remotes"40264027for line inread_pipe_lines(cmdline):4028 line = line.strip()40294030if not line.startswith('p4/')or line =="p4/HEAD":4031continue4032 branch = line40334034 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)4035 settings =extractSettingsGitLog(log)40364037print("%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"]))4038return True40394040classHelpFormatter(optparse.IndentedHelpFormatter):4041def__init__(self):4042 optparse.IndentedHelpFormatter.__init__(self)40434044defformat_description(self, description):4045if description:4046return description +"\n"4047else:4048return""40494050defprintUsage(commands):4051print("usage:%s<command> [options]"% sys.argv[0])4052print("")4053print("valid commands:%s"%", ".join(commands))4054print("")4055print("Try%s<command> --help for command specific help."% sys.argv[0])4056print("")40574058commands = {4059"debug": P4Debug,4060"submit": P4Submit,4061"commit": P4Submit,4062"sync": P4Sync,4063"rebase": P4Rebase,4064"clone": P4Clone,4065"rollback": P4RollBack,4066"branches": P4Branches,4067"unshelve": P4Unshelve,4068}406940704071defmain():4072iflen(sys.argv[1:]) ==0:4073printUsage(commands.keys())4074 sys.exit(2)40754076 cmdName = sys.argv[1]4077try:4078 klass = commands[cmdName]4079 cmd =klass()4080exceptKeyError:4081print("unknown command%s"% cmdName)4082print("")4083printUsage(commands.keys())4084 sys.exit(2)40854086 options = cmd.options4087 cmd.gitdir = os.environ.get("GIT_DIR",None)40884089 args = sys.argv[2:]40904091 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))4092if cmd.needsGit:4093 options.append(optparse.make_option("--git-dir", dest="gitdir"))40944095 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),4096 options,4097 description = cmd.description,4098 formatter =HelpFormatter())40994100(cmd, args) = parser.parse_args(sys.argv[2:], cmd);4101global verbose4102 verbose = cmd.verbose4103if cmd.needsGit:4104if cmd.gitdir ==None:4105 cmd.gitdir = os.path.abspath(".git")4106if notisValidGitDir(cmd.gitdir):4107# "rev-parse --git-dir" without arguments will try $PWD/.git4108 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()4109if os.path.exists(cmd.gitdir):4110 cdup =read_pipe("git rev-parse --show-cdup").strip()4111iflen(cdup) >0:4112chdir(cdup);41134114if notisValidGitDir(cmd.gitdir):4115ifisValidGitDir(cmd.gitdir +"/.git"):4116 cmd.gitdir +="/.git"4117else:4118die("fatal: cannot locate git repository at%s"% cmd.gitdir)41194120# so git commands invoked from the P4 workspace will succeed4121 os.environ["GIT_DIR"] = cmd.gitdir41224123if not cmd.run(args):4124 parser.print_help()4125 sys.exit(2)412641274128if __name__ =='__main__':4129main()