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 28 29try: 30from subprocess import CalledProcessError 31exceptImportError: 32# from python2.7:subprocess.py 33# Exception classes used by this module. 34classCalledProcessError(Exception): 35"""This exception is raised when a process run by check_call() returns 36 a non-zero exit status. The exit status will be stored in the 37 returncode attribute.""" 38def__init__(self, returncode, cmd): 39 self.returncode = returncode 40 self.cmd = cmd 41def__str__(self): 42return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 43 44verbose =False 45 46# Only labels/tags matching this will be imported/exported 47defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 48 49# Grab changes in blocks of this many revisions, unless otherwise requested 50defaultBlockSize =512 51 52defp4_build_cmd(cmd): 53"""Build a suitable p4 command line. 54 55 This consolidates building and returning a p4 command line into one 56 location. It means that hooking into the environment, or other configuration 57 can be done more easily. 58 """ 59 real_cmd = ["p4"] 60 61 user =gitConfig("git-p4.user") 62iflen(user) >0: 63 real_cmd += ["-u",user] 64 65 password =gitConfig("git-p4.password") 66iflen(password) >0: 67 real_cmd += ["-P", password] 68 69 port =gitConfig("git-p4.port") 70iflen(port) >0: 71 real_cmd += ["-p", port] 72 73 host =gitConfig("git-p4.host") 74iflen(host) >0: 75 real_cmd += ["-H", host] 76 77 client =gitConfig("git-p4.client") 78iflen(client) >0: 79 real_cmd += ["-c", client] 80 81 retries =gitConfigInt("git-p4.retries") 82if retries is None: 83# Perform 3 retries by default 84 retries =3 85if retries >0: 86# Provide a way to not pass this option by setting git-p4.retries to 0 87 real_cmd += ["-r",str(retries)] 88 89ifisinstance(cmd,basestring): 90 real_cmd =' '.join(real_cmd) +' '+ cmd 91else: 92 real_cmd += cmd 93return real_cmd 94 95defchdir(path, is_client_path=False): 96"""Do chdir to the given path, and set the PWD environment 97 variable for use by P4. It does not look at getcwd() output. 98 Since we're not using the shell, it is necessary to set the 99 PWD environment variable explicitly. 100 101 Normally, expand the path to force it to be absolute. This 102 addresses the use of relative path names inside P4 settings, 103 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 104 as given; it looks for .p4config using PWD. 105 106 If is_client_path, the path was handed to us directly by p4, 107 and may be a symbolic link. Do not call os.getcwd() in this 108 case, because it will cause p4 to think that PWD is not inside 109 the client path. 110 """ 111 112 os.chdir(path) 113if not is_client_path: 114 path = os.getcwd() 115 os.environ['PWD'] = path 116 117defcalcDiskFree(): 118"""Return free space in bytes on the disk of the given dirname.""" 119if platform.system() =='Windows': 120 free_bytes = ctypes.c_ulonglong(0) 121 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 122return free_bytes.value 123else: 124 st = os.statvfs(os.getcwd()) 125return st.f_bavail * st.f_frsize 126 127defdie(msg): 128if verbose: 129raiseException(msg) 130else: 131 sys.stderr.write(msg +"\n") 132 sys.exit(1) 133 134defwrite_pipe(c, stdin): 135if verbose: 136 sys.stderr.write('Writing pipe:%s\n'%str(c)) 137 138 expand =isinstance(c,basestring) 139 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 140 pipe = p.stdin 141 val = pipe.write(stdin) 142 pipe.close() 143if p.wait(): 144die('Command failed:%s'%str(c)) 145 146return val 147 148defp4_write_pipe(c, stdin): 149 real_cmd =p4_build_cmd(c) 150returnwrite_pipe(real_cmd, stdin) 151 152defread_pipe(c, ignore_error=False): 153if verbose: 154 sys.stderr.write('Reading pipe:%s\n'%str(c)) 155 156 expand =isinstance(c,basestring) 157 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 158(out, err) = p.communicate() 159if p.returncode !=0and not ignore_error: 160die('Command failed:%s\nError:%s'% (str(c), err)) 161return out 162 163defp4_read_pipe(c, ignore_error=False): 164 real_cmd =p4_build_cmd(c) 165returnread_pipe(real_cmd, ignore_error) 166 167defread_pipe_lines(c): 168if verbose: 169 sys.stderr.write('Reading pipe:%s\n'%str(c)) 170 171 expand =isinstance(c, basestring) 172 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 173 pipe = p.stdout 174 val = pipe.readlines() 175if pipe.close()or p.wait(): 176die('Command failed:%s'%str(c)) 177 178return val 179 180defp4_read_pipe_lines(c): 181"""Specifically invoke p4 on the command supplied. """ 182 real_cmd =p4_build_cmd(c) 183returnread_pipe_lines(real_cmd) 184 185defp4_has_command(cmd): 186"""Ask p4 for help on this command. If it returns an error, the 187 command does not exist in this version of p4.""" 188 real_cmd =p4_build_cmd(["help", cmd]) 189 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 190 stderr=subprocess.PIPE) 191 p.communicate() 192return p.returncode ==0 193 194defp4_has_move_command(): 195"""See if the move command exists, that it supports -k, and that 196 it has not been administratively disabled. The arguments 197 must be correct, but the filenames do not have to exist. Use 198 ones with wildcards so even if they exist, it will fail.""" 199 200if notp4_has_command("move"): 201return False 202 cmd =p4_build_cmd(["move","-k","@from","@to"]) 203 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 204(out, err) = p.communicate() 205# return code will be 1 in either case 206if err.find("Invalid option") >=0: 207return False 208if err.find("disabled") >=0: 209return False 210# assume it failed because @... was invalid changelist 211return True 212 213defsystem(cmd, ignore_error=False): 214 expand =isinstance(cmd,basestring) 215if verbose: 216 sys.stderr.write("executing%s\n"%str(cmd)) 217 retcode = subprocess.call(cmd, shell=expand) 218if retcode and not ignore_error: 219raiseCalledProcessError(retcode, cmd) 220 221return retcode 222 223defp4_system(cmd): 224"""Specifically invoke p4 as the system command. """ 225 real_cmd =p4_build_cmd(cmd) 226 expand =isinstance(real_cmd, basestring) 227 retcode = subprocess.call(real_cmd, shell=expand) 228if retcode: 229raiseCalledProcessError(retcode, real_cmd) 230 231_p4_version_string =None 232defp4_version_string(): 233"""Read the version string, showing just the last line, which 234 hopefully is the interesting version bit. 235 236 $ p4 -V 237 Perforce - The Fast Software Configuration Management System. 238 Copyright 1995-2011 Perforce Software. All rights reserved. 239 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 240 """ 241global _p4_version_string 242if not _p4_version_string: 243 a =p4_read_pipe_lines(["-V"]) 244 _p4_version_string = a[-1].rstrip() 245return _p4_version_string 246 247defp4_integrate(src, dest): 248p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 249 250defp4_sync(f, *options): 251p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 252 253defp4_add(f): 254# forcibly add file names with wildcards 255ifwildcard_present(f): 256p4_system(["add","-f", f]) 257else: 258p4_system(["add", f]) 259 260defp4_delete(f): 261p4_system(["delete",wildcard_encode(f)]) 262 263defp4_edit(f, *options): 264p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 265 266defp4_revert(f): 267p4_system(["revert",wildcard_encode(f)]) 268 269defp4_reopen(type, f): 270p4_system(["reopen","-t",type,wildcard_encode(f)]) 271 272defp4_move(src, dest): 273p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 274 275defp4_last_change(): 276 results =p4CmdList(["changes","-m","1"]) 277returnint(results[0]['change']) 278 279defp4_describe(change): 280"""Make sure it returns a valid result by checking for 281 the presence of field "time". Return a dict of the 282 results.""" 283 284 ds =p4CmdList(["describe","-s",str(change)]) 285iflen(ds) !=1: 286die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 287 288 d = ds[0] 289 290if"p4ExitCode"in d: 291die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 292str(d))) 293if"code"in d: 294if d["code"] =="error": 295die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 296 297if"time"not in d: 298die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 299 300return d 301 302# 303# Canonicalize the p4 type and return a tuple of the 304# base type, plus any modifiers. See "p4 help filetypes" 305# for a list and explanation. 306# 307defsplit_p4_type(p4type): 308 309 p4_filetypes_historical = { 310"ctempobj":"binary+Sw", 311"ctext":"text+C", 312"cxtext":"text+Cx", 313"ktext":"text+k", 314"kxtext":"text+kx", 315"ltext":"text+F", 316"tempobj":"binary+FSw", 317"ubinary":"binary+F", 318"uresource":"resource+F", 319"uxbinary":"binary+Fx", 320"xbinary":"binary+x", 321"xltext":"text+Fx", 322"xtempobj":"binary+Swx", 323"xtext":"text+x", 324"xunicode":"unicode+x", 325"xutf16":"utf16+x", 326} 327if p4type in p4_filetypes_historical: 328 p4type = p4_filetypes_historical[p4type] 329 mods ="" 330 s = p4type.split("+") 331 base = s[0] 332 mods ="" 333iflen(s) >1: 334 mods = s[1] 335return(base, mods) 336 337# 338# return the raw p4 type of a file (text, text+ko, etc) 339# 340defp4_type(f): 341 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 342return results[0]['headType'] 343 344# 345# Given a type base and modifier, return a regexp matching 346# the keywords that can be expanded in the file 347# 348defp4_keywords_regexp_for_type(base, type_mods): 349if base in("text","unicode","binary"): 350 kwords =None 351if"ko"in type_mods: 352 kwords ='Id|Header' 353elif"k"in type_mods: 354 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 355else: 356return None 357 pattern = r""" 358 \$ # Starts with a dollar, followed by... 359 (%s) # one of the keywords, followed by... 360 (:[^$\n]+)? # possibly an old expansion, followed by... 361 \$ # another dollar 362 """% kwords 363return pattern 364else: 365return None 366 367# 368# Given a file, return a regexp matching the possible 369# RCS keywords that will be expanded, or None for files 370# with kw expansion turned off. 371# 372defp4_keywords_regexp_for_file(file): 373if not os.path.exists(file): 374return None 375else: 376(type_base, type_mods) =split_p4_type(p4_type(file)) 377returnp4_keywords_regexp_for_type(type_base, type_mods) 378 379defsetP4ExecBit(file, mode): 380# Reopens an already open file and changes the execute bit to match 381# the execute bit setting in the passed in mode. 382 383 p4Type ="+x" 384 385if notisModeExec(mode): 386 p4Type =getP4OpenedType(file) 387 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 388 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 389if p4Type[-1] =="+": 390 p4Type = p4Type[0:-1] 391 392p4_reopen(p4Type,file) 393 394defgetP4OpenedType(file): 395# Returns the perforce file type for the given file. 396 397 result =p4_read_pipe(["opened",wildcard_encode(file)]) 398 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 399if match: 400return match.group(1) 401else: 402die("Could not determine file type for%s(result: '%s')"% (file, result)) 403 404# Return the set of all p4 labels 405defgetP4Labels(depotPaths): 406 labels =set() 407ifisinstance(depotPaths,basestring): 408 depotPaths = [depotPaths] 409 410for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 411 label = l['label'] 412 labels.add(label) 413 414return labels 415 416# Return the set of all git tags 417defgetGitTags(): 418 gitTags =set() 419for line inread_pipe_lines(["git","tag"]): 420 tag = line.strip() 421 gitTags.add(tag) 422return gitTags 423 424defdiffTreePattern(): 425# This is a simple generator for the diff tree regex pattern. This could be 426# a class variable if this and parseDiffTreeEntry were a part of a class. 427 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 428while True: 429yield pattern 430 431defparseDiffTreeEntry(entry): 432"""Parses a single diff tree entry into its component elements. 433 434 See git-diff-tree(1) manpage for details about the format of the diff 435 output. This method returns a dictionary with the following elements: 436 437 src_mode - The mode of the source file 438 dst_mode - The mode of the destination file 439 src_sha1 - The sha1 for the source file 440 dst_sha1 - The sha1 fr the destination file 441 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 442 status_score - The score for the status (applicable for 'C' and 'R' 443 statuses). This is None if there is no score. 444 src - The path for the source file. 445 dst - The path for the destination file. This is only present for 446 copy or renames. If it is not present, this is None. 447 448 If the pattern is not matched, None is returned.""" 449 450 match =diffTreePattern().next().match(entry) 451if match: 452return{ 453'src_mode': match.group(1), 454'dst_mode': match.group(2), 455'src_sha1': match.group(3), 456'dst_sha1': match.group(4), 457'status': match.group(5), 458'status_score': match.group(6), 459'src': match.group(7), 460'dst': match.group(10) 461} 462return None 463 464defisModeExec(mode): 465# Returns True if the given git mode represents an executable file, 466# otherwise False. 467return mode[-3:] =="755" 468 469defisModeExecChanged(src_mode, dst_mode): 470returnisModeExec(src_mode) !=isModeExec(dst_mode) 471 472defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 473 474ifisinstance(cmd,basestring): 475 cmd ="-G "+ cmd 476 expand =True 477else: 478 cmd = ["-G"] + cmd 479 expand =False 480 481 cmd =p4_build_cmd(cmd) 482if verbose: 483 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 484 485# Use a temporary file to avoid deadlocks without 486# subprocess.communicate(), which would put another copy 487# of stdout into memory. 488 stdin_file =None 489if stdin is not None: 490 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 491ifisinstance(stdin,basestring): 492 stdin_file.write(stdin) 493else: 494for i in stdin: 495 stdin_file.write(i +'\n') 496 stdin_file.flush() 497 stdin_file.seek(0) 498 499 p4 = subprocess.Popen(cmd, 500 shell=expand, 501 stdin=stdin_file, 502 stdout=subprocess.PIPE) 503 504 result = [] 505try: 506while True: 507 entry = marshal.load(p4.stdout) 508if cb is not None: 509cb(entry) 510else: 511 result.append(entry) 512exceptEOFError: 513pass 514 exitCode = p4.wait() 515if exitCode !=0: 516 entry = {} 517 entry["p4ExitCode"] = exitCode 518 result.append(entry) 519 520return result 521 522defp4Cmd(cmd): 523list=p4CmdList(cmd) 524 result = {} 525for entry inlist: 526 result.update(entry) 527return result; 528 529defp4Where(depotPath): 530if not depotPath.endswith("/"): 531 depotPath +="/" 532 depotPathLong = depotPath +"..." 533 outputList =p4CmdList(["where", depotPathLong]) 534 output =None 535for entry in outputList: 536if"depotFile"in entry: 537# Search for the base client side depot path, as long as it starts with the branch's P4 path. 538# The base path always ends with "/...". 539if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 540 output = entry 541break 542elif"data"in entry: 543 data = entry.get("data") 544 space = data.find(" ") 545if data[:space] == depotPath: 546 output = entry 547break 548if output ==None: 549return"" 550if output["code"] =="error": 551return"" 552 clientPath ="" 553if"path"in output: 554 clientPath = output.get("path") 555elif"data"in output: 556 data = output.get("data") 557 lastSpace = data.rfind(" ") 558 clientPath = data[lastSpace +1:] 559 560if clientPath.endswith("..."): 561 clientPath = clientPath[:-3] 562return clientPath 563 564defcurrentGitBranch(): 565 retcode =system(["git","symbolic-ref","-q","HEAD"], ignore_error=True) 566if retcode !=0: 567# on a detached head 568return None 569else: 570returnread_pipe(["git","name-rev","HEAD"]).split(" ")[1].strip() 571 572defisValidGitDir(path): 573if(os.path.exists(path +"/HEAD") 574and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 575return True; 576return False 577 578defparseRevision(ref): 579returnread_pipe("git rev-parse%s"% ref).strip() 580 581defbranchExists(ref): 582 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 583 ignore_error=True) 584returnlen(rev) >0 585 586defextractLogMessageFromGitCommit(commit): 587 logMessage ="" 588 589## fixme: title is first line of commit, not 1st paragraph. 590 foundTitle =False 591for log inread_pipe_lines("git cat-file commit%s"% commit): 592if not foundTitle: 593iflen(log) ==1: 594 foundTitle =True 595continue 596 597 logMessage += log 598return logMessage 599 600defextractSettingsGitLog(log): 601 values = {} 602for line in log.split("\n"): 603 line = line.strip() 604 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 605if not m: 606continue 607 608 assignments = m.group(1).split(':') 609for a in assignments: 610 vals = a.split('=') 611 key = vals[0].strip() 612 val = ('='.join(vals[1:])).strip() 613if val.endswith('\"')and val.startswith('"'): 614 val = val[1:-1] 615 616 values[key] = val 617 618 paths = values.get("depot-paths") 619if not paths: 620 paths = values.get("depot-path") 621if paths: 622 values['depot-paths'] = paths.split(',') 623return values 624 625defgitBranchExists(branch): 626 proc = subprocess.Popen(["git","rev-parse", branch], 627 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 628return proc.wait() ==0; 629 630_gitConfig = {} 631 632defgitConfig(key, typeSpecifier=None): 633if not _gitConfig.has_key(key): 634 cmd = ["git","config"] 635if typeSpecifier: 636 cmd += [ typeSpecifier ] 637 cmd += [ key ] 638 s =read_pipe(cmd, ignore_error=True) 639 _gitConfig[key] = s.strip() 640return _gitConfig[key] 641 642defgitConfigBool(key): 643"""Return a bool, using git config --bool. It is True only if the 644 variable is set to true, and False if set to false or not present 645 in the config.""" 646 647if not _gitConfig.has_key(key): 648 _gitConfig[key] =gitConfig(key,'--bool') =="true" 649return _gitConfig[key] 650 651defgitConfigInt(key): 652if not _gitConfig.has_key(key): 653 cmd = ["git","config","--int", key ] 654 s =read_pipe(cmd, ignore_error=True) 655 v = s.strip() 656try: 657 _gitConfig[key] =int(gitConfig(key,'--int')) 658exceptValueError: 659 _gitConfig[key] =None 660return _gitConfig[key] 661 662defgitConfigList(key): 663if not _gitConfig.has_key(key): 664 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 665 _gitConfig[key] = s.strip().split(os.linesep) 666if _gitConfig[key] == ['']: 667 _gitConfig[key] = [] 668return _gitConfig[key] 669 670defp4BranchesInGit(branchesAreInRemotes=True): 671"""Find all the branches whose names start with "p4/", looking 672 in remotes or heads as specified by the argument. Return 673 a dictionary of{ branch: revision }for each one found. 674 The branch names are the short names, without any 675 "p4/" prefix.""" 676 677 branches = {} 678 679 cmdline ="git rev-parse --symbolic " 680if branchesAreInRemotes: 681 cmdline +="--remotes" 682else: 683 cmdline +="--branches" 684 685for line inread_pipe_lines(cmdline): 686 line = line.strip() 687 688# only import to p4/ 689if not line.startswith('p4/'): 690continue 691# special symbolic ref to p4/master 692if line =="p4/HEAD": 693continue 694 695# strip off p4/ prefix 696 branch = line[len("p4/"):] 697 698 branches[branch] =parseRevision(line) 699 700return branches 701 702defbranch_exists(branch): 703"""Make sure that the given ref name really exists.""" 704 705 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 706 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 707 out, _ = p.communicate() 708if p.returncode: 709return False 710# expect exactly one line of output: the branch name 711return out.rstrip() == branch 712 713deffindUpstreamBranchPoint(head ="HEAD"): 714 branches =p4BranchesInGit() 715# map from depot-path to branch name 716 branchByDepotPath = {} 717for branch in branches.keys(): 718 tip = branches[branch] 719 log =extractLogMessageFromGitCommit(tip) 720 settings =extractSettingsGitLog(log) 721if settings.has_key("depot-paths"): 722 paths =",".join(settings["depot-paths"]) 723 branchByDepotPath[paths] ="remotes/p4/"+ branch 724 725 settings =None 726 parent =0 727while parent <65535: 728 commit = head +"~%s"% parent 729 log =extractLogMessageFromGitCommit(commit) 730 settings =extractSettingsGitLog(log) 731if settings.has_key("depot-paths"): 732 paths =",".join(settings["depot-paths"]) 733if branchByDepotPath.has_key(paths): 734return[branchByDepotPath[paths], settings] 735 736 parent = parent +1 737 738return["", settings] 739 740defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 741if not silent: 742print("Creating/updating branch(es) in%sbased on origin branch(es)" 743% localRefPrefix) 744 745 originPrefix ="origin/p4/" 746 747for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 748 line = line.strip() 749if(not line.startswith(originPrefix))or line.endswith("HEAD"): 750continue 751 752 headName = line[len(originPrefix):] 753 remoteHead = localRefPrefix + headName 754 originHead = line 755 756 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 757if(not original.has_key('depot-paths') 758or not original.has_key('change')): 759continue 760 761 update =False 762if notgitBranchExists(remoteHead): 763if verbose: 764print"creating%s"% remoteHead 765 update =True 766else: 767 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 768if settings.has_key('change') >0: 769if settings['depot-paths'] == original['depot-paths']: 770 originP4Change =int(original['change']) 771 p4Change =int(settings['change']) 772if originP4Change > p4Change: 773print("%s(%s) is newer than%s(%s). " 774"Updating p4 branch from origin." 775% (originHead, originP4Change, 776 remoteHead, p4Change)) 777 update =True 778else: 779print("Ignoring:%swas imported from%swhile " 780"%swas imported from%s" 781% (originHead,','.join(original['depot-paths']), 782 remoteHead,','.join(settings['depot-paths']))) 783 784if update: 785system("git update-ref%s %s"% (remoteHead, originHead)) 786 787deforiginP4BranchesExist(): 788returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 789 790 791defp4ParseNumericChangeRange(parts): 792 changeStart =int(parts[0][1:]) 793if parts[1] =='#head': 794 changeEnd =p4_last_change() 795else: 796 changeEnd =int(parts[1]) 797 798return(changeStart, changeEnd) 799 800defchooseBlockSize(blockSize): 801if blockSize: 802return blockSize 803else: 804return defaultBlockSize 805 806defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 807assert depotPaths 808 809# Parse the change range into start and end. Try to find integer 810# revision ranges as these can be broken up into blocks to avoid 811# hitting server-side limits (maxrows, maxscanresults). But if 812# that doesn't work, fall back to using the raw revision specifier 813# strings, without using block mode. 814 815if changeRange is None or changeRange =='': 816 changeStart =1 817 changeEnd =p4_last_change() 818 block_size =chooseBlockSize(requestedBlockSize) 819else: 820 parts = changeRange.split(',') 821assertlen(parts) ==2 822try: 823(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 824 block_size =chooseBlockSize(requestedBlockSize) 825except: 826 changeStart = parts[0][1:] 827 changeEnd = parts[1] 828if requestedBlockSize: 829die("cannot use --changes-block-size with non-numeric revisions") 830 block_size =None 831 832 changes = [] 833 834# Retrieve changes a block at a time, to prevent running 835# into a MaxResults/MaxScanRows error from the server. 836 837while True: 838 cmd = ['changes'] 839 840if block_size: 841 end =min(changeEnd, changeStart + block_size) 842 revisionRange ="%d,%d"% (changeStart, end) 843else: 844 revisionRange ="%s,%s"% (changeStart, changeEnd) 845 846for p in depotPaths: 847 cmd += ["%s...@%s"% (p, revisionRange)] 848 849# Insert changes in chronological order 850for line inreversed(p4_read_pipe_lines(cmd)): 851 changes.append(int(line.split(" ")[1])) 852 853if not block_size: 854break 855 856if end >= changeEnd: 857break 858 859 changeStart = end +1 860 861 changes =sorted(changes) 862return changes 863 864defp4PathStartsWith(path, prefix): 865# This method tries to remedy a potential mixed-case issue: 866# 867# If UserA adds //depot/DirA/file1 868# and UserB adds //depot/dira/file2 869# 870# we may or may not have a problem. If you have core.ignorecase=true, 871# we treat DirA and dira as the same directory 872ifgitConfigBool("core.ignorecase"): 873return path.lower().startswith(prefix.lower()) 874return path.startswith(prefix) 875 876defgetClientSpec(): 877"""Look at the p4 client spec, create a View() object that contains 878 all the mappings, and return it.""" 879 880 specList =p4CmdList("client -o") 881iflen(specList) !=1: 882die('Output from "client -o" is%dlines, expecting 1'% 883len(specList)) 884 885# dictionary of all client parameters 886 entry = specList[0] 887 888# the //client/ name 889 client_name = entry["Client"] 890 891# just the keys that start with "View" 892 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 893 894# hold this new View 895 view =View(client_name) 896 897# append the lines, in order, to the view 898for view_num inrange(len(view_keys)): 899 k ="View%d"% view_num 900if k not in view_keys: 901die("Expected view key%smissing"% k) 902 view.append(entry[k]) 903 904return view 905 906defgetClientRoot(): 907"""Grab the client directory.""" 908 909 output =p4CmdList("client -o") 910iflen(output) !=1: 911die('Output from "client -o" is%dlines, expecting 1'%len(output)) 912 913 entry = output[0] 914if"Root"not in entry: 915die('Client has no "Root"') 916 917return entry["Root"] 918 919# 920# P4 wildcards are not allowed in filenames. P4 complains 921# if you simply add them, but you can force it with "-f", in 922# which case it translates them into %xx encoding internally. 923# 924defwildcard_decode(path): 925# Search for and fix just these four characters. Do % last so 926# that fixing it does not inadvertently create new %-escapes. 927# Cannot have * in a filename in windows; untested as to 928# what p4 would do in such a case. 929if not platform.system() =="Windows": 930 path = path.replace("%2A","*") 931 path = path.replace("%23","#") \ 932.replace("%40","@") \ 933.replace("%25","%") 934return path 935 936defwildcard_encode(path): 937# do % first to avoid double-encoding the %s introduced here 938 path = path.replace("%","%25") \ 939.replace("*","%2A") \ 940.replace("#","%23") \ 941.replace("@","%40") 942return path 943 944defwildcard_present(path): 945 m = re.search("[*#@%]", path) 946return m is not None 947 948classLargeFileSystem(object): 949"""Base class for large file system support.""" 950 951def__init__(self, writeToGitStream): 952 self.largeFiles =set() 953 self.writeToGitStream = writeToGitStream 954 955defgeneratePointer(self, cloneDestination, contentFile): 956"""Return the content of a pointer file that is stored in Git instead of 957 the actual content.""" 958assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 959 960defpushFile(self, localLargeFile): 961"""Push the actual content which is not stored in the Git repository to 962 a server.""" 963assert False,"Method 'pushFile' required in "+ self.__class__.__name__ 964 965defhasLargeFileExtension(self, relPath): 966returnreduce( 967lambda a, b: a or b, 968[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')], 969False 970) 971 972defgenerateTempFile(self, contents): 973 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 974for d in contents: 975 contentFile.write(d) 976 contentFile.close() 977return contentFile.name 978 979defexceedsLargeFileThreshold(self, relPath, contents): 980ifgitConfigInt('git-p4.largeFileThreshold'): 981 contentsSize =sum(len(d)for d in contents) 982if contentsSize >gitConfigInt('git-p4.largeFileThreshold'): 983return True 984ifgitConfigInt('git-p4.largeFileCompressedThreshold'): 985 contentsSize =sum(len(d)for d in contents) 986if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'): 987return False 988 contentTempFile = self.generateTempFile(contents) 989 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 990 zf = zipfile.ZipFile(compressedContentFile.name, mode='w') 991 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED) 992 zf.close() 993 compressedContentsSize = zf.infolist()[0].compress_size 994 os.remove(contentTempFile) 995 os.remove(compressedContentFile.name) 996if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'): 997return True 998return False 9991000defaddLargeFile(self, relPath):1001 self.largeFiles.add(relPath)10021003defremoveLargeFile(self, relPath):1004 self.largeFiles.remove(relPath)10051006defisLargeFile(self, relPath):1007return relPath in self.largeFiles10081009defprocessContent(self, git_mode, relPath, contents):1010"""Processes the content of git fast import. This method decides if a1011 file is stored in the large file system and handles all necessary1012 steps."""1013if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1014 contentTempFile = self.generateTempFile(contents)1015(git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)10161017# Move temp file to final location in large file system1018 largeFileDir = os.path.dirname(localLargeFile)1019if not os.path.isdir(largeFileDir):1020 os.makedirs(largeFileDir)1021 shutil.move(contentTempFile, localLargeFile)1022 self.addLargeFile(relPath)1023ifgitConfigBool('git-p4.largeFilePush'):1024 self.pushFile(localLargeFile)1025if verbose:1026 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1027return(git_mode, contents)10281029classMockLFS(LargeFileSystem):1030"""Mock large file system for testing."""10311032defgeneratePointer(self, contentFile):1033"""The pointer content is the original content prefixed with "pointer-".1034 The local filename of the large file storage is derived from the file content.1035 """1036withopen(contentFile,'r')as f:1037 content =next(f)1038 gitMode ='100644'1039 pointerContents ='pointer-'+ content1040 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1041return(gitMode, pointerContents, localLargeFile)10421043defpushFile(self, localLargeFile):1044"""The remote filename of the large file storage is the same as the local1045 one but in a different directory.1046 """1047 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1048if not os.path.exists(remotePath):1049 os.makedirs(remotePath)1050 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10511052classGitLFS(LargeFileSystem):1053"""Git LFS as backend for the git-p4 large file system.1054 See https://git-lfs.github.com/ for details."""10551056def__init__(self, *args):1057 LargeFileSystem.__init__(self, *args)1058 self.baseGitAttributes = []10591060defgeneratePointer(self, contentFile):1061"""Generate a Git LFS pointer for the content. Return LFS Pointer file1062 mode and content which is stored in the Git repository instead of1063 the actual content. Return also the new location of the actual1064 content.1065 """1066 pointerProcess = subprocess.Popen(1067['git','lfs','pointer','--file='+ contentFile],1068 stdout=subprocess.PIPE1069)1070 pointerFile = pointerProcess.stdout.read()1071if pointerProcess.wait():1072 os.remove(contentFile)1073die('git-lfs pointer command failed. Did you install the extension?')10741075# Git LFS removed the preamble in the output of the 'pointer' command1076# starting from version 1.2.0. Check for the preamble here to support1077# earlier versions.1078# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431079if pointerFile.startswith('Git LFS pointer for'):1080 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)10811082 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1083 localLargeFile = os.path.join(1084 os.getcwd(),1085'.git','lfs','objects', oid[:2], oid[2:4],1086 oid,1087)1088# LFS Spec states that pointer files should not have the executable bit set.1089 gitMode ='100644'1090return(gitMode, pointerFile, localLargeFile)10911092defpushFile(self, localLargeFile):1093 uploadProcess = subprocess.Popen(1094['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1095)1096if uploadProcess.wait():1097die('git-lfs push command failed. Did you define a remote?')10981099defgenerateGitAttributes(self):1100return(1101 self.baseGitAttributes +1102[1103'\n',1104'#\n',1105'# Git LFS (see https://git-lfs.github.com/)\n',1106'#\n',1107] +1108['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1109for f insorted(gitConfigList('git-p4.largeFileExtensions'))1110] +1111['/'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1112for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1113]1114)11151116defaddLargeFile(self, relPath):1117 LargeFileSystem.addLargeFile(self, relPath)1118 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11191120defremoveLargeFile(self, relPath):1121 LargeFileSystem.removeLargeFile(self, relPath)1122 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11231124defprocessContent(self, git_mode, relPath, contents):1125if relPath =='.gitattributes':1126 self.baseGitAttributes = contents1127return(git_mode, self.generateGitAttributes())1128else:1129return LargeFileSystem.processContent(self, git_mode, relPath, contents)11301131class Command:1132def__init__(self):1133 self.usage ="usage: %prog [options]"1134 self.needsGit =True1135 self.verbose =False11361137class P4UserMap:1138def__init__(self):1139 self.userMapFromPerforceServer =False1140 self.myP4UserId =None11411142defp4UserId(self):1143if self.myP4UserId:1144return self.myP4UserId11451146 results =p4CmdList("user -o")1147for r in results:1148if r.has_key('User'):1149 self.myP4UserId = r['User']1150return r['User']1151die("Could not find your p4 user id")11521153defp4UserIsMe(self, p4User):1154# return True if the given p4 user is actually me1155 me = self.p4UserId()1156if not p4User or p4User != me:1157return False1158else:1159return True11601161defgetUserCacheFilename(self):1162 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1163return home +"/.gitp4-usercache.txt"11641165defgetUserMapFromPerforceServer(self):1166if self.userMapFromPerforceServer:1167return1168 self.users = {}1169 self.emails = {}11701171for output inp4CmdList("users"):1172if not output.has_key("User"):1173continue1174 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1175 self.emails[output["Email"]] = output["User"]11761177 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1178for mapUserConfig ingitConfigList("git-p4.mapUser"):1179 mapUser = mapUserConfigRegex.findall(mapUserConfig)1180if mapUser andlen(mapUser[0]) ==3:1181 user = mapUser[0][0]1182 fullname = mapUser[0][1]1183 email = mapUser[0][2]1184 self.users[user] = fullname +" <"+ email +">"1185 self.emails[email] = user11861187 s =''1188for(key, val)in self.users.items():1189 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))11901191open(self.getUserCacheFilename(),"wb").write(s)1192 self.userMapFromPerforceServer =True11931194defloadUserMapFromCache(self):1195 self.users = {}1196 self.userMapFromPerforceServer =False1197try:1198 cache =open(self.getUserCacheFilename(),"rb")1199 lines = cache.readlines()1200 cache.close()1201for line in lines:1202 entry = line.strip().split("\t")1203 self.users[entry[0]] = entry[1]1204exceptIOError:1205 self.getUserMapFromPerforceServer()12061207classP4Debug(Command):1208def__init__(self):1209 Command.__init__(self)1210 self.options = []1211 self.description ="A tool to debug the output of p4 -G."1212 self.needsGit =False12131214defrun(self, args):1215 j =01216for output inp4CmdList(args):1217print'Element:%d'% j1218 j +=11219print output1220return True12211222classP4RollBack(Command):1223def__init__(self):1224 Command.__init__(self)1225 self.options = [1226 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1227]1228 self.description ="A tool to debug the multi-branch import. Don't use :)"1229 self.rollbackLocalBranches =False12301231defrun(self, args):1232iflen(args) !=1:1233return False1234 maxChange =int(args[0])12351236if"p4ExitCode"inp4Cmd("changes -m 1"):1237die("Problems executing p4");12381239if self.rollbackLocalBranches:1240 refPrefix ="refs/heads/"1241 lines =read_pipe_lines("git rev-parse --symbolic --branches")1242else:1243 refPrefix ="refs/remotes/"1244 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12451246for line in lines:1247if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1248 line = line.strip()1249 ref = refPrefix + line1250 log =extractLogMessageFromGitCommit(ref)1251 settings =extractSettingsGitLog(log)12521253 depotPaths = settings['depot-paths']1254 change = settings['change']12551256 changed =False12571258iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1259for p in depotPaths]))) ==0:1260print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1261system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1262continue12631264while change andint(change) > maxChange:1265 changed =True1266if self.verbose:1267print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1268system("git update-ref%s\"%s^\""% (ref, ref))1269 log =extractLogMessageFromGitCommit(ref)1270 settings =extractSettingsGitLog(log)127112721273 depotPaths = settings['depot-paths']1274 change = settings['change']12751276if changed:1277print"%srewound to%s"% (ref, change)12781279return True12801281classP4Submit(Command, P4UserMap):12821283 conflict_behavior_choices = ("ask","skip","quit")12841285def__init__(self):1286 Command.__init__(self)1287 P4UserMap.__init__(self)1288 self.options = [1289 optparse.make_option("--origin", dest="origin"),1290 optparse.make_option("-M", dest="detectRenames", action="store_true"),1291# preserve the user, requires relevant p4 permissions1292 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1293 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1294 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1295 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1296 optparse.make_option("--conflict", dest="conflict_behavior",1297 choices=self.conflict_behavior_choices),1298 optparse.make_option("--branch", dest="branch"),1299]1300 self.description ="Submit changes from git to the perforce depot."1301 self.usage +=" [name of git branch to submit into perforce depot]"1302 self.origin =""1303 self.detectRenames =False1304 self.preserveUser =gitConfigBool("git-p4.preserveUser")1305 self.dry_run =False1306 self.prepare_p4_only =False1307 self.conflict_behavior =None1308 self.isWindows = (platform.system() =="Windows")1309 self.exportLabels =False1310 self.p4HasMoveCommand =p4_has_move_command()1311 self.branch =None13121313ifgitConfig('git-p4.largeFileSystem'):1314die("Large file system not supported for git-p4 submit command. Please remove it from config.")13151316defcheck(self):1317iflen(p4CmdList("opened ...")) >0:1318die("You have files opened with perforce! Close them before starting the sync.")13191320defseparate_jobs_from_description(self, message):1321"""Extract and return a possible Jobs field in the commit1322 message. It goes into a separate section in the p4 change1323 specification.13241325 A jobs line starts with "Jobs:" and looks like a new field1326 in a form. Values are white-space separated on the same1327 line or on following lines that start with a tab.13281329 This does not parse and extract the full git commit message1330 like a p4 form. It just sees the Jobs: line as a marker1331 to pass everything from then on directly into the p4 form,1332 but outside the description section.13331334 Return a tuple (stripped log message, jobs string)."""13351336 m = re.search(r'^Jobs:', message, re.MULTILINE)1337if m is None:1338return(message,None)13391340 jobtext = message[m.start():]1341 stripped_message = message[:m.start()].rstrip()1342return(stripped_message, jobtext)13431344defprepareLogMessage(self, template, message, jobs):1345"""Edits the template returned from "p4 change -o" to insert1346 the message in the Description field, and the jobs text in1347 the Jobs field."""1348 result =""13491350 inDescriptionSection =False13511352for line in template.split("\n"):1353if line.startswith("#"):1354 result += line +"\n"1355continue13561357if inDescriptionSection:1358if line.startswith("Files:")or line.startswith("Jobs:"):1359 inDescriptionSection =False1360# insert Jobs section1361if jobs:1362 result += jobs +"\n"1363else:1364continue1365else:1366if line.startswith("Description:"):1367 inDescriptionSection =True1368 line +="\n"1369for messageLine in message.split("\n"):1370 line +="\t"+ messageLine +"\n"13711372 result += line +"\n"13731374return result13751376defpatchRCSKeywords(self,file, pattern):1377# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1378(handle, outFileName) = tempfile.mkstemp(dir='.')1379try:1380 outFile = os.fdopen(handle,"w+")1381 inFile =open(file,"r")1382 regexp = re.compile(pattern, re.VERBOSE)1383for line in inFile.readlines():1384 line = regexp.sub(r'$\1$', line)1385 outFile.write(line)1386 inFile.close()1387 outFile.close()1388# Forcibly overwrite the original file1389 os.unlink(file)1390 shutil.move(outFileName,file)1391except:1392# cleanup our temporary file1393 os.unlink(outFileName)1394print"Failed to strip RCS keywords in%s"%file1395raise13961397print"Patched up RCS keywords in%s"%file13981399defp4UserForCommit(self,id):1400# Return the tuple (perforce user,git email) for a given git commit id1401 self.getUserMapFromPerforceServer()1402 gitEmail =read_pipe(["git","log","--max-count=1",1403"--format=%ae",id])1404 gitEmail = gitEmail.strip()1405if not self.emails.has_key(gitEmail):1406return(None,gitEmail)1407else:1408return(self.emails[gitEmail],gitEmail)14091410defcheckValidP4Users(self,commits):1411# check if any git authors cannot be mapped to p4 users1412foridin commits:1413(user,email) = self.p4UserForCommit(id)1414if not user:1415 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1416ifgitConfigBool("git-p4.allowMissingP4Users"):1417print"%s"% msg1418else:1419die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14201421deflastP4Changelist(self):1422# Get back the last changelist number submitted in this client spec. This1423# then gets used to patch up the username in the change. If the same1424# client spec is being used by multiple processes then this might go1425# wrong.1426 results =p4CmdList("client -o")# find the current client1427 client =None1428for r in results:1429if r.has_key('Client'):1430 client = r['Client']1431break1432if not client:1433die("could not get client spec")1434 results =p4CmdList(["changes","-c", client,"-m","1"])1435for r in results:1436if r.has_key('change'):1437return r['change']1438die("Could not get changelist number for last submit - cannot patch up user details")14391440defmodifyChangelistUser(self, changelist, newUser):1441# fixup the user field of a changelist after it has been submitted.1442 changes =p4CmdList("change -o%s"% changelist)1443iflen(changes) !=1:1444die("Bad output from p4 change modifying%sto user%s"%1445(changelist, newUser))14461447 c = changes[0]1448if c['User'] == newUser:return# nothing to do1449 c['User'] = newUser1450input= marshal.dumps(c)14511452 result =p4CmdList("change -f -i", stdin=input)1453for r in result:1454if r.has_key('code'):1455if r['code'] =='error':1456die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1457if r.has_key('data'):1458print("Updated user field for changelist%sto%s"% (changelist, newUser))1459return1460die("Could not modify user field of changelist%sto%s"% (changelist, newUser))14611462defcanChangeChangelists(self):1463# check to see if we have p4 admin or super-user permissions, either of1464# which are required to modify changelists.1465 results =p4CmdList(["protects", self.depotPath])1466for r in results:1467if r.has_key('perm'):1468if r['perm'] =='admin':1469return11470if r['perm'] =='super':1471return11472return014731474defprepareSubmitTemplate(self):1475"""Run "p4 change -o" to grab a change specification template.1476 This does not use "p4 -G", as it is nice to keep the submission1477 template in original order, since a human might edit it.14781479 Remove lines in the Files section that show changes to files1480 outside the depot path we're committing into."""14811482[upstream, settings] =findUpstreamBranchPoint()14831484 template =""1485 inFilesSection =False1486for line inp4_read_pipe_lines(['change','-o']):1487if line.endswith("\r\n"):1488 line = line[:-2] +"\n"1489if inFilesSection:1490if line.startswith("\t"):1491# path starts and ends with a tab1492 path = line[1:]1493 lastTab = path.rfind("\t")1494if lastTab != -1:1495 path = path[:lastTab]1496if settings.has_key('depot-paths'):1497if not[p for p in settings['depot-paths']1498ifp4PathStartsWith(path, p)]:1499continue1500else:1501if notp4PathStartsWith(path, self.depotPath):1502continue1503else:1504 inFilesSection =False1505else:1506if line.startswith("Files:"):1507 inFilesSection =True15081509 template += line15101511return template15121513defedit_template(self, template_file):1514"""Invoke the editor to let the user change the submission1515 message. Return true if okay to continue with the submit."""15161517# if configured to skip the editing part, just submit1518ifgitConfigBool("git-p4.skipSubmitEdit"):1519return True15201521# look at the modification time, to check later if the user saved1522# the file1523 mtime = os.stat(template_file).st_mtime15241525# invoke the editor1526if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1527 editor = os.environ.get("P4EDITOR")1528else:1529 editor =read_pipe("git var GIT_EDITOR").strip()1530system(["sh","-c", ('%s"$@"'% editor), editor, template_file])15311532# If the file was not saved, prompt to see if this patch should1533# be skipped. But skip this verification step if configured so.1534ifgitConfigBool("git-p4.skipSubmitEditCheck"):1535return True15361537# modification time updated means user saved the file1538if os.stat(template_file).st_mtime > mtime:1539return True15401541while True:1542 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1543if response =='y':1544return True1545if response =='n':1546return False15471548defget_diff_description(self, editedFiles, filesToAdd):1549# diff1550if os.environ.has_key("P4DIFF"):1551del(os.environ["P4DIFF"])1552 diff =""1553for editedFile in editedFiles:1554 diff +=p4_read_pipe(['diff','-du',1555wildcard_encode(editedFile)])15561557# new file diff1558 newdiff =""1559for newFile in filesToAdd:1560 newdiff +="==== new file ====\n"1561 newdiff +="--- /dev/null\n"1562 newdiff +="+++%s\n"% newFile1563 f =open(newFile,"r")1564for line in f.readlines():1565 newdiff +="+"+ line1566 f.close()15671568return(diff + newdiff).replace('\r\n','\n')15691570defapplyCommit(self,id):1571"""Apply one commit, return True if it succeeded."""15721573print"Applying",read_pipe(["git","show","-s",1574"--format=format:%h%s",id])15751576(p4User, gitEmail) = self.p4UserForCommit(id)15771578 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1579 filesToAdd =set()1580 filesToChangeType =set()1581 filesToDelete =set()1582 editedFiles =set()1583 pureRenameCopy =set()1584 filesToChangeExecBit = {}15851586for line in diff:1587 diff =parseDiffTreeEntry(line)1588 modifier = diff['status']1589 path = diff['src']1590if modifier =="M":1591p4_edit(path)1592ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1593 filesToChangeExecBit[path] = diff['dst_mode']1594 editedFiles.add(path)1595elif modifier =="A":1596 filesToAdd.add(path)1597 filesToChangeExecBit[path] = diff['dst_mode']1598if path in filesToDelete:1599 filesToDelete.remove(path)1600elif modifier =="D":1601 filesToDelete.add(path)1602if path in filesToAdd:1603 filesToAdd.remove(path)1604elif modifier =="C":1605 src, dest = diff['src'], diff['dst']1606p4_integrate(src, dest)1607 pureRenameCopy.add(dest)1608if diff['src_sha1'] != diff['dst_sha1']:1609p4_edit(dest)1610 pureRenameCopy.discard(dest)1611ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1612p4_edit(dest)1613 pureRenameCopy.discard(dest)1614 filesToChangeExecBit[dest] = diff['dst_mode']1615if self.isWindows:1616# turn off read-only attribute1617 os.chmod(dest, stat.S_IWRITE)1618 os.unlink(dest)1619 editedFiles.add(dest)1620elif modifier =="R":1621 src, dest = diff['src'], diff['dst']1622if self.p4HasMoveCommand:1623p4_edit(src)# src must be open before move1624p4_move(src, dest)# opens for (move/delete, move/add)1625else:1626p4_integrate(src, dest)1627if diff['src_sha1'] != diff['dst_sha1']:1628p4_edit(dest)1629else:1630 pureRenameCopy.add(dest)1631ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1632if not self.p4HasMoveCommand:1633p4_edit(dest)# with move: already open, writable1634 filesToChangeExecBit[dest] = diff['dst_mode']1635if not self.p4HasMoveCommand:1636if self.isWindows:1637 os.chmod(dest, stat.S_IWRITE)1638 os.unlink(dest)1639 filesToDelete.add(src)1640 editedFiles.add(dest)1641elif modifier =="T":1642 filesToChangeType.add(path)1643else:1644die("unknown modifier%sfor%s"% (modifier, path))16451646 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1647 patchcmd = diffcmd +" | git apply "1648 tryPatchCmd = patchcmd +"--check -"1649 applyPatchCmd = patchcmd +"--check --apply -"1650 patch_succeeded =True16511652if os.system(tryPatchCmd) !=0:1653 fixed_rcs_keywords =False1654 patch_succeeded =False1655print"Unfortunately applying the change failed!"16561657# Patch failed, maybe it's just RCS keyword woes. Look through1658# the patch to see if that's possible.1659ifgitConfigBool("git-p4.attemptRCSCleanup"):1660file=None1661 pattern =None1662 kwfiles = {}1663forfilein editedFiles | filesToDelete:1664# did this file's delta contain RCS keywords?1665 pattern =p4_keywords_regexp_for_file(file)16661667if pattern:1668# this file is a possibility...look for RCS keywords.1669 regexp = re.compile(pattern, re.VERBOSE)1670for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1671if regexp.search(line):1672if verbose:1673print"got keyword match on%sin%sin%s"% (pattern, line,file)1674 kwfiles[file] = pattern1675break16761677forfilein kwfiles:1678if verbose:1679print"zapping%swith%s"% (line,pattern)1680# File is being deleted, so not open in p4. Must1681# disable the read-only bit on windows.1682if self.isWindows andfilenot in editedFiles:1683 os.chmod(file, stat.S_IWRITE)1684 self.patchRCSKeywords(file, kwfiles[file])1685 fixed_rcs_keywords =True16861687if fixed_rcs_keywords:1688print"Retrying the patch with RCS keywords cleaned up"1689if os.system(tryPatchCmd) ==0:1690 patch_succeeded =True16911692if not patch_succeeded:1693for f in editedFiles:1694p4_revert(f)1695return False16961697#1698# Apply the patch for real, and do add/delete/+x handling.1699#1700system(applyPatchCmd)17011702for f in filesToChangeType:1703p4_edit(f,"-t","auto")1704for f in filesToAdd:1705p4_add(f)1706for f in filesToDelete:1707p4_revert(f)1708p4_delete(f)17091710# Set/clear executable bits1711for f in filesToChangeExecBit.keys():1712 mode = filesToChangeExecBit[f]1713setP4ExecBit(f, mode)17141715#1716# Build p4 change description, starting with the contents1717# of the git commit message.1718#1719 logMessage =extractLogMessageFromGitCommit(id)1720 logMessage = logMessage.strip()1721(logMessage, jobs) = self.separate_jobs_from_description(logMessage)17221723 template = self.prepareSubmitTemplate()1724 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)17251726if self.preserveUser:1727 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User17281729if self.checkAuthorship and not self.p4UserIsMe(p4User):1730 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1731 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1732 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"17331734 separatorLine ="######## everything below this line is just the diff #######\n"1735if not self.prepare_p4_only:1736 submitTemplate += separatorLine1737 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)17381739(handle, fileName) = tempfile.mkstemp()1740 tmpFile = os.fdopen(handle,"w+b")1741if self.isWindows:1742 submitTemplate = submitTemplate.replace("\n","\r\n")1743 tmpFile.write(submitTemplate)1744 tmpFile.close()17451746if self.prepare_p4_only:1747#1748# Leave the p4 tree prepared, and the submit template around1749# and let the user decide what to do next1750#1751print1752print"P4 workspace prepared for submission."1753print"To submit or revert, go to client workspace"1754print" "+ self.clientPath1755print1756print"To submit, use\"p4 submit\"to write a new description,"1757print"or\"p4 submit -i <%s\"to use the one prepared by" \1758"\"git p4\"."% fileName1759print"You can delete the file\"%s\"when finished."% fileName17601761if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1762print"To preserve change ownership by user%s, you must\n" \1763"do\"p4 change -f <change>\"after submitting and\n" \1764"edit the User field."1765if pureRenameCopy:1766print"After submitting, renamed files must be re-synced."1767print"Invoke\"p4 sync -f\"on each of these files:"1768for f in pureRenameCopy:1769print" "+ f17701771print1772print"To revert the changes, use\"p4 revert ...\", and delete"1773print"the submit template file\"%s\""% fileName1774if filesToAdd:1775print"Since the commit adds new files, they must be deleted:"1776for f in filesToAdd:1777print" "+ f1778print1779return True17801781#1782# Let the user edit the change description, then submit it.1783#1784 submitted =False17851786try:1787if self.edit_template(fileName):1788# read the edited message and submit1789 tmpFile =open(fileName,"rb")1790 message = tmpFile.read()1791 tmpFile.close()1792if self.isWindows:1793 message = message.replace("\r\n","\n")1794 submitTemplate = message[:message.index(separatorLine)]1795p4_write_pipe(['submit','-i'], submitTemplate)17961797if self.preserveUser:1798if p4User:1799# Get last changelist number. Cannot easily get it from1800# the submit command output as the output is1801# unmarshalled.1802 changelist = self.lastP4Changelist()1803 self.modifyChangelistUser(changelist, p4User)18041805# The rename/copy happened by applying a patch that created a1806# new file. This leaves it writable, which confuses p4.1807for f in pureRenameCopy:1808p4_sync(f,"-f")1809 submitted =True18101811finally:1812# skip this patch1813if not submitted:1814print"Submission cancelled, undoing p4 changes."1815for f in editedFiles:1816p4_revert(f)1817for f in filesToAdd:1818p4_revert(f)1819 os.remove(f)1820for f in filesToDelete:1821p4_revert(f)18221823 os.remove(fileName)1824return submitted18251826# Export git tags as p4 labels. Create a p4 label and then tag1827# with that.1828defexportGitTags(self, gitTags):1829 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1830iflen(validLabelRegexp) ==0:1831 validLabelRegexp = defaultLabelRegexp1832 m = re.compile(validLabelRegexp)18331834for name in gitTags:18351836if not m.match(name):1837if verbose:1838print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1839continue18401841# Get the p4 commit this corresponds to1842 logMessage =extractLogMessageFromGitCommit(name)1843 values =extractSettingsGitLog(logMessage)18441845if not values.has_key('change'):1846# a tag pointing to something not sent to p4; ignore1847if verbose:1848print"git tag%sdoes not give a p4 commit"% name1849continue1850else:1851 changelist = values['change']18521853# Get the tag details.1854 inHeader =True1855 isAnnotated =False1856 body = []1857for l inread_pipe_lines(["git","cat-file","-p", name]):1858 l = l.strip()1859if inHeader:1860if re.match(r'tag\s+', l):1861 isAnnotated =True1862elif re.match(r'\s*$', l):1863 inHeader =False1864continue1865else:1866 body.append(l)18671868if not isAnnotated:1869 body = ["lightweight tag imported by git p4\n"]18701871# Create the label - use the same view as the client spec we are using1872 clientSpec =getClientSpec()18731874 labelTemplate ="Label:%s\n"% name1875 labelTemplate +="Description:\n"1876for b in body:1877 labelTemplate +="\t"+ b +"\n"1878 labelTemplate +="View:\n"1879for depot_side in clientSpec.mappings:1880 labelTemplate +="\t%s\n"% depot_side18811882if self.dry_run:1883print"Would create p4 label%sfor tag"% name1884elif self.prepare_p4_only:1885print"Not creating p4 label%sfor tag due to option" \1886" --prepare-p4-only"% name1887else:1888p4_write_pipe(["label","-i"], labelTemplate)18891890# Use the label1891p4_system(["tag","-l", name] +1892["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])18931894if verbose:1895print"created p4 label for tag%s"% name18961897defrun(self, args):1898iflen(args) ==0:1899 self.master =currentGitBranch()1900eliflen(args) ==1:1901 self.master = args[0]1902if notbranchExists(self.master):1903die("Branch%sdoes not exist"% self.master)1904else:1905return False19061907if self.master:1908 allowSubmit =gitConfig("git-p4.allowSubmit")1909iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1910die("%sis not in git-p4.allowSubmit"% self.master)19111912[upstream, settings] =findUpstreamBranchPoint()1913 self.depotPath = settings['depot-paths'][0]1914iflen(self.origin) ==0:1915 self.origin = upstream19161917if self.preserveUser:1918if not self.canChangeChangelists():1919die("Cannot preserve user names without p4 super-user or admin permissions")19201921# if not set from the command line, try the config file1922if self.conflict_behavior is None:1923 val =gitConfig("git-p4.conflict")1924if val:1925if val not in self.conflict_behavior_choices:1926die("Invalid value '%s' for config git-p4.conflict"% val)1927else:1928 val ="ask"1929 self.conflict_behavior = val19301931if self.verbose:1932print"Origin branch is "+ self.origin19331934iflen(self.depotPath) ==0:1935print"Internal error: cannot locate perforce depot path from existing branches"1936 sys.exit(128)19371938 self.useClientSpec =False1939ifgitConfigBool("git-p4.useclientspec"):1940 self.useClientSpec =True1941if self.useClientSpec:1942 self.clientSpecDirs =getClientSpec()19431944# Check for the existence of P4 branches1945 branchesDetected = (len(p4BranchesInGit().keys()) >1)19461947if self.useClientSpec and not branchesDetected:1948# all files are relative to the client spec1949 self.clientPath =getClientRoot()1950else:1951 self.clientPath =p4Where(self.depotPath)19521953if self.clientPath =="":1954die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)19551956print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1957 self.oldWorkingDirectory = os.getcwd()19581959# ensure the clientPath exists1960 new_client_dir =False1961if not os.path.exists(self.clientPath):1962 new_client_dir =True1963 os.makedirs(self.clientPath)19641965chdir(self.clientPath, is_client_path=True)1966if self.dry_run:1967print"Would synchronize p4 checkout in%s"% self.clientPath1968else:1969print"Synchronizing p4 checkout..."1970if new_client_dir:1971# old one was destroyed, and maybe nobody told p41972p4_sync("...","-f")1973else:1974p4_sync("...")1975 self.check()19761977 commits = []1978if self.master:1979 commitish = self.master1980else:1981 commitish ='HEAD'19821983for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):1984 commits.append(line.strip())1985 commits.reverse()19861987if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1988 self.checkAuthorship =False1989else:1990 self.checkAuthorship =True19911992if self.preserveUser:1993 self.checkValidP4Users(commits)19941995#1996# Build up a set of options to be passed to diff when1997# submitting each commit to p4.1998#1999if self.detectRenames:2000# command-line -M arg2001 self.diffOpts ="-M"2002else:2003# If not explicitly set check the config variable2004 detectRenames =gitConfig("git-p4.detectRenames")20052006if detectRenames.lower() =="false"or detectRenames =="":2007 self.diffOpts =""2008elif detectRenames.lower() =="true":2009 self.diffOpts ="-M"2010else:2011 self.diffOpts ="-M%s"% detectRenames20122013# no command-line arg for -C or --find-copies-harder, just2014# config variables2015 detectCopies =gitConfig("git-p4.detectCopies")2016if detectCopies.lower() =="false"or detectCopies =="":2017pass2018elif detectCopies.lower() =="true":2019 self.diffOpts +=" -C"2020else:2021 self.diffOpts +=" -C%s"% detectCopies20222023ifgitConfigBool("git-p4.detectCopiesHarder"):2024 self.diffOpts +=" --find-copies-harder"20252026#2027# Apply the commits, one at a time. On failure, ask if should2028# continue to try the rest of the patches, or quit.2029#2030if self.dry_run:2031print"Would apply"2032 applied = []2033 last =len(commits) -12034for i, commit inenumerate(commits):2035if self.dry_run:2036print" ",read_pipe(["git","show","-s",2037"--format=format:%h%s", commit])2038 ok =True2039else:2040 ok = self.applyCommit(commit)2041if ok:2042 applied.append(commit)2043else:2044if self.prepare_p4_only and i < last:2045print"Processing only the first commit due to option" \2046" --prepare-p4-only"2047break2048if i < last:2049 quit =False2050while True:2051# prompt for what to do, or use the option/variable2052if self.conflict_behavior =="ask":2053print"What do you want to do?"2054 response =raw_input("[s]kip this commit but apply"2055" the rest, or [q]uit? ")2056if not response:2057continue2058elif self.conflict_behavior =="skip":2059 response ="s"2060elif self.conflict_behavior =="quit":2061 response ="q"2062else:2063die("Unknown conflict_behavior '%s'"%2064 self.conflict_behavior)20652066if response[0] =="s":2067print"Skipping this commit, but applying the rest"2068break2069if response[0] =="q":2070print"Quitting"2071 quit =True2072break2073if quit:2074break20752076chdir(self.oldWorkingDirectory)20772078if self.dry_run:2079pass2080elif self.prepare_p4_only:2081pass2082eliflen(commits) ==len(applied):2083print"All commits applied!"20842085 sync =P4Sync()2086if self.branch:2087 sync.branch = self.branch2088 sync.run([])20892090 rebase =P4Rebase()2091 rebase.rebase()20922093else:2094iflen(applied) ==0:2095print"No commits applied."2096else:2097print"Applied only the commits marked with '*':"2098for c in commits:2099if c in applied:2100 star ="*"2101else:2102 star =" "2103print star,read_pipe(["git","show","-s",2104"--format=format:%h%s", c])2105print"You will have to do 'git p4 sync' and rebase."21062107ifgitConfigBool("git-p4.exportLabels"):2108 self.exportLabels =True21092110if self.exportLabels:2111 p4Labels =getP4Labels(self.depotPath)2112 gitTags =getGitTags()21132114 missingGitTags = gitTags - p4Labels2115 self.exportGitTags(missingGitTags)21162117# exit with error unless everything applied perfectly2118iflen(commits) !=len(applied):2119 sys.exit(1)21202121return True21222123classView(object):2124"""Represent a p4 view ("p4 help views"), and map files in a2125 repo according to the view."""21262127def__init__(self, client_name):2128 self.mappings = []2129 self.client_prefix ="//%s/"% client_name2130# cache results of "p4 where" to lookup client file locations2131 self.client_spec_path_cache = {}21322133defappend(self, view_line):2134"""Parse a view line, splitting it into depot and client2135 sides. Append to self.mappings, preserving order. This2136 is only needed for tag creation."""21372138# Split the view line into exactly two words. P4 enforces2139# structure on these lines that simplifies this quite a bit.2140#2141# Either or both words may be double-quoted.2142# Single quotes do not matter.2143# Double-quote marks cannot occur inside the words.2144# A + or - prefix is also inside the quotes.2145# There are no quotes unless they contain a space.2146# The line is already white-space stripped.2147# The two words are separated by a single space.2148#2149if view_line[0] =='"':2150# First word is double quoted. Find its end.2151 close_quote_index = view_line.find('"',1)2152if close_quote_index <=0:2153die("No first-word closing quote found:%s"% view_line)2154 depot_side = view_line[1:close_quote_index]2155# skip closing quote and space2156 rhs_index = close_quote_index +1+12157else:2158 space_index = view_line.find(" ")2159if space_index <=0:2160die("No word-splitting space found:%s"% view_line)2161 depot_side = view_line[0:space_index]2162 rhs_index = space_index +121632164# prefix + means overlay on previous mapping2165if depot_side.startswith("+"):2166 depot_side = depot_side[1:]21672168# prefix - means exclude this path, leave out of mappings2169 exclude =False2170if depot_side.startswith("-"):2171 exclude =True2172 depot_side = depot_side[1:]21732174if not exclude:2175 self.mappings.append(depot_side)21762177defconvert_client_path(self, clientFile):2178# chop off //client/ part to make it relative2179if not clientFile.startswith(self.client_prefix):2180die("No prefix '%s' on clientFile '%s'"%2181(self.client_prefix, clientFile))2182return clientFile[len(self.client_prefix):]21832184defupdate_client_spec_path_cache(self, files):2185""" Caching file paths by "p4 where" batch query """21862187# List depot file paths exclude that already cached2188 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]21892190iflen(fileArgs) ==0:2191return# All files in cache21922193 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2194for res in where_result:2195if"code"in res and res["code"] =="error":2196# assume error is "... file(s) not in client view"2197continue2198if"clientFile"not in res:2199die("No clientFile in 'p4 where' output")2200if"unmap"in res:2201# it will list all of them, but only one not unmap-ped2202continue2203ifgitConfigBool("core.ignorecase"):2204 res['depotFile'] = res['depotFile'].lower()2205 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])22062207# not found files or unmap files set to ""2208for depotFile in fileArgs:2209ifgitConfigBool("core.ignorecase"):2210 depotFile = depotFile.lower()2211if depotFile not in self.client_spec_path_cache:2212 self.client_spec_path_cache[depotFile] =""22132214defmap_in_client(self, depot_path):2215"""Return the relative location in the client where this2216 depot file should live. Returns "" if the file should2217 not be mapped in the client."""22182219ifgitConfigBool("core.ignorecase"):2220 depot_path = depot_path.lower()22212222if depot_path in self.client_spec_path_cache:2223return self.client_spec_path_cache[depot_path]22242225die("Error:%sis not found in client spec path"% depot_path )2226return""22272228classP4Sync(Command, P4UserMap):2229 delete_actions = ("delete","move/delete","purge")22302231def__init__(self):2232 Command.__init__(self)2233 P4UserMap.__init__(self)2234 self.options = [2235 optparse.make_option("--branch", dest="branch"),2236 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2237 optparse.make_option("--changesfile", dest="changesFile"),2238 optparse.make_option("--silent", dest="silent", action="store_true"),2239 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2240 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2241 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2242help="Import into refs/heads/ , not refs/remotes"),2243 optparse.make_option("--max-changes", dest="maxChanges",2244help="Maximum number of changes to import"),2245 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2246help="Internal block size to use when iteratively calling p4 changes"),2247 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2248help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2249 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2250help="Only sync files that are included in the Perforce Client Spec"),2251 optparse.make_option("-/", dest="cloneExclude",2252 action="append",type="string",2253help="exclude depot path"),2254]2255 self.description ="""Imports from Perforce into a git repository.\n2256 example:2257 //depot/my/project/ -- to import the current head2258 //depot/my/project/@all -- to import everything2259 //depot/my/project/@1,6 -- to import only from revision 1 to 622602261 (a ... is not needed in the path p4 specification, it's added implicitly)"""22622263 self.usage +=" //depot/path[@revRange]"2264 self.silent =False2265 self.createdBranches =set()2266 self.committedChanges =set()2267 self.branch =""2268 self.detectBranches =False2269 self.detectLabels =False2270 self.importLabels =False2271 self.changesFile =""2272 self.syncWithOrigin =True2273 self.importIntoRemotes =True2274 self.maxChanges =""2275 self.changes_block_size =None2276 self.keepRepoPath =False2277 self.depotPaths =None2278 self.p4BranchesInGit = []2279 self.cloneExclude = []2280 self.useClientSpec =False2281 self.useClientSpec_from_options =False2282 self.clientSpecDirs =None2283 self.tempBranches = []2284 self.tempBranchLocation ="refs/git-p4-tmp"2285 self.largeFileSystem =None22862287ifgitConfig('git-p4.largeFileSystem'):2288 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2289 self.largeFileSystem =largeFileSystemConstructor(2290lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2291)22922293ifgitConfig("git-p4.syncFromOrigin") =="false":2294 self.syncWithOrigin =False22952296# This is required for the "append" cloneExclude action2297defensure_value(self, attr, value):2298if nothasattr(self, attr)orgetattr(self, attr)is None:2299setattr(self, attr, value)2300returngetattr(self, attr)23012302# Force a checkpoint in fast-import and wait for it to finish2303defcheckpoint(self):2304 self.gitStream.write("checkpoint\n\n")2305 self.gitStream.write("progress checkpoint\n\n")2306 out = self.gitOutput.readline()2307if self.verbose:2308print"checkpoint finished: "+ out23092310defextractFilesFromCommit(self, commit):2311 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2312for path in self.cloneExclude]2313 files = []2314 fnum =02315while commit.has_key("depotFile%s"% fnum):2316 path = commit["depotFile%s"% fnum]23172318if[p for p in self.cloneExclude2319ifp4PathStartsWith(path, p)]:2320 found =False2321else:2322 found = [p for p in self.depotPaths2323ifp4PathStartsWith(path, p)]2324if not found:2325 fnum = fnum +12326continue23272328file= {}2329file["path"] = path2330file["rev"] = commit["rev%s"% fnum]2331file["action"] = commit["action%s"% fnum]2332file["type"] = commit["type%s"% fnum]2333 files.append(file)2334 fnum = fnum +12335return files23362337defextractJobsFromCommit(self, commit):2338 jobs = []2339 jnum =02340while commit.has_key("job%s"% jnum):2341 job = commit["job%s"% jnum]2342 jobs.append(job)2343 jnum = jnum +12344return jobs23452346defstripRepoPath(self, path, prefixes):2347"""When streaming files, this is called to map a p4 depot path2348 to where it should go in git. The prefixes are either2349 self.depotPaths, or self.branchPrefixes in the case of2350 branch detection."""23512352if self.useClientSpec:2353# branch detection moves files up a level (the branch name)2354# from what client spec interpretation gives2355 path = self.clientSpecDirs.map_in_client(path)2356if self.detectBranches:2357for b in self.knownBranches:2358if path.startswith(b +"/"):2359 path = path[len(b)+1:]23602361elif self.keepRepoPath:2362# Preserve everything in relative path name except leading2363# //depot/; just look at first prefix as they all should2364# be in the same depot.2365 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2366ifp4PathStartsWith(path, depot):2367 path = path[len(depot):]23682369else:2370for p in prefixes:2371ifp4PathStartsWith(path, p):2372 path = path[len(p):]2373break23742375 path =wildcard_decode(path)2376return path23772378defsplitFilesIntoBranches(self, commit):2379"""Look at each depotFile in the commit to figure out to what2380 branch it belongs."""23812382if self.clientSpecDirs:2383 files = self.extractFilesFromCommit(commit)2384 self.clientSpecDirs.update_client_spec_path_cache(files)23852386 branches = {}2387 fnum =02388while commit.has_key("depotFile%s"% fnum):2389 path = commit["depotFile%s"% fnum]2390 found = [p for p in self.depotPaths2391ifp4PathStartsWith(path, p)]2392if not found:2393 fnum = fnum +12394continue23952396file= {}2397file["path"] = path2398file["rev"] = commit["rev%s"% fnum]2399file["action"] = commit["action%s"% fnum]2400file["type"] = commit["type%s"% fnum]2401 fnum = fnum +124022403# start with the full relative path where this file would2404# go in a p4 client2405if self.useClientSpec:2406 relPath = self.clientSpecDirs.map_in_client(path)2407else:2408 relPath = self.stripRepoPath(path, self.depotPaths)24092410for branch in self.knownBranches.keys():2411# add a trailing slash so that a commit into qt/4.2foo2412# doesn't end up in qt/4.2, e.g.2413if relPath.startswith(branch +"/"):2414if branch not in branches:2415 branches[branch] = []2416 branches[branch].append(file)2417break24182419return branches24202421defwriteToGitStream(self, gitMode, relPath, contents):2422 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2423 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2424for d in contents:2425 self.gitStream.write(d)2426 self.gitStream.write('\n')24272428# output one file from the P4 stream2429# - helper for streamP4Files24302431defstreamOneP4File(self,file, contents):2432 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2433if verbose:2434 size =int(self.stream_file['fileSize'])2435 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2436 sys.stdout.flush()24372438(type_base, type_mods) =split_p4_type(file["type"])24392440 git_mode ="100644"2441if"x"in type_mods:2442 git_mode ="100755"2443if type_base =="symlink":2444 git_mode ="120000"2445# p4 print on a symlink sometimes contains "target\n";2446# if it does, remove the newline2447 data =''.join(contents)2448if not data:2449# Some version of p4 allowed creating a symlink that pointed2450# to nothing. This causes p4 errors when checking out such2451# a change, and errors here too. Work around it by ignoring2452# the bad symlink; hopefully a future change fixes it.2453print"\nIgnoring empty symlink in%s"%file['depotFile']2454return2455elif data[-1] =='\n':2456 contents = [data[:-1]]2457else:2458 contents = [data]24592460if type_base =="utf16":2461# p4 delivers different text in the python output to -G2462# than it does when using "print -o", or normal p4 client2463# operations. utf16 is converted to ascii or utf8, perhaps.2464# But ascii text saved as -t utf16 is completely mangled.2465# Invoke print -o to get the real contents.2466#2467# On windows, the newlines will always be mangled by print, so put2468# them back too. This is not needed to the cygwin windows version,2469# just the native "NT" type.2470#2471try:2472 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2473exceptExceptionas e:2474if'Translation of file content failed'instr(e):2475 type_base ='binary'2476else:2477raise e2478else:2479ifp4_version_string().find('/NT') >=0:2480 text = text.replace('\r\n','\n')2481 contents = [ text ]24822483if type_base =="apple":2484# Apple filetype files will be streamed as a concatenation of2485# its appledouble header and the contents. This is useless2486# on both macs and non-macs. If using "print -q -o xx", it2487# will create "xx" with the data, and "%xx" with the header.2488# This is also not very useful.2489#2490# Ideally, someday, this script can learn how to generate2491# appledouble files directly and import those to git, but2492# non-mac machines can never find a use for apple filetype.2493print"\nIgnoring apple filetype file%s"%file['depotFile']2494return24952496# Note that we do not try to de-mangle keywords on utf16 files,2497# even though in theory somebody may want that.2498 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2499if pattern:2500 regexp = re.compile(pattern, re.VERBOSE)2501 text =''.join(contents)2502 text = regexp.sub(r'$\1$', text)2503 contents = [ text ]25042505try:2506 relPath.decode('ascii')2507except:2508 encoding ='utf8'2509ifgitConfig('git-p4.pathEncoding'):2510 encoding =gitConfig('git-p4.pathEncoding')2511 relPath = relPath.decode(encoding,'replace').encode('utf8','replace')2512if self.verbose:2513print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, relPath)25142515if self.largeFileSystem:2516(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)25172518 self.writeToGitStream(git_mode, relPath, contents)25192520defstreamOneP4Deletion(self,file):2521 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2522if verbose:2523 sys.stdout.write("delete%s\n"% relPath)2524 sys.stdout.flush()2525 self.gitStream.write("D%s\n"% relPath)25262527if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2528 self.largeFileSystem.removeLargeFile(relPath)25292530# handle another chunk of streaming data2531defstreamP4FilesCb(self, marshalled):25322533# catch p4 errors and complain2534 err =None2535if"code"in marshalled:2536if marshalled["code"] =="error":2537if"data"in marshalled:2538 err = marshalled["data"].rstrip()25392540if not err and'fileSize'in self.stream_file:2541 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2542if required_bytes >0:2543 err ='Not enough space left on%s! Free at least%iMB.'% (2544 os.getcwd(), required_bytes/1024/10242545)25462547if err:2548 f =None2549if self.stream_have_file_info:2550if"depotFile"in self.stream_file:2551 f = self.stream_file["depotFile"]2552# force a failure in fast-import, else an empty2553# commit will be made2554 self.gitStream.write("\n")2555 self.gitStream.write("die-now\n")2556 self.gitStream.close()2557# ignore errors, but make sure it exits first2558 self.importProcess.wait()2559if f:2560die("Error from p4 print for%s:%s"% (f, err))2561else:2562die("Error from p4 print:%s"% err)25632564if marshalled.has_key('depotFile')and self.stream_have_file_info:2565# start of a new file - output the old one first2566 self.streamOneP4File(self.stream_file, self.stream_contents)2567 self.stream_file = {}2568 self.stream_contents = []2569 self.stream_have_file_info =False25702571# pick up the new file information... for the2572# 'data' field we need to append to our array2573for k in marshalled.keys():2574if k =='data':2575if'streamContentSize'not in self.stream_file:2576 self.stream_file['streamContentSize'] =02577 self.stream_file['streamContentSize'] +=len(marshalled['data'])2578 self.stream_contents.append(marshalled['data'])2579else:2580 self.stream_file[k] = marshalled[k]25812582if(verbose and2583'streamContentSize'in self.stream_file and2584'fileSize'in self.stream_file and2585'depotFile'in self.stream_file):2586 size =int(self.stream_file["fileSize"])2587if size >0:2588 progress =100*self.stream_file['streamContentSize']/size2589 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2590 sys.stdout.flush()25912592 self.stream_have_file_info =True25932594# Stream directly from "p4 files" into "git fast-import"2595defstreamP4Files(self, files):2596 filesForCommit = []2597 filesToRead = []2598 filesToDelete = []25992600for f in files:2601 filesForCommit.append(f)2602if f['action']in self.delete_actions:2603 filesToDelete.append(f)2604else:2605 filesToRead.append(f)26062607# deleted files...2608for f in filesToDelete:2609 self.streamOneP4Deletion(f)26102611iflen(filesToRead) >0:2612 self.stream_file = {}2613 self.stream_contents = []2614 self.stream_have_file_info =False26152616# curry self argument2617defstreamP4FilesCbSelf(entry):2618 self.streamP4FilesCb(entry)26192620 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]26212622p4CmdList(["-x","-","print"],2623 stdin=fileArgs,2624 cb=streamP4FilesCbSelf)26252626# do the last chunk2627if self.stream_file.has_key('depotFile'):2628 self.streamOneP4File(self.stream_file, self.stream_contents)26292630defmake_email(self, userid):2631if userid in self.users:2632return self.users[userid]2633else:2634return"%s<a@b>"% userid26352636defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2637""" Stream a p4 tag.2638 commit is either a git commit, or a fast-import mark, ":<p4commit>"2639 """26402641if verbose:2642print"writing tag%sfor commit%s"% (labelName, commit)2643 gitStream.write("tag%s\n"% labelName)2644 gitStream.write("from%s\n"% commit)26452646if labelDetails.has_key('Owner'):2647 owner = labelDetails["Owner"]2648else:2649 owner =None26502651# Try to use the owner of the p4 label, or failing that,2652# the current p4 user id.2653if owner:2654 email = self.make_email(owner)2655else:2656 email = self.make_email(self.p4UserId())2657 tagger ="%s %s %s"% (email, epoch, self.tz)26582659 gitStream.write("tagger%s\n"% tagger)26602661print"labelDetails=",labelDetails2662if labelDetails.has_key('Description'):2663 description = labelDetails['Description']2664else:2665 description ='Label from git p4'26662667 gitStream.write("data%d\n"%len(description))2668 gitStream.write(description)2669 gitStream.write("\n")26702671definClientSpec(self, path):2672if not self.clientSpecDirs:2673return True2674 inClientSpec = self.clientSpecDirs.map_in_client(path)2675if not inClientSpec and self.verbose:2676print('Ignoring file outside of client spec:{0}'.format(path))2677return inClientSpec26782679defhasBranchPrefix(self, path):2680if not self.branchPrefixes:2681return True2682 hasPrefix = [p for p in self.branchPrefixes2683ifp4PathStartsWith(path, p)]2684if not hasPrefix and self.verbose:2685print('Ignoring file outside of prefix:{0}'.format(path))2686return hasPrefix26872688defcommit(self, details, files, branch, parent =""):2689 epoch = details["time"]2690 author = details["user"]2691 jobs = self.extractJobsFromCommit(details)26922693if self.verbose:2694print('commit into{0}'.format(branch))26952696if self.clientSpecDirs:2697 self.clientSpecDirs.update_client_spec_path_cache(files)26982699 files = [f for f in files2700if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]27012702if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2703print('Ignoring revision{0}as it would produce an empty commit.'2704.format(details['change']))2705return27062707 self.gitStream.write("commit%s\n"% branch)2708 self.gitStream.write("mark :%s\n"% details["change"])2709 self.committedChanges.add(int(details["change"]))2710 committer =""2711if author not in self.users:2712 self.getUserMapFromPerforceServer()2713 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)27142715 self.gitStream.write("committer%s\n"% committer)27162717 self.gitStream.write("data <<EOT\n")2718 self.gitStream.write(details["desc"])2719iflen(jobs) >0:2720 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2721 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2722(','.join(self.branchPrefixes), details["change"]))2723iflen(details['options']) >0:2724 self.gitStream.write(": options =%s"% details['options'])2725 self.gitStream.write("]\nEOT\n\n")27262727iflen(parent) >0:2728if self.verbose:2729print"parent%s"% parent2730 self.gitStream.write("from%s\n"% parent)27312732 self.streamP4Files(files)2733 self.gitStream.write("\n")27342735 change =int(details["change"])27362737if self.labels.has_key(change):2738 label = self.labels[change]2739 labelDetails = label[0]2740 labelRevisions = label[1]2741if self.verbose:2742print"Change%sis labelled%s"% (change, labelDetails)27432744 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2745for p in self.branchPrefixes])27462747iflen(files) ==len(labelRevisions):27482749 cleanedFiles = {}2750for info in files:2751if info["action"]in self.delete_actions:2752continue2753 cleanedFiles[info["depotFile"]] = info["rev"]27542755if cleanedFiles == labelRevisions:2756 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)27572758else:2759if not self.silent:2760print("Tag%sdoes not match with change%s: files do not match."2761% (labelDetails["label"], change))27622763else:2764if not self.silent:2765print("Tag%sdoes not match with change%s: file count is different."2766% (labelDetails["label"], change))27672768# Build a dictionary of changelists and labels, for "detect-labels" option.2769defgetLabels(self):2770 self.labels = {}27712772 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2773iflen(l) >0and not self.silent:2774print"Finding files belonging to labels in%s"% `self.depotPaths`27752776for output in l:2777 label = output["label"]2778 revisions = {}2779 newestChange =02780if self.verbose:2781print"Querying files for label%s"% label2782forfileinp4CmdList(["files"] +2783["%s...@%s"% (p, label)2784for p in self.depotPaths]):2785 revisions[file["depotFile"]] =file["rev"]2786 change =int(file["change"])2787if change > newestChange:2788 newestChange = change27892790 self.labels[newestChange] = [output, revisions]27912792if self.verbose:2793print"Label changes:%s"% self.labels.keys()27942795# Import p4 labels as git tags. A direct mapping does not2796# exist, so assume that if all the files are at the same revision2797# then we can use that, or it's something more complicated we should2798# just ignore.2799defimportP4Labels(self, stream, p4Labels):2800if verbose:2801print"import p4 labels: "+' '.join(p4Labels)28022803 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2804 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2805iflen(validLabelRegexp) ==0:2806 validLabelRegexp = defaultLabelRegexp2807 m = re.compile(validLabelRegexp)28082809for name in p4Labels:2810 commitFound =False28112812if not m.match(name):2813if verbose:2814print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2815continue28162817if name in ignoredP4Labels:2818continue28192820 labelDetails =p4CmdList(['label',"-o", name])[0]28212822# get the most recent changelist for each file in this label2823 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2824for p in self.depotPaths])28252826if change.has_key('change'):2827# find the corresponding git commit; take the oldest commit2828 changelist =int(change['change'])2829if changelist in self.committedChanges:2830 gitCommit =":%d"% changelist # use a fast-import mark2831 commitFound =True2832else:2833 gitCommit =read_pipe(["git","rev-list","--max-count=1",2834"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2835iflen(gitCommit) ==0:2836print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2837else:2838 commitFound =True2839 gitCommit = gitCommit.strip()28402841if commitFound:2842# Convert from p4 time format2843try:2844 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2845exceptValueError:2846print"Could not convert label time%s"% labelDetails['Update']2847 tmwhen =128482849 when =int(time.mktime(tmwhen))2850 self.streamTag(stream, name, labelDetails, gitCommit, when)2851if verbose:2852print"p4 label%smapped to git commit%s"% (name, gitCommit)2853else:2854if verbose:2855print"Label%shas no changelists - possibly deleted?"% name28562857if not commitFound:2858# We can't import this label; don't try again as it will get very2859# expensive repeatedly fetching all the files for labels that will2860# never be imported. If the label is moved in the future, the2861# ignore will need to be removed manually.2862system(["git","config","--add","git-p4.ignoredP4Labels", name])28632864defguessProjectName(self):2865for p in self.depotPaths:2866if p.endswith("/"):2867 p = p[:-1]2868 p = p[p.strip().rfind("/") +1:]2869if not p.endswith("/"):2870 p +="/"2871return p28722873defgetBranchMapping(self):2874 lostAndFoundBranches =set()28752876 user =gitConfig("git-p4.branchUser")2877iflen(user) >0:2878 command ="branches -u%s"% user2879else:2880 command ="branches"28812882for info inp4CmdList(command):2883 details =p4Cmd(["branch","-o", info["branch"]])2884 viewIdx =02885while details.has_key("View%s"% viewIdx):2886 paths = details["View%s"% viewIdx].split(" ")2887 viewIdx = viewIdx +12888# require standard //depot/foo/... //depot/bar/... mapping2889iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2890continue2891 source = paths[0]2892 destination = paths[1]2893## HACK2894ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2895 source = source[len(self.depotPaths[0]):-4]2896 destination = destination[len(self.depotPaths[0]):-4]28972898if destination in self.knownBranches:2899if not self.silent:2900print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2901print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2902continue29032904 self.knownBranches[destination] = source29052906 lostAndFoundBranches.discard(destination)29072908if source not in self.knownBranches:2909 lostAndFoundBranches.add(source)29102911# Perforce does not strictly require branches to be defined, so we also2912# check git config for a branch list.2913#2914# Example of branch definition in git config file:2915# [git-p4]2916# branchList=main:branchA2917# branchList=main:branchB2918# branchList=branchA:branchC2919 configBranches =gitConfigList("git-p4.branchList")2920for branch in configBranches:2921if branch:2922(source, destination) = branch.split(":")2923 self.knownBranches[destination] = source29242925 lostAndFoundBranches.discard(destination)29262927if source not in self.knownBranches:2928 lostAndFoundBranches.add(source)292929302931for branch in lostAndFoundBranches:2932 self.knownBranches[branch] = branch29332934defgetBranchMappingFromGitBranches(self):2935 branches =p4BranchesInGit(self.importIntoRemotes)2936for branch in branches.keys():2937if branch =="master":2938 branch ="main"2939else:2940 branch = branch[len(self.projectName):]2941 self.knownBranches[branch] = branch29422943defupdateOptionDict(self, d):2944 option_keys = {}2945if self.keepRepoPath:2946 option_keys['keepRepoPath'] =129472948 d["options"] =' '.join(sorted(option_keys.keys()))29492950defreadOptions(self, d):2951 self.keepRepoPath = (d.has_key('options')2952and('keepRepoPath'in d['options']))29532954defgitRefForBranch(self, branch):2955if branch =="main":2956return self.refPrefix +"master"29572958iflen(branch) <=0:2959return branch29602961return self.refPrefix + self.projectName + branch29622963defgitCommitByP4Change(self, ref, change):2964if self.verbose:2965print"looking in ref "+ ref +" for change%susing bisect..."% change29662967 earliestCommit =""2968 latestCommit =parseRevision(ref)29692970while True:2971if self.verbose:2972print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2973 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2974iflen(next) ==0:2975if self.verbose:2976print"argh"2977return""2978 log =extractLogMessageFromGitCommit(next)2979 settings =extractSettingsGitLog(log)2980 currentChange =int(settings['change'])2981if self.verbose:2982print"current change%s"% currentChange29832984if currentChange == change:2985if self.verbose:2986print"found%s"% next2987return next29882989if currentChange < change:2990 earliestCommit ="^%s"% next2991else:2992 latestCommit ="%s"% next29932994return""29952996defimportNewBranch(self, branch, maxChange):2997# make fast-import flush all changes to disk and update the refs using the checkpoint2998# command so that we can try to find the branch parent in the git history2999 self.gitStream.write("checkpoint\n\n");3000 self.gitStream.flush();3001 branchPrefix = self.depotPaths[0] + branch +"/"3002range="@1,%s"% maxChange3003#print "prefix" + branchPrefix3004 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3005iflen(changes) <=0:3006return False3007 firstChange = changes[0]3008#print "first change in branch: %s" % firstChange3009 sourceBranch = self.knownBranches[branch]3010 sourceDepotPath = self.depotPaths[0] + sourceBranch3011 sourceRef = self.gitRefForBranch(sourceBranch)3012#print "source " + sourceBranch30133014 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3015#print "branch parent: %s" % branchParentChange3016 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3017iflen(gitParent) >0:3018 self.initialParents[self.gitRefForBranch(branch)] = gitParent3019#print "parent git commit: %s" % gitParent30203021 self.importChanges(changes)3022return True30233024defsearchParent(self, parent, branch, target):3025 parentFound =False3026for blob inread_pipe_lines(["git","rev-list","--reverse",3027"--no-merges", parent]):3028 blob = blob.strip()3029iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3030 parentFound =True3031if self.verbose:3032print"Found parent of%sin commit%s"% (branch, blob)3033break3034if parentFound:3035return blob3036else:3037return None30383039defimportChanges(self, changes):3040 cnt =13041for change in changes:3042 description =p4_describe(change)3043 self.updateOptionDict(description)30443045if not self.silent:3046 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3047 sys.stdout.flush()3048 cnt = cnt +130493050try:3051if self.detectBranches:3052 branches = self.splitFilesIntoBranches(description)3053for branch in branches.keys():3054## HACK --hwn3055 branchPrefix = self.depotPaths[0] + branch +"/"3056 self.branchPrefixes = [ branchPrefix ]30573058 parent =""30593060 filesForCommit = branches[branch]30613062if self.verbose:3063print"branch is%s"% branch30643065 self.updatedBranches.add(branch)30663067if branch not in self.createdBranches:3068 self.createdBranches.add(branch)3069 parent = self.knownBranches[branch]3070if parent == branch:3071 parent =""3072else:3073 fullBranch = self.projectName + branch3074if fullBranch not in self.p4BranchesInGit:3075if not self.silent:3076print("\nImporting new branch%s"% fullBranch);3077if self.importNewBranch(branch, change -1):3078 parent =""3079 self.p4BranchesInGit.append(fullBranch)3080if not self.silent:3081print("\nResuming with change%s"% change);30823083if self.verbose:3084print"parent determined through known branches:%s"% parent30853086 branch = self.gitRefForBranch(branch)3087 parent = self.gitRefForBranch(parent)30883089if self.verbose:3090print"looking for initial parent for%s; current parent is%s"% (branch, parent)30913092iflen(parent) ==0and branch in self.initialParents:3093 parent = self.initialParents[branch]3094del self.initialParents[branch]30953096 blob =None3097iflen(parent) >0:3098 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3099if self.verbose:3100print"Creating temporary branch: "+ tempBranch3101 self.commit(description, filesForCommit, tempBranch)3102 self.tempBranches.append(tempBranch)3103 self.checkpoint()3104 blob = self.searchParent(parent, branch, tempBranch)3105if blob:3106 self.commit(description, filesForCommit, branch, blob)3107else:3108if self.verbose:3109print"Parent of%snot found. Committing into head of%s"% (branch, parent)3110 self.commit(description, filesForCommit, branch, parent)3111else:3112 files = self.extractFilesFromCommit(description)3113 self.commit(description, files, self.branch,3114 self.initialParent)3115# only needed once, to connect to the previous commit3116 self.initialParent =""3117exceptIOError:3118print self.gitError.read()3119 sys.exit(1)31203121defimportHeadRevision(self, revision):3122print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)31233124 details = {}3125 details["user"] ="git perforce import user"3126 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3127% (' '.join(self.depotPaths), revision))3128 details["change"] = revision3129 newestRevision =031303131 fileCnt =03132 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]31333134for info inp4CmdList(["files"] + fileArgs):31353136if'code'in info and info['code'] =='error':3137 sys.stderr.write("p4 returned an error:%s\n"3138% info['data'])3139if info['data'].find("must refer to client") >=0:3140 sys.stderr.write("This particular p4 error is misleading.\n")3141 sys.stderr.write("Perhaps the depot path was misspelled.\n");3142 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3143 sys.exit(1)3144if'p4ExitCode'in info:3145 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3146 sys.exit(1)314731483149 change =int(info["change"])3150if change > newestRevision:3151 newestRevision = change31523153if info["action"]in self.delete_actions:3154# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3155#fileCnt = fileCnt + 13156continue31573158for prop in["depotFile","rev","action","type"]:3159 details["%s%s"% (prop, fileCnt)] = info[prop]31603161 fileCnt = fileCnt +131623163 details["change"] = newestRevision31643165# Use time from top-most change so that all git p4 clones of3166# the same p4 repo have the same commit SHA1s.3167 res =p4_describe(newestRevision)3168 details["time"] = res["time"]31693170 self.updateOptionDict(details)3171try:3172 self.commit(details, self.extractFilesFromCommit(details), self.branch)3173exceptIOError:3174print"IO error with git fast-import. Is your git version recent enough?"3175print self.gitError.read()317631773178defrun(self, args):3179 self.depotPaths = []3180 self.changeRange =""3181 self.previousDepotPaths = []3182 self.hasOrigin =False31833184# map from branch depot path to parent branch3185 self.knownBranches = {}3186 self.initialParents = {}31873188if self.importIntoRemotes:3189 self.refPrefix ="refs/remotes/p4/"3190else:3191 self.refPrefix ="refs/heads/p4/"31923193if self.syncWithOrigin:3194 self.hasOrigin =originP4BranchesExist()3195if self.hasOrigin:3196if not self.silent:3197print'Syncing with origin first, using "git fetch origin"'3198system("git fetch origin")31993200 branch_arg_given =bool(self.branch)3201iflen(self.branch) ==0:3202 self.branch = self.refPrefix +"master"3203ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3204system("git update-ref%srefs/heads/p4"% self.branch)3205system("git branch -D p4")32063207# accept either the command-line option, or the configuration variable3208if self.useClientSpec:3209# will use this after clone to set the variable3210 self.useClientSpec_from_options =True3211else:3212ifgitConfigBool("git-p4.useclientspec"):3213 self.useClientSpec =True3214if self.useClientSpec:3215 self.clientSpecDirs =getClientSpec()32163217# TODO: should always look at previous commits,3218# merge with previous imports, if possible.3219if args == []:3220if self.hasOrigin:3221createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)32223223# branches holds mapping from branch name to sha13224 branches =p4BranchesInGit(self.importIntoRemotes)32253226# restrict to just this one, disabling detect-branches3227if branch_arg_given:3228 short = self.branch.split("/")[-1]3229if short in branches:3230 self.p4BranchesInGit = [ short ]3231else:3232 self.p4BranchesInGit = branches.keys()32333234iflen(self.p4BranchesInGit) >1:3235if not self.silent:3236print"Importing from/into multiple branches"3237 self.detectBranches =True3238for branch in branches.keys():3239 self.initialParents[self.refPrefix + branch] = \3240 branches[branch]32413242if self.verbose:3243print"branches:%s"% self.p4BranchesInGit32443245 p4Change =03246for branch in self.p4BranchesInGit:3247 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)32483249 settings =extractSettingsGitLog(logMsg)32503251 self.readOptions(settings)3252if(settings.has_key('depot-paths')3253and settings.has_key('change')):3254 change =int(settings['change']) +13255 p4Change =max(p4Change, change)32563257 depotPaths =sorted(settings['depot-paths'])3258if self.previousDepotPaths == []:3259 self.previousDepotPaths = depotPaths3260else:3261 paths = []3262for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3263 prev_list = prev.split("/")3264 cur_list = cur.split("/")3265for i inrange(0,min(len(cur_list),len(prev_list))):3266if cur_list[i] <> prev_list[i]:3267 i = i -13268break32693270 paths.append("/".join(cur_list[:i +1]))32713272 self.previousDepotPaths = paths32733274if p4Change >0:3275 self.depotPaths =sorted(self.previousDepotPaths)3276 self.changeRange ="@%s,#head"% p4Change3277if not self.silent and not self.detectBranches:3278print"Performing incremental import into%sgit branch"% self.branch32793280# accept multiple ref name abbreviations:3281# refs/foo/bar/branch -> use it exactly3282# p4/branch -> prepend refs/remotes/ or refs/heads/3283# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3284if not self.branch.startswith("refs/"):3285if self.importIntoRemotes:3286 prepend ="refs/remotes/"3287else:3288 prepend ="refs/heads/"3289if not self.branch.startswith("p4/"):3290 prepend +="p4/"3291 self.branch = prepend + self.branch32923293iflen(args) ==0and self.depotPaths:3294if not self.silent:3295print"Depot paths:%s"%' '.join(self.depotPaths)3296else:3297if self.depotPaths and self.depotPaths != args:3298print("previous import used depot path%sand now%swas specified. "3299"This doesn't work!"% (' '.join(self.depotPaths),3300' '.join(args)))3301 sys.exit(1)33023303 self.depotPaths =sorted(args)33043305 revision =""3306 self.users = {}33073308# Make sure no revision specifiers are used when --changesfile3309# is specified.3310 bad_changesfile =False3311iflen(self.changesFile) >0:3312for p in self.depotPaths:3313if p.find("@") >=0or p.find("#") >=0:3314 bad_changesfile =True3315break3316if bad_changesfile:3317die("Option --changesfile is incompatible with revision specifiers")33183319 newPaths = []3320for p in self.depotPaths:3321if p.find("@") != -1:3322 atIdx = p.index("@")3323 self.changeRange = p[atIdx:]3324if self.changeRange =="@all":3325 self.changeRange =""3326elif','not in self.changeRange:3327 revision = self.changeRange3328 self.changeRange =""3329 p = p[:atIdx]3330elif p.find("#") != -1:3331 hashIdx = p.index("#")3332 revision = p[hashIdx:]3333 p = p[:hashIdx]3334elif self.previousDepotPaths == []:3335# pay attention to changesfile, if given, else import3336# the entire p4 tree at the head revision3337iflen(self.changesFile) ==0:3338 revision ="#head"33393340 p = re.sub("\.\.\.$","", p)3341if not p.endswith("/"):3342 p +="/"33433344 newPaths.append(p)33453346 self.depotPaths = newPaths33473348# --detect-branches may change this for each branch3349 self.branchPrefixes = self.depotPaths33503351 self.loadUserMapFromCache()3352 self.labels = {}3353if self.detectLabels:3354 self.getLabels();33553356if self.detectBranches:3357## FIXME - what's a P4 projectName ?3358 self.projectName = self.guessProjectName()33593360if self.hasOrigin:3361 self.getBranchMappingFromGitBranches()3362else:3363 self.getBranchMapping()3364if self.verbose:3365print"p4-git branches:%s"% self.p4BranchesInGit3366print"initial parents:%s"% self.initialParents3367for b in self.p4BranchesInGit:3368if b !="master":33693370## FIXME3371 b = b[len(self.projectName):]3372 self.createdBranches.add(b)33733374 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))33753376 self.importProcess = subprocess.Popen(["git","fast-import"],3377 stdin=subprocess.PIPE,3378 stdout=subprocess.PIPE,3379 stderr=subprocess.PIPE);3380 self.gitOutput = self.importProcess.stdout3381 self.gitStream = self.importProcess.stdin3382 self.gitError = self.importProcess.stderr33833384if revision:3385 self.importHeadRevision(revision)3386else:3387 changes = []33883389iflen(self.changesFile) >0:3390 output =open(self.changesFile).readlines()3391 changeSet =set()3392for line in output:3393 changeSet.add(int(line))33943395for change in changeSet:3396 changes.append(change)33973398 changes.sort()3399else:3400# catch "git p4 sync" with no new branches, in a repo that3401# does not have any existing p4 branches3402iflen(args) ==0:3403if not self.p4BranchesInGit:3404die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")34053406# The default branch is master, unless --branch is used to3407# specify something else. Make sure it exists, or complain3408# nicely about how to use --branch.3409if not self.detectBranches:3410if notbranch_exists(self.branch):3411if branch_arg_given:3412die("Error: branch%sdoes not exist."% self.branch)3413else:3414die("Error: no branch%s; perhaps specify one with --branch."%3415 self.branch)34163417if self.verbose:3418print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3419 self.changeRange)3420 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)34213422iflen(self.maxChanges) >0:3423 changes = changes[:min(int(self.maxChanges),len(changes))]34243425iflen(changes) ==0:3426if not self.silent:3427print"No changes to import!"3428else:3429if not self.silent and not self.detectBranches:3430print"Import destination:%s"% self.branch34313432 self.updatedBranches =set()34333434if not self.detectBranches:3435if args:3436# start a new branch3437 self.initialParent =""3438else:3439# build on a previous revision3440 self.initialParent =parseRevision(self.branch)34413442 self.importChanges(changes)34433444if not self.silent:3445print""3446iflen(self.updatedBranches) >0:3447 sys.stdout.write("Updated branches: ")3448for b in self.updatedBranches:3449 sys.stdout.write("%s"% b)3450 sys.stdout.write("\n")34513452ifgitConfigBool("git-p4.importLabels"):3453 self.importLabels =True34543455if self.importLabels:3456 p4Labels =getP4Labels(self.depotPaths)3457 gitTags =getGitTags()34583459 missingP4Labels = p4Labels - gitTags3460 self.importP4Labels(self.gitStream, missingP4Labels)34613462 self.gitStream.close()3463if self.importProcess.wait() !=0:3464die("fast-import failed:%s"% self.gitError.read())3465 self.gitOutput.close()3466 self.gitError.close()34673468# Cleanup temporary branches created during import3469if self.tempBranches != []:3470for branch in self.tempBranches:3471read_pipe("git update-ref -d%s"% branch)3472 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))34733474# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3475# a convenient shortcut refname "p4".3476if self.importIntoRemotes:3477 head_ref = self.refPrefix +"HEAD"3478if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3479system(["git","symbolic-ref", head_ref, self.branch])34803481return True34823483classP4Rebase(Command):3484def__init__(self):3485 Command.__init__(self)3486 self.options = [3487 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3488]3489 self.importLabels =False3490 self.description = ("Fetches the latest revision from perforce and "3491+"rebases the current work (branch) against it")34923493defrun(self, args):3494 sync =P4Sync()3495 sync.importLabels = self.importLabels3496 sync.run([])34973498return self.rebase()34993500defrebase(self):3501if os.system("git update-index --refresh") !=0:3502die("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.");3503iflen(read_pipe("git diff-index HEAD --")) >0:3504die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");35053506[upstream, settings] =findUpstreamBranchPoint()3507iflen(upstream) ==0:3508die("Cannot find upstream branchpoint for rebase")35093510# the branchpoint may be p4/foo~3, so strip off the parent3511 upstream = re.sub("~[0-9]+$","", upstream)35123513print"Rebasing the current branch onto%s"% upstream3514 oldHead =read_pipe("git rev-parse HEAD").strip()3515system("git rebase%s"% upstream)3516system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3517return True35183519classP4Clone(P4Sync):3520def__init__(self):3521 P4Sync.__init__(self)3522 self.description ="Creates a new git repository and imports from Perforce into it"3523 self.usage ="usage: %prog [options] //depot/path[@revRange]"3524 self.options += [3525 optparse.make_option("--destination", dest="cloneDestination",3526 action='store', default=None,3527help="where to leave result of the clone"),3528 optparse.make_option("--bare", dest="cloneBare",3529 action="store_true", default=False),3530]3531 self.cloneDestination =None3532 self.needsGit =False3533 self.cloneBare =False35343535defdefaultDestination(self, args):3536## TODO: use common prefix of args?3537 depotPath = args[0]3538 depotDir = re.sub("(@[^@]*)$","", depotPath)3539 depotDir = re.sub("(#[^#]*)$","", depotDir)3540 depotDir = re.sub(r"\.\.\.$","", depotDir)3541 depotDir = re.sub(r"/$","", depotDir)3542return os.path.split(depotDir)[1]35433544defrun(self, args):3545iflen(args) <1:3546return False35473548if self.keepRepoPath and not self.cloneDestination:3549 sys.stderr.write("Must specify destination for --keep-path\n")3550 sys.exit(1)35513552 depotPaths = args35533554if not self.cloneDestination andlen(depotPaths) >1:3555 self.cloneDestination = depotPaths[-1]3556 depotPaths = depotPaths[:-1]35573558 self.cloneExclude = ["/"+p for p in self.cloneExclude]3559for p in depotPaths:3560if not p.startswith("//"):3561 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3562return False35633564if not self.cloneDestination:3565 self.cloneDestination = self.defaultDestination(args)35663567print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)35683569if not os.path.exists(self.cloneDestination):3570 os.makedirs(self.cloneDestination)3571chdir(self.cloneDestination)35723573 init_cmd = ["git","init"]3574if self.cloneBare:3575 init_cmd.append("--bare")3576 retcode = subprocess.call(init_cmd)3577if retcode:3578raiseCalledProcessError(retcode, init_cmd)35793580if not P4Sync.run(self, depotPaths):3581return False35823583# create a master branch and check out a work tree3584ifgitBranchExists(self.branch):3585system(["git","branch","master", self.branch ])3586if not self.cloneBare:3587system(["git","checkout","-f"])3588else:3589print'Not checking out any branch, use ' \3590'"git checkout -q -b master <branch>"'35913592# auto-set this variable if invoked with --use-client-spec3593if self.useClientSpec_from_options:3594system("git config --bool git-p4.useclientspec true")35953596return True35973598classP4Branches(Command):3599def__init__(self):3600 Command.__init__(self)3601 self.options = [ ]3602 self.description = ("Shows the git branches that hold imports and their "3603+"corresponding perforce depot paths")3604 self.verbose =False36053606defrun(self, args):3607iforiginP4BranchesExist():3608createOrUpdateBranchesFromOrigin()36093610 cmdline ="git rev-parse --symbolic "3611 cmdline +=" --remotes"36123613for line inread_pipe_lines(cmdline):3614 line = line.strip()36153616if not line.startswith('p4/')or line =="p4/HEAD":3617continue3618 branch = line36193620 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3621 settings =extractSettingsGitLog(log)36223623print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3624return True36253626classHelpFormatter(optparse.IndentedHelpFormatter):3627def__init__(self):3628 optparse.IndentedHelpFormatter.__init__(self)36293630defformat_description(self, description):3631if description:3632return description +"\n"3633else:3634return""36353636defprintUsage(commands):3637print"usage:%s<command> [options]"% sys.argv[0]3638print""3639print"valid commands:%s"%", ".join(commands)3640print""3641print"Try%s<command> --help for command specific help."% sys.argv[0]3642print""36433644commands = {3645"debug": P4Debug,3646"submit": P4Submit,3647"commit": P4Submit,3648"sync": P4Sync,3649"rebase": P4Rebase,3650"clone": P4Clone,3651"rollback": P4RollBack,3652"branches": P4Branches3653}365436553656defmain():3657iflen(sys.argv[1:]) ==0:3658printUsage(commands.keys())3659 sys.exit(2)36603661 cmdName = sys.argv[1]3662try:3663 klass = commands[cmdName]3664 cmd =klass()3665exceptKeyError:3666print"unknown command%s"% cmdName3667print""3668printUsage(commands.keys())3669 sys.exit(2)36703671 options = cmd.options3672 cmd.gitdir = os.environ.get("GIT_DIR",None)36733674 args = sys.argv[2:]36753676 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3677if cmd.needsGit:3678 options.append(optparse.make_option("--git-dir", dest="gitdir"))36793680 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3681 options,3682 description = cmd.description,3683 formatter =HelpFormatter())36843685(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3686global verbose3687 verbose = cmd.verbose3688if cmd.needsGit:3689if cmd.gitdir ==None:3690 cmd.gitdir = os.path.abspath(".git")3691if notisValidGitDir(cmd.gitdir):3692 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3693if os.path.exists(cmd.gitdir):3694 cdup =read_pipe("git rev-parse --show-cdup").strip()3695iflen(cdup) >0:3696chdir(cdup);36973698if notisValidGitDir(cmd.gitdir):3699ifisValidGitDir(cmd.gitdir +"/.git"):3700 cmd.gitdir +="/.git"3701else:3702die("fatal: cannot locate git repository at%s"% cmd.gitdir)37033704 os.environ["GIT_DIR"] = cmd.gitdir37053706if not cmd.run(args):3707 parser.print_help()3708 sys.exit(2)370937103711if __name__ =='__main__':3712main()