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 46defp4_build_cmd(cmd): 47"""Build a suitable p4 command line. 48 49 This consolidates building and returning a p4 command line into one 50 location. It means that hooking into the environment, or other configuration 51 can be done more easily. 52 """ 53 real_cmd = ["p4"] 54 55 user =gitConfig("git-p4.user") 56iflen(user) >0: 57 real_cmd += ["-u",user] 58 59 password =gitConfig("git-p4.password") 60iflen(password) >0: 61 real_cmd += ["-P", password] 62 63 port =gitConfig("git-p4.port") 64iflen(port) >0: 65 real_cmd += ["-p", port] 66 67 host =gitConfig("git-p4.host") 68iflen(host) >0: 69 real_cmd += ["-H", host] 70 71 client =gitConfig("git-p4.client") 72iflen(client) >0: 73 real_cmd += ["-c", client] 74 75 76ifisinstance(cmd,basestring): 77 real_cmd =' '.join(real_cmd) +' '+ cmd 78else: 79 real_cmd += cmd 80return real_cmd 81 82defchdir(path, is_client_path=False): 83"""Do chdir to the given path, and set the PWD environment 84 variable for use by P4. It does not look at getcwd() output. 85 Since we're not using the shell, it is necessary to set the 86 PWD environment variable explicitly. 87 88 Normally, expand the path to force it to be absolute. This 89 addresses the use of relative path names inside P4 settings, 90 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 91 as given; it looks for .p4config using PWD. 92 93 If is_client_path, the path was handed to us directly by p4, 94 and may be a symbolic link. Do not call os.getcwd() in this 95 case, because it will cause p4 to think that PWD is not inside 96 the client path. 97 """ 98 99 os.chdir(path) 100if not is_client_path: 101 path = os.getcwd() 102 os.environ['PWD'] = path 103 104defdie(msg): 105if verbose: 106raiseException(msg) 107else: 108 sys.stderr.write(msg +"\n") 109 sys.exit(1) 110 111defwrite_pipe(c, stdin): 112if verbose: 113 sys.stderr.write('Writing pipe:%s\n'%str(c)) 114 115 expand =isinstance(c,basestring) 116 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 117 pipe = p.stdin 118 val = pipe.write(stdin) 119 pipe.close() 120if p.wait(): 121die('Command failed:%s'%str(c)) 122 123return val 124 125defp4_write_pipe(c, stdin): 126 real_cmd =p4_build_cmd(c) 127returnwrite_pipe(real_cmd, stdin) 128 129defread_pipe(c, ignore_error=False): 130if verbose: 131 sys.stderr.write('Reading pipe:%s\n'%str(c)) 132 133 expand =isinstance(c,basestring) 134 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 135 pipe = p.stdout 136 val = pipe.read() 137if p.wait()and not ignore_error: 138die('Command failed:%s'%str(c)) 139 140return val 141 142defp4_read_pipe(c, ignore_error=False): 143 real_cmd =p4_build_cmd(c) 144returnread_pipe(real_cmd, ignore_error) 145 146defread_pipe_lines(c): 147if verbose: 148 sys.stderr.write('Reading pipe:%s\n'%str(c)) 149 150 expand =isinstance(c, basestring) 151 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 152 pipe = p.stdout 153 val = pipe.readlines() 154if pipe.close()or p.wait(): 155die('Command failed:%s'%str(c)) 156 157return val 158 159defp4_read_pipe_lines(c): 160"""Specifically invoke p4 on the command supplied. """ 161 real_cmd =p4_build_cmd(c) 162returnread_pipe_lines(real_cmd) 163 164defp4_has_command(cmd): 165"""Ask p4 for help on this command. If it returns an error, the 166 command does not exist in this version of p4.""" 167 real_cmd =p4_build_cmd(["help", cmd]) 168 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 169 stderr=subprocess.PIPE) 170 p.communicate() 171return p.returncode ==0 172 173defp4_has_move_command(): 174"""See if the move command exists, that it supports -k, and that 175 it has not been administratively disabled. The arguments 176 must be correct, but the filenames do not have to exist. Use 177 ones with wildcards so even if they exist, it will fail.""" 178 179if notp4_has_command("move"): 180return False 181 cmd =p4_build_cmd(["move","-k","@from","@to"]) 182 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 183(out, err) = p.communicate() 184# return code will be 1 in either case 185if err.find("Invalid option") >=0: 186return False 187if err.find("disabled") >=0: 188return False 189# assume it failed because @... was invalid changelist 190return True 191 192defsystem(cmd): 193 expand =isinstance(cmd,basestring) 194if verbose: 195 sys.stderr.write("executing%s\n"%str(cmd)) 196 retcode = subprocess.call(cmd, shell=expand) 197if retcode: 198raiseCalledProcessError(retcode, cmd) 199 200defp4_system(cmd): 201"""Specifically invoke p4 as the system command. """ 202 real_cmd =p4_build_cmd(cmd) 203 expand =isinstance(real_cmd, basestring) 204 retcode = subprocess.call(real_cmd, shell=expand) 205if retcode: 206raiseCalledProcessError(retcode, real_cmd) 207 208_p4_version_string =None 209defp4_version_string(): 210"""Read the version string, showing just the last line, which 211 hopefully is the interesting version bit. 212 213 $ p4 -V 214 Perforce - The Fast Software Configuration Management System. 215 Copyright 1995-2011 Perforce Software. All rights reserved. 216 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 217 """ 218global _p4_version_string 219if not _p4_version_string: 220 a =p4_read_pipe_lines(["-V"]) 221 _p4_version_string = a[-1].rstrip() 222return _p4_version_string 223 224defp4_integrate(src, dest): 225p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 226 227defp4_sync(f, *options): 228p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 229 230defp4_add(f): 231# forcibly add file names with wildcards 232ifwildcard_present(f): 233p4_system(["add","-f", f]) 234else: 235p4_system(["add", f]) 236 237defp4_delete(f): 238p4_system(["delete",wildcard_encode(f)]) 239 240defp4_edit(f): 241p4_system(["edit",wildcard_encode(f)]) 242 243defp4_revert(f): 244p4_system(["revert",wildcard_encode(f)]) 245 246defp4_reopen(type, f): 247p4_system(["reopen","-t",type,wildcard_encode(f)]) 248 249defp4_move(src, dest): 250p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 251 252defp4_describe(change): 253"""Make sure it returns a valid result by checking for 254 the presence of field "time". Return a dict of the 255 results.""" 256 257 ds =p4CmdList(["describe","-s",str(change)]) 258iflen(ds) !=1: 259die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 260 261 d = ds[0] 262 263if"p4ExitCode"in d: 264die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 265str(d))) 266if"code"in d: 267if d["code"] =="error": 268die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 269 270if"time"not in d: 271die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 272 273return d 274 275# 276# Canonicalize the p4 type and return a tuple of the 277# base type, plus any modifiers. See "p4 help filetypes" 278# for a list and explanation. 279# 280defsplit_p4_type(p4type): 281 282 p4_filetypes_historical = { 283"ctempobj":"binary+Sw", 284"ctext":"text+C", 285"cxtext":"text+Cx", 286"ktext":"text+k", 287"kxtext":"text+kx", 288"ltext":"text+F", 289"tempobj":"binary+FSw", 290"ubinary":"binary+F", 291"uresource":"resource+F", 292"uxbinary":"binary+Fx", 293"xbinary":"binary+x", 294"xltext":"text+Fx", 295"xtempobj":"binary+Swx", 296"xtext":"text+x", 297"xunicode":"unicode+x", 298"xutf16":"utf16+x", 299} 300if p4type in p4_filetypes_historical: 301 p4type = p4_filetypes_historical[p4type] 302 mods ="" 303 s = p4type.split("+") 304 base = s[0] 305 mods ="" 306iflen(s) >1: 307 mods = s[1] 308return(base, mods) 309 310# 311# return the raw p4 type of a file (text, text+ko, etc) 312# 313defp4_type(file): 314 results =p4CmdList(["fstat","-T","headType",file]) 315return results[0]['headType'] 316 317# 318# Given a type base and modifier, return a regexp matching 319# the keywords that can be expanded in the file 320# 321defp4_keywords_regexp_for_type(base, type_mods): 322if base in("text","unicode","binary"): 323 kwords =None 324if"ko"in type_mods: 325 kwords ='Id|Header' 326elif"k"in type_mods: 327 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 328else: 329return None 330 pattern = r""" 331 \$ # Starts with a dollar, followed by... 332 (%s) # one of the keywords, followed by... 333 (:[^$\n]+)? # possibly an old expansion, followed by... 334 \$ # another dollar 335 """% kwords 336return pattern 337else: 338return None 339 340# 341# Given a file, return a regexp matching the possible 342# RCS keywords that will be expanded, or None for files 343# with kw expansion turned off. 344# 345defp4_keywords_regexp_for_file(file): 346if not os.path.exists(file): 347return None 348else: 349(type_base, type_mods) =split_p4_type(p4_type(file)) 350returnp4_keywords_regexp_for_type(type_base, type_mods) 351 352defsetP4ExecBit(file, mode): 353# Reopens an already open file and changes the execute bit to match 354# the execute bit setting in the passed in mode. 355 356 p4Type ="+x" 357 358if notisModeExec(mode): 359 p4Type =getP4OpenedType(file) 360 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 361 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 362if p4Type[-1] =="+": 363 p4Type = p4Type[0:-1] 364 365p4_reopen(p4Type,file) 366 367defgetP4OpenedType(file): 368# Returns the perforce file type for the given file. 369 370 result =p4_read_pipe(["opened",wildcard_encode(file)]) 371 match = re.match(".*\((.+)\)\r?$", result) 372if match: 373return match.group(1) 374else: 375die("Could not determine file type for%s(result: '%s')"% (file, result)) 376 377# Return the set of all p4 labels 378defgetP4Labels(depotPaths): 379 labels =set() 380ifisinstance(depotPaths,basestring): 381 depotPaths = [depotPaths] 382 383for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 384 label = l['label'] 385 labels.add(label) 386 387return labels 388 389# Return the set of all git tags 390defgetGitTags(): 391 gitTags =set() 392for line inread_pipe_lines(["git","tag"]): 393 tag = line.strip() 394 gitTags.add(tag) 395return gitTags 396 397defdiffTreePattern(): 398# This is a simple generator for the diff tree regex pattern. This could be 399# a class variable if this and parseDiffTreeEntry were a part of a class. 400 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 401while True: 402yield pattern 403 404defparseDiffTreeEntry(entry): 405"""Parses a single diff tree entry into its component elements. 406 407 See git-diff-tree(1) manpage for details about the format of the diff 408 output. This method returns a dictionary with the following elements: 409 410 src_mode - The mode of the source file 411 dst_mode - The mode of the destination file 412 src_sha1 - The sha1 for the source file 413 dst_sha1 - The sha1 fr the destination file 414 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 415 status_score - The score for the status (applicable for 'C' and 'R' 416 statuses). This is None if there is no score. 417 src - The path for the source file. 418 dst - The path for the destination file. This is only present for 419 copy or renames. If it is not present, this is None. 420 421 If the pattern is not matched, None is returned.""" 422 423 match =diffTreePattern().next().match(entry) 424if match: 425return{ 426'src_mode': match.group(1), 427'dst_mode': match.group(2), 428'src_sha1': match.group(3), 429'dst_sha1': match.group(4), 430'status': match.group(5), 431'status_score': match.group(6), 432'src': match.group(7), 433'dst': match.group(10) 434} 435return None 436 437defisModeExec(mode): 438# Returns True if the given git mode represents an executable file, 439# otherwise False. 440return mode[-3:] =="755" 441 442defisModeExecChanged(src_mode, dst_mode): 443returnisModeExec(src_mode) !=isModeExec(dst_mode) 444 445defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 446 447ifisinstance(cmd,basestring): 448 cmd ="-G "+ cmd 449 expand =True 450else: 451 cmd = ["-G"] + cmd 452 expand =False 453 454 cmd =p4_build_cmd(cmd) 455if verbose: 456 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 457 458# Use a temporary file to avoid deadlocks without 459# subprocess.communicate(), which would put another copy 460# of stdout into memory. 461 stdin_file =None 462if stdin is not None: 463 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 464ifisinstance(stdin,basestring): 465 stdin_file.write(stdin) 466else: 467for i in stdin: 468 stdin_file.write(i +'\n') 469 stdin_file.flush() 470 stdin_file.seek(0) 471 472 p4 = subprocess.Popen(cmd, 473 shell=expand, 474 stdin=stdin_file, 475 stdout=subprocess.PIPE) 476 477 result = [] 478try: 479while True: 480 entry = marshal.load(p4.stdout) 481if cb is not None: 482cb(entry) 483else: 484 result.append(entry) 485exceptEOFError: 486pass 487 exitCode = p4.wait() 488if exitCode !=0: 489 entry = {} 490 entry["p4ExitCode"] = exitCode 491 result.append(entry) 492 493return result 494 495defp4Cmd(cmd): 496list=p4CmdList(cmd) 497 result = {} 498for entry inlist: 499 result.update(entry) 500return result; 501 502defp4Where(depotPath): 503if not depotPath.endswith("/"): 504 depotPath +="/" 505 depotPath = depotPath +"..." 506 outputList =p4CmdList(["where", depotPath]) 507 output =None 508for entry in outputList: 509if"depotFile"in entry: 510if entry["depotFile"] == depotPath: 511 output = entry 512break 513elif"data"in entry: 514 data = entry.get("data") 515 space = data.find(" ") 516if data[:space] == depotPath: 517 output = entry 518break 519if output ==None: 520return"" 521if output["code"] =="error": 522return"" 523 clientPath ="" 524if"path"in output: 525 clientPath = output.get("path") 526elif"data"in output: 527 data = output.get("data") 528 lastSpace = data.rfind(" ") 529 clientPath = data[lastSpace +1:] 530 531if clientPath.endswith("..."): 532 clientPath = clientPath[:-3] 533return clientPath 534 535defcurrentGitBranch(): 536returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 537 538defisValidGitDir(path): 539if(os.path.exists(path +"/HEAD") 540and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 541return True; 542return False 543 544defparseRevision(ref): 545returnread_pipe("git rev-parse%s"% ref).strip() 546 547defbranchExists(ref): 548 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 549 ignore_error=True) 550returnlen(rev) >0 551 552defextractLogMessageFromGitCommit(commit): 553 logMessage ="" 554 555## fixme: title is first line of commit, not 1st paragraph. 556 foundTitle =False 557for log inread_pipe_lines("git cat-file commit%s"% commit): 558if not foundTitle: 559iflen(log) ==1: 560 foundTitle =True 561continue 562 563 logMessage += log 564return logMessage 565 566defextractSettingsGitLog(log): 567 values = {} 568for line in log.split("\n"): 569 line = line.strip() 570 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 571if not m: 572continue 573 574 assignments = m.group(1).split(':') 575for a in assignments: 576 vals = a.split('=') 577 key = vals[0].strip() 578 val = ('='.join(vals[1:])).strip() 579if val.endswith('\"')and val.startswith('"'): 580 val = val[1:-1] 581 582 values[key] = val 583 584 paths = values.get("depot-paths") 585if not paths: 586 paths = values.get("depot-path") 587if paths: 588 values['depot-paths'] = paths.split(',') 589return values 590 591defgitBranchExists(branch): 592 proc = subprocess.Popen(["git","rev-parse", branch], 593 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 594return proc.wait() ==0; 595 596_gitConfig = {} 597 598defgitConfig(key): 599if not _gitConfig.has_key(key): 600 cmd = ["git","config", key ] 601 s =read_pipe(cmd, ignore_error=True) 602 _gitConfig[key] = s.strip() 603return _gitConfig[key] 604 605defgitConfigBool(key): 606"""Return a bool, using git config --bool. It is True only if the 607 variable is set to true, and False if set to false or not present 608 in the config.""" 609 610if not _gitConfig.has_key(key): 611 cmd = ["git","config","--bool", key ] 612 s =read_pipe(cmd, ignore_error=True) 613 v = s.strip() 614 _gitConfig[key] = v =="true" 615return _gitConfig[key] 616 617defgitConfigList(key): 618if not _gitConfig.has_key(key): 619 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 620 _gitConfig[key] = s.strip().split(os.linesep) 621return _gitConfig[key] 622 623defp4BranchesInGit(branchesAreInRemotes=True): 624"""Find all the branches whose names start with "p4/", looking 625 in remotes or heads as specified by the argument. Return 626 a dictionary of{ branch: revision }for each one found. 627 The branch names are the short names, without any 628 "p4/" prefix.""" 629 630 branches = {} 631 632 cmdline ="git rev-parse --symbolic " 633if branchesAreInRemotes: 634 cmdline +="--remotes" 635else: 636 cmdline +="--branches" 637 638for line inread_pipe_lines(cmdline): 639 line = line.strip() 640 641# only import to p4/ 642if not line.startswith('p4/'): 643continue 644# special symbolic ref to p4/master 645if line =="p4/HEAD": 646continue 647 648# strip off p4/ prefix 649 branch = line[len("p4/"):] 650 651 branches[branch] =parseRevision(line) 652 653return branches 654 655defbranch_exists(branch): 656"""Make sure that the given ref name really exists.""" 657 658 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 659 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 660 out, _ = p.communicate() 661if p.returncode: 662return False 663# expect exactly one line of output: the branch name 664return out.rstrip() == branch 665 666deffindUpstreamBranchPoint(head ="HEAD"): 667 branches =p4BranchesInGit() 668# map from depot-path to branch name 669 branchByDepotPath = {} 670for branch in branches.keys(): 671 tip = branches[branch] 672 log =extractLogMessageFromGitCommit(tip) 673 settings =extractSettingsGitLog(log) 674if settings.has_key("depot-paths"): 675 paths =",".join(settings["depot-paths"]) 676 branchByDepotPath[paths] ="remotes/p4/"+ branch 677 678 settings =None 679 parent =0 680while parent <65535: 681 commit = head +"~%s"% parent 682 log =extractLogMessageFromGitCommit(commit) 683 settings =extractSettingsGitLog(log) 684if settings.has_key("depot-paths"): 685 paths =",".join(settings["depot-paths"]) 686if branchByDepotPath.has_key(paths): 687return[branchByDepotPath[paths], settings] 688 689 parent = parent +1 690 691return["", settings] 692 693defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 694if not silent: 695print("Creating/updating branch(es) in%sbased on origin branch(es)" 696% localRefPrefix) 697 698 originPrefix ="origin/p4/" 699 700for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 701 line = line.strip() 702if(not line.startswith(originPrefix))or line.endswith("HEAD"): 703continue 704 705 headName = line[len(originPrefix):] 706 remoteHead = localRefPrefix + headName 707 originHead = line 708 709 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 710if(not original.has_key('depot-paths') 711or not original.has_key('change')): 712continue 713 714 update =False 715if notgitBranchExists(remoteHead): 716if verbose: 717print"creating%s"% remoteHead 718 update =True 719else: 720 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 721if settings.has_key('change') >0: 722if settings['depot-paths'] == original['depot-paths']: 723 originP4Change =int(original['change']) 724 p4Change =int(settings['change']) 725if originP4Change > p4Change: 726print("%s(%s) is newer than%s(%s). " 727"Updating p4 branch from origin." 728% (originHead, originP4Change, 729 remoteHead, p4Change)) 730 update =True 731else: 732print("Ignoring:%swas imported from%swhile " 733"%swas imported from%s" 734% (originHead,','.join(original['depot-paths']), 735 remoteHead,','.join(settings['depot-paths']))) 736 737if update: 738system("git update-ref%s %s"% (remoteHead, originHead)) 739 740deforiginP4BranchesExist(): 741returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 742 743defp4ChangesForPaths(depotPaths, changeRange): 744assert depotPaths 745 cmd = ['changes'] 746for p in depotPaths: 747 cmd += ["%s...%s"% (p, changeRange)] 748 output =p4_read_pipe_lines(cmd) 749 750 changes = {} 751for line in output: 752 changeNum =int(line.split(" ")[1]) 753 changes[changeNum] =True 754 755 changelist = changes.keys() 756 changelist.sort() 757return changelist 758 759defp4PathStartsWith(path, prefix): 760# This method tries to remedy a potential mixed-case issue: 761# 762# If UserA adds //depot/DirA/file1 763# and UserB adds //depot/dira/file2 764# 765# we may or may not have a problem. If you have core.ignorecase=true, 766# we treat DirA and dira as the same directory 767ifgitConfigBool("core.ignorecase"): 768return path.lower().startswith(prefix.lower()) 769return path.startswith(prefix) 770 771defgetClientSpec(): 772"""Look at the p4 client spec, create a View() object that contains 773 all the mappings, and return it.""" 774 775 specList =p4CmdList("client -o") 776iflen(specList) !=1: 777die('Output from "client -o" is%dlines, expecting 1'% 778len(specList)) 779 780# dictionary of all client parameters 781 entry = specList[0] 782 783# just the keys that start with "View" 784 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 785 786# hold this new View 787 view =View() 788 789# append the lines, in order, to the view 790for view_num inrange(len(view_keys)): 791 k ="View%d"% view_num 792if k not in view_keys: 793die("Expected view key%smissing"% k) 794 view.append(entry[k]) 795 796return view 797 798defgetClientRoot(): 799"""Grab the client directory.""" 800 801 output =p4CmdList("client -o") 802iflen(output) !=1: 803die('Output from "client -o" is%dlines, expecting 1'%len(output)) 804 805 entry = output[0] 806if"Root"not in entry: 807die('Client has no "Root"') 808 809return entry["Root"] 810 811# 812# P4 wildcards are not allowed in filenames. P4 complains 813# if you simply add them, but you can force it with "-f", in 814# which case it translates them into %xx encoding internally. 815# 816defwildcard_decode(path): 817# Search for and fix just these four characters. Do % last so 818# that fixing it does not inadvertently create new %-escapes. 819# Cannot have * in a filename in windows; untested as to 820# what p4 would do in such a case. 821if not platform.system() =="Windows": 822 path = path.replace("%2A","*") 823 path = path.replace("%23","#") \ 824.replace("%40","@") \ 825.replace("%25","%") 826return path 827 828defwildcard_encode(path): 829# do % first to avoid double-encoding the %s introduced here 830 path = path.replace("%","%25") \ 831.replace("*","%2A") \ 832.replace("#","%23") \ 833.replace("@","%40") 834return path 835 836defwildcard_present(path): 837 m = re.search("[*#@%]", path) 838return m is not None 839 840class Command: 841def__init__(self): 842 self.usage ="usage: %prog [options]" 843 self.needsGit =True 844 self.verbose =False 845 846class P4UserMap: 847def__init__(self): 848 self.userMapFromPerforceServer =False 849 self.myP4UserId =None 850 851defp4UserId(self): 852if self.myP4UserId: 853return self.myP4UserId 854 855 results =p4CmdList("user -o") 856for r in results: 857if r.has_key('User'): 858 self.myP4UserId = r['User'] 859return r['User'] 860die("Could not find your p4 user id") 861 862defp4UserIsMe(self, p4User): 863# return True if the given p4 user is actually me 864 me = self.p4UserId() 865if not p4User or p4User != me: 866return False 867else: 868return True 869 870defgetUserCacheFilename(self): 871 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 872return home +"/.gitp4-usercache.txt" 873 874defgetUserMapFromPerforceServer(self): 875if self.userMapFromPerforceServer: 876return 877 self.users = {} 878 self.emails = {} 879 880for output inp4CmdList("users"): 881if not output.has_key("User"): 882continue 883 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 884 self.emails[output["Email"]] = output["User"] 885 886 887 s ='' 888for(key, val)in self.users.items(): 889 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 890 891open(self.getUserCacheFilename(),"wb").write(s) 892 self.userMapFromPerforceServer =True 893 894defloadUserMapFromCache(self): 895 self.users = {} 896 self.userMapFromPerforceServer =False 897try: 898 cache =open(self.getUserCacheFilename(),"rb") 899 lines = cache.readlines() 900 cache.close() 901for line in lines: 902 entry = line.strip().split("\t") 903 self.users[entry[0]] = entry[1] 904exceptIOError: 905 self.getUserMapFromPerforceServer() 906 907classP4Debug(Command): 908def__init__(self): 909 Command.__init__(self) 910 self.options = [] 911 self.description ="A tool to debug the output of p4 -G." 912 self.needsGit =False 913 914defrun(self, args): 915 j =0 916for output inp4CmdList(args): 917print'Element:%d'% j 918 j +=1 919print output 920return True 921 922classP4RollBack(Command): 923def__init__(self): 924 Command.__init__(self) 925 self.options = [ 926 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 927] 928 self.description ="A tool to debug the multi-branch import. Don't use :)" 929 self.rollbackLocalBranches =False 930 931defrun(self, args): 932iflen(args) !=1: 933return False 934 maxChange =int(args[0]) 935 936if"p4ExitCode"inp4Cmd("changes -m 1"): 937die("Problems executing p4"); 938 939if self.rollbackLocalBranches: 940 refPrefix ="refs/heads/" 941 lines =read_pipe_lines("git rev-parse --symbolic --branches") 942else: 943 refPrefix ="refs/remotes/" 944 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 945 946for line in lines: 947if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 948 line = line.strip() 949 ref = refPrefix + line 950 log =extractLogMessageFromGitCommit(ref) 951 settings =extractSettingsGitLog(log) 952 953 depotPaths = settings['depot-paths'] 954 change = settings['change'] 955 956 changed =False 957 958iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 959for p in depotPaths]))) ==0: 960print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 961system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 962continue 963 964while change andint(change) > maxChange: 965 changed =True 966if self.verbose: 967print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 968system("git update-ref%s\"%s^\""% (ref, ref)) 969 log =extractLogMessageFromGitCommit(ref) 970 settings =extractSettingsGitLog(log) 971 972 973 depotPaths = settings['depot-paths'] 974 change = settings['change'] 975 976if changed: 977print"%srewound to%s"% (ref, change) 978 979return True 980 981classP4Submit(Command, P4UserMap): 982 983 conflict_behavior_choices = ("ask","skip","quit") 984 985def__init__(self): 986 Command.__init__(self) 987 P4UserMap.__init__(self) 988 self.options = [ 989 optparse.make_option("--origin", dest="origin"), 990 optparse.make_option("-M", dest="detectRenames", action="store_true"), 991# preserve the user, requires relevant p4 permissions 992 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 993 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 994 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 995 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 996 optparse.make_option("--conflict", dest="conflict_behavior", 997 choices=self.conflict_behavior_choices), 998 optparse.make_option("--branch", dest="branch"), 999]1000 self.description ="Submit changes from git to the perforce depot."1001 self.usage +=" [name of git branch to submit into perforce depot]"1002 self.origin =""1003 self.detectRenames =False1004 self.preserveUser =gitConfigBool("git-p4.preserveUser")1005 self.dry_run =False1006 self.prepare_p4_only =False1007 self.conflict_behavior =None1008 self.isWindows = (platform.system() =="Windows")1009 self.exportLabels =False1010 self.p4HasMoveCommand =p4_has_move_command()1011 self.branch =None10121013defcheck(self):1014iflen(p4CmdList("opened ...")) >0:1015die("You have files opened with perforce! Close them before starting the sync.")10161017defseparate_jobs_from_description(self, message):1018"""Extract and return a possible Jobs field in the commit1019 message. It goes into a separate section in the p4 change1020 specification.10211022 A jobs line starts with "Jobs:" and looks like a new field1023 in a form. Values are white-space separated on the same1024 line or on following lines that start with a tab.10251026 This does not parse and extract the full git commit message1027 like a p4 form. It just sees the Jobs: line as a marker1028 to pass everything from then on directly into the p4 form,1029 but outside the description section.10301031 Return a tuple (stripped log message, jobs string)."""10321033 m = re.search(r'^Jobs:', message, re.MULTILINE)1034if m is None:1035return(message,None)10361037 jobtext = message[m.start():]1038 stripped_message = message[:m.start()].rstrip()1039return(stripped_message, jobtext)10401041defprepareLogMessage(self, template, message, jobs):1042"""Edits the template returned from "p4 change -o" to insert1043 the message in the Description field, and the jobs text in1044 the Jobs field."""1045 result =""10461047 inDescriptionSection =False10481049for line in template.split("\n"):1050if line.startswith("#"):1051 result += line +"\n"1052continue10531054if inDescriptionSection:1055if line.startswith("Files:")or line.startswith("Jobs:"):1056 inDescriptionSection =False1057# insert Jobs section1058if jobs:1059 result += jobs +"\n"1060else:1061continue1062else:1063if line.startswith("Description:"):1064 inDescriptionSection =True1065 line +="\n"1066for messageLine in message.split("\n"):1067 line +="\t"+ messageLine +"\n"10681069 result += line +"\n"10701071return result10721073defpatchRCSKeywords(self,file, pattern):1074# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1075(handle, outFileName) = tempfile.mkstemp(dir='.')1076try:1077 outFile = os.fdopen(handle,"w+")1078 inFile =open(file,"r")1079 regexp = re.compile(pattern, re.VERBOSE)1080for line in inFile.readlines():1081 line = regexp.sub(r'$\1$', line)1082 outFile.write(line)1083 inFile.close()1084 outFile.close()1085# Forcibly overwrite the original file1086 os.unlink(file)1087 shutil.move(outFileName,file)1088except:1089# cleanup our temporary file1090 os.unlink(outFileName)1091print"Failed to strip RCS keywords in%s"%file1092raise10931094print"Patched up RCS keywords in%s"%file10951096defp4UserForCommit(self,id):1097# Return the tuple (perforce user,git email) for a given git commit id1098 self.getUserMapFromPerforceServer()1099 gitEmail =read_pipe(["git","log","--max-count=1",1100"--format=%ae",id])1101 gitEmail = gitEmail.strip()1102if not self.emails.has_key(gitEmail):1103return(None,gitEmail)1104else:1105return(self.emails[gitEmail],gitEmail)11061107defcheckValidP4Users(self,commits):1108# check if any git authors cannot be mapped to p4 users1109foridin commits:1110(user,email) = self.p4UserForCommit(id)1111if not user:1112 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1113ifgitConfigBool("git-p4.allowMissingP4Users"):1114print"%s"% msg1115else:1116die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11171118deflastP4Changelist(self):1119# Get back the last changelist number submitted in this client spec. This1120# then gets used to patch up the username in the change. If the same1121# client spec is being used by multiple processes then this might go1122# wrong.1123 results =p4CmdList("client -o")# find the current client1124 client =None1125for r in results:1126if r.has_key('Client'):1127 client = r['Client']1128break1129if not client:1130die("could not get client spec")1131 results =p4CmdList(["changes","-c", client,"-m","1"])1132for r in results:1133if r.has_key('change'):1134return r['change']1135die("Could not get changelist number for last submit - cannot patch up user details")11361137defmodifyChangelistUser(self, changelist, newUser):1138# fixup the user field of a changelist after it has been submitted.1139 changes =p4CmdList("change -o%s"% changelist)1140iflen(changes) !=1:1141die("Bad output from p4 change modifying%sto user%s"%1142(changelist, newUser))11431144 c = changes[0]1145if c['User'] == newUser:return# nothing to do1146 c['User'] = newUser1147input= marshal.dumps(c)11481149 result =p4CmdList("change -f -i", stdin=input)1150for r in result:1151if r.has_key('code'):1152if r['code'] =='error':1153die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1154if r.has_key('data'):1155print("Updated user field for changelist%sto%s"% (changelist, newUser))1156return1157die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11581159defcanChangeChangelists(self):1160# check to see if we have p4 admin or super-user permissions, either of1161# which are required to modify changelists.1162 results =p4CmdList(["protects", self.depotPath])1163for r in results:1164if r.has_key('perm'):1165if r['perm'] =='admin':1166return11167if r['perm'] =='super':1168return11169return011701171defprepareSubmitTemplate(self):1172"""Run "p4 change -o" to grab a change specification template.1173 This does not use "p4 -G", as it is nice to keep the submission1174 template in original order, since a human might edit it.11751176 Remove lines in the Files section that show changes to files1177 outside the depot path we're committing into."""11781179 template =""1180 inFilesSection =False1181for line inp4_read_pipe_lines(['change','-o']):1182if line.endswith("\r\n"):1183 line = line[:-2] +"\n"1184if inFilesSection:1185if line.startswith("\t"):1186# path starts and ends with a tab1187 path = line[1:]1188 lastTab = path.rfind("\t")1189if lastTab != -1:1190 path = path[:lastTab]1191if notp4PathStartsWith(path, self.depotPath):1192continue1193else:1194 inFilesSection =False1195else:1196if line.startswith("Files:"):1197 inFilesSection =True11981199 template += line12001201return template12021203defedit_template(self, template_file):1204"""Invoke the editor to let the user change the submission1205 message. Return true if okay to continue with the submit."""12061207# if configured to skip the editing part, just submit1208ifgitConfigBool("git-p4.skipSubmitEdit"):1209return True12101211# look at the modification time, to check later if the user saved1212# the file1213 mtime = os.stat(template_file).st_mtime12141215# invoke the editor1216if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1217 editor = os.environ.get("P4EDITOR")1218else:1219 editor =read_pipe("git var GIT_EDITOR").strip()1220system(editor +" "+ template_file)12211222# If the file was not saved, prompt to see if this patch should1223# be skipped. But skip this verification step if configured so.1224ifgitConfigBool("git-p4.skipSubmitEditCheck"):1225return True12261227# modification time updated means user saved the file1228if os.stat(template_file).st_mtime > mtime:1229return True12301231while True:1232 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1233if response =='y':1234return True1235if response =='n':1236return False12371238defapplyCommit(self,id):1239"""Apply one commit, return True if it succeeded."""12401241print"Applying",read_pipe(["git","show","-s",1242"--format=format:%h%s",id])12431244(p4User, gitEmail) = self.p4UserForCommit(id)12451246 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1247 filesToAdd =set()1248 filesToDelete =set()1249 editedFiles =set()1250 pureRenameCopy =set()1251 filesToChangeExecBit = {}12521253for line in diff:1254 diff =parseDiffTreeEntry(line)1255 modifier = diff['status']1256 path = diff['src']1257if modifier =="M":1258p4_edit(path)1259ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1260 filesToChangeExecBit[path] = diff['dst_mode']1261 editedFiles.add(path)1262elif modifier =="A":1263 filesToAdd.add(path)1264 filesToChangeExecBit[path] = diff['dst_mode']1265if path in filesToDelete:1266 filesToDelete.remove(path)1267elif modifier =="D":1268 filesToDelete.add(path)1269if path in filesToAdd:1270 filesToAdd.remove(path)1271elif modifier =="C":1272 src, dest = diff['src'], diff['dst']1273p4_integrate(src, dest)1274 pureRenameCopy.add(dest)1275if diff['src_sha1'] != diff['dst_sha1']:1276p4_edit(dest)1277 pureRenameCopy.discard(dest)1278ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1279p4_edit(dest)1280 pureRenameCopy.discard(dest)1281 filesToChangeExecBit[dest] = diff['dst_mode']1282if self.isWindows:1283# turn off read-only attribute1284 os.chmod(dest, stat.S_IWRITE)1285 os.unlink(dest)1286 editedFiles.add(dest)1287elif modifier =="R":1288 src, dest = diff['src'], diff['dst']1289if self.p4HasMoveCommand:1290p4_edit(src)# src must be open before move1291p4_move(src, dest)# opens for (move/delete, move/add)1292else:1293p4_integrate(src, dest)1294if diff['src_sha1'] != diff['dst_sha1']:1295p4_edit(dest)1296else:1297 pureRenameCopy.add(dest)1298ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1299if not self.p4HasMoveCommand:1300p4_edit(dest)# with move: already open, writable1301 filesToChangeExecBit[dest] = diff['dst_mode']1302if not self.p4HasMoveCommand:1303if self.isWindows:1304 os.chmod(dest, stat.S_IWRITE)1305 os.unlink(dest)1306 filesToDelete.add(src)1307 editedFiles.add(dest)1308else:1309die("unknown modifier%sfor%s"% (modifier, path))13101311 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1312 patchcmd = diffcmd +" | git apply "1313 tryPatchCmd = patchcmd +"--check -"1314 applyPatchCmd = patchcmd +"--check --apply -"1315 patch_succeeded =True13161317if os.system(tryPatchCmd) !=0:1318 fixed_rcs_keywords =False1319 patch_succeeded =False1320print"Unfortunately applying the change failed!"13211322# Patch failed, maybe it's just RCS keyword woes. Look through1323# the patch to see if that's possible.1324ifgitConfigBool("git-p4.attemptRCSCleanup"):1325file=None1326 pattern =None1327 kwfiles = {}1328forfilein editedFiles | filesToDelete:1329# did this file's delta contain RCS keywords?1330 pattern =p4_keywords_regexp_for_file(file)13311332if pattern:1333# this file is a possibility...look for RCS keywords.1334 regexp = re.compile(pattern, re.VERBOSE)1335for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1336if regexp.search(line):1337if verbose:1338print"got keyword match on%sin%sin%s"% (pattern, line,file)1339 kwfiles[file] = pattern1340break13411342forfilein kwfiles:1343if verbose:1344print"zapping%swith%s"% (line,pattern)1345# File is being deleted, so not open in p4. Must1346# disable the read-only bit on windows.1347if self.isWindows andfilenot in editedFiles:1348 os.chmod(file, stat.S_IWRITE)1349 self.patchRCSKeywords(file, kwfiles[file])1350 fixed_rcs_keywords =True13511352if fixed_rcs_keywords:1353print"Retrying the patch with RCS keywords cleaned up"1354if os.system(tryPatchCmd) ==0:1355 patch_succeeded =True13561357if not patch_succeeded:1358for f in editedFiles:1359p4_revert(f)1360return False13611362#1363# Apply the patch for real, and do add/delete/+x handling.1364#1365system(applyPatchCmd)13661367for f in filesToAdd:1368p4_add(f)1369for f in filesToDelete:1370p4_revert(f)1371p4_delete(f)13721373# Set/clear executable bits1374for f in filesToChangeExecBit.keys():1375 mode = filesToChangeExecBit[f]1376setP4ExecBit(f, mode)13771378#1379# Build p4 change description, starting with the contents1380# of the git commit message.1381#1382 logMessage =extractLogMessageFromGitCommit(id)1383 logMessage = logMessage.strip()1384(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13851386 template = self.prepareSubmitTemplate()1387 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13881389if self.preserveUser:1390 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13911392if self.checkAuthorship and not self.p4UserIsMe(p4User):1393 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1394 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1395 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13961397 separatorLine ="######## everything below this line is just the diff #######\n"13981399# diff1400if os.environ.has_key("P4DIFF"):1401del(os.environ["P4DIFF"])1402 diff =""1403for editedFile in editedFiles:1404 diff +=p4_read_pipe(['diff','-du',1405wildcard_encode(editedFile)])14061407# new file diff1408 newdiff =""1409for newFile in filesToAdd:1410 newdiff +="==== new file ====\n"1411 newdiff +="--- /dev/null\n"1412 newdiff +="+++%s\n"% newFile1413 f =open(newFile,"r")1414for line in f.readlines():1415 newdiff +="+"+ line1416 f.close()14171418# change description file: submitTemplate, separatorLine, diff, newdiff1419(handle, fileName) = tempfile.mkstemp()1420 tmpFile = os.fdopen(handle,"w+")1421if self.isWindows:1422 submitTemplate = submitTemplate.replace("\n","\r\n")1423 separatorLine = separatorLine.replace("\n","\r\n")1424 newdiff = newdiff.replace("\n","\r\n")1425 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1426 tmpFile.close()14271428if self.prepare_p4_only:1429#1430# Leave the p4 tree prepared, and the submit template around1431# and let the user decide what to do next1432#1433print1434print"P4 workspace prepared for submission."1435print"To submit or revert, go to client workspace"1436print" "+ self.clientPath1437print1438print"To submit, use\"p4 submit\"to write a new description,"1439print"or\"p4 submit -i%s\"to use the one prepared by" \1440"\"git p4\"."% fileName1441print"You can delete the file\"%s\"when finished."% fileName14421443if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1444print"To preserve change ownership by user%s, you must\n" \1445"do\"p4 change -f <change>\"after submitting and\n" \1446"edit the User field."1447if pureRenameCopy:1448print"After submitting, renamed files must be re-synced."1449print"Invoke\"p4 sync -f\"on each of these files:"1450for f in pureRenameCopy:1451print" "+ f14521453print1454print"To revert the changes, use\"p4 revert ...\", and delete"1455print"the submit template file\"%s\""% fileName1456if filesToAdd:1457print"Since the commit adds new files, they must be deleted:"1458for f in filesToAdd:1459print" "+ f1460print1461return True14621463#1464# Let the user edit the change description, then submit it.1465#1466if self.edit_template(fileName):1467# read the edited message and submit1468 ret =True1469 tmpFile =open(fileName,"rb")1470 message = tmpFile.read()1471 tmpFile.close()1472 submitTemplate = message[:message.index(separatorLine)]1473if self.isWindows:1474 submitTemplate = submitTemplate.replace("\r\n","\n")1475p4_write_pipe(['submit','-i'], submitTemplate)14761477if self.preserveUser:1478if p4User:1479# Get last changelist number. Cannot easily get it from1480# the submit command output as the output is1481# unmarshalled.1482 changelist = self.lastP4Changelist()1483 self.modifyChangelistUser(changelist, p4User)14841485# The rename/copy happened by applying a patch that created a1486# new file. This leaves it writable, which confuses p4.1487for f in pureRenameCopy:1488p4_sync(f,"-f")14891490else:1491# skip this patch1492 ret =False1493print"Submission cancelled, undoing p4 changes."1494for f in editedFiles:1495p4_revert(f)1496for f in filesToAdd:1497p4_revert(f)1498 os.remove(f)1499for f in filesToDelete:1500p4_revert(f)15011502 os.remove(fileName)1503return ret15041505# Export git tags as p4 labels. Create a p4 label and then tag1506# with that.1507defexportGitTags(self, gitTags):1508 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1509iflen(validLabelRegexp) ==0:1510 validLabelRegexp = defaultLabelRegexp1511 m = re.compile(validLabelRegexp)15121513for name in gitTags:15141515if not m.match(name):1516if verbose:1517print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1518continue15191520# Get the p4 commit this corresponds to1521 logMessage =extractLogMessageFromGitCommit(name)1522 values =extractSettingsGitLog(logMessage)15231524if not values.has_key('change'):1525# a tag pointing to something not sent to p4; ignore1526if verbose:1527print"git tag%sdoes not give a p4 commit"% name1528continue1529else:1530 changelist = values['change']15311532# Get the tag details.1533 inHeader =True1534 isAnnotated =False1535 body = []1536for l inread_pipe_lines(["git","cat-file","-p", name]):1537 l = l.strip()1538if inHeader:1539if re.match(r'tag\s+', l):1540 isAnnotated =True1541elif re.match(r'\s*$', l):1542 inHeader =False1543continue1544else:1545 body.append(l)15461547if not isAnnotated:1548 body = ["lightweight tag imported by git p4\n"]15491550# Create the label - use the same view as the client spec we are using1551 clientSpec =getClientSpec()15521553 labelTemplate ="Label:%s\n"% name1554 labelTemplate +="Description:\n"1555for b in body:1556 labelTemplate +="\t"+ b +"\n"1557 labelTemplate +="View:\n"1558for mapping in clientSpec.mappings:1559 labelTemplate +="\t%s\n"% mapping.depot_side.path15601561if self.dry_run:1562print"Would create p4 label%sfor tag"% name1563elif self.prepare_p4_only:1564print"Not creating p4 label%sfor tag due to option" \1565" --prepare-p4-only"% name1566else:1567p4_write_pipe(["label","-i"], labelTemplate)15681569# Use the label1570p4_system(["tag","-l", name] +1571["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])15721573if verbose:1574print"created p4 label for tag%s"% name15751576defrun(self, args):1577iflen(args) ==0:1578 self.master =currentGitBranch()1579iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1580die("Detecting current git branch failed!")1581eliflen(args) ==1:1582 self.master = args[0]1583if notbranchExists(self.master):1584die("Branch%sdoes not exist"% self.master)1585else:1586return False15871588 allowSubmit =gitConfig("git-p4.allowSubmit")1589iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1590die("%sis not in git-p4.allowSubmit"% self.master)15911592[upstream, settings] =findUpstreamBranchPoint()1593 self.depotPath = settings['depot-paths'][0]1594iflen(self.origin) ==0:1595 self.origin = upstream15961597if self.preserveUser:1598if not self.canChangeChangelists():1599die("Cannot preserve user names without p4 super-user or admin permissions")16001601# if not set from the command line, try the config file1602if self.conflict_behavior is None:1603 val =gitConfig("git-p4.conflict")1604if val:1605if val not in self.conflict_behavior_choices:1606die("Invalid value '%s' for config git-p4.conflict"% val)1607else:1608 val ="ask"1609 self.conflict_behavior = val16101611if self.verbose:1612print"Origin branch is "+ self.origin16131614iflen(self.depotPath) ==0:1615print"Internal error: cannot locate perforce depot path from existing branches"1616 sys.exit(128)16171618 self.useClientSpec =False1619ifgitConfigBool("git-p4.useclientspec"):1620 self.useClientSpec =True1621if self.useClientSpec:1622 self.clientSpecDirs =getClientSpec()16231624if self.useClientSpec:1625# all files are relative to the client spec1626 self.clientPath =getClientRoot()1627else:1628 self.clientPath =p4Where(self.depotPath)16291630if self.clientPath =="":1631die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)16321633print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1634 self.oldWorkingDirectory = os.getcwd()16351636# ensure the clientPath exists1637 new_client_dir =False1638if not os.path.exists(self.clientPath):1639 new_client_dir =True1640 os.makedirs(self.clientPath)16411642chdir(self.clientPath, is_client_path=True)1643if self.dry_run:1644print"Would synchronize p4 checkout in%s"% self.clientPath1645else:1646print"Synchronizing p4 checkout..."1647if new_client_dir:1648# old one was destroyed, and maybe nobody told p41649p4_sync("...","-f")1650else:1651p4_sync("...")1652 self.check()16531654 commits = []1655for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1656 commits.append(line.strip())1657 commits.reverse()16581659if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1660 self.checkAuthorship =False1661else:1662 self.checkAuthorship =True16631664if self.preserveUser:1665 self.checkValidP4Users(commits)16661667#1668# Build up a set of options to be passed to diff when1669# submitting each commit to p4.1670#1671if self.detectRenames:1672# command-line -M arg1673 self.diffOpts ="-M"1674else:1675# If not explicitly set check the config variable1676 detectRenames =gitConfig("git-p4.detectRenames")16771678if detectRenames.lower() =="false"or detectRenames =="":1679 self.diffOpts =""1680elif detectRenames.lower() =="true":1681 self.diffOpts ="-M"1682else:1683 self.diffOpts ="-M%s"% detectRenames16841685# no command-line arg for -C or --find-copies-harder, just1686# config variables1687 detectCopies =gitConfig("git-p4.detectCopies")1688if detectCopies.lower() =="false"or detectCopies =="":1689pass1690elif detectCopies.lower() =="true":1691 self.diffOpts +=" -C"1692else:1693 self.diffOpts +=" -C%s"% detectCopies16941695ifgitConfigBool("git-p4.detectCopiesHarder"):1696 self.diffOpts +=" --find-copies-harder"16971698#1699# Apply the commits, one at a time. On failure, ask if should1700# continue to try the rest of the patches, or quit.1701#1702if self.dry_run:1703print"Would apply"1704 applied = []1705 last =len(commits) -11706for i, commit inenumerate(commits):1707if self.dry_run:1708print" ",read_pipe(["git","show","-s",1709"--format=format:%h%s", commit])1710 ok =True1711else:1712 ok = self.applyCommit(commit)1713if ok:1714 applied.append(commit)1715else:1716if self.prepare_p4_only and i < last:1717print"Processing only the first commit due to option" \1718" --prepare-p4-only"1719break1720if i < last:1721 quit =False1722while True:1723# prompt for what to do, or use the option/variable1724if self.conflict_behavior =="ask":1725print"What do you want to do?"1726 response =raw_input("[s]kip this commit but apply"1727" the rest, or [q]uit? ")1728if not response:1729continue1730elif self.conflict_behavior =="skip":1731 response ="s"1732elif self.conflict_behavior =="quit":1733 response ="q"1734else:1735die("Unknown conflict_behavior '%s'"%1736 self.conflict_behavior)17371738if response[0] =="s":1739print"Skipping this commit, but applying the rest"1740break1741if response[0] =="q":1742print"Quitting"1743 quit =True1744break1745if quit:1746break17471748chdir(self.oldWorkingDirectory)17491750if self.dry_run:1751pass1752elif self.prepare_p4_only:1753pass1754eliflen(commits) ==len(applied):1755print"All commits applied!"17561757 sync =P4Sync()1758if self.branch:1759 sync.branch = self.branch1760 sync.run([])17611762 rebase =P4Rebase()1763 rebase.rebase()17641765else:1766iflen(applied) ==0:1767print"No commits applied."1768else:1769print"Applied only the commits marked with '*':"1770for c in commits:1771if c in applied:1772 star ="*"1773else:1774 star =" "1775print star,read_pipe(["git","show","-s",1776"--format=format:%h%s", c])1777print"You will have to do 'git p4 sync' and rebase."17781779ifgitConfigBool("git-p4.exportLabels"):1780 self.exportLabels =True17811782if self.exportLabels:1783 p4Labels =getP4Labels(self.depotPath)1784 gitTags =getGitTags()17851786 missingGitTags = gitTags - p4Labels1787 self.exportGitTags(missingGitTags)17881789# exit with error unless everything applied perfecly1790iflen(commits) !=len(applied):1791 sys.exit(1)17921793return True17941795classView(object):1796"""Represent a p4 view ("p4 help views"), and map files in a1797 repo according to the view."""17981799classPath(object):1800"""A depot or client path, possibly containing wildcards.1801 The only one supported is ... at the end, currently.1802 Initialize with the full path, with //depot or //client."""18031804def__init__(self, path, is_depot):1805 self.path = path1806 self.is_depot = is_depot1807 self.find_wildcards()1808# remember the prefix bit, useful for relative mappings1809 m = re.match("(//[^/]+/)", self.path)1810if not m:1811die("Path%sdoes not start with //prefix/"% self.path)1812 prefix = m.group(1)1813if not self.is_depot:1814# strip //client/ on client paths1815 self.path = self.path[len(prefix):]18161817deffind_wildcards(self):1818"""Make sure wildcards are valid, and set up internal1819 variables."""18201821 self.ends_triple_dot =False1822# There are three wildcards allowed in p4 views1823# (see "p4 help views"). This code knows how to1824# handle "..." (only at the end), but cannot deal with1825# "%%n" or "*". Only check the depot_side, as p4 should1826# validate that the client_side matches too.1827if re.search(r'%%[1-9]', self.path):1828die("Can't handle%%n wildcards in view:%s"% self.path)1829if self.path.find("*") >=0:1830die("Can't handle * wildcards in view:%s"% self.path)1831 triple_dot_index = self.path.find("...")1832if triple_dot_index >=0:1833if triple_dot_index !=len(self.path) -3:1834die("Can handle only single ... wildcard, at end:%s"%1835 self.path)1836 self.ends_triple_dot =True18371838defensure_compatible(self, other_path):1839"""Make sure the wildcards agree."""1840if self.ends_triple_dot != other_path.ends_triple_dot:1841die("Both paths must end with ... if either does;\n"+1842"paths:%s %s"% (self.path, other_path.path))18431844defmatch_wildcards(self, test_path):1845"""See if this test_path matches us, and fill in the value1846 of the wildcards if so. Returns a tuple of1847 (True|False, wildcards[]). For now, only the ... at end1848 is supported, so at most one wildcard."""1849if self.ends_triple_dot:1850 dotless = self.path[:-3]1851if test_path.startswith(dotless):1852 wildcard = test_path[len(dotless):]1853return(True, [ wildcard ])1854else:1855if test_path == self.path:1856return(True, [])1857return(False, [])18581859defmatch(self, test_path):1860"""Just return if it matches; don't bother with the wildcards."""1861 b, _ = self.match_wildcards(test_path)1862return b18631864deffill_in_wildcards(self, wildcards):1865"""Return the relative path, with the wildcards filled in1866 if there are any."""1867if self.ends_triple_dot:1868return self.path[:-3] + wildcards[0]1869else:1870return self.path18711872classMapping(object):1873def__init__(self, depot_side, client_side, overlay, exclude):1874# depot_side is without the trailing /... if it had one1875 self.depot_side = View.Path(depot_side, is_depot=True)1876 self.client_side = View.Path(client_side, is_depot=False)1877 self.overlay = overlay # started with "+"1878 self.exclude = exclude # started with "-"1879assert not(self.overlay and self.exclude)1880 self.depot_side.ensure_compatible(self.client_side)18811882def__str__(self):1883 c =" "1884if self.overlay:1885 c ="+"1886if self.exclude:1887 c ="-"1888return"View.Mapping:%s%s->%s"% \1889(c, self.depot_side.path, self.client_side.path)18901891defmap_depot_to_client(self, depot_path):1892"""Calculate the client path if using this mapping on the1893 given depot path; does not consider the effect of other1894 mappings in a view. Even excluded mappings are returned."""1895 matches, wildcards = self.depot_side.match_wildcards(depot_path)1896if not matches:1897return""1898 client_path = self.client_side.fill_in_wildcards(wildcards)1899return client_path19001901#1902# View methods1903#1904def__init__(self):1905 self.mappings = []19061907defappend(self, view_line):1908"""Parse a view line, splitting it into depot and client1909 sides. Append to self.mappings, preserving order."""19101911# Split the view line into exactly two words. P4 enforces1912# structure on these lines that simplifies this quite a bit.1913#1914# Either or both words may be double-quoted.1915# Single quotes do not matter.1916# Double-quote marks cannot occur inside the words.1917# A + or - prefix is also inside the quotes.1918# There are no quotes unless they contain a space.1919# The line is already white-space stripped.1920# The two words are separated by a single space.1921#1922if view_line[0] =='"':1923# First word is double quoted. Find its end.1924 close_quote_index = view_line.find('"',1)1925if close_quote_index <=0:1926die("No first-word closing quote found:%s"% view_line)1927 depot_side = view_line[1:close_quote_index]1928# skip closing quote and space1929 rhs_index = close_quote_index +1+11930else:1931 space_index = view_line.find(" ")1932if space_index <=0:1933die("No word-splitting space found:%s"% view_line)1934 depot_side = view_line[0:space_index]1935 rhs_index = space_index +119361937if view_line[rhs_index] =='"':1938# Second word is double quoted. Make sure there is a1939# double quote at the end too.1940if not view_line.endswith('"'):1941die("View line with rhs quote should end with one:%s"%1942 view_line)1943# skip the quotes1944 client_side = view_line[rhs_index+1:-1]1945else:1946 client_side = view_line[rhs_index:]19471948# prefix + means overlay on previous mapping1949 overlay =False1950if depot_side.startswith("+"):1951 overlay =True1952 depot_side = depot_side[1:]19531954# prefix - means exclude this path1955 exclude =False1956if depot_side.startswith("-"):1957 exclude =True1958 depot_side = depot_side[1:]19591960 m = View.Mapping(depot_side, client_side, overlay, exclude)1961 self.mappings.append(m)19621963defmap_in_client(self, depot_path):1964"""Return the relative location in the client where this1965 depot file should live. Returns "" if the file should1966 not be mapped in the client."""19671968 paths_filled = []1969 client_path =""19701971# look at later entries first1972for m in self.mappings[::-1]:19731974# see where will this path end up in the client1975 p = m.map_depot_to_client(depot_path)19761977if p =="":1978# Depot path does not belong in client. Must remember1979# this, as previous items should not cause files to1980# exist in this path either. Remember that the list is1981# being walked from the end, which has higher precedence.1982# Overlap mappings do not exclude previous mappings.1983if not m.overlay:1984 paths_filled.append(m.client_side)19851986else:1987# This mapping matched; no need to search any further.1988# But, the mapping could be rejected if the client path1989# has already been claimed by an earlier mapping (i.e.1990# one later in the list, which we are walking backwards).1991 already_mapped_in_client =False1992for f in paths_filled:1993# this is View.Path.match1994if f.match(p):1995 already_mapped_in_client =True1996break1997if not already_mapped_in_client:1998# Include this file, unless it is from a line that1999# explicitly said to exclude it.2000if not m.exclude:2001 client_path = p20022003# a match, even if rejected, always stops the search2004break20052006return client_path20072008classP4Sync(Command, P4UserMap):2009 delete_actions = ("delete","move/delete","purge")20102011def__init__(self):2012 Command.__init__(self)2013 P4UserMap.__init__(self)2014 self.options = [2015 optparse.make_option("--branch", dest="branch"),2016 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2017 optparse.make_option("--changesfile", dest="changesFile"),2018 optparse.make_option("--silent", dest="silent", action="store_true"),2019 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2020 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2021 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2022help="Import into refs/heads/ , not refs/remotes"),2023 optparse.make_option("--max-changes", dest="maxChanges"),2024 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2025help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2026 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2027help="Only sync files that are included in the Perforce Client Spec")2028]2029 self.description ="""Imports from Perforce into a git repository.\n2030 example:2031 //depot/my/project/ -- to import the current head2032 //depot/my/project/@all -- to import everything2033 //depot/my/project/@1,6 -- to import only from revision 1 to 620342035 (a ... is not needed in the path p4 specification, it's added implicitly)"""20362037 self.usage +=" //depot/path[@revRange]"2038 self.silent =False2039 self.createdBranches =set()2040 self.committedChanges =set()2041 self.branch =""2042 self.detectBranches =False2043 self.detectLabels =False2044 self.importLabels =False2045 self.changesFile =""2046 self.syncWithOrigin =True2047 self.importIntoRemotes =True2048 self.maxChanges =""2049 self.keepRepoPath =False2050 self.depotPaths =None2051 self.p4BranchesInGit = []2052 self.cloneExclude = []2053 self.useClientSpec =False2054 self.useClientSpec_from_options =False2055 self.clientSpecDirs =None2056 self.tempBranches = []2057 self.tempBranchLocation ="git-p4-tmp"20582059ifgitConfig("git-p4.syncFromOrigin") =="false":2060 self.syncWithOrigin =False20612062# Force a checkpoint in fast-import and wait for it to finish2063defcheckpoint(self):2064 self.gitStream.write("checkpoint\n\n")2065 self.gitStream.write("progress checkpoint\n\n")2066 out = self.gitOutput.readline()2067if self.verbose:2068print"checkpoint finished: "+ out20692070defextractFilesFromCommit(self, commit):2071 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2072for path in self.cloneExclude]2073 files = []2074 fnum =02075while commit.has_key("depotFile%s"% fnum):2076 path = commit["depotFile%s"% fnum]20772078if[p for p in self.cloneExclude2079ifp4PathStartsWith(path, p)]:2080 found =False2081else:2082 found = [p for p in self.depotPaths2083ifp4PathStartsWith(path, p)]2084if not found:2085 fnum = fnum +12086continue20872088file= {}2089file["path"] = path2090file["rev"] = commit["rev%s"% fnum]2091file["action"] = commit["action%s"% fnum]2092file["type"] = commit["type%s"% fnum]2093 files.append(file)2094 fnum = fnum +12095return files20962097defstripRepoPath(self, path, prefixes):2098"""When streaming files, this is called to map a p4 depot path2099 to where it should go in git. The prefixes are either2100 self.depotPaths, or self.branchPrefixes in the case of2101 branch detection."""21022103if self.useClientSpec:2104# branch detection moves files up a level (the branch name)2105# from what client spec interpretation gives2106 path = self.clientSpecDirs.map_in_client(path)2107if self.detectBranches:2108for b in self.knownBranches:2109if path.startswith(b +"/"):2110 path = path[len(b)+1:]21112112elif self.keepRepoPath:2113# Preserve everything in relative path name except leading2114# //depot/; just look at first prefix as they all should2115# be in the same depot.2116 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2117ifp4PathStartsWith(path, depot):2118 path = path[len(depot):]21192120else:2121for p in prefixes:2122ifp4PathStartsWith(path, p):2123 path = path[len(p):]2124break21252126 path =wildcard_decode(path)2127return path21282129defsplitFilesIntoBranches(self, commit):2130"""Look at each depotFile in the commit to figure out to what2131 branch it belongs."""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 contains "target\n"; remove the newline2184 data =''.join(contents)2185 contents = [data[:-1]]21862187if type_base =="utf16":2188# p4 delivers different text in the python output to -G2189# than it does when using "print -o", or normal p4 client2190# operations. utf16 is converted to ascii or utf8, perhaps.2191# But ascii text saved as -t utf16 is completely mangled.2192# Invoke print -o to get the real contents.2193#2194# On windows, the newlines will always be mangled by print, so put2195# them back too. This is not needed to the cygwin windows version,2196# just the native "NT" type.2197#2198 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2199ifp4_version_string().find("/NT") >=0:2200 text = text.replace("\r\n","\n")2201 contents = [ text ]22022203if type_base =="apple":2204# Apple filetype files will be streamed as a concatenation of2205# its appledouble header and the contents. This is useless2206# on both macs and non-macs. If using "print -q -o xx", it2207# will create "xx" with the data, and "%xx" with the header.2208# This is also not very useful.2209#2210# Ideally, someday, this script can learn how to generate2211# appledouble files directly and import those to git, but2212# non-mac machines can never find a use for apple filetype.2213print"\nIgnoring apple filetype file%s"%file['depotFile']2214return22152216# Note that we do not try to de-mangle keywords on utf16 files,2217# even though in theory somebody may want that.2218 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2219if pattern:2220 regexp = re.compile(pattern, re.VERBOSE)2221 text =''.join(contents)2222 text = regexp.sub(r'$\1$', text)2223 contents = [ text ]22242225 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22262227# total length...2228 length =02229for d in contents:2230 length = length +len(d)22312232 self.gitStream.write("data%d\n"% length)2233for d in contents:2234 self.gitStream.write(d)2235 self.gitStream.write("\n")22362237defstreamOneP4Deletion(self,file):2238 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2239if verbose:2240 sys.stderr.write("delete%s\n"% relPath)2241 self.gitStream.write("D%s\n"% relPath)22422243# handle another chunk of streaming data2244defstreamP4FilesCb(self, marshalled):22452246# catch p4 errors and complain2247 err =None2248if"code"in marshalled:2249if marshalled["code"] =="error":2250if"data"in marshalled:2251 err = marshalled["data"].rstrip()2252if err:2253 f =None2254if self.stream_have_file_info:2255if"depotFile"in self.stream_file:2256 f = self.stream_file["depotFile"]2257# force a failure in fast-import, else an empty2258# commit will be made2259 self.gitStream.write("\n")2260 self.gitStream.write("die-now\n")2261 self.gitStream.close()2262# ignore errors, but make sure it exits first2263 self.importProcess.wait()2264if f:2265die("Error from p4 print for%s:%s"% (f, err))2266else:2267die("Error from p4 print:%s"% err)22682269if marshalled.has_key('depotFile')and self.stream_have_file_info:2270# start of a new file - output the old one first2271 self.streamOneP4File(self.stream_file, self.stream_contents)2272 self.stream_file = {}2273 self.stream_contents = []2274 self.stream_have_file_info =False22752276# pick up the new file information... for the2277# 'data' field we need to append to our array2278for k in marshalled.keys():2279if k =='data':2280 self.stream_contents.append(marshalled['data'])2281else:2282 self.stream_file[k] = marshalled[k]22832284 self.stream_have_file_info =True22852286# Stream directly from "p4 files" into "git fast-import"2287defstreamP4Files(self, files):2288 filesForCommit = []2289 filesToRead = []2290 filesToDelete = []22912292for f in files:2293# if using a client spec, only add the files that have2294# a path in the client2295if self.clientSpecDirs:2296if self.clientSpecDirs.map_in_client(f['path']) =="":2297continue22982299 filesForCommit.append(f)2300if f['action']in self.delete_actions:2301 filesToDelete.append(f)2302else:2303 filesToRead.append(f)23042305# deleted files...2306for f in filesToDelete:2307 self.streamOneP4Deletion(f)23082309iflen(filesToRead) >0:2310 self.stream_file = {}2311 self.stream_contents = []2312 self.stream_have_file_info =False23132314# curry self argument2315defstreamP4FilesCbSelf(entry):2316 self.streamP4FilesCb(entry)23172318 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23192320p4CmdList(["-x","-","print"],2321 stdin=fileArgs,2322 cb=streamP4FilesCbSelf)23232324# do the last chunk2325if self.stream_file.has_key('depotFile'):2326 self.streamOneP4File(self.stream_file, self.stream_contents)23272328defmake_email(self, userid):2329if userid in self.users:2330return self.users[userid]2331else:2332return"%s<a@b>"% userid23332334# Stream a p4 tag2335defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2336if verbose:2337print"writing tag%sfor commit%s"% (labelName, commit)2338 gitStream.write("tag%s\n"% labelName)2339 gitStream.write("from%s\n"% commit)23402341if labelDetails.has_key('Owner'):2342 owner = labelDetails["Owner"]2343else:2344 owner =None23452346# Try to use the owner of the p4 label, or failing that,2347# the current p4 user id.2348if owner:2349 email = self.make_email(owner)2350else:2351 email = self.make_email(self.p4UserId())2352 tagger ="%s %s %s"% (email, epoch, self.tz)23532354 gitStream.write("tagger%s\n"% tagger)23552356print"labelDetails=",labelDetails2357if labelDetails.has_key('Description'):2358 description = labelDetails['Description']2359else:2360 description ='Label from git p4'23612362 gitStream.write("data%d\n"%len(description))2363 gitStream.write(description)2364 gitStream.write("\n")23652366defcommit(self, details, files, branch, parent =""):2367 epoch = details["time"]2368 author = details["user"]23692370if self.verbose:2371print"commit into%s"% branch23722373# start with reading files; if that fails, we should not2374# create a commit.2375 new_files = []2376for f in files:2377if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2378 new_files.append(f)2379else:2380 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23812382 self.gitStream.write("commit%s\n"% branch)2383# gitStream.write("mark :%s\n" % details["change"])2384 self.committedChanges.add(int(details["change"]))2385 committer =""2386if author not in self.users:2387 self.getUserMapFromPerforceServer()2388 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23892390 self.gitStream.write("committer%s\n"% committer)23912392 self.gitStream.write("data <<EOT\n")2393 self.gitStream.write(details["desc"])2394 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2395(','.join(self.branchPrefixes), details["change"]))2396iflen(details['options']) >0:2397 self.gitStream.write(": options =%s"% details['options'])2398 self.gitStream.write("]\nEOT\n\n")23992400iflen(parent) >0:2401if self.verbose:2402print"parent%s"% parent2403 self.gitStream.write("from%s\n"% parent)24042405 self.streamP4Files(new_files)2406 self.gitStream.write("\n")24072408 change =int(details["change"])24092410if self.labels.has_key(change):2411 label = self.labels[change]2412 labelDetails = label[0]2413 labelRevisions = label[1]2414if self.verbose:2415print"Change%sis labelled%s"% (change, labelDetails)24162417 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2418for p in self.branchPrefixes])24192420iflen(files) ==len(labelRevisions):24212422 cleanedFiles = {}2423for info in files:2424if info["action"]in self.delete_actions:2425continue2426 cleanedFiles[info["depotFile"]] = info["rev"]24272428if cleanedFiles == labelRevisions:2429 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24302431else:2432if not self.silent:2433print("Tag%sdoes not match with change%s: files do not match."2434% (labelDetails["label"], change))24352436else:2437if not self.silent:2438print("Tag%sdoes not match with change%s: file count is different."2439% (labelDetails["label"], change))24402441# Build a dictionary of changelists and labels, for "detect-labels" option.2442defgetLabels(self):2443 self.labels = {}24442445 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2446iflen(l) >0and not self.silent:2447print"Finding files belonging to labels in%s"% `self.depotPaths`24482449for output in l:2450 label = output["label"]2451 revisions = {}2452 newestChange =02453if self.verbose:2454print"Querying files for label%s"% label2455forfileinp4CmdList(["files"] +2456["%s...@%s"% (p, label)2457for p in self.depotPaths]):2458 revisions[file["depotFile"]] =file["rev"]2459 change =int(file["change"])2460if change > newestChange:2461 newestChange = change24622463 self.labels[newestChange] = [output, revisions]24642465if self.verbose:2466print"Label changes:%s"% self.labels.keys()24672468# Import p4 labels as git tags. A direct mapping does not2469# exist, so assume that if all the files are at the same revision2470# then we can use that, or it's something more complicated we should2471# just ignore.2472defimportP4Labels(self, stream, p4Labels):2473if verbose:2474print"import p4 labels: "+' '.join(p4Labels)24752476 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2477 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2478iflen(validLabelRegexp) ==0:2479 validLabelRegexp = defaultLabelRegexp2480 m = re.compile(validLabelRegexp)24812482for name in p4Labels:2483 commitFound =False24842485if not m.match(name):2486if verbose:2487print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2488continue24892490if name in ignoredP4Labels:2491continue24922493 labelDetails =p4CmdList(['label',"-o", name])[0]24942495# get the most recent changelist for each file in this label2496 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2497for p in self.depotPaths])24982499if change.has_key('change'):2500# find the corresponding git commit; take the oldest commit2501 changelist =int(change['change'])2502 gitCommit =read_pipe(["git","rev-list","--max-count=1",2503"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2504iflen(gitCommit) ==0:2505print"could not find git commit for changelist%d"% changelist2506else:2507 gitCommit = gitCommit.strip()2508 commitFound =True2509# Convert from p4 time format2510try:2511 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2512exceptValueError:2513print"Could not convert label time%s"% labelDetails['Update']2514 tmwhen =125152516 when =int(time.mktime(tmwhen))2517 self.streamTag(stream, name, labelDetails, gitCommit, when)2518if verbose:2519print"p4 label%smapped to git commit%s"% (name, gitCommit)2520else:2521if verbose:2522print"Label%shas no changelists - possibly deleted?"% name25232524if not commitFound:2525# We can't import this label; don't try again as it will get very2526# expensive repeatedly fetching all the files for labels that will2527# never be imported. If the label is moved in the future, the2528# ignore will need to be removed manually.2529system(["git","config","--add","git-p4.ignoredP4Labels", name])25302531defguessProjectName(self):2532for p in self.depotPaths:2533if p.endswith("/"):2534 p = p[:-1]2535 p = p[p.strip().rfind("/") +1:]2536if not p.endswith("/"):2537 p +="/"2538return p25392540defgetBranchMapping(self):2541 lostAndFoundBranches =set()25422543 user =gitConfig("git-p4.branchUser")2544iflen(user) >0:2545 command ="branches -u%s"% user2546else:2547 command ="branches"25482549for info inp4CmdList(command):2550 details =p4Cmd(["branch","-o", info["branch"]])2551 viewIdx =02552while details.has_key("View%s"% viewIdx):2553 paths = details["View%s"% viewIdx].split(" ")2554 viewIdx = viewIdx +12555# require standard //depot/foo/... //depot/bar/... mapping2556iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2557continue2558 source = paths[0]2559 destination = paths[1]2560## HACK2561ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2562 source = source[len(self.depotPaths[0]):-4]2563 destination = destination[len(self.depotPaths[0]):-4]25642565if destination in self.knownBranches:2566if not self.silent:2567print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2568print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2569continue25702571 self.knownBranches[destination] = source25722573 lostAndFoundBranches.discard(destination)25742575if source not in self.knownBranches:2576 lostAndFoundBranches.add(source)25772578# Perforce does not strictly require branches to be defined, so we also2579# check git config for a branch list.2580#2581# Example of branch definition in git config file:2582# [git-p4]2583# branchList=main:branchA2584# branchList=main:branchB2585# branchList=branchA:branchC2586 configBranches =gitConfigList("git-p4.branchList")2587for branch in configBranches:2588if branch:2589(source, destination) = branch.split(":")2590 self.knownBranches[destination] = source25912592 lostAndFoundBranches.discard(destination)25932594if source not in self.knownBranches:2595 lostAndFoundBranches.add(source)259625972598for branch in lostAndFoundBranches:2599 self.knownBranches[branch] = branch26002601defgetBranchMappingFromGitBranches(self):2602 branches =p4BranchesInGit(self.importIntoRemotes)2603for branch in branches.keys():2604if branch =="master":2605 branch ="main"2606else:2607 branch = branch[len(self.projectName):]2608 self.knownBranches[branch] = branch26092610defupdateOptionDict(self, d):2611 option_keys = {}2612if self.keepRepoPath:2613 option_keys['keepRepoPath'] =126142615 d["options"] =' '.join(sorted(option_keys.keys()))26162617defreadOptions(self, d):2618 self.keepRepoPath = (d.has_key('options')2619and('keepRepoPath'in d['options']))26202621defgitRefForBranch(self, branch):2622if branch =="main":2623return self.refPrefix +"master"26242625iflen(branch) <=0:2626return branch26272628return self.refPrefix + self.projectName + branch26292630defgitCommitByP4Change(self, ref, change):2631if self.verbose:2632print"looking in ref "+ ref +" for change%susing bisect..."% change26332634 earliestCommit =""2635 latestCommit =parseRevision(ref)26362637while True:2638if self.verbose:2639print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2640 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2641iflen(next) ==0:2642if self.verbose:2643print"argh"2644return""2645 log =extractLogMessageFromGitCommit(next)2646 settings =extractSettingsGitLog(log)2647 currentChange =int(settings['change'])2648if self.verbose:2649print"current change%s"% currentChange26502651if currentChange == change:2652if self.verbose:2653print"found%s"% next2654return next26552656if currentChange < change:2657 earliestCommit ="^%s"% next2658else:2659 latestCommit ="%s"% next26602661return""26622663defimportNewBranch(self, branch, maxChange):2664# make fast-import flush all changes to disk and update the refs using the checkpoint2665# command so that we can try to find the branch parent in the git history2666 self.gitStream.write("checkpoint\n\n");2667 self.gitStream.flush();2668 branchPrefix = self.depotPaths[0] + branch +"/"2669range="@1,%s"% maxChange2670#print "prefix" + branchPrefix2671 changes =p4ChangesForPaths([branchPrefix],range)2672iflen(changes) <=0:2673return False2674 firstChange = changes[0]2675#print "first change in branch: %s" % firstChange2676 sourceBranch = self.knownBranches[branch]2677 sourceDepotPath = self.depotPaths[0] + sourceBranch2678 sourceRef = self.gitRefForBranch(sourceBranch)2679#print "source " + sourceBranch26802681 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2682#print "branch parent: %s" % branchParentChange2683 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2684iflen(gitParent) >0:2685 self.initialParents[self.gitRefForBranch(branch)] = gitParent2686#print "parent git commit: %s" % gitParent26872688 self.importChanges(changes)2689return True26902691defsearchParent(self, parent, branch, target):2692 parentFound =False2693for blob inread_pipe_lines(["git","rev-list","--reverse",2694"--no-merges", parent]):2695 blob = blob.strip()2696iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2697 parentFound =True2698if self.verbose:2699print"Found parent of%sin commit%s"% (branch, blob)2700break2701if parentFound:2702return blob2703else:2704return None27052706defimportChanges(self, changes):2707 cnt =12708for change in changes:2709 description =p4_describe(change)2710 self.updateOptionDict(description)27112712if not self.silent:2713 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2714 sys.stdout.flush()2715 cnt = cnt +127162717try:2718if self.detectBranches:2719 branches = self.splitFilesIntoBranches(description)2720for branch in branches.keys():2721## HACK --hwn2722 branchPrefix = self.depotPaths[0] + branch +"/"2723 self.branchPrefixes = [ branchPrefix ]27242725 parent =""27262727 filesForCommit = branches[branch]27282729if self.verbose:2730print"branch is%s"% branch27312732 self.updatedBranches.add(branch)27332734if branch not in self.createdBranches:2735 self.createdBranches.add(branch)2736 parent = self.knownBranches[branch]2737if parent == branch:2738 parent =""2739else:2740 fullBranch = self.projectName + branch2741if fullBranch not in self.p4BranchesInGit:2742if not self.silent:2743print("\nImporting new branch%s"% fullBranch);2744if self.importNewBranch(branch, change -1):2745 parent =""2746 self.p4BranchesInGit.append(fullBranch)2747if not self.silent:2748print("\nResuming with change%s"% change);27492750if self.verbose:2751print"parent determined through known branches:%s"% parent27522753 branch = self.gitRefForBranch(branch)2754 parent = self.gitRefForBranch(parent)27552756if self.verbose:2757print"looking for initial parent for%s; current parent is%s"% (branch, parent)27582759iflen(parent) ==0and branch in self.initialParents:2760 parent = self.initialParents[branch]2761del self.initialParents[branch]27622763 blob =None2764iflen(parent) >0:2765 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2766if self.verbose:2767print"Creating temporary branch: "+ tempBranch2768 self.commit(description, filesForCommit, tempBranch)2769 self.tempBranches.append(tempBranch)2770 self.checkpoint()2771 blob = self.searchParent(parent, branch, tempBranch)2772if blob:2773 self.commit(description, filesForCommit, branch, blob)2774else:2775if self.verbose:2776print"Parent of%snot found. Committing into head of%s"% (branch, parent)2777 self.commit(description, filesForCommit, branch, parent)2778else:2779 files = self.extractFilesFromCommit(description)2780 self.commit(description, files, self.branch,2781 self.initialParent)2782# only needed once, to connect to the previous commit2783 self.initialParent =""2784exceptIOError:2785print self.gitError.read()2786 sys.exit(1)27872788defimportHeadRevision(self, revision):2789print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27902791 details = {}2792 details["user"] ="git perforce import user"2793 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2794% (' '.join(self.depotPaths), revision))2795 details["change"] = revision2796 newestRevision =027972798 fileCnt =02799 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28002801for info inp4CmdList(["files"] + fileArgs):28022803if'code'in info and info['code'] =='error':2804 sys.stderr.write("p4 returned an error:%s\n"2805% info['data'])2806if info['data'].find("must refer to client") >=0:2807 sys.stderr.write("This particular p4 error is misleading.\n")2808 sys.stderr.write("Perhaps the depot path was misspelled.\n");2809 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2810 sys.exit(1)2811if'p4ExitCode'in info:2812 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2813 sys.exit(1)281428152816 change =int(info["change"])2817if change > newestRevision:2818 newestRevision = change28192820if info["action"]in self.delete_actions:2821# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2822#fileCnt = fileCnt + 12823continue28242825for prop in["depotFile","rev","action","type"]:2826 details["%s%s"% (prop, fileCnt)] = info[prop]28272828 fileCnt = fileCnt +128292830 details["change"] = newestRevision28312832# Use time from top-most change so that all git p4 clones of2833# the same p4 repo have the same commit SHA1s.2834 res =p4_describe(newestRevision)2835 details["time"] = res["time"]28362837 self.updateOptionDict(details)2838try:2839 self.commit(details, self.extractFilesFromCommit(details), self.branch)2840exceptIOError:2841print"IO error with git fast-import. Is your git version recent enough?"2842print self.gitError.read()284328442845defrun(self, args):2846 self.depotPaths = []2847 self.changeRange =""2848 self.previousDepotPaths = []2849 self.hasOrigin =False28502851# map from branch depot path to parent branch2852 self.knownBranches = {}2853 self.initialParents = {}28542855if self.importIntoRemotes:2856 self.refPrefix ="refs/remotes/p4/"2857else:2858 self.refPrefix ="refs/heads/p4/"28592860if self.syncWithOrigin:2861 self.hasOrigin =originP4BranchesExist()2862if self.hasOrigin:2863if not self.silent:2864print'Syncing with origin first, using "git fetch origin"'2865system("git fetch origin")28662867 branch_arg_given =bool(self.branch)2868iflen(self.branch) ==0:2869 self.branch = self.refPrefix +"master"2870ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2871system("git update-ref%srefs/heads/p4"% self.branch)2872system("git branch -D p4")28732874# accept either the command-line option, or the configuration variable2875if self.useClientSpec:2876# will use this after clone to set the variable2877 self.useClientSpec_from_options =True2878else:2879ifgitConfigBool("git-p4.useclientspec"):2880 self.useClientSpec =True2881if self.useClientSpec:2882 self.clientSpecDirs =getClientSpec()28832884# TODO: should always look at previous commits,2885# merge with previous imports, if possible.2886if args == []:2887if self.hasOrigin:2888createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28892890# branches holds mapping from branch name to sha12891 branches =p4BranchesInGit(self.importIntoRemotes)28922893# restrict to just this one, disabling detect-branches2894if branch_arg_given:2895 short = self.branch.split("/")[-1]2896if short in branches:2897 self.p4BranchesInGit = [ short ]2898else:2899 self.p4BranchesInGit = branches.keys()29002901iflen(self.p4BranchesInGit) >1:2902if not self.silent:2903print"Importing from/into multiple branches"2904 self.detectBranches =True2905for branch in branches.keys():2906 self.initialParents[self.refPrefix + branch] = \2907 branches[branch]29082909if self.verbose:2910print"branches:%s"% self.p4BranchesInGit29112912 p4Change =02913for branch in self.p4BranchesInGit:2914 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29152916 settings =extractSettingsGitLog(logMsg)29172918 self.readOptions(settings)2919if(settings.has_key('depot-paths')2920and settings.has_key('change')):2921 change =int(settings['change']) +12922 p4Change =max(p4Change, change)29232924 depotPaths =sorted(settings['depot-paths'])2925if self.previousDepotPaths == []:2926 self.previousDepotPaths = depotPaths2927else:2928 paths = []2929for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2930 prev_list = prev.split("/")2931 cur_list = cur.split("/")2932for i inrange(0,min(len(cur_list),len(prev_list))):2933if cur_list[i] <> prev_list[i]:2934 i = i -12935break29362937 paths.append("/".join(cur_list[:i +1]))29382939 self.previousDepotPaths = paths29402941if p4Change >0:2942 self.depotPaths =sorted(self.previousDepotPaths)2943 self.changeRange ="@%s,#head"% p4Change2944if not self.silent and not self.detectBranches:2945print"Performing incremental import into%sgit branch"% self.branch29462947# accept multiple ref name abbreviations:2948# refs/foo/bar/branch -> use it exactly2949# p4/branch -> prepend refs/remotes/ or refs/heads/2950# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2951if not self.branch.startswith("refs/"):2952if self.importIntoRemotes:2953 prepend ="refs/remotes/"2954else:2955 prepend ="refs/heads/"2956if not self.branch.startswith("p4/"):2957 prepend +="p4/"2958 self.branch = prepend + self.branch29592960iflen(args) ==0and self.depotPaths:2961if not self.silent:2962print"Depot paths:%s"%' '.join(self.depotPaths)2963else:2964if self.depotPaths and self.depotPaths != args:2965print("previous import used depot path%sand now%swas specified. "2966"This doesn't work!"% (' '.join(self.depotPaths),2967' '.join(args)))2968 sys.exit(1)29692970 self.depotPaths =sorted(args)29712972 revision =""2973 self.users = {}29742975# Make sure no revision specifiers are used when --changesfile2976# is specified.2977 bad_changesfile =False2978iflen(self.changesFile) >0:2979for p in self.depotPaths:2980if p.find("@") >=0or p.find("#") >=0:2981 bad_changesfile =True2982break2983if bad_changesfile:2984die("Option --changesfile is incompatible with revision specifiers")29852986 newPaths = []2987for p in self.depotPaths:2988if p.find("@") != -1:2989 atIdx = p.index("@")2990 self.changeRange = p[atIdx:]2991if self.changeRange =="@all":2992 self.changeRange =""2993elif','not in self.changeRange:2994 revision = self.changeRange2995 self.changeRange =""2996 p = p[:atIdx]2997elif p.find("#") != -1:2998 hashIdx = p.index("#")2999 revision = p[hashIdx:]3000 p = p[:hashIdx]3001elif self.previousDepotPaths == []:3002# pay attention to changesfile, if given, else import3003# the entire p4 tree at the head revision3004iflen(self.changesFile) ==0:3005 revision ="#head"30063007 p = re.sub("\.\.\.$","", p)3008if not p.endswith("/"):3009 p +="/"30103011 newPaths.append(p)30123013 self.depotPaths = newPaths30143015# --detect-branches may change this for each branch3016 self.branchPrefixes = self.depotPaths30173018 self.loadUserMapFromCache()3019 self.labels = {}3020if self.detectLabels:3021 self.getLabels();30223023if self.detectBranches:3024## FIXME - what's a P4 projectName ?3025 self.projectName = self.guessProjectName()30263027if self.hasOrigin:3028 self.getBranchMappingFromGitBranches()3029else:3030 self.getBranchMapping()3031if self.verbose:3032print"p4-git branches:%s"% self.p4BranchesInGit3033print"initial parents:%s"% self.initialParents3034for b in self.p4BranchesInGit:3035if b !="master":30363037## FIXME3038 b = b[len(self.projectName):]3039 self.createdBranches.add(b)30403041 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30423043 self.importProcess = subprocess.Popen(["git","fast-import"],3044 stdin=subprocess.PIPE,3045 stdout=subprocess.PIPE,3046 stderr=subprocess.PIPE);3047 self.gitOutput = self.importProcess.stdout3048 self.gitStream = self.importProcess.stdin3049 self.gitError = self.importProcess.stderr30503051if revision:3052 self.importHeadRevision(revision)3053else:3054 changes = []30553056iflen(self.changesFile) >0:3057 output =open(self.changesFile).readlines()3058 changeSet =set()3059for line in output:3060 changeSet.add(int(line))30613062for change in changeSet:3063 changes.append(change)30643065 changes.sort()3066else:3067# catch "git p4 sync" with no new branches, in a repo that3068# does not have any existing p4 branches3069iflen(args) ==0:3070if not self.p4BranchesInGit:3071die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30723073# The default branch is master, unless --branch is used to3074# specify something else. Make sure it exists, or complain3075# nicely about how to use --branch.3076if not self.detectBranches:3077if notbranch_exists(self.branch):3078if branch_arg_given:3079die("Error: branch%sdoes not exist."% self.branch)3080else:3081die("Error: no branch%s; perhaps specify one with --branch."%3082 self.branch)30833084if self.verbose:3085print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3086 self.changeRange)3087 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30883089iflen(self.maxChanges) >0:3090 changes = changes[:min(int(self.maxChanges),len(changes))]30913092iflen(changes) ==0:3093if not self.silent:3094print"No changes to import!"3095else:3096if not self.silent and not self.detectBranches:3097print"Import destination:%s"% self.branch30983099 self.updatedBranches =set()31003101if not self.detectBranches:3102if args:3103# start a new branch3104 self.initialParent =""3105else:3106# build on a previous revision3107 self.initialParent =parseRevision(self.branch)31083109 self.importChanges(changes)31103111if not self.silent:3112print""3113iflen(self.updatedBranches) >0:3114 sys.stdout.write("Updated branches: ")3115for b in self.updatedBranches:3116 sys.stdout.write("%s"% b)3117 sys.stdout.write("\n")31183119ifgitConfigBool("git-p4.importLabels"):3120 self.importLabels =True31213122if self.importLabels:3123 p4Labels =getP4Labels(self.depotPaths)3124 gitTags =getGitTags()31253126 missingP4Labels = p4Labels - gitTags3127 self.importP4Labels(self.gitStream, missingP4Labels)31283129 self.gitStream.close()3130if self.importProcess.wait() !=0:3131die("fast-import failed:%s"% self.gitError.read())3132 self.gitOutput.close()3133 self.gitError.close()31343135# Cleanup temporary branches created during import3136if self.tempBranches != []:3137for branch in self.tempBranches:3138read_pipe("git update-ref -d%s"% branch)3139 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31403141# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3142# a convenient shortcut refname "p4".3143if self.importIntoRemotes:3144 head_ref = self.refPrefix +"HEAD"3145if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3146system(["git","symbolic-ref", head_ref, self.branch])31473148return True31493150classP4Rebase(Command):3151def__init__(self):3152 Command.__init__(self)3153 self.options = [3154 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3155]3156 self.importLabels =False3157 self.description = ("Fetches the latest revision from perforce and "3158+"rebases the current work (branch) against it")31593160defrun(self, args):3161 sync =P4Sync()3162 sync.importLabels = self.importLabels3163 sync.run([])31643165return self.rebase()31663167defrebase(self):3168if os.system("git update-index --refresh") !=0:3169die("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.");3170iflen(read_pipe("git diff-index HEAD --")) >0:3171die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");31723173[upstream, settings] =findUpstreamBranchPoint()3174iflen(upstream) ==0:3175die("Cannot find upstream branchpoint for rebase")31763177# the branchpoint may be p4/foo~3, so strip off the parent3178 upstream = re.sub("~[0-9]+$","", upstream)31793180print"Rebasing the current branch onto%s"% upstream3181 oldHead =read_pipe("git rev-parse HEAD").strip()3182system("git rebase%s"% upstream)3183system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3184return True31853186classP4Clone(P4Sync):3187def__init__(self):3188 P4Sync.__init__(self)3189 self.description ="Creates a new git repository and imports from Perforce into it"3190 self.usage ="usage: %prog [options] //depot/path[@revRange]"3191 self.options += [3192 optparse.make_option("--destination", dest="cloneDestination",3193 action='store', default=None,3194help="where to leave result of the clone"),3195 optparse.make_option("-/", dest="cloneExclude",3196 action="append",type="string",3197help="exclude depot path"),3198 optparse.make_option("--bare", dest="cloneBare",3199 action="store_true", default=False),3200]3201 self.cloneDestination =None3202 self.needsGit =False3203 self.cloneBare =False32043205# This is required for the "append" cloneExclude action3206defensure_value(self, attr, value):3207if nothasattr(self, attr)orgetattr(self, attr)is None:3208setattr(self, attr, value)3209returngetattr(self, attr)32103211defdefaultDestination(self, args):3212## TODO: use common prefix of args?3213 depotPath = args[0]3214 depotDir = re.sub("(@[^@]*)$","", depotPath)3215 depotDir = re.sub("(#[^#]*)$","", depotDir)3216 depotDir = re.sub(r"\.\.\.$","", depotDir)3217 depotDir = re.sub(r"/$","", depotDir)3218return os.path.split(depotDir)[1]32193220defrun(self, args):3221iflen(args) <1:3222return False32233224if self.keepRepoPath and not self.cloneDestination:3225 sys.stderr.write("Must specify destination for --keep-path\n")3226 sys.exit(1)32273228 depotPaths = args32293230if not self.cloneDestination andlen(depotPaths) >1:3231 self.cloneDestination = depotPaths[-1]3232 depotPaths = depotPaths[:-1]32333234 self.cloneExclude = ["/"+p for p in self.cloneExclude]3235for p in depotPaths:3236if not p.startswith("//"):3237 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3238return False32393240if not self.cloneDestination:3241 self.cloneDestination = self.defaultDestination(args)32423243print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32443245if not os.path.exists(self.cloneDestination):3246 os.makedirs(self.cloneDestination)3247chdir(self.cloneDestination)32483249 init_cmd = ["git","init"]3250if self.cloneBare:3251 init_cmd.append("--bare")3252 retcode = subprocess.call(init_cmd)3253if retcode:3254raiseCalledProcessError(retcode, init_cmd)32553256if not P4Sync.run(self, depotPaths):3257return False32583259# create a master branch and check out a work tree3260ifgitBranchExists(self.branch):3261system(["git","branch","master", self.branch ])3262if not self.cloneBare:3263system(["git","checkout","-f"])3264else:3265print'Not checking out any branch, use ' \3266'"git checkout -q -b master <branch>"'32673268# auto-set this variable if invoked with --use-client-spec3269if self.useClientSpec_from_options:3270system("git config --bool git-p4.useclientspec true")32713272return True32733274classP4Branches(Command):3275def__init__(self):3276 Command.__init__(self)3277 self.options = [ ]3278 self.description = ("Shows the git branches that hold imports and their "3279+"corresponding perforce depot paths")3280 self.verbose =False32813282defrun(self, args):3283iforiginP4BranchesExist():3284createOrUpdateBranchesFromOrigin()32853286 cmdline ="git rev-parse --symbolic "3287 cmdline +=" --remotes"32883289for line inread_pipe_lines(cmdline):3290 line = line.strip()32913292if not line.startswith('p4/')or line =="p4/HEAD":3293continue3294 branch = line32953296 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3297 settings =extractSettingsGitLog(log)32983299print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3300return True33013302classHelpFormatter(optparse.IndentedHelpFormatter):3303def__init__(self):3304 optparse.IndentedHelpFormatter.__init__(self)33053306defformat_description(self, description):3307if description:3308return description +"\n"3309else:3310return""33113312defprintUsage(commands):3313print"usage:%s<command> [options]"% sys.argv[0]3314print""3315print"valid commands:%s"%", ".join(commands)3316print""3317print"Try%s<command> --help for command specific help."% sys.argv[0]3318print""33193320commands = {3321"debug": P4Debug,3322"submit": P4Submit,3323"commit": P4Submit,3324"sync": P4Sync,3325"rebase": P4Rebase,3326"clone": P4Clone,3327"rollback": P4RollBack,3328"branches": P4Branches3329}333033313332defmain():3333iflen(sys.argv[1:]) ==0:3334printUsage(commands.keys())3335 sys.exit(2)33363337 cmdName = sys.argv[1]3338try:3339 klass = commands[cmdName]3340 cmd =klass()3341exceptKeyError:3342print"unknown command%s"% cmdName3343print""3344printUsage(commands.keys())3345 sys.exit(2)33463347 options = cmd.options3348 cmd.gitdir = os.environ.get("GIT_DIR",None)33493350 args = sys.argv[2:]33513352 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3353if cmd.needsGit:3354 options.append(optparse.make_option("--git-dir", dest="gitdir"))33553356 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3357 options,3358 description = cmd.description,3359 formatter =HelpFormatter())33603361(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3362global verbose3363 verbose = cmd.verbose3364if cmd.needsGit:3365if cmd.gitdir ==None:3366 cmd.gitdir = os.path.abspath(".git")3367if notisValidGitDir(cmd.gitdir):3368 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3369if os.path.exists(cmd.gitdir):3370 cdup =read_pipe("git rev-parse --show-cdup").strip()3371iflen(cdup) >0:3372chdir(cdup);33733374if notisValidGitDir(cmd.gitdir):3375ifisValidGitDir(cmd.gitdir +"/.git"):3376 cmd.gitdir +="/.git"3377else:3378die("fatal: cannot locate git repository at%s"% cmd.gitdir)33793380 os.environ["GIT_DIR"] = cmd.gitdir33813382if not cmd.run(args):3383 parser.print_help()3384 sys.exit(2)338533863387if __name__ =='__main__':3388main()