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 30try: 31from subprocess import CalledProcessError 32exceptImportError: 33# from python2.7:subprocess.py 34# Exception classes used by this module. 35classCalledProcessError(Exception): 36"""This exception is raised when a process run by check_call() returns 37 a non-zero exit status. The exit status will be stored in the 38 returncode attribute.""" 39def__init__(self, returncode, cmd): 40 self.returncode = returncode 41 self.cmd = cmd 42def__str__(self): 43return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 44 45verbose =False 46 47# Only labels/tags matching this will be imported/exported 48defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 49 50# Grab changes in blocks of this many revisions, unless otherwise requested 51defaultBlockSize =512 52 53p4_access_checked =False 54 55defp4_build_cmd(cmd): 56"""Build a suitable p4 command line. 57 58 This consolidates building and returning a p4 command line into one 59 location. It means that hooking into the environment, or other configuration 60 can be done more easily. 61 """ 62 real_cmd = ["p4"] 63 64 user =gitConfig("git-p4.user") 65iflen(user) >0: 66 real_cmd += ["-u",user] 67 68 password =gitConfig("git-p4.password") 69iflen(password) >0: 70 real_cmd += ["-P", password] 71 72 port =gitConfig("git-p4.port") 73iflen(port) >0: 74 real_cmd += ["-p", port] 75 76 host =gitConfig("git-p4.host") 77iflen(host) >0: 78 real_cmd += ["-H", host] 79 80 client =gitConfig("git-p4.client") 81iflen(client) >0: 82 real_cmd += ["-c", client] 83 84 retries =gitConfigInt("git-p4.retries") 85if retries is None: 86# Perform 3 retries by default 87 retries =3 88if retries >0: 89# Provide a way to not pass this option by setting git-p4.retries to 0 90 real_cmd += ["-r",str(retries)] 91 92ifisinstance(cmd,basestring): 93 real_cmd =' '.join(real_cmd) +' '+ cmd 94else: 95 real_cmd += cmd 96 97# now check that we can actually talk to the server 98global p4_access_checked 99if not p4_access_checked: 100 p4_access_checked =True# suppress access checks in p4_check_access itself 101p4_check_access() 102 103return real_cmd 104 105defgit_dir(path): 106""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 107 This won't automatically add ".git" to a directory. 108 """ 109 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 110if not d orlen(d) ==0: 111return None 112else: 113return d 114 115defchdir(path, is_client_path=False): 116"""Do chdir to the given path, and set the PWD environment 117 variable for use by P4. It does not look at getcwd() output. 118 Since we're not using the shell, it is necessary to set the 119 PWD environment variable explicitly. 120 121 Normally, expand the path to force it to be absolute. This 122 addresses the use of relative path names inside P4 settings, 123 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 124 as given; it looks for .p4config using PWD. 125 126 If is_client_path, the path was handed to us directly by p4, 127 and may be a symbolic link. Do not call os.getcwd() in this 128 case, because it will cause p4 to think that PWD is not inside 129 the client path. 130 """ 131 132 os.chdir(path) 133if not is_client_path: 134 path = os.getcwd() 135 os.environ['PWD'] = path 136 137defcalcDiskFree(): 138"""Return free space in bytes on the disk of the given dirname.""" 139if platform.system() =='Windows': 140 free_bytes = ctypes.c_ulonglong(0) 141 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 142return free_bytes.value 143else: 144 st = os.statvfs(os.getcwd()) 145return st.f_bavail * st.f_frsize 146 147defdie(msg): 148if verbose: 149raiseException(msg) 150else: 151 sys.stderr.write(msg +"\n") 152 sys.exit(1) 153 154defwrite_pipe(c, stdin): 155if verbose: 156 sys.stderr.write('Writing pipe:%s\n'%str(c)) 157 158 expand =isinstance(c,basestring) 159 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 160 pipe = p.stdin 161 val = pipe.write(stdin) 162 pipe.close() 163if p.wait(): 164die('Command failed:%s'%str(c)) 165 166return val 167 168defp4_write_pipe(c, stdin): 169 real_cmd =p4_build_cmd(c) 170returnwrite_pipe(real_cmd, stdin) 171 172defread_pipe_full(c): 173""" Read output from command. Returns a tuple 174 of the return status, stdout text and stderr 175 text. 176 """ 177if verbose: 178 sys.stderr.write('Reading pipe:%s\n'%str(c)) 179 180 expand =isinstance(c,basestring) 181 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 182(out, err) = p.communicate() 183return(p.returncode, out, err) 184 185defread_pipe(c, ignore_error=False): 186""" Read output from command. Returns the output text on 187 success. On failure, terminates execution, unless 188 ignore_error is True, when it returns an empty string. 189 """ 190(retcode, out, err) =read_pipe_full(c) 191if retcode !=0: 192if ignore_error: 193 out ="" 194else: 195die('Command failed:%s\nError:%s'% (str(c), err)) 196return out 197 198defread_pipe_text(c): 199""" Read output from a command with trailing whitespace stripped. 200 On error, returns None. 201 """ 202(retcode, out, err) =read_pipe_full(c) 203if retcode !=0: 204return None 205else: 206return out.rstrip() 207 208defp4_read_pipe(c, ignore_error=False): 209 real_cmd =p4_build_cmd(c) 210returnread_pipe(real_cmd, ignore_error) 211 212defread_pipe_lines(c): 213if verbose: 214 sys.stderr.write('Reading pipe:%s\n'%str(c)) 215 216 expand =isinstance(c, basestring) 217 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 218 pipe = p.stdout 219 val = pipe.readlines() 220if pipe.close()or p.wait(): 221die('Command failed:%s'%str(c)) 222 223return val 224 225defp4_read_pipe_lines(c): 226"""Specifically invoke p4 on the command supplied. """ 227 real_cmd =p4_build_cmd(c) 228returnread_pipe_lines(real_cmd) 229 230defp4_has_command(cmd): 231"""Ask p4 for help on this command. If it returns an error, the 232 command does not exist in this version of p4.""" 233 real_cmd =p4_build_cmd(["help", cmd]) 234 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 235 stderr=subprocess.PIPE) 236 p.communicate() 237return p.returncode ==0 238 239defp4_has_move_command(): 240"""See if the move command exists, that it supports -k, and that 241 it has not been administratively disabled. The arguments 242 must be correct, but the filenames do not have to exist. Use 243 ones with wildcards so even if they exist, it will fail.""" 244 245if notp4_has_command("move"): 246return False 247 cmd =p4_build_cmd(["move","-k","@from","@to"]) 248 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 249(out, err) = p.communicate() 250# return code will be 1 in either case 251if err.find("Invalid option") >=0: 252return False 253if err.find("disabled") >=0: 254return False 255# assume it failed because @... was invalid changelist 256return True 257 258defsystem(cmd, ignore_error=False): 259 expand =isinstance(cmd,basestring) 260if verbose: 261 sys.stderr.write("executing%s\n"%str(cmd)) 262 retcode = subprocess.call(cmd, shell=expand) 263if retcode and not ignore_error: 264raiseCalledProcessError(retcode, cmd) 265 266return retcode 267 268defp4_system(cmd): 269"""Specifically invoke p4 as the system command. """ 270 real_cmd =p4_build_cmd(cmd) 271 expand =isinstance(real_cmd, basestring) 272 retcode = subprocess.call(real_cmd, shell=expand) 273if retcode: 274raiseCalledProcessError(retcode, real_cmd) 275 276defdie_bad_access(s): 277die("failure accessing depot:{0}".format(s.rstrip())) 278 279defp4_check_access(min_expiration=1): 280""" Check if we can access Perforce - account still logged in 281 """ 282 results =p4CmdList(["login","-s"]) 283 284iflen(results) ==0: 285# should never get here: always get either some results, or a p4ExitCode 286assert("could not parse response from perforce") 287 288 result = results[0] 289 290if'p4ExitCode'in result: 291# p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path 292die_bad_access("could not run p4") 293 294 code = result.get("code") 295if not code: 296# we get here if we couldn't connect and there was nothing to unmarshal 297die_bad_access("could not connect") 298 299elif code =="stat": 300 expiry = result.get("TicketExpiration") 301if expiry: 302 expiry =int(expiry) 303if expiry > min_expiration: 304# ok to carry on 305return 306else: 307die_bad_access("perforce ticket expires in{0}seconds".format(expiry)) 308 309else: 310# account without a timeout - all ok 311return 312 313elif code =="error": 314 data = result.get("data") 315if data: 316die_bad_access("p4 error:{0}".format(data)) 317else: 318die_bad_access("unknown error") 319else: 320die_bad_access("unknown error code{0}".format(code)) 321 322_p4_version_string =None 323defp4_version_string(): 324"""Read the version string, showing just the last line, which 325 hopefully is the interesting version bit. 326 327 $ p4 -V 328 Perforce - The Fast Software Configuration Management System. 329 Copyright 1995-2011 Perforce Software. All rights reserved. 330 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 331 """ 332global _p4_version_string 333if not _p4_version_string: 334 a =p4_read_pipe_lines(["-V"]) 335 _p4_version_string = a[-1].rstrip() 336return _p4_version_string 337 338defp4_integrate(src, dest): 339p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 340 341defp4_sync(f, *options): 342p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 343 344defp4_add(f): 345# forcibly add file names with wildcards 346ifwildcard_present(f): 347p4_system(["add","-f", f]) 348else: 349p4_system(["add", f]) 350 351defp4_delete(f): 352p4_system(["delete",wildcard_encode(f)]) 353 354defp4_edit(f, *options): 355p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 356 357defp4_revert(f): 358p4_system(["revert",wildcard_encode(f)]) 359 360defp4_reopen(type, f): 361p4_system(["reopen","-t",type,wildcard_encode(f)]) 362 363defp4_reopen_in_change(changelist, files): 364 cmd = ["reopen","-c",str(changelist)] + files 365p4_system(cmd) 366 367defp4_move(src, dest): 368p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 369 370defp4_last_change(): 371 results =p4CmdList(["changes","-m","1"], skip_info=True) 372returnint(results[0]['change']) 373 374defp4_describe(change): 375"""Make sure it returns a valid result by checking for 376 the presence of field "time". Return a dict of the 377 results.""" 378 379 ds =p4CmdList(["describe","-s",str(change)], skip_info=True) 380iflen(ds) !=1: 381die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 382 383 d = ds[0] 384 385if"p4ExitCode"in d: 386die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 387str(d))) 388if"code"in d: 389if d["code"] =="error": 390die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 391 392if"time"not in d: 393die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 394 395return d 396 397# 398# Canonicalize the p4 type and return a tuple of the 399# base type, plus any modifiers. See "p4 help filetypes" 400# for a list and explanation. 401# 402defsplit_p4_type(p4type): 403 404 p4_filetypes_historical = { 405"ctempobj":"binary+Sw", 406"ctext":"text+C", 407"cxtext":"text+Cx", 408"ktext":"text+k", 409"kxtext":"text+kx", 410"ltext":"text+F", 411"tempobj":"binary+FSw", 412"ubinary":"binary+F", 413"uresource":"resource+F", 414"uxbinary":"binary+Fx", 415"xbinary":"binary+x", 416"xltext":"text+Fx", 417"xtempobj":"binary+Swx", 418"xtext":"text+x", 419"xunicode":"unicode+x", 420"xutf16":"utf16+x", 421} 422if p4type in p4_filetypes_historical: 423 p4type = p4_filetypes_historical[p4type] 424 mods ="" 425 s = p4type.split("+") 426 base = s[0] 427 mods ="" 428iflen(s) >1: 429 mods = s[1] 430return(base, mods) 431 432# 433# return the raw p4 type of a file (text, text+ko, etc) 434# 435defp4_type(f): 436 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 437return results[0]['headType'] 438 439# 440# Given a type base and modifier, return a regexp matching 441# the keywords that can be expanded in the file 442# 443defp4_keywords_regexp_for_type(base, type_mods): 444if base in("text","unicode","binary"): 445 kwords =None 446if"ko"in type_mods: 447 kwords ='Id|Header' 448elif"k"in type_mods: 449 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 450else: 451return None 452 pattern = r""" 453 \$ # Starts with a dollar, followed by... 454 (%s) # one of the keywords, followed by... 455 (:[^$\n]+)? # possibly an old expansion, followed by... 456 \$ # another dollar 457 """% kwords 458return pattern 459else: 460return None 461 462# 463# Given a file, return a regexp matching the possible 464# RCS keywords that will be expanded, or None for files 465# with kw expansion turned off. 466# 467defp4_keywords_regexp_for_file(file): 468if not os.path.exists(file): 469return None 470else: 471(type_base, type_mods) =split_p4_type(p4_type(file)) 472returnp4_keywords_regexp_for_type(type_base, type_mods) 473 474defsetP4ExecBit(file, mode): 475# Reopens an already open file and changes the execute bit to match 476# the execute bit setting in the passed in mode. 477 478 p4Type ="+x" 479 480if notisModeExec(mode): 481 p4Type =getP4OpenedType(file) 482 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 483 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 484if p4Type[-1] =="+": 485 p4Type = p4Type[0:-1] 486 487p4_reopen(p4Type,file) 488 489defgetP4OpenedType(file): 490# Returns the perforce file type for the given file. 491 492 result =p4_read_pipe(["opened",wildcard_encode(file)]) 493 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 494if match: 495return match.group(1) 496else: 497die("Could not determine file type for%s(result: '%s')"% (file, result)) 498 499# Return the set of all p4 labels 500defgetP4Labels(depotPaths): 501 labels =set() 502ifisinstance(depotPaths,basestring): 503 depotPaths = [depotPaths] 504 505for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 506 label = l['label'] 507 labels.add(label) 508 509return labels 510 511# Return the set of all git tags 512defgetGitTags(): 513 gitTags =set() 514for line inread_pipe_lines(["git","tag"]): 515 tag = line.strip() 516 gitTags.add(tag) 517return gitTags 518 519defdiffTreePattern(): 520# This is a simple generator for the diff tree regex pattern. This could be 521# a class variable if this and parseDiffTreeEntry were a part of a class. 522 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 523while True: 524yield pattern 525 526defparseDiffTreeEntry(entry): 527"""Parses a single diff tree entry into its component elements. 528 529 See git-diff-tree(1) manpage for details about the format of the diff 530 output. This method returns a dictionary with the following elements: 531 532 src_mode - The mode of the source file 533 dst_mode - The mode of the destination file 534 src_sha1 - The sha1 for the source file 535 dst_sha1 - The sha1 fr the destination file 536 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 537 status_score - The score for the status (applicable for 'C' and 'R' 538 statuses). This is None if there is no score. 539 src - The path for the source file. 540 dst - The path for the destination file. This is only present for 541 copy or renames. If it is not present, this is None. 542 543 If the pattern is not matched, None is returned.""" 544 545 match =diffTreePattern().next().match(entry) 546if match: 547return{ 548'src_mode': match.group(1), 549'dst_mode': match.group(2), 550'src_sha1': match.group(3), 551'dst_sha1': match.group(4), 552'status': match.group(5), 553'status_score': match.group(6), 554'src': match.group(7), 555'dst': match.group(10) 556} 557return None 558 559defisModeExec(mode): 560# Returns True if the given git mode represents an executable file, 561# otherwise False. 562return mode[-3:] =="755" 563 564classP4Exception(Exception): 565""" Base class for exceptions from the p4 client """ 566def__init__(self, exit_code): 567 self.p4ExitCode = exit_code 568 569classP4ServerException(P4Exception): 570""" Base class for exceptions where we get some kind of marshalled up result from the server """ 571def__init__(self, exit_code, p4_result): 572super(P4ServerException, self).__init__(exit_code) 573 self.p4_result = p4_result 574 self.code = p4_result[0]['code'] 575 self.data = p4_result[0]['data'] 576 577classP4RequestSizeException(P4ServerException): 578""" One of the maxresults or maxscanrows errors """ 579def__init__(self, exit_code, p4_result, limit): 580super(P4RequestSizeException, self).__init__(exit_code, p4_result) 581 self.limit = limit 582 583defisModeExecChanged(src_mode, dst_mode): 584returnisModeExec(src_mode) !=isModeExec(dst_mode) 585 586defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False, 587 errors_as_exceptions=False): 588 589ifisinstance(cmd,basestring): 590 cmd ="-G "+ cmd 591 expand =True 592else: 593 cmd = ["-G"] + cmd 594 expand =False 595 596 cmd =p4_build_cmd(cmd) 597if verbose: 598 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 599 600# Use a temporary file to avoid deadlocks without 601# subprocess.communicate(), which would put another copy 602# of stdout into memory. 603 stdin_file =None 604if stdin is not None: 605 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 606ifisinstance(stdin,basestring): 607 stdin_file.write(stdin) 608else: 609for i in stdin: 610 stdin_file.write(i +'\n') 611 stdin_file.flush() 612 stdin_file.seek(0) 613 614 p4 = subprocess.Popen(cmd, 615 shell=expand, 616 stdin=stdin_file, 617 stdout=subprocess.PIPE) 618 619 result = [] 620try: 621while True: 622 entry = marshal.load(p4.stdout) 623if skip_info: 624if'code'in entry and entry['code'] =='info': 625continue 626if cb is not None: 627cb(entry) 628else: 629 result.append(entry) 630exceptEOFError: 631pass 632 exitCode = p4.wait() 633if exitCode !=0: 634if errors_as_exceptions: 635iflen(result) >0: 636 data = result[0].get('data') 637if data: 638 m = re.search('Too many rows scanned \(over (\d+)\)', data) 639if not m: 640 m = re.search('Request too large \(over (\d+)\)', data) 641 642if m: 643 limit =int(m.group(1)) 644raiseP4RequestSizeException(exitCode, result, limit) 645 646raiseP4ServerException(exitCode, result) 647else: 648raiseP4Exception(exitCode) 649else: 650 entry = {} 651 entry["p4ExitCode"] = exitCode 652 result.append(entry) 653 654return result 655 656defp4Cmd(cmd): 657list=p4CmdList(cmd) 658 result = {} 659for entry inlist: 660 result.update(entry) 661return result; 662 663defp4Where(depotPath): 664if not depotPath.endswith("/"): 665 depotPath +="/" 666 depotPathLong = depotPath +"..." 667 outputList =p4CmdList(["where", depotPathLong]) 668 output =None 669for entry in outputList: 670if"depotFile"in entry: 671# Search for the base client side depot path, as long as it starts with the branch's P4 path. 672# The base path always ends with "/...". 673if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 674 output = entry 675break 676elif"data"in entry: 677 data = entry.get("data") 678 space = data.find(" ") 679if data[:space] == depotPath: 680 output = entry 681break 682if output ==None: 683return"" 684if output["code"] =="error": 685return"" 686 clientPath ="" 687if"path"in output: 688 clientPath = output.get("path") 689elif"data"in output: 690 data = output.get("data") 691 lastSpace = data.rfind(" ") 692 clientPath = data[lastSpace +1:] 693 694if clientPath.endswith("..."): 695 clientPath = clientPath[:-3] 696return clientPath 697 698defcurrentGitBranch(): 699returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 700 701defisValidGitDir(path): 702returngit_dir(path) !=None 703 704defparseRevision(ref): 705returnread_pipe("git rev-parse%s"% ref).strip() 706 707defbranchExists(ref): 708 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 709 ignore_error=True) 710returnlen(rev) >0 711 712defextractLogMessageFromGitCommit(commit): 713 logMessage ="" 714 715## fixme: title is first line of commit, not 1st paragraph. 716 foundTitle =False 717for log inread_pipe_lines("git cat-file commit%s"% commit): 718if not foundTitle: 719iflen(log) ==1: 720 foundTitle =True 721continue 722 723 logMessage += log 724return logMessage 725 726defextractSettingsGitLog(log): 727 values = {} 728for line in log.split("\n"): 729 line = line.strip() 730 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 731if not m: 732continue 733 734 assignments = m.group(1).split(':') 735for a in assignments: 736 vals = a.split('=') 737 key = vals[0].strip() 738 val = ('='.join(vals[1:])).strip() 739if val.endswith('\"')and val.startswith('"'): 740 val = val[1:-1] 741 742 values[key] = val 743 744 paths = values.get("depot-paths") 745if not paths: 746 paths = values.get("depot-path") 747if paths: 748 values['depot-paths'] = paths.split(',') 749return values 750 751defgitBranchExists(branch): 752 proc = subprocess.Popen(["git","rev-parse", branch], 753 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 754return proc.wait() ==0; 755 756_gitConfig = {} 757 758defgitConfig(key, typeSpecifier=None): 759if not _gitConfig.has_key(key): 760 cmd = ["git","config"] 761if typeSpecifier: 762 cmd += [ typeSpecifier ] 763 cmd += [ key ] 764 s =read_pipe(cmd, ignore_error=True) 765 _gitConfig[key] = s.strip() 766return _gitConfig[key] 767 768defgitConfigBool(key): 769"""Return a bool, using git config --bool. It is True only if the 770 variable is set to true, and False if set to false or not present 771 in the config.""" 772 773if not _gitConfig.has_key(key): 774 _gitConfig[key] =gitConfig(key,'--bool') =="true" 775return _gitConfig[key] 776 777defgitConfigInt(key): 778if not _gitConfig.has_key(key): 779 cmd = ["git","config","--int", key ] 780 s =read_pipe(cmd, ignore_error=True) 781 v = s.strip() 782try: 783 _gitConfig[key] =int(gitConfig(key,'--int')) 784exceptValueError: 785 _gitConfig[key] =None 786return _gitConfig[key] 787 788defgitConfigList(key): 789if not _gitConfig.has_key(key): 790 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 791 _gitConfig[key] = s.strip().splitlines() 792if _gitConfig[key] == ['']: 793 _gitConfig[key] = [] 794return _gitConfig[key] 795 796defp4BranchesInGit(branchesAreInRemotes=True): 797"""Find all the branches whose names start with "p4/", looking 798 in remotes or heads as specified by the argument. Return 799 a dictionary of{ branch: revision }for each one found. 800 The branch names are the short names, without any 801 "p4/" prefix.""" 802 803 branches = {} 804 805 cmdline ="git rev-parse --symbolic " 806if branchesAreInRemotes: 807 cmdline +="--remotes" 808else: 809 cmdline +="--branches" 810 811for line inread_pipe_lines(cmdline): 812 line = line.strip() 813 814# only import to p4/ 815if not line.startswith('p4/'): 816continue 817# special symbolic ref to p4/master 818if line =="p4/HEAD": 819continue 820 821# strip off p4/ prefix 822 branch = line[len("p4/"):] 823 824 branches[branch] =parseRevision(line) 825 826return branches 827 828defbranch_exists(branch): 829"""Make sure that the given ref name really exists.""" 830 831 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 832 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 833 out, _ = p.communicate() 834if p.returncode: 835return False 836# expect exactly one line of output: the branch name 837return out.rstrip() == branch 838 839deffindUpstreamBranchPoint(head ="HEAD"): 840 branches =p4BranchesInGit() 841# map from depot-path to branch name 842 branchByDepotPath = {} 843for branch in branches.keys(): 844 tip = branches[branch] 845 log =extractLogMessageFromGitCommit(tip) 846 settings =extractSettingsGitLog(log) 847if settings.has_key("depot-paths"): 848 paths =",".join(settings["depot-paths"]) 849 branchByDepotPath[paths] ="remotes/p4/"+ branch 850 851 settings =None 852 parent =0 853while parent <65535: 854 commit = head +"~%s"% parent 855 log =extractLogMessageFromGitCommit(commit) 856 settings =extractSettingsGitLog(log) 857if settings.has_key("depot-paths"): 858 paths =",".join(settings["depot-paths"]) 859if branchByDepotPath.has_key(paths): 860return[branchByDepotPath[paths], settings] 861 862 parent = parent +1 863 864return["", settings] 865 866defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 867if not silent: 868print("Creating/updating branch(es) in%sbased on origin branch(es)" 869% localRefPrefix) 870 871 originPrefix ="origin/p4/" 872 873for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 874 line = line.strip() 875if(not line.startswith(originPrefix))or line.endswith("HEAD"): 876continue 877 878 headName = line[len(originPrefix):] 879 remoteHead = localRefPrefix + headName 880 originHead = line 881 882 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 883if(not original.has_key('depot-paths') 884or not original.has_key('change')): 885continue 886 887 update =False 888if notgitBranchExists(remoteHead): 889if verbose: 890print"creating%s"% remoteHead 891 update =True 892else: 893 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 894if settings.has_key('change') >0: 895if settings['depot-paths'] == original['depot-paths']: 896 originP4Change =int(original['change']) 897 p4Change =int(settings['change']) 898if originP4Change > p4Change: 899print("%s(%s) is newer than%s(%s). " 900"Updating p4 branch from origin." 901% (originHead, originP4Change, 902 remoteHead, p4Change)) 903 update =True 904else: 905print("Ignoring:%swas imported from%swhile " 906"%swas imported from%s" 907% (originHead,','.join(original['depot-paths']), 908 remoteHead,','.join(settings['depot-paths']))) 909 910if update: 911system("git update-ref%s %s"% (remoteHead, originHead)) 912 913deforiginP4BranchesExist(): 914returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 915 916 917defp4ParseNumericChangeRange(parts): 918 changeStart =int(parts[0][1:]) 919if parts[1] =='#head': 920 changeEnd =p4_last_change() 921else: 922 changeEnd =int(parts[1]) 923 924return(changeStart, changeEnd) 925 926defchooseBlockSize(blockSize): 927if blockSize: 928return blockSize 929else: 930return defaultBlockSize 931 932defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 933assert depotPaths 934 935# Parse the change range into start and end. Try to find integer 936# revision ranges as these can be broken up into blocks to avoid 937# hitting server-side limits (maxrows, maxscanresults). But if 938# that doesn't work, fall back to using the raw revision specifier 939# strings, without using block mode. 940 941if changeRange is None or changeRange =='': 942 changeStart =1 943 changeEnd =p4_last_change() 944 block_size =chooseBlockSize(requestedBlockSize) 945else: 946 parts = changeRange.split(',') 947assertlen(parts) ==2 948try: 949(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 950 block_size =chooseBlockSize(requestedBlockSize) 951exceptValueError: 952 changeStart = parts[0][1:] 953 changeEnd = parts[1] 954if requestedBlockSize: 955die("cannot use --changes-block-size with non-numeric revisions") 956 block_size =None 957 958 changes =set() 959 960# Retrieve changes a block at a time, to prevent running 961# into a MaxResults/MaxScanRows error from the server. 962 963while True: 964 cmd = ['changes'] 965 966if block_size: 967 end =min(changeEnd, changeStart + block_size) 968 revisionRange ="%d,%d"% (changeStart, end) 969else: 970 revisionRange ="%s,%s"% (changeStart, changeEnd) 971 972for p in depotPaths: 973 cmd += ["%s...@%s"% (p, revisionRange)] 974 975# Insert changes in chronological order 976for entry inreversed(p4CmdList(cmd)): 977if entry.has_key('p4ExitCode'): 978die('Error retrieving changes descriptions ({})'.format(entry['p4ExitCode'])) 979if not entry.has_key('change'): 980continue 981 changes.add(int(entry['change'])) 982 983if not block_size: 984break 985 986if end >= changeEnd: 987break 988 989 changeStart = end +1 990 991 changes =sorted(changes) 992return changes 993 994defp4PathStartsWith(path, prefix): 995# This method tries to remedy a potential mixed-case issue: 996# 997# If UserA adds //depot/DirA/file1 998# and UserB adds //depot/dira/file2 999#1000# we may or may not have a problem. If you have core.ignorecase=true,1001# we treat DirA and dira as the same directory1002ifgitConfigBool("core.ignorecase"):1003return path.lower().startswith(prefix.lower())1004return path.startswith(prefix)10051006defgetClientSpec():1007"""Look at the p4 client spec, create a View() object that contains1008 all the mappings, and return it."""10091010 specList =p4CmdList("client -o")1011iflen(specList) !=1:1012die('Output from "client -o" is%dlines, expecting 1'%1013len(specList))10141015# dictionary of all client parameters1016 entry = specList[0]10171018# the //client/ name1019 client_name = entry["Client"]10201021# just the keys that start with "View"1022 view_keys = [ k for k in entry.keys()if k.startswith("View") ]10231024# hold this new View1025 view =View(client_name)10261027# append the lines, in order, to the view1028for view_num inrange(len(view_keys)):1029 k ="View%d"% view_num1030if k not in view_keys:1031die("Expected view key%smissing"% k)1032 view.append(entry[k])10331034return view10351036defgetClientRoot():1037"""Grab the client directory."""10381039 output =p4CmdList("client -o")1040iflen(output) !=1:1041die('Output from "client -o" is%dlines, expecting 1'%len(output))10421043 entry = output[0]1044if"Root"not in entry:1045die('Client has no "Root"')10461047return entry["Root"]10481049#1050# P4 wildcards are not allowed in filenames. P4 complains1051# if you simply add them, but you can force it with "-f", in1052# which case it translates them into %xx encoding internally.1053#1054defwildcard_decode(path):1055# Search for and fix just these four characters. Do % last so1056# that fixing it does not inadvertently create new %-escapes.1057# Cannot have * in a filename in windows; untested as to1058# what p4 would do in such a case.1059if not platform.system() =="Windows":1060 path = path.replace("%2A","*")1061 path = path.replace("%23","#") \1062.replace("%40","@") \1063.replace("%25","%")1064return path10651066defwildcard_encode(path):1067# do % first to avoid double-encoding the %s introduced here1068 path = path.replace("%","%25") \1069.replace("*","%2A") \1070.replace("#","%23") \1071.replace("@","%40")1072return path10731074defwildcard_present(path):1075 m = re.search("[*#@%]", path)1076return m is not None10771078classLargeFileSystem(object):1079"""Base class for large file system support."""10801081def__init__(self, writeToGitStream):1082 self.largeFiles =set()1083 self.writeToGitStream = writeToGitStream10841085defgeneratePointer(self, cloneDestination, contentFile):1086"""Return the content of a pointer file that is stored in Git instead of1087 the actual content."""1088assert False,"Method 'generatePointer' required in "+ self.__class__.__name__10891090defpushFile(self, localLargeFile):1091"""Push the actual content which is not stored in the Git repository to1092 a server."""1093assert False,"Method 'pushFile' required in "+ self.__class__.__name__10941095defhasLargeFileExtension(self, relPath):1096returnreduce(1097lambda a, b: a or b,1098[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1099False1100)11011102defgenerateTempFile(self, contents):1103 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1104for d in contents:1105 contentFile.write(d)1106 contentFile.close()1107return contentFile.name11081109defexceedsLargeFileThreshold(self, relPath, contents):1110ifgitConfigInt('git-p4.largeFileThreshold'):1111 contentsSize =sum(len(d)for d in contents)1112if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1113return True1114ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1115 contentsSize =sum(len(d)for d in contents)1116if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1117return False1118 contentTempFile = self.generateTempFile(contents)1119 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1120 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1121 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1122 zf.close()1123 compressedContentsSize = zf.infolist()[0].compress_size1124 os.remove(contentTempFile)1125 os.remove(compressedContentFile.name)1126if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1127return True1128return False11291130defaddLargeFile(self, relPath):1131 self.largeFiles.add(relPath)11321133defremoveLargeFile(self, relPath):1134 self.largeFiles.remove(relPath)11351136defisLargeFile(self, relPath):1137return relPath in self.largeFiles11381139defprocessContent(self, git_mode, relPath, contents):1140"""Processes the content of git fast import. This method decides if a1141 file is stored in the large file system and handles all necessary1142 steps."""1143if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1144 contentTempFile = self.generateTempFile(contents)1145(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1146if pointer_git_mode:1147 git_mode = pointer_git_mode1148if localLargeFile:1149# Move temp file to final location in large file system1150 largeFileDir = os.path.dirname(localLargeFile)1151if not os.path.isdir(largeFileDir):1152 os.makedirs(largeFileDir)1153 shutil.move(contentTempFile, localLargeFile)1154 self.addLargeFile(relPath)1155ifgitConfigBool('git-p4.largeFilePush'):1156 self.pushFile(localLargeFile)1157if verbose:1158 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1159return(git_mode, contents)11601161classMockLFS(LargeFileSystem):1162"""Mock large file system for testing."""11631164defgeneratePointer(self, contentFile):1165"""The pointer content is the original content prefixed with "pointer-".1166 The local filename of the large file storage is derived from the file content.1167 """1168withopen(contentFile,'r')as f:1169 content =next(f)1170 gitMode ='100644'1171 pointerContents ='pointer-'+ content1172 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1173return(gitMode, pointerContents, localLargeFile)11741175defpushFile(self, localLargeFile):1176"""The remote filename of the large file storage is the same as the local1177 one but in a different directory.1178 """1179 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1180if not os.path.exists(remotePath):1181 os.makedirs(remotePath)1182 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))11831184classGitLFS(LargeFileSystem):1185"""Git LFS as backend for the git-p4 large file system.1186 See https://git-lfs.github.com/ for details."""11871188def__init__(self, *args):1189 LargeFileSystem.__init__(self, *args)1190 self.baseGitAttributes = []11911192defgeneratePointer(self, contentFile):1193"""Generate a Git LFS pointer for the content. Return LFS Pointer file1194 mode and content which is stored in the Git repository instead of1195 the actual content. Return also the new location of the actual1196 content.1197 """1198if os.path.getsize(contentFile) ==0:1199return(None,'',None)12001201 pointerProcess = subprocess.Popen(1202['git','lfs','pointer','--file='+ contentFile],1203 stdout=subprocess.PIPE1204)1205 pointerFile = pointerProcess.stdout.read()1206if pointerProcess.wait():1207 os.remove(contentFile)1208die('git-lfs pointer command failed. Did you install the extension?')12091210# Git LFS removed the preamble in the output of the 'pointer' command1211# starting from version 1.2.0. Check for the preamble here to support1212# earlier versions.1213# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431214if pointerFile.startswith('Git LFS pointer for'):1215 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)12161217 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1218 localLargeFile = os.path.join(1219 os.getcwd(),1220'.git','lfs','objects', oid[:2], oid[2:4],1221 oid,1222)1223# LFS Spec states that pointer files should not have the executable bit set.1224 gitMode ='100644'1225return(gitMode, pointerFile, localLargeFile)12261227defpushFile(self, localLargeFile):1228 uploadProcess = subprocess.Popen(1229['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1230)1231if uploadProcess.wait():1232die('git-lfs push command failed. Did you define a remote?')12331234defgenerateGitAttributes(self):1235return(1236 self.baseGitAttributes +1237[1238'\n',1239'#\n',1240'# Git LFS (see https://git-lfs.github.com/)\n',1241'#\n',1242] +1243['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1244for f insorted(gitConfigList('git-p4.largeFileExtensions'))1245] +1246['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1247for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1248]1249)12501251defaddLargeFile(self, relPath):1252 LargeFileSystem.addLargeFile(self, relPath)1253 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12541255defremoveLargeFile(self, relPath):1256 LargeFileSystem.removeLargeFile(self, relPath)1257 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12581259defprocessContent(self, git_mode, relPath, contents):1260if relPath =='.gitattributes':1261 self.baseGitAttributes = contents1262return(git_mode, self.generateGitAttributes())1263else:1264return LargeFileSystem.processContent(self, git_mode, relPath, contents)12651266class Command:1267def__init__(self):1268 self.usage ="usage: %prog [options]"1269 self.needsGit =True1270 self.verbose =False12711272# This is required for the "append" cloneExclude action1273defensure_value(self, attr, value):1274if nothasattr(self, attr)orgetattr(self, attr)is None:1275setattr(self, attr, value)1276returngetattr(self, attr)12771278class P4UserMap:1279def__init__(self):1280 self.userMapFromPerforceServer =False1281 self.myP4UserId =None12821283defp4UserId(self):1284if self.myP4UserId:1285return self.myP4UserId12861287 results =p4CmdList("user -o")1288for r in results:1289if r.has_key('User'):1290 self.myP4UserId = r['User']1291return r['User']1292die("Could not find your p4 user id")12931294defp4UserIsMe(self, p4User):1295# return True if the given p4 user is actually me1296 me = self.p4UserId()1297if not p4User or p4User != me:1298return False1299else:1300return True13011302defgetUserCacheFilename(self):1303 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1304return home +"/.gitp4-usercache.txt"13051306defgetUserMapFromPerforceServer(self):1307if self.userMapFromPerforceServer:1308return1309 self.users = {}1310 self.emails = {}13111312for output inp4CmdList("users"):1313if not output.has_key("User"):1314continue1315 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1316 self.emails[output["Email"]] = output["User"]13171318 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1319for mapUserConfig ingitConfigList("git-p4.mapUser"):1320 mapUser = mapUserConfigRegex.findall(mapUserConfig)1321if mapUser andlen(mapUser[0]) ==3:1322 user = mapUser[0][0]1323 fullname = mapUser[0][1]1324 email = mapUser[0][2]1325 self.users[user] = fullname +" <"+ email +">"1326 self.emails[email] = user13271328 s =''1329for(key, val)in self.users.items():1330 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))13311332open(self.getUserCacheFilename(),"wb").write(s)1333 self.userMapFromPerforceServer =True13341335defloadUserMapFromCache(self):1336 self.users = {}1337 self.userMapFromPerforceServer =False1338try:1339 cache =open(self.getUserCacheFilename(),"rb")1340 lines = cache.readlines()1341 cache.close()1342for line in lines:1343 entry = line.strip().split("\t")1344 self.users[entry[0]] = entry[1]1345exceptIOError:1346 self.getUserMapFromPerforceServer()13471348classP4Debug(Command):1349def__init__(self):1350 Command.__init__(self)1351 self.options = []1352 self.description ="A tool to debug the output of p4 -G."1353 self.needsGit =False13541355defrun(self, args):1356 j =01357for output inp4CmdList(args):1358print'Element:%d'% j1359 j +=11360print output1361return True13621363classP4RollBack(Command):1364def__init__(self):1365 Command.__init__(self)1366 self.options = [1367 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1368]1369 self.description ="A tool to debug the multi-branch import. Don't use :)"1370 self.rollbackLocalBranches =False13711372defrun(self, args):1373iflen(args) !=1:1374return False1375 maxChange =int(args[0])13761377if"p4ExitCode"inp4Cmd("changes -m 1"):1378die("Problems executing p4");13791380if self.rollbackLocalBranches:1381 refPrefix ="refs/heads/"1382 lines =read_pipe_lines("git rev-parse --symbolic --branches")1383else:1384 refPrefix ="refs/remotes/"1385 lines =read_pipe_lines("git rev-parse --symbolic --remotes")13861387for line in lines:1388if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1389 line = line.strip()1390 ref = refPrefix + line1391 log =extractLogMessageFromGitCommit(ref)1392 settings =extractSettingsGitLog(log)13931394 depotPaths = settings['depot-paths']1395 change = settings['change']13961397 changed =False13981399iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1400for p in depotPaths]))) ==0:1401print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1402system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1403continue14041405while change andint(change) > maxChange:1406 changed =True1407if self.verbose:1408print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1409system("git update-ref%s\"%s^\""% (ref, ref))1410 log =extractLogMessageFromGitCommit(ref)1411 settings =extractSettingsGitLog(log)141214131414 depotPaths = settings['depot-paths']1415 change = settings['change']14161417if changed:1418print"%srewound to%s"% (ref, change)14191420return True14211422classP4Submit(Command, P4UserMap):14231424 conflict_behavior_choices = ("ask","skip","quit")14251426def__init__(self):1427 Command.__init__(self)1428 P4UserMap.__init__(self)1429 self.options = [1430 optparse.make_option("--origin", dest="origin"),1431 optparse.make_option("-M", dest="detectRenames", action="store_true"),1432# preserve the user, requires relevant p4 permissions1433 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1434 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1435 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1436 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1437 optparse.make_option("--conflict", dest="conflict_behavior",1438 choices=self.conflict_behavior_choices),1439 optparse.make_option("--branch", dest="branch"),1440 optparse.make_option("--shelve", dest="shelve", action="store_true",1441help="Shelve instead of submit. Shelved files are reverted, "1442"restoring the workspace to the state before the shelve"),1443 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1444 metavar="CHANGELIST",1445help="update an existing shelved changelist, implies --shelve, "1446"repeat in-order for multiple shelved changelists"),1447 optparse.make_option("--commit", dest="commit", metavar="COMMIT",1448help="submit only the specified commit(s), one commit or xxx..xxx"),1449 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",1450help="Disable rebase after submit is completed. Can be useful if you "1451"work from a local git branch that is not master"),1452 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",1453help="Skip Perforce sync of p4/master after submit or shelve"),1454]1455 self.description ="Submit changes from git to the perforce depot."1456 self.usage +=" [name of git branch to submit into perforce depot]"1457 self.origin =""1458 self.detectRenames =False1459 self.preserveUser =gitConfigBool("git-p4.preserveUser")1460 self.dry_run =False1461 self.shelve =False1462 self.update_shelve =list()1463 self.commit =""1464 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1465 self.disable_p4sync =gitConfigBool("git-p4.disableP4Sync")1466 self.prepare_p4_only =False1467 self.conflict_behavior =None1468 self.isWindows = (platform.system() =="Windows")1469 self.exportLabels =False1470 self.p4HasMoveCommand =p4_has_move_command()1471 self.branch =None14721473ifgitConfig('git-p4.largeFileSystem'):1474die("Large file system not supported for git-p4 submit command. Please remove it from config.")14751476defcheck(self):1477iflen(p4CmdList("opened ...")) >0:1478die("You have files opened with perforce! Close them before starting the sync.")14791480defseparate_jobs_from_description(self, message):1481"""Extract and return a possible Jobs field in the commit1482 message. It goes into a separate section in the p4 change1483 specification.14841485 A jobs line starts with "Jobs:" and looks like a new field1486 in a form. Values are white-space separated on the same1487 line or on following lines that start with a tab.14881489 This does not parse and extract the full git commit message1490 like a p4 form. It just sees the Jobs: line as a marker1491 to pass everything from then on directly into the p4 form,1492 but outside the description section.14931494 Return a tuple (stripped log message, jobs string)."""14951496 m = re.search(r'^Jobs:', message, re.MULTILINE)1497if m is None:1498return(message,None)14991500 jobtext = message[m.start():]1501 stripped_message = message[:m.start()].rstrip()1502return(stripped_message, jobtext)15031504defprepareLogMessage(self, template, message, jobs):1505"""Edits the template returned from "p4 change -o" to insert1506 the message in the Description field, and the jobs text in1507 the Jobs field."""1508 result =""15091510 inDescriptionSection =False15111512for line in template.split("\n"):1513if line.startswith("#"):1514 result += line +"\n"1515continue15161517if inDescriptionSection:1518if line.startswith("Files:")or line.startswith("Jobs:"):1519 inDescriptionSection =False1520# insert Jobs section1521if jobs:1522 result += jobs +"\n"1523else:1524continue1525else:1526if line.startswith("Description:"):1527 inDescriptionSection =True1528 line +="\n"1529for messageLine in message.split("\n"):1530 line +="\t"+ messageLine +"\n"15311532 result += line +"\n"15331534return result15351536defpatchRCSKeywords(self,file, pattern):1537# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1538(handle, outFileName) = tempfile.mkstemp(dir='.')1539try:1540 outFile = os.fdopen(handle,"w+")1541 inFile =open(file,"r")1542 regexp = re.compile(pattern, re.VERBOSE)1543for line in inFile.readlines():1544 line = regexp.sub(r'$\1$', line)1545 outFile.write(line)1546 inFile.close()1547 outFile.close()1548# Forcibly overwrite the original file1549 os.unlink(file)1550 shutil.move(outFileName,file)1551except:1552# cleanup our temporary file1553 os.unlink(outFileName)1554print"Failed to strip RCS keywords in%s"%file1555raise15561557print"Patched up RCS keywords in%s"%file15581559defp4UserForCommit(self,id):1560# Return the tuple (perforce user,git email) for a given git commit id1561 self.getUserMapFromPerforceServer()1562 gitEmail =read_pipe(["git","log","--max-count=1",1563"--format=%ae",id])1564 gitEmail = gitEmail.strip()1565if not self.emails.has_key(gitEmail):1566return(None,gitEmail)1567else:1568return(self.emails[gitEmail],gitEmail)15691570defcheckValidP4Users(self,commits):1571# check if any git authors cannot be mapped to p4 users1572foridin commits:1573(user,email) = self.p4UserForCommit(id)1574if not user:1575 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1576ifgitConfigBool("git-p4.allowMissingP4Users"):1577print"%s"% msg1578else:1579die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)15801581deflastP4Changelist(self):1582# Get back the last changelist number submitted in this client spec. This1583# then gets used to patch up the username in the change. If the same1584# client spec is being used by multiple processes then this might go1585# wrong.1586 results =p4CmdList("client -o")# find the current client1587 client =None1588for r in results:1589if r.has_key('Client'):1590 client = r['Client']1591break1592if not client:1593die("could not get client spec")1594 results =p4CmdList(["changes","-c", client,"-m","1"])1595for r in results:1596if r.has_key('change'):1597return r['change']1598die("Could not get changelist number for last submit - cannot patch up user details")15991600defmodifyChangelistUser(self, changelist, newUser):1601# fixup the user field of a changelist after it has been submitted.1602 changes =p4CmdList("change -o%s"% changelist)1603iflen(changes) !=1:1604die("Bad output from p4 change modifying%sto user%s"%1605(changelist, newUser))16061607 c = changes[0]1608if c['User'] == newUser:return# nothing to do1609 c['User'] = newUser1610input= marshal.dumps(c)16111612 result =p4CmdList("change -f -i", stdin=input)1613for r in result:1614if r.has_key('code'):1615if r['code'] =='error':1616die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1617if r.has_key('data'):1618print("Updated user field for changelist%sto%s"% (changelist, newUser))1619return1620die("Could not modify user field of changelist%sto%s"% (changelist, newUser))16211622defcanChangeChangelists(self):1623# check to see if we have p4 admin or super-user permissions, either of1624# which are required to modify changelists.1625 results =p4CmdList(["protects", self.depotPath])1626for r in results:1627if r.has_key('perm'):1628if r['perm'] =='admin':1629return11630if r['perm'] =='super':1631return11632return016331634defprepareSubmitTemplate(self, changelist=None):1635"""Run "p4 change -o" to grab a change specification template.1636 This does not use "p4 -G", as it is nice to keep the submission1637 template in original order, since a human might edit it.16381639 Remove lines in the Files section that show changes to files1640 outside the depot path we're committing into."""16411642[upstream, settings] =findUpstreamBranchPoint()16431644 template ="""\1645# A Perforce Change Specification.1646#1647# Change: The change number. 'new' on a new changelist.1648# Date: The date this specification was last modified.1649# Client: The client on which the changelist was created. Read-only.1650# User: The user who created the changelist.1651# Status: Either 'pending' or 'submitted'. Read-only.1652# Type: Either 'public' or 'restricted'. Default is 'public'.1653# Description: Comments about the changelist. Required.1654# Jobs: What opened jobs are to be closed by this changelist.1655# You may delete jobs from this list. (New changelists only.)1656# Files: What opened files from the default changelist are to be added1657# to this changelist. You may delete files from this list.1658# (New changelists only.)1659"""1660 files_list = []1661 inFilesSection =False1662 change_entry =None1663 args = ['change','-o']1664if changelist:1665 args.append(str(changelist))1666for entry inp4CmdList(args):1667if not entry.has_key('code'):1668continue1669if entry['code'] =='stat':1670 change_entry = entry1671break1672if not change_entry:1673die('Failed to decode output of p4 change -o')1674for key, value in change_entry.iteritems():1675if key.startswith('File'):1676if settings.has_key('depot-paths'):1677if not[p for p in settings['depot-paths']1678ifp4PathStartsWith(value, p)]:1679continue1680else:1681if notp4PathStartsWith(value, self.depotPath):1682continue1683 files_list.append(value)1684continue1685# Output in the order expected by prepareLogMessage1686for key in['Change','Client','User','Status','Description','Jobs']:1687if not change_entry.has_key(key):1688continue1689 template +='\n'1690 template += key +':'1691if key =='Description':1692 template +='\n'1693for field_line in change_entry[key].splitlines():1694 template +='\t'+field_line+'\n'1695iflen(files_list) >0:1696 template +='\n'1697 template +='Files:\n'1698for path in files_list:1699 template +='\t'+path+'\n'1700return template17011702defedit_template(self, template_file):1703"""Invoke the editor to let the user change the submission1704 message. Return true if okay to continue with the submit."""17051706# if configured to skip the editing part, just submit1707ifgitConfigBool("git-p4.skipSubmitEdit"):1708return True17091710# look at the modification time, to check later if the user saved1711# the file1712 mtime = os.stat(template_file).st_mtime17131714# invoke the editor1715if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1716 editor = os.environ.get("P4EDITOR")1717else:1718 editor =read_pipe("git var GIT_EDITOR").strip()1719system(["sh","-c", ('%s"$@"'% editor), editor, template_file])17201721# If the file was not saved, prompt to see if this patch should1722# be skipped. But skip this verification step if configured so.1723ifgitConfigBool("git-p4.skipSubmitEditCheck"):1724return True17251726# modification time updated means user saved the file1727if os.stat(template_file).st_mtime > mtime:1728return True17291730while True:1731 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1732if response =='y':1733return True1734if response =='n':1735return False17361737defget_diff_description(self, editedFiles, filesToAdd, symlinks):1738# diff1739if os.environ.has_key("P4DIFF"):1740del(os.environ["P4DIFF"])1741 diff =""1742for editedFile in editedFiles:1743 diff +=p4_read_pipe(['diff','-du',1744wildcard_encode(editedFile)])17451746# new file diff1747 newdiff =""1748for newFile in filesToAdd:1749 newdiff +="==== new file ====\n"1750 newdiff +="--- /dev/null\n"1751 newdiff +="+++%s\n"% newFile17521753 is_link = os.path.islink(newFile)1754 expect_link = newFile in symlinks17551756if is_link and expect_link:1757 newdiff +="+%s\n"% os.readlink(newFile)1758else:1759 f =open(newFile,"r")1760for line in f.readlines():1761 newdiff +="+"+ line1762 f.close()17631764return(diff + newdiff).replace('\r\n','\n')17651766defapplyCommit(self,id):1767"""Apply one commit, return True if it succeeded."""17681769print"Applying",read_pipe(["git","show","-s",1770"--format=format:%h%s",id])17711772(p4User, gitEmail) = self.p4UserForCommit(id)17731774 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1775 filesToAdd =set()1776 filesToChangeType =set()1777 filesToDelete =set()1778 editedFiles =set()1779 pureRenameCopy =set()1780 symlinks =set()1781 filesToChangeExecBit = {}1782 all_files =list()17831784for line in diff:1785 diff =parseDiffTreeEntry(line)1786 modifier = diff['status']1787 path = diff['src']1788 all_files.append(path)17891790if modifier =="M":1791p4_edit(path)1792ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1793 filesToChangeExecBit[path] = diff['dst_mode']1794 editedFiles.add(path)1795elif modifier =="A":1796 filesToAdd.add(path)1797 filesToChangeExecBit[path] = diff['dst_mode']1798if path in filesToDelete:1799 filesToDelete.remove(path)18001801 dst_mode =int(diff['dst_mode'],8)1802if dst_mode ==0120000:1803 symlinks.add(path)18041805elif modifier =="D":1806 filesToDelete.add(path)1807if path in filesToAdd:1808 filesToAdd.remove(path)1809elif modifier =="C":1810 src, dest = diff['src'], diff['dst']1811p4_integrate(src, dest)1812 pureRenameCopy.add(dest)1813if diff['src_sha1'] != diff['dst_sha1']:1814p4_edit(dest)1815 pureRenameCopy.discard(dest)1816ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1817p4_edit(dest)1818 pureRenameCopy.discard(dest)1819 filesToChangeExecBit[dest] = diff['dst_mode']1820if self.isWindows:1821# turn off read-only attribute1822 os.chmod(dest, stat.S_IWRITE)1823 os.unlink(dest)1824 editedFiles.add(dest)1825elif modifier =="R":1826 src, dest = diff['src'], diff['dst']1827if self.p4HasMoveCommand:1828p4_edit(src)# src must be open before move1829p4_move(src, dest)# opens for (move/delete, move/add)1830else:1831p4_integrate(src, dest)1832if diff['src_sha1'] != diff['dst_sha1']:1833p4_edit(dest)1834else:1835 pureRenameCopy.add(dest)1836ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1837if not self.p4HasMoveCommand:1838p4_edit(dest)# with move: already open, writable1839 filesToChangeExecBit[dest] = diff['dst_mode']1840if not self.p4HasMoveCommand:1841if self.isWindows:1842 os.chmod(dest, stat.S_IWRITE)1843 os.unlink(dest)1844 filesToDelete.add(src)1845 editedFiles.add(dest)1846elif modifier =="T":1847 filesToChangeType.add(path)1848else:1849die("unknown modifier%sfor%s"% (modifier, path))18501851 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1852 patchcmd = diffcmd +" | git apply "1853 tryPatchCmd = patchcmd +"--check -"1854 applyPatchCmd = patchcmd +"--check --apply -"1855 patch_succeeded =True18561857if os.system(tryPatchCmd) !=0:1858 fixed_rcs_keywords =False1859 patch_succeeded =False1860print"Unfortunately applying the change failed!"18611862# Patch failed, maybe it's just RCS keyword woes. Look through1863# the patch to see if that's possible.1864ifgitConfigBool("git-p4.attemptRCSCleanup"):1865file=None1866 pattern =None1867 kwfiles = {}1868forfilein editedFiles | filesToDelete:1869# did this file's delta contain RCS keywords?1870 pattern =p4_keywords_regexp_for_file(file)18711872if pattern:1873# this file is a possibility...look for RCS keywords.1874 regexp = re.compile(pattern, re.VERBOSE)1875for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1876if regexp.search(line):1877if verbose:1878print"got keyword match on%sin%sin%s"% (pattern, line,file)1879 kwfiles[file] = pattern1880break18811882forfilein kwfiles:1883if verbose:1884print"zapping%swith%s"% (line,pattern)1885# File is being deleted, so not open in p4. Must1886# disable the read-only bit on windows.1887if self.isWindows andfilenot in editedFiles:1888 os.chmod(file, stat.S_IWRITE)1889 self.patchRCSKeywords(file, kwfiles[file])1890 fixed_rcs_keywords =True18911892if fixed_rcs_keywords:1893print"Retrying the patch with RCS keywords cleaned up"1894if os.system(tryPatchCmd) ==0:1895 patch_succeeded =True18961897if not patch_succeeded:1898for f in editedFiles:1899p4_revert(f)1900return False19011902#1903# Apply the patch for real, and do add/delete/+x handling.1904#1905system(applyPatchCmd)19061907for f in filesToChangeType:1908p4_edit(f,"-t","auto")1909for f in filesToAdd:1910p4_add(f)1911for f in filesToDelete:1912p4_revert(f)1913p4_delete(f)19141915# Set/clear executable bits1916for f in filesToChangeExecBit.keys():1917 mode = filesToChangeExecBit[f]1918setP4ExecBit(f, mode)19191920 update_shelve =01921iflen(self.update_shelve) >0:1922 update_shelve = self.update_shelve.pop(0)1923p4_reopen_in_change(update_shelve, all_files)19241925#1926# Build p4 change description, starting with the contents1927# of the git commit message.1928#1929 logMessage =extractLogMessageFromGitCommit(id)1930 logMessage = logMessage.strip()1931(logMessage, jobs) = self.separate_jobs_from_description(logMessage)19321933 template = self.prepareSubmitTemplate(update_shelve)1934 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)19351936if self.preserveUser:1937 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User19381939if self.checkAuthorship and not self.p4UserIsMe(p4User):1940 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1941 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1942 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"19431944 separatorLine ="######## everything below this line is just the diff #######\n"1945if not self.prepare_p4_only:1946 submitTemplate += separatorLine1947 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)19481949(handle, fileName) = tempfile.mkstemp()1950 tmpFile = os.fdopen(handle,"w+b")1951if self.isWindows:1952 submitTemplate = submitTemplate.replace("\n","\r\n")1953 tmpFile.write(submitTemplate)1954 tmpFile.close()19551956if self.prepare_p4_only:1957#1958# Leave the p4 tree prepared, and the submit template around1959# and let the user decide what to do next1960#1961print1962print"P4 workspace prepared for submission."1963print"To submit or revert, go to client workspace"1964print" "+ self.clientPath1965print1966print"To submit, use\"p4 submit\"to write a new description,"1967print"or\"p4 submit -i <%s\"to use the one prepared by" \1968"\"git p4\"."% fileName1969print"You can delete the file\"%s\"when finished."% fileName19701971if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1972print"To preserve change ownership by user%s, you must\n" \1973"do\"p4 change -f <change>\"after submitting and\n" \1974"edit the User field."1975if pureRenameCopy:1976print"After submitting, renamed files must be re-synced."1977print"Invoke\"p4 sync -f\"on each of these files:"1978for f in pureRenameCopy:1979print" "+ f19801981print1982print"To revert the changes, use\"p4 revert ...\", and delete"1983print"the submit template file\"%s\""% fileName1984if filesToAdd:1985print"Since the commit adds new files, they must be deleted:"1986for f in filesToAdd:1987print" "+ f1988print1989return True19901991#1992# Let the user edit the change description, then submit it.1993#1994 submitted =False19951996try:1997if self.edit_template(fileName):1998# read the edited message and submit1999 tmpFile =open(fileName,"rb")2000 message = tmpFile.read()2001 tmpFile.close()2002if self.isWindows:2003 message = message.replace("\r\n","\n")2004 submitTemplate = message[:message.index(separatorLine)]20052006if update_shelve:2007p4_write_pipe(['shelve','-r','-i'], submitTemplate)2008elif self.shelve:2009p4_write_pipe(['shelve','-i'], submitTemplate)2010else:2011p4_write_pipe(['submit','-i'], submitTemplate)2012# The rename/copy happened by applying a patch that created a2013# new file. This leaves it writable, which confuses p4.2014for f in pureRenameCopy:2015p4_sync(f,"-f")20162017if self.preserveUser:2018if p4User:2019# Get last changelist number. Cannot easily get it from2020# the submit command output as the output is2021# unmarshalled.2022 changelist = self.lastP4Changelist()2023 self.modifyChangelistUser(changelist, p4User)20242025 submitted =True20262027finally:2028# skip this patch2029if not submitted or self.shelve:2030if self.shelve:2031print("Reverting shelved files.")2032else:2033print("Submission cancelled, undoing p4 changes.")2034for f in editedFiles | filesToDelete:2035p4_revert(f)2036for f in filesToAdd:2037p4_revert(f)2038 os.remove(f)20392040 os.remove(fileName)2041return submitted20422043# Export git tags as p4 labels. Create a p4 label and then tag2044# with that.2045defexportGitTags(self, gitTags):2046 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")2047iflen(validLabelRegexp) ==0:2048 validLabelRegexp = defaultLabelRegexp2049 m = re.compile(validLabelRegexp)20502051for name in gitTags:20522053if not m.match(name):2054if verbose:2055print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)2056continue20572058# Get the p4 commit this corresponds to2059 logMessage =extractLogMessageFromGitCommit(name)2060 values =extractSettingsGitLog(logMessage)20612062if not values.has_key('change'):2063# a tag pointing to something not sent to p4; ignore2064if verbose:2065print"git tag%sdoes not give a p4 commit"% name2066continue2067else:2068 changelist = values['change']20692070# Get the tag details.2071 inHeader =True2072 isAnnotated =False2073 body = []2074for l inread_pipe_lines(["git","cat-file","-p", name]):2075 l = l.strip()2076if inHeader:2077if re.match(r'tag\s+', l):2078 isAnnotated =True2079elif re.match(r'\s*$', l):2080 inHeader =False2081continue2082else:2083 body.append(l)20842085if not isAnnotated:2086 body = ["lightweight tag imported by git p4\n"]20872088# Create the label - use the same view as the client spec we are using2089 clientSpec =getClientSpec()20902091 labelTemplate ="Label:%s\n"% name2092 labelTemplate +="Description:\n"2093for b in body:2094 labelTemplate +="\t"+ b +"\n"2095 labelTemplate +="View:\n"2096for depot_side in clientSpec.mappings:2097 labelTemplate +="\t%s\n"% depot_side20982099if self.dry_run:2100print"Would create p4 label%sfor tag"% name2101elif self.prepare_p4_only:2102print"Not creating p4 label%sfor tag due to option" \2103" --prepare-p4-only"% name2104else:2105p4_write_pipe(["label","-i"], labelTemplate)21062107# Use the label2108p4_system(["tag","-l", name] +2109["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])21102111if verbose:2112print"created p4 label for tag%s"% name21132114defrun(self, args):2115iflen(args) ==0:2116 self.master =currentGitBranch()2117eliflen(args) ==1:2118 self.master = args[0]2119if notbranchExists(self.master):2120die("Branch%sdoes not exist"% self.master)2121else:2122return False21232124for i in self.update_shelve:2125if i <=0:2126 sys.exit("invalid changelist%d"% i)21272128if self.master:2129 allowSubmit =gitConfig("git-p4.allowSubmit")2130iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2131die("%sis not in git-p4.allowSubmit"% self.master)21322133[upstream, settings] =findUpstreamBranchPoint()2134 self.depotPath = settings['depot-paths'][0]2135iflen(self.origin) ==0:2136 self.origin = upstream21372138iflen(self.update_shelve) >0:2139 self.shelve =True21402141if self.preserveUser:2142if not self.canChangeChangelists():2143die("Cannot preserve user names without p4 super-user or admin permissions")21442145# if not set from the command line, try the config file2146if self.conflict_behavior is None:2147 val =gitConfig("git-p4.conflict")2148if val:2149if val not in self.conflict_behavior_choices:2150die("Invalid value '%s' for config git-p4.conflict"% val)2151else:2152 val ="ask"2153 self.conflict_behavior = val21542155if self.verbose:2156print"Origin branch is "+ self.origin21572158iflen(self.depotPath) ==0:2159print"Internal error: cannot locate perforce depot path from existing branches"2160 sys.exit(128)21612162 self.useClientSpec =False2163ifgitConfigBool("git-p4.useclientspec"):2164 self.useClientSpec =True2165if self.useClientSpec:2166 self.clientSpecDirs =getClientSpec()21672168# Check for the existence of P4 branches2169 branchesDetected = (len(p4BranchesInGit().keys()) >1)21702171if self.useClientSpec and not branchesDetected:2172# all files are relative to the client spec2173 self.clientPath =getClientRoot()2174else:2175 self.clientPath =p4Where(self.depotPath)21762177if self.clientPath =="":2178die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)21792180print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2181 self.oldWorkingDirectory = os.getcwd()21822183# ensure the clientPath exists2184 new_client_dir =False2185if not os.path.exists(self.clientPath):2186 new_client_dir =True2187 os.makedirs(self.clientPath)21882189chdir(self.clientPath, is_client_path=True)2190if self.dry_run:2191print"Would synchronize p4 checkout in%s"% self.clientPath2192else:2193print"Synchronizing p4 checkout..."2194if new_client_dir:2195# old one was destroyed, and maybe nobody told p42196p4_sync("...","-f")2197else:2198p4_sync("...")2199 self.check()22002201 commits = []2202if self.master:2203 commitish = self.master2204else:2205 commitish ='HEAD'22062207if self.commit !="":2208if self.commit.find("..") != -1:2209 limits_ish = self.commit.split("..")2210for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2211 commits.append(line.strip())2212 commits.reverse()2213else:2214 commits.append(self.commit)2215else:2216for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2217 commits.append(line.strip())2218 commits.reverse()22192220if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2221 self.checkAuthorship =False2222else:2223 self.checkAuthorship =True22242225if self.preserveUser:2226 self.checkValidP4Users(commits)22272228#2229# Build up a set of options to be passed to diff when2230# submitting each commit to p4.2231#2232if self.detectRenames:2233# command-line -M arg2234 self.diffOpts ="-M"2235else:2236# If not explicitly set check the config variable2237 detectRenames =gitConfig("git-p4.detectRenames")22382239if detectRenames.lower() =="false"or detectRenames =="":2240 self.diffOpts =""2241elif detectRenames.lower() =="true":2242 self.diffOpts ="-M"2243else:2244 self.diffOpts ="-M%s"% detectRenames22452246# no command-line arg for -C or --find-copies-harder, just2247# config variables2248 detectCopies =gitConfig("git-p4.detectCopies")2249if detectCopies.lower() =="false"or detectCopies =="":2250pass2251elif detectCopies.lower() =="true":2252 self.diffOpts +=" -C"2253else:2254 self.diffOpts +=" -C%s"% detectCopies22552256ifgitConfigBool("git-p4.detectCopiesHarder"):2257 self.diffOpts +=" --find-copies-harder"22582259 num_shelves =len(self.update_shelve)2260if num_shelves >0and num_shelves !=len(commits):2261 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2262(len(commits), num_shelves))22632264#2265# Apply the commits, one at a time. On failure, ask if should2266# continue to try the rest of the patches, or quit.2267#2268if self.dry_run:2269print"Would apply"2270 applied = []2271 last =len(commits) -12272for i, commit inenumerate(commits):2273if self.dry_run:2274print" ",read_pipe(["git","show","-s",2275"--format=format:%h%s", commit])2276 ok =True2277else:2278 ok = self.applyCommit(commit)2279if ok:2280 applied.append(commit)2281else:2282if self.prepare_p4_only and i < last:2283print"Processing only the first commit due to option" \2284" --prepare-p4-only"2285break2286if i < last:2287 quit =False2288while True:2289# prompt for what to do, or use the option/variable2290if self.conflict_behavior =="ask":2291print"What do you want to do?"2292 response =raw_input("[s]kip this commit but apply"2293" the rest, or [q]uit? ")2294if not response:2295continue2296elif self.conflict_behavior =="skip":2297 response ="s"2298elif self.conflict_behavior =="quit":2299 response ="q"2300else:2301die("Unknown conflict_behavior '%s'"%2302 self.conflict_behavior)23032304if response[0] =="s":2305print"Skipping this commit, but applying the rest"2306break2307if response[0] =="q":2308print"Quitting"2309 quit =True2310break2311if quit:2312break23132314chdir(self.oldWorkingDirectory)2315 shelved_applied ="shelved"if self.shelve else"applied"2316if self.dry_run:2317pass2318elif self.prepare_p4_only:2319pass2320eliflen(commits) ==len(applied):2321print("All commits{0}!".format(shelved_applied))23222323 sync =P4Sync()2324if self.branch:2325 sync.branch = self.branch2326if self.disable_p4sync:2327 sync.sync_origin_only()2328else:2329 sync.run([])23302331if not self.disable_rebase:2332 rebase =P4Rebase()2333 rebase.rebase()23342335else:2336iflen(applied) ==0:2337print("No commits{0}.".format(shelved_applied))2338else:2339print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2340for c in commits:2341if c in applied:2342 star ="*"2343else:2344 star =" "2345print star,read_pipe(["git","show","-s",2346"--format=format:%h%s", c])2347print"You will have to do 'git p4 sync' and rebase."23482349ifgitConfigBool("git-p4.exportLabels"):2350 self.exportLabels =True23512352if self.exportLabels:2353 p4Labels =getP4Labels(self.depotPath)2354 gitTags =getGitTags()23552356 missingGitTags = gitTags - p4Labels2357 self.exportGitTags(missingGitTags)23582359# exit with error unless everything applied perfectly2360iflen(commits) !=len(applied):2361 sys.exit(1)23622363return True23642365classView(object):2366"""Represent a p4 view ("p4 help views"), and map files in a2367 repo according to the view."""23682369def__init__(self, client_name):2370 self.mappings = []2371 self.client_prefix ="//%s/"% client_name2372# cache results of "p4 where" to lookup client file locations2373 self.client_spec_path_cache = {}23742375defappend(self, view_line):2376"""Parse a view line, splitting it into depot and client2377 sides. Append to self.mappings, preserving order. This2378 is only needed for tag creation."""23792380# Split the view line into exactly two words. P4 enforces2381# structure on these lines that simplifies this quite a bit.2382#2383# Either or both words may be double-quoted.2384# Single quotes do not matter.2385# Double-quote marks cannot occur inside the words.2386# A + or - prefix is also inside the quotes.2387# There are no quotes unless they contain a space.2388# The line is already white-space stripped.2389# The two words are separated by a single space.2390#2391if view_line[0] =='"':2392# First word is double quoted. Find its end.2393 close_quote_index = view_line.find('"',1)2394if close_quote_index <=0:2395die("No first-word closing quote found:%s"% view_line)2396 depot_side = view_line[1:close_quote_index]2397# skip closing quote and space2398 rhs_index = close_quote_index +1+12399else:2400 space_index = view_line.find(" ")2401if space_index <=0:2402die("No word-splitting space found:%s"% view_line)2403 depot_side = view_line[0:space_index]2404 rhs_index = space_index +124052406# prefix + means overlay on previous mapping2407if depot_side.startswith("+"):2408 depot_side = depot_side[1:]24092410# prefix - means exclude this path, leave out of mappings2411 exclude =False2412if depot_side.startswith("-"):2413 exclude =True2414 depot_side = depot_side[1:]24152416if not exclude:2417 self.mappings.append(depot_side)24182419defconvert_client_path(self, clientFile):2420# chop off //client/ part to make it relative2421if not clientFile.startswith(self.client_prefix):2422die("No prefix '%s' on clientFile '%s'"%2423(self.client_prefix, clientFile))2424return clientFile[len(self.client_prefix):]24252426defupdate_client_spec_path_cache(self, files):2427""" Caching file paths by "p4 where" batch query """24282429# List depot file paths exclude that already cached2430 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]24312432iflen(fileArgs) ==0:2433return# All files in cache24342435 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2436for res in where_result:2437if"code"in res and res["code"] =="error":2438# assume error is "... file(s) not in client view"2439continue2440if"clientFile"not in res:2441die("No clientFile in 'p4 where' output")2442if"unmap"in res:2443# it will list all of them, but only one not unmap-ped2444continue2445ifgitConfigBool("core.ignorecase"):2446 res['depotFile'] = res['depotFile'].lower()2447 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])24482449# not found files or unmap files set to ""2450for depotFile in fileArgs:2451ifgitConfigBool("core.ignorecase"):2452 depotFile = depotFile.lower()2453if depotFile not in self.client_spec_path_cache:2454 self.client_spec_path_cache[depotFile] =""24552456defmap_in_client(self, depot_path):2457"""Return the relative location in the client where this2458 depot file should live. Returns "" if the file should2459 not be mapped in the client."""24602461ifgitConfigBool("core.ignorecase"):2462 depot_path = depot_path.lower()24632464if depot_path in self.client_spec_path_cache:2465return self.client_spec_path_cache[depot_path]24662467die("Error:%sis not found in client spec path"% depot_path )2468return""24692470classP4Sync(Command, P4UserMap):2471 delete_actions = ("delete","move/delete","purge")24722473def__init__(self):2474 Command.__init__(self)2475 P4UserMap.__init__(self)2476 self.options = [2477 optparse.make_option("--branch", dest="branch"),2478 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2479 optparse.make_option("--changesfile", dest="changesFile"),2480 optparse.make_option("--silent", dest="silent", action="store_true"),2481 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2482 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2483 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2484help="Import into refs/heads/ , not refs/remotes"),2485 optparse.make_option("--max-changes", dest="maxChanges",2486help="Maximum number of changes to import"),2487 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2488help="Internal block size to use when iteratively calling p4 changes"),2489 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2490help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2491 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2492help="Only sync files that are included in the Perforce Client Spec"),2493 optparse.make_option("-/", dest="cloneExclude",2494 action="append",type="string",2495help="exclude depot path"),2496]2497 self.description ="""Imports from Perforce into a git repository.\n2498 example:2499 //depot/my/project/ -- to import the current head2500 //depot/my/project/@all -- to import everything2501 //depot/my/project/@1,6 -- to import only from revision 1 to 625022503 (a ... is not needed in the path p4 specification, it's added implicitly)"""25042505 self.usage +=" //depot/path[@revRange]"2506 self.silent =False2507 self.createdBranches =set()2508 self.committedChanges =set()2509 self.branch =""2510 self.detectBranches =False2511 self.detectLabels =False2512 self.importLabels =False2513 self.changesFile =""2514 self.syncWithOrigin =True2515 self.importIntoRemotes =True2516 self.maxChanges =""2517 self.changes_block_size =None2518 self.keepRepoPath =False2519 self.depotPaths =None2520 self.p4BranchesInGit = []2521 self.cloneExclude = []2522 self.useClientSpec =False2523 self.useClientSpec_from_options =False2524 self.clientSpecDirs =None2525 self.tempBranches = []2526 self.tempBranchLocation ="refs/git-p4-tmp"2527 self.largeFileSystem =None25282529ifgitConfig('git-p4.largeFileSystem'):2530 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2531 self.largeFileSystem =largeFileSystemConstructor(2532lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2533)25342535ifgitConfig("git-p4.syncFromOrigin") =="false":2536 self.syncWithOrigin =False25372538# Force a checkpoint in fast-import and wait for it to finish2539defcheckpoint(self):2540 self.gitStream.write("checkpoint\n\n")2541 self.gitStream.write("progress checkpoint\n\n")2542 out = self.gitOutput.readline()2543if self.verbose:2544print"checkpoint finished: "+ out25452546defextractFilesFromCommit(self, commit):2547 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2548for path in self.cloneExclude]2549 files = []2550 fnum =02551while commit.has_key("depotFile%s"% fnum):2552 path = commit["depotFile%s"% fnum]25532554if[p for p in self.cloneExclude2555ifp4PathStartsWith(path, p)]:2556 found =False2557else:2558 found = [p for p in self.depotPaths2559ifp4PathStartsWith(path, p)]2560if not found:2561 fnum = fnum +12562continue25632564file= {}2565file["path"] = path2566file["rev"] = commit["rev%s"% fnum]2567file["action"] = commit["action%s"% fnum]2568file["type"] = commit["type%s"% fnum]2569 files.append(file)2570 fnum = fnum +12571return files25722573defextractJobsFromCommit(self, commit):2574 jobs = []2575 jnum =02576while commit.has_key("job%s"% jnum):2577 job = commit["job%s"% jnum]2578 jobs.append(job)2579 jnum = jnum +12580return jobs25812582defstripRepoPath(self, path, prefixes):2583"""When streaming files, this is called to map a p4 depot path2584 to where it should go in git. The prefixes are either2585 self.depotPaths, or self.branchPrefixes in the case of2586 branch detection."""25872588if self.useClientSpec:2589# branch detection moves files up a level (the branch name)2590# from what client spec interpretation gives2591 path = self.clientSpecDirs.map_in_client(path)2592if self.detectBranches:2593for b in self.knownBranches:2594if path.startswith(b +"/"):2595 path = path[len(b)+1:]25962597elif self.keepRepoPath:2598# Preserve everything in relative path name except leading2599# //depot/; just look at first prefix as they all should2600# be in the same depot.2601 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2602ifp4PathStartsWith(path, depot):2603 path = path[len(depot):]26042605else:2606for p in prefixes:2607ifp4PathStartsWith(path, p):2608 path = path[len(p):]2609break26102611 path =wildcard_decode(path)2612return path26132614defsplitFilesIntoBranches(self, commit):2615"""Look at each depotFile in the commit to figure out to what2616 branch it belongs."""26172618if self.clientSpecDirs:2619 files = self.extractFilesFromCommit(commit)2620 self.clientSpecDirs.update_client_spec_path_cache(files)26212622 branches = {}2623 fnum =02624while commit.has_key("depotFile%s"% fnum):2625 path = commit["depotFile%s"% fnum]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]2637 fnum = fnum +126382639# start with the full relative path where this file would2640# go in a p4 client2641if self.useClientSpec:2642 relPath = self.clientSpecDirs.map_in_client(path)2643else:2644 relPath = self.stripRepoPath(path, self.depotPaths)26452646for branch in self.knownBranches.keys():2647# add a trailing slash so that a commit into qt/4.2foo2648# doesn't end up in qt/4.2, e.g.2649if relPath.startswith(branch +"/"):2650if branch not in branches:2651 branches[branch] = []2652 branches[branch].append(file)2653break26542655return branches26562657defwriteToGitStream(self, gitMode, relPath, contents):2658 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2659 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2660for d in contents:2661 self.gitStream.write(d)2662 self.gitStream.write('\n')26632664defencodeWithUTF8(self, path):2665try:2666 path.decode('ascii')2667except:2668 encoding ='utf8'2669ifgitConfig('git-p4.pathEncoding'):2670 encoding =gitConfig('git-p4.pathEncoding')2671 path = path.decode(encoding,'replace').encode('utf8','replace')2672if self.verbose:2673print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2674return path26752676# output one file from the P4 stream2677# - helper for streamP4Files26782679defstreamOneP4File(self,file, contents):2680 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2681 relPath = self.encodeWithUTF8(relPath)2682if verbose:2683 size =int(self.stream_file['fileSize'])2684 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2685 sys.stdout.flush()26862687(type_base, type_mods) =split_p4_type(file["type"])26882689 git_mode ="100644"2690if"x"in type_mods:2691 git_mode ="100755"2692if type_base =="symlink":2693 git_mode ="120000"2694# p4 print on a symlink sometimes contains "target\n";2695# if it does, remove the newline2696 data =''.join(contents)2697if not data:2698# Some version of p4 allowed creating a symlink that pointed2699# to nothing. This causes p4 errors when checking out such2700# a change, and errors here too. Work around it by ignoring2701# the bad symlink; hopefully a future change fixes it.2702print"\nIgnoring empty symlink in%s"%file['depotFile']2703return2704elif data[-1] =='\n':2705 contents = [data[:-1]]2706else:2707 contents = [data]27082709if type_base =="utf16":2710# p4 delivers different text in the python output to -G2711# than it does when using "print -o", or normal p4 client2712# operations. utf16 is converted to ascii or utf8, perhaps.2713# But ascii text saved as -t utf16 is completely mangled.2714# Invoke print -o to get the real contents.2715#2716# On windows, the newlines will always be mangled by print, so put2717# them back too. This is not needed to the cygwin windows version,2718# just the native "NT" type.2719#2720try:2721 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2722exceptExceptionas e:2723if'Translation of file content failed'instr(e):2724 type_base ='binary'2725else:2726raise e2727else:2728ifp4_version_string().find('/NT') >=0:2729 text = text.replace('\r\n','\n')2730 contents = [ text ]27312732if type_base =="apple":2733# Apple filetype files will be streamed as a concatenation of2734# its appledouble header and the contents. This is useless2735# on both macs and non-macs. If using "print -q -o xx", it2736# will create "xx" with the data, and "%xx" with the header.2737# This is also not very useful.2738#2739# Ideally, someday, this script can learn how to generate2740# appledouble files directly and import those to git, but2741# non-mac machines can never find a use for apple filetype.2742print"\nIgnoring apple filetype file%s"%file['depotFile']2743return27442745# Note that we do not try to de-mangle keywords on utf16 files,2746# even though in theory somebody may want that.2747 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2748if pattern:2749 regexp = re.compile(pattern, re.VERBOSE)2750 text =''.join(contents)2751 text = regexp.sub(r'$\1$', text)2752 contents = [ text ]27532754if self.largeFileSystem:2755(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)27562757 self.writeToGitStream(git_mode, relPath, contents)27582759defstreamOneP4Deletion(self,file):2760 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2761 relPath = self.encodeWithUTF8(relPath)2762if verbose:2763 sys.stdout.write("delete%s\n"% relPath)2764 sys.stdout.flush()2765 self.gitStream.write("D%s\n"% relPath)27662767if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2768 self.largeFileSystem.removeLargeFile(relPath)27692770# handle another chunk of streaming data2771defstreamP4FilesCb(self, marshalled):27722773# catch p4 errors and complain2774 err =None2775if"code"in marshalled:2776if marshalled["code"] =="error":2777if"data"in marshalled:2778 err = marshalled["data"].rstrip()27792780if not err and'fileSize'in self.stream_file:2781 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2782if required_bytes >0:2783 err ='Not enough space left on%s! Free at least%iMB.'% (2784 os.getcwd(), required_bytes/1024/10242785)27862787if err:2788 f =None2789if self.stream_have_file_info:2790if"depotFile"in self.stream_file:2791 f = self.stream_file["depotFile"]2792# force a failure in fast-import, else an empty2793# commit will be made2794 self.gitStream.write("\n")2795 self.gitStream.write("die-now\n")2796 self.gitStream.close()2797# ignore errors, but make sure it exits first2798 self.importProcess.wait()2799if f:2800die("Error from p4 print for%s:%s"% (f, err))2801else:2802die("Error from p4 print:%s"% err)28032804if marshalled.has_key('depotFile')and self.stream_have_file_info:2805# start of a new file - output the old one first2806 self.streamOneP4File(self.stream_file, self.stream_contents)2807 self.stream_file = {}2808 self.stream_contents = []2809 self.stream_have_file_info =False28102811# pick up the new file information... for the2812# 'data' field we need to append to our array2813for k in marshalled.keys():2814if k =='data':2815if'streamContentSize'not in self.stream_file:2816 self.stream_file['streamContentSize'] =02817 self.stream_file['streamContentSize'] +=len(marshalled['data'])2818 self.stream_contents.append(marshalled['data'])2819else:2820 self.stream_file[k] = marshalled[k]28212822if(verbose and2823'streamContentSize'in self.stream_file and2824'fileSize'in self.stream_file and2825'depotFile'in self.stream_file):2826 size =int(self.stream_file["fileSize"])2827if size >0:2828 progress =100*self.stream_file['streamContentSize']/size2829 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2830 sys.stdout.flush()28312832 self.stream_have_file_info =True28332834# Stream directly from "p4 files" into "git fast-import"2835defstreamP4Files(self, files):2836 filesForCommit = []2837 filesToRead = []2838 filesToDelete = []28392840for f in files:2841 filesForCommit.append(f)2842if f['action']in self.delete_actions:2843 filesToDelete.append(f)2844else:2845 filesToRead.append(f)28462847# deleted files...2848for f in filesToDelete:2849 self.streamOneP4Deletion(f)28502851iflen(filesToRead) >0:2852 self.stream_file = {}2853 self.stream_contents = []2854 self.stream_have_file_info =False28552856# curry self argument2857defstreamP4FilesCbSelf(entry):2858 self.streamP4FilesCb(entry)28592860 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]28612862p4CmdList(["-x","-","print"],2863 stdin=fileArgs,2864 cb=streamP4FilesCbSelf)28652866# do the last chunk2867if self.stream_file.has_key('depotFile'):2868 self.streamOneP4File(self.stream_file, self.stream_contents)28692870defmake_email(self, userid):2871if userid in self.users:2872return self.users[userid]2873else:2874return"%s<a@b>"% userid28752876defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2877""" Stream a p4 tag.2878 commit is either a git commit, or a fast-import mark, ":<p4commit>"2879 """28802881if verbose:2882print"writing tag%sfor commit%s"% (labelName, commit)2883 gitStream.write("tag%s\n"% labelName)2884 gitStream.write("from%s\n"% commit)28852886if labelDetails.has_key('Owner'):2887 owner = labelDetails["Owner"]2888else:2889 owner =None28902891# Try to use the owner of the p4 label, or failing that,2892# the current p4 user id.2893if owner:2894 email = self.make_email(owner)2895else:2896 email = self.make_email(self.p4UserId())2897 tagger ="%s %s %s"% (email, epoch, self.tz)28982899 gitStream.write("tagger%s\n"% tagger)29002901print"labelDetails=",labelDetails2902if labelDetails.has_key('Description'):2903 description = labelDetails['Description']2904else:2905 description ='Label from git p4'29062907 gitStream.write("data%d\n"%len(description))2908 gitStream.write(description)2909 gitStream.write("\n")29102911definClientSpec(self, path):2912if not self.clientSpecDirs:2913return True2914 inClientSpec = self.clientSpecDirs.map_in_client(path)2915if not inClientSpec and self.verbose:2916print('Ignoring file outside of client spec:{0}'.format(path))2917return inClientSpec29182919defhasBranchPrefix(self, path):2920if not self.branchPrefixes:2921return True2922 hasPrefix = [p for p in self.branchPrefixes2923ifp4PathStartsWith(path, p)]2924if not hasPrefix and self.verbose:2925print('Ignoring file outside of prefix:{0}'.format(path))2926return hasPrefix29272928defcommit(self, details, files, branch, parent =""):2929 epoch = details["time"]2930 author = details["user"]2931 jobs = self.extractJobsFromCommit(details)29322933if self.verbose:2934print('commit into{0}'.format(branch))29352936if self.clientSpecDirs:2937 self.clientSpecDirs.update_client_spec_path_cache(files)29382939 files = [f for f in files2940if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]29412942if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2943print('Ignoring revision{0}as it would produce an empty commit.'2944.format(details['change']))2945return29462947 self.gitStream.write("commit%s\n"% branch)2948 self.gitStream.write("mark :%s\n"% details["change"])2949 self.committedChanges.add(int(details["change"]))2950 committer =""2951if author not in self.users:2952 self.getUserMapFromPerforceServer()2953 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)29542955 self.gitStream.write("committer%s\n"% committer)29562957 self.gitStream.write("data <<EOT\n")2958 self.gitStream.write(details["desc"])2959iflen(jobs) >0:2960 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2961 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2962(','.join(self.branchPrefixes), details["change"]))2963iflen(details['options']) >0:2964 self.gitStream.write(": options =%s"% details['options'])2965 self.gitStream.write("]\nEOT\n\n")29662967iflen(parent) >0:2968if self.verbose:2969print"parent%s"% parent2970 self.gitStream.write("from%s\n"% parent)29712972 self.streamP4Files(files)2973 self.gitStream.write("\n")29742975 change =int(details["change"])29762977if self.labels.has_key(change):2978 label = self.labels[change]2979 labelDetails = label[0]2980 labelRevisions = label[1]2981if self.verbose:2982print"Change%sis labelled%s"% (change, labelDetails)29832984 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2985for p in self.branchPrefixes])29862987iflen(files) ==len(labelRevisions):29882989 cleanedFiles = {}2990for info in files:2991if info["action"]in self.delete_actions:2992continue2993 cleanedFiles[info["depotFile"]] = info["rev"]29942995if cleanedFiles == labelRevisions:2996 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)29972998else:2999if not self.silent:3000print("Tag%sdoes not match with change%s: files do not match."3001% (labelDetails["label"], change))30023003else:3004if not self.silent:3005print("Tag%sdoes not match with change%s: file count is different."3006% (labelDetails["label"], change))30073008# Build a dictionary of changelists and labels, for "detect-labels" option.3009defgetLabels(self):3010 self.labels = {}30113012 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])3013iflen(l) >0and not self.silent:3014print"Finding files belonging to labels in%s"% `self.depotPaths`30153016for output in l:3017 label = output["label"]3018 revisions = {}3019 newestChange =03020if self.verbose:3021print"Querying files for label%s"% label3022forfileinp4CmdList(["files"] +3023["%s...@%s"% (p, label)3024for p in self.depotPaths]):3025 revisions[file["depotFile"]] =file["rev"]3026 change =int(file["change"])3027if change > newestChange:3028 newestChange = change30293030 self.labels[newestChange] = [output, revisions]30313032if self.verbose:3033print"Label changes:%s"% self.labels.keys()30343035# Import p4 labels as git tags. A direct mapping does not3036# exist, so assume that if all the files are at the same revision3037# then we can use that, or it's something more complicated we should3038# just ignore.3039defimportP4Labels(self, stream, p4Labels):3040if verbose:3041print"import p4 labels: "+' '.join(p4Labels)30423043 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")3044 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")3045iflen(validLabelRegexp) ==0:3046 validLabelRegexp = defaultLabelRegexp3047 m = re.compile(validLabelRegexp)30483049for name in p4Labels:3050 commitFound =False30513052if not m.match(name):3053if verbose:3054print"label%sdoes not match regexp%s"% (name,validLabelRegexp)3055continue30563057if name in ignoredP4Labels:3058continue30593060 labelDetails =p4CmdList(['label',"-o", name])[0]30613062# get the most recent changelist for each file in this label3063 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)3064for p in self.depotPaths])30653066if change.has_key('change'):3067# find the corresponding git commit; take the oldest commit3068 changelist =int(change['change'])3069if changelist in self.committedChanges:3070 gitCommit =":%d"% changelist # use a fast-import mark3071 commitFound =True3072else:3073 gitCommit =read_pipe(["git","rev-list","--max-count=1",3074"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)3075iflen(gitCommit) ==0:3076print"importing label%s: could not find git commit for changelist%d"% (name, changelist)3077else:3078 commitFound =True3079 gitCommit = gitCommit.strip()30803081if commitFound:3082# Convert from p4 time format3083try:3084 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")3085exceptValueError:3086print"Could not convert label time%s"% labelDetails['Update']3087 tmwhen =130883089 when =int(time.mktime(tmwhen))3090 self.streamTag(stream, name, labelDetails, gitCommit, when)3091if verbose:3092print"p4 label%smapped to git commit%s"% (name, gitCommit)3093else:3094if verbose:3095print"Label%shas no changelists - possibly deleted?"% name30963097if not commitFound:3098# We can't import this label; don't try again as it will get very3099# expensive repeatedly fetching all the files for labels that will3100# never be imported. If the label is moved in the future, the3101# ignore will need to be removed manually.3102system(["git","config","--add","git-p4.ignoredP4Labels", name])31033104defguessProjectName(self):3105for p in self.depotPaths:3106if p.endswith("/"):3107 p = p[:-1]3108 p = p[p.strip().rfind("/") +1:]3109if not p.endswith("/"):3110 p +="/"3111return p31123113defgetBranchMapping(self):3114 lostAndFoundBranches =set()31153116 user =gitConfig("git-p4.branchUser")3117iflen(user) >0:3118 command ="branches -u%s"% user3119else:3120 command ="branches"31213122for info inp4CmdList(command):3123 details =p4Cmd(["branch","-o", info["branch"]])3124 viewIdx =03125while details.has_key("View%s"% viewIdx):3126 paths = details["View%s"% viewIdx].split(" ")3127 viewIdx = viewIdx +13128# require standard //depot/foo/... //depot/bar/... mapping3129iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3130continue3131 source = paths[0]3132 destination = paths[1]3133## HACK3134ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3135 source = source[len(self.depotPaths[0]):-4]3136 destination = destination[len(self.depotPaths[0]):-4]31373138if destination in self.knownBranches:3139if not self.silent:3140print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)3141print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)3142continue31433144 self.knownBranches[destination] = source31453146 lostAndFoundBranches.discard(destination)31473148if source not in self.knownBranches:3149 lostAndFoundBranches.add(source)31503151# Perforce does not strictly require branches to be defined, so we also3152# check git config for a branch list.3153#3154# Example of branch definition in git config file:3155# [git-p4]3156# branchList=main:branchA3157# branchList=main:branchB3158# branchList=branchA:branchC3159 configBranches =gitConfigList("git-p4.branchList")3160for branch in configBranches:3161if branch:3162(source, destination) = branch.split(":")3163 self.knownBranches[destination] = source31643165 lostAndFoundBranches.discard(destination)31663167if source not in self.knownBranches:3168 lostAndFoundBranches.add(source)316931703171for branch in lostAndFoundBranches:3172 self.knownBranches[branch] = branch31733174defgetBranchMappingFromGitBranches(self):3175 branches =p4BranchesInGit(self.importIntoRemotes)3176for branch in branches.keys():3177if branch =="master":3178 branch ="main"3179else:3180 branch = branch[len(self.projectName):]3181 self.knownBranches[branch] = branch31823183defupdateOptionDict(self, d):3184 option_keys = {}3185if self.keepRepoPath:3186 option_keys['keepRepoPath'] =131873188 d["options"] =' '.join(sorted(option_keys.keys()))31893190defreadOptions(self, d):3191 self.keepRepoPath = (d.has_key('options')3192and('keepRepoPath'in d['options']))31933194defgitRefForBranch(self, branch):3195if branch =="main":3196return self.refPrefix +"master"31973198iflen(branch) <=0:3199return branch32003201return self.refPrefix + self.projectName + branch32023203defgitCommitByP4Change(self, ref, change):3204if self.verbose:3205print"looking in ref "+ ref +" for change%susing bisect..."% change32063207 earliestCommit =""3208 latestCommit =parseRevision(ref)32093210while True:3211if self.verbose:3212print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3213 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3214iflen(next) ==0:3215if self.verbose:3216print"argh"3217return""3218 log =extractLogMessageFromGitCommit(next)3219 settings =extractSettingsGitLog(log)3220 currentChange =int(settings['change'])3221if self.verbose:3222print"current change%s"% currentChange32233224if currentChange == change:3225if self.verbose:3226print"found%s"% next3227return next32283229if currentChange < change:3230 earliestCommit ="^%s"% next3231else:3232 latestCommit ="%s"% next32333234return""32353236defimportNewBranch(self, branch, maxChange):3237# make fast-import flush all changes to disk and update the refs using the checkpoint3238# command so that we can try to find the branch parent in the git history3239 self.gitStream.write("checkpoint\n\n");3240 self.gitStream.flush();3241 branchPrefix = self.depotPaths[0] + branch +"/"3242range="@1,%s"% maxChange3243#print "prefix" + branchPrefix3244 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3245iflen(changes) <=0:3246return False3247 firstChange = changes[0]3248#print "first change in branch: %s" % firstChange3249 sourceBranch = self.knownBranches[branch]3250 sourceDepotPath = self.depotPaths[0] + sourceBranch3251 sourceRef = self.gitRefForBranch(sourceBranch)3252#print "source " + sourceBranch32533254 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3255#print "branch parent: %s" % branchParentChange3256 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3257iflen(gitParent) >0:3258 self.initialParents[self.gitRefForBranch(branch)] = gitParent3259#print "parent git commit: %s" % gitParent32603261 self.importChanges(changes)3262return True32633264defsearchParent(self, parent, branch, target):3265 parentFound =False3266for blob inread_pipe_lines(["git","rev-list","--reverse",3267"--no-merges", parent]):3268 blob = blob.strip()3269iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3270 parentFound =True3271if self.verbose:3272print"Found parent of%sin commit%s"% (branch, blob)3273break3274if parentFound:3275return blob3276else:3277return None32783279defimportChanges(self, changes):3280 cnt =13281for change in changes:3282 description =p4_describe(change)3283 self.updateOptionDict(description)32843285if not self.silent:3286 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3287 sys.stdout.flush()3288 cnt = cnt +132893290try:3291if self.detectBranches:3292 branches = self.splitFilesIntoBranches(description)3293for branch in branches.keys():3294## HACK --hwn3295 branchPrefix = self.depotPaths[0] + branch +"/"3296 self.branchPrefixes = [ branchPrefix ]32973298 parent =""32993300 filesForCommit = branches[branch]33013302if self.verbose:3303print"branch is%s"% branch33043305 self.updatedBranches.add(branch)33063307if branch not in self.createdBranches:3308 self.createdBranches.add(branch)3309 parent = self.knownBranches[branch]3310if parent == branch:3311 parent =""3312else:3313 fullBranch = self.projectName + branch3314if fullBranch not in self.p4BranchesInGit:3315if not self.silent:3316print("\nImporting new branch%s"% fullBranch);3317if self.importNewBranch(branch, change -1):3318 parent =""3319 self.p4BranchesInGit.append(fullBranch)3320if not self.silent:3321print("\nResuming with change%s"% change);33223323if self.verbose:3324print"parent determined through known branches:%s"% parent33253326 branch = self.gitRefForBranch(branch)3327 parent = self.gitRefForBranch(parent)33283329if self.verbose:3330print"looking for initial parent for%s; current parent is%s"% (branch, parent)33313332iflen(parent) ==0and branch in self.initialParents:3333 parent = self.initialParents[branch]3334del self.initialParents[branch]33353336 blob =None3337iflen(parent) >0:3338 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3339if self.verbose:3340print"Creating temporary branch: "+ tempBranch3341 self.commit(description, filesForCommit, tempBranch)3342 self.tempBranches.append(tempBranch)3343 self.checkpoint()3344 blob = self.searchParent(parent, branch, tempBranch)3345if blob:3346 self.commit(description, filesForCommit, branch, blob)3347else:3348if self.verbose:3349print"Parent of%snot found. Committing into head of%s"% (branch, parent)3350 self.commit(description, filesForCommit, branch, parent)3351else:3352 files = self.extractFilesFromCommit(description)3353 self.commit(description, files, self.branch,3354 self.initialParent)3355# only needed once, to connect to the previous commit3356 self.initialParent =""3357exceptIOError:3358print self.gitError.read()3359 sys.exit(1)33603361defsync_origin_only(self):3362if self.syncWithOrigin:3363 self.hasOrigin =originP4BranchesExist()3364if self.hasOrigin:3365if not self.silent:3366print'Syncing with origin first, using "git fetch origin"'3367system("git fetch origin")33683369defimportHeadRevision(self, revision):3370print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)33713372 details = {}3373 details["user"] ="git perforce import user"3374 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3375% (' '.join(self.depotPaths), revision))3376 details["change"] = revision3377 newestRevision =033783379 fileCnt =03380 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]33813382for info inp4CmdList(["files"] + fileArgs):33833384if'code'in info and info['code'] =='error':3385 sys.stderr.write("p4 returned an error:%s\n"3386% info['data'])3387if info['data'].find("must refer to client") >=0:3388 sys.stderr.write("This particular p4 error is misleading.\n")3389 sys.stderr.write("Perhaps the depot path was misspelled.\n");3390 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3391 sys.exit(1)3392if'p4ExitCode'in info:3393 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3394 sys.exit(1)339533963397 change =int(info["change"])3398if change > newestRevision:3399 newestRevision = change34003401if info["action"]in self.delete_actions:3402# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3403#fileCnt = fileCnt + 13404continue34053406for prop in["depotFile","rev","action","type"]:3407 details["%s%s"% (prop, fileCnt)] = info[prop]34083409 fileCnt = fileCnt +134103411 details["change"] = newestRevision34123413# Use time from top-most change so that all git p4 clones of3414# the same p4 repo have the same commit SHA1s.3415 res =p4_describe(newestRevision)3416 details["time"] = res["time"]34173418 self.updateOptionDict(details)3419try:3420 self.commit(details, self.extractFilesFromCommit(details), self.branch)3421exceptIOError:3422print"IO error with git fast-import. Is your git version recent enough?"3423print self.gitError.read()342434253426defrun(self, args):3427 self.depotPaths = []3428 self.changeRange =""3429 self.previousDepotPaths = []3430 self.hasOrigin =False34313432# map from branch depot path to parent branch3433 self.knownBranches = {}3434 self.initialParents = {}34353436if self.importIntoRemotes:3437 self.refPrefix ="refs/remotes/p4/"3438else:3439 self.refPrefix ="refs/heads/p4/"34403441 self.sync_origin_only()34423443 branch_arg_given =bool(self.branch)3444iflen(self.branch) ==0:3445 self.branch = self.refPrefix +"master"3446ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3447system("git update-ref%srefs/heads/p4"% self.branch)3448system("git branch -D p4")34493450# accept either the command-line option, or the configuration variable3451if self.useClientSpec:3452# will use this after clone to set the variable3453 self.useClientSpec_from_options =True3454else:3455ifgitConfigBool("git-p4.useclientspec"):3456 self.useClientSpec =True3457if self.useClientSpec:3458 self.clientSpecDirs =getClientSpec()34593460# TODO: should always look at previous commits,3461# merge with previous imports, if possible.3462if args == []:3463if self.hasOrigin:3464createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)34653466# branches holds mapping from branch name to sha13467 branches =p4BranchesInGit(self.importIntoRemotes)34683469# restrict to just this one, disabling detect-branches3470if branch_arg_given:3471 short = self.branch.split("/")[-1]3472if short in branches:3473 self.p4BranchesInGit = [ short ]3474else:3475 self.p4BranchesInGit = branches.keys()34763477iflen(self.p4BranchesInGit) >1:3478if not self.silent:3479print"Importing from/into multiple branches"3480 self.detectBranches =True3481for branch in branches.keys():3482 self.initialParents[self.refPrefix + branch] = \3483 branches[branch]34843485if self.verbose:3486print"branches:%s"% self.p4BranchesInGit34873488 p4Change =03489for branch in self.p4BranchesInGit:3490 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)34913492 settings =extractSettingsGitLog(logMsg)34933494 self.readOptions(settings)3495if(settings.has_key('depot-paths')3496and settings.has_key('change')):3497 change =int(settings['change']) +13498 p4Change =max(p4Change, change)34993500 depotPaths =sorted(settings['depot-paths'])3501if self.previousDepotPaths == []:3502 self.previousDepotPaths = depotPaths3503else:3504 paths = []3505for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3506 prev_list = prev.split("/")3507 cur_list = cur.split("/")3508for i inrange(0,min(len(cur_list),len(prev_list))):3509if cur_list[i] <> prev_list[i]:3510 i = i -13511break35123513 paths.append("/".join(cur_list[:i +1]))35143515 self.previousDepotPaths = paths35163517if p4Change >0:3518 self.depotPaths =sorted(self.previousDepotPaths)3519 self.changeRange ="@%s,#head"% p4Change3520if not self.silent and not self.detectBranches:3521print"Performing incremental import into%sgit branch"% self.branch35223523# accept multiple ref name abbreviations:3524# refs/foo/bar/branch -> use it exactly3525# p4/branch -> prepend refs/remotes/ or refs/heads/3526# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3527if not self.branch.startswith("refs/"):3528if self.importIntoRemotes:3529 prepend ="refs/remotes/"3530else:3531 prepend ="refs/heads/"3532if not self.branch.startswith("p4/"):3533 prepend +="p4/"3534 self.branch = prepend + self.branch35353536iflen(args) ==0and self.depotPaths:3537if not self.silent:3538print"Depot paths:%s"%' '.join(self.depotPaths)3539else:3540if self.depotPaths and self.depotPaths != args:3541print("previous import used depot path%sand now%swas specified. "3542"This doesn't work!"% (' '.join(self.depotPaths),3543' '.join(args)))3544 sys.exit(1)35453546 self.depotPaths =sorted(args)35473548 revision =""3549 self.users = {}35503551# Make sure no revision specifiers are used when --changesfile3552# is specified.3553 bad_changesfile =False3554iflen(self.changesFile) >0:3555for p in self.depotPaths:3556if p.find("@") >=0or p.find("#") >=0:3557 bad_changesfile =True3558break3559if bad_changesfile:3560die("Option --changesfile is incompatible with revision specifiers")35613562 newPaths = []3563for p in self.depotPaths:3564if p.find("@") != -1:3565 atIdx = p.index("@")3566 self.changeRange = p[atIdx:]3567if self.changeRange =="@all":3568 self.changeRange =""3569elif','not in self.changeRange:3570 revision = self.changeRange3571 self.changeRange =""3572 p = p[:atIdx]3573elif p.find("#") != -1:3574 hashIdx = p.index("#")3575 revision = p[hashIdx:]3576 p = p[:hashIdx]3577elif self.previousDepotPaths == []:3578# pay attention to changesfile, if given, else import3579# the entire p4 tree at the head revision3580iflen(self.changesFile) ==0:3581 revision ="#head"35823583 p = re.sub("\.\.\.$","", p)3584if not p.endswith("/"):3585 p +="/"35863587 newPaths.append(p)35883589 self.depotPaths = newPaths35903591# --detect-branches may change this for each branch3592 self.branchPrefixes = self.depotPaths35933594 self.loadUserMapFromCache()3595 self.labels = {}3596if self.detectLabels:3597 self.getLabels();35983599if self.detectBranches:3600## FIXME - what's a P4 projectName ?3601 self.projectName = self.guessProjectName()36023603if self.hasOrigin:3604 self.getBranchMappingFromGitBranches()3605else:3606 self.getBranchMapping()3607if self.verbose:3608print"p4-git branches:%s"% self.p4BranchesInGit3609print"initial parents:%s"% self.initialParents3610for b in self.p4BranchesInGit:3611if b !="master":36123613## FIXME3614 b = b[len(self.projectName):]3615 self.createdBranches.add(b)36163617 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))36183619 self.importProcess = subprocess.Popen(["git","fast-import"],3620 stdin=subprocess.PIPE,3621 stdout=subprocess.PIPE,3622 stderr=subprocess.PIPE);3623 self.gitOutput = self.importProcess.stdout3624 self.gitStream = self.importProcess.stdin3625 self.gitError = self.importProcess.stderr36263627if revision:3628 self.importHeadRevision(revision)3629else:3630 changes = []36313632iflen(self.changesFile) >0:3633 output =open(self.changesFile).readlines()3634 changeSet =set()3635for line in output:3636 changeSet.add(int(line))36373638for change in changeSet:3639 changes.append(change)36403641 changes.sort()3642else:3643# catch "git p4 sync" with no new branches, in a repo that3644# does not have any existing p4 branches3645iflen(args) ==0:3646if not self.p4BranchesInGit:3647die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")36483649# The default branch is master, unless --branch is used to3650# specify something else. Make sure it exists, or complain3651# nicely about how to use --branch.3652if not self.detectBranches:3653if notbranch_exists(self.branch):3654if branch_arg_given:3655die("Error: branch%sdoes not exist."% self.branch)3656else:3657die("Error: no branch%s; perhaps specify one with --branch."%3658 self.branch)36593660if self.verbose:3661print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3662 self.changeRange)3663 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)36643665iflen(self.maxChanges) >0:3666 changes = changes[:min(int(self.maxChanges),len(changes))]36673668iflen(changes) ==0:3669if not self.silent:3670print"No changes to import!"3671else:3672if not self.silent and not self.detectBranches:3673print"Import destination:%s"% self.branch36743675 self.updatedBranches =set()36763677if not self.detectBranches:3678if args:3679# start a new branch3680 self.initialParent =""3681else:3682# build on a previous revision3683 self.initialParent =parseRevision(self.branch)36843685 self.importChanges(changes)36863687if not self.silent:3688print""3689iflen(self.updatedBranches) >0:3690 sys.stdout.write("Updated branches: ")3691for b in self.updatedBranches:3692 sys.stdout.write("%s"% b)3693 sys.stdout.write("\n")36943695ifgitConfigBool("git-p4.importLabels"):3696 self.importLabels =True36973698if self.importLabels:3699 p4Labels =getP4Labels(self.depotPaths)3700 gitTags =getGitTags()37013702 missingP4Labels = p4Labels - gitTags3703 self.importP4Labels(self.gitStream, missingP4Labels)37043705 self.gitStream.close()3706if self.importProcess.wait() !=0:3707die("fast-import failed:%s"% self.gitError.read())3708 self.gitOutput.close()3709 self.gitError.close()37103711# Cleanup temporary branches created during import3712if self.tempBranches != []:3713for branch in self.tempBranches:3714read_pipe("git update-ref -d%s"% branch)3715 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))37163717# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3718# a convenient shortcut refname "p4".3719if self.importIntoRemotes:3720 head_ref = self.refPrefix +"HEAD"3721if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3722system(["git","symbolic-ref", head_ref, self.branch])37233724return True37253726classP4Rebase(Command):3727def__init__(self):3728 Command.__init__(self)3729 self.options = [3730 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3731]3732 self.importLabels =False3733 self.description = ("Fetches the latest revision from perforce and "3734+"rebases the current work (branch) against it")37353736defrun(self, args):3737 sync =P4Sync()3738 sync.importLabels = self.importLabels3739 sync.run([])37403741return self.rebase()37423743defrebase(self):3744if os.system("git update-index --refresh") !=0:3745die("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.");3746iflen(read_pipe("git diff-index HEAD --")) >0:3747die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");37483749[upstream, settings] =findUpstreamBranchPoint()3750iflen(upstream) ==0:3751die("Cannot find upstream branchpoint for rebase")37523753# the branchpoint may be p4/foo~3, so strip off the parent3754 upstream = re.sub("~[0-9]+$","", upstream)37553756print"Rebasing the current branch onto%s"% upstream3757 oldHead =read_pipe("git rev-parse HEAD").strip()3758system("git rebase%s"% upstream)3759system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3760return True37613762classP4Clone(P4Sync):3763def__init__(self):3764 P4Sync.__init__(self)3765 self.description ="Creates a new git repository and imports from Perforce into it"3766 self.usage ="usage: %prog [options] //depot/path[@revRange]"3767 self.options += [3768 optparse.make_option("--destination", dest="cloneDestination",3769 action='store', default=None,3770help="where to leave result of the clone"),3771 optparse.make_option("--bare", dest="cloneBare",3772 action="store_true", default=False),3773]3774 self.cloneDestination =None3775 self.needsGit =False3776 self.cloneBare =False37773778defdefaultDestination(self, args):3779## TODO: use common prefix of args?3780 depotPath = args[0]3781 depotDir = re.sub("(@[^@]*)$","", depotPath)3782 depotDir = re.sub("(#[^#]*)$","", depotDir)3783 depotDir = re.sub(r"\.\.\.$","", depotDir)3784 depotDir = re.sub(r"/$","", depotDir)3785return os.path.split(depotDir)[1]37863787defrun(self, args):3788iflen(args) <1:3789return False37903791if self.keepRepoPath and not self.cloneDestination:3792 sys.stderr.write("Must specify destination for --keep-path\n")3793 sys.exit(1)37943795 depotPaths = args37963797if not self.cloneDestination andlen(depotPaths) >1:3798 self.cloneDestination = depotPaths[-1]3799 depotPaths = depotPaths[:-1]38003801 self.cloneExclude = ["/"+p for p in self.cloneExclude]3802for p in depotPaths:3803if not p.startswith("//"):3804 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3805return False38063807if not self.cloneDestination:3808 self.cloneDestination = self.defaultDestination(args)38093810print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)38113812if not os.path.exists(self.cloneDestination):3813 os.makedirs(self.cloneDestination)3814chdir(self.cloneDestination)38153816 init_cmd = ["git","init"]3817if self.cloneBare:3818 init_cmd.append("--bare")3819 retcode = subprocess.call(init_cmd)3820if retcode:3821raiseCalledProcessError(retcode, init_cmd)38223823if not P4Sync.run(self, depotPaths):3824return False38253826# create a master branch and check out a work tree3827ifgitBranchExists(self.branch):3828system(["git","branch","master", self.branch ])3829if not self.cloneBare:3830system(["git","checkout","-f"])3831else:3832print'Not checking out any branch, use ' \3833'"git checkout -q -b master <branch>"'38343835# auto-set this variable if invoked with --use-client-spec3836if self.useClientSpec_from_options:3837system("git config --bool git-p4.useclientspec true")38383839return True38403841classP4Branches(Command):3842def__init__(self):3843 Command.__init__(self)3844 self.options = [ ]3845 self.description = ("Shows the git branches that hold imports and their "3846+"corresponding perforce depot paths")3847 self.verbose =False38483849defrun(self, args):3850iforiginP4BranchesExist():3851createOrUpdateBranchesFromOrigin()38523853 cmdline ="git rev-parse --symbolic "3854 cmdline +=" --remotes"38553856for line inread_pipe_lines(cmdline):3857 line = line.strip()38583859if not line.startswith('p4/')or line =="p4/HEAD":3860continue3861 branch = line38623863 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3864 settings =extractSettingsGitLog(log)38653866print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3867return True38683869classHelpFormatter(optparse.IndentedHelpFormatter):3870def__init__(self):3871 optparse.IndentedHelpFormatter.__init__(self)38723873defformat_description(self, description):3874if description:3875return description +"\n"3876else:3877return""38783879defprintUsage(commands):3880print"usage:%s<command> [options]"% sys.argv[0]3881print""3882print"valid commands:%s"%", ".join(commands)3883print""3884print"Try%s<command> --help for command specific help."% sys.argv[0]3885print""38863887commands = {3888"debug": P4Debug,3889"submit": P4Submit,3890"commit": P4Submit,3891"sync": P4Sync,3892"rebase": P4Rebase,3893"clone": P4Clone,3894"rollback": P4RollBack,3895"branches": P4Branches3896}389738983899defmain():3900iflen(sys.argv[1:]) ==0:3901printUsage(commands.keys())3902 sys.exit(2)39033904 cmdName = sys.argv[1]3905try:3906 klass = commands[cmdName]3907 cmd =klass()3908exceptKeyError:3909print"unknown command%s"% cmdName3910print""3911printUsage(commands.keys())3912 sys.exit(2)39133914 options = cmd.options3915 cmd.gitdir = os.environ.get("GIT_DIR",None)39163917 args = sys.argv[2:]39183919 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3920if cmd.needsGit:3921 options.append(optparse.make_option("--git-dir", dest="gitdir"))39223923 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3924 options,3925 description = cmd.description,3926 formatter =HelpFormatter())39273928(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3929global verbose3930 verbose = cmd.verbose3931if cmd.needsGit:3932if cmd.gitdir ==None:3933 cmd.gitdir = os.path.abspath(".git")3934if notisValidGitDir(cmd.gitdir):3935# "rev-parse --git-dir" without arguments will try $PWD/.git3936 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3937if os.path.exists(cmd.gitdir):3938 cdup =read_pipe("git rev-parse --show-cdup").strip()3939iflen(cdup) >0:3940chdir(cdup);39413942if notisValidGitDir(cmd.gitdir):3943ifisValidGitDir(cmd.gitdir +"/.git"):3944 cmd.gitdir +="/.git"3945else:3946die("fatal: cannot locate git repository at%s"% cmd.gitdir)39473948# so git commands invoked from the P4 workspace will succeed3949 os.environ["GIT_DIR"] = cmd.gitdir39503951if not cmd.run(args):3952 parser.print_help()3953 sys.exit(2)395439553956if __name__ =='__main__':3957main()