1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25 26try: 27from subprocess import CalledProcessError 28exceptImportError: 29# from python2.7:subprocess.py 30# Exception classes used by this module. 31classCalledProcessError(Exception): 32"""This exception is raised when a process run by check_call() returns 33 a non-zero exit status. The exit status will be stored in the 34 returncode attribute.""" 35def__init__(self, returncode, cmd): 36 self.returncode = returncode 37 self.cmd = cmd 38def__str__(self): 39return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 40 41verbose =False 42 43# Only labels/tags matching this will be imported/exported 44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 45 46defp4_build_cmd(cmd): 47"""Build a suitable p4 command line. 48 49 This consolidates building and returning a p4 command line into one 50 location. It means that hooking into the environment, or other configuration 51 can be done more easily. 52 """ 53 real_cmd = ["p4"] 54 55 user =gitConfig("git-p4.user") 56iflen(user) >0: 57 real_cmd += ["-u",user] 58 59 password =gitConfig("git-p4.password") 60iflen(password) >0: 61 real_cmd += ["-P", password] 62 63 port =gitConfig("git-p4.port") 64iflen(port) >0: 65 real_cmd += ["-p", port] 66 67 host =gitConfig("git-p4.host") 68iflen(host) >0: 69 real_cmd += ["-H", host] 70 71 client =gitConfig("git-p4.client") 72iflen(client) >0: 73 real_cmd += ["-c", client] 74 75 76ifisinstance(cmd,basestring): 77 real_cmd =' '.join(real_cmd) +' '+ cmd 78else: 79 real_cmd += cmd 80return real_cmd 81 82defchdir(path, is_client_path=False): 83"""Do chdir to the given path, and set the PWD environment 84 variable for use by P4. It does not look at getcwd() output. 85 Since we're not using the shell, it is necessary to set the 86 PWD environment variable explicitly. 87 88 Normally, expand the path to force it to be absolute. This 89 addresses the use of relative path names inside P4 settings, 90 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 91 as given; it looks for .p4config using PWD. 92 93 If is_client_path, the path was handed to us directly by p4, 94 and may be a symbolic link. Do not call os.getcwd() in this 95 case, because it will cause p4 to think that PWD is not inside 96 the client path. 97 """ 98 99 os.chdir(path) 100if not is_client_path: 101 path = os.getcwd() 102 os.environ['PWD'] = path 103 104defdie(msg): 105if verbose: 106raiseException(msg) 107else: 108 sys.stderr.write(msg +"\n") 109 sys.exit(1) 110 111defwrite_pipe(c, stdin): 112if verbose: 113 sys.stderr.write('Writing pipe:%s\n'%str(c)) 114 115 expand =isinstance(c,basestring) 116 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 117 pipe = p.stdin 118 val = pipe.write(stdin) 119 pipe.close() 120if p.wait(): 121die('Command failed:%s'%str(c)) 122 123return val 124 125defp4_write_pipe(c, stdin): 126 real_cmd =p4_build_cmd(c) 127returnwrite_pipe(real_cmd, stdin) 128 129defread_pipe(c, ignore_error=False): 130if verbose: 131 sys.stderr.write('Reading pipe:%s\n'%str(c)) 132 133 expand =isinstance(c,basestring) 134 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 135 pipe = p.stdout 136 val = pipe.read() 137if p.wait()and not ignore_error: 138die('Command failed:%s'%str(c)) 139 140return val 141 142defp4_read_pipe(c, ignore_error=False): 143 real_cmd =p4_build_cmd(c) 144returnread_pipe(real_cmd, ignore_error) 145 146defread_pipe_lines(c): 147if verbose: 148 sys.stderr.write('Reading pipe:%s\n'%str(c)) 149 150 expand =isinstance(c, basestring) 151 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 152 pipe = p.stdout 153 val = pipe.readlines() 154if pipe.close()or p.wait(): 155die('Command failed:%s'%str(c)) 156 157return val 158 159defp4_read_pipe_lines(c): 160"""Specifically invoke p4 on the command supplied. """ 161 real_cmd =p4_build_cmd(c) 162returnread_pipe_lines(real_cmd) 163 164defp4_has_command(cmd): 165"""Ask p4 for help on this command. If it returns an error, the 166 command does not exist in this version of p4.""" 167 real_cmd =p4_build_cmd(["help", cmd]) 168 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 169 stderr=subprocess.PIPE) 170 p.communicate() 171return p.returncode ==0 172 173defp4_has_move_command(): 174"""See if the move command exists, that it supports -k, and that 175 it has not been administratively disabled. The arguments 176 must be correct, but the filenames do not have to exist. Use 177 ones with wildcards so even if they exist, it will fail.""" 178 179if notp4_has_command("move"): 180return False 181 cmd =p4_build_cmd(["move","-k","@from","@to"]) 182 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 183(out, err) = p.communicate() 184# return code will be 1 in either case 185if err.find("Invalid option") >=0: 186return False 187if err.find("disabled") >=0: 188return False 189# assume it failed because @... was invalid changelist 190return True 191 192defsystem(cmd): 193 expand =isinstance(cmd,basestring) 194if verbose: 195 sys.stderr.write("executing%s\n"%str(cmd)) 196 retcode = subprocess.call(cmd, shell=expand) 197if retcode: 198raiseCalledProcessError(retcode, cmd) 199 200defp4_system(cmd): 201"""Specifically invoke p4 as the system command. """ 202 real_cmd =p4_build_cmd(cmd) 203 expand =isinstance(real_cmd, basestring) 204 retcode = subprocess.call(real_cmd, shell=expand) 205if retcode: 206raiseCalledProcessError(retcode, real_cmd) 207 208_p4_version_string =None 209defp4_version_string(): 210"""Read the version string, showing just the last line, which 211 hopefully is the interesting version bit. 212 213 $ p4 -V 214 Perforce - The Fast Software Configuration Management System. 215 Copyright 1995-2011 Perforce Software. All rights reserved. 216 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 217 """ 218global _p4_version_string 219if not _p4_version_string: 220 a =p4_read_pipe_lines(["-V"]) 221 _p4_version_string = a[-1].rstrip() 222return _p4_version_string 223 224defp4_integrate(src, dest): 225p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 226 227defp4_sync(f, *options): 228p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 229 230defp4_add(f): 231# forcibly add file names with wildcards 232ifwildcard_present(f): 233p4_system(["add","-f", f]) 234else: 235p4_system(["add", f]) 236 237defp4_delete(f): 238p4_system(["delete",wildcard_encode(f)]) 239 240defp4_edit(f): 241p4_system(["edit",wildcard_encode(f)]) 242 243defp4_revert(f): 244p4_system(["revert",wildcard_encode(f)]) 245 246defp4_reopen(type, f): 247p4_system(["reopen","-t",type,wildcard_encode(f)]) 248 249defp4_move(src, dest): 250p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 251 252defp4_describe(change): 253"""Make sure it returns a valid result by checking for 254 the presence of field "time". Return a dict of the 255 results.""" 256 257 ds =p4CmdList(["describe","-s",str(change)]) 258iflen(ds) !=1: 259die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 260 261 d = ds[0] 262 263if"p4ExitCode"in d: 264die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 265str(d))) 266if"code"in d: 267if d["code"] =="error": 268die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 269 270if"time"not in d: 271die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 272 273return d 274 275# 276# Canonicalize the p4 type and return a tuple of the 277# base type, plus any modifiers. See "p4 help filetypes" 278# for a list and explanation. 279# 280defsplit_p4_type(p4type): 281 282 p4_filetypes_historical = { 283"ctempobj":"binary+Sw", 284"ctext":"text+C", 285"cxtext":"text+Cx", 286"ktext":"text+k", 287"kxtext":"text+kx", 288"ltext":"text+F", 289"tempobj":"binary+FSw", 290"ubinary":"binary+F", 291"uresource":"resource+F", 292"uxbinary":"binary+Fx", 293"xbinary":"binary+x", 294"xltext":"text+Fx", 295"xtempobj":"binary+Swx", 296"xtext":"text+x", 297"xunicode":"unicode+x", 298"xutf16":"utf16+x", 299} 300if p4type in p4_filetypes_historical: 301 p4type = p4_filetypes_historical[p4type] 302 mods ="" 303 s = p4type.split("+") 304 base = s[0] 305 mods ="" 306iflen(s) >1: 307 mods = s[1] 308return(base, mods) 309 310# 311# return the raw p4 type of a file (text, text+ko, etc) 312# 313defp4_type(file): 314 results =p4CmdList(["fstat","-T","headType",file]) 315return results[0]['headType'] 316 317# 318# Given a type base and modifier, return a regexp matching 319# the keywords that can be expanded in the file 320# 321defp4_keywords_regexp_for_type(base, type_mods): 322if base in("text","unicode","binary"): 323 kwords =None 324if"ko"in type_mods: 325 kwords ='Id|Header' 326elif"k"in type_mods: 327 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 328else: 329return None 330 pattern = r""" 331 \$ # Starts with a dollar, followed by... 332 (%s) # one of the keywords, followed by... 333 (:[^$\n]+)? # possibly an old expansion, followed by... 334 \$ # another dollar 335 """% kwords 336return pattern 337else: 338return None 339 340# 341# Given a file, return a regexp matching the possible 342# RCS keywords that will be expanded, or None for files 343# with kw expansion turned off. 344# 345defp4_keywords_regexp_for_file(file): 346if not os.path.exists(file): 347return None 348else: 349(type_base, type_mods) =split_p4_type(p4_type(file)) 350returnp4_keywords_regexp_for_type(type_base, type_mods) 351 352defsetP4ExecBit(file, mode): 353# Reopens an already open file and changes the execute bit to match 354# the execute bit setting in the passed in mode. 355 356 p4Type ="+x" 357 358if notisModeExec(mode): 359 p4Type =getP4OpenedType(file) 360 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 361 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 362if p4Type[-1] =="+": 363 p4Type = p4Type[0:-1] 364 365p4_reopen(p4Type,file) 366 367defgetP4OpenedType(file): 368# Returns the perforce file type for the given file. 369 370 result =p4_read_pipe(["opened",wildcard_encode(file)]) 371 match = re.match(".*\((.+)\)\r?$", result) 372if match: 373return match.group(1) 374else: 375die("Could not determine file type for%s(result: '%s')"% (file, result)) 376 377# Return the set of all p4 labels 378defgetP4Labels(depotPaths): 379 labels =set() 380ifisinstance(depotPaths,basestring): 381 depotPaths = [depotPaths] 382 383for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 384 label = l['label'] 385 labels.add(label) 386 387return labels 388 389# Return the set of all git tags 390defgetGitTags(): 391 gitTags =set() 392for line inread_pipe_lines(["git","tag"]): 393 tag = line.strip() 394 gitTags.add(tag) 395return gitTags 396 397defdiffTreePattern(): 398# This is a simple generator for the diff tree regex pattern. This could be 399# a class variable if this and parseDiffTreeEntry were a part of a class. 400 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 401while True: 402yield pattern 403 404defparseDiffTreeEntry(entry): 405"""Parses a single diff tree entry into its component elements. 406 407 See git-diff-tree(1) manpage for details about the format of the diff 408 output. This method returns a dictionary with the following elements: 409 410 src_mode - The mode of the source file 411 dst_mode - The mode of the destination file 412 src_sha1 - The sha1 for the source file 413 dst_sha1 - The sha1 fr the destination file 414 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 415 status_score - The score for the status (applicable for 'C' and 'R' 416 statuses). This is None if there is no score. 417 src - The path for the source file. 418 dst - The path for the destination file. This is only present for 419 copy or renames. If it is not present, this is None. 420 421 If the pattern is not matched, None is returned.""" 422 423 match =diffTreePattern().next().match(entry) 424if match: 425return{ 426'src_mode': match.group(1), 427'dst_mode': match.group(2), 428'src_sha1': match.group(3), 429'dst_sha1': match.group(4), 430'status': match.group(5), 431'status_score': match.group(6), 432'src': match.group(7), 433'dst': match.group(10) 434} 435return None 436 437defisModeExec(mode): 438# Returns True if the given git mode represents an executable file, 439# otherwise False. 440return mode[-3:] =="755" 441 442defisModeExecChanged(src_mode, dst_mode): 443returnisModeExec(src_mode) !=isModeExec(dst_mode) 444 445defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 446 447ifisinstance(cmd,basestring): 448 cmd ="-G "+ cmd 449 expand =True 450else: 451 cmd = ["-G"] + cmd 452 expand =False 453 454 cmd =p4_build_cmd(cmd) 455if verbose: 456 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 457 458# Use a temporary file to avoid deadlocks without 459# subprocess.communicate(), which would put another copy 460# of stdout into memory. 461 stdin_file =None 462if stdin is not None: 463 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 464ifisinstance(stdin,basestring): 465 stdin_file.write(stdin) 466else: 467for i in stdin: 468 stdin_file.write(i +'\n') 469 stdin_file.flush() 470 stdin_file.seek(0) 471 472 p4 = subprocess.Popen(cmd, 473 shell=expand, 474 stdin=stdin_file, 475 stdout=subprocess.PIPE) 476 477 result = [] 478try: 479while True: 480 entry = marshal.load(p4.stdout) 481if cb is not None: 482cb(entry) 483else: 484 result.append(entry) 485exceptEOFError: 486pass 487 exitCode = p4.wait() 488if exitCode !=0: 489 entry = {} 490 entry["p4ExitCode"] = exitCode 491 result.append(entry) 492 493return result 494 495defp4Cmd(cmd): 496list=p4CmdList(cmd) 497 result = {} 498for entry inlist: 499 result.update(entry) 500return result; 501 502defp4Where(depotPath): 503if not depotPath.endswith("/"): 504 depotPath +="/" 505 depotPath = depotPath +"..." 506 outputList =p4CmdList(["where", depotPath]) 507 output =None 508for entry in outputList: 509if"depotFile"in entry: 510if entry["depotFile"] == depotPath: 511 output = entry 512break 513elif"data"in entry: 514 data = entry.get("data") 515 space = data.find(" ") 516if data[:space] == depotPath: 517 output = entry 518break 519if output ==None: 520return"" 521if output["code"] =="error": 522return"" 523 clientPath ="" 524if"path"in output: 525 clientPath = output.get("path") 526elif"data"in output: 527 data = output.get("data") 528 lastSpace = data.rfind(" ") 529 clientPath = data[lastSpace +1:] 530 531if clientPath.endswith("..."): 532 clientPath = clientPath[:-3] 533return clientPath 534 535defcurrentGitBranch(): 536returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 537 538defisValidGitDir(path): 539if(os.path.exists(path +"/HEAD") 540and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 541return True; 542return False 543 544defparseRevision(ref): 545returnread_pipe("git rev-parse%s"% ref).strip() 546 547defbranchExists(ref): 548 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 549 ignore_error=True) 550returnlen(rev) >0 551 552defextractLogMessageFromGitCommit(commit): 553 logMessage ="" 554 555## fixme: title is first line of commit, not 1st paragraph. 556 foundTitle =False 557for log inread_pipe_lines("git cat-file commit%s"% commit): 558if not foundTitle: 559iflen(log) ==1: 560 foundTitle =True 561continue 562 563 logMessage += log 564return logMessage 565 566defextractSettingsGitLog(log): 567 values = {} 568for line in log.split("\n"): 569 line = line.strip() 570 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 571if not m: 572continue 573 574 assignments = m.group(1).split(':') 575for a in assignments: 576 vals = a.split('=') 577 key = vals[0].strip() 578 val = ('='.join(vals[1:])).strip() 579if val.endswith('\"')and val.startswith('"'): 580 val = val[1:-1] 581 582 values[key] = val 583 584 paths = values.get("depot-paths") 585if not paths: 586 paths = values.get("depot-path") 587if paths: 588 values['depot-paths'] = paths.split(',') 589return values 590 591defgitBranchExists(branch): 592 proc = subprocess.Popen(["git","rev-parse", branch], 593 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 594return proc.wait() ==0; 595 596_gitConfig = {} 597 598defgitConfig(key): 599if not _gitConfig.has_key(key): 600 cmd = ["git","config", key ] 601 s =read_pipe(cmd, ignore_error=True) 602 _gitConfig[key] = s.strip() 603return _gitConfig[key] 604 605defgitConfigBool(key): 606"""Return a bool, using git config --bool. It is True only if the 607 variable is set to true, and False if set to false or not present 608 in the config.""" 609 610if not _gitConfig.has_key(key): 611 cmd = ["git","config","--bool", key ] 612 s =read_pipe(cmd, ignore_error=True) 613 v = s.strip() 614 _gitConfig[key] = v =="true" 615return _gitConfig[key] 616 617defgitConfigList(key): 618if not _gitConfig.has_key(key): 619 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 620 _gitConfig[key] = s.strip().split(os.linesep) 621return _gitConfig[key] 622 623defp4BranchesInGit(branchesAreInRemotes=True): 624"""Find all the branches whose names start with "p4/", looking 625 in remotes or heads as specified by the argument. Return 626 a dictionary of{ branch: revision }for each one found. 627 The branch names are the short names, without any 628 "p4/" prefix.""" 629 630 branches = {} 631 632 cmdline ="git rev-parse --symbolic " 633if branchesAreInRemotes: 634 cmdline +="--remotes" 635else: 636 cmdline +="--branches" 637 638for line inread_pipe_lines(cmdline): 639 line = line.strip() 640 641# only import to p4/ 642if not line.startswith('p4/'): 643continue 644# special symbolic ref to p4/master 645if line =="p4/HEAD": 646continue 647 648# strip off p4/ prefix 649 branch = line[len("p4/"):] 650 651 branches[branch] =parseRevision(line) 652 653return branches 654 655defbranch_exists(branch): 656"""Make sure that the given ref name really exists.""" 657 658 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 659 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 660 out, _ = p.communicate() 661if p.returncode: 662return False 663# expect exactly one line of output: the branch name 664return out.rstrip() == branch 665 666deffindUpstreamBranchPoint(head ="HEAD"): 667 branches =p4BranchesInGit() 668# map from depot-path to branch name 669 branchByDepotPath = {} 670for branch in branches.keys(): 671 tip = branches[branch] 672 log =extractLogMessageFromGitCommit(tip) 673 settings =extractSettingsGitLog(log) 674if settings.has_key("depot-paths"): 675 paths =",".join(settings["depot-paths"]) 676 branchByDepotPath[paths] ="remotes/p4/"+ branch 677 678 settings =None 679 parent =0 680while parent <65535: 681 commit = head +"~%s"% parent 682 log =extractLogMessageFromGitCommit(commit) 683 settings =extractSettingsGitLog(log) 684if settings.has_key("depot-paths"): 685 paths =",".join(settings["depot-paths"]) 686if branchByDepotPath.has_key(paths): 687return[branchByDepotPath[paths], settings] 688 689 parent = parent +1 690 691return["", settings] 692 693defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 694if not silent: 695print("Creating/updating branch(es) in%sbased on origin branch(es)" 696% localRefPrefix) 697 698 originPrefix ="origin/p4/" 699 700for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 701 line = line.strip() 702if(not line.startswith(originPrefix))or line.endswith("HEAD"): 703continue 704 705 headName = line[len(originPrefix):] 706 remoteHead = localRefPrefix + headName 707 originHead = line 708 709 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 710if(not original.has_key('depot-paths') 711or not original.has_key('change')): 712continue 713 714 update =False 715if notgitBranchExists(remoteHead): 716if verbose: 717print"creating%s"% remoteHead 718 update =True 719else: 720 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 721if settings.has_key('change') >0: 722if settings['depot-paths'] == original['depot-paths']: 723 originP4Change =int(original['change']) 724 p4Change =int(settings['change']) 725if originP4Change > p4Change: 726print("%s(%s) is newer than%s(%s). " 727"Updating p4 branch from origin." 728% (originHead, originP4Change, 729 remoteHead, p4Change)) 730 update =True 731else: 732print("Ignoring:%swas imported from%swhile " 733"%swas imported from%s" 734% (originHead,','.join(original['depot-paths']), 735 remoteHead,','.join(settings['depot-paths']))) 736 737if update: 738system("git update-ref%s %s"% (remoteHead, originHead)) 739 740deforiginP4BranchesExist(): 741returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 742 743defp4ChangesForPaths(depotPaths, changeRange): 744assert depotPaths 745 cmd = ['changes'] 746for p in depotPaths: 747 cmd += ["%s...%s"% (p, changeRange)] 748 output =p4_read_pipe_lines(cmd) 749 750 changes = {} 751for line in output: 752 changeNum =int(line.split(" ")[1]) 753 changes[changeNum] =True 754 755 changelist = changes.keys() 756 changelist.sort() 757return changelist 758 759defp4PathStartsWith(path, prefix): 760# This method tries to remedy a potential mixed-case issue: 761# 762# If UserA adds //depot/DirA/file1 763# and UserB adds //depot/dira/file2 764# 765# we may or may not have a problem. If you have core.ignorecase=true, 766# we treat DirA and dira as the same directory 767ifgitConfigBool("core.ignorecase"): 768return path.lower().startswith(prefix.lower()) 769return path.startswith(prefix) 770 771defgetClientSpec(): 772"""Look at the p4 client spec, create a View() object that contains 773 all the mappings, and return it.""" 774 775 specList =p4CmdList("client -o") 776iflen(specList) !=1: 777die('Output from "client -o" is%dlines, expecting 1'% 778len(specList)) 779 780# dictionary of all client parameters 781 entry = specList[0] 782 783# 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 False12401241defapplyCommit(self,id):1242"""Apply one commit, return True if it succeeded."""12431244print"Applying",read_pipe(["git","show","-s",1245"--format=format:%h%s",id])12461247(p4User, gitEmail) = self.p4UserForCommit(id)12481249 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1250 filesToAdd =set()1251 filesToDelete =set()1252 editedFiles =set()1253 pureRenameCopy =set()1254 filesToChangeExecBit = {}12551256for line in diff:1257 diff =parseDiffTreeEntry(line)1258 modifier = diff['status']1259 path = diff['src']1260if modifier =="M":1261p4_edit(path)1262ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1263 filesToChangeExecBit[path] = diff['dst_mode']1264 editedFiles.add(path)1265elif modifier =="A":1266 filesToAdd.add(path)1267 filesToChangeExecBit[path] = diff['dst_mode']1268if path in filesToDelete:1269 filesToDelete.remove(path)1270elif modifier =="D":1271 filesToDelete.add(path)1272if path in filesToAdd:1273 filesToAdd.remove(path)1274elif modifier =="C":1275 src, dest = diff['src'], diff['dst']1276p4_integrate(src, dest)1277 pureRenameCopy.add(dest)1278if diff['src_sha1'] != diff['dst_sha1']:1279p4_edit(dest)1280 pureRenameCopy.discard(dest)1281ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1282p4_edit(dest)1283 pureRenameCopy.discard(dest)1284 filesToChangeExecBit[dest] = diff['dst_mode']1285if self.isWindows:1286# turn off read-only attribute1287 os.chmod(dest, stat.S_IWRITE)1288 os.unlink(dest)1289 editedFiles.add(dest)1290elif modifier =="R":1291 src, dest = diff['src'], diff['dst']1292if self.p4HasMoveCommand:1293p4_edit(src)# src must be open before move1294p4_move(src, dest)# opens for (move/delete, move/add)1295else:1296p4_integrate(src, dest)1297if diff['src_sha1'] != diff['dst_sha1']:1298p4_edit(dest)1299else:1300 pureRenameCopy.add(dest)1301ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1302if not self.p4HasMoveCommand:1303p4_edit(dest)# with move: already open, writable1304 filesToChangeExecBit[dest] = diff['dst_mode']1305if not self.p4HasMoveCommand:1306if self.isWindows:1307 os.chmod(dest, stat.S_IWRITE)1308 os.unlink(dest)1309 filesToDelete.add(src)1310 editedFiles.add(dest)1311else:1312die("unknown modifier%sfor%s"% (modifier, path))13131314 diffcmd ="git diff-tree -p\"%s\""% (id)1315 patchcmd = diffcmd +" | git apply "1316 tryPatchCmd = patchcmd +"--check -"1317 applyPatchCmd = patchcmd +"--check --apply -"1318 patch_succeeded =True13191320if os.system(tryPatchCmd) !=0:1321 fixed_rcs_keywords =False1322 patch_succeeded =False1323print"Unfortunately applying the change failed!"13241325# Patch failed, maybe it's just RCS keyword woes. Look through1326# the patch to see if that's possible.1327ifgitConfigBool("git-p4.attemptRCSCleanup"):1328file=None1329 pattern =None1330 kwfiles = {}1331forfilein editedFiles | filesToDelete:1332# did this file's delta contain RCS keywords?1333 pattern =p4_keywords_regexp_for_file(file)13341335if pattern:1336# this file is a possibility...look for RCS keywords.1337 regexp = re.compile(pattern, re.VERBOSE)1338for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1339if regexp.search(line):1340if verbose:1341print"got keyword match on%sin%sin%s"% (pattern, line,file)1342 kwfiles[file] = pattern1343break13441345forfilein kwfiles:1346if verbose:1347print"zapping%swith%s"% (line,pattern)1348# File is being deleted, so not open in p4. Must1349# disable the read-only bit on windows.1350if self.isWindows andfilenot in editedFiles:1351 os.chmod(file, stat.S_IWRITE)1352 self.patchRCSKeywords(file, kwfiles[file])1353 fixed_rcs_keywords =True13541355if fixed_rcs_keywords:1356print"Retrying the patch with RCS keywords cleaned up"1357if os.system(tryPatchCmd) ==0:1358 patch_succeeded =True13591360if not patch_succeeded:1361for f in editedFiles:1362p4_revert(f)1363return False13641365#1366# Apply the patch for real, and do add/delete/+x handling.1367#1368system(applyPatchCmd)13691370for f in filesToAdd:1371p4_add(f)1372for f in filesToDelete:1373p4_revert(f)1374p4_delete(f)13751376# Set/clear executable bits1377for f in filesToChangeExecBit.keys():1378 mode = filesToChangeExecBit[f]1379setP4ExecBit(f, mode)13801381#1382# Build p4 change description, starting with the contents1383# of the git commit message.1384#1385 logMessage =extractLogMessageFromGitCommit(id)1386 logMessage = logMessage.strip()1387(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13881389 template = self.prepareSubmitTemplate()1390 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13911392if self.preserveUser:1393 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13941395if self.checkAuthorship and not self.p4UserIsMe(p4User):1396 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1397 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1398 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13991400 separatorLine ="######## everything below this line is just the diff #######\n"14011402# diff1403if os.environ.has_key("P4DIFF"):1404del(os.environ["P4DIFF"])1405 diff =""1406for editedFile in editedFiles:1407 diff +=p4_read_pipe(['diff','-du',1408wildcard_encode(editedFile)])14091410# new file diff1411 newdiff =""1412for newFile in filesToAdd:1413 newdiff +="==== new file ====\n"1414 newdiff +="--- /dev/null\n"1415 newdiff +="+++%s\n"% newFile1416 f =open(newFile,"r")1417for line in f.readlines():1418 newdiff +="+"+ line1419 f.close()14201421# change description file: submitTemplate, separatorLine, diff, newdiff1422(handle, fileName) = tempfile.mkstemp()1423 tmpFile = os.fdopen(handle,"w+")1424if self.isWindows:1425 submitTemplate = submitTemplate.replace("\n","\r\n")1426 separatorLine = separatorLine.replace("\n","\r\n")1427 newdiff = newdiff.replace("\n","\r\n")1428 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1429 tmpFile.close()14301431if self.prepare_p4_only:1432#1433# Leave the p4 tree prepared, and the submit template around1434# and let the user decide what to do next1435#1436print1437print"P4 workspace prepared for submission."1438print"To submit or revert, go to client workspace"1439print" "+ self.clientPath1440print1441print"To submit, use\"p4 submit\"to write a new description,"1442print"or\"p4 submit -i%s\"to use the one prepared by" \1443"\"git p4\"."% fileName1444print"You can delete the file\"%s\"when finished."% fileName14451446if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1447print"To preserve change ownership by user%s, you must\n" \1448"do\"p4 change -f <change>\"after submitting and\n" \1449"edit the User field."1450if pureRenameCopy:1451print"After submitting, renamed files must be re-synced."1452print"Invoke\"p4 sync -f\"on each of these files:"1453for f in pureRenameCopy:1454print" "+ f14551456print1457print"To revert the changes, use\"p4 revert ...\", and delete"1458print"the submit template file\"%s\""% fileName1459if filesToAdd:1460print"Since the commit adds new files, they must be deleted:"1461for f in filesToAdd:1462print" "+ f1463print1464return True14651466#1467# Let the user edit the change description, then submit it.1468#1469if self.edit_template(fileName):1470# read the edited message and submit1471 ret =True1472 tmpFile =open(fileName,"rb")1473 message = tmpFile.read()1474 tmpFile.close()1475 submitTemplate = message[:message.index(separatorLine)]1476if self.isWindows:1477 submitTemplate = submitTemplate.replace("\r\n","\n")1478p4_write_pipe(['submit','-i'], submitTemplate)14791480if self.preserveUser:1481if p4User:1482# Get last changelist number. Cannot easily get it from1483# the submit command output as the output is1484# unmarshalled.1485 changelist = self.lastP4Changelist()1486 self.modifyChangelistUser(changelist, p4User)14871488# The rename/copy happened by applying a patch that created a1489# new file. This leaves it writable, which confuses p4.1490for f in pureRenameCopy:1491p4_sync(f,"-f")14921493else:1494# skip this patch1495 ret =False1496print"Submission cancelled, undoing p4 changes."1497for f in editedFiles:1498p4_revert(f)1499for f in filesToAdd:1500p4_revert(f)1501 os.remove(f)1502for f in filesToDelete:1503p4_revert(f)15041505 os.remove(fileName)1506return ret15071508# Export git tags as p4 labels. Create a p4 label and then tag1509# with that.1510defexportGitTags(self, gitTags):1511 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1512iflen(validLabelRegexp) ==0:1513 validLabelRegexp = defaultLabelRegexp1514 m = re.compile(validLabelRegexp)15151516for name in gitTags:15171518if not m.match(name):1519if verbose:1520print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1521continue15221523# Get the p4 commit this corresponds to1524 logMessage =extractLogMessageFromGitCommit(name)1525 values =extractSettingsGitLog(logMessage)15261527if not values.has_key('change'):1528# a tag pointing to something not sent to p4; ignore1529if verbose:1530print"git tag%sdoes not give a p4 commit"% name1531continue1532else:1533 changelist = values['change']15341535# Get the tag details.1536 inHeader =True1537 isAnnotated =False1538 body = []1539for l inread_pipe_lines(["git","cat-file","-p", name]):1540 l = l.strip()1541if inHeader:1542if re.match(r'tag\s+', l):1543 isAnnotated =True1544elif re.match(r'\s*$', l):1545 inHeader =False1546continue1547else:1548 body.append(l)15491550if not isAnnotated:1551 body = ["lightweight tag imported by git p4\n"]15521553# Create the label - use the same view as the client spec we are using1554 clientSpec =getClientSpec()15551556 labelTemplate ="Label:%s\n"% name1557 labelTemplate +="Description:\n"1558for b in body:1559 labelTemplate +="\t"+ b +"\n"1560 labelTemplate +="View:\n"1561for depot_side in clientSpec.mappings:1562 labelTemplate +="\t%s\n"% depot_side15631564if self.dry_run:1565print"Would create p4 label%sfor tag"% name1566elif self.prepare_p4_only:1567print"Not creating p4 label%sfor tag due to option" \1568" --prepare-p4-only"% name1569else:1570p4_write_pipe(["label","-i"], labelTemplate)15711572# Use the label1573p4_system(["tag","-l", name] +1574["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])15751576if verbose:1577print"created p4 label for tag%s"% name15781579defrun(self, args):1580iflen(args) ==0:1581 self.master =currentGitBranch()1582iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1583die("Detecting current git branch failed!")1584eliflen(args) ==1:1585 self.master = args[0]1586if notbranchExists(self.master):1587die("Branch%sdoes not exist"% self.master)1588else:1589return False15901591 allowSubmit =gitConfig("git-p4.allowSubmit")1592iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1593die("%sis not in git-p4.allowSubmit"% self.master)15941595[upstream, settings] =findUpstreamBranchPoint()1596 self.depotPath = settings['depot-paths'][0]1597iflen(self.origin) ==0:1598 self.origin = upstream15991600if self.preserveUser:1601if not self.canChangeChangelists():1602die("Cannot preserve user names without p4 super-user or admin permissions")16031604# if not set from the command line, try the config file1605if self.conflict_behavior is None:1606 val =gitConfig("git-p4.conflict")1607if val:1608if val not in self.conflict_behavior_choices:1609die("Invalid value '%s' for config git-p4.conflict"% val)1610else:1611 val ="ask"1612 self.conflict_behavior = val16131614if self.verbose:1615print"Origin branch is "+ self.origin16161617iflen(self.depotPath) ==0:1618print"Internal error: cannot locate perforce depot path from existing branches"1619 sys.exit(128)16201621 self.useClientSpec =False1622ifgitConfigBool("git-p4.useclientspec"):1623 self.useClientSpec =True1624if self.useClientSpec:1625 self.clientSpecDirs =getClientSpec()16261627if self.useClientSpec:1628# all files are relative to the client spec1629 self.clientPath =getClientRoot()1630else:1631 self.clientPath =p4Where(self.depotPath)16321633if self.clientPath =="":1634die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)16351636print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1637 self.oldWorkingDirectory = os.getcwd()16381639# ensure the clientPath exists1640 new_client_dir =False1641if not os.path.exists(self.clientPath):1642 new_client_dir =True1643 os.makedirs(self.clientPath)16441645chdir(self.clientPath, is_client_path=True)1646if self.dry_run:1647print"Would synchronize p4 checkout in%s"% self.clientPath1648else:1649print"Synchronizing p4 checkout..."1650if new_client_dir:1651# old one was destroyed, and maybe nobody told p41652p4_sync("...","-f")1653else:1654p4_sync("...")1655 self.check()16561657 commits = []1658for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1659 commits.append(line.strip())1660 commits.reverse()16611662if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1663 self.checkAuthorship =False1664else:1665 self.checkAuthorship =True16661667if self.preserveUser:1668 self.checkValidP4Users(commits)16691670#1671# Build up a set of options to be passed to diff when1672# submitting each commit to p4.1673#1674if self.detectRenames:1675# command-line -M arg1676 self.diffOpts ="-M"1677else:1678# If not explicitly set check the config variable1679 detectRenames =gitConfig("git-p4.detectRenames")16801681if detectRenames.lower() =="false"or detectRenames =="":1682 self.diffOpts =""1683elif detectRenames.lower() =="true":1684 self.diffOpts ="-M"1685else:1686 self.diffOpts ="-M%s"% detectRenames16871688# no command-line arg for -C or --find-copies-harder, just1689# config variables1690 detectCopies =gitConfig("git-p4.detectCopies")1691if detectCopies.lower() =="false"or detectCopies =="":1692pass1693elif detectCopies.lower() =="true":1694 self.diffOpts +=" -C"1695else:1696 self.diffOpts +=" -C%s"% detectCopies16971698ifgitConfigBool("git-p4.detectCopiesHarder"):1699 self.diffOpts +=" --find-copies-harder"17001701#1702# Apply the commits, one at a time. On failure, ask if should1703# continue to try the rest of the patches, or quit.1704#1705if self.dry_run:1706print"Would apply"1707 applied = []1708 last =len(commits) -11709for i, commit inenumerate(commits):1710if self.dry_run:1711print" ",read_pipe(["git","show","-s",1712"--format=format:%h%s", commit])1713 ok =True1714else:1715 ok = self.applyCommit(commit)1716if ok:1717 applied.append(commit)1718else:1719if self.prepare_p4_only and i < last:1720print"Processing only the first commit due to option" \1721" --prepare-p4-only"1722break1723if i < last:1724 quit =False1725while True:1726# prompt for what to do, or use the option/variable1727if self.conflict_behavior =="ask":1728print"What do you want to do?"1729 response =raw_input("[s]kip this commit but apply"1730" the rest, or [q]uit? ")1731if not response:1732continue1733elif self.conflict_behavior =="skip":1734 response ="s"1735elif self.conflict_behavior =="quit":1736 response ="q"1737else:1738die("Unknown conflict_behavior '%s'"%1739 self.conflict_behavior)17401741if response[0] =="s":1742print"Skipping this commit, but applying the rest"1743break1744if response[0] =="q":1745print"Quitting"1746 quit =True1747break1748if quit:1749break17501751chdir(self.oldWorkingDirectory)17521753if self.dry_run:1754pass1755elif self.prepare_p4_only:1756pass1757eliflen(commits) ==len(applied):1758print"All commits applied!"17591760 sync =P4Sync()1761if self.branch:1762 sync.branch = self.branch1763 sync.run([])17641765 rebase =P4Rebase()1766 rebase.rebase()17671768else:1769iflen(applied) ==0:1770print"No commits applied."1771else:1772print"Applied only the commits marked with '*':"1773for c in commits:1774if c in applied:1775 star ="*"1776else:1777 star =" "1778print star,read_pipe(["git","show","-s",1779"--format=format:%h%s", c])1780print"You will have to do 'git p4 sync' and rebase."17811782ifgitConfigBool("git-p4.exportLabels"):1783 self.exportLabels =True17841785if self.exportLabels:1786 p4Labels =getP4Labels(self.depotPath)1787 gitTags =getGitTags()17881789 missingGitTags = gitTags - p4Labels1790 self.exportGitTags(missingGitTags)17911792# exit with error unless everything applied perfectly1793iflen(commits) !=len(applied):1794 sys.exit(1)17951796return True17971798classView(object):1799"""Represent a p4 view ("p4 help views"), and map files in a1800 repo according to the view."""18011802def__init__(self, client_name):1803 self.mappings = []1804 self.client_prefix ="//%s/"% client_name1805# cache results of "p4 where" to lookup client file locations1806 self.client_spec_path_cache = {}18071808defappend(self, view_line):1809"""Parse a view line, splitting it into depot and client1810 sides. Append to self.mappings, preserving order. This1811 is only needed for tag creation."""18121813# Split the view line into exactly two words. P4 enforces1814# structure on these lines that simplifies this quite a bit.1815#1816# Either or both words may be double-quoted.1817# Single quotes do not matter.1818# Double-quote marks cannot occur inside the words.1819# A + or - prefix is also inside the quotes.1820# There are no quotes unless they contain a space.1821# The line is already white-space stripped.1822# The two words are separated by a single space.1823#1824if view_line[0] =='"':1825# First word is double quoted. Find its end.1826 close_quote_index = view_line.find('"',1)1827if close_quote_index <=0:1828die("No first-word closing quote found:%s"% view_line)1829 depot_side = view_line[1:close_quote_index]1830# skip closing quote and space1831 rhs_index = close_quote_index +1+11832else:1833 space_index = view_line.find(" ")1834if space_index <=0:1835die("No word-splitting space found:%s"% view_line)1836 depot_side = view_line[0:space_index]1837 rhs_index = space_index +118381839# prefix + means overlay on previous mapping1840if depot_side.startswith("+"):1841 depot_side = depot_side[1:]18421843# prefix - means exclude this path, leave out of mappings1844 exclude =False1845if depot_side.startswith("-"):1846 exclude =True1847 depot_side = depot_side[1:]18481849if not exclude:1850 self.mappings.append(depot_side)18511852defconvert_client_path(self, clientFile):1853# chop off //client/ part to make it relative1854if not clientFile.startswith(self.client_prefix):1855die("No prefix '%s' on clientFile '%s'"%1856(self.client_prefix, clientFile))1857return clientFile[len(self.client_prefix):]18581859defupdate_client_spec_path_cache(self, files):1860""" Caching file paths by "p4 where" batch query """18611862# List depot file paths exclude that already cached1863 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]18641865iflen(fileArgs) ==0:1866return# All files in cache18671868 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1869for res in where_result:1870if"code"in res and res["code"] =="error":1871# assume error is "... file(s) not in client view"1872continue1873if"clientFile"not in res:1874die("No clientFile from 'p4 where%s'"% depot_path)1875if"unmap"in res:1876# it will list all of them, but only one not unmap-ped1877continue1878 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])18791880# not found files or unmap files set to ""1881for depotFile in fileArgs:1882if depotFile not in self.client_spec_path_cache:1883 self.client_spec_path_cache[depotFile] =""18841885defmap_in_client(self, depot_path):1886"""Return the relative location in the client where this1887 depot file should live. Returns "" if the file should1888 not be mapped in the client."""18891890if depot_path in self.client_spec_path_cache:1891return self.client_spec_path_cache[depot_path]18921893die("Error:%sis not found in client spec path"% depot_path )1894return""18951896classP4Sync(Command, P4UserMap):1897 delete_actions = ("delete","move/delete","purge")18981899def__init__(self):1900 Command.__init__(self)1901 P4UserMap.__init__(self)1902 self.options = [1903 optparse.make_option("--branch", dest="branch"),1904 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1905 optparse.make_option("--changesfile", dest="changesFile"),1906 optparse.make_option("--silent", dest="silent", action="store_true"),1907 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1908 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1909 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1910help="Import into refs/heads/ , not refs/remotes"),1911 optparse.make_option("--max-changes", dest="maxChanges"),1912 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1913help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1914 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1915help="Only sync files that are included in the Perforce Client Spec")1916]1917 self.description ="""Imports from Perforce into a git repository.\n1918 example:1919 //depot/my/project/ -- to import the current head1920 //depot/my/project/@all -- to import everything1921 //depot/my/project/@1,6 -- to import only from revision 1 to 619221923 (a ... is not needed in the path p4 specification, it's added implicitly)"""19241925 self.usage +=" //depot/path[@revRange]"1926 self.silent =False1927 self.createdBranches =set()1928 self.committedChanges =set()1929 self.branch =""1930 self.detectBranches =False1931 self.detectLabels =False1932 self.importLabels =False1933 self.changesFile =""1934 self.syncWithOrigin =True1935 self.importIntoRemotes =True1936 self.maxChanges =""1937 self.keepRepoPath =False1938 self.depotPaths =None1939 self.p4BranchesInGit = []1940 self.cloneExclude = []1941 self.useClientSpec =False1942 self.useClientSpec_from_options =False1943 self.clientSpecDirs =None1944 self.tempBranches = []1945 self.tempBranchLocation ="git-p4-tmp"19461947ifgitConfig("git-p4.syncFromOrigin") =="false":1948 self.syncWithOrigin =False19491950# Force a checkpoint in fast-import and wait for it to finish1951defcheckpoint(self):1952 self.gitStream.write("checkpoint\n\n")1953 self.gitStream.write("progress checkpoint\n\n")1954 out = self.gitOutput.readline()1955if self.verbose:1956print"checkpoint finished: "+ out19571958defextractFilesFromCommit(self, commit):1959 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1960for path in self.cloneExclude]1961 files = []1962 fnum =01963while commit.has_key("depotFile%s"% fnum):1964 path = commit["depotFile%s"% fnum]19651966if[p for p in self.cloneExclude1967ifp4PathStartsWith(path, p)]:1968 found =False1969else:1970 found = [p for p in self.depotPaths1971ifp4PathStartsWith(path, p)]1972if not found:1973 fnum = fnum +11974continue19751976file= {}1977file["path"] = path1978file["rev"] = commit["rev%s"% fnum]1979file["action"] = commit["action%s"% fnum]1980file["type"] = commit["type%s"% fnum]1981 files.append(file)1982 fnum = fnum +11983return files19841985defstripRepoPath(self, path, prefixes):1986"""When streaming files, this is called to map a p4 depot path1987 to where it should go in git. The prefixes are either1988 self.depotPaths, or self.branchPrefixes in the case of1989 branch detection."""19901991if self.useClientSpec:1992# branch detection moves files up a level (the branch name)1993# from what client spec interpretation gives1994 path = self.clientSpecDirs.map_in_client(path)1995if self.detectBranches:1996for b in self.knownBranches:1997if path.startswith(b +"/"):1998 path = path[len(b)+1:]19992000elif self.keepRepoPath:2001# Preserve everything in relative path name except leading2002# //depot/; just look at first prefix as they all should2003# be in the same depot.2004 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2005ifp4PathStartsWith(path, depot):2006 path = path[len(depot):]20072008else:2009for p in prefixes:2010ifp4PathStartsWith(path, p):2011 path = path[len(p):]2012break20132014 path =wildcard_decode(path)2015return path20162017defsplitFilesIntoBranches(self, commit):2018"""Look at each depotFile in the commit to figure out to what2019 branch it belongs."""20202021if self.clientSpecDirs:2022 files = self.extractFilesFromCommit(commit)2023 self.clientSpecDirs.update_client_spec_path_cache(files)20242025 branches = {}2026 fnum =02027while commit.has_key("depotFile%s"% fnum):2028 path = commit["depotFile%s"% fnum]2029 found = [p for p in self.depotPaths2030ifp4PathStartsWith(path, p)]2031if not found:2032 fnum = fnum +12033continue20342035file= {}2036file["path"] = path2037file["rev"] = commit["rev%s"% fnum]2038file["action"] = commit["action%s"% fnum]2039file["type"] = commit["type%s"% fnum]2040 fnum = fnum +120412042# start with the full relative path where this file would2043# go in a p4 client2044if self.useClientSpec:2045 relPath = self.clientSpecDirs.map_in_client(path)2046else:2047 relPath = self.stripRepoPath(path, self.depotPaths)20482049for branch in self.knownBranches.keys():2050# add a trailing slash so that a commit into qt/4.2foo2051# doesn't end up in qt/4.2, e.g.2052if relPath.startswith(branch +"/"):2053if branch not in branches:2054 branches[branch] = []2055 branches[branch].append(file)2056break20572058return branches20592060# output one file from the P4 stream2061# - helper for streamP4Files20622063defstreamOneP4File(self,file, contents):2064 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2065if verbose:2066 sys.stderr.write("%s\n"% relPath)20672068(type_base, type_mods) =split_p4_type(file["type"])20692070 git_mode ="100644"2071if"x"in type_mods:2072 git_mode ="100755"2073if type_base =="symlink":2074 git_mode ="120000"2075# p4 print on a symlink sometimes contains "target\n";2076# if it does, remove the newline2077 data =''.join(contents)2078if data[-1] =='\n':2079 contents = [data[:-1]]2080else:2081 contents = [data]20822083if type_base =="utf16":2084# p4 delivers different text in the python output to -G2085# than it does when using "print -o", or normal p4 client2086# operations. utf16 is converted to ascii or utf8, perhaps.2087# But ascii text saved as -t utf16 is completely mangled.2088# Invoke print -o to get the real contents.2089#2090# On windows, the newlines will always be mangled by print, so put2091# them back too. This is not needed to the cygwin windows version,2092# just the native "NT" type.2093#2094 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2095ifp4_version_string().find("/NT") >=0:2096 text = text.replace("\r\n","\n")2097 contents = [ text ]20982099if type_base =="apple":2100# Apple filetype files will be streamed as a concatenation of2101# its appledouble header and the contents. This is useless2102# on both macs and non-macs. If using "print -q -o xx", it2103# will create "xx" with the data, and "%xx" with the header.2104# This is also not very useful.2105#2106# Ideally, someday, this script can learn how to generate2107# appledouble files directly and import those to git, but2108# non-mac machines can never find a use for apple filetype.2109print"\nIgnoring apple filetype file%s"%file['depotFile']2110return21112112# Note that we do not try to de-mangle keywords on utf16 files,2113# even though in theory somebody may want that.2114 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2115if pattern:2116 regexp = re.compile(pattern, re.VERBOSE)2117 text =''.join(contents)2118 text = regexp.sub(r'$\1$', text)2119 contents = [ text ]21202121 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21222123# total length...2124 length =02125for d in contents:2126 length = length +len(d)21272128 self.gitStream.write("data%d\n"% length)2129for d in contents:2130 self.gitStream.write(d)2131 self.gitStream.write("\n")21322133defstreamOneP4Deletion(self,file):2134 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2135if verbose:2136 sys.stderr.write("delete%s\n"% relPath)2137 self.gitStream.write("D%s\n"% relPath)21382139# handle another chunk of streaming data2140defstreamP4FilesCb(self, marshalled):21412142# catch p4 errors and complain2143 err =None2144if"code"in marshalled:2145if marshalled["code"] =="error":2146if"data"in marshalled:2147 err = marshalled["data"].rstrip()2148if err:2149 f =None2150if self.stream_have_file_info:2151if"depotFile"in self.stream_file:2152 f = self.stream_file["depotFile"]2153# force a failure in fast-import, else an empty2154# commit will be made2155 self.gitStream.write("\n")2156 self.gitStream.write("die-now\n")2157 self.gitStream.close()2158# ignore errors, but make sure it exits first2159 self.importProcess.wait()2160if f:2161die("Error from p4 print for%s:%s"% (f, err))2162else:2163die("Error from p4 print:%s"% err)21642165if marshalled.has_key('depotFile')and self.stream_have_file_info:2166# start of a new file - output the old one first2167 self.streamOneP4File(self.stream_file, self.stream_contents)2168 self.stream_file = {}2169 self.stream_contents = []2170 self.stream_have_file_info =False21712172# pick up the new file information... for the2173# 'data' field we need to append to our array2174for k in marshalled.keys():2175if k =='data':2176 self.stream_contents.append(marshalled['data'])2177else:2178 self.stream_file[k] = marshalled[k]21792180 self.stream_have_file_info =True21812182# Stream directly from "p4 files" into "git fast-import"2183defstreamP4Files(self, files):2184 filesForCommit = []2185 filesToRead = []2186 filesToDelete = []21872188for f in files:2189# if using a client spec, only add the files that have2190# a path in the client2191if self.clientSpecDirs:2192if self.clientSpecDirs.map_in_client(f['path']) =="":2193continue21942195 filesForCommit.append(f)2196if f['action']in self.delete_actions:2197 filesToDelete.append(f)2198else:2199 filesToRead.append(f)22002201# deleted files...2202for f in filesToDelete:2203 self.streamOneP4Deletion(f)22042205iflen(filesToRead) >0:2206 self.stream_file = {}2207 self.stream_contents = []2208 self.stream_have_file_info =False22092210# curry self argument2211defstreamP4FilesCbSelf(entry):2212 self.streamP4FilesCb(entry)22132214 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22152216p4CmdList(["-x","-","print"],2217 stdin=fileArgs,2218 cb=streamP4FilesCbSelf)22192220# do the last chunk2221if self.stream_file.has_key('depotFile'):2222 self.streamOneP4File(self.stream_file, self.stream_contents)22232224defmake_email(self, userid):2225if userid in self.users:2226return self.users[userid]2227else:2228return"%s<a@b>"% userid22292230# Stream a p4 tag2231defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2232if verbose:2233print"writing tag%sfor commit%s"% (labelName, commit)2234 gitStream.write("tag%s\n"% labelName)2235 gitStream.write("from%s\n"% commit)22362237if labelDetails.has_key('Owner'):2238 owner = labelDetails["Owner"]2239else:2240 owner =None22412242# Try to use the owner of the p4 label, or failing that,2243# the current p4 user id.2244if owner:2245 email = self.make_email(owner)2246else:2247 email = self.make_email(self.p4UserId())2248 tagger ="%s %s %s"% (email, epoch, self.tz)22492250 gitStream.write("tagger%s\n"% tagger)22512252print"labelDetails=",labelDetails2253if labelDetails.has_key('Description'):2254 description = labelDetails['Description']2255else:2256 description ='Label from git p4'22572258 gitStream.write("data%d\n"%len(description))2259 gitStream.write(description)2260 gitStream.write("\n")22612262defcommit(self, details, files, branch, parent =""):2263 epoch = details["time"]2264 author = details["user"]22652266if self.verbose:2267print"commit into%s"% branch22682269# start with reading files; if that fails, we should not2270# create a commit.2271 new_files = []2272for f in files:2273if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2274 new_files.append(f)2275else:2276 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])22772278if self.clientSpecDirs:2279 self.clientSpecDirs.update_client_spec_path_cache(files)22802281 self.gitStream.write("commit%s\n"% branch)2282# gitStream.write("mark :%s\n" % details["change"])2283 self.committedChanges.add(int(details["change"]))2284 committer =""2285if author not in self.users:2286 self.getUserMapFromPerforceServer()2287 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)22882289 self.gitStream.write("committer%s\n"% committer)22902291 self.gitStream.write("data <<EOT\n")2292 self.gitStream.write(details["desc"])2293 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2294(','.join(self.branchPrefixes), details["change"]))2295iflen(details['options']) >0:2296 self.gitStream.write(": options =%s"% details['options'])2297 self.gitStream.write("]\nEOT\n\n")22982299iflen(parent) >0:2300if self.verbose:2301print"parent%s"% parent2302 self.gitStream.write("from%s\n"% parent)23032304 self.streamP4Files(new_files)2305 self.gitStream.write("\n")23062307 change =int(details["change"])23082309if self.labels.has_key(change):2310 label = self.labels[change]2311 labelDetails = label[0]2312 labelRevisions = label[1]2313if self.verbose:2314print"Change%sis labelled%s"% (change, labelDetails)23152316 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2317for p in self.branchPrefixes])23182319iflen(files) ==len(labelRevisions):23202321 cleanedFiles = {}2322for info in files:2323if info["action"]in self.delete_actions:2324continue2325 cleanedFiles[info["depotFile"]] = info["rev"]23262327if cleanedFiles == labelRevisions:2328 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23292330else:2331if not self.silent:2332print("Tag%sdoes not match with change%s: files do not match."2333% (labelDetails["label"], change))23342335else:2336if not self.silent:2337print("Tag%sdoes not match with change%s: file count is different."2338% (labelDetails["label"], change))23392340# Build a dictionary of changelists and labels, for "detect-labels" option.2341defgetLabels(self):2342 self.labels = {}23432344 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2345iflen(l) >0and not self.silent:2346print"Finding files belonging to labels in%s"% `self.depotPaths`23472348for output in l:2349 label = output["label"]2350 revisions = {}2351 newestChange =02352if self.verbose:2353print"Querying files for label%s"% label2354forfileinp4CmdList(["files"] +2355["%s...@%s"% (p, label)2356for p in self.depotPaths]):2357 revisions[file["depotFile"]] =file["rev"]2358 change =int(file["change"])2359if change > newestChange:2360 newestChange = change23612362 self.labels[newestChange] = [output, revisions]23632364if self.verbose:2365print"Label changes:%s"% self.labels.keys()23662367# Import p4 labels as git tags. A direct mapping does not2368# exist, so assume that if all the files are at the same revision2369# then we can use that, or it's something more complicated we should2370# just ignore.2371defimportP4Labels(self, stream, p4Labels):2372if verbose:2373print"import p4 labels: "+' '.join(p4Labels)23742375 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2376 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2377iflen(validLabelRegexp) ==0:2378 validLabelRegexp = defaultLabelRegexp2379 m = re.compile(validLabelRegexp)23802381for name in p4Labels:2382 commitFound =False23832384if not m.match(name):2385if verbose:2386print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2387continue23882389if name in ignoredP4Labels:2390continue23912392 labelDetails =p4CmdList(['label',"-o", name])[0]23932394# get the most recent changelist for each file in this label2395 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2396for p in self.depotPaths])23972398if change.has_key('change'):2399# find the corresponding git commit; take the oldest commit2400 changelist =int(change['change'])2401 gitCommit =read_pipe(["git","rev-list","--max-count=1",2402"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2403iflen(gitCommit) ==0:2404print"could not find git commit for changelist%d"% changelist2405else:2406 gitCommit = gitCommit.strip()2407 commitFound =True2408# Convert from p4 time format2409try:2410 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2411exceptValueError:2412print"Could not convert label time%s"% labelDetails['Update']2413 tmwhen =124142415 when =int(time.mktime(tmwhen))2416 self.streamTag(stream, name, labelDetails, gitCommit, when)2417if verbose:2418print"p4 label%smapped to git commit%s"% (name, gitCommit)2419else:2420if verbose:2421print"Label%shas no changelists - possibly deleted?"% name24222423if not commitFound:2424# We can't import this label; don't try again as it will get very2425# expensive repeatedly fetching all the files for labels that will2426# never be imported. If the label is moved in the future, the2427# ignore will need to be removed manually.2428system(["git","config","--add","git-p4.ignoredP4Labels", name])24292430defguessProjectName(self):2431for p in self.depotPaths:2432if p.endswith("/"):2433 p = p[:-1]2434 p = p[p.strip().rfind("/") +1:]2435if not p.endswith("/"):2436 p +="/"2437return p24382439defgetBranchMapping(self):2440 lostAndFoundBranches =set()24412442 user =gitConfig("git-p4.branchUser")2443iflen(user) >0:2444 command ="branches -u%s"% user2445else:2446 command ="branches"24472448for info inp4CmdList(command):2449 details =p4Cmd(["branch","-o", info["branch"]])2450 viewIdx =02451while details.has_key("View%s"% viewIdx):2452 paths = details["View%s"% viewIdx].split(" ")2453 viewIdx = viewIdx +12454# require standard //depot/foo/... //depot/bar/... mapping2455iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2456continue2457 source = paths[0]2458 destination = paths[1]2459## HACK2460ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2461 source = source[len(self.depotPaths[0]):-4]2462 destination = destination[len(self.depotPaths[0]):-4]24632464if destination in self.knownBranches:2465if not self.silent:2466print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2467print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2468continue24692470 self.knownBranches[destination] = source24712472 lostAndFoundBranches.discard(destination)24732474if source not in self.knownBranches:2475 lostAndFoundBranches.add(source)24762477# Perforce does not strictly require branches to be defined, so we also2478# check git config for a branch list.2479#2480# Example of branch definition in git config file:2481# [git-p4]2482# branchList=main:branchA2483# branchList=main:branchB2484# branchList=branchA:branchC2485 configBranches =gitConfigList("git-p4.branchList")2486for branch in configBranches:2487if branch:2488(source, destination) = branch.split(":")2489 self.knownBranches[destination] = source24902491 lostAndFoundBranches.discard(destination)24922493if source not in self.knownBranches:2494 lostAndFoundBranches.add(source)249524962497for branch in lostAndFoundBranches:2498 self.knownBranches[branch] = branch24992500defgetBranchMappingFromGitBranches(self):2501 branches =p4BranchesInGit(self.importIntoRemotes)2502for branch in branches.keys():2503if branch =="master":2504 branch ="main"2505else:2506 branch = branch[len(self.projectName):]2507 self.knownBranches[branch] = branch25082509defupdateOptionDict(self, d):2510 option_keys = {}2511if self.keepRepoPath:2512 option_keys['keepRepoPath'] =125132514 d["options"] =' '.join(sorted(option_keys.keys()))25152516defreadOptions(self, d):2517 self.keepRepoPath = (d.has_key('options')2518and('keepRepoPath'in d['options']))25192520defgitRefForBranch(self, branch):2521if branch =="main":2522return self.refPrefix +"master"25232524iflen(branch) <=0:2525return branch25262527return self.refPrefix + self.projectName + branch25282529defgitCommitByP4Change(self, ref, change):2530if self.verbose:2531print"looking in ref "+ ref +" for change%susing bisect..."% change25322533 earliestCommit =""2534 latestCommit =parseRevision(ref)25352536while True:2537if self.verbose:2538print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2539 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2540iflen(next) ==0:2541if self.verbose:2542print"argh"2543return""2544 log =extractLogMessageFromGitCommit(next)2545 settings =extractSettingsGitLog(log)2546 currentChange =int(settings['change'])2547if self.verbose:2548print"current change%s"% currentChange25492550if currentChange == change:2551if self.verbose:2552print"found%s"% next2553return next25542555if currentChange < change:2556 earliestCommit ="^%s"% next2557else:2558 latestCommit ="%s"% next25592560return""25612562defimportNewBranch(self, branch, maxChange):2563# make fast-import flush all changes to disk and update the refs using the checkpoint2564# command so that we can try to find the branch parent in the git history2565 self.gitStream.write("checkpoint\n\n");2566 self.gitStream.flush();2567 branchPrefix = self.depotPaths[0] + branch +"/"2568range="@1,%s"% maxChange2569#print "prefix" + branchPrefix2570 changes =p4ChangesForPaths([branchPrefix],range)2571iflen(changes) <=0:2572return False2573 firstChange = changes[0]2574#print "first change in branch: %s" % firstChange2575 sourceBranch = self.knownBranches[branch]2576 sourceDepotPath = self.depotPaths[0] + sourceBranch2577 sourceRef = self.gitRefForBranch(sourceBranch)2578#print "source " + sourceBranch25792580 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2581#print "branch parent: %s" % branchParentChange2582 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2583iflen(gitParent) >0:2584 self.initialParents[self.gitRefForBranch(branch)] = gitParent2585#print "parent git commit: %s" % gitParent25862587 self.importChanges(changes)2588return True25892590defsearchParent(self, parent, branch, target):2591 parentFound =False2592for blob inread_pipe_lines(["git","rev-list","--reverse",2593"--no-merges", parent]):2594 blob = blob.strip()2595iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2596 parentFound =True2597if self.verbose:2598print"Found parent of%sin commit%s"% (branch, blob)2599break2600if parentFound:2601return blob2602else:2603return None26042605defimportChanges(self, changes):2606 cnt =12607for change in changes:2608 description =p4_describe(change)2609 self.updateOptionDict(description)26102611if not self.silent:2612 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2613 sys.stdout.flush()2614 cnt = cnt +126152616try:2617if self.detectBranches:2618 branches = self.splitFilesIntoBranches(description)2619for branch in branches.keys():2620## HACK --hwn2621 branchPrefix = self.depotPaths[0] + branch +"/"2622 self.branchPrefixes = [ branchPrefix ]26232624 parent =""26252626 filesForCommit = branches[branch]26272628if self.verbose:2629print"branch is%s"% branch26302631 self.updatedBranches.add(branch)26322633if branch not in self.createdBranches:2634 self.createdBranches.add(branch)2635 parent = self.knownBranches[branch]2636if parent == branch:2637 parent =""2638else:2639 fullBranch = self.projectName + branch2640if fullBranch not in self.p4BranchesInGit:2641if not self.silent:2642print("\nImporting new branch%s"% fullBranch);2643if self.importNewBranch(branch, change -1):2644 parent =""2645 self.p4BranchesInGit.append(fullBranch)2646if not self.silent:2647print("\nResuming with change%s"% change);26482649if self.verbose:2650print"parent determined through known branches:%s"% parent26512652 branch = self.gitRefForBranch(branch)2653 parent = self.gitRefForBranch(parent)26542655if self.verbose:2656print"looking for initial parent for%s; current parent is%s"% (branch, parent)26572658iflen(parent) ==0and branch in self.initialParents:2659 parent = self.initialParents[branch]2660del self.initialParents[branch]26612662 blob =None2663iflen(parent) >0:2664 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2665if self.verbose:2666print"Creating temporary branch: "+ tempBranch2667 self.commit(description, filesForCommit, tempBranch)2668 self.tempBranches.append(tempBranch)2669 self.checkpoint()2670 blob = self.searchParent(parent, branch, tempBranch)2671if blob:2672 self.commit(description, filesForCommit, branch, blob)2673else:2674if self.verbose:2675print"Parent of%snot found. Committing into head of%s"% (branch, parent)2676 self.commit(description, filesForCommit, branch, parent)2677else:2678 files = self.extractFilesFromCommit(description)2679 self.commit(description, files, self.branch,2680 self.initialParent)2681# only needed once, to connect to the previous commit2682 self.initialParent =""2683exceptIOError:2684print self.gitError.read()2685 sys.exit(1)26862687defimportHeadRevision(self, revision):2688print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)26892690 details = {}2691 details["user"] ="git perforce import user"2692 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2693% (' '.join(self.depotPaths), revision))2694 details["change"] = revision2695 newestRevision =026962697 fileCnt =02698 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]26992700for info inp4CmdList(["files"] + fileArgs):27012702if'code'in info and info['code'] =='error':2703 sys.stderr.write("p4 returned an error:%s\n"2704% info['data'])2705if info['data'].find("must refer to client") >=0:2706 sys.stderr.write("This particular p4 error is misleading.\n")2707 sys.stderr.write("Perhaps the depot path was misspelled.\n");2708 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2709 sys.exit(1)2710if'p4ExitCode'in info:2711 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2712 sys.exit(1)271327142715 change =int(info["change"])2716if change > newestRevision:2717 newestRevision = change27182719if info["action"]in self.delete_actions:2720# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2721#fileCnt = fileCnt + 12722continue27232724for prop in["depotFile","rev","action","type"]:2725 details["%s%s"% (prop, fileCnt)] = info[prop]27262727 fileCnt = fileCnt +127282729 details["change"] = newestRevision27302731# Use time from top-most change so that all git p4 clones of2732# the same p4 repo have the same commit SHA1s.2733 res =p4_describe(newestRevision)2734 details["time"] = res["time"]27352736 self.updateOptionDict(details)2737try:2738 self.commit(details, self.extractFilesFromCommit(details), self.branch)2739exceptIOError:2740print"IO error with git fast-import. Is your git version recent enough?"2741print self.gitError.read()274227432744defrun(self, args):2745 self.depotPaths = []2746 self.changeRange =""2747 self.previousDepotPaths = []2748 self.hasOrigin =False27492750# map from branch depot path to parent branch2751 self.knownBranches = {}2752 self.initialParents = {}27532754if self.importIntoRemotes:2755 self.refPrefix ="refs/remotes/p4/"2756else:2757 self.refPrefix ="refs/heads/p4/"27582759if self.syncWithOrigin:2760 self.hasOrigin =originP4BranchesExist()2761if self.hasOrigin:2762if not self.silent:2763print'Syncing with origin first, using "git fetch origin"'2764system("git fetch origin")27652766 branch_arg_given =bool(self.branch)2767iflen(self.branch) ==0:2768 self.branch = self.refPrefix +"master"2769ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2770system("git update-ref%srefs/heads/p4"% self.branch)2771system("git branch -D p4")27722773# accept either the command-line option, or the configuration variable2774if self.useClientSpec:2775# will use this after clone to set the variable2776 self.useClientSpec_from_options =True2777else:2778ifgitConfigBool("git-p4.useclientspec"):2779 self.useClientSpec =True2780if self.useClientSpec:2781 self.clientSpecDirs =getClientSpec()27822783# TODO: should always look at previous commits,2784# merge with previous imports, if possible.2785if args == []:2786if self.hasOrigin:2787createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)27882789# branches holds mapping from branch name to sha12790 branches =p4BranchesInGit(self.importIntoRemotes)27912792# restrict to just this one, disabling detect-branches2793if branch_arg_given:2794 short = self.branch.split("/")[-1]2795if short in branches:2796 self.p4BranchesInGit = [ short ]2797else:2798 self.p4BranchesInGit = branches.keys()27992800iflen(self.p4BranchesInGit) >1:2801if not self.silent:2802print"Importing from/into multiple branches"2803 self.detectBranches =True2804for branch in branches.keys():2805 self.initialParents[self.refPrefix + branch] = \2806 branches[branch]28072808if self.verbose:2809print"branches:%s"% self.p4BranchesInGit28102811 p4Change =02812for branch in self.p4BranchesInGit:2813 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28142815 settings =extractSettingsGitLog(logMsg)28162817 self.readOptions(settings)2818if(settings.has_key('depot-paths')2819and settings.has_key('change')):2820 change =int(settings['change']) +12821 p4Change =max(p4Change, change)28222823 depotPaths =sorted(settings['depot-paths'])2824if self.previousDepotPaths == []:2825 self.previousDepotPaths = depotPaths2826else:2827 paths = []2828for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2829 prev_list = prev.split("/")2830 cur_list = cur.split("/")2831for i inrange(0,min(len(cur_list),len(prev_list))):2832if cur_list[i] <> prev_list[i]:2833 i = i -12834break28352836 paths.append("/".join(cur_list[:i +1]))28372838 self.previousDepotPaths = paths28392840if p4Change >0:2841 self.depotPaths =sorted(self.previousDepotPaths)2842 self.changeRange ="@%s,#head"% p4Change2843if not self.silent and not self.detectBranches:2844print"Performing incremental import into%sgit branch"% self.branch28452846# accept multiple ref name abbreviations:2847# refs/foo/bar/branch -> use it exactly2848# p4/branch -> prepend refs/remotes/ or refs/heads/2849# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2850if not self.branch.startswith("refs/"):2851if self.importIntoRemotes:2852 prepend ="refs/remotes/"2853else:2854 prepend ="refs/heads/"2855if not self.branch.startswith("p4/"):2856 prepend +="p4/"2857 self.branch = prepend + self.branch28582859iflen(args) ==0and self.depotPaths:2860if not self.silent:2861print"Depot paths:%s"%' '.join(self.depotPaths)2862else:2863if self.depotPaths and self.depotPaths != args:2864print("previous import used depot path%sand now%swas specified. "2865"This doesn't work!"% (' '.join(self.depotPaths),2866' '.join(args)))2867 sys.exit(1)28682869 self.depotPaths =sorted(args)28702871 revision =""2872 self.users = {}28732874# Make sure no revision specifiers are used when --changesfile2875# is specified.2876 bad_changesfile =False2877iflen(self.changesFile) >0:2878for p in self.depotPaths:2879if p.find("@") >=0or p.find("#") >=0:2880 bad_changesfile =True2881break2882if bad_changesfile:2883die("Option --changesfile is incompatible with revision specifiers")28842885 newPaths = []2886for p in self.depotPaths:2887if p.find("@") != -1:2888 atIdx = p.index("@")2889 self.changeRange = p[atIdx:]2890if self.changeRange =="@all":2891 self.changeRange =""2892elif','not in self.changeRange:2893 revision = self.changeRange2894 self.changeRange =""2895 p = p[:atIdx]2896elif p.find("#") != -1:2897 hashIdx = p.index("#")2898 revision = p[hashIdx:]2899 p = p[:hashIdx]2900elif self.previousDepotPaths == []:2901# pay attention to changesfile, if given, else import2902# the entire p4 tree at the head revision2903iflen(self.changesFile) ==0:2904 revision ="#head"29052906 p = re.sub("\.\.\.$","", p)2907if not p.endswith("/"):2908 p +="/"29092910 newPaths.append(p)29112912 self.depotPaths = newPaths29132914# --detect-branches may change this for each branch2915 self.branchPrefixes = self.depotPaths29162917 self.loadUserMapFromCache()2918 self.labels = {}2919if self.detectLabels:2920 self.getLabels();29212922if self.detectBranches:2923## FIXME - what's a P4 projectName ?2924 self.projectName = self.guessProjectName()29252926if self.hasOrigin:2927 self.getBranchMappingFromGitBranches()2928else:2929 self.getBranchMapping()2930if self.verbose:2931print"p4-git branches:%s"% self.p4BranchesInGit2932print"initial parents:%s"% self.initialParents2933for b in self.p4BranchesInGit:2934if b !="master":29352936## FIXME2937 b = b[len(self.projectName):]2938 self.createdBranches.add(b)29392940 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29412942 self.importProcess = subprocess.Popen(["git","fast-import"],2943 stdin=subprocess.PIPE,2944 stdout=subprocess.PIPE,2945 stderr=subprocess.PIPE);2946 self.gitOutput = self.importProcess.stdout2947 self.gitStream = self.importProcess.stdin2948 self.gitError = self.importProcess.stderr29492950if revision:2951 self.importHeadRevision(revision)2952else:2953 changes = []29542955iflen(self.changesFile) >0:2956 output =open(self.changesFile).readlines()2957 changeSet =set()2958for line in output:2959 changeSet.add(int(line))29602961for change in changeSet:2962 changes.append(change)29632964 changes.sort()2965else:2966# catch "git p4 sync" with no new branches, in a repo that2967# does not have any existing p4 branches2968iflen(args) ==0:2969if not self.p4BranchesInGit:2970die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")29712972# The default branch is master, unless --branch is used to2973# specify something else. Make sure it exists, or complain2974# nicely about how to use --branch.2975if not self.detectBranches:2976if notbranch_exists(self.branch):2977if branch_arg_given:2978die("Error: branch%sdoes not exist."% self.branch)2979else:2980die("Error: no branch%s; perhaps specify one with --branch."%2981 self.branch)29822983if self.verbose:2984print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2985 self.changeRange)2986 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)29872988iflen(self.maxChanges) >0:2989 changes = changes[:min(int(self.maxChanges),len(changes))]29902991iflen(changes) ==0:2992if not self.silent:2993print"No changes to import!"2994else:2995if not self.silent and not self.detectBranches:2996print"Import destination:%s"% self.branch29972998 self.updatedBranches =set()29993000if not self.detectBranches:3001if args:3002# start a new branch3003 self.initialParent =""3004else:3005# build on a previous revision3006 self.initialParent =parseRevision(self.branch)30073008 self.importChanges(changes)30093010if not self.silent:3011print""3012iflen(self.updatedBranches) >0:3013 sys.stdout.write("Updated branches: ")3014for b in self.updatedBranches:3015 sys.stdout.write("%s"% b)3016 sys.stdout.write("\n")30173018ifgitConfigBool("git-p4.importLabels"):3019 self.importLabels =True30203021if self.importLabels:3022 p4Labels =getP4Labels(self.depotPaths)3023 gitTags =getGitTags()30243025 missingP4Labels = p4Labels - gitTags3026 self.importP4Labels(self.gitStream, missingP4Labels)30273028 self.gitStream.close()3029if self.importProcess.wait() !=0:3030die("fast-import failed:%s"% self.gitError.read())3031 self.gitOutput.close()3032 self.gitError.close()30333034# Cleanup temporary branches created during import3035if self.tempBranches != []:3036for branch in self.tempBranches:3037read_pipe("git update-ref -d%s"% branch)3038 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30393040# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3041# a convenient shortcut refname "p4".3042if self.importIntoRemotes:3043 head_ref = self.refPrefix +"HEAD"3044if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3045system(["git","symbolic-ref", head_ref, self.branch])30463047return True30483049classP4Rebase(Command):3050def__init__(self):3051 Command.__init__(self)3052 self.options = [3053 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3054]3055 self.importLabels =False3056 self.description = ("Fetches the latest revision from perforce and "3057+"rebases the current work (branch) against it")30583059defrun(self, args):3060 sync =P4Sync()3061 sync.importLabels = self.importLabels3062 sync.run([])30633064return self.rebase()30653066defrebase(self):3067if os.system("git update-index --refresh") !=0:3068die("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.");3069iflen(read_pipe("git diff-index HEAD --")) >0:3070die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");30713072[upstream, settings] =findUpstreamBranchPoint()3073iflen(upstream) ==0:3074die("Cannot find upstream branchpoint for rebase")30753076# the branchpoint may be p4/foo~3, so strip off the parent3077 upstream = re.sub("~[0-9]+$","", upstream)30783079print"Rebasing the current branch onto%s"% upstream3080 oldHead =read_pipe("git rev-parse HEAD").strip()3081system("git rebase%s"% upstream)3082system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3083return True30843085classP4Clone(P4Sync):3086def__init__(self):3087 P4Sync.__init__(self)3088 self.description ="Creates a new git repository and imports from Perforce into it"3089 self.usage ="usage: %prog [options] //depot/path[@revRange]"3090 self.options += [3091 optparse.make_option("--destination", dest="cloneDestination",3092 action='store', default=None,3093help="where to leave result of the clone"),3094 optparse.make_option("-/", dest="cloneExclude",3095 action="append",type="string",3096help="exclude depot path"),3097 optparse.make_option("--bare", dest="cloneBare",3098 action="store_true", default=False),3099]3100 self.cloneDestination =None3101 self.needsGit =False3102 self.cloneBare =False31033104# This is required for the "append" cloneExclude action3105defensure_value(self, attr, value):3106if nothasattr(self, attr)orgetattr(self, attr)is None:3107setattr(self, attr, value)3108returngetattr(self, attr)31093110defdefaultDestination(self, args):3111## TODO: use common prefix of args?3112 depotPath = args[0]3113 depotDir = re.sub("(@[^@]*)$","", depotPath)3114 depotDir = re.sub("(#[^#]*)$","", depotDir)3115 depotDir = re.sub(r"\.\.\.$","", depotDir)3116 depotDir = re.sub(r"/$","", depotDir)3117return os.path.split(depotDir)[1]31183119defrun(self, args):3120iflen(args) <1:3121return False31223123if self.keepRepoPath and not self.cloneDestination:3124 sys.stderr.write("Must specify destination for --keep-path\n")3125 sys.exit(1)31263127 depotPaths = args31283129if not self.cloneDestination andlen(depotPaths) >1:3130 self.cloneDestination = depotPaths[-1]3131 depotPaths = depotPaths[:-1]31323133 self.cloneExclude = ["/"+p for p in self.cloneExclude]3134for p in depotPaths:3135if not p.startswith("//"):3136 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3137return False31383139if not self.cloneDestination:3140 self.cloneDestination = self.defaultDestination(args)31413142print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31433144if not os.path.exists(self.cloneDestination):3145 os.makedirs(self.cloneDestination)3146chdir(self.cloneDestination)31473148 init_cmd = ["git","init"]3149if self.cloneBare:3150 init_cmd.append("--bare")3151 retcode = subprocess.call(init_cmd)3152if retcode:3153raiseCalledProcessError(retcode, init_cmd)31543155if not P4Sync.run(self, depotPaths):3156return False31573158# create a master branch and check out a work tree3159ifgitBranchExists(self.branch):3160system(["git","branch","master", self.branch ])3161if not self.cloneBare:3162system(["git","checkout","-f"])3163else:3164print'Not checking out any branch, use ' \3165'"git checkout -q -b master <branch>"'31663167# auto-set this variable if invoked with --use-client-spec3168if self.useClientSpec_from_options:3169system("git config --bool git-p4.useclientspec true")31703171return True31723173classP4Branches(Command):3174def__init__(self):3175 Command.__init__(self)3176 self.options = [ ]3177 self.description = ("Shows the git branches that hold imports and their "3178+"corresponding perforce depot paths")3179 self.verbose =False31803181defrun(self, args):3182iforiginP4BranchesExist():3183createOrUpdateBranchesFromOrigin()31843185 cmdline ="git rev-parse --symbolic "3186 cmdline +=" --remotes"31873188for line inread_pipe_lines(cmdline):3189 line = line.strip()31903191if not line.startswith('p4/')or line =="p4/HEAD":3192continue3193 branch = line31943195 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3196 settings =extractSettingsGitLog(log)31973198print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3199return True32003201classHelpFormatter(optparse.IndentedHelpFormatter):3202def__init__(self):3203 optparse.IndentedHelpFormatter.__init__(self)32043205defformat_description(self, description):3206if description:3207return description +"\n"3208else:3209return""32103211defprintUsage(commands):3212print"usage:%s<command> [options]"% sys.argv[0]3213print""3214print"valid commands:%s"%", ".join(commands)3215print""3216print"Try%s<command> --help for command specific help."% sys.argv[0]3217print""32183219commands = {3220"debug": P4Debug,3221"submit": P4Submit,3222"commit": P4Submit,3223"sync": P4Sync,3224"rebase": P4Rebase,3225"clone": P4Clone,3226"rollback": P4RollBack,3227"branches": P4Branches3228}322932303231defmain():3232iflen(sys.argv[1:]) ==0:3233printUsage(commands.keys())3234 sys.exit(2)32353236 cmdName = sys.argv[1]3237try:3238 klass = commands[cmdName]3239 cmd =klass()3240exceptKeyError:3241print"unknown command%s"% cmdName3242print""3243printUsage(commands.keys())3244 sys.exit(2)32453246 options = cmd.options3247 cmd.gitdir = os.environ.get("GIT_DIR",None)32483249 args = sys.argv[2:]32503251 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3252if cmd.needsGit:3253 options.append(optparse.make_option("--git-dir", dest="gitdir"))32543255 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3256 options,3257 description = cmd.description,3258 formatter =HelpFormatter())32593260(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3261global verbose3262 verbose = cmd.verbose3263if cmd.needsGit:3264if cmd.gitdir ==None:3265 cmd.gitdir = os.path.abspath(".git")3266if notisValidGitDir(cmd.gitdir):3267 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3268if os.path.exists(cmd.gitdir):3269 cdup =read_pipe("git rev-parse --show-cdup").strip()3270iflen(cdup) >0:3271chdir(cdup);32723273if notisValidGitDir(cmd.gitdir):3274ifisValidGitDir(cmd.gitdir +"/.git"):3275 cmd.gitdir +="/.git"3276else:3277die("fatal: cannot locate git repository at%s"% cmd.gitdir)32783279 os.environ["GIT_DIR"] = cmd.gitdir32803281if not cmd.run(args):3282 parser.print_help()3283 sys.exit(2)328432853286if __name__ =='__main__':3287main()