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 depotPathLong = depotPath +"..." 506 outputList =p4CmdList(["where", depotPathLong]) 507 output =None 508for entry in outputList: 509if"depotFile"in entry: 510# Search for the base client side depot path, as long as it starts with the branch's P4 path. 511# The base path always ends with "/...". 512if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 513 output = entry 514break 515elif"data"in entry: 516 data = entry.get("data") 517 space = data.find(" ") 518if data[:space] == depotPath: 519 output = entry 520break 521if output ==None: 522return"" 523if output["code"] =="error": 524return"" 525 clientPath ="" 526if"path"in output: 527 clientPath = output.get("path") 528elif"data"in output: 529 data = output.get("data") 530 lastSpace = data.rfind(" ") 531 clientPath = data[lastSpace +1:] 532 533if clientPath.endswith("..."): 534 clientPath = clientPath[:-3] 535return clientPath 536 537defcurrentGitBranch(): 538returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 539 540defisValidGitDir(path): 541if(os.path.exists(path +"/HEAD") 542and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 543return True; 544return False 545 546defparseRevision(ref): 547returnread_pipe("git rev-parse%s"% ref).strip() 548 549defbranchExists(ref): 550 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 551 ignore_error=True) 552returnlen(rev) >0 553 554defextractLogMessageFromGitCommit(commit): 555 logMessage ="" 556 557## fixme: title is first line of commit, not 1st paragraph. 558 foundTitle =False 559for log inread_pipe_lines("git cat-file commit%s"% commit): 560if not foundTitle: 561iflen(log) ==1: 562 foundTitle =True 563continue 564 565 logMessage += log 566return logMessage 567 568defextractSettingsGitLog(log): 569 values = {} 570for line in log.split("\n"): 571 line = line.strip() 572 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 573if not m: 574continue 575 576 assignments = m.group(1).split(':') 577for a in assignments: 578 vals = a.split('=') 579 key = vals[0].strip() 580 val = ('='.join(vals[1:])).strip() 581if val.endswith('\"')and val.startswith('"'): 582 val = val[1:-1] 583 584 values[key] = val 585 586 paths = values.get("depot-paths") 587if not paths: 588 paths = values.get("depot-path") 589if paths: 590 values['depot-paths'] = paths.split(',') 591return values 592 593defgitBranchExists(branch): 594 proc = subprocess.Popen(["git","rev-parse", branch], 595 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 596return proc.wait() ==0; 597 598_gitConfig = {} 599 600defgitConfig(key): 601if not _gitConfig.has_key(key): 602 cmd = ["git","config", key ] 603 s =read_pipe(cmd, ignore_error=True) 604 _gitConfig[key] = s.strip() 605return _gitConfig[key] 606 607defgitConfigBool(key): 608"""Return a bool, using git config --bool. It is True only if the 609 variable is set to true, and False if set to false or not present 610 in the config.""" 611 612if not _gitConfig.has_key(key): 613 cmd = ["git","config","--bool", key ] 614 s =read_pipe(cmd, ignore_error=True) 615 v = s.strip() 616 _gitConfig[key] = v =="true" 617return _gitConfig[key] 618 619defgitConfigList(key): 620if not _gitConfig.has_key(key): 621 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 622 _gitConfig[key] = s.strip().split(os.linesep) 623return _gitConfig[key] 624 625defp4BranchesInGit(branchesAreInRemotes=True): 626"""Find all the branches whose names start with "p4/", looking 627 in remotes or heads as specified by the argument. Return 628 a dictionary of{ branch: revision }for each one found. 629 The branch names are the short names, without any 630 "p4/" prefix.""" 631 632 branches = {} 633 634 cmdline ="git rev-parse --symbolic " 635if branchesAreInRemotes: 636 cmdline +="--remotes" 637else: 638 cmdline +="--branches" 639 640for line inread_pipe_lines(cmdline): 641 line = line.strip() 642 643# only import to p4/ 644if not line.startswith('p4/'): 645continue 646# special symbolic ref to p4/master 647if line =="p4/HEAD": 648continue 649 650# strip off p4/ prefix 651 branch = line[len("p4/"):] 652 653 branches[branch] =parseRevision(line) 654 655return branches 656 657defbranch_exists(branch): 658"""Make sure that the given ref name really exists.""" 659 660 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 661 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 662 out, _ = p.communicate() 663if p.returncode: 664return False 665# expect exactly one line of output: the branch name 666return out.rstrip() == branch 667 668deffindUpstreamBranchPoint(head ="HEAD"): 669 branches =p4BranchesInGit() 670# map from depot-path to branch name 671 branchByDepotPath = {} 672for branch in branches.keys(): 673 tip = branches[branch] 674 log =extractLogMessageFromGitCommit(tip) 675 settings =extractSettingsGitLog(log) 676if settings.has_key("depot-paths"): 677 paths =",".join(settings["depot-paths"]) 678 branchByDepotPath[paths] ="remotes/p4/"+ branch 679 680 settings =None 681 parent =0 682while parent <65535: 683 commit = head +"~%s"% parent 684 log =extractLogMessageFromGitCommit(commit) 685 settings =extractSettingsGitLog(log) 686if settings.has_key("depot-paths"): 687 paths =",".join(settings["depot-paths"]) 688if branchByDepotPath.has_key(paths): 689return[branchByDepotPath[paths], settings] 690 691 parent = parent +1 692 693return["", settings] 694 695defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 696if not silent: 697print("Creating/updating branch(es) in%sbased on origin branch(es)" 698% localRefPrefix) 699 700 originPrefix ="origin/p4/" 701 702for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 703 line = line.strip() 704if(not line.startswith(originPrefix))or line.endswith("HEAD"): 705continue 706 707 headName = line[len(originPrefix):] 708 remoteHead = localRefPrefix + headName 709 originHead = line 710 711 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 712if(not original.has_key('depot-paths') 713or not original.has_key('change')): 714continue 715 716 update =False 717if notgitBranchExists(remoteHead): 718if verbose: 719print"creating%s"% remoteHead 720 update =True 721else: 722 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 723if settings.has_key('change') >0: 724if settings['depot-paths'] == original['depot-paths']: 725 originP4Change =int(original['change']) 726 p4Change =int(settings['change']) 727if originP4Change > p4Change: 728print("%s(%s) is newer than%s(%s). " 729"Updating p4 branch from origin." 730% (originHead, originP4Change, 731 remoteHead, p4Change)) 732 update =True 733else: 734print("Ignoring:%swas imported from%swhile " 735"%swas imported from%s" 736% (originHead,','.join(original['depot-paths']), 737 remoteHead,','.join(settings['depot-paths']))) 738 739if update: 740system("git update-ref%s %s"% (remoteHead, originHead)) 741 742deforiginP4BranchesExist(): 743returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 744 745defp4ChangesForPaths(depotPaths, changeRange, block_size): 746assert depotPaths 747assert block_size 748 749# Parse the change range into start and end 750if changeRange is None or changeRange =='': 751 changeStart ='@1' 752 changeEnd ='#head' 753else: 754 parts = changeRange.split(',') 755assertlen(parts) ==2 756 changeStart = parts[0] 757 changeEnd = parts[1] 758 759# Accumulate change numbers in a dictionary to avoid duplicates 760 changes = {} 761 762for p in depotPaths: 763# Retrieve changes a block at a time, to prevent running 764# into a MaxScanRows error from the server. 765 start = changeStart 766 end = changeEnd 767 get_another_block =True 768while get_another_block: 769 new_changes = [] 770 cmd = ['changes'] 771 cmd += ['-m',str(block_size)] 772 cmd += ["%s...%s,%s"% (p, start, end)] 773for line inp4_read_pipe_lines(cmd): 774 changeNum =int(line.split(" ")[1]) 775 new_changes.append(changeNum) 776 changes[changeNum] =True 777iflen(new_changes) == block_size: 778 get_another_block =True 779 end ='@'+str(min(new_changes)) 780else: 781 get_another_block =False 782 783 changelist = changes.keys() 784 changelist.sort() 785return changelist 786 787defp4PathStartsWith(path, prefix): 788# This method tries to remedy a potential mixed-case issue: 789# 790# If UserA adds //depot/DirA/file1 791# and UserB adds //depot/dira/file2 792# 793# we may or may not have a problem. If you have core.ignorecase=true, 794# we treat DirA and dira as the same directory 795ifgitConfigBool("core.ignorecase"): 796return path.lower().startswith(prefix.lower()) 797return path.startswith(prefix) 798 799defgetClientSpec(): 800"""Look at the p4 client spec, create a View() object that contains 801 all the mappings, and return it.""" 802 803 specList =p4CmdList("client -o") 804iflen(specList) !=1: 805die('Output from "client -o" is%dlines, expecting 1'% 806len(specList)) 807 808# dictionary of all client parameters 809 entry = specList[0] 810 811# the //client/ name 812 client_name = entry["Client"] 813 814# just the keys that start with "View" 815 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 816 817# hold this new View 818 view =View(client_name) 819 820# append the lines, in order, to the view 821for view_num inrange(len(view_keys)): 822 k ="View%d"% view_num 823if k not in view_keys: 824die("Expected view key%smissing"% k) 825 view.append(entry[k]) 826 827return view 828 829defgetClientRoot(): 830"""Grab the client directory.""" 831 832 output =p4CmdList("client -o") 833iflen(output) !=1: 834die('Output from "client -o" is%dlines, expecting 1'%len(output)) 835 836 entry = output[0] 837if"Root"not in entry: 838die('Client has no "Root"') 839 840return entry["Root"] 841 842# 843# P4 wildcards are not allowed in filenames. P4 complains 844# if you simply add them, but you can force it with "-f", in 845# which case it translates them into %xx encoding internally. 846# 847defwildcard_decode(path): 848# Search for and fix just these four characters. Do % last so 849# that fixing it does not inadvertently create new %-escapes. 850# Cannot have * in a filename in windows; untested as to 851# what p4 would do in such a case. 852if not platform.system() =="Windows": 853 path = path.replace("%2A","*") 854 path = path.replace("%23","#") \ 855.replace("%40","@") \ 856.replace("%25","%") 857return path 858 859defwildcard_encode(path): 860# do % first to avoid double-encoding the %s introduced here 861 path = path.replace("%","%25") \ 862.replace("*","%2A") \ 863.replace("#","%23") \ 864.replace("@","%40") 865return path 866 867defwildcard_present(path): 868 m = re.search("[*#@%]", path) 869return m is not None 870 871class Command: 872def__init__(self): 873 self.usage ="usage: %prog [options]" 874 self.needsGit =True 875 self.verbose =False 876 877class P4UserMap: 878def__init__(self): 879 self.userMapFromPerforceServer =False 880 self.myP4UserId =None 881 882defp4UserId(self): 883if self.myP4UserId: 884return self.myP4UserId 885 886 results =p4CmdList("user -o") 887for r in results: 888if r.has_key('User'): 889 self.myP4UserId = r['User'] 890return r['User'] 891die("Could not find your p4 user id") 892 893defp4UserIsMe(self, p4User): 894# return True if the given p4 user is actually me 895 me = self.p4UserId() 896if not p4User or p4User != me: 897return False 898else: 899return True 900 901defgetUserCacheFilename(self): 902 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 903return home +"/.gitp4-usercache.txt" 904 905defgetUserMapFromPerforceServer(self): 906if self.userMapFromPerforceServer: 907return 908 self.users = {} 909 self.emails = {} 910 911for output inp4CmdList("users"): 912if not output.has_key("User"): 913continue 914 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 915 self.emails[output["Email"]] = output["User"] 916 917 918 s ='' 919for(key, val)in self.users.items(): 920 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 921 922open(self.getUserCacheFilename(),"wb").write(s) 923 self.userMapFromPerforceServer =True 924 925defloadUserMapFromCache(self): 926 self.users = {} 927 self.userMapFromPerforceServer =False 928try: 929 cache =open(self.getUserCacheFilename(),"rb") 930 lines = cache.readlines() 931 cache.close() 932for line in lines: 933 entry = line.strip().split("\t") 934 self.users[entry[0]] = entry[1] 935exceptIOError: 936 self.getUserMapFromPerforceServer() 937 938classP4Debug(Command): 939def__init__(self): 940 Command.__init__(self) 941 self.options = [] 942 self.description ="A tool to debug the output of p4 -G." 943 self.needsGit =False 944 945defrun(self, args): 946 j =0 947for output inp4CmdList(args): 948print'Element:%d'% j 949 j +=1 950print output 951return True 952 953classP4RollBack(Command): 954def__init__(self): 955 Command.__init__(self) 956 self.options = [ 957 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 958] 959 self.description ="A tool to debug the multi-branch import. Don't use :)" 960 self.rollbackLocalBranches =False 961 962defrun(self, args): 963iflen(args) !=1: 964return False 965 maxChange =int(args[0]) 966 967if"p4ExitCode"inp4Cmd("changes -m 1"): 968die("Problems executing p4"); 969 970if self.rollbackLocalBranches: 971 refPrefix ="refs/heads/" 972 lines =read_pipe_lines("git rev-parse --symbolic --branches") 973else: 974 refPrefix ="refs/remotes/" 975 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 976 977for line in lines: 978if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 979 line = line.strip() 980 ref = refPrefix + line 981 log =extractLogMessageFromGitCommit(ref) 982 settings =extractSettingsGitLog(log) 983 984 depotPaths = settings['depot-paths'] 985 change = settings['change'] 986 987 changed =False 988 989iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 990for p in depotPaths]))) ==0: 991print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 992system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 993continue 994 995while change andint(change) > maxChange: 996 changed =True 997if self.verbose: 998print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 999system("git update-ref%s\"%s^\""% (ref, ref))1000 log =extractLogMessageFromGitCommit(ref)1001 settings =extractSettingsGitLog(log)100210031004 depotPaths = settings['depot-paths']1005 change = settings['change']10061007if changed:1008print"%srewound to%s"% (ref, change)10091010return True10111012classP4Submit(Command, P4UserMap):10131014 conflict_behavior_choices = ("ask","skip","quit")10151016def__init__(self):1017 Command.__init__(self)1018 P4UserMap.__init__(self)1019 self.options = [1020 optparse.make_option("--origin", dest="origin"),1021 optparse.make_option("-M", dest="detectRenames", action="store_true"),1022# preserve the user, requires relevant p4 permissions1023 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1024 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1025 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1026 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1027 optparse.make_option("--conflict", dest="conflict_behavior",1028 choices=self.conflict_behavior_choices),1029 optparse.make_option("--branch", dest="branch"),1030]1031 self.description ="Submit changes from git to the perforce depot."1032 self.usage +=" [name of git branch to submit into perforce depot]"1033 self.origin =""1034 self.detectRenames =False1035 self.preserveUser =gitConfigBool("git-p4.preserveUser")1036 self.dry_run =False1037 self.prepare_p4_only =False1038 self.conflict_behavior =None1039 self.isWindows = (platform.system() =="Windows")1040 self.exportLabels =False1041 self.p4HasMoveCommand =p4_has_move_command()1042 self.branch =None10431044defcheck(self):1045iflen(p4CmdList("opened ...")) >0:1046die("You have files opened with perforce! Close them before starting the sync.")10471048defseparate_jobs_from_description(self, message):1049"""Extract and return a possible Jobs field in the commit1050 message. It goes into a separate section in the p4 change1051 specification.10521053 A jobs line starts with "Jobs:" and looks like a new field1054 in a form. Values are white-space separated on the same1055 line or on following lines that start with a tab.10561057 This does not parse and extract the full git commit message1058 like a p4 form. It just sees the Jobs: line as a marker1059 to pass everything from then on directly into the p4 form,1060 but outside the description section.10611062 Return a tuple (stripped log message, jobs string)."""10631064 m = re.search(r'^Jobs:', message, re.MULTILINE)1065if m is None:1066return(message,None)10671068 jobtext = message[m.start():]1069 stripped_message = message[:m.start()].rstrip()1070return(stripped_message, jobtext)10711072defprepareLogMessage(self, template, message, jobs):1073"""Edits the template returned from "p4 change -o" to insert1074 the message in the Description field, and the jobs text in1075 the Jobs field."""1076 result =""10771078 inDescriptionSection =False10791080for line in template.split("\n"):1081if line.startswith("#"):1082 result += line +"\n"1083continue10841085if inDescriptionSection:1086if line.startswith("Files:")or line.startswith("Jobs:"):1087 inDescriptionSection =False1088# insert Jobs section1089if jobs:1090 result += jobs +"\n"1091else:1092continue1093else:1094if line.startswith("Description:"):1095 inDescriptionSection =True1096 line +="\n"1097for messageLine in message.split("\n"):1098 line +="\t"+ messageLine +"\n"10991100 result += line +"\n"11011102return result11031104defpatchRCSKeywords(self,file, pattern):1105# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1106(handle, outFileName) = tempfile.mkstemp(dir='.')1107try:1108 outFile = os.fdopen(handle,"w+")1109 inFile =open(file,"r")1110 regexp = re.compile(pattern, re.VERBOSE)1111for line in inFile.readlines():1112 line = regexp.sub(r'$\1$', line)1113 outFile.write(line)1114 inFile.close()1115 outFile.close()1116# Forcibly overwrite the original file1117 os.unlink(file)1118 shutil.move(outFileName,file)1119except:1120# cleanup our temporary file1121 os.unlink(outFileName)1122print"Failed to strip RCS keywords in%s"%file1123raise11241125print"Patched up RCS keywords in%s"%file11261127defp4UserForCommit(self,id):1128# Return the tuple (perforce user,git email) for a given git commit id1129 self.getUserMapFromPerforceServer()1130 gitEmail =read_pipe(["git","log","--max-count=1",1131"--format=%ae",id])1132 gitEmail = gitEmail.strip()1133if not self.emails.has_key(gitEmail):1134return(None,gitEmail)1135else:1136return(self.emails[gitEmail],gitEmail)11371138defcheckValidP4Users(self,commits):1139# check if any git authors cannot be mapped to p4 users1140foridin commits:1141(user,email) = self.p4UserForCommit(id)1142if not user:1143 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1144ifgitConfigBool("git-p4.allowMissingP4Users"):1145print"%s"% msg1146else:1147die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11481149deflastP4Changelist(self):1150# Get back the last changelist number submitted in this client spec. This1151# then gets used to patch up the username in the change. If the same1152# client spec is being used by multiple processes then this might go1153# wrong.1154 results =p4CmdList("client -o")# find the current client1155 client =None1156for r in results:1157if r.has_key('Client'):1158 client = r['Client']1159break1160if not client:1161die("could not get client spec")1162 results =p4CmdList(["changes","-c", client,"-m","1"])1163for r in results:1164if r.has_key('change'):1165return r['change']1166die("Could not get changelist number for last submit - cannot patch up user details")11671168defmodifyChangelistUser(self, changelist, newUser):1169# fixup the user field of a changelist after it has been submitted.1170 changes =p4CmdList("change -o%s"% changelist)1171iflen(changes) !=1:1172die("Bad output from p4 change modifying%sto user%s"%1173(changelist, newUser))11741175 c = changes[0]1176if c['User'] == newUser:return# nothing to do1177 c['User'] = newUser1178input= marshal.dumps(c)11791180 result =p4CmdList("change -f -i", stdin=input)1181for r in result:1182if r.has_key('code'):1183if r['code'] =='error':1184die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1185if r.has_key('data'):1186print("Updated user field for changelist%sto%s"% (changelist, newUser))1187return1188die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11891190defcanChangeChangelists(self):1191# check to see if we have p4 admin or super-user permissions, either of1192# which are required to modify changelists.1193 results =p4CmdList(["protects", self.depotPath])1194for r in results:1195if r.has_key('perm'):1196if r['perm'] =='admin':1197return11198if r['perm'] =='super':1199return11200return012011202defprepareSubmitTemplate(self):1203"""Run "p4 change -o" to grab a change specification template.1204 This does not use "p4 -G", as it is nice to keep the submission1205 template in original order, since a human might edit it.12061207 Remove lines in the Files section that show changes to files1208 outside the depot path we're committing into."""12091210 template =""1211 inFilesSection =False1212for line inp4_read_pipe_lines(['change','-o']):1213if line.endswith("\r\n"):1214 line = line[:-2] +"\n"1215if inFilesSection:1216if line.startswith("\t"):1217# path starts and ends with a tab1218 path = line[1:]1219 lastTab = path.rfind("\t")1220if lastTab != -1:1221 path = path[:lastTab]1222if notp4PathStartsWith(path, self.depotPath):1223continue1224else:1225 inFilesSection =False1226else:1227if line.startswith("Files:"):1228 inFilesSection =True12291230 template += line12311232return template12331234defedit_template(self, template_file):1235"""Invoke the editor to let the user change the submission1236 message. Return true if okay to continue with the submit."""12371238# if configured to skip the editing part, just submit1239ifgitConfigBool("git-p4.skipSubmitEdit"):1240return True12411242# look at the modification time, to check later if the user saved1243# the file1244 mtime = os.stat(template_file).st_mtime12451246# invoke the editor1247if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1248 editor = os.environ.get("P4EDITOR")1249else:1250 editor =read_pipe("git var GIT_EDITOR").strip()1251system([editor, template_file])12521253# If the file was not saved, prompt to see if this patch should1254# be skipped. But skip this verification step if configured so.1255ifgitConfigBool("git-p4.skipSubmitEditCheck"):1256return True12571258# modification time updated means user saved the file1259if os.stat(template_file).st_mtime > mtime:1260return True12611262while True:1263 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1264if response =='y':1265return True1266if response =='n':1267return False12681269defget_diff_description(self, editedFiles, filesToAdd):1270# diff1271if os.environ.has_key("P4DIFF"):1272del(os.environ["P4DIFF"])1273 diff =""1274for editedFile in editedFiles:1275 diff +=p4_read_pipe(['diff','-du',1276wildcard_encode(editedFile)])12771278# new file diff1279 newdiff =""1280for newFile in filesToAdd:1281 newdiff +="==== new file ====\n"1282 newdiff +="--- /dev/null\n"1283 newdiff +="+++%s\n"% newFile1284 f =open(newFile,"r")1285for line in f.readlines():1286 newdiff +="+"+ line1287 f.close()12881289return(diff + newdiff).replace('\r\n','\n')12901291defapplyCommit(self,id):1292"""Apply one commit, return True if it succeeded."""12931294print"Applying",read_pipe(["git","show","-s",1295"--format=format:%h%s",id])12961297(p4User, gitEmail) = self.p4UserForCommit(id)12981299 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1300 filesToAdd =set()1301 filesToDelete =set()1302 editedFiles =set()1303 pureRenameCopy =set()1304 filesToChangeExecBit = {}13051306for line in diff:1307 diff =parseDiffTreeEntry(line)1308 modifier = diff['status']1309 path = diff['src']1310if modifier =="M":1311p4_edit(path)1312ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1313 filesToChangeExecBit[path] = diff['dst_mode']1314 editedFiles.add(path)1315elif modifier =="A":1316 filesToAdd.add(path)1317 filesToChangeExecBit[path] = diff['dst_mode']1318if path in filesToDelete:1319 filesToDelete.remove(path)1320elif modifier =="D":1321 filesToDelete.add(path)1322if path in filesToAdd:1323 filesToAdd.remove(path)1324elif modifier =="C":1325 src, dest = diff['src'], diff['dst']1326p4_integrate(src, dest)1327 pureRenameCopy.add(dest)1328if diff['src_sha1'] != diff['dst_sha1']:1329p4_edit(dest)1330 pureRenameCopy.discard(dest)1331ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1332p4_edit(dest)1333 pureRenameCopy.discard(dest)1334 filesToChangeExecBit[dest] = diff['dst_mode']1335if self.isWindows:1336# turn off read-only attribute1337 os.chmod(dest, stat.S_IWRITE)1338 os.unlink(dest)1339 editedFiles.add(dest)1340elif modifier =="R":1341 src, dest = diff['src'], diff['dst']1342if self.p4HasMoveCommand:1343p4_edit(src)# src must be open before move1344p4_move(src, dest)# opens for (move/delete, move/add)1345else:1346p4_integrate(src, dest)1347if diff['src_sha1'] != diff['dst_sha1']:1348p4_edit(dest)1349else:1350 pureRenameCopy.add(dest)1351ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1352if not self.p4HasMoveCommand:1353p4_edit(dest)# with move: already open, writable1354 filesToChangeExecBit[dest] = diff['dst_mode']1355if not self.p4HasMoveCommand:1356if self.isWindows:1357 os.chmod(dest, stat.S_IWRITE)1358 os.unlink(dest)1359 filesToDelete.add(src)1360 editedFiles.add(dest)1361else:1362die("unknown modifier%sfor%s"% (modifier, path))13631364 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1365 patchcmd = diffcmd +" | git apply "1366 tryPatchCmd = patchcmd +"--check -"1367 applyPatchCmd = patchcmd +"--check --apply -"1368 patch_succeeded =True13691370if os.system(tryPatchCmd) !=0:1371 fixed_rcs_keywords =False1372 patch_succeeded =False1373print"Unfortunately applying the change failed!"13741375# Patch failed, maybe it's just RCS keyword woes. Look through1376# the patch to see if that's possible.1377ifgitConfigBool("git-p4.attemptRCSCleanup"):1378file=None1379 pattern =None1380 kwfiles = {}1381forfilein editedFiles | filesToDelete:1382# did this file's delta contain RCS keywords?1383 pattern =p4_keywords_regexp_for_file(file)13841385if pattern:1386# this file is a possibility...look for RCS keywords.1387 regexp = re.compile(pattern, re.VERBOSE)1388for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1389if regexp.search(line):1390if verbose:1391print"got keyword match on%sin%sin%s"% (pattern, line,file)1392 kwfiles[file] = pattern1393break13941395forfilein kwfiles:1396if verbose:1397print"zapping%swith%s"% (line,pattern)1398# File is being deleted, so not open in p4. Must1399# disable the read-only bit on windows.1400if self.isWindows andfilenot in editedFiles:1401 os.chmod(file, stat.S_IWRITE)1402 self.patchRCSKeywords(file, kwfiles[file])1403 fixed_rcs_keywords =True14041405if fixed_rcs_keywords:1406print"Retrying the patch with RCS keywords cleaned up"1407if os.system(tryPatchCmd) ==0:1408 patch_succeeded =True14091410if not patch_succeeded:1411for f in editedFiles:1412p4_revert(f)1413return False14141415#1416# Apply the patch for real, and do add/delete/+x handling.1417#1418system(applyPatchCmd)14191420for f in filesToAdd:1421p4_add(f)1422for f in filesToDelete:1423p4_revert(f)1424p4_delete(f)14251426# Set/clear executable bits1427for f in filesToChangeExecBit.keys():1428 mode = filesToChangeExecBit[f]1429setP4ExecBit(f, mode)14301431#1432# Build p4 change description, starting with the contents1433# of the git commit message.1434#1435 logMessage =extractLogMessageFromGitCommit(id)1436 logMessage = logMessage.strip()1437(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14381439 template = self.prepareSubmitTemplate()1440 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14411442if self.preserveUser:1443 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14441445if self.checkAuthorship and not self.p4UserIsMe(p4User):1446 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1447 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1448 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"14491450 separatorLine ="######## everything below this line is just the diff #######\n"1451if not self.prepare_p4_only:1452 submitTemplate += separatorLine1453 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)14541455(handle, fileName) = tempfile.mkstemp()1456 tmpFile = os.fdopen(handle,"w+b")1457if self.isWindows:1458 submitTemplate = submitTemplate.replace("\n","\r\n")1459 tmpFile.write(submitTemplate)1460 tmpFile.close()14611462if self.prepare_p4_only:1463#1464# Leave the p4 tree prepared, and the submit template around1465# and let the user decide what to do next1466#1467print1468print"P4 workspace prepared for submission."1469print"To submit or revert, go to client workspace"1470print" "+ self.clientPath1471print1472print"To submit, use\"p4 submit\"to write a new description,"1473print"or\"p4 submit -i <%s\"to use the one prepared by" \1474"\"git p4\"."% fileName1475print"You can delete the file\"%s\"when finished."% fileName14761477if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1478print"To preserve change ownership by user%s, you must\n" \1479"do\"p4 change -f <change>\"after submitting and\n" \1480"edit the User field."1481if pureRenameCopy:1482print"After submitting, renamed files must be re-synced."1483print"Invoke\"p4 sync -f\"on each of these files:"1484for f in pureRenameCopy:1485print" "+ f14861487print1488print"To revert the changes, use\"p4 revert ...\", and delete"1489print"the submit template file\"%s\""% fileName1490if filesToAdd:1491print"Since the commit adds new files, they must be deleted:"1492for f in filesToAdd:1493print" "+ f1494print1495return True14961497#1498# Let the user edit the change description, then submit it.1499#1500if self.edit_template(fileName):1501# read the edited message and submit1502 ret =True1503 tmpFile =open(fileName,"rb")1504 message = tmpFile.read()1505 tmpFile.close()1506if self.isWindows:1507 message = message.replace("\r\n","\n")1508 submitTemplate = message[:message.index(separatorLine)]1509p4_write_pipe(['submit','-i'], submitTemplate)15101511if self.preserveUser:1512if p4User:1513# Get last changelist number. Cannot easily get it from1514# the submit command output as the output is1515# unmarshalled.1516 changelist = self.lastP4Changelist()1517 self.modifyChangelistUser(changelist, p4User)15181519# The rename/copy happened by applying a patch that created a1520# new file. This leaves it writable, which confuses p4.1521for f in pureRenameCopy:1522p4_sync(f,"-f")15231524else:1525# skip this patch1526 ret =False1527print"Submission cancelled, undoing p4 changes."1528for f in editedFiles:1529p4_revert(f)1530for f in filesToAdd:1531p4_revert(f)1532 os.remove(f)1533for f in filesToDelete:1534p4_revert(f)15351536 os.remove(fileName)1537return ret15381539# Export git tags as p4 labels. Create a p4 label and then tag1540# with that.1541defexportGitTags(self, gitTags):1542 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1543iflen(validLabelRegexp) ==0:1544 validLabelRegexp = defaultLabelRegexp1545 m = re.compile(validLabelRegexp)15461547for name in gitTags:15481549if not m.match(name):1550if verbose:1551print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1552continue15531554# Get the p4 commit this corresponds to1555 logMessage =extractLogMessageFromGitCommit(name)1556 values =extractSettingsGitLog(logMessage)15571558if not values.has_key('change'):1559# a tag pointing to something not sent to p4; ignore1560if verbose:1561print"git tag%sdoes not give a p4 commit"% name1562continue1563else:1564 changelist = values['change']15651566# Get the tag details.1567 inHeader =True1568 isAnnotated =False1569 body = []1570for l inread_pipe_lines(["git","cat-file","-p", name]):1571 l = l.strip()1572if inHeader:1573if re.match(r'tag\s+', l):1574 isAnnotated =True1575elif re.match(r'\s*$', l):1576 inHeader =False1577continue1578else:1579 body.append(l)15801581if not isAnnotated:1582 body = ["lightweight tag imported by git p4\n"]15831584# Create the label - use the same view as the client spec we are using1585 clientSpec =getClientSpec()15861587 labelTemplate ="Label:%s\n"% name1588 labelTemplate +="Description:\n"1589for b in body:1590 labelTemplate +="\t"+ b +"\n"1591 labelTemplate +="View:\n"1592for depot_side in clientSpec.mappings:1593 labelTemplate +="\t%s\n"% depot_side15941595if self.dry_run:1596print"Would create p4 label%sfor tag"% name1597elif self.prepare_p4_only:1598print"Not creating p4 label%sfor tag due to option" \1599" --prepare-p4-only"% name1600else:1601p4_write_pipe(["label","-i"], labelTemplate)16021603# Use the label1604p4_system(["tag","-l", name] +1605["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16061607if verbose:1608print"created p4 label for tag%s"% name16091610defrun(self, args):1611iflen(args) ==0:1612 self.master =currentGitBranch()1613iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1614die("Detecting current git branch failed!")1615eliflen(args) ==1:1616 self.master = args[0]1617if notbranchExists(self.master):1618die("Branch%sdoes not exist"% self.master)1619else:1620return False16211622 allowSubmit =gitConfig("git-p4.allowSubmit")1623iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1624die("%sis not in git-p4.allowSubmit"% self.master)16251626[upstream, settings] =findUpstreamBranchPoint()1627 self.depotPath = settings['depot-paths'][0]1628iflen(self.origin) ==0:1629 self.origin = upstream16301631if self.preserveUser:1632if not self.canChangeChangelists():1633die("Cannot preserve user names without p4 super-user or admin permissions")16341635# if not set from the command line, try the config file1636if self.conflict_behavior is None:1637 val =gitConfig("git-p4.conflict")1638if val:1639if val not in self.conflict_behavior_choices:1640die("Invalid value '%s' for config git-p4.conflict"% val)1641else:1642 val ="ask"1643 self.conflict_behavior = val16441645if self.verbose:1646print"Origin branch is "+ self.origin16471648iflen(self.depotPath) ==0:1649print"Internal error: cannot locate perforce depot path from existing branches"1650 sys.exit(128)16511652 self.useClientSpec =False1653ifgitConfigBool("git-p4.useclientspec"):1654 self.useClientSpec =True1655if self.useClientSpec:1656 self.clientSpecDirs =getClientSpec()16571658# Check for the existance of P4 branches1659 branchesDetected = (len(p4BranchesInGit().keys()) >1)16601661if self.useClientSpec and not branchesDetected:1662# all files are relative to the client spec1663 self.clientPath =getClientRoot()1664else:1665 self.clientPath =p4Where(self.depotPath)16661667if self.clientPath =="":1668die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)16691670print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1671 self.oldWorkingDirectory = os.getcwd()16721673# ensure the clientPath exists1674 new_client_dir =False1675if not os.path.exists(self.clientPath):1676 new_client_dir =True1677 os.makedirs(self.clientPath)16781679chdir(self.clientPath, is_client_path=True)1680if self.dry_run:1681print"Would synchronize p4 checkout in%s"% self.clientPath1682else:1683print"Synchronizing p4 checkout..."1684if new_client_dir:1685# old one was destroyed, and maybe nobody told p41686p4_sync("...","-f")1687else:1688p4_sync("...")1689 self.check()16901691 commits = []1692for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1693 commits.append(line.strip())1694 commits.reverse()16951696if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1697 self.checkAuthorship =False1698else:1699 self.checkAuthorship =True17001701if self.preserveUser:1702 self.checkValidP4Users(commits)17031704#1705# Build up a set of options to be passed to diff when1706# submitting each commit to p4.1707#1708if self.detectRenames:1709# command-line -M arg1710 self.diffOpts ="-M"1711else:1712# If not explicitly set check the config variable1713 detectRenames =gitConfig("git-p4.detectRenames")17141715if detectRenames.lower() =="false"or detectRenames =="":1716 self.diffOpts =""1717elif detectRenames.lower() =="true":1718 self.diffOpts ="-M"1719else:1720 self.diffOpts ="-M%s"% detectRenames17211722# no command-line arg for -C or --find-copies-harder, just1723# config variables1724 detectCopies =gitConfig("git-p4.detectCopies")1725if detectCopies.lower() =="false"or detectCopies =="":1726pass1727elif detectCopies.lower() =="true":1728 self.diffOpts +=" -C"1729else:1730 self.diffOpts +=" -C%s"% detectCopies17311732ifgitConfigBool("git-p4.detectCopiesHarder"):1733 self.diffOpts +=" --find-copies-harder"17341735#1736# Apply the commits, one at a time. On failure, ask if should1737# continue to try the rest of the patches, or quit.1738#1739if self.dry_run:1740print"Would apply"1741 applied = []1742 last =len(commits) -11743for i, commit inenumerate(commits):1744if self.dry_run:1745print" ",read_pipe(["git","show","-s",1746"--format=format:%h%s", commit])1747 ok =True1748else:1749 ok = self.applyCommit(commit)1750if ok:1751 applied.append(commit)1752else:1753if self.prepare_p4_only and i < last:1754print"Processing only the first commit due to option" \1755" --prepare-p4-only"1756break1757if i < last:1758 quit =False1759while True:1760# prompt for what to do, or use the option/variable1761if self.conflict_behavior =="ask":1762print"What do you want to do?"1763 response =raw_input("[s]kip this commit but apply"1764" the rest, or [q]uit? ")1765if not response:1766continue1767elif self.conflict_behavior =="skip":1768 response ="s"1769elif self.conflict_behavior =="quit":1770 response ="q"1771else:1772die("Unknown conflict_behavior '%s'"%1773 self.conflict_behavior)17741775if response[0] =="s":1776print"Skipping this commit, but applying the rest"1777break1778if response[0] =="q":1779print"Quitting"1780 quit =True1781break1782if quit:1783break17841785chdir(self.oldWorkingDirectory)17861787if self.dry_run:1788pass1789elif self.prepare_p4_only:1790pass1791eliflen(commits) ==len(applied):1792print"All commits applied!"17931794 sync =P4Sync()1795if self.branch:1796 sync.branch = self.branch1797 sync.run([])17981799 rebase =P4Rebase()1800 rebase.rebase()18011802else:1803iflen(applied) ==0:1804print"No commits applied."1805else:1806print"Applied only the commits marked with '*':"1807for c in commits:1808if c in applied:1809 star ="*"1810else:1811 star =" "1812print star,read_pipe(["git","show","-s",1813"--format=format:%h%s", c])1814print"You will have to do 'git p4 sync' and rebase."18151816ifgitConfigBool("git-p4.exportLabels"):1817 self.exportLabels =True18181819if self.exportLabels:1820 p4Labels =getP4Labels(self.depotPath)1821 gitTags =getGitTags()18221823 missingGitTags = gitTags - p4Labels1824 self.exportGitTags(missingGitTags)18251826# exit with error unless everything applied perfectly1827iflen(commits) !=len(applied):1828 sys.exit(1)18291830return True18311832classView(object):1833"""Represent a p4 view ("p4 help views"), and map files in a1834 repo according to the view."""18351836def__init__(self, client_name):1837 self.mappings = []1838 self.client_prefix ="//%s/"% client_name1839# cache results of "p4 where" to lookup client file locations1840 self.client_spec_path_cache = {}18411842defappend(self, view_line):1843"""Parse a view line, splitting it into depot and client1844 sides. Append to self.mappings, preserving order. This1845 is only needed for tag creation."""18461847# Split the view line into exactly two words. P4 enforces1848# structure on these lines that simplifies this quite a bit.1849#1850# Either or both words may be double-quoted.1851# Single quotes do not matter.1852# Double-quote marks cannot occur inside the words.1853# A + or - prefix is also inside the quotes.1854# There are no quotes unless they contain a space.1855# The line is already white-space stripped.1856# The two words are separated by a single space.1857#1858if view_line[0] =='"':1859# First word is double quoted. Find its end.1860 close_quote_index = view_line.find('"',1)1861if close_quote_index <=0:1862die("No first-word closing quote found:%s"% view_line)1863 depot_side = view_line[1:close_quote_index]1864# skip closing quote and space1865 rhs_index = close_quote_index +1+11866else:1867 space_index = view_line.find(" ")1868if space_index <=0:1869die("No word-splitting space found:%s"% view_line)1870 depot_side = view_line[0:space_index]1871 rhs_index = space_index +118721873# prefix + means overlay on previous mapping1874if depot_side.startswith("+"):1875 depot_side = depot_side[1:]18761877# prefix - means exclude this path, leave out of mappings1878 exclude =False1879if depot_side.startswith("-"):1880 exclude =True1881 depot_side = depot_side[1:]18821883if not exclude:1884 self.mappings.append(depot_side)18851886defconvert_client_path(self, clientFile):1887# chop off //client/ part to make it relative1888if not clientFile.startswith(self.client_prefix):1889die("No prefix '%s' on clientFile '%s'"%1890(self.client_prefix, clientFile))1891return clientFile[len(self.client_prefix):]18921893defupdate_client_spec_path_cache(self, files):1894""" Caching file paths by "p4 where" batch query """18951896# List depot file paths exclude that already cached1897 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]18981899iflen(fileArgs) ==0:1900return# All files in cache19011902 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1903for res in where_result:1904if"code"in res and res["code"] =="error":1905# assume error is "... file(s) not in client view"1906continue1907if"clientFile"not in res:1908die("No clientFile in 'p4 where' output")1909if"unmap"in res:1910# it will list all of them, but only one not unmap-ped1911continue1912 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19131914# not found files or unmap files set to ""1915for depotFile in fileArgs:1916if depotFile not in self.client_spec_path_cache:1917 self.client_spec_path_cache[depotFile] =""19181919defmap_in_client(self, depot_path):1920"""Return the relative location in the client where this1921 depot file should live. Returns "" if the file should1922 not be mapped in the client."""19231924if depot_path in self.client_spec_path_cache:1925return self.client_spec_path_cache[depot_path]19261927die("Error:%sis not found in client spec path"% depot_path )1928return""19291930classP4Sync(Command, P4UserMap):1931 delete_actions = ("delete","move/delete","purge")19321933def__init__(self):1934 Command.__init__(self)1935 P4UserMap.__init__(self)1936 self.options = [1937 optparse.make_option("--branch", dest="branch"),1938 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1939 optparse.make_option("--changesfile", dest="changesFile"),1940 optparse.make_option("--silent", dest="silent", action="store_true"),1941 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1942 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1943 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1944help="Import into refs/heads/ , not refs/remotes"),1945 optparse.make_option("--max-changes", dest="maxChanges",1946help="Maximum number of changes to import"),1947 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",1948help="Internal block size to use when iteratively calling p4 changes"),1949 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1950help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1951 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1952help="Only sync files that are included in the Perforce Client Spec"),1953 optparse.make_option("-/", dest="cloneExclude",1954 action="append",type="string",1955help="exclude depot path"),1956]1957 self.description ="""Imports from Perforce into a git repository.\n1958 example:1959 //depot/my/project/ -- to import the current head1960 //depot/my/project/@all -- to import everything1961 //depot/my/project/@1,6 -- to import only from revision 1 to 619621963 (a ... is not needed in the path p4 specification, it's added implicitly)"""19641965 self.usage +=" //depot/path[@revRange]"1966 self.silent =False1967 self.createdBranches =set()1968 self.committedChanges =set()1969 self.branch =""1970 self.detectBranches =False1971 self.detectLabels =False1972 self.importLabels =False1973 self.changesFile =""1974 self.syncWithOrigin =True1975 self.importIntoRemotes =True1976 self.maxChanges =""1977 self.changes_block_size =5001978 self.keepRepoPath =False1979 self.depotPaths =None1980 self.p4BranchesInGit = []1981 self.cloneExclude = []1982 self.useClientSpec =False1983 self.useClientSpec_from_options =False1984 self.clientSpecDirs =None1985 self.tempBranches = []1986 self.tempBranchLocation ="git-p4-tmp"19871988ifgitConfig("git-p4.syncFromOrigin") =="false":1989 self.syncWithOrigin =False19901991# This is required for the "append" cloneExclude action1992defensure_value(self, attr, value):1993if nothasattr(self, attr)orgetattr(self, attr)is None:1994setattr(self, attr, value)1995returngetattr(self, attr)19961997# Force a checkpoint in fast-import and wait for it to finish1998defcheckpoint(self):1999 self.gitStream.write("checkpoint\n\n")2000 self.gitStream.write("progress checkpoint\n\n")2001 out = self.gitOutput.readline()2002if self.verbose:2003print"checkpoint finished: "+ out20042005defextractFilesFromCommit(self, commit):2006 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2007for path in self.cloneExclude]2008 files = []2009 fnum =02010while commit.has_key("depotFile%s"% fnum):2011 path = commit["depotFile%s"% fnum]20122013if[p for p in self.cloneExclude2014ifp4PathStartsWith(path, p)]:2015 found =False2016else:2017 found = [p for p in self.depotPaths2018ifp4PathStartsWith(path, p)]2019if not found:2020 fnum = fnum +12021continue20222023file= {}2024file["path"] = path2025file["rev"] = commit["rev%s"% fnum]2026file["action"] = commit["action%s"% fnum]2027file["type"] = commit["type%s"% fnum]2028 files.append(file)2029 fnum = fnum +12030return files20312032defstripRepoPath(self, path, prefixes):2033"""When streaming files, this is called to map a p4 depot path2034 to where it should go in git. The prefixes are either2035 self.depotPaths, or self.branchPrefixes in the case of2036 branch detection."""20372038if self.useClientSpec:2039# branch detection moves files up a level (the branch name)2040# from what client spec interpretation gives2041 path = self.clientSpecDirs.map_in_client(path)2042if self.detectBranches:2043for b in self.knownBranches:2044if path.startswith(b +"/"):2045 path = path[len(b)+1:]20462047elif self.keepRepoPath:2048# Preserve everything in relative path name except leading2049# //depot/; just look at first prefix as they all should2050# be in the same depot.2051 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2052ifp4PathStartsWith(path, depot):2053 path = path[len(depot):]20542055else:2056for p in prefixes:2057ifp4PathStartsWith(path, p):2058 path = path[len(p):]2059break20602061 path =wildcard_decode(path)2062return path20632064defsplitFilesIntoBranches(self, commit):2065"""Look at each depotFile in the commit to figure out to what2066 branch it belongs."""20672068if self.clientSpecDirs:2069 files = self.extractFilesFromCommit(commit)2070 self.clientSpecDirs.update_client_spec_path_cache(files)20712072 branches = {}2073 fnum =02074while commit.has_key("depotFile%s"% fnum):2075 path = commit["depotFile%s"% fnum]2076 found = [p for p in self.depotPaths2077ifp4PathStartsWith(path, p)]2078if not found:2079 fnum = fnum +12080continue20812082file= {}2083file["path"] = path2084file["rev"] = commit["rev%s"% fnum]2085file["action"] = commit["action%s"% fnum]2086file["type"] = commit["type%s"% fnum]2087 fnum = fnum +120882089# start with the full relative path where this file would2090# go in a p4 client2091if self.useClientSpec:2092 relPath = self.clientSpecDirs.map_in_client(path)2093else:2094 relPath = self.stripRepoPath(path, self.depotPaths)20952096for branch in self.knownBranches.keys():2097# add a trailing slash so that a commit into qt/4.2foo2098# doesn't end up in qt/4.2, e.g.2099if relPath.startswith(branch +"/"):2100if branch not in branches:2101 branches[branch] = []2102 branches[branch].append(file)2103break21042105return branches21062107# output one file from the P4 stream2108# - helper for streamP4Files21092110defstreamOneP4File(self,file, contents):2111 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2112if verbose:2113 sys.stderr.write("%s\n"% relPath)21142115(type_base, type_mods) =split_p4_type(file["type"])21162117 git_mode ="100644"2118if"x"in type_mods:2119 git_mode ="100755"2120if type_base =="symlink":2121 git_mode ="120000"2122# p4 print on a symlink sometimes contains "target\n";2123# if it does, remove the newline2124 data =''.join(contents)2125if not data:2126# Some version of p4 allowed creating a symlink that pointed2127# to nothing. This causes p4 errors when checking out such2128# a change, and errors here too. Work around it by ignoring2129# the bad symlink; hopefully a future change fixes it.2130print"\nIgnoring empty symlink in%s"%file['depotFile']2131return2132elif data[-1] =='\n':2133 contents = [data[:-1]]2134else:2135 contents = [data]21362137if type_base =="utf16":2138# p4 delivers different text in the python output to -G2139# than it does when using "print -o", or normal p4 client2140# operations. utf16 is converted to ascii or utf8, perhaps.2141# But ascii text saved as -t utf16 is completely mangled.2142# Invoke print -o to get the real contents.2143#2144# On windows, the newlines will always be mangled by print, so put2145# them back too. This is not needed to the cygwin windows version,2146# just the native "NT" type.2147#2148 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2149ifp4_version_string().find("/NT") >=0:2150 text = text.replace("\r\n","\n")2151 contents = [ text ]21522153if type_base =="apple":2154# Apple filetype files will be streamed as a concatenation of2155# its appledouble header and the contents. This is useless2156# on both macs and non-macs. If using "print -q -o xx", it2157# will create "xx" with the data, and "%xx" with the header.2158# This is also not very useful.2159#2160# Ideally, someday, this script can learn how to generate2161# appledouble files directly and import those to git, but2162# non-mac machines can never find a use for apple filetype.2163print"\nIgnoring apple filetype file%s"%file['depotFile']2164return21652166# Note that we do not try to de-mangle keywords on utf16 files,2167# even though in theory somebody may want that.2168 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2169if pattern:2170 regexp = re.compile(pattern, re.VERBOSE)2171 text =''.join(contents)2172 text = regexp.sub(r'$\1$', text)2173 contents = [ text ]21742175 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21762177# total length...2178 length =02179for d in contents:2180 length = length +len(d)21812182 self.gitStream.write("data%d\n"% length)2183for d in contents:2184 self.gitStream.write(d)2185 self.gitStream.write("\n")21862187defstreamOneP4Deletion(self,file):2188 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2189if verbose:2190 sys.stderr.write("delete%s\n"% relPath)2191 self.gitStream.write("D%s\n"% relPath)21922193# handle another chunk of streaming data2194defstreamP4FilesCb(self, marshalled):21952196# catch p4 errors and complain2197 err =None2198if"code"in marshalled:2199if marshalled["code"] =="error":2200if"data"in marshalled:2201 err = marshalled["data"].rstrip()2202if err:2203 f =None2204if self.stream_have_file_info:2205if"depotFile"in self.stream_file:2206 f = self.stream_file["depotFile"]2207# force a failure in fast-import, else an empty2208# commit will be made2209 self.gitStream.write("\n")2210 self.gitStream.write("die-now\n")2211 self.gitStream.close()2212# ignore errors, but make sure it exits first2213 self.importProcess.wait()2214if f:2215die("Error from p4 print for%s:%s"% (f, err))2216else:2217die("Error from p4 print:%s"% err)22182219if marshalled.has_key('depotFile')and self.stream_have_file_info:2220# start of a new file - output the old one first2221 self.streamOneP4File(self.stream_file, self.stream_contents)2222 self.stream_file = {}2223 self.stream_contents = []2224 self.stream_have_file_info =False22252226# pick up the new file information... for the2227# 'data' field we need to append to our array2228for k in marshalled.keys():2229if k =='data':2230 self.stream_contents.append(marshalled['data'])2231else:2232 self.stream_file[k] = marshalled[k]22332234 self.stream_have_file_info =True22352236# Stream directly from "p4 files" into "git fast-import"2237defstreamP4Files(self, files):2238 filesForCommit = []2239 filesToRead = []2240 filesToDelete = []22412242for f in files:2243# if using a client spec, only add the files that have2244# a path in the client2245if self.clientSpecDirs:2246if self.clientSpecDirs.map_in_client(f['path']) =="":2247continue22482249 filesForCommit.append(f)2250if f['action']in self.delete_actions:2251 filesToDelete.append(f)2252else:2253 filesToRead.append(f)22542255# deleted files...2256for f in filesToDelete:2257 self.streamOneP4Deletion(f)22582259iflen(filesToRead) >0:2260 self.stream_file = {}2261 self.stream_contents = []2262 self.stream_have_file_info =False22632264# curry self argument2265defstreamP4FilesCbSelf(entry):2266 self.streamP4FilesCb(entry)22672268 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22692270p4CmdList(["-x","-","print"],2271 stdin=fileArgs,2272 cb=streamP4FilesCbSelf)22732274# do the last chunk2275if self.stream_file.has_key('depotFile'):2276 self.streamOneP4File(self.stream_file, self.stream_contents)22772278defmake_email(self, userid):2279if userid in self.users:2280return self.users[userid]2281else:2282return"%s<a@b>"% userid22832284# Stream a p4 tag2285defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2286if verbose:2287print"writing tag%sfor commit%s"% (labelName, commit)2288 gitStream.write("tag%s\n"% labelName)2289 gitStream.write("from%s\n"% commit)22902291if labelDetails.has_key('Owner'):2292 owner = labelDetails["Owner"]2293else:2294 owner =None22952296# Try to use the owner of the p4 label, or failing that,2297# the current p4 user id.2298if owner:2299 email = self.make_email(owner)2300else:2301 email = self.make_email(self.p4UserId())2302 tagger ="%s %s %s"% (email, epoch, self.tz)23032304 gitStream.write("tagger%s\n"% tagger)23052306print"labelDetails=",labelDetails2307if labelDetails.has_key('Description'):2308 description = labelDetails['Description']2309else:2310 description ='Label from git p4'23112312 gitStream.write("data%d\n"%len(description))2313 gitStream.write(description)2314 gitStream.write("\n")23152316defcommit(self, details, files, branch, parent =""):2317 epoch = details["time"]2318 author = details["user"]23192320if self.verbose:2321print"commit into%s"% branch23222323# start with reading files; if that fails, we should not2324# create a commit.2325 new_files = []2326for f in files:2327if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2328 new_files.append(f)2329else:2330 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23312332if self.clientSpecDirs:2333 self.clientSpecDirs.update_client_spec_path_cache(files)23342335 self.gitStream.write("commit%s\n"% branch)2336# gitStream.write("mark :%s\n" % details["change"])2337 self.committedChanges.add(int(details["change"]))2338 committer =""2339if author not in self.users:2340 self.getUserMapFromPerforceServer()2341 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23422343 self.gitStream.write("committer%s\n"% committer)23442345 self.gitStream.write("data <<EOT\n")2346 self.gitStream.write(details["desc"])2347 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2348(','.join(self.branchPrefixes), details["change"]))2349iflen(details['options']) >0:2350 self.gitStream.write(": options =%s"% details['options'])2351 self.gitStream.write("]\nEOT\n\n")23522353iflen(parent) >0:2354if self.verbose:2355print"parent%s"% parent2356 self.gitStream.write("from%s\n"% parent)23572358 self.streamP4Files(new_files)2359 self.gitStream.write("\n")23602361 change =int(details["change"])23622363if self.labels.has_key(change):2364 label = self.labels[change]2365 labelDetails = label[0]2366 labelRevisions = label[1]2367if self.verbose:2368print"Change%sis labelled%s"% (change, labelDetails)23692370 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2371for p in self.branchPrefixes])23722373iflen(files) ==len(labelRevisions):23742375 cleanedFiles = {}2376for info in files:2377if info["action"]in self.delete_actions:2378continue2379 cleanedFiles[info["depotFile"]] = info["rev"]23802381if cleanedFiles == labelRevisions:2382 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23832384else:2385if not self.silent:2386print("Tag%sdoes not match with change%s: files do not match."2387% (labelDetails["label"], change))23882389else:2390if not self.silent:2391print("Tag%sdoes not match with change%s: file count is different."2392% (labelDetails["label"], change))23932394# Build a dictionary of changelists and labels, for "detect-labels" option.2395defgetLabels(self):2396 self.labels = {}23972398 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2399iflen(l) >0and not self.silent:2400print"Finding files belonging to labels in%s"% `self.depotPaths`24012402for output in l:2403 label = output["label"]2404 revisions = {}2405 newestChange =02406if self.verbose:2407print"Querying files for label%s"% label2408forfileinp4CmdList(["files"] +2409["%s...@%s"% (p, label)2410for p in self.depotPaths]):2411 revisions[file["depotFile"]] =file["rev"]2412 change =int(file["change"])2413if change > newestChange:2414 newestChange = change24152416 self.labels[newestChange] = [output, revisions]24172418if self.verbose:2419print"Label changes:%s"% self.labels.keys()24202421# Import p4 labels as git tags. A direct mapping does not2422# exist, so assume that if all the files are at the same revision2423# then we can use that, or it's something more complicated we should2424# just ignore.2425defimportP4Labels(self, stream, p4Labels):2426if verbose:2427print"import p4 labels: "+' '.join(p4Labels)24282429 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2430 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2431iflen(validLabelRegexp) ==0:2432 validLabelRegexp = defaultLabelRegexp2433 m = re.compile(validLabelRegexp)24342435for name in p4Labels:2436 commitFound =False24372438if not m.match(name):2439if verbose:2440print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2441continue24422443if name in ignoredP4Labels:2444continue24452446 labelDetails =p4CmdList(['label',"-o", name])[0]24472448# get the most recent changelist for each file in this label2449 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2450for p in self.depotPaths])24512452if change.has_key('change'):2453# find the corresponding git commit; take the oldest commit2454 changelist =int(change['change'])2455 gitCommit =read_pipe(["git","rev-list","--max-count=1",2456"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2457iflen(gitCommit) ==0:2458print"could not find git commit for changelist%d"% changelist2459else:2460 gitCommit = gitCommit.strip()2461 commitFound =True2462# Convert from p4 time format2463try:2464 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2465exceptValueError:2466print"Could not convert label time%s"% labelDetails['Update']2467 tmwhen =124682469 when =int(time.mktime(tmwhen))2470 self.streamTag(stream, name, labelDetails, gitCommit, when)2471if verbose:2472print"p4 label%smapped to git commit%s"% (name, gitCommit)2473else:2474if verbose:2475print"Label%shas no changelists - possibly deleted?"% name24762477if not commitFound:2478# We can't import this label; don't try again as it will get very2479# expensive repeatedly fetching all the files for labels that will2480# never be imported. If the label is moved in the future, the2481# ignore will need to be removed manually.2482system(["git","config","--add","git-p4.ignoredP4Labels", name])24832484defguessProjectName(self):2485for p in self.depotPaths:2486if p.endswith("/"):2487 p = p[:-1]2488 p = p[p.strip().rfind("/") +1:]2489if not p.endswith("/"):2490 p +="/"2491return p24922493defgetBranchMapping(self):2494 lostAndFoundBranches =set()24952496 user =gitConfig("git-p4.branchUser")2497iflen(user) >0:2498 command ="branches -u%s"% user2499else:2500 command ="branches"25012502for info inp4CmdList(command):2503 details =p4Cmd(["branch","-o", info["branch"]])2504 viewIdx =02505while details.has_key("View%s"% viewIdx):2506 paths = details["View%s"% viewIdx].split(" ")2507 viewIdx = viewIdx +12508# require standard //depot/foo/... //depot/bar/... mapping2509iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2510continue2511 source = paths[0]2512 destination = paths[1]2513## HACK2514ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2515 source = source[len(self.depotPaths[0]):-4]2516 destination = destination[len(self.depotPaths[0]):-4]25172518if destination in self.knownBranches:2519if not self.silent:2520print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2521print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2522continue25232524 self.knownBranches[destination] = source25252526 lostAndFoundBranches.discard(destination)25272528if source not in self.knownBranches:2529 lostAndFoundBranches.add(source)25302531# Perforce does not strictly require branches to be defined, so we also2532# check git config for a branch list.2533#2534# Example of branch definition in git config file:2535# [git-p4]2536# branchList=main:branchA2537# branchList=main:branchB2538# branchList=branchA:branchC2539 configBranches =gitConfigList("git-p4.branchList")2540for branch in configBranches:2541if branch:2542(source, destination) = branch.split(":")2543 self.knownBranches[destination] = source25442545 lostAndFoundBranches.discard(destination)25462547if source not in self.knownBranches:2548 lostAndFoundBranches.add(source)254925502551for branch in lostAndFoundBranches:2552 self.knownBranches[branch] = branch25532554defgetBranchMappingFromGitBranches(self):2555 branches =p4BranchesInGit(self.importIntoRemotes)2556for branch in branches.keys():2557if branch =="master":2558 branch ="main"2559else:2560 branch = branch[len(self.projectName):]2561 self.knownBranches[branch] = branch25622563defupdateOptionDict(self, d):2564 option_keys = {}2565if self.keepRepoPath:2566 option_keys['keepRepoPath'] =125672568 d["options"] =' '.join(sorted(option_keys.keys()))25692570defreadOptions(self, d):2571 self.keepRepoPath = (d.has_key('options')2572and('keepRepoPath'in d['options']))25732574defgitRefForBranch(self, branch):2575if branch =="main":2576return self.refPrefix +"master"25772578iflen(branch) <=0:2579return branch25802581return self.refPrefix + self.projectName + branch25822583defgitCommitByP4Change(self, ref, change):2584if self.verbose:2585print"looking in ref "+ ref +" for change%susing bisect..."% change25862587 earliestCommit =""2588 latestCommit =parseRevision(ref)25892590while True:2591if self.verbose:2592print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2593 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2594iflen(next) ==0:2595if self.verbose:2596print"argh"2597return""2598 log =extractLogMessageFromGitCommit(next)2599 settings =extractSettingsGitLog(log)2600 currentChange =int(settings['change'])2601if self.verbose:2602print"current change%s"% currentChange26032604if currentChange == change:2605if self.verbose:2606print"found%s"% next2607return next26082609if currentChange < change:2610 earliestCommit ="^%s"% next2611else:2612 latestCommit ="%s"% next26132614return""26152616defimportNewBranch(self, branch, maxChange):2617# make fast-import flush all changes to disk and update the refs using the checkpoint2618# command so that we can try to find the branch parent in the git history2619 self.gitStream.write("checkpoint\n\n");2620 self.gitStream.flush();2621 branchPrefix = self.depotPaths[0] + branch +"/"2622range="@1,%s"% maxChange2623#print "prefix" + branchPrefix2624 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2625iflen(changes) <=0:2626return False2627 firstChange = changes[0]2628#print "first change in branch: %s" % firstChange2629 sourceBranch = self.knownBranches[branch]2630 sourceDepotPath = self.depotPaths[0] + sourceBranch2631 sourceRef = self.gitRefForBranch(sourceBranch)2632#print "source " + sourceBranch26332634 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2635#print "branch parent: %s" % branchParentChange2636 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2637iflen(gitParent) >0:2638 self.initialParents[self.gitRefForBranch(branch)] = gitParent2639#print "parent git commit: %s" % gitParent26402641 self.importChanges(changes)2642return True26432644defsearchParent(self, parent, branch, target):2645 parentFound =False2646for blob inread_pipe_lines(["git","rev-list","--reverse",2647"--no-merges", parent]):2648 blob = blob.strip()2649iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2650 parentFound =True2651if self.verbose:2652print"Found parent of%sin commit%s"% (branch, blob)2653break2654if parentFound:2655return blob2656else:2657return None26582659defimportChanges(self, changes):2660 cnt =12661for change in changes:2662 description =p4_describe(change)2663 self.updateOptionDict(description)26642665if not self.silent:2666 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2667 sys.stdout.flush()2668 cnt = cnt +126692670try:2671if self.detectBranches:2672 branches = self.splitFilesIntoBranches(description)2673for branch in branches.keys():2674## HACK --hwn2675 branchPrefix = self.depotPaths[0] + branch +"/"2676 self.branchPrefixes = [ branchPrefix ]26772678 parent =""26792680 filesForCommit = branches[branch]26812682if self.verbose:2683print"branch is%s"% branch26842685 self.updatedBranches.add(branch)26862687if branch not in self.createdBranches:2688 self.createdBranches.add(branch)2689 parent = self.knownBranches[branch]2690if parent == branch:2691 parent =""2692else:2693 fullBranch = self.projectName + branch2694if fullBranch not in self.p4BranchesInGit:2695if not self.silent:2696print("\nImporting new branch%s"% fullBranch);2697if self.importNewBranch(branch, change -1):2698 parent =""2699 self.p4BranchesInGit.append(fullBranch)2700if not self.silent:2701print("\nResuming with change%s"% change);27022703if self.verbose:2704print"parent determined through known branches:%s"% parent27052706 branch = self.gitRefForBranch(branch)2707 parent = self.gitRefForBranch(parent)27082709if self.verbose:2710print"looking for initial parent for%s; current parent is%s"% (branch, parent)27112712iflen(parent) ==0and branch in self.initialParents:2713 parent = self.initialParents[branch]2714del self.initialParents[branch]27152716 blob =None2717iflen(parent) >0:2718 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2719if self.verbose:2720print"Creating temporary branch: "+ tempBranch2721 self.commit(description, filesForCommit, tempBranch)2722 self.tempBranches.append(tempBranch)2723 self.checkpoint()2724 blob = self.searchParent(parent, branch, tempBranch)2725if blob:2726 self.commit(description, filesForCommit, branch, blob)2727else:2728if self.verbose:2729print"Parent of%snot found. Committing into head of%s"% (branch, parent)2730 self.commit(description, filesForCommit, branch, parent)2731else:2732 files = self.extractFilesFromCommit(description)2733 self.commit(description, files, self.branch,2734 self.initialParent)2735# only needed once, to connect to the previous commit2736 self.initialParent =""2737exceptIOError:2738print self.gitError.read()2739 sys.exit(1)27402741defimportHeadRevision(self, revision):2742print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27432744 details = {}2745 details["user"] ="git perforce import user"2746 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2747% (' '.join(self.depotPaths), revision))2748 details["change"] = revision2749 newestRevision =027502751 fileCnt =02752 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27532754for info inp4CmdList(["files"] + fileArgs):27552756if'code'in info and info['code'] =='error':2757 sys.stderr.write("p4 returned an error:%s\n"2758% info['data'])2759if info['data'].find("must refer to client") >=0:2760 sys.stderr.write("This particular p4 error is misleading.\n")2761 sys.stderr.write("Perhaps the depot path was misspelled.\n");2762 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2763 sys.exit(1)2764if'p4ExitCode'in info:2765 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2766 sys.exit(1)276727682769 change =int(info["change"])2770if change > newestRevision:2771 newestRevision = change27722773if info["action"]in self.delete_actions:2774# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2775#fileCnt = fileCnt + 12776continue27772778for prop in["depotFile","rev","action","type"]:2779 details["%s%s"% (prop, fileCnt)] = info[prop]27802781 fileCnt = fileCnt +127822783 details["change"] = newestRevision27842785# Use time from top-most change so that all git p4 clones of2786# the same p4 repo have the same commit SHA1s.2787 res =p4_describe(newestRevision)2788 details["time"] = res["time"]27892790 self.updateOptionDict(details)2791try:2792 self.commit(details, self.extractFilesFromCommit(details), self.branch)2793exceptIOError:2794print"IO error with git fast-import. Is your git version recent enough?"2795print self.gitError.read()279627972798defrun(self, args):2799 self.depotPaths = []2800 self.changeRange =""2801 self.previousDepotPaths = []2802 self.hasOrigin =False28032804# map from branch depot path to parent branch2805 self.knownBranches = {}2806 self.initialParents = {}28072808if self.importIntoRemotes:2809 self.refPrefix ="refs/remotes/p4/"2810else:2811 self.refPrefix ="refs/heads/p4/"28122813if self.syncWithOrigin:2814 self.hasOrigin =originP4BranchesExist()2815if self.hasOrigin:2816if not self.silent:2817print'Syncing with origin first, using "git fetch origin"'2818system("git fetch origin")28192820 branch_arg_given =bool(self.branch)2821iflen(self.branch) ==0:2822 self.branch = self.refPrefix +"master"2823ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2824system("git update-ref%srefs/heads/p4"% self.branch)2825system("git branch -D p4")28262827# accept either the command-line option, or the configuration variable2828if self.useClientSpec:2829# will use this after clone to set the variable2830 self.useClientSpec_from_options =True2831else:2832ifgitConfigBool("git-p4.useclientspec"):2833 self.useClientSpec =True2834if self.useClientSpec:2835 self.clientSpecDirs =getClientSpec()28362837# TODO: should always look at previous commits,2838# merge with previous imports, if possible.2839if args == []:2840if self.hasOrigin:2841createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28422843# branches holds mapping from branch name to sha12844 branches =p4BranchesInGit(self.importIntoRemotes)28452846# restrict to just this one, disabling detect-branches2847if branch_arg_given:2848 short = self.branch.split("/")[-1]2849if short in branches:2850 self.p4BranchesInGit = [ short ]2851else:2852 self.p4BranchesInGit = branches.keys()28532854iflen(self.p4BranchesInGit) >1:2855if not self.silent:2856print"Importing from/into multiple branches"2857 self.detectBranches =True2858for branch in branches.keys():2859 self.initialParents[self.refPrefix + branch] = \2860 branches[branch]28612862if self.verbose:2863print"branches:%s"% self.p4BranchesInGit28642865 p4Change =02866for branch in self.p4BranchesInGit:2867 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28682869 settings =extractSettingsGitLog(logMsg)28702871 self.readOptions(settings)2872if(settings.has_key('depot-paths')2873and settings.has_key('change')):2874 change =int(settings['change']) +12875 p4Change =max(p4Change, change)28762877 depotPaths =sorted(settings['depot-paths'])2878if self.previousDepotPaths == []:2879 self.previousDepotPaths = depotPaths2880else:2881 paths = []2882for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2883 prev_list = prev.split("/")2884 cur_list = cur.split("/")2885for i inrange(0,min(len(cur_list),len(prev_list))):2886if cur_list[i] <> prev_list[i]:2887 i = i -12888break28892890 paths.append("/".join(cur_list[:i +1]))28912892 self.previousDepotPaths = paths28932894if p4Change >0:2895 self.depotPaths =sorted(self.previousDepotPaths)2896 self.changeRange ="@%s,#head"% p4Change2897if not self.silent and not self.detectBranches:2898print"Performing incremental import into%sgit branch"% self.branch28992900# accept multiple ref name abbreviations:2901# refs/foo/bar/branch -> use it exactly2902# p4/branch -> prepend refs/remotes/ or refs/heads/2903# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2904if not self.branch.startswith("refs/"):2905if self.importIntoRemotes:2906 prepend ="refs/remotes/"2907else:2908 prepend ="refs/heads/"2909if not self.branch.startswith("p4/"):2910 prepend +="p4/"2911 self.branch = prepend + self.branch29122913iflen(args) ==0and self.depotPaths:2914if not self.silent:2915print"Depot paths:%s"%' '.join(self.depotPaths)2916else:2917if self.depotPaths and self.depotPaths != args:2918print("previous import used depot path%sand now%swas specified. "2919"This doesn't work!"% (' '.join(self.depotPaths),2920' '.join(args)))2921 sys.exit(1)29222923 self.depotPaths =sorted(args)29242925 revision =""2926 self.users = {}29272928# Make sure no revision specifiers are used when --changesfile2929# is specified.2930 bad_changesfile =False2931iflen(self.changesFile) >0:2932for p in self.depotPaths:2933if p.find("@") >=0or p.find("#") >=0:2934 bad_changesfile =True2935break2936if bad_changesfile:2937die("Option --changesfile is incompatible with revision specifiers")29382939 newPaths = []2940for p in self.depotPaths:2941if p.find("@") != -1:2942 atIdx = p.index("@")2943 self.changeRange = p[atIdx:]2944if self.changeRange =="@all":2945 self.changeRange =""2946elif','not in self.changeRange:2947 revision = self.changeRange2948 self.changeRange =""2949 p = p[:atIdx]2950elif p.find("#") != -1:2951 hashIdx = p.index("#")2952 revision = p[hashIdx:]2953 p = p[:hashIdx]2954elif self.previousDepotPaths == []:2955# pay attention to changesfile, if given, else import2956# the entire p4 tree at the head revision2957iflen(self.changesFile) ==0:2958 revision ="#head"29592960 p = re.sub("\.\.\.$","", p)2961if not p.endswith("/"):2962 p +="/"29632964 newPaths.append(p)29652966 self.depotPaths = newPaths29672968# --detect-branches may change this for each branch2969 self.branchPrefixes = self.depotPaths29702971 self.loadUserMapFromCache()2972 self.labels = {}2973if self.detectLabels:2974 self.getLabels();29752976if self.detectBranches:2977## FIXME - what's a P4 projectName ?2978 self.projectName = self.guessProjectName()29792980if self.hasOrigin:2981 self.getBranchMappingFromGitBranches()2982else:2983 self.getBranchMapping()2984if self.verbose:2985print"p4-git branches:%s"% self.p4BranchesInGit2986print"initial parents:%s"% self.initialParents2987for b in self.p4BranchesInGit:2988if b !="master":29892990## FIXME2991 b = b[len(self.projectName):]2992 self.createdBranches.add(b)29932994 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29952996 self.importProcess = subprocess.Popen(["git","fast-import"],2997 stdin=subprocess.PIPE,2998 stdout=subprocess.PIPE,2999 stderr=subprocess.PIPE);3000 self.gitOutput = self.importProcess.stdout3001 self.gitStream = self.importProcess.stdin3002 self.gitError = self.importProcess.stderr30033004if revision:3005 self.importHeadRevision(revision)3006else:3007 changes = []30083009iflen(self.changesFile) >0:3010 output =open(self.changesFile).readlines()3011 changeSet =set()3012for line in output:3013 changeSet.add(int(line))30143015for change in changeSet:3016 changes.append(change)30173018 changes.sort()3019else:3020# catch "git p4 sync" with no new branches, in a repo that3021# does not have any existing p4 branches3022iflen(args) ==0:3023if not self.p4BranchesInGit:3024die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30253026# The default branch is master, unless --branch is used to3027# specify something else. Make sure it exists, or complain3028# nicely about how to use --branch.3029if not self.detectBranches:3030if notbranch_exists(self.branch):3031if branch_arg_given:3032die("Error: branch%sdoes not exist."% self.branch)3033else:3034die("Error: no branch%s; perhaps specify one with --branch."%3035 self.branch)30363037if self.verbose:3038print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3039 self.changeRange)3040 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)30413042iflen(self.maxChanges) >0:3043 changes = changes[:min(int(self.maxChanges),len(changes))]30443045iflen(changes) ==0:3046if not self.silent:3047print"No changes to import!"3048else:3049if not self.silent and not self.detectBranches:3050print"Import destination:%s"% self.branch30513052 self.updatedBranches =set()30533054if not self.detectBranches:3055if args:3056# start a new branch3057 self.initialParent =""3058else:3059# build on a previous revision3060 self.initialParent =parseRevision(self.branch)30613062 self.importChanges(changes)30633064if not self.silent:3065print""3066iflen(self.updatedBranches) >0:3067 sys.stdout.write("Updated branches: ")3068for b in self.updatedBranches:3069 sys.stdout.write("%s"% b)3070 sys.stdout.write("\n")30713072ifgitConfigBool("git-p4.importLabels"):3073 self.importLabels =True30743075if self.importLabels:3076 p4Labels =getP4Labels(self.depotPaths)3077 gitTags =getGitTags()30783079 missingP4Labels = p4Labels - gitTags3080 self.importP4Labels(self.gitStream, missingP4Labels)30813082 self.gitStream.close()3083if self.importProcess.wait() !=0:3084die("fast-import failed:%s"% self.gitError.read())3085 self.gitOutput.close()3086 self.gitError.close()30873088# Cleanup temporary branches created during import3089if self.tempBranches != []:3090for branch in self.tempBranches:3091read_pipe("git update-ref -d%s"% branch)3092 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30933094# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3095# a convenient shortcut refname "p4".3096if self.importIntoRemotes:3097 head_ref = self.refPrefix +"HEAD"3098if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3099system(["git","symbolic-ref", head_ref, self.branch])31003101return True31023103classP4Rebase(Command):3104def__init__(self):3105 Command.__init__(self)3106 self.options = [3107 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3108]3109 self.importLabels =False3110 self.description = ("Fetches the latest revision from perforce and "3111+"rebases the current work (branch) against it")31123113defrun(self, args):3114 sync =P4Sync()3115 sync.importLabels = self.importLabels3116 sync.run([])31173118return self.rebase()31193120defrebase(self):3121if os.system("git update-index --refresh") !=0:3122die("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.");3123iflen(read_pipe("git diff-index HEAD --")) >0:3124die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");31253126[upstream, settings] =findUpstreamBranchPoint()3127iflen(upstream) ==0:3128die("Cannot find upstream branchpoint for rebase")31293130# the branchpoint may be p4/foo~3, so strip off the parent3131 upstream = re.sub("~[0-9]+$","", upstream)31323133print"Rebasing the current branch onto%s"% upstream3134 oldHead =read_pipe("git rev-parse HEAD").strip()3135system("git rebase%s"% upstream)3136system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3137return True31383139classP4Clone(P4Sync):3140def__init__(self):3141 P4Sync.__init__(self)3142 self.description ="Creates a new git repository and imports from Perforce into it"3143 self.usage ="usage: %prog [options] //depot/path[@revRange]"3144 self.options += [3145 optparse.make_option("--destination", dest="cloneDestination",3146 action='store', default=None,3147help="where to leave result of the clone"),3148 optparse.make_option("--bare", dest="cloneBare",3149 action="store_true", default=False),3150]3151 self.cloneDestination =None3152 self.needsGit =False3153 self.cloneBare =False31543155defdefaultDestination(self, args):3156## TODO: use common prefix of args?3157 depotPath = args[0]3158 depotDir = re.sub("(@[^@]*)$","", depotPath)3159 depotDir = re.sub("(#[^#]*)$","", depotDir)3160 depotDir = re.sub(r"\.\.\.$","", depotDir)3161 depotDir = re.sub(r"/$","", depotDir)3162return os.path.split(depotDir)[1]31633164defrun(self, args):3165iflen(args) <1:3166return False31673168if self.keepRepoPath and not self.cloneDestination:3169 sys.stderr.write("Must specify destination for --keep-path\n")3170 sys.exit(1)31713172 depotPaths = args31733174if not self.cloneDestination andlen(depotPaths) >1:3175 self.cloneDestination = depotPaths[-1]3176 depotPaths = depotPaths[:-1]31773178 self.cloneExclude = ["/"+p for p in self.cloneExclude]3179for p in depotPaths:3180if not p.startswith("//"):3181 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3182return False31833184if not self.cloneDestination:3185 self.cloneDestination = self.defaultDestination(args)31863187print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31883189if not os.path.exists(self.cloneDestination):3190 os.makedirs(self.cloneDestination)3191chdir(self.cloneDestination)31923193 init_cmd = ["git","init"]3194if self.cloneBare:3195 init_cmd.append("--bare")3196 retcode = subprocess.call(init_cmd)3197if retcode:3198raiseCalledProcessError(retcode, init_cmd)31993200if not P4Sync.run(self, depotPaths):3201return False32023203# create a master branch and check out a work tree3204ifgitBranchExists(self.branch):3205system(["git","branch","master", self.branch ])3206if not self.cloneBare:3207system(["git","checkout","-f"])3208else:3209print'Not checking out any branch, use ' \3210'"git checkout -q -b master <branch>"'32113212# auto-set this variable if invoked with --use-client-spec3213if self.useClientSpec_from_options:3214system("git config --bool git-p4.useclientspec true")32153216return True32173218classP4Branches(Command):3219def__init__(self):3220 Command.__init__(self)3221 self.options = [ ]3222 self.description = ("Shows the git branches that hold imports and their "3223+"corresponding perforce depot paths")3224 self.verbose =False32253226defrun(self, args):3227iforiginP4BranchesExist():3228createOrUpdateBranchesFromOrigin()32293230 cmdline ="git rev-parse --symbolic "3231 cmdline +=" --remotes"32323233for line inread_pipe_lines(cmdline):3234 line = line.strip()32353236if not line.startswith('p4/')or line =="p4/HEAD":3237continue3238 branch = line32393240 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3241 settings =extractSettingsGitLog(log)32423243print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3244return True32453246classHelpFormatter(optparse.IndentedHelpFormatter):3247def__init__(self):3248 optparse.IndentedHelpFormatter.__init__(self)32493250defformat_description(self, description):3251if description:3252return description +"\n"3253else:3254return""32553256defprintUsage(commands):3257print"usage:%s<command> [options]"% sys.argv[0]3258print""3259print"valid commands:%s"%", ".join(commands)3260print""3261print"Try%s<command> --help for command specific help."% sys.argv[0]3262print""32633264commands = {3265"debug": P4Debug,3266"submit": P4Submit,3267"commit": P4Submit,3268"sync": P4Sync,3269"rebase": P4Rebase,3270"clone": P4Clone,3271"rollback": P4RollBack,3272"branches": P4Branches3273}327432753276defmain():3277iflen(sys.argv[1:]) ==0:3278printUsage(commands.keys())3279 sys.exit(2)32803281 cmdName = sys.argv[1]3282try:3283 klass = commands[cmdName]3284 cmd =klass()3285exceptKeyError:3286print"unknown command%s"% cmdName3287print""3288printUsage(commands.keys())3289 sys.exit(2)32903291 options = cmd.options3292 cmd.gitdir = os.environ.get("GIT_DIR",None)32933294 args = sys.argv[2:]32953296 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3297if cmd.needsGit:3298 options.append(optparse.make_option("--git-dir", dest="gitdir"))32993300 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3301 options,3302 description = cmd.description,3303 formatter =HelpFormatter())33043305(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3306global verbose3307 verbose = cmd.verbose3308if cmd.needsGit:3309if cmd.gitdir ==None:3310 cmd.gitdir = os.path.abspath(".git")3311if notisValidGitDir(cmd.gitdir):3312 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3313if os.path.exists(cmd.gitdir):3314 cdup =read_pipe("git rev-parse --show-cdup").strip()3315iflen(cdup) >0:3316chdir(cdup);33173318if notisValidGitDir(cmd.gitdir):3319ifisValidGitDir(cmd.gitdir +"/.git"):3320 cmd.gitdir +="/.git"3321else:3322die("fatal: cannot locate git repository at%s"% cmd.gitdir)33233324 os.environ["GIT_DIR"] = cmd.gitdir33253326if not cmd.run(args):3327 parser.print_help()3328 sys.exit(2)332933303331if __name__ =='__main__':3332main()