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(".*\((.+)\)( \*exclusive\*)?\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, block_size): 744assert depotPaths 745assert block_size 746 747# Parse the change range into start and end 748if changeRange is None or changeRange =='': 749 changeStart ='@1' 750 changeEnd ='#head' 751else: 752 parts = changeRange.split(',') 753assertlen(parts) ==2 754 changeStart = parts[0] 755 changeEnd = parts[1] 756 757# Accumulate change numbers in a dictionary to avoid duplicates 758 changes = {} 759 760for p in depotPaths: 761# Retrieve changes a block at a time, to prevent running 762# into a MaxScanRows error from the server. 763 start = changeStart 764 end = changeEnd 765 get_another_block =True 766while get_another_block: 767 new_changes = [] 768 cmd = ['changes'] 769 cmd += ['-m',str(block_size)] 770 cmd += ["%s...%s,%s"% (p, start, end)] 771for line inp4_read_pipe_lines(cmd): 772 changeNum =int(line.split(" ")[1]) 773 new_changes.append(changeNum) 774 changes[changeNum] =True 775iflen(new_changes) == block_size: 776 get_another_block =True 777 end ='@'+str(min(new_changes)) 778else: 779 get_another_block =False 780 781 changelist = changes.keys() 782 changelist.sort() 783return changelist 784 785defp4PathStartsWith(path, prefix): 786# This method tries to remedy a potential mixed-case issue: 787# 788# If UserA adds //depot/DirA/file1 789# and UserB adds //depot/dira/file2 790# 791# we may or may not have a problem. If you have core.ignorecase=true, 792# we treat DirA and dira as the same directory 793ifgitConfigBool("core.ignorecase"): 794return path.lower().startswith(prefix.lower()) 795return path.startswith(prefix) 796 797defgetClientSpec(): 798"""Look at the p4 client spec, create a View() object that contains 799 all the mappings, and return it.""" 800 801 specList =p4CmdList("client -o") 802iflen(specList) !=1: 803die('Output from "client -o" is%dlines, expecting 1'% 804len(specList)) 805 806# dictionary of all client parameters 807 entry = specList[0] 808 809# the //client/ name 810 client_name = entry["Client"] 811 812# just the keys that start with "View" 813 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 814 815# hold this new View 816 view =View(client_name) 817 818# append the lines, in order, to the view 819for view_num inrange(len(view_keys)): 820 k ="View%d"% view_num 821if k not in view_keys: 822die("Expected view key%smissing"% k) 823 view.append(entry[k]) 824 825return view 826 827defgetClientRoot(): 828"""Grab the client directory.""" 829 830 output =p4CmdList("client -o") 831iflen(output) !=1: 832die('Output from "client -o" is%dlines, expecting 1'%len(output)) 833 834 entry = output[0] 835if"Root"not in entry: 836die('Client has no "Root"') 837 838return entry["Root"] 839 840# 841# P4 wildcards are not allowed in filenames. P4 complains 842# if you simply add them, but you can force it with "-f", in 843# which case it translates them into %xx encoding internally. 844# 845defwildcard_decode(path): 846# Search for and fix just these four characters. Do % last so 847# that fixing it does not inadvertently create new %-escapes. 848# Cannot have * in a filename in windows; untested as to 849# what p4 would do in such a case. 850if not platform.system() =="Windows": 851 path = path.replace("%2A","*") 852 path = path.replace("%23","#") \ 853.replace("%40","@") \ 854.replace("%25","%") 855return path 856 857defwildcard_encode(path): 858# do % first to avoid double-encoding the %s introduced here 859 path = path.replace("%","%25") \ 860.replace("*","%2A") \ 861.replace("#","%23") \ 862.replace("@","%40") 863return path 864 865defwildcard_present(path): 866 m = re.search("[*#@%]", path) 867return m is not None 868 869class Command: 870def__init__(self): 871 self.usage ="usage: %prog [options]" 872 self.needsGit =True 873 self.verbose =False 874 875class P4UserMap: 876def__init__(self): 877 self.userMapFromPerforceServer =False 878 self.myP4UserId =None 879 880defp4UserId(self): 881if self.myP4UserId: 882return self.myP4UserId 883 884 results =p4CmdList("user -o") 885for r in results: 886if r.has_key('User'): 887 self.myP4UserId = r['User'] 888return r['User'] 889die("Could not find your p4 user id") 890 891defp4UserIsMe(self, p4User): 892# return True if the given p4 user is actually me 893 me = self.p4UserId() 894if not p4User or p4User != me: 895return False 896else: 897return True 898 899defgetUserCacheFilename(self): 900 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 901return home +"/.gitp4-usercache.txt" 902 903defgetUserMapFromPerforceServer(self): 904if self.userMapFromPerforceServer: 905return 906 self.users = {} 907 self.emails = {} 908 909for output inp4CmdList("users"): 910if not output.has_key("User"): 911continue 912 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 913 self.emails[output["Email"]] = output["User"] 914 915 916 s ='' 917for(key, val)in self.users.items(): 918 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 919 920open(self.getUserCacheFilename(),"wb").write(s) 921 self.userMapFromPerforceServer =True 922 923defloadUserMapFromCache(self): 924 self.users = {} 925 self.userMapFromPerforceServer =False 926try: 927 cache =open(self.getUserCacheFilename(),"rb") 928 lines = cache.readlines() 929 cache.close() 930for line in lines: 931 entry = line.strip().split("\t") 932 self.users[entry[0]] = entry[1] 933exceptIOError: 934 self.getUserMapFromPerforceServer() 935 936classP4Debug(Command): 937def__init__(self): 938 Command.__init__(self) 939 self.options = [] 940 self.description ="A tool to debug the output of p4 -G." 941 self.needsGit =False 942 943defrun(self, args): 944 j =0 945for output inp4CmdList(args): 946print'Element:%d'% j 947 j +=1 948print output 949return True 950 951classP4RollBack(Command): 952def__init__(self): 953 Command.__init__(self) 954 self.options = [ 955 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 956] 957 self.description ="A tool to debug the multi-branch import. Don't use :)" 958 self.rollbackLocalBranches =False 959 960defrun(self, args): 961iflen(args) !=1: 962return False 963 maxChange =int(args[0]) 964 965if"p4ExitCode"inp4Cmd("changes -m 1"): 966die("Problems executing p4"); 967 968if self.rollbackLocalBranches: 969 refPrefix ="refs/heads/" 970 lines =read_pipe_lines("git rev-parse --symbolic --branches") 971else: 972 refPrefix ="refs/remotes/" 973 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 974 975for line in lines: 976if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 977 line = line.strip() 978 ref = refPrefix + line 979 log =extractLogMessageFromGitCommit(ref) 980 settings =extractSettingsGitLog(log) 981 982 depotPaths = settings['depot-paths'] 983 change = settings['change'] 984 985 changed =False 986 987iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 988for p in depotPaths]))) ==0: 989print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 990system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 991continue 992 993while change andint(change) > maxChange: 994 changed =True 995if self.verbose: 996print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 997system("git update-ref%s\"%s^\""% (ref, ref)) 998 log =extractLogMessageFromGitCommit(ref) 999 settings =extractSettingsGitLog(log)100010011002 depotPaths = settings['depot-paths']1003 change = settings['change']10041005if changed:1006print"%srewound to%s"% (ref, change)10071008return True10091010classP4Submit(Command, P4UserMap):10111012 conflict_behavior_choices = ("ask","skip","quit")10131014def__init__(self):1015 Command.__init__(self)1016 P4UserMap.__init__(self)1017 self.options = [1018 optparse.make_option("--origin", dest="origin"),1019 optparse.make_option("-M", dest="detectRenames", action="store_true"),1020# preserve the user, requires relevant p4 permissions1021 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1022 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1023 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1024 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1025 optparse.make_option("--conflict", dest="conflict_behavior",1026 choices=self.conflict_behavior_choices),1027 optparse.make_option("--branch", dest="branch"),1028]1029 self.description ="Submit changes from git to the perforce depot."1030 self.usage +=" [name of git branch to submit into perforce depot]"1031 self.origin =""1032 self.detectRenames =False1033 self.preserveUser =gitConfigBool("git-p4.preserveUser")1034 self.dry_run =False1035 self.prepare_p4_only =False1036 self.conflict_behavior =None1037 self.isWindows = (platform.system() =="Windows")1038 self.exportLabels =False1039 self.p4HasMoveCommand =p4_has_move_command()1040 self.branch =None10411042defcheck(self):1043iflen(p4CmdList("opened ...")) >0:1044die("You have files opened with perforce! Close them before starting the sync.")10451046defseparate_jobs_from_description(self, message):1047"""Extract and return a possible Jobs field in the commit1048 message. It goes into a separate section in the p4 change1049 specification.10501051 A jobs line starts with "Jobs:" and looks like a new field1052 in a form. Values are white-space separated on the same1053 line or on following lines that start with a tab.10541055 This does not parse and extract the full git commit message1056 like a p4 form. It just sees the Jobs: line as a marker1057 to pass everything from then on directly into the p4 form,1058 but outside the description section.10591060 Return a tuple (stripped log message, jobs string)."""10611062 m = re.search(r'^Jobs:', message, re.MULTILINE)1063if m is None:1064return(message,None)10651066 jobtext = message[m.start():]1067 stripped_message = message[:m.start()].rstrip()1068return(stripped_message, jobtext)10691070defprepareLogMessage(self, template, message, jobs):1071"""Edits the template returned from "p4 change -o" to insert1072 the message in the Description field, and the jobs text in1073 the Jobs field."""1074 result =""10751076 inDescriptionSection =False10771078for line in template.split("\n"):1079if line.startswith("#"):1080 result += line +"\n"1081continue10821083if inDescriptionSection:1084if line.startswith("Files:")or line.startswith("Jobs:"):1085 inDescriptionSection =False1086# insert Jobs section1087if jobs:1088 result += jobs +"\n"1089else:1090continue1091else:1092if line.startswith("Description:"):1093 inDescriptionSection =True1094 line +="\n"1095for messageLine in message.split("\n"):1096 line +="\t"+ messageLine +"\n"10971098 result += line +"\n"10991100return result11011102defpatchRCSKeywords(self,file, pattern):1103# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1104(handle, outFileName) = tempfile.mkstemp(dir='.')1105try:1106 outFile = os.fdopen(handle,"w+")1107 inFile =open(file,"r")1108 regexp = re.compile(pattern, re.VERBOSE)1109for line in inFile.readlines():1110 line = regexp.sub(r'$\1$', line)1111 outFile.write(line)1112 inFile.close()1113 outFile.close()1114# Forcibly overwrite the original file1115 os.unlink(file)1116 shutil.move(outFileName,file)1117except:1118# cleanup our temporary file1119 os.unlink(outFileName)1120print"Failed to strip RCS keywords in%s"%file1121raise11221123print"Patched up RCS keywords in%s"%file11241125defp4UserForCommit(self,id):1126# Return the tuple (perforce user,git email) for a given git commit id1127 self.getUserMapFromPerforceServer()1128 gitEmail =read_pipe(["git","log","--max-count=1",1129"--format=%ae",id])1130 gitEmail = gitEmail.strip()1131if not self.emails.has_key(gitEmail):1132return(None,gitEmail)1133else:1134return(self.emails[gitEmail],gitEmail)11351136defcheckValidP4Users(self,commits):1137# check if any git authors cannot be mapped to p4 users1138foridin commits:1139(user,email) = self.p4UserForCommit(id)1140if not user:1141 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1142ifgitConfigBool("git-p4.allowMissingP4Users"):1143print"%s"% msg1144else:1145die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11461147deflastP4Changelist(self):1148# Get back the last changelist number submitted in this client spec. This1149# then gets used to patch up the username in the change. If the same1150# client spec is being used by multiple processes then this might go1151# wrong.1152 results =p4CmdList("client -o")# find the current client1153 client =None1154for r in results:1155if r.has_key('Client'):1156 client = r['Client']1157break1158if not client:1159die("could not get client spec")1160 results =p4CmdList(["changes","-c", client,"-m","1"])1161for r in results:1162if r.has_key('change'):1163return r['change']1164die("Could not get changelist number for last submit - cannot patch up user details")11651166defmodifyChangelistUser(self, changelist, newUser):1167# fixup the user field of a changelist after it has been submitted.1168 changes =p4CmdList("change -o%s"% changelist)1169iflen(changes) !=1:1170die("Bad output from p4 change modifying%sto user%s"%1171(changelist, newUser))11721173 c = changes[0]1174if c['User'] == newUser:return# nothing to do1175 c['User'] = newUser1176input= marshal.dumps(c)11771178 result =p4CmdList("change -f -i", stdin=input)1179for r in result:1180if r.has_key('code'):1181if r['code'] =='error':1182die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1183if r.has_key('data'):1184print("Updated user field for changelist%sto%s"% (changelist, newUser))1185return1186die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11871188defcanChangeChangelists(self):1189# check to see if we have p4 admin or super-user permissions, either of1190# which are required to modify changelists.1191 results =p4CmdList(["protects", self.depotPath])1192for r in results:1193if r.has_key('perm'):1194if r['perm'] =='admin':1195return11196if r['perm'] =='super':1197return11198return011991200defprepareSubmitTemplate(self):1201"""Run "p4 change -o" to grab a change specification template.1202 This does not use "p4 -G", as it is nice to keep the submission1203 template in original order, since a human might edit it.12041205 Remove lines in the Files section that show changes to files1206 outside the depot path we're committing into."""12071208 template =""1209 inFilesSection =False1210for line inp4_read_pipe_lines(['change','-o']):1211if line.endswith("\r\n"):1212 line = line[:-2] +"\n"1213if inFilesSection:1214if line.startswith("\t"):1215# path starts and ends with a tab1216 path = line[1:]1217 lastTab = path.rfind("\t")1218if lastTab != -1:1219 path = path[:lastTab]1220if notp4PathStartsWith(path, self.depotPath):1221continue1222else:1223 inFilesSection =False1224else:1225if line.startswith("Files:"):1226 inFilesSection =True12271228 template += line12291230return template12311232defedit_template(self, template_file):1233"""Invoke the editor to let the user change the submission1234 message. Return true if okay to continue with the submit."""12351236# if configured to skip the editing part, just submit1237ifgitConfigBool("git-p4.skipSubmitEdit"):1238return True12391240# look at the modification time, to check later if the user saved1241# the file1242 mtime = os.stat(template_file).st_mtime12431244# invoke the editor1245if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1246 editor = os.environ.get("P4EDITOR")1247else:1248 editor =read_pipe("git var GIT_EDITOR").strip()1249system([editor, template_file])12501251# If the file was not saved, prompt to see if this patch should1252# be skipped. But skip this verification step if configured so.1253ifgitConfigBool("git-p4.skipSubmitEditCheck"):1254return True12551256# modification time updated means user saved the file1257if os.stat(template_file).st_mtime > mtime:1258return True12591260while True:1261 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1262if response =='y':1263return True1264if response =='n':1265return False12661267defget_diff_description(self, editedFiles, filesToAdd):1268# diff1269if os.environ.has_key("P4DIFF"):1270del(os.environ["P4DIFF"])1271 diff =""1272for editedFile in editedFiles:1273 diff +=p4_read_pipe(['diff','-du',1274wildcard_encode(editedFile)])12751276# new file diff1277 newdiff =""1278for newFile in filesToAdd:1279 newdiff +="==== new file ====\n"1280 newdiff +="--- /dev/null\n"1281 newdiff +="+++%s\n"% newFile1282 f =open(newFile,"r")1283for line in f.readlines():1284 newdiff +="+"+ line1285 f.close()12861287return(diff + newdiff).replace('\r\n','\n')12881289defapplyCommit(self,id):1290"""Apply one commit, return True if it succeeded."""12911292print"Applying",read_pipe(["git","show","-s",1293"--format=format:%h%s",id])12941295(p4User, gitEmail) = self.p4UserForCommit(id)12961297 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1298 filesToAdd =set()1299 filesToDelete =set()1300 editedFiles =set()1301 pureRenameCopy =set()1302 filesToChangeExecBit = {}13031304for line in diff:1305 diff =parseDiffTreeEntry(line)1306 modifier = diff['status']1307 path = diff['src']1308if modifier =="M":1309p4_edit(path)1310ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1311 filesToChangeExecBit[path] = diff['dst_mode']1312 editedFiles.add(path)1313elif modifier =="A":1314 filesToAdd.add(path)1315 filesToChangeExecBit[path] = diff['dst_mode']1316if path in filesToDelete:1317 filesToDelete.remove(path)1318elif modifier =="D":1319 filesToDelete.add(path)1320if path in filesToAdd:1321 filesToAdd.remove(path)1322elif modifier =="C":1323 src, dest = diff['src'], diff['dst']1324p4_integrate(src, dest)1325 pureRenameCopy.add(dest)1326if diff['src_sha1'] != diff['dst_sha1']:1327p4_edit(dest)1328 pureRenameCopy.discard(dest)1329ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1330p4_edit(dest)1331 pureRenameCopy.discard(dest)1332 filesToChangeExecBit[dest] = diff['dst_mode']1333if self.isWindows:1334# turn off read-only attribute1335 os.chmod(dest, stat.S_IWRITE)1336 os.unlink(dest)1337 editedFiles.add(dest)1338elif modifier =="R":1339 src, dest = diff['src'], diff['dst']1340if self.p4HasMoveCommand:1341p4_edit(src)# src must be open before move1342p4_move(src, dest)# opens for (move/delete, move/add)1343else:1344p4_integrate(src, dest)1345if diff['src_sha1'] != diff['dst_sha1']:1346p4_edit(dest)1347else:1348 pureRenameCopy.add(dest)1349ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1350if not self.p4HasMoveCommand:1351p4_edit(dest)# with move: already open, writable1352 filesToChangeExecBit[dest] = diff['dst_mode']1353if not self.p4HasMoveCommand:1354if self.isWindows:1355 os.chmod(dest, stat.S_IWRITE)1356 os.unlink(dest)1357 filesToDelete.add(src)1358 editedFiles.add(dest)1359else:1360die("unknown modifier%sfor%s"% (modifier, path))13611362 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1363 patchcmd = diffcmd +" | git apply "1364 tryPatchCmd = patchcmd +"--check -"1365 applyPatchCmd = patchcmd +"--check --apply -"1366 patch_succeeded =True13671368if os.system(tryPatchCmd) !=0:1369 fixed_rcs_keywords =False1370 patch_succeeded =False1371print"Unfortunately applying the change failed!"13721373# Patch failed, maybe it's just RCS keyword woes. Look through1374# the patch to see if that's possible.1375ifgitConfigBool("git-p4.attemptRCSCleanup"):1376file=None1377 pattern =None1378 kwfiles = {}1379forfilein editedFiles | filesToDelete:1380# did this file's delta contain RCS keywords?1381 pattern =p4_keywords_regexp_for_file(file)13821383if pattern:1384# this file is a possibility...look for RCS keywords.1385 regexp = re.compile(pattern, re.VERBOSE)1386for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1387if regexp.search(line):1388if verbose:1389print"got keyword match on%sin%sin%s"% (pattern, line,file)1390 kwfiles[file] = pattern1391break13921393forfilein kwfiles:1394if verbose:1395print"zapping%swith%s"% (line,pattern)1396# File is being deleted, so not open in p4. Must1397# disable the read-only bit on windows.1398if self.isWindows andfilenot in editedFiles:1399 os.chmod(file, stat.S_IWRITE)1400 self.patchRCSKeywords(file, kwfiles[file])1401 fixed_rcs_keywords =True14021403if fixed_rcs_keywords:1404print"Retrying the patch with RCS keywords cleaned up"1405if os.system(tryPatchCmd) ==0:1406 patch_succeeded =True14071408if not patch_succeeded:1409for f in editedFiles:1410p4_revert(f)1411return False14121413#1414# Apply the patch for real, and do add/delete/+x handling.1415#1416system(applyPatchCmd)14171418for f in filesToAdd:1419p4_add(f)1420for f in filesToDelete:1421p4_revert(f)1422p4_delete(f)14231424# Set/clear executable bits1425for f in filesToChangeExecBit.keys():1426 mode = filesToChangeExecBit[f]1427setP4ExecBit(f, mode)14281429#1430# Build p4 change description, starting with the contents1431# of the git commit message.1432#1433 logMessage =extractLogMessageFromGitCommit(id)1434 logMessage = logMessage.strip()1435(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14361437 template = self.prepareSubmitTemplate()1438 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14391440if self.preserveUser:1441 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14421443if self.checkAuthorship and not self.p4UserIsMe(p4User):1444 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1445 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1446 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"14471448 separatorLine ="######## everything below this line is just the diff #######\n"1449if not self.prepare_p4_only:1450 submitTemplate += separatorLine1451 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)14521453(handle, fileName) = tempfile.mkstemp()1454 tmpFile = os.fdopen(handle,"w+b")1455if self.isWindows:1456 submitTemplate = submitTemplate.replace("\n","\r\n")1457 tmpFile.write(submitTemplate)1458 tmpFile.close()14591460if self.prepare_p4_only:1461#1462# Leave the p4 tree prepared, and the submit template around1463# and let the user decide what to do next1464#1465print1466print"P4 workspace prepared for submission."1467print"To submit or revert, go to client workspace"1468print" "+ self.clientPath1469print1470print"To submit, use\"p4 submit\"to write a new description,"1471print"or\"p4 submit -i <%s\"to use the one prepared by" \1472"\"git p4\"."% fileName1473print"You can delete the file\"%s\"when finished."% fileName14741475if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1476print"To preserve change ownership by user%s, you must\n" \1477"do\"p4 change -f <change>\"after submitting and\n" \1478"edit the User field."1479if pureRenameCopy:1480print"After submitting, renamed files must be re-synced."1481print"Invoke\"p4 sync -f\"on each of these files:"1482for f in pureRenameCopy:1483print" "+ f14841485print1486print"To revert the changes, use\"p4 revert ...\", and delete"1487print"the submit template file\"%s\""% fileName1488if filesToAdd:1489print"Since the commit adds new files, they must be deleted:"1490for f in filesToAdd:1491print" "+ f1492print1493return True14941495#1496# Let the user edit the change description, then submit it.1497#1498if self.edit_template(fileName):1499# read the edited message and submit1500 ret =True1501 tmpFile =open(fileName,"rb")1502 message = tmpFile.read()1503 tmpFile.close()1504if self.isWindows:1505 message = message.replace("\r\n","\n")1506 submitTemplate = message[:message.index(separatorLine)]1507p4_write_pipe(['submit','-i'], submitTemplate)15081509if self.preserveUser:1510if p4User:1511# Get last changelist number. Cannot easily get it from1512# the submit command output as the output is1513# unmarshalled.1514 changelist = self.lastP4Changelist()1515 self.modifyChangelistUser(changelist, p4User)15161517# The rename/copy happened by applying a patch that created a1518# new file. This leaves it writable, which confuses p4.1519for f in pureRenameCopy:1520p4_sync(f,"-f")15211522else:1523# skip this patch1524 ret =False1525print"Submission cancelled, undoing p4 changes."1526for f in editedFiles:1527p4_revert(f)1528for f in filesToAdd:1529p4_revert(f)1530 os.remove(f)1531for f in filesToDelete:1532p4_revert(f)15331534 os.remove(fileName)1535return ret15361537# Export git tags as p4 labels. Create a p4 label and then tag1538# with that.1539defexportGitTags(self, gitTags):1540 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1541iflen(validLabelRegexp) ==0:1542 validLabelRegexp = defaultLabelRegexp1543 m = re.compile(validLabelRegexp)15441545for name in gitTags:15461547if not m.match(name):1548if verbose:1549print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1550continue15511552# Get the p4 commit this corresponds to1553 logMessage =extractLogMessageFromGitCommit(name)1554 values =extractSettingsGitLog(logMessage)15551556if not values.has_key('change'):1557# a tag pointing to something not sent to p4; ignore1558if verbose:1559print"git tag%sdoes not give a p4 commit"% name1560continue1561else:1562 changelist = values['change']15631564# Get the tag details.1565 inHeader =True1566 isAnnotated =False1567 body = []1568for l inread_pipe_lines(["git","cat-file","-p", name]):1569 l = l.strip()1570if inHeader:1571if re.match(r'tag\s+', l):1572 isAnnotated =True1573elif re.match(r'\s*$', l):1574 inHeader =False1575continue1576else:1577 body.append(l)15781579if not isAnnotated:1580 body = ["lightweight tag imported by git p4\n"]15811582# Create the label - use the same view as the client spec we are using1583 clientSpec =getClientSpec()15841585 labelTemplate ="Label:%s\n"% name1586 labelTemplate +="Description:\n"1587for b in body:1588 labelTemplate +="\t"+ b +"\n"1589 labelTemplate +="View:\n"1590for depot_side in clientSpec.mappings:1591 labelTemplate +="\t%s\n"% depot_side15921593if self.dry_run:1594print"Would create p4 label%sfor tag"% name1595elif self.prepare_p4_only:1596print"Not creating p4 label%sfor tag due to option" \1597" --prepare-p4-only"% name1598else:1599p4_write_pipe(["label","-i"], labelTemplate)16001601# Use the label1602p4_system(["tag","-l", name] +1603["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16041605if verbose:1606print"created p4 label for tag%s"% name16071608defrun(self, args):1609iflen(args) ==0:1610 self.master =currentGitBranch()1611iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1612die("Detecting current git branch failed!")1613eliflen(args) ==1:1614 self.master = args[0]1615if notbranchExists(self.master):1616die("Branch%sdoes not exist"% self.master)1617else:1618return False16191620 allowSubmit =gitConfig("git-p4.allowSubmit")1621iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1622die("%sis not in git-p4.allowSubmit"% self.master)16231624[upstream, settings] =findUpstreamBranchPoint()1625 self.depotPath = settings['depot-paths'][0]1626iflen(self.origin) ==0:1627 self.origin = upstream16281629if self.preserveUser:1630if not self.canChangeChangelists():1631die("Cannot preserve user names without p4 super-user or admin permissions")16321633# if not set from the command line, try the config file1634if self.conflict_behavior is None:1635 val =gitConfig("git-p4.conflict")1636if val:1637if val not in self.conflict_behavior_choices:1638die("Invalid value '%s' for config git-p4.conflict"% val)1639else:1640 val ="ask"1641 self.conflict_behavior = val16421643if self.verbose:1644print"Origin branch is "+ self.origin16451646iflen(self.depotPath) ==0:1647print"Internal error: cannot locate perforce depot path from existing branches"1648 sys.exit(128)16491650 self.useClientSpec =False1651ifgitConfigBool("git-p4.useclientspec"):1652 self.useClientSpec =True1653if self.useClientSpec:1654 self.clientSpecDirs =getClientSpec()16551656if self.useClientSpec:1657# all files are relative to the client spec1658 self.clientPath =getClientRoot()1659else:1660 self.clientPath =p4Where(self.depotPath)16611662if self.clientPath =="":1663die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)16641665print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1666 self.oldWorkingDirectory = os.getcwd()16671668# ensure the clientPath exists1669 new_client_dir =False1670if not os.path.exists(self.clientPath):1671 new_client_dir =True1672 os.makedirs(self.clientPath)16731674chdir(self.clientPath, is_client_path=True)1675if self.dry_run:1676print"Would synchronize p4 checkout in%s"% self.clientPath1677else:1678print"Synchronizing p4 checkout..."1679if new_client_dir:1680# old one was destroyed, and maybe nobody told p41681p4_sync("...","-f")1682else:1683p4_sync("...")1684 self.check()16851686 commits = []1687for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1688 commits.append(line.strip())1689 commits.reverse()16901691if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1692 self.checkAuthorship =False1693else:1694 self.checkAuthorship =True16951696if self.preserveUser:1697 self.checkValidP4Users(commits)16981699#1700# Build up a set of options to be passed to diff when1701# submitting each commit to p4.1702#1703if self.detectRenames:1704# command-line -M arg1705 self.diffOpts ="-M"1706else:1707# If not explicitly set check the config variable1708 detectRenames =gitConfig("git-p4.detectRenames")17091710if detectRenames.lower() =="false"or detectRenames =="":1711 self.diffOpts =""1712elif detectRenames.lower() =="true":1713 self.diffOpts ="-M"1714else:1715 self.diffOpts ="-M%s"% detectRenames17161717# no command-line arg for -C or --find-copies-harder, just1718# config variables1719 detectCopies =gitConfig("git-p4.detectCopies")1720if detectCopies.lower() =="false"or detectCopies =="":1721pass1722elif detectCopies.lower() =="true":1723 self.diffOpts +=" -C"1724else:1725 self.diffOpts +=" -C%s"% detectCopies17261727ifgitConfigBool("git-p4.detectCopiesHarder"):1728 self.diffOpts +=" --find-copies-harder"17291730#1731# Apply the commits, one at a time. On failure, ask if should1732# continue to try the rest of the patches, or quit.1733#1734if self.dry_run:1735print"Would apply"1736 applied = []1737 last =len(commits) -11738for i, commit inenumerate(commits):1739if self.dry_run:1740print" ",read_pipe(["git","show","-s",1741"--format=format:%h%s", commit])1742 ok =True1743else:1744 ok = self.applyCommit(commit)1745if ok:1746 applied.append(commit)1747else:1748if self.prepare_p4_only and i < last:1749print"Processing only the first commit due to option" \1750" --prepare-p4-only"1751break1752if i < last:1753 quit =False1754while True:1755# prompt for what to do, or use the option/variable1756if self.conflict_behavior =="ask":1757print"What do you want to do?"1758 response =raw_input("[s]kip this commit but apply"1759" the rest, or [q]uit? ")1760if not response:1761continue1762elif self.conflict_behavior =="skip":1763 response ="s"1764elif self.conflict_behavior =="quit":1765 response ="q"1766else:1767die("Unknown conflict_behavior '%s'"%1768 self.conflict_behavior)17691770if response[0] =="s":1771print"Skipping this commit, but applying the rest"1772break1773if response[0] =="q":1774print"Quitting"1775 quit =True1776break1777if quit:1778break17791780chdir(self.oldWorkingDirectory)17811782if self.dry_run:1783pass1784elif self.prepare_p4_only:1785pass1786eliflen(commits) ==len(applied):1787print"All commits applied!"17881789 sync =P4Sync()1790if self.branch:1791 sync.branch = self.branch1792 sync.run([])17931794 rebase =P4Rebase()1795 rebase.rebase()17961797else:1798iflen(applied) ==0:1799print"No commits applied."1800else:1801print"Applied only the commits marked with '*':"1802for c in commits:1803if c in applied:1804 star ="*"1805else:1806 star =" "1807print star,read_pipe(["git","show","-s",1808"--format=format:%h%s", c])1809print"You will have to do 'git p4 sync' and rebase."18101811ifgitConfigBool("git-p4.exportLabels"):1812 self.exportLabels =True18131814if self.exportLabels:1815 p4Labels =getP4Labels(self.depotPath)1816 gitTags =getGitTags()18171818 missingGitTags = gitTags - p4Labels1819 self.exportGitTags(missingGitTags)18201821# exit with error unless everything applied perfectly1822iflen(commits) !=len(applied):1823 sys.exit(1)18241825return True18261827classView(object):1828"""Represent a p4 view ("p4 help views"), and map files in a1829 repo according to the view."""18301831def__init__(self, client_name):1832 self.mappings = []1833 self.client_prefix ="//%s/"% client_name1834# cache results of "p4 where" to lookup client file locations1835 self.client_spec_path_cache = {}18361837defappend(self, view_line):1838"""Parse a view line, splitting it into depot and client1839 sides. Append to self.mappings, preserving order. This1840 is only needed for tag creation."""18411842# Split the view line into exactly two words. P4 enforces1843# structure on these lines that simplifies this quite a bit.1844#1845# Either or both words may be double-quoted.1846# Single quotes do not matter.1847# Double-quote marks cannot occur inside the words.1848# A + or - prefix is also inside the quotes.1849# There are no quotes unless they contain a space.1850# The line is already white-space stripped.1851# The two words are separated by a single space.1852#1853if view_line[0] =='"':1854# First word is double quoted. Find its end.1855 close_quote_index = view_line.find('"',1)1856if close_quote_index <=0:1857die("No first-word closing quote found:%s"% view_line)1858 depot_side = view_line[1:close_quote_index]1859# skip closing quote and space1860 rhs_index = close_quote_index +1+11861else:1862 space_index = view_line.find(" ")1863if space_index <=0:1864die("No word-splitting space found:%s"% view_line)1865 depot_side = view_line[0:space_index]1866 rhs_index = space_index +118671868# prefix + means overlay on previous mapping1869if depot_side.startswith("+"):1870 depot_side = depot_side[1:]18711872# prefix - means exclude this path, leave out of mappings1873 exclude =False1874if depot_side.startswith("-"):1875 exclude =True1876 depot_side = depot_side[1:]18771878if not exclude:1879 self.mappings.append(depot_side)18801881defconvert_client_path(self, clientFile):1882# chop off //client/ part to make it relative1883if not clientFile.startswith(self.client_prefix):1884die("No prefix '%s' on clientFile '%s'"%1885(self.client_prefix, clientFile))1886return clientFile[len(self.client_prefix):]18871888defupdate_client_spec_path_cache(self, files):1889""" Caching file paths by "p4 where" batch query """18901891# List depot file paths exclude that already cached1892 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]18931894iflen(fileArgs) ==0:1895return# All files in cache18961897 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1898for res in where_result:1899if"code"in res and res["code"] =="error":1900# assume error is "... file(s) not in client view"1901continue1902if"clientFile"not in res:1903die("No clientFile in 'p4 where' output")1904if"unmap"in res:1905# it will list all of them, but only one not unmap-ped1906continue1907 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19081909# not found files or unmap files set to ""1910for depotFile in fileArgs:1911if depotFile not in self.client_spec_path_cache:1912 self.client_spec_path_cache[depotFile] =""19131914defmap_in_client(self, depot_path):1915"""Return the relative location in the client where this1916 depot file should live. Returns "" if the file should1917 not be mapped in the client."""19181919if depot_path in self.client_spec_path_cache:1920return self.client_spec_path_cache[depot_path]19211922die("Error:%sis not found in client spec path"% depot_path )1923return""19241925classP4Sync(Command, P4UserMap):1926 delete_actions = ("delete","move/delete","purge")19271928def__init__(self):1929 Command.__init__(self)1930 P4UserMap.__init__(self)1931 self.options = [1932 optparse.make_option("--branch", dest="branch"),1933 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1934 optparse.make_option("--changesfile", dest="changesFile"),1935 optparse.make_option("--silent", dest="silent", action="store_true"),1936 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1937 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1938 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1939help="Import into refs/heads/ , not refs/remotes"),1940 optparse.make_option("--max-changes", dest="maxChanges",1941help="Maximum number of changes to import"),1942 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",1943help="Internal block size to use when iteratively calling p4 changes"),1944 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1945help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1946 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1947help="Only sync files that are included in the Perforce Client Spec"),1948 optparse.make_option("-/", dest="cloneExclude",1949 action="append",type="string",1950help="exclude depot path"),1951]1952 self.description ="""Imports from Perforce into a git repository.\n1953 example:1954 //depot/my/project/ -- to import the current head1955 //depot/my/project/@all -- to import everything1956 //depot/my/project/@1,6 -- to import only from revision 1 to 619571958 (a ... is not needed in the path p4 specification, it's added implicitly)"""19591960 self.usage +=" //depot/path[@revRange]"1961 self.silent =False1962 self.createdBranches =set()1963 self.committedChanges =set()1964 self.branch =""1965 self.detectBranches =False1966 self.detectLabels =False1967 self.importLabels =False1968 self.changesFile =""1969 self.syncWithOrigin =True1970 self.importIntoRemotes =True1971 self.maxChanges =""1972 self.changes_block_size =5001973 self.keepRepoPath =False1974 self.depotPaths =None1975 self.p4BranchesInGit = []1976 self.cloneExclude = []1977 self.useClientSpec =False1978 self.useClientSpec_from_options =False1979 self.clientSpecDirs =None1980 self.tempBranches = []1981 self.tempBranchLocation ="git-p4-tmp"19821983ifgitConfig("git-p4.syncFromOrigin") =="false":1984 self.syncWithOrigin =False19851986# This is required for the "append" cloneExclude action1987defensure_value(self, attr, value):1988if nothasattr(self, attr)orgetattr(self, attr)is None:1989setattr(self, attr, value)1990returngetattr(self, attr)19911992# Force a checkpoint in fast-import and wait for it to finish1993defcheckpoint(self):1994 self.gitStream.write("checkpoint\n\n")1995 self.gitStream.write("progress checkpoint\n\n")1996 out = self.gitOutput.readline()1997if self.verbose:1998print"checkpoint finished: "+ out19992000defextractFilesFromCommit(self, commit):2001 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2002for path in self.cloneExclude]2003 files = []2004 fnum =02005while commit.has_key("depotFile%s"% fnum):2006 path = commit["depotFile%s"% fnum]20072008if[p for p in self.cloneExclude2009ifp4PathStartsWith(path, p)]:2010 found =False2011else:2012 found = [p for p in self.depotPaths2013ifp4PathStartsWith(path, p)]2014if not found:2015 fnum = fnum +12016continue20172018file= {}2019file["path"] = path2020file["rev"] = commit["rev%s"% fnum]2021file["action"] = commit["action%s"% fnum]2022file["type"] = commit["type%s"% fnum]2023 files.append(file)2024 fnum = fnum +12025return files20262027defstripRepoPath(self, path, prefixes):2028"""When streaming files, this is called to map a p4 depot path2029 to where it should go in git. The prefixes are either2030 self.depotPaths, or self.branchPrefixes in the case of2031 branch detection."""20322033if self.useClientSpec:2034# branch detection moves files up a level (the branch name)2035# from what client spec interpretation gives2036 path = self.clientSpecDirs.map_in_client(path)2037if self.detectBranches:2038for b in self.knownBranches:2039if path.startswith(b +"/"):2040 path = path[len(b)+1:]20412042elif self.keepRepoPath:2043# Preserve everything in relative path name except leading2044# //depot/; just look at first prefix as they all should2045# be in the same depot.2046 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2047ifp4PathStartsWith(path, depot):2048 path = path[len(depot):]20492050else:2051for p in prefixes:2052ifp4PathStartsWith(path, p):2053 path = path[len(p):]2054break20552056 path =wildcard_decode(path)2057return path20582059defsplitFilesIntoBranches(self, commit):2060"""Look at each depotFile in the commit to figure out to what2061 branch it belongs."""20622063if self.clientSpecDirs:2064 files = self.extractFilesFromCommit(commit)2065 self.clientSpecDirs.update_client_spec_path_cache(files)20662067 branches = {}2068 fnum =02069while commit.has_key("depotFile%s"% fnum):2070 path = commit["depotFile%s"% fnum]2071 found = [p for p in self.depotPaths2072ifp4PathStartsWith(path, p)]2073if not found:2074 fnum = fnum +12075continue20762077file= {}2078file["path"] = path2079file["rev"] = commit["rev%s"% fnum]2080file["action"] = commit["action%s"% fnum]2081file["type"] = commit["type%s"% fnum]2082 fnum = fnum +120832084# start with the full relative path where this file would2085# go in a p4 client2086if self.useClientSpec:2087 relPath = self.clientSpecDirs.map_in_client(path)2088else:2089 relPath = self.stripRepoPath(path, self.depotPaths)20902091for branch in self.knownBranches.keys():2092# add a trailing slash so that a commit into qt/4.2foo2093# doesn't end up in qt/4.2, e.g.2094if relPath.startswith(branch +"/"):2095if branch not in branches:2096 branches[branch] = []2097 branches[branch].append(file)2098break20992100return branches21012102# output one file from the P4 stream2103# - helper for streamP4Files21042105defstreamOneP4File(self,file, contents):2106 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2107if verbose:2108 sys.stderr.write("%s\n"% relPath)21092110(type_base, type_mods) =split_p4_type(file["type"])21112112 git_mode ="100644"2113if"x"in type_mods:2114 git_mode ="100755"2115if type_base =="symlink":2116 git_mode ="120000"2117# p4 print on a symlink sometimes contains "target\n";2118# if it does, remove the newline2119 data =''.join(contents)2120if not data:2121# Some version of p4 allowed creating a symlink that pointed2122# to nothing. This causes p4 errors when checking out such2123# a change, and errors here too. Work around it by ignoring2124# the bad symlink; hopefully a future change fixes it.2125print"\nIgnoring empty symlink in%s"%file['depotFile']2126return2127elif data[-1] =='\n':2128 contents = [data[:-1]]2129else:2130 contents = [data]21312132if type_base =="utf16":2133# p4 delivers different text in the python output to -G2134# than it does when using "print -o", or normal p4 client2135# operations. utf16 is converted to ascii or utf8, perhaps.2136# But ascii text saved as -t utf16 is completely mangled.2137# Invoke print -o to get the real contents.2138#2139# On windows, the newlines will always be mangled by print, so put2140# them back too. This is not needed to the cygwin windows version,2141# just the native "NT" type.2142#2143 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2144ifp4_version_string().find("/NT") >=0:2145 text = text.replace("\r\n","\n")2146 contents = [ text ]21472148if type_base =="apple":2149# Apple filetype files will be streamed as a concatenation of2150# its appledouble header and the contents. This is useless2151# on both macs and non-macs. If using "print -q -o xx", it2152# will create "xx" with the data, and "%xx" with the header.2153# This is also not very useful.2154#2155# Ideally, someday, this script can learn how to generate2156# appledouble files directly and import those to git, but2157# non-mac machines can never find a use for apple filetype.2158print"\nIgnoring apple filetype file%s"%file['depotFile']2159return21602161# Note that we do not try to de-mangle keywords on utf16 files,2162# even though in theory somebody may want that.2163 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2164if pattern:2165 regexp = re.compile(pattern, re.VERBOSE)2166 text =''.join(contents)2167 text = regexp.sub(r'$\1$', text)2168 contents = [ text ]21692170 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21712172# total length...2173 length =02174for d in contents:2175 length = length +len(d)21762177 self.gitStream.write("data%d\n"% length)2178for d in contents:2179 self.gitStream.write(d)2180 self.gitStream.write("\n")21812182defstreamOneP4Deletion(self,file):2183 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2184if verbose:2185 sys.stderr.write("delete%s\n"% relPath)2186 self.gitStream.write("D%s\n"% relPath)21872188# handle another chunk of streaming data2189defstreamP4FilesCb(self, marshalled):21902191# catch p4 errors and complain2192 err =None2193if"code"in marshalled:2194if marshalled["code"] =="error":2195if"data"in marshalled:2196 err = marshalled["data"].rstrip()2197if err:2198 f =None2199if self.stream_have_file_info:2200if"depotFile"in self.stream_file:2201 f = self.stream_file["depotFile"]2202# force a failure in fast-import, else an empty2203# commit will be made2204 self.gitStream.write("\n")2205 self.gitStream.write("die-now\n")2206 self.gitStream.close()2207# ignore errors, but make sure it exits first2208 self.importProcess.wait()2209if f:2210die("Error from p4 print for%s:%s"% (f, err))2211else:2212die("Error from p4 print:%s"% err)22132214if marshalled.has_key('depotFile')and self.stream_have_file_info:2215# start of a new file - output the old one first2216 self.streamOneP4File(self.stream_file, self.stream_contents)2217 self.stream_file = {}2218 self.stream_contents = []2219 self.stream_have_file_info =False22202221# pick up the new file information... for the2222# 'data' field we need to append to our array2223for k in marshalled.keys():2224if k =='data':2225 self.stream_contents.append(marshalled['data'])2226else:2227 self.stream_file[k] = marshalled[k]22282229 self.stream_have_file_info =True22302231# Stream directly from "p4 files" into "git fast-import"2232defstreamP4Files(self, files):2233 filesForCommit = []2234 filesToRead = []2235 filesToDelete = []22362237for f in files:2238# if using a client spec, only add the files that have2239# a path in the client2240if self.clientSpecDirs:2241if self.clientSpecDirs.map_in_client(f['path']) =="":2242continue22432244 filesForCommit.append(f)2245if f['action']in self.delete_actions:2246 filesToDelete.append(f)2247else:2248 filesToRead.append(f)22492250# deleted files...2251for f in filesToDelete:2252 self.streamOneP4Deletion(f)22532254iflen(filesToRead) >0:2255 self.stream_file = {}2256 self.stream_contents = []2257 self.stream_have_file_info =False22582259# curry self argument2260defstreamP4FilesCbSelf(entry):2261 self.streamP4FilesCb(entry)22622263 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22642265p4CmdList(["-x","-","print"],2266 stdin=fileArgs,2267 cb=streamP4FilesCbSelf)22682269# do the last chunk2270if self.stream_file.has_key('depotFile'):2271 self.streamOneP4File(self.stream_file, self.stream_contents)22722273defmake_email(self, userid):2274if userid in self.users:2275return self.users[userid]2276else:2277return"%s<a@b>"% userid22782279# Stream a p4 tag2280defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2281if verbose:2282print"writing tag%sfor commit%s"% (labelName, commit)2283 gitStream.write("tag%s\n"% labelName)2284 gitStream.write("from%s\n"% commit)22852286if labelDetails.has_key('Owner'):2287 owner = labelDetails["Owner"]2288else:2289 owner =None22902291# Try to use the owner of the p4 label, or failing that,2292# the current p4 user id.2293if owner:2294 email = self.make_email(owner)2295else:2296 email = self.make_email(self.p4UserId())2297 tagger ="%s %s %s"% (email, epoch, self.tz)22982299 gitStream.write("tagger%s\n"% tagger)23002301print"labelDetails=",labelDetails2302if labelDetails.has_key('Description'):2303 description = labelDetails['Description']2304else:2305 description ='Label from git p4'23062307 gitStream.write("data%d\n"%len(description))2308 gitStream.write(description)2309 gitStream.write("\n")23102311defcommit(self, details, files, branch, parent =""):2312 epoch = details["time"]2313 author = details["user"]23142315if self.verbose:2316print"commit into%s"% branch23172318# start with reading files; if that fails, we should not2319# create a commit.2320 new_files = []2321for f in files:2322if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2323 new_files.append(f)2324else:2325 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23262327if self.clientSpecDirs:2328 self.clientSpecDirs.update_client_spec_path_cache(files)23292330 self.gitStream.write("commit%s\n"% branch)2331# gitStream.write("mark :%s\n" % details["change"])2332 self.committedChanges.add(int(details["change"]))2333 committer =""2334if author not in self.users:2335 self.getUserMapFromPerforceServer()2336 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23372338 self.gitStream.write("committer%s\n"% committer)23392340 self.gitStream.write("data <<EOT\n")2341 self.gitStream.write(details["desc"])2342 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2343(','.join(self.branchPrefixes), details["change"]))2344iflen(details['options']) >0:2345 self.gitStream.write(": options =%s"% details['options'])2346 self.gitStream.write("]\nEOT\n\n")23472348iflen(parent) >0:2349if self.verbose:2350print"parent%s"% parent2351 self.gitStream.write("from%s\n"% parent)23522353 self.streamP4Files(new_files)2354 self.gitStream.write("\n")23552356 change =int(details["change"])23572358if self.labels.has_key(change):2359 label = self.labels[change]2360 labelDetails = label[0]2361 labelRevisions = label[1]2362if self.verbose:2363print"Change%sis labelled%s"% (change, labelDetails)23642365 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2366for p in self.branchPrefixes])23672368iflen(files) ==len(labelRevisions):23692370 cleanedFiles = {}2371for info in files:2372if info["action"]in self.delete_actions:2373continue2374 cleanedFiles[info["depotFile"]] = info["rev"]23752376if cleanedFiles == labelRevisions:2377 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23782379else:2380if not self.silent:2381print("Tag%sdoes not match with change%s: files do not match."2382% (labelDetails["label"], change))23832384else:2385if not self.silent:2386print("Tag%sdoes not match with change%s: file count is different."2387% (labelDetails["label"], change))23882389# Build a dictionary of changelists and labels, for "detect-labels" option.2390defgetLabels(self):2391 self.labels = {}23922393 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2394iflen(l) >0and not self.silent:2395print"Finding files belonging to labels in%s"% `self.depotPaths`23962397for output in l:2398 label = output["label"]2399 revisions = {}2400 newestChange =02401if self.verbose:2402print"Querying files for label%s"% label2403forfileinp4CmdList(["files"] +2404["%s...@%s"% (p, label)2405for p in self.depotPaths]):2406 revisions[file["depotFile"]] =file["rev"]2407 change =int(file["change"])2408if change > newestChange:2409 newestChange = change24102411 self.labels[newestChange] = [output, revisions]24122413if self.verbose:2414print"Label changes:%s"% self.labels.keys()24152416# Import p4 labels as git tags. A direct mapping does not2417# exist, so assume that if all the files are at the same revision2418# then we can use that, or it's something more complicated we should2419# just ignore.2420defimportP4Labels(self, stream, p4Labels):2421if verbose:2422print"import p4 labels: "+' '.join(p4Labels)24232424 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2425 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2426iflen(validLabelRegexp) ==0:2427 validLabelRegexp = defaultLabelRegexp2428 m = re.compile(validLabelRegexp)24292430for name in p4Labels:2431 commitFound =False24322433if not m.match(name):2434if verbose:2435print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2436continue24372438if name in ignoredP4Labels:2439continue24402441 labelDetails =p4CmdList(['label',"-o", name])[0]24422443# get the most recent changelist for each file in this label2444 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2445for p in self.depotPaths])24462447if change.has_key('change'):2448# find the corresponding git commit; take the oldest commit2449 changelist =int(change['change'])2450 gitCommit =read_pipe(["git","rev-list","--max-count=1",2451"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2452iflen(gitCommit) ==0:2453print"could not find git commit for changelist%d"% changelist2454else:2455 gitCommit = gitCommit.strip()2456 commitFound =True2457# Convert from p4 time format2458try:2459 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2460exceptValueError:2461print"Could not convert label time%s"% labelDetails['Update']2462 tmwhen =124632464 when =int(time.mktime(tmwhen))2465 self.streamTag(stream, name, labelDetails, gitCommit, when)2466if verbose:2467print"p4 label%smapped to git commit%s"% (name, gitCommit)2468else:2469if verbose:2470print"Label%shas no changelists - possibly deleted?"% name24712472if not commitFound:2473# We can't import this label; don't try again as it will get very2474# expensive repeatedly fetching all the files for labels that will2475# never be imported. If the label is moved in the future, the2476# ignore will need to be removed manually.2477system(["git","config","--add","git-p4.ignoredP4Labels", name])24782479defguessProjectName(self):2480for p in self.depotPaths:2481if p.endswith("/"):2482 p = p[:-1]2483 p = p[p.strip().rfind("/") +1:]2484if not p.endswith("/"):2485 p +="/"2486return p24872488defgetBranchMapping(self):2489 lostAndFoundBranches =set()24902491 user =gitConfig("git-p4.branchUser")2492iflen(user) >0:2493 command ="branches -u%s"% user2494else:2495 command ="branches"24962497for info inp4CmdList(command):2498 details =p4Cmd(["branch","-o", info["branch"]])2499 viewIdx =02500while details.has_key("View%s"% viewIdx):2501 paths = details["View%s"% viewIdx].split(" ")2502 viewIdx = viewIdx +12503# require standard //depot/foo/... //depot/bar/... mapping2504iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2505continue2506 source = paths[0]2507 destination = paths[1]2508## HACK2509ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2510 source = source[len(self.depotPaths[0]):-4]2511 destination = destination[len(self.depotPaths[0]):-4]25122513if destination in self.knownBranches:2514if not self.silent:2515print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2516print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2517continue25182519 self.knownBranches[destination] = source25202521 lostAndFoundBranches.discard(destination)25222523if source not in self.knownBranches:2524 lostAndFoundBranches.add(source)25252526# Perforce does not strictly require branches to be defined, so we also2527# check git config for a branch list.2528#2529# Example of branch definition in git config file:2530# [git-p4]2531# branchList=main:branchA2532# branchList=main:branchB2533# branchList=branchA:branchC2534 configBranches =gitConfigList("git-p4.branchList")2535for branch in configBranches:2536if branch:2537(source, destination) = branch.split(":")2538 self.knownBranches[destination] = source25392540 lostAndFoundBranches.discard(destination)25412542if source not in self.knownBranches:2543 lostAndFoundBranches.add(source)254425452546for branch in lostAndFoundBranches:2547 self.knownBranches[branch] = branch25482549defgetBranchMappingFromGitBranches(self):2550 branches =p4BranchesInGit(self.importIntoRemotes)2551for branch in branches.keys():2552if branch =="master":2553 branch ="main"2554else:2555 branch = branch[len(self.projectName):]2556 self.knownBranches[branch] = branch25572558defupdateOptionDict(self, d):2559 option_keys = {}2560if self.keepRepoPath:2561 option_keys['keepRepoPath'] =125622563 d["options"] =' '.join(sorted(option_keys.keys()))25642565defreadOptions(self, d):2566 self.keepRepoPath = (d.has_key('options')2567and('keepRepoPath'in d['options']))25682569defgitRefForBranch(self, branch):2570if branch =="main":2571return self.refPrefix +"master"25722573iflen(branch) <=0:2574return branch25752576return self.refPrefix + self.projectName + branch25772578defgitCommitByP4Change(self, ref, change):2579if self.verbose:2580print"looking in ref "+ ref +" for change%susing bisect..."% change25812582 earliestCommit =""2583 latestCommit =parseRevision(ref)25842585while True:2586if self.verbose:2587print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2588 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2589iflen(next) ==0:2590if self.verbose:2591print"argh"2592return""2593 log =extractLogMessageFromGitCommit(next)2594 settings =extractSettingsGitLog(log)2595 currentChange =int(settings['change'])2596if self.verbose:2597print"current change%s"% currentChange25982599if currentChange == change:2600if self.verbose:2601print"found%s"% next2602return next26032604if currentChange < change:2605 earliestCommit ="^%s"% next2606else:2607 latestCommit ="%s"% next26082609return""26102611defimportNewBranch(self, branch, maxChange):2612# make fast-import flush all changes to disk and update the refs using the checkpoint2613# command so that we can try to find the branch parent in the git history2614 self.gitStream.write("checkpoint\n\n");2615 self.gitStream.flush();2616 branchPrefix = self.depotPaths[0] + branch +"/"2617range="@1,%s"% maxChange2618#print "prefix" + branchPrefix2619 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2620iflen(changes) <=0:2621return False2622 firstChange = changes[0]2623#print "first change in branch: %s" % firstChange2624 sourceBranch = self.knownBranches[branch]2625 sourceDepotPath = self.depotPaths[0] + sourceBranch2626 sourceRef = self.gitRefForBranch(sourceBranch)2627#print "source " + sourceBranch26282629 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2630#print "branch parent: %s" % branchParentChange2631 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2632iflen(gitParent) >0:2633 self.initialParents[self.gitRefForBranch(branch)] = gitParent2634#print "parent git commit: %s" % gitParent26352636 self.importChanges(changes)2637return True26382639defsearchParent(self, parent, branch, target):2640 parentFound =False2641for blob inread_pipe_lines(["git","rev-list","--reverse",2642"--no-merges", parent]):2643 blob = blob.strip()2644iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2645 parentFound =True2646if self.verbose:2647print"Found parent of%sin commit%s"% (branch, blob)2648break2649if parentFound:2650return blob2651else:2652return None26532654defimportChanges(self, changes):2655 cnt =12656for change in changes:2657 description =p4_describe(change)2658 self.updateOptionDict(description)26592660if not self.silent:2661 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2662 sys.stdout.flush()2663 cnt = cnt +126642665try:2666if self.detectBranches:2667 branches = self.splitFilesIntoBranches(description)2668for branch in branches.keys():2669## HACK --hwn2670 branchPrefix = self.depotPaths[0] + branch +"/"2671 self.branchPrefixes = [ branchPrefix ]26722673 parent =""26742675 filesForCommit = branches[branch]26762677if self.verbose:2678print"branch is%s"% branch26792680 self.updatedBranches.add(branch)26812682if branch not in self.createdBranches:2683 self.createdBranches.add(branch)2684 parent = self.knownBranches[branch]2685if parent == branch:2686 parent =""2687else:2688 fullBranch = self.projectName + branch2689if fullBranch not in self.p4BranchesInGit:2690if not self.silent:2691print("\nImporting new branch%s"% fullBranch);2692if self.importNewBranch(branch, change -1):2693 parent =""2694 self.p4BranchesInGit.append(fullBranch)2695if not self.silent:2696print("\nResuming with change%s"% change);26972698if self.verbose:2699print"parent determined through known branches:%s"% parent27002701 branch = self.gitRefForBranch(branch)2702 parent = self.gitRefForBranch(parent)27032704if self.verbose:2705print"looking for initial parent for%s; current parent is%s"% (branch, parent)27062707iflen(parent) ==0and branch in self.initialParents:2708 parent = self.initialParents[branch]2709del self.initialParents[branch]27102711 blob =None2712iflen(parent) >0:2713 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2714if self.verbose:2715print"Creating temporary branch: "+ tempBranch2716 self.commit(description, filesForCommit, tempBranch)2717 self.tempBranches.append(tempBranch)2718 self.checkpoint()2719 blob = self.searchParent(parent, branch, tempBranch)2720if blob:2721 self.commit(description, filesForCommit, branch, blob)2722else:2723if self.verbose:2724print"Parent of%snot found. Committing into head of%s"% (branch, parent)2725 self.commit(description, filesForCommit, branch, parent)2726else:2727 files = self.extractFilesFromCommit(description)2728 self.commit(description, files, self.branch,2729 self.initialParent)2730# only needed once, to connect to the previous commit2731 self.initialParent =""2732exceptIOError:2733print self.gitError.read()2734 sys.exit(1)27352736defimportHeadRevision(self, revision):2737print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27382739 details = {}2740 details["user"] ="git perforce import user"2741 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2742% (' '.join(self.depotPaths), revision))2743 details["change"] = revision2744 newestRevision =027452746 fileCnt =02747 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27482749for info inp4CmdList(["files"] + fileArgs):27502751if'code'in info and info['code'] =='error':2752 sys.stderr.write("p4 returned an error:%s\n"2753% info['data'])2754if info['data'].find("must refer to client") >=0:2755 sys.stderr.write("This particular p4 error is misleading.\n")2756 sys.stderr.write("Perhaps the depot path was misspelled.\n");2757 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2758 sys.exit(1)2759if'p4ExitCode'in info:2760 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2761 sys.exit(1)276227632764 change =int(info["change"])2765if change > newestRevision:2766 newestRevision = change27672768if info["action"]in self.delete_actions:2769# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2770#fileCnt = fileCnt + 12771continue27722773for prop in["depotFile","rev","action","type"]:2774 details["%s%s"% (prop, fileCnt)] = info[prop]27752776 fileCnt = fileCnt +127772778 details["change"] = newestRevision27792780# Use time from top-most change so that all git p4 clones of2781# the same p4 repo have the same commit SHA1s.2782 res =p4_describe(newestRevision)2783 details["time"] = res["time"]27842785 self.updateOptionDict(details)2786try:2787 self.commit(details, self.extractFilesFromCommit(details), self.branch)2788exceptIOError:2789print"IO error with git fast-import. Is your git version recent enough?"2790print self.gitError.read()279127922793defrun(self, args):2794 self.depotPaths = []2795 self.changeRange =""2796 self.previousDepotPaths = []2797 self.hasOrigin =False27982799# map from branch depot path to parent branch2800 self.knownBranches = {}2801 self.initialParents = {}28022803if self.importIntoRemotes:2804 self.refPrefix ="refs/remotes/p4/"2805else:2806 self.refPrefix ="refs/heads/p4/"28072808if self.syncWithOrigin:2809 self.hasOrigin =originP4BranchesExist()2810if self.hasOrigin:2811if not self.silent:2812print'Syncing with origin first, using "git fetch origin"'2813system("git fetch origin")28142815 branch_arg_given =bool(self.branch)2816iflen(self.branch) ==0:2817 self.branch = self.refPrefix +"master"2818ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2819system("git update-ref%srefs/heads/p4"% self.branch)2820system("git branch -D p4")28212822# accept either the command-line option, or the configuration variable2823if self.useClientSpec:2824# will use this after clone to set the variable2825 self.useClientSpec_from_options =True2826else:2827ifgitConfigBool("git-p4.useclientspec"):2828 self.useClientSpec =True2829if self.useClientSpec:2830 self.clientSpecDirs =getClientSpec()28312832# TODO: should always look at previous commits,2833# merge with previous imports, if possible.2834if args == []:2835if self.hasOrigin:2836createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28372838# branches holds mapping from branch name to sha12839 branches =p4BranchesInGit(self.importIntoRemotes)28402841# restrict to just this one, disabling detect-branches2842if branch_arg_given:2843 short = self.branch.split("/")[-1]2844if short in branches:2845 self.p4BranchesInGit = [ short ]2846else:2847 self.p4BranchesInGit = branches.keys()28482849iflen(self.p4BranchesInGit) >1:2850if not self.silent:2851print"Importing from/into multiple branches"2852 self.detectBranches =True2853for branch in branches.keys():2854 self.initialParents[self.refPrefix + branch] = \2855 branches[branch]28562857if self.verbose:2858print"branches:%s"% self.p4BranchesInGit28592860 p4Change =02861for branch in self.p4BranchesInGit:2862 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28632864 settings =extractSettingsGitLog(logMsg)28652866 self.readOptions(settings)2867if(settings.has_key('depot-paths')2868and settings.has_key('change')):2869 change =int(settings['change']) +12870 p4Change =max(p4Change, change)28712872 depotPaths =sorted(settings['depot-paths'])2873if self.previousDepotPaths == []:2874 self.previousDepotPaths = depotPaths2875else:2876 paths = []2877for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2878 prev_list = prev.split("/")2879 cur_list = cur.split("/")2880for i inrange(0,min(len(cur_list),len(prev_list))):2881if cur_list[i] <> prev_list[i]:2882 i = i -12883break28842885 paths.append("/".join(cur_list[:i +1]))28862887 self.previousDepotPaths = paths28882889if p4Change >0:2890 self.depotPaths =sorted(self.previousDepotPaths)2891 self.changeRange ="@%s,#head"% p4Change2892if not self.silent and not self.detectBranches:2893print"Performing incremental import into%sgit branch"% self.branch28942895# accept multiple ref name abbreviations:2896# refs/foo/bar/branch -> use it exactly2897# p4/branch -> prepend refs/remotes/ or refs/heads/2898# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2899if not self.branch.startswith("refs/"):2900if self.importIntoRemotes:2901 prepend ="refs/remotes/"2902else:2903 prepend ="refs/heads/"2904if not self.branch.startswith("p4/"):2905 prepend +="p4/"2906 self.branch = prepend + self.branch29072908iflen(args) ==0and self.depotPaths:2909if not self.silent:2910print"Depot paths:%s"%' '.join(self.depotPaths)2911else:2912if self.depotPaths and self.depotPaths != args:2913print("previous import used depot path%sand now%swas specified. "2914"This doesn't work!"% (' '.join(self.depotPaths),2915' '.join(args)))2916 sys.exit(1)29172918 self.depotPaths =sorted(args)29192920 revision =""2921 self.users = {}29222923# Make sure no revision specifiers are used when --changesfile2924# is specified.2925 bad_changesfile =False2926iflen(self.changesFile) >0:2927for p in self.depotPaths:2928if p.find("@") >=0or p.find("#") >=0:2929 bad_changesfile =True2930break2931if bad_changesfile:2932die("Option --changesfile is incompatible with revision specifiers")29332934 newPaths = []2935for p in self.depotPaths:2936if p.find("@") != -1:2937 atIdx = p.index("@")2938 self.changeRange = p[atIdx:]2939if self.changeRange =="@all":2940 self.changeRange =""2941elif','not in self.changeRange:2942 revision = self.changeRange2943 self.changeRange =""2944 p = p[:atIdx]2945elif p.find("#") != -1:2946 hashIdx = p.index("#")2947 revision = p[hashIdx:]2948 p = p[:hashIdx]2949elif self.previousDepotPaths == []:2950# pay attention to changesfile, if given, else import2951# the entire p4 tree at the head revision2952iflen(self.changesFile) ==0:2953 revision ="#head"29542955 p = re.sub("\.\.\.$","", p)2956if not p.endswith("/"):2957 p +="/"29582959 newPaths.append(p)29602961 self.depotPaths = newPaths29622963# --detect-branches may change this for each branch2964 self.branchPrefixes = self.depotPaths29652966 self.loadUserMapFromCache()2967 self.labels = {}2968if self.detectLabels:2969 self.getLabels();29702971if self.detectBranches:2972## FIXME - what's a P4 projectName ?2973 self.projectName = self.guessProjectName()29742975if self.hasOrigin:2976 self.getBranchMappingFromGitBranches()2977else:2978 self.getBranchMapping()2979if self.verbose:2980print"p4-git branches:%s"% self.p4BranchesInGit2981print"initial parents:%s"% self.initialParents2982for b in self.p4BranchesInGit:2983if b !="master":29842985## FIXME2986 b = b[len(self.projectName):]2987 self.createdBranches.add(b)29882989 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29902991 self.importProcess = subprocess.Popen(["git","fast-import"],2992 stdin=subprocess.PIPE,2993 stdout=subprocess.PIPE,2994 stderr=subprocess.PIPE);2995 self.gitOutput = self.importProcess.stdout2996 self.gitStream = self.importProcess.stdin2997 self.gitError = self.importProcess.stderr29982999if revision:3000 self.importHeadRevision(revision)3001else:3002 changes = []30033004iflen(self.changesFile) >0:3005 output =open(self.changesFile).readlines()3006 changeSet =set()3007for line in output:3008 changeSet.add(int(line))30093010for change in changeSet:3011 changes.append(change)30123013 changes.sort()3014else:3015# catch "git p4 sync" with no new branches, in a repo that3016# does not have any existing p4 branches3017iflen(args) ==0:3018if not self.p4BranchesInGit:3019die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30203021# The default branch is master, unless --branch is used to3022# specify something else. Make sure it exists, or complain3023# nicely about how to use --branch.3024if not self.detectBranches:3025if notbranch_exists(self.branch):3026if branch_arg_given:3027die("Error: branch%sdoes not exist."% self.branch)3028else:3029die("Error: no branch%s; perhaps specify one with --branch."%3030 self.branch)30313032if self.verbose:3033print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3034 self.changeRange)3035 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)30363037iflen(self.maxChanges) >0:3038 changes = changes[:min(int(self.maxChanges),len(changes))]30393040iflen(changes) ==0:3041if not self.silent:3042print"No changes to import!"3043else:3044if not self.silent and not self.detectBranches:3045print"Import destination:%s"% self.branch30463047 self.updatedBranches =set()30483049if not self.detectBranches:3050if args:3051# start a new branch3052 self.initialParent =""3053else:3054# build on a previous revision3055 self.initialParent =parseRevision(self.branch)30563057 self.importChanges(changes)30583059if not self.silent:3060print""3061iflen(self.updatedBranches) >0:3062 sys.stdout.write("Updated branches: ")3063for b in self.updatedBranches:3064 sys.stdout.write("%s"% b)3065 sys.stdout.write("\n")30663067ifgitConfigBool("git-p4.importLabels"):3068 self.importLabels =True30693070if self.importLabels:3071 p4Labels =getP4Labels(self.depotPaths)3072 gitTags =getGitTags()30733074 missingP4Labels = p4Labels - gitTags3075 self.importP4Labels(self.gitStream, missingP4Labels)30763077 self.gitStream.close()3078if self.importProcess.wait() !=0:3079die("fast-import failed:%s"% self.gitError.read())3080 self.gitOutput.close()3081 self.gitError.close()30823083# Cleanup temporary branches created during import3084if self.tempBranches != []:3085for branch in self.tempBranches:3086read_pipe("git update-ref -d%s"% branch)3087 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30883089# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3090# a convenient shortcut refname "p4".3091if self.importIntoRemotes:3092 head_ref = self.refPrefix +"HEAD"3093if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3094system(["git","symbolic-ref", head_ref, self.branch])30953096return True30973098classP4Rebase(Command):3099def__init__(self):3100 Command.__init__(self)3101 self.options = [3102 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3103]3104 self.importLabels =False3105 self.description = ("Fetches the latest revision from perforce and "3106+"rebases the current work (branch) against it")31073108defrun(self, args):3109 sync =P4Sync()3110 sync.importLabels = self.importLabels3111 sync.run([])31123113return self.rebase()31143115defrebase(self):3116if os.system("git update-index --refresh") !=0:3117die("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.");3118iflen(read_pipe("git diff-index HEAD --")) >0:3119die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");31203121[upstream, settings] =findUpstreamBranchPoint()3122iflen(upstream) ==0:3123die("Cannot find upstream branchpoint for rebase")31243125# the branchpoint may be p4/foo~3, so strip off the parent3126 upstream = re.sub("~[0-9]+$","", upstream)31273128print"Rebasing the current branch onto%s"% upstream3129 oldHead =read_pipe("git rev-parse HEAD").strip()3130system("git rebase%s"% upstream)3131system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3132return True31333134classP4Clone(P4Sync):3135def__init__(self):3136 P4Sync.__init__(self)3137 self.description ="Creates a new git repository and imports from Perforce into it"3138 self.usage ="usage: %prog [options] //depot/path[@revRange]"3139 self.options += [3140 optparse.make_option("--destination", dest="cloneDestination",3141 action='store', default=None,3142help="where to leave result of the clone"),3143 optparse.make_option("--bare", dest="cloneBare",3144 action="store_true", default=False),3145]3146 self.cloneDestination =None3147 self.needsGit =False3148 self.cloneBare =False31493150defdefaultDestination(self, args):3151## TODO: use common prefix of args?3152 depotPath = args[0]3153 depotDir = re.sub("(@[^@]*)$","", depotPath)3154 depotDir = re.sub("(#[^#]*)$","", depotDir)3155 depotDir = re.sub(r"\.\.\.$","", depotDir)3156 depotDir = re.sub(r"/$","", depotDir)3157return os.path.split(depotDir)[1]31583159defrun(self, args):3160iflen(args) <1:3161return False31623163if self.keepRepoPath and not self.cloneDestination:3164 sys.stderr.write("Must specify destination for --keep-path\n")3165 sys.exit(1)31663167 depotPaths = args31683169if not self.cloneDestination andlen(depotPaths) >1:3170 self.cloneDestination = depotPaths[-1]3171 depotPaths = depotPaths[:-1]31723173 self.cloneExclude = ["/"+p for p in self.cloneExclude]3174for p in depotPaths:3175if not p.startswith("//"):3176 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3177return False31783179if not self.cloneDestination:3180 self.cloneDestination = self.defaultDestination(args)31813182print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31833184if not os.path.exists(self.cloneDestination):3185 os.makedirs(self.cloneDestination)3186chdir(self.cloneDestination)31873188 init_cmd = ["git","init"]3189if self.cloneBare:3190 init_cmd.append("--bare")3191 retcode = subprocess.call(init_cmd)3192if retcode:3193raiseCalledProcessError(retcode, init_cmd)31943195if not P4Sync.run(self, depotPaths):3196return False31973198# create a master branch and check out a work tree3199ifgitBranchExists(self.branch):3200system(["git","branch","master", self.branch ])3201if not self.cloneBare:3202system(["git","checkout","-f"])3203else:3204print'Not checking out any branch, use ' \3205'"git checkout -q -b master <branch>"'32063207# auto-set this variable if invoked with --use-client-spec3208if self.useClientSpec_from_options:3209system("git config --bool git-p4.useclientspec true")32103211return True32123213classP4Branches(Command):3214def__init__(self):3215 Command.__init__(self)3216 self.options = [ ]3217 self.description = ("Shows the git branches that hold imports and their "3218+"corresponding perforce depot paths")3219 self.verbose =False32203221defrun(self, args):3222iforiginP4BranchesExist():3223createOrUpdateBranchesFromOrigin()32243225 cmdline ="git rev-parse --symbolic "3226 cmdline +=" --remotes"32273228for line inread_pipe_lines(cmdline):3229 line = line.strip()32303231if not line.startswith('p4/')or line =="p4/HEAD":3232continue3233 branch = line32343235 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3236 settings =extractSettingsGitLog(log)32373238print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3239return True32403241classHelpFormatter(optparse.IndentedHelpFormatter):3242def__init__(self):3243 optparse.IndentedHelpFormatter.__init__(self)32443245defformat_description(self, description):3246if description:3247return description +"\n"3248else:3249return""32503251defprintUsage(commands):3252print"usage:%s<command> [options]"% sys.argv[0]3253print""3254print"valid commands:%s"%", ".join(commands)3255print""3256print"Try%s<command> --help for command specific help."% sys.argv[0]3257print""32583259commands = {3260"debug": P4Debug,3261"submit": P4Submit,3262"commit": P4Submit,3263"sync": P4Sync,3264"rebase": P4Rebase,3265"clone": P4Clone,3266"rollback": P4RollBack,3267"branches": P4Branches3268}326932703271defmain():3272iflen(sys.argv[1:]) ==0:3273printUsage(commands.keys())3274 sys.exit(2)32753276 cmdName = sys.argv[1]3277try:3278 klass = commands[cmdName]3279 cmd =klass()3280exceptKeyError:3281print"unknown command%s"% cmdName3282print""3283printUsage(commands.keys())3284 sys.exit(2)32853286 options = cmd.options3287 cmd.gitdir = os.environ.get("GIT_DIR",None)32883289 args = sys.argv[2:]32903291 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3292if cmd.needsGit:3293 options.append(optparse.make_option("--git-dir", dest="gitdir"))32943295 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3296 options,3297 description = cmd.description,3298 formatter =HelpFormatter())32993300(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3301global verbose3302 verbose = cmd.verbose3303if cmd.needsGit:3304if cmd.gitdir ==None:3305 cmd.gitdir = os.path.abspath(".git")3306if notisValidGitDir(cmd.gitdir):3307 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3308if os.path.exists(cmd.gitdir):3309 cdup =read_pipe("git rev-parse --show-cdup").strip()3310iflen(cdup) >0:3311chdir(cdup);33123313if notisValidGitDir(cmd.gitdir):3314ifisValidGitDir(cmd.gitdir +"/.git"):3315 cmd.gitdir +="/.git"3316else:3317die("fatal: cannot locate git repository at%s"% cmd.gitdir)33183319 os.environ["GIT_DIR"] = cmd.gitdir33203321if not cmd.run(args):3322 parser.print_help()3323 sys.exit(2)332433253326if __name__ =='__main__':3327main()