1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25 26try: 27from subprocess import CalledProcessError 28exceptImportError: 29# from python2.7:subprocess.py 30# Exception classes used by this module. 31classCalledProcessError(Exception): 32"""This exception is raised when a process run by check_call() returns 33 a non-zero exit status. The exit status will be stored in the 34 returncode attribute.""" 35def__init__(self, returncode, cmd): 36 self.returncode = returncode 37 self.cmd = cmd 38def__str__(self): 39return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 40 41verbose =False 42 43# Only labels/tags matching this will be imported/exported 44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 45 46defp4_build_cmd(cmd): 47"""Build a suitable p4 command line. 48 49 This consolidates building and returning a p4 command line into one 50 location. It means that hooking into the environment, or other configuration 51 can be done more easily. 52 """ 53 real_cmd = ["p4"] 54 55 user =gitConfig("git-p4.user") 56iflen(user) >0: 57 real_cmd += ["-u",user] 58 59 password =gitConfig("git-p4.password") 60iflen(password) >0: 61 real_cmd += ["-P", password] 62 63 port =gitConfig("git-p4.port") 64iflen(port) >0: 65 real_cmd += ["-p", port] 66 67 host =gitConfig("git-p4.host") 68iflen(host) >0: 69 real_cmd += ["-H", host] 70 71 client =gitConfig("git-p4.client") 72iflen(client) >0: 73 real_cmd += ["-c", client] 74 75 76ifisinstance(cmd,basestring): 77 real_cmd =' '.join(real_cmd) +' '+ cmd 78else: 79 real_cmd += cmd 80return real_cmd 81 82defchdir(path, is_client_path=False): 83"""Do chdir to the given path, and set the PWD environment 84 variable for use by P4. It does not look at getcwd() output. 85 Since we're not using the shell, it is necessary to set the 86 PWD environment variable explicitly. 87 88 Normally, expand the path to force it to be absolute. This 89 addresses the use of relative path names inside P4 settings, 90 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 91 as given; it looks for .p4config using PWD. 92 93 If is_client_path, the path was handed to us directly by p4, 94 and may be a symbolic link. Do not call os.getcwd() in this 95 case, because it will cause p4 to think that PWD is not inside 96 the client path. 97 """ 98 99 os.chdir(path) 100if not is_client_path: 101 path = os.getcwd() 102 os.environ['PWD'] = path 103 104defdie(msg): 105if verbose: 106raiseException(msg) 107else: 108 sys.stderr.write(msg +"\n") 109 sys.exit(1) 110 111defwrite_pipe(c, stdin): 112if verbose: 113 sys.stderr.write('Writing pipe:%s\n'%str(c)) 114 115 expand =isinstance(c,basestring) 116 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 117 pipe = p.stdin 118 val = pipe.write(stdin) 119 pipe.close() 120if p.wait(): 121die('Command failed:%s'%str(c)) 122 123return val 124 125defp4_write_pipe(c, stdin): 126 real_cmd =p4_build_cmd(c) 127returnwrite_pipe(real_cmd, stdin) 128 129defread_pipe(c, ignore_error=False): 130if verbose: 131 sys.stderr.write('Reading pipe:%s\n'%str(c)) 132 133 expand =isinstance(c,basestring) 134 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 135 pipe = p.stdout 136 val = pipe.read() 137if p.wait()and not ignore_error: 138die('Command failed:%s'%str(c)) 139 140return val 141 142defp4_read_pipe(c, ignore_error=False): 143 real_cmd =p4_build_cmd(c) 144returnread_pipe(real_cmd, ignore_error) 145 146defread_pipe_lines(c): 147if verbose: 148 sys.stderr.write('Reading pipe:%s\n'%str(c)) 149 150 expand =isinstance(c, basestring) 151 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 152 pipe = p.stdout 153 val = pipe.readlines() 154if pipe.close()or p.wait(): 155die('Command failed:%s'%str(c)) 156 157return val 158 159defp4_read_pipe_lines(c): 160"""Specifically invoke p4 on the command supplied. """ 161 real_cmd =p4_build_cmd(c) 162returnread_pipe_lines(real_cmd) 163 164defp4_has_command(cmd): 165"""Ask p4 for help on this command. If it returns an error, the 166 command does not exist in this version of p4.""" 167 real_cmd =p4_build_cmd(["help", cmd]) 168 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 169 stderr=subprocess.PIPE) 170 p.communicate() 171return p.returncode ==0 172 173defp4_has_move_command(): 174"""See if the move command exists, that it supports -k, and that 175 it has not been administratively disabled. The arguments 176 must be correct, but the filenames do not have to exist. Use 177 ones with wildcards so even if they exist, it will fail.""" 178 179if notp4_has_command("move"): 180return False 181 cmd =p4_build_cmd(["move","-k","@from","@to"]) 182 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 183(out, err) = p.communicate() 184# return code will be 1 in either case 185if err.find("Invalid option") >=0: 186return False 187if err.find("disabled") >=0: 188return False 189# assume it failed because @... was invalid changelist 190return True 191 192defsystem(cmd): 193 expand =isinstance(cmd,basestring) 194if verbose: 195 sys.stderr.write("executing%s\n"%str(cmd)) 196 retcode = subprocess.call(cmd, shell=expand) 197if retcode: 198raiseCalledProcessError(retcode, cmd) 199 200defp4_system(cmd): 201"""Specifically invoke p4 as the system command. """ 202 real_cmd =p4_build_cmd(cmd) 203 expand =isinstance(real_cmd, basestring) 204 retcode = subprocess.call(real_cmd, shell=expand) 205if retcode: 206raiseCalledProcessError(retcode, real_cmd) 207 208_p4_version_string =None 209defp4_version_string(): 210"""Read the version string, showing just the last line, which 211 hopefully is the interesting version bit. 212 213 $ p4 -V 214 Perforce - The Fast Software Configuration Management System. 215 Copyright 1995-2011 Perforce Software. All rights reserved. 216 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 217 """ 218global _p4_version_string 219if not _p4_version_string: 220 a =p4_read_pipe_lines(["-V"]) 221 _p4_version_string = a[-1].rstrip() 222return _p4_version_string 223 224defp4_integrate(src, dest): 225p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 226 227defp4_sync(f, *options): 228p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 229 230defp4_add(f): 231# forcibly add file names with wildcards 232ifwildcard_present(f): 233p4_system(["add","-f", f]) 234else: 235p4_system(["add", f]) 236 237defp4_delete(f): 238p4_system(["delete",wildcard_encode(f)]) 239 240defp4_edit(f): 241p4_system(["edit",wildcard_encode(f)]) 242 243defp4_revert(f): 244p4_system(["revert",wildcard_encode(f)]) 245 246defp4_reopen(type, f): 247p4_system(["reopen","-t",type,wildcard_encode(f)]) 248 249defp4_move(src, dest): 250p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 251 252defp4_describe(change): 253"""Make sure it returns a valid result by checking for 254 the presence of field "time". Return a dict of the 255 results.""" 256 257 ds =p4CmdList(["describe","-s",str(change)]) 258iflen(ds) !=1: 259die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 260 261 d = ds[0] 262 263if"p4ExitCode"in d: 264die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 265str(d))) 266if"code"in d: 267if d["code"] =="error": 268die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 269 270if"time"not in d: 271die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 272 273return d 274 275# 276# Canonicalize the p4 type and return a tuple of the 277# base type, plus any modifiers. See "p4 help filetypes" 278# for a list and explanation. 279# 280defsplit_p4_type(p4type): 281 282 p4_filetypes_historical = { 283"ctempobj":"binary+Sw", 284"ctext":"text+C", 285"cxtext":"text+Cx", 286"ktext":"text+k", 287"kxtext":"text+kx", 288"ltext":"text+F", 289"tempobj":"binary+FSw", 290"ubinary":"binary+F", 291"uresource":"resource+F", 292"uxbinary":"binary+Fx", 293"xbinary":"binary+x", 294"xltext":"text+Fx", 295"xtempobj":"binary+Swx", 296"xtext":"text+x", 297"xunicode":"unicode+x", 298"xutf16":"utf16+x", 299} 300if p4type in p4_filetypes_historical: 301 p4type = p4_filetypes_historical[p4type] 302 mods ="" 303 s = p4type.split("+") 304 base = s[0] 305 mods ="" 306iflen(s) >1: 307 mods = s[1] 308return(base, mods) 309 310# 311# return the raw p4 type of a file (text, text+ko, etc) 312# 313defp4_type(f): 314 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 315return results[0]['headType'] 316 317# 318# Given a type base and modifier, return a regexp matching 319# the keywords that can be expanded in the file 320# 321defp4_keywords_regexp_for_type(base, type_mods): 322if base in("text","unicode","binary"): 323 kwords =None 324if"ko"in type_mods: 325 kwords ='Id|Header' 326elif"k"in type_mods: 327 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 328else: 329return None 330 pattern = r""" 331 \$ # Starts with a dollar, followed by... 332 (%s) # one of the keywords, followed by... 333 (:[^$\n]+)? # possibly an old expansion, followed by... 334 \$ # another dollar 335 """% kwords 336return pattern 337else: 338return None 339 340# 341# Given a file, return a regexp matching the possible 342# RCS keywords that will be expanded, or None for files 343# with kw expansion turned off. 344# 345defp4_keywords_regexp_for_file(file): 346if not os.path.exists(file): 347return None 348else: 349(type_base, type_mods) =split_p4_type(p4_type(file)) 350returnp4_keywords_regexp_for_type(type_base, type_mods) 351 352defsetP4ExecBit(file, mode): 353# Reopens an already open file and changes the execute bit to match 354# the execute bit setting in the passed in mode. 355 356 p4Type ="+x" 357 358if notisModeExec(mode): 359 p4Type =getP4OpenedType(file) 360 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 361 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 362if p4Type[-1] =="+": 363 p4Type = p4Type[0:-1] 364 365p4_reopen(p4Type,file) 366 367defgetP4OpenedType(file): 368# Returns the perforce file type for the given file. 369 370 result =p4_read_pipe(["opened",wildcard_encode(file)]) 371 match = re.match(".*\((.+)\)\r?$", result) 372if match: 373return match.group(1) 374else: 375die("Could not determine file type for%s(result: '%s')"% (file, result)) 376 377# Return the set of all p4 labels 378defgetP4Labels(depotPaths): 379 labels =set() 380ifisinstance(depotPaths,basestring): 381 depotPaths = [depotPaths] 382 383for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 384 label = l['label'] 385 labels.add(label) 386 387return labels 388 389# Return the set of all git tags 390defgetGitTags(): 391 gitTags =set() 392for line inread_pipe_lines(["git","tag"]): 393 tag = line.strip() 394 gitTags.add(tag) 395return gitTags 396 397defdiffTreePattern(): 398# This is a simple generator for the diff tree regex pattern. This could be 399# a class variable if this and parseDiffTreeEntry were a part of a class. 400 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 401while True: 402yield pattern 403 404defparseDiffTreeEntry(entry): 405"""Parses a single diff tree entry into its component elements. 406 407 See git-diff-tree(1) manpage for details about the format of the diff 408 output. This method returns a dictionary with the following elements: 409 410 src_mode - The mode of the source file 411 dst_mode - The mode of the destination file 412 src_sha1 - The sha1 for the source file 413 dst_sha1 - The sha1 fr the destination file 414 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 415 status_score - The score for the status (applicable for 'C' and 'R' 416 statuses). This is None if there is no score. 417 src - The path for the source file. 418 dst - The path for the destination file. This is only present for 419 copy or renames. If it is not present, this is None. 420 421 If the pattern is not matched, None is returned.""" 422 423 match =diffTreePattern().next().match(entry) 424if match: 425return{ 426'src_mode': match.group(1), 427'dst_mode': match.group(2), 428'src_sha1': match.group(3), 429'dst_sha1': match.group(4), 430'status': match.group(5), 431'status_score': match.group(6), 432'src': match.group(7), 433'dst': match.group(10) 434} 435return None 436 437defisModeExec(mode): 438# Returns True if the given git mode represents an executable file, 439# otherwise False. 440return mode[-3:] =="755" 441 442defisModeExecChanged(src_mode, dst_mode): 443returnisModeExec(src_mode) !=isModeExec(dst_mode) 444 445defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 446 447ifisinstance(cmd,basestring): 448 cmd ="-G "+ cmd 449 expand =True 450else: 451 cmd = ["-G"] + cmd 452 expand =False 453 454 cmd =p4_build_cmd(cmd) 455if verbose: 456 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 457 458# Use a temporary file to avoid deadlocks without 459# subprocess.communicate(), which would put another copy 460# of stdout into memory. 461 stdin_file =None 462if stdin is not None: 463 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 464ifisinstance(stdin,basestring): 465 stdin_file.write(stdin) 466else: 467for i in stdin: 468 stdin_file.write(i +'\n') 469 stdin_file.flush() 470 stdin_file.seek(0) 471 472 p4 = subprocess.Popen(cmd, 473 shell=expand, 474 stdin=stdin_file, 475 stdout=subprocess.PIPE) 476 477 result = [] 478try: 479while True: 480 entry = marshal.load(p4.stdout) 481if cb is not None: 482cb(entry) 483else: 484 result.append(entry) 485exceptEOFError: 486pass 487 exitCode = p4.wait() 488if exitCode !=0: 489 entry = {} 490 entry["p4ExitCode"] = exitCode 491 result.append(entry) 492 493return result 494 495defp4Cmd(cmd): 496list=p4CmdList(cmd) 497 result = {} 498for entry inlist: 499 result.update(entry) 500return result; 501 502defp4Where(depotPath): 503if not depotPath.endswith("/"): 504 depotPath +="/" 505 depotPath = depotPath +"..." 506 outputList =p4CmdList(["where", depotPath]) 507 output =None 508for entry in outputList: 509if"depotFile"in entry: 510if entry["depotFile"] == depotPath: 511 output = entry 512break 513elif"data"in entry: 514 data = entry.get("data") 515 space = data.find(" ") 516if data[:space] == depotPath: 517 output = entry 518break 519if output ==None: 520return"" 521if output["code"] =="error": 522return"" 523 clientPath ="" 524if"path"in output: 525 clientPath = output.get("path") 526elif"data"in output: 527 data = output.get("data") 528 lastSpace = data.rfind(" ") 529 clientPath = data[lastSpace +1:] 530 531if clientPath.endswith("..."): 532 clientPath = clientPath[:-3] 533return clientPath 534 535defcurrentGitBranch(): 536returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 537 538defisValidGitDir(path): 539if(os.path.exists(path +"/HEAD") 540and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 541return True; 542return False 543 544defparseRevision(ref): 545returnread_pipe("git rev-parse%s"% ref).strip() 546 547defbranchExists(ref): 548 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 549 ignore_error=True) 550returnlen(rev) >0 551 552defextractLogMessageFromGitCommit(commit): 553 logMessage ="" 554 555## fixme: title is first line of commit, not 1st paragraph. 556 foundTitle =False 557for log inread_pipe_lines("git cat-file commit%s"% commit): 558if not foundTitle: 559iflen(log) ==1: 560 foundTitle =True 561continue 562 563 logMessage += log 564return logMessage 565 566defextractSettingsGitLog(log): 567 values = {} 568for line in log.split("\n"): 569 line = line.strip() 570 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 571if not m: 572continue 573 574 assignments = m.group(1).split(':') 575for a in assignments: 576 vals = a.split('=') 577 key = vals[0].strip() 578 val = ('='.join(vals[1:])).strip() 579if val.endswith('\"')and val.startswith('"'): 580 val = val[1:-1] 581 582 values[key] = val 583 584 paths = values.get("depot-paths") 585if not paths: 586 paths = values.get("depot-path") 587if paths: 588 values['depot-paths'] = paths.split(',') 589return values 590 591defgitBranchExists(branch): 592 proc = subprocess.Popen(["git","rev-parse", branch], 593 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 594return proc.wait() ==0; 595 596_gitConfig = {} 597 598defgitConfig(key): 599if not _gitConfig.has_key(key): 600 cmd = ["git","config", key ] 601 s =read_pipe(cmd, ignore_error=True) 602 _gitConfig[key] = s.strip() 603return _gitConfig[key] 604 605defgitConfigBool(key): 606"""Return a bool, using git config --bool. It is True only if the 607 variable is set to true, and False if set to false or not present 608 in the config.""" 609 610if not _gitConfig.has_key(key): 611 cmd = ["git","config","--bool", key ] 612 s =read_pipe(cmd, ignore_error=True) 613 v = s.strip() 614 _gitConfig[key] = v =="true" 615return _gitConfig[key] 616 617defgitConfigList(key): 618if not _gitConfig.has_key(key): 619 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 620 _gitConfig[key] = s.strip().split(os.linesep) 621return _gitConfig[key] 622 623defp4BranchesInGit(branchesAreInRemotes=True): 624"""Find all the branches whose names start with "p4/", looking 625 in remotes or heads as specified by the argument. Return 626 a dictionary of{ branch: revision }for each one found. 627 The branch names are the short names, without any 628 "p4/" prefix.""" 629 630 branches = {} 631 632 cmdline ="git rev-parse --symbolic " 633if branchesAreInRemotes: 634 cmdline +="--remotes" 635else: 636 cmdline +="--branches" 637 638for line inread_pipe_lines(cmdline): 639 line = line.strip() 640 641# only import to p4/ 642if not line.startswith('p4/'): 643continue 644# special symbolic ref to p4/master 645if line =="p4/HEAD": 646continue 647 648# strip off p4/ prefix 649 branch = line[len("p4/"):] 650 651 branches[branch] =parseRevision(line) 652 653return branches 654 655defbranch_exists(branch): 656"""Make sure that the given ref name really exists.""" 657 658 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 659 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 660 out, _ = p.communicate() 661if p.returncode: 662return False 663# expect exactly one line of output: the branch name 664return out.rstrip() == branch 665 666deffindUpstreamBranchPoint(head ="HEAD"): 667 branches =p4BranchesInGit() 668# map from depot-path to branch name 669 branchByDepotPath = {} 670for branch in branches.keys(): 671 tip = branches[branch] 672 log =extractLogMessageFromGitCommit(tip) 673 settings =extractSettingsGitLog(log) 674if settings.has_key("depot-paths"): 675 paths =",".join(settings["depot-paths"]) 676 branchByDepotPath[paths] ="remotes/p4/"+ branch 677 678 settings =None 679 parent =0 680while parent <65535: 681 commit = head +"~%s"% parent 682 log =extractLogMessageFromGitCommit(commit) 683 settings =extractSettingsGitLog(log) 684if settings.has_key("depot-paths"): 685 paths =",".join(settings["depot-paths"]) 686if branchByDepotPath.has_key(paths): 687return[branchByDepotPath[paths], settings] 688 689 parent = parent +1 690 691return["", settings] 692 693defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 694if not silent: 695print("Creating/updating branch(es) in%sbased on origin branch(es)" 696% localRefPrefix) 697 698 originPrefix ="origin/p4/" 699 700for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 701 line = line.strip() 702if(not line.startswith(originPrefix))or line.endswith("HEAD"): 703continue 704 705 headName = line[len(originPrefix):] 706 remoteHead = localRefPrefix + headName 707 originHead = line 708 709 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 710if(not original.has_key('depot-paths') 711or not original.has_key('change')): 712continue 713 714 update =False 715if notgitBranchExists(remoteHead): 716if verbose: 717print"creating%s"% remoteHead 718 update =True 719else: 720 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 721if settings.has_key('change') >0: 722if settings['depot-paths'] == original['depot-paths']: 723 originP4Change =int(original['change']) 724 p4Change =int(settings['change']) 725if originP4Change > p4Change: 726print("%s(%s) is newer than%s(%s). " 727"Updating p4 branch from origin." 728% (originHead, originP4Change, 729 remoteHead, p4Change)) 730 update =True 731else: 732print("Ignoring:%swas imported from%swhile " 733"%swas imported from%s" 734% (originHead,','.join(original['depot-paths']), 735 remoteHead,','.join(settings['depot-paths']))) 736 737if update: 738system("git update-ref%s %s"% (remoteHead, originHead)) 739 740deforiginP4BranchesExist(): 741returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 742 743defp4ChangesForPaths(depotPaths, changeRange): 744assert depotPaths 745 cmd = ['changes'] 746for p in depotPaths: 747 cmd += ["%s...%s"% (p, changeRange)] 748 output =p4_read_pipe_lines(cmd) 749 750 changes = {} 751for line in output: 752 changeNum =int(line.split(" ")[1]) 753 changes[changeNum] =True 754 755 changelist = changes.keys() 756 changelist.sort() 757return changelist 758 759defp4PathStartsWith(path, prefix): 760# This method tries to remedy a potential mixed-case issue: 761# 762# If UserA adds //depot/DirA/file1 763# and UserB adds //depot/dira/file2 764# 765# we may or may not have a problem. If you have core.ignorecase=true, 766# we treat DirA and dira as the same directory 767ifgitConfigBool("core.ignorecase"): 768return path.lower().startswith(prefix.lower()) 769return path.startswith(prefix) 770 771defgetClientSpec(): 772"""Look at the p4 client spec, create a View() object that contains 773 all the mappings, and return it.""" 774 775 specList =p4CmdList("client -o") 776iflen(specList) !=1: 777die('Output from "client -o" is%dlines, expecting 1'% 778len(specList)) 779 780# dictionary of all client parameters 781 entry = specList[0] 782 783# the //client/ name 784 client_name = entry["Client"] 785 786# just the keys that start with "View" 787 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 788 789# hold this new View 790 view =View(client_name) 791 792# append the lines, in order, to the view 793for view_num inrange(len(view_keys)): 794 k ="View%d"% view_num 795if k not in view_keys: 796die("Expected view key%smissing"% k) 797 view.append(entry[k]) 798 799return view 800 801defgetClientRoot(): 802"""Grab the client directory.""" 803 804 output =p4CmdList("client -o") 805iflen(output) !=1: 806die('Output from "client -o" is%dlines, expecting 1'%len(output)) 807 808 entry = output[0] 809if"Root"not in entry: 810die('Client has no "Root"') 811 812return entry["Root"] 813 814# 815# P4 wildcards are not allowed in filenames. P4 complains 816# if you simply add them, but you can force it with "-f", in 817# which case it translates them into %xx encoding internally. 818# 819defwildcard_decode(path): 820# Search for and fix just these four characters. Do % last so 821# that fixing it does not inadvertently create new %-escapes. 822# Cannot have * in a filename in windows; untested as to 823# what p4 would do in such a case. 824if not platform.system() =="Windows": 825 path = path.replace("%2A","*") 826 path = path.replace("%23","#") \ 827.replace("%40","@") \ 828.replace("%25","%") 829return path 830 831defwildcard_encode(path): 832# do % first to avoid double-encoding the %s introduced here 833 path = path.replace("%","%25") \ 834.replace("*","%2A") \ 835.replace("#","%23") \ 836.replace("@","%40") 837return path 838 839defwildcard_present(path): 840 m = re.search("[*#@%]", path) 841return m is not None 842 843class Command: 844def__init__(self): 845 self.usage ="usage: %prog [options]" 846 self.needsGit =True 847 self.verbose =False 848 849class P4UserMap: 850def__init__(self): 851 self.userMapFromPerforceServer =False 852 self.myP4UserId =None 853 854defp4UserId(self): 855if self.myP4UserId: 856return self.myP4UserId 857 858 results =p4CmdList("user -o") 859for r in results: 860if r.has_key('User'): 861 self.myP4UserId = r['User'] 862return r['User'] 863die("Could not find your p4 user id") 864 865defp4UserIsMe(self, p4User): 866# return True if the given p4 user is actually me 867 me = self.p4UserId() 868if not p4User or p4User != me: 869return False 870else: 871return True 872 873defgetUserCacheFilename(self): 874 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 875return home +"/.gitp4-usercache.txt" 876 877defgetUserMapFromPerforceServer(self): 878if self.userMapFromPerforceServer: 879return 880 self.users = {} 881 self.emails = {} 882 883for output inp4CmdList("users"): 884if not output.has_key("User"): 885continue 886 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 887 self.emails[output["Email"]] = output["User"] 888 889 890 s ='' 891for(key, val)in self.users.items(): 892 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 893 894open(self.getUserCacheFilename(),"wb").write(s) 895 self.userMapFromPerforceServer =True 896 897defloadUserMapFromCache(self): 898 self.users = {} 899 self.userMapFromPerforceServer =False 900try: 901 cache =open(self.getUserCacheFilename(),"rb") 902 lines = cache.readlines() 903 cache.close() 904for line in lines: 905 entry = line.strip().split("\t") 906 self.users[entry[0]] = entry[1] 907exceptIOError: 908 self.getUserMapFromPerforceServer() 909 910classP4Debug(Command): 911def__init__(self): 912 Command.__init__(self) 913 self.options = [] 914 self.description ="A tool to debug the output of p4 -G." 915 self.needsGit =False 916 917defrun(self, args): 918 j =0 919for output inp4CmdList(args): 920print'Element:%d'% j 921 j +=1 922print output 923return True 924 925classP4RollBack(Command): 926def__init__(self): 927 Command.__init__(self) 928 self.options = [ 929 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 930] 931 self.description ="A tool to debug the multi-branch import. Don't use :)" 932 self.rollbackLocalBranches =False 933 934defrun(self, args): 935iflen(args) !=1: 936return False 937 maxChange =int(args[0]) 938 939if"p4ExitCode"inp4Cmd("changes -m 1"): 940die("Problems executing p4"); 941 942if self.rollbackLocalBranches: 943 refPrefix ="refs/heads/" 944 lines =read_pipe_lines("git rev-parse --symbolic --branches") 945else: 946 refPrefix ="refs/remotes/" 947 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 948 949for line in lines: 950if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 951 line = line.strip() 952 ref = refPrefix + line 953 log =extractLogMessageFromGitCommit(ref) 954 settings =extractSettingsGitLog(log) 955 956 depotPaths = settings['depot-paths'] 957 change = settings['change'] 958 959 changed =False 960 961iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 962for p in depotPaths]))) ==0: 963print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 964system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 965continue 966 967while change andint(change) > maxChange: 968 changed =True 969if self.verbose: 970print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 971system("git update-ref%s\"%s^\""% (ref, ref)) 972 log =extractLogMessageFromGitCommit(ref) 973 settings =extractSettingsGitLog(log) 974 975 976 depotPaths = settings['depot-paths'] 977 change = settings['change'] 978 979if changed: 980print"%srewound to%s"% (ref, change) 981 982return True 983 984classP4Submit(Command, P4UserMap): 985 986 conflict_behavior_choices = ("ask","skip","quit") 987 988def__init__(self): 989 Command.__init__(self) 990 P4UserMap.__init__(self) 991 self.options = [ 992 optparse.make_option("--origin", dest="origin"), 993 optparse.make_option("-M", dest="detectRenames", action="store_true"), 994# preserve the user, requires relevant p4 permissions 995 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 996 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 997 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 998 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 999 optparse.make_option("--conflict", dest="conflict_behavior",1000 choices=self.conflict_behavior_choices),1001 optparse.make_option("--branch", dest="branch"),1002]1003 self.description ="Submit changes from git to the perforce depot."1004 self.usage +=" [name of git branch to submit into perforce depot]"1005 self.origin =""1006 self.detectRenames =False1007 self.preserveUser =gitConfigBool("git-p4.preserveUser")1008 self.dry_run =False1009 self.prepare_p4_only =False1010 self.conflict_behavior =None1011 self.isWindows = (platform.system() =="Windows")1012 self.exportLabels =False1013 self.p4HasMoveCommand =p4_has_move_command()1014 self.branch =None10151016defcheck(self):1017iflen(p4CmdList("opened ...")) >0:1018die("You have files opened with perforce! Close them before starting the sync.")10191020defseparate_jobs_from_description(self, message):1021"""Extract and return a possible Jobs field in the commit1022 message. It goes into a separate section in the p4 change1023 specification.10241025 A jobs line starts with "Jobs:" and looks like a new field1026 in a form. Values are white-space separated on the same1027 line or on following lines that start with a tab.10281029 This does not parse and extract the full git commit message1030 like a p4 form. It just sees the Jobs: line as a marker1031 to pass everything from then on directly into the p4 form,1032 but outside the description section.10331034 Return a tuple (stripped log message, jobs string)."""10351036 m = re.search(r'^Jobs:', message, re.MULTILINE)1037if m is None:1038return(message,None)10391040 jobtext = message[m.start():]1041 stripped_message = message[:m.start()].rstrip()1042return(stripped_message, jobtext)10431044defprepareLogMessage(self, template, message, jobs):1045"""Edits the template returned from "p4 change -o" to insert1046 the message in the Description field, and the jobs text in1047 the Jobs field."""1048 result =""10491050 inDescriptionSection =False10511052for line in template.split("\n"):1053if line.startswith("#"):1054 result += line +"\n"1055continue10561057if inDescriptionSection:1058if line.startswith("Files:")or line.startswith("Jobs:"):1059 inDescriptionSection =False1060# insert Jobs section1061if jobs:1062 result += jobs +"\n"1063else:1064continue1065else:1066if line.startswith("Description:"):1067 inDescriptionSection =True1068 line +="\n"1069for messageLine in message.split("\n"):1070 line +="\t"+ messageLine +"\n"10711072 result += line +"\n"10731074return result10751076defpatchRCSKeywords(self,file, pattern):1077# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1078(handle, outFileName) = tempfile.mkstemp(dir='.')1079try:1080 outFile = os.fdopen(handle,"w+")1081 inFile =open(file,"r")1082 regexp = re.compile(pattern, re.VERBOSE)1083for line in inFile.readlines():1084 line = regexp.sub(r'$\1$', line)1085 outFile.write(line)1086 inFile.close()1087 outFile.close()1088# Forcibly overwrite the original file1089 os.unlink(file)1090 shutil.move(outFileName,file)1091except:1092# cleanup our temporary file1093 os.unlink(outFileName)1094print"Failed to strip RCS keywords in%s"%file1095raise10961097print"Patched up RCS keywords in%s"%file10981099defp4UserForCommit(self,id):1100# Return the tuple (perforce user,git email) for a given git commit id1101 self.getUserMapFromPerforceServer()1102 gitEmail =read_pipe(["git","log","--max-count=1",1103"--format=%ae",id])1104 gitEmail = gitEmail.strip()1105if not self.emails.has_key(gitEmail):1106return(None,gitEmail)1107else:1108return(self.emails[gitEmail],gitEmail)11091110defcheckValidP4Users(self,commits):1111# check if any git authors cannot be mapped to p4 users1112foridin commits:1113(user,email) = self.p4UserForCommit(id)1114if not user:1115 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1116ifgitConfigBool("git-p4.allowMissingP4Users"):1117print"%s"% msg1118else:1119die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11201121deflastP4Changelist(self):1122# Get back the last changelist number submitted in this client spec. This1123# then gets used to patch up the username in the change. If the same1124# client spec is being used by multiple processes then this might go1125# wrong.1126 results =p4CmdList("client -o")# find the current client1127 client =None1128for r in results:1129if r.has_key('Client'):1130 client = r['Client']1131break1132if not client:1133die("could not get client spec")1134 results =p4CmdList(["changes","-c", client,"-m","1"])1135for r in results:1136if r.has_key('change'):1137return r['change']1138die("Could not get changelist number for last submit - cannot patch up user details")11391140defmodifyChangelistUser(self, changelist, newUser):1141# fixup the user field of a changelist after it has been submitted.1142 changes =p4CmdList("change -o%s"% changelist)1143iflen(changes) !=1:1144die("Bad output from p4 change modifying%sto user%s"%1145(changelist, newUser))11461147 c = changes[0]1148if c['User'] == newUser:return# nothing to do1149 c['User'] = newUser1150input= marshal.dumps(c)11511152 result =p4CmdList("change -f -i", stdin=input)1153for r in result:1154if r.has_key('code'):1155if r['code'] =='error':1156die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1157if r.has_key('data'):1158print("Updated user field for changelist%sto%s"% (changelist, newUser))1159return1160die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11611162defcanChangeChangelists(self):1163# check to see if we have p4 admin or super-user permissions, either of1164# which are required to modify changelists.1165 results =p4CmdList(["protects", self.depotPath])1166for r in results:1167if r.has_key('perm'):1168if r['perm'] =='admin':1169return11170if r['perm'] =='super':1171return11172return011731174defprepareSubmitTemplate(self):1175"""Run "p4 change -o" to grab a change specification template.1176 This does not use "p4 -G", as it is nice to keep the submission1177 template in original order, since a human might edit it.11781179 Remove lines in the Files section that show changes to files1180 outside the depot path we're committing into."""11811182 template =""1183 inFilesSection =False1184for line inp4_read_pipe_lines(['change','-o']):1185if line.endswith("\r\n"):1186 line = line[:-2] +"\n"1187if inFilesSection:1188if line.startswith("\t"):1189# path starts and ends with a tab1190 path = line[1:]1191 lastTab = path.rfind("\t")1192if lastTab != -1:1193 path = path[:lastTab]1194if notp4PathStartsWith(path, self.depotPath):1195continue1196else:1197 inFilesSection =False1198else:1199if line.startswith("Files:"):1200 inFilesSection =True12011202 template += line12031204return template12051206defedit_template(self, template_file):1207"""Invoke the editor to let the user change the submission1208 message. Return true if okay to continue with the submit."""12091210# if configured to skip the editing part, just submit1211ifgitConfigBool("git-p4.skipSubmitEdit"):1212return True12131214# look at the modification time, to check later if the user saved1215# the file1216 mtime = os.stat(template_file).st_mtime12171218# invoke the editor1219if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1220 editor = os.environ.get("P4EDITOR")1221else:1222 editor =read_pipe("git var GIT_EDITOR").strip()1223system([editor, template_file])12241225# If the file was not saved, prompt to see if this patch should1226# be skipped. But skip this verification step if configured so.1227ifgitConfigBool("git-p4.skipSubmitEditCheck"):1228return True12291230# modification time updated means user saved the file1231if os.stat(template_file).st_mtime > mtime:1232return True12331234while True:1235 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1236if response =='y':1237return True1238if response =='n':1239return False12401241defapplyCommit(self,id):1242"""Apply one commit, return True if it succeeded."""12431244print"Applying",read_pipe(["git","show","-s",1245"--format=format:%h%s",id])12461247(p4User, gitEmail) = self.p4UserForCommit(id)12481249 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1250 filesToAdd =set()1251 filesToDelete =set()1252 editedFiles =set()1253 pureRenameCopy =set()1254 filesToChangeExecBit = {}12551256for line in diff:1257 diff =parseDiffTreeEntry(line)1258 modifier = diff['status']1259 path = diff['src']1260if modifier =="M":1261p4_edit(path)1262ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1263 filesToChangeExecBit[path] = diff['dst_mode']1264 editedFiles.add(path)1265elif modifier =="A":1266 filesToAdd.add(path)1267 filesToChangeExecBit[path] = diff['dst_mode']1268if path in filesToDelete:1269 filesToDelete.remove(path)1270elif modifier =="D":1271 filesToDelete.add(path)1272if path in filesToAdd:1273 filesToAdd.remove(path)1274elif modifier =="C":1275 src, dest = diff['src'], diff['dst']1276p4_integrate(src, dest)1277 pureRenameCopy.add(dest)1278if diff['src_sha1'] != diff['dst_sha1']:1279p4_edit(dest)1280 pureRenameCopy.discard(dest)1281ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1282p4_edit(dest)1283 pureRenameCopy.discard(dest)1284 filesToChangeExecBit[dest] = diff['dst_mode']1285if self.isWindows:1286# turn off read-only attribute1287 os.chmod(dest, stat.S_IWRITE)1288 os.unlink(dest)1289 editedFiles.add(dest)1290elif modifier =="R":1291 src, dest = diff['src'], diff['dst']1292if self.p4HasMoveCommand:1293p4_edit(src)# src must be open before move1294p4_move(src, dest)# opens for (move/delete, move/add)1295else:1296p4_integrate(src, dest)1297if diff['src_sha1'] != diff['dst_sha1']:1298p4_edit(dest)1299else:1300 pureRenameCopy.add(dest)1301ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1302if not self.p4HasMoveCommand:1303p4_edit(dest)# with move: already open, writable1304 filesToChangeExecBit[dest] = diff['dst_mode']1305if not self.p4HasMoveCommand:1306if self.isWindows:1307 os.chmod(dest, stat.S_IWRITE)1308 os.unlink(dest)1309 filesToDelete.add(src)1310 editedFiles.add(dest)1311else:1312die("unknown modifier%sfor%s"% (modifier, path))13131314 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1315 patchcmd = diffcmd +" | git apply "1316 tryPatchCmd = patchcmd +"--check -"1317 applyPatchCmd = patchcmd +"--check --apply -"1318 patch_succeeded =True13191320if os.system(tryPatchCmd) !=0:1321 fixed_rcs_keywords =False1322 patch_succeeded =False1323print"Unfortunately applying the change failed!"13241325# Patch failed, maybe it's just RCS keyword woes. Look through1326# the patch to see if that's possible.1327ifgitConfigBool("git-p4.attemptRCSCleanup"):1328file=None1329 pattern =None1330 kwfiles = {}1331forfilein editedFiles | filesToDelete:1332# did this file's delta contain RCS keywords?1333 pattern =p4_keywords_regexp_for_file(file)13341335if pattern:1336# this file is a possibility...look for RCS keywords.1337 regexp = re.compile(pattern, re.VERBOSE)1338for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1339if regexp.search(line):1340if verbose:1341print"got keyword match on%sin%sin%s"% (pattern, line,file)1342 kwfiles[file] = pattern1343break13441345forfilein kwfiles:1346if verbose:1347print"zapping%swith%s"% (line,pattern)1348# File is being deleted, so not open in p4. Must1349# disable the read-only bit on windows.1350if self.isWindows andfilenot in editedFiles:1351 os.chmod(file, stat.S_IWRITE)1352 self.patchRCSKeywords(file, kwfiles[file])1353 fixed_rcs_keywords =True13541355if fixed_rcs_keywords:1356print"Retrying the patch with RCS keywords cleaned up"1357if os.system(tryPatchCmd) ==0:1358 patch_succeeded =True13591360if not patch_succeeded:1361for f in editedFiles:1362p4_revert(f)1363return False13641365#1366# Apply the patch for real, and do add/delete/+x handling.1367#1368system(applyPatchCmd)13691370for f in filesToAdd:1371p4_add(f)1372for f in filesToDelete:1373p4_revert(f)1374p4_delete(f)13751376# Set/clear executable bits1377for f in filesToChangeExecBit.keys():1378 mode = filesToChangeExecBit[f]1379setP4ExecBit(f, mode)13801381#1382# Build p4 change description, starting with the contents1383# of the git commit message.1384#1385 logMessage =extractLogMessageFromGitCommit(id)1386 logMessage = logMessage.strip()1387(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13881389 template = self.prepareSubmitTemplate()1390 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13911392if self.preserveUser:1393 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13941395if self.checkAuthorship and not self.p4UserIsMe(p4User):1396 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1397 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1398 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13991400 separatorLine ="######## everything below this line is just the diff #######\n"14011402# diff1403if os.environ.has_key("P4DIFF"):1404del(os.environ["P4DIFF"])1405 diff =""1406for editedFile in editedFiles:1407 diff +=p4_read_pipe(['diff','-du',1408wildcard_encode(editedFile)])14091410# new file diff1411 newdiff =""1412for newFile in filesToAdd:1413 newdiff +="==== new file ====\n"1414 newdiff +="--- /dev/null\n"1415 newdiff +="+++%s\n"% newFile1416 f =open(newFile,"r")1417for line in f.readlines():1418 newdiff +="+"+ line1419 f.close()14201421# change description file: submitTemplate, separatorLine, diff, newdiff1422(handle, fileName) = tempfile.mkstemp()1423 tmpFile = os.fdopen(handle,"w+")1424if self.isWindows:1425 submitTemplate = submitTemplate.replace("\n","\r\n")1426 separatorLine = separatorLine.replace("\n","\r\n")1427 newdiff = newdiff.replace("\n","\r\n")1428 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1429 tmpFile.close()14301431if self.prepare_p4_only:1432#1433# Leave the p4 tree prepared, and the submit template around1434# and let the user decide what to do next1435#1436print1437print"P4 workspace prepared for submission."1438print"To submit or revert, go to client workspace"1439print" "+ self.clientPath1440print1441print"To submit, use\"p4 submit\"to write a new description,"1442print"or\"p4 submit -i%s\"to use the one prepared by" \1443"\"git p4\"."% fileName1444print"You can delete the file\"%s\"when finished."% fileName14451446if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1447print"To preserve change ownership by user%s, you must\n" \1448"do\"p4 change -f <change>\"after submitting and\n" \1449"edit the User field."1450if pureRenameCopy:1451print"After submitting, renamed files must be re-synced."1452print"Invoke\"p4 sync -f\"on each of these files:"1453for f in pureRenameCopy:1454print" "+ f14551456print1457print"To revert the changes, use\"p4 revert ...\", and delete"1458print"the submit template file\"%s\""% fileName1459if filesToAdd:1460print"Since the commit adds new files, they must be deleted:"1461for f in filesToAdd:1462print" "+ f1463print1464return True14651466#1467# Let the user edit the change description, then submit it.1468#1469if self.edit_template(fileName):1470# read the edited message and submit1471 ret =True1472 tmpFile =open(fileName,"rb")1473 message = tmpFile.read()1474 tmpFile.close()1475 submitTemplate = message[:message.index(separatorLine)]1476if self.isWindows:1477 submitTemplate = submitTemplate.replace("\r\n","\n")1478p4_write_pipe(['submit','-i'], submitTemplate)14791480if self.preserveUser:1481if p4User:1482# Get last changelist number. Cannot easily get it from1483# the submit command output as the output is1484# unmarshalled.1485 changelist = self.lastP4Changelist()1486 self.modifyChangelistUser(changelist, p4User)14871488# The rename/copy happened by applying a patch that created a1489# new file. This leaves it writable, which confuses p4.1490for f in pureRenameCopy:1491p4_sync(f,"-f")14921493else:1494# skip this patch1495 ret =False1496print"Submission cancelled, undoing p4 changes."1497for f in editedFiles:1498p4_revert(f)1499for f in filesToAdd:1500p4_revert(f)1501 os.remove(f)1502for f in filesToDelete:1503p4_revert(f)15041505 os.remove(fileName)1506return ret15071508# Export git tags as p4 labels. Create a p4 label and then tag1509# with that.1510defexportGitTags(self, gitTags):1511 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1512iflen(validLabelRegexp) ==0:1513 validLabelRegexp = defaultLabelRegexp1514 m = re.compile(validLabelRegexp)15151516for name in gitTags:15171518if not m.match(name):1519if verbose:1520print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1521continue15221523# Get the p4 commit this corresponds to1524 logMessage =extractLogMessageFromGitCommit(name)1525 values =extractSettingsGitLog(logMessage)15261527if not values.has_key('change'):1528# a tag pointing to something not sent to p4; ignore1529if verbose:1530print"git tag%sdoes not give a p4 commit"% name1531continue1532else:1533 changelist = values['change']15341535# Get the tag details.1536 inHeader =True1537 isAnnotated =False1538 body = []1539for l inread_pipe_lines(["git","cat-file","-p", name]):1540 l = l.strip()1541if inHeader:1542if re.match(r'tag\s+', l):1543 isAnnotated =True1544elif re.match(r'\s*$', l):1545 inHeader =False1546continue1547else:1548 body.append(l)15491550if not isAnnotated:1551 body = ["lightweight tag imported by git p4\n"]15521553# Create the label - use the same view as the client spec we are using1554 clientSpec =getClientSpec()15551556 labelTemplate ="Label:%s\n"% name1557 labelTemplate +="Description:\n"1558for b in body:1559 labelTemplate +="\t"+ b +"\n"1560 labelTemplate +="View:\n"1561for depot_side in clientSpec.mappings:1562 labelTemplate +="\t%s\n"% depot_side15631564if self.dry_run:1565print"Would create p4 label%sfor tag"% name1566elif self.prepare_p4_only:1567print"Not creating p4 label%sfor tag due to option" \1568" --prepare-p4-only"% name1569else:1570p4_write_pipe(["label","-i"], labelTemplate)15711572# Use the label1573p4_system(["tag","-l", name] +1574["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])15751576if verbose:1577print"created p4 label for tag%s"% name15781579defrun(self, args):1580iflen(args) ==0:1581 self.master =currentGitBranch()1582iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1583die("Detecting current git branch failed!")1584eliflen(args) ==1:1585 self.master = args[0]1586if notbranchExists(self.master):1587die("Branch%sdoes not exist"% self.master)1588else:1589return False15901591 allowSubmit =gitConfig("git-p4.allowSubmit")1592iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1593die("%sis not in git-p4.allowSubmit"% self.master)15941595[upstream, settings] =findUpstreamBranchPoint()1596 self.depotPath = settings['depot-paths'][0]1597iflen(self.origin) ==0:1598 self.origin = upstream15991600if self.preserveUser:1601if not self.canChangeChangelists():1602die("Cannot preserve user names without p4 super-user or admin permissions")16031604# if not set from the command line, try the config file1605if self.conflict_behavior is None:1606 val =gitConfig("git-p4.conflict")1607if val:1608if val not in self.conflict_behavior_choices:1609die("Invalid value '%s' for config git-p4.conflict"% val)1610else:1611 val ="ask"1612 self.conflict_behavior = val16131614if self.verbose:1615print"Origin branch is "+ self.origin16161617iflen(self.depotPath) ==0:1618print"Internal error: cannot locate perforce depot path from existing branches"1619 sys.exit(128)16201621 self.useClientSpec =False1622ifgitConfigBool("git-p4.useclientspec"):1623 self.useClientSpec =True1624if self.useClientSpec:1625 self.clientSpecDirs =getClientSpec()16261627if self.useClientSpec:1628# all files are relative to the client spec1629 self.clientPath =getClientRoot()1630else:1631 self.clientPath =p4Where(self.depotPath)16321633if self.clientPath =="":1634die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)16351636print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1637 self.oldWorkingDirectory = os.getcwd()16381639# ensure the clientPath exists1640 new_client_dir =False1641if not os.path.exists(self.clientPath):1642 new_client_dir =True1643 os.makedirs(self.clientPath)16441645chdir(self.clientPath, is_client_path=True)1646if self.dry_run:1647print"Would synchronize p4 checkout in%s"% self.clientPath1648else:1649print"Synchronizing p4 checkout..."1650if new_client_dir:1651# old one was destroyed, and maybe nobody told p41652p4_sync("...","-f")1653else:1654p4_sync("...")1655 self.check()16561657 commits = []1658for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1659 commits.append(line.strip())1660 commits.reverse()16611662if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1663 self.checkAuthorship =False1664else:1665 self.checkAuthorship =True16661667if self.preserveUser:1668 self.checkValidP4Users(commits)16691670#1671# Build up a set of options to be passed to diff when1672# submitting each commit to p4.1673#1674if self.detectRenames:1675# command-line -M arg1676 self.diffOpts ="-M"1677else:1678# If not explicitly set check the config variable1679 detectRenames =gitConfig("git-p4.detectRenames")16801681if detectRenames.lower() =="false"or detectRenames =="":1682 self.diffOpts =""1683elif detectRenames.lower() =="true":1684 self.diffOpts ="-M"1685else:1686 self.diffOpts ="-M%s"% detectRenames16871688# no command-line arg for -C or --find-copies-harder, just1689# config variables1690 detectCopies =gitConfig("git-p4.detectCopies")1691if detectCopies.lower() =="false"or detectCopies =="":1692pass1693elif detectCopies.lower() =="true":1694 self.diffOpts +=" -C"1695else:1696 self.diffOpts +=" -C%s"% detectCopies16971698ifgitConfigBool("git-p4.detectCopiesHarder"):1699 self.diffOpts +=" --find-copies-harder"17001701#1702# Apply the commits, one at a time. On failure, ask if should1703# continue to try the rest of the patches, or quit.1704#1705if self.dry_run:1706print"Would apply"1707 applied = []1708 last =len(commits) -11709for i, commit inenumerate(commits):1710if self.dry_run:1711print" ",read_pipe(["git","show","-s",1712"--format=format:%h%s", commit])1713 ok =True1714else:1715 ok = self.applyCommit(commit)1716if ok:1717 applied.append(commit)1718else:1719if self.prepare_p4_only and i < last:1720print"Processing only the first commit due to option" \1721" --prepare-p4-only"1722break1723if i < last:1724 quit =False1725while True:1726# prompt for what to do, or use the option/variable1727if self.conflict_behavior =="ask":1728print"What do you want to do?"1729 response =raw_input("[s]kip this commit but apply"1730" the rest, or [q]uit? ")1731if not response:1732continue1733elif self.conflict_behavior =="skip":1734 response ="s"1735elif self.conflict_behavior =="quit":1736 response ="q"1737else:1738die("Unknown conflict_behavior '%s'"%1739 self.conflict_behavior)17401741if response[0] =="s":1742print"Skipping this commit, but applying the rest"1743break1744if response[0] =="q":1745print"Quitting"1746 quit =True1747break1748if quit:1749break17501751chdir(self.oldWorkingDirectory)17521753if self.dry_run:1754pass1755elif self.prepare_p4_only:1756pass1757eliflen(commits) ==len(applied):1758print"All commits applied!"17591760 sync =P4Sync()1761if self.branch:1762 sync.branch = self.branch1763 sync.run([])17641765 rebase =P4Rebase()1766 rebase.rebase()17671768else:1769iflen(applied) ==0:1770print"No commits applied."1771else:1772print"Applied only the commits marked with '*':"1773for c in commits:1774if c in applied:1775 star ="*"1776else:1777 star =" "1778print star,read_pipe(["git","show","-s",1779"--format=format:%h%s", c])1780print"You will have to do 'git p4 sync' and rebase."17811782ifgitConfigBool("git-p4.exportLabels"):1783 self.exportLabels =True17841785if self.exportLabels:1786 p4Labels =getP4Labels(self.depotPath)1787 gitTags =getGitTags()17881789 missingGitTags = gitTags - p4Labels1790 self.exportGitTags(missingGitTags)17911792# exit with error unless everything applied perfectly1793iflen(commits) !=len(applied):1794 sys.exit(1)17951796return True17971798classView(object):1799"""Represent a p4 view ("p4 help views"), and map files in a1800 repo according to the view."""18011802def__init__(self, client_name):1803 self.mappings = []1804 self.client_prefix ="//%s/"% client_name1805# cache results of "p4 where" to lookup client file locations1806 self.client_spec_path_cache = {}18071808defappend(self, view_line):1809"""Parse a view line, splitting it into depot and client1810 sides. Append to self.mappings, preserving order. This1811 is only needed for tag creation."""18121813# Split the view line into exactly two words. P4 enforces1814# structure on these lines that simplifies this quite a bit.1815#1816# Either or both words may be double-quoted.1817# Single quotes do not matter.1818# Double-quote marks cannot occur inside the words.1819# A + or - prefix is also inside the quotes.1820# There are no quotes unless they contain a space.1821# The line is already white-space stripped.1822# The two words are separated by a single space.1823#1824if view_line[0] =='"':1825# First word is double quoted. Find its end.1826 close_quote_index = view_line.find('"',1)1827if close_quote_index <=0:1828die("No first-word closing quote found:%s"% view_line)1829 depot_side = view_line[1:close_quote_index]1830# skip closing quote and space1831 rhs_index = close_quote_index +1+11832else:1833 space_index = view_line.find(" ")1834if space_index <=0:1835die("No word-splitting space found:%s"% view_line)1836 depot_side = view_line[0:space_index]1837 rhs_index = space_index +118381839# prefix + means overlay on previous mapping1840if depot_side.startswith("+"):1841 depot_side = depot_side[1:]18421843# prefix - means exclude this path, leave out of mappings1844 exclude =False1845if depot_side.startswith("-"):1846 exclude =True1847 depot_side = depot_side[1:]18481849if not exclude:1850 self.mappings.append(depot_side)18511852defconvert_client_path(self, clientFile):1853# chop off //client/ part to make it relative1854if not clientFile.startswith(self.client_prefix):1855die("No prefix '%s' on clientFile '%s'"%1856(self.client_prefix, clientFile))1857return clientFile[len(self.client_prefix):]18581859defupdate_client_spec_path_cache(self, files):1860""" Caching file paths by "p4 where" batch query """18611862# List depot file paths exclude that already cached1863 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]18641865iflen(fileArgs) ==0:1866return# All files in cache18671868 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1869for res in where_result:1870if"code"in res and res["code"] =="error":1871# assume error is "... file(s) not in client view"1872continue1873if"clientFile"not in res:1874die("No clientFile in 'p4 where' output")1875if"unmap"in res:1876# it will list all of them, but only one not unmap-ped1877continue1878 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])18791880# not found files or unmap files set to ""1881for depotFile in fileArgs:1882if depotFile not in self.client_spec_path_cache:1883 self.client_spec_path_cache[depotFile] =""18841885defmap_in_client(self, depot_path):1886"""Return the relative location in the client where this1887 depot file should live. Returns "" if the file should1888 not be mapped in the client."""18891890if depot_path in self.client_spec_path_cache:1891return self.client_spec_path_cache[depot_path]18921893die("Error:%sis not found in client spec path"% depot_path )1894return""18951896classP4Sync(Command, P4UserMap):1897 delete_actions = ("delete","move/delete","purge")18981899def__init__(self):1900 Command.__init__(self)1901 P4UserMap.__init__(self)1902 self.options = [1903 optparse.make_option("--branch", dest="branch"),1904 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1905 optparse.make_option("--changesfile", dest="changesFile"),1906 optparse.make_option("--silent", dest="silent", action="store_true"),1907 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1908 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1909 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1910help="Import into refs/heads/ , not refs/remotes"),1911 optparse.make_option("--max-changes", dest="maxChanges"),1912 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1913help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1914 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1915help="Only sync files that are included in the Perforce Client Spec")1916]1917 self.description ="""Imports from Perforce into a git repository.\n1918 example:1919 //depot/my/project/ -- to import the current head1920 //depot/my/project/@all -- to import everything1921 //depot/my/project/@1,6 -- to import only from revision 1 to 619221923 (a ... is not needed in the path p4 specification, it's added implicitly)"""19241925 self.usage +=" //depot/path[@revRange]"1926 self.silent =False1927 self.createdBranches =set()1928 self.committedChanges =set()1929 self.branch =""1930 self.detectBranches =False1931 self.detectLabels =False1932 self.importLabels =False1933 self.changesFile =""1934 self.syncWithOrigin =True1935 self.importIntoRemotes =True1936 self.maxChanges =""1937 self.keepRepoPath =False1938 self.depotPaths =None1939 self.p4BranchesInGit = []1940 self.cloneExclude = []1941 self.useClientSpec =False1942 self.useClientSpec_from_options =False1943 self.clientSpecDirs =None1944 self.tempBranches = []1945 self.tempBranchLocation ="git-p4-tmp"19461947ifgitConfig("git-p4.syncFromOrigin") =="false":1948 self.syncWithOrigin =False19491950# Force a checkpoint in fast-import and wait for it to finish1951defcheckpoint(self):1952 self.gitStream.write("checkpoint\n\n")1953 self.gitStream.write("progress checkpoint\n\n")1954 out = self.gitOutput.readline()1955if self.verbose:1956print"checkpoint finished: "+ out19571958defextractFilesFromCommit(self, commit):1959 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1960for path in self.cloneExclude]1961 files = []1962 fnum =01963while commit.has_key("depotFile%s"% fnum):1964 path = commit["depotFile%s"% fnum]19651966if[p for p in self.cloneExclude1967ifp4PathStartsWith(path, p)]:1968 found =False1969else:1970 found = [p for p in self.depotPaths1971ifp4PathStartsWith(path, p)]1972if not found:1973 fnum = fnum +11974continue19751976file= {}1977file["path"] = path1978file["rev"] = commit["rev%s"% fnum]1979file["action"] = commit["action%s"% fnum]1980file["type"] = commit["type%s"% fnum]1981 files.append(file)1982 fnum = fnum +11983return files19841985defstripRepoPath(self, path, prefixes):1986"""When streaming files, this is called to map a p4 depot path1987 to where it should go in git. The prefixes are either1988 self.depotPaths, or self.branchPrefixes in the case of1989 branch detection."""19901991if self.useClientSpec:1992# branch detection moves files up a level (the branch name)1993# from what client spec interpretation gives1994 path = self.clientSpecDirs.map_in_client(path)1995if self.detectBranches:1996for b in self.knownBranches:1997if path.startswith(b +"/"):1998 path = path[len(b)+1:]19992000elif self.keepRepoPath:2001# Preserve everything in relative path name except leading2002# //depot/; just look at first prefix as they all should2003# be in the same depot.2004 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2005ifp4PathStartsWith(path, depot):2006 path = path[len(depot):]20072008else:2009for p in prefixes:2010ifp4PathStartsWith(path, p):2011 path = path[len(p):]2012break20132014 path =wildcard_decode(path)2015return path20162017defsplitFilesIntoBranches(self, commit):2018"""Look at each depotFile in the commit to figure out to what2019 branch it belongs."""20202021if self.clientSpecDirs:2022 files = self.extractFilesFromCommit(commit)2023 self.clientSpecDirs.update_client_spec_path_cache(files)20242025 branches = {}2026 fnum =02027while commit.has_key("depotFile%s"% fnum):2028 path = commit["depotFile%s"% fnum]2029 found = [p for p in self.depotPaths2030ifp4PathStartsWith(path, p)]2031if not found:2032 fnum = fnum +12033continue20342035file= {}2036file["path"] = path2037file["rev"] = commit["rev%s"% fnum]2038file["action"] = commit["action%s"% fnum]2039file["type"] = commit["type%s"% fnum]2040 fnum = fnum +120412042# start with the full relative path where this file would2043# go in a p4 client2044if self.useClientSpec:2045 relPath = self.clientSpecDirs.map_in_client(path)2046else:2047 relPath = self.stripRepoPath(path, self.depotPaths)20482049for branch in self.knownBranches.keys():2050# add a trailing slash so that a commit into qt/4.2foo2051# doesn't end up in qt/4.2, e.g.2052if relPath.startswith(branch +"/"):2053if branch not in branches:2054 branches[branch] = []2055 branches[branch].append(file)2056break20572058return branches20592060# output one file from the P4 stream2061# - helper for streamP4Files20622063defstreamOneP4File(self,file, contents):2064 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2065if verbose:2066 sys.stderr.write("%s\n"% relPath)20672068(type_base, type_mods) =split_p4_type(file["type"])20692070 git_mode ="100644"2071if"x"in type_mods:2072 git_mode ="100755"2073if type_base =="symlink":2074 git_mode ="120000"2075# p4 print on a symlink sometimes contains "target\n";2076# if it does, remove the newline2077 data =''.join(contents)2078if not data:2079# Some version of p4 allowed creating a symlink that pointed2080# to nothing. This causes p4 errors when checking out such2081# a change, and errors here too. Work around it by ignoring2082# the bad symlink; hopefully a future change fixes it.2083print"\nIgnoring empty symlink in%s"%file['depotFile']2084return2085elif data[-1] =='\n':2086 contents = [data[:-1]]2087else:2088 contents = [data]20892090if type_base =="utf16":2091# p4 delivers different text in the python output to -G2092# than it does when using "print -o", or normal p4 client2093# operations. utf16 is converted to ascii or utf8, perhaps.2094# But ascii text saved as -t utf16 is completely mangled.2095# Invoke print -o to get the real contents.2096#2097# On windows, the newlines will always be mangled by print, so put2098# them back too. This is not needed to the cygwin windows version,2099# just the native "NT" type.2100#2101 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2102ifp4_version_string().find("/NT") >=0:2103 text = text.replace("\r\n","\n")2104 contents = [ text ]21052106if type_base =="apple":2107# Apple filetype files will be streamed as a concatenation of2108# its appledouble header and the contents. This is useless2109# on both macs and non-macs. If using "print -q -o xx", it2110# will create "xx" with the data, and "%xx" with the header.2111# This is also not very useful.2112#2113# Ideally, someday, this script can learn how to generate2114# appledouble files directly and import those to git, but2115# non-mac machines can never find a use for apple filetype.2116print"\nIgnoring apple filetype file%s"%file['depotFile']2117return21182119# Note that we do not try to de-mangle keywords on utf16 files,2120# even though in theory somebody may want that.2121 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2122if pattern:2123 regexp = re.compile(pattern, re.VERBOSE)2124 text =''.join(contents)2125 text = regexp.sub(r'$\1$', text)2126 contents = [ text ]21272128 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21292130# total length...2131 length =02132for d in contents:2133 length = length +len(d)21342135 self.gitStream.write("data%d\n"% length)2136for d in contents:2137 self.gitStream.write(d)2138 self.gitStream.write("\n")21392140defstreamOneP4Deletion(self,file):2141 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2142if verbose:2143 sys.stderr.write("delete%s\n"% relPath)2144 self.gitStream.write("D%s\n"% relPath)21452146# handle another chunk of streaming data2147defstreamP4FilesCb(self, marshalled):21482149# catch p4 errors and complain2150 err =None2151if"code"in marshalled:2152if marshalled["code"] =="error":2153if"data"in marshalled:2154 err = marshalled["data"].rstrip()2155if err:2156 f =None2157if self.stream_have_file_info:2158if"depotFile"in self.stream_file:2159 f = self.stream_file["depotFile"]2160# force a failure in fast-import, else an empty2161# commit will be made2162 self.gitStream.write("\n")2163 self.gitStream.write("die-now\n")2164 self.gitStream.close()2165# ignore errors, but make sure it exits first2166 self.importProcess.wait()2167if f:2168die("Error from p4 print for%s:%s"% (f, err))2169else:2170die("Error from p4 print:%s"% err)21712172if marshalled.has_key('depotFile')and self.stream_have_file_info:2173# start of a new file - output the old one first2174 self.streamOneP4File(self.stream_file, self.stream_contents)2175 self.stream_file = {}2176 self.stream_contents = []2177 self.stream_have_file_info =False21782179# pick up the new file information... for the2180# 'data' field we need to append to our array2181for k in marshalled.keys():2182if k =='data':2183 self.stream_contents.append(marshalled['data'])2184else:2185 self.stream_file[k] = marshalled[k]21862187 self.stream_have_file_info =True21882189# Stream directly from "p4 files" into "git fast-import"2190defstreamP4Files(self, files):2191 filesForCommit = []2192 filesToRead = []2193 filesToDelete = []21942195for f in files:2196# if using a client spec, only add the files that have2197# a path in the client2198if self.clientSpecDirs:2199if self.clientSpecDirs.map_in_client(f['path']) =="":2200continue22012202 filesForCommit.append(f)2203if f['action']in self.delete_actions:2204 filesToDelete.append(f)2205else:2206 filesToRead.append(f)22072208# deleted files...2209for f in filesToDelete:2210 self.streamOneP4Deletion(f)22112212iflen(filesToRead) >0:2213 self.stream_file = {}2214 self.stream_contents = []2215 self.stream_have_file_info =False22162217# curry self argument2218defstreamP4FilesCbSelf(entry):2219 self.streamP4FilesCb(entry)22202221 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22222223p4CmdList(["-x","-","print"],2224 stdin=fileArgs,2225 cb=streamP4FilesCbSelf)22262227# do the last chunk2228if self.stream_file.has_key('depotFile'):2229 self.streamOneP4File(self.stream_file, self.stream_contents)22302231defmake_email(self, userid):2232if userid in self.users:2233return self.users[userid]2234else:2235return"%s<a@b>"% userid22362237# Stream a p4 tag2238defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2239if verbose:2240print"writing tag%sfor commit%s"% (labelName, commit)2241 gitStream.write("tag%s\n"% labelName)2242 gitStream.write("from%s\n"% commit)22432244if labelDetails.has_key('Owner'):2245 owner = labelDetails["Owner"]2246else:2247 owner =None22482249# Try to use the owner of the p4 label, or failing that,2250# the current p4 user id.2251if owner:2252 email = self.make_email(owner)2253else:2254 email = self.make_email(self.p4UserId())2255 tagger ="%s %s %s"% (email, epoch, self.tz)22562257 gitStream.write("tagger%s\n"% tagger)22582259print"labelDetails=",labelDetails2260if labelDetails.has_key('Description'):2261 description = labelDetails['Description']2262else:2263 description ='Label from git p4'22642265 gitStream.write("data%d\n"%len(description))2266 gitStream.write(description)2267 gitStream.write("\n")22682269defcommit(self, details, files, branch, parent =""):2270 epoch = details["time"]2271 author = details["user"]22722273if self.verbose:2274print"commit into%s"% branch22752276# start with reading files; if that fails, we should not2277# create a commit.2278 new_files = []2279for f in files:2280if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2281 new_files.append(f)2282else:2283 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])22842285if self.clientSpecDirs:2286 self.clientSpecDirs.update_client_spec_path_cache(files)22872288 self.gitStream.write("commit%s\n"% branch)2289# gitStream.write("mark :%s\n" % details["change"])2290 self.committedChanges.add(int(details["change"]))2291 committer =""2292if author not in self.users:2293 self.getUserMapFromPerforceServer()2294 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)22952296 self.gitStream.write("committer%s\n"% committer)22972298 self.gitStream.write("data <<EOT\n")2299 self.gitStream.write(details["desc"])2300 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2301(','.join(self.branchPrefixes), details["change"]))2302iflen(details['options']) >0:2303 self.gitStream.write(": options =%s"% details['options'])2304 self.gitStream.write("]\nEOT\n\n")23052306iflen(parent) >0:2307if self.verbose:2308print"parent%s"% parent2309 self.gitStream.write("from%s\n"% parent)23102311 self.streamP4Files(new_files)2312 self.gitStream.write("\n")23132314 change =int(details["change"])23152316if self.labels.has_key(change):2317 label = self.labels[change]2318 labelDetails = label[0]2319 labelRevisions = label[1]2320if self.verbose:2321print"Change%sis labelled%s"% (change, labelDetails)23222323 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2324for p in self.branchPrefixes])23252326iflen(files) ==len(labelRevisions):23272328 cleanedFiles = {}2329for info in files:2330if info["action"]in self.delete_actions:2331continue2332 cleanedFiles[info["depotFile"]] = info["rev"]23332334if cleanedFiles == labelRevisions:2335 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23362337else:2338if not self.silent:2339print("Tag%sdoes not match with change%s: files do not match."2340% (labelDetails["label"], change))23412342else:2343if not self.silent:2344print("Tag%sdoes not match with change%s: file count is different."2345% (labelDetails["label"], change))23462347# Build a dictionary of changelists and labels, for "detect-labels" option.2348defgetLabels(self):2349 self.labels = {}23502351 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2352iflen(l) >0and not self.silent:2353print"Finding files belonging to labels in%s"% `self.depotPaths`23542355for output in l:2356 label = output["label"]2357 revisions = {}2358 newestChange =02359if self.verbose:2360print"Querying files for label%s"% label2361forfileinp4CmdList(["files"] +2362["%s...@%s"% (p, label)2363for p in self.depotPaths]):2364 revisions[file["depotFile"]] =file["rev"]2365 change =int(file["change"])2366if change > newestChange:2367 newestChange = change23682369 self.labels[newestChange] = [output, revisions]23702371if self.verbose:2372print"Label changes:%s"% self.labels.keys()23732374# Import p4 labels as git tags. A direct mapping does not2375# exist, so assume that if all the files are at the same revision2376# then we can use that, or it's something more complicated we should2377# just ignore.2378defimportP4Labels(self, stream, p4Labels):2379if verbose:2380print"import p4 labels: "+' '.join(p4Labels)23812382 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2383 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2384iflen(validLabelRegexp) ==0:2385 validLabelRegexp = defaultLabelRegexp2386 m = re.compile(validLabelRegexp)23872388for name in p4Labels:2389 commitFound =False23902391if not m.match(name):2392if verbose:2393print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2394continue23952396if name in ignoredP4Labels:2397continue23982399 labelDetails =p4CmdList(['label',"-o", name])[0]24002401# get the most recent changelist for each file in this label2402 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2403for p in self.depotPaths])24042405if change.has_key('change'):2406# find the corresponding git commit; take the oldest commit2407 changelist =int(change['change'])2408 gitCommit =read_pipe(["git","rev-list","--max-count=1",2409"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2410iflen(gitCommit) ==0:2411print"could not find git commit for changelist%d"% changelist2412else:2413 gitCommit = gitCommit.strip()2414 commitFound =True2415# Convert from p4 time format2416try:2417 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2418exceptValueError:2419print"Could not convert label time%s"% labelDetails['Update']2420 tmwhen =124212422 when =int(time.mktime(tmwhen))2423 self.streamTag(stream, name, labelDetails, gitCommit, when)2424if verbose:2425print"p4 label%smapped to git commit%s"% (name, gitCommit)2426else:2427if verbose:2428print"Label%shas no changelists - possibly deleted?"% name24292430if not commitFound:2431# We can't import this label; don't try again as it will get very2432# expensive repeatedly fetching all the files for labels that will2433# never be imported. If the label is moved in the future, the2434# ignore will need to be removed manually.2435system(["git","config","--add","git-p4.ignoredP4Labels", name])24362437defguessProjectName(self):2438for p in self.depotPaths:2439if p.endswith("/"):2440 p = p[:-1]2441 p = p[p.strip().rfind("/") +1:]2442if not p.endswith("/"):2443 p +="/"2444return p24452446defgetBranchMapping(self):2447 lostAndFoundBranches =set()24482449 user =gitConfig("git-p4.branchUser")2450iflen(user) >0:2451 command ="branches -u%s"% user2452else:2453 command ="branches"24542455for info inp4CmdList(command):2456 details =p4Cmd(["branch","-o", info["branch"]])2457 viewIdx =02458while details.has_key("View%s"% viewIdx):2459 paths = details["View%s"% viewIdx].split(" ")2460 viewIdx = viewIdx +12461# require standard //depot/foo/... //depot/bar/... mapping2462iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2463continue2464 source = paths[0]2465 destination = paths[1]2466## HACK2467ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2468 source = source[len(self.depotPaths[0]):-4]2469 destination = destination[len(self.depotPaths[0]):-4]24702471if destination in self.knownBranches:2472if not self.silent:2473print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2474print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2475continue24762477 self.knownBranches[destination] = source24782479 lostAndFoundBranches.discard(destination)24802481if source not in self.knownBranches:2482 lostAndFoundBranches.add(source)24832484# Perforce does not strictly require branches to be defined, so we also2485# check git config for a branch list.2486#2487# Example of branch definition in git config file:2488# [git-p4]2489# branchList=main:branchA2490# branchList=main:branchB2491# branchList=branchA:branchC2492 configBranches =gitConfigList("git-p4.branchList")2493for branch in configBranches:2494if branch:2495(source, destination) = branch.split(":")2496 self.knownBranches[destination] = source24972498 lostAndFoundBranches.discard(destination)24992500if source not in self.knownBranches:2501 lostAndFoundBranches.add(source)250225032504for branch in lostAndFoundBranches:2505 self.knownBranches[branch] = branch25062507defgetBranchMappingFromGitBranches(self):2508 branches =p4BranchesInGit(self.importIntoRemotes)2509for branch in branches.keys():2510if branch =="master":2511 branch ="main"2512else:2513 branch = branch[len(self.projectName):]2514 self.knownBranches[branch] = branch25152516defupdateOptionDict(self, d):2517 option_keys = {}2518if self.keepRepoPath:2519 option_keys['keepRepoPath'] =125202521 d["options"] =' '.join(sorted(option_keys.keys()))25222523defreadOptions(self, d):2524 self.keepRepoPath = (d.has_key('options')2525and('keepRepoPath'in d['options']))25262527defgitRefForBranch(self, branch):2528if branch =="main":2529return self.refPrefix +"master"25302531iflen(branch) <=0:2532return branch25332534return self.refPrefix + self.projectName + branch25352536defgitCommitByP4Change(self, ref, change):2537if self.verbose:2538print"looking in ref "+ ref +" for change%susing bisect..."% change25392540 earliestCommit =""2541 latestCommit =parseRevision(ref)25422543while True:2544if self.verbose:2545print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2546 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2547iflen(next) ==0:2548if self.verbose:2549print"argh"2550return""2551 log =extractLogMessageFromGitCommit(next)2552 settings =extractSettingsGitLog(log)2553 currentChange =int(settings['change'])2554if self.verbose:2555print"current change%s"% currentChange25562557if currentChange == change:2558if self.verbose:2559print"found%s"% next2560return next25612562if currentChange < change:2563 earliestCommit ="^%s"% next2564else:2565 latestCommit ="%s"% next25662567return""25682569defimportNewBranch(self, branch, maxChange):2570# make fast-import flush all changes to disk and update the refs using the checkpoint2571# command so that we can try to find the branch parent in the git history2572 self.gitStream.write("checkpoint\n\n");2573 self.gitStream.flush();2574 branchPrefix = self.depotPaths[0] + branch +"/"2575range="@1,%s"% maxChange2576#print "prefix" + branchPrefix2577 changes =p4ChangesForPaths([branchPrefix],range)2578iflen(changes) <=0:2579return False2580 firstChange = changes[0]2581#print "first change in branch: %s" % firstChange2582 sourceBranch = self.knownBranches[branch]2583 sourceDepotPath = self.depotPaths[0] + sourceBranch2584 sourceRef = self.gitRefForBranch(sourceBranch)2585#print "source " + sourceBranch25862587 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2588#print "branch parent: %s" % branchParentChange2589 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2590iflen(gitParent) >0:2591 self.initialParents[self.gitRefForBranch(branch)] = gitParent2592#print "parent git commit: %s" % gitParent25932594 self.importChanges(changes)2595return True25962597defsearchParent(self, parent, branch, target):2598 parentFound =False2599for blob inread_pipe_lines(["git","rev-list","--reverse",2600"--no-merges", parent]):2601 blob = blob.strip()2602iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2603 parentFound =True2604if self.verbose:2605print"Found parent of%sin commit%s"% (branch, blob)2606break2607if parentFound:2608return blob2609else:2610return None26112612defimportChanges(self, changes):2613 cnt =12614for change in changes:2615 description =p4_describe(change)2616 self.updateOptionDict(description)26172618if not self.silent:2619 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2620 sys.stdout.flush()2621 cnt = cnt +126222623try:2624if self.detectBranches:2625 branches = self.splitFilesIntoBranches(description)2626for branch in branches.keys():2627## HACK --hwn2628 branchPrefix = self.depotPaths[0] + branch +"/"2629 self.branchPrefixes = [ branchPrefix ]26302631 parent =""26322633 filesForCommit = branches[branch]26342635if self.verbose:2636print"branch is%s"% branch26372638 self.updatedBranches.add(branch)26392640if branch not in self.createdBranches:2641 self.createdBranches.add(branch)2642 parent = self.knownBranches[branch]2643if parent == branch:2644 parent =""2645else:2646 fullBranch = self.projectName + branch2647if fullBranch not in self.p4BranchesInGit:2648if not self.silent:2649print("\nImporting new branch%s"% fullBranch);2650if self.importNewBranch(branch, change -1):2651 parent =""2652 self.p4BranchesInGit.append(fullBranch)2653if not self.silent:2654print("\nResuming with change%s"% change);26552656if self.verbose:2657print"parent determined through known branches:%s"% parent26582659 branch = self.gitRefForBranch(branch)2660 parent = self.gitRefForBranch(parent)26612662if self.verbose:2663print"looking for initial parent for%s; current parent is%s"% (branch, parent)26642665iflen(parent) ==0and branch in self.initialParents:2666 parent = self.initialParents[branch]2667del self.initialParents[branch]26682669 blob =None2670iflen(parent) >0:2671 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2672if self.verbose:2673print"Creating temporary branch: "+ tempBranch2674 self.commit(description, filesForCommit, tempBranch)2675 self.tempBranches.append(tempBranch)2676 self.checkpoint()2677 blob = self.searchParent(parent, branch, tempBranch)2678if blob:2679 self.commit(description, filesForCommit, branch, blob)2680else:2681if self.verbose:2682print"Parent of%snot found. Committing into head of%s"% (branch, parent)2683 self.commit(description, filesForCommit, branch, parent)2684else:2685 files = self.extractFilesFromCommit(description)2686 self.commit(description, files, self.branch,2687 self.initialParent)2688# only needed once, to connect to the previous commit2689 self.initialParent =""2690exceptIOError:2691print self.gitError.read()2692 sys.exit(1)26932694defimportHeadRevision(self, revision):2695print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)26962697 details = {}2698 details["user"] ="git perforce import user"2699 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2700% (' '.join(self.depotPaths), revision))2701 details["change"] = revision2702 newestRevision =027032704 fileCnt =02705 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27062707for info inp4CmdList(["files"] + fileArgs):27082709if'code'in info and info['code'] =='error':2710 sys.stderr.write("p4 returned an error:%s\n"2711% info['data'])2712if info['data'].find("must refer to client") >=0:2713 sys.stderr.write("This particular p4 error is misleading.\n")2714 sys.stderr.write("Perhaps the depot path was misspelled.\n");2715 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2716 sys.exit(1)2717if'p4ExitCode'in info:2718 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2719 sys.exit(1)272027212722 change =int(info["change"])2723if change > newestRevision:2724 newestRevision = change27252726if info["action"]in self.delete_actions:2727# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2728#fileCnt = fileCnt + 12729continue27302731for prop in["depotFile","rev","action","type"]:2732 details["%s%s"% (prop, fileCnt)] = info[prop]27332734 fileCnt = fileCnt +127352736 details["change"] = newestRevision27372738# Use time from top-most change so that all git p4 clones of2739# the same p4 repo have the same commit SHA1s.2740 res =p4_describe(newestRevision)2741 details["time"] = res["time"]27422743 self.updateOptionDict(details)2744try:2745 self.commit(details, self.extractFilesFromCommit(details), self.branch)2746exceptIOError:2747print"IO error with git fast-import. Is your git version recent enough?"2748print self.gitError.read()274927502751defrun(self, args):2752 self.depotPaths = []2753 self.changeRange =""2754 self.previousDepotPaths = []2755 self.hasOrigin =False27562757# map from branch depot path to parent branch2758 self.knownBranches = {}2759 self.initialParents = {}27602761if self.importIntoRemotes:2762 self.refPrefix ="refs/remotes/p4/"2763else:2764 self.refPrefix ="refs/heads/p4/"27652766if self.syncWithOrigin:2767 self.hasOrigin =originP4BranchesExist()2768if self.hasOrigin:2769if not self.silent:2770print'Syncing with origin first, using "git fetch origin"'2771system("git fetch origin")27722773 branch_arg_given =bool(self.branch)2774iflen(self.branch) ==0:2775 self.branch = self.refPrefix +"master"2776ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2777system("git update-ref%srefs/heads/p4"% self.branch)2778system("git branch -D p4")27792780# accept either the command-line option, or the configuration variable2781if self.useClientSpec:2782# will use this after clone to set the variable2783 self.useClientSpec_from_options =True2784else:2785ifgitConfigBool("git-p4.useclientspec"):2786 self.useClientSpec =True2787if self.useClientSpec:2788 self.clientSpecDirs =getClientSpec()27892790# TODO: should always look at previous commits,2791# merge with previous imports, if possible.2792if args == []:2793if self.hasOrigin:2794createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)27952796# branches holds mapping from branch name to sha12797 branches =p4BranchesInGit(self.importIntoRemotes)27982799# restrict to just this one, disabling detect-branches2800if branch_arg_given:2801 short = self.branch.split("/")[-1]2802if short in branches:2803 self.p4BranchesInGit = [ short ]2804else:2805 self.p4BranchesInGit = branches.keys()28062807iflen(self.p4BranchesInGit) >1:2808if not self.silent:2809print"Importing from/into multiple branches"2810 self.detectBranches =True2811for branch in branches.keys():2812 self.initialParents[self.refPrefix + branch] = \2813 branches[branch]28142815if self.verbose:2816print"branches:%s"% self.p4BranchesInGit28172818 p4Change =02819for branch in self.p4BranchesInGit:2820 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28212822 settings =extractSettingsGitLog(logMsg)28232824 self.readOptions(settings)2825if(settings.has_key('depot-paths')2826and settings.has_key('change')):2827 change =int(settings['change']) +12828 p4Change =max(p4Change, change)28292830 depotPaths =sorted(settings['depot-paths'])2831if self.previousDepotPaths == []:2832 self.previousDepotPaths = depotPaths2833else:2834 paths = []2835for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2836 prev_list = prev.split("/")2837 cur_list = cur.split("/")2838for i inrange(0,min(len(cur_list),len(prev_list))):2839if cur_list[i] <> prev_list[i]:2840 i = i -12841break28422843 paths.append("/".join(cur_list[:i +1]))28442845 self.previousDepotPaths = paths28462847if p4Change >0:2848 self.depotPaths =sorted(self.previousDepotPaths)2849 self.changeRange ="@%s,#head"% p4Change2850if not self.silent and not self.detectBranches:2851print"Performing incremental import into%sgit branch"% self.branch28522853# accept multiple ref name abbreviations:2854# refs/foo/bar/branch -> use it exactly2855# p4/branch -> prepend refs/remotes/ or refs/heads/2856# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2857if not self.branch.startswith("refs/"):2858if self.importIntoRemotes:2859 prepend ="refs/remotes/"2860else:2861 prepend ="refs/heads/"2862if not self.branch.startswith("p4/"):2863 prepend +="p4/"2864 self.branch = prepend + self.branch28652866iflen(args) ==0and self.depotPaths:2867if not self.silent:2868print"Depot paths:%s"%' '.join(self.depotPaths)2869else:2870if self.depotPaths and self.depotPaths != args:2871print("previous import used depot path%sand now%swas specified. "2872"This doesn't work!"% (' '.join(self.depotPaths),2873' '.join(args)))2874 sys.exit(1)28752876 self.depotPaths =sorted(args)28772878 revision =""2879 self.users = {}28802881# Make sure no revision specifiers are used when --changesfile2882# is specified.2883 bad_changesfile =False2884iflen(self.changesFile) >0:2885for p in self.depotPaths:2886if p.find("@") >=0or p.find("#") >=0:2887 bad_changesfile =True2888break2889if bad_changesfile:2890die("Option --changesfile is incompatible with revision specifiers")28912892 newPaths = []2893for p in self.depotPaths:2894if p.find("@") != -1:2895 atIdx = p.index("@")2896 self.changeRange = p[atIdx:]2897if self.changeRange =="@all":2898 self.changeRange =""2899elif','not in self.changeRange:2900 revision = self.changeRange2901 self.changeRange =""2902 p = p[:atIdx]2903elif p.find("#") != -1:2904 hashIdx = p.index("#")2905 revision = p[hashIdx:]2906 p = p[:hashIdx]2907elif self.previousDepotPaths == []:2908# pay attention to changesfile, if given, else import2909# the entire p4 tree at the head revision2910iflen(self.changesFile) ==0:2911 revision ="#head"29122913 p = re.sub("\.\.\.$","", p)2914if not p.endswith("/"):2915 p +="/"29162917 newPaths.append(p)29182919 self.depotPaths = newPaths29202921# --detect-branches may change this for each branch2922 self.branchPrefixes = self.depotPaths29232924 self.loadUserMapFromCache()2925 self.labels = {}2926if self.detectLabels:2927 self.getLabels();29282929if self.detectBranches:2930## FIXME - what's a P4 projectName ?2931 self.projectName = self.guessProjectName()29322933if self.hasOrigin:2934 self.getBranchMappingFromGitBranches()2935else:2936 self.getBranchMapping()2937if self.verbose:2938print"p4-git branches:%s"% self.p4BranchesInGit2939print"initial parents:%s"% self.initialParents2940for b in self.p4BranchesInGit:2941if b !="master":29422943## FIXME2944 b = b[len(self.projectName):]2945 self.createdBranches.add(b)29462947 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29482949 self.importProcess = subprocess.Popen(["git","fast-import"],2950 stdin=subprocess.PIPE,2951 stdout=subprocess.PIPE,2952 stderr=subprocess.PIPE);2953 self.gitOutput = self.importProcess.stdout2954 self.gitStream = self.importProcess.stdin2955 self.gitError = self.importProcess.stderr29562957if revision:2958 self.importHeadRevision(revision)2959else:2960 changes = []29612962iflen(self.changesFile) >0:2963 output =open(self.changesFile).readlines()2964 changeSet =set()2965for line in output:2966 changeSet.add(int(line))29672968for change in changeSet:2969 changes.append(change)29702971 changes.sort()2972else:2973# catch "git p4 sync" with no new branches, in a repo that2974# does not have any existing p4 branches2975iflen(args) ==0:2976if not self.p4BranchesInGit:2977die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")29782979# The default branch is master, unless --branch is used to2980# specify something else. Make sure it exists, or complain2981# nicely about how to use --branch.2982if not self.detectBranches:2983if notbranch_exists(self.branch):2984if branch_arg_given:2985die("Error: branch%sdoes not exist."% self.branch)2986else:2987die("Error: no branch%s; perhaps specify one with --branch."%2988 self.branch)29892990if self.verbose:2991print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2992 self.changeRange)2993 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)29942995iflen(self.maxChanges) >0:2996 changes = changes[:min(int(self.maxChanges),len(changes))]29972998iflen(changes) ==0:2999if not self.silent:3000print"No changes to import!"3001else:3002if not self.silent and not self.detectBranches:3003print"Import destination:%s"% self.branch30043005 self.updatedBranches =set()30063007if not self.detectBranches:3008if args:3009# start a new branch3010 self.initialParent =""3011else:3012# build on a previous revision3013 self.initialParent =parseRevision(self.branch)30143015 self.importChanges(changes)30163017if not self.silent:3018print""3019iflen(self.updatedBranches) >0:3020 sys.stdout.write("Updated branches: ")3021for b in self.updatedBranches:3022 sys.stdout.write("%s"% b)3023 sys.stdout.write("\n")30243025ifgitConfigBool("git-p4.importLabels"):3026 self.importLabels =True30273028if self.importLabels:3029 p4Labels =getP4Labels(self.depotPaths)3030 gitTags =getGitTags()30313032 missingP4Labels = p4Labels - gitTags3033 self.importP4Labels(self.gitStream, missingP4Labels)30343035 self.gitStream.close()3036if self.importProcess.wait() !=0:3037die("fast-import failed:%s"% self.gitError.read())3038 self.gitOutput.close()3039 self.gitError.close()30403041# Cleanup temporary branches created during import3042if self.tempBranches != []:3043for branch in self.tempBranches:3044read_pipe("git update-ref -d%s"% branch)3045 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30463047# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3048# a convenient shortcut refname "p4".3049if self.importIntoRemotes:3050 head_ref = self.refPrefix +"HEAD"3051if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3052system(["git","symbolic-ref", head_ref, self.branch])30533054return True30553056classP4Rebase(Command):3057def__init__(self):3058 Command.__init__(self)3059 self.options = [3060 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3061]3062 self.importLabels =False3063 self.description = ("Fetches the latest revision from perforce and "3064+"rebases the current work (branch) against it")30653066defrun(self, args):3067 sync =P4Sync()3068 sync.importLabels = self.importLabels3069 sync.run([])30703071return self.rebase()30723073defrebase(self):3074if os.system("git update-index --refresh") !=0:3075die("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.");3076iflen(read_pipe("git diff-index HEAD --")) >0:3077die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");30783079[upstream, settings] =findUpstreamBranchPoint()3080iflen(upstream) ==0:3081die("Cannot find upstream branchpoint for rebase")30823083# the branchpoint may be p4/foo~3, so strip off the parent3084 upstream = re.sub("~[0-9]+$","", upstream)30853086print"Rebasing the current branch onto%s"% upstream3087 oldHead =read_pipe("git rev-parse HEAD").strip()3088system("git rebase%s"% upstream)3089system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3090return True30913092classP4Clone(P4Sync):3093def__init__(self):3094 P4Sync.__init__(self)3095 self.description ="Creates a new git repository and imports from Perforce into it"3096 self.usage ="usage: %prog [options] //depot/path[@revRange]"3097 self.options += [3098 optparse.make_option("--destination", dest="cloneDestination",3099 action='store', default=None,3100help="where to leave result of the clone"),3101 optparse.make_option("-/", dest="cloneExclude",3102 action="append",type="string",3103help="exclude depot path"),3104 optparse.make_option("--bare", dest="cloneBare",3105 action="store_true", default=False),3106]3107 self.cloneDestination =None3108 self.needsGit =False3109 self.cloneBare =False31103111# This is required for the "append" cloneExclude action3112defensure_value(self, attr, value):3113if nothasattr(self, attr)orgetattr(self, attr)is None:3114setattr(self, attr, value)3115returngetattr(self, attr)31163117defdefaultDestination(self, args):3118## TODO: use common prefix of args?3119 depotPath = args[0]3120 depotDir = re.sub("(@[^@]*)$","", depotPath)3121 depotDir = re.sub("(#[^#]*)$","", depotDir)3122 depotDir = re.sub(r"\.\.\.$","", depotDir)3123 depotDir = re.sub(r"/$","", depotDir)3124return os.path.split(depotDir)[1]31253126defrun(self, args):3127iflen(args) <1:3128return False31293130if self.keepRepoPath and not self.cloneDestination:3131 sys.stderr.write("Must specify destination for --keep-path\n")3132 sys.exit(1)31333134 depotPaths = args31353136if not self.cloneDestination andlen(depotPaths) >1:3137 self.cloneDestination = depotPaths[-1]3138 depotPaths = depotPaths[:-1]31393140 self.cloneExclude = ["/"+p for p in self.cloneExclude]3141for p in depotPaths:3142if not p.startswith("//"):3143 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3144return False31453146if not self.cloneDestination:3147 self.cloneDestination = self.defaultDestination(args)31483149print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31503151if not os.path.exists(self.cloneDestination):3152 os.makedirs(self.cloneDestination)3153chdir(self.cloneDestination)31543155 init_cmd = ["git","init"]3156if self.cloneBare:3157 init_cmd.append("--bare")3158 retcode = subprocess.call(init_cmd)3159if retcode:3160raiseCalledProcessError(retcode, init_cmd)31613162if not P4Sync.run(self, depotPaths):3163return False31643165# create a master branch and check out a work tree3166ifgitBranchExists(self.branch):3167system(["git","branch","master", self.branch ])3168if not self.cloneBare:3169system(["git","checkout","-f"])3170else:3171print'Not checking out any branch, use ' \3172'"git checkout -q -b master <branch>"'31733174# auto-set this variable if invoked with --use-client-spec3175if self.useClientSpec_from_options:3176system("git config --bool git-p4.useclientspec true")31773178return True31793180classP4Branches(Command):3181def__init__(self):3182 Command.__init__(self)3183 self.options = [ ]3184 self.description = ("Shows the git branches that hold imports and their "3185+"corresponding perforce depot paths")3186 self.verbose =False31873188defrun(self, args):3189iforiginP4BranchesExist():3190createOrUpdateBranchesFromOrigin()31913192 cmdline ="git rev-parse --symbolic "3193 cmdline +=" --remotes"31943195for line inread_pipe_lines(cmdline):3196 line = line.strip()31973198if not line.startswith('p4/')or line =="p4/HEAD":3199continue3200 branch = line32013202 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3203 settings =extractSettingsGitLog(log)32043205print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3206return True32073208classHelpFormatter(optparse.IndentedHelpFormatter):3209def__init__(self):3210 optparse.IndentedHelpFormatter.__init__(self)32113212defformat_description(self, description):3213if description:3214return description +"\n"3215else:3216return""32173218defprintUsage(commands):3219print"usage:%s<command> [options]"% sys.argv[0]3220print""3221print"valid commands:%s"%", ".join(commands)3222print""3223print"Try%s<command> --help for command specific help."% sys.argv[0]3224print""32253226commands = {3227"debug": P4Debug,3228"submit": P4Submit,3229"commit": P4Submit,3230"sync": P4Sync,3231"rebase": P4Rebase,3232"clone": P4Clone,3233"rollback": P4RollBack,3234"branches": P4Branches3235}323632373238defmain():3239iflen(sys.argv[1:]) ==0:3240printUsage(commands.keys())3241 sys.exit(2)32423243 cmdName = sys.argv[1]3244try:3245 klass = commands[cmdName]3246 cmd =klass()3247exceptKeyError:3248print"unknown command%s"% cmdName3249print""3250printUsage(commands.keys())3251 sys.exit(2)32523253 options = cmd.options3254 cmd.gitdir = os.environ.get("GIT_DIR",None)32553256 args = sys.argv[2:]32573258 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3259if cmd.needsGit:3260 options.append(optparse.make_option("--git-dir", dest="gitdir"))32613262 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3263 options,3264 description = cmd.description,3265 formatter =HelpFormatter())32663267(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3268global verbose3269 verbose = cmd.verbose3270if cmd.needsGit:3271if cmd.gitdir ==None:3272 cmd.gitdir = os.path.abspath(".git")3273if notisValidGitDir(cmd.gitdir):3274 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3275if os.path.exists(cmd.gitdir):3276 cdup =read_pipe("git rev-parse --show-cdup").strip()3277iflen(cdup) >0:3278chdir(cdup);32793280if notisValidGitDir(cmd.gitdir):3281ifisValidGitDir(cmd.gitdir +"/.git"):3282 cmd.gitdir +="/.git"3283else:3284die("fatal: cannot locate git repository at%s"% cmd.gitdir)32853286 os.environ["GIT_DIR"] = cmd.gitdir32873288if not cmd.run(args):3289 parser.print_help()3290 sys.exit(2)329132923293if __name__ =='__main__':3294main()