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# The block size is reduced automatically if required 51defaultBlockSize =1<<20 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, shelved=False): 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 cmd = ["describe","-s"] 380if shelved: 381 cmd += ["-S"] 382 cmd += [str(change)] 383 384 ds =p4CmdList(cmd, skip_info=True) 385iflen(ds) !=1: 386die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 387 388 d = ds[0] 389 390if"p4ExitCode"in d: 391die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 392str(d))) 393if"code"in d: 394if d["code"] =="error": 395die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 396 397if"time"not in d: 398die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 399 400return d 401 402# 403# Canonicalize the p4 type and return a tuple of the 404# base type, plus any modifiers. See "p4 help filetypes" 405# for a list and explanation. 406# 407defsplit_p4_type(p4type): 408 409 p4_filetypes_historical = { 410"ctempobj":"binary+Sw", 411"ctext":"text+C", 412"cxtext":"text+Cx", 413"ktext":"text+k", 414"kxtext":"text+kx", 415"ltext":"text+F", 416"tempobj":"binary+FSw", 417"ubinary":"binary+F", 418"uresource":"resource+F", 419"uxbinary":"binary+Fx", 420"xbinary":"binary+x", 421"xltext":"text+Fx", 422"xtempobj":"binary+Swx", 423"xtext":"text+x", 424"xunicode":"unicode+x", 425"xutf16":"utf16+x", 426} 427if p4type in p4_filetypes_historical: 428 p4type = p4_filetypes_historical[p4type] 429 mods ="" 430 s = p4type.split("+") 431 base = s[0] 432 mods ="" 433iflen(s) >1: 434 mods = s[1] 435return(base, mods) 436 437# 438# return the raw p4 type of a file (text, text+ko, etc) 439# 440defp4_type(f): 441 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 442return results[0]['headType'] 443 444# 445# Given a type base and modifier, return a regexp matching 446# the keywords that can be expanded in the file 447# 448defp4_keywords_regexp_for_type(base, type_mods): 449if base in("text","unicode","binary"): 450 kwords =None 451if"ko"in type_mods: 452 kwords ='Id|Header' 453elif"k"in type_mods: 454 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 455else: 456return None 457 pattern = r""" 458 \$ # Starts with a dollar, followed by... 459 (%s) # one of the keywords, followed by... 460 (:[^$\n]+)? # possibly an old expansion, followed by... 461 \$ # another dollar 462 """% kwords 463return pattern 464else: 465return None 466 467# 468# Given a file, return a regexp matching the possible 469# RCS keywords that will be expanded, or None for files 470# with kw expansion turned off. 471# 472defp4_keywords_regexp_for_file(file): 473if not os.path.exists(file): 474return None 475else: 476(type_base, type_mods) =split_p4_type(p4_type(file)) 477returnp4_keywords_regexp_for_type(type_base, type_mods) 478 479defsetP4ExecBit(file, mode): 480# Reopens an already open file and changes the execute bit to match 481# the execute bit setting in the passed in mode. 482 483 p4Type ="+x" 484 485if notisModeExec(mode): 486 p4Type =getP4OpenedType(file) 487 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 488 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 489if p4Type[-1] =="+": 490 p4Type = p4Type[0:-1] 491 492p4_reopen(p4Type,file) 493 494defgetP4OpenedType(file): 495# Returns the perforce file type for the given file. 496 497 result =p4_read_pipe(["opened",wildcard_encode(file)]) 498 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 499if match: 500return match.group(1) 501else: 502die("Could not determine file type for%s(result: '%s')"% (file, result)) 503 504# Return the set of all p4 labels 505defgetP4Labels(depotPaths): 506 labels =set() 507ifisinstance(depotPaths,basestring): 508 depotPaths = [depotPaths] 509 510for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 511 label = l['label'] 512 labels.add(label) 513 514return labels 515 516# Return the set of all git tags 517defgetGitTags(): 518 gitTags =set() 519for line inread_pipe_lines(["git","tag"]): 520 tag = line.strip() 521 gitTags.add(tag) 522return gitTags 523 524defdiffTreePattern(): 525# This is a simple generator for the diff tree regex pattern. This could be 526# a class variable if this and parseDiffTreeEntry were a part of a class. 527 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 528while True: 529yield pattern 530 531defparseDiffTreeEntry(entry): 532"""Parses a single diff tree entry into its component elements. 533 534 See git-diff-tree(1) manpage for details about the format of the diff 535 output. This method returns a dictionary with the following elements: 536 537 src_mode - The mode of the source file 538 dst_mode - The mode of the destination file 539 src_sha1 - The sha1 for the source file 540 dst_sha1 - The sha1 fr the destination file 541 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 542 status_score - The score for the status (applicable for 'C' and 'R' 543 statuses). This is None if there is no score. 544 src - The path for the source file. 545 dst - The path for the destination file. This is only present for 546 copy or renames. If it is not present, this is None. 547 548 If the pattern is not matched, None is returned.""" 549 550 match =diffTreePattern().next().match(entry) 551if match: 552return{ 553'src_mode': match.group(1), 554'dst_mode': match.group(2), 555'src_sha1': match.group(3), 556'dst_sha1': match.group(4), 557'status': match.group(5), 558'status_score': match.group(6), 559'src': match.group(7), 560'dst': match.group(10) 561} 562return None 563 564defisModeExec(mode): 565# Returns True if the given git mode represents an executable file, 566# otherwise False. 567return mode[-3:] =="755" 568 569classP4Exception(Exception): 570""" Base class for exceptions from the p4 client """ 571def__init__(self, exit_code): 572 self.p4ExitCode = exit_code 573 574classP4ServerException(P4Exception): 575""" Base class for exceptions where we get some kind of marshalled up result from the server """ 576def__init__(self, exit_code, p4_result): 577super(P4ServerException, self).__init__(exit_code) 578 self.p4_result = p4_result 579 self.code = p4_result[0]['code'] 580 self.data = p4_result[0]['data'] 581 582classP4RequestSizeException(P4ServerException): 583""" One of the maxresults or maxscanrows errors """ 584def__init__(self, exit_code, p4_result, limit): 585super(P4RequestSizeException, self).__init__(exit_code, p4_result) 586 self.limit = limit 587 588defisModeExecChanged(src_mode, dst_mode): 589returnisModeExec(src_mode) !=isModeExec(dst_mode) 590 591defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False, 592 errors_as_exceptions=False): 593 594ifisinstance(cmd,basestring): 595 cmd ="-G "+ cmd 596 expand =True 597else: 598 cmd = ["-G"] + cmd 599 expand =False 600 601 cmd =p4_build_cmd(cmd) 602if verbose: 603 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 604 605# Use a temporary file to avoid deadlocks without 606# subprocess.communicate(), which would put another copy 607# of stdout into memory. 608 stdin_file =None 609if stdin is not None: 610 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 611ifisinstance(stdin,basestring): 612 stdin_file.write(stdin) 613else: 614for i in stdin: 615 stdin_file.write(i +'\n') 616 stdin_file.flush() 617 stdin_file.seek(0) 618 619 p4 = subprocess.Popen(cmd, 620 shell=expand, 621 stdin=stdin_file, 622 stdout=subprocess.PIPE) 623 624 result = [] 625try: 626while True: 627 entry = marshal.load(p4.stdout) 628if skip_info: 629if'code'in entry and entry['code'] =='info': 630continue 631if cb is not None: 632cb(entry) 633else: 634 result.append(entry) 635exceptEOFError: 636pass 637 exitCode = p4.wait() 638if exitCode !=0: 639if errors_as_exceptions: 640iflen(result) >0: 641 data = result[0].get('data') 642if data: 643 m = re.search('Too many rows scanned \(over (\d+)\)', data) 644if not m: 645 m = re.search('Request too large \(over (\d+)\)', data) 646 647if m: 648 limit =int(m.group(1)) 649raiseP4RequestSizeException(exitCode, result, limit) 650 651raiseP4ServerException(exitCode, result) 652else: 653raiseP4Exception(exitCode) 654else: 655 entry = {} 656 entry["p4ExitCode"] = exitCode 657 result.append(entry) 658 659return result 660 661defp4Cmd(cmd): 662list=p4CmdList(cmd) 663 result = {} 664for entry inlist: 665 result.update(entry) 666return result; 667 668defp4Where(depotPath): 669if not depotPath.endswith("/"): 670 depotPath +="/" 671 depotPathLong = depotPath +"..." 672 outputList =p4CmdList(["where", depotPathLong]) 673 output =None 674for entry in outputList: 675if"depotFile"in entry: 676# Search for the base client side depot path, as long as it starts with the branch's P4 path. 677# The base path always ends with "/...". 678if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 679 output = entry 680break 681elif"data"in entry: 682 data = entry.get("data") 683 space = data.find(" ") 684if data[:space] == depotPath: 685 output = entry 686break 687if output ==None: 688return"" 689if output["code"] =="error": 690return"" 691 clientPath ="" 692if"path"in output: 693 clientPath = output.get("path") 694elif"data"in output: 695 data = output.get("data") 696 lastSpace = data.rfind(" ") 697 clientPath = data[lastSpace +1:] 698 699if clientPath.endswith("..."): 700 clientPath = clientPath[:-3] 701return clientPath 702 703defcurrentGitBranch(): 704returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 705 706defisValidGitDir(path): 707returngit_dir(path) !=None 708 709defparseRevision(ref): 710returnread_pipe("git rev-parse%s"% ref).strip() 711 712defbranchExists(ref): 713 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 714 ignore_error=True) 715returnlen(rev) >0 716 717defextractLogMessageFromGitCommit(commit): 718 logMessage ="" 719 720## fixme: title is first line of commit, not 1st paragraph. 721 foundTitle =False 722for log inread_pipe_lines("git cat-file commit%s"% commit): 723if not foundTitle: 724iflen(log) ==1: 725 foundTitle =True 726continue 727 728 logMessage += log 729return logMessage 730 731defextractSettingsGitLog(log): 732 values = {} 733for line in log.split("\n"): 734 line = line.strip() 735 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 736if not m: 737continue 738 739 assignments = m.group(1).split(':') 740for a in assignments: 741 vals = a.split('=') 742 key = vals[0].strip() 743 val = ('='.join(vals[1:])).strip() 744if val.endswith('\"')and val.startswith('"'): 745 val = val[1:-1] 746 747 values[key] = val 748 749 paths = values.get("depot-paths") 750if not paths: 751 paths = values.get("depot-path") 752if paths: 753 values['depot-paths'] = paths.split(',') 754return values 755 756defgitBranchExists(branch): 757 proc = subprocess.Popen(["git","rev-parse", branch], 758 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 759return proc.wait() ==0; 760 761defgitUpdateRef(ref, newvalue): 762 subprocess.check_call(["git","update-ref", ref, newvalue]) 763 764defgitDeleteRef(ref): 765 subprocess.check_call(["git","update-ref","-d", ref]) 766 767_gitConfig = {} 768 769defgitConfig(key, typeSpecifier=None): 770if not _gitConfig.has_key(key): 771 cmd = ["git","config"] 772if typeSpecifier: 773 cmd += [ typeSpecifier ] 774 cmd += [ key ] 775 s =read_pipe(cmd, ignore_error=True) 776 _gitConfig[key] = s.strip() 777return _gitConfig[key] 778 779defgitConfigBool(key): 780"""Return a bool, using git config --bool. It is True only if the 781 variable is set to true, and False if set to false or not present 782 in the config.""" 783 784if not _gitConfig.has_key(key): 785 _gitConfig[key] =gitConfig(key,'--bool') =="true" 786return _gitConfig[key] 787 788defgitConfigInt(key): 789if not _gitConfig.has_key(key): 790 cmd = ["git","config","--int", key ] 791 s =read_pipe(cmd, ignore_error=True) 792 v = s.strip() 793try: 794 _gitConfig[key] =int(gitConfig(key,'--int')) 795exceptValueError: 796 _gitConfig[key] =None 797return _gitConfig[key] 798 799defgitConfigList(key): 800if not _gitConfig.has_key(key): 801 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 802 _gitConfig[key] = s.strip().splitlines() 803if _gitConfig[key] == ['']: 804 _gitConfig[key] = [] 805return _gitConfig[key] 806 807defp4BranchesInGit(branchesAreInRemotes=True): 808"""Find all the branches whose names start with "p4/", looking 809 in remotes or heads as specified by the argument. Return 810 a dictionary of{ branch: revision }for each one found. 811 The branch names are the short names, without any 812 "p4/" prefix.""" 813 814 branches = {} 815 816 cmdline ="git rev-parse --symbolic " 817if branchesAreInRemotes: 818 cmdline +="--remotes" 819else: 820 cmdline +="--branches" 821 822for line inread_pipe_lines(cmdline): 823 line = line.strip() 824 825# only import to p4/ 826if not line.startswith('p4/'): 827continue 828# special symbolic ref to p4/master 829if line =="p4/HEAD": 830continue 831 832# strip off p4/ prefix 833 branch = line[len("p4/"):] 834 835 branches[branch] =parseRevision(line) 836 837return branches 838 839defbranch_exists(branch): 840"""Make sure that the given ref name really exists.""" 841 842 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 843 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 844 out, _ = p.communicate() 845if p.returncode: 846return False 847# expect exactly one line of output: the branch name 848return out.rstrip() == branch 849 850deffindUpstreamBranchPoint(head ="HEAD"): 851 branches =p4BranchesInGit() 852# map from depot-path to branch name 853 branchByDepotPath = {} 854for branch in branches.keys(): 855 tip = branches[branch] 856 log =extractLogMessageFromGitCommit(tip) 857 settings =extractSettingsGitLog(log) 858if settings.has_key("depot-paths"): 859 paths =",".join(settings["depot-paths"]) 860 branchByDepotPath[paths] ="remotes/p4/"+ branch 861 862 settings =None 863 parent =0 864while parent <65535: 865 commit = head +"~%s"% parent 866 log =extractLogMessageFromGitCommit(commit) 867 settings =extractSettingsGitLog(log) 868if settings.has_key("depot-paths"): 869 paths =",".join(settings["depot-paths"]) 870if branchByDepotPath.has_key(paths): 871return[branchByDepotPath[paths], settings] 872 873 parent = parent +1 874 875return["", settings] 876 877defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 878if not silent: 879print("Creating/updating branch(es) in%sbased on origin branch(es)" 880% localRefPrefix) 881 882 originPrefix ="origin/p4/" 883 884for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 885 line = line.strip() 886if(not line.startswith(originPrefix))or line.endswith("HEAD"): 887continue 888 889 headName = line[len(originPrefix):] 890 remoteHead = localRefPrefix + headName 891 originHead = line 892 893 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 894if(not original.has_key('depot-paths') 895or not original.has_key('change')): 896continue 897 898 update =False 899if notgitBranchExists(remoteHead): 900if verbose: 901print"creating%s"% remoteHead 902 update =True 903else: 904 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 905if settings.has_key('change') >0: 906if settings['depot-paths'] == original['depot-paths']: 907 originP4Change =int(original['change']) 908 p4Change =int(settings['change']) 909if originP4Change > p4Change: 910print("%s(%s) is newer than%s(%s). " 911"Updating p4 branch from origin." 912% (originHead, originP4Change, 913 remoteHead, p4Change)) 914 update =True 915else: 916print("Ignoring:%swas imported from%swhile " 917"%swas imported from%s" 918% (originHead,','.join(original['depot-paths']), 919 remoteHead,','.join(settings['depot-paths']))) 920 921if update: 922system("git update-ref%s %s"% (remoteHead, originHead)) 923 924deforiginP4BranchesExist(): 925returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 926 927 928defp4ParseNumericChangeRange(parts): 929 changeStart =int(parts[0][1:]) 930if parts[1] =='#head': 931 changeEnd =p4_last_change() 932else: 933 changeEnd =int(parts[1]) 934 935return(changeStart, changeEnd) 936 937defchooseBlockSize(blockSize): 938if blockSize: 939return blockSize 940else: 941return defaultBlockSize 942 943defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 944assert depotPaths 945 946# Parse the change range into start and end. Try to find integer 947# revision ranges as these can be broken up into blocks to avoid 948# hitting server-side limits (maxrows, maxscanresults). But if 949# that doesn't work, fall back to using the raw revision specifier 950# strings, without using block mode. 951 952if changeRange is None or changeRange =='': 953 changeStart =1 954 changeEnd =p4_last_change() 955 block_size =chooseBlockSize(requestedBlockSize) 956else: 957 parts = changeRange.split(',') 958assertlen(parts) ==2 959try: 960(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 961 block_size =chooseBlockSize(requestedBlockSize) 962exceptValueError: 963 changeStart = parts[0][1:] 964 changeEnd = parts[1] 965if requestedBlockSize: 966die("cannot use --changes-block-size with non-numeric revisions") 967 block_size =None 968 969 changes =set() 970 971# Retrieve changes a block at a time, to prevent running 972# into a MaxResults/MaxScanRows error from the server. If 973# we _do_ hit one of those errors, turn down the block size 974 975while True: 976 cmd = ['changes'] 977 978if block_size: 979 end =min(changeEnd, changeStart + block_size) 980 revisionRange ="%d,%d"% (changeStart, end) 981else: 982 revisionRange ="%s,%s"% (changeStart, changeEnd) 983 984for p in depotPaths: 985 cmd += ["%s...@%s"% (p, revisionRange)] 986 987# fetch the changes 988try: 989 result =p4CmdList(cmd, errors_as_exceptions=True) 990except P4RequestSizeException as e: 991if not block_size: 992 block_size = e.limit 993elif block_size > e.limit: 994 block_size = e.limit 995else: 996 block_size =max(2, block_size //2) 997 998if verbose:print("block size error, retrying with block size{0}".format(block_size)) 999continue1000except P4Exception as e:1001die('Error retrieving changes description ({0})'.format(e.p4ExitCode))10021003# Insert changes in chronological order1004for entry inreversed(result):1005if not entry.has_key('change'):1006continue1007 changes.add(int(entry['change']))10081009if not block_size:1010break10111012if end >= changeEnd:1013break10141015 changeStart = end +110161017 changes =sorted(changes)1018return changes10191020defp4PathStartsWith(path, prefix):1021# This method tries to remedy a potential mixed-case issue:1022#1023# If UserA adds //depot/DirA/file11024# and UserB adds //depot/dira/file21025#1026# we may or may not have a problem. If you have core.ignorecase=true,1027# we treat DirA and dira as the same directory1028ifgitConfigBool("core.ignorecase"):1029return path.lower().startswith(prefix.lower())1030return path.startswith(prefix)10311032defgetClientSpec():1033"""Look at the p4 client spec, create a View() object that contains1034 all the mappings, and return it."""10351036 specList =p4CmdList("client -o")1037iflen(specList) !=1:1038die('Output from "client -o" is%dlines, expecting 1'%1039len(specList))10401041# dictionary of all client parameters1042 entry = specList[0]10431044# the //client/ name1045 client_name = entry["Client"]10461047# just the keys that start with "View"1048 view_keys = [ k for k in entry.keys()if k.startswith("View") ]10491050# hold this new View1051 view =View(client_name)10521053# append the lines, in order, to the view1054for view_num inrange(len(view_keys)):1055 k ="View%d"% view_num1056if k not in view_keys:1057die("Expected view key%smissing"% k)1058 view.append(entry[k])10591060return view10611062defgetClientRoot():1063"""Grab the client directory."""10641065 output =p4CmdList("client -o")1066iflen(output) !=1:1067die('Output from "client -o" is%dlines, expecting 1'%len(output))10681069 entry = output[0]1070if"Root"not in entry:1071die('Client has no "Root"')10721073return entry["Root"]10741075#1076# P4 wildcards are not allowed in filenames. P4 complains1077# if you simply add them, but you can force it with "-f", in1078# which case it translates them into %xx encoding internally.1079#1080defwildcard_decode(path):1081# Search for and fix just these four characters. Do % last so1082# that fixing it does not inadvertently create new %-escapes.1083# Cannot have * in a filename in windows; untested as to1084# what p4 would do in such a case.1085if not platform.system() =="Windows":1086 path = path.replace("%2A","*")1087 path = path.replace("%23","#") \1088.replace("%40","@") \1089.replace("%25","%")1090return path10911092defwildcard_encode(path):1093# do % first to avoid double-encoding the %s introduced here1094 path = path.replace("%","%25") \1095.replace("*","%2A") \1096.replace("#","%23") \1097.replace("@","%40")1098return path10991100defwildcard_present(path):1101 m = re.search("[*#@%]", path)1102return m is not None11031104classLargeFileSystem(object):1105"""Base class for large file system support."""11061107def__init__(self, writeToGitStream):1108 self.largeFiles =set()1109 self.writeToGitStream = writeToGitStream11101111defgeneratePointer(self, cloneDestination, contentFile):1112"""Return the content of a pointer file that is stored in Git instead of1113 the actual content."""1114assert False,"Method 'generatePointer' required in "+ self.__class__.__name__11151116defpushFile(self, localLargeFile):1117"""Push the actual content which is not stored in the Git repository to1118 a server."""1119assert False,"Method 'pushFile' required in "+ self.__class__.__name__11201121defhasLargeFileExtension(self, relPath):1122returnreduce(1123lambda a, b: a or b,1124[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1125False1126)11271128defgenerateTempFile(self, contents):1129 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1130for d in contents:1131 contentFile.write(d)1132 contentFile.close()1133return contentFile.name11341135defexceedsLargeFileThreshold(self, relPath, contents):1136ifgitConfigInt('git-p4.largeFileThreshold'):1137 contentsSize =sum(len(d)for d in contents)1138if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1139return True1140ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1141 contentsSize =sum(len(d)for d in contents)1142if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1143return False1144 contentTempFile = self.generateTempFile(contents)1145 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1146 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1147 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1148 zf.close()1149 compressedContentsSize = zf.infolist()[0].compress_size1150 os.remove(contentTempFile)1151 os.remove(compressedContentFile.name)1152if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1153return True1154return False11551156defaddLargeFile(self, relPath):1157 self.largeFiles.add(relPath)11581159defremoveLargeFile(self, relPath):1160 self.largeFiles.remove(relPath)11611162defisLargeFile(self, relPath):1163return relPath in self.largeFiles11641165defprocessContent(self, git_mode, relPath, contents):1166"""Processes the content of git fast import. This method decides if a1167 file is stored in the large file system and handles all necessary1168 steps."""1169if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1170 contentTempFile = self.generateTempFile(contents)1171(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1172if pointer_git_mode:1173 git_mode = pointer_git_mode1174if localLargeFile:1175# Move temp file to final location in large file system1176 largeFileDir = os.path.dirname(localLargeFile)1177if not os.path.isdir(largeFileDir):1178 os.makedirs(largeFileDir)1179 shutil.move(contentTempFile, localLargeFile)1180 self.addLargeFile(relPath)1181ifgitConfigBool('git-p4.largeFilePush'):1182 self.pushFile(localLargeFile)1183if verbose:1184 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1185return(git_mode, contents)11861187classMockLFS(LargeFileSystem):1188"""Mock large file system for testing."""11891190defgeneratePointer(self, contentFile):1191"""The pointer content is the original content prefixed with "pointer-".1192 The local filename of the large file storage is derived from the file content.1193 """1194withopen(contentFile,'r')as f:1195 content =next(f)1196 gitMode ='100644'1197 pointerContents ='pointer-'+ content1198 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1199return(gitMode, pointerContents, localLargeFile)12001201defpushFile(self, localLargeFile):1202"""The remote filename of the large file storage is the same as the local1203 one but in a different directory.1204 """1205 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1206if not os.path.exists(remotePath):1207 os.makedirs(remotePath)1208 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))12091210classGitLFS(LargeFileSystem):1211"""Git LFS as backend for the git-p4 large file system.1212 See https://git-lfs.github.com/ for details."""12131214def__init__(self, *args):1215 LargeFileSystem.__init__(self, *args)1216 self.baseGitAttributes = []12171218defgeneratePointer(self, contentFile):1219"""Generate a Git LFS pointer for the content. Return LFS Pointer file1220 mode and content which is stored in the Git repository instead of1221 the actual content. Return also the new location of the actual1222 content.1223 """1224if os.path.getsize(contentFile) ==0:1225return(None,'',None)12261227 pointerProcess = subprocess.Popen(1228['git','lfs','pointer','--file='+ contentFile],1229 stdout=subprocess.PIPE1230)1231 pointerFile = pointerProcess.stdout.read()1232if pointerProcess.wait():1233 os.remove(contentFile)1234die('git-lfs pointer command failed. Did you install the extension?')12351236# Git LFS removed the preamble in the output of the 'pointer' command1237# starting from version 1.2.0. Check for the preamble here to support1238# earlier versions.1239# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431240if pointerFile.startswith('Git LFS pointer for'):1241 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)12421243 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1244 localLargeFile = os.path.join(1245 os.getcwd(),1246'.git','lfs','objects', oid[:2], oid[2:4],1247 oid,1248)1249# LFS Spec states that pointer files should not have the executable bit set.1250 gitMode ='100644'1251return(gitMode, pointerFile, localLargeFile)12521253defpushFile(self, localLargeFile):1254 uploadProcess = subprocess.Popen(1255['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1256)1257if uploadProcess.wait():1258die('git-lfs push command failed. Did you define a remote?')12591260defgenerateGitAttributes(self):1261return(1262 self.baseGitAttributes +1263[1264'\n',1265'#\n',1266'# Git LFS (see https://git-lfs.github.com/)\n',1267'#\n',1268] +1269['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1270for f insorted(gitConfigList('git-p4.largeFileExtensions'))1271] +1272['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1273for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1274]1275)12761277defaddLargeFile(self, relPath):1278 LargeFileSystem.addLargeFile(self, relPath)1279 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12801281defremoveLargeFile(self, relPath):1282 LargeFileSystem.removeLargeFile(self, relPath)1283 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12841285defprocessContent(self, git_mode, relPath, contents):1286if relPath =='.gitattributes':1287 self.baseGitAttributes = contents1288return(git_mode, self.generateGitAttributes())1289else:1290return LargeFileSystem.processContent(self, git_mode, relPath, contents)12911292class Command:1293def__init__(self):1294 self.usage ="usage: %prog [options]"1295 self.needsGit =True1296 self.verbose =False12971298# This is required for the "append" cloneExclude action1299defensure_value(self, attr, value):1300if nothasattr(self, attr)orgetattr(self, attr)is None:1301setattr(self, attr, value)1302returngetattr(self, attr)13031304class P4UserMap:1305def__init__(self):1306 self.userMapFromPerforceServer =False1307 self.myP4UserId =None13081309defp4UserId(self):1310if self.myP4UserId:1311return self.myP4UserId13121313 results =p4CmdList("user -o")1314for r in results:1315if r.has_key('User'):1316 self.myP4UserId = r['User']1317return r['User']1318die("Could not find your p4 user id")13191320defp4UserIsMe(self, p4User):1321# return True if the given p4 user is actually me1322 me = self.p4UserId()1323if not p4User or p4User != me:1324return False1325else:1326return True13271328defgetUserCacheFilename(self):1329 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1330return home +"/.gitp4-usercache.txt"13311332defgetUserMapFromPerforceServer(self):1333if self.userMapFromPerforceServer:1334return1335 self.users = {}1336 self.emails = {}13371338for output inp4CmdList("users"):1339if not output.has_key("User"):1340continue1341 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1342 self.emails[output["Email"]] = output["User"]13431344 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1345for mapUserConfig ingitConfigList("git-p4.mapUser"):1346 mapUser = mapUserConfigRegex.findall(mapUserConfig)1347if mapUser andlen(mapUser[0]) ==3:1348 user = mapUser[0][0]1349 fullname = mapUser[0][1]1350 email = mapUser[0][2]1351 self.users[user] = fullname +" <"+ email +">"1352 self.emails[email] = user13531354 s =''1355for(key, val)in self.users.items():1356 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))13571358open(self.getUserCacheFilename(),"wb").write(s)1359 self.userMapFromPerforceServer =True13601361defloadUserMapFromCache(self):1362 self.users = {}1363 self.userMapFromPerforceServer =False1364try:1365 cache =open(self.getUserCacheFilename(),"rb")1366 lines = cache.readlines()1367 cache.close()1368for line in lines:1369 entry = line.strip().split("\t")1370 self.users[entry[0]] = entry[1]1371exceptIOError:1372 self.getUserMapFromPerforceServer()13731374classP4Debug(Command):1375def__init__(self):1376 Command.__init__(self)1377 self.options = []1378 self.description ="A tool to debug the output of p4 -G."1379 self.needsGit =False13801381defrun(self, args):1382 j =01383for output inp4CmdList(args):1384print'Element:%d'% j1385 j +=11386print output1387return True13881389classP4RollBack(Command):1390def__init__(self):1391 Command.__init__(self)1392 self.options = [1393 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1394]1395 self.description ="A tool to debug the multi-branch import. Don't use :)"1396 self.rollbackLocalBranches =False13971398defrun(self, args):1399iflen(args) !=1:1400return False1401 maxChange =int(args[0])14021403if"p4ExitCode"inp4Cmd("changes -m 1"):1404die("Problems executing p4");14051406if self.rollbackLocalBranches:1407 refPrefix ="refs/heads/"1408 lines =read_pipe_lines("git rev-parse --symbolic --branches")1409else:1410 refPrefix ="refs/remotes/"1411 lines =read_pipe_lines("git rev-parse --symbolic --remotes")14121413for line in lines:1414if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1415 line = line.strip()1416 ref = refPrefix + line1417 log =extractLogMessageFromGitCommit(ref)1418 settings =extractSettingsGitLog(log)14191420 depotPaths = settings['depot-paths']1421 change = settings['change']14221423 changed =False14241425iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1426for p in depotPaths]))) ==0:1427print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1428system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1429continue14301431while change andint(change) > maxChange:1432 changed =True1433if self.verbose:1434print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1435system("git update-ref%s\"%s^\""% (ref, ref))1436 log =extractLogMessageFromGitCommit(ref)1437 settings =extractSettingsGitLog(log)143814391440 depotPaths = settings['depot-paths']1441 change = settings['change']14421443if changed:1444print"%srewound to%s"% (ref, change)14451446return True14471448classP4Submit(Command, P4UserMap):14491450 conflict_behavior_choices = ("ask","skip","quit")14511452def__init__(self):1453 Command.__init__(self)1454 P4UserMap.__init__(self)1455 self.options = [1456 optparse.make_option("--origin", dest="origin"),1457 optparse.make_option("-M", dest="detectRenames", action="store_true"),1458# preserve the user, requires relevant p4 permissions1459 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1460 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1461 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1462 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1463 optparse.make_option("--conflict", dest="conflict_behavior",1464 choices=self.conflict_behavior_choices),1465 optparse.make_option("--branch", dest="branch"),1466 optparse.make_option("--shelve", dest="shelve", action="store_true",1467help="Shelve instead of submit. Shelved files are reverted, "1468"restoring the workspace to the state before the shelve"),1469 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1470 metavar="CHANGELIST",1471help="update an existing shelved changelist, implies --shelve, "1472"repeat in-order for multiple shelved changelists"),1473 optparse.make_option("--commit", dest="commit", metavar="COMMIT",1474help="submit only the specified commit(s), one commit or xxx..xxx"),1475 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",1476help="Disable rebase after submit is completed. Can be useful if you "1477"work from a local git branch that is not master"),1478 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",1479help="Skip Perforce sync of p4/master after submit or shelve"),1480]1481 self.description ="Submit changes from git to the perforce depot."1482 self.usage +=" [name of git branch to submit into perforce depot]"1483 self.origin =""1484 self.detectRenames =False1485 self.preserveUser =gitConfigBool("git-p4.preserveUser")1486 self.dry_run =False1487 self.shelve =False1488 self.update_shelve =list()1489 self.commit =""1490 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1491 self.disable_p4sync =gitConfigBool("git-p4.disableP4Sync")1492 self.prepare_p4_only =False1493 self.conflict_behavior =None1494 self.isWindows = (platform.system() =="Windows")1495 self.exportLabels =False1496 self.p4HasMoveCommand =p4_has_move_command()1497 self.branch =None14981499ifgitConfig('git-p4.largeFileSystem'):1500die("Large file system not supported for git-p4 submit command. Please remove it from config.")15011502defcheck(self):1503iflen(p4CmdList("opened ...")) >0:1504die("You have files opened with perforce! Close them before starting the sync.")15051506defseparate_jobs_from_description(self, message):1507"""Extract and return a possible Jobs field in the commit1508 message. It goes into a separate section in the p4 change1509 specification.15101511 A jobs line starts with "Jobs:" and looks like a new field1512 in a form. Values are white-space separated on the same1513 line or on following lines that start with a tab.15141515 This does not parse and extract the full git commit message1516 like a p4 form. It just sees the Jobs: line as a marker1517 to pass everything from then on directly into the p4 form,1518 but outside the description section.15191520 Return a tuple (stripped log message, jobs string)."""15211522 m = re.search(r'^Jobs:', message, re.MULTILINE)1523if m is None:1524return(message,None)15251526 jobtext = message[m.start():]1527 stripped_message = message[:m.start()].rstrip()1528return(stripped_message, jobtext)15291530defprepareLogMessage(self, template, message, jobs):1531"""Edits the template returned from "p4 change -o" to insert1532 the message in the Description field, and the jobs text in1533 the Jobs field."""1534 result =""15351536 inDescriptionSection =False15371538for line in template.split("\n"):1539if line.startswith("#"):1540 result += line +"\n"1541continue15421543if inDescriptionSection:1544if line.startswith("Files:")or line.startswith("Jobs:"):1545 inDescriptionSection =False1546# insert Jobs section1547if jobs:1548 result += jobs +"\n"1549else:1550continue1551else:1552if line.startswith("Description:"):1553 inDescriptionSection =True1554 line +="\n"1555for messageLine in message.split("\n"):1556 line +="\t"+ messageLine +"\n"15571558 result += line +"\n"15591560return result15611562defpatchRCSKeywords(self,file, pattern):1563# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1564(handle, outFileName) = tempfile.mkstemp(dir='.')1565try:1566 outFile = os.fdopen(handle,"w+")1567 inFile =open(file,"r")1568 regexp = re.compile(pattern, re.VERBOSE)1569for line in inFile.readlines():1570 line = regexp.sub(r'$\1$', line)1571 outFile.write(line)1572 inFile.close()1573 outFile.close()1574# Forcibly overwrite the original file1575 os.unlink(file)1576 shutil.move(outFileName,file)1577except:1578# cleanup our temporary file1579 os.unlink(outFileName)1580print"Failed to strip RCS keywords in%s"%file1581raise15821583print"Patched up RCS keywords in%s"%file15841585defp4UserForCommit(self,id):1586# Return the tuple (perforce user,git email) for a given git commit id1587 self.getUserMapFromPerforceServer()1588 gitEmail =read_pipe(["git","log","--max-count=1",1589"--format=%ae",id])1590 gitEmail = gitEmail.strip()1591if not self.emails.has_key(gitEmail):1592return(None,gitEmail)1593else:1594return(self.emails[gitEmail],gitEmail)15951596defcheckValidP4Users(self,commits):1597# check if any git authors cannot be mapped to p4 users1598foridin commits:1599(user,email) = self.p4UserForCommit(id)1600if not user:1601 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1602ifgitConfigBool("git-p4.allowMissingP4Users"):1603print"%s"% msg1604else:1605die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)16061607deflastP4Changelist(self):1608# Get back the last changelist number submitted in this client spec. This1609# then gets used to patch up the username in the change. If the same1610# client spec is being used by multiple processes then this might go1611# wrong.1612 results =p4CmdList("client -o")# find the current client1613 client =None1614for r in results:1615if r.has_key('Client'):1616 client = r['Client']1617break1618if not client:1619die("could not get client spec")1620 results =p4CmdList(["changes","-c", client,"-m","1"])1621for r in results:1622if r.has_key('change'):1623return r['change']1624die("Could not get changelist number for last submit - cannot patch up user details")16251626defmodifyChangelistUser(self, changelist, newUser):1627# fixup the user field of a changelist after it has been submitted.1628 changes =p4CmdList("change -o%s"% changelist)1629iflen(changes) !=1:1630die("Bad output from p4 change modifying%sto user%s"%1631(changelist, newUser))16321633 c = changes[0]1634if c['User'] == newUser:return# nothing to do1635 c['User'] = newUser1636input= marshal.dumps(c)16371638 result =p4CmdList("change -f -i", stdin=input)1639for r in result:1640if r.has_key('code'):1641if r['code'] =='error':1642die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1643if r.has_key('data'):1644print("Updated user field for changelist%sto%s"% (changelist, newUser))1645return1646die("Could not modify user field of changelist%sto%s"% (changelist, newUser))16471648defcanChangeChangelists(self):1649# check to see if we have p4 admin or super-user permissions, either of1650# which are required to modify changelists.1651 results =p4CmdList(["protects", self.depotPath])1652for r in results:1653if r.has_key('perm'):1654if r['perm'] =='admin':1655return11656if r['perm'] =='super':1657return11658return016591660defprepareSubmitTemplate(self, changelist=None):1661"""Run "p4 change -o" to grab a change specification template.1662 This does not use "p4 -G", as it is nice to keep the submission1663 template in original order, since a human might edit it.16641665 Remove lines in the Files section that show changes to files1666 outside the depot path we're committing into."""16671668[upstream, settings] =findUpstreamBranchPoint()16691670 template ="""\1671# A Perforce Change Specification.1672#1673# Change: The change number. 'new' on a new changelist.1674# Date: The date this specification was last modified.1675# Client: The client on which the changelist was created. Read-only.1676# User: The user who created the changelist.1677# Status: Either 'pending' or 'submitted'. Read-only.1678# Type: Either 'public' or 'restricted'. Default is 'public'.1679# Description: Comments about the changelist. Required.1680# Jobs: What opened jobs are to be closed by this changelist.1681# You may delete jobs from this list. (New changelists only.)1682# Files: What opened files from the default changelist are to be added1683# to this changelist. You may delete files from this list.1684# (New changelists only.)1685"""1686 files_list = []1687 inFilesSection =False1688 change_entry =None1689 args = ['change','-o']1690if changelist:1691 args.append(str(changelist))1692for entry inp4CmdList(args):1693if not entry.has_key('code'):1694continue1695if entry['code'] =='stat':1696 change_entry = entry1697break1698if not change_entry:1699die('Failed to decode output of p4 change -o')1700for key, value in change_entry.iteritems():1701if key.startswith('File'):1702if settings.has_key('depot-paths'):1703if not[p for p in settings['depot-paths']1704ifp4PathStartsWith(value, p)]:1705continue1706else:1707if notp4PathStartsWith(value, self.depotPath):1708continue1709 files_list.append(value)1710continue1711# Output in the order expected by prepareLogMessage1712for key in['Change','Client','User','Status','Description','Jobs']:1713if not change_entry.has_key(key):1714continue1715 template +='\n'1716 template += key +':'1717if key =='Description':1718 template +='\n'1719for field_line in change_entry[key].splitlines():1720 template +='\t'+field_line+'\n'1721iflen(files_list) >0:1722 template +='\n'1723 template +='Files:\n'1724for path in files_list:1725 template +='\t'+path+'\n'1726return template17271728defedit_template(self, template_file):1729"""Invoke the editor to let the user change the submission1730 message. Return true if okay to continue with the submit."""17311732# if configured to skip the editing part, just submit1733ifgitConfigBool("git-p4.skipSubmitEdit"):1734return True17351736# look at the modification time, to check later if the user saved1737# the file1738 mtime = os.stat(template_file).st_mtime17391740# invoke the editor1741if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1742 editor = os.environ.get("P4EDITOR")1743else:1744 editor =read_pipe("git var GIT_EDITOR").strip()1745system(["sh","-c", ('%s"$@"'% editor), editor, template_file])17461747# If the file was not saved, prompt to see if this patch should1748# be skipped. But skip this verification step if configured so.1749ifgitConfigBool("git-p4.skipSubmitEditCheck"):1750return True17511752# modification time updated means user saved the file1753if os.stat(template_file).st_mtime > mtime:1754return True17551756while True:1757 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1758if response =='y':1759return True1760if response =='n':1761return False17621763defget_diff_description(self, editedFiles, filesToAdd, symlinks):1764# diff1765if os.environ.has_key("P4DIFF"):1766del(os.environ["P4DIFF"])1767 diff =""1768for editedFile in editedFiles:1769 diff +=p4_read_pipe(['diff','-du',1770wildcard_encode(editedFile)])17711772# new file diff1773 newdiff =""1774for newFile in filesToAdd:1775 newdiff +="==== new file ====\n"1776 newdiff +="--- /dev/null\n"1777 newdiff +="+++%s\n"% newFile17781779 is_link = os.path.islink(newFile)1780 expect_link = newFile in symlinks17811782if is_link and expect_link:1783 newdiff +="+%s\n"% os.readlink(newFile)1784else:1785 f =open(newFile,"r")1786for line in f.readlines():1787 newdiff +="+"+ line1788 f.close()17891790return(diff + newdiff).replace('\r\n','\n')17911792defapplyCommit(self,id):1793"""Apply one commit, return True if it succeeded."""17941795print"Applying",read_pipe(["git","show","-s",1796"--format=format:%h%s",id])17971798(p4User, gitEmail) = self.p4UserForCommit(id)17991800 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1801 filesToAdd =set()1802 filesToChangeType =set()1803 filesToDelete =set()1804 editedFiles =set()1805 pureRenameCopy =set()1806 symlinks =set()1807 filesToChangeExecBit = {}1808 all_files =list()18091810for line in diff:1811 diff =parseDiffTreeEntry(line)1812 modifier = diff['status']1813 path = diff['src']1814 all_files.append(path)18151816if modifier =="M":1817p4_edit(path)1818ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1819 filesToChangeExecBit[path] = diff['dst_mode']1820 editedFiles.add(path)1821elif modifier =="A":1822 filesToAdd.add(path)1823 filesToChangeExecBit[path] = diff['dst_mode']1824if path in filesToDelete:1825 filesToDelete.remove(path)18261827 dst_mode =int(diff['dst_mode'],8)1828if dst_mode ==0120000:1829 symlinks.add(path)18301831elif modifier =="D":1832 filesToDelete.add(path)1833if path in filesToAdd:1834 filesToAdd.remove(path)1835elif modifier =="C":1836 src, dest = diff['src'], diff['dst']1837p4_integrate(src, dest)1838 pureRenameCopy.add(dest)1839if diff['src_sha1'] != diff['dst_sha1']:1840p4_edit(dest)1841 pureRenameCopy.discard(dest)1842ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1843p4_edit(dest)1844 pureRenameCopy.discard(dest)1845 filesToChangeExecBit[dest] = diff['dst_mode']1846if self.isWindows:1847# turn off read-only attribute1848 os.chmod(dest, stat.S_IWRITE)1849 os.unlink(dest)1850 editedFiles.add(dest)1851elif modifier =="R":1852 src, dest = diff['src'], diff['dst']1853if self.p4HasMoveCommand:1854p4_edit(src)# src must be open before move1855p4_move(src, dest)# opens for (move/delete, move/add)1856else:1857p4_integrate(src, dest)1858if diff['src_sha1'] != diff['dst_sha1']:1859p4_edit(dest)1860else:1861 pureRenameCopy.add(dest)1862ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1863if not self.p4HasMoveCommand:1864p4_edit(dest)# with move: already open, writable1865 filesToChangeExecBit[dest] = diff['dst_mode']1866if not self.p4HasMoveCommand:1867if self.isWindows:1868 os.chmod(dest, stat.S_IWRITE)1869 os.unlink(dest)1870 filesToDelete.add(src)1871 editedFiles.add(dest)1872elif modifier =="T":1873 filesToChangeType.add(path)1874else:1875die("unknown modifier%sfor%s"% (modifier, path))18761877 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1878 patchcmd = diffcmd +" | git apply "1879 tryPatchCmd = patchcmd +"--check -"1880 applyPatchCmd = patchcmd +"--check --apply -"1881 patch_succeeded =True18821883if os.system(tryPatchCmd) !=0:1884 fixed_rcs_keywords =False1885 patch_succeeded =False1886print"Unfortunately applying the change failed!"18871888# Patch failed, maybe it's just RCS keyword woes. Look through1889# the patch to see if that's possible.1890ifgitConfigBool("git-p4.attemptRCSCleanup"):1891file=None1892 pattern =None1893 kwfiles = {}1894forfilein editedFiles | filesToDelete:1895# did this file's delta contain RCS keywords?1896 pattern =p4_keywords_regexp_for_file(file)18971898if pattern:1899# this file is a possibility...look for RCS keywords.1900 regexp = re.compile(pattern, re.VERBOSE)1901for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1902if regexp.search(line):1903if verbose:1904print"got keyword match on%sin%sin%s"% (pattern, line,file)1905 kwfiles[file] = pattern1906break19071908forfilein kwfiles:1909if verbose:1910print"zapping%swith%s"% (line,pattern)1911# File is being deleted, so not open in p4. Must1912# disable the read-only bit on windows.1913if self.isWindows andfilenot in editedFiles:1914 os.chmod(file, stat.S_IWRITE)1915 self.patchRCSKeywords(file, kwfiles[file])1916 fixed_rcs_keywords =True19171918if fixed_rcs_keywords:1919print"Retrying the patch with RCS keywords cleaned up"1920if os.system(tryPatchCmd) ==0:1921 patch_succeeded =True19221923if not patch_succeeded:1924for f in editedFiles:1925p4_revert(f)1926return False19271928#1929# Apply the patch for real, and do add/delete/+x handling.1930#1931system(applyPatchCmd)19321933for f in filesToChangeType:1934p4_edit(f,"-t","auto")1935for f in filesToAdd:1936p4_add(f)1937for f in filesToDelete:1938p4_revert(f)1939p4_delete(f)19401941# Set/clear executable bits1942for f in filesToChangeExecBit.keys():1943 mode = filesToChangeExecBit[f]1944setP4ExecBit(f, mode)19451946 update_shelve =01947iflen(self.update_shelve) >0:1948 update_shelve = self.update_shelve.pop(0)1949p4_reopen_in_change(update_shelve, all_files)19501951#1952# Build p4 change description, starting with the contents1953# of the git commit message.1954#1955 logMessage =extractLogMessageFromGitCommit(id)1956 logMessage = logMessage.strip()1957(logMessage, jobs) = self.separate_jobs_from_description(logMessage)19581959 template = self.prepareSubmitTemplate(update_shelve)1960 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)19611962if self.preserveUser:1963 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User19641965if self.checkAuthorship and not self.p4UserIsMe(p4User):1966 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1967 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1968 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"19691970 separatorLine ="######## everything below this line is just the diff #######\n"1971if not self.prepare_p4_only:1972 submitTemplate += separatorLine1973 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)19741975(handle, fileName) = tempfile.mkstemp()1976 tmpFile = os.fdopen(handle,"w+b")1977if self.isWindows:1978 submitTemplate = submitTemplate.replace("\n","\r\n")1979 tmpFile.write(submitTemplate)1980 tmpFile.close()19811982if self.prepare_p4_only:1983#1984# Leave the p4 tree prepared, and the submit template around1985# and let the user decide what to do next1986#1987print1988print"P4 workspace prepared for submission."1989print"To submit or revert, go to client workspace"1990print" "+ self.clientPath1991print1992print"To submit, use\"p4 submit\"to write a new description,"1993print"or\"p4 submit -i <%s\"to use the one prepared by" \1994"\"git p4\"."% fileName1995print"You can delete the file\"%s\"when finished."% fileName19961997if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1998print"To preserve change ownership by user%s, you must\n" \1999"do\"p4 change -f <change>\"after submitting and\n" \2000"edit the User field."2001if pureRenameCopy:2002print"After submitting, renamed files must be re-synced."2003print"Invoke\"p4 sync -f\"on each of these files:"2004for f in pureRenameCopy:2005print" "+ f20062007print2008print"To revert the changes, use\"p4 revert ...\", and delete"2009print"the submit template file\"%s\""% fileName2010if filesToAdd:2011print"Since the commit adds new files, they must be deleted:"2012for f in filesToAdd:2013print" "+ f2014print2015return True20162017#2018# Let the user edit the change description, then submit it.2019#2020 submitted =False20212022try:2023if self.edit_template(fileName):2024# read the edited message and submit2025 tmpFile =open(fileName,"rb")2026 message = tmpFile.read()2027 tmpFile.close()2028if self.isWindows:2029 message = message.replace("\r\n","\n")2030 submitTemplate = message[:message.index(separatorLine)]20312032if update_shelve:2033p4_write_pipe(['shelve','-r','-i'], submitTemplate)2034elif self.shelve:2035p4_write_pipe(['shelve','-i'], submitTemplate)2036else:2037p4_write_pipe(['submit','-i'], submitTemplate)2038# The rename/copy happened by applying a patch that created a2039# new file. This leaves it writable, which confuses p4.2040for f in pureRenameCopy:2041p4_sync(f,"-f")20422043if self.preserveUser:2044if p4User:2045# Get last changelist number. Cannot easily get it from2046# the submit command output as the output is2047# unmarshalled.2048 changelist = self.lastP4Changelist()2049 self.modifyChangelistUser(changelist, p4User)20502051 submitted =True20522053finally:2054# skip this patch2055if not submitted or self.shelve:2056if self.shelve:2057print("Reverting shelved files.")2058else:2059print("Submission cancelled, undoing p4 changes.")2060for f in editedFiles | filesToDelete:2061p4_revert(f)2062for f in filesToAdd:2063p4_revert(f)2064 os.remove(f)20652066 os.remove(fileName)2067return submitted20682069# Export git tags as p4 labels. Create a p4 label and then tag2070# with that.2071defexportGitTags(self, gitTags):2072 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")2073iflen(validLabelRegexp) ==0:2074 validLabelRegexp = defaultLabelRegexp2075 m = re.compile(validLabelRegexp)20762077for name in gitTags:20782079if not m.match(name):2080if verbose:2081print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)2082continue20832084# Get the p4 commit this corresponds to2085 logMessage =extractLogMessageFromGitCommit(name)2086 values =extractSettingsGitLog(logMessage)20872088if not values.has_key('change'):2089# a tag pointing to something not sent to p4; ignore2090if verbose:2091print"git tag%sdoes not give a p4 commit"% name2092continue2093else:2094 changelist = values['change']20952096# Get the tag details.2097 inHeader =True2098 isAnnotated =False2099 body = []2100for l inread_pipe_lines(["git","cat-file","-p", name]):2101 l = l.strip()2102if inHeader:2103if re.match(r'tag\s+', l):2104 isAnnotated =True2105elif re.match(r'\s*$', l):2106 inHeader =False2107continue2108else:2109 body.append(l)21102111if not isAnnotated:2112 body = ["lightweight tag imported by git p4\n"]21132114# Create the label - use the same view as the client spec we are using2115 clientSpec =getClientSpec()21162117 labelTemplate ="Label:%s\n"% name2118 labelTemplate +="Description:\n"2119for b in body:2120 labelTemplate +="\t"+ b +"\n"2121 labelTemplate +="View:\n"2122for depot_side in clientSpec.mappings:2123 labelTemplate +="\t%s\n"% depot_side21242125if self.dry_run:2126print"Would create p4 label%sfor tag"% name2127elif self.prepare_p4_only:2128print"Not creating p4 label%sfor tag due to option" \2129" --prepare-p4-only"% name2130else:2131p4_write_pipe(["label","-i"], labelTemplate)21322133# Use the label2134p4_system(["tag","-l", name] +2135["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])21362137if verbose:2138print"created p4 label for tag%s"% name21392140defrun(self, args):2141iflen(args) ==0:2142 self.master =currentGitBranch()2143eliflen(args) ==1:2144 self.master = args[0]2145if notbranchExists(self.master):2146die("Branch%sdoes not exist"% self.master)2147else:2148return False21492150for i in self.update_shelve:2151if i <=0:2152 sys.exit("invalid changelist%d"% i)21532154if self.master:2155 allowSubmit =gitConfig("git-p4.allowSubmit")2156iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2157die("%sis not in git-p4.allowSubmit"% self.master)21582159[upstream, settings] =findUpstreamBranchPoint()2160 self.depotPath = settings['depot-paths'][0]2161iflen(self.origin) ==0:2162 self.origin = upstream21632164iflen(self.update_shelve) >0:2165 self.shelve =True21662167if self.preserveUser:2168if not self.canChangeChangelists():2169die("Cannot preserve user names without p4 super-user or admin permissions")21702171# if not set from the command line, try the config file2172if self.conflict_behavior is None:2173 val =gitConfig("git-p4.conflict")2174if val:2175if val not in self.conflict_behavior_choices:2176die("Invalid value '%s' for config git-p4.conflict"% val)2177else:2178 val ="ask"2179 self.conflict_behavior = val21802181if self.verbose:2182print"Origin branch is "+ self.origin21832184iflen(self.depotPath) ==0:2185print"Internal error: cannot locate perforce depot path from existing branches"2186 sys.exit(128)21872188 self.useClientSpec =False2189ifgitConfigBool("git-p4.useclientspec"):2190 self.useClientSpec =True2191if self.useClientSpec:2192 self.clientSpecDirs =getClientSpec()21932194# Check for the existence of P4 branches2195 branchesDetected = (len(p4BranchesInGit().keys()) >1)21962197if self.useClientSpec and not branchesDetected:2198# all files are relative to the client spec2199 self.clientPath =getClientRoot()2200else:2201 self.clientPath =p4Where(self.depotPath)22022203if self.clientPath =="":2204die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)22052206print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2207 self.oldWorkingDirectory = os.getcwd()22082209# ensure the clientPath exists2210 new_client_dir =False2211if not os.path.exists(self.clientPath):2212 new_client_dir =True2213 os.makedirs(self.clientPath)22142215chdir(self.clientPath, is_client_path=True)2216if self.dry_run:2217print"Would synchronize p4 checkout in%s"% self.clientPath2218else:2219print"Synchronizing p4 checkout..."2220if new_client_dir:2221# old one was destroyed, and maybe nobody told p42222p4_sync("...","-f")2223else:2224p4_sync("...")2225 self.check()22262227 commits = []2228if self.master:2229 committish = self.master2230else:2231 committish ='HEAD'22322233if self.commit !="":2234if self.commit.find("..") != -1:2235 limits_ish = self.commit.split("..")2236for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2237 commits.append(line.strip())2238 commits.reverse()2239else:2240 commits.append(self.commit)2241else:2242for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, committish)]):2243 commits.append(line.strip())2244 commits.reverse()22452246if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2247 self.checkAuthorship =False2248else:2249 self.checkAuthorship =True22502251if self.preserveUser:2252 self.checkValidP4Users(commits)22532254#2255# Build up a set of options to be passed to diff when2256# submitting each commit to p4.2257#2258if self.detectRenames:2259# command-line -M arg2260 self.diffOpts ="-M"2261else:2262# If not explicitly set check the config variable2263 detectRenames =gitConfig("git-p4.detectRenames")22642265if detectRenames.lower() =="false"or detectRenames =="":2266 self.diffOpts =""2267elif detectRenames.lower() =="true":2268 self.diffOpts ="-M"2269else:2270 self.diffOpts ="-M%s"% detectRenames22712272# no command-line arg for -C or --find-copies-harder, just2273# config variables2274 detectCopies =gitConfig("git-p4.detectCopies")2275if detectCopies.lower() =="false"or detectCopies =="":2276pass2277elif detectCopies.lower() =="true":2278 self.diffOpts +=" -C"2279else:2280 self.diffOpts +=" -C%s"% detectCopies22812282ifgitConfigBool("git-p4.detectCopiesHarder"):2283 self.diffOpts +=" --find-copies-harder"22842285 num_shelves =len(self.update_shelve)2286if num_shelves >0and num_shelves !=len(commits):2287 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2288(len(commits), num_shelves))22892290#2291# Apply the commits, one at a time. On failure, ask if should2292# continue to try the rest of the patches, or quit.2293#2294if self.dry_run:2295print"Would apply"2296 applied = []2297 last =len(commits) -12298for i, commit inenumerate(commits):2299if self.dry_run:2300print" ",read_pipe(["git","show","-s",2301"--format=format:%h%s", commit])2302 ok =True2303else:2304 ok = self.applyCommit(commit)2305if ok:2306 applied.append(commit)2307else:2308if self.prepare_p4_only and i < last:2309print"Processing only the first commit due to option" \2310" --prepare-p4-only"2311break2312if i < last:2313 quit =False2314while True:2315# prompt for what to do, or use the option/variable2316if self.conflict_behavior =="ask":2317print"What do you want to do?"2318 response =raw_input("[s]kip this commit but apply"2319" the rest, or [q]uit? ")2320if not response:2321continue2322elif self.conflict_behavior =="skip":2323 response ="s"2324elif self.conflict_behavior =="quit":2325 response ="q"2326else:2327die("Unknown conflict_behavior '%s'"%2328 self.conflict_behavior)23292330if response[0] =="s":2331print"Skipping this commit, but applying the rest"2332break2333if response[0] =="q":2334print"Quitting"2335 quit =True2336break2337if quit:2338break23392340chdir(self.oldWorkingDirectory)2341 shelved_applied ="shelved"if self.shelve else"applied"2342if self.dry_run:2343pass2344elif self.prepare_p4_only:2345pass2346eliflen(commits) ==len(applied):2347print("All commits{0}!".format(shelved_applied))23482349 sync =P4Sync()2350if self.branch:2351 sync.branch = self.branch2352if self.disable_p4sync:2353 sync.sync_origin_only()2354else:2355 sync.run([])23562357if not self.disable_rebase:2358 rebase =P4Rebase()2359 rebase.rebase()23602361else:2362iflen(applied) ==0:2363print("No commits{0}.".format(shelved_applied))2364else:2365print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2366for c in commits:2367if c in applied:2368 star ="*"2369else:2370 star =" "2371print star,read_pipe(["git","show","-s",2372"--format=format:%h%s", c])2373print"You will have to do 'git p4 sync' and rebase."23742375ifgitConfigBool("git-p4.exportLabels"):2376 self.exportLabels =True23772378if self.exportLabels:2379 p4Labels =getP4Labels(self.depotPath)2380 gitTags =getGitTags()23812382 missingGitTags = gitTags - p4Labels2383 self.exportGitTags(missingGitTags)23842385# exit with error unless everything applied perfectly2386iflen(commits) !=len(applied):2387 sys.exit(1)23882389return True23902391classView(object):2392"""Represent a p4 view ("p4 help views"), and map files in a2393 repo according to the view."""23942395def__init__(self, client_name):2396 self.mappings = []2397 self.client_prefix ="//%s/"% client_name2398# cache results of "p4 where" to lookup client file locations2399 self.client_spec_path_cache = {}24002401defappend(self, view_line):2402"""Parse a view line, splitting it into depot and client2403 sides. Append to self.mappings, preserving order. This2404 is only needed for tag creation."""24052406# Split the view line into exactly two words. P4 enforces2407# structure on these lines that simplifies this quite a bit.2408#2409# Either or both words may be double-quoted.2410# Single quotes do not matter.2411# Double-quote marks cannot occur inside the words.2412# A + or - prefix is also inside the quotes.2413# There are no quotes unless they contain a space.2414# The line is already white-space stripped.2415# The two words are separated by a single space.2416#2417if view_line[0] =='"':2418# First word is double quoted. Find its end.2419 close_quote_index = view_line.find('"',1)2420if close_quote_index <=0:2421die("No first-word closing quote found:%s"% view_line)2422 depot_side = view_line[1:close_quote_index]2423# skip closing quote and space2424 rhs_index = close_quote_index +1+12425else:2426 space_index = view_line.find(" ")2427if space_index <=0:2428die("No word-splitting space found:%s"% view_line)2429 depot_side = view_line[0:space_index]2430 rhs_index = space_index +124312432# prefix + means overlay on previous mapping2433if depot_side.startswith("+"):2434 depot_side = depot_side[1:]24352436# prefix - means exclude this path, leave out of mappings2437 exclude =False2438if depot_side.startswith("-"):2439 exclude =True2440 depot_side = depot_side[1:]24412442if not exclude:2443 self.mappings.append(depot_side)24442445defconvert_client_path(self, clientFile):2446# chop off //client/ part to make it relative2447if not clientFile.startswith(self.client_prefix):2448die("No prefix '%s' on clientFile '%s'"%2449(self.client_prefix, clientFile))2450return clientFile[len(self.client_prefix):]24512452defupdate_client_spec_path_cache(self, files):2453""" Caching file paths by "p4 where" batch query """24542455# List depot file paths exclude that already cached2456 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]24572458iflen(fileArgs) ==0:2459return# All files in cache24602461 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2462for res in where_result:2463if"code"in res and res["code"] =="error":2464# assume error is "... file(s) not in client view"2465continue2466if"clientFile"not in res:2467die("No clientFile in 'p4 where' output")2468if"unmap"in res:2469# it will list all of them, but only one not unmap-ped2470continue2471ifgitConfigBool("core.ignorecase"):2472 res['depotFile'] = res['depotFile'].lower()2473 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])24742475# not found files or unmap files set to ""2476for depotFile in fileArgs:2477ifgitConfigBool("core.ignorecase"):2478 depotFile = depotFile.lower()2479if depotFile not in self.client_spec_path_cache:2480 self.client_spec_path_cache[depotFile] =""24812482defmap_in_client(self, depot_path):2483"""Return the relative location in the client where this2484 depot file should live. Returns "" if the file should2485 not be mapped in the client."""24862487ifgitConfigBool("core.ignorecase"):2488 depot_path = depot_path.lower()24892490if depot_path in self.client_spec_path_cache:2491return self.client_spec_path_cache[depot_path]24922493die("Error:%sis not found in client spec path"% depot_path )2494return""24952496classP4Sync(Command, P4UserMap):2497 delete_actions = ("delete","move/delete","purge")24982499def__init__(self):2500 Command.__init__(self)2501 P4UserMap.__init__(self)2502 self.options = [2503 optparse.make_option("--branch", dest="branch"),2504 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2505 optparse.make_option("--changesfile", dest="changesFile"),2506 optparse.make_option("--silent", dest="silent", action="store_true"),2507 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2508 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2509 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2510help="Import into refs/heads/ , not refs/remotes"),2511 optparse.make_option("--max-changes", dest="maxChanges",2512help="Maximum number of changes to import"),2513 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2514help="Internal block size to use when iteratively calling p4 changes"),2515 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2516help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2517 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2518help="Only sync files that are included in the Perforce Client Spec"),2519 optparse.make_option("-/", dest="cloneExclude",2520 action="append",type="string",2521help="exclude depot path"),2522]2523 self.description ="""Imports from Perforce into a git repository.\n2524 example:2525 //depot/my/project/ -- to import the current head2526 //depot/my/project/@all -- to import everything2527 //depot/my/project/@1,6 -- to import only from revision 1 to 625282529 (a ... is not needed in the path p4 specification, it's added implicitly)"""25302531 self.usage +=" //depot/path[@revRange]"2532 self.silent =False2533 self.createdBranches =set()2534 self.committedChanges =set()2535 self.branch =""2536 self.detectBranches =False2537 self.detectLabels =False2538 self.importLabels =False2539 self.changesFile =""2540 self.syncWithOrigin =True2541 self.importIntoRemotes =True2542 self.maxChanges =""2543 self.changes_block_size =None2544 self.keepRepoPath =False2545 self.depotPaths =None2546 self.p4BranchesInGit = []2547 self.cloneExclude = []2548 self.useClientSpec =False2549 self.useClientSpec_from_options =False2550 self.clientSpecDirs =None2551 self.tempBranches = []2552 self.tempBranchLocation ="refs/git-p4-tmp"2553 self.largeFileSystem =None2554 self.suppress_meta_comment =False25552556ifgitConfig('git-p4.largeFileSystem'):2557 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2558 self.largeFileSystem =largeFileSystemConstructor(2559lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2560)25612562ifgitConfig("git-p4.syncFromOrigin") =="false":2563 self.syncWithOrigin =False25642565 self.depotPaths = []2566 self.changeRange =""2567 self.previousDepotPaths = []2568 self.hasOrigin =False25692570# map from branch depot path to parent branch2571 self.knownBranches = {}2572 self.initialParents = {}25732574 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))2575 self.labels = {}25762577# Force a checkpoint in fast-import and wait for it to finish2578defcheckpoint(self):2579 self.gitStream.write("checkpoint\n\n")2580 self.gitStream.write("progress checkpoint\n\n")2581 out = self.gitOutput.readline()2582if self.verbose:2583print"checkpoint finished: "+ out25842585defcmp_shelved(self, path, filerev, revision):2586""" Determine if a path at revision #filerev is the same as the file2587 at revision @revision for a shelved changelist. If they don't match,2588 unshelving won't be safe (we will get other changes mixed in).25892590 This is comparing the revision that the shelved changelist is *based* on, not2591 the shelved changelist itself.2592 """2593 ret =p4Cmd(["diff2","{0}#{1}".format(path, filerev),"{0}@{1}".format(path, revision)])2594if verbose:2595print("p4 diff2 path%sfilerev%srevision%s=>%s"% (path, filerev, revision, ret))2596return ret["status"] =="identical"25972598defextractFilesFromCommit(self, commit, shelved=False, shelved_cl =0, origin_revision =0):2599 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2600for path in self.cloneExclude]2601 files = []2602 fnum =02603while commit.has_key("depotFile%s"% fnum):2604 path = commit["depotFile%s"% fnum]26052606if[p for p in self.cloneExclude2607ifp4PathStartsWith(path, p)]:2608 found =False2609else:2610 found = [p for p in self.depotPaths2611ifp4PathStartsWith(path, p)]2612if not found:2613 fnum = fnum +12614continue26152616file= {}2617file["path"] = path2618file["rev"] = commit["rev%s"% fnum]2619file["action"] = commit["action%s"% fnum]2620file["type"] = commit["type%s"% fnum]2621if shelved:2622file["shelved_cl"] =int(shelved_cl)26232624# For shelved changelists, check that the revision of each file that the2625# shelve was based on matches the revision that we are using for the2626# starting point for git-fast-import (self.initialParent). Otherwise2627# the resulting diff will contain deltas from multiple commits.26282629iffile["action"] !="add"and \2630not self.cmp_shelved(path,file["rev"], origin_revision):2631 sys.exit("change{0}not based on{1}for{2}, cannot unshelve".format(2632 commit["change"], self.initialParent, path))26332634 files.append(file)2635 fnum = fnum +12636return files26372638defextractJobsFromCommit(self, commit):2639 jobs = []2640 jnum =02641while commit.has_key("job%s"% jnum):2642 job = commit["job%s"% jnum]2643 jobs.append(job)2644 jnum = jnum +12645return jobs26462647defstripRepoPath(self, path, prefixes):2648"""When streaming files, this is called to map a p4 depot path2649 to where it should go in git. The prefixes are either2650 self.depotPaths, or self.branchPrefixes in the case of2651 branch detection."""26522653if self.useClientSpec:2654# branch detection moves files up a level (the branch name)2655# from what client spec interpretation gives2656 path = self.clientSpecDirs.map_in_client(path)2657if self.detectBranches:2658for b in self.knownBranches:2659if path.startswith(b +"/"):2660 path = path[len(b)+1:]26612662elif self.keepRepoPath:2663# Preserve everything in relative path name except leading2664# //depot/; just look at first prefix as they all should2665# be in the same depot.2666 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2667ifp4PathStartsWith(path, depot):2668 path = path[len(depot):]26692670else:2671for p in prefixes:2672ifp4PathStartsWith(path, p):2673 path = path[len(p):]2674break26752676 path =wildcard_decode(path)2677return path26782679defsplitFilesIntoBranches(self, commit):2680"""Look at each depotFile in the commit to figure out to what2681 branch it belongs."""26822683if self.clientSpecDirs:2684 files = self.extractFilesFromCommit(commit)2685 self.clientSpecDirs.update_client_spec_path_cache(files)26862687 branches = {}2688 fnum =02689while commit.has_key("depotFile%s"% fnum):2690 path = commit["depotFile%s"% fnum]2691 found = [p for p in self.depotPaths2692ifp4PathStartsWith(path, p)]2693if not found:2694 fnum = fnum +12695continue26962697file= {}2698file["path"] = path2699file["rev"] = commit["rev%s"% fnum]2700file["action"] = commit["action%s"% fnum]2701file["type"] = commit["type%s"% fnum]2702 fnum = fnum +127032704# start with the full relative path where this file would2705# go in a p4 client2706if self.useClientSpec:2707 relPath = self.clientSpecDirs.map_in_client(path)2708else:2709 relPath = self.stripRepoPath(path, self.depotPaths)27102711for branch in self.knownBranches.keys():2712# add a trailing slash so that a commit into qt/4.2foo2713# doesn't end up in qt/4.2, e.g.2714if relPath.startswith(branch +"/"):2715if branch not in branches:2716 branches[branch] = []2717 branches[branch].append(file)2718break27192720return branches27212722defwriteToGitStream(self, gitMode, relPath, contents):2723 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2724 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2725for d in contents:2726 self.gitStream.write(d)2727 self.gitStream.write('\n')27282729defencodeWithUTF8(self, path):2730try:2731 path.decode('ascii')2732except:2733 encoding ='utf8'2734ifgitConfig('git-p4.pathEncoding'):2735 encoding =gitConfig('git-p4.pathEncoding')2736 path = path.decode(encoding,'replace').encode('utf8','replace')2737if self.verbose:2738print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2739return path27402741# output one file from the P4 stream2742# - helper for streamP4Files27432744defstreamOneP4File(self,file, contents):2745 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2746 relPath = self.encodeWithUTF8(relPath)2747if verbose:2748 size =int(self.stream_file['fileSize'])2749 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2750 sys.stdout.flush()27512752(type_base, type_mods) =split_p4_type(file["type"])27532754 git_mode ="100644"2755if"x"in type_mods:2756 git_mode ="100755"2757if type_base =="symlink":2758 git_mode ="120000"2759# p4 print on a symlink sometimes contains "target\n";2760# if it does, remove the newline2761 data =''.join(contents)2762if not data:2763# Some version of p4 allowed creating a symlink that pointed2764# to nothing. This causes p4 errors when checking out such2765# a change, and errors here too. Work around it by ignoring2766# the bad symlink; hopefully a future change fixes it.2767print"\nIgnoring empty symlink in%s"%file['depotFile']2768return2769elif data[-1] =='\n':2770 contents = [data[:-1]]2771else:2772 contents = [data]27732774if type_base =="utf16":2775# p4 delivers different text in the python output to -G2776# than it does when using "print -o", or normal p4 client2777# operations. utf16 is converted to ascii or utf8, perhaps.2778# But ascii text saved as -t utf16 is completely mangled.2779# Invoke print -o to get the real contents.2780#2781# On windows, the newlines will always be mangled by print, so put2782# them back too. This is not needed to the cygwin windows version,2783# just the native "NT" type.2784#2785try:2786 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2787exceptExceptionas e:2788if'Translation of file content failed'instr(e):2789 type_base ='binary'2790else:2791raise e2792else:2793ifp4_version_string().find('/NT') >=0:2794 text = text.replace('\r\n','\n')2795 contents = [ text ]27962797if type_base =="apple":2798# Apple filetype files will be streamed as a concatenation of2799# its appledouble header and the contents. This is useless2800# on both macs and non-macs. If using "print -q -o xx", it2801# will create "xx" with the data, and "%xx" with the header.2802# This is also not very useful.2803#2804# Ideally, someday, this script can learn how to generate2805# appledouble files directly and import those to git, but2806# non-mac machines can never find a use for apple filetype.2807print"\nIgnoring apple filetype file%s"%file['depotFile']2808return28092810# Note that we do not try to de-mangle keywords on utf16 files,2811# even though in theory somebody may want that.2812 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2813if pattern:2814 regexp = re.compile(pattern, re.VERBOSE)2815 text =''.join(contents)2816 text = regexp.sub(r'$\1$', text)2817 contents = [ text ]28182819if self.largeFileSystem:2820(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)28212822 self.writeToGitStream(git_mode, relPath, contents)28232824defstreamOneP4Deletion(self,file):2825 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2826 relPath = self.encodeWithUTF8(relPath)2827if verbose:2828 sys.stdout.write("delete%s\n"% relPath)2829 sys.stdout.flush()2830 self.gitStream.write("D%s\n"% relPath)28312832if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2833 self.largeFileSystem.removeLargeFile(relPath)28342835# handle another chunk of streaming data2836defstreamP4FilesCb(self, marshalled):28372838# catch p4 errors and complain2839 err =None2840if"code"in marshalled:2841if marshalled["code"] =="error":2842if"data"in marshalled:2843 err = marshalled["data"].rstrip()28442845if not err and'fileSize'in self.stream_file:2846 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2847if required_bytes >0:2848 err ='Not enough space left on%s! Free at least%iMB.'% (2849 os.getcwd(), required_bytes/1024/10242850)28512852if err:2853 f =None2854if self.stream_have_file_info:2855if"depotFile"in self.stream_file:2856 f = self.stream_file["depotFile"]2857# force a failure in fast-import, else an empty2858# commit will be made2859 self.gitStream.write("\n")2860 self.gitStream.write("die-now\n")2861 self.gitStream.close()2862# ignore errors, but make sure it exits first2863 self.importProcess.wait()2864if f:2865die("Error from p4 print for%s:%s"% (f, err))2866else:2867die("Error from p4 print:%s"% err)28682869if marshalled.has_key('depotFile')and self.stream_have_file_info:2870# start of a new file - output the old one first2871 self.streamOneP4File(self.stream_file, self.stream_contents)2872 self.stream_file = {}2873 self.stream_contents = []2874 self.stream_have_file_info =False28752876# pick up the new file information... for the2877# 'data' field we need to append to our array2878for k in marshalled.keys():2879if k =='data':2880if'streamContentSize'not in self.stream_file:2881 self.stream_file['streamContentSize'] =02882 self.stream_file['streamContentSize'] +=len(marshalled['data'])2883 self.stream_contents.append(marshalled['data'])2884else:2885 self.stream_file[k] = marshalled[k]28862887if(verbose and2888'streamContentSize'in self.stream_file and2889'fileSize'in self.stream_file and2890'depotFile'in self.stream_file):2891 size =int(self.stream_file["fileSize"])2892if size >0:2893 progress =100*self.stream_file['streamContentSize']/size2894 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2895 sys.stdout.flush()28962897 self.stream_have_file_info =True28982899# Stream directly from "p4 files" into "git fast-import"2900defstreamP4Files(self, files):2901 filesForCommit = []2902 filesToRead = []2903 filesToDelete = []29042905for f in files:2906 filesForCommit.append(f)2907if f['action']in self.delete_actions:2908 filesToDelete.append(f)2909else:2910 filesToRead.append(f)29112912# deleted files...2913for f in filesToDelete:2914 self.streamOneP4Deletion(f)29152916iflen(filesToRead) >0:2917 self.stream_file = {}2918 self.stream_contents = []2919 self.stream_have_file_info =False29202921# curry self argument2922defstreamP4FilesCbSelf(entry):2923 self.streamP4FilesCb(entry)29242925 fileArgs = []2926for f in filesToRead:2927if'shelved_cl'in f:2928# Handle shelved CLs using the "p4 print file@=N" syntax to print2929# the contents2930 fileArg ='%s@=%d'% (f['path'], f['shelved_cl'])2931else:2932 fileArg ='%s#%s'% (f['path'], f['rev'])29332934 fileArgs.append(fileArg)29352936p4CmdList(["-x","-","print"],2937 stdin=fileArgs,2938 cb=streamP4FilesCbSelf)29392940# do the last chunk2941if self.stream_file.has_key('depotFile'):2942 self.streamOneP4File(self.stream_file, self.stream_contents)29432944defmake_email(self, userid):2945if userid in self.users:2946return self.users[userid]2947else:2948return"%s<a@b>"% userid29492950defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2951""" Stream a p4 tag.2952 commit is either a git commit, or a fast-import mark, ":<p4commit>"2953 """29542955if verbose:2956print"writing tag%sfor commit%s"% (labelName, commit)2957 gitStream.write("tag%s\n"% labelName)2958 gitStream.write("from%s\n"% commit)29592960if labelDetails.has_key('Owner'):2961 owner = labelDetails["Owner"]2962else:2963 owner =None29642965# Try to use the owner of the p4 label, or failing that,2966# the current p4 user id.2967if owner:2968 email = self.make_email(owner)2969else:2970 email = self.make_email(self.p4UserId())2971 tagger ="%s %s %s"% (email, epoch, self.tz)29722973 gitStream.write("tagger%s\n"% tagger)29742975print"labelDetails=",labelDetails2976if labelDetails.has_key('Description'):2977 description = labelDetails['Description']2978else:2979 description ='Label from git p4'29802981 gitStream.write("data%d\n"%len(description))2982 gitStream.write(description)2983 gitStream.write("\n")29842985definClientSpec(self, path):2986if not self.clientSpecDirs:2987return True2988 inClientSpec = self.clientSpecDirs.map_in_client(path)2989if not inClientSpec and self.verbose:2990print('Ignoring file outside of client spec:{0}'.format(path))2991return inClientSpec29922993defhasBranchPrefix(self, path):2994if not self.branchPrefixes:2995return True2996 hasPrefix = [p for p in self.branchPrefixes2997ifp4PathStartsWith(path, p)]2998if not hasPrefix and self.verbose:2999print('Ignoring file outside of prefix:{0}'.format(path))3000return hasPrefix30013002defcommit(self, details, files, branch, parent =""):3003 epoch = details["time"]3004 author = details["user"]3005 jobs = self.extractJobsFromCommit(details)30063007if self.verbose:3008print('commit into{0}'.format(branch))30093010if self.clientSpecDirs:3011 self.clientSpecDirs.update_client_spec_path_cache(files)30123013 files = [f for f in files3014if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]30153016if not files and notgitConfigBool('git-p4.keepEmptyCommits'):3017print('Ignoring revision{0}as it would produce an empty commit.'3018.format(details['change']))3019return30203021 self.gitStream.write("commit%s\n"% branch)3022 self.gitStream.write("mark :%s\n"% details["change"])3023 self.committedChanges.add(int(details["change"]))3024 committer =""3025if author not in self.users:3026 self.getUserMapFromPerforceServer()3027 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)30283029 self.gitStream.write("committer%s\n"% committer)30303031 self.gitStream.write("data <<EOT\n")3032 self.gitStream.write(details["desc"])3033iflen(jobs) >0:3034 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))30353036if not self.suppress_meta_comment:3037 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%3038(','.join(self.branchPrefixes), details["change"]))3039iflen(details['options']) >0:3040 self.gitStream.write(": options =%s"% details['options'])3041 self.gitStream.write("]\n")30423043 self.gitStream.write("EOT\n\n")30443045iflen(parent) >0:3046if self.verbose:3047print"parent%s"% parent3048 self.gitStream.write("from%s\n"% parent)30493050 self.streamP4Files(files)3051 self.gitStream.write("\n")30523053 change =int(details["change"])30543055if self.labels.has_key(change):3056 label = self.labels[change]3057 labelDetails = label[0]3058 labelRevisions = label[1]3059if self.verbose:3060print"Change%sis labelled%s"% (change, labelDetails)30613062 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)3063for p in self.branchPrefixes])30643065iflen(files) ==len(labelRevisions):30663067 cleanedFiles = {}3068for info in files:3069if info["action"]in self.delete_actions:3070continue3071 cleanedFiles[info["depotFile"]] = info["rev"]30723073if cleanedFiles == labelRevisions:3074 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)30753076else:3077if not self.silent:3078print("Tag%sdoes not match with change%s: files do not match."3079% (labelDetails["label"], change))30803081else:3082if not self.silent:3083print("Tag%sdoes not match with change%s: file count is different."3084% (labelDetails["label"], change))30853086# Build a dictionary of changelists and labels, for "detect-labels" option.3087defgetLabels(self):3088 self.labels = {}30893090 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])3091iflen(l) >0and not self.silent:3092print"Finding files belonging to labels in%s"% `self.depotPaths`30933094for output in l:3095 label = output["label"]3096 revisions = {}3097 newestChange =03098if self.verbose:3099print"Querying files for label%s"% label3100forfileinp4CmdList(["files"] +3101["%s...@%s"% (p, label)3102for p in self.depotPaths]):3103 revisions[file["depotFile"]] =file["rev"]3104 change =int(file["change"])3105if change > newestChange:3106 newestChange = change31073108 self.labels[newestChange] = [output, revisions]31093110if self.verbose:3111print"Label changes:%s"% self.labels.keys()31123113# Import p4 labels as git tags. A direct mapping does not3114# exist, so assume that if all the files are at the same revision3115# then we can use that, or it's something more complicated we should3116# just ignore.3117defimportP4Labels(self, stream, p4Labels):3118if verbose:3119print"import p4 labels: "+' '.join(p4Labels)31203121 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")3122 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")3123iflen(validLabelRegexp) ==0:3124 validLabelRegexp = defaultLabelRegexp3125 m = re.compile(validLabelRegexp)31263127for name in p4Labels:3128 commitFound =False31293130if not m.match(name):3131if verbose:3132print"label%sdoes not match regexp%s"% (name,validLabelRegexp)3133continue31343135if name in ignoredP4Labels:3136continue31373138 labelDetails =p4CmdList(['label',"-o", name])[0]31393140# get the most recent changelist for each file in this label3141 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)3142for p in self.depotPaths])31433144if change.has_key('change'):3145# find the corresponding git commit; take the oldest commit3146 changelist =int(change['change'])3147if changelist in self.committedChanges:3148 gitCommit =":%d"% changelist # use a fast-import mark3149 commitFound =True3150else:3151 gitCommit =read_pipe(["git","rev-list","--max-count=1",3152"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)3153iflen(gitCommit) ==0:3154print"importing label%s: could not find git commit for changelist%d"% (name, changelist)3155else:3156 commitFound =True3157 gitCommit = gitCommit.strip()31583159if commitFound:3160# Convert from p4 time format3161try:3162 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")3163exceptValueError:3164print"Could not convert label time%s"% labelDetails['Update']3165 tmwhen =131663167 when =int(time.mktime(tmwhen))3168 self.streamTag(stream, name, labelDetails, gitCommit, when)3169if verbose:3170print"p4 label%smapped to git commit%s"% (name, gitCommit)3171else:3172if verbose:3173print"Label%shas no changelists - possibly deleted?"% name31743175if not commitFound:3176# We can't import this label; don't try again as it will get very3177# expensive repeatedly fetching all the files for labels that will3178# never be imported. If the label is moved in the future, the3179# ignore will need to be removed manually.3180system(["git","config","--add","git-p4.ignoredP4Labels", name])31813182defguessProjectName(self):3183for p in self.depotPaths:3184if p.endswith("/"):3185 p = p[:-1]3186 p = p[p.strip().rfind("/") +1:]3187if not p.endswith("/"):3188 p +="/"3189return p31903191defgetBranchMapping(self):3192 lostAndFoundBranches =set()31933194 user =gitConfig("git-p4.branchUser")3195iflen(user) >0:3196 command ="branches -u%s"% user3197else:3198 command ="branches"31993200for info inp4CmdList(command):3201 details =p4Cmd(["branch","-o", info["branch"]])3202 viewIdx =03203while details.has_key("View%s"% viewIdx):3204 paths = details["View%s"% viewIdx].split(" ")3205 viewIdx = viewIdx +13206# require standard //depot/foo/... //depot/bar/... mapping3207iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3208continue3209 source = paths[0]3210 destination = paths[1]3211## HACK3212ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3213 source = source[len(self.depotPaths[0]):-4]3214 destination = destination[len(self.depotPaths[0]):-4]32153216if destination in self.knownBranches:3217if not self.silent:3218print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)3219print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)3220continue32213222 self.knownBranches[destination] = source32233224 lostAndFoundBranches.discard(destination)32253226if source not in self.knownBranches:3227 lostAndFoundBranches.add(source)32283229# Perforce does not strictly require branches to be defined, so we also3230# check git config for a branch list.3231#3232# Example of branch definition in git config file:3233# [git-p4]3234# branchList=main:branchA3235# branchList=main:branchB3236# branchList=branchA:branchC3237 configBranches =gitConfigList("git-p4.branchList")3238for branch in configBranches:3239if branch:3240(source, destination) = branch.split(":")3241 self.knownBranches[destination] = source32423243 lostAndFoundBranches.discard(destination)32443245if source not in self.knownBranches:3246 lostAndFoundBranches.add(source)324732483249for branch in lostAndFoundBranches:3250 self.knownBranches[branch] = branch32513252defgetBranchMappingFromGitBranches(self):3253 branches =p4BranchesInGit(self.importIntoRemotes)3254for branch in branches.keys():3255if branch =="master":3256 branch ="main"3257else:3258 branch = branch[len(self.projectName):]3259 self.knownBranches[branch] = branch32603261defupdateOptionDict(self, d):3262 option_keys = {}3263if self.keepRepoPath:3264 option_keys['keepRepoPath'] =132653266 d["options"] =' '.join(sorted(option_keys.keys()))32673268defreadOptions(self, d):3269 self.keepRepoPath = (d.has_key('options')3270and('keepRepoPath'in d['options']))32713272defgitRefForBranch(self, branch):3273if branch =="main":3274return self.refPrefix +"master"32753276iflen(branch) <=0:3277return branch32783279return self.refPrefix + self.projectName + branch32803281defgitCommitByP4Change(self, ref, change):3282if self.verbose:3283print"looking in ref "+ ref +" for change%susing bisect..."% change32843285 earliestCommit =""3286 latestCommit =parseRevision(ref)32873288while True:3289if self.verbose:3290print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3291 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3292iflen(next) ==0:3293if self.verbose:3294print"argh"3295return""3296 log =extractLogMessageFromGitCommit(next)3297 settings =extractSettingsGitLog(log)3298 currentChange =int(settings['change'])3299if self.verbose:3300print"current change%s"% currentChange33013302if currentChange == change:3303if self.verbose:3304print"found%s"% next3305return next33063307if currentChange < change:3308 earliestCommit ="^%s"% next3309else:3310 latestCommit ="%s"% next33113312return""33133314defimportNewBranch(self, branch, maxChange):3315# make fast-import flush all changes to disk and update the refs using the checkpoint3316# command so that we can try to find the branch parent in the git history3317 self.gitStream.write("checkpoint\n\n");3318 self.gitStream.flush();3319 branchPrefix = self.depotPaths[0] + branch +"/"3320range="@1,%s"% maxChange3321#print "prefix" + branchPrefix3322 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3323iflen(changes) <=0:3324return False3325 firstChange = changes[0]3326#print "first change in branch: %s" % firstChange3327 sourceBranch = self.knownBranches[branch]3328 sourceDepotPath = self.depotPaths[0] + sourceBranch3329 sourceRef = self.gitRefForBranch(sourceBranch)3330#print "source " + sourceBranch33313332 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3333#print "branch parent: %s" % branchParentChange3334 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3335iflen(gitParent) >0:3336 self.initialParents[self.gitRefForBranch(branch)] = gitParent3337#print "parent git commit: %s" % gitParent33383339 self.importChanges(changes)3340return True33413342defsearchParent(self, parent, branch, target):3343 parentFound =False3344for blob inread_pipe_lines(["git","rev-list","--reverse",3345"--no-merges", parent]):3346 blob = blob.strip()3347iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3348 parentFound =True3349if self.verbose:3350print"Found parent of%sin commit%s"% (branch, blob)3351break3352if parentFound:3353return blob3354else:3355return None33563357defimportChanges(self, changes, shelved=False, origin_revision=0):3358 cnt =13359for change in changes:3360 description =p4_describe(change, shelved)3361 self.updateOptionDict(description)33623363if not self.silent:3364 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3365 sys.stdout.flush()3366 cnt = cnt +133673368try:3369if self.detectBranches:3370 branches = self.splitFilesIntoBranches(description)3371for branch in branches.keys():3372## HACK --hwn3373 branchPrefix = self.depotPaths[0] + branch +"/"3374 self.branchPrefixes = [ branchPrefix ]33753376 parent =""33773378 filesForCommit = branches[branch]33793380if self.verbose:3381print"branch is%s"% branch33823383 self.updatedBranches.add(branch)33843385if branch not in self.createdBranches:3386 self.createdBranches.add(branch)3387 parent = self.knownBranches[branch]3388if parent == branch:3389 parent =""3390else:3391 fullBranch = self.projectName + branch3392if fullBranch not in self.p4BranchesInGit:3393if not self.silent:3394print("\nImporting new branch%s"% fullBranch);3395if self.importNewBranch(branch, change -1):3396 parent =""3397 self.p4BranchesInGit.append(fullBranch)3398if not self.silent:3399print("\nResuming with change%s"% change);34003401if self.verbose:3402print"parent determined through known branches:%s"% parent34033404 branch = self.gitRefForBranch(branch)3405 parent = self.gitRefForBranch(parent)34063407if self.verbose:3408print"looking for initial parent for%s; current parent is%s"% (branch, parent)34093410iflen(parent) ==0and branch in self.initialParents:3411 parent = self.initialParents[branch]3412del self.initialParents[branch]34133414 blob =None3415iflen(parent) >0:3416 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3417if self.verbose:3418print"Creating temporary branch: "+ tempBranch3419 self.commit(description, filesForCommit, tempBranch)3420 self.tempBranches.append(tempBranch)3421 self.checkpoint()3422 blob = self.searchParent(parent, branch, tempBranch)3423if blob:3424 self.commit(description, filesForCommit, branch, blob)3425else:3426if self.verbose:3427print"Parent of%snot found. Committing into head of%s"% (branch, parent)3428 self.commit(description, filesForCommit, branch, parent)3429else:3430 files = self.extractFilesFromCommit(description, shelved, change, origin_revision)3431 self.commit(description, files, self.branch,3432 self.initialParent)3433# only needed once, to connect to the previous commit3434 self.initialParent =""3435exceptIOError:3436print self.gitError.read()3437 sys.exit(1)34383439defsync_origin_only(self):3440if self.syncWithOrigin:3441 self.hasOrigin =originP4BranchesExist()3442if self.hasOrigin:3443if not self.silent:3444print'Syncing with origin first, using "git fetch origin"'3445system("git fetch origin")34463447defimportHeadRevision(self, revision):3448print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)34493450 details = {}3451 details["user"] ="git perforce import user"3452 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3453% (' '.join(self.depotPaths), revision))3454 details["change"] = revision3455 newestRevision =034563457 fileCnt =03458 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]34593460for info inp4CmdList(["files"] + fileArgs):34613462if'code'in info and info['code'] =='error':3463 sys.stderr.write("p4 returned an error:%s\n"3464% info['data'])3465if info['data'].find("must refer to client") >=0:3466 sys.stderr.write("This particular p4 error is misleading.\n")3467 sys.stderr.write("Perhaps the depot path was misspelled.\n");3468 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3469 sys.exit(1)3470if'p4ExitCode'in info:3471 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3472 sys.exit(1)347334743475 change =int(info["change"])3476if change > newestRevision:3477 newestRevision = change34783479if info["action"]in self.delete_actions:3480# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3481#fileCnt = fileCnt + 13482continue34833484for prop in["depotFile","rev","action","type"]:3485 details["%s%s"% (prop, fileCnt)] = info[prop]34863487 fileCnt = fileCnt +134883489 details["change"] = newestRevision34903491# Use time from top-most change so that all git p4 clones of3492# the same p4 repo have the same commit SHA1s.3493 res =p4_describe(newestRevision)3494 details["time"] = res["time"]34953496 self.updateOptionDict(details)3497try:3498 self.commit(details, self.extractFilesFromCommit(details), self.branch)3499exceptIOError:3500print"IO error with git fast-import. Is your git version recent enough?"3501print self.gitError.read()35023503defopenStreams(self):3504 self.importProcess = subprocess.Popen(["git","fast-import"],3505 stdin=subprocess.PIPE,3506 stdout=subprocess.PIPE,3507 stderr=subprocess.PIPE);3508 self.gitOutput = self.importProcess.stdout3509 self.gitStream = self.importProcess.stdin3510 self.gitError = self.importProcess.stderr35113512defcloseStreams(self):3513 self.gitStream.close()3514if self.importProcess.wait() !=0:3515die("fast-import failed:%s"% self.gitError.read())3516 self.gitOutput.close()3517 self.gitError.close()35183519defrun(self, args):3520if self.importIntoRemotes:3521 self.refPrefix ="refs/remotes/p4/"3522else:3523 self.refPrefix ="refs/heads/p4/"35243525 self.sync_origin_only()35263527 branch_arg_given =bool(self.branch)3528iflen(self.branch) ==0:3529 self.branch = self.refPrefix +"master"3530ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3531system("git update-ref%srefs/heads/p4"% self.branch)3532system("git branch -D p4")35333534# accept either the command-line option, or the configuration variable3535if self.useClientSpec:3536# will use this after clone to set the variable3537 self.useClientSpec_from_options =True3538else:3539ifgitConfigBool("git-p4.useclientspec"):3540 self.useClientSpec =True3541if self.useClientSpec:3542 self.clientSpecDirs =getClientSpec()35433544# TODO: should always look at previous commits,3545# merge with previous imports, if possible.3546if args == []:3547if self.hasOrigin:3548createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)35493550# branches holds mapping from branch name to sha13551 branches =p4BranchesInGit(self.importIntoRemotes)35523553# restrict to just this one, disabling detect-branches3554if branch_arg_given:3555 short = self.branch.split("/")[-1]3556if short in branches:3557 self.p4BranchesInGit = [ short ]3558else:3559 self.p4BranchesInGit = branches.keys()35603561iflen(self.p4BranchesInGit) >1:3562if not self.silent:3563print"Importing from/into multiple branches"3564 self.detectBranches =True3565for branch in branches.keys():3566 self.initialParents[self.refPrefix + branch] = \3567 branches[branch]35683569if self.verbose:3570print"branches:%s"% self.p4BranchesInGit35713572 p4Change =03573for branch in self.p4BranchesInGit:3574 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)35753576 settings =extractSettingsGitLog(logMsg)35773578 self.readOptions(settings)3579if(settings.has_key('depot-paths')3580and settings.has_key('change')):3581 change =int(settings['change']) +13582 p4Change =max(p4Change, change)35833584 depotPaths =sorted(settings['depot-paths'])3585if self.previousDepotPaths == []:3586 self.previousDepotPaths = depotPaths3587else:3588 paths = []3589for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3590 prev_list = prev.split("/")3591 cur_list = cur.split("/")3592for i inrange(0,min(len(cur_list),len(prev_list))):3593if cur_list[i] <> prev_list[i]:3594 i = i -13595break35963597 paths.append("/".join(cur_list[:i +1]))35983599 self.previousDepotPaths = paths36003601if p4Change >0:3602 self.depotPaths =sorted(self.previousDepotPaths)3603 self.changeRange ="@%s,#head"% p4Change3604if not self.silent and not self.detectBranches:3605print"Performing incremental import into%sgit branch"% self.branch36063607# accept multiple ref name abbreviations:3608# refs/foo/bar/branch -> use it exactly3609# p4/branch -> prepend refs/remotes/ or refs/heads/3610# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3611if not self.branch.startswith("refs/"):3612if self.importIntoRemotes:3613 prepend ="refs/remotes/"3614else:3615 prepend ="refs/heads/"3616if not self.branch.startswith("p4/"):3617 prepend +="p4/"3618 self.branch = prepend + self.branch36193620iflen(args) ==0and self.depotPaths:3621if not self.silent:3622print"Depot paths:%s"%' '.join(self.depotPaths)3623else:3624if self.depotPaths and self.depotPaths != args:3625print("previous import used depot path%sand now%swas specified. "3626"This doesn't work!"% (' '.join(self.depotPaths),3627' '.join(args)))3628 sys.exit(1)36293630 self.depotPaths =sorted(args)36313632 revision =""3633 self.users = {}36343635# Make sure no revision specifiers are used when --changesfile3636# is specified.3637 bad_changesfile =False3638iflen(self.changesFile) >0:3639for p in self.depotPaths:3640if p.find("@") >=0or p.find("#") >=0:3641 bad_changesfile =True3642break3643if bad_changesfile:3644die("Option --changesfile is incompatible with revision specifiers")36453646 newPaths = []3647for p in self.depotPaths:3648if p.find("@") != -1:3649 atIdx = p.index("@")3650 self.changeRange = p[atIdx:]3651if self.changeRange =="@all":3652 self.changeRange =""3653elif','not in self.changeRange:3654 revision = self.changeRange3655 self.changeRange =""3656 p = p[:atIdx]3657elif p.find("#") != -1:3658 hashIdx = p.index("#")3659 revision = p[hashIdx:]3660 p = p[:hashIdx]3661elif self.previousDepotPaths == []:3662# pay attention to changesfile, if given, else import3663# the entire p4 tree at the head revision3664iflen(self.changesFile) ==0:3665 revision ="#head"36663667 p = re.sub("\.\.\.$","", p)3668if not p.endswith("/"):3669 p +="/"36703671 newPaths.append(p)36723673 self.depotPaths = newPaths36743675# --detect-branches may change this for each branch3676 self.branchPrefixes = self.depotPaths36773678 self.loadUserMapFromCache()3679 self.labels = {}3680if self.detectLabels:3681 self.getLabels();36823683if self.detectBranches:3684## FIXME - what's a P4 projectName ?3685 self.projectName = self.guessProjectName()36863687if self.hasOrigin:3688 self.getBranchMappingFromGitBranches()3689else:3690 self.getBranchMapping()3691if self.verbose:3692print"p4-git branches:%s"% self.p4BranchesInGit3693print"initial parents:%s"% self.initialParents3694for b in self.p4BranchesInGit:3695if b !="master":36963697## FIXME3698 b = b[len(self.projectName):]3699 self.createdBranches.add(b)37003701 self.openStreams()37023703if revision:3704 self.importHeadRevision(revision)3705else:3706 changes = []37073708iflen(self.changesFile) >0:3709 output =open(self.changesFile).readlines()3710 changeSet =set()3711for line in output:3712 changeSet.add(int(line))37133714for change in changeSet:3715 changes.append(change)37163717 changes.sort()3718else:3719# catch "git p4 sync" with no new branches, in a repo that3720# does not have any existing p4 branches3721iflen(args) ==0:3722if not self.p4BranchesInGit:3723die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")37243725# The default branch is master, unless --branch is used to3726# specify something else. Make sure it exists, or complain3727# nicely about how to use --branch.3728if not self.detectBranches:3729if notbranch_exists(self.branch):3730if branch_arg_given:3731die("Error: branch%sdoes not exist."% self.branch)3732else:3733die("Error: no branch%s; perhaps specify one with --branch."%3734 self.branch)37353736if self.verbose:3737print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3738 self.changeRange)3739 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)37403741iflen(self.maxChanges) >0:3742 changes = changes[:min(int(self.maxChanges),len(changes))]37433744iflen(changes) ==0:3745if not self.silent:3746print"No changes to import!"3747else:3748if not self.silent and not self.detectBranches:3749print"Import destination:%s"% self.branch37503751 self.updatedBranches =set()37523753if not self.detectBranches:3754if args:3755# start a new branch3756 self.initialParent =""3757else:3758# build on a previous revision3759 self.initialParent =parseRevision(self.branch)37603761 self.importChanges(changes)37623763if not self.silent:3764print""3765iflen(self.updatedBranches) >0:3766 sys.stdout.write("Updated branches: ")3767for b in self.updatedBranches:3768 sys.stdout.write("%s"% b)3769 sys.stdout.write("\n")37703771ifgitConfigBool("git-p4.importLabels"):3772 self.importLabels =True37733774if self.importLabels:3775 p4Labels =getP4Labels(self.depotPaths)3776 gitTags =getGitTags()37773778 missingP4Labels = p4Labels - gitTags3779 self.importP4Labels(self.gitStream, missingP4Labels)37803781 self.closeStreams()37823783# Cleanup temporary branches created during import3784if self.tempBranches != []:3785for branch in self.tempBranches:3786read_pipe("git update-ref -d%s"% branch)3787 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))37883789# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3790# a convenient shortcut refname "p4".3791if self.importIntoRemotes:3792 head_ref = self.refPrefix +"HEAD"3793if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3794system(["git","symbolic-ref", head_ref, self.branch])37953796return True37973798classP4Rebase(Command):3799def__init__(self):3800 Command.__init__(self)3801 self.options = [3802 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3803]3804 self.importLabels =False3805 self.description = ("Fetches the latest revision from perforce and "3806+"rebases the current work (branch) against it")38073808defrun(self, args):3809 sync =P4Sync()3810 sync.importLabels = self.importLabels3811 sync.run([])38123813return self.rebase()38143815defrebase(self):3816if os.system("git update-index --refresh") !=0:3817die("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.");3818iflen(read_pipe("git diff-index HEAD --")) >0:3819die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");38203821[upstream, settings] =findUpstreamBranchPoint()3822iflen(upstream) ==0:3823die("Cannot find upstream branchpoint for rebase")38243825# the branchpoint may be p4/foo~3, so strip off the parent3826 upstream = re.sub("~[0-9]+$","", upstream)38273828print"Rebasing the current branch onto%s"% upstream3829 oldHead =read_pipe("git rev-parse HEAD").strip()3830system("git rebase%s"% upstream)3831system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3832return True38333834classP4Clone(P4Sync):3835def__init__(self):3836 P4Sync.__init__(self)3837 self.description ="Creates a new git repository and imports from Perforce into it"3838 self.usage ="usage: %prog [options] //depot/path[@revRange]"3839 self.options += [3840 optparse.make_option("--destination", dest="cloneDestination",3841 action='store', default=None,3842help="where to leave result of the clone"),3843 optparse.make_option("--bare", dest="cloneBare",3844 action="store_true", default=False),3845]3846 self.cloneDestination =None3847 self.needsGit =False3848 self.cloneBare =False38493850defdefaultDestination(self, args):3851## TODO: use common prefix of args?3852 depotPath = args[0]3853 depotDir = re.sub("(@[^@]*)$","", depotPath)3854 depotDir = re.sub("(#[^#]*)$","", depotDir)3855 depotDir = re.sub(r"\.\.\.$","", depotDir)3856 depotDir = re.sub(r"/$","", depotDir)3857return os.path.split(depotDir)[1]38583859defrun(self, args):3860iflen(args) <1:3861return False38623863if self.keepRepoPath and not self.cloneDestination:3864 sys.stderr.write("Must specify destination for --keep-path\n")3865 sys.exit(1)38663867 depotPaths = args38683869if not self.cloneDestination andlen(depotPaths) >1:3870 self.cloneDestination = depotPaths[-1]3871 depotPaths = depotPaths[:-1]38723873 self.cloneExclude = ["/"+p for p in self.cloneExclude]3874for p in depotPaths:3875if not p.startswith("//"):3876 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3877return False38783879if not self.cloneDestination:3880 self.cloneDestination = self.defaultDestination(args)38813882print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)38833884if not os.path.exists(self.cloneDestination):3885 os.makedirs(self.cloneDestination)3886chdir(self.cloneDestination)38873888 init_cmd = ["git","init"]3889if self.cloneBare:3890 init_cmd.append("--bare")3891 retcode = subprocess.call(init_cmd)3892if retcode:3893raiseCalledProcessError(retcode, init_cmd)38943895if not P4Sync.run(self, depotPaths):3896return False38973898# create a master branch and check out a work tree3899ifgitBranchExists(self.branch):3900system(["git","branch","master", self.branch ])3901if not self.cloneBare:3902system(["git","checkout","-f"])3903else:3904print'Not checking out any branch, use ' \3905'"git checkout -q -b master <branch>"'39063907# auto-set this variable if invoked with --use-client-spec3908if self.useClientSpec_from_options:3909system("git config --bool git-p4.useclientspec true")39103911return True39123913classP4Unshelve(Command):3914def__init__(self):3915 Command.__init__(self)3916 self.options = []3917 self.origin ="HEAD"3918 self.description ="Unshelve a P4 changelist into a git commit"3919 self.usage ="usage: %prog [options] changelist"3920 self.options += [3921 optparse.make_option("--origin", dest="origin",3922help="Use this base revision instead of the default (%s)"% self.origin),3923]3924 self.verbose =False3925 self.noCommit =False3926 self.destbranch ="refs/remotes/p4/unshelved"39273928defrenameBranch(self, branch_name):3929""" Rename the existing branch to branch_name.N3930 """39313932 found =True3933for i inrange(0,1000):3934 backup_branch_name ="{0}.{1}".format(branch_name, i)3935if notgitBranchExists(backup_branch_name):3936gitUpdateRef(backup_branch_name, branch_name)# copy ref to backup3937gitDeleteRef(branch_name)3938 found =True3939print("renamed old unshelve branch to{0}".format(backup_branch_name))3940break39413942if not found:3943 sys.exit("gave up trying to rename existing branch{0}".format(sync.branch))39443945deffindLastP4Revision(self, starting_point):3946""" Look back from starting_point for the first commit created by git-p43947 to find the P4 commit we are based on, and the depot-paths.3948 """39493950for parent in(range(65535)):3951 log =extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))3952 settings =extractSettingsGitLog(log)3953if settings.has_key('change'):3954return settings39553956 sys.exit("could not find git-p4 commits in{0}".format(self.origin))39573958defrun(self, args):3959iflen(args) !=1:3960return False39613962if notgitBranchExists(self.origin):3963 sys.exit("origin branch{0}does not exist".format(self.origin))39643965 sync =P4Sync()3966 changes = args3967 sync.initialParent = self.origin39683969# use the first change in the list to construct the branch to unshelve into3970 change = changes[0]39713972# if the target branch already exists, rename it3973 branch_name ="{0}/{1}".format(self.destbranch, change)3974ifgitBranchExists(branch_name):3975 self.renameBranch(branch_name)3976 sync.branch = branch_name39773978 sync.verbose = self.verbose3979 sync.suppress_meta_comment =True39803981 settings = self.findLastP4Revision(self.origin)3982 origin_revision = settings['change']3983 sync.depotPaths = settings['depot-paths']3984 sync.branchPrefixes = sync.depotPaths39853986 sync.openStreams()3987 sync.loadUserMapFromCache()3988 sync.silent =True3989 sync.importChanges(changes, shelved=True, origin_revision=origin_revision)3990 sync.closeStreams()39913992print("unshelved changelist{0}into{1}".format(change, branch_name))39933994return True39953996classP4Branches(Command):3997def__init__(self):3998 Command.__init__(self)3999 self.options = [ ]4000 self.description = ("Shows the git branches that hold imports and their "4001+"corresponding perforce depot paths")4002 self.verbose =False40034004defrun(self, args):4005iforiginP4BranchesExist():4006createOrUpdateBranchesFromOrigin()40074008 cmdline ="git rev-parse --symbolic "4009 cmdline +=" --remotes"40104011for line inread_pipe_lines(cmdline):4012 line = line.strip()40134014if not line.startswith('p4/')or line =="p4/HEAD":4015continue4016 branch = line40174018 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)4019 settings =extractSettingsGitLog(log)40204021print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])4022return True40234024classHelpFormatter(optparse.IndentedHelpFormatter):4025def__init__(self):4026 optparse.IndentedHelpFormatter.__init__(self)40274028defformat_description(self, description):4029if description:4030return description +"\n"4031else:4032return""40334034defprintUsage(commands):4035print"usage:%s<command> [options]"% sys.argv[0]4036print""4037print"valid commands:%s"%", ".join(commands)4038print""4039print"Try%s<command> --help for command specific help."% sys.argv[0]4040print""40414042commands = {4043"debug": P4Debug,4044"submit": P4Submit,4045"commit": P4Submit,4046"sync": P4Sync,4047"rebase": P4Rebase,4048"clone": P4Clone,4049"rollback": P4RollBack,4050"branches": P4Branches,4051"unshelve": P4Unshelve,4052}405340544055defmain():4056iflen(sys.argv[1:]) ==0:4057printUsage(commands.keys())4058 sys.exit(2)40594060 cmdName = sys.argv[1]4061try:4062 klass = commands[cmdName]4063 cmd =klass()4064exceptKeyError:4065print"unknown command%s"% cmdName4066print""4067printUsage(commands.keys())4068 sys.exit(2)40694070 options = cmd.options4071 cmd.gitdir = os.environ.get("GIT_DIR",None)40724073 args = sys.argv[2:]40744075 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))4076if cmd.needsGit:4077 options.append(optparse.make_option("--git-dir", dest="gitdir"))40784079 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),4080 options,4081 description = cmd.description,4082 formatter =HelpFormatter())40834084(cmd, args) = parser.parse_args(sys.argv[2:], cmd);4085global verbose4086 verbose = cmd.verbose4087if cmd.needsGit:4088if cmd.gitdir ==None:4089 cmd.gitdir = os.path.abspath(".git")4090if notisValidGitDir(cmd.gitdir):4091# "rev-parse --git-dir" without arguments will try $PWD/.git4092 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()4093if os.path.exists(cmd.gitdir):4094 cdup =read_pipe("git rev-parse --show-cdup").strip()4095iflen(cdup) >0:4096chdir(cdup);40974098if notisValidGitDir(cmd.gitdir):4099ifisValidGitDir(cmd.gitdir +"/.git"):4100 cmd.gitdir +="/.git"4101else:4102die("fatal: cannot locate git repository at%s"% cmd.gitdir)41034104# so git commands invoked from the P4 workspace will succeed4105 os.environ["GIT_DIR"] = cmd.gitdir41064107if not cmd.run(args):4108 parser.print_help()4109 sys.exit(2)411041114112if __name__ =='__main__':4113main()