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)))10401041class Command:1042def__init__(self):1043 self.usage ="usage: %prog [options]"1044 self.needsGit =True1045 self.verbose =False10461047class P4UserMap:1048def__init__(self):1049 self.userMapFromPerforceServer =False1050 self.myP4UserId =None10511052defp4UserId(self):1053if self.myP4UserId:1054return self.myP4UserId10551056 results =p4CmdList("user -o")1057for r in results:1058if r.has_key('User'):1059 self.myP4UserId = r['User']1060return r['User']1061die("Could not find your p4 user id")10621063defp4UserIsMe(self, p4User):1064# return True if the given p4 user is actually me1065 me = self.p4UserId()1066if not p4User or p4User != me:1067return False1068else:1069return True10701071defgetUserCacheFilename(self):1072 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1073return home +"/.gitp4-usercache.txt"10741075defgetUserMapFromPerforceServer(self):1076if self.userMapFromPerforceServer:1077return1078 self.users = {}1079 self.emails = {}10801081for output inp4CmdList("users"):1082if not output.has_key("User"):1083continue1084 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1085 self.emails[output["Email"]] = output["User"]108610871088 s =''1089for(key, val)in self.users.items():1090 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))10911092open(self.getUserCacheFilename(),"wb").write(s)1093 self.userMapFromPerforceServer =True10941095defloadUserMapFromCache(self):1096 self.users = {}1097 self.userMapFromPerforceServer =False1098try:1099 cache =open(self.getUserCacheFilename(),"rb")1100 lines = cache.readlines()1101 cache.close()1102for line in lines:1103 entry = line.strip().split("\t")1104 self.users[entry[0]] = entry[1]1105exceptIOError:1106 self.getUserMapFromPerforceServer()11071108classP4Debug(Command):1109def__init__(self):1110 Command.__init__(self)1111 self.options = []1112 self.description ="A tool to debug the output of p4 -G."1113 self.needsGit =False11141115defrun(self, args):1116 j =01117for output inp4CmdList(args):1118print'Element:%d'% j1119 j +=11120print output1121return True11221123classP4RollBack(Command):1124def__init__(self):1125 Command.__init__(self)1126 self.options = [1127 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1128]1129 self.description ="A tool to debug the multi-branch import. Don't use :)"1130 self.rollbackLocalBranches =False11311132defrun(self, args):1133iflen(args) !=1:1134return False1135 maxChange =int(args[0])11361137if"p4ExitCode"inp4Cmd("changes -m 1"):1138die("Problems executing p4");11391140if self.rollbackLocalBranches:1141 refPrefix ="refs/heads/"1142 lines =read_pipe_lines("git rev-parse --symbolic --branches")1143else:1144 refPrefix ="refs/remotes/"1145 lines =read_pipe_lines("git rev-parse --symbolic --remotes")11461147for line in lines:1148if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1149 line = line.strip()1150 ref = refPrefix + line1151 log =extractLogMessageFromGitCommit(ref)1152 settings =extractSettingsGitLog(log)11531154 depotPaths = settings['depot-paths']1155 change = settings['change']11561157 changed =False11581159iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1160for p in depotPaths]))) ==0:1161print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1162system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1163continue11641165while change andint(change) > maxChange:1166 changed =True1167if self.verbose:1168print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1169system("git update-ref%s\"%s^\""% (ref, ref))1170 log =extractLogMessageFromGitCommit(ref)1171 settings =extractSettingsGitLog(log)117211731174 depotPaths = settings['depot-paths']1175 change = settings['change']11761177if changed:1178print"%srewound to%s"% (ref, change)11791180return True11811182classP4Submit(Command, P4UserMap):11831184 conflict_behavior_choices = ("ask","skip","quit")11851186def__init__(self):1187 Command.__init__(self)1188 P4UserMap.__init__(self)1189 self.options = [1190 optparse.make_option("--origin", dest="origin"),1191 optparse.make_option("-M", dest="detectRenames", action="store_true"),1192# preserve the user, requires relevant p4 permissions1193 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1194 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1195 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1196 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1197 optparse.make_option("--conflict", dest="conflict_behavior",1198 choices=self.conflict_behavior_choices),1199 optparse.make_option("--branch", dest="branch"),1200]1201 self.description ="Submit changes from git to the perforce depot."1202 self.usage +=" [name of git branch to submit into perforce depot]"1203 self.origin =""1204 self.detectRenames =False1205 self.preserveUser =gitConfigBool("git-p4.preserveUser")1206 self.dry_run =False1207 self.prepare_p4_only =False1208 self.conflict_behavior =None1209 self.isWindows = (platform.system() =="Windows")1210 self.exportLabels =False1211 self.p4HasMoveCommand =p4_has_move_command()1212 self.branch =None12131214ifgitConfig('git-p4.largeFileSystem'):1215die("Large file system not supported for git-p4 submit command. Please remove it from config.")12161217defcheck(self):1218iflen(p4CmdList("opened ...")) >0:1219die("You have files opened with perforce! Close them before starting the sync.")12201221defseparate_jobs_from_description(self, message):1222"""Extract and return a possible Jobs field in the commit1223 message. It goes into a separate section in the p4 change1224 specification.12251226 A jobs line starts with "Jobs:" and looks like a new field1227 in a form. Values are white-space separated on the same1228 line or on following lines that start with a tab.12291230 This does not parse and extract the full git commit message1231 like a p4 form. It just sees the Jobs: line as a marker1232 to pass everything from then on directly into the p4 form,1233 but outside the description section.12341235 Return a tuple (stripped log message, jobs string)."""12361237 m = re.search(r'^Jobs:', message, re.MULTILINE)1238if m is None:1239return(message,None)12401241 jobtext = message[m.start():]1242 stripped_message = message[:m.start()].rstrip()1243return(stripped_message, jobtext)12441245defprepareLogMessage(self, template, message, jobs):1246"""Edits the template returned from "p4 change -o" to insert1247 the message in the Description field, and the jobs text in1248 the Jobs field."""1249 result =""12501251 inDescriptionSection =False12521253for line in template.split("\n"):1254if line.startswith("#"):1255 result += line +"\n"1256continue12571258if inDescriptionSection:1259if line.startswith("Files:")or line.startswith("Jobs:"):1260 inDescriptionSection =False1261# insert Jobs section1262if jobs:1263 result += jobs +"\n"1264else:1265continue1266else:1267if line.startswith("Description:"):1268 inDescriptionSection =True1269 line +="\n"1270for messageLine in message.split("\n"):1271 line +="\t"+ messageLine +"\n"12721273 result += line +"\n"12741275return result12761277defpatchRCSKeywords(self,file, pattern):1278# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1279(handle, outFileName) = tempfile.mkstemp(dir='.')1280try:1281 outFile = os.fdopen(handle,"w+")1282 inFile =open(file,"r")1283 regexp = re.compile(pattern, re.VERBOSE)1284for line in inFile.readlines():1285 line = regexp.sub(r'$\1$', line)1286 outFile.write(line)1287 inFile.close()1288 outFile.close()1289# Forcibly overwrite the original file1290 os.unlink(file)1291 shutil.move(outFileName,file)1292except:1293# cleanup our temporary file1294 os.unlink(outFileName)1295print"Failed to strip RCS keywords in%s"%file1296raise12971298print"Patched up RCS keywords in%s"%file12991300defp4UserForCommit(self,id):1301# Return the tuple (perforce user,git email) for a given git commit id1302 self.getUserMapFromPerforceServer()1303 gitEmail =read_pipe(["git","log","--max-count=1",1304"--format=%ae",id])1305 gitEmail = gitEmail.strip()1306if not self.emails.has_key(gitEmail):1307return(None,gitEmail)1308else:1309return(self.emails[gitEmail],gitEmail)13101311defcheckValidP4Users(self,commits):1312# check if any git authors cannot be mapped to p4 users1313foridin commits:1314(user,email) = self.p4UserForCommit(id)1315if not user:1316 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1317ifgitConfigBool("git-p4.allowMissingP4Users"):1318print"%s"% msg1319else:1320die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)13211322deflastP4Changelist(self):1323# Get back the last changelist number submitted in this client spec. This1324# then gets used to patch up the username in the change. If the same1325# client spec is being used by multiple processes then this might go1326# wrong.1327 results =p4CmdList("client -o")# find the current client1328 client =None1329for r in results:1330if r.has_key('Client'):1331 client = r['Client']1332break1333if not client:1334die("could not get client spec")1335 results =p4CmdList(["changes","-c", client,"-m","1"])1336for r in results:1337if r.has_key('change'):1338return r['change']1339die("Could not get changelist number for last submit - cannot patch up user details")13401341defmodifyChangelistUser(self, changelist, newUser):1342# fixup the user field of a changelist after it has been submitted.1343 changes =p4CmdList("change -o%s"% changelist)1344iflen(changes) !=1:1345die("Bad output from p4 change modifying%sto user%s"%1346(changelist, newUser))13471348 c = changes[0]1349if c['User'] == newUser:return# nothing to do1350 c['User'] = newUser1351input= marshal.dumps(c)13521353 result =p4CmdList("change -f -i", stdin=input)1354for r in result:1355if r.has_key('code'):1356if r['code'] =='error':1357die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1358if r.has_key('data'):1359print("Updated user field for changelist%sto%s"% (changelist, newUser))1360return1361die("Could not modify user field of changelist%sto%s"% (changelist, newUser))13621363defcanChangeChangelists(self):1364# check to see if we have p4 admin or super-user permissions, either of1365# which are required to modify changelists.1366 results =p4CmdList(["protects", self.depotPath])1367for r in results:1368if r.has_key('perm'):1369if r['perm'] =='admin':1370return11371if r['perm'] =='super':1372return11373return013741375defprepareSubmitTemplate(self):1376"""Run "p4 change -o" to grab a change specification template.1377 This does not use "p4 -G", as it is nice to keep the submission1378 template in original order, since a human might edit it.13791380 Remove lines in the Files section that show changes to files1381 outside the depot path we're committing into."""13821383 template =""1384 inFilesSection =False1385for line inp4_read_pipe_lines(['change','-o']):1386if line.endswith("\r\n"):1387 line = line[:-2] +"\n"1388if inFilesSection:1389if line.startswith("\t"):1390# path starts and ends with a tab1391 path = line[1:]1392 lastTab = path.rfind("\t")1393if lastTab != -1:1394 path = path[:lastTab]1395if notp4PathStartsWith(path, self.depotPath):1396continue1397else:1398 inFilesSection =False1399else:1400if line.startswith("Files:"):1401 inFilesSection =True14021403 template += line14041405return template14061407defedit_template(self, template_file):1408"""Invoke the editor to let the user change the submission1409 message. Return true if okay to continue with the submit."""14101411# if configured to skip the editing part, just submit1412ifgitConfigBool("git-p4.skipSubmitEdit"):1413return True14141415# look at the modification time, to check later if the user saved1416# the file1417 mtime = os.stat(template_file).st_mtime14181419# invoke the editor1420if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1421 editor = os.environ.get("P4EDITOR")1422else:1423 editor =read_pipe("git var GIT_EDITOR").strip()1424system(["sh","-c", ('%s"$@"'% editor), editor, template_file])14251426# If the file was not saved, prompt to see if this patch should1427# be skipped. But skip this verification step if configured so.1428ifgitConfigBool("git-p4.skipSubmitEditCheck"):1429return True14301431# modification time updated means user saved the file1432if os.stat(template_file).st_mtime > mtime:1433return True14341435while True:1436 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1437if response =='y':1438return True1439if response =='n':1440return False14411442defget_diff_description(self, editedFiles, filesToAdd):1443# diff1444if os.environ.has_key("P4DIFF"):1445del(os.environ["P4DIFF"])1446 diff =""1447for editedFile in editedFiles:1448 diff +=p4_read_pipe(['diff','-du',1449wildcard_encode(editedFile)])14501451# new file diff1452 newdiff =""1453for newFile in filesToAdd:1454 newdiff +="==== new file ====\n"1455 newdiff +="--- /dev/null\n"1456 newdiff +="+++%s\n"% newFile1457 f =open(newFile,"r")1458for line in f.readlines():1459 newdiff +="+"+ line1460 f.close()14611462return(diff + newdiff).replace('\r\n','\n')14631464defapplyCommit(self,id):1465"""Apply one commit, return True if it succeeded."""14661467print"Applying",read_pipe(["git","show","-s",1468"--format=format:%h%s",id])14691470(p4User, gitEmail) = self.p4UserForCommit(id)14711472 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1473 filesToAdd =set()1474 filesToDelete =set()1475 editedFiles =set()1476 pureRenameCopy =set()1477 filesToChangeExecBit = {}14781479for line in diff:1480 diff =parseDiffTreeEntry(line)1481 modifier = diff['status']1482 path = diff['src']1483if modifier =="M":1484p4_edit(path)1485ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1486 filesToChangeExecBit[path] = diff['dst_mode']1487 editedFiles.add(path)1488elif modifier =="A":1489 filesToAdd.add(path)1490 filesToChangeExecBit[path] = diff['dst_mode']1491if path in filesToDelete:1492 filesToDelete.remove(path)1493elif modifier =="D":1494 filesToDelete.add(path)1495if path in filesToAdd:1496 filesToAdd.remove(path)1497elif modifier =="C":1498 src, dest = diff['src'], diff['dst']1499p4_integrate(src, dest)1500 pureRenameCopy.add(dest)1501if diff['src_sha1'] != diff['dst_sha1']:1502p4_edit(dest)1503 pureRenameCopy.discard(dest)1504ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1505p4_edit(dest)1506 pureRenameCopy.discard(dest)1507 filesToChangeExecBit[dest] = diff['dst_mode']1508if self.isWindows:1509# turn off read-only attribute1510 os.chmod(dest, stat.S_IWRITE)1511 os.unlink(dest)1512 editedFiles.add(dest)1513elif modifier =="R":1514 src, dest = diff['src'], diff['dst']1515if self.p4HasMoveCommand:1516p4_edit(src)# src must be open before move1517p4_move(src, dest)# opens for (move/delete, move/add)1518else:1519p4_integrate(src, dest)1520if diff['src_sha1'] != diff['dst_sha1']:1521p4_edit(dest)1522else:1523 pureRenameCopy.add(dest)1524ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1525if not self.p4HasMoveCommand:1526p4_edit(dest)# with move: already open, writable1527 filesToChangeExecBit[dest] = diff['dst_mode']1528if not self.p4HasMoveCommand:1529if self.isWindows:1530 os.chmod(dest, stat.S_IWRITE)1531 os.unlink(dest)1532 filesToDelete.add(src)1533 editedFiles.add(dest)1534else:1535die("unknown modifier%sfor%s"% (modifier, path))15361537 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1538 patchcmd = diffcmd +" | git apply "1539 tryPatchCmd = patchcmd +"--check -"1540 applyPatchCmd = patchcmd +"--check --apply -"1541 patch_succeeded =True15421543if os.system(tryPatchCmd) !=0:1544 fixed_rcs_keywords =False1545 patch_succeeded =False1546print"Unfortunately applying the change failed!"15471548# Patch failed, maybe it's just RCS keyword woes. Look through1549# the patch to see if that's possible.1550ifgitConfigBool("git-p4.attemptRCSCleanup"):1551file=None1552 pattern =None1553 kwfiles = {}1554forfilein editedFiles | filesToDelete:1555# did this file's delta contain RCS keywords?1556 pattern =p4_keywords_regexp_for_file(file)15571558if pattern:1559# this file is a possibility...look for RCS keywords.1560 regexp = re.compile(pattern, re.VERBOSE)1561for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1562if regexp.search(line):1563if verbose:1564print"got keyword match on%sin%sin%s"% (pattern, line,file)1565 kwfiles[file] = pattern1566break15671568forfilein kwfiles:1569if verbose:1570print"zapping%swith%s"% (line,pattern)1571# File is being deleted, so not open in p4. Must1572# disable the read-only bit on windows.1573if self.isWindows andfilenot in editedFiles:1574 os.chmod(file, stat.S_IWRITE)1575 self.patchRCSKeywords(file, kwfiles[file])1576 fixed_rcs_keywords =True15771578if fixed_rcs_keywords:1579print"Retrying the patch with RCS keywords cleaned up"1580if os.system(tryPatchCmd) ==0:1581 patch_succeeded =True15821583if not patch_succeeded:1584for f in editedFiles:1585p4_revert(f)1586return False15871588#1589# Apply the patch for real, and do add/delete/+x handling.1590#1591system(applyPatchCmd)15921593for f in filesToAdd:1594p4_add(f)1595for f in filesToDelete:1596p4_revert(f)1597p4_delete(f)15981599# Set/clear executable bits1600for f in filesToChangeExecBit.keys():1601 mode = filesToChangeExecBit[f]1602setP4ExecBit(f, mode)16031604#1605# Build p4 change description, starting with the contents1606# of the git commit message.1607#1608 logMessage =extractLogMessageFromGitCommit(id)1609 logMessage = logMessage.strip()1610(logMessage, jobs) = self.separate_jobs_from_description(logMessage)16111612 template = self.prepareSubmitTemplate()1613 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)16141615if self.preserveUser:1616 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User16171618if self.checkAuthorship and not self.p4UserIsMe(p4User):1619 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1620 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1621 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"16221623 separatorLine ="######## everything below this line is just the diff #######\n"1624if not self.prepare_p4_only:1625 submitTemplate += separatorLine1626 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)16271628(handle, fileName) = tempfile.mkstemp()1629 tmpFile = os.fdopen(handle,"w+b")1630if self.isWindows:1631 submitTemplate = submitTemplate.replace("\n","\r\n")1632 tmpFile.write(submitTemplate)1633 tmpFile.close()16341635if self.prepare_p4_only:1636#1637# Leave the p4 tree prepared, and the submit template around1638# and let the user decide what to do next1639#1640print1641print"P4 workspace prepared for submission."1642print"To submit or revert, go to client workspace"1643print" "+ self.clientPath1644print1645print"To submit, use\"p4 submit\"to write a new description,"1646print"or\"p4 submit -i <%s\"to use the one prepared by" \1647"\"git p4\"."% fileName1648print"You can delete the file\"%s\"when finished."% fileName16491650if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1651print"To preserve change ownership by user%s, you must\n" \1652"do\"p4 change -f <change>\"after submitting and\n" \1653"edit the User field."1654if pureRenameCopy:1655print"After submitting, renamed files must be re-synced."1656print"Invoke\"p4 sync -f\"on each of these files:"1657for f in pureRenameCopy:1658print" "+ f16591660print1661print"To revert the changes, use\"p4 revert ...\", and delete"1662print"the submit template file\"%s\""% fileName1663if filesToAdd:1664print"Since the commit adds new files, they must be deleted:"1665for f in filesToAdd:1666print" "+ f1667print1668return True16691670#1671# Let the user edit the change description, then submit it.1672#1673if self.edit_template(fileName):1674# read the edited message and submit1675 ret =True1676 tmpFile =open(fileName,"rb")1677 message = tmpFile.read()1678 tmpFile.close()1679if self.isWindows:1680 message = message.replace("\r\n","\n")1681 submitTemplate = message[:message.index(separatorLine)]1682p4_write_pipe(['submit','-i'], submitTemplate)16831684if self.preserveUser:1685if p4User:1686# Get last changelist number. Cannot easily get it from1687# the submit command output as the output is1688# unmarshalled.1689 changelist = self.lastP4Changelist()1690 self.modifyChangelistUser(changelist, p4User)16911692# The rename/copy happened by applying a patch that created a1693# new file. This leaves it writable, which confuses p4.1694for f in pureRenameCopy:1695p4_sync(f,"-f")16961697else:1698# skip this patch1699 ret =False1700print"Submission cancelled, undoing p4 changes."1701for f in editedFiles:1702p4_revert(f)1703for f in filesToAdd:1704p4_revert(f)1705 os.remove(f)1706for f in filesToDelete:1707p4_revert(f)17081709 os.remove(fileName)1710return ret17111712# Export git tags as p4 labels. Create a p4 label and then tag1713# with that.1714defexportGitTags(self, gitTags):1715 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1716iflen(validLabelRegexp) ==0:1717 validLabelRegexp = defaultLabelRegexp1718 m = re.compile(validLabelRegexp)17191720for name in gitTags:17211722if not m.match(name):1723if verbose:1724print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1725continue17261727# Get the p4 commit this corresponds to1728 logMessage =extractLogMessageFromGitCommit(name)1729 values =extractSettingsGitLog(logMessage)17301731if not values.has_key('change'):1732# a tag pointing to something not sent to p4; ignore1733if verbose:1734print"git tag%sdoes not give a p4 commit"% name1735continue1736else:1737 changelist = values['change']17381739# Get the tag details.1740 inHeader =True1741 isAnnotated =False1742 body = []1743for l inread_pipe_lines(["git","cat-file","-p", name]):1744 l = l.strip()1745if inHeader:1746if re.match(r'tag\s+', l):1747 isAnnotated =True1748elif re.match(r'\s*$', l):1749 inHeader =False1750continue1751else:1752 body.append(l)17531754if not isAnnotated:1755 body = ["lightweight tag imported by git p4\n"]17561757# Create the label - use the same view as the client spec we are using1758 clientSpec =getClientSpec()17591760 labelTemplate ="Label:%s\n"% name1761 labelTemplate +="Description:\n"1762for b in body:1763 labelTemplate +="\t"+ b +"\n"1764 labelTemplate +="View:\n"1765for depot_side in clientSpec.mappings:1766 labelTemplate +="\t%s\n"% depot_side17671768if self.dry_run:1769print"Would create p4 label%sfor tag"% name1770elif self.prepare_p4_only:1771print"Not creating p4 label%sfor tag due to option" \1772" --prepare-p4-only"% name1773else:1774p4_write_pipe(["label","-i"], labelTemplate)17751776# Use the label1777p4_system(["tag","-l", name] +1778["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])17791780if verbose:1781print"created p4 label for tag%s"% name17821783defrun(self, args):1784iflen(args) ==0:1785 self.master =currentGitBranch()1786iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1787die("Detecting current git branch failed!")1788eliflen(args) ==1:1789 self.master = args[0]1790if notbranchExists(self.master):1791die("Branch%sdoes not exist"% self.master)1792else:1793return False17941795 allowSubmit =gitConfig("git-p4.allowSubmit")1796iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1797die("%sis not in git-p4.allowSubmit"% self.master)17981799[upstream, settings] =findUpstreamBranchPoint()1800 self.depotPath = settings['depot-paths'][0]1801iflen(self.origin) ==0:1802 self.origin = upstream18031804if self.preserveUser:1805if not self.canChangeChangelists():1806die("Cannot preserve user names without p4 super-user or admin permissions")18071808# if not set from the command line, try the config file1809if self.conflict_behavior is None:1810 val =gitConfig("git-p4.conflict")1811if val:1812if val not in self.conflict_behavior_choices:1813die("Invalid value '%s' for config git-p4.conflict"% val)1814else:1815 val ="ask"1816 self.conflict_behavior = val18171818if self.verbose:1819print"Origin branch is "+ self.origin18201821iflen(self.depotPath) ==0:1822print"Internal error: cannot locate perforce depot path from existing branches"1823 sys.exit(128)18241825 self.useClientSpec =False1826ifgitConfigBool("git-p4.useclientspec"):1827 self.useClientSpec =True1828if self.useClientSpec:1829 self.clientSpecDirs =getClientSpec()18301831# Check for the existance of P4 branches1832 branchesDetected = (len(p4BranchesInGit().keys()) >1)18331834if self.useClientSpec and not branchesDetected:1835# all files are relative to the client spec1836 self.clientPath =getClientRoot()1837else:1838 self.clientPath =p4Where(self.depotPath)18391840if self.clientPath =="":1841die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)18421843print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1844 self.oldWorkingDirectory = os.getcwd()18451846# ensure the clientPath exists1847 new_client_dir =False1848if not os.path.exists(self.clientPath):1849 new_client_dir =True1850 os.makedirs(self.clientPath)18511852chdir(self.clientPath, is_client_path=True)1853if self.dry_run:1854print"Would synchronize p4 checkout in%s"% self.clientPath1855else:1856print"Synchronizing p4 checkout..."1857if new_client_dir:1858# old one was destroyed, and maybe nobody told p41859p4_sync("...","-f")1860else:1861p4_sync("...")1862 self.check()18631864 commits = []1865for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1866 commits.append(line.strip())1867 commits.reverse()18681869if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1870 self.checkAuthorship =False1871else:1872 self.checkAuthorship =True18731874if self.preserveUser:1875 self.checkValidP4Users(commits)18761877#1878# Build up a set of options to be passed to diff when1879# submitting each commit to p4.1880#1881if self.detectRenames:1882# command-line -M arg1883 self.diffOpts ="-M"1884else:1885# If not explicitly set check the config variable1886 detectRenames =gitConfig("git-p4.detectRenames")18871888if detectRenames.lower() =="false"or detectRenames =="":1889 self.diffOpts =""1890elif detectRenames.lower() =="true":1891 self.diffOpts ="-M"1892else:1893 self.diffOpts ="-M%s"% detectRenames18941895# no command-line arg for -C or --find-copies-harder, just1896# config variables1897 detectCopies =gitConfig("git-p4.detectCopies")1898if detectCopies.lower() =="false"or detectCopies =="":1899pass1900elif detectCopies.lower() =="true":1901 self.diffOpts +=" -C"1902else:1903 self.diffOpts +=" -C%s"% detectCopies19041905ifgitConfigBool("git-p4.detectCopiesHarder"):1906 self.diffOpts +=" --find-copies-harder"19071908#1909# Apply the commits, one at a time. On failure, ask if should1910# continue to try the rest of the patches, or quit.1911#1912if self.dry_run:1913print"Would apply"1914 applied = []1915 last =len(commits) -11916for i, commit inenumerate(commits):1917if self.dry_run:1918print" ",read_pipe(["git","show","-s",1919"--format=format:%h%s", commit])1920 ok =True1921else:1922 ok = self.applyCommit(commit)1923if ok:1924 applied.append(commit)1925else:1926if self.prepare_p4_only and i < last:1927print"Processing only the first commit due to option" \1928" --prepare-p4-only"1929break1930if i < last:1931 quit =False1932while True:1933# prompt for what to do, or use the option/variable1934if self.conflict_behavior =="ask":1935print"What do you want to do?"1936 response =raw_input("[s]kip this commit but apply"1937" the rest, or [q]uit? ")1938if not response:1939continue1940elif self.conflict_behavior =="skip":1941 response ="s"1942elif self.conflict_behavior =="quit":1943 response ="q"1944else:1945die("Unknown conflict_behavior '%s'"%1946 self.conflict_behavior)19471948if response[0] =="s":1949print"Skipping this commit, but applying the rest"1950break1951if response[0] =="q":1952print"Quitting"1953 quit =True1954break1955if quit:1956break19571958chdir(self.oldWorkingDirectory)19591960if self.dry_run:1961pass1962elif self.prepare_p4_only:1963pass1964eliflen(commits) ==len(applied):1965print"All commits applied!"19661967 sync =P4Sync()1968if self.branch:1969 sync.branch = self.branch1970 sync.run([])19711972 rebase =P4Rebase()1973 rebase.rebase()19741975else:1976iflen(applied) ==0:1977print"No commits applied."1978else:1979print"Applied only the commits marked with '*':"1980for c in commits:1981if c in applied:1982 star ="*"1983else:1984 star =" "1985print star,read_pipe(["git","show","-s",1986"--format=format:%h%s", c])1987print"You will have to do 'git p4 sync' and rebase."19881989ifgitConfigBool("git-p4.exportLabels"):1990 self.exportLabels =True19911992if self.exportLabels:1993 p4Labels =getP4Labels(self.depotPath)1994 gitTags =getGitTags()19951996 missingGitTags = gitTags - p4Labels1997 self.exportGitTags(missingGitTags)19981999# exit with error unless everything applied perfectly2000iflen(commits) !=len(applied):2001 sys.exit(1)20022003return True20042005classView(object):2006"""Represent a p4 view ("p4 help views"), and map files in a2007 repo according to the view."""20082009def__init__(self, client_name):2010 self.mappings = []2011 self.client_prefix ="//%s/"% client_name2012# cache results of "p4 where" to lookup client file locations2013 self.client_spec_path_cache = {}20142015defappend(self, view_line):2016"""Parse a view line, splitting it into depot and client2017 sides. Append to self.mappings, preserving order. This2018 is only needed for tag creation."""20192020# Split the view line into exactly two words. P4 enforces2021# structure on these lines that simplifies this quite a bit.2022#2023# Either or both words may be double-quoted.2024# Single quotes do not matter.2025# Double-quote marks cannot occur inside the words.2026# A + or - prefix is also inside the quotes.2027# There are no quotes unless they contain a space.2028# The line is already white-space stripped.2029# The two words are separated by a single space.2030#2031if view_line[0] =='"':2032# First word is double quoted. Find its end.2033 close_quote_index = view_line.find('"',1)2034if close_quote_index <=0:2035die("No first-word closing quote found:%s"% view_line)2036 depot_side = view_line[1:close_quote_index]2037# skip closing quote and space2038 rhs_index = close_quote_index +1+12039else:2040 space_index = view_line.find(" ")2041if space_index <=0:2042die("No word-splitting space found:%s"% view_line)2043 depot_side = view_line[0:space_index]2044 rhs_index = space_index +120452046# prefix + means overlay on previous mapping2047if depot_side.startswith("+"):2048 depot_side = depot_side[1:]20492050# prefix - means exclude this path, leave out of mappings2051 exclude =False2052if depot_side.startswith("-"):2053 exclude =True2054 depot_side = depot_side[1:]20552056if not exclude:2057 self.mappings.append(depot_side)20582059defconvert_client_path(self, clientFile):2060# chop off //client/ part to make it relative2061if not clientFile.startswith(self.client_prefix):2062die("No prefix '%s' on clientFile '%s'"%2063(self.client_prefix, clientFile))2064return clientFile[len(self.client_prefix):]20652066defupdate_client_spec_path_cache(self, files):2067""" Caching file paths by "p4 where" batch query """20682069# List depot file paths exclude that already cached2070 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]20712072iflen(fileArgs) ==0:2073return# All files in cache20742075 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2076for res in where_result:2077if"code"in res and res["code"] =="error":2078# assume error is "... file(s) not in client view"2079continue2080if"clientFile"not in res:2081die("No clientFile in 'p4 where' output")2082if"unmap"in res:2083# it will list all of them, but only one not unmap-ped2084continue2085ifgitConfigBool("core.ignorecase"):2086 res['depotFile'] = res['depotFile'].lower()2087 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])20882089# not found files or unmap files set to ""2090for depotFile in fileArgs:2091ifgitConfigBool("core.ignorecase"):2092 depotFile = depotFile.lower()2093if depotFile not in self.client_spec_path_cache:2094 self.client_spec_path_cache[depotFile] =""20952096defmap_in_client(self, depot_path):2097"""Return the relative location in the client where this2098 depot file should live. Returns "" if the file should2099 not be mapped in the client."""21002101ifgitConfigBool("core.ignorecase"):2102 depot_path = depot_path.lower()21032104if depot_path in self.client_spec_path_cache:2105return self.client_spec_path_cache[depot_path]21062107die("Error:%sis not found in client spec path"% depot_path )2108return""21092110classP4Sync(Command, P4UserMap):2111 delete_actions = ("delete","move/delete","purge")21122113def__init__(self):2114 Command.__init__(self)2115 P4UserMap.__init__(self)2116 self.options = [2117 optparse.make_option("--branch", dest="branch"),2118 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2119 optparse.make_option("--changesfile", dest="changesFile"),2120 optparse.make_option("--silent", dest="silent", action="store_true"),2121 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2122 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2123 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2124help="Import into refs/heads/ , not refs/remotes"),2125 optparse.make_option("--max-changes", dest="maxChanges",2126help="Maximum number of changes to import"),2127 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2128help="Internal block size to use when iteratively calling p4 changes"),2129 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2130help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2131 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2132help="Only sync files that are included in the Perforce Client Spec"),2133 optparse.make_option("-/", dest="cloneExclude",2134 action="append",type="string",2135help="exclude depot path"),2136]2137 self.description ="""Imports from Perforce into a git repository.\n2138 example:2139 //depot/my/project/ -- to import the current head2140 //depot/my/project/@all -- to import everything2141 //depot/my/project/@1,6 -- to import only from revision 1 to 621422143 (a ... is not needed in the path p4 specification, it's added implicitly)"""21442145 self.usage +=" //depot/path[@revRange]"2146 self.silent =False2147 self.createdBranches =set()2148 self.committedChanges =set()2149 self.branch =""2150 self.detectBranches =False2151 self.detectLabels =False2152 self.importLabels =False2153 self.changesFile =""2154 self.syncWithOrigin =True2155 self.importIntoRemotes =True2156 self.maxChanges =""2157 self.changes_block_size =None2158 self.keepRepoPath =False2159 self.depotPaths =None2160 self.p4BranchesInGit = []2161 self.cloneExclude = []2162 self.useClientSpec =False2163 self.useClientSpec_from_options =False2164 self.clientSpecDirs =None2165 self.tempBranches = []2166 self.tempBranchLocation ="git-p4-tmp"2167 self.largeFileSystem =None21682169ifgitConfig('git-p4.largeFileSystem'):2170 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2171 self.largeFileSystem =largeFileSystemConstructor(2172lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2173)21742175ifgitConfig("git-p4.syncFromOrigin") =="false":2176 self.syncWithOrigin =False21772178# This is required for the "append" cloneExclude action2179defensure_value(self, attr, value):2180if nothasattr(self, attr)orgetattr(self, attr)is None:2181setattr(self, attr, value)2182returngetattr(self, attr)21832184# Force a checkpoint in fast-import and wait for it to finish2185defcheckpoint(self):2186 self.gitStream.write("checkpoint\n\n")2187 self.gitStream.write("progress checkpoint\n\n")2188 out = self.gitOutput.readline()2189if self.verbose:2190print"checkpoint finished: "+ out21912192defextractFilesFromCommit(self, commit):2193 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2194for path in self.cloneExclude]2195 files = []2196 fnum =02197while commit.has_key("depotFile%s"% fnum):2198 path = commit["depotFile%s"% fnum]21992200if[p for p in self.cloneExclude2201ifp4PathStartsWith(path, p)]:2202 found =False2203else:2204 found = [p for p in self.depotPaths2205ifp4PathStartsWith(path, p)]2206if not found:2207 fnum = fnum +12208continue22092210file= {}2211file["path"] = path2212file["rev"] = commit["rev%s"% fnum]2213file["action"] = commit["action%s"% fnum]2214file["type"] = commit["type%s"% fnum]2215 files.append(file)2216 fnum = fnum +12217return files22182219defstripRepoPath(self, path, prefixes):2220"""When streaming files, this is called to map a p4 depot path2221 to where it should go in git. The prefixes are either2222 self.depotPaths, or self.branchPrefixes in the case of2223 branch detection."""22242225if self.useClientSpec:2226# branch detection moves files up a level (the branch name)2227# from what client spec interpretation gives2228 path = self.clientSpecDirs.map_in_client(path)2229if self.detectBranches:2230for b in self.knownBranches:2231if path.startswith(b +"/"):2232 path = path[len(b)+1:]22332234elif self.keepRepoPath:2235# Preserve everything in relative path name except leading2236# //depot/; just look at first prefix as they all should2237# be in the same depot.2238 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2239ifp4PathStartsWith(path, depot):2240 path = path[len(depot):]22412242else:2243for p in prefixes:2244ifp4PathStartsWith(path, p):2245 path = path[len(p):]2246break22472248 path =wildcard_decode(path)2249return path22502251defsplitFilesIntoBranches(self, commit):2252"""Look at each depotFile in the commit to figure out to what2253 branch it belongs."""22542255if self.clientSpecDirs:2256 files = self.extractFilesFromCommit(commit)2257 self.clientSpecDirs.update_client_spec_path_cache(files)22582259 branches = {}2260 fnum =02261while commit.has_key("depotFile%s"% fnum):2262 path = commit["depotFile%s"% fnum]2263 found = [p for p in self.depotPaths2264ifp4PathStartsWith(path, p)]2265if not found:2266 fnum = fnum +12267continue22682269file= {}2270file["path"] = path2271file["rev"] = commit["rev%s"% fnum]2272file["action"] = commit["action%s"% fnum]2273file["type"] = commit["type%s"% fnum]2274 fnum = fnum +122752276# start with the full relative path where this file would2277# go in a p4 client2278if self.useClientSpec:2279 relPath = self.clientSpecDirs.map_in_client(path)2280else:2281 relPath = self.stripRepoPath(path, self.depotPaths)22822283for branch in self.knownBranches.keys():2284# add a trailing slash so that a commit into qt/4.2foo2285# doesn't end up in qt/4.2, e.g.2286if relPath.startswith(branch +"/"):2287if branch not in branches:2288 branches[branch] = []2289 branches[branch].append(file)2290break22912292return branches22932294defwriteToGitStream(self, gitMode, relPath, contents):2295 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2296 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2297for d in contents:2298 self.gitStream.write(d)2299 self.gitStream.write('\n')23002301# output one file from the P4 stream2302# - helper for streamP4Files23032304defstreamOneP4File(self,file, contents):2305 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2306if verbose:2307 size =int(self.stream_file['fileSize'])2308 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2309 sys.stdout.flush()23102311(type_base, type_mods) =split_p4_type(file["type"])23122313 git_mode ="100644"2314if"x"in type_mods:2315 git_mode ="100755"2316if type_base =="symlink":2317 git_mode ="120000"2318# p4 print on a symlink sometimes contains "target\n";2319# if it does, remove the newline2320 data =''.join(contents)2321if not data:2322# Some version of p4 allowed creating a symlink that pointed2323# to nothing. This causes p4 errors when checking out such2324# a change, and errors here too. Work around it by ignoring2325# the bad symlink; hopefully a future change fixes it.2326print"\nIgnoring empty symlink in%s"%file['depotFile']2327return2328elif data[-1] =='\n':2329 contents = [data[:-1]]2330else:2331 contents = [data]23322333if type_base =="utf16":2334# p4 delivers different text in the python output to -G2335# than it does when using "print -o", or normal p4 client2336# operations. utf16 is converted to ascii or utf8, perhaps.2337# But ascii text saved as -t utf16 is completely mangled.2338# Invoke print -o to get the real contents.2339#2340# On windows, the newlines will always be mangled by print, so put2341# them back too. This is not needed to the cygwin windows version,2342# just the native "NT" type.2343#2344 text =p4_read_pipe(['print','-q','-o','-',"%s@%s"% (file['depotFile'],file['change']) ])2345ifp4_version_string().find("/NT") >=0:2346 text = text.replace("\r\n","\n")2347 contents = [ text ]23482349if type_base =="apple":2350# Apple filetype files will be streamed as a concatenation of2351# its appledouble header and the contents. This is useless2352# on both macs and non-macs. If using "print -q -o xx", it2353# will create "xx" with the data, and "%xx" with the header.2354# This is also not very useful.2355#2356# Ideally, someday, this script can learn how to generate2357# appledouble files directly and import those to git, but2358# non-mac machines can never find a use for apple filetype.2359print"\nIgnoring apple filetype file%s"%file['depotFile']2360return23612362# Note that we do not try to de-mangle keywords on utf16 files,2363# even though in theory somebody may want that.2364 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2365if pattern:2366 regexp = re.compile(pattern, re.VERBOSE)2367 text =''.join(contents)2368 text = regexp.sub(r'$\1$', text)2369 contents = [ text ]23702371if self.largeFileSystem:2372(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)23732374 self.writeToGitStream(git_mode, relPath, contents)23752376defstreamOneP4Deletion(self,file):2377 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2378if verbose:2379 sys.stdout.write("delete%s\n"% relPath)2380 sys.stdout.flush()2381 self.gitStream.write("D%s\n"% relPath)23822383if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2384 self.largeFileSystem.removeLargeFile(relPath)23852386# handle another chunk of streaming data2387defstreamP4FilesCb(self, marshalled):23882389# catch p4 errors and complain2390 err =None2391if"code"in marshalled:2392if marshalled["code"] =="error":2393if"data"in marshalled:2394 err = marshalled["data"].rstrip()23952396if not err and'fileSize'in self.stream_file:2397 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2398if required_bytes >0:2399 err ='Not enough space left on%s! Free at least%iMB.'% (2400 os.getcwd(), required_bytes/1024/10242401)24022403if err:2404 f =None2405if self.stream_have_file_info:2406if"depotFile"in self.stream_file:2407 f = self.stream_file["depotFile"]2408# force a failure in fast-import, else an empty2409# commit will be made2410 self.gitStream.write("\n")2411 self.gitStream.write("die-now\n")2412 self.gitStream.close()2413# ignore errors, but make sure it exits first2414 self.importProcess.wait()2415if f:2416die("Error from p4 print for%s:%s"% (f, err))2417else:2418die("Error from p4 print:%s"% err)24192420if marshalled.has_key('depotFile')and self.stream_have_file_info:2421# start of a new file - output the old one first2422 self.streamOneP4File(self.stream_file, self.stream_contents)2423 self.stream_file = {}2424 self.stream_contents = []2425 self.stream_have_file_info =False24262427# pick up the new file information... for the2428# 'data' field we need to append to our array2429for k in marshalled.keys():2430if k =='data':2431if'streamContentSize'not in self.stream_file:2432 self.stream_file['streamContentSize'] =02433 self.stream_file['streamContentSize'] +=len(marshalled['data'])2434 self.stream_contents.append(marshalled['data'])2435else:2436 self.stream_file[k] = marshalled[k]24372438if(verbose and2439'streamContentSize'in self.stream_file and2440'fileSize'in self.stream_file and2441'depotFile'in self.stream_file):2442 size =int(self.stream_file["fileSize"])2443if size >0:2444 progress =100*self.stream_file['streamContentSize']/size2445 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2446 sys.stdout.flush()24472448 self.stream_have_file_info =True24492450# Stream directly from "p4 files" into "git fast-import"2451defstreamP4Files(self, files):2452 filesForCommit = []2453 filesToRead = []2454 filesToDelete = []24552456for f in files:2457# if using a client spec, only add the files that have2458# a path in the client2459if self.clientSpecDirs:2460if self.clientSpecDirs.map_in_client(f['path']) =="":2461continue24622463 filesForCommit.append(f)2464if f['action']in self.delete_actions:2465 filesToDelete.append(f)2466else:2467 filesToRead.append(f)24682469# deleted files...2470for f in filesToDelete:2471 self.streamOneP4Deletion(f)24722473iflen(filesToRead) >0:2474 self.stream_file = {}2475 self.stream_contents = []2476 self.stream_have_file_info =False24772478# curry self argument2479defstreamP4FilesCbSelf(entry):2480 self.streamP4FilesCb(entry)24812482 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]24832484p4CmdList(["-x","-","print"],2485 stdin=fileArgs,2486 cb=streamP4FilesCbSelf)24872488# do the last chunk2489if self.stream_file.has_key('depotFile'):2490 self.streamOneP4File(self.stream_file, self.stream_contents)24912492defmake_email(self, userid):2493if userid in self.users:2494return self.users[userid]2495else:2496return"%s<a@b>"% userid24972498# Stream a p4 tag2499defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2500if verbose:2501print"writing tag%sfor commit%s"% (labelName, commit)2502 gitStream.write("tag%s\n"% labelName)2503 gitStream.write("from%s\n"% commit)25042505if labelDetails.has_key('Owner'):2506 owner = labelDetails["Owner"]2507else:2508 owner =None25092510# Try to use the owner of the p4 label, or failing that,2511# the current p4 user id.2512if owner:2513 email = self.make_email(owner)2514else:2515 email = self.make_email(self.p4UserId())2516 tagger ="%s %s %s"% (email, epoch, self.tz)25172518 gitStream.write("tagger%s\n"% tagger)25192520print"labelDetails=",labelDetails2521if labelDetails.has_key('Description'):2522 description = labelDetails['Description']2523else:2524 description ='Label from git p4'25252526 gitStream.write("data%d\n"%len(description))2527 gitStream.write(description)2528 gitStream.write("\n")25292530defcommit(self, details, files, branch, parent =""):2531 epoch = details["time"]2532 author = details["user"]25332534if self.verbose:2535print"commit into%s"% branch25362537# start with reading files; if that fails, we should not2538# create a commit.2539 new_files = []2540for f in files:2541if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2542 new_files.append(f)2543else:2544 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])25452546if self.clientSpecDirs:2547 self.clientSpecDirs.update_client_spec_path_cache(files)25482549 self.gitStream.write("commit%s\n"% branch)2550# gitStream.write("mark :%s\n" % details["change"])2551 self.committedChanges.add(int(details["change"]))2552 committer =""2553if author not in self.users:2554 self.getUserMapFromPerforceServer()2555 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)25562557 self.gitStream.write("committer%s\n"% committer)25582559 self.gitStream.write("data <<EOT\n")2560 self.gitStream.write(details["desc"])2561 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2562(','.join(self.branchPrefixes), details["change"]))2563iflen(details['options']) >0:2564 self.gitStream.write(": options =%s"% details['options'])2565 self.gitStream.write("]\nEOT\n\n")25662567iflen(parent) >0:2568if self.verbose:2569print"parent%s"% parent2570 self.gitStream.write("from%s\n"% parent)25712572 self.streamP4Files(new_files)2573 self.gitStream.write("\n")25742575 change =int(details["change"])25762577if self.labels.has_key(change):2578 label = self.labels[change]2579 labelDetails = label[0]2580 labelRevisions = label[1]2581if self.verbose:2582print"Change%sis labelled%s"% (change, labelDetails)25832584 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2585for p in self.branchPrefixes])25862587iflen(files) ==len(labelRevisions):25882589 cleanedFiles = {}2590for info in files:2591if info["action"]in self.delete_actions:2592continue2593 cleanedFiles[info["depotFile"]] = info["rev"]25942595if cleanedFiles == labelRevisions:2596 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)25972598else:2599if not self.silent:2600print("Tag%sdoes not match with change%s: files do not match."2601% (labelDetails["label"], change))26022603else:2604if not self.silent:2605print("Tag%sdoes not match with change%s: file count is different."2606% (labelDetails["label"], change))26072608# Build a dictionary of changelists and labels, for "detect-labels" option.2609defgetLabels(self):2610 self.labels = {}26112612 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2613iflen(l) >0and not self.silent:2614print"Finding files belonging to labels in%s"% `self.depotPaths`26152616for output in l:2617 label = output["label"]2618 revisions = {}2619 newestChange =02620if self.verbose:2621print"Querying files for label%s"% label2622forfileinp4CmdList(["files"] +2623["%s...@%s"% (p, label)2624for p in self.depotPaths]):2625 revisions[file["depotFile"]] =file["rev"]2626 change =int(file["change"])2627if change > newestChange:2628 newestChange = change26292630 self.labels[newestChange] = [output, revisions]26312632if self.verbose:2633print"Label changes:%s"% self.labels.keys()26342635# Import p4 labels as git tags. A direct mapping does not2636# exist, so assume that if all the files are at the same revision2637# then we can use that, or it's something more complicated we should2638# just ignore.2639defimportP4Labels(self, stream, p4Labels):2640if verbose:2641print"import p4 labels: "+' '.join(p4Labels)26422643 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2644 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2645iflen(validLabelRegexp) ==0:2646 validLabelRegexp = defaultLabelRegexp2647 m = re.compile(validLabelRegexp)26482649for name in p4Labels:2650 commitFound =False26512652if not m.match(name):2653if verbose:2654print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2655continue26562657if name in ignoredP4Labels:2658continue26592660 labelDetails =p4CmdList(['label',"-o", name])[0]26612662# get the most recent changelist for each file in this label2663 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2664for p in self.depotPaths])26652666if change.has_key('change'):2667# find the corresponding git commit; take the oldest commit2668 changelist =int(change['change'])2669 gitCommit =read_pipe(["git","rev-list","--max-count=1",2670"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2671iflen(gitCommit) ==0:2672print"could not find git commit for changelist%d"% changelist2673else:2674 gitCommit = gitCommit.strip()2675 commitFound =True2676# Convert from p4 time format2677try:2678 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2679exceptValueError:2680print"Could not convert label time%s"% labelDetails['Update']2681 tmwhen =126822683 when =int(time.mktime(tmwhen))2684 self.streamTag(stream, name, labelDetails, gitCommit, when)2685if verbose:2686print"p4 label%smapped to git commit%s"% (name, gitCommit)2687else:2688if verbose:2689print"Label%shas no changelists - possibly deleted?"% name26902691if not commitFound:2692# We can't import this label; don't try again as it will get very2693# expensive repeatedly fetching all the files for labels that will2694# never be imported. If the label is moved in the future, the2695# ignore will need to be removed manually.2696system(["git","config","--add","git-p4.ignoredP4Labels", name])26972698defguessProjectName(self):2699for p in self.depotPaths:2700if p.endswith("/"):2701 p = p[:-1]2702 p = p[p.strip().rfind("/") +1:]2703if not p.endswith("/"):2704 p +="/"2705return p27062707defgetBranchMapping(self):2708 lostAndFoundBranches =set()27092710 user =gitConfig("git-p4.branchUser")2711iflen(user) >0:2712 command ="branches -u%s"% user2713else:2714 command ="branches"27152716for info inp4CmdList(command):2717 details =p4Cmd(["branch","-o", info["branch"]])2718 viewIdx =02719while details.has_key("View%s"% viewIdx):2720 paths = details["View%s"% viewIdx].split(" ")2721 viewIdx = viewIdx +12722# require standard //depot/foo/... //depot/bar/... mapping2723iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2724continue2725 source = paths[0]2726 destination = paths[1]2727## HACK2728ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2729 source = source[len(self.depotPaths[0]):-4]2730 destination = destination[len(self.depotPaths[0]):-4]27312732if destination in self.knownBranches:2733if not self.silent:2734print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2735print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2736continue27372738 self.knownBranches[destination] = source27392740 lostAndFoundBranches.discard(destination)27412742if source not in self.knownBranches:2743 lostAndFoundBranches.add(source)27442745# Perforce does not strictly require branches to be defined, so we also2746# check git config for a branch list.2747#2748# Example of branch definition in git config file:2749# [git-p4]2750# branchList=main:branchA2751# branchList=main:branchB2752# branchList=branchA:branchC2753 configBranches =gitConfigList("git-p4.branchList")2754for branch in configBranches:2755if branch:2756(source, destination) = branch.split(":")2757 self.knownBranches[destination] = source27582759 lostAndFoundBranches.discard(destination)27602761if source not in self.knownBranches:2762 lostAndFoundBranches.add(source)276327642765for branch in lostAndFoundBranches:2766 self.knownBranches[branch] = branch27672768defgetBranchMappingFromGitBranches(self):2769 branches =p4BranchesInGit(self.importIntoRemotes)2770for branch in branches.keys():2771if branch =="master":2772 branch ="main"2773else:2774 branch = branch[len(self.projectName):]2775 self.knownBranches[branch] = branch27762777defupdateOptionDict(self, d):2778 option_keys = {}2779if self.keepRepoPath:2780 option_keys['keepRepoPath'] =127812782 d["options"] =' '.join(sorted(option_keys.keys()))27832784defreadOptions(self, d):2785 self.keepRepoPath = (d.has_key('options')2786and('keepRepoPath'in d['options']))27872788defgitRefForBranch(self, branch):2789if branch =="main":2790return self.refPrefix +"master"27912792iflen(branch) <=0:2793return branch27942795return self.refPrefix + self.projectName + branch27962797defgitCommitByP4Change(self, ref, change):2798if self.verbose:2799print"looking in ref "+ ref +" for change%susing bisect..."% change28002801 earliestCommit =""2802 latestCommit =parseRevision(ref)28032804while True:2805if self.verbose:2806print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2807 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2808iflen(next) ==0:2809if self.verbose:2810print"argh"2811return""2812 log =extractLogMessageFromGitCommit(next)2813 settings =extractSettingsGitLog(log)2814 currentChange =int(settings['change'])2815if self.verbose:2816print"current change%s"% currentChange28172818if currentChange == change:2819if self.verbose:2820print"found%s"% next2821return next28222823if currentChange < change:2824 earliestCommit ="^%s"% next2825else:2826 latestCommit ="%s"% next28272828return""28292830defimportNewBranch(self, branch, maxChange):2831# make fast-import flush all changes to disk and update the refs using the checkpoint2832# command so that we can try to find the branch parent in the git history2833 self.gitStream.write("checkpoint\n\n");2834 self.gitStream.flush();2835 branchPrefix = self.depotPaths[0] + branch +"/"2836range="@1,%s"% maxChange2837#print "prefix" + branchPrefix2838 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2839iflen(changes) <=0:2840return False2841 firstChange = changes[0]2842#print "first change in branch: %s" % firstChange2843 sourceBranch = self.knownBranches[branch]2844 sourceDepotPath = self.depotPaths[0] + sourceBranch2845 sourceRef = self.gitRefForBranch(sourceBranch)2846#print "source " + sourceBranch28472848 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2849#print "branch parent: %s" % branchParentChange2850 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2851iflen(gitParent) >0:2852 self.initialParents[self.gitRefForBranch(branch)] = gitParent2853#print "parent git commit: %s" % gitParent28542855 self.importChanges(changes)2856return True28572858defsearchParent(self, parent, branch, target):2859 parentFound =False2860for blob inread_pipe_lines(["git","rev-list","--reverse",2861"--no-merges", parent]):2862 blob = blob.strip()2863iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2864 parentFound =True2865if self.verbose:2866print"Found parent of%sin commit%s"% (branch, blob)2867break2868if parentFound:2869return blob2870else:2871return None28722873defimportChanges(self, changes):2874 cnt =12875for change in changes:2876 description =p4_describe(change)2877 self.updateOptionDict(description)28782879if not self.silent:2880 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2881 sys.stdout.flush()2882 cnt = cnt +128832884try:2885if self.detectBranches:2886 branches = self.splitFilesIntoBranches(description)2887for branch in branches.keys():2888## HACK --hwn2889 branchPrefix = self.depotPaths[0] + branch +"/"2890 self.branchPrefixes = [ branchPrefix ]28912892 parent =""28932894 filesForCommit = branches[branch]28952896if self.verbose:2897print"branch is%s"% branch28982899 self.updatedBranches.add(branch)29002901if branch not in self.createdBranches:2902 self.createdBranches.add(branch)2903 parent = self.knownBranches[branch]2904if parent == branch:2905 parent =""2906else:2907 fullBranch = self.projectName + branch2908if fullBranch not in self.p4BranchesInGit:2909if not self.silent:2910print("\nImporting new branch%s"% fullBranch);2911if self.importNewBranch(branch, change -1):2912 parent =""2913 self.p4BranchesInGit.append(fullBranch)2914if not self.silent:2915print("\nResuming with change%s"% change);29162917if self.verbose:2918print"parent determined through known branches:%s"% parent29192920 branch = self.gitRefForBranch(branch)2921 parent = self.gitRefForBranch(parent)29222923if self.verbose:2924print"looking for initial parent for%s; current parent is%s"% (branch, parent)29252926iflen(parent) ==0and branch in self.initialParents:2927 parent = self.initialParents[branch]2928del self.initialParents[branch]29292930 blob =None2931iflen(parent) >0:2932 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2933if self.verbose:2934print"Creating temporary branch: "+ tempBranch2935 self.commit(description, filesForCommit, tempBranch)2936 self.tempBranches.append(tempBranch)2937 self.checkpoint()2938 blob = self.searchParent(parent, branch, tempBranch)2939if blob:2940 self.commit(description, filesForCommit, branch, blob)2941else:2942if self.verbose:2943print"Parent of%snot found. Committing into head of%s"% (branch, parent)2944 self.commit(description, filesForCommit, branch, parent)2945else:2946 files = self.extractFilesFromCommit(description)2947 self.commit(description, files, self.branch,2948 self.initialParent)2949# only needed once, to connect to the previous commit2950 self.initialParent =""2951exceptIOError:2952print self.gitError.read()2953 sys.exit(1)29542955defimportHeadRevision(self, revision):2956print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)29572958 details = {}2959 details["user"] ="git perforce import user"2960 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2961% (' '.join(self.depotPaths), revision))2962 details["change"] = revision2963 newestRevision =029642965 fileCnt =02966 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]29672968for info inp4CmdList(["files"] + fileArgs):29692970if'code'in info and info['code'] =='error':2971 sys.stderr.write("p4 returned an error:%s\n"2972% info['data'])2973if info['data'].find("must refer to client") >=0:2974 sys.stderr.write("This particular p4 error is misleading.\n")2975 sys.stderr.write("Perhaps the depot path was misspelled.\n");2976 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2977 sys.exit(1)2978if'p4ExitCode'in info:2979 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2980 sys.exit(1)298129822983 change =int(info["change"])2984if change > newestRevision:2985 newestRevision = change29862987if info["action"]in self.delete_actions:2988# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2989#fileCnt = fileCnt + 12990continue29912992for prop in["depotFile","rev","action","type"]:2993 details["%s%s"% (prop, fileCnt)] = info[prop]29942995 fileCnt = fileCnt +129962997 details["change"] = newestRevision29982999# Use time from top-most change so that all git p4 clones of3000# the same p4 repo have the same commit SHA1s.3001 res =p4_describe(newestRevision)3002 details["time"] = res["time"]30033004 self.updateOptionDict(details)3005try:3006 self.commit(details, self.extractFilesFromCommit(details), self.branch)3007exceptIOError:3008print"IO error with git fast-import. Is your git version recent enough?"3009print self.gitError.read()301030113012defrun(self, args):3013 self.depotPaths = []3014 self.changeRange =""3015 self.previousDepotPaths = []3016 self.hasOrigin =False30173018# map from branch depot path to parent branch3019 self.knownBranches = {}3020 self.initialParents = {}30213022if self.importIntoRemotes:3023 self.refPrefix ="refs/remotes/p4/"3024else:3025 self.refPrefix ="refs/heads/p4/"30263027if self.syncWithOrigin:3028 self.hasOrigin =originP4BranchesExist()3029if self.hasOrigin:3030if not self.silent:3031print'Syncing with origin first, using "git fetch origin"'3032system("git fetch origin")30333034 branch_arg_given =bool(self.branch)3035iflen(self.branch) ==0:3036 self.branch = self.refPrefix +"master"3037ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3038system("git update-ref%srefs/heads/p4"% self.branch)3039system("git branch -D p4")30403041# accept either the command-line option, or the configuration variable3042if self.useClientSpec:3043# will use this after clone to set the variable3044 self.useClientSpec_from_options =True3045else:3046ifgitConfigBool("git-p4.useclientspec"):3047 self.useClientSpec =True3048if self.useClientSpec:3049 self.clientSpecDirs =getClientSpec()30503051# TODO: should always look at previous commits,3052# merge with previous imports, if possible.3053if args == []:3054if self.hasOrigin:3055createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)30563057# branches holds mapping from branch name to sha13058 branches =p4BranchesInGit(self.importIntoRemotes)30593060# restrict to just this one, disabling detect-branches3061if branch_arg_given:3062 short = self.branch.split("/")[-1]3063if short in branches:3064 self.p4BranchesInGit = [ short ]3065else:3066 self.p4BranchesInGit = branches.keys()30673068iflen(self.p4BranchesInGit) >1:3069if not self.silent:3070print"Importing from/into multiple branches"3071 self.detectBranches =True3072for branch in branches.keys():3073 self.initialParents[self.refPrefix + branch] = \3074 branches[branch]30753076if self.verbose:3077print"branches:%s"% self.p4BranchesInGit30783079 p4Change =03080for branch in self.p4BranchesInGit:3081 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)30823083 settings =extractSettingsGitLog(logMsg)30843085 self.readOptions(settings)3086if(settings.has_key('depot-paths')3087and settings.has_key('change')):3088 change =int(settings['change']) +13089 p4Change =max(p4Change, change)30903091 depotPaths =sorted(settings['depot-paths'])3092if self.previousDepotPaths == []:3093 self.previousDepotPaths = depotPaths3094else:3095 paths = []3096for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3097 prev_list = prev.split("/")3098 cur_list = cur.split("/")3099for i inrange(0,min(len(cur_list),len(prev_list))):3100if cur_list[i] <> prev_list[i]:3101 i = i -13102break31033104 paths.append("/".join(cur_list[:i +1]))31053106 self.previousDepotPaths = paths31073108if p4Change >0:3109 self.depotPaths =sorted(self.previousDepotPaths)3110 self.changeRange ="@%s,#head"% p4Change3111if not self.silent and not self.detectBranches:3112print"Performing incremental import into%sgit branch"% self.branch31133114# accept multiple ref name abbreviations:3115# refs/foo/bar/branch -> use it exactly3116# p4/branch -> prepend refs/remotes/ or refs/heads/3117# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3118if not self.branch.startswith("refs/"):3119if self.importIntoRemotes:3120 prepend ="refs/remotes/"3121else:3122 prepend ="refs/heads/"3123if not self.branch.startswith("p4/"):3124 prepend +="p4/"3125 self.branch = prepend + self.branch31263127iflen(args) ==0and self.depotPaths:3128if not self.silent:3129print"Depot paths:%s"%' '.join(self.depotPaths)3130else:3131if self.depotPaths and self.depotPaths != args:3132print("previous import used depot path%sand now%swas specified. "3133"This doesn't work!"% (' '.join(self.depotPaths),3134' '.join(args)))3135 sys.exit(1)31363137 self.depotPaths =sorted(args)31383139 revision =""3140 self.users = {}31413142# Make sure no revision specifiers are used when --changesfile3143# is specified.3144 bad_changesfile =False3145iflen(self.changesFile) >0:3146for p in self.depotPaths:3147if p.find("@") >=0or p.find("#") >=0:3148 bad_changesfile =True3149break3150if bad_changesfile:3151die("Option --changesfile is incompatible with revision specifiers")31523153 newPaths = []3154for p in self.depotPaths:3155if p.find("@") != -1:3156 atIdx = p.index("@")3157 self.changeRange = p[atIdx:]3158if self.changeRange =="@all":3159 self.changeRange =""3160elif','not in self.changeRange:3161 revision = self.changeRange3162 self.changeRange =""3163 p = p[:atIdx]3164elif p.find("#") != -1:3165 hashIdx = p.index("#")3166 revision = p[hashIdx:]3167 p = p[:hashIdx]3168elif self.previousDepotPaths == []:3169# pay attention to changesfile, if given, else import3170# the entire p4 tree at the head revision3171iflen(self.changesFile) ==0:3172 revision ="#head"31733174 p = re.sub("\.\.\.$","", p)3175if not p.endswith("/"):3176 p +="/"31773178 newPaths.append(p)31793180 self.depotPaths = newPaths31813182# --detect-branches may change this for each branch3183 self.branchPrefixes = self.depotPaths31843185 self.loadUserMapFromCache()3186 self.labels = {}3187if self.detectLabels:3188 self.getLabels();31893190if self.detectBranches:3191## FIXME - what's a P4 projectName ?3192 self.projectName = self.guessProjectName()31933194if self.hasOrigin:3195 self.getBranchMappingFromGitBranches()3196else:3197 self.getBranchMapping()3198if self.verbose:3199print"p4-git branches:%s"% self.p4BranchesInGit3200print"initial parents:%s"% self.initialParents3201for b in self.p4BranchesInGit:3202if b !="master":32033204## FIXME3205 b = b[len(self.projectName):]3206 self.createdBranches.add(b)32073208 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))32093210 self.importProcess = subprocess.Popen(["git","fast-import"],3211 stdin=subprocess.PIPE,3212 stdout=subprocess.PIPE,3213 stderr=subprocess.PIPE);3214 self.gitOutput = self.importProcess.stdout3215 self.gitStream = self.importProcess.stdin3216 self.gitError = self.importProcess.stderr32173218if revision:3219 self.importHeadRevision(revision)3220else:3221 changes = []32223223iflen(self.changesFile) >0:3224 output =open(self.changesFile).readlines()3225 changeSet =set()3226for line in output:3227 changeSet.add(int(line))32283229for change in changeSet:3230 changes.append(change)32313232 changes.sort()3233else:3234# catch "git p4 sync" with no new branches, in a repo that3235# does not have any existing p4 branches3236iflen(args) ==0:3237if not self.p4BranchesInGit:3238die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")32393240# The default branch is master, unless --branch is used to3241# specify something else. Make sure it exists, or complain3242# nicely about how to use --branch.3243if not self.detectBranches:3244if notbranch_exists(self.branch):3245if branch_arg_given:3246die("Error: branch%sdoes not exist."% self.branch)3247else:3248die("Error: no branch%s; perhaps specify one with --branch."%3249 self.branch)32503251if self.verbose:3252print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3253 self.changeRange)3254 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)32553256iflen(self.maxChanges) >0:3257 changes = changes[:min(int(self.maxChanges),len(changes))]32583259iflen(changes) ==0:3260if not self.silent:3261print"No changes to import!"3262else:3263if not self.silent and not self.detectBranches:3264print"Import destination:%s"% self.branch32653266 self.updatedBranches =set()32673268if not self.detectBranches:3269if args:3270# start a new branch3271 self.initialParent =""3272else:3273# build on a previous revision3274 self.initialParent =parseRevision(self.branch)32753276 self.importChanges(changes)32773278if not self.silent:3279print""3280iflen(self.updatedBranches) >0:3281 sys.stdout.write("Updated branches: ")3282for b in self.updatedBranches:3283 sys.stdout.write("%s"% b)3284 sys.stdout.write("\n")32853286ifgitConfigBool("git-p4.importLabels"):3287 self.importLabels =True32883289if self.importLabels:3290 p4Labels =getP4Labels(self.depotPaths)3291 gitTags =getGitTags()32923293 missingP4Labels = p4Labels - gitTags3294 self.importP4Labels(self.gitStream, missingP4Labels)32953296 self.gitStream.close()3297if self.importProcess.wait() !=0:3298die("fast-import failed:%s"% self.gitError.read())3299 self.gitOutput.close()3300 self.gitError.close()33013302# Cleanup temporary branches created during import3303if self.tempBranches != []:3304for branch in self.tempBranches:3305read_pipe("git update-ref -d%s"% branch)3306 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))33073308# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3309# a convenient shortcut refname "p4".3310if self.importIntoRemotes:3311 head_ref = self.refPrefix +"HEAD"3312if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3313system(["git","symbolic-ref", head_ref, self.branch])33143315return True33163317classP4Rebase(Command):3318def__init__(self):3319 Command.__init__(self)3320 self.options = [3321 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3322]3323 self.importLabels =False3324 self.description = ("Fetches the latest revision from perforce and "3325+"rebases the current work (branch) against it")33263327defrun(self, args):3328 sync =P4Sync()3329 sync.importLabels = self.importLabels3330 sync.run([])33313332return self.rebase()33333334defrebase(self):3335if os.system("git update-index --refresh") !=0:3336die("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.");3337iflen(read_pipe("git diff-index HEAD --")) >0:3338die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");33393340[upstream, settings] =findUpstreamBranchPoint()3341iflen(upstream) ==0:3342die("Cannot find upstream branchpoint for rebase")33433344# the branchpoint may be p4/foo~3, so strip off the parent3345 upstream = re.sub("~[0-9]+$","", upstream)33463347print"Rebasing the current branch onto%s"% upstream3348 oldHead =read_pipe("git rev-parse HEAD").strip()3349system("git rebase%s"% upstream)3350system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3351return True33523353classP4Clone(P4Sync):3354def__init__(self):3355 P4Sync.__init__(self)3356 self.description ="Creates a new git repository and imports from Perforce into it"3357 self.usage ="usage: %prog [options] //depot/path[@revRange]"3358 self.options += [3359 optparse.make_option("--destination", dest="cloneDestination",3360 action='store', default=None,3361help="where to leave result of the clone"),3362 optparse.make_option("--bare", dest="cloneBare",3363 action="store_true", default=False),3364]3365 self.cloneDestination =None3366 self.needsGit =False3367 self.cloneBare =False33683369defdefaultDestination(self, args):3370## TODO: use common prefix of args?3371 depotPath = args[0]3372 depotDir = re.sub("(@[^@]*)$","", depotPath)3373 depotDir = re.sub("(#[^#]*)$","", depotDir)3374 depotDir = re.sub(r"\.\.\.$","", depotDir)3375 depotDir = re.sub(r"/$","", depotDir)3376return os.path.split(depotDir)[1]33773378defrun(self, args):3379iflen(args) <1:3380return False33813382if self.keepRepoPath and not self.cloneDestination:3383 sys.stderr.write("Must specify destination for --keep-path\n")3384 sys.exit(1)33853386 depotPaths = args33873388if not self.cloneDestination andlen(depotPaths) >1:3389 self.cloneDestination = depotPaths[-1]3390 depotPaths = depotPaths[:-1]33913392 self.cloneExclude = ["/"+p for p in self.cloneExclude]3393for p in depotPaths:3394if not p.startswith("//"):3395 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3396return False33973398if not self.cloneDestination:3399 self.cloneDestination = self.defaultDestination(args)34003401print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)34023403if not os.path.exists(self.cloneDestination):3404 os.makedirs(self.cloneDestination)3405chdir(self.cloneDestination)34063407 init_cmd = ["git","init"]3408if self.cloneBare:3409 init_cmd.append("--bare")3410 retcode = subprocess.call(init_cmd)3411if retcode:3412raiseCalledProcessError(retcode, init_cmd)34133414if not P4Sync.run(self, depotPaths):3415return False34163417# create a master branch and check out a work tree3418ifgitBranchExists(self.branch):3419system(["git","branch","master", self.branch ])3420if not self.cloneBare:3421system(["git","checkout","-f"])3422else:3423print'Not checking out any branch, use ' \3424'"git checkout -q -b master <branch>"'34253426# auto-set this variable if invoked with --use-client-spec3427if self.useClientSpec_from_options:3428system("git config --bool git-p4.useclientspec true")34293430return True34313432classP4Branches(Command):3433def__init__(self):3434 Command.__init__(self)3435 self.options = [ ]3436 self.description = ("Shows the git branches that hold imports and their "3437+"corresponding perforce depot paths")3438 self.verbose =False34393440defrun(self, args):3441iforiginP4BranchesExist():3442createOrUpdateBranchesFromOrigin()34433444 cmdline ="git rev-parse --symbolic "3445 cmdline +=" --remotes"34463447for line inread_pipe_lines(cmdline):3448 line = line.strip()34493450if not line.startswith('p4/')or line =="p4/HEAD":3451continue3452 branch = line34533454 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3455 settings =extractSettingsGitLog(log)34563457print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3458return True34593460classHelpFormatter(optparse.IndentedHelpFormatter):3461def__init__(self):3462 optparse.IndentedHelpFormatter.__init__(self)34633464defformat_description(self, description):3465if description:3466return description +"\n"3467else:3468return""34693470defprintUsage(commands):3471print"usage:%s<command> [options]"% sys.argv[0]3472print""3473print"valid commands:%s"%", ".join(commands)3474print""3475print"Try%s<command> --help for command specific help."% sys.argv[0]3476print""34773478commands = {3479"debug": P4Debug,3480"submit": P4Submit,3481"commit": P4Submit,3482"sync": P4Sync,3483"rebase": P4Rebase,3484"clone": P4Clone,3485"rollback": P4RollBack,3486"branches": P4Branches3487}348834893490defmain():3491iflen(sys.argv[1:]) ==0:3492printUsage(commands.keys())3493 sys.exit(2)34943495 cmdName = sys.argv[1]3496try:3497 klass = commands[cmdName]3498 cmd =klass()3499exceptKeyError:3500print"unknown command%s"% cmdName3501print""3502printUsage(commands.keys())3503 sys.exit(2)35043505 options = cmd.options3506 cmd.gitdir = os.environ.get("GIT_DIR",None)35073508 args = sys.argv[2:]35093510 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3511if cmd.needsGit:3512 options.append(optparse.make_option("--git-dir", dest="gitdir"))35133514 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3515 options,3516 description = cmd.description,3517 formatter =HelpFormatter())35183519(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3520global verbose3521 verbose = cmd.verbose3522if cmd.needsGit:3523if cmd.gitdir ==None:3524 cmd.gitdir = os.path.abspath(".git")3525if notisValidGitDir(cmd.gitdir):3526 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3527if os.path.exists(cmd.gitdir):3528 cdup =read_pipe("git rev-parse --show-cdup").strip()3529iflen(cdup) >0:3530chdir(cdup);35313532if notisValidGitDir(cmd.gitdir):3533ifisValidGitDir(cmd.gitdir +"/.git"):3534 cmd.gitdir +="/.git"3535else:3536die("fatal: cannot locate git repository at%s"% cmd.gitdir)35373538 os.environ["GIT_DIR"] = cmd.gitdir35393540if not cmd.run(args):3541 parser.print_help()3542 sys.exit(2)354335443545if __name__ =='__main__':3546main()