1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25 26try: 27from subprocess import CalledProcessError 28exceptImportError: 29# from python2.7:subprocess.py 30# Exception classes used by this module. 31classCalledProcessError(Exception): 32"""This exception is raised when a process run by check_call() returns 33 a non-zero exit status. The exit status will be stored in the 34 returncode attribute.""" 35def__init__(self, returncode, cmd): 36 self.returncode = returncode 37 self.cmd = cmd 38def__str__(self): 39return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 40 41verbose =False 42 43# Only labels/tags matching this will be imported/exported 44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 45 46defp4_build_cmd(cmd): 47"""Build a suitable p4 command line. 48 49 This consolidates building and returning a p4 command line into one 50 location. It means that hooking into the environment, or other configuration 51 can be done more easily. 52 """ 53 real_cmd = ["p4"] 54 55 user =gitConfig("git-p4.user") 56iflen(user) >0: 57 real_cmd += ["-u",user] 58 59 password =gitConfig("git-p4.password") 60iflen(password) >0: 61 real_cmd += ["-P", password] 62 63 port =gitConfig("git-p4.port") 64iflen(port) >0: 65 real_cmd += ["-p", port] 66 67 host =gitConfig("git-p4.host") 68iflen(host) >0: 69 real_cmd += ["-H", host] 70 71 client =gitConfig("git-p4.client") 72iflen(client) >0: 73 real_cmd += ["-c", client] 74 75 76ifisinstance(cmd,basestring): 77 real_cmd =' '.join(real_cmd) +' '+ cmd 78else: 79 real_cmd += cmd 80return real_cmd 81 82defchdir(path, is_client_path=False): 83"""Do chdir to the given path, and set the PWD environment 84 variable for use by P4. It does not look at getcwd() output. 85 Since we're not using the shell, it is necessary to set the 86 PWD environment variable explicitly. 87 88 Normally, expand the path to force it to be absolute. This 89 addresses the use of relative path names inside P4 settings, 90 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 91 as given; it looks for .p4config using PWD. 92 93 If is_client_path, the path was handed to us directly by p4, 94 and may be a symbolic link. Do not call os.getcwd() in this 95 case, because it will cause p4 to think that PWD is not inside 96 the client path. 97 """ 98 99 os.chdir(path) 100if not is_client_path: 101 path = os.getcwd() 102 os.environ['PWD'] = path 103 104defdie(msg): 105if verbose: 106raiseException(msg) 107else: 108 sys.stderr.write(msg +"\n") 109 sys.exit(1) 110 111defwrite_pipe(c, stdin): 112if verbose: 113 sys.stderr.write('Writing pipe:%s\n'%str(c)) 114 115 expand =isinstance(c,basestring) 116 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 117 pipe = p.stdin 118 val = pipe.write(stdin) 119 pipe.close() 120if p.wait(): 121die('Command failed:%s'%str(c)) 122 123return val 124 125defp4_write_pipe(c, stdin): 126 real_cmd =p4_build_cmd(c) 127returnwrite_pipe(real_cmd, stdin) 128 129defread_pipe(c, ignore_error=False): 130if verbose: 131 sys.stderr.write('Reading pipe:%s\n'%str(c)) 132 133 expand =isinstance(c,basestring) 134 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 135 pipe = p.stdout 136 val = pipe.read() 137if p.wait()and not ignore_error: 138die('Command failed:%s'%str(c)) 139 140return val 141 142defp4_read_pipe(c, ignore_error=False): 143 real_cmd =p4_build_cmd(c) 144returnread_pipe(real_cmd, ignore_error) 145 146defread_pipe_lines(c): 147if verbose: 148 sys.stderr.write('Reading pipe:%s\n'%str(c)) 149 150 expand =isinstance(c, basestring) 151 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 152 pipe = p.stdout 153 val = pipe.readlines() 154if pipe.close()or p.wait(): 155die('Command failed:%s'%str(c)) 156 157return val 158 159defp4_read_pipe_lines(c): 160"""Specifically invoke p4 on the command supplied. """ 161 real_cmd =p4_build_cmd(c) 162returnread_pipe_lines(real_cmd) 163 164defp4_has_command(cmd): 165"""Ask p4 for help on this command. If it returns an error, the 166 command does not exist in this version of p4.""" 167 real_cmd =p4_build_cmd(["help", cmd]) 168 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 169 stderr=subprocess.PIPE) 170 p.communicate() 171return p.returncode ==0 172 173defp4_has_move_command(): 174"""See if the move command exists, that it supports -k, and that 175 it has not been administratively disabled. The arguments 176 must be correct, but the filenames do not have to exist. Use 177 ones with wildcards so even if they exist, it will fail.""" 178 179if notp4_has_command("move"): 180return False 181 cmd =p4_build_cmd(["move","-k","@from","@to"]) 182 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 183(out, err) = p.communicate() 184# return code will be 1 in either case 185if err.find("Invalid option") >=0: 186return False 187if err.find("disabled") >=0: 188return False 189# assume it failed because @... was invalid changelist 190return True 191 192defsystem(cmd): 193 expand =isinstance(cmd,basestring) 194if verbose: 195 sys.stderr.write("executing%s\n"%str(cmd)) 196 retcode = subprocess.call(cmd, shell=expand) 197if retcode: 198raiseCalledProcessError(retcode, cmd) 199 200defp4_system(cmd): 201"""Specifically invoke p4 as the system command. """ 202 real_cmd =p4_build_cmd(cmd) 203 expand =isinstance(real_cmd, basestring) 204 retcode = subprocess.call(real_cmd, shell=expand) 205if retcode: 206raiseCalledProcessError(retcode, real_cmd) 207 208_p4_version_string =None 209defp4_version_string(): 210"""Read the version string, showing just the last line, which 211 hopefully is the interesting version bit. 212 213 $ p4 -V 214 Perforce - The Fast Software Configuration Management System. 215 Copyright 1995-2011 Perforce Software. All rights reserved. 216 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 217 """ 218global _p4_version_string 219if not _p4_version_string: 220 a =p4_read_pipe_lines(["-V"]) 221 _p4_version_string = a[-1].rstrip() 222return _p4_version_string 223 224defp4_integrate(src, dest): 225p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 226 227defp4_sync(f, *options): 228p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 229 230defp4_add(f): 231# forcibly add file names with wildcards 232ifwildcard_present(f): 233p4_system(["add","-f", f]) 234else: 235p4_system(["add", f]) 236 237defp4_delete(f): 238p4_system(["delete",wildcard_encode(f)]) 239 240defp4_edit(f): 241p4_system(["edit",wildcard_encode(f)]) 242 243defp4_revert(f): 244p4_system(["revert",wildcard_encode(f)]) 245 246defp4_reopen(type, f): 247p4_system(["reopen","-t",type,wildcard_encode(f)]) 248 249defp4_move(src, dest): 250p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 251 252defp4_describe(change): 253"""Make sure it returns a valid result by checking for 254 the presence of field "time". Return a dict of the 255 results.""" 256 257 ds =p4CmdList(["describe","-s",str(change)]) 258iflen(ds) !=1: 259die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 260 261 d = ds[0] 262 263if"p4ExitCode"in d: 264die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 265str(d))) 266if"code"in d: 267if d["code"] =="error": 268die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 269 270if"time"not in d: 271die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 272 273return d 274 275# 276# Canonicalize the p4 type and return a tuple of the 277# base type, plus any modifiers. See "p4 help filetypes" 278# for a list and explanation. 279# 280defsplit_p4_type(p4type): 281 282 p4_filetypes_historical = { 283"ctempobj":"binary+Sw", 284"ctext":"text+C", 285"cxtext":"text+Cx", 286"ktext":"text+k", 287"kxtext":"text+kx", 288"ltext":"text+F", 289"tempobj":"binary+FSw", 290"ubinary":"binary+F", 291"uresource":"resource+F", 292"uxbinary":"binary+Fx", 293"xbinary":"binary+x", 294"xltext":"text+Fx", 295"xtempobj":"binary+Swx", 296"xtext":"text+x", 297"xunicode":"unicode+x", 298"xutf16":"utf16+x", 299} 300if p4type in p4_filetypes_historical: 301 p4type = p4_filetypes_historical[p4type] 302 mods ="" 303 s = p4type.split("+") 304 base = s[0] 305 mods ="" 306iflen(s) >1: 307 mods = s[1] 308return(base, mods) 309 310# 311# return the raw p4 type of a file (text, text+ko, etc) 312# 313defp4_type(f): 314 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 315return results[0]['headType'] 316 317# 318# Given a type base and modifier, return a regexp matching 319# the keywords that can be expanded in the file 320# 321defp4_keywords_regexp_for_type(base, type_mods): 322if base in("text","unicode","binary"): 323 kwords =None 324if"ko"in type_mods: 325 kwords ='Id|Header' 326elif"k"in type_mods: 327 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 328else: 329return None 330 pattern = r""" 331 \$ # Starts with a dollar, followed by... 332 (%s) # one of the keywords, followed by... 333 (:[^$\n]+)? # possibly an old expansion, followed by... 334 \$ # another dollar 335 """% kwords 336return pattern 337else: 338return None 339 340# 341# Given a file, return a regexp matching the possible 342# RCS keywords that will be expanded, or None for files 343# with kw expansion turned off. 344# 345defp4_keywords_regexp_for_file(file): 346if not os.path.exists(file): 347return None 348else: 349(type_base, type_mods) =split_p4_type(p4_type(file)) 350returnp4_keywords_regexp_for_type(type_base, type_mods) 351 352defsetP4ExecBit(file, mode): 353# Reopens an already open file and changes the execute bit to match 354# the execute bit setting in the passed in mode. 355 356 p4Type ="+x" 357 358if notisModeExec(mode): 359 p4Type =getP4OpenedType(file) 360 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 361 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 362if p4Type[-1] =="+": 363 p4Type = p4Type[0:-1] 364 365p4_reopen(p4Type,file) 366 367defgetP4OpenedType(file): 368# Returns the perforce file type for the given file. 369 370 result =p4_read_pipe(["opened",wildcard_encode(file)]) 371 match = re.match(".*\((.+)\)\r?$", result) 372if match: 373return match.group(1) 374else: 375die("Could not determine file type for%s(result: '%s')"% (file, result)) 376 377# Return the set of all p4 labels 378defgetP4Labels(depotPaths): 379 labels =set() 380ifisinstance(depotPaths,basestring): 381 depotPaths = [depotPaths] 382 383for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 384 label = l['label'] 385 labels.add(label) 386 387return labels 388 389# Return the set of all git tags 390defgetGitTags(): 391 gitTags =set() 392for line inread_pipe_lines(["git","tag"]): 393 tag = line.strip() 394 gitTags.add(tag) 395return gitTags 396 397defdiffTreePattern(): 398# This is a simple generator for the diff tree regex pattern. This could be 399# a class variable if this and parseDiffTreeEntry were a part of a class. 400 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 401while True: 402yield pattern 403 404defparseDiffTreeEntry(entry): 405"""Parses a single diff tree entry into its component elements. 406 407 See git-diff-tree(1) manpage for details about the format of the diff 408 output. This method returns a dictionary with the following elements: 409 410 src_mode - The mode of the source file 411 dst_mode - The mode of the destination file 412 src_sha1 - The sha1 for the source file 413 dst_sha1 - The sha1 fr the destination file 414 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 415 status_score - The score for the status (applicable for 'C' and 'R' 416 statuses). This is None if there is no score. 417 src - The path for the source file. 418 dst - The path for the destination file. This is only present for 419 copy or renames. If it is not present, this is None. 420 421 If the pattern is not matched, None is returned.""" 422 423 match =diffTreePattern().next().match(entry) 424if match: 425return{ 426'src_mode': match.group(1), 427'dst_mode': match.group(2), 428'src_sha1': match.group(3), 429'dst_sha1': match.group(4), 430'status': match.group(5), 431'status_score': match.group(6), 432'src': match.group(7), 433'dst': match.group(10) 434} 435return None 436 437defisModeExec(mode): 438# Returns True if the given git mode represents an executable file, 439# otherwise False. 440return mode[-3:] =="755" 441 442defisModeExecChanged(src_mode, dst_mode): 443returnisModeExec(src_mode) !=isModeExec(dst_mode) 444 445defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 446 447ifisinstance(cmd,basestring): 448 cmd ="-G "+ cmd 449 expand =True 450else: 451 cmd = ["-G"] + cmd 452 expand =False 453 454 cmd =p4_build_cmd(cmd) 455if verbose: 456 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 457 458# Use a temporary file to avoid deadlocks without 459# subprocess.communicate(), which would put another copy 460# of stdout into memory. 461 stdin_file =None 462if stdin is not None: 463 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 464ifisinstance(stdin,basestring): 465 stdin_file.write(stdin) 466else: 467for i in stdin: 468 stdin_file.write(i +'\n') 469 stdin_file.flush() 470 stdin_file.seek(0) 471 472 p4 = subprocess.Popen(cmd, 473 shell=expand, 474 stdin=stdin_file, 475 stdout=subprocess.PIPE) 476 477 result = [] 478try: 479while True: 480 entry = marshal.load(p4.stdout) 481if cb is not None: 482cb(entry) 483else: 484 result.append(entry) 485exceptEOFError: 486pass 487 exitCode = p4.wait() 488if exitCode !=0: 489 entry = {} 490 entry["p4ExitCode"] = exitCode 491 result.append(entry) 492 493return result 494 495defp4Cmd(cmd): 496list=p4CmdList(cmd) 497 result = {} 498for entry inlist: 499 result.update(entry) 500return result; 501 502defp4Where(depotPath): 503if not depotPath.endswith("/"): 504 depotPath +="/" 505 depotPath = depotPath +"..." 506 outputList =p4CmdList(["where", depotPath]) 507 output =None 508for entry in outputList: 509if"depotFile"in entry: 510if entry["depotFile"] == depotPath: 511 output = entry 512break 513elif"data"in entry: 514 data = entry.get("data") 515 space = data.find(" ") 516if data[:space] == depotPath: 517 output = entry 518break 519if output ==None: 520return"" 521if output["code"] =="error": 522return"" 523 clientPath ="" 524if"path"in output: 525 clientPath = output.get("path") 526elif"data"in output: 527 data = output.get("data") 528 lastSpace = data.rfind(" ") 529 clientPath = data[lastSpace +1:] 530 531if clientPath.endswith("..."): 532 clientPath = clientPath[:-3] 533return clientPath 534 535defcurrentGitBranch(): 536returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 537 538defisValidGitDir(path): 539if(os.path.exists(path +"/HEAD") 540and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 541return True; 542return False 543 544defparseRevision(ref): 545returnread_pipe("git rev-parse%s"% ref).strip() 546 547defbranchExists(ref): 548 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 549 ignore_error=True) 550returnlen(rev) >0 551 552defextractLogMessageFromGitCommit(commit): 553 logMessage ="" 554 555## fixme: title is first line of commit, not 1st paragraph. 556 foundTitle =False 557for log inread_pipe_lines("git cat-file commit%s"% commit): 558if not foundTitle: 559iflen(log) ==1: 560 foundTitle =True 561continue 562 563 logMessage += log 564return logMessage 565 566defextractSettingsGitLog(log): 567 values = {} 568for line in log.split("\n"): 569 line = line.strip() 570 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 571if not m: 572continue 573 574 assignments = m.group(1).split(':') 575for a in assignments: 576 vals = a.split('=') 577 key = vals[0].strip() 578 val = ('='.join(vals[1:])).strip() 579if val.endswith('\"')and val.startswith('"'): 580 val = val[1:-1] 581 582 values[key] = val 583 584 paths = values.get("depot-paths") 585if not paths: 586 paths = values.get("depot-path") 587if paths: 588 values['depot-paths'] = paths.split(',') 589return values 590 591defgitBranchExists(branch): 592 proc = subprocess.Popen(["git","rev-parse", branch], 593 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 594return proc.wait() ==0; 595 596_gitConfig = {} 597 598defgitConfig(key): 599if not _gitConfig.has_key(key): 600 cmd = ["git","config", key ] 601 s =read_pipe(cmd, ignore_error=True) 602 _gitConfig[key] = s.strip() 603return _gitConfig[key] 604 605defgitConfigBool(key): 606"""Return a bool, using git config --bool. It is True only if the 607 variable is set to true, and False if set to false or not present 608 in the config.""" 609 610if not _gitConfig.has_key(key): 611 cmd = ["git","config","--bool", key ] 612 s =read_pipe(cmd, ignore_error=True) 613 v = s.strip() 614 _gitConfig[key] = v =="true" 615return _gitConfig[key] 616 617defgitConfigList(key): 618if not _gitConfig.has_key(key): 619 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 620 _gitConfig[key] = s.strip().split(os.linesep) 621return _gitConfig[key] 622 623defp4BranchesInGit(branchesAreInRemotes=True): 624"""Find all the branches whose names start with "p4/", looking 625 in remotes or heads as specified by the argument. Return 626 a dictionary of{ branch: revision }for each one found. 627 The branch names are the short names, without any 628 "p4/" prefix.""" 629 630 branches = {} 631 632 cmdline ="git rev-parse --symbolic " 633if branchesAreInRemotes: 634 cmdline +="--remotes" 635else: 636 cmdline +="--branches" 637 638for line inread_pipe_lines(cmdline): 639 line = line.strip() 640 641# only import to p4/ 642if not line.startswith('p4/'): 643continue 644# special symbolic ref to p4/master 645if line =="p4/HEAD": 646continue 647 648# strip off p4/ prefix 649 branch = line[len("p4/"):] 650 651 branches[branch] =parseRevision(line) 652 653return branches 654 655defbranch_exists(branch): 656"""Make sure that the given ref name really exists.""" 657 658 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 659 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 660 out, _ = p.communicate() 661if p.returncode: 662return False 663# expect exactly one line of output: the branch name 664return out.rstrip() == branch 665 666deffindUpstreamBranchPoint(head ="HEAD"): 667 branches =p4BranchesInGit() 668# map from depot-path to branch name 669 branchByDepotPath = {} 670for branch in branches.keys(): 671 tip = branches[branch] 672 log =extractLogMessageFromGitCommit(tip) 673 settings =extractSettingsGitLog(log) 674if settings.has_key("depot-paths"): 675 paths =",".join(settings["depot-paths"]) 676 branchByDepotPath[paths] ="remotes/p4/"+ branch 677 678 settings =None 679 parent =0 680while parent <65535: 681 commit = head +"~%s"% parent 682 log =extractLogMessageFromGitCommit(commit) 683 settings =extractSettingsGitLog(log) 684if settings.has_key("depot-paths"): 685 paths =",".join(settings["depot-paths"]) 686if branchByDepotPath.has_key(paths): 687return[branchByDepotPath[paths], settings] 688 689 parent = parent +1 690 691return["", settings] 692 693defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 694if not silent: 695print("Creating/updating branch(es) in%sbased on origin branch(es)" 696% localRefPrefix) 697 698 originPrefix ="origin/p4/" 699 700for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 701 line = line.strip() 702if(not line.startswith(originPrefix))or line.endswith("HEAD"): 703continue 704 705 headName = line[len(originPrefix):] 706 remoteHead = localRefPrefix + headName 707 originHead = line 708 709 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 710if(not original.has_key('depot-paths') 711or not original.has_key('change')): 712continue 713 714 update =False 715if notgitBranchExists(remoteHead): 716if verbose: 717print"creating%s"% remoteHead 718 update =True 719else: 720 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 721if settings.has_key('change') >0: 722if settings['depot-paths'] == original['depot-paths']: 723 originP4Change =int(original['change']) 724 p4Change =int(settings['change']) 725if originP4Change > p4Change: 726print("%s(%s) is newer than%s(%s). " 727"Updating p4 branch from origin." 728% (originHead, originP4Change, 729 remoteHead, p4Change)) 730 update =True 731else: 732print("Ignoring:%swas imported from%swhile " 733"%swas imported from%s" 734% (originHead,','.join(original['depot-paths']), 735 remoteHead,','.join(settings['depot-paths']))) 736 737if update: 738system("git update-ref%s %s"% (remoteHead, originHead)) 739 740deforiginP4BranchesExist(): 741returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 742 743defp4ChangesForPaths(depotPaths, changeRange): 744assert depotPaths 745 cmd = ['changes'] 746for p in depotPaths: 747 cmd += ["%s...%s"% (p, changeRange)] 748 output =p4_read_pipe_lines(cmd) 749 750 changes = {} 751for line in output: 752 changeNum =int(line.split(" ")[1]) 753 changes[changeNum] =True 754 755 changelist = changes.keys() 756 changelist.sort() 757return changelist 758 759defp4PathStartsWith(path, prefix): 760# This method tries to remedy a potential mixed-case issue: 761# 762# If UserA adds //depot/DirA/file1 763# and UserB adds //depot/dira/file2 764# 765# we may or may not have a problem. If you have core.ignorecase=true, 766# we treat DirA and dira as the same directory 767ifgitConfigBool("core.ignorecase"): 768return path.lower().startswith(prefix.lower()) 769return path.startswith(prefix) 770 771defgetClientSpec(): 772"""Look at the p4 client spec, create a View() object that contains 773 all the mappings, and return it.""" 774 775 specList =p4CmdList("client -o") 776iflen(specList) !=1: 777die('Output from "client -o" is%dlines, expecting 1'% 778len(specList)) 779 780# dictionary of all client parameters 781 entry = specList[0] 782 783# the //client/ name 784 client_name = entry["Client"] 785 786# just the keys that start with "View" 787 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 788 789# hold this new View 790 view =View(client_name) 791 792# append the lines, in order, to the view 793for view_num inrange(len(view_keys)): 794 k ="View%d"% view_num 795if k not in view_keys: 796die("Expected view key%smissing"% k) 797 view.append(entry[k]) 798 799return view 800 801defgetClientRoot(): 802"""Grab the client directory.""" 803 804 output =p4CmdList("client -o") 805iflen(output) !=1: 806die('Output from "client -o" is%dlines, expecting 1'%len(output)) 807 808 entry = output[0] 809if"Root"not in entry: 810die('Client has no "Root"') 811 812return entry["Root"] 813 814# 815# P4 wildcards are not allowed in filenames. P4 complains 816# if you simply add them, but you can force it with "-f", in 817# which case it translates them into %xx encoding internally. 818# 819defwildcard_decode(path): 820# Search for and fix just these four characters. Do % last so 821# that fixing it does not inadvertently create new %-escapes. 822# Cannot have * in a filename in windows; untested as to 823# what p4 would do in such a case. 824if not platform.system() =="Windows": 825 path = path.replace("%2A","*") 826 path = path.replace("%23","#") \ 827.replace("%40","@") \ 828.replace("%25","%") 829return path 830 831defwildcard_encode(path): 832# do % first to avoid double-encoding the %s introduced here 833 path = path.replace("%","%25") \ 834.replace("*","%2A") \ 835.replace("#","%23") \ 836.replace("@","%40") 837return path 838 839defwildcard_present(path): 840 m = re.search("[*#@%]", path) 841return m is not None 842 843class Command: 844def__init__(self): 845 self.usage ="usage: %prog [options]" 846 self.needsGit =True 847 self.verbose =False 848 849class P4UserMap: 850def__init__(self): 851 self.userMapFromPerforceServer =False 852 self.myP4UserId =None 853 854defp4UserId(self): 855if self.myP4UserId: 856return self.myP4UserId 857 858 results =p4CmdList("user -o") 859for r in results: 860if r.has_key('User'): 861 self.myP4UserId = r['User'] 862return r['User'] 863die("Could not find your p4 user id") 864 865defp4UserIsMe(self, p4User): 866# return True if the given p4 user is actually me 867 me = self.p4UserId() 868if not p4User or p4User != me: 869return False 870else: 871return True 872 873defgetUserCacheFilename(self): 874 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 875return home +"/.gitp4-usercache.txt" 876 877defgetUserMapFromPerforceServer(self): 878if self.userMapFromPerforceServer: 879return 880 self.users = {} 881 self.emails = {} 882 883for output inp4CmdList("users"): 884if not output.has_key("User"): 885continue 886 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 887 self.emails[output["Email"]] = output["User"] 888 889 890 s ='' 891for(key, val)in self.users.items(): 892 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 893 894open(self.getUserCacheFilename(),"wb").write(s) 895 self.userMapFromPerforceServer =True 896 897defloadUserMapFromCache(self): 898 self.users = {} 899 self.userMapFromPerforceServer =False 900try: 901 cache =open(self.getUserCacheFilename(),"rb") 902 lines = cache.readlines() 903 cache.close() 904for line in lines: 905 entry = line.strip().split("\t") 906 self.users[entry[0]] = entry[1] 907exceptIOError: 908 self.getUserMapFromPerforceServer() 909 910classP4Debug(Command): 911def__init__(self): 912 Command.__init__(self) 913 self.options = [] 914 self.description ="A tool to debug the output of p4 -G." 915 self.needsGit =False 916 917defrun(self, args): 918 j =0 919for output inp4CmdList(args): 920print'Element:%d'% j 921 j +=1 922print output 923return True 924 925classP4RollBack(Command): 926def__init__(self): 927 Command.__init__(self) 928 self.options = [ 929 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 930] 931 self.description ="A tool to debug the multi-branch import. Don't use :)" 932 self.rollbackLocalBranches =False 933 934defrun(self, args): 935iflen(args) !=1: 936return False 937 maxChange =int(args[0]) 938 939if"p4ExitCode"inp4Cmd("changes -m 1"): 940die("Problems executing p4"); 941 942if self.rollbackLocalBranches: 943 refPrefix ="refs/heads/" 944 lines =read_pipe_lines("git rev-parse --symbolic --branches") 945else: 946 refPrefix ="refs/remotes/" 947 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 948 949for line in lines: 950if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 951 line = line.strip() 952 ref = refPrefix + line 953 log =extractLogMessageFromGitCommit(ref) 954 settings =extractSettingsGitLog(log) 955 956 depotPaths = settings['depot-paths'] 957 change = settings['change'] 958 959 changed =False 960 961iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 962for p in depotPaths]))) ==0: 963print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 964system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 965continue 966 967while change andint(change) > maxChange: 968 changed =True 969if self.verbose: 970print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 971system("git update-ref%s\"%s^\""% (ref, ref)) 972 log =extractLogMessageFromGitCommit(ref) 973 settings =extractSettingsGitLog(log) 974 975 976 depotPaths = settings['depot-paths'] 977 change = settings['change'] 978 979if changed: 980print"%srewound to%s"% (ref, change) 981 982return True 983 984classP4Submit(Command, P4UserMap): 985 986 conflict_behavior_choices = ("ask","skip","quit") 987 988def__init__(self): 989 Command.__init__(self) 990 P4UserMap.__init__(self) 991 self.options = [ 992 optparse.make_option("--origin", dest="origin"), 993 optparse.make_option("-M", dest="detectRenames", action="store_true"), 994# preserve the user, requires relevant p4 permissions 995 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 996 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 997 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 998 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 999 optparse.make_option("--conflict", dest="conflict_behavior",1000 choices=self.conflict_behavior_choices),1001 optparse.make_option("--branch", dest="branch"),1002]1003 self.description ="Submit changes from git to the perforce depot."1004 self.usage +=" [name of git branch to submit into perforce depot]"1005 self.origin =""1006 self.detectRenames =False1007 self.preserveUser =gitConfigBool("git-p4.preserveUser")1008 self.dry_run =False1009 self.prepare_p4_only =False1010 self.conflict_behavior =None1011 self.isWindows = (platform.system() =="Windows")1012 self.exportLabels =False1013 self.p4HasMoveCommand =p4_has_move_command()1014 self.branch =None10151016defcheck(self):1017iflen(p4CmdList("opened ...")) >0:1018die("You have files opened with perforce! Close them before starting the sync.")10191020defseparate_jobs_from_description(self, message):1021"""Extract and return a possible Jobs field in the commit1022 message. It goes into a separate section in the p4 change1023 specification.10241025 A jobs line starts with "Jobs:" and looks like a new field1026 in a form. Values are white-space separated on the same1027 line or on following lines that start with a tab.10281029 This does not parse and extract the full git commit message1030 like a p4 form. It just sees the Jobs: line as a marker1031 to pass everything from then on directly into the p4 form,1032 but outside the description section.10331034 Return a tuple (stripped log message, jobs string)."""10351036 m = re.search(r'^Jobs:', message, re.MULTILINE)1037if m is None:1038return(message,None)10391040 jobtext = message[m.start():]1041 stripped_message = message[:m.start()].rstrip()1042return(stripped_message, jobtext)10431044defprepareLogMessage(self, template, message, jobs):1045"""Edits the template returned from "p4 change -o" to insert1046 the message in the Description field, and the jobs text in1047 the Jobs field."""1048 result =""10491050 inDescriptionSection =False10511052for line in template.split("\n"):1053if line.startswith("#"):1054 result += line +"\n"1055continue10561057if inDescriptionSection:1058if line.startswith("Files:")or line.startswith("Jobs:"):1059 inDescriptionSection =False1060# insert Jobs section1061if jobs:1062 result += jobs +"\n"1063else:1064continue1065else:1066if line.startswith("Description:"):1067 inDescriptionSection =True1068 line +="\n"1069for messageLine in message.split("\n"):1070 line +="\t"+ messageLine +"\n"10711072 result += line +"\n"10731074return result10751076defpatchRCSKeywords(self,file, pattern):1077# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1078(handle, outFileName) = tempfile.mkstemp(dir='.')1079try:1080 outFile = os.fdopen(handle,"w+")1081 inFile =open(file,"r")1082 regexp = re.compile(pattern, re.VERBOSE)1083for line in inFile.readlines():1084 line = regexp.sub(r'$\1$', line)1085 outFile.write(line)1086 inFile.close()1087 outFile.close()1088# Forcibly overwrite the original file1089 os.unlink(file)1090 shutil.move(outFileName,file)1091except:1092# cleanup our temporary file1093 os.unlink(outFileName)1094print"Failed to strip RCS keywords in%s"%file1095raise10961097print"Patched up RCS keywords in%s"%file10981099defp4UserForCommit(self,id):1100# Return the tuple (perforce user,git email) for a given git commit id1101 self.getUserMapFromPerforceServer()1102 gitEmail =read_pipe(["git","log","--max-count=1",1103"--format=%ae",id])1104 gitEmail = gitEmail.strip()1105if not self.emails.has_key(gitEmail):1106return(None,gitEmail)1107else:1108return(self.emails[gitEmail],gitEmail)11091110defcheckValidP4Users(self,commits):1111# check if any git authors cannot be mapped to p4 users1112foridin commits:1113(user,email) = self.p4UserForCommit(id)1114if not user:1115 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1116ifgitConfigBool("git-p4.allowMissingP4Users"):1117print"%s"% msg1118else:1119die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11201121deflastP4Changelist(self):1122# Get back the last changelist number submitted in this client spec. This1123# then gets used to patch up the username in the change. If the same1124# client spec is being used by multiple processes then this might go1125# wrong.1126 results =p4CmdList("client -o")# find the current client1127 client =None1128for r in results:1129if r.has_key('Client'):1130 client = r['Client']1131break1132if not client:1133die("could not get client spec")1134 results =p4CmdList(["changes","-c", client,"-m","1"])1135for r in results:1136if r.has_key('change'):1137return r['change']1138die("Could not get changelist number for last submit - cannot patch up user details")11391140defmodifyChangelistUser(self, changelist, newUser):1141# fixup the user field of a changelist after it has been submitted.1142 changes =p4CmdList("change -o%s"% changelist)1143iflen(changes) !=1:1144die("Bad output from p4 change modifying%sto user%s"%1145(changelist, newUser))11461147 c = changes[0]1148if c['User'] == newUser:return# nothing to do1149 c['User'] = newUser1150input= marshal.dumps(c)11511152 result =p4CmdList("change -f -i", stdin=input)1153for r in result:1154if r.has_key('code'):1155if r['code'] =='error':1156die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1157if r.has_key('data'):1158print("Updated user field for changelist%sto%s"% (changelist, newUser))1159return1160die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11611162defcanChangeChangelists(self):1163# check to see if we have p4 admin or super-user permissions, either of1164# which are required to modify changelists.1165 results =p4CmdList(["protects", self.depotPath])1166for r in results:1167if r.has_key('perm'):1168if r['perm'] =='admin':1169return11170if r['perm'] =='super':1171return11172return011731174defprepareSubmitTemplate(self):1175"""Run "p4 change -o" to grab a change specification template.1176 This does not use "p4 -G", as it is nice to keep the submission1177 template in original order, since a human might edit it.11781179 Remove lines in the Files section that show changes to files1180 outside the depot path we're committing into."""11811182 template =""1183 inFilesSection =False1184for line inp4_read_pipe_lines(['change','-o']):1185if line.endswith("\r\n"):1186 line = line[:-2] +"\n"1187if inFilesSection:1188if line.startswith("\t"):1189# path starts and ends with a tab1190 path = line[1:]1191 lastTab = path.rfind("\t")1192if lastTab != -1:1193 path = path[:lastTab]1194if notp4PathStartsWith(path, self.depotPath):1195continue1196else:1197 inFilesSection =False1198else:1199if line.startswith("Files:"):1200 inFilesSection =True12011202 template += line12031204return template12051206defedit_template(self, template_file):1207"""Invoke the editor to let the user change the submission1208 message. Return true if okay to continue with the submit."""12091210# if configured to skip the editing part, just submit1211ifgitConfigBool("git-p4.skipSubmitEdit"):1212return True12131214# look at the modification time, to check later if the user saved1215# the file1216 mtime = os.stat(template_file).st_mtime12171218# invoke the editor1219if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1220 editor = os.environ.get("P4EDITOR")1221else:1222 editor =read_pipe("git var GIT_EDITOR").strip()1223system([editor, template_file])12241225# If the file was not saved, prompt to see if this patch should1226# be skipped. But skip this verification step if configured so.1227ifgitConfigBool("git-p4.skipSubmitEditCheck"):1228return True12291230# modification time updated means user saved the file1231if os.stat(template_file).st_mtime > mtime:1232return True12331234while True:1235 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1236if response =='y':1237return True1238if response =='n':1239return False12401241defget_diff_description(self, editedFiles):1242# diff1243if os.environ.has_key("P4DIFF"):1244del(os.environ["P4DIFF"])1245 diff =""1246for editedFile in editedFiles:1247 diff +=p4_read_pipe(['diff','-du',1248wildcard_encode(editedFile)])12491250# new file diff1251 newdiff =""1252for newFile in filesToAdd:1253 newdiff +="==== new file ====\n"1254 newdiff +="--- /dev/null\n"1255 newdiff +="+++%s\n"% newFile1256 f =open(newFile,"r")1257for line in f.readlines():1258 newdiff +="+"+ line1259 f.close()12601261return diff + newdiff12621263defapplyCommit(self,id):1264"""Apply one commit, return True if it succeeded."""12651266print"Applying",read_pipe(["git","show","-s",1267"--format=format:%h%s",id])12681269(p4User, gitEmail) = self.p4UserForCommit(id)12701271 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1272 filesToAdd =set()1273 filesToDelete =set()1274 editedFiles =set()1275 pureRenameCopy =set()1276 filesToChangeExecBit = {}12771278for line in diff:1279 diff =parseDiffTreeEntry(line)1280 modifier = diff['status']1281 path = diff['src']1282if modifier =="M":1283p4_edit(path)1284ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1285 filesToChangeExecBit[path] = diff['dst_mode']1286 editedFiles.add(path)1287elif modifier =="A":1288 filesToAdd.add(path)1289 filesToChangeExecBit[path] = diff['dst_mode']1290if path in filesToDelete:1291 filesToDelete.remove(path)1292elif modifier =="D":1293 filesToDelete.add(path)1294if path in filesToAdd:1295 filesToAdd.remove(path)1296elif modifier =="C":1297 src, dest = diff['src'], diff['dst']1298p4_integrate(src, dest)1299 pureRenameCopy.add(dest)1300if diff['src_sha1'] != diff['dst_sha1']:1301p4_edit(dest)1302 pureRenameCopy.discard(dest)1303ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1304p4_edit(dest)1305 pureRenameCopy.discard(dest)1306 filesToChangeExecBit[dest] = diff['dst_mode']1307if self.isWindows:1308# turn off read-only attribute1309 os.chmod(dest, stat.S_IWRITE)1310 os.unlink(dest)1311 editedFiles.add(dest)1312elif modifier =="R":1313 src, dest = diff['src'], diff['dst']1314if self.p4HasMoveCommand:1315p4_edit(src)# src must be open before move1316p4_move(src, dest)# opens for (move/delete, move/add)1317else:1318p4_integrate(src, dest)1319if diff['src_sha1'] != diff['dst_sha1']:1320p4_edit(dest)1321else:1322 pureRenameCopy.add(dest)1323ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1324if not self.p4HasMoveCommand:1325p4_edit(dest)# with move: already open, writable1326 filesToChangeExecBit[dest] = diff['dst_mode']1327if not self.p4HasMoveCommand:1328if self.isWindows:1329 os.chmod(dest, stat.S_IWRITE)1330 os.unlink(dest)1331 filesToDelete.add(src)1332 editedFiles.add(dest)1333else:1334die("unknown modifier%sfor%s"% (modifier, path))13351336 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1337 patchcmd = diffcmd +" | git apply "1338 tryPatchCmd = patchcmd +"--check -"1339 applyPatchCmd = patchcmd +"--check --apply -"1340 patch_succeeded =True13411342if os.system(tryPatchCmd) !=0:1343 fixed_rcs_keywords =False1344 patch_succeeded =False1345print"Unfortunately applying the change failed!"13461347# Patch failed, maybe it's just RCS keyword woes. Look through1348# the patch to see if that's possible.1349ifgitConfigBool("git-p4.attemptRCSCleanup"):1350file=None1351 pattern =None1352 kwfiles = {}1353forfilein editedFiles | filesToDelete:1354# did this file's delta contain RCS keywords?1355 pattern =p4_keywords_regexp_for_file(file)13561357if pattern:1358# this file is a possibility...look for RCS keywords.1359 regexp = re.compile(pattern, re.VERBOSE)1360for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1361if regexp.search(line):1362if verbose:1363print"got keyword match on%sin%sin%s"% (pattern, line,file)1364 kwfiles[file] = pattern1365break13661367forfilein kwfiles:1368if verbose:1369print"zapping%swith%s"% (line,pattern)1370# File is being deleted, so not open in p4. Must1371# disable the read-only bit on windows.1372if self.isWindows andfilenot in editedFiles:1373 os.chmod(file, stat.S_IWRITE)1374 self.patchRCSKeywords(file, kwfiles[file])1375 fixed_rcs_keywords =True13761377if fixed_rcs_keywords:1378print"Retrying the patch with RCS keywords cleaned up"1379if os.system(tryPatchCmd) ==0:1380 patch_succeeded =True13811382if not patch_succeeded:1383for f in editedFiles:1384p4_revert(f)1385return False13861387#1388# Apply the patch for real, and do add/delete/+x handling.1389#1390system(applyPatchCmd)13911392for f in filesToAdd:1393p4_add(f)1394for f in filesToDelete:1395p4_revert(f)1396p4_delete(f)13971398# Set/clear executable bits1399for f in filesToChangeExecBit.keys():1400 mode = filesToChangeExecBit[f]1401setP4ExecBit(f, mode)14021403#1404# Build p4 change description, starting with the contents1405# of the git commit message.1406#1407 logMessage =extractLogMessageFromGitCommit(id)1408 logMessage = logMessage.strip()1409(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14101411 template = self.prepareSubmitTemplate()1412 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14131414if self.preserveUser:1415 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14161417if self.checkAuthorship and not self.p4UserIsMe(p4User):1418 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1419 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1420 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"14211422 separatorLine ="######## everything below this line is just the diff #######\n"1423if not self.prepare_p4_only:1424 submitTemplate += separatorLine1425 submitTemplate += self.get_diff_description(editedFiles)14261427(handle, fileName) = tempfile.mkstemp()1428 tmpFile = os.fdopen(handle,"w+")1429if self.isWindows:1430 submitTemplate = submitTemplate.replace("\n","\r\n")1431 tmpFile.write(submitTemplate)1432 tmpFile.close()14331434if self.prepare_p4_only:1435#1436# Leave the p4 tree prepared, and the submit template around1437# and let the user decide what to do next1438#1439print1440print"P4 workspace prepared for submission."1441print"To submit or revert, go to client workspace"1442print" "+ self.clientPath1443print1444print"To submit, use\"p4 submit\"to write a new description,"1445print"or\"p4 submit -i%s\"to use the one prepared by" \1446"\"git p4\"."% fileName1447print"You can delete the file\"%s\"when finished."% fileName14481449if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1450print"To preserve change ownership by user%s, you must\n" \1451"do\"p4 change -f <change>\"after submitting and\n" \1452"edit the User field."1453if pureRenameCopy:1454print"After submitting, renamed files must be re-synced."1455print"Invoke\"p4 sync -f\"on each of these files:"1456for f in pureRenameCopy:1457print" "+ f14581459print1460print"To revert the changes, use\"p4 revert ...\", and delete"1461print"the submit template file\"%s\""% fileName1462if filesToAdd:1463print"Since the commit adds new files, they must be deleted:"1464for f in filesToAdd:1465print" "+ f1466print1467return True14681469#1470# Let the user edit the change description, then submit it.1471#1472if self.edit_template(fileName):1473# read the edited message and submit1474 ret =True1475 tmpFile =open(fileName,"rb")1476 message = tmpFile.read()1477 tmpFile.close()1478 submitTemplate = message[:message.index(separatorLine)]1479if self.isWindows:1480 submitTemplate = submitTemplate.replace("\r\n","\n")1481p4_write_pipe(['submit','-i'], submitTemplate)14821483if self.preserveUser:1484if p4User:1485# Get last changelist number. Cannot easily get it from1486# the submit command output as the output is1487# unmarshalled.1488 changelist = self.lastP4Changelist()1489 self.modifyChangelistUser(changelist, p4User)14901491# The rename/copy happened by applying a patch that created a1492# new file. This leaves it writable, which confuses p4.1493for f in pureRenameCopy:1494p4_sync(f,"-f")14951496else:1497# skip this patch1498 ret =False1499print"Submission cancelled, undoing p4 changes."1500for f in editedFiles:1501p4_revert(f)1502for f in filesToAdd:1503p4_revert(f)1504 os.remove(f)1505for f in filesToDelete:1506p4_revert(f)15071508 os.remove(fileName)1509return ret15101511# Export git tags as p4 labels. Create a p4 label and then tag1512# with that.1513defexportGitTags(self, gitTags):1514 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1515iflen(validLabelRegexp) ==0:1516 validLabelRegexp = defaultLabelRegexp1517 m = re.compile(validLabelRegexp)15181519for name in gitTags:15201521if not m.match(name):1522if verbose:1523print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1524continue15251526# Get the p4 commit this corresponds to1527 logMessage =extractLogMessageFromGitCommit(name)1528 values =extractSettingsGitLog(logMessage)15291530if not values.has_key('change'):1531# a tag pointing to something not sent to p4; ignore1532if verbose:1533print"git tag%sdoes not give a p4 commit"% name1534continue1535else:1536 changelist = values['change']15371538# Get the tag details.1539 inHeader =True1540 isAnnotated =False1541 body = []1542for l inread_pipe_lines(["git","cat-file","-p", name]):1543 l = l.strip()1544if inHeader:1545if re.match(r'tag\s+', l):1546 isAnnotated =True1547elif re.match(r'\s*$', l):1548 inHeader =False1549continue1550else:1551 body.append(l)15521553if not isAnnotated:1554 body = ["lightweight tag imported by git p4\n"]15551556# Create the label - use the same view as the client spec we are using1557 clientSpec =getClientSpec()15581559 labelTemplate ="Label:%s\n"% name1560 labelTemplate +="Description:\n"1561for b in body:1562 labelTemplate +="\t"+ b +"\n"1563 labelTemplate +="View:\n"1564for depot_side in clientSpec.mappings:1565 labelTemplate +="\t%s\n"% depot_side15661567if self.dry_run:1568print"Would create p4 label%sfor tag"% name1569elif self.prepare_p4_only:1570print"Not creating p4 label%sfor tag due to option" \1571" --prepare-p4-only"% name1572else:1573p4_write_pipe(["label","-i"], labelTemplate)15741575# Use the label1576p4_system(["tag","-l", name] +1577["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])15781579if verbose:1580print"created p4 label for tag%s"% name15811582defrun(self, args):1583iflen(args) ==0:1584 self.master =currentGitBranch()1585iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1586die("Detecting current git branch failed!")1587eliflen(args) ==1:1588 self.master = args[0]1589if notbranchExists(self.master):1590die("Branch%sdoes not exist"% self.master)1591else:1592return False15931594 allowSubmit =gitConfig("git-p4.allowSubmit")1595iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1596die("%sis not in git-p4.allowSubmit"% self.master)15971598[upstream, settings] =findUpstreamBranchPoint()1599 self.depotPath = settings['depot-paths'][0]1600iflen(self.origin) ==0:1601 self.origin = upstream16021603if self.preserveUser:1604if not self.canChangeChangelists():1605die("Cannot preserve user names without p4 super-user or admin permissions")16061607# if not set from the command line, try the config file1608if self.conflict_behavior is None:1609 val =gitConfig("git-p4.conflict")1610if val:1611if val not in self.conflict_behavior_choices:1612die("Invalid value '%s' for config git-p4.conflict"% val)1613else:1614 val ="ask"1615 self.conflict_behavior = val16161617if self.verbose:1618print"Origin branch is "+ self.origin16191620iflen(self.depotPath) ==0:1621print"Internal error: cannot locate perforce depot path from existing branches"1622 sys.exit(128)16231624 self.useClientSpec =False1625ifgitConfigBool("git-p4.useclientspec"):1626 self.useClientSpec =True1627if self.useClientSpec:1628 self.clientSpecDirs =getClientSpec()16291630if self.useClientSpec:1631# all files are relative to the client spec1632 self.clientPath =getClientRoot()1633else:1634 self.clientPath =p4Where(self.depotPath)16351636if self.clientPath =="":1637die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)16381639print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1640 self.oldWorkingDirectory = os.getcwd()16411642# ensure the clientPath exists1643 new_client_dir =False1644if not os.path.exists(self.clientPath):1645 new_client_dir =True1646 os.makedirs(self.clientPath)16471648chdir(self.clientPath, is_client_path=True)1649if self.dry_run:1650print"Would synchronize p4 checkout in%s"% self.clientPath1651else:1652print"Synchronizing p4 checkout..."1653if new_client_dir:1654# old one was destroyed, and maybe nobody told p41655p4_sync("...","-f")1656else:1657p4_sync("...")1658 self.check()16591660 commits = []1661for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1662 commits.append(line.strip())1663 commits.reverse()16641665if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1666 self.checkAuthorship =False1667else:1668 self.checkAuthorship =True16691670if self.preserveUser:1671 self.checkValidP4Users(commits)16721673#1674# Build up a set of options to be passed to diff when1675# submitting each commit to p4.1676#1677if self.detectRenames:1678# command-line -M arg1679 self.diffOpts ="-M"1680else:1681# If not explicitly set check the config variable1682 detectRenames =gitConfig("git-p4.detectRenames")16831684if detectRenames.lower() =="false"or detectRenames =="":1685 self.diffOpts =""1686elif detectRenames.lower() =="true":1687 self.diffOpts ="-M"1688else:1689 self.diffOpts ="-M%s"% detectRenames16901691# no command-line arg for -C or --find-copies-harder, just1692# config variables1693 detectCopies =gitConfig("git-p4.detectCopies")1694if detectCopies.lower() =="false"or detectCopies =="":1695pass1696elif detectCopies.lower() =="true":1697 self.diffOpts +=" -C"1698else:1699 self.diffOpts +=" -C%s"% detectCopies17001701ifgitConfigBool("git-p4.detectCopiesHarder"):1702 self.diffOpts +=" --find-copies-harder"17031704#1705# Apply the commits, one at a time. On failure, ask if should1706# continue to try the rest of the patches, or quit.1707#1708if self.dry_run:1709print"Would apply"1710 applied = []1711 last =len(commits) -11712for i, commit inenumerate(commits):1713if self.dry_run:1714print" ",read_pipe(["git","show","-s",1715"--format=format:%h%s", commit])1716 ok =True1717else:1718 ok = self.applyCommit(commit)1719if ok:1720 applied.append(commit)1721else:1722if self.prepare_p4_only and i < last:1723print"Processing only the first commit due to option" \1724" --prepare-p4-only"1725break1726if i < last:1727 quit =False1728while True:1729# prompt for what to do, or use the option/variable1730if self.conflict_behavior =="ask":1731print"What do you want to do?"1732 response =raw_input("[s]kip this commit but apply"1733" the rest, or [q]uit? ")1734if not response:1735continue1736elif self.conflict_behavior =="skip":1737 response ="s"1738elif self.conflict_behavior =="quit":1739 response ="q"1740else:1741die("Unknown conflict_behavior '%s'"%1742 self.conflict_behavior)17431744if response[0] =="s":1745print"Skipping this commit, but applying the rest"1746break1747if response[0] =="q":1748print"Quitting"1749 quit =True1750break1751if quit:1752break17531754chdir(self.oldWorkingDirectory)17551756if self.dry_run:1757pass1758elif self.prepare_p4_only:1759pass1760eliflen(commits) ==len(applied):1761print"All commits applied!"17621763 sync =P4Sync()1764if self.branch:1765 sync.branch = self.branch1766 sync.run([])17671768 rebase =P4Rebase()1769 rebase.rebase()17701771else:1772iflen(applied) ==0:1773print"No commits applied."1774else:1775print"Applied only the commits marked with '*':"1776for c in commits:1777if c in applied:1778 star ="*"1779else:1780 star =" "1781print star,read_pipe(["git","show","-s",1782"--format=format:%h%s", c])1783print"You will have to do 'git p4 sync' and rebase."17841785ifgitConfigBool("git-p4.exportLabels"):1786 self.exportLabels =True17871788if self.exportLabels:1789 p4Labels =getP4Labels(self.depotPath)1790 gitTags =getGitTags()17911792 missingGitTags = gitTags - p4Labels1793 self.exportGitTags(missingGitTags)17941795# exit with error unless everything applied perfectly1796iflen(commits) !=len(applied):1797 sys.exit(1)17981799return True18001801classView(object):1802"""Represent a p4 view ("p4 help views"), and map files in a1803 repo according to the view."""18041805def__init__(self, client_name):1806 self.mappings = []1807 self.client_prefix ="//%s/"% client_name1808# cache results of "p4 where" to lookup client file locations1809 self.client_spec_path_cache = {}18101811defappend(self, view_line):1812"""Parse a view line, splitting it into depot and client1813 sides. Append to self.mappings, preserving order. This1814 is only needed for tag creation."""18151816# Split the view line into exactly two words. P4 enforces1817# structure on these lines that simplifies this quite a bit.1818#1819# Either or both words may be double-quoted.1820# Single quotes do not matter.1821# Double-quote marks cannot occur inside the words.1822# A + or - prefix is also inside the quotes.1823# There are no quotes unless they contain a space.1824# The line is already white-space stripped.1825# The two words are separated by a single space.1826#1827if view_line[0] =='"':1828# First word is double quoted. Find its end.1829 close_quote_index = view_line.find('"',1)1830if close_quote_index <=0:1831die("No first-word closing quote found:%s"% view_line)1832 depot_side = view_line[1:close_quote_index]1833# skip closing quote and space1834 rhs_index = close_quote_index +1+11835else:1836 space_index = view_line.find(" ")1837if space_index <=0:1838die("No word-splitting space found:%s"% view_line)1839 depot_side = view_line[0:space_index]1840 rhs_index = space_index +118411842# prefix + means overlay on previous mapping1843if depot_side.startswith("+"):1844 depot_side = depot_side[1:]18451846# prefix - means exclude this path, leave out of mappings1847 exclude =False1848if depot_side.startswith("-"):1849 exclude =True1850 depot_side = depot_side[1:]18511852if not exclude:1853 self.mappings.append(depot_side)18541855defconvert_client_path(self, clientFile):1856# chop off //client/ part to make it relative1857if not clientFile.startswith(self.client_prefix):1858die("No prefix '%s' on clientFile '%s'"%1859(self.client_prefix, clientFile))1860return clientFile[len(self.client_prefix):]18611862defupdate_client_spec_path_cache(self, files):1863""" Caching file paths by "p4 where" batch query """18641865# List depot file paths exclude that already cached1866 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]18671868iflen(fileArgs) ==0:1869return# All files in cache18701871 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1872for res in where_result:1873if"code"in res and res["code"] =="error":1874# assume error is "... file(s) not in client view"1875continue1876if"clientFile"not in res:1877die("No clientFile in 'p4 where' output")1878if"unmap"in res:1879# it will list all of them, but only one not unmap-ped1880continue1881 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])18821883# not found files or unmap files set to ""1884for depotFile in fileArgs:1885if depotFile not in self.client_spec_path_cache:1886 self.client_spec_path_cache[depotFile] =""18871888defmap_in_client(self, depot_path):1889"""Return the relative location in the client where this1890 depot file should live. Returns "" if the file should1891 not be mapped in the client."""18921893if depot_path in self.client_spec_path_cache:1894return self.client_spec_path_cache[depot_path]18951896die("Error:%sis not found in client spec path"% depot_path )1897return""18981899classP4Sync(Command, P4UserMap):1900 delete_actions = ("delete","move/delete","purge")19011902def__init__(self):1903 Command.__init__(self)1904 P4UserMap.__init__(self)1905 self.options = [1906 optparse.make_option("--branch", dest="branch"),1907 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1908 optparse.make_option("--changesfile", dest="changesFile"),1909 optparse.make_option("--silent", dest="silent", action="store_true"),1910 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1911 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1912 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1913help="Import into refs/heads/ , not refs/remotes"),1914 optparse.make_option("--max-changes", dest="maxChanges"),1915 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1916help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1917 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1918help="Only sync files that are included in the Perforce Client Spec")1919]1920 self.description ="""Imports from Perforce into a git repository.\n1921 example:1922 //depot/my/project/ -- to import the current head1923 //depot/my/project/@all -- to import everything1924 //depot/my/project/@1,6 -- to import only from revision 1 to 619251926 (a ... is not needed in the path p4 specification, it's added implicitly)"""19271928 self.usage +=" //depot/path[@revRange]"1929 self.silent =False1930 self.createdBranches =set()1931 self.committedChanges =set()1932 self.branch =""1933 self.detectBranches =False1934 self.detectLabels =False1935 self.importLabels =False1936 self.changesFile =""1937 self.syncWithOrigin =True1938 self.importIntoRemotes =True1939 self.maxChanges =""1940 self.keepRepoPath =False1941 self.depotPaths =None1942 self.p4BranchesInGit = []1943 self.cloneExclude = []1944 self.useClientSpec =False1945 self.useClientSpec_from_options =False1946 self.clientSpecDirs =None1947 self.tempBranches = []1948 self.tempBranchLocation ="git-p4-tmp"19491950ifgitConfig("git-p4.syncFromOrigin") =="false":1951 self.syncWithOrigin =False19521953# Force a checkpoint in fast-import and wait for it to finish1954defcheckpoint(self):1955 self.gitStream.write("checkpoint\n\n")1956 self.gitStream.write("progress checkpoint\n\n")1957 out = self.gitOutput.readline()1958if self.verbose:1959print"checkpoint finished: "+ out19601961defextractFilesFromCommit(self, commit):1962 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1963for path in self.cloneExclude]1964 files = []1965 fnum =01966while commit.has_key("depotFile%s"% fnum):1967 path = commit["depotFile%s"% fnum]19681969if[p for p in self.cloneExclude1970ifp4PathStartsWith(path, p)]:1971 found =False1972else:1973 found = [p for p in self.depotPaths1974ifp4PathStartsWith(path, p)]1975if not found:1976 fnum = fnum +11977continue19781979file= {}1980file["path"] = path1981file["rev"] = commit["rev%s"% fnum]1982file["action"] = commit["action%s"% fnum]1983file["type"] = commit["type%s"% fnum]1984 files.append(file)1985 fnum = fnum +11986return files19871988defstripRepoPath(self, path, prefixes):1989"""When streaming files, this is called to map a p4 depot path1990 to where it should go in git. The prefixes are either1991 self.depotPaths, or self.branchPrefixes in the case of1992 branch detection."""19931994if self.useClientSpec:1995# branch detection moves files up a level (the branch name)1996# from what client spec interpretation gives1997 path = self.clientSpecDirs.map_in_client(path)1998if self.detectBranches:1999for b in self.knownBranches:2000if path.startswith(b +"/"):2001 path = path[len(b)+1:]20022003elif self.keepRepoPath:2004# Preserve everything in relative path name except leading2005# //depot/; just look at first prefix as they all should2006# be in the same depot.2007 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2008ifp4PathStartsWith(path, depot):2009 path = path[len(depot):]20102011else:2012for p in prefixes:2013ifp4PathStartsWith(path, p):2014 path = path[len(p):]2015break20162017 path =wildcard_decode(path)2018return path20192020defsplitFilesIntoBranches(self, commit):2021"""Look at each depotFile in the commit to figure out to what2022 branch it belongs."""20232024if self.clientSpecDirs:2025 files = self.extractFilesFromCommit(commit)2026 self.clientSpecDirs.update_client_spec_path_cache(files)20272028 branches = {}2029 fnum =02030while commit.has_key("depotFile%s"% fnum):2031 path = commit["depotFile%s"% fnum]2032 found = [p for p in self.depotPaths2033ifp4PathStartsWith(path, p)]2034if not found:2035 fnum = fnum +12036continue20372038file= {}2039file["path"] = path2040file["rev"] = commit["rev%s"% fnum]2041file["action"] = commit["action%s"% fnum]2042file["type"] = commit["type%s"% fnum]2043 fnum = fnum +120442045# start with the full relative path where this file would2046# go in a p4 client2047if self.useClientSpec:2048 relPath = self.clientSpecDirs.map_in_client(path)2049else:2050 relPath = self.stripRepoPath(path, self.depotPaths)20512052for branch in self.knownBranches.keys():2053# add a trailing slash so that a commit into qt/4.2foo2054# doesn't end up in qt/4.2, e.g.2055if relPath.startswith(branch +"/"):2056if branch not in branches:2057 branches[branch] = []2058 branches[branch].append(file)2059break20602061return branches20622063# output one file from the P4 stream2064# - helper for streamP4Files20652066defstreamOneP4File(self,file, contents):2067 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2068if verbose:2069 sys.stderr.write("%s\n"% relPath)20702071(type_base, type_mods) =split_p4_type(file["type"])20722073 git_mode ="100644"2074if"x"in type_mods:2075 git_mode ="100755"2076if type_base =="symlink":2077 git_mode ="120000"2078# p4 print on a symlink sometimes contains "target\n";2079# if it does, remove the newline2080 data =''.join(contents)2081if not data:2082# Some version of p4 allowed creating a symlink that pointed2083# to nothing. This causes p4 errors when checking out such2084# a change, and errors here too. Work around it by ignoring2085# the bad symlink; hopefully a future change fixes it.2086print"\nIgnoring empty symlink in%s"%file['depotFile']2087return2088elif data[-1] =='\n':2089 contents = [data[:-1]]2090else:2091 contents = [data]20922093if type_base =="utf16":2094# p4 delivers different text in the python output to -G2095# than it does when using "print -o", or normal p4 client2096# operations. utf16 is converted to ascii or utf8, perhaps.2097# But ascii text saved as -t utf16 is completely mangled.2098# Invoke print -o to get the real contents.2099#2100# On windows, the newlines will always be mangled by print, so put2101# them back too. This is not needed to the cygwin windows version,2102# just the native "NT" type.2103#2104 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2105ifp4_version_string().find("/NT") >=0:2106 text = text.replace("\r\n","\n")2107 contents = [ text ]21082109if type_base =="apple":2110# Apple filetype files will be streamed as a concatenation of2111# its appledouble header and the contents. This is useless2112# on both macs and non-macs. If using "print -q -o xx", it2113# will create "xx" with the data, and "%xx" with the header.2114# This is also not very useful.2115#2116# Ideally, someday, this script can learn how to generate2117# appledouble files directly and import those to git, but2118# non-mac machines can never find a use for apple filetype.2119print"\nIgnoring apple filetype file%s"%file['depotFile']2120return21212122# Note that we do not try to de-mangle keywords on utf16 files,2123# even though in theory somebody may want that.2124 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2125if pattern:2126 regexp = re.compile(pattern, re.VERBOSE)2127 text =''.join(contents)2128 text = regexp.sub(r'$\1$', text)2129 contents = [ text ]21302131 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21322133# total length...2134 length =02135for d in contents:2136 length = length +len(d)21372138 self.gitStream.write("data%d\n"% length)2139for d in contents:2140 self.gitStream.write(d)2141 self.gitStream.write("\n")21422143defstreamOneP4Deletion(self,file):2144 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2145if verbose:2146 sys.stderr.write("delete%s\n"% relPath)2147 self.gitStream.write("D%s\n"% relPath)21482149# handle another chunk of streaming data2150defstreamP4FilesCb(self, marshalled):21512152# catch p4 errors and complain2153 err =None2154if"code"in marshalled:2155if marshalled["code"] =="error":2156if"data"in marshalled:2157 err = marshalled["data"].rstrip()2158if err:2159 f =None2160if self.stream_have_file_info:2161if"depotFile"in self.stream_file:2162 f = self.stream_file["depotFile"]2163# force a failure in fast-import, else an empty2164# commit will be made2165 self.gitStream.write("\n")2166 self.gitStream.write("die-now\n")2167 self.gitStream.close()2168# ignore errors, but make sure it exits first2169 self.importProcess.wait()2170if f:2171die("Error from p4 print for%s:%s"% (f, err))2172else:2173die("Error from p4 print:%s"% err)21742175if marshalled.has_key('depotFile')and self.stream_have_file_info:2176# start of a new file - output the old one first2177 self.streamOneP4File(self.stream_file, self.stream_contents)2178 self.stream_file = {}2179 self.stream_contents = []2180 self.stream_have_file_info =False21812182# pick up the new file information... for the2183# 'data' field we need to append to our array2184for k in marshalled.keys():2185if k =='data':2186 self.stream_contents.append(marshalled['data'])2187else:2188 self.stream_file[k] = marshalled[k]21892190 self.stream_have_file_info =True21912192# Stream directly from "p4 files" into "git fast-import"2193defstreamP4Files(self, files):2194 filesForCommit = []2195 filesToRead = []2196 filesToDelete = []21972198for f in files:2199# if using a client spec, only add the files that have2200# a path in the client2201if self.clientSpecDirs:2202if self.clientSpecDirs.map_in_client(f['path']) =="":2203continue22042205 filesForCommit.append(f)2206if f['action']in self.delete_actions:2207 filesToDelete.append(f)2208else:2209 filesToRead.append(f)22102211# deleted files...2212for f in filesToDelete:2213 self.streamOneP4Deletion(f)22142215iflen(filesToRead) >0:2216 self.stream_file = {}2217 self.stream_contents = []2218 self.stream_have_file_info =False22192220# curry self argument2221defstreamP4FilesCbSelf(entry):2222 self.streamP4FilesCb(entry)22232224 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22252226p4CmdList(["-x","-","print"],2227 stdin=fileArgs,2228 cb=streamP4FilesCbSelf)22292230# do the last chunk2231if self.stream_file.has_key('depotFile'):2232 self.streamOneP4File(self.stream_file, self.stream_contents)22332234defmake_email(self, userid):2235if userid in self.users:2236return self.users[userid]2237else:2238return"%s<a@b>"% userid22392240# Stream a p4 tag2241defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2242if verbose:2243print"writing tag%sfor commit%s"% (labelName, commit)2244 gitStream.write("tag%s\n"% labelName)2245 gitStream.write("from%s\n"% commit)22462247if labelDetails.has_key('Owner'):2248 owner = labelDetails["Owner"]2249else:2250 owner =None22512252# Try to use the owner of the p4 label, or failing that,2253# the current p4 user id.2254if owner:2255 email = self.make_email(owner)2256else:2257 email = self.make_email(self.p4UserId())2258 tagger ="%s %s %s"% (email, epoch, self.tz)22592260 gitStream.write("tagger%s\n"% tagger)22612262print"labelDetails=",labelDetails2263if labelDetails.has_key('Description'):2264 description = labelDetails['Description']2265else:2266 description ='Label from git p4'22672268 gitStream.write("data%d\n"%len(description))2269 gitStream.write(description)2270 gitStream.write("\n")22712272defcommit(self, details, files, branch, parent =""):2273 epoch = details["time"]2274 author = details["user"]22752276if self.verbose:2277print"commit into%s"% branch22782279# start with reading files; if that fails, we should not2280# create a commit.2281 new_files = []2282for f in files:2283if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2284 new_files.append(f)2285else:2286 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])22872288if self.clientSpecDirs:2289 self.clientSpecDirs.update_client_spec_path_cache(files)22902291 self.gitStream.write("commit%s\n"% branch)2292# gitStream.write("mark :%s\n" % details["change"])2293 self.committedChanges.add(int(details["change"]))2294 committer =""2295if author not in self.users:2296 self.getUserMapFromPerforceServer()2297 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)22982299 self.gitStream.write("committer%s\n"% committer)23002301 self.gitStream.write("data <<EOT\n")2302 self.gitStream.write(details["desc"])2303 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2304(','.join(self.branchPrefixes), details["change"]))2305iflen(details['options']) >0:2306 self.gitStream.write(": options =%s"% details['options'])2307 self.gitStream.write("]\nEOT\n\n")23082309iflen(parent) >0:2310if self.verbose:2311print"parent%s"% parent2312 self.gitStream.write("from%s\n"% parent)23132314 self.streamP4Files(new_files)2315 self.gitStream.write("\n")23162317 change =int(details["change"])23182319if self.labels.has_key(change):2320 label = self.labels[change]2321 labelDetails = label[0]2322 labelRevisions = label[1]2323if self.verbose:2324print"Change%sis labelled%s"% (change, labelDetails)23252326 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2327for p in self.branchPrefixes])23282329iflen(files) ==len(labelRevisions):23302331 cleanedFiles = {}2332for info in files:2333if info["action"]in self.delete_actions:2334continue2335 cleanedFiles[info["depotFile"]] = info["rev"]23362337if cleanedFiles == labelRevisions:2338 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23392340else:2341if not self.silent:2342print("Tag%sdoes not match with change%s: files do not match."2343% (labelDetails["label"], change))23442345else:2346if not self.silent:2347print("Tag%sdoes not match with change%s: file count is different."2348% (labelDetails["label"], change))23492350# Build a dictionary of changelists and labels, for "detect-labels" option.2351defgetLabels(self):2352 self.labels = {}23532354 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2355iflen(l) >0and not self.silent:2356print"Finding files belonging to labels in%s"% `self.depotPaths`23572358for output in l:2359 label = output["label"]2360 revisions = {}2361 newestChange =02362if self.verbose:2363print"Querying files for label%s"% label2364forfileinp4CmdList(["files"] +2365["%s...@%s"% (p, label)2366for p in self.depotPaths]):2367 revisions[file["depotFile"]] =file["rev"]2368 change =int(file["change"])2369if change > newestChange:2370 newestChange = change23712372 self.labels[newestChange] = [output, revisions]23732374if self.verbose:2375print"Label changes:%s"% self.labels.keys()23762377# Import p4 labels as git tags. A direct mapping does not2378# exist, so assume that if all the files are at the same revision2379# then we can use that, or it's something more complicated we should2380# just ignore.2381defimportP4Labels(self, stream, p4Labels):2382if verbose:2383print"import p4 labels: "+' '.join(p4Labels)23842385 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2386 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2387iflen(validLabelRegexp) ==0:2388 validLabelRegexp = defaultLabelRegexp2389 m = re.compile(validLabelRegexp)23902391for name in p4Labels:2392 commitFound =False23932394if not m.match(name):2395if verbose:2396print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2397continue23982399if name in ignoredP4Labels:2400continue24012402 labelDetails =p4CmdList(['label',"-o", name])[0]24032404# get the most recent changelist for each file in this label2405 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2406for p in self.depotPaths])24072408if change.has_key('change'):2409# find the corresponding git commit; take the oldest commit2410 changelist =int(change['change'])2411 gitCommit =read_pipe(["git","rev-list","--max-count=1",2412"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2413iflen(gitCommit) ==0:2414print"could not find git commit for changelist%d"% changelist2415else:2416 gitCommit = gitCommit.strip()2417 commitFound =True2418# Convert from p4 time format2419try:2420 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2421exceptValueError:2422print"Could not convert label time%s"% labelDetails['Update']2423 tmwhen =124242425 when =int(time.mktime(tmwhen))2426 self.streamTag(stream, name, labelDetails, gitCommit, when)2427if verbose:2428print"p4 label%smapped to git commit%s"% (name, gitCommit)2429else:2430if verbose:2431print"Label%shas no changelists - possibly deleted?"% name24322433if not commitFound:2434# We can't import this label; don't try again as it will get very2435# expensive repeatedly fetching all the files for labels that will2436# never be imported. If the label is moved in the future, the2437# ignore will need to be removed manually.2438system(["git","config","--add","git-p4.ignoredP4Labels", name])24392440defguessProjectName(self):2441for p in self.depotPaths:2442if p.endswith("/"):2443 p = p[:-1]2444 p = p[p.strip().rfind("/") +1:]2445if not p.endswith("/"):2446 p +="/"2447return p24482449defgetBranchMapping(self):2450 lostAndFoundBranches =set()24512452 user =gitConfig("git-p4.branchUser")2453iflen(user) >0:2454 command ="branches -u%s"% user2455else:2456 command ="branches"24572458for info inp4CmdList(command):2459 details =p4Cmd(["branch","-o", info["branch"]])2460 viewIdx =02461while details.has_key("View%s"% viewIdx):2462 paths = details["View%s"% viewIdx].split(" ")2463 viewIdx = viewIdx +12464# require standard //depot/foo/... //depot/bar/... mapping2465iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2466continue2467 source = paths[0]2468 destination = paths[1]2469## HACK2470ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2471 source = source[len(self.depotPaths[0]):-4]2472 destination = destination[len(self.depotPaths[0]):-4]24732474if destination in self.knownBranches:2475if not self.silent:2476print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2477print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2478continue24792480 self.knownBranches[destination] = source24812482 lostAndFoundBranches.discard(destination)24832484if source not in self.knownBranches:2485 lostAndFoundBranches.add(source)24862487# Perforce does not strictly require branches to be defined, so we also2488# check git config for a branch list.2489#2490# Example of branch definition in git config file:2491# [git-p4]2492# branchList=main:branchA2493# branchList=main:branchB2494# branchList=branchA:branchC2495 configBranches =gitConfigList("git-p4.branchList")2496for branch in configBranches:2497if branch:2498(source, destination) = branch.split(":")2499 self.knownBranches[destination] = source25002501 lostAndFoundBranches.discard(destination)25022503if source not in self.knownBranches:2504 lostAndFoundBranches.add(source)250525062507for branch in lostAndFoundBranches:2508 self.knownBranches[branch] = branch25092510defgetBranchMappingFromGitBranches(self):2511 branches =p4BranchesInGit(self.importIntoRemotes)2512for branch in branches.keys():2513if branch =="master":2514 branch ="main"2515else:2516 branch = branch[len(self.projectName):]2517 self.knownBranches[branch] = branch25182519defupdateOptionDict(self, d):2520 option_keys = {}2521if self.keepRepoPath:2522 option_keys['keepRepoPath'] =125232524 d["options"] =' '.join(sorted(option_keys.keys()))25252526defreadOptions(self, d):2527 self.keepRepoPath = (d.has_key('options')2528and('keepRepoPath'in d['options']))25292530defgitRefForBranch(self, branch):2531if branch =="main":2532return self.refPrefix +"master"25332534iflen(branch) <=0:2535return branch25362537return self.refPrefix + self.projectName + branch25382539defgitCommitByP4Change(self, ref, change):2540if self.verbose:2541print"looking in ref "+ ref +" for change%susing bisect..."% change25422543 earliestCommit =""2544 latestCommit =parseRevision(ref)25452546while True:2547if self.verbose:2548print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2549 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2550iflen(next) ==0:2551if self.verbose:2552print"argh"2553return""2554 log =extractLogMessageFromGitCommit(next)2555 settings =extractSettingsGitLog(log)2556 currentChange =int(settings['change'])2557if self.verbose:2558print"current change%s"% currentChange25592560if currentChange == change:2561if self.verbose:2562print"found%s"% next2563return next25642565if currentChange < change:2566 earliestCommit ="^%s"% next2567else:2568 latestCommit ="%s"% next25692570return""25712572defimportNewBranch(self, branch, maxChange):2573# make fast-import flush all changes to disk and update the refs using the checkpoint2574# command so that we can try to find the branch parent in the git history2575 self.gitStream.write("checkpoint\n\n");2576 self.gitStream.flush();2577 branchPrefix = self.depotPaths[0] + branch +"/"2578range="@1,%s"% maxChange2579#print "prefix" + branchPrefix2580 changes =p4ChangesForPaths([branchPrefix],range)2581iflen(changes) <=0:2582return False2583 firstChange = changes[0]2584#print "first change in branch: %s" % firstChange2585 sourceBranch = self.knownBranches[branch]2586 sourceDepotPath = self.depotPaths[0] + sourceBranch2587 sourceRef = self.gitRefForBranch(sourceBranch)2588#print "source " + sourceBranch25892590 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2591#print "branch parent: %s" % branchParentChange2592 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2593iflen(gitParent) >0:2594 self.initialParents[self.gitRefForBranch(branch)] = gitParent2595#print "parent git commit: %s" % gitParent25962597 self.importChanges(changes)2598return True25992600defsearchParent(self, parent, branch, target):2601 parentFound =False2602for blob inread_pipe_lines(["git","rev-list","--reverse",2603"--no-merges", parent]):2604 blob = blob.strip()2605iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2606 parentFound =True2607if self.verbose:2608print"Found parent of%sin commit%s"% (branch, blob)2609break2610if parentFound:2611return blob2612else:2613return None26142615defimportChanges(self, changes):2616 cnt =12617for change in changes:2618 description =p4_describe(change)2619 self.updateOptionDict(description)26202621if not self.silent:2622 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2623 sys.stdout.flush()2624 cnt = cnt +126252626try:2627if self.detectBranches:2628 branches = self.splitFilesIntoBranches(description)2629for branch in branches.keys():2630## HACK --hwn2631 branchPrefix = self.depotPaths[0] + branch +"/"2632 self.branchPrefixes = [ branchPrefix ]26332634 parent =""26352636 filesForCommit = branches[branch]26372638if self.verbose:2639print"branch is%s"% branch26402641 self.updatedBranches.add(branch)26422643if branch not in self.createdBranches:2644 self.createdBranches.add(branch)2645 parent = self.knownBranches[branch]2646if parent == branch:2647 parent =""2648else:2649 fullBranch = self.projectName + branch2650if fullBranch not in self.p4BranchesInGit:2651if not self.silent:2652print("\nImporting new branch%s"% fullBranch);2653if self.importNewBranch(branch, change -1):2654 parent =""2655 self.p4BranchesInGit.append(fullBranch)2656if not self.silent:2657print("\nResuming with change%s"% change);26582659if self.verbose:2660print"parent determined through known branches:%s"% parent26612662 branch = self.gitRefForBranch(branch)2663 parent = self.gitRefForBranch(parent)26642665if self.verbose:2666print"looking for initial parent for%s; current parent is%s"% (branch, parent)26672668iflen(parent) ==0and branch in self.initialParents:2669 parent = self.initialParents[branch]2670del self.initialParents[branch]26712672 blob =None2673iflen(parent) >0:2674 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2675if self.verbose:2676print"Creating temporary branch: "+ tempBranch2677 self.commit(description, filesForCommit, tempBranch)2678 self.tempBranches.append(tempBranch)2679 self.checkpoint()2680 blob = self.searchParent(parent, branch, tempBranch)2681if blob:2682 self.commit(description, filesForCommit, branch, blob)2683else:2684if self.verbose:2685print"Parent of%snot found. Committing into head of%s"% (branch, parent)2686 self.commit(description, filesForCommit, branch, parent)2687else:2688 files = self.extractFilesFromCommit(description)2689 self.commit(description, files, self.branch,2690 self.initialParent)2691# only needed once, to connect to the previous commit2692 self.initialParent =""2693exceptIOError:2694print self.gitError.read()2695 sys.exit(1)26962697defimportHeadRevision(self, revision):2698print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)26992700 details = {}2701 details["user"] ="git perforce import user"2702 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2703% (' '.join(self.depotPaths), revision))2704 details["change"] = revision2705 newestRevision =027062707 fileCnt =02708 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27092710for info inp4CmdList(["files"] + fileArgs):27112712if'code'in info and info['code'] =='error':2713 sys.stderr.write("p4 returned an error:%s\n"2714% info['data'])2715if info['data'].find("must refer to client") >=0:2716 sys.stderr.write("This particular p4 error is misleading.\n")2717 sys.stderr.write("Perhaps the depot path was misspelled.\n");2718 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2719 sys.exit(1)2720if'p4ExitCode'in info:2721 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2722 sys.exit(1)272327242725 change =int(info["change"])2726if change > newestRevision:2727 newestRevision = change27282729if info["action"]in self.delete_actions:2730# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2731#fileCnt = fileCnt + 12732continue27332734for prop in["depotFile","rev","action","type"]:2735 details["%s%s"% (prop, fileCnt)] = info[prop]27362737 fileCnt = fileCnt +127382739 details["change"] = newestRevision27402741# Use time from top-most change so that all git p4 clones of2742# the same p4 repo have the same commit SHA1s.2743 res =p4_describe(newestRevision)2744 details["time"] = res["time"]27452746 self.updateOptionDict(details)2747try:2748 self.commit(details, self.extractFilesFromCommit(details), self.branch)2749exceptIOError:2750print"IO error with git fast-import. Is your git version recent enough?"2751print self.gitError.read()275227532754defrun(self, args):2755 self.depotPaths = []2756 self.changeRange =""2757 self.previousDepotPaths = []2758 self.hasOrigin =False27592760# map from branch depot path to parent branch2761 self.knownBranches = {}2762 self.initialParents = {}27632764if self.importIntoRemotes:2765 self.refPrefix ="refs/remotes/p4/"2766else:2767 self.refPrefix ="refs/heads/p4/"27682769if self.syncWithOrigin:2770 self.hasOrigin =originP4BranchesExist()2771if self.hasOrigin:2772if not self.silent:2773print'Syncing with origin first, using "git fetch origin"'2774system("git fetch origin")27752776 branch_arg_given =bool(self.branch)2777iflen(self.branch) ==0:2778 self.branch = self.refPrefix +"master"2779ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2780system("git update-ref%srefs/heads/p4"% self.branch)2781system("git branch -D p4")27822783# accept either the command-line option, or the configuration variable2784if self.useClientSpec:2785# will use this after clone to set the variable2786 self.useClientSpec_from_options =True2787else:2788ifgitConfigBool("git-p4.useclientspec"):2789 self.useClientSpec =True2790if self.useClientSpec:2791 self.clientSpecDirs =getClientSpec()27922793# TODO: should always look at previous commits,2794# merge with previous imports, if possible.2795if args == []:2796if self.hasOrigin:2797createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)27982799# branches holds mapping from branch name to sha12800 branches =p4BranchesInGit(self.importIntoRemotes)28012802# restrict to just this one, disabling detect-branches2803if branch_arg_given:2804 short = self.branch.split("/")[-1]2805if short in branches:2806 self.p4BranchesInGit = [ short ]2807else:2808 self.p4BranchesInGit = branches.keys()28092810iflen(self.p4BranchesInGit) >1:2811if not self.silent:2812print"Importing from/into multiple branches"2813 self.detectBranches =True2814for branch in branches.keys():2815 self.initialParents[self.refPrefix + branch] = \2816 branches[branch]28172818if self.verbose:2819print"branches:%s"% self.p4BranchesInGit28202821 p4Change =02822for branch in self.p4BranchesInGit:2823 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28242825 settings =extractSettingsGitLog(logMsg)28262827 self.readOptions(settings)2828if(settings.has_key('depot-paths')2829and settings.has_key('change')):2830 change =int(settings['change']) +12831 p4Change =max(p4Change, change)28322833 depotPaths =sorted(settings['depot-paths'])2834if self.previousDepotPaths == []:2835 self.previousDepotPaths = depotPaths2836else:2837 paths = []2838for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2839 prev_list = prev.split("/")2840 cur_list = cur.split("/")2841for i inrange(0,min(len(cur_list),len(prev_list))):2842if cur_list[i] <> prev_list[i]:2843 i = i -12844break28452846 paths.append("/".join(cur_list[:i +1]))28472848 self.previousDepotPaths = paths28492850if p4Change >0:2851 self.depotPaths =sorted(self.previousDepotPaths)2852 self.changeRange ="@%s,#head"% p4Change2853if not self.silent and not self.detectBranches:2854print"Performing incremental import into%sgit branch"% self.branch28552856# accept multiple ref name abbreviations:2857# refs/foo/bar/branch -> use it exactly2858# p4/branch -> prepend refs/remotes/ or refs/heads/2859# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2860if not self.branch.startswith("refs/"):2861if self.importIntoRemotes:2862 prepend ="refs/remotes/"2863else:2864 prepend ="refs/heads/"2865if not self.branch.startswith("p4/"):2866 prepend +="p4/"2867 self.branch = prepend + self.branch28682869iflen(args) ==0and self.depotPaths:2870if not self.silent:2871print"Depot paths:%s"%' '.join(self.depotPaths)2872else:2873if self.depotPaths and self.depotPaths != args:2874print("previous import used depot path%sand now%swas specified. "2875"This doesn't work!"% (' '.join(self.depotPaths),2876' '.join(args)))2877 sys.exit(1)28782879 self.depotPaths =sorted(args)28802881 revision =""2882 self.users = {}28832884# Make sure no revision specifiers are used when --changesfile2885# is specified.2886 bad_changesfile =False2887iflen(self.changesFile) >0:2888for p in self.depotPaths:2889if p.find("@") >=0or p.find("#") >=0:2890 bad_changesfile =True2891break2892if bad_changesfile:2893die("Option --changesfile is incompatible with revision specifiers")28942895 newPaths = []2896for p in self.depotPaths:2897if p.find("@") != -1:2898 atIdx = p.index("@")2899 self.changeRange = p[atIdx:]2900if self.changeRange =="@all":2901 self.changeRange =""2902elif','not in self.changeRange:2903 revision = self.changeRange2904 self.changeRange =""2905 p = p[:atIdx]2906elif p.find("#") != -1:2907 hashIdx = p.index("#")2908 revision = p[hashIdx:]2909 p = p[:hashIdx]2910elif self.previousDepotPaths == []:2911# pay attention to changesfile, if given, else import2912# the entire p4 tree at the head revision2913iflen(self.changesFile) ==0:2914 revision ="#head"29152916 p = re.sub("\.\.\.$","", p)2917if not p.endswith("/"):2918 p +="/"29192920 newPaths.append(p)29212922 self.depotPaths = newPaths29232924# --detect-branches may change this for each branch2925 self.branchPrefixes = self.depotPaths29262927 self.loadUserMapFromCache()2928 self.labels = {}2929if self.detectLabels:2930 self.getLabels();29312932if self.detectBranches:2933## FIXME - what's a P4 projectName ?2934 self.projectName = self.guessProjectName()29352936if self.hasOrigin:2937 self.getBranchMappingFromGitBranches()2938else:2939 self.getBranchMapping()2940if self.verbose:2941print"p4-git branches:%s"% self.p4BranchesInGit2942print"initial parents:%s"% self.initialParents2943for b in self.p4BranchesInGit:2944if b !="master":29452946## FIXME2947 b = b[len(self.projectName):]2948 self.createdBranches.add(b)29492950 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29512952 self.importProcess = subprocess.Popen(["git","fast-import"],2953 stdin=subprocess.PIPE,2954 stdout=subprocess.PIPE,2955 stderr=subprocess.PIPE);2956 self.gitOutput = self.importProcess.stdout2957 self.gitStream = self.importProcess.stdin2958 self.gitError = self.importProcess.stderr29592960if revision:2961 self.importHeadRevision(revision)2962else:2963 changes = []29642965iflen(self.changesFile) >0:2966 output =open(self.changesFile).readlines()2967 changeSet =set()2968for line in output:2969 changeSet.add(int(line))29702971for change in changeSet:2972 changes.append(change)29732974 changes.sort()2975else:2976# catch "git p4 sync" with no new branches, in a repo that2977# does not have any existing p4 branches2978iflen(args) ==0:2979if not self.p4BranchesInGit:2980die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")29812982# The default branch is master, unless --branch is used to2983# specify something else. Make sure it exists, or complain2984# nicely about how to use --branch.2985if not self.detectBranches:2986if notbranch_exists(self.branch):2987if branch_arg_given:2988die("Error: branch%sdoes not exist."% self.branch)2989else:2990die("Error: no branch%s; perhaps specify one with --branch."%2991 self.branch)29922993if self.verbose:2994print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2995 self.changeRange)2996 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)29972998iflen(self.maxChanges) >0:2999 changes = changes[:min(int(self.maxChanges),len(changes))]30003001iflen(changes) ==0:3002if not self.silent:3003print"No changes to import!"3004else:3005if not self.silent and not self.detectBranches:3006print"Import destination:%s"% self.branch30073008 self.updatedBranches =set()30093010if not self.detectBranches:3011if args:3012# start a new branch3013 self.initialParent =""3014else:3015# build on a previous revision3016 self.initialParent =parseRevision(self.branch)30173018 self.importChanges(changes)30193020if not self.silent:3021print""3022iflen(self.updatedBranches) >0:3023 sys.stdout.write("Updated branches: ")3024for b in self.updatedBranches:3025 sys.stdout.write("%s"% b)3026 sys.stdout.write("\n")30273028ifgitConfigBool("git-p4.importLabels"):3029 self.importLabels =True30303031if self.importLabels:3032 p4Labels =getP4Labels(self.depotPaths)3033 gitTags =getGitTags()30343035 missingP4Labels = p4Labels - gitTags3036 self.importP4Labels(self.gitStream, missingP4Labels)30373038 self.gitStream.close()3039if self.importProcess.wait() !=0:3040die("fast-import failed:%s"% self.gitError.read())3041 self.gitOutput.close()3042 self.gitError.close()30433044# Cleanup temporary branches created during import3045if self.tempBranches != []:3046for branch in self.tempBranches:3047read_pipe("git update-ref -d%s"% branch)3048 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30493050# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3051# a convenient shortcut refname "p4".3052if self.importIntoRemotes:3053 head_ref = self.refPrefix +"HEAD"3054if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3055system(["git","symbolic-ref", head_ref, self.branch])30563057return True30583059classP4Rebase(Command):3060def__init__(self):3061 Command.__init__(self)3062 self.options = [3063 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3064]3065 self.importLabels =False3066 self.description = ("Fetches the latest revision from perforce and "3067+"rebases the current work (branch) against it")30683069defrun(self, args):3070 sync =P4Sync()3071 sync.importLabels = self.importLabels3072 sync.run([])30733074return self.rebase()30753076defrebase(self):3077if os.system("git update-index --refresh") !=0:3078die("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.");3079iflen(read_pipe("git diff-index HEAD --")) >0:3080die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");30813082[upstream, settings] =findUpstreamBranchPoint()3083iflen(upstream) ==0:3084die("Cannot find upstream branchpoint for rebase")30853086# the branchpoint may be p4/foo~3, so strip off the parent3087 upstream = re.sub("~[0-9]+$","", upstream)30883089print"Rebasing the current branch onto%s"% upstream3090 oldHead =read_pipe("git rev-parse HEAD").strip()3091system("git rebase%s"% upstream)3092system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3093return True30943095classP4Clone(P4Sync):3096def__init__(self):3097 P4Sync.__init__(self)3098 self.description ="Creates a new git repository and imports from Perforce into it"3099 self.usage ="usage: %prog [options] //depot/path[@revRange]"3100 self.options += [3101 optparse.make_option("--destination", dest="cloneDestination",3102 action='store', default=None,3103help="where to leave result of the clone"),3104 optparse.make_option("-/", dest="cloneExclude",3105 action="append",type="string",3106help="exclude depot path"),3107 optparse.make_option("--bare", dest="cloneBare",3108 action="store_true", default=False),3109]3110 self.cloneDestination =None3111 self.needsGit =False3112 self.cloneBare =False31133114# This is required for the "append" cloneExclude action3115defensure_value(self, attr, value):3116if nothasattr(self, attr)orgetattr(self, attr)is None:3117setattr(self, attr, value)3118returngetattr(self, attr)31193120defdefaultDestination(self, args):3121## TODO: use common prefix of args?3122 depotPath = args[0]3123 depotDir = re.sub("(@[^@]*)$","", depotPath)3124 depotDir = re.sub("(#[^#]*)$","", depotDir)3125 depotDir = re.sub(r"\.\.\.$","", depotDir)3126 depotDir = re.sub(r"/$","", depotDir)3127return os.path.split(depotDir)[1]31283129defrun(self, args):3130iflen(args) <1:3131return False31323133if self.keepRepoPath and not self.cloneDestination:3134 sys.stderr.write("Must specify destination for --keep-path\n")3135 sys.exit(1)31363137 depotPaths = args31383139if not self.cloneDestination andlen(depotPaths) >1:3140 self.cloneDestination = depotPaths[-1]3141 depotPaths = depotPaths[:-1]31423143 self.cloneExclude = ["/"+p for p in self.cloneExclude]3144for p in depotPaths:3145if not p.startswith("//"):3146 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3147return False31483149if not self.cloneDestination:3150 self.cloneDestination = self.defaultDestination(args)31513152print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31533154if not os.path.exists(self.cloneDestination):3155 os.makedirs(self.cloneDestination)3156chdir(self.cloneDestination)31573158 init_cmd = ["git","init"]3159if self.cloneBare:3160 init_cmd.append("--bare")3161 retcode = subprocess.call(init_cmd)3162if retcode:3163raiseCalledProcessError(retcode, init_cmd)31643165if not P4Sync.run(self, depotPaths):3166return False31673168# create a master branch and check out a work tree3169ifgitBranchExists(self.branch):3170system(["git","branch","master", self.branch ])3171if not self.cloneBare:3172system(["git","checkout","-f"])3173else:3174print'Not checking out any branch, use ' \3175'"git checkout -q -b master <branch>"'31763177# auto-set this variable if invoked with --use-client-spec3178if self.useClientSpec_from_options:3179system("git config --bool git-p4.useclientspec true")31803181return True31823183classP4Branches(Command):3184def__init__(self):3185 Command.__init__(self)3186 self.options = [ ]3187 self.description = ("Shows the git branches that hold imports and their "3188+"corresponding perforce depot paths")3189 self.verbose =False31903191defrun(self, args):3192iforiginP4BranchesExist():3193createOrUpdateBranchesFromOrigin()31943195 cmdline ="git rev-parse --symbolic "3196 cmdline +=" --remotes"31973198for line inread_pipe_lines(cmdline):3199 line = line.strip()32003201if not line.startswith('p4/')or line =="p4/HEAD":3202continue3203 branch = line32043205 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3206 settings =extractSettingsGitLog(log)32073208print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3209return True32103211classHelpFormatter(optparse.IndentedHelpFormatter):3212def__init__(self):3213 optparse.IndentedHelpFormatter.__init__(self)32143215defformat_description(self, description):3216if description:3217return description +"\n"3218else:3219return""32203221defprintUsage(commands):3222print"usage:%s<command> [options]"% sys.argv[0]3223print""3224print"valid commands:%s"%", ".join(commands)3225print""3226print"Try%s<command> --help for command specific help."% sys.argv[0]3227print""32283229commands = {3230"debug": P4Debug,3231"submit": P4Submit,3232"commit": P4Submit,3233"sync": P4Sync,3234"rebase": P4Rebase,3235"clone": P4Clone,3236"rollback": P4RollBack,3237"branches": P4Branches3238}323932403241defmain():3242iflen(sys.argv[1:]) ==0:3243printUsage(commands.keys())3244 sys.exit(2)32453246 cmdName = sys.argv[1]3247try:3248 klass = commands[cmdName]3249 cmd =klass()3250exceptKeyError:3251print"unknown command%s"% cmdName3252print""3253printUsage(commands.keys())3254 sys.exit(2)32553256 options = cmd.options3257 cmd.gitdir = os.environ.get("GIT_DIR",None)32583259 args = sys.argv[2:]32603261 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3262if cmd.needsGit:3263 options.append(optparse.make_option("--git-dir", dest="gitdir"))32643265 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3266 options,3267 description = cmd.description,3268 formatter =HelpFormatter())32693270(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3271global verbose3272 verbose = cmd.verbose3273if cmd.needsGit:3274if cmd.gitdir ==None:3275 cmd.gitdir = os.path.abspath(".git")3276if notisValidGitDir(cmd.gitdir):3277 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3278if os.path.exists(cmd.gitdir):3279 cdup =read_pipe("git rev-parse --show-cdup").strip()3280iflen(cdup) >0:3281chdir(cdup);32823283if notisValidGitDir(cmd.gitdir):3284ifisValidGitDir(cmd.gitdir +"/.git"):3285 cmd.gitdir +="/.git"3286else:3287die("fatal: cannot locate git repository at%s"% cmd.gitdir)32883289 os.environ["GIT_DIR"] = cmd.gitdir32903291if not cmd.run(args):3292 parser.print_help()3293 sys.exit(2)329432953296if __name__ =='__main__':3297main()