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, stderr=subprocess.PIPE, shell=expand) 150(out, err) = p.communicate() 151if p.returncode !=0and not ignore_error: 152die('Command failed:%s\nError:%s'% (str(c), err)) 153return out 154 155defp4_read_pipe(c, ignore_error=False): 156 real_cmd =p4_build_cmd(c) 157returnread_pipe(real_cmd, ignore_error) 158 159defread_pipe_lines(c): 160if verbose: 161 sys.stderr.write('Reading pipe:%s\n'%str(c)) 162 163 expand =isinstance(c, basestring) 164 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 165 pipe = p.stdout 166 val = pipe.readlines() 167if pipe.close()or p.wait(): 168die('Command failed:%s'%str(c)) 169 170return val 171 172defp4_read_pipe_lines(c): 173"""Specifically invoke p4 on the command supplied. """ 174 real_cmd =p4_build_cmd(c) 175returnread_pipe_lines(real_cmd) 176 177defp4_has_command(cmd): 178"""Ask p4 for help on this command. If it returns an error, the 179 command does not exist in this version of p4.""" 180 real_cmd =p4_build_cmd(["help", cmd]) 181 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 182 stderr=subprocess.PIPE) 183 p.communicate() 184return p.returncode ==0 185 186defp4_has_move_command(): 187"""See if the move command exists, that it supports -k, and that 188 it has not been administratively disabled. The arguments 189 must be correct, but the filenames do not have to exist. Use 190 ones with wildcards so even if they exist, it will fail.""" 191 192if notp4_has_command("move"): 193return False 194 cmd =p4_build_cmd(["move","-k","@from","@to"]) 195 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 196(out, err) = p.communicate() 197# return code will be 1 in either case 198if err.find("Invalid option") >=0: 199return False 200if err.find("disabled") >=0: 201return False 202# assume it failed because @... was invalid changelist 203return True 204 205defsystem(cmd): 206 expand =isinstance(cmd,basestring) 207if verbose: 208 sys.stderr.write("executing%s\n"%str(cmd)) 209 retcode = subprocess.call(cmd, shell=expand) 210if retcode: 211raiseCalledProcessError(retcode, cmd) 212 213defp4_system(cmd): 214"""Specifically invoke p4 as the system command. """ 215 real_cmd =p4_build_cmd(cmd) 216 expand =isinstance(real_cmd, basestring) 217 retcode = subprocess.call(real_cmd, shell=expand) 218if retcode: 219raiseCalledProcessError(retcode, real_cmd) 220 221_p4_version_string =None 222defp4_version_string(): 223"""Read the version string, showing just the last line, which 224 hopefully is the interesting version bit. 225 226 $ p4 -V 227 Perforce - The Fast Software Configuration Management System. 228 Copyright 1995-2011 Perforce Software. All rights reserved. 229 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 230 """ 231global _p4_version_string 232if not _p4_version_string: 233 a =p4_read_pipe_lines(["-V"]) 234 _p4_version_string = a[-1].rstrip() 235return _p4_version_string 236 237defp4_integrate(src, dest): 238p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 239 240defp4_sync(f, *options): 241p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 242 243defp4_add(f): 244# forcibly add file names with wildcards 245ifwildcard_present(f): 246p4_system(["add","-f", f]) 247else: 248p4_system(["add", f]) 249 250defp4_delete(f): 251p4_system(["delete",wildcard_encode(f)]) 252 253defp4_edit(f): 254p4_system(["edit",wildcard_encode(f)]) 255 256defp4_revert(f): 257p4_system(["revert",wildcard_encode(f)]) 258 259defp4_reopen(type, f): 260p4_system(["reopen","-t",type,wildcard_encode(f)]) 261 262defp4_move(src, dest): 263p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 264 265defp4_last_change(): 266 results =p4CmdList(["changes","-m","1"]) 267returnint(results[0]['change']) 268 269defp4_describe(change): 270"""Make sure it returns a valid result by checking for 271 the presence of field "time". Return a dict of the 272 results.""" 273 274 ds =p4CmdList(["describe","-s",str(change)]) 275iflen(ds) !=1: 276die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 277 278 d = ds[0] 279 280if"p4ExitCode"in d: 281die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 282str(d))) 283if"code"in d: 284if d["code"] =="error": 285die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 286 287if"time"not in d: 288die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 289 290return d 291 292# 293# Canonicalize the p4 type and return a tuple of the 294# base type, plus any modifiers. See "p4 help filetypes" 295# for a list and explanation. 296# 297defsplit_p4_type(p4type): 298 299 p4_filetypes_historical = { 300"ctempobj":"binary+Sw", 301"ctext":"text+C", 302"cxtext":"text+Cx", 303"ktext":"text+k", 304"kxtext":"text+kx", 305"ltext":"text+F", 306"tempobj":"binary+FSw", 307"ubinary":"binary+F", 308"uresource":"resource+F", 309"uxbinary":"binary+Fx", 310"xbinary":"binary+x", 311"xltext":"text+Fx", 312"xtempobj":"binary+Swx", 313"xtext":"text+x", 314"xunicode":"unicode+x", 315"xutf16":"utf16+x", 316} 317if p4type in p4_filetypes_historical: 318 p4type = p4_filetypes_historical[p4type] 319 mods ="" 320 s = p4type.split("+") 321 base = s[0] 322 mods ="" 323iflen(s) >1: 324 mods = s[1] 325return(base, mods) 326 327# 328# return the raw p4 type of a file (text, text+ko, etc) 329# 330defp4_type(f): 331 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 332return results[0]['headType'] 333 334# 335# Given a type base and modifier, return a regexp matching 336# the keywords that can be expanded in the file 337# 338defp4_keywords_regexp_for_type(base, type_mods): 339if base in("text","unicode","binary"): 340 kwords =None 341if"ko"in type_mods: 342 kwords ='Id|Header' 343elif"k"in type_mods: 344 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 345else: 346return None 347 pattern = r""" 348 \$ # Starts with a dollar, followed by... 349 (%s) # one of the keywords, followed by... 350 (:[^$\n]+)? # possibly an old expansion, followed by... 351 \$ # another dollar 352 """% kwords 353return pattern 354else: 355return None 356 357# 358# Given a file, return a regexp matching the possible 359# RCS keywords that will be expanded, or None for files 360# with kw expansion turned off. 361# 362defp4_keywords_regexp_for_file(file): 363if not os.path.exists(file): 364return None 365else: 366(type_base, type_mods) =split_p4_type(p4_type(file)) 367returnp4_keywords_regexp_for_type(type_base, type_mods) 368 369defsetP4ExecBit(file, mode): 370# Reopens an already open file and changes the execute bit to match 371# the execute bit setting in the passed in mode. 372 373 p4Type ="+x" 374 375if notisModeExec(mode): 376 p4Type =getP4OpenedType(file) 377 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 378 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 379if p4Type[-1] =="+": 380 p4Type = p4Type[0:-1] 381 382p4_reopen(p4Type,file) 383 384defgetP4OpenedType(file): 385# Returns the perforce file type for the given file. 386 387 result =p4_read_pipe(["opened",wildcard_encode(file)]) 388 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 389if match: 390return match.group(1) 391else: 392die("Could not determine file type for%s(result: '%s')"% (file, result)) 393 394# Return the set of all p4 labels 395defgetP4Labels(depotPaths): 396 labels =set() 397ifisinstance(depotPaths,basestring): 398 depotPaths = [depotPaths] 399 400for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 401 label = l['label'] 402 labels.add(label) 403 404return labels 405 406# Return the set of all git tags 407defgetGitTags(): 408 gitTags =set() 409for line inread_pipe_lines(["git","tag"]): 410 tag = line.strip() 411 gitTags.add(tag) 412return gitTags 413 414defdiffTreePattern(): 415# This is a simple generator for the diff tree regex pattern. This could be 416# a class variable if this and parseDiffTreeEntry were a part of a class. 417 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 418while True: 419yield pattern 420 421defparseDiffTreeEntry(entry): 422"""Parses a single diff tree entry into its component elements. 423 424 See git-diff-tree(1) manpage for details about the format of the diff 425 output. This method returns a dictionary with the following elements: 426 427 src_mode - The mode of the source file 428 dst_mode - The mode of the destination file 429 src_sha1 - The sha1 for the source file 430 dst_sha1 - The sha1 fr the destination file 431 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 432 status_score - The score for the status (applicable for 'C' and 'R' 433 statuses). This is None if there is no score. 434 src - The path for the source file. 435 dst - The path for the destination file. This is only present for 436 copy or renames. If it is not present, this is None. 437 438 If the pattern is not matched, None is returned.""" 439 440 match =diffTreePattern().next().match(entry) 441if match: 442return{ 443'src_mode': match.group(1), 444'dst_mode': match.group(2), 445'src_sha1': match.group(3), 446'dst_sha1': match.group(4), 447'status': match.group(5), 448'status_score': match.group(6), 449'src': match.group(7), 450'dst': match.group(10) 451} 452return None 453 454defisModeExec(mode): 455# Returns True if the given git mode represents an executable file, 456# otherwise False. 457return mode[-3:] =="755" 458 459defisModeExecChanged(src_mode, dst_mode): 460returnisModeExec(src_mode) !=isModeExec(dst_mode) 461 462defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 463 464ifisinstance(cmd,basestring): 465 cmd ="-G "+ cmd 466 expand =True 467else: 468 cmd = ["-G"] + cmd 469 expand =False 470 471 cmd =p4_build_cmd(cmd) 472if verbose: 473 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 474 475# Use a temporary file to avoid deadlocks without 476# subprocess.communicate(), which would put another copy 477# of stdout into memory. 478 stdin_file =None 479if stdin is not None: 480 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 481ifisinstance(stdin,basestring): 482 stdin_file.write(stdin) 483else: 484for i in stdin: 485 stdin_file.write(i +'\n') 486 stdin_file.flush() 487 stdin_file.seek(0) 488 489 p4 = subprocess.Popen(cmd, 490 shell=expand, 491 stdin=stdin_file, 492 stdout=subprocess.PIPE) 493 494 result = [] 495try: 496while True: 497 entry = marshal.load(p4.stdout) 498if cb is not None: 499cb(entry) 500else: 501 result.append(entry) 502exceptEOFError: 503pass 504 exitCode = p4.wait() 505if exitCode !=0: 506 entry = {} 507 entry["p4ExitCode"] = exitCode 508 result.append(entry) 509 510return result 511 512defp4Cmd(cmd): 513list=p4CmdList(cmd) 514 result = {} 515for entry inlist: 516 result.update(entry) 517return result; 518 519defp4Where(depotPath): 520if not depotPath.endswith("/"): 521 depotPath +="/" 522 depotPathLong = depotPath +"..." 523 outputList =p4CmdList(["where", depotPathLong]) 524 output =None 525for entry in outputList: 526if"depotFile"in entry: 527# Search for the base client side depot path, as long as it starts with the branch's P4 path. 528# The base path always ends with "/...". 529if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 530 output = entry 531break 532elif"data"in entry: 533 data = entry.get("data") 534 space = data.find(" ") 535if data[:space] == depotPath: 536 output = entry 537break 538if output ==None: 539return"" 540if output["code"] =="error": 541return"" 542 clientPath ="" 543if"path"in output: 544 clientPath = output.get("path") 545elif"data"in output: 546 data = output.get("data") 547 lastSpace = data.rfind(" ") 548 clientPath = data[lastSpace +1:] 549 550if clientPath.endswith("..."): 551 clientPath = clientPath[:-3] 552return clientPath 553 554defcurrentGitBranch(): 555returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 556 557defisValidGitDir(path): 558if(os.path.exists(path +"/HEAD") 559and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 560return True; 561return False 562 563defparseRevision(ref): 564returnread_pipe("git rev-parse%s"% ref).strip() 565 566defbranchExists(ref): 567 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 568 ignore_error=True) 569returnlen(rev) >0 570 571defextractLogMessageFromGitCommit(commit): 572 logMessage ="" 573 574## fixme: title is first line of commit, not 1st paragraph. 575 foundTitle =False 576for log inread_pipe_lines("git cat-file commit%s"% commit): 577if not foundTitle: 578iflen(log) ==1: 579 foundTitle =True 580continue 581 582 logMessage += log 583return logMessage 584 585defextractSettingsGitLog(log): 586 values = {} 587for line in log.split("\n"): 588 line = line.strip() 589 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 590if not m: 591continue 592 593 assignments = m.group(1).split(':') 594for a in assignments: 595 vals = a.split('=') 596 key = vals[0].strip() 597 val = ('='.join(vals[1:])).strip() 598if val.endswith('\"')and val.startswith('"'): 599 val = val[1:-1] 600 601 values[key] = val 602 603 paths = values.get("depot-paths") 604if not paths: 605 paths = values.get("depot-path") 606if paths: 607 values['depot-paths'] = paths.split(',') 608return values 609 610defgitBranchExists(branch): 611 proc = subprocess.Popen(["git","rev-parse", branch], 612 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 613return proc.wait() ==0; 614 615_gitConfig = {} 616 617defgitConfig(key, typeSpecifier=None): 618if not _gitConfig.has_key(key): 619 cmd = ["git","config"] 620if typeSpecifier: 621 cmd += [ typeSpecifier ] 622 cmd += [ key ] 623 s =read_pipe(cmd, ignore_error=True) 624 _gitConfig[key] = s.strip() 625return _gitConfig[key] 626 627defgitConfigBool(key): 628"""Return a bool, using git config --bool. It is True only if the 629 variable is set to true, and False if set to false or not present 630 in the config.""" 631 632if not _gitConfig.has_key(key): 633 _gitConfig[key] =gitConfig(key,'--bool') =="true" 634return _gitConfig[key] 635 636defgitConfigInt(key): 637if not _gitConfig.has_key(key): 638 cmd = ["git","config","--int", key ] 639 s =read_pipe(cmd, ignore_error=True) 640 v = s.strip() 641try: 642 _gitConfig[key] =int(gitConfig(key,'--int')) 643exceptValueError: 644 _gitConfig[key] =None 645return _gitConfig[key] 646 647defgitConfigList(key): 648if not _gitConfig.has_key(key): 649 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 650 _gitConfig[key] = s.strip().split(os.linesep) 651if _gitConfig[key] == ['']: 652 _gitConfig[key] = [] 653return _gitConfig[key] 654 655defp4BranchesInGit(branchesAreInRemotes=True): 656"""Find all the branches whose names start with "p4/", looking 657 in remotes or heads as specified by the argument. Return 658 a dictionary of{ branch: revision }for each one found. 659 The branch names are the short names, without any 660 "p4/" prefix.""" 661 662 branches = {} 663 664 cmdline ="git rev-parse --symbolic " 665if branchesAreInRemotes: 666 cmdline +="--remotes" 667else: 668 cmdline +="--branches" 669 670for line inread_pipe_lines(cmdline): 671 line = line.strip() 672 673# only import to p4/ 674if not line.startswith('p4/'): 675continue 676# special symbolic ref to p4/master 677if line =="p4/HEAD": 678continue 679 680# strip off p4/ prefix 681 branch = line[len("p4/"):] 682 683 branches[branch] =parseRevision(line) 684 685return branches 686 687defbranch_exists(branch): 688"""Make sure that the given ref name really exists.""" 689 690 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 691 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 692 out, _ = p.communicate() 693if p.returncode: 694return False 695# expect exactly one line of output: the branch name 696return out.rstrip() == branch 697 698deffindUpstreamBranchPoint(head ="HEAD"): 699 branches =p4BranchesInGit() 700# map from depot-path to branch name 701 branchByDepotPath = {} 702for branch in branches.keys(): 703 tip = branches[branch] 704 log =extractLogMessageFromGitCommit(tip) 705 settings =extractSettingsGitLog(log) 706if settings.has_key("depot-paths"): 707 paths =",".join(settings["depot-paths"]) 708 branchByDepotPath[paths] ="remotes/p4/"+ branch 709 710 settings =None 711 parent =0 712while parent <65535: 713 commit = head +"~%s"% parent 714 log =extractLogMessageFromGitCommit(commit) 715 settings =extractSettingsGitLog(log) 716if settings.has_key("depot-paths"): 717 paths =",".join(settings["depot-paths"]) 718if branchByDepotPath.has_key(paths): 719return[branchByDepotPath[paths], settings] 720 721 parent = parent +1 722 723return["", settings] 724 725defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 726if not silent: 727print("Creating/updating branch(es) in%sbased on origin branch(es)" 728% localRefPrefix) 729 730 originPrefix ="origin/p4/" 731 732for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 733 line = line.strip() 734if(not line.startswith(originPrefix))or line.endswith("HEAD"): 735continue 736 737 headName = line[len(originPrefix):] 738 remoteHead = localRefPrefix + headName 739 originHead = line 740 741 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 742if(not original.has_key('depot-paths') 743or not original.has_key('change')): 744continue 745 746 update =False 747if notgitBranchExists(remoteHead): 748if verbose: 749print"creating%s"% remoteHead 750 update =True 751else: 752 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 753if settings.has_key('change') >0: 754if settings['depot-paths'] == original['depot-paths']: 755 originP4Change =int(original['change']) 756 p4Change =int(settings['change']) 757if originP4Change > p4Change: 758print("%s(%s) is newer than%s(%s). " 759"Updating p4 branch from origin." 760% (originHead, originP4Change, 761 remoteHead, p4Change)) 762 update =True 763else: 764print("Ignoring:%swas imported from%swhile " 765"%swas imported from%s" 766% (originHead,','.join(original['depot-paths']), 767 remoteHead,','.join(settings['depot-paths']))) 768 769if update: 770system("git update-ref%s %s"% (remoteHead, originHead)) 771 772deforiginP4BranchesExist(): 773returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 774 775 776defp4ParseNumericChangeRange(parts): 777 changeStart =int(parts[0][1:]) 778if parts[1] =='#head': 779 changeEnd =p4_last_change() 780else: 781 changeEnd =int(parts[1]) 782 783return(changeStart, changeEnd) 784 785defchooseBlockSize(blockSize): 786if blockSize: 787return blockSize 788else: 789return defaultBlockSize 790 791defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 792assert depotPaths 793 794# Parse the change range into start and end. Try to find integer 795# revision ranges as these can be broken up into blocks to avoid 796# hitting server-side limits (maxrows, maxscanresults). But if 797# that doesn't work, fall back to using the raw revision specifier 798# strings, without using block mode. 799 800if changeRange is None or changeRange =='': 801 changeStart =1 802 changeEnd =p4_last_change() 803 block_size =chooseBlockSize(requestedBlockSize) 804else: 805 parts = changeRange.split(',') 806assertlen(parts) ==2 807try: 808(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 809 block_size =chooseBlockSize(requestedBlockSize) 810except: 811 changeStart = parts[0][1:] 812 changeEnd = parts[1] 813if requestedBlockSize: 814die("cannot use --changes-block-size with non-numeric revisions") 815 block_size =None 816 817# Accumulate change numbers in a dictionary to avoid duplicates 818 changes = {} 819 820for p in depotPaths: 821# Retrieve changes a block at a time, to prevent running 822# into a MaxResults/MaxScanRows error from the server. 823 824while True: 825 cmd = ['changes'] 826 827if block_size: 828 end =min(changeEnd, changeStart + block_size) 829 revisionRange ="%d,%d"% (changeStart, end) 830else: 831 revisionRange ="%s,%s"% (changeStart, changeEnd) 832 833 cmd += ["%s...@%s"% (p, revisionRange)] 834 835for line inp4_read_pipe_lines(cmd): 836 changeNum =int(line.split(" ")[1]) 837 changes[changeNum] =True 838 839if not block_size: 840break 841 842if end >= changeEnd: 843break 844 845 changeStart = end +1 846 847 changelist = changes.keys() 848 changelist.sort() 849return changelist 850 851defp4PathStartsWith(path, prefix): 852# This method tries to remedy a potential mixed-case issue: 853# 854# If UserA adds //depot/DirA/file1 855# and UserB adds //depot/dira/file2 856# 857# we may or may not have a problem. If you have core.ignorecase=true, 858# we treat DirA and dira as the same directory 859ifgitConfigBool("core.ignorecase"): 860return path.lower().startswith(prefix.lower()) 861return path.startswith(prefix) 862 863defgetClientSpec(): 864"""Look at the p4 client spec, create a View() object that contains 865 all the mappings, and return it.""" 866 867 specList =p4CmdList("client -o") 868iflen(specList) !=1: 869die('Output from "client -o" is%dlines, expecting 1'% 870len(specList)) 871 872# dictionary of all client parameters 873 entry = specList[0] 874 875# the //client/ name 876 client_name = entry["Client"] 877 878# just the keys that start with "View" 879 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 880 881# hold this new View 882 view =View(client_name) 883 884# append the lines, in order, to the view 885for view_num inrange(len(view_keys)): 886 k ="View%d"% view_num 887if k not in view_keys: 888die("Expected view key%smissing"% k) 889 view.append(entry[k]) 890 891return view 892 893defgetClientRoot(): 894"""Grab the client directory.""" 895 896 output =p4CmdList("client -o") 897iflen(output) !=1: 898die('Output from "client -o" is%dlines, expecting 1'%len(output)) 899 900 entry = output[0] 901if"Root"not in entry: 902die('Client has no "Root"') 903 904return entry["Root"] 905 906# 907# P4 wildcards are not allowed in filenames. P4 complains 908# if you simply add them, but you can force it with "-f", in 909# which case it translates them into %xx encoding internally. 910# 911defwildcard_decode(path): 912# Search for and fix just these four characters. Do % last so 913# that fixing it does not inadvertently create new %-escapes. 914# Cannot have * in a filename in windows; untested as to 915# what p4 would do in such a case. 916if not platform.system() =="Windows": 917 path = path.replace("%2A","*") 918 path = path.replace("%23","#") \ 919.replace("%40","@") \ 920.replace("%25","%") 921return path 922 923defwildcard_encode(path): 924# do % first to avoid double-encoding the %s introduced here 925 path = path.replace("%","%25") \ 926.replace("*","%2A") \ 927.replace("#","%23") \ 928.replace("@","%40") 929return path 930 931defwildcard_present(path): 932 m = re.search("[*#@%]", path) 933return m is not None 934 935classLargeFileSystem(object): 936"""Base class for large file system support.""" 937 938def__init__(self, writeToGitStream): 939 self.largeFiles =set() 940 self.writeToGitStream = writeToGitStream 941 942defgeneratePointer(self, cloneDestination, contentFile): 943"""Return the content of a pointer file that is stored in Git instead of 944 the actual content.""" 945assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 946 947defpushFile(self, localLargeFile): 948"""Push the actual content which is not stored in the Git repository to 949 a server.""" 950assert False,"Method 'pushFile' required in "+ self.__class__.__name__ 951 952defhasLargeFileExtension(self, relPath): 953returnreduce( 954lambda a, b: a or b, 955[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')], 956False 957) 958 959defgenerateTempFile(self, contents): 960 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 961for d in contents: 962 contentFile.write(d) 963 contentFile.close() 964return contentFile.name 965 966defexceedsLargeFileThreshold(self, relPath, contents): 967ifgitConfigInt('git-p4.largeFileThreshold'): 968 contentsSize =sum(len(d)for d in contents) 969if contentsSize >gitConfigInt('git-p4.largeFileThreshold'): 970return True 971ifgitConfigInt('git-p4.largeFileCompressedThreshold'): 972 contentsSize =sum(len(d)for d in contents) 973if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'): 974return False 975 contentTempFile = self.generateTempFile(contents) 976 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) 977 zf = zipfile.ZipFile(compressedContentFile.name, mode='w') 978 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED) 979 zf.close() 980 compressedContentsSize = zf.infolist()[0].compress_size 981 os.remove(contentTempFile) 982 os.remove(compressedContentFile.name) 983if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'): 984return True 985return False 986 987defaddLargeFile(self, relPath): 988 self.largeFiles.add(relPath) 989 990defremoveLargeFile(self, relPath): 991 self.largeFiles.remove(relPath) 992 993defisLargeFile(self, relPath): 994return relPath in self.largeFiles 995 996defprocessContent(self, git_mode, relPath, contents): 997"""Processes the content of git fast import. This method decides if a 998 file is stored in the large file system and handles all necessary 999 steps."""1000if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1001 contentTempFile = self.generateTempFile(contents)1002(git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)10031004# Move temp file to final location in large file system1005 largeFileDir = os.path.dirname(localLargeFile)1006if not os.path.isdir(largeFileDir):1007 os.makedirs(largeFileDir)1008 shutil.move(contentTempFile, localLargeFile)1009 self.addLargeFile(relPath)1010ifgitConfigBool('git-p4.largeFilePush'):1011 self.pushFile(localLargeFile)1012if verbose:1013 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1014return(git_mode, contents)10151016classMockLFS(LargeFileSystem):1017"""Mock large file system for testing."""10181019defgeneratePointer(self, contentFile):1020"""The pointer content is the original content prefixed with "pointer-".1021 The local filename of the large file storage is derived from the file content.1022 """1023withopen(contentFile,'r')as f:1024 content =next(f)1025 gitMode ='100644'1026 pointerContents ='pointer-'+ content1027 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1028return(gitMode, pointerContents, localLargeFile)10291030defpushFile(self, localLargeFile):1031"""The remote filename of the large file storage is the same as the local1032 one but in a different directory.1033 """1034 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1035if not os.path.exists(remotePath):1036 os.makedirs(remotePath)1037 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10381039classGitLFS(LargeFileSystem):1040"""Git LFS as backend for the git-p4 large file system.1041 See https://git-lfs.github.com/ for details."""10421043def__init__(self, *args):1044 LargeFileSystem.__init__(self, *args)1045 self.baseGitAttributes = []10461047defgeneratePointer(self, contentFile):1048"""Generate a Git LFS pointer for the content. Return LFS Pointer file1049 mode and content which is stored in the Git repository instead of1050 the actual content. Return also the new location of the actual1051 content.1052 """1053 pointerProcess = subprocess.Popen(1054['git','lfs','pointer','--file='+ contentFile],1055 stdout=subprocess.PIPE1056)1057 pointerFile = pointerProcess.stdout.read()1058if pointerProcess.wait():1059 os.remove(contentFile)1060die('git-lfs pointer command failed. Did you install the extension?')1061 pointerContents = [i+'\n'for i in pointerFile.split('\n')[2:][:-1]]1062 oid = pointerContents[1].split(' ')[1].split(':')[1][:-1]1063 localLargeFile = os.path.join(1064 os.getcwd(),1065'.git','lfs','objects', oid[:2], oid[2:4],1066 oid,1067)1068# LFS Spec states that pointer files should not have the executable bit set.1069 gitMode ='100644'1070return(gitMode, pointerContents, localLargeFile)10711072defpushFile(self, localLargeFile):1073 uploadProcess = subprocess.Popen(1074['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1075)1076if uploadProcess.wait():1077die('git-lfs push command failed. Did you define a remote?')10781079defgenerateGitAttributes(self):1080return(1081 self.baseGitAttributes +1082[1083'\n',1084'#\n',1085'# Git LFS (see https://git-lfs.github.com/)\n',1086'#\n',1087] +1088['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1089for f insorted(gitConfigList('git-p4.largeFileExtensions'))1090] +1091['/'+ f.replace(' ','[[:space:]]') +' filter=lfs -text\n'1092for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1093]1094)10951096defaddLargeFile(self, relPath):1097 LargeFileSystem.addLargeFile(self, relPath)1098 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())10991100defremoveLargeFile(self, relPath):1101 LargeFileSystem.removeLargeFile(self, relPath)1102 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11031104defprocessContent(self, git_mode, relPath, contents):1105if relPath =='.gitattributes':1106 self.baseGitAttributes = contents1107return(git_mode, self.generateGitAttributes())1108else:1109return LargeFileSystem.processContent(self, git_mode, relPath, contents)11101111class Command:1112def__init__(self):1113 self.usage ="usage: %prog [options]"1114 self.needsGit =True1115 self.verbose =False11161117class P4UserMap:1118def__init__(self):1119 self.userMapFromPerforceServer =False1120 self.myP4UserId =None11211122defp4UserId(self):1123if self.myP4UserId:1124return self.myP4UserId11251126 results =p4CmdList("user -o")1127for r in results:1128if r.has_key('User'):1129 self.myP4UserId = r['User']1130return r['User']1131die("Could not find your p4 user id")11321133defp4UserIsMe(self, p4User):1134# return True if the given p4 user is actually me1135 me = self.p4UserId()1136if not p4User or p4User != me:1137return False1138else:1139return True11401141defgetUserCacheFilename(self):1142 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1143return home +"/.gitp4-usercache.txt"11441145defgetUserMapFromPerforceServer(self):1146if self.userMapFromPerforceServer:1147return1148 self.users = {}1149 self.emails = {}11501151for output inp4CmdList("users"):1152if not output.has_key("User"):1153continue1154 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1155 self.emails[output["Email"]] = output["User"]115611571158 s =''1159for(key, val)in self.users.items():1160 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))11611162open(self.getUserCacheFilename(),"wb").write(s)1163 self.userMapFromPerforceServer =True11641165defloadUserMapFromCache(self):1166 self.users = {}1167 self.userMapFromPerforceServer =False1168try:1169 cache =open(self.getUserCacheFilename(),"rb")1170 lines = cache.readlines()1171 cache.close()1172for line in lines:1173 entry = line.strip().split("\t")1174 self.users[entry[0]] = entry[1]1175exceptIOError:1176 self.getUserMapFromPerforceServer()11771178classP4Debug(Command):1179def__init__(self):1180 Command.__init__(self)1181 self.options = []1182 self.description ="A tool to debug the output of p4 -G."1183 self.needsGit =False11841185defrun(self, args):1186 j =01187for output inp4CmdList(args):1188print'Element:%d'% j1189 j +=11190print output1191return True11921193classP4RollBack(Command):1194def__init__(self):1195 Command.__init__(self)1196 self.options = [1197 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1198]1199 self.description ="A tool to debug the multi-branch import. Don't use :)"1200 self.rollbackLocalBranches =False12011202defrun(self, args):1203iflen(args) !=1:1204return False1205 maxChange =int(args[0])12061207if"p4ExitCode"inp4Cmd("changes -m 1"):1208die("Problems executing p4");12091210if self.rollbackLocalBranches:1211 refPrefix ="refs/heads/"1212 lines =read_pipe_lines("git rev-parse --symbolic --branches")1213else:1214 refPrefix ="refs/remotes/"1215 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12161217for line in lines:1218if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1219 line = line.strip()1220 ref = refPrefix + line1221 log =extractLogMessageFromGitCommit(ref)1222 settings =extractSettingsGitLog(log)12231224 depotPaths = settings['depot-paths']1225 change = settings['change']12261227 changed =False12281229iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1230for p in depotPaths]))) ==0:1231print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1232system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1233continue12341235while change andint(change) > maxChange:1236 changed =True1237if self.verbose:1238print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1239system("git update-ref%s\"%s^\""% (ref, ref))1240 log =extractLogMessageFromGitCommit(ref)1241 settings =extractSettingsGitLog(log)124212431244 depotPaths = settings['depot-paths']1245 change = settings['change']12461247if changed:1248print"%srewound to%s"% (ref, change)12491250return True12511252classP4Submit(Command, P4UserMap):12531254 conflict_behavior_choices = ("ask","skip","quit")12551256def__init__(self):1257 Command.__init__(self)1258 P4UserMap.__init__(self)1259 self.options = [1260 optparse.make_option("--origin", dest="origin"),1261 optparse.make_option("-M", dest="detectRenames", action="store_true"),1262# preserve the user, requires relevant p4 permissions1263 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1264 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1265 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1266 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1267 optparse.make_option("--conflict", dest="conflict_behavior",1268 choices=self.conflict_behavior_choices),1269 optparse.make_option("--branch", dest="branch"),1270]1271 self.description ="Submit changes from git to the perforce depot."1272 self.usage +=" [name of git branch to submit into perforce depot]"1273 self.origin =""1274 self.detectRenames =False1275 self.preserveUser =gitConfigBool("git-p4.preserveUser")1276 self.dry_run =False1277 self.prepare_p4_only =False1278 self.conflict_behavior =None1279 self.isWindows = (platform.system() =="Windows")1280 self.exportLabels =False1281 self.p4HasMoveCommand =p4_has_move_command()1282 self.branch =None12831284ifgitConfig('git-p4.largeFileSystem'):1285die("Large file system not supported for git-p4 submit command. Please remove it from config.")12861287defcheck(self):1288iflen(p4CmdList("opened ...")) >0:1289die("You have files opened with perforce! Close them before starting the sync.")12901291defseparate_jobs_from_description(self, message):1292"""Extract and return a possible Jobs field in the commit1293 message. It goes into a separate section in the p4 change1294 specification.12951296 A jobs line starts with "Jobs:" and looks like a new field1297 in a form. Values are white-space separated on the same1298 line or on following lines that start with a tab.12991300 This does not parse and extract the full git commit message1301 like a p4 form. It just sees the Jobs: line as a marker1302 to pass everything from then on directly into the p4 form,1303 but outside the description section.13041305 Return a tuple (stripped log message, jobs string)."""13061307 m = re.search(r'^Jobs:', message, re.MULTILINE)1308if m is None:1309return(message,None)13101311 jobtext = message[m.start():]1312 stripped_message = message[:m.start()].rstrip()1313return(stripped_message, jobtext)13141315defprepareLogMessage(self, template, message, jobs):1316"""Edits the template returned from "p4 change -o" to insert1317 the message in the Description field, and the jobs text in1318 the Jobs field."""1319 result =""13201321 inDescriptionSection =False13221323for line in template.split("\n"):1324if line.startswith("#"):1325 result += line +"\n"1326continue13271328if inDescriptionSection:1329if line.startswith("Files:")or line.startswith("Jobs:"):1330 inDescriptionSection =False1331# insert Jobs section1332if jobs:1333 result += jobs +"\n"1334else:1335continue1336else:1337if line.startswith("Description:"):1338 inDescriptionSection =True1339 line +="\n"1340for messageLine in message.split("\n"):1341 line +="\t"+ messageLine +"\n"13421343 result += line +"\n"13441345return result13461347defpatchRCSKeywords(self,file, pattern):1348# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1349(handle, outFileName) = tempfile.mkstemp(dir='.')1350try:1351 outFile = os.fdopen(handle,"w+")1352 inFile =open(file,"r")1353 regexp = re.compile(pattern, re.VERBOSE)1354for line in inFile.readlines():1355 line = regexp.sub(r'$\1$', line)1356 outFile.write(line)1357 inFile.close()1358 outFile.close()1359# Forcibly overwrite the original file1360 os.unlink(file)1361 shutil.move(outFileName,file)1362except:1363# cleanup our temporary file1364 os.unlink(outFileName)1365print"Failed to strip RCS keywords in%s"%file1366raise13671368print"Patched up RCS keywords in%s"%file13691370defp4UserForCommit(self,id):1371# Return the tuple (perforce user,git email) for a given git commit id1372 self.getUserMapFromPerforceServer()1373 gitEmail =read_pipe(["git","log","--max-count=1",1374"--format=%ae",id])1375 gitEmail = gitEmail.strip()1376if not self.emails.has_key(gitEmail):1377return(None,gitEmail)1378else:1379return(self.emails[gitEmail],gitEmail)13801381defcheckValidP4Users(self,commits):1382# check if any git authors cannot be mapped to p4 users1383foridin commits:1384(user,email) = self.p4UserForCommit(id)1385if not user:1386 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1387ifgitConfigBool("git-p4.allowMissingP4Users"):1388print"%s"% msg1389else:1390die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)13911392deflastP4Changelist(self):1393# Get back the last changelist number submitted in this client spec. This1394# then gets used to patch up the username in the change. If the same1395# client spec is being used by multiple processes then this might go1396# wrong.1397 results =p4CmdList("client -o")# find the current client1398 client =None1399for r in results:1400if r.has_key('Client'):1401 client = r['Client']1402break1403if not client:1404die("could not get client spec")1405 results =p4CmdList(["changes","-c", client,"-m","1"])1406for r in results:1407if r.has_key('change'):1408return r['change']1409die("Could not get changelist number for last submit - cannot patch up user details")14101411defmodifyChangelistUser(self, changelist, newUser):1412# fixup the user field of a changelist after it has been submitted.1413 changes =p4CmdList("change -o%s"% changelist)1414iflen(changes) !=1:1415die("Bad output from p4 change modifying%sto user%s"%1416(changelist, newUser))14171418 c = changes[0]1419if c['User'] == newUser:return# nothing to do1420 c['User'] = newUser1421input= marshal.dumps(c)14221423 result =p4CmdList("change -f -i", stdin=input)1424for r in result:1425if r.has_key('code'):1426if r['code'] =='error':1427die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1428if r.has_key('data'):1429print("Updated user field for changelist%sto%s"% (changelist, newUser))1430return1431die("Could not modify user field of changelist%sto%s"% (changelist, newUser))14321433defcanChangeChangelists(self):1434# check to see if we have p4 admin or super-user permissions, either of1435# which are required to modify changelists.1436 results =p4CmdList(["protects", self.depotPath])1437for r in results:1438if r.has_key('perm'):1439if r['perm'] =='admin':1440return11441if r['perm'] =='super':1442return11443return014441445defprepareSubmitTemplate(self):1446"""Run "p4 change -o" to grab a change specification template.1447 This does not use "p4 -G", as it is nice to keep the submission1448 template in original order, since a human might edit it.14491450 Remove lines in the Files section that show changes to files1451 outside the depot path we're committing into."""14521453 template =""1454 inFilesSection =False1455for line inp4_read_pipe_lines(['change','-o']):1456if line.endswith("\r\n"):1457 line = line[:-2] +"\n"1458if inFilesSection:1459if line.startswith("\t"):1460# path starts and ends with a tab1461 path = line[1:]1462 lastTab = path.rfind("\t")1463if lastTab != -1:1464 path = path[:lastTab]1465if notp4PathStartsWith(path, self.depotPath):1466continue1467else:1468 inFilesSection =False1469else:1470if line.startswith("Files:"):1471 inFilesSection =True14721473 template += line14741475return template14761477defedit_template(self, template_file):1478"""Invoke the editor to let the user change the submission1479 message. Return true if okay to continue with the submit."""14801481# if configured to skip the editing part, just submit1482ifgitConfigBool("git-p4.skipSubmitEdit"):1483return True14841485# look at the modification time, to check later if the user saved1486# the file1487 mtime = os.stat(template_file).st_mtime14881489# invoke the editor1490if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1491 editor = os.environ.get("P4EDITOR")1492else:1493 editor =read_pipe("git var GIT_EDITOR").strip()1494system(["sh","-c", ('%s"$@"'% editor), editor, template_file])14951496# If the file was not saved, prompt to see if this patch should1497# be skipped. But skip this verification step if configured so.1498ifgitConfigBool("git-p4.skipSubmitEditCheck"):1499return True15001501# modification time updated means user saved the file1502if os.stat(template_file).st_mtime > mtime:1503return True15041505while True:1506 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1507if response =='y':1508return True1509if response =='n':1510return False15111512defget_diff_description(self, editedFiles, filesToAdd):1513# diff1514if os.environ.has_key("P4DIFF"):1515del(os.environ["P4DIFF"])1516 diff =""1517for editedFile in editedFiles:1518 diff +=p4_read_pipe(['diff','-du',1519wildcard_encode(editedFile)])15201521# new file diff1522 newdiff =""1523for newFile in filesToAdd:1524 newdiff +="==== new file ====\n"1525 newdiff +="--- /dev/null\n"1526 newdiff +="+++%s\n"% newFile1527 f =open(newFile,"r")1528for line in f.readlines():1529 newdiff +="+"+ line1530 f.close()15311532return(diff + newdiff).replace('\r\n','\n')15331534defapplyCommit(self,id):1535"""Apply one commit, return True if it succeeded."""15361537print"Applying",read_pipe(["git","show","-s",1538"--format=format:%h%s",id])15391540(p4User, gitEmail) = self.p4UserForCommit(id)15411542 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1543 filesToAdd =set()1544 filesToDelete =set()1545 editedFiles =set()1546 pureRenameCopy =set()1547 filesToChangeExecBit = {}15481549for line in diff:1550 diff =parseDiffTreeEntry(line)1551 modifier = diff['status']1552 path = diff['src']1553if modifier =="M":1554p4_edit(path)1555ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1556 filesToChangeExecBit[path] = diff['dst_mode']1557 editedFiles.add(path)1558elif modifier =="A":1559 filesToAdd.add(path)1560 filesToChangeExecBit[path] = diff['dst_mode']1561if path in filesToDelete:1562 filesToDelete.remove(path)1563elif modifier =="D":1564 filesToDelete.add(path)1565if path in filesToAdd:1566 filesToAdd.remove(path)1567elif modifier =="C":1568 src, dest = diff['src'], diff['dst']1569p4_integrate(src, dest)1570 pureRenameCopy.add(dest)1571if diff['src_sha1'] != diff['dst_sha1']:1572p4_edit(dest)1573 pureRenameCopy.discard(dest)1574ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1575p4_edit(dest)1576 pureRenameCopy.discard(dest)1577 filesToChangeExecBit[dest] = diff['dst_mode']1578if self.isWindows:1579# turn off read-only attribute1580 os.chmod(dest, stat.S_IWRITE)1581 os.unlink(dest)1582 editedFiles.add(dest)1583elif modifier =="R":1584 src, dest = diff['src'], diff['dst']1585if self.p4HasMoveCommand:1586p4_edit(src)# src must be open before move1587p4_move(src, dest)# opens for (move/delete, move/add)1588else:1589p4_integrate(src, dest)1590if diff['src_sha1'] != diff['dst_sha1']:1591p4_edit(dest)1592else:1593 pureRenameCopy.add(dest)1594ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1595if not self.p4HasMoveCommand:1596p4_edit(dest)# with move: already open, writable1597 filesToChangeExecBit[dest] = diff['dst_mode']1598if not self.p4HasMoveCommand:1599if self.isWindows:1600 os.chmod(dest, stat.S_IWRITE)1601 os.unlink(dest)1602 filesToDelete.add(src)1603 editedFiles.add(dest)1604else:1605die("unknown modifier%sfor%s"% (modifier, path))16061607 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1608 patchcmd = diffcmd +" | git apply "1609 tryPatchCmd = patchcmd +"--check -"1610 applyPatchCmd = patchcmd +"--check --apply -"1611 patch_succeeded =True16121613if os.system(tryPatchCmd) !=0:1614 fixed_rcs_keywords =False1615 patch_succeeded =False1616print"Unfortunately applying the change failed!"16171618# Patch failed, maybe it's just RCS keyword woes. Look through1619# the patch to see if that's possible.1620ifgitConfigBool("git-p4.attemptRCSCleanup"):1621file=None1622 pattern =None1623 kwfiles = {}1624forfilein editedFiles | filesToDelete:1625# did this file's delta contain RCS keywords?1626 pattern =p4_keywords_regexp_for_file(file)16271628if pattern:1629# this file is a possibility...look for RCS keywords.1630 regexp = re.compile(pattern, re.VERBOSE)1631for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1632if regexp.search(line):1633if verbose:1634print"got keyword match on%sin%sin%s"% (pattern, line,file)1635 kwfiles[file] = pattern1636break16371638forfilein kwfiles:1639if verbose:1640print"zapping%swith%s"% (line,pattern)1641# File is being deleted, so not open in p4. Must1642# disable the read-only bit on windows.1643if self.isWindows andfilenot in editedFiles:1644 os.chmod(file, stat.S_IWRITE)1645 self.patchRCSKeywords(file, kwfiles[file])1646 fixed_rcs_keywords =True16471648if fixed_rcs_keywords:1649print"Retrying the patch with RCS keywords cleaned up"1650if os.system(tryPatchCmd) ==0:1651 patch_succeeded =True16521653if not patch_succeeded:1654for f in editedFiles:1655p4_revert(f)1656return False16571658#1659# Apply the patch for real, and do add/delete/+x handling.1660#1661system(applyPatchCmd)16621663for f in filesToAdd:1664p4_add(f)1665for f in filesToDelete:1666p4_revert(f)1667p4_delete(f)16681669# Set/clear executable bits1670for f in filesToChangeExecBit.keys():1671 mode = filesToChangeExecBit[f]1672setP4ExecBit(f, mode)16731674#1675# Build p4 change description, starting with the contents1676# of the git commit message.1677#1678 logMessage =extractLogMessageFromGitCommit(id)1679 logMessage = logMessage.strip()1680(logMessage, jobs) = self.separate_jobs_from_description(logMessage)16811682 template = self.prepareSubmitTemplate()1683 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)16841685if self.preserveUser:1686 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User16871688if self.checkAuthorship and not self.p4UserIsMe(p4User):1689 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1690 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1691 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"16921693 separatorLine ="######## everything below this line is just the diff #######\n"1694if not self.prepare_p4_only:1695 submitTemplate += separatorLine1696 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)16971698(handle, fileName) = tempfile.mkstemp()1699 tmpFile = os.fdopen(handle,"w+b")1700if self.isWindows:1701 submitTemplate = submitTemplate.replace("\n","\r\n")1702 tmpFile.write(submitTemplate)1703 tmpFile.close()17041705if self.prepare_p4_only:1706#1707# Leave the p4 tree prepared, and the submit template around1708# and let the user decide what to do next1709#1710print1711print"P4 workspace prepared for submission."1712print"To submit or revert, go to client workspace"1713print" "+ self.clientPath1714print1715print"To submit, use\"p4 submit\"to write a new description,"1716print"or\"p4 submit -i <%s\"to use the one prepared by" \1717"\"git p4\"."% fileName1718print"You can delete the file\"%s\"when finished."% fileName17191720if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1721print"To preserve change ownership by user%s, you must\n" \1722"do\"p4 change -f <change>\"after submitting and\n" \1723"edit the User field."1724if pureRenameCopy:1725print"After submitting, renamed files must be re-synced."1726print"Invoke\"p4 sync -f\"on each of these files:"1727for f in pureRenameCopy:1728print" "+ f17291730print1731print"To revert the changes, use\"p4 revert ...\", and delete"1732print"the submit template file\"%s\""% fileName1733if filesToAdd:1734print"Since the commit adds new files, they must be deleted:"1735for f in filesToAdd:1736print" "+ f1737print1738return True17391740#1741# Let the user edit the change description, then submit it.1742#1743if self.edit_template(fileName):1744# read the edited message and submit1745 ret =True1746 tmpFile =open(fileName,"rb")1747 message = tmpFile.read()1748 tmpFile.close()1749if self.isWindows:1750 message = message.replace("\r\n","\n")1751 submitTemplate = message[:message.index(separatorLine)]1752p4_write_pipe(['submit','-i'], submitTemplate)17531754if self.preserveUser:1755if p4User:1756# Get last changelist number. Cannot easily get it from1757# the submit command output as the output is1758# unmarshalled.1759 changelist = self.lastP4Changelist()1760 self.modifyChangelistUser(changelist, p4User)17611762# The rename/copy happened by applying a patch that created a1763# new file. This leaves it writable, which confuses p4.1764for f in pureRenameCopy:1765p4_sync(f,"-f")17661767else:1768# skip this patch1769 ret =False1770print"Submission cancelled, undoing p4 changes."1771for f in editedFiles:1772p4_revert(f)1773for f in filesToAdd:1774p4_revert(f)1775 os.remove(f)1776for f in filesToDelete:1777p4_revert(f)17781779 os.remove(fileName)1780return ret17811782# Export git tags as p4 labels. Create a p4 label and then tag1783# with that.1784defexportGitTags(self, gitTags):1785 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1786iflen(validLabelRegexp) ==0:1787 validLabelRegexp = defaultLabelRegexp1788 m = re.compile(validLabelRegexp)17891790for name in gitTags:17911792if not m.match(name):1793if verbose:1794print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1795continue17961797# Get the p4 commit this corresponds to1798 logMessage =extractLogMessageFromGitCommit(name)1799 values =extractSettingsGitLog(logMessage)18001801if not values.has_key('change'):1802# a tag pointing to something not sent to p4; ignore1803if verbose:1804print"git tag%sdoes not give a p4 commit"% name1805continue1806else:1807 changelist = values['change']18081809# Get the tag details.1810 inHeader =True1811 isAnnotated =False1812 body = []1813for l inread_pipe_lines(["git","cat-file","-p", name]):1814 l = l.strip()1815if inHeader:1816if re.match(r'tag\s+', l):1817 isAnnotated =True1818elif re.match(r'\s*$', l):1819 inHeader =False1820continue1821else:1822 body.append(l)18231824if not isAnnotated:1825 body = ["lightweight tag imported by git p4\n"]18261827# Create the label - use the same view as the client spec we are using1828 clientSpec =getClientSpec()18291830 labelTemplate ="Label:%s\n"% name1831 labelTemplate +="Description:\n"1832for b in body:1833 labelTemplate +="\t"+ b +"\n"1834 labelTemplate +="View:\n"1835for depot_side in clientSpec.mappings:1836 labelTemplate +="\t%s\n"% depot_side18371838if self.dry_run:1839print"Would create p4 label%sfor tag"% name1840elif self.prepare_p4_only:1841print"Not creating p4 label%sfor tag due to option" \1842" --prepare-p4-only"% name1843else:1844p4_write_pipe(["label","-i"], labelTemplate)18451846# Use the label1847p4_system(["tag","-l", name] +1848["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])18491850if verbose:1851print"created p4 label for tag%s"% name18521853defrun(self, args):1854iflen(args) ==0:1855 self.master =currentGitBranch()1856iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1857die("Detecting current git branch failed!")1858eliflen(args) ==1:1859 self.master = args[0]1860if notbranchExists(self.master):1861die("Branch%sdoes not exist"% self.master)1862else:1863return False18641865 allowSubmit =gitConfig("git-p4.allowSubmit")1866iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1867die("%sis not in git-p4.allowSubmit"% self.master)18681869[upstream, settings] =findUpstreamBranchPoint()1870 self.depotPath = settings['depot-paths'][0]1871iflen(self.origin) ==0:1872 self.origin = upstream18731874if self.preserveUser:1875if not self.canChangeChangelists():1876die("Cannot preserve user names without p4 super-user or admin permissions")18771878# if not set from the command line, try the config file1879if self.conflict_behavior is None:1880 val =gitConfig("git-p4.conflict")1881if val:1882if val not in self.conflict_behavior_choices:1883die("Invalid value '%s' for config git-p4.conflict"% val)1884else:1885 val ="ask"1886 self.conflict_behavior = val18871888if self.verbose:1889print"Origin branch is "+ self.origin18901891iflen(self.depotPath) ==0:1892print"Internal error: cannot locate perforce depot path from existing branches"1893 sys.exit(128)18941895 self.useClientSpec =False1896ifgitConfigBool("git-p4.useclientspec"):1897 self.useClientSpec =True1898if self.useClientSpec:1899 self.clientSpecDirs =getClientSpec()19001901# Check for the existance of P4 branches1902 branchesDetected = (len(p4BranchesInGit().keys()) >1)19031904if self.useClientSpec and not branchesDetected:1905# all files are relative to the client spec1906 self.clientPath =getClientRoot()1907else:1908 self.clientPath =p4Where(self.depotPath)19091910if self.clientPath =="":1911die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)19121913print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1914 self.oldWorkingDirectory = os.getcwd()19151916# ensure the clientPath exists1917 new_client_dir =False1918if not os.path.exists(self.clientPath):1919 new_client_dir =True1920 os.makedirs(self.clientPath)19211922chdir(self.clientPath, is_client_path=True)1923if self.dry_run:1924print"Would synchronize p4 checkout in%s"% self.clientPath1925else:1926print"Synchronizing p4 checkout..."1927if new_client_dir:1928# old one was destroyed, and maybe nobody told p41929p4_sync("...","-f")1930else:1931p4_sync("...")1932 self.check()19331934 commits = []1935for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1936 commits.append(line.strip())1937 commits.reverse()19381939if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1940 self.checkAuthorship =False1941else:1942 self.checkAuthorship =True19431944if self.preserveUser:1945 self.checkValidP4Users(commits)19461947#1948# Build up a set of options to be passed to diff when1949# submitting each commit to p4.1950#1951if self.detectRenames:1952# command-line -M arg1953 self.diffOpts ="-M"1954else:1955# If not explicitly set check the config variable1956 detectRenames =gitConfig("git-p4.detectRenames")19571958if detectRenames.lower() =="false"or detectRenames =="":1959 self.diffOpts =""1960elif detectRenames.lower() =="true":1961 self.diffOpts ="-M"1962else:1963 self.diffOpts ="-M%s"% detectRenames19641965# no command-line arg for -C or --find-copies-harder, just1966# config variables1967 detectCopies =gitConfig("git-p4.detectCopies")1968if detectCopies.lower() =="false"or detectCopies =="":1969pass1970elif detectCopies.lower() =="true":1971 self.diffOpts +=" -C"1972else:1973 self.diffOpts +=" -C%s"% detectCopies19741975ifgitConfigBool("git-p4.detectCopiesHarder"):1976 self.diffOpts +=" --find-copies-harder"19771978#1979# Apply the commits, one at a time. On failure, ask if should1980# continue to try the rest of the patches, or quit.1981#1982if self.dry_run:1983print"Would apply"1984 applied = []1985 last =len(commits) -11986for i, commit inenumerate(commits):1987if self.dry_run:1988print" ",read_pipe(["git","show","-s",1989"--format=format:%h%s", commit])1990 ok =True1991else:1992 ok = self.applyCommit(commit)1993if ok:1994 applied.append(commit)1995else:1996if self.prepare_p4_only and i < last:1997print"Processing only the first commit due to option" \1998" --prepare-p4-only"1999break2000if i < last:2001 quit =False2002while True:2003# prompt for what to do, or use the option/variable2004if self.conflict_behavior =="ask":2005print"What do you want to do?"2006 response =raw_input("[s]kip this commit but apply"2007" the rest, or [q]uit? ")2008if not response:2009continue2010elif self.conflict_behavior =="skip":2011 response ="s"2012elif self.conflict_behavior =="quit":2013 response ="q"2014else:2015die("Unknown conflict_behavior '%s'"%2016 self.conflict_behavior)20172018if response[0] =="s":2019print"Skipping this commit, but applying the rest"2020break2021if response[0] =="q":2022print"Quitting"2023 quit =True2024break2025if quit:2026break20272028chdir(self.oldWorkingDirectory)20292030if self.dry_run:2031pass2032elif self.prepare_p4_only:2033pass2034eliflen(commits) ==len(applied):2035print"All commits applied!"20362037 sync =P4Sync()2038if self.branch:2039 sync.branch = self.branch2040 sync.run([])20412042 rebase =P4Rebase()2043 rebase.rebase()20442045else:2046iflen(applied) ==0:2047print"No commits applied."2048else:2049print"Applied only the commits marked with '*':"2050for c in commits:2051if c in applied:2052 star ="*"2053else:2054 star =" "2055print star,read_pipe(["git","show","-s",2056"--format=format:%h%s", c])2057print"You will have to do 'git p4 sync' and rebase."20582059ifgitConfigBool("git-p4.exportLabels"):2060 self.exportLabels =True20612062if self.exportLabels:2063 p4Labels =getP4Labels(self.depotPath)2064 gitTags =getGitTags()20652066 missingGitTags = gitTags - p4Labels2067 self.exportGitTags(missingGitTags)20682069# exit with error unless everything applied perfectly2070iflen(commits) !=len(applied):2071 sys.exit(1)20722073return True20742075classView(object):2076"""Represent a p4 view ("p4 help views"), and map files in a2077 repo according to the view."""20782079def__init__(self, client_name):2080 self.mappings = []2081 self.client_prefix ="//%s/"% client_name2082# cache results of "p4 where" to lookup client file locations2083 self.client_spec_path_cache = {}20842085defappend(self, view_line):2086"""Parse a view line, splitting it into depot and client2087 sides. Append to self.mappings, preserving order. This2088 is only needed for tag creation."""20892090# Split the view line into exactly two words. P4 enforces2091# structure on these lines that simplifies this quite a bit.2092#2093# Either or both words may be double-quoted.2094# Single quotes do not matter.2095# Double-quote marks cannot occur inside the words.2096# A + or - prefix is also inside the quotes.2097# There are no quotes unless they contain a space.2098# The line is already white-space stripped.2099# The two words are separated by a single space.2100#2101if view_line[0] =='"':2102# First word is double quoted. Find its end.2103 close_quote_index = view_line.find('"',1)2104if close_quote_index <=0:2105die("No first-word closing quote found:%s"% view_line)2106 depot_side = view_line[1:close_quote_index]2107# skip closing quote and space2108 rhs_index = close_quote_index +1+12109else:2110 space_index = view_line.find(" ")2111if space_index <=0:2112die("No word-splitting space found:%s"% view_line)2113 depot_side = view_line[0:space_index]2114 rhs_index = space_index +121152116# prefix + means overlay on previous mapping2117if depot_side.startswith("+"):2118 depot_side = depot_side[1:]21192120# prefix - means exclude this path, leave out of mappings2121 exclude =False2122if depot_side.startswith("-"):2123 exclude =True2124 depot_side = depot_side[1:]21252126if not exclude:2127 self.mappings.append(depot_side)21282129defconvert_client_path(self, clientFile):2130# chop off //client/ part to make it relative2131if not clientFile.startswith(self.client_prefix):2132die("No prefix '%s' on clientFile '%s'"%2133(self.client_prefix, clientFile))2134return clientFile[len(self.client_prefix):]21352136defupdate_client_spec_path_cache(self, files):2137""" Caching file paths by "p4 where" batch query """21382139# List depot file paths exclude that already cached2140 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]21412142iflen(fileArgs) ==0:2143return# All files in cache21442145 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2146for res in where_result:2147if"code"in res and res["code"] =="error":2148# assume error is "... file(s) not in client view"2149continue2150if"clientFile"not in res:2151die("No clientFile in 'p4 where' output")2152if"unmap"in res:2153# it will list all of them, but only one not unmap-ped2154continue2155ifgitConfigBool("core.ignorecase"):2156 res['depotFile'] = res['depotFile'].lower()2157 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])21582159# not found files or unmap files set to ""2160for depotFile in fileArgs:2161ifgitConfigBool("core.ignorecase"):2162 depotFile = depotFile.lower()2163if depotFile not in self.client_spec_path_cache:2164 self.client_spec_path_cache[depotFile] =""21652166defmap_in_client(self, depot_path):2167"""Return the relative location in the client where this2168 depot file should live. Returns "" if the file should2169 not be mapped in the client."""21702171ifgitConfigBool("core.ignorecase"):2172 depot_path = depot_path.lower()21732174if depot_path in self.client_spec_path_cache:2175return self.client_spec_path_cache[depot_path]21762177die("Error:%sis not found in client spec path"% depot_path )2178return""21792180classP4Sync(Command, P4UserMap):2181 delete_actions = ("delete","move/delete","purge")21822183def__init__(self):2184 Command.__init__(self)2185 P4UserMap.__init__(self)2186 self.options = [2187 optparse.make_option("--branch", dest="branch"),2188 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2189 optparse.make_option("--changesfile", dest="changesFile"),2190 optparse.make_option("--silent", dest="silent", action="store_true"),2191 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2192 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2193 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2194help="Import into refs/heads/ , not refs/remotes"),2195 optparse.make_option("--max-changes", dest="maxChanges",2196help="Maximum number of changes to import"),2197 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2198help="Internal block size to use when iteratively calling p4 changes"),2199 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2200help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2201 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2202help="Only sync files that are included in the Perforce Client Spec"),2203 optparse.make_option("-/", dest="cloneExclude",2204 action="append",type="string",2205help="exclude depot path"),2206]2207 self.description ="""Imports from Perforce into a git repository.\n2208 example:2209 //depot/my/project/ -- to import the current head2210 //depot/my/project/@all -- to import everything2211 //depot/my/project/@1,6 -- to import only from revision 1 to 622122213 (a ... is not needed in the path p4 specification, it's added implicitly)"""22142215 self.usage +=" //depot/path[@revRange]"2216 self.silent =False2217 self.createdBranches =set()2218 self.committedChanges =set()2219 self.branch =""2220 self.detectBranches =False2221 self.detectLabels =False2222 self.importLabels =False2223 self.changesFile =""2224 self.syncWithOrigin =True2225 self.importIntoRemotes =True2226 self.maxChanges =""2227 self.changes_block_size =None2228 self.keepRepoPath =False2229 self.depotPaths =None2230 self.p4BranchesInGit = []2231 self.cloneExclude = []2232 self.useClientSpec =False2233 self.useClientSpec_from_options =False2234 self.clientSpecDirs =None2235 self.tempBranches = []2236 self.tempBranchLocation ="git-p4-tmp"2237 self.largeFileSystem =None22382239ifgitConfig('git-p4.largeFileSystem'):2240 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2241 self.largeFileSystem =largeFileSystemConstructor(2242lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2243)22442245ifgitConfig("git-p4.syncFromOrigin") =="false":2246 self.syncWithOrigin =False22472248# This is required for the "append" cloneExclude action2249defensure_value(self, attr, value):2250if nothasattr(self, attr)orgetattr(self, attr)is None:2251setattr(self, attr, value)2252returngetattr(self, attr)22532254# Force a checkpoint in fast-import and wait for it to finish2255defcheckpoint(self):2256 self.gitStream.write("checkpoint\n\n")2257 self.gitStream.write("progress checkpoint\n\n")2258 out = self.gitOutput.readline()2259if self.verbose:2260print"checkpoint finished: "+ out22612262defextractFilesFromCommit(self, commit):2263 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2264for path in self.cloneExclude]2265 files = []2266 fnum =02267while commit.has_key("depotFile%s"% fnum):2268 path = commit["depotFile%s"% fnum]22692270if[p for p in self.cloneExclude2271ifp4PathStartsWith(path, p)]:2272 found =False2273else:2274 found = [p for p in self.depotPaths2275ifp4PathStartsWith(path, p)]2276if not found:2277 fnum = fnum +12278continue22792280file= {}2281file["path"] = path2282file["rev"] = commit["rev%s"% fnum]2283file["action"] = commit["action%s"% fnum]2284file["type"] = commit["type%s"% fnum]2285 files.append(file)2286 fnum = fnum +12287return files22882289defstripRepoPath(self, path, prefixes):2290"""When streaming files, this is called to map a p4 depot path2291 to where it should go in git. The prefixes are either2292 self.depotPaths, or self.branchPrefixes in the case of2293 branch detection."""22942295if self.useClientSpec:2296# branch detection moves files up a level (the branch name)2297# from what client spec interpretation gives2298 path = self.clientSpecDirs.map_in_client(path)2299if self.detectBranches:2300for b in self.knownBranches:2301if path.startswith(b +"/"):2302 path = path[len(b)+1:]23032304elif self.keepRepoPath:2305# Preserve everything in relative path name except leading2306# //depot/; just look at first prefix as they all should2307# be in the same depot.2308 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2309ifp4PathStartsWith(path, depot):2310 path = path[len(depot):]23112312else:2313for p in prefixes:2314ifp4PathStartsWith(path, p):2315 path = path[len(p):]2316break23172318 path =wildcard_decode(path)2319return path23202321defsplitFilesIntoBranches(self, commit):2322"""Look at each depotFile in the commit to figure out to what2323 branch it belongs."""23242325if self.clientSpecDirs:2326 files = self.extractFilesFromCommit(commit)2327 self.clientSpecDirs.update_client_spec_path_cache(files)23282329 branches = {}2330 fnum =02331while commit.has_key("depotFile%s"% fnum):2332 path = commit["depotFile%s"% fnum]2333 found = [p for p in self.depotPaths2334ifp4PathStartsWith(path, p)]2335if not found:2336 fnum = fnum +12337continue23382339file= {}2340file["path"] = path2341file["rev"] = commit["rev%s"% fnum]2342file["action"] = commit["action%s"% fnum]2343file["type"] = commit["type%s"% fnum]2344 fnum = fnum +123452346# start with the full relative path where this file would2347# go in a p4 client2348if self.useClientSpec:2349 relPath = self.clientSpecDirs.map_in_client(path)2350else:2351 relPath = self.stripRepoPath(path, self.depotPaths)23522353for branch in self.knownBranches.keys():2354# add a trailing slash so that a commit into qt/4.2foo2355# doesn't end up in qt/4.2, e.g.2356if relPath.startswith(branch +"/"):2357if branch not in branches:2358 branches[branch] = []2359 branches[branch].append(file)2360break23612362return branches23632364defwriteToGitStream(self, gitMode, relPath, contents):2365 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2366 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2367for d in contents:2368 self.gitStream.write(d)2369 self.gitStream.write('\n')23702371# output one file from the P4 stream2372# - helper for streamP4Files23732374defstreamOneP4File(self,file, contents):2375 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2376if verbose:2377 size =int(self.stream_file['fileSize'])2378 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2379 sys.stdout.flush()23802381(type_base, type_mods) =split_p4_type(file["type"])23822383 git_mode ="100644"2384if"x"in type_mods:2385 git_mode ="100755"2386if type_base =="symlink":2387 git_mode ="120000"2388# p4 print on a symlink sometimes contains "target\n";2389# if it does, remove the newline2390 data =''.join(contents)2391if not data:2392# Some version of p4 allowed creating a symlink that pointed2393# to nothing. This causes p4 errors when checking out such2394# a change, and errors here too. Work around it by ignoring2395# the bad symlink; hopefully a future change fixes it.2396print"\nIgnoring empty symlink in%s"%file['depotFile']2397return2398elif data[-1] =='\n':2399 contents = [data[:-1]]2400else:2401 contents = [data]24022403if type_base =="utf16":2404# p4 delivers different text in the python output to -G2405# than it does when using "print -o", or normal p4 client2406# operations. utf16 is converted to ascii or utf8, perhaps.2407# But ascii text saved as -t utf16 is completely mangled.2408# Invoke print -o to get the real contents.2409#2410# On windows, the newlines will always be mangled by print, so put2411# them back too. This is not needed to the cygwin windows version,2412# just the native "NT" type.2413#2414try:2415 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2416exceptExceptionas e:2417if'Translation of file content failed'instr(e):2418 type_base ='binary'2419else:2420raise e2421else:2422ifp4_version_string().find('/NT') >=0:2423 text = text.replace('\r\n','\n')2424 contents = [ text ]24252426if type_base =="apple":2427# Apple filetype files will be streamed as a concatenation of2428# its appledouble header and the contents. This is useless2429# on both macs and non-macs. If using "print -q -o xx", it2430# will create "xx" with the data, and "%xx" with the header.2431# This is also not very useful.2432#2433# Ideally, someday, this script can learn how to generate2434# appledouble files directly and import those to git, but2435# non-mac machines can never find a use for apple filetype.2436print"\nIgnoring apple filetype file%s"%file['depotFile']2437return24382439# Note that we do not try to de-mangle keywords on utf16 files,2440# even though in theory somebody may want that.2441 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2442if pattern:2443 regexp = re.compile(pattern, re.VERBOSE)2444 text =''.join(contents)2445 text = regexp.sub(r'$\1$', text)2446 contents = [ text ]24472448try:2449 relPath.decode('ascii')2450except:2451 encoding ='utf8'2452ifgitConfig('git-p4.pathEncoding'):2453 encoding =gitConfig('git-p4.pathEncoding')2454 relPath = relPath.decode(encoding,'replace').encode('utf8','replace')2455if self.verbose:2456print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, relPath)24572458if self.largeFileSystem:2459(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)24602461 self.writeToGitStream(git_mode, relPath, contents)24622463defstreamOneP4Deletion(self,file):2464 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2465if verbose:2466 sys.stdout.write("delete%s\n"% relPath)2467 sys.stdout.flush()2468 self.gitStream.write("D%s\n"% relPath)24692470if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2471 self.largeFileSystem.removeLargeFile(relPath)24722473# handle another chunk of streaming data2474defstreamP4FilesCb(self, marshalled):24752476# catch p4 errors and complain2477 err =None2478if"code"in marshalled:2479if marshalled["code"] =="error":2480if"data"in marshalled:2481 err = marshalled["data"].rstrip()24822483if not err and'fileSize'in self.stream_file:2484 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2485if required_bytes >0:2486 err ='Not enough space left on%s! Free at least%iMB.'% (2487 os.getcwd(), required_bytes/1024/10242488)24892490if err:2491 f =None2492if self.stream_have_file_info:2493if"depotFile"in self.stream_file:2494 f = self.stream_file["depotFile"]2495# force a failure in fast-import, else an empty2496# commit will be made2497 self.gitStream.write("\n")2498 self.gitStream.write("die-now\n")2499 self.gitStream.close()2500# ignore errors, but make sure it exits first2501 self.importProcess.wait()2502if f:2503die("Error from p4 print for%s:%s"% (f, err))2504else:2505die("Error from p4 print:%s"% err)25062507if marshalled.has_key('depotFile')and self.stream_have_file_info:2508# start of a new file - output the old one first2509 self.streamOneP4File(self.stream_file, self.stream_contents)2510 self.stream_file = {}2511 self.stream_contents = []2512 self.stream_have_file_info =False25132514# pick up the new file information... for the2515# 'data' field we need to append to our array2516for k in marshalled.keys():2517if k =='data':2518if'streamContentSize'not in self.stream_file:2519 self.stream_file['streamContentSize'] =02520 self.stream_file['streamContentSize'] +=len(marshalled['data'])2521 self.stream_contents.append(marshalled['data'])2522else:2523 self.stream_file[k] = marshalled[k]25242525if(verbose and2526'streamContentSize'in self.stream_file and2527'fileSize'in self.stream_file and2528'depotFile'in self.stream_file):2529 size =int(self.stream_file["fileSize"])2530if size >0:2531 progress =100*self.stream_file['streamContentSize']/size2532 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2533 sys.stdout.flush()25342535 self.stream_have_file_info =True25362537# Stream directly from "p4 files" into "git fast-import"2538defstreamP4Files(self, files):2539 filesForCommit = []2540 filesToRead = []2541 filesToDelete = []25422543for f in files:2544# if using a client spec, only add the files that have2545# a path in the client2546if self.clientSpecDirs:2547if self.clientSpecDirs.map_in_client(f['path']) =="":2548continue25492550 filesForCommit.append(f)2551if f['action']in self.delete_actions:2552 filesToDelete.append(f)2553else:2554 filesToRead.append(f)25552556# deleted files...2557for f in filesToDelete:2558 self.streamOneP4Deletion(f)25592560iflen(filesToRead) >0:2561 self.stream_file = {}2562 self.stream_contents = []2563 self.stream_have_file_info =False25642565# curry self argument2566defstreamP4FilesCbSelf(entry):2567 self.streamP4FilesCb(entry)25682569 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]25702571p4CmdList(["-x","-","print"],2572 stdin=fileArgs,2573 cb=streamP4FilesCbSelf)25742575# do the last chunk2576if self.stream_file.has_key('depotFile'):2577 self.streamOneP4File(self.stream_file, self.stream_contents)25782579defmake_email(self, userid):2580if userid in self.users:2581return self.users[userid]2582else:2583return"%s<a@b>"% userid25842585defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2586""" Stream a p4 tag.2587 commit is either a git commit, or a fast-import mark, ":<p4commit>"2588 """25892590if verbose:2591print"writing tag%sfor commit%s"% (labelName, commit)2592 gitStream.write("tag%s\n"% labelName)2593 gitStream.write("from%s\n"% commit)25942595if labelDetails.has_key('Owner'):2596 owner = labelDetails["Owner"]2597else:2598 owner =None25992600# Try to use the owner of the p4 label, or failing that,2601# the current p4 user id.2602if owner:2603 email = self.make_email(owner)2604else:2605 email = self.make_email(self.p4UserId())2606 tagger ="%s %s %s"% (email, epoch, self.tz)26072608 gitStream.write("tagger%s\n"% tagger)26092610print"labelDetails=",labelDetails2611if labelDetails.has_key('Description'):2612 description = labelDetails['Description']2613else:2614 description ='Label from git p4'26152616 gitStream.write("data%d\n"%len(description))2617 gitStream.write(description)2618 gitStream.write("\n")26192620defcommit(self, details, files, branch, parent =""):2621 epoch = details["time"]2622 author = details["user"]26232624if self.verbose:2625print"commit into%s"% branch26262627# start with reading files; if that fails, we should not2628# create a commit.2629 new_files = []2630for f in files:2631if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2632 new_files.append(f)2633else:2634 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])26352636if self.clientSpecDirs:2637 self.clientSpecDirs.update_client_spec_path_cache(files)26382639 self.gitStream.write("commit%s\n"% branch)2640 self.gitStream.write("mark :%s\n"% details["change"])2641 self.committedChanges.add(int(details["change"]))2642 committer =""2643if author not in self.users:2644 self.getUserMapFromPerforceServer()2645 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)26462647 self.gitStream.write("committer%s\n"% committer)26482649 self.gitStream.write("data <<EOT\n")2650 self.gitStream.write(details["desc"])2651 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2652(','.join(self.branchPrefixes), details["change"]))2653iflen(details['options']) >0:2654 self.gitStream.write(": options =%s"% details['options'])2655 self.gitStream.write("]\nEOT\n\n")26562657iflen(parent) >0:2658if self.verbose:2659print"parent%s"% parent2660 self.gitStream.write("from%s\n"% parent)26612662 self.streamP4Files(new_files)2663 self.gitStream.write("\n")26642665 change =int(details["change"])26662667if self.labels.has_key(change):2668 label = self.labels[change]2669 labelDetails = label[0]2670 labelRevisions = label[1]2671if self.verbose:2672print"Change%sis labelled%s"% (change, labelDetails)26732674 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2675for p in self.branchPrefixes])26762677iflen(files) ==len(labelRevisions):26782679 cleanedFiles = {}2680for info in files:2681if info["action"]in self.delete_actions:2682continue2683 cleanedFiles[info["depotFile"]] = info["rev"]26842685if cleanedFiles == labelRevisions:2686 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)26872688else:2689if not self.silent:2690print("Tag%sdoes not match with change%s: files do not match."2691% (labelDetails["label"], change))26922693else:2694if not self.silent:2695print("Tag%sdoes not match with change%s: file count is different."2696% (labelDetails["label"], change))26972698# Build a dictionary of changelists and labels, for "detect-labels" option.2699defgetLabels(self):2700 self.labels = {}27012702 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2703iflen(l) >0and not self.silent:2704print"Finding files belonging to labels in%s"% `self.depotPaths`27052706for output in l:2707 label = output["label"]2708 revisions = {}2709 newestChange =02710if self.verbose:2711print"Querying files for label%s"% label2712forfileinp4CmdList(["files"] +2713["%s...@%s"% (p, label)2714for p in self.depotPaths]):2715 revisions[file["depotFile"]] =file["rev"]2716 change =int(file["change"])2717if change > newestChange:2718 newestChange = change27192720 self.labels[newestChange] = [output, revisions]27212722if self.verbose:2723print"Label changes:%s"% self.labels.keys()27242725# Import p4 labels as git tags. A direct mapping does not2726# exist, so assume that if all the files are at the same revision2727# then we can use that, or it's something more complicated we should2728# just ignore.2729defimportP4Labels(self, stream, p4Labels):2730if verbose:2731print"import p4 labels: "+' '.join(p4Labels)27322733 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2734 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2735iflen(validLabelRegexp) ==0:2736 validLabelRegexp = defaultLabelRegexp2737 m = re.compile(validLabelRegexp)27382739for name in p4Labels:2740 commitFound =False27412742if not m.match(name):2743if verbose:2744print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2745continue27462747if name in ignoredP4Labels:2748continue27492750 labelDetails =p4CmdList(['label',"-o", name])[0]27512752# get the most recent changelist for each file in this label2753 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2754for p in self.depotPaths])27552756if change.has_key('change'):2757# find the corresponding git commit; take the oldest commit2758 changelist =int(change['change'])2759if changelist in self.committedChanges:2760 gitCommit =":%d"% changelist # use a fast-import mark2761 commitFound =True2762else:2763 gitCommit =read_pipe(["git","rev-list","--max-count=1",2764"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2765iflen(gitCommit) ==0:2766print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2767else:2768 commitFound =True2769 gitCommit = gitCommit.strip()27702771if commitFound:2772# Convert from p4 time format2773try:2774 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2775exceptValueError:2776print"Could not convert label time%s"% labelDetails['Update']2777 tmwhen =127782779 when =int(time.mktime(tmwhen))2780 self.streamTag(stream, name, labelDetails, gitCommit, when)2781if verbose:2782print"p4 label%smapped to git commit%s"% (name, gitCommit)2783else:2784if verbose:2785print"Label%shas no changelists - possibly deleted?"% name27862787if not commitFound:2788# We can't import this label; don't try again as it will get very2789# expensive repeatedly fetching all the files for labels that will2790# never be imported. If the label is moved in the future, the2791# ignore will need to be removed manually.2792system(["git","config","--add","git-p4.ignoredP4Labels", name])27932794defguessProjectName(self):2795for p in self.depotPaths:2796if p.endswith("/"):2797 p = p[:-1]2798 p = p[p.strip().rfind("/") +1:]2799if not p.endswith("/"):2800 p +="/"2801return p28022803defgetBranchMapping(self):2804 lostAndFoundBranches =set()28052806 user =gitConfig("git-p4.branchUser")2807iflen(user) >0:2808 command ="branches -u%s"% user2809else:2810 command ="branches"28112812for info inp4CmdList(command):2813 details =p4Cmd(["branch","-o", info["branch"]])2814 viewIdx =02815while details.has_key("View%s"% viewIdx):2816 paths = details["View%s"% viewIdx].split(" ")2817 viewIdx = viewIdx +12818# require standard //depot/foo/... //depot/bar/... mapping2819iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2820continue2821 source = paths[0]2822 destination = paths[1]2823## HACK2824ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2825 source = source[len(self.depotPaths[0]):-4]2826 destination = destination[len(self.depotPaths[0]):-4]28272828if destination in self.knownBranches:2829if not self.silent:2830print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2831print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2832continue28332834 self.knownBranches[destination] = source28352836 lostAndFoundBranches.discard(destination)28372838if source not in self.knownBranches:2839 lostAndFoundBranches.add(source)28402841# Perforce does not strictly require branches to be defined, so we also2842# check git config for a branch list.2843#2844# Example of branch definition in git config file:2845# [git-p4]2846# branchList=main:branchA2847# branchList=main:branchB2848# branchList=branchA:branchC2849 configBranches =gitConfigList("git-p4.branchList")2850for branch in configBranches:2851if branch:2852(source, destination) = branch.split(":")2853 self.knownBranches[destination] = source28542855 lostAndFoundBranches.discard(destination)28562857if source not in self.knownBranches:2858 lostAndFoundBranches.add(source)285928602861for branch in lostAndFoundBranches:2862 self.knownBranches[branch] = branch28632864defgetBranchMappingFromGitBranches(self):2865 branches =p4BranchesInGit(self.importIntoRemotes)2866for branch in branches.keys():2867if branch =="master":2868 branch ="main"2869else:2870 branch = branch[len(self.projectName):]2871 self.knownBranches[branch] = branch28722873defupdateOptionDict(self, d):2874 option_keys = {}2875if self.keepRepoPath:2876 option_keys['keepRepoPath'] =128772878 d["options"] =' '.join(sorted(option_keys.keys()))28792880defreadOptions(self, d):2881 self.keepRepoPath = (d.has_key('options')2882and('keepRepoPath'in d['options']))28832884defgitRefForBranch(self, branch):2885if branch =="main":2886return self.refPrefix +"master"28872888iflen(branch) <=0:2889return branch28902891return self.refPrefix + self.projectName + branch28922893defgitCommitByP4Change(self, ref, change):2894if self.verbose:2895print"looking in ref "+ ref +" for change%susing bisect..."% change28962897 earliestCommit =""2898 latestCommit =parseRevision(ref)28992900while True:2901if self.verbose:2902print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2903 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2904iflen(next) ==0:2905if self.verbose:2906print"argh"2907return""2908 log =extractLogMessageFromGitCommit(next)2909 settings =extractSettingsGitLog(log)2910 currentChange =int(settings['change'])2911if self.verbose:2912print"current change%s"% currentChange29132914if currentChange == change:2915if self.verbose:2916print"found%s"% next2917return next29182919if currentChange < change:2920 earliestCommit ="^%s"% next2921else:2922 latestCommit ="%s"% next29232924return""29252926defimportNewBranch(self, branch, maxChange):2927# make fast-import flush all changes to disk and update the refs using the checkpoint2928# command so that we can try to find the branch parent in the git history2929 self.gitStream.write("checkpoint\n\n");2930 self.gitStream.flush();2931 branchPrefix = self.depotPaths[0] + branch +"/"2932range="@1,%s"% maxChange2933#print "prefix" + branchPrefix2934 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2935iflen(changes) <=0:2936return False2937 firstChange = changes[0]2938#print "first change in branch: %s" % firstChange2939 sourceBranch = self.knownBranches[branch]2940 sourceDepotPath = self.depotPaths[0] + sourceBranch2941 sourceRef = self.gitRefForBranch(sourceBranch)2942#print "source " + sourceBranch29432944 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2945#print "branch parent: %s" % branchParentChange2946 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2947iflen(gitParent) >0:2948 self.initialParents[self.gitRefForBranch(branch)] = gitParent2949#print "parent git commit: %s" % gitParent29502951 self.importChanges(changes)2952return True29532954defsearchParent(self, parent, branch, target):2955 parentFound =False2956for blob inread_pipe_lines(["git","rev-list","--reverse",2957"--no-merges", parent]):2958 blob = blob.strip()2959iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2960 parentFound =True2961if self.verbose:2962print"Found parent of%sin commit%s"% (branch, blob)2963break2964if parentFound:2965return blob2966else:2967return None29682969defimportChanges(self, changes):2970 cnt =12971for change in changes:2972 description =p4_describe(change)2973 self.updateOptionDict(description)29742975if not self.silent:2976 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2977 sys.stdout.flush()2978 cnt = cnt +129792980try:2981if self.detectBranches:2982 branches = self.splitFilesIntoBranches(description)2983for branch in branches.keys():2984## HACK --hwn2985 branchPrefix = self.depotPaths[0] + branch +"/"2986 self.branchPrefixes = [ branchPrefix ]29872988 parent =""29892990 filesForCommit = branches[branch]29912992if self.verbose:2993print"branch is%s"% branch29942995 self.updatedBranches.add(branch)29962997if branch not in self.createdBranches:2998 self.createdBranches.add(branch)2999 parent = self.knownBranches[branch]3000if parent == branch:3001 parent =""3002else:3003 fullBranch = self.projectName + branch3004if fullBranch not in self.p4BranchesInGit:3005if not self.silent:3006print("\nImporting new branch%s"% fullBranch);3007if self.importNewBranch(branch, change -1):3008 parent =""3009 self.p4BranchesInGit.append(fullBranch)3010if not self.silent:3011print("\nResuming with change%s"% change);30123013if self.verbose:3014print"parent determined through known branches:%s"% parent30153016 branch = self.gitRefForBranch(branch)3017 parent = self.gitRefForBranch(parent)30183019if self.verbose:3020print"looking for initial parent for%s; current parent is%s"% (branch, parent)30213022iflen(parent) ==0and branch in self.initialParents:3023 parent = self.initialParents[branch]3024del self.initialParents[branch]30253026 blob =None3027iflen(parent) >0:3028 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3029if self.verbose:3030print"Creating temporary branch: "+ tempBranch3031 self.commit(description, filesForCommit, tempBranch)3032 self.tempBranches.append(tempBranch)3033 self.checkpoint()3034 blob = self.searchParent(parent, branch, tempBranch)3035if blob:3036 self.commit(description, filesForCommit, branch, blob)3037else:3038if self.verbose:3039print"Parent of%snot found. Committing into head of%s"% (branch, parent)3040 self.commit(description, filesForCommit, branch, parent)3041else:3042 files = self.extractFilesFromCommit(description)3043 self.commit(description, files, self.branch,3044 self.initialParent)3045# only needed once, to connect to the previous commit3046 self.initialParent =""3047exceptIOError:3048print self.gitError.read()3049 sys.exit(1)30503051defimportHeadRevision(self, revision):3052print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)30533054 details = {}3055 details["user"] ="git perforce import user"3056 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3057% (' '.join(self.depotPaths), revision))3058 details["change"] = revision3059 newestRevision =030603061 fileCnt =03062 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]30633064for info inp4CmdList(["files"] + fileArgs):30653066if'code'in info and info['code'] =='error':3067 sys.stderr.write("p4 returned an error:%s\n"3068% info['data'])3069if info['data'].find("must refer to client") >=0:3070 sys.stderr.write("This particular p4 error is misleading.\n")3071 sys.stderr.write("Perhaps the depot path was misspelled.\n");3072 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3073 sys.exit(1)3074if'p4ExitCode'in info:3075 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3076 sys.exit(1)307730783079 change =int(info["change"])3080if change > newestRevision:3081 newestRevision = change30823083if info["action"]in self.delete_actions:3084# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3085#fileCnt = fileCnt + 13086continue30873088for prop in["depotFile","rev","action","type"]:3089 details["%s%s"% (prop, fileCnt)] = info[prop]30903091 fileCnt = fileCnt +130923093 details["change"] = newestRevision30943095# Use time from top-most change so that all git p4 clones of3096# the same p4 repo have the same commit SHA1s.3097 res =p4_describe(newestRevision)3098 details["time"] = res["time"]30993100 self.updateOptionDict(details)3101try:3102 self.commit(details, self.extractFilesFromCommit(details), self.branch)3103exceptIOError:3104print"IO error with git fast-import. Is your git version recent enough?"3105print self.gitError.read()310631073108defrun(self, args):3109 self.depotPaths = []3110 self.changeRange =""3111 self.previousDepotPaths = []3112 self.hasOrigin =False31133114# map from branch depot path to parent branch3115 self.knownBranches = {}3116 self.initialParents = {}31173118if self.importIntoRemotes:3119 self.refPrefix ="refs/remotes/p4/"3120else:3121 self.refPrefix ="refs/heads/p4/"31223123if self.syncWithOrigin:3124 self.hasOrigin =originP4BranchesExist()3125if self.hasOrigin:3126if not self.silent:3127print'Syncing with origin first, using "git fetch origin"'3128system("git fetch origin")31293130 branch_arg_given =bool(self.branch)3131iflen(self.branch) ==0:3132 self.branch = self.refPrefix +"master"3133ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3134system("git update-ref%srefs/heads/p4"% self.branch)3135system("git branch -D p4")31363137# accept either the command-line option, or the configuration variable3138if self.useClientSpec:3139# will use this after clone to set the variable3140 self.useClientSpec_from_options =True3141else:3142ifgitConfigBool("git-p4.useclientspec"):3143 self.useClientSpec =True3144if self.useClientSpec:3145 self.clientSpecDirs =getClientSpec()31463147# TODO: should always look at previous commits,3148# merge with previous imports, if possible.3149if args == []:3150if self.hasOrigin:3151createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)31523153# branches holds mapping from branch name to sha13154 branches =p4BranchesInGit(self.importIntoRemotes)31553156# restrict to just this one, disabling detect-branches3157if branch_arg_given:3158 short = self.branch.split("/")[-1]3159if short in branches:3160 self.p4BranchesInGit = [ short ]3161else:3162 self.p4BranchesInGit = branches.keys()31633164iflen(self.p4BranchesInGit) >1:3165if not self.silent:3166print"Importing from/into multiple branches"3167 self.detectBranches =True3168for branch in branches.keys():3169 self.initialParents[self.refPrefix + branch] = \3170 branches[branch]31713172if self.verbose:3173print"branches:%s"% self.p4BranchesInGit31743175 p4Change =03176for branch in self.p4BranchesInGit:3177 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)31783179 settings =extractSettingsGitLog(logMsg)31803181 self.readOptions(settings)3182if(settings.has_key('depot-paths')3183and settings.has_key('change')):3184 change =int(settings['change']) +13185 p4Change =max(p4Change, change)31863187 depotPaths =sorted(settings['depot-paths'])3188if self.previousDepotPaths == []:3189 self.previousDepotPaths = depotPaths3190else:3191 paths = []3192for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3193 prev_list = prev.split("/")3194 cur_list = cur.split("/")3195for i inrange(0,min(len(cur_list),len(prev_list))):3196if cur_list[i] <> prev_list[i]:3197 i = i -13198break31993200 paths.append("/".join(cur_list[:i +1]))32013202 self.previousDepotPaths = paths32033204if p4Change >0:3205 self.depotPaths =sorted(self.previousDepotPaths)3206 self.changeRange ="@%s,#head"% p4Change3207if not self.silent and not self.detectBranches:3208print"Performing incremental import into%sgit branch"% self.branch32093210# accept multiple ref name abbreviations:3211# refs/foo/bar/branch -> use it exactly3212# p4/branch -> prepend refs/remotes/ or refs/heads/3213# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3214if not self.branch.startswith("refs/"):3215if self.importIntoRemotes:3216 prepend ="refs/remotes/"3217else:3218 prepend ="refs/heads/"3219if not self.branch.startswith("p4/"):3220 prepend +="p4/"3221 self.branch = prepend + self.branch32223223iflen(args) ==0and self.depotPaths:3224if not self.silent:3225print"Depot paths:%s"%' '.join(self.depotPaths)3226else:3227if self.depotPaths and self.depotPaths != args:3228print("previous import used depot path%sand now%swas specified. "3229"This doesn't work!"% (' '.join(self.depotPaths),3230' '.join(args)))3231 sys.exit(1)32323233 self.depotPaths =sorted(args)32343235 revision =""3236 self.users = {}32373238# Make sure no revision specifiers are used when --changesfile3239# is specified.3240 bad_changesfile =False3241iflen(self.changesFile) >0:3242for p in self.depotPaths:3243if p.find("@") >=0or p.find("#") >=0:3244 bad_changesfile =True3245break3246if bad_changesfile:3247die("Option --changesfile is incompatible with revision specifiers")32483249 newPaths = []3250for p in self.depotPaths:3251if p.find("@") != -1:3252 atIdx = p.index("@")3253 self.changeRange = p[atIdx:]3254if self.changeRange =="@all":3255 self.changeRange =""3256elif','not in self.changeRange:3257 revision = self.changeRange3258 self.changeRange =""3259 p = p[:atIdx]3260elif p.find("#") != -1:3261 hashIdx = p.index("#")3262 revision = p[hashIdx:]3263 p = p[:hashIdx]3264elif self.previousDepotPaths == []:3265# pay attention to changesfile, if given, else import3266# the entire p4 tree at the head revision3267iflen(self.changesFile) ==0:3268 revision ="#head"32693270 p = re.sub("\.\.\.$","", p)3271if not p.endswith("/"):3272 p +="/"32733274 newPaths.append(p)32753276 self.depotPaths = newPaths32773278# --detect-branches may change this for each branch3279 self.branchPrefixes = self.depotPaths32803281 self.loadUserMapFromCache()3282 self.labels = {}3283if self.detectLabels:3284 self.getLabels();32853286if self.detectBranches:3287## FIXME - what's a P4 projectName ?3288 self.projectName = self.guessProjectName()32893290if self.hasOrigin:3291 self.getBranchMappingFromGitBranches()3292else:3293 self.getBranchMapping()3294if self.verbose:3295print"p4-git branches:%s"% self.p4BranchesInGit3296print"initial parents:%s"% self.initialParents3297for b in self.p4BranchesInGit:3298if b !="master":32993300## FIXME3301 b = b[len(self.projectName):]3302 self.createdBranches.add(b)33033304 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))33053306 self.importProcess = subprocess.Popen(["git","fast-import"],3307 stdin=subprocess.PIPE,3308 stdout=subprocess.PIPE,3309 stderr=subprocess.PIPE);3310 self.gitOutput = self.importProcess.stdout3311 self.gitStream = self.importProcess.stdin3312 self.gitError = self.importProcess.stderr33133314if revision:3315 self.importHeadRevision(revision)3316else:3317 changes = []33183319iflen(self.changesFile) >0:3320 output =open(self.changesFile).readlines()3321 changeSet =set()3322for line in output:3323 changeSet.add(int(line))33243325for change in changeSet:3326 changes.append(change)33273328 changes.sort()3329else:3330# catch "git p4 sync" with no new branches, in a repo that3331# does not have any existing p4 branches3332iflen(args) ==0:3333if not self.p4BranchesInGit:3334die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")33353336# The default branch is master, unless --branch is used to3337# specify something else. Make sure it exists, or complain3338# nicely about how to use --branch.3339if not self.detectBranches:3340if notbranch_exists(self.branch):3341if branch_arg_given:3342die("Error: branch%sdoes not exist."% self.branch)3343else:3344die("Error: no branch%s; perhaps specify one with --branch."%3345 self.branch)33463347if self.verbose:3348print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3349 self.changeRange)3350 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)33513352iflen(self.maxChanges) >0:3353 changes = changes[:min(int(self.maxChanges),len(changes))]33543355iflen(changes) ==0:3356if not self.silent:3357print"No changes to import!"3358else:3359if not self.silent and not self.detectBranches:3360print"Import destination:%s"% self.branch33613362 self.updatedBranches =set()33633364if not self.detectBranches:3365if args:3366# start a new branch3367 self.initialParent =""3368else:3369# build on a previous revision3370 self.initialParent =parseRevision(self.branch)33713372 self.importChanges(changes)33733374if not self.silent:3375print""3376iflen(self.updatedBranches) >0:3377 sys.stdout.write("Updated branches: ")3378for b in self.updatedBranches:3379 sys.stdout.write("%s"% b)3380 sys.stdout.write("\n")33813382ifgitConfigBool("git-p4.importLabels"):3383 self.importLabels =True33843385if self.importLabels:3386 p4Labels =getP4Labels(self.depotPaths)3387 gitTags =getGitTags()33883389 missingP4Labels = p4Labels - gitTags3390 self.importP4Labels(self.gitStream, missingP4Labels)33913392 self.gitStream.close()3393if self.importProcess.wait() !=0:3394die("fast-import failed:%s"% self.gitError.read())3395 self.gitOutput.close()3396 self.gitError.close()33973398# Cleanup temporary branches created during import3399if self.tempBranches != []:3400for branch in self.tempBranches:3401read_pipe("git update-ref -d%s"% branch)3402 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))34033404# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3405# a convenient shortcut refname "p4".3406if self.importIntoRemotes:3407 head_ref = self.refPrefix +"HEAD"3408if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3409system(["git","symbolic-ref", head_ref, self.branch])34103411return True34123413classP4Rebase(Command):3414def__init__(self):3415 Command.__init__(self)3416 self.options = [3417 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3418]3419 self.importLabels =False3420 self.description = ("Fetches the latest revision from perforce and "3421+"rebases the current work (branch) against it")34223423defrun(self, args):3424 sync =P4Sync()3425 sync.importLabels = self.importLabels3426 sync.run([])34273428return self.rebase()34293430defrebase(self):3431if os.system("git update-index --refresh") !=0:3432die("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.");3433iflen(read_pipe("git diff-index HEAD --")) >0:3434die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");34353436[upstream, settings] =findUpstreamBranchPoint()3437iflen(upstream) ==0:3438die("Cannot find upstream branchpoint for rebase")34393440# the branchpoint may be p4/foo~3, so strip off the parent3441 upstream = re.sub("~[0-9]+$","", upstream)34423443print"Rebasing the current branch onto%s"% upstream3444 oldHead =read_pipe("git rev-parse HEAD").strip()3445system("git rebase%s"% upstream)3446system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3447return True34483449classP4Clone(P4Sync):3450def__init__(self):3451 P4Sync.__init__(self)3452 self.description ="Creates a new git repository and imports from Perforce into it"3453 self.usage ="usage: %prog [options] //depot/path[@revRange]"3454 self.options += [3455 optparse.make_option("--destination", dest="cloneDestination",3456 action='store', default=None,3457help="where to leave result of the clone"),3458 optparse.make_option("--bare", dest="cloneBare",3459 action="store_true", default=False),3460]3461 self.cloneDestination =None3462 self.needsGit =False3463 self.cloneBare =False34643465defdefaultDestination(self, args):3466## TODO: use common prefix of args?3467 depotPath = args[0]3468 depotDir = re.sub("(@[^@]*)$","", depotPath)3469 depotDir = re.sub("(#[^#]*)$","", depotDir)3470 depotDir = re.sub(r"\.\.\.$","", depotDir)3471 depotDir = re.sub(r"/$","", depotDir)3472return os.path.split(depotDir)[1]34733474defrun(self, args):3475iflen(args) <1:3476return False34773478if self.keepRepoPath and not self.cloneDestination:3479 sys.stderr.write("Must specify destination for --keep-path\n")3480 sys.exit(1)34813482 depotPaths = args34833484if not self.cloneDestination andlen(depotPaths) >1:3485 self.cloneDestination = depotPaths[-1]3486 depotPaths = depotPaths[:-1]34873488 self.cloneExclude = ["/"+p for p in self.cloneExclude]3489for p in depotPaths:3490if not p.startswith("//"):3491 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3492return False34933494if not self.cloneDestination:3495 self.cloneDestination = self.defaultDestination(args)34963497print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)34983499if not os.path.exists(self.cloneDestination):3500 os.makedirs(self.cloneDestination)3501chdir(self.cloneDestination)35023503 init_cmd = ["git","init"]3504if self.cloneBare:3505 init_cmd.append("--bare")3506 retcode = subprocess.call(init_cmd)3507if retcode:3508raiseCalledProcessError(retcode, init_cmd)35093510if not P4Sync.run(self, depotPaths):3511return False35123513# create a master branch and check out a work tree3514ifgitBranchExists(self.branch):3515system(["git","branch","master", self.branch ])3516if not self.cloneBare:3517system(["git","checkout","-f"])3518else:3519print'Not checking out any branch, use ' \3520'"git checkout -q -b master <branch>"'35213522# auto-set this variable if invoked with --use-client-spec3523if self.useClientSpec_from_options:3524system("git config --bool git-p4.useclientspec true")35253526return True35273528classP4Branches(Command):3529def__init__(self):3530 Command.__init__(self)3531 self.options = [ ]3532 self.description = ("Shows the git branches that hold imports and their "3533+"corresponding perforce depot paths")3534 self.verbose =False35353536defrun(self, args):3537iforiginP4BranchesExist():3538createOrUpdateBranchesFromOrigin()35393540 cmdline ="git rev-parse --symbolic "3541 cmdline +=" --remotes"35423543for line inread_pipe_lines(cmdline):3544 line = line.strip()35453546if not line.startswith('p4/')or line =="p4/HEAD":3547continue3548 branch = line35493550 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3551 settings =extractSettingsGitLog(log)35523553print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3554return True35553556classHelpFormatter(optparse.IndentedHelpFormatter):3557def__init__(self):3558 optparse.IndentedHelpFormatter.__init__(self)35593560defformat_description(self, description):3561if description:3562return description +"\n"3563else:3564return""35653566defprintUsage(commands):3567print"usage:%s<command> [options]"% sys.argv[0]3568print""3569print"valid commands:%s"%", ".join(commands)3570print""3571print"Try%s<command> --help for command specific help."% sys.argv[0]3572print""35733574commands = {3575"debug": P4Debug,3576"submit": P4Submit,3577"commit": P4Submit,3578"sync": P4Sync,3579"rebase": P4Rebase,3580"clone": P4Clone,3581"rollback": P4RollBack,3582"branches": P4Branches3583}358435853586defmain():3587iflen(sys.argv[1:]) ==0:3588printUsage(commands.keys())3589 sys.exit(2)35903591 cmdName = sys.argv[1]3592try:3593 klass = commands[cmdName]3594 cmd =klass()3595exceptKeyError:3596print"unknown command%s"% cmdName3597print""3598printUsage(commands.keys())3599 sys.exit(2)36003601 options = cmd.options3602 cmd.gitdir = os.environ.get("GIT_DIR",None)36033604 args = sys.argv[2:]36053606 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3607if cmd.needsGit:3608 options.append(optparse.make_option("--git-dir", dest="gitdir"))36093610 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3611 options,3612 description = cmd.description,3613 formatter =HelpFormatter())36143615(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3616global verbose3617 verbose = cmd.verbose3618if cmd.needsGit:3619if cmd.gitdir ==None:3620 cmd.gitdir = os.path.abspath(".git")3621if notisValidGitDir(cmd.gitdir):3622 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3623if os.path.exists(cmd.gitdir):3624 cdup =read_pipe("git rev-parse --show-cdup").strip()3625iflen(cdup) >0:3626chdir(cdup);36273628if notisValidGitDir(cmd.gitdir):3629ifisValidGitDir(cmd.gitdir +"/.git"):3630 cmd.gitdir +="/.git"3631else:3632die("fatal: cannot locate git repository at%s"% cmd.gitdir)36333634 os.environ["GIT_DIR"] = cmd.gitdir36353636if not cmd.run(args):3637 parser.print_help()3638 sys.exit(2)363936403641if __name__ =='__main__':3642main()