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(f): 314 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 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# the //client/ name 784 client_name = entry["Client"] 785 786# just the keys that start with "View" 787 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 788 789# hold this new View 790 view =View(client_name) 791 792# append the lines, in order, to the view 793for view_num inrange(len(view_keys)): 794 k ="View%d"% view_num 795if k not in view_keys: 796die("Expected view key%smissing"% k) 797 view.append(entry[k]) 798 799return view 800 801defgetClientRoot(): 802"""Grab the client directory.""" 803 804 output =p4CmdList("client -o") 805iflen(output) !=1: 806die('Output from "client -o" is%dlines, expecting 1'%len(output)) 807 808 entry = output[0] 809if"Root"not in entry: 810die('Client has no "Root"') 811 812return entry["Root"] 813 814# 815# P4 wildcards are not allowed in filenames. P4 complains 816# if you simply add them, but you can force it with "-f", in 817# which case it translates them into %xx encoding internally. 818# 819defwildcard_decode(path): 820# Search for and fix just these four characters. Do % last so 821# that fixing it does not inadvertently create new %-escapes. 822# Cannot have * in a filename in windows; untested as to 823# what p4 would do in such a case. 824if not platform.system() =="Windows": 825 path = path.replace("%2A","*") 826 path = path.replace("%23","#") \ 827.replace("%40","@") \ 828.replace("%25","%") 829return path 830 831defwildcard_encode(path): 832# do % first to avoid double-encoding the %s introduced here 833 path = path.replace("%","%25") \ 834.replace("*","%2A") \ 835.replace("#","%23") \ 836.replace("@","%40") 837return path 838 839defwildcard_present(path): 840 m = re.search("[*#@%]", path) 841return m is not None 842 843class Command: 844def__init__(self): 845 self.usage ="usage: %prog [options]" 846 self.needsGit =True 847 self.verbose =False 848 849class P4UserMap: 850def__init__(self): 851 self.userMapFromPerforceServer =False 852 self.myP4UserId =None 853 854defp4UserId(self): 855if self.myP4UserId: 856return self.myP4UserId 857 858 results =p4CmdList("user -o") 859for r in results: 860if r.has_key('User'): 861 self.myP4UserId = r['User'] 862return r['User'] 863die("Could not find your p4 user id") 864 865defp4UserIsMe(self, p4User): 866# return True if the given p4 user is actually me 867 me = self.p4UserId() 868if not p4User or p4User != me: 869return False 870else: 871return True 872 873defgetUserCacheFilename(self): 874 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 875return home +"/.gitp4-usercache.txt" 876 877defgetUserMapFromPerforceServer(self): 878if self.userMapFromPerforceServer: 879return 880 self.users = {} 881 self.emails = {} 882 883for output inp4CmdList("users"): 884if not output.has_key("User"): 885continue 886 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 887 self.emails[output["Email"]] = output["User"] 888 889 890 s ='' 891for(key, val)in self.users.items(): 892 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 893 894open(self.getUserCacheFilename(),"wb").write(s) 895 self.userMapFromPerforceServer =True 896 897defloadUserMapFromCache(self): 898 self.users = {} 899 self.userMapFromPerforceServer =False 900try: 901 cache =open(self.getUserCacheFilename(),"rb") 902 lines = cache.readlines() 903 cache.close() 904for line in lines: 905 entry = line.strip().split("\t") 906 self.users[entry[0]] = entry[1] 907exceptIOError: 908 self.getUserMapFromPerforceServer() 909 910classP4Debug(Command): 911def__init__(self): 912 Command.__init__(self) 913 self.options = [] 914 self.description ="A tool to debug the output of p4 -G." 915 self.needsGit =False 916 917defrun(self, args): 918 j =0 919for output inp4CmdList(args): 920print'Element:%d'% j 921 j +=1 922print output 923return True 924 925classP4RollBack(Command): 926def__init__(self): 927 Command.__init__(self) 928 self.options = [ 929 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 930] 931 self.description ="A tool to debug the multi-branch import. Don't use :)" 932 self.rollbackLocalBranches =False 933 934defrun(self, args): 935iflen(args) !=1: 936return False 937 maxChange =int(args[0]) 938 939if"p4ExitCode"inp4Cmd("changes -m 1"): 940die("Problems executing p4"); 941 942if self.rollbackLocalBranches: 943 refPrefix ="refs/heads/" 944 lines =read_pipe_lines("git rev-parse --symbolic --branches") 945else: 946 refPrefix ="refs/remotes/" 947 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 948 949for line in lines: 950if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 951 line = line.strip() 952 ref = refPrefix + line 953 log =extractLogMessageFromGitCommit(ref) 954 settings =extractSettingsGitLog(log) 955 956 depotPaths = settings['depot-paths'] 957 change = settings['change'] 958 959 changed =False 960 961iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 962for p in depotPaths]))) ==0: 963print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 964system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 965continue 966 967while change andint(change) > maxChange: 968 changed =True 969if self.verbose: 970print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 971system("git update-ref%s\"%s^\""% (ref, ref)) 972 log =extractLogMessageFromGitCommit(ref) 973 settings =extractSettingsGitLog(log) 974 975 976 depotPaths = settings['depot-paths'] 977 change = settings['change'] 978 979if changed: 980print"%srewound to%s"% (ref, change) 981 982return True 983 984classP4Submit(Command, P4UserMap): 985 986 conflict_behavior_choices = ("ask","skip","quit") 987 988def__init__(self): 989 Command.__init__(self) 990 P4UserMap.__init__(self) 991 self.options = [ 992 optparse.make_option("--origin", dest="origin"), 993 optparse.make_option("-M", dest="detectRenames", action="store_true"), 994# preserve the user, requires relevant p4 permissions 995 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 996 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 997 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 998 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 999 optparse.make_option("--conflict", dest="conflict_behavior",1000 choices=self.conflict_behavior_choices),1001 optparse.make_option("--branch", dest="branch"),1002]1003 self.description ="Submit changes from git to the perforce depot."1004 self.usage +=" [name of git branch to submit into perforce depot]"1005 self.origin =""1006 self.detectRenames =False1007 self.preserveUser =gitConfigBool("git-p4.preserveUser")1008 self.dry_run =False1009 self.prepare_p4_only =False1010 self.conflict_behavior =None1011 self.isWindows = (platform.system() =="Windows")1012 self.exportLabels =False1013 self.p4HasMoveCommand =p4_has_move_command()1014 self.branch =None10151016defcheck(self):1017iflen(p4CmdList("opened ...")) >0:1018die("You have files opened with perforce! Close them before starting the sync.")10191020defseparate_jobs_from_description(self, message):1021"""Extract and return a possible Jobs field in the commit1022 message. It goes into a separate section in the p4 change1023 specification.10241025 A jobs line starts with "Jobs:" and looks like a new field1026 in a form. Values are white-space separated on the same1027 line or on following lines that start with a tab.10281029 This does not parse and extract the full git commit message1030 like a p4 form. It just sees the Jobs: line as a marker1031 to pass everything from then on directly into the p4 form,1032 but outside the description section.10331034 Return a tuple (stripped log message, jobs string)."""10351036 m = re.search(r'^Jobs:', message, re.MULTILINE)1037if m is None:1038return(message,None)10391040 jobtext = message[m.start():]1041 stripped_message = message[:m.start()].rstrip()1042return(stripped_message, jobtext)10431044defprepareLogMessage(self, template, message, jobs):1045"""Edits the template returned from "p4 change -o" to insert1046 the message in the Description field, and the jobs text in1047 the Jobs field."""1048 result =""10491050 inDescriptionSection =False10511052for line in template.split("\n"):1053if line.startswith("#"):1054 result += line +"\n"1055continue10561057if inDescriptionSection:1058if line.startswith("Files:")or line.startswith("Jobs:"):1059 inDescriptionSection =False1060# insert Jobs section1061if jobs:1062 result += jobs +"\n"1063else:1064continue1065else:1066if line.startswith("Description:"):1067 inDescriptionSection =True1068 line +="\n"1069for messageLine in message.split("\n"):1070 line +="\t"+ messageLine +"\n"10711072 result += line +"\n"10731074return result10751076defpatchRCSKeywords(self,file, pattern):1077# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1078(handle, outFileName) = tempfile.mkstemp(dir='.')1079try:1080 outFile = os.fdopen(handle,"w+")1081 inFile =open(file,"r")1082 regexp = re.compile(pattern, re.VERBOSE)1083for line in inFile.readlines():1084 line = regexp.sub(r'$\1$', line)1085 outFile.write(line)1086 inFile.close()1087 outFile.close()1088# Forcibly overwrite the original file1089 os.unlink(file)1090 shutil.move(outFileName,file)1091except:1092# cleanup our temporary file1093 os.unlink(outFileName)1094print"Failed to strip RCS keywords in%s"%file1095raise10961097print"Patched up RCS keywords in%s"%file10981099defp4UserForCommit(self,id):1100# Return the tuple (perforce user,git email) for a given git commit id1101 self.getUserMapFromPerforceServer()1102 gitEmail =read_pipe(["git","log","--max-count=1",1103"--format=%ae",id])1104 gitEmail = gitEmail.strip()1105if not self.emails.has_key(gitEmail):1106return(None,gitEmail)1107else:1108return(self.emails[gitEmail],gitEmail)11091110defcheckValidP4Users(self,commits):1111# check if any git authors cannot be mapped to p4 users1112foridin commits:1113(user,email) = self.p4UserForCommit(id)1114if not user:1115 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1116ifgitConfigBool("git-p4.allowMissingP4Users"):1117print"%s"% msg1118else:1119die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11201121deflastP4Changelist(self):1122# Get back the last changelist number submitted in this client spec. This1123# then gets used to patch up the username in the change. If the same1124# client spec is being used by multiple processes then this might go1125# wrong.1126 results =p4CmdList("client -o")# find the current client1127 client =None1128for r in results:1129if r.has_key('Client'):1130 client = r['Client']1131break1132if not client:1133die("could not get client spec")1134 results =p4CmdList(["changes","-c", client,"-m","1"])1135for r in results:1136if r.has_key('change'):1137return r['change']1138die("Could not get changelist number for last submit - cannot patch up user details")11391140defmodifyChangelistUser(self, changelist, newUser):1141# fixup the user field of a changelist after it has been submitted.1142 changes =p4CmdList("change -o%s"% changelist)1143iflen(changes) !=1:1144die("Bad output from p4 change modifying%sto user%s"%1145(changelist, newUser))11461147 c = changes[0]1148if c['User'] == newUser:return# nothing to do1149 c['User'] = newUser1150input= marshal.dumps(c)11511152 result =p4CmdList("change -f -i", stdin=input)1153for r in result:1154if r.has_key('code'):1155if r['code'] =='error':1156die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1157if r.has_key('data'):1158print("Updated user field for changelist%sto%s"% (changelist, newUser))1159return1160die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11611162defcanChangeChangelists(self):1163# check to see if we have p4 admin or super-user permissions, either of1164# which are required to modify changelists.1165 results =p4CmdList(["protects", self.depotPath])1166for r in results:1167if r.has_key('perm'):1168if r['perm'] =='admin':1169return11170if r['perm'] =='super':1171return11172return011731174defprepareSubmitTemplate(self):1175"""Run "p4 change -o" to grab a change specification template.1176 This does not use "p4 -G", as it is nice to keep the submission1177 template in original order, since a human might edit it.11781179 Remove lines in the Files section that show changes to files1180 outside the depot path we're committing into."""11811182 template =""1183 inFilesSection =False1184for line inp4_read_pipe_lines(['change','-o']):1185if line.endswith("\r\n"):1186 line = line[:-2] +"\n"1187if inFilesSection:1188if line.startswith("\t"):1189# path starts and ends with a tab1190 path = line[1:]1191 lastTab = path.rfind("\t")1192if lastTab != -1:1193 path = path[:lastTab]1194if notp4PathStartsWith(path, self.depotPath):1195continue1196else:1197 inFilesSection =False1198else:1199if line.startswith("Files:"):1200 inFilesSection =True12011202 template += line12031204return template12051206defedit_template(self, template_file):1207"""Invoke the editor to let the user change the submission1208 message. Return true if okay to continue with the submit."""12091210# if configured to skip the editing part, just submit1211ifgitConfigBool("git-p4.skipSubmitEdit"):1212return True12131214# look at the modification time, to check later if the user saved1215# the file1216 mtime = os.stat(template_file).st_mtime12171218# invoke the editor1219if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1220 editor = os.environ.get("P4EDITOR")1221else:1222 editor =read_pipe("git var GIT_EDITOR").strip()1223system([editor, template_file])12241225# If the file was not saved, prompt to see if this patch should1226# be skipped. But skip this verification step if configured so.1227ifgitConfigBool("git-p4.skipSubmitEditCheck"):1228return True12291230# modification time updated means user saved the file1231if os.stat(template_file).st_mtime > mtime:1232return True12331234while True:1235 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1236if response =='y':1237return True1238if response =='n':1239return False12401241defget_diff_description(self, editedFiles, filesToAdd):1242# diff1243if os.environ.has_key("P4DIFF"):1244del(os.environ["P4DIFF"])1245 diff =""1246for editedFile in editedFiles:1247 diff +=p4_read_pipe(['diff','-du',1248wildcard_encode(editedFile)])12491250# new file diff1251 newdiff =""1252for newFile in filesToAdd:1253 newdiff +="==== new file ====\n"1254 newdiff +="--- /dev/null\n"1255 newdiff +="+++%s\n"% newFile1256 f =open(newFile,"r")1257for line in f.readlines():1258 newdiff +="+"+ line1259 f.close()12601261return(diff + newdiff).replace('\r\n','\n')12621263defapplyCommit(self,id):1264"""Apply one commit, return True if it succeeded."""12651266print"Applying",read_pipe(["git","show","-s",1267"--format=format:%h%s",id])12681269(p4User, gitEmail) = self.p4UserForCommit(id)12701271 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1272 filesToAdd =set()1273 filesToDelete =set()1274 editedFiles =set()1275 pureRenameCopy =set()1276 filesToChangeExecBit = {}12771278for line in diff:1279 diff =parseDiffTreeEntry(line)1280 modifier = diff['status']1281 path = diff['src']1282if modifier =="M":1283p4_edit(path)1284ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1285 filesToChangeExecBit[path] = diff['dst_mode']1286 editedFiles.add(path)1287elif modifier =="A":1288 filesToAdd.add(path)1289 filesToChangeExecBit[path] = diff['dst_mode']1290if path in filesToDelete:1291 filesToDelete.remove(path)1292elif modifier =="D":1293 filesToDelete.add(path)1294if path in filesToAdd:1295 filesToAdd.remove(path)1296elif modifier =="C":1297 src, dest = diff['src'], diff['dst']1298p4_integrate(src, dest)1299 pureRenameCopy.add(dest)1300if diff['src_sha1'] != diff['dst_sha1']:1301p4_edit(dest)1302 pureRenameCopy.discard(dest)1303ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1304p4_edit(dest)1305 pureRenameCopy.discard(dest)1306 filesToChangeExecBit[dest] = diff['dst_mode']1307if self.isWindows:1308# turn off read-only attribute1309 os.chmod(dest, stat.S_IWRITE)1310 os.unlink(dest)1311 editedFiles.add(dest)1312elif modifier =="R":1313 src, dest = diff['src'], diff['dst']1314if self.p4HasMoveCommand:1315p4_edit(src)# src must be open before move1316p4_move(src, dest)# opens for (move/delete, move/add)1317else:1318p4_integrate(src, dest)1319if diff['src_sha1'] != diff['dst_sha1']:1320p4_edit(dest)1321else:1322 pureRenameCopy.add(dest)1323ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1324if not self.p4HasMoveCommand:1325p4_edit(dest)# with move: already open, writable1326 filesToChangeExecBit[dest] = diff['dst_mode']1327if not self.p4HasMoveCommand:1328if self.isWindows:1329 os.chmod(dest, stat.S_IWRITE)1330 os.unlink(dest)1331 filesToDelete.add(src)1332 editedFiles.add(dest)1333else:1334die("unknown modifier%sfor%s"% (modifier, path))13351336 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1337 patchcmd = diffcmd +" | git apply "1338 tryPatchCmd = patchcmd +"--check -"1339 applyPatchCmd = patchcmd +"--check --apply -"1340 patch_succeeded =True13411342if os.system(tryPatchCmd) !=0:1343 fixed_rcs_keywords =False1344 patch_succeeded =False1345print"Unfortunately applying the change failed!"13461347# Patch failed, maybe it's just RCS keyword woes. Look through1348# the patch to see if that's possible.1349ifgitConfigBool("git-p4.attemptRCSCleanup"):1350file=None1351 pattern =None1352 kwfiles = {}1353forfilein editedFiles | filesToDelete:1354# did this file's delta contain RCS keywords?1355 pattern =p4_keywords_regexp_for_file(file)13561357if pattern:1358# this file is a possibility...look for RCS keywords.1359 regexp = re.compile(pattern, re.VERBOSE)1360for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1361if regexp.search(line):1362if verbose:1363print"got keyword match on%sin%sin%s"% (pattern, line,file)1364 kwfiles[file] = pattern1365break13661367forfilein kwfiles:1368if verbose:1369print"zapping%swith%s"% (line,pattern)1370# File is being deleted, so not open in p4. Must1371# disable the read-only bit on windows.1372if self.isWindows andfilenot in editedFiles:1373 os.chmod(file, stat.S_IWRITE)1374 self.patchRCSKeywords(file, kwfiles[file])1375 fixed_rcs_keywords =True13761377if fixed_rcs_keywords:1378print"Retrying the patch with RCS keywords cleaned up"1379if os.system(tryPatchCmd) ==0:1380 patch_succeeded =True13811382if not patch_succeeded:1383for f in editedFiles:1384p4_revert(f)1385return False13861387#1388# Apply the patch for real, and do add/delete/+x handling.1389#1390system(applyPatchCmd)13911392for f in filesToAdd:1393p4_add(f)1394for f in filesToDelete:1395p4_revert(f)1396p4_delete(f)13971398# Set/clear executable bits1399for f in filesToChangeExecBit.keys():1400 mode = filesToChangeExecBit[f]1401setP4ExecBit(f, mode)14021403#1404# Build p4 change description, starting with the contents1405# of the git commit message.1406#1407 logMessage =extractLogMessageFromGitCommit(id)1408 logMessage = logMessage.strip()1409(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14101411 template = self.prepareSubmitTemplate()1412 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14131414if self.preserveUser:1415 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14161417if self.checkAuthorship and not self.p4UserIsMe(p4User):1418 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1419 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1420 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"14211422 separatorLine ="######## everything below this line is just the diff #######\n"1423if not self.prepare_p4_only:1424 submitTemplate += separatorLine1425 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)14261427(handle, fileName) = tempfile.mkstemp()1428 tmpFile = os.fdopen(handle,"w+b")1429if self.isWindows:1430 submitTemplate = submitTemplate.replace("\n","\r\n")1431 tmpFile.write(submitTemplate)1432 tmpFile.close()14331434if self.prepare_p4_only:1435#1436# Leave the p4 tree prepared, and the submit template around1437# and let the user decide what to do next1438#1439print1440print"P4 workspace prepared for submission."1441print"To submit or revert, go to client workspace"1442print" "+ self.clientPath1443print1444print"To submit, use\"p4 submit\"to write a new description,"1445print"or\"p4 submit -i <%s\"to use the one prepared by" \1446"\"git p4\"."% fileName1447print"You can delete the file\"%s\"when finished."% fileName14481449if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1450print"To preserve change ownership by user%s, you must\n" \1451"do\"p4 change -f <change>\"after submitting and\n" \1452"edit the User field."1453if pureRenameCopy:1454print"After submitting, renamed files must be re-synced."1455print"Invoke\"p4 sync -f\"on each of these files:"1456for f in pureRenameCopy:1457print" "+ f14581459print1460print"To revert the changes, use\"p4 revert ...\", and delete"1461print"the submit template file\"%s\""% fileName1462if filesToAdd:1463print"Since the commit adds new files, they must be deleted:"1464for f in filesToAdd:1465print" "+ f1466print1467return True14681469#1470# Let the user edit the change description, then submit it.1471#1472if self.edit_template(fileName):1473# read the edited message and submit1474 ret =True1475 tmpFile =open(fileName,"rb")1476 message = tmpFile.read()1477 tmpFile.close()1478if self.isWindows:1479 message = message.replace("\r\n","\n")1480 submitTemplate = message[:message.index(separatorLine)]1481p4_write_pipe(['submit','-i'], submitTemplate)14821483if self.preserveUser:1484if p4User:1485# Get last changelist number. Cannot easily get it from1486# the submit command output as the output is1487# unmarshalled.1488 changelist = self.lastP4Changelist()1489 self.modifyChangelistUser(changelist, p4User)14901491# The rename/copy happened by applying a patch that created a1492# new file. This leaves it writable, which confuses p4.1493for f in pureRenameCopy:1494p4_sync(f,"-f")14951496else:1497# skip this patch1498 ret =False1499print"Submission cancelled, undoing p4 changes."1500for f in editedFiles:1501p4_revert(f)1502for f in filesToAdd:1503p4_revert(f)1504 os.remove(f)1505for f in filesToDelete:1506p4_revert(f)15071508 os.remove(fileName)1509return ret15101511# Export git tags as p4 labels. Create a p4 label and then tag1512# with that.1513defexportGitTags(self, gitTags):1514 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1515iflen(validLabelRegexp) ==0:1516 validLabelRegexp = defaultLabelRegexp1517 m = re.compile(validLabelRegexp)15181519for name in gitTags:15201521if not m.match(name):1522if verbose:1523print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1524continue15251526# Get the p4 commit this corresponds to1527 logMessage =extractLogMessageFromGitCommit(name)1528 values =extractSettingsGitLog(logMessage)15291530if not values.has_key('change'):1531# a tag pointing to something not sent to p4; ignore1532if verbose:1533print"git tag%sdoes not give a p4 commit"% name1534continue1535else:1536 changelist = values['change']15371538# Get the tag details.1539 inHeader =True1540 isAnnotated =False1541 body = []1542for l inread_pipe_lines(["git","cat-file","-p", name]):1543 l = l.strip()1544if inHeader:1545if re.match(r'tag\s+', l):1546 isAnnotated =True1547elif re.match(r'\s*$', l):1548 inHeader =False1549continue1550else:1551 body.append(l)15521553if not isAnnotated:1554 body = ["lightweight tag imported by git p4\n"]15551556# Create the label - use the same view as the client spec we are using1557 clientSpec =getClientSpec()15581559 labelTemplate ="Label:%s\n"% name1560 labelTemplate +="Description:\n"1561for b in body:1562 labelTemplate +="\t"+ b +"\n"1563 labelTemplate +="View:\n"1564for depot_side in clientSpec.mappings:1565 labelTemplate +="\t%s\n"% depot_side15661567if self.dry_run:1568print"Would create p4 label%sfor tag"% name1569elif self.prepare_p4_only:1570print"Not creating p4 label%sfor tag due to option" \1571" --prepare-p4-only"% name1572else:1573p4_write_pipe(["label","-i"], labelTemplate)15741575# Use the label1576p4_system(["tag","-l", name] +1577["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])15781579if verbose:1580print"created p4 label for tag%s"% name15811582defrun(self, args):1583iflen(args) ==0:1584 self.master =currentGitBranch()1585iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1586die("Detecting current git branch failed!")1587eliflen(args) ==1:1588 self.master = args[0]1589if notbranchExists(self.master):1590die("Branch%sdoes not exist"% self.master)1591else:1592return False15931594 allowSubmit =gitConfig("git-p4.allowSubmit")1595iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1596die("%sis not in git-p4.allowSubmit"% self.master)15971598[upstream, settings] =findUpstreamBranchPoint()1599 self.depotPath = settings['depot-paths'][0]1600iflen(self.origin) ==0:1601 self.origin = upstream16021603if self.preserveUser:1604if not self.canChangeChangelists():1605die("Cannot preserve user names without p4 super-user or admin permissions")16061607# if not set from the command line, try the config file1608if self.conflict_behavior is None:1609 val =gitConfig("git-p4.conflict")1610if val:1611if val not in self.conflict_behavior_choices:1612die("Invalid value '%s' for config git-p4.conflict"% val)1613else:1614 val ="ask"1615 self.conflict_behavior = val16161617if self.verbose:1618print"Origin branch is "+ self.origin16191620iflen(self.depotPath) ==0:1621print"Internal error: cannot locate perforce depot path from existing branches"1622 sys.exit(128)16231624 self.useClientSpec =False1625ifgitConfigBool("git-p4.useclientspec"):1626 self.useClientSpec =True1627if self.useClientSpec:1628 self.clientSpecDirs =getClientSpec()16291630if self.useClientSpec:1631# all files are relative to the client spec1632 self.clientPath =getClientRoot()1633else:1634 self.clientPath =p4Where(self.depotPath)16351636if self.clientPath =="":1637die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)16381639print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1640 self.oldWorkingDirectory = os.getcwd()16411642# ensure the clientPath exists1643 new_client_dir =False1644if not os.path.exists(self.clientPath):1645 new_client_dir =True1646 os.makedirs(self.clientPath)16471648chdir(self.clientPath, is_client_path=True)1649if self.dry_run:1650print"Would synchronize p4 checkout in%s"% self.clientPath1651else:1652print"Synchronizing p4 checkout..."1653if new_client_dir:1654# old one was destroyed, and maybe nobody told p41655p4_sync("...","-f")1656else:1657p4_sync("...")1658 self.check()16591660 commits = []1661for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1662 commits.append(line.strip())1663 commits.reverse()16641665if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1666 self.checkAuthorship =False1667else:1668 self.checkAuthorship =True16691670if self.preserveUser:1671 self.checkValidP4Users(commits)16721673#1674# Build up a set of options to be passed to diff when1675# submitting each commit to p4.1676#1677if self.detectRenames:1678# command-line -M arg1679 self.diffOpts ="-M"1680else:1681# If not explicitly set check the config variable1682 detectRenames =gitConfig("git-p4.detectRenames")16831684if detectRenames.lower() =="false"or detectRenames =="":1685 self.diffOpts =""1686elif detectRenames.lower() =="true":1687 self.diffOpts ="-M"1688else:1689 self.diffOpts ="-M%s"% detectRenames16901691# no command-line arg for -C or --find-copies-harder, just1692# config variables1693 detectCopies =gitConfig("git-p4.detectCopies")1694if detectCopies.lower() =="false"or detectCopies =="":1695pass1696elif detectCopies.lower() =="true":1697 self.diffOpts +=" -C"1698else:1699 self.diffOpts +=" -C%s"% detectCopies17001701ifgitConfigBool("git-p4.detectCopiesHarder"):1702 self.diffOpts +=" --find-copies-harder"17031704#1705# Apply the commits, one at a time. On failure, ask if should1706# continue to try the rest of the patches, or quit.1707#1708if self.dry_run:1709print"Would apply"1710 applied = []1711 last =len(commits) -11712for i, commit inenumerate(commits):1713if self.dry_run:1714print" ",read_pipe(["git","show","-s",1715"--format=format:%h%s", commit])1716 ok =True1717else:1718 ok = self.applyCommit(commit)1719if ok:1720 applied.append(commit)1721else:1722if self.prepare_p4_only and i < last:1723print"Processing only the first commit due to option" \1724" --prepare-p4-only"1725break1726if i < last:1727 quit =False1728while True:1729# prompt for what to do, or use the option/variable1730if self.conflict_behavior =="ask":1731print"What do you want to do?"1732 response =raw_input("[s]kip this commit but apply"1733" the rest, or [q]uit? ")1734if not response:1735continue1736elif self.conflict_behavior =="skip":1737 response ="s"1738elif self.conflict_behavior =="quit":1739 response ="q"1740else:1741die("Unknown conflict_behavior '%s'"%1742 self.conflict_behavior)17431744if response[0] =="s":1745print"Skipping this commit, but applying the rest"1746break1747if response[0] =="q":1748print"Quitting"1749 quit =True1750break1751if quit:1752break17531754chdir(self.oldWorkingDirectory)17551756if self.dry_run:1757pass1758elif self.prepare_p4_only:1759pass1760eliflen(commits) ==len(applied):1761print"All commits applied!"17621763 sync =P4Sync()1764if self.branch:1765 sync.branch = self.branch1766 sync.run([])17671768 rebase =P4Rebase()1769 rebase.rebase()17701771else:1772iflen(applied) ==0:1773print"No commits applied."1774else:1775print"Applied only the commits marked with '*':"1776for c in commits:1777if c in applied:1778 star ="*"1779else:1780 star =" "1781print star,read_pipe(["git","show","-s",1782"--format=format:%h%s", c])1783print"You will have to do 'git p4 sync' and rebase."17841785ifgitConfigBool("git-p4.exportLabels"):1786 self.exportLabels =True17871788if self.exportLabels:1789 p4Labels =getP4Labels(self.depotPath)1790 gitTags =getGitTags()17911792 missingGitTags = gitTags - p4Labels1793 self.exportGitTags(missingGitTags)17941795# exit with error unless everything applied perfectly1796iflen(commits) !=len(applied):1797 sys.exit(1)17981799return True18001801classView(object):1802"""Represent a p4 view ("p4 help views"), and map files in a1803 repo according to the view."""18041805def__init__(self, client_name):1806 self.mappings = []1807 self.client_prefix ="//%s/"% client_name1808# cache results of "p4 where" to lookup client file locations1809 self.client_spec_path_cache = {}18101811defappend(self, view_line):1812"""Parse a view line, splitting it into depot and client1813 sides. Append to self.mappings, preserving order. This1814 is only needed for tag creation."""18151816# Split the view line into exactly two words. P4 enforces1817# structure on these lines that simplifies this quite a bit.1818#1819# Either or both words may be double-quoted.1820# Single quotes do not matter.1821# Double-quote marks cannot occur inside the words.1822# A + or - prefix is also inside the quotes.1823# There are no quotes unless they contain a space.1824# The line is already white-space stripped.1825# The two words are separated by a single space.1826#1827if view_line[0] =='"':1828# First word is double quoted. Find its end.1829 close_quote_index = view_line.find('"',1)1830if close_quote_index <=0:1831die("No first-word closing quote found:%s"% view_line)1832 depot_side = view_line[1:close_quote_index]1833# skip closing quote and space1834 rhs_index = close_quote_index +1+11835else:1836 space_index = view_line.find(" ")1837if space_index <=0:1838die("No word-splitting space found:%s"% view_line)1839 depot_side = view_line[0:space_index]1840 rhs_index = space_index +118411842# prefix + means overlay on previous mapping1843if depot_side.startswith("+"):1844 depot_side = depot_side[1:]18451846# prefix - means exclude this path, leave out of mappings1847 exclude =False1848if depot_side.startswith("-"):1849 exclude =True1850 depot_side = depot_side[1:]18511852if not exclude:1853 self.mappings.append(depot_side)18541855defconvert_client_path(self, clientFile):1856# chop off //client/ part to make it relative1857if not clientFile.startswith(self.client_prefix):1858die("No prefix '%s' on clientFile '%s'"%1859(self.client_prefix, clientFile))1860return clientFile[len(self.client_prefix):]18611862defupdate_client_spec_path_cache(self, files):1863""" Caching file paths by "p4 where" batch query """18641865# List depot file paths exclude that already cached1866 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]18671868iflen(fileArgs) ==0:1869return# All files in cache18701871 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1872for res in where_result:1873if"code"in res and res["code"] =="error":1874# assume error is "... file(s) not in client view"1875continue1876if"clientFile"not in res:1877die("No clientFile in 'p4 where' output")1878if"unmap"in res:1879# it will list all of them, but only one not unmap-ped1880continue1881 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])18821883# not found files or unmap files set to ""1884for depotFile in fileArgs:1885if depotFile not in self.client_spec_path_cache:1886 self.client_spec_path_cache[depotFile] =""18871888defmap_in_client(self, depot_path):1889"""Return the relative location in the client where this1890 depot file should live. Returns "" if the file should1891 not be mapped in the client."""18921893if depot_path in self.client_spec_path_cache:1894return self.client_spec_path_cache[depot_path]18951896die("Error:%sis not found in client spec path"% depot_path )1897return""18981899classP4Sync(Command, P4UserMap):1900 delete_actions = ("delete","move/delete","purge")19011902def__init__(self):1903 Command.__init__(self)1904 P4UserMap.__init__(self)1905 self.options = [1906 optparse.make_option("--branch", dest="branch"),1907 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1908 optparse.make_option("--changesfile", dest="changesFile"),1909 optparse.make_option("--silent", dest="silent", action="store_true"),1910 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1911 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1912 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1913help="Import into refs/heads/ , not refs/remotes"),1914 optparse.make_option("--max-changes", dest="maxChanges"),1915 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1916help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1917 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1918help="Only sync files that are included in the Perforce Client Spec"),1919 optparse.make_option("-/", dest="cloneExclude",1920 action="append",type="string",1921help="exclude depot path"),1922]1923 self.description ="""Imports from Perforce into a git repository.\n1924 example:1925 //depot/my/project/ -- to import the current head1926 //depot/my/project/@all -- to import everything1927 //depot/my/project/@1,6 -- to import only from revision 1 to 619281929 (a ... is not needed in the path p4 specification, it's added implicitly)"""19301931 self.usage +=" //depot/path[@revRange]"1932 self.silent =False1933 self.createdBranches =set()1934 self.committedChanges =set()1935 self.branch =""1936 self.detectBranches =False1937 self.detectLabels =False1938 self.importLabels =False1939 self.changesFile =""1940 self.syncWithOrigin =True1941 self.importIntoRemotes =True1942 self.maxChanges =""1943 self.keepRepoPath =False1944 self.depotPaths =None1945 self.p4BranchesInGit = []1946 self.cloneExclude = []1947 self.useClientSpec =False1948 self.useClientSpec_from_options =False1949 self.clientSpecDirs =None1950 self.tempBranches = []1951 self.tempBranchLocation ="git-p4-tmp"19521953ifgitConfig("git-p4.syncFromOrigin") =="false":1954 self.syncWithOrigin =False19551956# This is required for the "append" cloneExclude action1957defensure_value(self, attr, value):1958if nothasattr(self, attr)orgetattr(self, attr)is None:1959setattr(self, attr, value)1960returngetattr(self, attr)19611962# Force a checkpoint in fast-import and wait for it to finish1963defcheckpoint(self):1964 self.gitStream.write("checkpoint\n\n")1965 self.gitStream.write("progress checkpoint\n\n")1966 out = self.gitOutput.readline()1967if self.verbose:1968print"checkpoint finished: "+ out19691970defextractFilesFromCommit(self, commit):1971 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1972for path in self.cloneExclude]1973 files = []1974 fnum =01975while commit.has_key("depotFile%s"% fnum):1976 path = commit["depotFile%s"% fnum]19771978if[p for p in self.cloneExclude1979ifp4PathStartsWith(path, p)]:1980 found =False1981else:1982 found = [p for p in self.depotPaths1983ifp4PathStartsWith(path, p)]1984if not found:1985 fnum = fnum +11986continue19871988file= {}1989file["path"] = path1990file["rev"] = commit["rev%s"% fnum]1991file["action"] = commit["action%s"% fnum]1992file["type"] = commit["type%s"% fnum]1993 files.append(file)1994 fnum = fnum +11995return files19961997defstripRepoPath(self, path, prefixes):1998"""When streaming files, this is called to map a p4 depot path1999 to where it should go in git. The prefixes are either2000 self.depotPaths, or self.branchPrefixes in the case of2001 branch detection."""20022003if self.useClientSpec:2004# branch detection moves files up a level (the branch name)2005# from what client spec interpretation gives2006 path = self.clientSpecDirs.map_in_client(path)2007if self.detectBranches:2008for b in self.knownBranches:2009if path.startswith(b +"/"):2010 path = path[len(b)+1:]20112012elif self.keepRepoPath:2013# Preserve everything in relative path name except leading2014# //depot/; just look at first prefix as they all should2015# be in the same depot.2016 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2017ifp4PathStartsWith(path, depot):2018 path = path[len(depot):]20192020else:2021for p in prefixes:2022ifp4PathStartsWith(path, p):2023 path = path[len(p):]2024break20252026 path =wildcard_decode(path)2027return path20282029defsplitFilesIntoBranches(self, commit):2030"""Look at each depotFile in the commit to figure out to what2031 branch it belongs."""20322033if self.clientSpecDirs:2034 files = self.extractFilesFromCommit(commit)2035 self.clientSpecDirs.update_client_spec_path_cache(files)20362037 branches = {}2038 fnum =02039while commit.has_key("depotFile%s"% fnum):2040 path = commit["depotFile%s"% fnum]2041 found = [p for p in self.depotPaths2042ifp4PathStartsWith(path, p)]2043if not found:2044 fnum = fnum +12045continue20462047file= {}2048file["path"] = path2049file["rev"] = commit["rev%s"% fnum]2050file["action"] = commit["action%s"% fnum]2051file["type"] = commit["type%s"% fnum]2052 fnum = fnum +120532054# start with the full relative path where this file would2055# go in a p4 client2056if self.useClientSpec:2057 relPath = self.clientSpecDirs.map_in_client(path)2058else:2059 relPath = self.stripRepoPath(path, self.depotPaths)20602061for branch in self.knownBranches.keys():2062# add a trailing slash so that a commit into qt/4.2foo2063# doesn't end up in qt/4.2, e.g.2064if relPath.startswith(branch +"/"):2065if branch not in branches:2066 branches[branch] = []2067 branches[branch].append(file)2068break20692070return branches20712072# output one file from the P4 stream2073# - helper for streamP4Files20742075defstreamOneP4File(self,file, contents):2076 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2077if verbose:2078 sys.stderr.write("%s\n"% relPath)20792080(type_base, type_mods) =split_p4_type(file["type"])20812082 git_mode ="100644"2083if"x"in type_mods:2084 git_mode ="100755"2085if type_base =="symlink":2086 git_mode ="120000"2087# p4 print on a symlink sometimes contains "target\n";2088# if it does, remove the newline2089 data =''.join(contents)2090if not data:2091# Some version of p4 allowed creating a symlink that pointed2092# to nothing. This causes p4 errors when checking out such2093# a change, and errors here too. Work around it by ignoring2094# the bad symlink; hopefully a future change fixes it.2095print"\nIgnoring empty symlink in%s"%file['depotFile']2096return2097elif data[-1] =='\n':2098 contents = [data[:-1]]2099else:2100 contents = [data]21012102if type_base =="utf16":2103# p4 delivers different text in the python output to -G2104# than it does when using "print -o", or normal p4 client2105# operations. utf16 is converted to ascii or utf8, perhaps.2106# But ascii text saved as -t utf16 is completely mangled.2107# Invoke print -o to get the real contents.2108#2109# On windows, the newlines will always be mangled by print, so put2110# them back too. This is not needed to the cygwin windows version,2111# just the native "NT" type.2112#2113 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2114ifp4_version_string().find("/NT") >=0:2115 text = text.replace("\r\n","\n")2116 contents = [ text ]21172118if type_base =="apple":2119# Apple filetype files will be streamed as a concatenation of2120# its appledouble header and the contents. This is useless2121# on both macs and non-macs. If using "print -q -o xx", it2122# will create "xx" with the data, and "%xx" with the header.2123# This is also not very useful.2124#2125# Ideally, someday, this script can learn how to generate2126# appledouble files directly and import those to git, but2127# non-mac machines can never find a use for apple filetype.2128print"\nIgnoring apple filetype file%s"%file['depotFile']2129return21302131# Note that we do not try to de-mangle keywords on utf16 files,2132# even though in theory somebody may want that.2133 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2134if pattern:2135 regexp = re.compile(pattern, re.VERBOSE)2136 text =''.join(contents)2137 text = regexp.sub(r'$\1$', text)2138 contents = [ text ]21392140 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21412142# total length...2143 length =02144for d in contents:2145 length = length +len(d)21462147 self.gitStream.write("data%d\n"% length)2148for d in contents:2149 self.gitStream.write(d)2150 self.gitStream.write("\n")21512152defstreamOneP4Deletion(self,file):2153 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2154if verbose:2155 sys.stderr.write("delete%s\n"% relPath)2156 self.gitStream.write("D%s\n"% relPath)21572158# handle another chunk of streaming data2159defstreamP4FilesCb(self, marshalled):21602161# catch p4 errors and complain2162 err =None2163if"code"in marshalled:2164if marshalled["code"] =="error":2165if"data"in marshalled:2166 err = marshalled["data"].rstrip()2167if err:2168 f =None2169if self.stream_have_file_info:2170if"depotFile"in self.stream_file:2171 f = self.stream_file["depotFile"]2172# force a failure in fast-import, else an empty2173# commit will be made2174 self.gitStream.write("\n")2175 self.gitStream.write("die-now\n")2176 self.gitStream.close()2177# ignore errors, but make sure it exits first2178 self.importProcess.wait()2179if f:2180die("Error from p4 print for%s:%s"% (f, err))2181else:2182die("Error from p4 print:%s"% err)21832184if marshalled.has_key('depotFile')and self.stream_have_file_info:2185# start of a new file - output the old one first2186 self.streamOneP4File(self.stream_file, self.stream_contents)2187 self.stream_file = {}2188 self.stream_contents = []2189 self.stream_have_file_info =False21902191# pick up the new file information... for the2192# 'data' field we need to append to our array2193for k in marshalled.keys():2194if k =='data':2195 self.stream_contents.append(marshalled['data'])2196else:2197 self.stream_file[k] = marshalled[k]21982199 self.stream_have_file_info =True22002201# Stream directly from "p4 files" into "git fast-import"2202defstreamP4Files(self, files):2203 filesForCommit = []2204 filesToRead = []2205 filesToDelete = []22062207for f in files:2208# if using a client spec, only add the files that have2209# a path in the client2210if self.clientSpecDirs:2211if self.clientSpecDirs.map_in_client(f['path']) =="":2212continue22132214 filesForCommit.append(f)2215if f['action']in self.delete_actions:2216 filesToDelete.append(f)2217else:2218 filesToRead.append(f)22192220# deleted files...2221for f in filesToDelete:2222 self.streamOneP4Deletion(f)22232224iflen(filesToRead) >0:2225 self.stream_file = {}2226 self.stream_contents = []2227 self.stream_have_file_info =False22282229# curry self argument2230defstreamP4FilesCbSelf(entry):2231 self.streamP4FilesCb(entry)22322233 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22342235p4CmdList(["-x","-","print"],2236 stdin=fileArgs,2237 cb=streamP4FilesCbSelf)22382239# do the last chunk2240if self.stream_file.has_key('depotFile'):2241 self.streamOneP4File(self.stream_file, self.stream_contents)22422243defmake_email(self, userid):2244if userid in self.users:2245return self.users[userid]2246else:2247return"%s<a@b>"% userid22482249# Stream a p4 tag2250defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2251if verbose:2252print"writing tag%sfor commit%s"% (labelName, commit)2253 gitStream.write("tag%s\n"% labelName)2254 gitStream.write("from%s\n"% commit)22552256if labelDetails.has_key('Owner'):2257 owner = labelDetails["Owner"]2258else:2259 owner =None22602261# Try to use the owner of the p4 label, or failing that,2262# the current p4 user id.2263if owner:2264 email = self.make_email(owner)2265else:2266 email = self.make_email(self.p4UserId())2267 tagger ="%s %s %s"% (email, epoch, self.tz)22682269 gitStream.write("tagger%s\n"% tagger)22702271print"labelDetails=",labelDetails2272if labelDetails.has_key('Description'):2273 description = labelDetails['Description']2274else:2275 description ='Label from git p4'22762277 gitStream.write("data%d\n"%len(description))2278 gitStream.write(description)2279 gitStream.write("\n")22802281defcommit(self, details, files, branch, parent =""):2282 epoch = details["time"]2283 author = details["user"]22842285if self.verbose:2286print"commit into%s"% branch22872288# start with reading files; if that fails, we should not2289# create a commit.2290 new_files = []2291for f in files:2292if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2293 new_files.append(f)2294else:2295 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])22962297if self.clientSpecDirs:2298 self.clientSpecDirs.update_client_spec_path_cache(files)22992300 self.gitStream.write("commit%s\n"% branch)2301# gitStream.write("mark :%s\n" % details["change"])2302 self.committedChanges.add(int(details["change"]))2303 committer =""2304if author not in self.users:2305 self.getUserMapFromPerforceServer()2306 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23072308 self.gitStream.write("committer%s\n"% committer)23092310 self.gitStream.write("data <<EOT\n")2311 self.gitStream.write(details["desc"])2312 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2313(','.join(self.branchPrefixes), details["change"]))2314iflen(details['options']) >0:2315 self.gitStream.write(": options =%s"% details['options'])2316 self.gitStream.write("]\nEOT\n\n")23172318iflen(parent) >0:2319if self.verbose:2320print"parent%s"% parent2321 self.gitStream.write("from%s\n"% parent)23222323 self.streamP4Files(new_files)2324 self.gitStream.write("\n")23252326 change =int(details["change"])23272328if self.labels.has_key(change):2329 label = self.labels[change]2330 labelDetails = label[0]2331 labelRevisions = label[1]2332if self.verbose:2333print"Change%sis labelled%s"% (change, labelDetails)23342335 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2336for p in self.branchPrefixes])23372338iflen(files) ==len(labelRevisions):23392340 cleanedFiles = {}2341for info in files:2342if info["action"]in self.delete_actions:2343continue2344 cleanedFiles[info["depotFile"]] = info["rev"]23452346if cleanedFiles == labelRevisions:2347 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23482349else:2350if not self.silent:2351print("Tag%sdoes not match with change%s: files do not match."2352% (labelDetails["label"], change))23532354else:2355if not self.silent:2356print("Tag%sdoes not match with change%s: file count is different."2357% (labelDetails["label"], change))23582359# Build a dictionary of changelists and labels, for "detect-labels" option.2360defgetLabels(self):2361 self.labels = {}23622363 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2364iflen(l) >0and not self.silent:2365print"Finding files belonging to labels in%s"% `self.depotPaths`23662367for output in l:2368 label = output["label"]2369 revisions = {}2370 newestChange =02371if self.verbose:2372print"Querying files for label%s"% label2373forfileinp4CmdList(["files"] +2374["%s...@%s"% (p, label)2375for p in self.depotPaths]):2376 revisions[file["depotFile"]] =file["rev"]2377 change =int(file["change"])2378if change > newestChange:2379 newestChange = change23802381 self.labels[newestChange] = [output, revisions]23822383if self.verbose:2384print"Label changes:%s"% self.labels.keys()23852386# Import p4 labels as git tags. A direct mapping does not2387# exist, so assume that if all the files are at the same revision2388# then we can use that, or it's something more complicated we should2389# just ignore.2390defimportP4Labels(self, stream, p4Labels):2391if verbose:2392print"import p4 labels: "+' '.join(p4Labels)23932394 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2395 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2396iflen(validLabelRegexp) ==0:2397 validLabelRegexp = defaultLabelRegexp2398 m = re.compile(validLabelRegexp)23992400for name in p4Labels:2401 commitFound =False24022403if not m.match(name):2404if verbose:2405print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2406continue24072408if name in ignoredP4Labels:2409continue24102411 labelDetails =p4CmdList(['label',"-o", name])[0]24122413# get the most recent changelist for each file in this label2414 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2415for p in self.depotPaths])24162417if change.has_key('change'):2418# find the corresponding git commit; take the oldest commit2419 changelist =int(change['change'])2420 gitCommit =read_pipe(["git","rev-list","--max-count=1",2421"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2422iflen(gitCommit) ==0:2423print"could not find git commit for changelist%d"% changelist2424else:2425 gitCommit = gitCommit.strip()2426 commitFound =True2427# Convert from p4 time format2428try:2429 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2430exceptValueError:2431print"Could not convert label time%s"% labelDetails['Update']2432 tmwhen =124332434 when =int(time.mktime(tmwhen))2435 self.streamTag(stream, name, labelDetails, gitCommit, when)2436if verbose:2437print"p4 label%smapped to git commit%s"% (name, gitCommit)2438else:2439if verbose:2440print"Label%shas no changelists - possibly deleted?"% name24412442if not commitFound:2443# We can't import this label; don't try again as it will get very2444# expensive repeatedly fetching all the files for labels that will2445# never be imported. If the label is moved in the future, the2446# ignore will need to be removed manually.2447system(["git","config","--add","git-p4.ignoredP4Labels", name])24482449defguessProjectName(self):2450for p in self.depotPaths:2451if p.endswith("/"):2452 p = p[:-1]2453 p = p[p.strip().rfind("/") +1:]2454if not p.endswith("/"):2455 p +="/"2456return p24572458defgetBranchMapping(self):2459 lostAndFoundBranches =set()24602461 user =gitConfig("git-p4.branchUser")2462iflen(user) >0:2463 command ="branches -u%s"% user2464else:2465 command ="branches"24662467for info inp4CmdList(command):2468 details =p4Cmd(["branch","-o", info["branch"]])2469 viewIdx =02470while details.has_key("View%s"% viewIdx):2471 paths = details["View%s"% viewIdx].split(" ")2472 viewIdx = viewIdx +12473# require standard //depot/foo/... //depot/bar/... mapping2474iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2475continue2476 source = paths[0]2477 destination = paths[1]2478## HACK2479ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2480 source = source[len(self.depotPaths[0]):-4]2481 destination = destination[len(self.depotPaths[0]):-4]24822483if destination in self.knownBranches:2484if not self.silent:2485print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2486print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2487continue24882489 self.knownBranches[destination] = source24902491 lostAndFoundBranches.discard(destination)24922493if source not in self.knownBranches:2494 lostAndFoundBranches.add(source)24952496# Perforce does not strictly require branches to be defined, so we also2497# check git config for a branch list.2498#2499# Example of branch definition in git config file:2500# [git-p4]2501# branchList=main:branchA2502# branchList=main:branchB2503# branchList=branchA:branchC2504 configBranches =gitConfigList("git-p4.branchList")2505for branch in configBranches:2506if branch:2507(source, destination) = branch.split(":")2508 self.knownBranches[destination] = source25092510 lostAndFoundBranches.discard(destination)25112512if source not in self.knownBranches:2513 lostAndFoundBranches.add(source)251425152516for branch in lostAndFoundBranches:2517 self.knownBranches[branch] = branch25182519defgetBranchMappingFromGitBranches(self):2520 branches =p4BranchesInGit(self.importIntoRemotes)2521for branch in branches.keys():2522if branch =="master":2523 branch ="main"2524else:2525 branch = branch[len(self.projectName):]2526 self.knownBranches[branch] = branch25272528defupdateOptionDict(self, d):2529 option_keys = {}2530if self.keepRepoPath:2531 option_keys['keepRepoPath'] =125322533 d["options"] =' '.join(sorted(option_keys.keys()))25342535defreadOptions(self, d):2536 self.keepRepoPath = (d.has_key('options')2537and('keepRepoPath'in d['options']))25382539defgitRefForBranch(self, branch):2540if branch =="main":2541return self.refPrefix +"master"25422543iflen(branch) <=0:2544return branch25452546return self.refPrefix + self.projectName + branch25472548defgitCommitByP4Change(self, ref, change):2549if self.verbose:2550print"looking in ref "+ ref +" for change%susing bisect..."% change25512552 earliestCommit =""2553 latestCommit =parseRevision(ref)25542555while True:2556if self.verbose:2557print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2558 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2559iflen(next) ==0:2560if self.verbose:2561print"argh"2562return""2563 log =extractLogMessageFromGitCommit(next)2564 settings =extractSettingsGitLog(log)2565 currentChange =int(settings['change'])2566if self.verbose:2567print"current change%s"% currentChange25682569if currentChange == change:2570if self.verbose:2571print"found%s"% next2572return next25732574if currentChange < change:2575 earliestCommit ="^%s"% next2576else:2577 latestCommit ="%s"% next25782579return""25802581defimportNewBranch(self, branch, maxChange):2582# make fast-import flush all changes to disk and update the refs using the checkpoint2583# command so that we can try to find the branch parent in the git history2584 self.gitStream.write("checkpoint\n\n");2585 self.gitStream.flush();2586 branchPrefix = self.depotPaths[0] + branch +"/"2587range="@1,%s"% maxChange2588#print "prefix" + branchPrefix2589 changes =p4ChangesForPaths([branchPrefix],range)2590iflen(changes) <=0:2591return False2592 firstChange = changes[0]2593#print "first change in branch: %s" % firstChange2594 sourceBranch = self.knownBranches[branch]2595 sourceDepotPath = self.depotPaths[0] + sourceBranch2596 sourceRef = self.gitRefForBranch(sourceBranch)2597#print "source " + sourceBranch25982599 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2600#print "branch parent: %s" % branchParentChange2601 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2602iflen(gitParent) >0:2603 self.initialParents[self.gitRefForBranch(branch)] = gitParent2604#print "parent git commit: %s" % gitParent26052606 self.importChanges(changes)2607return True26082609defsearchParent(self, parent, branch, target):2610 parentFound =False2611for blob inread_pipe_lines(["git","rev-list","--reverse",2612"--no-merges", parent]):2613 blob = blob.strip()2614iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2615 parentFound =True2616if self.verbose:2617print"Found parent of%sin commit%s"% (branch, blob)2618break2619if parentFound:2620return blob2621else:2622return None26232624defimportChanges(self, changes):2625 cnt =12626for change in changes:2627 description =p4_describe(change)2628 self.updateOptionDict(description)26292630if not self.silent:2631 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2632 sys.stdout.flush()2633 cnt = cnt +126342635try:2636if self.detectBranches:2637 branches = self.splitFilesIntoBranches(description)2638for branch in branches.keys():2639## HACK --hwn2640 branchPrefix = self.depotPaths[0] + branch +"/"2641 self.branchPrefixes = [ branchPrefix ]26422643 parent =""26442645 filesForCommit = branches[branch]26462647if self.verbose:2648print"branch is%s"% branch26492650 self.updatedBranches.add(branch)26512652if branch not in self.createdBranches:2653 self.createdBranches.add(branch)2654 parent = self.knownBranches[branch]2655if parent == branch:2656 parent =""2657else:2658 fullBranch = self.projectName + branch2659if fullBranch not in self.p4BranchesInGit:2660if not self.silent:2661print("\nImporting new branch%s"% fullBranch);2662if self.importNewBranch(branch, change -1):2663 parent =""2664 self.p4BranchesInGit.append(fullBranch)2665if not self.silent:2666print("\nResuming with change%s"% change);26672668if self.verbose:2669print"parent determined through known branches:%s"% parent26702671 branch = self.gitRefForBranch(branch)2672 parent = self.gitRefForBranch(parent)26732674if self.verbose:2675print"looking for initial parent for%s; current parent is%s"% (branch, parent)26762677iflen(parent) ==0and branch in self.initialParents:2678 parent = self.initialParents[branch]2679del self.initialParents[branch]26802681 blob =None2682iflen(parent) >0:2683 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2684if self.verbose:2685print"Creating temporary branch: "+ tempBranch2686 self.commit(description, filesForCommit, tempBranch)2687 self.tempBranches.append(tempBranch)2688 self.checkpoint()2689 blob = self.searchParent(parent, branch, tempBranch)2690if blob:2691 self.commit(description, filesForCommit, branch, blob)2692else:2693if self.verbose:2694print"Parent of%snot found. Committing into head of%s"% (branch, parent)2695 self.commit(description, filesForCommit, branch, parent)2696else:2697 files = self.extractFilesFromCommit(description)2698 self.commit(description, files, self.branch,2699 self.initialParent)2700# only needed once, to connect to the previous commit2701 self.initialParent =""2702exceptIOError:2703print self.gitError.read()2704 sys.exit(1)27052706defimportHeadRevision(self, revision):2707print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27082709 details = {}2710 details["user"] ="git perforce import user"2711 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2712% (' '.join(self.depotPaths), revision))2713 details["change"] = revision2714 newestRevision =027152716 fileCnt =02717 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27182719for info inp4CmdList(["files"] + fileArgs):27202721if'code'in info and info['code'] =='error':2722 sys.stderr.write("p4 returned an error:%s\n"2723% info['data'])2724if info['data'].find("must refer to client") >=0:2725 sys.stderr.write("This particular p4 error is misleading.\n")2726 sys.stderr.write("Perhaps the depot path was misspelled.\n");2727 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2728 sys.exit(1)2729if'p4ExitCode'in info:2730 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2731 sys.exit(1)273227332734 change =int(info["change"])2735if change > newestRevision:2736 newestRevision = change27372738if info["action"]in self.delete_actions:2739# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2740#fileCnt = fileCnt + 12741continue27422743for prop in["depotFile","rev","action","type"]:2744 details["%s%s"% (prop, fileCnt)] = info[prop]27452746 fileCnt = fileCnt +127472748 details["change"] = newestRevision27492750# Use time from top-most change so that all git p4 clones of2751# the same p4 repo have the same commit SHA1s.2752 res =p4_describe(newestRevision)2753 details["time"] = res["time"]27542755 self.updateOptionDict(details)2756try:2757 self.commit(details, self.extractFilesFromCommit(details), self.branch)2758exceptIOError:2759print"IO error with git fast-import. Is your git version recent enough?"2760print self.gitError.read()276127622763defrun(self, args):2764 self.depotPaths = []2765 self.changeRange =""2766 self.previousDepotPaths = []2767 self.hasOrigin =False27682769# map from branch depot path to parent branch2770 self.knownBranches = {}2771 self.initialParents = {}27722773if self.importIntoRemotes:2774 self.refPrefix ="refs/remotes/p4/"2775else:2776 self.refPrefix ="refs/heads/p4/"27772778if self.syncWithOrigin:2779 self.hasOrigin =originP4BranchesExist()2780if self.hasOrigin:2781if not self.silent:2782print'Syncing with origin first, using "git fetch origin"'2783system("git fetch origin")27842785 branch_arg_given =bool(self.branch)2786iflen(self.branch) ==0:2787 self.branch = self.refPrefix +"master"2788ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2789system("git update-ref%srefs/heads/p4"% self.branch)2790system("git branch -D p4")27912792# accept either the command-line option, or the configuration variable2793if self.useClientSpec:2794# will use this after clone to set the variable2795 self.useClientSpec_from_options =True2796else:2797ifgitConfigBool("git-p4.useclientspec"):2798 self.useClientSpec =True2799if self.useClientSpec:2800 self.clientSpecDirs =getClientSpec()28012802# TODO: should always look at previous commits,2803# merge with previous imports, if possible.2804if args == []:2805if self.hasOrigin:2806createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28072808# branches holds mapping from branch name to sha12809 branches =p4BranchesInGit(self.importIntoRemotes)28102811# restrict to just this one, disabling detect-branches2812if branch_arg_given:2813 short = self.branch.split("/")[-1]2814if short in branches:2815 self.p4BranchesInGit = [ short ]2816else:2817 self.p4BranchesInGit = branches.keys()28182819iflen(self.p4BranchesInGit) >1:2820if not self.silent:2821print"Importing from/into multiple branches"2822 self.detectBranches =True2823for branch in branches.keys():2824 self.initialParents[self.refPrefix + branch] = \2825 branches[branch]28262827if self.verbose:2828print"branches:%s"% self.p4BranchesInGit28292830 p4Change =02831for branch in self.p4BranchesInGit:2832 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28332834 settings =extractSettingsGitLog(logMsg)28352836 self.readOptions(settings)2837if(settings.has_key('depot-paths')2838and settings.has_key('change')):2839 change =int(settings['change']) +12840 p4Change =max(p4Change, change)28412842 depotPaths =sorted(settings['depot-paths'])2843if self.previousDepotPaths == []:2844 self.previousDepotPaths = depotPaths2845else:2846 paths = []2847for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2848 prev_list = prev.split("/")2849 cur_list = cur.split("/")2850for i inrange(0,min(len(cur_list),len(prev_list))):2851if cur_list[i] <> prev_list[i]:2852 i = i -12853break28542855 paths.append("/".join(cur_list[:i +1]))28562857 self.previousDepotPaths = paths28582859if p4Change >0:2860 self.depotPaths =sorted(self.previousDepotPaths)2861 self.changeRange ="@%s,#head"% p4Change2862if not self.silent and not self.detectBranches:2863print"Performing incremental import into%sgit branch"% self.branch28642865# accept multiple ref name abbreviations:2866# refs/foo/bar/branch -> use it exactly2867# p4/branch -> prepend refs/remotes/ or refs/heads/2868# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2869if not self.branch.startswith("refs/"):2870if self.importIntoRemotes:2871 prepend ="refs/remotes/"2872else:2873 prepend ="refs/heads/"2874if not self.branch.startswith("p4/"):2875 prepend +="p4/"2876 self.branch = prepend + self.branch28772878iflen(args) ==0and self.depotPaths:2879if not self.silent:2880print"Depot paths:%s"%' '.join(self.depotPaths)2881else:2882if self.depotPaths and self.depotPaths != args:2883print("previous import used depot path%sand now%swas specified. "2884"This doesn't work!"% (' '.join(self.depotPaths),2885' '.join(args)))2886 sys.exit(1)28872888 self.depotPaths =sorted(args)28892890 revision =""2891 self.users = {}28922893# Make sure no revision specifiers are used when --changesfile2894# is specified.2895 bad_changesfile =False2896iflen(self.changesFile) >0:2897for p in self.depotPaths:2898if p.find("@") >=0or p.find("#") >=0:2899 bad_changesfile =True2900break2901if bad_changesfile:2902die("Option --changesfile is incompatible with revision specifiers")29032904 newPaths = []2905for p in self.depotPaths:2906if p.find("@") != -1:2907 atIdx = p.index("@")2908 self.changeRange = p[atIdx:]2909if self.changeRange =="@all":2910 self.changeRange =""2911elif','not in self.changeRange:2912 revision = self.changeRange2913 self.changeRange =""2914 p = p[:atIdx]2915elif p.find("#") != -1:2916 hashIdx = p.index("#")2917 revision = p[hashIdx:]2918 p = p[:hashIdx]2919elif self.previousDepotPaths == []:2920# pay attention to changesfile, if given, else import2921# the entire p4 tree at the head revision2922iflen(self.changesFile) ==0:2923 revision ="#head"29242925 p = re.sub("\.\.\.$","", p)2926if not p.endswith("/"):2927 p +="/"29282929 newPaths.append(p)29302931 self.depotPaths = newPaths29322933# --detect-branches may change this for each branch2934 self.branchPrefixes = self.depotPaths29352936 self.loadUserMapFromCache()2937 self.labels = {}2938if self.detectLabels:2939 self.getLabels();29402941if self.detectBranches:2942## FIXME - what's a P4 projectName ?2943 self.projectName = self.guessProjectName()29442945if self.hasOrigin:2946 self.getBranchMappingFromGitBranches()2947else:2948 self.getBranchMapping()2949if self.verbose:2950print"p4-git branches:%s"% self.p4BranchesInGit2951print"initial parents:%s"% self.initialParents2952for b in self.p4BranchesInGit:2953if b !="master":29542955## FIXME2956 b = b[len(self.projectName):]2957 self.createdBranches.add(b)29582959 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29602961 self.importProcess = subprocess.Popen(["git","fast-import"],2962 stdin=subprocess.PIPE,2963 stdout=subprocess.PIPE,2964 stderr=subprocess.PIPE);2965 self.gitOutput = self.importProcess.stdout2966 self.gitStream = self.importProcess.stdin2967 self.gitError = self.importProcess.stderr29682969if revision:2970 self.importHeadRevision(revision)2971else:2972 changes = []29732974iflen(self.changesFile) >0:2975 output =open(self.changesFile).readlines()2976 changeSet =set()2977for line in output:2978 changeSet.add(int(line))29792980for change in changeSet:2981 changes.append(change)29822983 changes.sort()2984else:2985# catch "git p4 sync" with no new branches, in a repo that2986# does not have any existing p4 branches2987iflen(args) ==0:2988if not self.p4BranchesInGit:2989die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")29902991# The default branch is master, unless --branch is used to2992# specify something else. Make sure it exists, or complain2993# nicely about how to use --branch.2994if not self.detectBranches:2995if notbranch_exists(self.branch):2996if branch_arg_given:2997die("Error: branch%sdoes not exist."% self.branch)2998else:2999die("Error: no branch%s; perhaps specify one with --branch."%3000 self.branch)30013002if self.verbose:3003print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3004 self.changeRange)3005 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30063007iflen(self.maxChanges) >0:3008 changes = changes[:min(int(self.maxChanges),len(changes))]30093010iflen(changes) ==0:3011if not self.silent:3012print"No changes to import!"3013else:3014if not self.silent and not self.detectBranches:3015print"Import destination:%s"% self.branch30163017 self.updatedBranches =set()30183019if not self.detectBranches:3020if args:3021# start a new branch3022 self.initialParent =""3023else:3024# build on a previous revision3025 self.initialParent =parseRevision(self.branch)30263027 self.importChanges(changes)30283029if not self.silent:3030print""3031iflen(self.updatedBranches) >0:3032 sys.stdout.write("Updated branches: ")3033for b in self.updatedBranches:3034 sys.stdout.write("%s"% b)3035 sys.stdout.write("\n")30363037ifgitConfigBool("git-p4.importLabels"):3038 self.importLabels =True30393040if self.importLabels:3041 p4Labels =getP4Labels(self.depotPaths)3042 gitTags =getGitTags()30433044 missingP4Labels = p4Labels - gitTags3045 self.importP4Labels(self.gitStream, missingP4Labels)30463047 self.gitStream.close()3048if self.importProcess.wait() !=0:3049die("fast-import failed:%s"% self.gitError.read())3050 self.gitOutput.close()3051 self.gitError.close()30523053# Cleanup temporary branches created during import3054if self.tempBranches != []:3055for branch in self.tempBranches:3056read_pipe("git update-ref -d%s"% branch)3057 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30583059# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3060# a convenient shortcut refname "p4".3061if self.importIntoRemotes:3062 head_ref = self.refPrefix +"HEAD"3063if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3064system(["git","symbolic-ref", head_ref, self.branch])30653066return True30673068classP4Rebase(Command):3069def__init__(self):3070 Command.__init__(self)3071 self.options = [3072 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3073]3074 self.importLabels =False3075 self.description = ("Fetches the latest revision from perforce and "3076+"rebases the current work (branch) against it")30773078defrun(self, args):3079 sync =P4Sync()3080 sync.importLabels = self.importLabels3081 sync.run([])30823083return self.rebase()30843085defrebase(self):3086if os.system("git update-index --refresh") !=0:3087die("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.");3088iflen(read_pipe("git diff-index HEAD --")) >0:3089die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");30903091[upstream, settings] =findUpstreamBranchPoint()3092iflen(upstream) ==0:3093die("Cannot find upstream branchpoint for rebase")30943095# the branchpoint may be p4/foo~3, so strip off the parent3096 upstream = re.sub("~[0-9]+$","", upstream)30973098print"Rebasing the current branch onto%s"% upstream3099 oldHead =read_pipe("git rev-parse HEAD").strip()3100system("git rebase%s"% upstream)3101system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3102return True31033104classP4Clone(P4Sync):3105def__init__(self):3106 P4Sync.__init__(self)3107 self.description ="Creates a new git repository and imports from Perforce into it"3108 self.usage ="usage: %prog [options] //depot/path[@revRange]"3109 self.options += [3110 optparse.make_option("--destination", dest="cloneDestination",3111 action='store', default=None,3112help="where to leave result of the clone"),3113 optparse.make_option("--bare", dest="cloneBare",3114 action="store_true", default=False),3115]3116 self.cloneDestination =None3117 self.needsGit =False3118 self.cloneBare =False31193120defdefaultDestination(self, args):3121## TODO: use common prefix of args?3122 depotPath = args[0]3123 depotDir = re.sub("(@[^@]*)$","", depotPath)3124 depotDir = re.sub("(#[^#]*)$","", depotDir)3125 depotDir = re.sub(r"\.\.\.$","", depotDir)3126 depotDir = re.sub(r"/$","", depotDir)3127return os.path.split(depotDir)[1]31283129defrun(self, args):3130iflen(args) <1:3131return False31323133if self.keepRepoPath and not self.cloneDestination:3134 sys.stderr.write("Must specify destination for --keep-path\n")3135 sys.exit(1)31363137 depotPaths = args31383139if not self.cloneDestination andlen(depotPaths) >1:3140 self.cloneDestination = depotPaths[-1]3141 depotPaths = depotPaths[:-1]31423143 self.cloneExclude = ["/"+p for p in self.cloneExclude]3144for p in depotPaths:3145if not p.startswith("//"):3146 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3147return False31483149if not self.cloneDestination:3150 self.cloneDestination = self.defaultDestination(args)31513152print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31533154if not os.path.exists(self.cloneDestination):3155 os.makedirs(self.cloneDestination)3156chdir(self.cloneDestination)31573158 init_cmd = ["git","init"]3159if self.cloneBare:3160 init_cmd.append("--bare")3161 retcode = subprocess.call(init_cmd)3162if retcode:3163raiseCalledProcessError(retcode, init_cmd)31643165if not P4Sync.run(self, depotPaths):3166return False31673168# create a master branch and check out a work tree3169ifgitBranchExists(self.branch):3170system(["git","branch","master", self.branch ])3171if not self.cloneBare:3172system(["git","checkout","-f"])3173else:3174print'Not checking out any branch, use ' \3175'"git checkout -q -b master <branch>"'31763177# auto-set this variable if invoked with --use-client-spec3178if self.useClientSpec_from_options:3179system("git config --bool git-p4.useclientspec true")31803181return True31823183classP4Branches(Command):3184def__init__(self):3185 Command.__init__(self)3186 self.options = [ ]3187 self.description = ("Shows the git branches that hold imports and their "3188+"corresponding perforce depot paths")3189 self.verbose =False31903191defrun(self, args):3192iforiginP4BranchesExist():3193createOrUpdateBranchesFromOrigin()31943195 cmdline ="git rev-parse --symbolic "3196 cmdline +=" --remotes"31973198for line inread_pipe_lines(cmdline):3199 line = line.strip()32003201if not line.startswith('p4/')or line =="p4/HEAD":3202continue3203 branch = line32043205 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3206 settings =extractSettingsGitLog(log)32073208print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3209return True32103211classHelpFormatter(optparse.IndentedHelpFormatter):3212def__init__(self):3213 optparse.IndentedHelpFormatter.__init__(self)32143215defformat_description(self, description):3216if description:3217return description +"\n"3218else:3219return""32203221defprintUsage(commands):3222print"usage:%s<command> [options]"% sys.argv[0]3223print""3224print"valid commands:%s"%", ".join(commands)3225print""3226print"Try%s<command> --help for command specific help."% sys.argv[0]3227print""32283229commands = {3230"debug": P4Debug,3231"submit": P4Submit,3232"commit": P4Submit,3233"sync": P4Sync,3234"rebase": P4Rebase,3235"clone": P4Clone,3236"rollback": P4RollBack,3237"branches": P4Branches3238}323932403241defmain():3242iflen(sys.argv[1:]) ==0:3243printUsage(commands.keys())3244 sys.exit(2)32453246 cmdName = sys.argv[1]3247try:3248 klass = commands[cmdName]3249 cmd =klass()3250exceptKeyError:3251print"unknown command%s"% cmdName3252print""3253printUsage(commands.keys())3254 sys.exit(2)32553256 options = cmd.options3257 cmd.gitdir = os.environ.get("GIT_DIR",None)32583259 args = sys.argv[2:]32603261 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3262if cmd.needsGit:3263 options.append(optparse.make_option("--git-dir", dest="gitdir"))32643265 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3266 options,3267 description = cmd.description,3268 formatter =HelpFormatter())32693270(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3271global verbose3272 verbose = cmd.verbose3273if cmd.needsGit:3274if cmd.gitdir ==None:3275 cmd.gitdir = os.path.abspath(".git")3276if notisValidGitDir(cmd.gitdir):3277 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3278if os.path.exists(cmd.gitdir):3279 cdup =read_pipe("git rev-parse --show-cdup").strip()3280iflen(cdup) >0:3281chdir(cdup);32823283if notisValidGitDir(cmd.gitdir):3284ifisValidGitDir(cmd.gitdir +"/.git"):3285 cmd.gitdir +="/.git"3286else:3287die("fatal: cannot locate git repository at%s"% cmd.gitdir)32883289 os.environ["GIT_DIR"] = cmd.gitdir32903291if not cmd.run(args):3292 parser.print_help()3293 sys.exit(2)329432953296if __name__ =='__main__':3297main()