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# 10 11import optparse, sys, os, marshal, subprocess, shelve 12import tempfile, getopt, os.path, time, platform 13import re, shutil 14 15verbose =False 16 17 18defp4_build_cmd(cmd): 19"""Build a suitable p4 command line. 20 21 This consolidates building and returning a p4 command line into one 22 location. It means that hooking into the environment, or other configuration 23 can be done more easily. 24 """ 25 real_cmd = ["p4"] 26 27 user =gitConfig("git-p4.user") 28iflen(user) >0: 29 real_cmd += ["-u",user] 30 31 password =gitConfig("git-p4.password") 32iflen(password) >0: 33 real_cmd += ["-P", password] 34 35 port =gitConfig("git-p4.port") 36iflen(port) >0: 37 real_cmd += ["-p", port] 38 39 host =gitConfig("git-p4.host") 40iflen(host) >0: 41 real_cmd += ["-h", host] 42 43 client =gitConfig("git-p4.client") 44iflen(client) >0: 45 real_cmd += ["-c", client] 46 47 48ifisinstance(cmd,basestring): 49 real_cmd =' '.join(real_cmd) +' '+ cmd 50else: 51 real_cmd += cmd 52return real_cmd 53 54defchdir(dir): 55# P4 uses the PWD environment variable rather than getcwd(). Since we're 56# not using the shell, we have to set it ourselves. This path could 57# be relative, so go there first, then figure out where we ended up. 58 os.chdir(dir) 59 os.environ['PWD'] = os.getcwd() 60 61defdie(msg): 62if verbose: 63raiseException(msg) 64else: 65 sys.stderr.write(msg +"\n") 66 sys.exit(1) 67 68defwrite_pipe(c, stdin): 69if verbose: 70 sys.stderr.write('Writing pipe:%s\n'%str(c)) 71 72 expand =isinstance(c,basestring) 73 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 74 pipe = p.stdin 75 val = pipe.write(stdin) 76 pipe.close() 77if p.wait(): 78die('Command failed:%s'%str(c)) 79 80return val 81 82defp4_write_pipe(c, stdin): 83 real_cmd =p4_build_cmd(c) 84returnwrite_pipe(real_cmd, stdin) 85 86defread_pipe(c, ignore_error=False): 87if verbose: 88 sys.stderr.write('Reading pipe:%s\n'%str(c)) 89 90 expand =isinstance(c,basestring) 91 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 92 pipe = p.stdout 93 val = pipe.read() 94if p.wait()and not ignore_error: 95die('Command failed:%s'%str(c)) 96 97return val 98 99defp4_read_pipe(c, ignore_error=False): 100 real_cmd =p4_build_cmd(c) 101returnread_pipe(real_cmd, ignore_error) 102 103defread_pipe_lines(c): 104if verbose: 105 sys.stderr.write('Reading pipe:%s\n'%str(c)) 106 107 expand =isinstance(c, basestring) 108 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 109 pipe = p.stdout 110 val = pipe.readlines() 111if pipe.close()or p.wait(): 112die('Command failed:%s'%str(c)) 113 114return val 115 116defp4_read_pipe_lines(c): 117"""Specifically invoke p4 on the command supplied. """ 118 real_cmd =p4_build_cmd(c) 119returnread_pipe_lines(real_cmd) 120 121defsystem(cmd): 122 expand =isinstance(cmd,basestring) 123if verbose: 124 sys.stderr.write("executing%s\n"%str(cmd)) 125 subprocess.check_call(cmd, shell=expand) 126 127defp4_system(cmd): 128"""Specifically invoke p4 as the system command. """ 129 real_cmd =p4_build_cmd(cmd) 130 expand =isinstance(real_cmd, basestring) 131 subprocess.check_call(real_cmd, shell=expand) 132 133defp4_integrate(src, dest): 134p4_system(["integrate","-Dt", src, dest]) 135 136defp4_sync(path): 137p4_system(["sync", path]) 138 139defp4_add(f): 140p4_system(["add", f]) 141 142defp4_delete(f): 143p4_system(["delete", f]) 144 145defp4_edit(f): 146p4_system(["edit", f]) 147 148defp4_revert(f): 149p4_system(["revert", f]) 150 151defp4_reopen(type,file): 152p4_system(["reopen","-t",type,file]) 153 154# 155# Canonicalize the p4 type and return a tuple of the 156# base type, plus any modifiers. See "p4 help filetypes" 157# for a list and explanation. 158# 159defsplit_p4_type(p4type): 160 161 p4_filetypes_historical = { 162"ctempobj":"binary+Sw", 163"ctext":"text+C", 164"cxtext":"text+Cx", 165"ktext":"text+k", 166"kxtext":"text+kx", 167"ltext":"text+F", 168"tempobj":"binary+FSw", 169"ubinary":"binary+F", 170"uresource":"resource+F", 171"uxbinary":"binary+Fx", 172"xbinary":"binary+x", 173"xltext":"text+Fx", 174"xtempobj":"binary+Swx", 175"xtext":"text+x", 176"xunicode":"unicode+x", 177"xutf16":"utf16+x", 178} 179if p4type in p4_filetypes_historical: 180 p4type = p4_filetypes_historical[p4type] 181 mods ="" 182 s = p4type.split("+") 183 base = s[0] 184 mods ="" 185iflen(s) >1: 186 mods = s[1] 187return(base, mods) 188 189# 190# return the raw p4 type of a file (text, text+ko, etc) 191# 192defp4_type(file): 193 results =p4CmdList(["fstat","-T","headType",file]) 194return results[0]['headType'] 195 196# 197# Given a type base and modifier, return a regexp matching 198# the keywords that can be expanded in the file 199# 200defp4_keywords_regexp_for_type(base, type_mods): 201if base in("text","unicode","binary"): 202 kwords =None 203if"ko"in type_mods: 204 kwords ='Id|Header' 205elif"k"in type_mods: 206 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 207else: 208return None 209 pattern = r""" 210 \$ # Starts with a dollar, followed by... 211 (%s) # one of the keywords, followed by... 212 (:[^$]+)? # possibly an old expansion, followed by... 213 \$ # another dollar 214 """% kwords 215return pattern 216else: 217return None 218 219# 220# Given a file, return a regexp matching the possible 221# RCS keywords that will be expanded, or None for files 222# with kw expansion turned off. 223# 224defp4_keywords_regexp_for_file(file): 225if not os.path.exists(file): 226return None 227else: 228(type_base, type_mods) =split_p4_type(p4_type(file)) 229returnp4_keywords_regexp_for_type(type_base, type_mods) 230 231defsetP4ExecBit(file, mode): 232# Reopens an already open file and changes the execute bit to match 233# the execute bit setting in the passed in mode. 234 235 p4Type ="+x" 236 237if notisModeExec(mode): 238 p4Type =getP4OpenedType(file) 239 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 240 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 241if p4Type[-1] =="+": 242 p4Type = p4Type[0:-1] 243 244p4_reopen(p4Type,file) 245 246defgetP4OpenedType(file): 247# Returns the perforce file type for the given file. 248 249 result =p4_read_pipe(["opened",file]) 250 match = re.match(".*\((.+)\)\r?$", result) 251if match: 252return match.group(1) 253else: 254die("Could not determine file type for%s(result: '%s')"% (file, result)) 255 256defdiffTreePattern(): 257# This is a simple generator for the diff tree regex pattern. This could be 258# a class variable if this and parseDiffTreeEntry were a part of a class. 259 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 260while True: 261yield pattern 262 263defparseDiffTreeEntry(entry): 264"""Parses a single diff tree entry into its component elements. 265 266 See git-diff-tree(1) manpage for details about the format of the diff 267 output. This method returns a dictionary with the following elements: 268 269 src_mode - The mode of the source file 270 dst_mode - The mode of the destination file 271 src_sha1 - The sha1 for the source file 272 dst_sha1 - The sha1 fr the destination file 273 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 274 status_score - The score for the status (applicable for 'C' and 'R' 275 statuses). This is None if there is no score. 276 src - The path for the source file. 277 dst - The path for the destination file. This is only present for 278 copy or renames. If it is not present, this is None. 279 280 If the pattern is not matched, None is returned.""" 281 282 match =diffTreePattern().next().match(entry) 283if match: 284return{ 285'src_mode': match.group(1), 286'dst_mode': match.group(2), 287'src_sha1': match.group(3), 288'dst_sha1': match.group(4), 289'status': match.group(5), 290'status_score': match.group(6), 291'src': match.group(7), 292'dst': match.group(10) 293} 294return None 295 296defisModeExec(mode): 297# Returns True if the given git mode represents an executable file, 298# otherwise False. 299return mode[-3:] =="755" 300 301defisModeExecChanged(src_mode, dst_mode): 302returnisModeExec(src_mode) !=isModeExec(dst_mode) 303 304defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 305 306ifisinstance(cmd,basestring): 307 cmd ="-G "+ cmd 308 expand =True 309else: 310 cmd = ["-G"] + cmd 311 expand =False 312 313 cmd =p4_build_cmd(cmd) 314if verbose: 315 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 316 317# Use a temporary file to avoid deadlocks without 318# subprocess.communicate(), which would put another copy 319# of stdout into memory. 320 stdin_file =None 321if stdin is not None: 322 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 323ifisinstance(stdin,basestring): 324 stdin_file.write(stdin) 325else: 326for i in stdin: 327 stdin_file.write(i +'\n') 328 stdin_file.flush() 329 stdin_file.seek(0) 330 331 p4 = subprocess.Popen(cmd, 332 shell=expand, 333 stdin=stdin_file, 334 stdout=subprocess.PIPE) 335 336 result = [] 337try: 338while True: 339 entry = marshal.load(p4.stdout) 340if cb is not None: 341cb(entry) 342else: 343 result.append(entry) 344exceptEOFError: 345pass 346 exitCode = p4.wait() 347if exitCode !=0: 348 entry = {} 349 entry["p4ExitCode"] = exitCode 350 result.append(entry) 351 352return result 353 354defp4Cmd(cmd): 355list=p4CmdList(cmd) 356 result = {} 357for entry inlist: 358 result.update(entry) 359return result; 360 361defp4Where(depotPath): 362if not depotPath.endswith("/"): 363 depotPath +="/" 364 depotPath = depotPath +"..." 365 outputList =p4CmdList(["where", depotPath]) 366 output =None 367for entry in outputList: 368if"depotFile"in entry: 369if entry["depotFile"] == depotPath: 370 output = entry 371break 372elif"data"in entry: 373 data = entry.get("data") 374 space = data.find(" ") 375if data[:space] == depotPath: 376 output = entry 377break 378if output ==None: 379return"" 380if output["code"] =="error": 381return"" 382 clientPath ="" 383if"path"in output: 384 clientPath = output.get("path") 385elif"data"in output: 386 data = output.get("data") 387 lastSpace = data.rfind(" ") 388 clientPath = data[lastSpace +1:] 389 390if clientPath.endswith("..."): 391 clientPath = clientPath[:-3] 392return clientPath 393 394defcurrentGitBranch(): 395returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 396 397defisValidGitDir(path): 398if(os.path.exists(path +"/HEAD") 399and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 400return True; 401return False 402 403defparseRevision(ref): 404returnread_pipe("git rev-parse%s"% ref).strip() 405 406defbranchExists(ref): 407 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 408 ignore_error=True) 409returnlen(rev) >0 410 411defextractLogMessageFromGitCommit(commit): 412 logMessage ="" 413 414## fixme: title is first line of commit, not 1st paragraph. 415 foundTitle =False 416for log inread_pipe_lines("git cat-file commit%s"% commit): 417if not foundTitle: 418iflen(log) ==1: 419 foundTitle =True 420continue 421 422 logMessage += log 423return logMessage 424 425defextractSettingsGitLog(log): 426 values = {} 427for line in log.split("\n"): 428 line = line.strip() 429 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 430if not m: 431continue 432 433 assignments = m.group(1).split(':') 434for a in assignments: 435 vals = a.split('=') 436 key = vals[0].strip() 437 val = ('='.join(vals[1:])).strip() 438if val.endswith('\"')and val.startswith('"'): 439 val = val[1:-1] 440 441 values[key] = val 442 443 paths = values.get("depot-paths") 444if not paths: 445 paths = values.get("depot-path") 446if paths: 447 values['depot-paths'] = paths.split(',') 448return values 449 450defgitBranchExists(branch): 451 proc = subprocess.Popen(["git","rev-parse", branch], 452 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 453return proc.wait() ==0; 454 455_gitConfig = {} 456defgitConfig(key, args =None):# set args to "--bool", for instance 457if not _gitConfig.has_key(key): 458 argsFilter ="" 459if args !=None: 460 argsFilter ="%s"% args 461 cmd ="git config%s%s"% (argsFilter, key) 462 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 463return _gitConfig[key] 464 465defgitConfigList(key): 466if not _gitConfig.has_key(key): 467 _gitConfig[key] =read_pipe("git config --get-all%s"% key, ignore_error=True).strip().split(os.linesep) 468return _gitConfig[key] 469 470defp4BranchesInGit(branchesAreInRemotes =True): 471 branches = {} 472 473 cmdline ="git rev-parse --symbolic " 474if branchesAreInRemotes: 475 cmdline +=" --remotes" 476else: 477 cmdline +=" --branches" 478 479for line inread_pipe_lines(cmdline): 480 line = line.strip() 481 482## only import to p4/ 483if not line.startswith('p4/')or line =="p4/HEAD": 484continue 485 branch = line 486 487# strip off p4 488 branch = re.sub("^p4/","", line) 489 490 branches[branch] =parseRevision(line) 491return branches 492 493deffindUpstreamBranchPoint(head ="HEAD"): 494 branches =p4BranchesInGit() 495# map from depot-path to branch name 496 branchByDepotPath = {} 497for branch in branches.keys(): 498 tip = branches[branch] 499 log =extractLogMessageFromGitCommit(tip) 500 settings =extractSettingsGitLog(log) 501if settings.has_key("depot-paths"): 502 paths =",".join(settings["depot-paths"]) 503 branchByDepotPath[paths] ="remotes/p4/"+ branch 504 505 settings =None 506 parent =0 507while parent <65535: 508 commit = head +"~%s"% parent 509 log =extractLogMessageFromGitCommit(commit) 510 settings =extractSettingsGitLog(log) 511if settings.has_key("depot-paths"): 512 paths =",".join(settings["depot-paths"]) 513if branchByDepotPath.has_key(paths): 514return[branchByDepotPath[paths], settings] 515 516 parent = parent +1 517 518return["", settings] 519 520defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 521if not silent: 522print("Creating/updating branch(es) in%sbased on origin branch(es)" 523% localRefPrefix) 524 525 originPrefix ="origin/p4/" 526 527for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 528 line = line.strip() 529if(not line.startswith(originPrefix))or line.endswith("HEAD"): 530continue 531 532 headName = line[len(originPrefix):] 533 remoteHead = localRefPrefix + headName 534 originHead = line 535 536 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 537if(not original.has_key('depot-paths') 538or not original.has_key('change')): 539continue 540 541 update =False 542if notgitBranchExists(remoteHead): 543if verbose: 544print"creating%s"% remoteHead 545 update =True 546else: 547 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 548if settings.has_key('change') >0: 549if settings['depot-paths'] == original['depot-paths']: 550 originP4Change =int(original['change']) 551 p4Change =int(settings['change']) 552if originP4Change > p4Change: 553print("%s(%s) is newer than%s(%s). " 554"Updating p4 branch from origin." 555% (originHead, originP4Change, 556 remoteHead, p4Change)) 557 update =True 558else: 559print("Ignoring:%swas imported from%swhile " 560"%swas imported from%s" 561% (originHead,','.join(original['depot-paths']), 562 remoteHead,','.join(settings['depot-paths']))) 563 564if update: 565system("git update-ref%s %s"% (remoteHead, originHead)) 566 567deforiginP4BranchesExist(): 568returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 569 570defp4ChangesForPaths(depotPaths, changeRange): 571assert depotPaths 572 cmd = ['changes'] 573for p in depotPaths: 574 cmd += ["%s...%s"% (p, changeRange)] 575 output =p4_read_pipe_lines(cmd) 576 577 changes = {} 578for line in output: 579 changeNum =int(line.split(" ")[1]) 580 changes[changeNum] =True 581 582 changelist = changes.keys() 583 changelist.sort() 584return changelist 585 586defp4PathStartsWith(path, prefix): 587# This method tries to remedy a potential mixed-case issue: 588# 589# If UserA adds //depot/DirA/file1 590# and UserB adds //depot/dira/file2 591# 592# we may or may not have a problem. If you have core.ignorecase=true, 593# we treat DirA and dira as the same directory 594 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 595if ignorecase: 596return path.lower().startswith(prefix.lower()) 597return path.startswith(prefix) 598 599class Command: 600def__init__(self): 601 self.usage ="usage: %prog [options]" 602 self.needsGit =True 603 604class P4UserMap: 605def__init__(self): 606 self.userMapFromPerforceServer =False 607 self.myP4UserId =None 608 609defp4UserId(self): 610if self.myP4UserId: 611return self.myP4UserId 612 613 results =p4CmdList("user -o") 614for r in results: 615if r.has_key('User'): 616 self.myP4UserId = r['User'] 617return r['User'] 618die("Could not find your p4 user id") 619 620defp4UserIsMe(self, p4User): 621# return True if the given p4 user is actually me 622 me = self.p4UserId() 623if not p4User or p4User != me: 624return False 625else: 626return True 627 628defgetUserCacheFilename(self): 629 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 630return home +"/.gitp4-usercache.txt" 631 632defgetUserMapFromPerforceServer(self): 633if self.userMapFromPerforceServer: 634return 635 self.users = {} 636 self.emails = {} 637 638for output inp4CmdList("users"): 639if not output.has_key("User"): 640continue 641 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 642 self.emails[output["Email"]] = output["User"] 643 644 645 s ='' 646for(key, val)in self.users.items(): 647 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 648 649open(self.getUserCacheFilename(),"wb").write(s) 650 self.userMapFromPerforceServer =True 651 652defloadUserMapFromCache(self): 653 self.users = {} 654 self.userMapFromPerforceServer =False 655try: 656 cache =open(self.getUserCacheFilename(),"rb") 657 lines = cache.readlines() 658 cache.close() 659for line in lines: 660 entry = line.strip().split("\t") 661 self.users[entry[0]] = entry[1] 662exceptIOError: 663 self.getUserMapFromPerforceServer() 664 665classP4Debug(Command): 666def__init__(self): 667 Command.__init__(self) 668 self.options = [ 669 optparse.make_option("--verbose", dest="verbose", action="store_true", 670 default=False), 671] 672 self.description ="A tool to debug the output of p4 -G." 673 self.needsGit =False 674 self.verbose =False 675 676defrun(self, args): 677 j =0 678for output inp4CmdList(args): 679print'Element:%d'% j 680 j +=1 681print output 682return True 683 684classP4RollBack(Command): 685def__init__(self): 686 Command.__init__(self) 687 self.options = [ 688 optparse.make_option("--verbose", dest="verbose", action="store_true"), 689 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 690] 691 self.description ="A tool to debug the multi-branch import. Don't use :)" 692 self.verbose =False 693 self.rollbackLocalBranches =False 694 695defrun(self, args): 696iflen(args) !=1: 697return False 698 maxChange =int(args[0]) 699 700if"p4ExitCode"inp4Cmd("changes -m 1"): 701die("Problems executing p4"); 702 703if self.rollbackLocalBranches: 704 refPrefix ="refs/heads/" 705 lines =read_pipe_lines("git rev-parse --symbolic --branches") 706else: 707 refPrefix ="refs/remotes/" 708 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 709 710for line in lines: 711if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 712 line = line.strip() 713 ref = refPrefix + line 714 log =extractLogMessageFromGitCommit(ref) 715 settings =extractSettingsGitLog(log) 716 717 depotPaths = settings['depot-paths'] 718 change = settings['change'] 719 720 changed =False 721 722iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 723for p in depotPaths]))) ==0: 724print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 725system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 726continue 727 728while change andint(change) > maxChange: 729 changed =True 730if self.verbose: 731print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 732system("git update-ref%s\"%s^\""% (ref, ref)) 733 log =extractLogMessageFromGitCommit(ref) 734 settings =extractSettingsGitLog(log) 735 736 737 depotPaths = settings['depot-paths'] 738 change = settings['change'] 739 740if changed: 741print"%srewound to%s"% (ref, change) 742 743return True 744 745classP4Submit(Command, P4UserMap): 746def__init__(self): 747 Command.__init__(self) 748 P4UserMap.__init__(self) 749 self.options = [ 750 optparse.make_option("--verbose", dest="verbose", action="store_true"), 751 optparse.make_option("--origin", dest="origin"), 752 optparse.make_option("-M", dest="detectRenames", action="store_true"), 753# preserve the user, requires relevant p4 permissions 754 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 755] 756 self.description ="Submit changes from git to the perforce depot." 757 self.usage +=" [name of git branch to submit into perforce depot]" 758 self.interactive =True 759 self.origin ="" 760 self.detectRenames =False 761 self.verbose =False 762 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 763 self.isWindows = (platform.system() =="Windows") 764 765defcheck(self): 766iflen(p4CmdList("opened ...")) >0: 767die("You have files opened with perforce! Close them before starting the sync.") 768 769# replaces everything between 'Description:' and the next P4 submit template field with the 770# commit message 771defprepareLogMessage(self, template, message): 772 result ="" 773 774 inDescriptionSection =False 775 776for line in template.split("\n"): 777if line.startswith("#"): 778 result += line +"\n" 779continue 780 781if inDescriptionSection: 782if line.startswith("Files:")or line.startswith("Jobs:"): 783 inDescriptionSection =False 784else: 785continue 786else: 787if line.startswith("Description:"): 788 inDescriptionSection =True 789 line +="\n" 790for messageLine in message.split("\n"): 791 line +="\t"+ messageLine +"\n" 792 793 result += line +"\n" 794 795return result 796 797defpatchRCSKeywords(self,file, pattern): 798# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern 799(handle, outFileName) = tempfile.mkstemp(dir='.') 800try: 801 outFile = os.fdopen(handle,"w+") 802 inFile =open(file,"r") 803 regexp = re.compile(pattern, re.VERBOSE) 804for line in inFile.readlines(): 805 line = regexp.sub(r'$\1$', line) 806 outFile.write(line) 807 inFile.close() 808 outFile.close() 809# Forcibly overwrite the original file 810 os.unlink(file) 811 shutil.move(outFileName,file) 812except: 813# cleanup our temporary file 814 os.unlink(outFileName) 815print"Failed to strip RCS keywords in%s"%file 816raise 817 818print"Patched up RCS keywords in%s"%file 819 820defp4UserForCommit(self,id): 821# Return the tuple (perforce user,git email) for a given git commit id 822 self.getUserMapFromPerforceServer() 823 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id) 824 gitEmail = gitEmail.strip() 825if not self.emails.has_key(gitEmail): 826return(None,gitEmail) 827else: 828return(self.emails[gitEmail],gitEmail) 829 830defcheckValidP4Users(self,commits): 831# check if any git authors cannot be mapped to p4 users 832foridin commits: 833(user,email) = self.p4UserForCommit(id) 834if not user: 835 msg ="Cannot find p4 user for email%sin commit%s."% (email,id) 836ifgitConfig('git-p4.allowMissingP4Users').lower() =="true": 837print"%s"% msg 838else: 839die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg) 840 841deflastP4Changelist(self): 842# Get back the last changelist number submitted in this client spec. This 843# then gets used to patch up the username in the change. If the same 844# client spec is being used by multiple processes then this might go 845# wrong. 846 results =p4CmdList("client -o")# find the current client 847 client =None 848for r in results: 849if r.has_key('Client'): 850 client = r['Client'] 851break 852if not client: 853die("could not get client spec") 854 results =p4CmdList(["changes","-c", client,"-m","1"]) 855for r in results: 856if r.has_key('change'): 857return r['change'] 858die("Could not get changelist number for last submit - cannot patch up user details") 859 860defmodifyChangelistUser(self, changelist, newUser): 861# fixup the user field of a changelist after it has been submitted. 862 changes =p4CmdList("change -o%s"% changelist) 863iflen(changes) !=1: 864die("Bad output from p4 change modifying%sto user%s"% 865(changelist, newUser)) 866 867 c = changes[0] 868if c['User'] == newUser:return# nothing to do 869 c['User'] = newUser 870input= marshal.dumps(c) 871 872 result =p4CmdList("change -f -i", stdin=input) 873for r in result: 874if r.has_key('code'): 875if r['code'] =='error': 876die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data'])) 877if r.has_key('data'): 878print("Updated user field for changelist%sto%s"% (changelist, newUser)) 879return 880die("Could not modify user field of changelist%sto%s"% (changelist, newUser)) 881 882defcanChangeChangelists(self): 883# check to see if we have p4 admin or super-user permissions, either of 884# which are required to modify changelists. 885 results =p4CmdList(["protects", self.depotPath]) 886for r in results: 887if r.has_key('perm'): 888if r['perm'] =='admin': 889return1 890if r['perm'] =='super': 891return1 892return0 893 894defprepareSubmitTemplate(self): 895# remove lines in the Files section that show changes to files outside the depot path we're committing into 896 template ="" 897 inFilesSection =False 898for line inp4_read_pipe_lines(['change','-o']): 899if line.endswith("\r\n"): 900 line = line[:-2] +"\n" 901if inFilesSection: 902if line.startswith("\t"): 903# path starts and ends with a tab 904 path = line[1:] 905 lastTab = path.rfind("\t") 906if lastTab != -1: 907 path = path[:lastTab] 908if notp4PathStartsWith(path, self.depotPath): 909continue 910else: 911 inFilesSection =False 912else: 913if line.startswith("Files:"): 914 inFilesSection =True 915 916 template += line 917 918return template 919 920defedit_template(self, template_file): 921"""Invoke the editor to let the user change the submission 922 message. Return true if okay to continue with the submit.""" 923 924# if configured to skip the editing part, just submit 925ifgitConfig("git-p4.skipSubmitEdit") =="true": 926return True 927 928# look at the modification time, to check later if the user saved 929# the file 930 mtime = os.stat(template_file).st_mtime 931 932# invoke the editor 933if os.environ.has_key("P4EDITOR"): 934 editor = os.environ.get("P4EDITOR") 935else: 936 editor =read_pipe("git var GIT_EDITOR").strip() 937system(editor +" "+ template_file) 938 939# If the file was not saved, prompt to see if this patch should 940# be skipped. But skip this verification step if configured so. 941ifgitConfig("git-p4.skipSubmitEditCheck") =="true": 942return True 943 944# modification time updated means user saved the file 945if os.stat(template_file).st_mtime > mtime: 946return True 947 948while True: 949 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ") 950if response =='y': 951return True 952if response =='n': 953return False 954 955defapplyCommit(self,id): 956print"Applying%s"% (read_pipe("git log --max-count=1 --pretty=oneline%s"%id)) 957 958(p4User, gitEmail) = self.p4UserForCommit(id) 959 960if not self.detectRenames: 961# If not explicitly set check the config variable 962 self.detectRenames =gitConfig("git-p4.detectRenames") 963 964if self.detectRenames.lower() =="false"or self.detectRenames =="": 965 diffOpts ="" 966elif self.detectRenames.lower() =="true": 967 diffOpts ="-M" 968else: 969 diffOpts ="-M%s"% self.detectRenames 970 971 detectCopies =gitConfig("git-p4.detectCopies") 972if detectCopies.lower() =="true": 973 diffOpts +=" -C" 974elif detectCopies !=""and detectCopies.lower() !="false": 975 diffOpts +=" -C%s"% detectCopies 976 977ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true": 978 diffOpts +=" --find-copies-harder" 979 980 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (diffOpts,id,id)) 981 filesToAdd =set() 982 filesToDelete =set() 983 editedFiles =set() 984 filesToChangeExecBit = {} 985 986for line in diff: 987 diff =parseDiffTreeEntry(line) 988 modifier = diff['status'] 989 path = diff['src'] 990if modifier =="M": 991p4_edit(path) 992ifisModeExecChanged(diff['src_mode'], diff['dst_mode']): 993 filesToChangeExecBit[path] = diff['dst_mode'] 994 editedFiles.add(path) 995elif modifier =="A": 996 filesToAdd.add(path) 997 filesToChangeExecBit[path] = diff['dst_mode'] 998if path in filesToDelete: 999 filesToDelete.remove(path)1000elif modifier =="D":1001 filesToDelete.add(path)1002if path in filesToAdd:1003 filesToAdd.remove(path)1004elif modifier =="C":1005 src, dest = diff['src'], diff['dst']1006p4_integrate(src, dest)1007if diff['src_sha1'] != diff['dst_sha1']:1008p4_edit(dest)1009ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1010p4_edit(dest)1011 filesToChangeExecBit[dest] = diff['dst_mode']1012 os.unlink(dest)1013 editedFiles.add(dest)1014elif modifier =="R":1015 src, dest = diff['src'], diff['dst']1016p4_integrate(src, dest)1017if diff['src_sha1'] != diff['dst_sha1']:1018p4_edit(dest)1019ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1020p4_edit(dest)1021 filesToChangeExecBit[dest] = diff['dst_mode']1022 os.unlink(dest)1023 editedFiles.add(dest)1024 filesToDelete.add(src)1025else:1026die("unknown modifier%sfor%s"% (modifier, path))10271028 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1029 patchcmd = diffcmd +" | git apply "1030 tryPatchCmd = patchcmd +"--check -"1031 applyPatchCmd = patchcmd +"--check --apply -"1032 patch_succeeded =True10331034if os.system(tryPatchCmd) !=0:1035 fixed_rcs_keywords =False1036 patch_succeeded =False1037print"Unfortunately applying the change failed!"10381039# Patch failed, maybe it's just RCS keyword woes. Look through1040# the patch to see if that's possible.1041ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1042file=None1043 pattern =None1044 kwfiles = {}1045forfilein editedFiles | filesToDelete:1046# did this file's delta contain RCS keywords?1047 pattern =p4_keywords_regexp_for_file(file)10481049if pattern:1050# this file is a possibility...look for RCS keywords.1051 regexp = re.compile(pattern, re.VERBOSE)1052for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1053if regexp.search(line):1054if verbose:1055print"got keyword match on%sin%sin%s"% (pattern, line,file)1056 kwfiles[file] = pattern1057break10581059forfilein kwfiles:1060if verbose:1061print"zapping%swith%s"% (line,pattern)1062 self.patchRCSKeywords(file, kwfiles[file])1063 fixed_rcs_keywords =True10641065if fixed_rcs_keywords:1066print"Retrying the patch with RCS keywords cleaned up"1067if os.system(tryPatchCmd) ==0:1068 patch_succeeded =True10691070if not patch_succeeded:1071print"What do you want to do?"1072 response ="x"1073while response !="s"and response !="a"and response !="w":1074 response =raw_input("[s]kip this patch / [a]pply the patch forcibly "1075"and with .rej files / [w]rite the patch to a file (patch.txt) ")1076if response =="s":1077print"Skipping! Good luck with the next patches..."1078for f in editedFiles:1079p4_revert(f)1080for f in filesToAdd:1081 os.remove(f)1082return1083elif response =="a":1084 os.system(applyPatchCmd)1085iflen(filesToAdd) >0:1086print"You may also want to call p4 add on the following files:"1087print" ".join(filesToAdd)1088iflen(filesToDelete):1089print"The following files should be scheduled for deletion with p4 delete:"1090print" ".join(filesToDelete)1091die("Please resolve and submit the conflict manually and "1092+"continue afterwards with git-p4 submit --continue")1093elif response =="w":1094system(diffcmd +" > patch.txt")1095print"Patch saved to patch.txt in%s!"% self.clientPath1096die("Please resolve and submit the conflict manually and "1097"continue afterwards with git-p4 submit --continue")10981099system(applyPatchCmd)11001101for f in filesToAdd:1102p4_add(f)1103for f in filesToDelete:1104p4_revert(f)1105p4_delete(f)11061107# Set/clear executable bits1108for f in filesToChangeExecBit.keys():1109 mode = filesToChangeExecBit[f]1110setP4ExecBit(f, mode)11111112 logMessage =extractLogMessageFromGitCommit(id)1113 logMessage = logMessage.strip()11141115 template = self.prepareSubmitTemplate()11161117if self.interactive:1118 submitTemplate = self.prepareLogMessage(template, logMessage)11191120if self.preserveUser:1121 submitTemplate = submitTemplate + ("\n######## Actual user%s, modified after commit\n"% p4User)11221123if os.environ.has_key("P4DIFF"):1124del(os.environ["P4DIFF"])1125 diff =""1126for editedFile in editedFiles:1127 diff +=p4_read_pipe(['diff','-du', editedFile])11281129 newdiff =""1130for newFile in filesToAdd:1131 newdiff +="==== new file ====\n"1132 newdiff +="--- /dev/null\n"1133 newdiff +="+++%s\n"% newFile1134 f =open(newFile,"r")1135for line in f.readlines():1136 newdiff +="+"+ line1137 f.close()11381139if self.checkAuthorship and not self.p4UserIsMe(p4User):1140 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1141 submitTemplate +="######## Use git-p4 option --preserve-user to modify authorship\n"1142 submitTemplate +="######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"11431144 separatorLine ="######## everything below this line is just the diff #######\n"11451146(handle, fileName) = tempfile.mkstemp()1147 tmpFile = os.fdopen(handle,"w+")1148if self.isWindows:1149 submitTemplate = submitTemplate.replace("\n","\r\n")1150 separatorLine = separatorLine.replace("\n","\r\n")1151 newdiff = newdiff.replace("\n","\r\n")1152 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1153 tmpFile.close()11541155if self.edit_template(fileName):1156# read the edited message and submit1157 tmpFile =open(fileName,"rb")1158 message = tmpFile.read()1159 tmpFile.close()1160 submitTemplate = message[:message.index(separatorLine)]1161if self.isWindows:1162 submitTemplate = submitTemplate.replace("\r\n","\n")1163p4_write_pipe(['submit','-i'], submitTemplate)11641165if self.preserveUser:1166if p4User:1167# Get last changelist number. Cannot easily get it from1168# the submit command output as the output is1169# unmarshalled.1170 changelist = self.lastP4Changelist()1171 self.modifyChangelistUser(changelist, p4User)1172else:1173# skip this patch1174print"Submission cancelled, undoing p4 changes."1175for f in editedFiles:1176p4_revert(f)1177for f in filesToAdd:1178p4_revert(f)1179 os.remove(f)11801181 os.remove(fileName)1182else:1183 fileName ="submit.txt"1184file=open(fileName,"w+")1185file.write(self.prepareLogMessage(template, logMessage))1186file.close()1187print("Perforce submit template written as%s. "1188+"Please review/edit and then use p4 submit -i <%sto submit directly!"1189% (fileName, fileName))11901191defrun(self, args):1192iflen(args) ==0:1193 self.master =currentGitBranch()1194iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1195die("Detecting current git branch failed!")1196eliflen(args) ==1:1197 self.master = args[0]1198if notbranchExists(self.master):1199die("Branch%sdoes not exist"% self.master)1200else:1201return False12021203 allowSubmit =gitConfig("git-p4.allowSubmit")1204iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1205die("%sis not in git-p4.allowSubmit"% self.master)12061207[upstream, settings] =findUpstreamBranchPoint()1208 self.depotPath = settings['depot-paths'][0]1209iflen(self.origin) ==0:1210 self.origin = upstream12111212if self.preserveUser:1213if not self.canChangeChangelists():1214die("Cannot preserve user names without p4 super-user or admin permissions")12151216if self.verbose:1217print"Origin branch is "+ self.origin12181219iflen(self.depotPath) ==0:1220print"Internal error: cannot locate perforce depot path from existing branches"1221 sys.exit(128)12221223 self.clientPath =p4Where(self.depotPath)12241225iflen(self.clientPath) ==0:1226print"Error: Cannot locate perforce checkout of%sin client view"% self.depotPath1227 sys.exit(128)12281229print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1230 self.oldWorkingDirectory = os.getcwd()12311232# ensure the clientPath exists1233if not os.path.exists(self.clientPath):1234 os.makedirs(self.clientPath)12351236chdir(self.clientPath)1237print"Synchronizing p4 checkout..."1238p4_sync("...")1239 self.check()12401241 commits = []1242for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1243 commits.append(line.strip())1244 commits.reverse()12451246if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1247 self.checkAuthorship =False1248else:1249 self.checkAuthorship =True12501251if self.preserveUser:1252 self.checkValidP4Users(commits)12531254whilelen(commits) >0:1255 commit = commits[0]1256 commits = commits[1:]1257 self.applyCommit(commit)1258if not self.interactive:1259break12601261iflen(commits) ==0:1262print"All changes applied!"1263chdir(self.oldWorkingDirectory)12641265 sync =P4Sync()1266 sync.run([])12671268 rebase =P4Rebase()1269 rebase.rebase()12701271return True12721273classView(object):1274"""Represent a p4 view ("p4 help views"), and map files in a1275 repo according to the view."""12761277classPath(object):1278"""A depot or client path, possibly containing wildcards.1279 The only one supported is ... at the end, currently.1280 Initialize with the full path, with //depot or //client."""12811282def__init__(self, path, is_depot):1283 self.path = path1284 self.is_depot = is_depot1285 self.find_wildcards()1286# remember the prefix bit, useful for relative mappings1287 m = re.match("(//[^/]+/)", self.path)1288if not m:1289die("Path%sdoes not start with //prefix/"% self.path)1290 prefix = m.group(1)1291if not self.is_depot:1292# strip //client/ on client paths1293 self.path = self.path[len(prefix):]12941295deffind_wildcards(self):1296"""Make sure wildcards are valid, and set up internal1297 variables."""12981299 self.ends_triple_dot =False1300# There are three wildcards allowed in p4 views1301# (see "p4 help views"). This code knows how to1302# handle "..." (only at the end), but cannot deal with1303# "%%n" or "*". Only check the depot_side, as p4 should1304# validate that the client_side matches too.1305if re.search(r'%%[1-9]', self.path):1306die("Can't handle%%n wildcards in view:%s"% self.path)1307if self.path.find("*") >=0:1308die("Can't handle * wildcards in view:%s"% self.path)1309 triple_dot_index = self.path.find("...")1310if triple_dot_index >=0:1311if triple_dot_index !=len(self.path) -3:1312die("Can handle only single ... wildcard, at end:%s"%1313 self.path)1314 self.ends_triple_dot =True13151316defensure_compatible(self, other_path):1317"""Make sure the wildcards agree."""1318if self.ends_triple_dot != other_path.ends_triple_dot:1319die("Both paths must end with ... if either does;\n"+1320"paths:%s %s"% (self.path, other_path.path))13211322defmatch_wildcards(self, test_path):1323"""See if this test_path matches us, and fill in the value1324 of the wildcards if so. Returns a tuple of1325 (True|False, wildcards[]). For now, only the ... at end1326 is supported, so at most one wildcard."""1327if self.ends_triple_dot:1328 dotless = self.path[:-3]1329if test_path.startswith(dotless):1330 wildcard = test_path[len(dotless):]1331return(True, [ wildcard ])1332else:1333if test_path == self.path:1334return(True, [])1335return(False, [])13361337defmatch(self, test_path):1338"""Just return if it matches; don't bother with the wildcards."""1339 b, _ = self.match_wildcards(test_path)1340return b13411342deffill_in_wildcards(self, wildcards):1343"""Return the relative path, with the wildcards filled in1344 if there are any."""1345if self.ends_triple_dot:1346return self.path[:-3] + wildcards[0]1347else:1348return self.path13491350classMapping(object):1351def__init__(self, depot_side, client_side, overlay, exclude):1352# depot_side is without the trailing /... if it had one1353 self.depot_side = View.Path(depot_side, is_depot=True)1354 self.client_side = View.Path(client_side, is_depot=False)1355 self.overlay = overlay # started with "+"1356 self.exclude = exclude # started with "-"1357assert not(self.overlay and self.exclude)1358 self.depot_side.ensure_compatible(self.client_side)13591360def__str__(self):1361 c =" "1362if self.overlay:1363 c ="+"1364if self.exclude:1365 c ="-"1366return"View.Mapping:%s%s->%s"% \1367(c, self.depot_side.path, self.client_side.path)13681369defmap_depot_to_client(self, depot_path):1370"""Calculate the client path if using this mapping on the1371 given depot path; does not consider the effect of other1372 mappings in a view. Even excluded mappings are returned."""1373 matches, wildcards = self.depot_side.match_wildcards(depot_path)1374if not matches:1375return""1376 client_path = self.client_side.fill_in_wildcards(wildcards)1377return client_path13781379#1380# View methods1381#1382def__init__(self):1383 self.mappings = []13841385defappend(self, view_line):1386"""Parse a view line, splitting it into depot and client1387 sides. Append to self.mappings, preserving order."""13881389# Split the view line into exactly two words. P4 enforces1390# structure on these lines that simplifies this quite a bit.1391#1392# Either or both words may be double-quoted.1393# Single quotes do not matter.1394# Double-quote marks cannot occur inside the words.1395# A + or - prefix is also inside the quotes.1396# There are no quotes unless they contain a space.1397# The line is already white-space stripped.1398# The two words are separated by a single space.1399#1400if view_line[0] =='"':1401# First word is double quoted. Find its end.1402 close_quote_index = view_line.find('"',1)1403if close_quote_index <=0:1404die("No first-word closing quote found:%s"% view_line)1405 depot_side = view_line[1:close_quote_index]1406# skip closing quote and space1407 rhs_index = close_quote_index +1+11408else:1409 space_index = view_line.find(" ")1410if space_index <=0:1411die("No word-splitting space found:%s"% view_line)1412 depot_side = view_line[0:space_index]1413 rhs_index = space_index +114141415if view_line[rhs_index] =='"':1416# Second word is double quoted. Make sure there is a1417# double quote at the end too.1418if not view_line.endswith('"'):1419die("View line with rhs quote should end with one:%s"%1420 view_line)1421# skip the quotes1422 client_side = view_line[rhs_index+1:-1]1423else:1424 client_side = view_line[rhs_index:]14251426# prefix + means overlay on previous mapping1427 overlay =False1428if depot_side.startswith("+"):1429 overlay =True1430 depot_side = depot_side[1:]14311432# prefix - means exclude this path1433 exclude =False1434if depot_side.startswith("-"):1435 exclude =True1436 depot_side = depot_side[1:]14371438 m = View.Mapping(depot_side, client_side, overlay, exclude)1439 self.mappings.append(m)14401441defmap_in_client(self, depot_path):1442"""Return the relative location in the client where this1443 depot file should live. Returns "" if the file should1444 not be mapped in the client."""14451446 paths_filled = []1447 client_path =""14481449# look at later entries first1450for m in self.mappings[::-1]:14511452# see where will this path end up in the client1453 p = m.map_depot_to_client(depot_path)14541455if p =="":1456# Depot path does not belong in client. Must remember1457# this, as previous items should not cause files to1458# exist in this path either. Remember that the list is1459# being walked from the end, which has higher precedence.1460# Overlap mappings do not exclude previous mappings.1461if not m.overlay:1462 paths_filled.append(m.client_side)14631464else:1465# This mapping matched; no need to search any further.1466# But, the mapping could be rejected if the client path1467# has already been claimed by an earlier mapping (i.e.1468# one later in the list, which we are walking backwards).1469 already_mapped_in_client =False1470for f in paths_filled:1471# this is View.Path.match1472if f.match(p):1473 already_mapped_in_client =True1474break1475if not already_mapped_in_client:1476# Include this file, unless it is from a line that1477# explicitly said to exclude it.1478if not m.exclude:1479 client_path = p14801481# a match, even if rejected, always stops the search1482break14831484return client_path14851486classP4Sync(Command, P4UserMap):1487 delete_actions = ("delete","move/delete","purge")14881489def__init__(self):1490 Command.__init__(self)1491 P4UserMap.__init__(self)1492 self.options = [1493 optparse.make_option("--branch", dest="branch"),1494 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1495 optparse.make_option("--changesfile", dest="changesFile"),1496 optparse.make_option("--silent", dest="silent", action="store_true"),1497 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1498 optparse.make_option("--verbose", dest="verbose", action="store_true"),1499 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1500help="Import into refs/heads/ , not refs/remotes"),1501 optparse.make_option("--max-changes", dest="maxChanges"),1502 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1503help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1504 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1505help="Only sync files that are included in the Perforce Client Spec")1506]1507 self.description ="""Imports from Perforce into a git repository.\n1508 example:1509 //depot/my/project/ -- to import the current head1510 //depot/my/project/@all -- to import everything1511 //depot/my/project/@1,6 -- to import only from revision 1 to 615121513 (a ... is not needed in the path p4 specification, it's added implicitly)"""15141515 self.usage +=" //depot/path[@revRange]"1516 self.silent =False1517 self.createdBranches =set()1518 self.committedChanges =set()1519 self.branch =""1520 self.detectBranches =False1521 self.detectLabels =False1522 self.changesFile =""1523 self.syncWithOrigin =True1524 self.verbose =False1525 self.importIntoRemotes =True1526 self.maxChanges =""1527 self.isWindows = (platform.system() =="Windows")1528 self.keepRepoPath =False1529 self.depotPaths =None1530 self.p4BranchesInGit = []1531 self.cloneExclude = []1532 self.useClientSpec =False1533 self.clientSpecDirs =None1534 self.tempBranches = []1535 self.tempBranchLocation ="git-p4-tmp"15361537ifgitConfig("git-p4.syncFromOrigin") =="false":1538 self.syncWithOrigin =False15391540#1541# P4 wildcards are not allowed in filenames. P4 complains1542# if you simply add them, but you can force it with "-f", in1543# which case it translates them into %xx encoding internally.1544# Search for and fix just these four characters. Do % last so1545# that fixing it does not inadvertently create new %-escapes.1546#1547defwildcard_decode(self, path):1548# Cannot have * in a filename in windows; untested as to1549# what p4 would do in such a case.1550if not self.isWindows:1551 path = path.replace("%2A","*")1552 path = path.replace("%23","#") \1553.replace("%40","@") \1554.replace("%25","%")1555return path15561557# Force a checkpoint in fast-import and wait for it to finish1558defcheckpoint(self):1559 self.gitStream.write("checkpoint\n\n")1560 self.gitStream.write("progress checkpoint\n\n")1561 out = self.gitOutput.readline()1562if self.verbose:1563print"checkpoint finished: "+ out15641565defextractFilesFromCommit(self, commit):1566 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1567for path in self.cloneExclude]1568 files = []1569 fnum =01570while commit.has_key("depotFile%s"% fnum):1571 path = commit["depotFile%s"% fnum]15721573if[p for p in self.cloneExclude1574ifp4PathStartsWith(path, p)]:1575 found =False1576else:1577 found = [p for p in self.depotPaths1578ifp4PathStartsWith(path, p)]1579if not found:1580 fnum = fnum +11581continue15821583file= {}1584file["path"] = path1585file["rev"] = commit["rev%s"% fnum]1586file["action"] = commit["action%s"% fnum]1587file["type"] = commit["type%s"% fnum]1588 files.append(file)1589 fnum = fnum +11590return files15911592defstripRepoPath(self, path, prefixes):1593if self.useClientSpec:1594return self.clientSpecDirs.map_in_client(path)15951596if self.keepRepoPath:1597 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]15981599for p in prefixes:1600ifp4PathStartsWith(path, p):1601 path = path[len(p):]16021603return path16041605defsplitFilesIntoBranches(self, commit):1606 branches = {}1607 fnum =01608while commit.has_key("depotFile%s"% fnum):1609 path = commit["depotFile%s"% fnum]1610 found = [p for p in self.depotPaths1611ifp4PathStartsWith(path, p)]1612if not found:1613 fnum = fnum +11614continue16151616file= {}1617file["path"] = path1618file["rev"] = commit["rev%s"% fnum]1619file["action"] = commit["action%s"% fnum]1620file["type"] = commit["type%s"% fnum]1621 fnum = fnum +116221623 relPath = self.stripRepoPath(path, self.depotPaths)16241625for branch in self.knownBranches.keys():16261627# add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.21628if relPath.startswith(branch +"/"):1629if branch not in branches:1630 branches[branch] = []1631 branches[branch].append(file)1632break16331634return branches16351636# output one file from the P4 stream1637# - helper for streamP4Files16381639defstreamOneP4File(self,file, contents):1640 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)1641 relPath = self.wildcard_decode(relPath)1642if verbose:1643 sys.stderr.write("%s\n"% relPath)16441645(type_base, type_mods) =split_p4_type(file["type"])16461647 git_mode ="100644"1648if"x"in type_mods:1649 git_mode ="100755"1650if type_base =="symlink":1651 git_mode ="120000"1652# p4 print on a symlink contains "target\n"; remove the newline1653 data =''.join(contents)1654 contents = [data[:-1]]16551656if type_base =="utf16":1657# p4 delivers different text in the python output to -G1658# than it does when using "print -o", or normal p4 client1659# operations. utf16 is converted to ascii or utf8, perhaps.1660# But ascii text saved as -t utf16 is completely mangled.1661# Invoke print -o to get the real contents.1662 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])1663 contents = [ text ]16641665if type_base =="apple":1666# Apple filetype files will be streamed as a concatenation of1667# its appledouble header and the contents. This is useless1668# on both macs and non-macs. If using "print -q -o xx", it1669# will create "xx" with the data, and "%xx" with the header.1670# This is also not very useful.1671#1672# Ideally, someday, this script can learn how to generate1673# appledouble files directly and import those to git, but1674# non-mac machines can never find a use for apple filetype.1675print"\nIgnoring apple filetype file%s"%file['depotFile']1676return16771678# Perhaps windows wants unicode, utf16 newlines translated too;1679# but this is not doing it.1680if self.isWindows and type_base =="text":1681 mangled = []1682for data in contents:1683 data = data.replace("\r\n","\n")1684 mangled.append(data)1685 contents = mangled16861687# Note that we do not try to de-mangle keywords on utf16 files,1688# even though in theory somebody may want that.1689 pattern =p4_keywords_regexp_for_type(type_base, type_mods)1690if pattern:1691 regexp = re.compile(pattern, re.VERBOSE)1692 text =''.join(contents)1693 text = regexp.sub(r'$\1$', text)1694 contents = [ text ]16951696 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))16971698# total length...1699 length =01700for d in contents:1701 length = length +len(d)17021703 self.gitStream.write("data%d\n"% length)1704for d in contents:1705 self.gitStream.write(d)1706 self.gitStream.write("\n")17071708defstreamOneP4Deletion(self,file):1709 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)1710if verbose:1711 sys.stderr.write("delete%s\n"% relPath)1712 self.gitStream.write("D%s\n"% relPath)17131714# handle another chunk of streaming data1715defstreamP4FilesCb(self, marshalled):17161717if marshalled.has_key('depotFile')and self.stream_have_file_info:1718# start of a new file - output the old one first1719 self.streamOneP4File(self.stream_file, self.stream_contents)1720 self.stream_file = {}1721 self.stream_contents = []1722 self.stream_have_file_info =False17231724# pick up the new file information... for the1725# 'data' field we need to append to our array1726for k in marshalled.keys():1727if k =='data':1728 self.stream_contents.append(marshalled['data'])1729else:1730 self.stream_file[k] = marshalled[k]17311732 self.stream_have_file_info =True17331734# Stream directly from "p4 files" into "git fast-import"1735defstreamP4Files(self, files):1736 filesForCommit = []1737 filesToRead = []1738 filesToDelete = []17391740for f in files:1741# if using a client spec, only add the files that have1742# a path in the client1743if self.clientSpecDirs:1744if self.clientSpecDirs.map_in_client(f['path']) =="":1745continue17461747 filesForCommit.append(f)1748if f['action']in self.delete_actions:1749 filesToDelete.append(f)1750else:1751 filesToRead.append(f)17521753# deleted files...1754for f in filesToDelete:1755 self.streamOneP4Deletion(f)17561757iflen(filesToRead) >0:1758 self.stream_file = {}1759 self.stream_contents = []1760 self.stream_have_file_info =False17611762# curry self argument1763defstreamP4FilesCbSelf(entry):1764 self.streamP4FilesCb(entry)17651766 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]17671768p4CmdList(["-x","-","print"],1769 stdin=fileArgs,1770 cb=streamP4FilesCbSelf)17711772# do the last chunk1773if self.stream_file.has_key('depotFile'):1774 self.streamOneP4File(self.stream_file, self.stream_contents)17751776defmake_email(self, userid):1777if userid in self.users:1778return self.users[userid]1779else:1780return"%s<a@b>"% userid17811782defcommit(self, details, files, branch, branchPrefixes, parent =""):1783 epoch = details["time"]1784 author = details["user"]1785 self.branchPrefixes = branchPrefixes17861787if self.verbose:1788print"commit into%s"% branch17891790# start with reading files; if that fails, we should not1791# create a commit.1792 new_files = []1793for f in files:1794if[p for p in branchPrefixes ifp4PathStartsWith(f['path'], p)]:1795 new_files.append(f)1796else:1797 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])17981799 self.gitStream.write("commit%s\n"% branch)1800# gitStream.write("mark :%s\n" % details["change"])1801 self.committedChanges.add(int(details["change"]))1802 committer =""1803if author not in self.users:1804 self.getUserMapFromPerforceServer()1805 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)18061807 self.gitStream.write("committer%s\n"% committer)18081809 self.gitStream.write("data <<EOT\n")1810 self.gitStream.write(details["desc"])1811 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"1812% (','.join(branchPrefixes), details["change"]))1813iflen(details['options']) >0:1814 self.gitStream.write(": options =%s"% details['options'])1815 self.gitStream.write("]\nEOT\n\n")18161817iflen(parent) >0:1818if self.verbose:1819print"parent%s"% parent1820 self.gitStream.write("from%s\n"% parent)18211822 self.streamP4Files(new_files)1823 self.gitStream.write("\n")18241825 change =int(details["change"])18261827if self.labels.has_key(change):1828 label = self.labels[change]1829 labelDetails = label[0]1830 labelRevisions = label[1]1831if self.verbose:1832print"Change%sis labelled%s"% (change, labelDetails)18331834 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)1835for p in branchPrefixes])18361837iflen(files) ==len(labelRevisions):18381839 cleanedFiles = {}1840for info in files:1841if info["action"]in self.delete_actions:1842continue1843 cleanedFiles[info["depotFile"]] = info["rev"]18441845if cleanedFiles == labelRevisions:1846 self.gitStream.write("tag tag_%s\n"% labelDetails["label"])1847 self.gitStream.write("from%s\n"% branch)18481849 owner = labelDetails["Owner"]18501851# Try to use the owner of the p4 label, or failing that,1852# the current p4 user id.1853if owner:1854 email = self.make_email(owner)1855else:1856 email = self.make_email(self.p4UserId())1857 tagger ="%s %s %s"% (email, epoch, self.tz)18581859 self.gitStream.write("tagger%s\n"% tagger)18601861 description = labelDetails["Description"]1862 self.gitStream.write("data%d\n"%len(description))1863 self.gitStream.write(description)1864 self.gitStream.write("\n")18651866else:1867if not self.silent:1868print("Tag%sdoes not match with change%s: files do not match."1869% (labelDetails["label"], change))18701871else:1872if not self.silent:1873print("Tag%sdoes not match with change%s: file count is different."1874% (labelDetails["label"], change))18751876defgetLabels(self):1877 self.labels = {}18781879 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])1880iflen(l) >0and not self.silent:1881print"Finding files belonging to labels in%s"% `self.depotPaths`18821883for output in l:1884 label = output["label"]1885 revisions = {}1886 newestChange =01887if self.verbose:1888print"Querying files for label%s"% label1889forfileinp4CmdList(["files"] +1890["%s...@%s"% (p, label)1891for p in self.depotPaths]):1892 revisions[file["depotFile"]] =file["rev"]1893 change =int(file["change"])1894if change > newestChange:1895 newestChange = change18961897 self.labels[newestChange] = [output, revisions]18981899if self.verbose:1900print"Label changes:%s"% self.labels.keys()19011902defguessProjectName(self):1903for p in self.depotPaths:1904if p.endswith("/"):1905 p = p[:-1]1906 p = p[p.strip().rfind("/") +1:]1907if not p.endswith("/"):1908 p +="/"1909return p19101911defgetBranchMapping(self):1912 lostAndFoundBranches =set()19131914 user =gitConfig("git-p4.branchUser")1915iflen(user) >0:1916 command ="branches -u%s"% user1917else:1918 command ="branches"19191920for info inp4CmdList(command):1921 details =p4Cmd(["branch","-o", info["branch"]])1922 viewIdx =01923while details.has_key("View%s"% viewIdx):1924 paths = details["View%s"% viewIdx].split(" ")1925 viewIdx = viewIdx +11926# require standard //depot/foo/... //depot/bar/... mapping1927iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):1928continue1929 source = paths[0]1930 destination = paths[1]1931## HACK1932ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):1933 source = source[len(self.depotPaths[0]):-4]1934 destination = destination[len(self.depotPaths[0]):-4]19351936if destination in self.knownBranches:1937if not self.silent:1938print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)1939print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)1940continue19411942 self.knownBranches[destination] = source19431944 lostAndFoundBranches.discard(destination)19451946if source not in self.knownBranches:1947 lostAndFoundBranches.add(source)19481949# Perforce does not strictly require branches to be defined, so we also1950# check git config for a branch list.1951#1952# Example of branch definition in git config file:1953# [git-p4]1954# branchList=main:branchA1955# branchList=main:branchB1956# branchList=branchA:branchC1957 configBranches =gitConfigList("git-p4.branchList")1958for branch in configBranches:1959if branch:1960(source, destination) = branch.split(":")1961 self.knownBranches[destination] = source19621963 lostAndFoundBranches.discard(destination)19641965if source not in self.knownBranches:1966 lostAndFoundBranches.add(source)196719681969for branch in lostAndFoundBranches:1970 self.knownBranches[branch] = branch19711972defgetBranchMappingFromGitBranches(self):1973 branches =p4BranchesInGit(self.importIntoRemotes)1974for branch in branches.keys():1975if branch =="master":1976 branch ="main"1977else:1978 branch = branch[len(self.projectName):]1979 self.knownBranches[branch] = branch19801981deflistExistingP4GitBranches(self):1982# branches holds mapping from name to commit1983 branches =p4BranchesInGit(self.importIntoRemotes)1984 self.p4BranchesInGit = branches.keys()1985for branch in branches.keys():1986 self.initialParents[self.refPrefix + branch] = branches[branch]19871988defupdateOptionDict(self, d):1989 option_keys = {}1990if self.keepRepoPath:1991 option_keys['keepRepoPath'] =119921993 d["options"] =' '.join(sorted(option_keys.keys()))19941995defreadOptions(self, d):1996 self.keepRepoPath = (d.has_key('options')1997and('keepRepoPath'in d['options']))19981999defgitRefForBranch(self, branch):2000if branch =="main":2001return self.refPrefix +"master"20022003iflen(branch) <=0:2004return branch20052006return self.refPrefix + self.projectName + branch20072008defgitCommitByP4Change(self, ref, change):2009if self.verbose:2010print"looking in ref "+ ref +" for change%susing bisect..."% change20112012 earliestCommit =""2013 latestCommit =parseRevision(ref)20142015while True:2016if self.verbose:2017print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2018 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2019iflen(next) ==0:2020if self.verbose:2021print"argh"2022return""2023 log =extractLogMessageFromGitCommit(next)2024 settings =extractSettingsGitLog(log)2025 currentChange =int(settings['change'])2026if self.verbose:2027print"current change%s"% currentChange20282029if currentChange == change:2030if self.verbose:2031print"found%s"% next2032return next20332034if currentChange < change:2035 earliestCommit ="^%s"% next2036else:2037 latestCommit ="%s"% next20382039return""20402041defimportNewBranch(self, branch, maxChange):2042# make fast-import flush all changes to disk and update the refs using the checkpoint2043# command so that we can try to find the branch parent in the git history2044 self.gitStream.write("checkpoint\n\n");2045 self.gitStream.flush();2046 branchPrefix = self.depotPaths[0] + branch +"/"2047range="@1,%s"% maxChange2048#print "prefix" + branchPrefix2049 changes =p4ChangesForPaths([branchPrefix],range)2050iflen(changes) <=0:2051return False2052 firstChange = changes[0]2053#print "first change in branch: %s" % firstChange2054 sourceBranch = self.knownBranches[branch]2055 sourceDepotPath = self.depotPaths[0] + sourceBranch2056 sourceRef = self.gitRefForBranch(sourceBranch)2057#print "source " + sourceBranch20582059 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2060#print "branch parent: %s" % branchParentChange2061 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2062iflen(gitParent) >0:2063 self.initialParents[self.gitRefForBranch(branch)] = gitParent2064#print "parent git commit: %s" % gitParent20652066 self.importChanges(changes)2067return True20682069defsearchParent(self, parent, branch, target):2070 parentFound =False2071for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2072 blob = blob.strip()2073iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2074 parentFound =True2075if self.verbose:2076print"Found parent of%sin commit%s"% (branch, blob)2077break2078if parentFound:2079return blob2080else:2081return None20822083defimportChanges(self, changes):2084 cnt =12085for change in changes:2086 description =p4Cmd(["describe",str(change)])2087 self.updateOptionDict(description)20882089if not self.silent:2090 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2091 sys.stdout.flush()2092 cnt = cnt +120932094try:2095if self.detectBranches:2096 branches = self.splitFilesIntoBranches(description)2097for branch in branches.keys():2098## HACK --hwn2099 branchPrefix = self.depotPaths[0] + branch +"/"21002101 parent =""21022103 filesForCommit = branches[branch]21042105if self.verbose:2106print"branch is%s"% branch21072108 self.updatedBranches.add(branch)21092110if branch not in self.createdBranches:2111 self.createdBranches.add(branch)2112 parent = self.knownBranches[branch]2113if parent == branch:2114 parent =""2115else:2116 fullBranch = self.projectName + branch2117if fullBranch not in self.p4BranchesInGit:2118if not self.silent:2119print("\nImporting new branch%s"% fullBranch);2120if self.importNewBranch(branch, change -1):2121 parent =""2122 self.p4BranchesInGit.append(fullBranch)2123if not self.silent:2124print("\nResuming with change%s"% change);21252126if self.verbose:2127print"parent determined through known branches:%s"% parent21282129 branch = self.gitRefForBranch(branch)2130 parent = self.gitRefForBranch(parent)21312132if self.verbose:2133print"looking for initial parent for%s; current parent is%s"% (branch, parent)21342135iflen(parent) ==0and branch in self.initialParents:2136 parent = self.initialParents[branch]2137del self.initialParents[branch]21382139 blob =None2140iflen(parent) >0:2141 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2142if self.verbose:2143print"Creating temporary branch: "+ tempBranch2144 self.commit(description, filesForCommit, tempBranch, [branchPrefix])2145 self.tempBranches.append(tempBranch)2146 self.checkpoint()2147 blob = self.searchParent(parent, branch, tempBranch)2148if blob:2149 self.commit(description, filesForCommit, branch, [branchPrefix], blob)2150else:2151if self.verbose:2152print"Parent of%snot found. Committing into head of%s"% (branch, parent)2153 self.commit(description, filesForCommit, branch, [branchPrefix], parent)2154else:2155 files = self.extractFilesFromCommit(description)2156 self.commit(description, files, self.branch, self.depotPaths,2157 self.initialParent)2158 self.initialParent =""2159exceptIOError:2160print self.gitError.read()2161 sys.exit(1)21622163defimportHeadRevision(self, revision):2164print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)21652166 details = {}2167 details["user"] ="git perforce import user"2168 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2169% (' '.join(self.depotPaths), revision))2170 details["change"] = revision2171 newestRevision =021722173 fileCnt =02174 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]21752176for info inp4CmdList(["files"] + fileArgs):21772178if'code'in info and info['code'] =='error':2179 sys.stderr.write("p4 returned an error:%s\n"2180% info['data'])2181if info['data'].find("must refer to client") >=0:2182 sys.stderr.write("This particular p4 error is misleading.\n")2183 sys.stderr.write("Perhaps the depot path was misspelled.\n");2184 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2185 sys.exit(1)2186if'p4ExitCode'in info:2187 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2188 sys.exit(1)218921902191 change =int(info["change"])2192if change > newestRevision:2193 newestRevision = change21942195if info["action"]in self.delete_actions:2196# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2197#fileCnt = fileCnt + 12198continue21992200for prop in["depotFile","rev","action","type"]:2201 details["%s%s"% (prop, fileCnt)] = info[prop]22022203 fileCnt = fileCnt +122042205 details["change"] = newestRevision22062207# Use time from top-most change so that all git-p4 clones of2208# the same p4 repo have the same commit SHA1s.2209 res =p4CmdList("describe -s%d"% newestRevision)2210 newestTime =None2211for r in res:2212if r.has_key('time'):2213 newestTime =int(r['time'])2214if newestTime is None:2215die("\"describe -s\"on newest change%ddid not give a time")2216 details["time"] = newestTime22172218 self.updateOptionDict(details)2219try:2220 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)2221exceptIOError:2222print"IO error with git fast-import. Is your git version recent enough?"2223print self.gitError.read()222422252226defgetClientSpec(self):2227 specList =p4CmdList("client -o")2228iflen(specList) !=1:2229die('Output from "client -o" is%dlines, expecting 1'%2230len(specList))22312232# dictionary of all client parameters2233 entry = specList[0]22342235# just the keys that start with "View"2236 view_keys = [ k for k in entry.keys()if k.startswith("View") ]22372238# hold this new View2239 view =View()22402241# append the lines, in order, to the view2242for view_num inrange(len(view_keys)):2243 k ="View%d"% view_num2244if k not in view_keys:2245die("Expected view key%smissing"% k)2246 view.append(entry[k])22472248 self.clientSpecDirs = view2249if self.verbose:2250for i, m inenumerate(self.clientSpecDirs.mappings):2251print"clientSpecDirs%d:%s"% (i,str(m))22522253defrun(self, args):2254 self.depotPaths = []2255 self.changeRange =""2256 self.initialParent =""2257 self.previousDepotPaths = []22582259# map from branch depot path to parent branch2260 self.knownBranches = {}2261 self.initialParents = {}2262 self.hasOrigin =originP4BranchesExist()2263if not self.syncWithOrigin:2264 self.hasOrigin =False22652266if self.importIntoRemotes:2267 self.refPrefix ="refs/remotes/p4/"2268else:2269 self.refPrefix ="refs/heads/p4/"22702271if self.syncWithOrigin and self.hasOrigin:2272if not self.silent:2273print"Syncing with origin first by calling git fetch origin"2274system("git fetch origin")22752276iflen(self.branch) ==0:2277 self.branch = self.refPrefix +"master"2278ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2279system("git update-ref%srefs/heads/p4"% self.branch)2280system("git branch -D p4");2281# create it /after/ importing, when master exists2282if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2283system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))22842285if not self.useClientSpec:2286ifgitConfig("git-p4.useclientspec","--bool") =="true":2287 self.useClientSpec =True2288if self.useClientSpec:2289 self.getClientSpec()22902291# TODO: should always look at previous commits,2292# merge with previous imports, if possible.2293if args == []:2294if self.hasOrigin:2295createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2296 self.listExistingP4GitBranches()22972298iflen(self.p4BranchesInGit) >1:2299if not self.silent:2300print"Importing from/into multiple branches"2301 self.detectBranches =True23022303if self.verbose:2304print"branches:%s"% self.p4BranchesInGit23052306 p4Change =02307for branch in self.p4BranchesInGit:2308 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)23092310 settings =extractSettingsGitLog(logMsg)23112312 self.readOptions(settings)2313if(settings.has_key('depot-paths')2314and settings.has_key('change')):2315 change =int(settings['change']) +12316 p4Change =max(p4Change, change)23172318 depotPaths =sorted(settings['depot-paths'])2319if self.previousDepotPaths == []:2320 self.previousDepotPaths = depotPaths2321else:2322 paths = []2323for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2324 prev_list = prev.split("/")2325 cur_list = cur.split("/")2326for i inrange(0,min(len(cur_list),len(prev_list))):2327if cur_list[i] <> prev_list[i]:2328 i = i -12329break23302331 paths.append("/".join(cur_list[:i +1]))23322333 self.previousDepotPaths = paths23342335if p4Change >0:2336 self.depotPaths =sorted(self.previousDepotPaths)2337 self.changeRange ="@%s,#head"% p4Change2338if not self.detectBranches:2339 self.initialParent =parseRevision(self.branch)2340if not self.silent and not self.detectBranches:2341print"Performing incremental import into%sgit branch"% self.branch23422343if not self.branch.startswith("refs/"):2344 self.branch ="refs/heads/"+ self.branch23452346iflen(args) ==0and self.depotPaths:2347if not self.silent:2348print"Depot paths:%s"%' '.join(self.depotPaths)2349else:2350if self.depotPaths and self.depotPaths != args:2351print("previous import used depot path%sand now%swas specified. "2352"This doesn't work!"% (' '.join(self.depotPaths),2353' '.join(args)))2354 sys.exit(1)23552356 self.depotPaths =sorted(args)23572358 revision =""2359 self.users = {}23602361# Make sure no revision specifiers are used when --changesfile2362# is specified.2363 bad_changesfile =False2364iflen(self.changesFile) >0:2365for p in self.depotPaths:2366if p.find("@") >=0or p.find("#") >=0:2367 bad_changesfile =True2368break2369if bad_changesfile:2370die("Option --changesfile is incompatible with revision specifiers")23712372 newPaths = []2373for p in self.depotPaths:2374if p.find("@") != -1:2375 atIdx = p.index("@")2376 self.changeRange = p[atIdx:]2377if self.changeRange =="@all":2378 self.changeRange =""2379elif','not in self.changeRange:2380 revision = self.changeRange2381 self.changeRange =""2382 p = p[:atIdx]2383elif p.find("#") != -1:2384 hashIdx = p.index("#")2385 revision = p[hashIdx:]2386 p = p[:hashIdx]2387elif self.previousDepotPaths == []:2388# pay attention to changesfile, if given, else import2389# the entire p4 tree at the head revision2390iflen(self.changesFile) ==0:2391 revision ="#head"23922393 p = re.sub("\.\.\.$","", p)2394if not p.endswith("/"):2395 p +="/"23962397 newPaths.append(p)23982399 self.depotPaths = newPaths240024012402 self.loadUserMapFromCache()2403 self.labels = {}2404if self.detectLabels:2405 self.getLabels();24062407if self.detectBranches:2408## FIXME - what's a P4 projectName ?2409 self.projectName = self.guessProjectName()24102411if self.hasOrigin:2412 self.getBranchMappingFromGitBranches()2413else:2414 self.getBranchMapping()2415if self.verbose:2416print"p4-git branches:%s"% self.p4BranchesInGit2417print"initial parents:%s"% self.initialParents2418for b in self.p4BranchesInGit:2419if b !="master":24202421## FIXME2422 b = b[len(self.projectName):]2423 self.createdBranches.add(b)24242425 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))24262427 importProcess = subprocess.Popen(["git","fast-import"],2428 stdin=subprocess.PIPE, stdout=subprocess.PIPE,2429 stderr=subprocess.PIPE);2430 self.gitOutput = importProcess.stdout2431 self.gitStream = importProcess.stdin2432 self.gitError = importProcess.stderr24332434if revision:2435 self.importHeadRevision(revision)2436else:2437 changes = []24382439iflen(self.changesFile) >0:2440 output =open(self.changesFile).readlines()2441 changeSet =set()2442for line in output:2443 changeSet.add(int(line))24442445for change in changeSet:2446 changes.append(change)24472448 changes.sort()2449else:2450# catch "git-p4 sync" with no new branches, in a repo that2451# does not have any existing git-p4 branches2452iflen(args) ==0and not self.p4BranchesInGit:2453die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2454if self.verbose:2455print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2456 self.changeRange)2457 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)24582459iflen(self.maxChanges) >0:2460 changes = changes[:min(int(self.maxChanges),len(changes))]24612462iflen(changes) ==0:2463if not self.silent:2464print"No changes to import!"2465return True24662467if not self.silent and not self.detectBranches:2468print"Import destination:%s"% self.branch24692470 self.updatedBranches =set()24712472 self.importChanges(changes)24732474if not self.silent:2475print""2476iflen(self.updatedBranches) >0:2477 sys.stdout.write("Updated branches: ")2478for b in self.updatedBranches:2479 sys.stdout.write("%s"% b)2480 sys.stdout.write("\n")24812482 self.gitStream.close()2483if importProcess.wait() !=0:2484die("fast-import failed:%s"% self.gitError.read())2485 self.gitOutput.close()2486 self.gitError.close()24872488# Cleanup temporary branches created during import2489if self.tempBranches != []:2490for branch in self.tempBranches:2491read_pipe("git update-ref -d%s"% branch)2492 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))24932494return True24952496classP4Rebase(Command):2497def__init__(self):2498 Command.__init__(self)2499 self.options = [ ]2500 self.description = ("Fetches the latest revision from perforce and "2501+"rebases the current work (branch) against it")2502 self.verbose =False25032504defrun(self, args):2505 sync =P4Sync()2506 sync.run([])25072508return self.rebase()25092510defrebase(self):2511if os.system("git update-index --refresh") !=0:2512die("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.");2513iflen(read_pipe("git diff-index HEAD --")) >0:2514die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");25152516[upstream, settings] =findUpstreamBranchPoint()2517iflen(upstream) ==0:2518die("Cannot find upstream branchpoint for rebase")25192520# the branchpoint may be p4/foo~3, so strip off the parent2521 upstream = re.sub("~[0-9]+$","", upstream)25222523print"Rebasing the current branch onto%s"% upstream2524 oldHead =read_pipe("git rev-parse HEAD").strip()2525system("git rebase%s"% upstream)2526system("git diff-tree --stat --summary -M%sHEAD"% oldHead)2527return True25282529classP4Clone(P4Sync):2530def__init__(self):2531 P4Sync.__init__(self)2532 self.description ="Creates a new git repository and imports from Perforce into it"2533 self.usage ="usage: %prog [options] //depot/path[@revRange]"2534 self.options += [2535 optparse.make_option("--destination", dest="cloneDestination",2536 action='store', default=None,2537help="where to leave result of the clone"),2538 optparse.make_option("-/", dest="cloneExclude",2539 action="append",type="string",2540help="exclude depot path"),2541 optparse.make_option("--bare", dest="cloneBare",2542 action="store_true", default=False),2543]2544 self.cloneDestination =None2545 self.needsGit =False2546 self.cloneBare =False25472548# This is required for the "append" cloneExclude action2549defensure_value(self, attr, value):2550if nothasattr(self, attr)orgetattr(self, attr)is None:2551setattr(self, attr, value)2552returngetattr(self, attr)25532554defdefaultDestination(self, args):2555## TODO: use common prefix of args?2556 depotPath = args[0]2557 depotDir = re.sub("(@[^@]*)$","", depotPath)2558 depotDir = re.sub("(#[^#]*)$","", depotDir)2559 depotDir = re.sub(r"\.\.\.$","", depotDir)2560 depotDir = re.sub(r"/$","", depotDir)2561return os.path.split(depotDir)[1]25622563defrun(self, args):2564iflen(args) <1:2565return False25662567if self.keepRepoPath and not self.cloneDestination:2568 sys.stderr.write("Must specify destination for --keep-path\n")2569 sys.exit(1)25702571 depotPaths = args25722573if not self.cloneDestination andlen(depotPaths) >1:2574 self.cloneDestination = depotPaths[-1]2575 depotPaths = depotPaths[:-1]25762577 self.cloneExclude = ["/"+p for p in self.cloneExclude]2578for p in depotPaths:2579if not p.startswith("//"):2580return False25812582if not self.cloneDestination:2583 self.cloneDestination = self.defaultDestination(args)25842585print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)25862587if not os.path.exists(self.cloneDestination):2588 os.makedirs(self.cloneDestination)2589chdir(self.cloneDestination)25902591 init_cmd = ["git","init"]2592if self.cloneBare:2593 init_cmd.append("--bare")2594 subprocess.check_call(init_cmd)25952596if not P4Sync.run(self, depotPaths):2597return False2598if self.branch !="master":2599if self.importIntoRemotes:2600 masterbranch ="refs/remotes/p4/master"2601else:2602 masterbranch ="refs/heads/p4/master"2603ifgitBranchExists(masterbranch):2604system("git branch master%s"% masterbranch)2605if not self.cloneBare:2606system("git checkout -f")2607else:2608print"Could not detect main branch. No checkout/master branch created."26092610return True26112612classP4Branches(Command):2613def__init__(self):2614 Command.__init__(self)2615 self.options = [ ]2616 self.description = ("Shows the git branches that hold imports and their "2617+"corresponding perforce depot paths")2618 self.verbose =False26192620defrun(self, args):2621iforiginP4BranchesExist():2622createOrUpdateBranchesFromOrigin()26232624 cmdline ="git rev-parse --symbolic "2625 cmdline +=" --remotes"26262627for line inread_pipe_lines(cmdline):2628 line = line.strip()26292630if not line.startswith('p4/')or line =="p4/HEAD":2631continue2632 branch = line26332634 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)2635 settings =extractSettingsGitLog(log)26362637print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])2638return True26392640classHelpFormatter(optparse.IndentedHelpFormatter):2641def__init__(self):2642 optparse.IndentedHelpFormatter.__init__(self)26432644defformat_description(self, description):2645if description:2646return description +"\n"2647else:2648return""26492650defprintUsage(commands):2651print"usage:%s<command> [options]"% sys.argv[0]2652print""2653print"valid commands:%s"%", ".join(commands)2654print""2655print"Try%s<command> --help for command specific help."% sys.argv[0]2656print""26572658commands = {2659"debug": P4Debug,2660"submit": P4Submit,2661"commit": P4Submit,2662"sync": P4Sync,2663"rebase": P4Rebase,2664"clone": P4Clone,2665"rollback": P4RollBack,2666"branches": P4Branches2667}266826692670defmain():2671iflen(sys.argv[1:]) ==0:2672printUsage(commands.keys())2673 sys.exit(2)26742675 cmd =""2676 cmdName = sys.argv[1]2677try:2678 klass = commands[cmdName]2679 cmd =klass()2680exceptKeyError:2681print"unknown command%s"% cmdName2682print""2683printUsage(commands.keys())2684 sys.exit(2)26852686 options = cmd.options2687 cmd.gitdir = os.environ.get("GIT_DIR",None)26882689 args = sys.argv[2:]26902691iflen(options) >0:2692if cmd.needsGit:2693 options.append(optparse.make_option("--git-dir", dest="gitdir"))26942695 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),2696 options,2697 description = cmd.description,2698 formatter =HelpFormatter())26992700(cmd, args) = parser.parse_args(sys.argv[2:], cmd);2701global verbose2702 verbose = cmd.verbose2703if cmd.needsGit:2704if cmd.gitdir ==None:2705 cmd.gitdir = os.path.abspath(".git")2706if notisValidGitDir(cmd.gitdir):2707 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()2708if os.path.exists(cmd.gitdir):2709 cdup =read_pipe("git rev-parse --show-cdup").strip()2710iflen(cdup) >0:2711chdir(cdup);27122713if notisValidGitDir(cmd.gitdir):2714ifisValidGitDir(cmd.gitdir +"/.git"):2715 cmd.gitdir +="/.git"2716else:2717die("fatal: cannot locate git repository at%s"% cmd.gitdir)27182719 os.environ["GIT_DIR"] = cmd.gitdir27202721if not cmd.run(args):2722 parser.print_help()2723 sys.exit(2)272427252726if __name__ =='__main__':2727main()