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) 641if _gitConfig[key] == ['']: 642 _gitConfig[key] = [] 643return _gitConfig[key] 644 645defp4BranchesInGit(branchesAreInRemotes=True): 646"""Find all the branches whose names start with "p4/", looking 647 in remotes or heads as specified by the argument. Return 648 a dictionary of{ branch: revision }for each one found. 649 The branch names are the short names, without any 650 "p4/" prefix.""" 651 652 branches = {} 653 654 cmdline ="git rev-parse --symbolic " 655if branchesAreInRemotes: 656 cmdline +="--remotes" 657else: 658 cmdline +="--branches" 659 660for line inread_pipe_lines(cmdline): 661 line = line.strip() 662 663# only import to p4/ 664if not line.startswith('p4/'): 665continue 666# special symbolic ref to p4/master 667if line =="p4/HEAD": 668continue 669 670# strip off p4/ prefix 671 branch = line[len("p4/"):] 672 673 branches[branch] =parseRevision(line) 674 675return branches 676 677defbranch_exists(branch): 678"""Make sure that the given ref name really exists.""" 679 680 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 681 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 682 out, _ = p.communicate() 683if p.returncode: 684return False 685# expect exactly one line of output: the branch name 686return out.rstrip() == branch 687 688deffindUpstreamBranchPoint(head ="HEAD"): 689 branches =p4BranchesInGit() 690# map from depot-path to branch name 691 branchByDepotPath = {} 692for branch in branches.keys(): 693 tip = branches[branch] 694 log =extractLogMessageFromGitCommit(tip) 695 settings =extractSettingsGitLog(log) 696if settings.has_key("depot-paths"): 697 paths =",".join(settings["depot-paths"]) 698 branchByDepotPath[paths] ="remotes/p4/"+ branch 699 700 settings =None 701 parent =0 702while parent <65535: 703 commit = head +"~%s"% parent 704 log =extractLogMessageFromGitCommit(commit) 705 settings =extractSettingsGitLog(log) 706if settings.has_key("depot-paths"): 707 paths =",".join(settings["depot-paths"]) 708if branchByDepotPath.has_key(paths): 709return[branchByDepotPath[paths], settings] 710 711 parent = parent +1 712 713return["", settings] 714 715defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 716if not silent: 717print("Creating/updating branch(es) in%sbased on origin branch(es)" 718% localRefPrefix) 719 720 originPrefix ="origin/p4/" 721 722for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 723 line = line.strip() 724if(not line.startswith(originPrefix))or line.endswith("HEAD"): 725continue 726 727 headName = line[len(originPrefix):] 728 remoteHead = localRefPrefix + headName 729 originHead = line 730 731 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 732if(not original.has_key('depot-paths') 733or not original.has_key('change')): 734continue 735 736 update =False 737if notgitBranchExists(remoteHead): 738if verbose: 739print"creating%s"% remoteHead 740 update =True 741else: 742 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 743if settings.has_key('change') >0: 744if settings['depot-paths'] == original['depot-paths']: 745 originP4Change =int(original['change']) 746 p4Change =int(settings['change']) 747if originP4Change > p4Change: 748print("%s(%s) is newer than%s(%s). " 749"Updating p4 branch from origin." 750% (originHead, originP4Change, 751 remoteHead, p4Change)) 752 update =True 753else: 754print("Ignoring:%swas imported from%swhile " 755"%swas imported from%s" 756% (originHead,','.join(original['depot-paths']), 757 remoteHead,','.join(settings['depot-paths']))) 758 759if update: 760system("git update-ref%s %s"% (remoteHead, originHead)) 761 762deforiginP4BranchesExist(): 763returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 764 765 766defp4ParseNumericChangeRange(parts): 767 changeStart =int(parts[0][1:]) 768if parts[1] =='#head': 769 changeEnd =p4_last_change() 770else: 771 changeEnd =int(parts[1]) 772 773return(changeStart, changeEnd) 774 775defchooseBlockSize(blockSize): 776if blockSize: 777return blockSize 778else: 779return defaultBlockSize 780 781defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 782assert depotPaths 783 784# Parse the change range into start and end. Try to find integer 785# revision ranges as these can be broken up into blocks to avoid 786# hitting server-side limits (maxrows, maxscanresults). But if 787# that doesn't work, fall back to using the raw revision specifier 788# strings, without using block mode. 789 790if changeRange is None or changeRange =='': 791 changeStart =1 792 changeEnd =p4_last_change() 793 block_size =chooseBlockSize(requestedBlockSize) 794else: 795 parts = changeRange.split(',') 796assertlen(parts) ==2 797try: 798(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 799 block_size =chooseBlockSize(requestedBlockSize) 800except: 801 changeStart = parts[0][1:] 802 changeEnd = parts[1] 803if requestedBlockSize: 804die("cannot use --changes-block-size with non-numeric revisions") 805 block_size =None 806 807# Accumulate change numbers in a dictionary to avoid duplicates 808 changes = {} 809 810for p in depotPaths: 811# Retrieve changes a block at a time, to prevent running 812# into a MaxResults/MaxScanRows error from the server. 813 814while True: 815 cmd = ['changes'] 816 817if block_size: 818 end =min(changeEnd, changeStart + block_size) 819 revisionRange ="%d,%d"% (changeStart, end) 820else: 821 revisionRange ="%s,%s"% (changeStart, changeEnd) 822 823 cmd += ["%s...@%s"% (p, revisionRange)] 824 825for line inp4_read_pipe_lines(cmd): 826 changeNum =int(line.split(" ")[1]) 827 changes[changeNum] =True 828 829if not block_size: 830break 831 832if end >= changeEnd: 833break 834 835 changeStart = end +1 836 837 changelist = changes.keys() 838 changelist.sort() 839return changelist 840 841defp4PathStartsWith(path, prefix): 842# This method tries to remedy a potential mixed-case issue: 843# 844# If UserA adds //depot/DirA/file1 845# and UserB adds //depot/dira/file2 846# 847# we may or may not have a problem. If you have core.ignorecase=true, 848# we treat DirA and dira as the same directory 849ifgitConfigBool("core.ignorecase"): 850return path.lower().startswith(prefix.lower()) 851return path.startswith(prefix) 852 853defgetClientSpec(): 854"""Look at the p4 client spec, create a View() object that contains 855 all the mappings, and return it.""" 856 857 specList =p4CmdList("client -o") 858iflen(specList) !=1: 859die('Output from "client -o" is%dlines, expecting 1'% 860len(specList)) 861 862# dictionary of all client parameters 863 entry = specList[0] 864 865# the //client/ name 866 client_name = entry["Client"] 867 868# just the keys that start with "View" 869 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 870 871# hold this new View 872 view =View(client_name) 873 874# append the lines, in order, to the view 875for view_num inrange(len(view_keys)): 876 k ="View%d"% view_num 877if k not in view_keys: 878die("Expected view key%smissing"% k) 879 view.append(entry[k]) 880 881return view 882 883defgetClientRoot(): 884"""Grab the client directory.""" 885 886 output =p4CmdList("client -o") 887iflen(output) !=1: 888die('Output from "client -o" is%dlines, expecting 1'%len(output)) 889 890 entry = output[0] 891if"Root"not in entry: 892die('Client has no "Root"') 893 894return entry["Root"] 895 896# 897# P4 wildcards are not allowed in filenames. P4 complains 898# if you simply add them, but you can force it with "-f", in 899# which case it translates them into %xx encoding internally. 900# 901defwildcard_decode(path): 902# Search for and fix just these four characters. Do % last so 903# that fixing it does not inadvertently create new %-escapes. 904# Cannot have * in a filename in windows; untested as to 905# what p4 would do in such a case. 906if not platform.system() =="Windows": 907 path = path.replace("%2A","*") 908 path = path.replace("%23","#") \ 909.replace("%40","@") \ 910.replace("%25","%") 911return path 912 913defwildcard_encode(path): 914# do % first to avoid double-encoding the %s introduced here 915 path = path.replace("%","%25") \ 916.replace("*","%2A") \ 917.replace("#","%23") \ 918.replace("@","%40") 919return path 920 921defwildcard_present(path): 922 m = re.search("[*#@%]", path) 923return m is not None 924 925class Command: 926def__init__(self): 927 self.usage ="usage: %prog [options]" 928 self.needsGit =True 929 self.verbose =False 930 931class P4UserMap: 932def__init__(self): 933 self.userMapFromPerforceServer =False 934 self.myP4UserId =None 935 936defp4UserId(self): 937if self.myP4UserId: 938return self.myP4UserId 939 940 results =p4CmdList("user -o") 941for r in results: 942if r.has_key('User'): 943 self.myP4UserId = r['User'] 944return r['User'] 945die("Could not find your p4 user id") 946 947defp4UserIsMe(self, p4User): 948# return True if the given p4 user is actually me 949 me = self.p4UserId() 950if not p4User or p4User != me: 951return False 952else: 953return True 954 955defgetUserCacheFilename(self): 956 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 957return home +"/.gitp4-usercache.txt" 958 959defgetUserMapFromPerforceServer(self): 960if self.userMapFromPerforceServer: 961return 962 self.users = {} 963 self.emails = {} 964 965for output inp4CmdList("users"): 966if not output.has_key("User"): 967continue 968 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 969 self.emails[output["Email"]] = output["User"] 970 971 972 s ='' 973for(key, val)in self.users.items(): 974 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 975 976open(self.getUserCacheFilename(),"wb").write(s) 977 self.userMapFromPerforceServer =True 978 979defloadUserMapFromCache(self): 980 self.users = {} 981 self.userMapFromPerforceServer =False 982try: 983 cache =open(self.getUserCacheFilename(),"rb") 984 lines = cache.readlines() 985 cache.close() 986for line in lines: 987 entry = line.strip().split("\t") 988 self.users[entry[0]] = entry[1] 989exceptIOError: 990 self.getUserMapFromPerforceServer() 991 992classP4Debug(Command): 993def__init__(self): 994 Command.__init__(self) 995 self.options = [] 996 self.description ="A tool to debug the output of p4 -G." 997 self.needsGit =False 998 999defrun(self, args):1000 j =01001for output inp4CmdList(args):1002print'Element:%d'% j1003 j +=11004print output1005return True10061007classP4RollBack(Command):1008def__init__(self):1009 Command.__init__(self)1010 self.options = [1011 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1012]1013 self.description ="A tool to debug the multi-branch import. Don't use :)"1014 self.rollbackLocalBranches =False10151016defrun(self, args):1017iflen(args) !=1:1018return False1019 maxChange =int(args[0])10201021if"p4ExitCode"inp4Cmd("changes -m 1"):1022die("Problems executing p4");10231024if self.rollbackLocalBranches:1025 refPrefix ="refs/heads/"1026 lines =read_pipe_lines("git rev-parse --symbolic --branches")1027else:1028 refPrefix ="refs/remotes/"1029 lines =read_pipe_lines("git rev-parse --symbolic --remotes")10301031for line in lines:1032if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1033 line = line.strip()1034 ref = refPrefix + line1035 log =extractLogMessageFromGitCommit(ref)1036 settings =extractSettingsGitLog(log)10371038 depotPaths = settings['depot-paths']1039 change = settings['change']10401041 changed =False10421043iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1044for p in depotPaths]))) ==0:1045print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1046system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1047continue10481049while change andint(change) > maxChange:1050 changed =True1051if self.verbose:1052print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1053system("git update-ref%s\"%s^\""% (ref, ref))1054 log =extractLogMessageFromGitCommit(ref)1055 settings =extractSettingsGitLog(log)105610571058 depotPaths = settings['depot-paths']1059 change = settings['change']10601061if changed:1062print"%srewound to%s"% (ref, change)10631064return True10651066classP4Submit(Command, P4UserMap):10671068 conflict_behavior_choices = ("ask","skip","quit")10691070def__init__(self):1071 Command.__init__(self)1072 P4UserMap.__init__(self)1073 self.options = [1074 optparse.make_option("--origin", dest="origin"),1075 optparse.make_option("-M", dest="detectRenames", action="store_true"),1076# preserve the user, requires relevant p4 permissions1077 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1078 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1079 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1080 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1081 optparse.make_option("--conflict", dest="conflict_behavior",1082 choices=self.conflict_behavior_choices),1083 optparse.make_option("--branch", dest="branch"),1084]1085 self.description ="Submit changes from git to the perforce depot."1086 self.usage +=" [name of git branch to submit into perforce depot]"1087 self.origin =""1088 self.detectRenames =False1089 self.preserveUser =gitConfigBool("git-p4.preserveUser")1090 self.dry_run =False1091 self.prepare_p4_only =False1092 self.conflict_behavior =None1093 self.isWindows = (platform.system() =="Windows")1094 self.exportLabels =False1095 self.p4HasMoveCommand =p4_has_move_command()1096 self.branch =None10971098defcheck(self):1099iflen(p4CmdList("opened ...")) >0:1100die("You have files opened with perforce! Close them before starting the sync.")11011102defseparate_jobs_from_description(self, message):1103"""Extract and return a possible Jobs field in the commit1104 message. It goes into a separate section in the p4 change1105 specification.11061107 A jobs line starts with "Jobs:" and looks like a new field1108 in a form. Values are white-space separated on the same1109 line or on following lines that start with a tab.11101111 This does not parse and extract the full git commit message1112 like a p4 form. It just sees the Jobs: line as a marker1113 to pass everything from then on directly into the p4 form,1114 but outside the description section.11151116 Return a tuple (stripped log message, jobs string)."""11171118 m = re.search(r'^Jobs:', message, re.MULTILINE)1119if m is None:1120return(message,None)11211122 jobtext = message[m.start():]1123 stripped_message = message[:m.start()].rstrip()1124return(stripped_message, jobtext)11251126defprepareLogMessage(self, template, message, jobs):1127"""Edits the template returned from "p4 change -o" to insert1128 the message in the Description field, and the jobs text in1129 the Jobs field."""1130 result =""11311132 inDescriptionSection =False11331134for line in template.split("\n"):1135if line.startswith("#"):1136 result += line +"\n"1137continue11381139if inDescriptionSection:1140if line.startswith("Files:")or line.startswith("Jobs:"):1141 inDescriptionSection =False1142# insert Jobs section1143if jobs:1144 result += jobs +"\n"1145else:1146continue1147else:1148if line.startswith("Description:"):1149 inDescriptionSection =True1150 line +="\n"1151for messageLine in message.split("\n"):1152 line +="\t"+ messageLine +"\n"11531154 result += line +"\n"11551156return result11571158defpatchRCSKeywords(self,file, pattern):1159# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1160(handle, outFileName) = tempfile.mkstemp(dir='.')1161try:1162 outFile = os.fdopen(handle,"w+")1163 inFile =open(file,"r")1164 regexp = re.compile(pattern, re.VERBOSE)1165for line in inFile.readlines():1166 line = regexp.sub(r'$\1$', line)1167 outFile.write(line)1168 inFile.close()1169 outFile.close()1170# Forcibly overwrite the original file1171 os.unlink(file)1172 shutil.move(outFileName,file)1173except:1174# cleanup our temporary file1175 os.unlink(outFileName)1176print"Failed to strip RCS keywords in%s"%file1177raise11781179print"Patched up RCS keywords in%s"%file11801181defp4UserForCommit(self,id):1182# Return the tuple (perforce user,git email) for a given git commit id1183 self.getUserMapFromPerforceServer()1184 gitEmail =read_pipe(["git","log","--max-count=1",1185"--format=%ae",id])1186 gitEmail = gitEmail.strip()1187if not self.emails.has_key(gitEmail):1188return(None,gitEmail)1189else:1190return(self.emails[gitEmail],gitEmail)11911192defcheckValidP4Users(self,commits):1193# check if any git authors cannot be mapped to p4 users1194foridin commits:1195(user,email) = self.p4UserForCommit(id)1196if not user:1197 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1198ifgitConfigBool("git-p4.allowMissingP4Users"):1199print"%s"% msg1200else:1201die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)12021203deflastP4Changelist(self):1204# Get back the last changelist number submitted in this client spec. This1205# then gets used to patch up the username in the change. If the same1206# client spec is being used by multiple processes then this might go1207# wrong.1208 results =p4CmdList("client -o")# find the current client1209 client =None1210for r in results:1211if r.has_key('Client'):1212 client = r['Client']1213break1214if not client:1215die("could not get client spec")1216 results =p4CmdList(["changes","-c", client,"-m","1"])1217for r in results:1218if r.has_key('change'):1219return r['change']1220die("Could not get changelist number for last submit - cannot patch up user details")12211222defmodifyChangelistUser(self, changelist, newUser):1223# fixup the user field of a changelist after it has been submitted.1224 changes =p4CmdList("change -o%s"% changelist)1225iflen(changes) !=1:1226die("Bad output from p4 change modifying%sto user%s"%1227(changelist, newUser))12281229 c = changes[0]1230if c['User'] == newUser:return# nothing to do1231 c['User'] = newUser1232input= marshal.dumps(c)12331234 result =p4CmdList("change -f -i", stdin=input)1235for r in result:1236if r.has_key('code'):1237if r['code'] =='error':1238die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1239if r.has_key('data'):1240print("Updated user field for changelist%sto%s"% (changelist, newUser))1241return1242die("Could not modify user field of changelist%sto%s"% (changelist, newUser))12431244defcanChangeChangelists(self):1245# check to see if we have p4 admin or super-user permissions, either of1246# which are required to modify changelists.1247 results =p4CmdList(["protects", self.depotPath])1248for r in results:1249if r.has_key('perm'):1250if r['perm'] =='admin':1251return11252if r['perm'] =='super':1253return11254return012551256defprepareSubmitTemplate(self):1257"""Run "p4 change -o" to grab a change specification template.1258 This does not use "p4 -G", as it is nice to keep the submission1259 template in original order, since a human might edit it.12601261 Remove lines in the Files section that show changes to files1262 outside the depot path we're committing into."""12631264 template =""1265 inFilesSection =False1266for line inp4_read_pipe_lines(['change','-o']):1267if line.endswith("\r\n"):1268 line = line[:-2] +"\n"1269if inFilesSection:1270if line.startswith("\t"):1271# path starts and ends with a tab1272 path = line[1:]1273 lastTab = path.rfind("\t")1274if lastTab != -1:1275 path = path[:lastTab]1276if notp4PathStartsWith(path, self.depotPath):1277continue1278else:1279 inFilesSection =False1280else:1281if line.startswith("Files:"):1282 inFilesSection =True12831284 template += line12851286return template12871288defedit_template(self, template_file):1289"""Invoke the editor to let the user change the submission1290 message. Return true if okay to continue with the submit."""12911292# if configured to skip the editing part, just submit1293ifgitConfigBool("git-p4.skipSubmitEdit"):1294return True12951296# look at the modification time, to check later if the user saved1297# the file1298 mtime = os.stat(template_file).st_mtime12991300# invoke the editor1301if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1302 editor = os.environ.get("P4EDITOR")1303else:1304 editor =read_pipe("git var GIT_EDITOR").strip()1305system(["sh","-c", ('%s"$@"'% editor), editor, template_file])13061307# If the file was not saved, prompt to see if this patch should1308# be skipped. But skip this verification step if configured so.1309ifgitConfigBool("git-p4.skipSubmitEditCheck"):1310return True13111312# modification time updated means user saved the file1313if os.stat(template_file).st_mtime > mtime:1314return True13151316while True:1317 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1318if response =='y':1319return True1320if response =='n':1321return False13221323defget_diff_description(self, editedFiles, filesToAdd):1324# diff1325if os.environ.has_key("P4DIFF"):1326del(os.environ["P4DIFF"])1327 diff =""1328for editedFile in editedFiles:1329 diff +=p4_read_pipe(['diff','-du',1330wildcard_encode(editedFile)])13311332# new file diff1333 newdiff =""1334for newFile in filesToAdd:1335 newdiff +="==== new file ====\n"1336 newdiff +="--- /dev/null\n"1337 newdiff +="+++%s\n"% newFile1338 f =open(newFile,"r")1339for line in f.readlines():1340 newdiff +="+"+ line1341 f.close()13421343return(diff + newdiff).replace('\r\n','\n')13441345defapplyCommit(self,id):1346"""Apply one commit, return True if it succeeded."""13471348print"Applying",read_pipe(["git","show","-s",1349"--format=format:%h%s",id])13501351(p4User, gitEmail) = self.p4UserForCommit(id)13521353 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1354 filesToAdd =set()1355 filesToDelete =set()1356 editedFiles =set()1357 pureRenameCopy =set()1358 filesToChangeExecBit = {}13591360for line in diff:1361 diff =parseDiffTreeEntry(line)1362 modifier = diff['status']1363 path = diff['src']1364if modifier =="M":1365p4_edit(path)1366ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1367 filesToChangeExecBit[path] = diff['dst_mode']1368 editedFiles.add(path)1369elif modifier =="A":1370 filesToAdd.add(path)1371 filesToChangeExecBit[path] = diff['dst_mode']1372if path in filesToDelete:1373 filesToDelete.remove(path)1374elif modifier =="D":1375 filesToDelete.add(path)1376if path in filesToAdd:1377 filesToAdd.remove(path)1378elif modifier =="C":1379 src, dest = diff['src'], diff['dst']1380p4_integrate(src, dest)1381 pureRenameCopy.add(dest)1382if diff['src_sha1'] != diff['dst_sha1']:1383p4_edit(dest)1384 pureRenameCopy.discard(dest)1385ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1386p4_edit(dest)1387 pureRenameCopy.discard(dest)1388 filesToChangeExecBit[dest] = diff['dst_mode']1389if self.isWindows:1390# turn off read-only attribute1391 os.chmod(dest, stat.S_IWRITE)1392 os.unlink(dest)1393 editedFiles.add(dest)1394elif modifier =="R":1395 src, dest = diff['src'], diff['dst']1396if self.p4HasMoveCommand:1397p4_edit(src)# src must be open before move1398p4_move(src, dest)# opens for (move/delete, move/add)1399else:1400p4_integrate(src, dest)1401if diff['src_sha1'] != diff['dst_sha1']:1402p4_edit(dest)1403else:1404 pureRenameCopy.add(dest)1405ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1406if not self.p4HasMoveCommand:1407p4_edit(dest)# with move: already open, writable1408 filesToChangeExecBit[dest] = diff['dst_mode']1409if not self.p4HasMoveCommand:1410if self.isWindows:1411 os.chmod(dest, stat.S_IWRITE)1412 os.unlink(dest)1413 filesToDelete.add(src)1414 editedFiles.add(dest)1415else:1416die("unknown modifier%sfor%s"% (modifier, path))14171418 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1419 patchcmd = diffcmd +" | git apply "1420 tryPatchCmd = patchcmd +"--check -"1421 applyPatchCmd = patchcmd +"--check --apply -"1422 patch_succeeded =True14231424if os.system(tryPatchCmd) !=0:1425 fixed_rcs_keywords =False1426 patch_succeeded =False1427print"Unfortunately applying the change failed!"14281429# Patch failed, maybe it's just RCS keyword woes. Look through1430# the patch to see if that's possible.1431ifgitConfigBool("git-p4.attemptRCSCleanup"):1432file=None1433 pattern =None1434 kwfiles = {}1435forfilein editedFiles | filesToDelete:1436# did this file's delta contain RCS keywords?1437 pattern =p4_keywords_regexp_for_file(file)14381439if pattern:1440# this file is a possibility...look for RCS keywords.1441 regexp = re.compile(pattern, re.VERBOSE)1442for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1443if regexp.search(line):1444if verbose:1445print"got keyword match on%sin%sin%s"% (pattern, line,file)1446 kwfiles[file] = pattern1447break14481449forfilein kwfiles:1450if verbose:1451print"zapping%swith%s"% (line,pattern)1452# File is being deleted, so not open in p4. Must1453# disable the read-only bit on windows.1454if self.isWindows andfilenot in editedFiles:1455 os.chmod(file, stat.S_IWRITE)1456 self.patchRCSKeywords(file, kwfiles[file])1457 fixed_rcs_keywords =True14581459if fixed_rcs_keywords:1460print"Retrying the patch with RCS keywords cleaned up"1461if os.system(tryPatchCmd) ==0:1462 patch_succeeded =True14631464if not patch_succeeded:1465for f in editedFiles:1466p4_revert(f)1467return False14681469#1470# Apply the patch for real, and do add/delete/+x handling.1471#1472system(applyPatchCmd)14731474for f in filesToAdd:1475p4_add(f)1476for f in filesToDelete:1477p4_revert(f)1478p4_delete(f)14791480# Set/clear executable bits1481for f in filesToChangeExecBit.keys():1482 mode = filesToChangeExecBit[f]1483setP4ExecBit(f, mode)14841485#1486# Build p4 change description, starting with the contents1487# of the git commit message.1488#1489 logMessage =extractLogMessageFromGitCommit(id)1490 logMessage = logMessage.strip()1491(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14921493 template = self.prepareSubmitTemplate()1494 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14951496if self.preserveUser:1497 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14981499if self.checkAuthorship and not self.p4UserIsMe(p4User):1500 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1501 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1502 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"15031504 separatorLine ="######## everything below this line is just the diff #######\n"1505if not self.prepare_p4_only:1506 submitTemplate += separatorLine1507 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)15081509(handle, fileName) = tempfile.mkstemp()1510 tmpFile = os.fdopen(handle,"w+b")1511if self.isWindows:1512 submitTemplate = submitTemplate.replace("\n","\r\n")1513 tmpFile.write(submitTemplate)1514 tmpFile.close()15151516if self.prepare_p4_only:1517#1518# Leave the p4 tree prepared, and the submit template around1519# and let the user decide what to do next1520#1521print1522print"P4 workspace prepared for submission."1523print"To submit or revert, go to client workspace"1524print" "+ self.clientPath1525print1526print"To submit, use\"p4 submit\"to write a new description,"1527print"or\"p4 submit -i <%s\"to use the one prepared by" \1528"\"git p4\"."% fileName1529print"You can delete the file\"%s\"when finished."% fileName15301531if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1532print"To preserve change ownership by user%s, you must\n" \1533"do\"p4 change -f <change>\"after submitting and\n" \1534"edit the User field."1535if pureRenameCopy:1536print"After submitting, renamed files must be re-synced."1537print"Invoke\"p4 sync -f\"on each of these files:"1538for f in pureRenameCopy:1539print" "+ f15401541print1542print"To revert the changes, use\"p4 revert ...\", and delete"1543print"the submit template file\"%s\""% fileName1544if filesToAdd:1545print"Since the commit adds new files, they must be deleted:"1546for f in filesToAdd:1547print" "+ f1548print1549return True15501551#1552# Let the user edit the change description, then submit it.1553#1554if self.edit_template(fileName):1555# read the edited message and submit1556 ret =True1557 tmpFile =open(fileName,"rb")1558 message = tmpFile.read()1559 tmpFile.close()1560if self.isWindows:1561 message = message.replace("\r\n","\n")1562 submitTemplate = message[:message.index(separatorLine)]1563p4_write_pipe(['submit','-i'], submitTemplate)15641565if self.preserveUser:1566if p4User:1567# Get last changelist number. Cannot easily get it from1568# the submit command output as the output is1569# unmarshalled.1570 changelist = self.lastP4Changelist()1571 self.modifyChangelistUser(changelist, p4User)15721573# The rename/copy happened by applying a patch that created a1574# new file. This leaves it writable, which confuses p4.1575for f in pureRenameCopy:1576p4_sync(f,"-f")15771578else:1579# skip this patch1580 ret =False1581print"Submission cancelled, undoing p4 changes."1582for f in editedFiles:1583p4_revert(f)1584for f in filesToAdd:1585p4_revert(f)1586 os.remove(f)1587for f in filesToDelete:1588p4_revert(f)15891590 os.remove(fileName)1591return ret15921593# Export git tags as p4 labels. Create a p4 label and then tag1594# with that.1595defexportGitTags(self, gitTags):1596 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1597iflen(validLabelRegexp) ==0:1598 validLabelRegexp = defaultLabelRegexp1599 m = re.compile(validLabelRegexp)16001601for name in gitTags:16021603if not m.match(name):1604if verbose:1605print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1606continue16071608# Get the p4 commit this corresponds to1609 logMessage =extractLogMessageFromGitCommit(name)1610 values =extractSettingsGitLog(logMessage)16111612if not values.has_key('change'):1613# a tag pointing to something not sent to p4; ignore1614if verbose:1615print"git tag%sdoes not give a p4 commit"% name1616continue1617else:1618 changelist = values['change']16191620# Get the tag details.1621 inHeader =True1622 isAnnotated =False1623 body = []1624for l inread_pipe_lines(["git","cat-file","-p", name]):1625 l = l.strip()1626if inHeader:1627if re.match(r'tag\s+', l):1628 isAnnotated =True1629elif re.match(r'\s*$', l):1630 inHeader =False1631continue1632else:1633 body.append(l)16341635if not isAnnotated:1636 body = ["lightweight tag imported by git p4\n"]16371638# Create the label - use the same view as the client spec we are using1639 clientSpec =getClientSpec()16401641 labelTemplate ="Label:%s\n"% name1642 labelTemplate +="Description:\n"1643for b in body:1644 labelTemplate +="\t"+ b +"\n"1645 labelTemplate +="View:\n"1646for depot_side in clientSpec.mappings:1647 labelTemplate +="\t%s\n"% depot_side16481649if self.dry_run:1650print"Would create p4 label%sfor tag"% name1651elif self.prepare_p4_only:1652print"Not creating p4 label%sfor tag due to option" \1653" --prepare-p4-only"% name1654else:1655p4_write_pipe(["label","-i"], labelTemplate)16561657# Use the label1658p4_system(["tag","-l", name] +1659["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16601661if verbose:1662print"created p4 label for tag%s"% name16631664defrun(self, args):1665iflen(args) ==0:1666 self.master =currentGitBranch()1667iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1668die("Detecting current git branch failed!")1669eliflen(args) ==1:1670 self.master = args[0]1671if notbranchExists(self.master):1672die("Branch%sdoes not exist"% self.master)1673else:1674return False16751676 allowSubmit =gitConfig("git-p4.allowSubmit")1677iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1678die("%sis not in git-p4.allowSubmit"% self.master)16791680[upstream, settings] =findUpstreamBranchPoint()1681 self.depotPath = settings['depot-paths'][0]1682iflen(self.origin) ==0:1683 self.origin = upstream16841685if self.preserveUser:1686if not self.canChangeChangelists():1687die("Cannot preserve user names without p4 super-user or admin permissions")16881689# if not set from the command line, try the config file1690if self.conflict_behavior is None:1691 val =gitConfig("git-p4.conflict")1692if val:1693if val not in self.conflict_behavior_choices:1694die("Invalid value '%s' for config git-p4.conflict"% val)1695else:1696 val ="ask"1697 self.conflict_behavior = val16981699if self.verbose:1700print"Origin branch is "+ self.origin17011702iflen(self.depotPath) ==0:1703print"Internal error: cannot locate perforce depot path from existing branches"1704 sys.exit(128)17051706 self.useClientSpec =False1707ifgitConfigBool("git-p4.useclientspec"):1708 self.useClientSpec =True1709if self.useClientSpec:1710 self.clientSpecDirs =getClientSpec()17111712# Check for the existance of P4 branches1713 branchesDetected = (len(p4BranchesInGit().keys()) >1)17141715if self.useClientSpec and not branchesDetected:1716# all files are relative to the client spec1717 self.clientPath =getClientRoot()1718else:1719 self.clientPath =p4Where(self.depotPath)17201721if self.clientPath =="":1722die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)17231724print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1725 self.oldWorkingDirectory = os.getcwd()17261727# ensure the clientPath exists1728 new_client_dir =False1729if not os.path.exists(self.clientPath):1730 new_client_dir =True1731 os.makedirs(self.clientPath)17321733chdir(self.clientPath, is_client_path=True)1734if self.dry_run:1735print"Would synchronize p4 checkout in%s"% self.clientPath1736else:1737print"Synchronizing p4 checkout..."1738if new_client_dir:1739# old one was destroyed, and maybe nobody told p41740p4_sync("...","-f")1741else:1742p4_sync("...")1743 self.check()17441745 commits = []1746for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1747 commits.append(line.strip())1748 commits.reverse()17491750if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1751 self.checkAuthorship =False1752else:1753 self.checkAuthorship =True17541755if self.preserveUser:1756 self.checkValidP4Users(commits)17571758#1759# Build up a set of options to be passed to diff when1760# submitting each commit to p4.1761#1762if self.detectRenames:1763# command-line -M arg1764 self.diffOpts ="-M"1765else:1766# If not explicitly set check the config variable1767 detectRenames =gitConfig("git-p4.detectRenames")17681769if detectRenames.lower() =="false"or detectRenames =="":1770 self.diffOpts =""1771elif detectRenames.lower() =="true":1772 self.diffOpts ="-M"1773else:1774 self.diffOpts ="-M%s"% detectRenames17751776# no command-line arg for -C or --find-copies-harder, just1777# config variables1778 detectCopies =gitConfig("git-p4.detectCopies")1779if detectCopies.lower() =="false"or detectCopies =="":1780pass1781elif detectCopies.lower() =="true":1782 self.diffOpts +=" -C"1783else:1784 self.diffOpts +=" -C%s"% detectCopies17851786ifgitConfigBool("git-p4.detectCopiesHarder"):1787 self.diffOpts +=" --find-copies-harder"17881789#1790# Apply the commits, one at a time. On failure, ask if should1791# continue to try the rest of the patches, or quit.1792#1793if self.dry_run:1794print"Would apply"1795 applied = []1796 last =len(commits) -11797for i, commit inenumerate(commits):1798if self.dry_run:1799print" ",read_pipe(["git","show","-s",1800"--format=format:%h%s", commit])1801 ok =True1802else:1803 ok = self.applyCommit(commit)1804if ok:1805 applied.append(commit)1806else:1807if self.prepare_p4_only and i < last:1808print"Processing only the first commit due to option" \1809" --prepare-p4-only"1810break1811if i < last:1812 quit =False1813while True:1814# prompt for what to do, or use the option/variable1815if self.conflict_behavior =="ask":1816print"What do you want to do?"1817 response =raw_input("[s]kip this commit but apply"1818" the rest, or [q]uit? ")1819if not response:1820continue1821elif self.conflict_behavior =="skip":1822 response ="s"1823elif self.conflict_behavior =="quit":1824 response ="q"1825else:1826die("Unknown conflict_behavior '%s'"%1827 self.conflict_behavior)18281829if response[0] =="s":1830print"Skipping this commit, but applying the rest"1831break1832if response[0] =="q":1833print"Quitting"1834 quit =True1835break1836if quit:1837break18381839chdir(self.oldWorkingDirectory)18401841if self.dry_run:1842pass1843elif self.prepare_p4_only:1844pass1845eliflen(commits) ==len(applied):1846print"All commits applied!"18471848 sync =P4Sync()1849if self.branch:1850 sync.branch = self.branch1851 sync.run([])18521853 rebase =P4Rebase()1854 rebase.rebase()18551856else:1857iflen(applied) ==0:1858print"No commits applied."1859else:1860print"Applied only the commits marked with '*':"1861for c in commits:1862if c in applied:1863 star ="*"1864else:1865 star =" "1866print star,read_pipe(["git","show","-s",1867"--format=format:%h%s", c])1868print"You will have to do 'git p4 sync' and rebase."18691870ifgitConfigBool("git-p4.exportLabels"):1871 self.exportLabels =True18721873if self.exportLabels:1874 p4Labels =getP4Labels(self.depotPath)1875 gitTags =getGitTags()18761877 missingGitTags = gitTags - p4Labels1878 self.exportGitTags(missingGitTags)18791880# exit with error unless everything applied perfectly1881iflen(commits) !=len(applied):1882 sys.exit(1)18831884return True18851886classView(object):1887"""Represent a p4 view ("p4 help views"), and map files in a1888 repo according to the view."""18891890def__init__(self, client_name):1891 self.mappings = []1892 self.client_prefix ="//%s/"% client_name1893# cache results of "p4 where" to lookup client file locations1894 self.client_spec_path_cache = {}18951896defappend(self, view_line):1897"""Parse a view line, splitting it into depot and client1898 sides. Append to self.mappings, preserving order. This1899 is only needed for tag creation."""19001901# Split the view line into exactly two words. P4 enforces1902# structure on these lines that simplifies this quite a bit.1903#1904# Either or both words may be double-quoted.1905# Single quotes do not matter.1906# Double-quote marks cannot occur inside the words.1907# A + or - prefix is also inside the quotes.1908# There are no quotes unless they contain a space.1909# The line is already white-space stripped.1910# The two words are separated by a single space.1911#1912if view_line[0] =='"':1913# First word is double quoted. Find its end.1914 close_quote_index = view_line.find('"',1)1915if close_quote_index <=0:1916die("No first-word closing quote found:%s"% view_line)1917 depot_side = view_line[1:close_quote_index]1918# skip closing quote and space1919 rhs_index = close_quote_index +1+11920else:1921 space_index = view_line.find(" ")1922if space_index <=0:1923die("No word-splitting space found:%s"% view_line)1924 depot_side = view_line[0:space_index]1925 rhs_index = space_index +119261927# prefix + means overlay on previous mapping1928if depot_side.startswith("+"):1929 depot_side = depot_side[1:]19301931# prefix - means exclude this path, leave out of mappings1932 exclude =False1933if depot_side.startswith("-"):1934 exclude =True1935 depot_side = depot_side[1:]19361937if not exclude:1938 self.mappings.append(depot_side)19391940defconvert_client_path(self, clientFile):1941# chop off //client/ part to make it relative1942if not clientFile.startswith(self.client_prefix):1943die("No prefix '%s' on clientFile '%s'"%1944(self.client_prefix, clientFile))1945return clientFile[len(self.client_prefix):]19461947defupdate_client_spec_path_cache(self, files):1948""" Caching file paths by "p4 where" batch query """19491950# List depot file paths exclude that already cached1951 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]19521953iflen(fileArgs) ==0:1954return# All files in cache19551956 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1957for res in where_result:1958if"code"in res and res["code"] =="error":1959# assume error is "... file(s) not in client view"1960continue1961if"clientFile"not in res:1962die("No clientFile in 'p4 where' output")1963if"unmap"in res:1964# it will list all of them, but only one not unmap-ped1965continue1966ifgitConfigBool("core.ignorecase"):1967 res['depotFile'] = res['depotFile'].lower()1968 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19691970# not found files or unmap files set to ""1971for depotFile in fileArgs:1972ifgitConfigBool("core.ignorecase"):1973 depotFile = depotFile.lower()1974if depotFile not in self.client_spec_path_cache:1975 self.client_spec_path_cache[depotFile] =""19761977defmap_in_client(self, depot_path):1978"""Return the relative location in the client where this1979 depot file should live. Returns "" if the file should1980 not be mapped in the client."""19811982ifgitConfigBool("core.ignorecase"):1983 depot_path = depot_path.lower()19841985if depot_path in self.client_spec_path_cache:1986return self.client_spec_path_cache[depot_path]19871988die("Error:%sis not found in client spec path"% depot_path )1989return""19901991classP4Sync(Command, P4UserMap):1992 delete_actions = ("delete","move/delete","purge")19931994def__init__(self):1995 Command.__init__(self)1996 P4UserMap.__init__(self)1997 self.options = [1998 optparse.make_option("--branch", dest="branch"),1999 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2000 optparse.make_option("--changesfile", dest="changesFile"),2001 optparse.make_option("--silent", dest="silent", action="store_true"),2002 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2003 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2004 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2005help="Import into refs/heads/ , not refs/remotes"),2006 optparse.make_option("--max-changes", dest="maxChanges",2007help="Maximum number of changes to import"),2008 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2009help="Internal block size to use when iteratively calling p4 changes"),2010 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2011help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2012 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2013help="Only sync files that are included in the Perforce Client Spec"),2014 optparse.make_option("-/", dest="cloneExclude",2015 action="append",type="string",2016help="exclude depot path"),2017]2018 self.description ="""Imports from Perforce into a git repository.\n2019 example:2020 //depot/my/project/ -- to import the current head2021 //depot/my/project/@all -- to import everything2022 //depot/my/project/@1,6 -- to import only from revision 1 to 620232024 (a ... is not needed in the path p4 specification, it's added implicitly)"""20252026 self.usage +=" //depot/path[@revRange]"2027 self.silent =False2028 self.createdBranches =set()2029 self.committedChanges =set()2030 self.branch =""2031 self.detectBranches =False2032 self.detectLabels =False2033 self.importLabels =False2034 self.changesFile =""2035 self.syncWithOrigin =True2036 self.importIntoRemotes =True2037 self.maxChanges =""2038 self.changes_block_size =None2039 self.keepRepoPath =False2040 self.depotPaths =None2041 self.p4BranchesInGit = []2042 self.cloneExclude = []2043 self.useClientSpec =False2044 self.useClientSpec_from_options =False2045 self.clientSpecDirs =None2046 self.tempBranches = []2047 self.tempBranchLocation ="git-p4-tmp"20482049ifgitConfig("git-p4.syncFromOrigin") =="false":2050 self.syncWithOrigin =False20512052# This is required for the "append" cloneExclude action2053defensure_value(self, attr, value):2054if nothasattr(self, attr)orgetattr(self, attr)is None:2055setattr(self, attr, value)2056returngetattr(self, attr)20572058# Force a checkpoint in fast-import and wait for it to finish2059defcheckpoint(self):2060 self.gitStream.write("checkpoint\n\n")2061 self.gitStream.write("progress checkpoint\n\n")2062 out = self.gitOutput.readline()2063if self.verbose:2064print"checkpoint finished: "+ out20652066defextractFilesFromCommit(self, commit):2067 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2068for path in self.cloneExclude]2069 files = []2070 fnum =02071while commit.has_key("depotFile%s"% fnum):2072 path = commit["depotFile%s"% fnum]20732074if[p for p in self.cloneExclude2075ifp4PathStartsWith(path, p)]:2076 found =False2077else:2078 found = [p for p in self.depotPaths2079ifp4PathStartsWith(path, p)]2080if not found:2081 fnum = fnum +12082continue20832084file= {}2085file["path"] = path2086file["rev"] = commit["rev%s"% fnum]2087file["action"] = commit["action%s"% fnum]2088file["type"] = commit["type%s"% fnum]2089 files.append(file)2090 fnum = fnum +12091return files20922093defstripRepoPath(self, path, prefixes):2094"""When streaming files, this is called to map a p4 depot path2095 to where it should go in git. The prefixes are either2096 self.depotPaths, or self.branchPrefixes in the case of2097 branch detection."""20982099if self.useClientSpec:2100# branch detection moves files up a level (the branch name)2101# from what client spec interpretation gives2102 path = self.clientSpecDirs.map_in_client(path)2103if self.detectBranches:2104for b in self.knownBranches:2105if path.startswith(b +"/"):2106 path = path[len(b)+1:]21072108elif self.keepRepoPath:2109# Preserve everything in relative path name except leading2110# //depot/; just look at first prefix as they all should2111# be in the same depot.2112 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2113ifp4PathStartsWith(path, depot):2114 path = path[len(depot):]21152116else:2117for p in prefixes:2118ifp4PathStartsWith(path, p):2119 path = path[len(p):]2120break21212122 path =wildcard_decode(path)2123return path21242125defsplitFilesIntoBranches(self, commit):2126"""Look at each depotFile in the commit to figure out to what2127 branch it belongs."""21282129if self.clientSpecDirs:2130 files = self.extractFilesFromCommit(commit)2131 self.clientSpecDirs.update_client_spec_path_cache(files)21322133 branches = {}2134 fnum =02135while commit.has_key("depotFile%s"% fnum):2136 path = commit["depotFile%s"% fnum]2137 found = [p for p in self.depotPaths2138ifp4PathStartsWith(path, p)]2139if not found:2140 fnum = fnum +12141continue21422143file= {}2144file["path"] = path2145file["rev"] = commit["rev%s"% fnum]2146file["action"] = commit["action%s"% fnum]2147file["type"] = commit["type%s"% fnum]2148 fnum = fnum +121492150# start with the full relative path where this file would2151# go in a p4 client2152if self.useClientSpec:2153 relPath = self.clientSpecDirs.map_in_client(path)2154else:2155 relPath = self.stripRepoPath(path, self.depotPaths)21562157for branch in self.knownBranches.keys():2158# add a trailing slash so that a commit into qt/4.2foo2159# doesn't end up in qt/4.2, e.g.2160if relPath.startswith(branch +"/"):2161if branch not in branches:2162 branches[branch] = []2163 branches[branch].append(file)2164break21652166return branches21672168# output one file from the P4 stream2169# - helper for streamP4Files21702171defstreamOneP4File(self,file, contents):2172 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2173if verbose:2174 sys.stderr.write("%s\n"% relPath)21752176(type_base, type_mods) =split_p4_type(file["type"])21772178 git_mode ="100644"2179if"x"in type_mods:2180 git_mode ="100755"2181if type_base =="symlink":2182 git_mode ="120000"2183# p4 print on a symlink sometimes contains "target\n";2184# if it does, remove the newline2185 data =''.join(contents)2186if not data:2187# Some version of p4 allowed creating a symlink that pointed2188# to nothing. This causes p4 errors when checking out such2189# a change, and errors here too. Work around it by ignoring2190# the bad symlink; hopefully a future change fixes it.2191print"\nIgnoring empty symlink in%s"%file['depotFile']2192return2193elif data[-1] =='\n':2194 contents = [data[:-1]]2195else:2196 contents = [data]21972198if type_base =="utf16":2199# p4 delivers different text in the python output to -G2200# than it does when using "print -o", or normal p4 client2201# operations. utf16 is converted to ascii or utf8, perhaps.2202# But ascii text saved as -t utf16 is completely mangled.2203# Invoke print -o to get the real contents.2204#2205# On windows, the newlines will always be mangled by print, so put2206# them back too. This is not needed to the cygwin windows version,2207# just the native "NT" type.2208#2209 text =p4_read_pipe(['print','-q','-o','-',"%s@%s"% (file['depotFile'],file['change']) ])2210ifp4_version_string().find("/NT") >=0:2211 text = text.replace("\r\n","\n")2212 contents = [ text ]22132214if type_base =="apple":2215# Apple filetype files will be streamed as a concatenation of2216# its appledouble header and the contents. This is useless2217# on both macs and non-macs. If using "print -q -o xx", it2218# will create "xx" with the data, and "%xx" with the header.2219# This is also not very useful.2220#2221# Ideally, someday, this script can learn how to generate2222# appledouble files directly and import those to git, but2223# non-mac machines can never find a use for apple filetype.2224print"\nIgnoring apple filetype file%s"%file['depotFile']2225return22262227# Note that we do not try to de-mangle keywords on utf16 files,2228# even though in theory somebody may want that.2229 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2230if pattern:2231 regexp = re.compile(pattern, re.VERBOSE)2232 text =''.join(contents)2233 text = regexp.sub(r'$\1$', text)2234 contents = [ text ]22352236 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22372238# total length...2239 length =02240for d in contents:2241 length = length +len(d)22422243 self.gitStream.write("data%d\n"% length)2244for d in contents:2245 self.gitStream.write(d)2246 self.gitStream.write("\n")22472248defstreamOneP4Deletion(self,file):2249 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2250if verbose:2251 sys.stderr.write("delete%s\n"% relPath)2252 self.gitStream.write("D%s\n"% relPath)22532254# handle another chunk of streaming data2255defstreamP4FilesCb(self, marshalled):22562257# catch p4 errors and complain2258 err =None2259if"code"in marshalled:2260if marshalled["code"] =="error":2261if"data"in marshalled:2262 err = marshalled["data"].rstrip()2263if err:2264 f =None2265if self.stream_have_file_info:2266if"depotFile"in self.stream_file:2267 f = self.stream_file["depotFile"]2268# force a failure in fast-import, else an empty2269# commit will be made2270 self.gitStream.write("\n")2271 self.gitStream.write("die-now\n")2272 self.gitStream.close()2273# ignore errors, but make sure it exits first2274 self.importProcess.wait()2275if f:2276die("Error from p4 print for%s:%s"% (f, err))2277else:2278die("Error from p4 print:%s"% err)22792280if marshalled.has_key('depotFile')and self.stream_have_file_info:2281# start of a new file - output the old one first2282 self.streamOneP4File(self.stream_file, self.stream_contents)2283 self.stream_file = {}2284 self.stream_contents = []2285 self.stream_have_file_info =False22862287# pick up the new file information... for the2288# 'data' field we need to append to our array2289for k in marshalled.keys():2290if k =='data':2291 self.stream_contents.append(marshalled['data'])2292else:2293 self.stream_file[k] = marshalled[k]22942295 self.stream_have_file_info =True22962297# Stream directly from "p4 files" into "git fast-import"2298defstreamP4Files(self, files):2299 filesForCommit = []2300 filesToRead = []2301 filesToDelete = []23022303for f in files:2304# if using a client spec, only add the files that have2305# a path in the client2306if self.clientSpecDirs:2307if self.clientSpecDirs.map_in_client(f['path']) =="":2308continue23092310 filesForCommit.append(f)2311if f['action']in self.delete_actions:2312 filesToDelete.append(f)2313else:2314 filesToRead.append(f)23152316# deleted files...2317for f in filesToDelete:2318 self.streamOneP4Deletion(f)23192320iflen(filesToRead) >0:2321 self.stream_file = {}2322 self.stream_contents = []2323 self.stream_have_file_info =False23242325# curry self argument2326defstreamP4FilesCbSelf(entry):2327 self.streamP4FilesCb(entry)23282329 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23302331p4CmdList(["-x","-","print"],2332 stdin=fileArgs,2333 cb=streamP4FilesCbSelf)23342335# do the last chunk2336if self.stream_file.has_key('depotFile'):2337 self.streamOneP4File(self.stream_file, self.stream_contents)23382339defmake_email(self, userid):2340if userid in self.users:2341return self.users[userid]2342else:2343return"%s<a@b>"% userid23442345# Stream a p4 tag2346defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2347if verbose:2348print"writing tag%sfor commit%s"% (labelName, commit)2349 gitStream.write("tag%s\n"% labelName)2350 gitStream.write("from%s\n"% commit)23512352if labelDetails.has_key('Owner'):2353 owner = labelDetails["Owner"]2354else:2355 owner =None23562357# Try to use the owner of the p4 label, or failing that,2358# the current p4 user id.2359if owner:2360 email = self.make_email(owner)2361else:2362 email = self.make_email(self.p4UserId())2363 tagger ="%s %s %s"% (email, epoch, self.tz)23642365 gitStream.write("tagger%s\n"% tagger)23662367print"labelDetails=",labelDetails2368if labelDetails.has_key('Description'):2369 description = labelDetails['Description']2370else:2371 description ='Label from git p4'23722373 gitStream.write("data%d\n"%len(description))2374 gitStream.write(description)2375 gitStream.write("\n")23762377defcommit(self, details, files, branch, parent =""):2378 epoch = details["time"]2379 author = details["user"]23802381if self.verbose:2382print"commit into%s"% branch23832384# start with reading files; if that fails, we should not2385# create a commit.2386 new_files = []2387for f in files:2388if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2389 new_files.append(f)2390else:2391 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23922393if self.clientSpecDirs:2394 self.clientSpecDirs.update_client_spec_path_cache(files)23952396 self.gitStream.write("commit%s\n"% branch)2397# gitStream.write("mark :%s\n" % details["change"])2398 self.committedChanges.add(int(details["change"]))2399 committer =""2400if author not in self.users:2401 self.getUserMapFromPerforceServer()2402 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)24032404 self.gitStream.write("committer%s\n"% committer)24052406 self.gitStream.write("data <<EOT\n")2407 self.gitStream.write(details["desc"])2408 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2409(','.join(self.branchPrefixes), details["change"]))2410iflen(details['options']) >0:2411 self.gitStream.write(": options =%s"% details['options'])2412 self.gitStream.write("]\nEOT\n\n")24132414iflen(parent) >0:2415if self.verbose:2416print"parent%s"% parent2417 self.gitStream.write("from%s\n"% parent)24182419 self.streamP4Files(new_files)2420 self.gitStream.write("\n")24212422 change =int(details["change"])24232424if self.labels.has_key(change):2425 label = self.labels[change]2426 labelDetails = label[0]2427 labelRevisions = label[1]2428if self.verbose:2429print"Change%sis labelled%s"% (change, labelDetails)24302431 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2432for p in self.branchPrefixes])24332434iflen(files) ==len(labelRevisions):24352436 cleanedFiles = {}2437for info in files:2438if info["action"]in self.delete_actions:2439continue2440 cleanedFiles[info["depotFile"]] = info["rev"]24412442if cleanedFiles == labelRevisions:2443 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24442445else:2446if not self.silent:2447print("Tag%sdoes not match with change%s: files do not match."2448% (labelDetails["label"], change))24492450else:2451if not self.silent:2452print("Tag%sdoes not match with change%s: file count is different."2453% (labelDetails["label"], change))24542455# Build a dictionary of changelists and labels, for "detect-labels" option.2456defgetLabels(self):2457 self.labels = {}24582459 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2460iflen(l) >0and not self.silent:2461print"Finding files belonging to labels in%s"% `self.depotPaths`24622463for output in l:2464 label = output["label"]2465 revisions = {}2466 newestChange =02467if self.verbose:2468print"Querying files for label%s"% label2469forfileinp4CmdList(["files"] +2470["%s...@%s"% (p, label)2471for p in self.depotPaths]):2472 revisions[file["depotFile"]] =file["rev"]2473 change =int(file["change"])2474if change > newestChange:2475 newestChange = change24762477 self.labels[newestChange] = [output, revisions]24782479if self.verbose:2480print"Label changes:%s"% self.labels.keys()24812482# Import p4 labels as git tags. A direct mapping does not2483# exist, so assume that if all the files are at the same revision2484# then we can use that, or it's something more complicated we should2485# just ignore.2486defimportP4Labels(self, stream, p4Labels):2487if verbose:2488print"import p4 labels: "+' '.join(p4Labels)24892490 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2491 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2492iflen(validLabelRegexp) ==0:2493 validLabelRegexp = defaultLabelRegexp2494 m = re.compile(validLabelRegexp)24952496for name in p4Labels:2497 commitFound =False24982499if not m.match(name):2500if verbose:2501print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2502continue25032504if name in ignoredP4Labels:2505continue25062507 labelDetails =p4CmdList(['label',"-o", name])[0]25082509# get the most recent changelist for each file in this label2510 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2511for p in self.depotPaths])25122513if change.has_key('change'):2514# find the corresponding git commit; take the oldest commit2515 changelist =int(change['change'])2516 gitCommit =read_pipe(["git","rev-list","--max-count=1",2517"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2518iflen(gitCommit) ==0:2519print"could not find git commit for changelist%d"% changelist2520else:2521 gitCommit = gitCommit.strip()2522 commitFound =True2523# Convert from p4 time format2524try:2525 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2526exceptValueError:2527print"Could not convert label time%s"% labelDetails['Update']2528 tmwhen =125292530 when =int(time.mktime(tmwhen))2531 self.streamTag(stream, name, labelDetails, gitCommit, when)2532if verbose:2533print"p4 label%smapped to git commit%s"% (name, gitCommit)2534else:2535if verbose:2536print"Label%shas no changelists - possibly deleted?"% name25372538if not commitFound:2539# We can't import this label; don't try again as it will get very2540# expensive repeatedly fetching all the files for labels that will2541# never be imported. If the label is moved in the future, the2542# ignore will need to be removed manually.2543system(["git","config","--add","git-p4.ignoredP4Labels", name])25442545defguessProjectName(self):2546for p in self.depotPaths:2547if p.endswith("/"):2548 p = p[:-1]2549 p = p[p.strip().rfind("/") +1:]2550if not p.endswith("/"):2551 p +="/"2552return p25532554defgetBranchMapping(self):2555 lostAndFoundBranches =set()25562557 user =gitConfig("git-p4.branchUser")2558iflen(user) >0:2559 command ="branches -u%s"% user2560else:2561 command ="branches"25622563for info inp4CmdList(command):2564 details =p4Cmd(["branch","-o", info["branch"]])2565 viewIdx =02566while details.has_key("View%s"% viewIdx):2567 paths = details["View%s"% viewIdx].split(" ")2568 viewIdx = viewIdx +12569# require standard //depot/foo/... //depot/bar/... mapping2570iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2571continue2572 source = paths[0]2573 destination = paths[1]2574## HACK2575ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2576 source = source[len(self.depotPaths[0]):-4]2577 destination = destination[len(self.depotPaths[0]):-4]25782579if destination in self.knownBranches:2580if not self.silent:2581print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2582print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2583continue25842585 self.knownBranches[destination] = source25862587 lostAndFoundBranches.discard(destination)25882589if source not in self.knownBranches:2590 lostAndFoundBranches.add(source)25912592# Perforce does not strictly require branches to be defined, so we also2593# check git config for a branch list.2594#2595# Example of branch definition in git config file:2596# [git-p4]2597# branchList=main:branchA2598# branchList=main:branchB2599# branchList=branchA:branchC2600 configBranches =gitConfigList("git-p4.branchList")2601for branch in configBranches:2602if branch:2603(source, destination) = branch.split(":")2604 self.knownBranches[destination] = source26052606 lostAndFoundBranches.discard(destination)26072608if source not in self.knownBranches:2609 lostAndFoundBranches.add(source)261026112612for branch in lostAndFoundBranches:2613 self.knownBranches[branch] = branch26142615defgetBranchMappingFromGitBranches(self):2616 branches =p4BranchesInGit(self.importIntoRemotes)2617for branch in branches.keys():2618if branch =="master":2619 branch ="main"2620else:2621 branch = branch[len(self.projectName):]2622 self.knownBranches[branch] = branch26232624defupdateOptionDict(self, d):2625 option_keys = {}2626if self.keepRepoPath:2627 option_keys['keepRepoPath'] =126282629 d["options"] =' '.join(sorted(option_keys.keys()))26302631defreadOptions(self, d):2632 self.keepRepoPath = (d.has_key('options')2633and('keepRepoPath'in d['options']))26342635defgitRefForBranch(self, branch):2636if branch =="main":2637return self.refPrefix +"master"26382639iflen(branch) <=0:2640return branch26412642return self.refPrefix + self.projectName + branch26432644defgitCommitByP4Change(self, ref, change):2645if self.verbose:2646print"looking in ref "+ ref +" for change%susing bisect..."% change26472648 earliestCommit =""2649 latestCommit =parseRevision(ref)26502651while True:2652if self.verbose:2653print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2654 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2655iflen(next) ==0:2656if self.verbose:2657print"argh"2658return""2659 log =extractLogMessageFromGitCommit(next)2660 settings =extractSettingsGitLog(log)2661 currentChange =int(settings['change'])2662if self.verbose:2663print"current change%s"% currentChange26642665if currentChange == change:2666if self.verbose:2667print"found%s"% next2668return next26692670if currentChange < change:2671 earliestCommit ="^%s"% next2672else:2673 latestCommit ="%s"% next26742675return""26762677defimportNewBranch(self, branch, maxChange):2678# make fast-import flush all changes to disk and update the refs using the checkpoint2679# command so that we can try to find the branch parent in the git history2680 self.gitStream.write("checkpoint\n\n");2681 self.gitStream.flush();2682 branchPrefix = self.depotPaths[0] + branch +"/"2683range="@1,%s"% maxChange2684#print "prefix" + branchPrefix2685 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2686iflen(changes) <=0:2687return False2688 firstChange = changes[0]2689#print "first change in branch: %s" % firstChange2690 sourceBranch = self.knownBranches[branch]2691 sourceDepotPath = self.depotPaths[0] + sourceBranch2692 sourceRef = self.gitRefForBranch(sourceBranch)2693#print "source " + sourceBranch26942695 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2696#print "branch parent: %s" % branchParentChange2697 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2698iflen(gitParent) >0:2699 self.initialParents[self.gitRefForBranch(branch)] = gitParent2700#print "parent git commit: %s" % gitParent27012702 self.importChanges(changes)2703return True27042705defsearchParent(self, parent, branch, target):2706 parentFound =False2707for blob inread_pipe_lines(["git","rev-list","--reverse",2708"--no-merges", parent]):2709 blob = blob.strip()2710iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2711 parentFound =True2712if self.verbose:2713print"Found parent of%sin commit%s"% (branch, blob)2714break2715if parentFound:2716return blob2717else:2718return None27192720defimportChanges(self, changes):2721 cnt =12722for change in changes:2723 description =p4_describe(change)2724 self.updateOptionDict(description)27252726if not self.silent:2727 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2728 sys.stdout.flush()2729 cnt = cnt +127302731try:2732if self.detectBranches:2733 branches = self.splitFilesIntoBranches(description)2734for branch in branches.keys():2735## HACK --hwn2736 branchPrefix = self.depotPaths[0] + branch +"/"2737 self.branchPrefixes = [ branchPrefix ]27382739 parent =""27402741 filesForCommit = branches[branch]27422743if self.verbose:2744print"branch is%s"% branch27452746 self.updatedBranches.add(branch)27472748if branch not in self.createdBranches:2749 self.createdBranches.add(branch)2750 parent = self.knownBranches[branch]2751if parent == branch:2752 parent =""2753else:2754 fullBranch = self.projectName + branch2755if fullBranch not in self.p4BranchesInGit:2756if not self.silent:2757print("\nImporting new branch%s"% fullBranch);2758if self.importNewBranch(branch, change -1):2759 parent =""2760 self.p4BranchesInGit.append(fullBranch)2761if not self.silent:2762print("\nResuming with change%s"% change);27632764if self.verbose:2765print"parent determined through known branches:%s"% parent27662767 branch = self.gitRefForBranch(branch)2768 parent = self.gitRefForBranch(parent)27692770if self.verbose:2771print"looking for initial parent for%s; current parent is%s"% (branch, parent)27722773iflen(parent) ==0and branch in self.initialParents:2774 parent = self.initialParents[branch]2775del self.initialParents[branch]27762777 blob =None2778iflen(parent) >0:2779 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2780if self.verbose:2781print"Creating temporary branch: "+ tempBranch2782 self.commit(description, filesForCommit, tempBranch)2783 self.tempBranches.append(tempBranch)2784 self.checkpoint()2785 blob = self.searchParent(parent, branch, tempBranch)2786if blob:2787 self.commit(description, filesForCommit, branch, blob)2788else:2789if self.verbose:2790print"Parent of%snot found. Committing into head of%s"% (branch, parent)2791 self.commit(description, filesForCommit, branch, parent)2792else:2793 files = self.extractFilesFromCommit(description)2794 self.commit(description, files, self.branch,2795 self.initialParent)2796# only needed once, to connect to the previous commit2797 self.initialParent =""2798exceptIOError:2799print self.gitError.read()2800 sys.exit(1)28012802defimportHeadRevision(self, revision):2803print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)28042805 details = {}2806 details["user"] ="git perforce import user"2807 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2808% (' '.join(self.depotPaths), revision))2809 details["change"] = revision2810 newestRevision =028112812 fileCnt =02813 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28142815for info inp4CmdList(["files"] + fileArgs):28162817if'code'in info and info['code'] =='error':2818 sys.stderr.write("p4 returned an error:%s\n"2819% info['data'])2820if info['data'].find("must refer to client") >=0:2821 sys.stderr.write("This particular p4 error is misleading.\n")2822 sys.stderr.write("Perhaps the depot path was misspelled.\n");2823 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2824 sys.exit(1)2825if'p4ExitCode'in info:2826 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2827 sys.exit(1)282828292830 change =int(info["change"])2831if change > newestRevision:2832 newestRevision = change28332834if info["action"]in self.delete_actions:2835# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2836#fileCnt = fileCnt + 12837continue28382839for prop in["depotFile","rev","action","type"]:2840 details["%s%s"% (prop, fileCnt)] = info[prop]28412842 fileCnt = fileCnt +128432844 details["change"] = newestRevision28452846# Use time from top-most change so that all git p4 clones of2847# the same p4 repo have the same commit SHA1s.2848 res =p4_describe(newestRevision)2849 details["time"] = res["time"]28502851 self.updateOptionDict(details)2852try:2853 self.commit(details, self.extractFilesFromCommit(details), self.branch)2854exceptIOError:2855print"IO error with git fast-import. Is your git version recent enough?"2856print self.gitError.read()285728582859defrun(self, args):2860 self.depotPaths = []2861 self.changeRange =""2862 self.previousDepotPaths = []2863 self.hasOrigin =False28642865# map from branch depot path to parent branch2866 self.knownBranches = {}2867 self.initialParents = {}28682869if self.importIntoRemotes:2870 self.refPrefix ="refs/remotes/p4/"2871else:2872 self.refPrefix ="refs/heads/p4/"28732874if self.syncWithOrigin:2875 self.hasOrigin =originP4BranchesExist()2876if self.hasOrigin:2877if not self.silent:2878print'Syncing with origin first, using "git fetch origin"'2879system("git fetch origin")28802881 branch_arg_given =bool(self.branch)2882iflen(self.branch) ==0:2883 self.branch = self.refPrefix +"master"2884ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2885system("git update-ref%srefs/heads/p4"% self.branch)2886system("git branch -D p4")28872888# accept either the command-line option, or the configuration variable2889if self.useClientSpec:2890# will use this after clone to set the variable2891 self.useClientSpec_from_options =True2892else:2893ifgitConfigBool("git-p4.useclientspec"):2894 self.useClientSpec =True2895if self.useClientSpec:2896 self.clientSpecDirs =getClientSpec()28972898# TODO: should always look at previous commits,2899# merge with previous imports, if possible.2900if args == []:2901if self.hasOrigin:2902createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)29032904# branches holds mapping from branch name to sha12905 branches =p4BranchesInGit(self.importIntoRemotes)29062907# restrict to just this one, disabling detect-branches2908if branch_arg_given:2909 short = self.branch.split("/")[-1]2910if short in branches:2911 self.p4BranchesInGit = [ short ]2912else:2913 self.p4BranchesInGit = branches.keys()29142915iflen(self.p4BranchesInGit) >1:2916if not self.silent:2917print"Importing from/into multiple branches"2918 self.detectBranches =True2919for branch in branches.keys():2920 self.initialParents[self.refPrefix + branch] = \2921 branches[branch]29222923if self.verbose:2924print"branches:%s"% self.p4BranchesInGit29252926 p4Change =02927for branch in self.p4BranchesInGit:2928 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29292930 settings =extractSettingsGitLog(logMsg)29312932 self.readOptions(settings)2933if(settings.has_key('depot-paths')2934and settings.has_key('change')):2935 change =int(settings['change']) +12936 p4Change =max(p4Change, change)29372938 depotPaths =sorted(settings['depot-paths'])2939if self.previousDepotPaths == []:2940 self.previousDepotPaths = depotPaths2941else:2942 paths = []2943for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2944 prev_list = prev.split("/")2945 cur_list = cur.split("/")2946for i inrange(0,min(len(cur_list),len(prev_list))):2947if cur_list[i] <> prev_list[i]:2948 i = i -12949break29502951 paths.append("/".join(cur_list[:i +1]))29522953 self.previousDepotPaths = paths29542955if p4Change >0:2956 self.depotPaths =sorted(self.previousDepotPaths)2957 self.changeRange ="@%s,#head"% p4Change2958if not self.silent and not self.detectBranches:2959print"Performing incremental import into%sgit branch"% self.branch29602961# accept multiple ref name abbreviations:2962# refs/foo/bar/branch -> use it exactly2963# p4/branch -> prepend refs/remotes/ or refs/heads/2964# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2965if not self.branch.startswith("refs/"):2966if self.importIntoRemotes:2967 prepend ="refs/remotes/"2968else:2969 prepend ="refs/heads/"2970if not self.branch.startswith("p4/"):2971 prepend +="p4/"2972 self.branch = prepend + self.branch29732974iflen(args) ==0and self.depotPaths:2975if not self.silent:2976print"Depot paths:%s"%' '.join(self.depotPaths)2977else:2978if self.depotPaths and self.depotPaths != args:2979print("previous import used depot path%sand now%swas specified. "2980"This doesn't work!"% (' '.join(self.depotPaths),2981' '.join(args)))2982 sys.exit(1)29832984 self.depotPaths =sorted(args)29852986 revision =""2987 self.users = {}29882989# Make sure no revision specifiers are used when --changesfile2990# is specified.2991 bad_changesfile =False2992iflen(self.changesFile) >0:2993for p in self.depotPaths:2994if p.find("@") >=0or p.find("#") >=0:2995 bad_changesfile =True2996break2997if bad_changesfile:2998die("Option --changesfile is incompatible with revision specifiers")29993000 newPaths = []3001for p in self.depotPaths:3002if p.find("@") != -1:3003 atIdx = p.index("@")3004 self.changeRange = p[atIdx:]3005if self.changeRange =="@all":3006 self.changeRange =""3007elif','not in self.changeRange:3008 revision = self.changeRange3009 self.changeRange =""3010 p = p[:atIdx]3011elif p.find("#") != -1:3012 hashIdx = p.index("#")3013 revision = p[hashIdx:]3014 p = p[:hashIdx]3015elif self.previousDepotPaths == []:3016# pay attention to changesfile, if given, else import3017# the entire p4 tree at the head revision3018iflen(self.changesFile) ==0:3019 revision ="#head"30203021 p = re.sub("\.\.\.$","", p)3022if not p.endswith("/"):3023 p +="/"30243025 newPaths.append(p)30263027 self.depotPaths = newPaths30283029# --detect-branches may change this for each branch3030 self.branchPrefixes = self.depotPaths30313032 self.loadUserMapFromCache()3033 self.labels = {}3034if self.detectLabels:3035 self.getLabels();30363037if self.detectBranches:3038## FIXME - what's a P4 projectName ?3039 self.projectName = self.guessProjectName()30403041if self.hasOrigin:3042 self.getBranchMappingFromGitBranches()3043else:3044 self.getBranchMapping()3045if self.verbose:3046print"p4-git branches:%s"% self.p4BranchesInGit3047print"initial parents:%s"% self.initialParents3048for b in self.p4BranchesInGit:3049if b !="master":30503051## FIXME3052 b = b[len(self.projectName):]3053 self.createdBranches.add(b)30543055 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30563057 self.importProcess = subprocess.Popen(["git","fast-import"],3058 stdin=subprocess.PIPE,3059 stdout=subprocess.PIPE,3060 stderr=subprocess.PIPE);3061 self.gitOutput = self.importProcess.stdout3062 self.gitStream = self.importProcess.stdin3063 self.gitError = self.importProcess.stderr30643065if revision:3066 self.importHeadRevision(revision)3067else:3068 changes = []30693070iflen(self.changesFile) >0:3071 output =open(self.changesFile).readlines()3072 changeSet =set()3073for line in output:3074 changeSet.add(int(line))30753076for change in changeSet:3077 changes.append(change)30783079 changes.sort()3080else:3081# catch "git p4 sync" with no new branches, in a repo that3082# does not have any existing p4 branches3083iflen(args) ==0:3084if not self.p4BranchesInGit:3085die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30863087# The default branch is master, unless --branch is used to3088# specify something else. Make sure it exists, or complain3089# nicely about how to use --branch.3090if not self.detectBranches:3091if notbranch_exists(self.branch):3092if branch_arg_given:3093die("Error: branch%sdoes not exist."% self.branch)3094else:3095die("Error: no branch%s; perhaps specify one with --branch."%3096 self.branch)30973098if self.verbose:3099print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3100 self.changeRange)3101 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)31023103iflen(self.maxChanges) >0:3104 changes = changes[:min(int(self.maxChanges),len(changes))]31053106iflen(changes) ==0:3107if not self.silent:3108print"No changes to import!"3109else:3110if not self.silent and not self.detectBranches:3111print"Import destination:%s"% self.branch31123113 self.updatedBranches =set()31143115if not self.detectBranches:3116if args:3117# start a new branch3118 self.initialParent =""3119else:3120# build on a previous revision3121 self.initialParent =parseRevision(self.branch)31223123 self.importChanges(changes)31243125if not self.silent:3126print""3127iflen(self.updatedBranches) >0:3128 sys.stdout.write("Updated branches: ")3129for b in self.updatedBranches:3130 sys.stdout.write("%s"% b)3131 sys.stdout.write("\n")31323133ifgitConfigBool("git-p4.importLabels"):3134 self.importLabels =True31353136if self.importLabels:3137 p4Labels =getP4Labels(self.depotPaths)3138 gitTags =getGitTags()31393140 missingP4Labels = p4Labels - gitTags3141 self.importP4Labels(self.gitStream, missingP4Labels)31423143 self.gitStream.close()3144if self.importProcess.wait() !=0:3145die("fast-import failed:%s"% self.gitError.read())3146 self.gitOutput.close()3147 self.gitError.close()31483149# Cleanup temporary branches created during import3150if self.tempBranches != []:3151for branch in self.tempBranches:3152read_pipe("git update-ref -d%s"% branch)3153 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31543155# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3156# a convenient shortcut refname "p4".3157if self.importIntoRemotes:3158 head_ref = self.refPrefix +"HEAD"3159if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3160system(["git","symbolic-ref", head_ref, self.branch])31613162return True31633164classP4Rebase(Command):3165def__init__(self):3166 Command.__init__(self)3167 self.options = [3168 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3169]3170 self.importLabels =False3171 self.description = ("Fetches the latest revision from perforce and "3172+"rebases the current work (branch) against it")31733174defrun(self, args):3175 sync =P4Sync()3176 sync.importLabels = self.importLabels3177 sync.run([])31783179return self.rebase()31803181defrebase(self):3182if os.system("git update-index --refresh") !=0:3183die("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.");3184iflen(read_pipe("git diff-index HEAD --")) >0:3185die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");31863187[upstream, settings] =findUpstreamBranchPoint()3188iflen(upstream) ==0:3189die("Cannot find upstream branchpoint for rebase")31903191# the branchpoint may be p4/foo~3, so strip off the parent3192 upstream = re.sub("~[0-9]+$","", upstream)31933194print"Rebasing the current branch onto%s"% upstream3195 oldHead =read_pipe("git rev-parse HEAD").strip()3196system("git rebase%s"% upstream)3197system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3198return True31993200classP4Clone(P4Sync):3201def__init__(self):3202 P4Sync.__init__(self)3203 self.description ="Creates a new git repository and imports from Perforce into it"3204 self.usage ="usage: %prog [options] //depot/path[@revRange]"3205 self.options += [3206 optparse.make_option("--destination", dest="cloneDestination",3207 action='store', default=None,3208help="where to leave result of the clone"),3209 optparse.make_option("--bare", dest="cloneBare",3210 action="store_true", default=False),3211]3212 self.cloneDestination =None3213 self.needsGit =False3214 self.cloneBare =False32153216defdefaultDestination(self, args):3217## TODO: use common prefix of args?3218 depotPath = args[0]3219 depotDir = re.sub("(@[^@]*)$","", depotPath)3220 depotDir = re.sub("(#[^#]*)$","", depotDir)3221 depotDir = re.sub(r"\.\.\.$","", depotDir)3222 depotDir = re.sub(r"/$","", depotDir)3223return os.path.split(depotDir)[1]32243225defrun(self, args):3226iflen(args) <1:3227return False32283229if self.keepRepoPath and not self.cloneDestination:3230 sys.stderr.write("Must specify destination for --keep-path\n")3231 sys.exit(1)32323233 depotPaths = args32343235if not self.cloneDestination andlen(depotPaths) >1:3236 self.cloneDestination = depotPaths[-1]3237 depotPaths = depotPaths[:-1]32383239 self.cloneExclude = ["/"+p for p in self.cloneExclude]3240for p in depotPaths:3241if not p.startswith("//"):3242 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3243return False32443245if not self.cloneDestination:3246 self.cloneDestination = self.defaultDestination(args)32473248print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32493250if not os.path.exists(self.cloneDestination):3251 os.makedirs(self.cloneDestination)3252chdir(self.cloneDestination)32533254 init_cmd = ["git","init"]3255if self.cloneBare:3256 init_cmd.append("--bare")3257 retcode = subprocess.call(init_cmd)3258if retcode:3259raiseCalledProcessError(retcode, init_cmd)32603261if not P4Sync.run(self, depotPaths):3262return False32633264# create a master branch and check out a work tree3265ifgitBranchExists(self.branch):3266system(["git","branch","master", self.branch ])3267if not self.cloneBare:3268system(["git","checkout","-f"])3269else:3270print'Not checking out any branch, use ' \3271'"git checkout -q -b master <branch>"'32723273# auto-set this variable if invoked with --use-client-spec3274if self.useClientSpec_from_options:3275system("git config --bool git-p4.useclientspec true")32763277return True32783279classP4Branches(Command):3280def__init__(self):3281 Command.__init__(self)3282 self.options = [ ]3283 self.description = ("Shows the git branches that hold imports and their "3284+"corresponding perforce depot paths")3285 self.verbose =False32863287defrun(self, args):3288iforiginP4BranchesExist():3289createOrUpdateBranchesFromOrigin()32903291 cmdline ="git rev-parse --symbolic "3292 cmdline +=" --remotes"32933294for line inread_pipe_lines(cmdline):3295 line = line.strip()32963297if not line.startswith('p4/')or line =="p4/HEAD":3298continue3299 branch = line33003301 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3302 settings =extractSettingsGitLog(log)33033304print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3305return True33063307classHelpFormatter(optparse.IndentedHelpFormatter):3308def__init__(self):3309 optparse.IndentedHelpFormatter.__init__(self)33103311defformat_description(self, description):3312if description:3313return description +"\n"3314else:3315return""33163317defprintUsage(commands):3318print"usage:%s<command> [options]"% sys.argv[0]3319print""3320print"valid commands:%s"%", ".join(commands)3321print""3322print"Try%s<command> --help for command specific help."% sys.argv[0]3323print""33243325commands = {3326"debug": P4Debug,3327"submit": P4Submit,3328"commit": P4Submit,3329"sync": P4Sync,3330"rebase": P4Rebase,3331"clone": P4Clone,3332"rollback": P4RollBack,3333"branches": P4Branches3334}333533363337defmain():3338iflen(sys.argv[1:]) ==0:3339printUsage(commands.keys())3340 sys.exit(2)33413342 cmdName = sys.argv[1]3343try:3344 klass = commands[cmdName]3345 cmd =klass()3346exceptKeyError:3347print"unknown command%s"% cmdName3348print""3349printUsage(commands.keys())3350 sys.exit(2)33513352 options = cmd.options3353 cmd.gitdir = os.environ.get("GIT_DIR",None)33543355 args = sys.argv[2:]33563357 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3358if cmd.needsGit:3359 options.append(optparse.make_option("--git-dir", dest="gitdir"))33603361 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3362 options,3363 description = cmd.description,3364 formatter =HelpFormatter())33653366(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3367global verbose3368 verbose = cmd.verbose3369if cmd.needsGit:3370if cmd.gitdir ==None:3371 cmd.gitdir = os.path.abspath(".git")3372if notisValidGitDir(cmd.gitdir):3373 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3374if os.path.exists(cmd.gitdir):3375 cdup =read_pipe("git rev-parse --show-cdup").strip()3376iflen(cdup) >0:3377chdir(cdup);33783379if notisValidGitDir(cmd.gitdir):3380ifisValidGitDir(cmd.gitdir +"/.git"):3381 cmd.gitdir +="/.git"3382else:3383die("fatal: cannot locate git repository at%s"% cmd.gitdir)33843385 os.environ["GIT_DIR"] = cmd.gitdir33863387if not cmd.run(args):3388 parser.print_help()3389 sys.exit(2)339033913392if __name__ =='__main__':3393main()