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 25 26try: 27from subprocess import CalledProcessError 28exceptImportError: 29# from python2.7:subprocess.py 30# Exception classes used by this module. 31classCalledProcessError(Exception): 32"""This exception is raised when a process run by check_call() returns 33 a non-zero exit status. The exit status will be stored in the 34 returncode attribute.""" 35def__init__(self, returncode, cmd): 36 self.returncode = returncode 37 self.cmd = cmd 38def__str__(self): 39return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 40 41verbose =False 42 43# Only labels/tags matching this will be imported/exported 44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 45 46# Grab changes in blocks of this many revisions, unless otherwise requested 47defaultBlockSize =512 48 49defp4_build_cmd(cmd): 50"""Build a suitable p4 command line. 51 52 This consolidates building and returning a p4 command line into one 53 location. It means that hooking into the environment, or other configuration 54 can be done more easily. 55 """ 56 real_cmd = ["p4"] 57 58 user =gitConfig("git-p4.user") 59iflen(user) >0: 60 real_cmd += ["-u",user] 61 62 password =gitConfig("git-p4.password") 63iflen(password) >0: 64 real_cmd += ["-P", password] 65 66 port =gitConfig("git-p4.port") 67iflen(port) >0: 68 real_cmd += ["-p", port] 69 70 host =gitConfig("git-p4.host") 71iflen(host) >0: 72 real_cmd += ["-H", host] 73 74 client =gitConfig("git-p4.client") 75iflen(client) >0: 76 real_cmd += ["-c", client] 77 78 79ifisinstance(cmd,basestring): 80 real_cmd =' '.join(real_cmd) +' '+ cmd 81else: 82 real_cmd += cmd 83return real_cmd 84 85defchdir(path, is_client_path=False): 86"""Do chdir to the given path, and set the PWD environment 87 variable for use by P4. It does not look at getcwd() output. 88 Since we're not using the shell, it is necessary to set the 89 PWD environment variable explicitly. 90 91 Normally, expand the path to force it to be absolute. This 92 addresses the use of relative path names inside P4 settings, 93 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 94 as given; it looks for .p4config using PWD. 95 96 If is_client_path, the path was handed to us directly by p4, 97 and may be a symbolic link. Do not call os.getcwd() in this 98 case, because it will cause p4 to think that PWD is not inside 99 the client path. 100 """ 101 102 os.chdir(path) 103if not is_client_path: 104 path = os.getcwd() 105 os.environ['PWD'] = path 106 107defdie(msg): 108if verbose: 109raiseException(msg) 110else: 111 sys.stderr.write(msg +"\n") 112 sys.exit(1) 113 114defwrite_pipe(c, stdin): 115if verbose: 116 sys.stderr.write('Writing pipe:%s\n'%str(c)) 117 118 expand =isinstance(c,basestring) 119 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 120 pipe = p.stdin 121 val = pipe.write(stdin) 122 pipe.close() 123if p.wait(): 124die('Command failed:%s'%str(c)) 125 126return val 127 128defp4_write_pipe(c, stdin): 129 real_cmd =p4_build_cmd(c) 130returnwrite_pipe(real_cmd, stdin) 131 132defread_pipe(c, ignore_error=False): 133if verbose: 134 sys.stderr.write('Reading pipe:%s\n'%str(c)) 135 136 expand =isinstance(c,basestring) 137 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 138 pipe = p.stdout 139 val = pipe.read() 140if p.wait()and not ignore_error: 141die('Command failed:%s'%str(c)) 142 143return val 144 145defp4_read_pipe(c, ignore_error=False): 146 real_cmd =p4_build_cmd(c) 147returnread_pipe(real_cmd, ignore_error) 148 149defread_pipe_lines(c): 150if verbose: 151 sys.stderr.write('Reading pipe:%s\n'%str(c)) 152 153 expand =isinstance(c, basestring) 154 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 155 pipe = p.stdout 156 val = pipe.readlines() 157if pipe.close()or p.wait(): 158die('Command failed:%s'%str(c)) 159 160return val 161 162defp4_read_pipe_lines(c): 163"""Specifically invoke p4 on the command supplied. """ 164 real_cmd =p4_build_cmd(c) 165returnread_pipe_lines(real_cmd) 166 167defp4_has_command(cmd): 168"""Ask p4 for help on this command. If it returns an error, the 169 command does not exist in this version of p4.""" 170 real_cmd =p4_build_cmd(["help", cmd]) 171 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 172 stderr=subprocess.PIPE) 173 p.communicate() 174return p.returncode ==0 175 176defp4_has_move_command(): 177"""See if the move command exists, that it supports -k, and that 178 it has not been administratively disabled. The arguments 179 must be correct, but the filenames do not have to exist. Use 180 ones with wildcards so even if they exist, it will fail.""" 181 182if notp4_has_command("move"): 183return False 184 cmd =p4_build_cmd(["move","-k","@from","@to"]) 185 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 186(out, err) = p.communicate() 187# return code will be 1 in either case 188if err.find("Invalid option") >=0: 189return False 190if err.find("disabled") >=0: 191return False 192# assume it failed because @... was invalid changelist 193return True 194 195defsystem(cmd): 196 expand =isinstance(cmd,basestring) 197if verbose: 198 sys.stderr.write("executing%s\n"%str(cmd)) 199 retcode = subprocess.call(cmd, shell=expand) 200if retcode: 201raiseCalledProcessError(retcode, cmd) 202 203defp4_system(cmd): 204"""Specifically invoke p4 as the system command. """ 205 real_cmd =p4_build_cmd(cmd) 206 expand =isinstance(real_cmd, basestring) 207 retcode = subprocess.call(real_cmd, shell=expand) 208if retcode: 209raiseCalledProcessError(retcode, real_cmd) 210 211_p4_version_string =None 212defp4_version_string(): 213"""Read the version string, showing just the last line, which 214 hopefully is the interesting version bit. 215 216 $ p4 -V 217 Perforce - The Fast Software Configuration Management System. 218 Copyright 1995-2011 Perforce Software. All rights reserved. 219 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 220 """ 221global _p4_version_string 222if not _p4_version_string: 223 a =p4_read_pipe_lines(["-V"]) 224 _p4_version_string = a[-1].rstrip() 225return _p4_version_string 226 227defp4_integrate(src, dest): 228p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 229 230defp4_sync(f, *options): 231p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 232 233defp4_add(f): 234# forcibly add file names with wildcards 235ifwildcard_present(f): 236p4_system(["add","-f", f]) 237else: 238p4_system(["add", f]) 239 240defp4_delete(f): 241p4_system(["delete",wildcard_encode(f)]) 242 243defp4_edit(f): 244p4_system(["edit",wildcard_encode(f)]) 245 246defp4_revert(f): 247p4_system(["revert",wildcard_encode(f)]) 248 249defp4_reopen(type, f): 250p4_system(["reopen","-t",type,wildcard_encode(f)]) 251 252defp4_move(src, dest): 253p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 254 255defp4_last_change(): 256 results =p4CmdList(["changes","-m","1"]) 257returnint(results[0]['change']) 258 259defp4_describe(change): 260"""Make sure it returns a valid result by checking for 261 the presence of field "time". Return a dict of the 262 results.""" 263 264 ds =p4CmdList(["describe","-s",str(change)]) 265iflen(ds) !=1: 266die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 267 268 d = ds[0] 269 270if"p4ExitCode"in d: 271die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 272str(d))) 273if"code"in d: 274if d["code"] =="error": 275die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 276 277if"time"not in d: 278die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 279 280return d 281 282# 283# Canonicalize the p4 type and return a tuple of the 284# base type, plus any modifiers. See "p4 help filetypes" 285# for a list and explanation. 286# 287defsplit_p4_type(p4type): 288 289 p4_filetypes_historical = { 290"ctempobj":"binary+Sw", 291"ctext":"text+C", 292"cxtext":"text+Cx", 293"ktext":"text+k", 294"kxtext":"text+kx", 295"ltext":"text+F", 296"tempobj":"binary+FSw", 297"ubinary":"binary+F", 298"uresource":"resource+F", 299"uxbinary":"binary+Fx", 300"xbinary":"binary+x", 301"xltext":"text+Fx", 302"xtempobj":"binary+Swx", 303"xtext":"text+x", 304"xunicode":"unicode+x", 305"xutf16":"utf16+x", 306} 307if p4type in p4_filetypes_historical: 308 p4type = p4_filetypes_historical[p4type] 309 mods ="" 310 s = p4type.split("+") 311 base = s[0] 312 mods ="" 313iflen(s) >1: 314 mods = s[1] 315return(base, mods) 316 317# 318# return the raw p4 type of a file (text, text+ko, etc) 319# 320defp4_type(f): 321 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 322return results[0]['headType'] 323 324# 325# Given a type base and modifier, return a regexp matching 326# the keywords that can be expanded in the file 327# 328defp4_keywords_regexp_for_type(base, type_mods): 329if base in("text","unicode","binary"): 330 kwords =None 331if"ko"in type_mods: 332 kwords ='Id|Header' 333elif"k"in type_mods: 334 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 335else: 336return None 337 pattern = r""" 338 \$ # Starts with a dollar, followed by... 339 (%s) # one of the keywords, followed by... 340 (:[^$\n]+)? # possibly an old expansion, followed by... 341 \$ # another dollar 342 """% kwords 343return pattern 344else: 345return None 346 347# 348# Given a file, return a regexp matching the possible 349# RCS keywords that will be expanded, or None for files 350# with kw expansion turned off. 351# 352defp4_keywords_regexp_for_file(file): 353if not os.path.exists(file): 354return None 355else: 356(type_base, type_mods) =split_p4_type(p4_type(file)) 357returnp4_keywords_regexp_for_type(type_base, type_mods) 358 359defsetP4ExecBit(file, mode): 360# Reopens an already open file and changes the execute bit to match 361# the execute bit setting in the passed in mode. 362 363 p4Type ="+x" 364 365if notisModeExec(mode): 366 p4Type =getP4OpenedType(file) 367 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 368 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 369if p4Type[-1] =="+": 370 p4Type = p4Type[0:-1] 371 372p4_reopen(p4Type,file) 373 374defgetP4OpenedType(file): 375# Returns the perforce file type for the given file. 376 377 result =p4_read_pipe(["opened",wildcard_encode(file)]) 378 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 379if match: 380return match.group(1) 381else: 382die("Could not determine file type for%s(result: '%s')"% (file, result)) 383 384# Return the set of all p4 labels 385defgetP4Labels(depotPaths): 386 labels =set() 387ifisinstance(depotPaths,basestring): 388 depotPaths = [depotPaths] 389 390for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 391 label = l['label'] 392 labels.add(label) 393 394return labels 395 396# Return the set of all git tags 397defgetGitTags(): 398 gitTags =set() 399for line inread_pipe_lines(["git","tag"]): 400 tag = line.strip() 401 gitTags.add(tag) 402return gitTags 403 404defdiffTreePattern(): 405# This is a simple generator for the diff tree regex pattern. This could be 406# a class variable if this and parseDiffTreeEntry were a part of a class. 407 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 408while True: 409yield pattern 410 411defparseDiffTreeEntry(entry): 412"""Parses a single diff tree entry into its component elements. 413 414 See git-diff-tree(1) manpage for details about the format of the diff 415 output. This method returns a dictionary with the following elements: 416 417 src_mode - The mode of the source file 418 dst_mode - The mode of the destination file 419 src_sha1 - The sha1 for the source file 420 dst_sha1 - The sha1 fr the destination file 421 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 422 status_score - The score for the status (applicable for 'C' and 'R' 423 statuses). This is None if there is no score. 424 src - The path for the source file. 425 dst - The path for the destination file. This is only present for 426 copy or renames. If it is not present, this is None. 427 428 If the pattern is not matched, None is returned.""" 429 430 match =diffTreePattern().next().match(entry) 431if match: 432return{ 433'src_mode': match.group(1), 434'dst_mode': match.group(2), 435'src_sha1': match.group(3), 436'dst_sha1': match.group(4), 437'status': match.group(5), 438'status_score': match.group(6), 439'src': match.group(7), 440'dst': match.group(10) 441} 442return None 443 444defisModeExec(mode): 445# Returns True if the given git mode represents an executable file, 446# otherwise False. 447return mode[-3:] =="755" 448 449defisModeExecChanged(src_mode, dst_mode): 450returnisModeExec(src_mode) !=isModeExec(dst_mode) 451 452defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 453 454ifisinstance(cmd,basestring): 455 cmd ="-G "+ cmd 456 expand =True 457else: 458 cmd = ["-G"] + cmd 459 expand =False 460 461 cmd =p4_build_cmd(cmd) 462if verbose: 463 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 464 465# Use a temporary file to avoid deadlocks without 466# subprocess.communicate(), which would put another copy 467# of stdout into memory. 468 stdin_file =None 469if stdin is not None: 470 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 471ifisinstance(stdin,basestring): 472 stdin_file.write(stdin) 473else: 474for i in stdin: 475 stdin_file.write(i +'\n') 476 stdin_file.flush() 477 stdin_file.seek(0) 478 479 p4 = subprocess.Popen(cmd, 480 shell=expand, 481 stdin=stdin_file, 482 stdout=subprocess.PIPE) 483 484 result = [] 485try: 486while True: 487 entry = marshal.load(p4.stdout) 488if cb is not None: 489cb(entry) 490else: 491 result.append(entry) 492exceptEOFError: 493pass 494 exitCode = p4.wait() 495if exitCode !=0: 496 entry = {} 497 entry["p4ExitCode"] = exitCode 498 result.append(entry) 499 500return result 501 502defp4Cmd(cmd): 503list=p4CmdList(cmd) 504 result = {} 505for entry inlist: 506 result.update(entry) 507return result; 508 509defp4Where(depotPath): 510if not depotPath.endswith("/"): 511 depotPath +="/" 512 depotPathLong = depotPath +"..." 513 outputList =p4CmdList(["where", depotPathLong]) 514 output =None 515for entry in outputList: 516if"depotFile"in entry: 517# Search for the base client side depot path, as long as it starts with the branch's P4 path. 518# The base path always ends with "/...". 519if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 520 output = entry 521break 522elif"data"in entry: 523 data = entry.get("data") 524 space = data.find(" ") 525if data[:space] == depotPath: 526 output = entry 527break 528if output ==None: 529return"" 530if output["code"] =="error": 531return"" 532 clientPath ="" 533if"path"in output: 534 clientPath = output.get("path") 535elif"data"in output: 536 data = output.get("data") 537 lastSpace = data.rfind(" ") 538 clientPath = data[lastSpace +1:] 539 540if clientPath.endswith("..."): 541 clientPath = clientPath[:-3] 542return clientPath 543 544defcurrentGitBranch(): 545returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 546 547defisValidGitDir(path): 548if(os.path.exists(path +"/HEAD") 549and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 550return True; 551return False 552 553defparseRevision(ref): 554returnread_pipe("git rev-parse%s"% ref).strip() 555 556defbranchExists(ref): 557 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 558 ignore_error=True) 559returnlen(rev) >0 560 561defextractLogMessageFromGitCommit(commit): 562 logMessage ="" 563 564## fixme: title is first line of commit, not 1st paragraph. 565 foundTitle =False 566for log inread_pipe_lines("git cat-file commit%s"% commit): 567if not foundTitle: 568iflen(log) ==1: 569 foundTitle =True 570continue 571 572 logMessage += log 573return logMessage 574 575defextractSettingsGitLog(log): 576 values = {} 577for line in log.split("\n"): 578 line = line.strip() 579 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 580if not m: 581continue 582 583 assignments = m.group(1).split(':') 584for a in assignments: 585 vals = a.split('=') 586 key = vals[0].strip() 587 val = ('='.join(vals[1:])).strip() 588if val.endswith('\"')and val.startswith('"'): 589 val = val[1:-1] 590 591 values[key] = val 592 593 paths = values.get("depot-paths") 594if not paths: 595 paths = values.get("depot-path") 596if paths: 597 values['depot-paths'] = paths.split(',') 598return values 599 600defgitBranchExists(branch): 601 proc = subprocess.Popen(["git","rev-parse", branch], 602 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 603return proc.wait() ==0; 604 605_gitConfig = {} 606 607defgitConfig(key, typeSpecifier=None): 608if not _gitConfig.has_key(key): 609 cmd = ["git","config"] 610if typeSpecifier: 611 cmd += [ typeSpecifier ] 612 cmd += [ key ] 613 s =read_pipe(cmd, ignore_error=True) 614 _gitConfig[key] = s.strip() 615return _gitConfig[key] 616 617defgitConfigBool(key): 618"""Return a bool, using git config --bool. It is True only if the 619 variable is set to true, and False if set to false or not present 620 in the config.""" 621 622if not _gitConfig.has_key(key): 623 _gitConfig[key] =gitConfig(key,'--bool') =="true" 624return _gitConfig[key] 625 626defgitConfigInt(key): 627if not _gitConfig.has_key(key): 628 cmd = ["git","config","--int", key ] 629 s =read_pipe(cmd, ignore_error=True) 630 v = s.strip() 631try: 632 _gitConfig[key] =int(gitConfig(key,'--int')) 633exceptValueError: 634 _gitConfig[key] =None 635return _gitConfig[key] 636 637defgitConfigList(key): 638if not _gitConfig.has_key(key): 639 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 640 _gitConfig[key] = s.strip().split(os.linesep) 641return _gitConfig[key] 642 643defp4BranchesInGit(branchesAreInRemotes=True): 644"""Find all the branches whose names start with "p4/", looking 645 in remotes or heads as specified by the argument. Return 646 a dictionary of{ branch: revision }for each one found. 647 The branch names are the short names, without any 648 "p4/" prefix.""" 649 650 branches = {} 651 652 cmdline ="git rev-parse --symbolic " 653if branchesAreInRemotes: 654 cmdline +="--remotes" 655else: 656 cmdline +="--branches" 657 658for line inread_pipe_lines(cmdline): 659 line = line.strip() 660 661# only import to p4/ 662if not line.startswith('p4/'): 663continue 664# special symbolic ref to p4/master 665if line =="p4/HEAD": 666continue 667 668# strip off p4/ prefix 669 branch = line[len("p4/"):] 670 671 branches[branch] =parseRevision(line) 672 673return branches 674 675defbranch_exists(branch): 676"""Make sure that the given ref name really exists.""" 677 678 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 679 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 680 out, _ = p.communicate() 681if p.returncode: 682return False 683# expect exactly one line of output: the branch name 684return out.rstrip() == branch 685 686deffindUpstreamBranchPoint(head ="HEAD"): 687 branches =p4BranchesInGit() 688# map from depot-path to branch name 689 branchByDepotPath = {} 690for branch in branches.keys(): 691 tip = branches[branch] 692 log =extractLogMessageFromGitCommit(tip) 693 settings =extractSettingsGitLog(log) 694if settings.has_key("depot-paths"): 695 paths =",".join(settings["depot-paths"]) 696 branchByDepotPath[paths] ="remotes/p4/"+ branch 697 698 settings =None 699 parent =0 700while parent <65535: 701 commit = head +"~%s"% parent 702 log =extractLogMessageFromGitCommit(commit) 703 settings =extractSettingsGitLog(log) 704if settings.has_key("depot-paths"): 705 paths =",".join(settings["depot-paths"]) 706if branchByDepotPath.has_key(paths): 707return[branchByDepotPath[paths], settings] 708 709 parent = parent +1 710 711return["", settings] 712 713defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 714if not silent: 715print("Creating/updating branch(es) in%sbased on origin branch(es)" 716% localRefPrefix) 717 718 originPrefix ="origin/p4/" 719 720for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 721 line = line.strip() 722if(not line.startswith(originPrefix))or line.endswith("HEAD"): 723continue 724 725 headName = line[len(originPrefix):] 726 remoteHead = localRefPrefix + headName 727 originHead = line 728 729 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 730if(not original.has_key('depot-paths') 731or not original.has_key('change')): 732continue 733 734 update =False 735if notgitBranchExists(remoteHead): 736if verbose: 737print"creating%s"% remoteHead 738 update =True 739else: 740 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 741if settings.has_key('change') >0: 742if settings['depot-paths'] == original['depot-paths']: 743 originP4Change =int(original['change']) 744 p4Change =int(settings['change']) 745if originP4Change > p4Change: 746print("%s(%s) is newer than%s(%s). " 747"Updating p4 branch from origin." 748% (originHead, originP4Change, 749 remoteHead, p4Change)) 750 update =True 751else: 752print("Ignoring:%swas imported from%swhile " 753"%swas imported from%s" 754% (originHead,','.join(original['depot-paths']), 755 remoteHead,','.join(settings['depot-paths']))) 756 757if update: 758system("git update-ref%s %s"% (remoteHead, originHead)) 759 760deforiginP4BranchesExist(): 761returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 762 763 764defp4ParseNumericChangeRange(parts): 765 changeStart =int(parts[0][1:]) 766if parts[1] =='#head': 767 changeEnd =p4_last_change() 768else: 769 changeEnd =int(parts[1]) 770 771return(changeStart, changeEnd) 772 773defchooseBlockSize(blockSize): 774if blockSize: 775return blockSize 776else: 777return defaultBlockSize 778 779defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 780assert depotPaths 781 782# Parse the change range into start and end. Try to find integer 783# revision ranges as these can be broken up into blocks to avoid 784# hitting server-side limits (maxrows, maxscanresults). But if 785# that doesn't work, fall back to using the raw revision specifier 786# strings, without using block mode. 787 788if changeRange is None or changeRange =='': 789 changeStart =1 790 changeEnd =p4_last_change() 791 block_size =chooseBlockSize(requestedBlockSize) 792else: 793 parts = changeRange.split(',') 794assertlen(parts) ==2 795try: 796(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 797 block_size =chooseBlockSize(requestedBlockSize) 798except: 799 changeStart = parts[0][1:] 800 changeEnd = parts[1] 801if requestedBlockSize: 802die("cannot use --changes-block-size with non-numeric revisions") 803 block_size =None 804 805# Accumulate change numbers in a dictionary to avoid duplicates 806 changes = {} 807 808for p in depotPaths: 809# Retrieve changes a block at a time, to prevent running 810# into a MaxResults/MaxScanRows error from the server. 811 812while True: 813 cmd = ['changes'] 814 815if block_size: 816 end =min(changeEnd, changeStart + block_size) 817 revisionRange ="%d,%d"% (changeStart, end) 818else: 819 revisionRange ="%s,%s"% (changeStart, changeEnd) 820 821 cmd += ["%s...@%s"% (p, revisionRange)] 822 823for line inp4_read_pipe_lines(cmd): 824 changeNum =int(line.split(" ")[1]) 825 changes[changeNum] =True 826 827if not block_size: 828break 829 830if end >= changeEnd: 831break 832 833 changeStart = end +1 834 835 changelist = changes.keys() 836 changelist.sort() 837return changelist 838 839defp4PathStartsWith(path, prefix): 840# This method tries to remedy a potential mixed-case issue: 841# 842# If UserA adds //depot/DirA/file1 843# and UserB adds //depot/dira/file2 844# 845# we may or may not have a problem. If you have core.ignorecase=true, 846# we treat DirA and dira as the same directory 847ifgitConfigBool("core.ignorecase"): 848return path.lower().startswith(prefix.lower()) 849return path.startswith(prefix) 850 851defgetClientSpec(): 852"""Look at the p4 client spec, create a View() object that contains 853 all the mappings, and return it.""" 854 855 specList =p4CmdList("client -o") 856iflen(specList) !=1: 857die('Output from "client -o" is%dlines, expecting 1'% 858len(specList)) 859 860# dictionary of all client parameters 861 entry = specList[0] 862 863# the //client/ name 864 client_name = entry["Client"] 865 866# just the keys that start with "View" 867 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 868 869# hold this new View 870 view =View(client_name) 871 872# append the lines, in order, to the view 873for view_num inrange(len(view_keys)): 874 k ="View%d"% view_num 875if k not in view_keys: 876die("Expected view key%smissing"% k) 877 view.append(entry[k]) 878 879return view 880 881defgetClientRoot(): 882"""Grab the client directory.""" 883 884 output =p4CmdList("client -o") 885iflen(output) !=1: 886die('Output from "client -o" is%dlines, expecting 1'%len(output)) 887 888 entry = output[0] 889if"Root"not in entry: 890die('Client has no "Root"') 891 892return entry["Root"] 893 894# 895# P4 wildcards are not allowed in filenames. P4 complains 896# if you simply add them, but you can force it with "-f", in 897# which case it translates them into %xx encoding internally. 898# 899defwildcard_decode(path): 900# Search for and fix just these four characters. Do % last so 901# that fixing it does not inadvertently create new %-escapes. 902# Cannot have * in a filename in windows; untested as to 903# what p4 would do in such a case. 904if not platform.system() =="Windows": 905 path = path.replace("%2A","*") 906 path = path.replace("%23","#") \ 907.replace("%40","@") \ 908.replace("%25","%") 909return path 910 911defwildcard_encode(path): 912# do % first to avoid double-encoding the %s introduced here 913 path = path.replace("%","%25") \ 914.replace("*","%2A") \ 915.replace("#","%23") \ 916.replace("@","%40") 917return path 918 919defwildcard_present(path): 920 m = re.search("[*#@%]", path) 921return m is not None 922 923class Command: 924def__init__(self): 925 self.usage ="usage: %prog [options]" 926 self.needsGit =True 927 self.verbose =False 928 929class P4UserMap: 930def__init__(self): 931 self.userMapFromPerforceServer =False 932 self.myP4UserId =None 933 934defp4UserId(self): 935if self.myP4UserId: 936return self.myP4UserId 937 938 results =p4CmdList("user -o") 939for r in results: 940if r.has_key('User'): 941 self.myP4UserId = r['User'] 942return r['User'] 943die("Could not find your p4 user id") 944 945defp4UserIsMe(self, p4User): 946# return True if the given p4 user is actually me 947 me = self.p4UserId() 948if not p4User or p4User != me: 949return False 950else: 951return True 952 953defgetUserCacheFilename(self): 954 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 955return home +"/.gitp4-usercache.txt" 956 957defgetUserMapFromPerforceServer(self): 958if self.userMapFromPerforceServer: 959return 960 self.users = {} 961 self.emails = {} 962 963for output inp4CmdList("users"): 964if not output.has_key("User"): 965continue 966 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 967 self.emails[output["Email"]] = output["User"] 968 969 970 s ='' 971for(key, val)in self.users.items(): 972 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 973 974open(self.getUserCacheFilename(),"wb").write(s) 975 self.userMapFromPerforceServer =True 976 977defloadUserMapFromCache(self): 978 self.users = {} 979 self.userMapFromPerforceServer =False 980try: 981 cache =open(self.getUserCacheFilename(),"rb") 982 lines = cache.readlines() 983 cache.close() 984for line in lines: 985 entry = line.strip().split("\t") 986 self.users[entry[0]] = entry[1] 987exceptIOError: 988 self.getUserMapFromPerforceServer() 989 990classP4Debug(Command): 991def__init__(self): 992 Command.__init__(self) 993 self.options = [] 994 self.description ="A tool to debug the output of p4 -G." 995 self.needsGit =False 996 997defrun(self, args): 998 j =0 999for output inp4CmdList(args):1000print'Element:%d'% j1001 j +=11002print output1003return True10041005classP4RollBack(Command):1006def__init__(self):1007 Command.__init__(self)1008 self.options = [1009 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1010]1011 self.description ="A tool to debug the multi-branch import. Don't use :)"1012 self.rollbackLocalBranches =False10131014defrun(self, args):1015iflen(args) !=1:1016return False1017 maxChange =int(args[0])10181019if"p4ExitCode"inp4Cmd("changes -m 1"):1020die("Problems executing p4");10211022if self.rollbackLocalBranches:1023 refPrefix ="refs/heads/"1024 lines =read_pipe_lines("git rev-parse --symbolic --branches")1025else:1026 refPrefix ="refs/remotes/"1027 lines =read_pipe_lines("git rev-parse --symbolic --remotes")10281029for line in lines:1030if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1031 line = line.strip()1032 ref = refPrefix + line1033 log =extractLogMessageFromGitCommit(ref)1034 settings =extractSettingsGitLog(log)10351036 depotPaths = settings['depot-paths']1037 change = settings['change']10381039 changed =False10401041iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1042for p in depotPaths]))) ==0:1043print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1044system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1045continue10461047while change andint(change) > maxChange:1048 changed =True1049if self.verbose:1050print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1051system("git update-ref%s\"%s^\""% (ref, ref))1052 log =extractLogMessageFromGitCommit(ref)1053 settings =extractSettingsGitLog(log)105410551056 depotPaths = settings['depot-paths']1057 change = settings['change']10581059if changed:1060print"%srewound to%s"% (ref, change)10611062return True10631064classP4Submit(Command, P4UserMap):10651066 conflict_behavior_choices = ("ask","skip","quit")10671068def__init__(self):1069 Command.__init__(self)1070 P4UserMap.__init__(self)1071 self.options = [1072 optparse.make_option("--origin", dest="origin"),1073 optparse.make_option("-M", dest="detectRenames", action="store_true"),1074# preserve the user, requires relevant p4 permissions1075 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1076 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1077 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1078 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1079 optparse.make_option("--conflict", dest="conflict_behavior",1080 choices=self.conflict_behavior_choices),1081 optparse.make_option("--branch", dest="branch"),1082]1083 self.description ="Submit changes from git to the perforce depot."1084 self.usage +=" [name of git branch to submit into perforce depot]"1085 self.origin =""1086 self.detectRenames =False1087 self.preserveUser =gitConfigBool("git-p4.preserveUser")1088 self.dry_run =False1089 self.prepare_p4_only =False1090 self.conflict_behavior =None1091 self.isWindows = (platform.system() =="Windows")1092 self.exportLabels =False1093 self.p4HasMoveCommand =p4_has_move_command()1094 self.branch =None10951096defcheck(self):1097iflen(p4CmdList("opened ...")) >0:1098die("You have files opened with perforce! Close them before starting the sync.")10991100defseparate_jobs_from_description(self, message):1101"""Extract and return a possible Jobs field in the commit1102 message. It goes into a separate section in the p4 change1103 specification.11041105 A jobs line starts with "Jobs:" and looks like a new field1106 in a form. Values are white-space separated on the same1107 line or on following lines that start with a tab.11081109 This does not parse and extract the full git commit message1110 like a p4 form. It just sees the Jobs: line as a marker1111 to pass everything from then on directly into the p4 form,1112 but outside the description section.11131114 Return a tuple (stripped log message, jobs string)."""11151116 m = re.search(r'^Jobs:', message, re.MULTILINE)1117if m is None:1118return(message,None)11191120 jobtext = message[m.start():]1121 stripped_message = message[:m.start()].rstrip()1122return(stripped_message, jobtext)11231124defprepareLogMessage(self, template, message, jobs):1125"""Edits the template returned from "p4 change -o" to insert1126 the message in the Description field, and the jobs text in1127 the Jobs field."""1128 result =""11291130 inDescriptionSection =False11311132for line in template.split("\n"):1133if line.startswith("#"):1134 result += line +"\n"1135continue11361137if inDescriptionSection:1138if line.startswith("Files:")or line.startswith("Jobs:"):1139 inDescriptionSection =False1140# insert Jobs section1141if jobs:1142 result += jobs +"\n"1143else:1144continue1145else:1146if line.startswith("Description:"):1147 inDescriptionSection =True1148 line +="\n"1149for messageLine in message.split("\n"):1150 line +="\t"+ messageLine +"\n"11511152 result += line +"\n"11531154return result11551156defpatchRCSKeywords(self,file, pattern):1157# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1158(handle, outFileName) = tempfile.mkstemp(dir='.')1159try:1160 outFile = os.fdopen(handle,"w+")1161 inFile =open(file,"r")1162 regexp = re.compile(pattern, re.VERBOSE)1163for line in inFile.readlines():1164 line = regexp.sub(r'$\1$', line)1165 outFile.write(line)1166 inFile.close()1167 outFile.close()1168# Forcibly overwrite the original file1169 os.unlink(file)1170 shutil.move(outFileName,file)1171except:1172# cleanup our temporary file1173 os.unlink(outFileName)1174print"Failed to strip RCS keywords in%s"%file1175raise11761177print"Patched up RCS keywords in%s"%file11781179defp4UserForCommit(self,id):1180# Return the tuple (perforce user,git email) for a given git commit id1181 self.getUserMapFromPerforceServer()1182 gitEmail =read_pipe(["git","log","--max-count=1",1183"--format=%ae",id])1184 gitEmail = gitEmail.strip()1185if not self.emails.has_key(gitEmail):1186return(None,gitEmail)1187else:1188return(self.emails[gitEmail],gitEmail)11891190defcheckValidP4Users(self,commits):1191# check if any git authors cannot be mapped to p4 users1192foridin commits:1193(user,email) = self.p4UserForCommit(id)1194if not user:1195 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1196ifgitConfigBool("git-p4.allowMissingP4Users"):1197print"%s"% msg1198else:1199die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)12001201deflastP4Changelist(self):1202# Get back the last changelist number submitted in this client spec. This1203# then gets used to patch up the username in the change. If the same1204# client spec is being used by multiple processes then this might go1205# wrong.1206 results =p4CmdList("client -o")# find the current client1207 client =None1208for r in results:1209if r.has_key('Client'):1210 client = r['Client']1211break1212if not client:1213die("could not get client spec")1214 results =p4CmdList(["changes","-c", client,"-m","1"])1215for r in results:1216if r.has_key('change'):1217return r['change']1218die("Could not get changelist number for last submit - cannot patch up user details")12191220defmodifyChangelistUser(self, changelist, newUser):1221# fixup the user field of a changelist after it has been submitted.1222 changes =p4CmdList("change -o%s"% changelist)1223iflen(changes) !=1:1224die("Bad output from p4 change modifying%sto user%s"%1225(changelist, newUser))12261227 c = changes[0]1228if c['User'] == newUser:return# nothing to do1229 c['User'] = newUser1230input= marshal.dumps(c)12311232 result =p4CmdList("change -f -i", stdin=input)1233for r in result:1234if r.has_key('code'):1235if r['code'] =='error':1236die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1237if r.has_key('data'):1238print("Updated user field for changelist%sto%s"% (changelist, newUser))1239return1240die("Could not modify user field of changelist%sto%s"% (changelist, newUser))12411242defcanChangeChangelists(self):1243# check to see if we have p4 admin or super-user permissions, either of1244# which are required to modify changelists.1245 results =p4CmdList(["protects", self.depotPath])1246for r in results:1247if r.has_key('perm'):1248if r['perm'] =='admin':1249return11250if r['perm'] =='super':1251return11252return012531254defprepareSubmitTemplate(self):1255"""Run "p4 change -o" to grab a change specification template.1256 This does not use "p4 -G", as it is nice to keep the submission1257 template in original order, since a human might edit it.12581259 Remove lines in the Files section that show changes to files1260 outside the depot path we're committing into."""12611262 template =""1263 inFilesSection =False1264for line inp4_read_pipe_lines(['change','-o']):1265if line.endswith("\r\n"):1266 line = line[:-2] +"\n"1267if inFilesSection:1268if line.startswith("\t"):1269# path starts and ends with a tab1270 path = line[1:]1271 lastTab = path.rfind("\t")1272if lastTab != -1:1273 path = path[:lastTab]1274if notp4PathStartsWith(path, self.depotPath):1275continue1276else:1277 inFilesSection =False1278else:1279if line.startswith("Files:"):1280 inFilesSection =True12811282 template += line12831284return template12851286defedit_template(self, template_file):1287"""Invoke the editor to let the user change the submission1288 message. Return true if okay to continue with the submit."""12891290# if configured to skip the editing part, just submit1291ifgitConfigBool("git-p4.skipSubmitEdit"):1292return True12931294# look at the modification time, to check later if the user saved1295# the file1296 mtime = os.stat(template_file).st_mtime12971298# invoke the editor1299if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1300 editor = os.environ.get("P4EDITOR")1301else:1302 editor =read_pipe("git var GIT_EDITOR").strip()1303system(["sh","-c", ('%s"$@"'% editor), editor, template_file])13041305# If the file was not saved, prompt to see if this patch should1306# be skipped. But skip this verification step if configured so.1307ifgitConfigBool("git-p4.skipSubmitEditCheck"):1308return True13091310# modification time updated means user saved the file1311if os.stat(template_file).st_mtime > mtime:1312return True13131314while True:1315 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1316if response =='y':1317return True1318if response =='n':1319return False13201321defget_diff_description(self, editedFiles, filesToAdd):1322# diff1323if os.environ.has_key("P4DIFF"):1324del(os.environ["P4DIFF"])1325 diff =""1326for editedFile in editedFiles:1327 diff +=p4_read_pipe(['diff','-du',1328wildcard_encode(editedFile)])13291330# new file diff1331 newdiff =""1332for newFile in filesToAdd:1333 newdiff +="==== new file ====\n"1334 newdiff +="--- /dev/null\n"1335 newdiff +="+++%s\n"% newFile1336 f =open(newFile,"r")1337for line in f.readlines():1338 newdiff +="+"+ line1339 f.close()13401341return(diff + newdiff).replace('\r\n','\n')13421343defapplyCommit(self,id):1344"""Apply one commit, return True if it succeeded."""13451346print"Applying",read_pipe(["git","show","-s",1347"--format=format:%h%s",id])13481349(p4User, gitEmail) = self.p4UserForCommit(id)13501351 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1352 filesToAdd =set()1353 filesToDelete =set()1354 editedFiles =set()1355 pureRenameCopy =set()1356 filesToChangeExecBit = {}13571358for line in diff:1359 diff =parseDiffTreeEntry(line)1360 modifier = diff['status']1361 path = diff['src']1362if modifier =="M":1363p4_edit(path)1364ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1365 filesToChangeExecBit[path] = diff['dst_mode']1366 editedFiles.add(path)1367elif modifier =="A":1368 filesToAdd.add(path)1369 filesToChangeExecBit[path] = diff['dst_mode']1370if path in filesToDelete:1371 filesToDelete.remove(path)1372elif modifier =="D":1373 filesToDelete.add(path)1374if path in filesToAdd:1375 filesToAdd.remove(path)1376elif modifier =="C":1377 src, dest = diff['src'], diff['dst']1378p4_integrate(src, dest)1379 pureRenameCopy.add(dest)1380if diff['src_sha1'] != diff['dst_sha1']:1381p4_edit(dest)1382 pureRenameCopy.discard(dest)1383ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1384p4_edit(dest)1385 pureRenameCopy.discard(dest)1386 filesToChangeExecBit[dest] = diff['dst_mode']1387if self.isWindows:1388# turn off read-only attribute1389 os.chmod(dest, stat.S_IWRITE)1390 os.unlink(dest)1391 editedFiles.add(dest)1392elif modifier =="R":1393 src, dest = diff['src'], diff['dst']1394if self.p4HasMoveCommand:1395p4_edit(src)# src must be open before move1396p4_move(src, dest)# opens for (move/delete, move/add)1397else:1398p4_integrate(src, dest)1399if diff['src_sha1'] != diff['dst_sha1']:1400p4_edit(dest)1401else:1402 pureRenameCopy.add(dest)1403ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1404if not self.p4HasMoveCommand:1405p4_edit(dest)# with move: already open, writable1406 filesToChangeExecBit[dest] = diff['dst_mode']1407if not self.p4HasMoveCommand:1408if self.isWindows:1409 os.chmod(dest, stat.S_IWRITE)1410 os.unlink(dest)1411 filesToDelete.add(src)1412 editedFiles.add(dest)1413else:1414die("unknown modifier%sfor%s"% (modifier, path))14151416 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1417 patchcmd = diffcmd +" | git apply "1418 tryPatchCmd = patchcmd +"--check -"1419 applyPatchCmd = patchcmd +"--check --apply -"1420 patch_succeeded =True14211422if os.system(tryPatchCmd) !=0:1423 fixed_rcs_keywords =False1424 patch_succeeded =False1425print"Unfortunately applying the change failed!"14261427# Patch failed, maybe it's just RCS keyword woes. Look through1428# the patch to see if that's possible.1429ifgitConfigBool("git-p4.attemptRCSCleanup"):1430file=None1431 pattern =None1432 kwfiles = {}1433forfilein editedFiles | filesToDelete:1434# did this file's delta contain RCS keywords?1435 pattern =p4_keywords_regexp_for_file(file)14361437if pattern:1438# this file is a possibility...look for RCS keywords.1439 regexp = re.compile(pattern, re.VERBOSE)1440for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1441if regexp.search(line):1442if verbose:1443print"got keyword match on%sin%sin%s"% (pattern, line,file)1444 kwfiles[file] = pattern1445break14461447forfilein kwfiles:1448if verbose:1449print"zapping%swith%s"% (line,pattern)1450# File is being deleted, so not open in p4. Must1451# disable the read-only bit on windows.1452if self.isWindows andfilenot in editedFiles:1453 os.chmod(file, stat.S_IWRITE)1454 self.patchRCSKeywords(file, kwfiles[file])1455 fixed_rcs_keywords =True14561457if fixed_rcs_keywords:1458print"Retrying the patch with RCS keywords cleaned up"1459if os.system(tryPatchCmd) ==0:1460 patch_succeeded =True14611462if not patch_succeeded:1463for f in editedFiles:1464p4_revert(f)1465return False14661467#1468# Apply the patch for real, and do add/delete/+x handling.1469#1470system(applyPatchCmd)14711472for f in filesToAdd:1473p4_add(f)1474for f in filesToDelete:1475p4_revert(f)1476p4_delete(f)14771478# Set/clear executable bits1479for f in filesToChangeExecBit.keys():1480 mode = filesToChangeExecBit[f]1481setP4ExecBit(f, mode)14821483#1484# Build p4 change description, starting with the contents1485# of the git commit message.1486#1487 logMessage =extractLogMessageFromGitCommit(id)1488 logMessage = logMessage.strip()1489(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14901491 template = self.prepareSubmitTemplate()1492 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14931494if self.preserveUser:1495 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14961497if self.checkAuthorship and not self.p4UserIsMe(p4User):1498 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1499 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1500 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"15011502 separatorLine ="######## everything below this line is just the diff #######\n"1503if not self.prepare_p4_only:1504 submitTemplate += separatorLine1505 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)15061507(handle, fileName) = tempfile.mkstemp()1508 tmpFile = os.fdopen(handle,"w+b")1509if self.isWindows:1510 submitTemplate = submitTemplate.replace("\n","\r\n")1511 tmpFile.write(submitTemplate)1512 tmpFile.close()15131514if self.prepare_p4_only:1515#1516# Leave the p4 tree prepared, and the submit template around1517# and let the user decide what to do next1518#1519print1520print"P4 workspace prepared for submission."1521print"To submit or revert, go to client workspace"1522print" "+ self.clientPath1523print1524print"To submit, use\"p4 submit\"to write a new description,"1525print"or\"p4 submit -i <%s\"to use the one prepared by" \1526"\"git p4\"."% fileName1527print"You can delete the file\"%s\"when finished."% fileName15281529if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1530print"To preserve change ownership by user%s, you must\n" \1531"do\"p4 change -f <change>\"after submitting and\n" \1532"edit the User field."1533if pureRenameCopy:1534print"After submitting, renamed files must be re-synced."1535print"Invoke\"p4 sync -f\"on each of these files:"1536for f in pureRenameCopy:1537print" "+ f15381539print1540print"To revert the changes, use\"p4 revert ...\", and delete"1541print"the submit template file\"%s\""% fileName1542if filesToAdd:1543print"Since the commit adds new files, they must be deleted:"1544for f in filesToAdd:1545print" "+ f1546print1547return True15481549#1550# Let the user edit the change description, then submit it.1551#1552if self.edit_template(fileName):1553# read the edited message and submit1554 ret =True1555 tmpFile =open(fileName,"rb")1556 message = tmpFile.read()1557 tmpFile.close()1558if self.isWindows:1559 message = message.replace("\r\n","\n")1560 submitTemplate = message[:message.index(separatorLine)]1561p4_write_pipe(['submit','-i'], submitTemplate)15621563if self.preserveUser:1564if p4User:1565# Get last changelist number. Cannot easily get it from1566# the submit command output as the output is1567# unmarshalled.1568 changelist = self.lastP4Changelist()1569 self.modifyChangelistUser(changelist, p4User)15701571# The rename/copy happened by applying a patch that created a1572# new file. This leaves it writable, which confuses p4.1573for f in pureRenameCopy:1574p4_sync(f,"-f")15751576else:1577# skip this patch1578 ret =False1579print"Submission cancelled, undoing p4 changes."1580for f in editedFiles:1581p4_revert(f)1582for f in filesToAdd:1583p4_revert(f)1584 os.remove(f)1585for f in filesToDelete:1586p4_revert(f)15871588 os.remove(fileName)1589return ret15901591# Export git tags as p4 labels. Create a p4 label and then tag1592# with that.1593defexportGitTags(self, gitTags):1594 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1595iflen(validLabelRegexp) ==0:1596 validLabelRegexp = defaultLabelRegexp1597 m = re.compile(validLabelRegexp)15981599for name in gitTags:16001601if not m.match(name):1602if verbose:1603print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1604continue16051606# Get the p4 commit this corresponds to1607 logMessage =extractLogMessageFromGitCommit(name)1608 values =extractSettingsGitLog(logMessage)16091610if not values.has_key('change'):1611# a tag pointing to something not sent to p4; ignore1612if verbose:1613print"git tag%sdoes not give a p4 commit"% name1614continue1615else:1616 changelist = values['change']16171618# Get the tag details.1619 inHeader =True1620 isAnnotated =False1621 body = []1622for l inread_pipe_lines(["git","cat-file","-p", name]):1623 l = l.strip()1624if inHeader:1625if re.match(r'tag\s+', l):1626 isAnnotated =True1627elif re.match(r'\s*$', l):1628 inHeader =False1629continue1630else:1631 body.append(l)16321633if not isAnnotated:1634 body = ["lightweight tag imported by git p4\n"]16351636# Create the label - use the same view as the client spec we are using1637 clientSpec =getClientSpec()16381639 labelTemplate ="Label:%s\n"% name1640 labelTemplate +="Description:\n"1641for b in body:1642 labelTemplate +="\t"+ b +"\n"1643 labelTemplate +="View:\n"1644for depot_side in clientSpec.mappings:1645 labelTemplate +="\t%s\n"% depot_side16461647if self.dry_run:1648print"Would create p4 label%sfor tag"% name1649elif self.prepare_p4_only:1650print"Not creating p4 label%sfor tag due to option" \1651" --prepare-p4-only"% name1652else:1653p4_write_pipe(["label","-i"], labelTemplate)16541655# Use the label1656p4_system(["tag","-l", name] +1657["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16581659if verbose:1660print"created p4 label for tag%s"% name16611662defrun(self, args):1663iflen(args) ==0:1664 self.master =currentGitBranch()1665iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1666die("Detecting current git branch failed!")1667eliflen(args) ==1:1668 self.master = args[0]1669if notbranchExists(self.master):1670die("Branch%sdoes not exist"% self.master)1671else:1672return False16731674 allowSubmit =gitConfig("git-p4.allowSubmit")1675iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1676die("%sis not in git-p4.allowSubmit"% self.master)16771678[upstream, settings] =findUpstreamBranchPoint()1679 self.depotPath = settings['depot-paths'][0]1680iflen(self.origin) ==0:1681 self.origin = upstream16821683if self.preserveUser:1684if not self.canChangeChangelists():1685die("Cannot preserve user names without p4 super-user or admin permissions")16861687# if not set from the command line, try the config file1688if self.conflict_behavior is None:1689 val =gitConfig("git-p4.conflict")1690if val:1691if val not in self.conflict_behavior_choices:1692die("Invalid value '%s' for config git-p4.conflict"% val)1693else:1694 val ="ask"1695 self.conflict_behavior = val16961697if self.verbose:1698print"Origin branch is "+ self.origin16991700iflen(self.depotPath) ==0:1701print"Internal error: cannot locate perforce depot path from existing branches"1702 sys.exit(128)17031704 self.useClientSpec =False1705ifgitConfigBool("git-p4.useclientspec"):1706 self.useClientSpec =True1707if self.useClientSpec:1708 self.clientSpecDirs =getClientSpec()17091710# Check for the existance of P4 branches1711 branchesDetected = (len(p4BranchesInGit().keys()) >1)17121713if self.useClientSpec and not branchesDetected:1714# all files are relative to the client spec1715 self.clientPath =getClientRoot()1716else:1717 self.clientPath =p4Where(self.depotPath)17181719if self.clientPath =="":1720die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)17211722print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1723 self.oldWorkingDirectory = os.getcwd()17241725# ensure the clientPath exists1726 new_client_dir =False1727if not os.path.exists(self.clientPath):1728 new_client_dir =True1729 os.makedirs(self.clientPath)17301731chdir(self.clientPath, is_client_path=True)1732if self.dry_run:1733print"Would synchronize p4 checkout in%s"% self.clientPath1734else:1735print"Synchronizing p4 checkout..."1736if new_client_dir:1737# old one was destroyed, and maybe nobody told p41738p4_sync("...","-f")1739else:1740p4_sync("...")1741 self.check()17421743 commits = []1744for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1745 commits.append(line.strip())1746 commits.reverse()17471748if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1749 self.checkAuthorship =False1750else:1751 self.checkAuthorship =True17521753if self.preserveUser:1754 self.checkValidP4Users(commits)17551756#1757# Build up a set of options to be passed to diff when1758# submitting each commit to p4.1759#1760if self.detectRenames:1761# command-line -M arg1762 self.diffOpts ="-M"1763else:1764# If not explicitly set check the config variable1765 detectRenames =gitConfig("git-p4.detectRenames")17661767if detectRenames.lower() =="false"or detectRenames =="":1768 self.diffOpts =""1769elif detectRenames.lower() =="true":1770 self.diffOpts ="-M"1771else:1772 self.diffOpts ="-M%s"% detectRenames17731774# no command-line arg for -C or --find-copies-harder, just1775# config variables1776 detectCopies =gitConfig("git-p4.detectCopies")1777if detectCopies.lower() =="false"or detectCopies =="":1778pass1779elif detectCopies.lower() =="true":1780 self.diffOpts +=" -C"1781else:1782 self.diffOpts +=" -C%s"% detectCopies17831784ifgitConfigBool("git-p4.detectCopiesHarder"):1785 self.diffOpts +=" --find-copies-harder"17861787#1788# Apply the commits, one at a time. On failure, ask if should1789# continue to try the rest of the patches, or quit.1790#1791if self.dry_run:1792print"Would apply"1793 applied = []1794 last =len(commits) -11795for i, commit inenumerate(commits):1796if self.dry_run:1797print" ",read_pipe(["git","show","-s",1798"--format=format:%h%s", commit])1799 ok =True1800else:1801 ok = self.applyCommit(commit)1802if ok:1803 applied.append(commit)1804else:1805if self.prepare_p4_only and i < last:1806print"Processing only the first commit due to option" \1807" --prepare-p4-only"1808break1809if i < last:1810 quit =False1811while True:1812# prompt for what to do, or use the option/variable1813if self.conflict_behavior =="ask":1814print"What do you want to do?"1815 response =raw_input("[s]kip this commit but apply"1816" the rest, or [q]uit? ")1817if not response:1818continue1819elif self.conflict_behavior =="skip":1820 response ="s"1821elif self.conflict_behavior =="quit":1822 response ="q"1823else:1824die("Unknown conflict_behavior '%s'"%1825 self.conflict_behavior)18261827if response[0] =="s":1828print"Skipping this commit, but applying the rest"1829break1830if response[0] =="q":1831print"Quitting"1832 quit =True1833break1834if quit:1835break18361837chdir(self.oldWorkingDirectory)18381839if self.dry_run:1840pass1841elif self.prepare_p4_only:1842pass1843eliflen(commits) ==len(applied):1844print"All commits applied!"18451846 sync =P4Sync()1847if self.branch:1848 sync.branch = self.branch1849 sync.run([])18501851 rebase =P4Rebase()1852 rebase.rebase()18531854else:1855iflen(applied) ==0:1856print"No commits applied."1857else:1858print"Applied only the commits marked with '*':"1859for c in commits:1860if c in applied:1861 star ="*"1862else:1863 star =" "1864print star,read_pipe(["git","show","-s",1865"--format=format:%h%s", c])1866print"You will have to do 'git p4 sync' and rebase."18671868ifgitConfigBool("git-p4.exportLabels"):1869 self.exportLabels =True18701871if self.exportLabels:1872 p4Labels =getP4Labels(self.depotPath)1873 gitTags =getGitTags()18741875 missingGitTags = gitTags - p4Labels1876 self.exportGitTags(missingGitTags)18771878# exit with error unless everything applied perfectly1879iflen(commits) !=len(applied):1880 sys.exit(1)18811882return True18831884classView(object):1885"""Represent a p4 view ("p4 help views"), and map files in a1886 repo according to the view."""18871888def__init__(self, client_name):1889 self.mappings = []1890 self.client_prefix ="//%s/"% client_name1891# cache results of "p4 where" to lookup client file locations1892 self.client_spec_path_cache = {}18931894defappend(self, view_line):1895"""Parse a view line, splitting it into depot and client1896 sides. Append to self.mappings, preserving order. This1897 is only needed for tag creation."""18981899# Split the view line into exactly two words. P4 enforces1900# structure on these lines that simplifies this quite a bit.1901#1902# Either or both words may be double-quoted.1903# Single quotes do not matter.1904# Double-quote marks cannot occur inside the words.1905# A + or - prefix is also inside the quotes.1906# There are no quotes unless they contain a space.1907# The line is already white-space stripped.1908# The two words are separated by a single space.1909#1910if view_line[0] =='"':1911# First word is double quoted. Find its end.1912 close_quote_index = view_line.find('"',1)1913if close_quote_index <=0:1914die("No first-word closing quote found:%s"% view_line)1915 depot_side = view_line[1:close_quote_index]1916# skip closing quote and space1917 rhs_index = close_quote_index +1+11918else:1919 space_index = view_line.find(" ")1920if space_index <=0:1921die("No word-splitting space found:%s"% view_line)1922 depot_side = view_line[0:space_index]1923 rhs_index = space_index +119241925# prefix + means overlay on previous mapping1926if depot_side.startswith("+"):1927 depot_side = depot_side[1:]19281929# prefix - means exclude this path, leave out of mappings1930 exclude =False1931if depot_side.startswith("-"):1932 exclude =True1933 depot_side = depot_side[1:]19341935if not exclude:1936 self.mappings.append(depot_side)19371938defconvert_client_path(self, clientFile):1939# chop off //client/ part to make it relative1940if not clientFile.startswith(self.client_prefix):1941die("No prefix '%s' on clientFile '%s'"%1942(self.client_prefix, clientFile))1943return clientFile[len(self.client_prefix):]19441945defupdate_client_spec_path_cache(self, files):1946""" Caching file paths by "p4 where" batch query """19471948# List depot file paths exclude that already cached1949 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]19501951iflen(fileArgs) ==0:1952return# All files in cache19531954 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1955for res in where_result:1956if"code"in res and res["code"] =="error":1957# assume error is "... file(s) not in client view"1958continue1959if"clientFile"not in res:1960die("No clientFile in 'p4 where' output")1961if"unmap"in res:1962# it will list all of them, but only one not unmap-ped1963continue1964ifgitConfigBool("core.ignorecase"):1965 res['depotFile'] = res['depotFile'].lower()1966 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19671968# not found files or unmap files set to ""1969for depotFile in fileArgs:1970ifgitConfigBool("core.ignorecase"):1971 depotFile = depotFile.lower()1972if depotFile not in self.client_spec_path_cache:1973 self.client_spec_path_cache[depotFile] =""19741975defmap_in_client(self, depot_path):1976"""Return the relative location in the client where this1977 depot file should live. Returns "" if the file should1978 not be mapped in the client."""19791980ifgitConfigBool("core.ignorecase"):1981 depot_path = depot_path.lower()19821983if depot_path in self.client_spec_path_cache:1984return self.client_spec_path_cache[depot_path]19851986die("Error:%sis not found in client spec path"% depot_path )1987return""19881989classP4Sync(Command, P4UserMap):1990 delete_actions = ("delete","move/delete","purge")19911992def__init__(self):1993 Command.__init__(self)1994 P4UserMap.__init__(self)1995 self.options = [1996 optparse.make_option("--branch", dest="branch"),1997 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1998 optparse.make_option("--changesfile", dest="changesFile"),1999 optparse.make_option("--silent", dest="silent", action="store_true"),2000 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2001 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2002 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2003help="Import into refs/heads/ , not refs/remotes"),2004 optparse.make_option("--max-changes", dest="maxChanges",2005help="Maximum number of changes to import"),2006 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2007help="Internal block size to use when iteratively calling p4 changes"),2008 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2009help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2010 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2011help="Only sync files that are included in the Perforce Client Spec"),2012 optparse.make_option("-/", dest="cloneExclude",2013 action="append",type="string",2014help="exclude depot path"),2015]2016 self.description ="""Imports from Perforce into a git repository.\n2017 example:2018 //depot/my/project/ -- to import the current head2019 //depot/my/project/@all -- to import everything2020 //depot/my/project/@1,6 -- to import only from revision 1 to 620212022 (a ... is not needed in the path p4 specification, it's added implicitly)"""20232024 self.usage +=" //depot/path[@revRange]"2025 self.silent =False2026 self.createdBranches =set()2027 self.committedChanges =set()2028 self.branch =""2029 self.detectBranches =False2030 self.detectLabels =False2031 self.importLabels =False2032 self.changesFile =""2033 self.syncWithOrigin =True2034 self.importIntoRemotes =True2035 self.maxChanges =""2036 self.changes_block_size =None2037 self.keepRepoPath =False2038 self.depotPaths =None2039 self.p4BranchesInGit = []2040 self.cloneExclude = []2041 self.useClientSpec =False2042 self.useClientSpec_from_options =False2043 self.clientSpecDirs =None2044 self.tempBranches = []2045 self.tempBranchLocation ="git-p4-tmp"20462047ifgitConfig("git-p4.syncFromOrigin") =="false":2048 self.syncWithOrigin =False20492050# This is required for the "append" cloneExclude action2051defensure_value(self, attr, value):2052if nothasattr(self, attr)orgetattr(self, attr)is None:2053setattr(self, attr, value)2054returngetattr(self, attr)20552056# Force a checkpoint in fast-import and wait for it to finish2057defcheckpoint(self):2058 self.gitStream.write("checkpoint\n\n")2059 self.gitStream.write("progress checkpoint\n\n")2060 out = self.gitOutput.readline()2061if self.verbose:2062print"checkpoint finished: "+ out20632064defextractFilesFromCommit(self, commit):2065 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2066for path in self.cloneExclude]2067 files = []2068 fnum =02069while commit.has_key("depotFile%s"% fnum):2070 path = commit["depotFile%s"% fnum]20712072if[p for p in self.cloneExclude2073ifp4PathStartsWith(path, p)]:2074 found =False2075else:2076 found = [p for p in self.depotPaths2077ifp4PathStartsWith(path, p)]2078if not found:2079 fnum = fnum +12080continue20812082file= {}2083file["path"] = path2084file["rev"] = commit["rev%s"% fnum]2085file["action"] = commit["action%s"% fnum]2086file["type"] = commit["type%s"% fnum]2087 files.append(file)2088 fnum = fnum +12089return files20902091defstripRepoPath(self, path, prefixes):2092"""When streaming files, this is called to map a p4 depot path2093 to where it should go in git. The prefixes are either2094 self.depotPaths, or self.branchPrefixes in the case of2095 branch detection."""20962097if self.useClientSpec:2098# branch detection moves files up a level (the branch name)2099# from what client spec interpretation gives2100 path = self.clientSpecDirs.map_in_client(path)2101if self.detectBranches:2102for b in self.knownBranches:2103if path.startswith(b +"/"):2104 path = path[len(b)+1:]21052106elif self.keepRepoPath:2107# Preserve everything in relative path name except leading2108# //depot/; just look at first prefix as they all should2109# be in the same depot.2110 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2111ifp4PathStartsWith(path, depot):2112 path = path[len(depot):]21132114else:2115for p in prefixes:2116ifp4PathStartsWith(path, p):2117 path = path[len(p):]2118break21192120 path =wildcard_decode(path)2121return path21222123defsplitFilesIntoBranches(self, commit):2124"""Look at each depotFile in the commit to figure out to what2125 branch it belongs."""21262127if self.clientSpecDirs:2128 files = self.extractFilesFromCommit(commit)2129 self.clientSpecDirs.update_client_spec_path_cache(files)21302131 branches = {}2132 fnum =02133while commit.has_key("depotFile%s"% fnum):2134 path = commit["depotFile%s"% fnum]2135 found = [p for p in self.depotPaths2136ifp4PathStartsWith(path, p)]2137if not found:2138 fnum = fnum +12139continue21402141file= {}2142file["path"] = path2143file["rev"] = commit["rev%s"% fnum]2144file["action"] = commit["action%s"% fnum]2145file["type"] = commit["type%s"% fnum]2146 fnum = fnum +121472148# start with the full relative path where this file would2149# go in a p4 client2150if self.useClientSpec:2151 relPath = self.clientSpecDirs.map_in_client(path)2152else:2153 relPath = self.stripRepoPath(path, self.depotPaths)21542155for branch in self.knownBranches.keys():2156# add a trailing slash so that a commit into qt/4.2foo2157# doesn't end up in qt/4.2, e.g.2158if relPath.startswith(branch +"/"):2159if branch not in branches:2160 branches[branch] = []2161 branches[branch].append(file)2162break21632164return branches21652166# output one file from the P4 stream2167# - helper for streamP4Files21682169defstreamOneP4File(self,file, contents):2170 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2171if verbose:2172 sys.stderr.write("%s\n"% relPath)21732174(type_base, type_mods) =split_p4_type(file["type"])21752176 git_mode ="100644"2177if"x"in type_mods:2178 git_mode ="100755"2179if type_base =="symlink":2180 git_mode ="120000"2181# p4 print on a symlink sometimes contains "target\n";2182# if it does, remove the newline2183 data =''.join(contents)2184if not data:2185# Some version of p4 allowed creating a symlink that pointed2186# to nothing. This causes p4 errors when checking out such2187# a change, and errors here too. Work around it by ignoring2188# the bad symlink; hopefully a future change fixes it.2189print"\nIgnoring empty symlink in%s"%file['depotFile']2190return2191elif data[-1] =='\n':2192 contents = [data[:-1]]2193else:2194 contents = [data]21952196if type_base =="utf16":2197# p4 delivers different text in the python output to -G2198# than it does when using "print -o", or normal p4 client2199# operations. utf16 is converted to ascii or utf8, perhaps.2200# But ascii text saved as -t utf16 is completely mangled.2201# Invoke print -o to get the real contents.2202#2203# On windows, the newlines will always be mangled by print, so put2204# them back too. This is not needed to the cygwin windows version,2205# just the native "NT" type.2206#2207 text =p4_read_pipe(['print','-q','-o','-',"%s@%s"% (file['depotFile'],file['change']) ])2208ifp4_version_string().find("/NT") >=0:2209 text = text.replace("\r\n","\n")2210 contents = [ text ]22112212if type_base =="apple":2213# Apple filetype files will be streamed as a concatenation of2214# its appledouble header and the contents. This is useless2215# on both macs and non-macs. If using "print -q -o xx", it2216# will create "xx" with the data, and "%xx" with the header.2217# This is also not very useful.2218#2219# Ideally, someday, this script can learn how to generate2220# appledouble files directly and import those to git, but2221# non-mac machines can never find a use for apple filetype.2222print"\nIgnoring apple filetype file%s"%file['depotFile']2223return22242225# Note that we do not try to de-mangle keywords on utf16 files,2226# even though in theory somebody may want that.2227 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2228if pattern:2229 regexp = re.compile(pattern, re.VERBOSE)2230 text =''.join(contents)2231 text = regexp.sub(r'$\1$', text)2232 contents = [ text ]22332234 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22352236# total length...2237 length =02238for d in contents:2239 length = length +len(d)22402241 self.gitStream.write("data%d\n"% length)2242for d in contents:2243 self.gitStream.write(d)2244 self.gitStream.write("\n")22452246defstreamOneP4Deletion(self,file):2247 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2248if verbose:2249 sys.stderr.write("delete%s\n"% relPath)2250 self.gitStream.write("D%s\n"% relPath)22512252# handle another chunk of streaming data2253defstreamP4FilesCb(self, marshalled):22542255# catch p4 errors and complain2256 err =None2257if"code"in marshalled:2258if marshalled["code"] =="error":2259if"data"in marshalled:2260 err = marshalled["data"].rstrip()2261if err:2262 f =None2263if self.stream_have_file_info:2264if"depotFile"in self.stream_file:2265 f = self.stream_file["depotFile"]2266# force a failure in fast-import, else an empty2267# commit will be made2268 self.gitStream.write("\n")2269 self.gitStream.write("die-now\n")2270 self.gitStream.close()2271# ignore errors, but make sure it exits first2272 self.importProcess.wait()2273if f:2274die("Error from p4 print for%s:%s"% (f, err))2275else:2276die("Error from p4 print:%s"% err)22772278if marshalled.has_key('depotFile')and self.stream_have_file_info:2279# start of a new file - output the old one first2280 self.streamOneP4File(self.stream_file, self.stream_contents)2281 self.stream_file = {}2282 self.stream_contents = []2283 self.stream_have_file_info =False22842285# pick up the new file information... for the2286# 'data' field we need to append to our array2287for k in marshalled.keys():2288if k =='data':2289 self.stream_contents.append(marshalled['data'])2290else:2291 self.stream_file[k] = marshalled[k]22922293 self.stream_have_file_info =True22942295# Stream directly from "p4 files" into "git fast-import"2296defstreamP4Files(self, files):2297 filesForCommit = []2298 filesToRead = []2299 filesToDelete = []23002301for f in files:2302# if using a client spec, only add the files that have2303# a path in the client2304if self.clientSpecDirs:2305if self.clientSpecDirs.map_in_client(f['path']) =="":2306continue23072308 filesForCommit.append(f)2309if f['action']in self.delete_actions:2310 filesToDelete.append(f)2311else:2312 filesToRead.append(f)23132314# deleted files...2315for f in filesToDelete:2316 self.streamOneP4Deletion(f)23172318iflen(filesToRead) >0:2319 self.stream_file = {}2320 self.stream_contents = []2321 self.stream_have_file_info =False23222323# curry self argument2324defstreamP4FilesCbSelf(entry):2325 self.streamP4FilesCb(entry)23262327 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23282329p4CmdList(["-x","-","print"],2330 stdin=fileArgs,2331 cb=streamP4FilesCbSelf)23322333# do the last chunk2334if self.stream_file.has_key('depotFile'):2335 self.streamOneP4File(self.stream_file, self.stream_contents)23362337defmake_email(self, userid):2338if userid in self.users:2339return self.users[userid]2340else:2341return"%s<a@b>"% userid23422343# Stream a p4 tag2344defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2345if verbose:2346print"writing tag%sfor commit%s"% (labelName, commit)2347 gitStream.write("tag%s\n"% labelName)2348 gitStream.write("from%s\n"% commit)23492350if labelDetails.has_key('Owner'):2351 owner = labelDetails["Owner"]2352else:2353 owner =None23542355# Try to use the owner of the p4 label, or failing that,2356# the current p4 user id.2357if owner:2358 email = self.make_email(owner)2359else:2360 email = self.make_email(self.p4UserId())2361 tagger ="%s %s %s"% (email, epoch, self.tz)23622363 gitStream.write("tagger%s\n"% tagger)23642365print"labelDetails=",labelDetails2366if labelDetails.has_key('Description'):2367 description = labelDetails['Description']2368else:2369 description ='Label from git p4'23702371 gitStream.write("data%d\n"%len(description))2372 gitStream.write(description)2373 gitStream.write("\n")23742375defcommit(self, details, files, branch, parent =""):2376 epoch = details["time"]2377 author = details["user"]23782379if self.verbose:2380print"commit into%s"% branch23812382# start with reading files; if that fails, we should not2383# create a commit.2384 new_files = []2385for f in files:2386if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2387 new_files.append(f)2388else:2389 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23902391if self.clientSpecDirs:2392 self.clientSpecDirs.update_client_spec_path_cache(files)23932394 self.gitStream.write("commit%s\n"% branch)2395# gitStream.write("mark :%s\n" % details["change"])2396 self.committedChanges.add(int(details["change"]))2397 committer =""2398if author not in self.users:2399 self.getUserMapFromPerforceServer()2400 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)24012402 self.gitStream.write("committer%s\n"% committer)24032404 self.gitStream.write("data <<EOT\n")2405 self.gitStream.write(details["desc"])2406 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2407(','.join(self.branchPrefixes), details["change"]))2408iflen(details['options']) >0:2409 self.gitStream.write(": options =%s"% details['options'])2410 self.gitStream.write("]\nEOT\n\n")24112412iflen(parent) >0:2413if self.verbose:2414print"parent%s"% parent2415 self.gitStream.write("from%s\n"% parent)24162417 self.streamP4Files(new_files)2418 self.gitStream.write("\n")24192420 change =int(details["change"])24212422if self.labels.has_key(change):2423 label = self.labels[change]2424 labelDetails = label[0]2425 labelRevisions = label[1]2426if self.verbose:2427print"Change%sis labelled%s"% (change, labelDetails)24282429 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2430for p in self.branchPrefixes])24312432iflen(files) ==len(labelRevisions):24332434 cleanedFiles = {}2435for info in files:2436if info["action"]in self.delete_actions:2437continue2438 cleanedFiles[info["depotFile"]] = info["rev"]24392440if cleanedFiles == labelRevisions:2441 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24422443else:2444if not self.silent:2445print("Tag%sdoes not match with change%s: files do not match."2446% (labelDetails["label"], change))24472448else:2449if not self.silent:2450print("Tag%sdoes not match with change%s: file count is different."2451% (labelDetails["label"], change))24522453# Build a dictionary of changelists and labels, for "detect-labels" option.2454defgetLabels(self):2455 self.labels = {}24562457 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2458iflen(l) >0and not self.silent:2459print"Finding files belonging to labels in%s"% `self.depotPaths`24602461for output in l:2462 label = output["label"]2463 revisions = {}2464 newestChange =02465if self.verbose:2466print"Querying files for label%s"% label2467forfileinp4CmdList(["files"] +2468["%s...@%s"% (p, label)2469for p in self.depotPaths]):2470 revisions[file["depotFile"]] =file["rev"]2471 change =int(file["change"])2472if change > newestChange:2473 newestChange = change24742475 self.labels[newestChange] = [output, revisions]24762477if self.verbose:2478print"Label changes:%s"% self.labels.keys()24792480# Import p4 labels as git tags. A direct mapping does not2481# exist, so assume that if all the files are at the same revision2482# then we can use that, or it's something more complicated we should2483# just ignore.2484defimportP4Labels(self, stream, p4Labels):2485if verbose:2486print"import p4 labels: "+' '.join(p4Labels)24872488 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2489 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2490iflen(validLabelRegexp) ==0:2491 validLabelRegexp = defaultLabelRegexp2492 m = re.compile(validLabelRegexp)24932494for name in p4Labels:2495 commitFound =False24962497if not m.match(name):2498if verbose:2499print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2500continue25012502if name in ignoredP4Labels:2503continue25042505 labelDetails =p4CmdList(['label',"-o", name])[0]25062507# get the most recent changelist for each file in this label2508 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2509for p in self.depotPaths])25102511if change.has_key('change'):2512# find the corresponding git commit; take the oldest commit2513 changelist =int(change['change'])2514 gitCommit =read_pipe(["git","rev-list","--max-count=1",2515"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2516iflen(gitCommit) ==0:2517print"could not find git commit for changelist%d"% changelist2518else:2519 gitCommit = gitCommit.strip()2520 commitFound =True2521# Convert from p4 time format2522try:2523 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2524exceptValueError:2525print"Could not convert label time%s"% labelDetails['Update']2526 tmwhen =125272528 when =int(time.mktime(tmwhen))2529 self.streamTag(stream, name, labelDetails, gitCommit, when)2530if verbose:2531print"p4 label%smapped to git commit%s"% (name, gitCommit)2532else:2533if verbose:2534print"Label%shas no changelists - possibly deleted?"% name25352536if not commitFound:2537# We can't import this label; don't try again as it will get very2538# expensive repeatedly fetching all the files for labels that will2539# never be imported. If the label is moved in the future, the2540# ignore will need to be removed manually.2541system(["git","config","--add","git-p4.ignoredP4Labels", name])25422543defguessProjectName(self):2544for p in self.depotPaths:2545if p.endswith("/"):2546 p = p[:-1]2547 p = p[p.strip().rfind("/") +1:]2548if not p.endswith("/"):2549 p +="/"2550return p25512552defgetBranchMapping(self):2553 lostAndFoundBranches =set()25542555 user =gitConfig("git-p4.branchUser")2556iflen(user) >0:2557 command ="branches -u%s"% user2558else:2559 command ="branches"25602561for info inp4CmdList(command):2562 details =p4Cmd(["branch","-o", info["branch"]])2563 viewIdx =02564while details.has_key("View%s"% viewIdx):2565 paths = details["View%s"% viewIdx].split(" ")2566 viewIdx = viewIdx +12567# require standard //depot/foo/... //depot/bar/... mapping2568iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2569continue2570 source = paths[0]2571 destination = paths[1]2572## HACK2573ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2574 source = source[len(self.depotPaths[0]):-4]2575 destination = destination[len(self.depotPaths[0]):-4]25762577if destination in self.knownBranches:2578if not self.silent:2579print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2580print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2581continue25822583 self.knownBranches[destination] = source25842585 lostAndFoundBranches.discard(destination)25862587if source not in self.knownBranches:2588 lostAndFoundBranches.add(source)25892590# Perforce does not strictly require branches to be defined, so we also2591# check git config for a branch list.2592#2593# Example of branch definition in git config file:2594# [git-p4]2595# branchList=main:branchA2596# branchList=main:branchB2597# branchList=branchA:branchC2598 configBranches =gitConfigList("git-p4.branchList")2599for branch in configBranches:2600if branch:2601(source, destination) = branch.split(":")2602 self.knownBranches[destination] = source26032604 lostAndFoundBranches.discard(destination)26052606if source not in self.knownBranches:2607 lostAndFoundBranches.add(source)260826092610for branch in lostAndFoundBranches:2611 self.knownBranches[branch] = branch26122613defgetBranchMappingFromGitBranches(self):2614 branches =p4BranchesInGit(self.importIntoRemotes)2615for branch in branches.keys():2616if branch =="master":2617 branch ="main"2618else:2619 branch = branch[len(self.projectName):]2620 self.knownBranches[branch] = branch26212622defupdateOptionDict(self, d):2623 option_keys = {}2624if self.keepRepoPath:2625 option_keys['keepRepoPath'] =126262627 d["options"] =' '.join(sorted(option_keys.keys()))26282629defreadOptions(self, d):2630 self.keepRepoPath = (d.has_key('options')2631and('keepRepoPath'in d['options']))26322633defgitRefForBranch(self, branch):2634if branch =="main":2635return self.refPrefix +"master"26362637iflen(branch) <=0:2638return branch26392640return self.refPrefix + self.projectName + branch26412642defgitCommitByP4Change(self, ref, change):2643if self.verbose:2644print"looking in ref "+ ref +" for change%susing bisect..."% change26452646 earliestCommit =""2647 latestCommit =parseRevision(ref)26482649while True:2650if self.verbose:2651print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2652 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2653iflen(next) ==0:2654if self.verbose:2655print"argh"2656return""2657 log =extractLogMessageFromGitCommit(next)2658 settings =extractSettingsGitLog(log)2659 currentChange =int(settings['change'])2660if self.verbose:2661print"current change%s"% currentChange26622663if currentChange == change:2664if self.verbose:2665print"found%s"% next2666return next26672668if currentChange < change:2669 earliestCommit ="^%s"% next2670else:2671 latestCommit ="%s"% next26722673return""26742675defimportNewBranch(self, branch, maxChange):2676# make fast-import flush all changes to disk and update the refs using the checkpoint2677# command so that we can try to find the branch parent in the git history2678 self.gitStream.write("checkpoint\n\n");2679 self.gitStream.flush();2680 branchPrefix = self.depotPaths[0] + branch +"/"2681range="@1,%s"% maxChange2682#print "prefix" + branchPrefix2683 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2684iflen(changes) <=0:2685return False2686 firstChange = changes[0]2687#print "first change in branch: %s" % firstChange2688 sourceBranch = self.knownBranches[branch]2689 sourceDepotPath = self.depotPaths[0] + sourceBranch2690 sourceRef = self.gitRefForBranch(sourceBranch)2691#print "source " + sourceBranch26922693 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2694#print "branch parent: %s" % branchParentChange2695 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2696iflen(gitParent) >0:2697 self.initialParents[self.gitRefForBranch(branch)] = gitParent2698#print "parent git commit: %s" % gitParent26992700 self.importChanges(changes)2701return True27022703defsearchParent(self, parent, branch, target):2704 parentFound =False2705for blob inread_pipe_lines(["git","rev-list","--reverse",2706"--no-merges", parent]):2707 blob = blob.strip()2708iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2709 parentFound =True2710if self.verbose:2711print"Found parent of%sin commit%s"% (branch, blob)2712break2713if parentFound:2714return blob2715else:2716return None27172718defimportChanges(self, changes):2719 cnt =12720for change in changes:2721 description =p4_describe(change)2722 self.updateOptionDict(description)27232724if not self.silent:2725 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2726 sys.stdout.flush()2727 cnt = cnt +127282729try:2730if self.detectBranches:2731 branches = self.splitFilesIntoBranches(description)2732for branch in branches.keys():2733## HACK --hwn2734 branchPrefix = self.depotPaths[0] + branch +"/"2735 self.branchPrefixes = [ branchPrefix ]27362737 parent =""27382739 filesForCommit = branches[branch]27402741if self.verbose:2742print"branch is%s"% branch27432744 self.updatedBranches.add(branch)27452746if branch not in self.createdBranches:2747 self.createdBranches.add(branch)2748 parent = self.knownBranches[branch]2749if parent == branch:2750 parent =""2751else:2752 fullBranch = self.projectName + branch2753if fullBranch not in self.p4BranchesInGit:2754if not self.silent:2755print("\nImporting new branch%s"% fullBranch);2756if self.importNewBranch(branch, change -1):2757 parent =""2758 self.p4BranchesInGit.append(fullBranch)2759if not self.silent:2760print("\nResuming with change%s"% change);27612762if self.verbose:2763print"parent determined through known branches:%s"% parent27642765 branch = self.gitRefForBranch(branch)2766 parent = self.gitRefForBranch(parent)27672768if self.verbose:2769print"looking for initial parent for%s; current parent is%s"% (branch, parent)27702771iflen(parent) ==0and branch in self.initialParents:2772 parent = self.initialParents[branch]2773del self.initialParents[branch]27742775 blob =None2776iflen(parent) >0:2777 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2778if self.verbose:2779print"Creating temporary branch: "+ tempBranch2780 self.commit(description, filesForCommit, tempBranch)2781 self.tempBranches.append(tempBranch)2782 self.checkpoint()2783 blob = self.searchParent(parent, branch, tempBranch)2784if blob:2785 self.commit(description, filesForCommit, branch, blob)2786else:2787if self.verbose:2788print"Parent of%snot found. Committing into head of%s"% (branch, parent)2789 self.commit(description, filesForCommit, branch, parent)2790else:2791 files = self.extractFilesFromCommit(description)2792 self.commit(description, files, self.branch,2793 self.initialParent)2794# only needed once, to connect to the previous commit2795 self.initialParent =""2796exceptIOError:2797print self.gitError.read()2798 sys.exit(1)27992800defimportHeadRevision(self, revision):2801print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)28022803 details = {}2804 details["user"] ="git perforce import user"2805 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2806% (' '.join(self.depotPaths), revision))2807 details["change"] = revision2808 newestRevision =028092810 fileCnt =02811 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28122813for info inp4CmdList(["files"] + fileArgs):28142815if'code'in info and info['code'] =='error':2816 sys.stderr.write("p4 returned an error:%s\n"2817% info['data'])2818if info['data'].find("must refer to client") >=0:2819 sys.stderr.write("This particular p4 error is misleading.\n")2820 sys.stderr.write("Perhaps the depot path was misspelled.\n");2821 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2822 sys.exit(1)2823if'p4ExitCode'in info:2824 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2825 sys.exit(1)282628272828 change =int(info["change"])2829if change > newestRevision:2830 newestRevision = change28312832if info["action"]in self.delete_actions:2833# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2834#fileCnt = fileCnt + 12835continue28362837for prop in["depotFile","rev","action","type"]:2838 details["%s%s"% (prop, fileCnt)] = info[prop]28392840 fileCnt = fileCnt +128412842 details["change"] = newestRevision28432844# Use time from top-most change so that all git p4 clones of2845# the same p4 repo have the same commit SHA1s.2846 res =p4_describe(newestRevision)2847 details["time"] = res["time"]28482849 self.updateOptionDict(details)2850try:2851 self.commit(details, self.extractFilesFromCommit(details), self.branch)2852exceptIOError:2853print"IO error with git fast-import. Is your git version recent enough?"2854print self.gitError.read()285528562857defrun(self, args):2858 self.depotPaths = []2859 self.changeRange =""2860 self.previousDepotPaths = []2861 self.hasOrigin =False28622863# map from branch depot path to parent branch2864 self.knownBranches = {}2865 self.initialParents = {}28662867if self.importIntoRemotes:2868 self.refPrefix ="refs/remotes/p4/"2869else:2870 self.refPrefix ="refs/heads/p4/"28712872if self.syncWithOrigin:2873 self.hasOrigin =originP4BranchesExist()2874if self.hasOrigin:2875if not self.silent:2876print'Syncing with origin first, using "git fetch origin"'2877system("git fetch origin")28782879 branch_arg_given =bool(self.branch)2880iflen(self.branch) ==0:2881 self.branch = self.refPrefix +"master"2882ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2883system("git update-ref%srefs/heads/p4"% self.branch)2884system("git branch -D p4")28852886# accept either the command-line option, or the configuration variable2887if self.useClientSpec:2888# will use this after clone to set the variable2889 self.useClientSpec_from_options =True2890else:2891ifgitConfigBool("git-p4.useclientspec"):2892 self.useClientSpec =True2893if self.useClientSpec:2894 self.clientSpecDirs =getClientSpec()28952896# TODO: should always look at previous commits,2897# merge with previous imports, if possible.2898if args == []:2899if self.hasOrigin:2900createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)29012902# branches holds mapping from branch name to sha12903 branches =p4BranchesInGit(self.importIntoRemotes)29042905# restrict to just this one, disabling detect-branches2906if branch_arg_given:2907 short = self.branch.split("/")[-1]2908if short in branches:2909 self.p4BranchesInGit = [ short ]2910else:2911 self.p4BranchesInGit = branches.keys()29122913iflen(self.p4BranchesInGit) >1:2914if not self.silent:2915print"Importing from/into multiple branches"2916 self.detectBranches =True2917for branch in branches.keys():2918 self.initialParents[self.refPrefix + branch] = \2919 branches[branch]29202921if self.verbose:2922print"branches:%s"% self.p4BranchesInGit29232924 p4Change =02925for branch in self.p4BranchesInGit:2926 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29272928 settings =extractSettingsGitLog(logMsg)29292930 self.readOptions(settings)2931if(settings.has_key('depot-paths')2932and settings.has_key('change')):2933 change =int(settings['change']) +12934 p4Change =max(p4Change, change)29352936 depotPaths =sorted(settings['depot-paths'])2937if self.previousDepotPaths == []:2938 self.previousDepotPaths = depotPaths2939else:2940 paths = []2941for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2942 prev_list = prev.split("/")2943 cur_list = cur.split("/")2944for i inrange(0,min(len(cur_list),len(prev_list))):2945if cur_list[i] <> prev_list[i]:2946 i = i -12947break29482949 paths.append("/".join(cur_list[:i +1]))29502951 self.previousDepotPaths = paths29522953if p4Change >0:2954 self.depotPaths =sorted(self.previousDepotPaths)2955 self.changeRange ="@%s,#head"% p4Change2956if not self.silent and not self.detectBranches:2957print"Performing incremental import into%sgit branch"% self.branch29582959# accept multiple ref name abbreviations:2960# refs/foo/bar/branch -> use it exactly2961# p4/branch -> prepend refs/remotes/ or refs/heads/2962# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2963if not self.branch.startswith("refs/"):2964if self.importIntoRemotes:2965 prepend ="refs/remotes/"2966else:2967 prepend ="refs/heads/"2968if not self.branch.startswith("p4/"):2969 prepend +="p4/"2970 self.branch = prepend + self.branch29712972iflen(args) ==0and self.depotPaths:2973if not self.silent:2974print"Depot paths:%s"%' '.join(self.depotPaths)2975else:2976if self.depotPaths and self.depotPaths != args:2977print("previous import used depot path%sand now%swas specified. "2978"This doesn't work!"% (' '.join(self.depotPaths),2979' '.join(args)))2980 sys.exit(1)29812982 self.depotPaths =sorted(args)29832984 revision =""2985 self.users = {}29862987# Make sure no revision specifiers are used when --changesfile2988# is specified.2989 bad_changesfile =False2990iflen(self.changesFile) >0:2991for p in self.depotPaths:2992if p.find("@") >=0or p.find("#") >=0:2993 bad_changesfile =True2994break2995if bad_changesfile:2996die("Option --changesfile is incompatible with revision specifiers")29972998 newPaths = []2999for p in self.depotPaths:3000if p.find("@") != -1:3001 atIdx = p.index("@")3002 self.changeRange = p[atIdx:]3003if self.changeRange =="@all":3004 self.changeRange =""3005elif','not in self.changeRange:3006 revision = self.changeRange3007 self.changeRange =""3008 p = p[:atIdx]3009elif p.find("#") != -1:3010 hashIdx = p.index("#")3011 revision = p[hashIdx:]3012 p = p[:hashIdx]3013elif self.previousDepotPaths == []:3014# pay attention to changesfile, if given, else import3015# the entire p4 tree at the head revision3016iflen(self.changesFile) ==0:3017 revision ="#head"30183019 p = re.sub("\.\.\.$","", p)3020if not p.endswith("/"):3021 p +="/"30223023 newPaths.append(p)30243025 self.depotPaths = newPaths30263027# --detect-branches may change this for each branch3028 self.branchPrefixes = self.depotPaths30293030 self.loadUserMapFromCache()3031 self.labels = {}3032if self.detectLabels:3033 self.getLabels();30343035if self.detectBranches:3036## FIXME - what's a P4 projectName ?3037 self.projectName = self.guessProjectName()30383039if self.hasOrigin:3040 self.getBranchMappingFromGitBranches()3041else:3042 self.getBranchMapping()3043if self.verbose:3044print"p4-git branches:%s"% self.p4BranchesInGit3045print"initial parents:%s"% self.initialParents3046for b in self.p4BranchesInGit:3047if b !="master":30483049## FIXME3050 b = b[len(self.projectName):]3051 self.createdBranches.add(b)30523053 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30543055 self.importProcess = subprocess.Popen(["git","fast-import"],3056 stdin=subprocess.PIPE,3057 stdout=subprocess.PIPE,3058 stderr=subprocess.PIPE);3059 self.gitOutput = self.importProcess.stdout3060 self.gitStream = self.importProcess.stdin3061 self.gitError = self.importProcess.stderr30623063if revision:3064 self.importHeadRevision(revision)3065else:3066 changes = []30673068iflen(self.changesFile) >0:3069 output =open(self.changesFile).readlines()3070 changeSet =set()3071for line in output:3072 changeSet.add(int(line))30733074for change in changeSet:3075 changes.append(change)30763077 changes.sort()3078else:3079# catch "git p4 sync" with no new branches, in a repo that3080# does not have any existing p4 branches3081iflen(args) ==0:3082if not self.p4BranchesInGit:3083die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30843085# The default branch is master, unless --branch is used to3086# specify something else. Make sure it exists, or complain3087# nicely about how to use --branch.3088if not self.detectBranches:3089if notbranch_exists(self.branch):3090if branch_arg_given:3091die("Error: branch%sdoes not exist."% self.branch)3092else:3093die("Error: no branch%s; perhaps specify one with --branch."%3094 self.branch)30953096if self.verbose:3097print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3098 self.changeRange)3099 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)31003101iflen(self.maxChanges) >0:3102 changes = changes[:min(int(self.maxChanges),len(changes))]31033104iflen(changes) ==0:3105if not self.silent:3106print"No changes to import!"3107else:3108if not self.silent and not self.detectBranches:3109print"Import destination:%s"% self.branch31103111 self.updatedBranches =set()31123113if not self.detectBranches:3114if args:3115# start a new branch3116 self.initialParent =""3117else:3118# build on a previous revision3119 self.initialParent =parseRevision(self.branch)31203121 self.importChanges(changes)31223123if not self.silent:3124print""3125iflen(self.updatedBranches) >0:3126 sys.stdout.write("Updated branches: ")3127for b in self.updatedBranches:3128 sys.stdout.write("%s"% b)3129 sys.stdout.write("\n")31303131ifgitConfigBool("git-p4.importLabels"):3132 self.importLabels =True31333134if self.importLabels:3135 p4Labels =getP4Labels(self.depotPaths)3136 gitTags =getGitTags()31373138 missingP4Labels = p4Labels - gitTags3139 self.importP4Labels(self.gitStream, missingP4Labels)31403141 self.gitStream.close()3142if self.importProcess.wait() !=0:3143die("fast-import failed:%s"% self.gitError.read())3144 self.gitOutput.close()3145 self.gitError.close()31463147# Cleanup temporary branches created during import3148if self.tempBranches != []:3149for branch in self.tempBranches:3150read_pipe("git update-ref -d%s"% branch)3151 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31523153# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3154# a convenient shortcut refname "p4".3155if self.importIntoRemotes:3156 head_ref = self.refPrefix +"HEAD"3157if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3158system(["git","symbolic-ref", head_ref, self.branch])31593160return True31613162classP4Rebase(Command):3163def__init__(self):3164 Command.__init__(self)3165 self.options = [3166 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3167]3168 self.importLabels =False3169 self.description = ("Fetches the latest revision from perforce and "3170+"rebases the current work (branch) against it")31713172defrun(self, args):3173 sync =P4Sync()3174 sync.importLabels = self.importLabels3175 sync.run([])31763177return self.rebase()31783179defrebase(self):3180if os.system("git update-index --refresh") !=0:3181die("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.");3182iflen(read_pipe("git diff-index HEAD --")) >0:3183die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");31843185[upstream, settings] =findUpstreamBranchPoint()3186iflen(upstream) ==0:3187die("Cannot find upstream branchpoint for rebase")31883189# the branchpoint may be p4/foo~3, so strip off the parent3190 upstream = re.sub("~[0-9]+$","", upstream)31913192print"Rebasing the current branch onto%s"% upstream3193 oldHead =read_pipe("git rev-parse HEAD").strip()3194system("git rebase%s"% upstream)3195system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3196return True31973198classP4Clone(P4Sync):3199def__init__(self):3200 P4Sync.__init__(self)3201 self.description ="Creates a new git repository and imports from Perforce into it"3202 self.usage ="usage: %prog [options] //depot/path[@revRange]"3203 self.options += [3204 optparse.make_option("--destination", dest="cloneDestination",3205 action='store', default=None,3206help="where to leave result of the clone"),3207 optparse.make_option("--bare", dest="cloneBare",3208 action="store_true", default=False),3209]3210 self.cloneDestination =None3211 self.needsGit =False3212 self.cloneBare =False32133214defdefaultDestination(self, args):3215## TODO: use common prefix of args?3216 depotPath = args[0]3217 depotDir = re.sub("(@[^@]*)$","", depotPath)3218 depotDir = re.sub("(#[^#]*)$","", depotDir)3219 depotDir = re.sub(r"\.\.\.$","", depotDir)3220 depotDir = re.sub(r"/$","", depotDir)3221return os.path.split(depotDir)[1]32223223defrun(self, args):3224iflen(args) <1:3225return False32263227if self.keepRepoPath and not self.cloneDestination:3228 sys.stderr.write("Must specify destination for --keep-path\n")3229 sys.exit(1)32303231 depotPaths = args32323233if not self.cloneDestination andlen(depotPaths) >1:3234 self.cloneDestination = depotPaths[-1]3235 depotPaths = depotPaths[:-1]32363237 self.cloneExclude = ["/"+p for p in self.cloneExclude]3238for p in depotPaths:3239if not p.startswith("//"):3240 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3241return False32423243if not self.cloneDestination:3244 self.cloneDestination = self.defaultDestination(args)32453246print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32473248if not os.path.exists(self.cloneDestination):3249 os.makedirs(self.cloneDestination)3250chdir(self.cloneDestination)32513252 init_cmd = ["git","init"]3253if self.cloneBare:3254 init_cmd.append("--bare")3255 retcode = subprocess.call(init_cmd)3256if retcode:3257raiseCalledProcessError(retcode, init_cmd)32583259if not P4Sync.run(self, depotPaths):3260return False32613262# create a master branch and check out a work tree3263ifgitBranchExists(self.branch):3264system(["git","branch","master", self.branch ])3265if not self.cloneBare:3266system(["git","checkout","-f"])3267else:3268print'Not checking out any branch, use ' \3269'"git checkout -q -b master <branch>"'32703271# auto-set this variable if invoked with --use-client-spec3272if self.useClientSpec_from_options:3273system("git config --bool git-p4.useclientspec true")32743275return True32763277classP4Branches(Command):3278def__init__(self):3279 Command.__init__(self)3280 self.options = [ ]3281 self.description = ("Shows the git branches that hold imports and their "3282+"corresponding perforce depot paths")3283 self.verbose =False32843285defrun(self, args):3286iforiginP4BranchesExist():3287createOrUpdateBranchesFromOrigin()32883289 cmdline ="git rev-parse --symbolic "3290 cmdline +=" --remotes"32913292for line inread_pipe_lines(cmdline):3293 line = line.strip()32943295if not line.startswith('p4/')or line =="p4/HEAD":3296continue3297 branch = line32983299 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3300 settings =extractSettingsGitLog(log)33013302print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3303return True33043305classHelpFormatter(optparse.IndentedHelpFormatter):3306def__init__(self):3307 optparse.IndentedHelpFormatter.__init__(self)33083309defformat_description(self, description):3310if description:3311return description +"\n"3312else:3313return""33143315defprintUsage(commands):3316print"usage:%s<command> [options]"% sys.argv[0]3317print""3318print"valid commands:%s"%", ".join(commands)3319print""3320print"Try%s<command> --help for command specific help."% sys.argv[0]3321print""33223323commands = {3324"debug": P4Debug,3325"submit": P4Submit,3326"commit": P4Submit,3327"sync": P4Sync,3328"rebase": P4Rebase,3329"clone": P4Clone,3330"rollback": P4RollBack,3331"branches": P4Branches3332}333333343335defmain():3336iflen(sys.argv[1:]) ==0:3337printUsage(commands.keys())3338 sys.exit(2)33393340 cmdName = sys.argv[1]3341try:3342 klass = commands[cmdName]3343 cmd =klass()3344exceptKeyError:3345print"unknown command%s"% cmdName3346print""3347printUsage(commands.keys())3348 sys.exit(2)33493350 options = cmd.options3351 cmd.gitdir = os.environ.get("GIT_DIR",None)33523353 args = sys.argv[2:]33543355 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3356if cmd.needsGit:3357 options.append(optparse.make_option("--git-dir", dest="gitdir"))33583359 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3360 options,3361 description = cmd.description,3362 formatter =HelpFormatter())33633364(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3365global verbose3366 verbose = cmd.verbose3367if cmd.needsGit:3368if cmd.gitdir ==None:3369 cmd.gitdir = os.path.abspath(".git")3370if notisValidGitDir(cmd.gitdir):3371 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3372if os.path.exists(cmd.gitdir):3373 cdup =read_pipe("git rev-parse --show-cdup").strip()3374iflen(cdup) >0:3375chdir(cdup);33763377if notisValidGitDir(cmd.gitdir):3378ifisValidGitDir(cmd.gitdir +"/.git"):3379 cmd.gitdir +="/.git"3380else:3381die("fatal: cannot locate git repository at%s"% cmd.gitdir)33823383 os.environ["GIT_DIR"] = cmd.gitdir33843385if not cmd.run(args):3386 parser.print_help()3387 sys.exit(2)338833893390if __name__ =='__main__':3391main()