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 27 28try: 29from subprocess import CalledProcessError 30exceptImportError: 31# from python2.7:subprocess.py 32# Exception classes used by this module. 33classCalledProcessError(Exception): 34"""This exception is raised when a process run by check_call() returns 35 a non-zero exit status. The exit status will be stored in the 36 returncode attribute.""" 37def__init__(self, returncode, cmd): 38 self.returncode = returncode 39 self.cmd = cmd 40def__str__(self): 41return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 42 43verbose =False 44 45# Only labels/tags matching this will be imported/exported 46defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 47 48# Grab changes in blocks of this many revisions, unless otherwise requested 49defaultBlockSize =512 50 51defp4_build_cmd(cmd): 52"""Build a suitable p4 command line. 53 54 This consolidates building and returning a p4 command line into one 55 location. It means that hooking into the environment, or other configuration 56 can be done more easily. 57 """ 58 real_cmd = ["p4"] 59 60 user =gitConfig("git-p4.user") 61iflen(user) >0: 62 real_cmd += ["-u",user] 63 64 password =gitConfig("git-p4.password") 65iflen(password) >0: 66 real_cmd += ["-P", password] 67 68 port =gitConfig("git-p4.port") 69iflen(port) >0: 70 real_cmd += ["-p", port] 71 72 host =gitConfig("git-p4.host") 73iflen(host) >0: 74 real_cmd += ["-H", host] 75 76 client =gitConfig("git-p4.client") 77iflen(client) >0: 78 real_cmd += ["-c", client] 79 80 81ifisinstance(cmd,basestring): 82 real_cmd =' '.join(real_cmd) +' '+ cmd 83else: 84 real_cmd += cmd 85return real_cmd 86 87defchdir(path, is_client_path=False): 88"""Do chdir to the given path, and set the PWD environment 89 variable for use by P4. It does not look at getcwd() output. 90 Since we're not using the shell, it is necessary to set the 91 PWD environment variable explicitly. 92 93 Normally, expand the path to force it to be absolute. This 94 addresses the use of relative path names inside P4 settings, 95 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 96 as given; it looks for .p4config using PWD. 97 98 If is_client_path, the path was handed to us directly by p4, 99 and may be a symbolic link. Do not call os.getcwd() in this 100 case, because it will cause p4 to think that PWD is not inside 101 the client path. 102 """ 103 104 os.chdir(path) 105if not is_client_path: 106 path = os.getcwd() 107 os.environ['PWD'] = path 108 109defcalcDiskFree(): 110"""Return free space in bytes on the disk of the given dirname.""" 111if platform.system() =='Windows': 112 free_bytes = ctypes.c_ulonglong(0) 113 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 114return free_bytes.value 115else: 116 st = os.statvfs(os.getcwd()) 117return st.f_bavail * st.f_frsize 118 119defdie(msg): 120if verbose: 121raiseException(msg) 122else: 123 sys.stderr.write(msg +"\n") 124 sys.exit(1) 125 126defwrite_pipe(c, stdin): 127if verbose: 128 sys.stderr.write('Writing pipe:%s\n'%str(c)) 129 130 expand =isinstance(c,basestring) 131 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 132 pipe = p.stdin 133 val = pipe.write(stdin) 134 pipe.close() 135if p.wait(): 136die('Command failed:%s'%str(c)) 137 138return val 139 140defp4_write_pipe(c, stdin): 141 real_cmd =p4_build_cmd(c) 142returnwrite_pipe(real_cmd, stdin) 143 144defread_pipe(c, ignore_error=False): 145if verbose: 146 sys.stderr.write('Reading pipe:%s\n'%str(c)) 147 148 expand =isinstance(c,basestring) 149 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 150 pipe = p.stdout 151 val = pipe.read() 152if p.wait()and not ignore_error: 153die('Command failed:%s'%str(c)) 154 155return val 156 157defp4_read_pipe(c, ignore_error=False): 158 real_cmd =p4_build_cmd(c) 159returnread_pipe(real_cmd, ignore_error) 160 161defread_pipe_lines(c): 162if verbose: 163 sys.stderr.write('Reading pipe:%s\n'%str(c)) 164 165 expand =isinstance(c, basestring) 166 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 167 pipe = p.stdout 168 val = pipe.readlines() 169if pipe.close()or p.wait(): 170die('Command failed:%s'%str(c)) 171 172return val 173 174defp4_read_pipe_lines(c): 175"""Specifically invoke p4 on the command supplied. """ 176 real_cmd =p4_build_cmd(c) 177returnread_pipe_lines(real_cmd) 178 179defp4_has_command(cmd): 180"""Ask p4 for help on this command. If it returns an error, the 181 command does not exist in this version of p4.""" 182 real_cmd =p4_build_cmd(["help", cmd]) 183 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 184 stderr=subprocess.PIPE) 185 p.communicate() 186return p.returncode ==0 187 188defp4_has_move_command(): 189"""See if the move command exists, that it supports -k, and that 190 it has not been administratively disabled. The arguments 191 must be correct, but the filenames do not have to exist. Use 192 ones with wildcards so even if they exist, it will fail.""" 193 194if notp4_has_command("move"): 195return False 196 cmd =p4_build_cmd(["move","-k","@from","@to"]) 197 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 198(out, err) = p.communicate() 199# return code will be 1 in either case 200if err.find("Invalid option") >=0: 201return False 202if err.find("disabled") >=0: 203return False 204# assume it failed because @... was invalid changelist 205return True 206 207defsystem(cmd): 208 expand =isinstance(cmd,basestring) 209if verbose: 210 sys.stderr.write("executing%s\n"%str(cmd)) 211 retcode = subprocess.call(cmd, shell=expand) 212if retcode: 213raiseCalledProcessError(retcode, cmd) 214 215defp4_system(cmd): 216"""Specifically invoke p4 as the system command. """ 217 real_cmd =p4_build_cmd(cmd) 218 expand =isinstance(real_cmd, basestring) 219 retcode = subprocess.call(real_cmd, shell=expand) 220if retcode: 221raiseCalledProcessError(retcode, real_cmd) 222 223_p4_version_string =None 224defp4_version_string(): 225"""Read the version string, showing just the last line, which 226 hopefully is the interesting version bit. 227 228 $ p4 -V 229 Perforce - The Fast Software Configuration Management System. 230 Copyright 1995-2011 Perforce Software. All rights reserved. 231 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 232 """ 233global _p4_version_string 234if not _p4_version_string: 235 a =p4_read_pipe_lines(["-V"]) 236 _p4_version_string = a[-1].rstrip() 237return _p4_version_string 238 239defp4_integrate(src, dest): 240p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 241 242defp4_sync(f, *options): 243p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 244 245defp4_add(f): 246# forcibly add file names with wildcards 247ifwildcard_present(f): 248p4_system(["add","-f", f]) 249else: 250p4_system(["add", f]) 251 252defp4_delete(f): 253p4_system(["delete",wildcard_encode(f)]) 254 255defp4_edit(f): 256p4_system(["edit",wildcard_encode(f)]) 257 258defp4_revert(f): 259p4_system(["revert",wildcard_encode(f)]) 260 261defp4_reopen(type, f): 262p4_system(["reopen","-t",type,wildcard_encode(f)]) 263 264defp4_move(src, dest): 265p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 266 267defp4_last_change(): 268 results =p4CmdList(["changes","-m","1"]) 269returnint(results[0]['change']) 270 271defp4_describe(change): 272"""Make sure it returns a valid result by checking for 273 the presence of field "time". Return a dict of the 274 results.""" 275 276 ds =p4CmdList(["describe","-s",str(change)]) 277iflen(ds) !=1: 278die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 279 280 d = ds[0] 281 282if"p4ExitCode"in d: 283die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 284str(d))) 285if"code"in d: 286if d["code"] =="error": 287die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 288 289if"time"not in d: 290die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 291 292return d 293 294# 295# Canonicalize the p4 type and return a tuple of the 296# base type, plus any modifiers. See "p4 help filetypes" 297# for a list and explanation. 298# 299defsplit_p4_type(p4type): 300 301 p4_filetypes_historical = { 302"ctempobj":"binary+Sw", 303"ctext":"text+C", 304"cxtext":"text+Cx", 305"ktext":"text+k", 306"kxtext":"text+kx", 307"ltext":"text+F", 308"tempobj":"binary+FSw", 309"ubinary":"binary+F", 310"uresource":"resource+F", 311"uxbinary":"binary+Fx", 312"xbinary":"binary+x", 313"xltext":"text+Fx", 314"xtempobj":"binary+Swx", 315"xtext":"text+x", 316"xunicode":"unicode+x", 317"xutf16":"utf16+x", 318} 319if p4type in p4_filetypes_historical: 320 p4type = p4_filetypes_historical[p4type] 321 mods ="" 322 s = p4type.split("+") 323 base = s[0] 324 mods ="" 325iflen(s) >1: 326 mods = s[1] 327return(base, mods) 328 329# 330# return the raw p4 type of a file (text, text+ko, etc) 331# 332defp4_type(f): 333 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 334return results[0]['headType'] 335 336# 337# Given a type base and modifier, return a regexp matching 338# the keywords that can be expanded in the file 339# 340defp4_keywords_regexp_for_type(base, type_mods): 341if base in("text","unicode","binary"): 342 kwords =None 343if"ko"in type_mods: 344 kwords ='Id|Header' 345elif"k"in type_mods: 346 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 347else: 348return None 349 pattern = r""" 350 \$ # Starts with a dollar, followed by... 351 (%s) # one of the keywords, followed by... 352 (:[^$\n]+)? # possibly an old expansion, followed by... 353 \$ # another dollar 354 """% kwords 355return pattern 356else: 357return None 358 359# 360# Given a file, return a regexp matching the possible 361# RCS keywords that will be expanded, or None for files 362# with kw expansion turned off. 363# 364defp4_keywords_regexp_for_file(file): 365if not os.path.exists(file): 366return None 367else: 368(type_base, type_mods) =split_p4_type(p4_type(file)) 369returnp4_keywords_regexp_for_type(type_base, type_mods) 370 371defsetP4ExecBit(file, mode): 372# Reopens an already open file and changes the execute bit to match 373# the execute bit setting in the passed in mode. 374 375 p4Type ="+x" 376 377if notisModeExec(mode): 378 p4Type =getP4OpenedType(file) 379 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 380 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 381if p4Type[-1] =="+": 382 p4Type = p4Type[0:-1] 383 384p4_reopen(p4Type,file) 385 386defgetP4OpenedType(file): 387# Returns the perforce file type for the given file. 388 389 result =p4_read_pipe(["opened",wildcard_encode(file)]) 390 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 391if match: 392return match.group(1) 393else: 394die("Could not determine file type for%s(result: '%s')"% (file, result)) 395 396# Return the set of all p4 labels 397defgetP4Labels(depotPaths): 398 labels =set() 399ifisinstance(depotPaths,basestring): 400 depotPaths = [depotPaths] 401 402for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 403 label = l['label'] 404 labels.add(label) 405 406return labels 407 408# Return the set of all git tags 409defgetGitTags(): 410 gitTags =set() 411for line inread_pipe_lines(["git","tag"]): 412 tag = line.strip() 413 gitTags.add(tag) 414return gitTags 415 416defdiffTreePattern(): 417# This is a simple generator for the diff tree regex pattern. This could be 418# a class variable if this and parseDiffTreeEntry were a part of a class. 419 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 420while True: 421yield pattern 422 423defparseDiffTreeEntry(entry): 424"""Parses a single diff tree entry into its component elements. 425 426 See git-diff-tree(1) manpage for details about the format of the diff 427 output. This method returns a dictionary with the following elements: 428 429 src_mode - The mode of the source file 430 dst_mode - The mode of the destination file 431 src_sha1 - The sha1 for the source file 432 dst_sha1 - The sha1 fr the destination file 433 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 434 status_score - The score for the status (applicable for 'C' and 'R' 435 statuses). This is None if there is no score. 436 src - The path for the source file. 437 dst - The path for the destination file. This is only present for 438 copy or renames. If it is not present, this is None. 439 440 If the pattern is not matched, None is returned.""" 441 442 match =diffTreePattern().next().match(entry) 443if match: 444return{ 445'src_mode': match.group(1), 446'dst_mode': match.group(2), 447'src_sha1': match.group(3), 448'dst_sha1': match.group(4), 449'status': match.group(5), 450'status_score': match.group(6), 451'src': match.group(7), 452'dst': match.group(10) 453} 454return None 455 456defisModeExec(mode): 457# Returns True if the given git mode represents an executable file, 458# otherwise False. 459return mode[-3:] =="755" 460 461defisModeExecChanged(src_mode, dst_mode): 462returnisModeExec(src_mode) !=isModeExec(dst_mode) 463 464defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 465 466ifisinstance(cmd,basestring): 467 cmd ="-G "+ cmd 468 expand =True 469else: 470 cmd = ["-G"] + cmd 471 expand =False 472 473 cmd =p4_build_cmd(cmd) 474if verbose: 475 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 476 477# Use a temporary file to avoid deadlocks without 478# subprocess.communicate(), which would put another copy 479# of stdout into memory. 480 stdin_file =None 481if stdin is not None: 482 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 483ifisinstance(stdin,basestring): 484 stdin_file.write(stdin) 485else: 486for i in stdin: 487 stdin_file.write(i +'\n') 488 stdin_file.flush() 489 stdin_file.seek(0) 490 491 p4 = subprocess.Popen(cmd, 492 shell=expand, 493 stdin=stdin_file, 494 stdout=subprocess.PIPE) 495 496 result = [] 497try: 498while True: 499 entry = marshal.load(p4.stdout) 500if cb is not None: 501cb(entry) 502else: 503 result.append(entry) 504exceptEOFError: 505pass 506 exitCode = p4.wait() 507if exitCode !=0: 508 entry = {} 509 entry["p4ExitCode"] = exitCode 510 result.append(entry) 511 512return result 513 514defp4Cmd(cmd): 515list=p4CmdList(cmd) 516 result = {} 517for entry inlist: 518 result.update(entry) 519return result; 520 521defp4Where(depotPath): 522if not depotPath.endswith("/"): 523 depotPath +="/" 524 depotPathLong = depotPath +"..." 525 outputList =p4CmdList(["where", depotPathLong]) 526 output =None 527for entry in outputList: 528if"depotFile"in entry: 529# Search for the base client side depot path, as long as it starts with the branch's P4 path. 530# The base path always ends with "/...". 531if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 532 output = entry 533break 534elif"data"in entry: 535 data = entry.get("data") 536 space = data.find(" ") 537if data[:space] == depotPath: 538 output = entry 539break 540if output ==None: 541return"" 542if output["code"] =="error": 543return"" 544 clientPath ="" 545if"path"in output: 546 clientPath = output.get("path") 547elif"data"in output: 548 data = output.get("data") 549 lastSpace = data.rfind(" ") 550 clientPath = data[lastSpace +1:] 551 552if clientPath.endswith("..."): 553 clientPath = clientPath[:-3] 554return clientPath 555 556defcurrentGitBranch(): 557returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 558 559defisValidGitDir(path): 560if(os.path.exists(path +"/HEAD") 561and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 562return True; 563return False 564 565defparseRevision(ref): 566returnread_pipe("git rev-parse%s"% ref).strip() 567 568defbranchExists(ref): 569 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 570 ignore_error=True) 571returnlen(rev) >0 572 573defextractLogMessageFromGitCommit(commit): 574 logMessage ="" 575 576## fixme: title is first line of commit, not 1st paragraph. 577 foundTitle =False 578for log inread_pipe_lines("git cat-file commit%s"% commit): 579if not foundTitle: 580iflen(log) ==1: 581 foundTitle =True 582continue 583 584 logMessage += log 585return logMessage 586 587defextractSettingsGitLog(log): 588 values = {} 589for line in log.split("\n"): 590 line = line.strip() 591 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 592if not m: 593continue 594 595 assignments = m.group(1).split(':') 596for a in assignments: 597 vals = a.split('=') 598 key = vals[0].strip() 599 val = ('='.join(vals[1:])).strip() 600if val.endswith('\"')and val.startswith('"'): 601 val = val[1:-1] 602 603 values[key] = val 604 605 paths = values.get("depot-paths") 606if not paths: 607 paths = values.get("depot-path") 608if paths: 609 values['depot-paths'] = paths.split(',') 610return values 611 612defgitBranchExists(branch): 613 proc = subprocess.Popen(["git","rev-parse", branch], 614 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 615return proc.wait() ==0; 616 617_gitConfig = {} 618 619defgitConfig(key, typeSpecifier=None): 620if not _gitConfig.has_key(key): 621 cmd = ["git","config"] 622if typeSpecifier: 623 cmd += [ typeSpecifier ] 624 cmd += [ key ] 625 s =read_pipe(cmd, ignore_error=True) 626 _gitConfig[key] = s.strip() 627return _gitConfig[key] 628 629defgitConfigBool(key): 630"""Return a bool, using git config --bool. It is True only if the 631 variable is set to true, and False if set to false or not present 632 in the config.""" 633 634if not _gitConfig.has_key(key): 635 _gitConfig[key] =gitConfig(key,'--bool') =="true" 636return _gitConfig[key] 637 638defgitConfigInt(key): 639if not _gitConfig.has_key(key): 640 cmd = ["git","config","--int", key ] 641 s =read_pipe(cmd, ignore_error=True) 642 v = s.strip() 643try: 644 _gitConfig[key] =int(gitConfig(key,'--int')) 645exceptValueError: 646 _gitConfig[key] =None 647return _gitConfig[key] 648 649defgitConfigList(key): 650if not _gitConfig.has_key(key): 651 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 652 _gitConfig[key] = s.strip().split(os.linesep) 653if _gitConfig[key] == ['']: 654 _gitConfig[key] = [] 655return _gitConfig[key] 656 657defp4BranchesInGit(branchesAreInRemotes=True): 658"""Find all the branches whose names start with "p4/", looking 659 in remotes or heads as specified by the argument. Return 660 a dictionary of{ branch: revision }for each one found. 661 The branch names are the short names, without any 662 "p4/" prefix.""" 663 664 branches = {} 665 666 cmdline ="git rev-parse --symbolic " 667if branchesAreInRemotes: 668 cmdline +="--remotes" 669else: 670 cmdline +="--branches" 671 672for line inread_pipe_lines(cmdline): 673 line = line.strip() 674 675# only import to p4/ 676if not line.startswith('p4/'): 677continue 678# special symbolic ref to p4/master 679if line =="p4/HEAD": 680continue 681 682# strip off p4/ prefix 683 branch = line[len("p4/"):] 684 685 branches[branch] =parseRevision(line) 686 687return branches 688 689defbranch_exists(branch): 690"""Make sure that the given ref name really exists.""" 691 692 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 693 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 694 out, _ = p.communicate() 695if p.returncode: 696return False 697# expect exactly one line of output: the branch name 698return out.rstrip() == branch 699 700deffindUpstreamBranchPoint(head ="HEAD"): 701 branches =p4BranchesInGit() 702# map from depot-path to branch name 703 branchByDepotPath = {} 704for branch in branches.keys(): 705 tip = branches[branch] 706 log =extractLogMessageFromGitCommit(tip) 707 settings =extractSettingsGitLog(log) 708if settings.has_key("depot-paths"): 709 paths =",".join(settings["depot-paths"]) 710 branchByDepotPath[paths] ="remotes/p4/"+ branch 711 712 settings =None 713 parent =0 714while parent <65535: 715 commit = head +"~%s"% parent 716 log =extractLogMessageFromGitCommit(commit) 717 settings =extractSettingsGitLog(log) 718if settings.has_key("depot-paths"): 719 paths =",".join(settings["depot-paths"]) 720if branchByDepotPath.has_key(paths): 721return[branchByDepotPath[paths], settings] 722 723 parent = parent +1 724 725return["", settings] 726 727defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 728if not silent: 729print("Creating/updating branch(es) in%sbased on origin branch(es)" 730% localRefPrefix) 731 732 originPrefix ="origin/p4/" 733 734for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 735 line = line.strip() 736if(not line.startswith(originPrefix))or line.endswith("HEAD"): 737continue 738 739 headName = line[len(originPrefix):] 740 remoteHead = localRefPrefix + headName 741 originHead = line 742 743 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 744if(not original.has_key('depot-paths') 745or not original.has_key('change')): 746continue 747 748 update =False 749if notgitBranchExists(remoteHead): 750if verbose: 751print"creating%s"% remoteHead 752 update =True 753else: 754 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 755if settings.has_key('change') >0: 756if settings['depot-paths'] == original['depot-paths']: 757 originP4Change =int(original['change']) 758 p4Change =int(settings['change']) 759if originP4Change > p4Change: 760print("%s(%s) is newer than%s(%s). " 761"Updating p4 branch from origin." 762% (originHead, originP4Change, 763 remoteHead, p4Change)) 764 update =True 765else: 766print("Ignoring:%swas imported from%swhile " 767"%swas imported from%s" 768% (originHead,','.join(original['depot-paths']), 769 remoteHead,','.join(settings['depot-paths']))) 770 771if update: 772system("git update-ref%s %s"% (remoteHead, originHead)) 773 774deforiginP4BranchesExist(): 775returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 776 777 778defp4ParseNumericChangeRange(parts): 779 changeStart =int(parts[0][1:]) 780if parts[1] =='#head': 781 changeEnd =p4_last_change() 782else: 783 changeEnd =int(parts[1]) 784 785return(changeStart, changeEnd) 786 787defchooseBlockSize(blockSize): 788if blockSize: 789return blockSize 790else: 791return defaultBlockSize 792 793defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 794assert depotPaths 795 796# Parse the change range into start and end. Try to find integer 797# revision ranges as these can be broken up into blocks to avoid 798# hitting server-side limits (maxrows, maxscanresults). But if 799# that doesn't work, fall back to using the raw revision specifier 800# strings, without using block mode. 801 802if changeRange is None or changeRange =='': 803 changeStart =1 804 changeEnd =p4_last_change() 805 block_size =chooseBlockSize(requestedBlockSize) 806else: 807 parts = changeRange.split(',') 808assertlen(parts) ==2 809try: 810(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 811 block_size =chooseBlockSize(requestedBlockSize) 812except: 813 changeStart = parts[0][1:] 814 changeEnd = parts[1] 815if requestedBlockSize: 816die("cannot use --changes-block-size with non-numeric revisions") 817 block_size =None 818 819# Accumulate change numbers in a dictionary to avoid duplicates 820 changes = {} 821 822for p in depotPaths: 823# Retrieve changes a block at a time, to prevent running 824# into a MaxResults/MaxScanRows error from the server. 825 826while True: 827 cmd = ['changes'] 828 829if block_size: 830 end =min(changeEnd, changeStart + block_size) 831 revisionRange ="%d,%d"% (changeStart, end) 832else: 833 revisionRange ="%s,%s"% (changeStart, changeEnd) 834 835 cmd += ["%s...@%s"% (p, revisionRange)] 836 837for line inp4_read_pipe_lines(cmd): 838 changeNum =int(line.split(" ")[1]) 839 changes[changeNum] =True 840 841if not block_size: 842break 843 844if end >= changeEnd: 845break 846 847 changeStart = end +1 848 849 changelist = changes.keys() 850 changelist.sort() 851return changelist 852 853defp4PathStartsWith(path, prefix): 854# This method tries to remedy a potential mixed-case issue: 855# 856# If UserA adds //depot/DirA/file1 857# and UserB adds //depot/dira/file2 858# 859# we may or may not have a problem. If you have core.ignorecase=true, 860# we treat DirA and dira as the same directory 861ifgitConfigBool("core.ignorecase"): 862return path.lower().startswith(prefix.lower()) 863return path.startswith(prefix) 864 865defgetClientSpec(): 866"""Look at the p4 client spec, create a View() object that contains 867 all the mappings, and return it.""" 868 869 specList =p4CmdList("client -o") 870iflen(specList) !=1: 871die('Output from "client -o" is%dlines, expecting 1'% 872len(specList)) 873 874# dictionary of all client parameters 875 entry = specList[0] 876 877# the //client/ name 878 client_name = entry["Client"] 879 880# just the keys that start with "View" 881 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 882 883# hold this new View 884 view =View(client_name) 885 886# append the lines, in order, to the view 887for view_num inrange(len(view_keys)): 888 k ="View%d"% view_num 889if k not in view_keys: 890die("Expected view key%smissing"% k) 891 view.append(entry[k]) 892 893return view 894 895defgetClientRoot(): 896"""Grab the client directory.""" 897 898 output =p4CmdList("client -o") 899iflen(output) !=1: 900die('Output from "client -o" is%dlines, expecting 1'%len(output)) 901 902 entry = output[0] 903if"Root"not in entry: 904die('Client has no "Root"') 905 906return entry["Root"] 907 908# 909# P4 wildcards are not allowed in filenames. P4 complains 910# if you simply add them, but you can force it with "-f", in 911# which case it translates them into %xx encoding internally. 912# 913defwildcard_decode(path): 914# Search for and fix just these four characters. Do % last so 915# that fixing it does not inadvertently create new %-escapes. 916# Cannot have * in a filename in windows; untested as to 917# what p4 would do in such a case. 918if not platform.system() =="Windows": 919 path = path.replace("%2A","*") 920 path = path.replace("%23","#") \ 921.replace("%40","@") \ 922.replace("%25","%") 923return path 924 925defwildcard_encode(path): 926# do % first to avoid double-encoding the %s introduced here 927 path = path.replace("%","%25") \ 928.replace("*","%2A") \ 929.replace("#","%23") \ 930.replace("@","%40") 931return path 932 933defwildcard_present(path): 934 m = re.search("[*#@%]", path) 935return m is not None 936 937classLargeFileSystem(object): 938"""Base class for large file system support.""" 939 940def__init__(self, writeToGitStream): 941 self.largeFiles =set() 942 self.writeToGitStream = writeToGitStream 943 944defgeneratePointer(self, cloneDestination, contentFile): 945"""Return the content of a pointer file that is stored in Git instead of 946 the actual content.""" 947assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 948 949defpushFile(self, localLargeFile): 950"""Push the actual content which is not stored in the Git repository to 951 a server.""" 952assert False,"Method 'pushFile' required in "+ self.__class__.__name__ 953 954defhasLargeFileExtension(self, relPath): 955returnreduce( 956lambda a, b: a or b, 957[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')], 958False 959) 960 961defgenerateTempFile(self, contents): 962 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 963for d in contents: 964 contentFile.write(d) 965 contentFile.close() 966return contentFile.name 967 968defexceedsLargeFileThreshold(self, relPath, contents): 969ifgitConfigInt('git-p4.largeFileThreshold'): 970 contentsSize =sum(len(d)for d in contents) 971if contentsSize >gitConfigInt('git-p4.largeFileThreshold'): 972return True 973ifgitConfigInt('git-p4.largeFileCompressedThreshold'): 974 contentsSize =sum(len(d)for d in contents) 975if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'): 976return False 977 contentTempFile = self.generateTempFile(contents) 978 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 979 zf = zipfile.ZipFile(compressedContentFile.name, mode='w') 980 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED) 981 zf.close() 982 compressedContentsSize = zf.infolist()[0].compress_size 983 os.remove(contentTempFile) 984 os.remove(compressedContentFile.name) 985if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'): 986return True 987return False 988 989defaddLargeFile(self, relPath): 990 self.largeFiles.add(relPath) 991 992defremoveLargeFile(self, relPath): 993 self.largeFiles.remove(relPath) 994 995defisLargeFile(self, relPath): 996return relPath in self.largeFiles 997 998defprocessContent(self, git_mode, relPath, contents): 999"""Processes the content of git fast import. This method decides if a1000 file is stored in the large file system and handles all necessary1001 steps."""1002if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1003 contentTempFile = self.generateTempFile(contents)1004(git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)10051006# Move temp file to final location in large file system1007 largeFileDir = os.path.dirname(localLargeFile)1008if not os.path.isdir(largeFileDir):1009 os.makedirs(largeFileDir)1010 shutil.move(contentTempFile, localLargeFile)1011 self.addLargeFile(relPath)1012ifgitConfigBool('git-p4.largeFilePush'):1013 self.pushFile(localLargeFile)1014if verbose:1015 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1016return(git_mode, contents)10171018classMockLFS(LargeFileSystem):1019"""Mock large file system for testing."""10201021defgeneratePointer(self, contentFile):1022"""The pointer content is the original content prefixed with "pointer-".1023 The local filename of the large file storage is derived from the file content.1024 """1025withopen(contentFile,'r')as f:1026 content =next(f)1027 gitMode ='100644'1028 pointerContents ='pointer-'+ content1029 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1030return(gitMode, pointerContents, localLargeFile)10311032defpushFile(self, localLargeFile):1033"""The remote filename of the large file storage is the same as the local1034 one but in a different directory.1035 """1036 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1037if not os.path.exists(remotePath):1038 os.makedirs(remotePath)1039 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10401041classGitLFS(LargeFileSystem):1042"""Git LFS as backend for the git-p4 large file system.1043 See https://git-lfs.github.com/ for details."""10441045def__init__(self, *args):1046 LargeFileSystem.__init__(self, *args)1047 self.baseGitAttributes = []10481049defgeneratePointer(self, contentFile):1050"""Generate a Git LFS pointer for the content. Return LFS Pointer file1051 mode and content which is stored in the Git repository instead of1052 the actual content. Return also the new location of the actual1053 content.1054 """1055 pointerProcess = subprocess.Popen(1056['git','lfs','pointer','--file='+ contentFile],1057 stdout=subprocess.PIPE1058)1059 pointerFile = pointerProcess.stdout.read()1060if pointerProcess.wait():1061 os.remove(contentFile)1062die('git-lfs pointer command failed. Did you install the extension?')1063 pointerContents = [i+'\n'for i in pointerFile.split('\n')[2:][:-1]]1064 oid = pointerContents[1].split(' ')[1].split(':')[1][:-1]1065 localLargeFile = os.path.join(1066 os.getcwd(),1067'.git','lfs','objects', oid[:2], oid[2:4],1068 oid,1069)1070# LFS Spec states that pointer files should not have the executable bit set.1071 gitMode ='100644'1072return(gitMode, pointerContents, localLargeFile)10731074defpushFile(self, localLargeFile):1075 uploadProcess = subprocess.Popen(1076['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1077)1078if uploadProcess.wait():1079die('git-lfs push command failed. Did you define a remote?')10801081defgenerateGitAttributes(self):1082return(1083 self.baseGitAttributes +1084[1085'\n',1086'#\n',1087'# Git LFS (see https://git-lfs.github.com/)\n',1088'#\n',1089] +1090['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1091for f insorted(gitConfigList('git-p4.largeFileExtensions'))1092] +1093['/'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1094for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1095]1096)10971098defaddLargeFile(self, relPath):1099 LargeFileSystem.addLargeFile(self, relPath)1100 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11011102defremoveLargeFile(self, relPath):1103 LargeFileSystem.removeLargeFile(self, relPath)1104 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11051106defprocessContent(self, git_mode, relPath, contents):1107if relPath =='.gitattributes':1108 self.baseGitAttributes = contents1109return(git_mode, self.generateGitAttributes())1110else:1111return LargeFileSystem.processContent(self, git_mode, relPath, contents)11121113class Command:1114def__init__(self):1115 self.usage ="usage: %prog [options]"1116 self.needsGit =True1117 self.verbose =False11181119class P4UserMap:1120def__init__(self):1121 self.userMapFromPerforceServer =False1122 self.myP4UserId =None11231124defp4UserId(self):1125if self.myP4UserId:1126return self.myP4UserId11271128 results =p4CmdList("user -o")1129for r in results:1130if r.has_key('User'):1131 self.myP4UserId = r['User']1132return r['User']1133die("Could not find your p4 user id")11341135defp4UserIsMe(self, p4User):1136# return True if the given p4 user is actually me1137 me = self.p4UserId()1138if not p4User or p4User != me:1139return False1140else:1141return True11421143defgetUserCacheFilename(self):1144 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1145return home +"/.gitp4-usercache.txt"11461147defgetUserMapFromPerforceServer(self):1148if self.userMapFromPerforceServer:1149return1150 self.users = {}1151 self.emails = {}11521153for output inp4CmdList("users"):1154if not output.has_key("User"):1155continue1156 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1157 self.emails[output["Email"]] = output["User"]115811591160 s =''1161for(key, val)in self.users.items():1162 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))11631164open(self.getUserCacheFilename(),"wb").write(s)1165 self.userMapFromPerforceServer =True11661167defloadUserMapFromCache(self):1168 self.users = {}1169 self.userMapFromPerforceServer =False1170try:1171 cache =open(self.getUserCacheFilename(),"rb")1172 lines = cache.readlines()1173 cache.close()1174for line in lines:1175 entry = line.strip().split("\t")1176 self.users[entry[0]] = entry[1]1177exceptIOError:1178 self.getUserMapFromPerforceServer()11791180classP4Debug(Command):1181def__init__(self):1182 Command.__init__(self)1183 self.options = []1184 self.description ="A tool to debug the output of p4 -G."1185 self.needsGit =False11861187defrun(self, args):1188 j =01189for output inp4CmdList(args):1190print'Element:%d'% j1191 j +=11192print output1193return True11941195classP4RollBack(Command):1196def__init__(self):1197 Command.__init__(self)1198 self.options = [1199 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1200]1201 self.description ="A tool to debug the multi-branch import. Don't use :)"1202 self.rollbackLocalBranches =False12031204defrun(self, args):1205iflen(args) !=1:1206return False1207 maxChange =int(args[0])12081209if"p4ExitCode"inp4Cmd("changes -m 1"):1210die("Problems executing p4");12111212if self.rollbackLocalBranches:1213 refPrefix ="refs/heads/"1214 lines =read_pipe_lines("git rev-parse --symbolic --branches")1215else:1216 refPrefix ="refs/remotes/"1217 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12181219for line in lines:1220if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1221 line = line.strip()1222 ref = refPrefix + line1223 log =extractLogMessageFromGitCommit(ref)1224 settings =extractSettingsGitLog(log)12251226 depotPaths = settings['depot-paths']1227 change = settings['change']12281229 changed =False12301231iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1232for p in depotPaths]))) ==0:1233print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1234system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1235continue12361237while change andint(change) > maxChange:1238 changed =True1239if self.verbose:1240print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1241system("git update-ref%s\"%s^\""% (ref, ref))1242 log =extractLogMessageFromGitCommit(ref)1243 settings =extractSettingsGitLog(log)124412451246 depotPaths = settings['depot-paths']1247 change = settings['change']12481249if changed:1250print"%srewound to%s"% (ref, change)12511252return True12531254classP4Submit(Command, P4UserMap):12551256 conflict_behavior_choices = ("ask","skip","quit")12571258def__init__(self):1259 Command.__init__(self)1260 P4UserMap.__init__(self)1261 self.options = [1262 optparse.make_option("--origin", dest="origin"),1263 optparse.make_option("-M", dest="detectRenames", action="store_true"),1264# preserve the user, requires relevant p4 permissions1265 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1266 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1267 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1268 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1269 optparse.make_option("--conflict", dest="conflict_behavior",1270 choices=self.conflict_behavior_choices),1271 optparse.make_option("--branch", dest="branch"),1272]1273 self.description ="Submit changes from git to the perforce depot."1274 self.usage +=" [name of git branch to submit into perforce depot]"1275 self.origin =""1276 self.detectRenames =False1277 self.preserveUser =gitConfigBool("git-p4.preserveUser")1278 self.dry_run =False1279 self.prepare_p4_only =False1280 self.conflict_behavior =None1281 self.isWindows = (platform.system() =="Windows")1282 self.exportLabels =False1283 self.p4HasMoveCommand =p4_has_move_command()1284 self.branch =None12851286ifgitConfig('git-p4.largeFileSystem'):1287die("Large file system not supported for git-p4 submit command. Please remove it from config.")12881289defcheck(self):1290iflen(p4CmdList("opened ...")) >0:1291die("You have files opened with perforce! Close them before starting the sync.")12921293defseparate_jobs_from_description(self, message):1294"""Extract and return a possible Jobs field in the commit1295 message. It goes into a separate section in the p4 change1296 specification.12971298 A jobs line starts with "Jobs:" and looks like a new field1299 in a form. Values are white-space separated on the same1300 line or on following lines that start with a tab.13011302 This does not parse and extract the full git commit message1303 like a p4 form. It just sees the Jobs: line as a marker1304 to pass everything from then on directly into the p4 form,1305 but outside the description section.13061307 Return a tuple (stripped log message, jobs string)."""13081309 m = re.search(r'^Jobs:', message, re.MULTILINE)1310if m is None:1311return(message,None)13121313 jobtext = message[m.start():]1314 stripped_message = message[:m.start()].rstrip()1315return(stripped_message, jobtext)13161317defprepareLogMessage(self, template, message, jobs):1318"""Edits the template returned from "p4 change -o" to insert1319 the message in the Description field, and the jobs text in1320 the Jobs field."""1321 result =""13221323 inDescriptionSection =False13241325for line in template.split("\n"):1326if line.startswith("#"):1327 result += line +"\n"1328continue13291330if inDescriptionSection:1331if line.startswith("Files:")or line.startswith("Jobs:"):1332 inDescriptionSection =False1333# insert Jobs section1334if jobs:1335 result += jobs +"\n"1336else:1337continue1338else:1339if line.startswith("Description:"):1340 inDescriptionSection =True1341 line +="\n"1342for messageLine in message.split("\n"):1343 line +="\t"+ messageLine +"\n"13441345 result += line +"\n"13461347return result13481349defpatchRCSKeywords(self,file, pattern):1350# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1351(handle, outFileName) = tempfile.mkstemp(dir='.')1352try:1353 outFile = os.fdopen(handle,"w+")1354 inFile =open(file,"r")1355 regexp = re.compile(pattern, re.VERBOSE)1356for line in inFile.readlines():1357 line = regexp.sub(r'$\1$', line)1358 outFile.write(line)1359 inFile.close()1360 outFile.close()1361# Forcibly overwrite the original file1362 os.unlink(file)1363 shutil.move(outFileName,file)1364except:1365# cleanup our temporary file1366 os.unlink(outFileName)1367print"Failed to strip RCS keywords in%s"%file1368raise13691370print"Patched up RCS keywords in%s"%file13711372defp4UserForCommit(self,id):1373# Return the tuple (perforce user,git email) for a given git commit id1374 self.getUserMapFromPerforceServer()1375 gitEmail =read_pipe(["git","log","--max-count=1",1376"--format=%ae",id])1377 gitEmail = gitEmail.strip()1378if not self.emails.has_key(gitEmail):1379return(None,gitEmail)1380else:1381return(self.emails[gitEmail],gitEmail)13821383defcheckValidP4Users(self,commits):1384# check if any git authors cannot be mapped to p4 users1385foridin commits:1386(user,email) = self.p4UserForCommit(id)1387if not user:1388 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1389ifgitConfigBool("git-p4.allowMissingP4Users"):1390print"%s"% msg1391else:1392die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)13931394deflastP4Changelist(self):1395# Get back the last changelist number submitted in this client spec. This1396# then gets used to patch up the username in the change. If the same1397# client spec is being used by multiple processes then this might go1398# wrong.1399 results =p4CmdList("client -o")# find the current client1400 client =None1401for r in results:1402if r.has_key('Client'):1403 client = r['Client']1404break1405if not client:1406die("could not get client spec")1407 results =p4CmdList(["changes","-c", client,"-m","1"])1408for r in results:1409if r.has_key('change'):1410return r['change']1411die("Could not get changelist number for last submit - cannot patch up user details")14121413defmodifyChangelistUser(self, changelist, newUser):1414# fixup the user field of a changelist after it has been submitted.1415 changes =p4CmdList("change -o%s"% changelist)1416iflen(changes) !=1:1417die("Bad output from p4 change modifying%sto user%s"%1418(changelist, newUser))14191420 c = changes[0]1421if c['User'] == newUser:return# nothing to do1422 c['User'] = newUser1423input= marshal.dumps(c)14241425 result =p4CmdList("change -f -i", stdin=input)1426for r in result:1427if r.has_key('code'):1428if r['code'] =='error':1429die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1430if r.has_key('data'):1431print("Updated user field for changelist%sto%s"% (changelist, newUser))1432return1433die("Could not modify user field of changelist%sto%s"% (changelist, newUser))14341435defcanChangeChangelists(self):1436# check to see if we have p4 admin or super-user permissions, either of1437# which are required to modify changelists.1438 results =p4CmdList(["protects", self.depotPath])1439for r in results:1440if r.has_key('perm'):1441if r['perm'] =='admin':1442return11443if r['perm'] =='super':1444return11445return014461447defprepareSubmitTemplate(self):1448"""Run "p4 change -o" to grab a change specification template.1449 This does not use "p4 -G", as it is nice to keep the submission1450 template in original order, since a human might edit it.14511452 Remove lines in the Files section that show changes to files1453 outside the depot path we're committing into."""14541455 template =""1456 inFilesSection =False1457for line inp4_read_pipe_lines(['change','-o']):1458if line.endswith("\r\n"):1459 line = line[:-2] +"\n"1460if inFilesSection:1461if line.startswith("\t"):1462# path starts and ends with a tab1463 path = line[1:]1464 lastTab = path.rfind("\t")1465if lastTab != -1:1466 path = path[:lastTab]1467if notp4PathStartsWith(path, self.depotPath):1468continue1469else:1470 inFilesSection =False1471else:1472if line.startswith("Files:"):1473 inFilesSection =True14741475 template += line14761477return template14781479defedit_template(self, template_file):1480"""Invoke the editor to let the user change the submission1481 message. Return true if okay to continue with the submit."""14821483# if configured to skip the editing part, just submit1484ifgitConfigBool("git-p4.skipSubmitEdit"):1485return True14861487# look at the modification time, to check later if the user saved1488# the file1489 mtime = os.stat(template_file).st_mtime14901491# invoke the editor1492if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1493 editor = os.environ.get("P4EDITOR")1494else:1495 editor =read_pipe("git var GIT_EDITOR").strip()1496system(["sh","-c", ('%s"$@"'% editor), editor, template_file])14971498# If the file was not saved, prompt to see if this patch should1499# be skipped. But skip this verification step if configured so.1500ifgitConfigBool("git-p4.skipSubmitEditCheck"):1501return True15021503# modification time updated means user saved the file1504if os.stat(template_file).st_mtime > mtime:1505return True15061507while True:1508 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1509if response =='y':1510return True1511if response =='n':1512return False15131514defget_diff_description(self, editedFiles, filesToAdd):1515# diff1516if os.environ.has_key("P4DIFF"):1517del(os.environ["P4DIFF"])1518 diff =""1519for editedFile in editedFiles:1520 diff +=p4_read_pipe(['diff','-du',1521wildcard_encode(editedFile)])15221523# new file diff1524 newdiff =""1525for newFile in filesToAdd:1526 newdiff +="==== new file ====\n"1527 newdiff +="--- /dev/null\n"1528 newdiff +="+++%s\n"% newFile1529 f =open(newFile,"r")1530for line in f.readlines():1531 newdiff +="+"+ line1532 f.close()15331534return(diff + newdiff).replace('\r\n','\n')15351536defapplyCommit(self,id):1537"""Apply one commit, return True if it succeeded."""15381539print"Applying",read_pipe(["git","show","-s",1540"--format=format:%h%s",id])15411542(p4User, gitEmail) = self.p4UserForCommit(id)15431544 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1545 filesToAdd =set()1546 filesToDelete =set()1547 editedFiles =set()1548 pureRenameCopy =set()1549 filesToChangeExecBit = {}15501551for line in diff:1552 diff =parseDiffTreeEntry(line)1553 modifier = diff['status']1554 path = diff['src']1555if modifier =="M":1556p4_edit(path)1557ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1558 filesToChangeExecBit[path] = diff['dst_mode']1559 editedFiles.add(path)1560elif modifier =="A":1561 filesToAdd.add(path)1562 filesToChangeExecBit[path] = diff['dst_mode']1563if path in filesToDelete:1564 filesToDelete.remove(path)1565elif modifier =="D":1566 filesToDelete.add(path)1567if path in filesToAdd:1568 filesToAdd.remove(path)1569elif modifier =="C":1570 src, dest = diff['src'], diff['dst']1571p4_integrate(src, dest)1572 pureRenameCopy.add(dest)1573if diff['src_sha1'] != diff['dst_sha1']:1574p4_edit(dest)1575 pureRenameCopy.discard(dest)1576ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1577p4_edit(dest)1578 pureRenameCopy.discard(dest)1579 filesToChangeExecBit[dest] = diff['dst_mode']1580if self.isWindows:1581# turn off read-only attribute1582 os.chmod(dest, stat.S_IWRITE)1583 os.unlink(dest)1584 editedFiles.add(dest)1585elif modifier =="R":1586 src, dest = diff['src'], diff['dst']1587if self.p4HasMoveCommand:1588p4_edit(src)# src must be open before move1589p4_move(src, dest)# opens for (move/delete, move/add)1590else:1591p4_integrate(src, dest)1592if diff['src_sha1'] != diff['dst_sha1']:1593p4_edit(dest)1594else:1595 pureRenameCopy.add(dest)1596ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1597if not self.p4HasMoveCommand:1598p4_edit(dest)# with move: already open, writable1599 filesToChangeExecBit[dest] = diff['dst_mode']1600if not self.p4HasMoveCommand:1601if self.isWindows:1602 os.chmod(dest, stat.S_IWRITE)1603 os.unlink(dest)1604 filesToDelete.add(src)1605 editedFiles.add(dest)1606else:1607die("unknown modifier%sfor%s"% (modifier, path))16081609 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1610 patchcmd = diffcmd +" | git apply "1611 tryPatchCmd = patchcmd +"--check -"1612 applyPatchCmd = patchcmd +"--check --apply -"1613 patch_succeeded =True16141615if os.system(tryPatchCmd) !=0:1616 fixed_rcs_keywords =False1617 patch_succeeded =False1618print"Unfortunately applying the change failed!"16191620# Patch failed, maybe it's just RCS keyword woes. Look through1621# the patch to see if that's possible.1622ifgitConfigBool("git-p4.attemptRCSCleanup"):1623file=None1624 pattern =None1625 kwfiles = {}1626forfilein editedFiles | filesToDelete:1627# did this file's delta contain RCS keywords?1628 pattern =p4_keywords_regexp_for_file(file)16291630if pattern:1631# this file is a possibility...look for RCS keywords.1632 regexp = re.compile(pattern, re.VERBOSE)1633for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1634if regexp.search(line):1635if verbose:1636print"got keyword match on%sin%sin%s"% (pattern, line,file)1637 kwfiles[file] = pattern1638break16391640forfilein kwfiles:1641if verbose:1642print"zapping%swith%s"% (line,pattern)1643# File is being deleted, so not open in p4. Must1644# disable the read-only bit on windows.1645if self.isWindows andfilenot in editedFiles:1646 os.chmod(file, stat.S_IWRITE)1647 self.patchRCSKeywords(file, kwfiles[file])1648 fixed_rcs_keywords =True16491650if fixed_rcs_keywords:1651print"Retrying the patch with RCS keywords cleaned up"1652if os.system(tryPatchCmd) ==0:1653 patch_succeeded =True16541655if not patch_succeeded:1656for f in editedFiles:1657p4_revert(f)1658return False16591660#1661# Apply the patch for real, and do add/delete/+x handling.1662#1663system(applyPatchCmd)16641665for f in filesToAdd:1666p4_add(f)1667for f in filesToDelete:1668p4_revert(f)1669p4_delete(f)16701671# Set/clear executable bits1672for f in filesToChangeExecBit.keys():1673 mode = filesToChangeExecBit[f]1674setP4ExecBit(f, mode)16751676#1677# Build p4 change description, starting with the contents1678# of the git commit message.1679#1680 logMessage =extractLogMessageFromGitCommit(id)1681 logMessage = logMessage.strip()1682(logMessage, jobs) = self.separate_jobs_from_description(logMessage)16831684 template = self.prepareSubmitTemplate()1685 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)16861687if self.preserveUser:1688 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User16891690if self.checkAuthorship and not self.p4UserIsMe(p4User):1691 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1692 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1693 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"16941695 separatorLine ="######## everything below this line is just the diff #######\n"1696if not self.prepare_p4_only:1697 submitTemplate += separatorLine1698 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)16991700(handle, fileName) = tempfile.mkstemp()1701 tmpFile = os.fdopen(handle,"w+b")1702if self.isWindows:1703 submitTemplate = submitTemplate.replace("\n","\r\n")1704 tmpFile.write(submitTemplate)1705 tmpFile.close()17061707if self.prepare_p4_only:1708#1709# Leave the p4 tree prepared, and the submit template around1710# and let the user decide what to do next1711#1712print1713print"P4 workspace prepared for submission."1714print"To submit or revert, go to client workspace"1715print" "+ self.clientPath1716print1717print"To submit, use\"p4 submit\"to write a new description,"1718print"or\"p4 submit -i <%s\"to use the one prepared by" \1719"\"git p4\"."% fileName1720print"You can delete the file\"%s\"when finished."% fileName17211722if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1723print"To preserve change ownership by user%s, you must\n" \1724"do\"p4 change -f <change>\"after submitting and\n" \1725"edit the User field."1726if pureRenameCopy:1727print"After submitting, renamed files must be re-synced."1728print"Invoke\"p4 sync -f\"on each of these files:"1729for f in pureRenameCopy:1730print" "+ f17311732print1733print"To revert the changes, use\"p4 revert ...\", and delete"1734print"the submit template file\"%s\""% fileName1735if filesToAdd:1736print"Since the commit adds new files, they must be deleted:"1737for f in filesToAdd:1738print" "+ f1739print1740return True17411742#1743# Let the user edit the change description, then submit it.1744#1745if self.edit_template(fileName):1746# read the edited message and submit1747 ret =True1748 tmpFile =open(fileName,"rb")1749 message = tmpFile.read()1750 tmpFile.close()1751if self.isWindows:1752 message = message.replace("\r\n","\n")1753 submitTemplate = message[:message.index(separatorLine)]1754p4_write_pipe(['submit','-i'], submitTemplate)17551756if self.preserveUser:1757if p4User:1758# Get last changelist number. Cannot easily get it from1759# the submit command output as the output is1760# unmarshalled.1761 changelist = self.lastP4Changelist()1762 self.modifyChangelistUser(changelist, p4User)17631764# The rename/copy happened by applying a patch that created a1765# new file. This leaves it writable, which confuses p4.1766for f in pureRenameCopy:1767p4_sync(f,"-f")17681769else:1770# skip this patch1771 ret =False1772print"Submission cancelled, undoing p4 changes."1773for f in editedFiles:1774p4_revert(f)1775for f in filesToAdd:1776p4_revert(f)1777 os.remove(f)1778for f in filesToDelete:1779p4_revert(f)17801781 os.remove(fileName)1782return ret17831784# Export git tags as p4 labels. Create a p4 label and then tag1785# with that.1786defexportGitTags(self, gitTags):1787 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1788iflen(validLabelRegexp) ==0:1789 validLabelRegexp = defaultLabelRegexp1790 m = re.compile(validLabelRegexp)17911792for name in gitTags:17931794if not m.match(name):1795if verbose:1796print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1797continue17981799# Get the p4 commit this corresponds to1800 logMessage =extractLogMessageFromGitCommit(name)1801 values =extractSettingsGitLog(logMessage)18021803if not values.has_key('change'):1804# a tag pointing to something not sent to p4; ignore1805if verbose:1806print"git tag%sdoes not give a p4 commit"% name1807continue1808else:1809 changelist = values['change']18101811# Get the tag details.1812 inHeader =True1813 isAnnotated =False1814 body = []1815for l inread_pipe_lines(["git","cat-file","-p", name]):1816 l = l.strip()1817if inHeader:1818if re.match(r'tag\s+', l):1819 isAnnotated =True1820elif re.match(r'\s*$', l):1821 inHeader =False1822continue1823else:1824 body.append(l)18251826if not isAnnotated:1827 body = ["lightweight tag imported by git p4\n"]18281829# Create the label - use the same view as the client spec we are using1830 clientSpec =getClientSpec()18311832 labelTemplate ="Label:%s\n"% name1833 labelTemplate +="Description:\n"1834for b in body:1835 labelTemplate +="\t"+ b +"\n"1836 labelTemplate +="View:\n"1837for depot_side in clientSpec.mappings:1838 labelTemplate +="\t%s\n"% depot_side18391840if self.dry_run:1841print"Would create p4 label%sfor tag"% name1842elif self.prepare_p4_only:1843print"Not creating p4 label%sfor tag due to option" \1844" --prepare-p4-only"% name1845else:1846p4_write_pipe(["label","-i"], labelTemplate)18471848# Use the label1849p4_system(["tag","-l", name] +1850["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])18511852if verbose:1853print"created p4 label for tag%s"% name18541855defrun(self, args):1856iflen(args) ==0:1857 self.master =currentGitBranch()1858iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1859die("Detecting current git branch failed!")1860eliflen(args) ==1:1861 self.master = args[0]1862if notbranchExists(self.master):1863die("Branch%sdoes not exist"% self.master)1864else:1865return False18661867 allowSubmit =gitConfig("git-p4.allowSubmit")1868iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1869die("%sis not in git-p4.allowSubmit"% self.master)18701871[upstream, settings] =findUpstreamBranchPoint()1872 self.depotPath = settings['depot-paths'][0]1873iflen(self.origin) ==0:1874 self.origin = upstream18751876if self.preserveUser:1877if not self.canChangeChangelists():1878die("Cannot preserve user names without p4 super-user or admin permissions")18791880# if not set from the command line, try the config file1881if self.conflict_behavior is None:1882 val =gitConfig("git-p4.conflict")1883if val:1884if val not in self.conflict_behavior_choices:1885die("Invalid value '%s' for config git-p4.conflict"% val)1886else:1887 val ="ask"1888 self.conflict_behavior = val18891890if self.verbose:1891print"Origin branch is "+ self.origin18921893iflen(self.depotPath) ==0:1894print"Internal error: cannot locate perforce depot path from existing branches"1895 sys.exit(128)18961897 self.useClientSpec =False1898ifgitConfigBool("git-p4.useclientspec"):1899 self.useClientSpec =True1900if self.useClientSpec:1901 self.clientSpecDirs =getClientSpec()19021903# Check for the existance of P4 branches1904 branchesDetected = (len(p4BranchesInGit().keys()) >1)19051906if self.useClientSpec and not branchesDetected:1907# all files are relative to the client spec1908 self.clientPath =getClientRoot()1909else:1910 self.clientPath =p4Where(self.depotPath)19111912if self.clientPath =="":1913die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)19141915print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1916 self.oldWorkingDirectory = os.getcwd()19171918# ensure the clientPath exists1919 new_client_dir =False1920if not os.path.exists(self.clientPath):1921 new_client_dir =True1922 os.makedirs(self.clientPath)19231924chdir(self.clientPath, is_client_path=True)1925if self.dry_run:1926print"Would synchronize p4 checkout in%s"% self.clientPath1927else:1928print"Synchronizing p4 checkout..."1929if new_client_dir:1930# old one was destroyed, and maybe nobody told p41931p4_sync("...","-f")1932else:1933p4_sync("...")1934 self.check()19351936 commits = []1937for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1938 commits.append(line.strip())1939 commits.reverse()19401941if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1942 self.checkAuthorship =False1943else:1944 self.checkAuthorship =True19451946if self.preserveUser:1947 self.checkValidP4Users(commits)19481949#1950# Build up a set of options to be passed to diff when1951# submitting each commit to p4.1952#1953if self.detectRenames:1954# command-line -M arg1955 self.diffOpts ="-M"1956else:1957# If not explicitly set check the config variable1958 detectRenames =gitConfig("git-p4.detectRenames")19591960if detectRenames.lower() =="false"or detectRenames =="":1961 self.diffOpts =""1962elif detectRenames.lower() =="true":1963 self.diffOpts ="-M"1964else:1965 self.diffOpts ="-M%s"% detectRenames19661967# no command-line arg for -C or --find-copies-harder, just1968# config variables1969 detectCopies =gitConfig("git-p4.detectCopies")1970if detectCopies.lower() =="false"or detectCopies =="":1971pass1972elif detectCopies.lower() =="true":1973 self.diffOpts +=" -C"1974else:1975 self.diffOpts +=" -C%s"% detectCopies19761977ifgitConfigBool("git-p4.detectCopiesHarder"):1978 self.diffOpts +=" --find-copies-harder"19791980#1981# Apply the commits, one at a time. On failure, ask if should1982# continue to try the rest of the patches, or quit.1983#1984if self.dry_run:1985print"Would apply"1986 applied = []1987 last =len(commits) -11988for i, commit inenumerate(commits):1989if self.dry_run:1990print" ",read_pipe(["git","show","-s",1991"--format=format:%h%s", commit])1992 ok =True1993else:1994 ok = self.applyCommit(commit)1995if ok:1996 applied.append(commit)1997else:1998if self.prepare_p4_only and i < last:1999print"Processing only the first commit due to option" \2000" --prepare-p4-only"2001break2002if i < last:2003 quit =False2004while True:2005# prompt for what to do, or use the option/variable2006if self.conflict_behavior =="ask":2007print"What do you want to do?"2008 response =raw_input("[s]kip this commit but apply"2009" the rest, or [q]uit? ")2010if not response:2011continue2012elif self.conflict_behavior =="skip":2013 response ="s"2014elif self.conflict_behavior =="quit":2015 response ="q"2016else:2017die("Unknown conflict_behavior '%s'"%2018 self.conflict_behavior)20192020if response[0] =="s":2021print"Skipping this commit, but applying the rest"2022break2023if response[0] =="q":2024print"Quitting"2025 quit =True2026break2027if quit:2028break20292030chdir(self.oldWorkingDirectory)20312032if self.dry_run:2033pass2034elif self.prepare_p4_only:2035pass2036eliflen(commits) ==len(applied):2037print"All commits applied!"20382039 sync =P4Sync()2040if self.branch:2041 sync.branch = self.branch2042 sync.run([])20432044 rebase =P4Rebase()2045 rebase.rebase()20462047else:2048iflen(applied) ==0:2049print"No commits applied."2050else:2051print"Applied only the commits marked with '*':"2052for c in commits:2053if c in applied:2054 star ="*"2055else:2056 star =" "2057print star,read_pipe(["git","show","-s",2058"--format=format:%h%s", c])2059print"You will have to do 'git p4 sync' and rebase."20602061ifgitConfigBool("git-p4.exportLabels"):2062 self.exportLabels =True20632064if self.exportLabels:2065 p4Labels =getP4Labels(self.depotPath)2066 gitTags =getGitTags()20672068 missingGitTags = gitTags - p4Labels2069 self.exportGitTags(missingGitTags)20702071# exit with error unless everything applied perfectly2072iflen(commits) !=len(applied):2073 sys.exit(1)20742075return True20762077classView(object):2078"""Represent a p4 view ("p4 help views"), and map files in a2079 repo according to the view."""20802081def__init__(self, client_name):2082 self.mappings = []2083 self.client_prefix ="//%s/"% client_name2084# cache results of "p4 where" to lookup client file locations2085 self.client_spec_path_cache = {}20862087defappend(self, view_line):2088"""Parse a view line, splitting it into depot and client2089 sides. Append to self.mappings, preserving order. This2090 is only needed for tag creation."""20912092# Split the view line into exactly two words. P4 enforces2093# structure on these lines that simplifies this quite a bit.2094#2095# Either or both words may be double-quoted.2096# Single quotes do not matter.2097# Double-quote marks cannot occur inside the words.2098# A + or - prefix is also inside the quotes.2099# There are no quotes unless they contain a space.2100# The line is already white-space stripped.2101# The two words are separated by a single space.2102#2103if view_line[0] =='"':2104# First word is double quoted. Find its end.2105 close_quote_index = view_line.find('"',1)2106if close_quote_index <=0:2107die("No first-word closing quote found:%s"% view_line)2108 depot_side = view_line[1:close_quote_index]2109# skip closing quote and space2110 rhs_index = close_quote_index +1+12111else:2112 space_index = view_line.find(" ")2113if space_index <=0:2114die("No word-splitting space found:%s"% view_line)2115 depot_side = view_line[0:space_index]2116 rhs_index = space_index +121172118# prefix + means overlay on previous mapping2119if depot_side.startswith("+"):2120 depot_side = depot_side[1:]21212122# prefix - means exclude this path, leave out of mappings2123 exclude =False2124if depot_side.startswith("-"):2125 exclude =True2126 depot_side = depot_side[1:]21272128if not exclude:2129 self.mappings.append(depot_side)21302131defconvert_client_path(self, clientFile):2132# chop off //client/ part to make it relative2133if not clientFile.startswith(self.client_prefix):2134die("No prefix '%s' on clientFile '%s'"%2135(self.client_prefix, clientFile))2136return clientFile[len(self.client_prefix):]21372138defupdate_client_spec_path_cache(self, files):2139""" Caching file paths by "p4 where" batch query """21402141# List depot file paths exclude that already cached2142 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]21432144iflen(fileArgs) ==0:2145return# All files in cache21462147 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2148for res in where_result:2149if"code"in res and res["code"] =="error":2150# assume error is "... file(s) not in client view"2151continue2152if"clientFile"not in res:2153die("No clientFile in 'p4 where' output")2154if"unmap"in res:2155# it will list all of them, but only one not unmap-ped2156continue2157ifgitConfigBool("core.ignorecase"):2158 res['depotFile'] = res['depotFile'].lower()2159 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])21602161# not found files or unmap files set to ""2162for depotFile in fileArgs:2163ifgitConfigBool("core.ignorecase"):2164 depotFile = depotFile.lower()2165if depotFile not in self.client_spec_path_cache:2166 self.client_spec_path_cache[depotFile] =""21672168defmap_in_client(self, depot_path):2169"""Return the relative location in the client where this2170 depot file should live. Returns "" if the file should2171 not be mapped in the client."""21722173ifgitConfigBool("core.ignorecase"):2174 depot_path = depot_path.lower()21752176if depot_path in self.client_spec_path_cache:2177return self.client_spec_path_cache[depot_path]21782179die("Error:%sis not found in client spec path"% depot_path )2180return""21812182classP4Sync(Command, P4UserMap):2183 delete_actions = ("delete","move/delete","purge")21842185def__init__(self):2186 Command.__init__(self)2187 P4UserMap.__init__(self)2188 self.options = [2189 optparse.make_option("--branch", dest="branch"),2190 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2191 optparse.make_option("--changesfile", dest="changesFile"),2192 optparse.make_option("--silent", dest="silent", action="store_true"),2193 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2194 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2195 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2196help="Import into refs/heads/ , not refs/remotes"),2197 optparse.make_option("--max-changes", dest="maxChanges",2198help="Maximum number of changes to import"),2199 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2200help="Internal block size to use when iteratively calling p4 changes"),2201 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2202help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2203 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2204help="Only sync files that are included in the Perforce Client Spec"),2205 optparse.make_option("-/", dest="cloneExclude",2206 action="append",type="string",2207help="exclude depot path"),2208]2209 self.description ="""Imports from Perforce into a git repository.\n2210 example:2211 //depot/my/project/ -- to import the current head2212 //depot/my/project/@all -- to import everything2213 //depot/my/project/@1,6 -- to import only from revision 1 to 622142215 (a ... is not needed in the path p4 specification, it's added implicitly)"""22162217 self.usage +=" //depot/path[@revRange]"2218 self.silent =False2219 self.createdBranches =set()2220 self.committedChanges =set()2221 self.branch =""2222 self.detectBranches =False2223 self.detectLabels =False2224 self.importLabels =False2225 self.changesFile =""2226 self.syncWithOrigin =True2227 self.importIntoRemotes =True2228 self.maxChanges =""2229 self.changes_block_size =None2230 self.keepRepoPath =False2231 self.depotPaths =None2232 self.p4BranchesInGit = []2233 self.cloneExclude = []2234 self.useClientSpec =False2235 self.useClientSpec_from_options =False2236 self.clientSpecDirs =None2237 self.tempBranches = []2238 self.tempBranchLocation ="git-p4-tmp"2239 self.largeFileSystem =None22402241ifgitConfig('git-p4.largeFileSystem'):2242 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2243 self.largeFileSystem =largeFileSystemConstructor(2244lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2245)22462247ifgitConfig("git-p4.syncFromOrigin") =="false":2248 self.syncWithOrigin =False22492250# This is required for the "append" cloneExclude action2251defensure_value(self, attr, value):2252if nothasattr(self, attr)orgetattr(self, attr)is None:2253setattr(self, attr, value)2254returngetattr(self, attr)22552256# Force a checkpoint in fast-import and wait for it to finish2257defcheckpoint(self):2258 self.gitStream.write("checkpoint\n\n")2259 self.gitStream.write("progress checkpoint\n\n")2260 out = self.gitOutput.readline()2261if self.verbose:2262print"checkpoint finished: "+ out22632264defextractFilesFromCommit(self, commit):2265 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2266for path in self.cloneExclude]2267 files = []2268 fnum =02269while commit.has_key("depotFile%s"% fnum):2270 path = commit["depotFile%s"% fnum]22712272if[p for p in self.cloneExclude2273ifp4PathStartsWith(path, p)]:2274 found =False2275else:2276 found = [p for p in self.depotPaths2277ifp4PathStartsWith(path, p)]2278if not found:2279 fnum = fnum +12280continue22812282file= {}2283file["path"] = path2284file["rev"] = commit["rev%s"% fnum]2285file["action"] = commit["action%s"% fnum]2286file["type"] = commit["type%s"% fnum]2287 files.append(file)2288 fnum = fnum +12289return files22902291defstripRepoPath(self, path, prefixes):2292"""When streaming files, this is called to map a p4 depot path2293 to where it should go in git. The prefixes are either2294 self.depotPaths, or self.branchPrefixes in the case of2295 branch detection."""22962297if self.useClientSpec:2298# branch detection moves files up a level (the branch name)2299# from what client spec interpretation gives2300 path = self.clientSpecDirs.map_in_client(path)2301if self.detectBranches:2302for b in self.knownBranches:2303if path.startswith(b +"/"):2304 path = path[len(b)+1:]23052306elif self.keepRepoPath:2307# Preserve everything in relative path name except leading2308# //depot/; just look at first prefix as they all should2309# be in the same depot.2310 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2311ifp4PathStartsWith(path, depot):2312 path = path[len(depot):]23132314else:2315for p in prefixes:2316ifp4PathStartsWith(path, p):2317 path = path[len(p):]2318break23192320 path =wildcard_decode(path)2321return path23222323defsplitFilesIntoBranches(self, commit):2324"""Look at each depotFile in the commit to figure out to what2325 branch it belongs."""23262327if self.clientSpecDirs:2328 files = self.extractFilesFromCommit(commit)2329 self.clientSpecDirs.update_client_spec_path_cache(files)23302331 branches = {}2332 fnum =02333while commit.has_key("depotFile%s"% fnum):2334 path = commit["depotFile%s"% fnum]2335 found = [p for p in self.depotPaths2336ifp4PathStartsWith(path, p)]2337if not found:2338 fnum = fnum +12339continue23402341file= {}2342file["path"] = path2343file["rev"] = commit["rev%s"% fnum]2344file["action"] = commit["action%s"% fnum]2345file["type"] = commit["type%s"% fnum]2346 fnum = fnum +123472348# start with the full relative path where this file would2349# go in a p4 client2350if self.useClientSpec:2351 relPath = self.clientSpecDirs.map_in_client(path)2352else:2353 relPath = self.stripRepoPath(path, self.depotPaths)23542355for branch in self.knownBranches.keys():2356# add a trailing slash so that a commit into qt/4.2foo2357# doesn't end up in qt/4.2, e.g.2358if relPath.startswith(branch +"/"):2359if branch not in branches:2360 branches[branch] = []2361 branches[branch].append(file)2362break23632364return branches23652366defwriteToGitStream(self, gitMode, relPath, contents):2367 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2368 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2369for d in contents:2370 self.gitStream.write(d)2371 self.gitStream.write('\n')23722373# output one file from the P4 stream2374# - helper for streamP4Files23752376defstreamOneP4File(self,file, contents):2377 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2378if verbose:2379 size =int(self.stream_file['fileSize'])2380 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2381 sys.stdout.flush()23822383(type_base, type_mods) =split_p4_type(file["type"])23842385 git_mode ="100644"2386if"x"in type_mods:2387 git_mode ="100755"2388if type_base =="symlink":2389 git_mode ="120000"2390# p4 print on a symlink sometimes contains "target\n";2391# if it does, remove the newline2392 data =''.join(contents)2393if not data:2394# Some version of p4 allowed creating a symlink that pointed2395# to nothing. This causes p4 errors when checking out such2396# a change, and errors here too. Work around it by ignoring2397# the bad symlink; hopefully a future change fixes it.2398print"\nIgnoring empty symlink in%s"%file['depotFile']2399return2400elif data[-1] =='\n':2401 contents = [data[:-1]]2402else:2403 contents = [data]24042405if type_base =="utf16":2406# p4 delivers different text in the python output to -G2407# than it does when using "print -o", or normal p4 client2408# operations. utf16 is converted to ascii or utf8, perhaps.2409# But ascii text saved as -t utf16 is completely mangled.2410# Invoke print -o to get the real contents.2411#2412# On windows, the newlines will always be mangled by print, so put2413# them back too. This is not needed to the cygwin windows version,2414# just the native "NT" type.2415#2416 text =p4_read_pipe(['print','-q','-o','-',"%s@%s"% (file['depotFile'],file['change']) ])2417ifp4_version_string().find("/NT") >=0:2418 text = text.replace("\r\n","\n")2419 contents = [ text ]24202421if type_base =="apple":2422# Apple filetype files will be streamed as a concatenation of2423# its appledouble header and the contents. This is useless2424# on both macs and non-macs. If using "print -q -o xx", it2425# will create "xx" with the data, and "%xx" with the header.2426# This is also not very useful.2427#2428# Ideally, someday, this script can learn how to generate2429# appledouble files directly and import those to git, but2430# non-mac machines can never find a use for apple filetype.2431print"\nIgnoring apple filetype file%s"%file['depotFile']2432return24332434# Note that we do not try to de-mangle keywords on utf16 files,2435# even though in theory somebody may want that.2436 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2437if pattern:2438 regexp = re.compile(pattern, re.VERBOSE)2439 text =''.join(contents)2440 text = regexp.sub(r'$\1$', text)2441 contents = [ text ]24422443if self.largeFileSystem:2444(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)24452446 self.writeToGitStream(git_mode, relPath, contents)24472448defstreamOneP4Deletion(self,file):2449 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2450if verbose:2451 sys.stdout.write("delete%s\n"% relPath)2452 sys.stdout.flush()2453 self.gitStream.write("D%s\n"% relPath)24542455if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2456 self.largeFileSystem.removeLargeFile(relPath)24572458# handle another chunk of streaming data2459defstreamP4FilesCb(self, marshalled):24602461# catch p4 errors and complain2462 err =None2463if"code"in marshalled:2464if marshalled["code"] =="error":2465if"data"in marshalled:2466 err = marshalled["data"].rstrip()24672468if not err and'fileSize'in self.stream_file:2469 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2470if required_bytes >0:2471 err ='Not enough space left on%s! Free at least%iMB.'% (2472 os.getcwd(), required_bytes/1024/10242473)24742475if err:2476 f =None2477if self.stream_have_file_info:2478if"depotFile"in self.stream_file:2479 f = self.stream_file["depotFile"]2480# force a failure in fast-import, else an empty2481# commit will be made2482 self.gitStream.write("\n")2483 self.gitStream.write("die-now\n")2484 self.gitStream.close()2485# ignore errors, but make sure it exits first2486 self.importProcess.wait()2487if f:2488die("Error from p4 print for%s:%s"% (f, err))2489else:2490die("Error from p4 print:%s"% err)24912492if marshalled.has_key('depotFile')and self.stream_have_file_info:2493# start of a new file - output the old one first2494 self.streamOneP4File(self.stream_file, self.stream_contents)2495 self.stream_file = {}2496 self.stream_contents = []2497 self.stream_have_file_info =False24982499# pick up the new file information... for the2500# 'data' field we need to append to our array2501for k in marshalled.keys():2502if k =='data':2503if'streamContentSize'not in self.stream_file:2504 self.stream_file['streamContentSize'] =02505 self.stream_file['streamContentSize'] +=len(marshalled['data'])2506 self.stream_contents.append(marshalled['data'])2507else:2508 self.stream_file[k] = marshalled[k]25092510if(verbose and2511'streamContentSize'in self.stream_file and2512'fileSize'in self.stream_file and2513'depotFile'in self.stream_file):2514 size =int(self.stream_file["fileSize"])2515if size >0:2516 progress =100*self.stream_file['streamContentSize']/size2517 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2518 sys.stdout.flush()25192520 self.stream_have_file_info =True25212522# Stream directly from "p4 files" into "git fast-import"2523defstreamP4Files(self, files):2524 filesForCommit = []2525 filesToRead = []2526 filesToDelete = []25272528for f in files:2529# if using a client spec, only add the files that have2530# a path in the client2531if self.clientSpecDirs:2532if self.clientSpecDirs.map_in_client(f['path']) =="":2533continue25342535 filesForCommit.append(f)2536if f['action']in self.delete_actions:2537 filesToDelete.append(f)2538else:2539 filesToRead.append(f)25402541# deleted files...2542for f in filesToDelete:2543 self.streamOneP4Deletion(f)25442545iflen(filesToRead) >0:2546 self.stream_file = {}2547 self.stream_contents = []2548 self.stream_have_file_info =False25492550# curry self argument2551defstreamP4FilesCbSelf(entry):2552 self.streamP4FilesCb(entry)25532554 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]25552556p4CmdList(["-x","-","print"],2557 stdin=fileArgs,2558 cb=streamP4FilesCbSelf)25592560# do the last chunk2561if self.stream_file.has_key('depotFile'):2562 self.streamOneP4File(self.stream_file, self.stream_contents)25632564defmake_email(self, userid):2565if userid in self.users:2566return self.users[userid]2567else:2568return"%s<a@b>"% userid25692570# Stream a p4 tag2571defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2572if verbose:2573print"writing tag%sfor commit%s"% (labelName, commit)2574 gitStream.write("tag%s\n"% labelName)2575 gitStream.write("from%s\n"% commit)25762577if labelDetails.has_key('Owner'):2578 owner = labelDetails["Owner"]2579else:2580 owner =None25812582# Try to use the owner of the p4 label, or failing that,2583# the current p4 user id.2584if owner:2585 email = self.make_email(owner)2586else:2587 email = self.make_email(self.p4UserId())2588 tagger ="%s %s %s"% (email, epoch, self.tz)25892590 gitStream.write("tagger%s\n"% tagger)25912592print"labelDetails=",labelDetails2593if labelDetails.has_key('Description'):2594 description = labelDetails['Description']2595else:2596 description ='Label from git p4'25972598 gitStream.write("data%d\n"%len(description))2599 gitStream.write(description)2600 gitStream.write("\n")26012602defcommit(self, details, files, branch, parent =""):2603 epoch = details["time"]2604 author = details["user"]26052606if self.verbose:2607print"commit into%s"% branch26082609# start with reading files; if that fails, we should not2610# create a commit.2611 new_files = []2612for f in files:2613if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2614 new_files.append(f)2615else:2616 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])26172618if self.clientSpecDirs:2619 self.clientSpecDirs.update_client_spec_path_cache(files)26202621 self.gitStream.write("commit%s\n"% branch)2622# gitStream.write("mark :%s\n" % details["change"])2623 self.committedChanges.add(int(details["change"]))2624 committer =""2625if author not in self.users:2626 self.getUserMapFromPerforceServer()2627 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)26282629 self.gitStream.write("committer%s\n"% committer)26302631 self.gitStream.write("data <<EOT\n")2632 self.gitStream.write(details["desc"])2633 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2634(','.join(self.branchPrefixes), details["change"]))2635iflen(details['options']) >0:2636 self.gitStream.write(": options =%s"% details['options'])2637 self.gitStream.write("]\nEOT\n\n")26382639iflen(parent) >0:2640if self.verbose:2641print"parent%s"% parent2642 self.gitStream.write("from%s\n"% parent)26432644 self.streamP4Files(new_files)2645 self.gitStream.write("\n")26462647 change =int(details["change"])26482649if self.labels.has_key(change):2650 label = self.labels[change]2651 labelDetails = label[0]2652 labelRevisions = label[1]2653if self.verbose:2654print"Change%sis labelled%s"% (change, labelDetails)26552656 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2657for p in self.branchPrefixes])26582659iflen(files) ==len(labelRevisions):26602661 cleanedFiles = {}2662for info in files:2663if info["action"]in self.delete_actions:2664continue2665 cleanedFiles[info["depotFile"]] = info["rev"]26662667if cleanedFiles == labelRevisions:2668 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)26692670else:2671if not self.silent:2672print("Tag%sdoes not match with change%s: files do not match."2673% (labelDetails["label"], change))26742675else:2676if not self.silent:2677print("Tag%sdoes not match with change%s: file count is different."2678% (labelDetails["label"], change))26792680# Build a dictionary of changelists and labels, for "detect-labels" option.2681defgetLabels(self):2682 self.labels = {}26832684 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2685iflen(l) >0and not self.silent:2686print"Finding files belonging to labels in%s"% `self.depotPaths`26872688for output in l:2689 label = output["label"]2690 revisions = {}2691 newestChange =02692if self.verbose:2693print"Querying files for label%s"% label2694forfileinp4CmdList(["files"] +2695["%s...@%s"% (p, label)2696for p in self.depotPaths]):2697 revisions[file["depotFile"]] =file["rev"]2698 change =int(file["change"])2699if change > newestChange:2700 newestChange = change27012702 self.labels[newestChange] = [output, revisions]27032704if self.verbose:2705print"Label changes:%s"% self.labels.keys()27062707# Import p4 labels as git tags. A direct mapping does not2708# exist, so assume that if all the files are at the same revision2709# then we can use that, or it's something more complicated we should2710# just ignore.2711defimportP4Labels(self, stream, p4Labels):2712if verbose:2713print"import p4 labels: "+' '.join(p4Labels)27142715 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2716 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2717iflen(validLabelRegexp) ==0:2718 validLabelRegexp = defaultLabelRegexp2719 m = re.compile(validLabelRegexp)27202721for name in p4Labels:2722 commitFound =False27232724if not m.match(name):2725if verbose:2726print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2727continue27282729if name in ignoredP4Labels:2730continue27312732 labelDetails =p4CmdList(['label',"-o", name])[0]27332734# get the most recent changelist for each file in this label2735 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2736for p in self.depotPaths])27372738if change.has_key('change'):2739# find the corresponding git commit; take the oldest commit2740 changelist =int(change['change'])2741 gitCommit =read_pipe(["git","rev-list","--max-count=1",2742"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2743iflen(gitCommit) ==0:2744print"could not find git commit for changelist%d"% changelist2745else:2746 gitCommit = gitCommit.strip()2747 commitFound =True2748# Convert from p4 time format2749try:2750 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2751exceptValueError:2752print"Could not convert label time%s"% labelDetails['Update']2753 tmwhen =127542755 when =int(time.mktime(tmwhen))2756 self.streamTag(stream, name, labelDetails, gitCommit, when)2757if verbose:2758print"p4 label%smapped to git commit%s"% (name, gitCommit)2759else:2760if verbose:2761print"Label%shas no changelists - possibly deleted?"% name27622763if not commitFound:2764# We can't import this label; don't try again as it will get very2765# expensive repeatedly fetching all the files for labels that will2766# never be imported. If the label is moved in the future, the2767# ignore will need to be removed manually.2768system(["git","config","--add","git-p4.ignoredP4Labels", name])27692770defguessProjectName(self):2771for p in self.depotPaths:2772if p.endswith("/"):2773 p = p[:-1]2774 p = p[p.strip().rfind("/") +1:]2775if not p.endswith("/"):2776 p +="/"2777return p27782779defgetBranchMapping(self):2780 lostAndFoundBranches =set()27812782 user =gitConfig("git-p4.branchUser")2783iflen(user) >0:2784 command ="branches -u%s"% user2785else:2786 command ="branches"27872788for info inp4CmdList(command):2789 details =p4Cmd(["branch","-o", info["branch"]])2790 viewIdx =02791while details.has_key("View%s"% viewIdx):2792 paths = details["View%s"% viewIdx].split(" ")2793 viewIdx = viewIdx +12794# require standard //depot/foo/... //depot/bar/... mapping2795iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2796continue2797 source = paths[0]2798 destination = paths[1]2799## HACK2800ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2801 source = source[len(self.depotPaths[0]):-4]2802 destination = destination[len(self.depotPaths[0]):-4]28032804if destination in self.knownBranches:2805if not self.silent:2806print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2807print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2808continue28092810 self.knownBranches[destination] = source28112812 lostAndFoundBranches.discard(destination)28132814if source not in self.knownBranches:2815 lostAndFoundBranches.add(source)28162817# Perforce does not strictly require branches to be defined, so we also2818# check git config for a branch list.2819#2820# Example of branch definition in git config file:2821# [git-p4]2822# branchList=main:branchA2823# branchList=main:branchB2824# branchList=branchA:branchC2825 configBranches =gitConfigList("git-p4.branchList")2826for branch in configBranches:2827if branch:2828(source, destination) = branch.split(":")2829 self.knownBranches[destination] = source28302831 lostAndFoundBranches.discard(destination)28322833if source not in self.knownBranches:2834 lostAndFoundBranches.add(source)283528362837for branch in lostAndFoundBranches:2838 self.knownBranches[branch] = branch28392840defgetBranchMappingFromGitBranches(self):2841 branches =p4BranchesInGit(self.importIntoRemotes)2842for branch in branches.keys():2843if branch =="master":2844 branch ="main"2845else:2846 branch = branch[len(self.projectName):]2847 self.knownBranches[branch] = branch28482849defupdateOptionDict(self, d):2850 option_keys = {}2851if self.keepRepoPath:2852 option_keys['keepRepoPath'] =128532854 d["options"] =' '.join(sorted(option_keys.keys()))28552856defreadOptions(self, d):2857 self.keepRepoPath = (d.has_key('options')2858and('keepRepoPath'in d['options']))28592860defgitRefForBranch(self, branch):2861if branch =="main":2862return self.refPrefix +"master"28632864iflen(branch) <=0:2865return branch28662867return self.refPrefix + self.projectName + branch28682869defgitCommitByP4Change(self, ref, change):2870if self.verbose:2871print"looking in ref "+ ref +" for change%susing bisect..."% change28722873 earliestCommit =""2874 latestCommit =parseRevision(ref)28752876while True:2877if self.verbose:2878print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2879 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2880iflen(next) ==0:2881if self.verbose:2882print"argh"2883return""2884 log =extractLogMessageFromGitCommit(next)2885 settings =extractSettingsGitLog(log)2886 currentChange =int(settings['change'])2887if self.verbose:2888print"current change%s"% currentChange28892890if currentChange == change:2891if self.verbose:2892print"found%s"% next2893return next28942895if currentChange < change:2896 earliestCommit ="^%s"% next2897else:2898 latestCommit ="%s"% next28992900return""29012902defimportNewBranch(self, branch, maxChange):2903# make fast-import flush all changes to disk and update the refs using the checkpoint2904# command so that we can try to find the branch parent in the git history2905 self.gitStream.write("checkpoint\n\n");2906 self.gitStream.flush();2907 branchPrefix = self.depotPaths[0] + branch +"/"2908range="@1,%s"% maxChange2909#print "prefix" + branchPrefix2910 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2911iflen(changes) <=0:2912return False2913 firstChange = changes[0]2914#print "first change in branch: %s" % firstChange2915 sourceBranch = self.knownBranches[branch]2916 sourceDepotPath = self.depotPaths[0] + sourceBranch2917 sourceRef = self.gitRefForBranch(sourceBranch)2918#print "source " + sourceBranch29192920 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2921#print "branch parent: %s" % branchParentChange2922 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2923iflen(gitParent) >0:2924 self.initialParents[self.gitRefForBranch(branch)] = gitParent2925#print "parent git commit: %s" % gitParent29262927 self.importChanges(changes)2928return True29292930defsearchParent(self, parent, branch, target):2931 parentFound =False2932for blob inread_pipe_lines(["git","rev-list","--reverse",2933"--no-merges", parent]):2934 blob = blob.strip()2935iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2936 parentFound =True2937if self.verbose:2938print"Found parent of%sin commit%s"% (branch, blob)2939break2940if parentFound:2941return blob2942else:2943return None29442945defimportChanges(self, changes):2946 cnt =12947for change in changes:2948 description =p4_describe(change)2949 self.updateOptionDict(description)29502951if not self.silent:2952 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2953 sys.stdout.flush()2954 cnt = cnt +129552956try:2957if self.detectBranches:2958 branches = self.splitFilesIntoBranches(description)2959for branch in branches.keys():2960## HACK --hwn2961 branchPrefix = self.depotPaths[0] + branch +"/"2962 self.branchPrefixes = [ branchPrefix ]29632964 parent =""29652966 filesForCommit = branches[branch]29672968if self.verbose:2969print"branch is%s"% branch29702971 self.updatedBranches.add(branch)29722973if branch not in self.createdBranches:2974 self.createdBranches.add(branch)2975 parent = self.knownBranches[branch]2976if parent == branch:2977 parent =""2978else:2979 fullBranch = self.projectName + branch2980if fullBranch not in self.p4BranchesInGit:2981if not self.silent:2982print("\nImporting new branch%s"% fullBranch);2983if self.importNewBranch(branch, change -1):2984 parent =""2985 self.p4BranchesInGit.append(fullBranch)2986if not self.silent:2987print("\nResuming with change%s"% change);29882989if self.verbose:2990print"parent determined through known branches:%s"% parent29912992 branch = self.gitRefForBranch(branch)2993 parent = self.gitRefForBranch(parent)29942995if self.verbose:2996print"looking for initial parent for%s; current parent is%s"% (branch, parent)29972998iflen(parent) ==0and branch in self.initialParents:2999 parent = self.initialParents[branch]3000del self.initialParents[branch]30013002 blob =None3003iflen(parent) >0:3004 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3005if self.verbose:3006print"Creating temporary branch: "+ tempBranch3007 self.commit(description, filesForCommit, tempBranch)3008 self.tempBranches.append(tempBranch)3009 self.checkpoint()3010 blob = self.searchParent(parent, branch, tempBranch)3011if blob:3012 self.commit(description, filesForCommit, branch, blob)3013else:3014if self.verbose:3015print"Parent of%snot found. Committing into head of%s"% (branch, parent)3016 self.commit(description, filesForCommit, branch, parent)3017else:3018 files = self.extractFilesFromCommit(description)3019 self.commit(description, files, self.branch,3020 self.initialParent)3021# only needed once, to connect to the previous commit3022 self.initialParent =""3023exceptIOError:3024print self.gitError.read()3025 sys.exit(1)30263027defimportHeadRevision(self, revision):3028print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)30293030 details = {}3031 details["user"] ="git perforce import user"3032 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3033% (' '.join(self.depotPaths), revision))3034 details["change"] = revision3035 newestRevision =030363037 fileCnt =03038 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]30393040for info inp4CmdList(["files"] + fileArgs):30413042if'code'in info and info['code'] =='error':3043 sys.stderr.write("p4 returned an error:%s\n"3044% info['data'])3045if info['data'].find("must refer to client") >=0:3046 sys.stderr.write("This particular p4 error is misleading.\n")3047 sys.stderr.write("Perhaps the depot path was misspelled.\n");3048 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3049 sys.exit(1)3050if'p4ExitCode'in info:3051 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3052 sys.exit(1)305330543055 change =int(info["change"])3056if change > newestRevision:3057 newestRevision = change30583059if info["action"]in self.delete_actions:3060# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3061#fileCnt = fileCnt + 13062continue30633064for prop in["depotFile","rev","action","type"]:3065 details["%s%s"% (prop, fileCnt)] = info[prop]30663067 fileCnt = fileCnt +130683069 details["change"] = newestRevision30703071# Use time from top-most change so that all git p4 clones of3072# the same p4 repo have the same commit SHA1s.3073 res =p4_describe(newestRevision)3074 details["time"] = res["time"]30753076 self.updateOptionDict(details)3077try:3078 self.commit(details, self.extractFilesFromCommit(details), self.branch)3079exceptIOError:3080print"IO error with git fast-import. Is your git version recent enough?"3081print self.gitError.read()308230833084defrun(self, args):3085 self.depotPaths = []3086 self.changeRange =""3087 self.previousDepotPaths = []3088 self.hasOrigin =False30893090# map from branch depot path to parent branch3091 self.knownBranches = {}3092 self.initialParents = {}30933094if self.importIntoRemotes:3095 self.refPrefix ="refs/remotes/p4/"3096else:3097 self.refPrefix ="refs/heads/p4/"30983099if self.syncWithOrigin:3100 self.hasOrigin =originP4BranchesExist()3101if self.hasOrigin:3102if not self.silent:3103print'Syncing with origin first, using "git fetch origin"'3104system("git fetch origin")31053106 branch_arg_given =bool(self.branch)3107iflen(self.branch) ==0:3108 self.branch = self.refPrefix +"master"3109ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3110system("git update-ref%srefs/heads/p4"% self.branch)3111system("git branch -D p4")31123113# accept either the command-line option, or the configuration variable3114if self.useClientSpec:3115# will use this after clone to set the variable3116 self.useClientSpec_from_options =True3117else:3118ifgitConfigBool("git-p4.useclientspec"):3119 self.useClientSpec =True3120if self.useClientSpec:3121 self.clientSpecDirs =getClientSpec()31223123# TODO: should always look at previous commits,3124# merge with previous imports, if possible.3125if args == []:3126if self.hasOrigin:3127createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)31283129# branches holds mapping from branch name to sha13130 branches =p4BranchesInGit(self.importIntoRemotes)31313132# restrict to just this one, disabling detect-branches3133if branch_arg_given:3134 short = self.branch.split("/")[-1]3135if short in branches:3136 self.p4BranchesInGit = [ short ]3137else:3138 self.p4BranchesInGit = branches.keys()31393140iflen(self.p4BranchesInGit) >1:3141if not self.silent:3142print"Importing from/into multiple branches"3143 self.detectBranches =True3144for branch in branches.keys():3145 self.initialParents[self.refPrefix + branch] = \3146 branches[branch]31473148if self.verbose:3149print"branches:%s"% self.p4BranchesInGit31503151 p4Change =03152for branch in self.p4BranchesInGit:3153 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)31543155 settings =extractSettingsGitLog(logMsg)31563157 self.readOptions(settings)3158if(settings.has_key('depot-paths')3159and settings.has_key('change')):3160 change =int(settings['change']) +13161 p4Change =max(p4Change, change)31623163 depotPaths =sorted(settings['depot-paths'])3164if self.previousDepotPaths == []:3165 self.previousDepotPaths = depotPaths3166else:3167 paths = []3168for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3169 prev_list = prev.split("/")3170 cur_list = cur.split("/")3171for i inrange(0,min(len(cur_list),len(prev_list))):3172if cur_list[i] <> prev_list[i]:3173 i = i -13174break31753176 paths.append("/".join(cur_list[:i +1]))31773178 self.previousDepotPaths = paths31793180if p4Change >0:3181 self.depotPaths =sorted(self.previousDepotPaths)3182 self.changeRange ="@%s,#head"% p4Change3183if not self.silent and not self.detectBranches:3184print"Performing incremental import into%sgit branch"% self.branch31853186# accept multiple ref name abbreviations:3187# refs/foo/bar/branch -> use it exactly3188# p4/branch -> prepend refs/remotes/ or refs/heads/3189# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3190if not self.branch.startswith("refs/"):3191if self.importIntoRemotes:3192 prepend ="refs/remotes/"3193else:3194 prepend ="refs/heads/"3195if not self.branch.startswith("p4/"):3196 prepend +="p4/"3197 self.branch = prepend + self.branch31983199iflen(args) ==0and self.depotPaths:3200if not self.silent:3201print"Depot paths:%s"%' '.join(self.depotPaths)3202else:3203if self.depotPaths and self.depotPaths != args:3204print("previous import used depot path%sand now%swas specified. "3205"This doesn't work!"% (' '.join(self.depotPaths),3206' '.join(args)))3207 sys.exit(1)32083209 self.depotPaths =sorted(args)32103211 revision =""3212 self.users = {}32133214# Make sure no revision specifiers are used when --changesfile3215# is specified.3216 bad_changesfile =False3217iflen(self.changesFile) >0:3218for p in self.depotPaths:3219if p.find("@") >=0or p.find("#") >=0:3220 bad_changesfile =True3221break3222if bad_changesfile:3223die("Option --changesfile is incompatible with revision specifiers")32243225 newPaths = []3226for p in self.depotPaths:3227if p.find("@") != -1:3228 atIdx = p.index("@")3229 self.changeRange = p[atIdx:]3230if self.changeRange =="@all":3231 self.changeRange =""3232elif','not in self.changeRange:3233 revision = self.changeRange3234 self.changeRange =""3235 p = p[:atIdx]3236elif p.find("#") != -1:3237 hashIdx = p.index("#")3238 revision = p[hashIdx:]3239 p = p[:hashIdx]3240elif self.previousDepotPaths == []:3241# pay attention to changesfile, if given, else import3242# the entire p4 tree at the head revision3243iflen(self.changesFile) ==0:3244 revision ="#head"32453246 p = re.sub("\.\.\.$","", p)3247if not p.endswith("/"):3248 p +="/"32493250 newPaths.append(p)32513252 self.depotPaths = newPaths32533254# --detect-branches may change this for each branch3255 self.branchPrefixes = self.depotPaths32563257 self.loadUserMapFromCache()3258 self.labels = {}3259if self.detectLabels:3260 self.getLabels();32613262if self.detectBranches:3263## FIXME - what's a P4 projectName ?3264 self.projectName = self.guessProjectName()32653266if self.hasOrigin:3267 self.getBranchMappingFromGitBranches()3268else:3269 self.getBranchMapping()3270if self.verbose:3271print"p4-git branches:%s"% self.p4BranchesInGit3272print"initial parents:%s"% self.initialParents3273for b in self.p4BranchesInGit:3274if b !="master":32753276## FIXME3277 b = b[len(self.projectName):]3278 self.createdBranches.add(b)32793280 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))32813282 self.importProcess = subprocess.Popen(["git","fast-import"],3283 stdin=subprocess.PIPE,3284 stdout=subprocess.PIPE,3285 stderr=subprocess.PIPE);3286 self.gitOutput = self.importProcess.stdout3287 self.gitStream = self.importProcess.stdin3288 self.gitError = self.importProcess.stderr32893290if revision:3291 self.importHeadRevision(revision)3292else:3293 changes = []32943295iflen(self.changesFile) >0:3296 output =open(self.changesFile).readlines()3297 changeSet =set()3298for line in output:3299 changeSet.add(int(line))33003301for change in changeSet:3302 changes.append(change)33033304 changes.sort()3305else:3306# catch "git p4 sync" with no new branches, in a repo that3307# does not have any existing p4 branches3308iflen(args) ==0:3309if not self.p4BranchesInGit:3310die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")33113312# The default branch is master, unless --branch is used to3313# specify something else. Make sure it exists, or complain3314# nicely about how to use --branch.3315if not self.detectBranches:3316if notbranch_exists(self.branch):3317if branch_arg_given:3318die("Error: branch%sdoes not exist."% self.branch)3319else:3320die("Error: no branch%s; perhaps specify one with --branch."%3321 self.branch)33223323if self.verbose:3324print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3325 self.changeRange)3326 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)33273328iflen(self.maxChanges) >0:3329 changes = changes[:min(int(self.maxChanges),len(changes))]33303331iflen(changes) ==0:3332if not self.silent:3333print"No changes to import!"3334else:3335if not self.silent and not self.detectBranches:3336print"Import destination:%s"% self.branch33373338 self.updatedBranches =set()33393340if not self.detectBranches:3341if args:3342# start a new branch3343 self.initialParent =""3344else:3345# build on a previous revision3346 self.initialParent =parseRevision(self.branch)33473348 self.importChanges(changes)33493350if not self.silent:3351print""3352iflen(self.updatedBranches) >0:3353 sys.stdout.write("Updated branches: ")3354for b in self.updatedBranches:3355 sys.stdout.write("%s"% b)3356 sys.stdout.write("\n")33573358ifgitConfigBool("git-p4.importLabels"):3359 self.importLabels =True33603361if self.importLabels:3362 p4Labels =getP4Labels(self.depotPaths)3363 gitTags =getGitTags()33643365 missingP4Labels = p4Labels - gitTags3366 self.importP4Labels(self.gitStream, missingP4Labels)33673368 self.gitStream.close()3369if self.importProcess.wait() !=0:3370die("fast-import failed:%s"% self.gitError.read())3371 self.gitOutput.close()3372 self.gitError.close()33733374# Cleanup temporary branches created during import3375if self.tempBranches != []:3376for branch in self.tempBranches:3377read_pipe("git update-ref -d%s"% branch)3378 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))33793380# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3381# a convenient shortcut refname "p4".3382if self.importIntoRemotes:3383 head_ref = self.refPrefix +"HEAD"3384if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3385system(["git","symbolic-ref", head_ref, self.branch])33863387return True33883389classP4Rebase(Command):3390def__init__(self):3391 Command.__init__(self)3392 self.options = [3393 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3394]3395 self.importLabels =False3396 self.description = ("Fetches the latest revision from perforce and "3397+"rebases the current work (branch) against it")33983399defrun(self, args):3400 sync =P4Sync()3401 sync.importLabels = self.importLabels3402 sync.run([])34033404return self.rebase()34053406defrebase(self):3407if os.system("git update-index --refresh") !=0:3408die("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.");3409iflen(read_pipe("git diff-index HEAD --")) >0:3410die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");34113412[upstream, settings] =findUpstreamBranchPoint()3413iflen(upstream) ==0:3414die("Cannot find upstream branchpoint for rebase")34153416# the branchpoint may be p4/foo~3, so strip off the parent3417 upstream = re.sub("~[0-9]+$","", upstream)34183419print"Rebasing the current branch onto%s"% upstream3420 oldHead =read_pipe("git rev-parse HEAD").strip()3421system("git rebase%s"% upstream)3422system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3423return True34243425classP4Clone(P4Sync):3426def__init__(self):3427 P4Sync.__init__(self)3428 self.description ="Creates a new git repository and imports from Perforce into it"3429 self.usage ="usage: %prog [options] //depot/path[@revRange]"3430 self.options += [3431 optparse.make_option("--destination", dest="cloneDestination",3432 action='store', default=None,3433help="where to leave result of the clone"),3434 optparse.make_option("--bare", dest="cloneBare",3435 action="store_true", default=False),3436]3437 self.cloneDestination =None3438 self.needsGit =False3439 self.cloneBare =False34403441defdefaultDestination(self, args):3442## TODO: use common prefix of args?3443 depotPath = args[0]3444 depotDir = re.sub("(@[^@]*)$","", depotPath)3445 depotDir = re.sub("(#[^#]*)$","", depotDir)3446 depotDir = re.sub(r"\.\.\.$","", depotDir)3447 depotDir = re.sub(r"/$","", depotDir)3448return os.path.split(depotDir)[1]34493450defrun(self, args):3451iflen(args) <1:3452return False34533454if self.keepRepoPath and not self.cloneDestination:3455 sys.stderr.write("Must specify destination for --keep-path\n")3456 sys.exit(1)34573458 depotPaths = args34593460if not self.cloneDestination andlen(depotPaths) >1:3461 self.cloneDestination = depotPaths[-1]3462 depotPaths = depotPaths[:-1]34633464 self.cloneExclude = ["/"+p for p in self.cloneExclude]3465for p in depotPaths:3466if not p.startswith("//"):3467 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3468return False34693470if not self.cloneDestination:3471 self.cloneDestination = self.defaultDestination(args)34723473print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)34743475if not os.path.exists(self.cloneDestination):3476 os.makedirs(self.cloneDestination)3477chdir(self.cloneDestination)34783479 init_cmd = ["git","init"]3480if self.cloneBare:3481 init_cmd.append("--bare")3482 retcode = subprocess.call(init_cmd)3483if retcode:3484raiseCalledProcessError(retcode, init_cmd)34853486if not P4Sync.run(self, depotPaths):3487return False34883489# create a master branch and check out a work tree3490ifgitBranchExists(self.branch):3491system(["git","branch","master", self.branch ])3492if not self.cloneBare:3493system(["git","checkout","-f"])3494else:3495print'Not checking out any branch, use ' \3496'"git checkout -q -b master <branch>"'34973498# auto-set this variable if invoked with --use-client-spec3499if self.useClientSpec_from_options:3500system("git config --bool git-p4.useclientspec true")35013502return True35033504classP4Branches(Command):3505def__init__(self):3506 Command.__init__(self)3507 self.options = [ ]3508 self.description = ("Shows the git branches that hold imports and their "3509+"corresponding perforce depot paths")3510 self.verbose =False35113512defrun(self, args):3513iforiginP4BranchesExist():3514createOrUpdateBranchesFromOrigin()35153516 cmdline ="git rev-parse --symbolic "3517 cmdline +=" --remotes"35183519for line inread_pipe_lines(cmdline):3520 line = line.strip()35213522if not line.startswith('p4/')or line =="p4/HEAD":3523continue3524 branch = line35253526 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3527 settings =extractSettingsGitLog(log)35283529print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3530return True35313532classHelpFormatter(optparse.IndentedHelpFormatter):3533def__init__(self):3534 optparse.IndentedHelpFormatter.__init__(self)35353536defformat_description(self, description):3537if description:3538return description +"\n"3539else:3540return""35413542defprintUsage(commands):3543print"usage:%s<command> [options]"% sys.argv[0]3544print""3545print"valid commands:%s"%", ".join(commands)3546print""3547print"Try%s<command> --help for command specific help."% sys.argv[0]3548print""35493550commands = {3551"debug": P4Debug,3552"submit": P4Submit,3553"commit": P4Submit,3554"sync": P4Sync,3555"rebase": P4Rebase,3556"clone": P4Clone,3557"rollback": P4RollBack,3558"branches": P4Branches3559}356035613562defmain():3563iflen(sys.argv[1:]) ==0:3564printUsage(commands.keys())3565 sys.exit(2)35663567 cmdName = sys.argv[1]3568try:3569 klass = commands[cmdName]3570 cmd =klass()3571exceptKeyError:3572print"unknown command%s"% cmdName3573print""3574printUsage(commands.keys())3575 sys.exit(2)35763577 options = cmd.options3578 cmd.gitdir = os.environ.get("GIT_DIR",None)35793580 args = sys.argv[2:]35813582 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3583if cmd.needsGit:3584 options.append(optparse.make_option("--git-dir", dest="gitdir"))35853586 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3587 options,3588 description = cmd.description,3589 formatter =HelpFormatter())35903591(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3592global verbose3593 verbose = cmd.verbose3594if cmd.needsGit:3595if cmd.gitdir ==None:3596 cmd.gitdir = os.path.abspath(".git")3597if notisValidGitDir(cmd.gitdir):3598 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3599if os.path.exists(cmd.gitdir):3600 cdup =read_pipe("git rev-parse --show-cdup").strip()3601iflen(cdup) >0:3602chdir(cdup);36033604if notisValidGitDir(cmd.gitdir):3605ifisValidGitDir(cmd.gitdir +"/.git"):3606 cmd.gitdir +="/.git"3607else:3608die("fatal: cannot locate git repository at%s"% cmd.gitdir)36093610 os.environ["GIT_DIR"] = cmd.gitdir36113612if not cmd.run(args):3613 parser.print_help()3614 sys.exit(2)361536163617if __name__ =='__main__':3618main()