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 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 190defsetP4ExecBit(file, mode): 191# Reopens an already open file and changes the execute bit to match 192# the execute bit setting in the passed in mode. 193 194 p4Type ="+x" 195 196if notisModeExec(mode): 197 p4Type =getP4OpenedType(file) 198 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 199 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 200if p4Type[-1] =="+": 201 p4Type = p4Type[0:-1] 202 203p4_reopen(p4Type,file) 204 205defgetP4OpenedType(file): 206# Returns the perforce file type for the given file. 207 208 result =p4_read_pipe(["opened",file]) 209 match = re.match(".*\((.+)\)\r?$", result) 210if match: 211return match.group(1) 212else: 213die("Could not determine file type for%s(result: '%s')"% (file, result)) 214 215defdiffTreePattern(): 216# This is a simple generator for the diff tree regex pattern. This could be 217# a class variable if this and parseDiffTreeEntry were a part of a class. 218 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 219while True: 220yield pattern 221 222defparseDiffTreeEntry(entry): 223"""Parses a single diff tree entry into its component elements. 224 225 See git-diff-tree(1) manpage for details about the format of the diff 226 output. This method returns a dictionary with the following elements: 227 228 src_mode - The mode of the source file 229 dst_mode - The mode of the destination file 230 src_sha1 - The sha1 for the source file 231 dst_sha1 - The sha1 fr the destination file 232 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 233 status_score - The score for the status (applicable for 'C' and 'R' 234 statuses). This is None if there is no score. 235 src - The path for the source file. 236 dst - The path for the destination file. This is only present for 237 copy or renames. If it is not present, this is None. 238 239 If the pattern is not matched, None is returned.""" 240 241 match =diffTreePattern().next().match(entry) 242if match: 243return{ 244'src_mode': match.group(1), 245'dst_mode': match.group(2), 246'src_sha1': match.group(3), 247'dst_sha1': match.group(4), 248'status': match.group(5), 249'status_score': match.group(6), 250'src': match.group(7), 251'dst': match.group(10) 252} 253return None 254 255defisModeExec(mode): 256# Returns True if the given git mode represents an executable file, 257# otherwise False. 258return mode[-3:] =="755" 259 260defisModeExecChanged(src_mode, dst_mode): 261returnisModeExec(src_mode) !=isModeExec(dst_mode) 262 263defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 264 265ifisinstance(cmd,basestring): 266 cmd ="-G "+ cmd 267 expand =True 268else: 269 cmd = ["-G"] + cmd 270 expand =False 271 272 cmd =p4_build_cmd(cmd) 273if verbose: 274 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 275 276# Use a temporary file to avoid deadlocks without 277# subprocess.communicate(), which would put another copy 278# of stdout into memory. 279 stdin_file =None 280if stdin is not None: 281 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 282ifisinstance(stdin,basestring): 283 stdin_file.write(stdin) 284else: 285for i in stdin: 286 stdin_file.write(i +'\n') 287 stdin_file.flush() 288 stdin_file.seek(0) 289 290 p4 = subprocess.Popen(cmd, 291 shell=expand, 292 stdin=stdin_file, 293 stdout=subprocess.PIPE) 294 295 result = [] 296try: 297while True: 298 entry = marshal.load(p4.stdout) 299if cb is not None: 300cb(entry) 301else: 302 result.append(entry) 303exceptEOFError: 304pass 305 exitCode = p4.wait() 306if exitCode !=0: 307 entry = {} 308 entry["p4ExitCode"] = exitCode 309 result.append(entry) 310 311return result 312 313defp4Cmd(cmd): 314list=p4CmdList(cmd) 315 result = {} 316for entry inlist: 317 result.update(entry) 318return result; 319 320defp4Where(depotPath): 321if not depotPath.endswith("/"): 322 depotPath +="/" 323 depotPath = depotPath +"..." 324 outputList =p4CmdList(["where", depotPath]) 325 output =None 326for entry in outputList: 327if"depotFile"in entry: 328if entry["depotFile"] == depotPath: 329 output = entry 330break 331elif"data"in entry: 332 data = entry.get("data") 333 space = data.find(" ") 334if data[:space] == depotPath: 335 output = entry 336break 337if output ==None: 338return"" 339if output["code"] =="error": 340return"" 341 clientPath ="" 342if"path"in output: 343 clientPath = output.get("path") 344elif"data"in output: 345 data = output.get("data") 346 lastSpace = data.rfind(" ") 347 clientPath = data[lastSpace +1:] 348 349if clientPath.endswith("..."): 350 clientPath = clientPath[:-3] 351return clientPath 352 353defcurrentGitBranch(): 354returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 355 356defisValidGitDir(path): 357if(os.path.exists(path +"/HEAD") 358and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 359return True; 360return False 361 362defparseRevision(ref): 363returnread_pipe("git rev-parse%s"% ref).strip() 364 365defbranchExists(ref): 366 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 367 ignore_error=True) 368returnlen(rev) >0 369 370defextractLogMessageFromGitCommit(commit): 371 logMessage ="" 372 373## fixme: title is first line of commit, not 1st paragraph. 374 foundTitle =False 375for log inread_pipe_lines("git cat-file commit%s"% commit): 376if not foundTitle: 377iflen(log) ==1: 378 foundTitle =True 379continue 380 381 logMessage += log 382return logMessage 383 384defextractSettingsGitLog(log): 385 values = {} 386for line in log.split("\n"): 387 line = line.strip() 388 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 389if not m: 390continue 391 392 assignments = m.group(1).split(':') 393for a in assignments: 394 vals = a.split('=') 395 key = vals[0].strip() 396 val = ('='.join(vals[1:])).strip() 397if val.endswith('\"')and val.startswith('"'): 398 val = val[1:-1] 399 400 values[key] = val 401 402 paths = values.get("depot-paths") 403if not paths: 404 paths = values.get("depot-path") 405if paths: 406 values['depot-paths'] = paths.split(',') 407return values 408 409defgitBranchExists(branch): 410 proc = subprocess.Popen(["git","rev-parse", branch], 411 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 412return proc.wait() ==0; 413 414_gitConfig = {} 415defgitConfig(key, args =None):# set args to "--bool", for instance 416if not _gitConfig.has_key(key): 417 argsFilter ="" 418if args !=None: 419 argsFilter ="%s"% args 420 cmd ="git config%s%s"% (argsFilter, key) 421 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 422return _gitConfig[key] 423 424defgitConfigList(key): 425if not _gitConfig.has_key(key): 426 _gitConfig[key] =read_pipe("git config --get-all%s"% key, ignore_error=True).strip().split(os.linesep) 427return _gitConfig[key] 428 429defp4BranchesInGit(branchesAreInRemotes =True): 430 branches = {} 431 432 cmdline ="git rev-parse --symbolic " 433if branchesAreInRemotes: 434 cmdline +=" --remotes" 435else: 436 cmdline +=" --branches" 437 438for line inread_pipe_lines(cmdline): 439 line = line.strip() 440 441## only import to p4/ 442if not line.startswith('p4/')or line =="p4/HEAD": 443continue 444 branch = line 445 446# strip off p4 447 branch = re.sub("^p4/","", line) 448 449 branches[branch] =parseRevision(line) 450return branches 451 452deffindUpstreamBranchPoint(head ="HEAD"): 453 branches =p4BranchesInGit() 454# map from depot-path to branch name 455 branchByDepotPath = {} 456for branch in branches.keys(): 457 tip = branches[branch] 458 log =extractLogMessageFromGitCommit(tip) 459 settings =extractSettingsGitLog(log) 460if settings.has_key("depot-paths"): 461 paths =",".join(settings["depot-paths"]) 462 branchByDepotPath[paths] ="remotes/p4/"+ branch 463 464 settings =None 465 parent =0 466while parent <65535: 467 commit = head +"~%s"% parent 468 log =extractLogMessageFromGitCommit(commit) 469 settings =extractSettingsGitLog(log) 470if settings.has_key("depot-paths"): 471 paths =",".join(settings["depot-paths"]) 472if branchByDepotPath.has_key(paths): 473return[branchByDepotPath[paths], settings] 474 475 parent = parent +1 476 477return["", settings] 478 479defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 480if not silent: 481print("Creating/updating branch(es) in%sbased on origin branch(es)" 482% localRefPrefix) 483 484 originPrefix ="origin/p4/" 485 486for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 487 line = line.strip() 488if(not line.startswith(originPrefix))or line.endswith("HEAD"): 489continue 490 491 headName = line[len(originPrefix):] 492 remoteHead = localRefPrefix + headName 493 originHead = line 494 495 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 496if(not original.has_key('depot-paths') 497or not original.has_key('change')): 498continue 499 500 update =False 501if notgitBranchExists(remoteHead): 502if verbose: 503print"creating%s"% remoteHead 504 update =True 505else: 506 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 507if settings.has_key('change') >0: 508if settings['depot-paths'] == original['depot-paths']: 509 originP4Change =int(original['change']) 510 p4Change =int(settings['change']) 511if originP4Change > p4Change: 512print("%s(%s) is newer than%s(%s). " 513"Updating p4 branch from origin." 514% (originHead, originP4Change, 515 remoteHead, p4Change)) 516 update =True 517else: 518print("Ignoring:%swas imported from%swhile " 519"%swas imported from%s" 520% (originHead,','.join(original['depot-paths']), 521 remoteHead,','.join(settings['depot-paths']))) 522 523if update: 524system("git update-ref%s %s"% (remoteHead, originHead)) 525 526deforiginP4BranchesExist(): 527returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 528 529defp4ChangesForPaths(depotPaths, changeRange): 530assert depotPaths 531 cmd = ['changes'] 532for p in depotPaths: 533 cmd += ["%s...%s"% (p, changeRange)] 534 output =p4_read_pipe_lines(cmd) 535 536 changes = {} 537for line in output: 538 changeNum =int(line.split(" ")[1]) 539 changes[changeNum] =True 540 541 changelist = changes.keys() 542 changelist.sort() 543return changelist 544 545defp4PathStartsWith(path, prefix): 546# This method tries to remedy a potential mixed-case issue: 547# 548# If UserA adds //depot/DirA/file1 549# and UserB adds //depot/dira/file2 550# 551# we may or may not have a problem. If you have core.ignorecase=true, 552# we treat DirA and dira as the same directory 553 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 554if ignorecase: 555return path.lower().startswith(prefix.lower()) 556return path.startswith(prefix) 557 558defgetClientSpec(): 559"""Look at the p4 client spec, create a View() object that contains 560 all the mappings, and return it.""" 561 562 specList =p4CmdList("client -o") 563iflen(specList) !=1: 564die('Output from "client -o" is%dlines, expecting 1'% 565len(specList)) 566 567# dictionary of all client parameters 568 entry = specList[0] 569 570# just the keys that start with "View" 571 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 572 573# hold this new View 574 view =View() 575 576# append the lines, in order, to the view 577for view_num inrange(len(view_keys)): 578 k ="View%d"% view_num 579if k not in view_keys: 580die("Expected view key%smissing"% k) 581 view.append(entry[k]) 582 583return view 584 585defgetClientRoot(): 586"""Grab the client directory.""" 587 588 output =p4CmdList("client -o") 589iflen(output) !=1: 590die('Output from "client -o" is%dlines, expecting 1'%len(output)) 591 592 entry = output[0] 593if"Root"not in entry: 594die('Client has no "Root"') 595 596return entry["Root"] 597 598class Command: 599def__init__(self): 600 self.usage ="usage: %prog [options]" 601 self.needsGit =True 602 603class P4UserMap: 604def__init__(self): 605 self.userMapFromPerforceServer =False 606 607defgetUserCacheFilename(self): 608 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 609return home +"/.gitp4-usercache.txt" 610 611defgetUserMapFromPerforceServer(self): 612if self.userMapFromPerforceServer: 613return 614 self.users = {} 615 self.emails = {} 616 617for output inp4CmdList("users"): 618if not output.has_key("User"): 619continue 620 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 621 self.emails[output["Email"]] = output["User"] 622 623 624 s ='' 625for(key, val)in self.users.items(): 626 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 627 628open(self.getUserCacheFilename(),"wb").write(s) 629 self.userMapFromPerforceServer =True 630 631defloadUserMapFromCache(self): 632 self.users = {} 633 self.userMapFromPerforceServer =False 634try: 635 cache =open(self.getUserCacheFilename(),"rb") 636 lines = cache.readlines() 637 cache.close() 638for line in lines: 639 entry = line.strip().split("\t") 640 self.users[entry[0]] = entry[1] 641exceptIOError: 642 self.getUserMapFromPerforceServer() 643 644classP4Debug(Command): 645def__init__(self): 646 Command.__init__(self) 647 self.options = [ 648 optparse.make_option("--verbose", dest="verbose", action="store_true", 649 default=False), 650] 651 self.description ="A tool to debug the output of p4 -G." 652 self.needsGit =False 653 self.verbose =False 654 655defrun(self, args): 656 j =0 657for output inp4CmdList(args): 658print'Element:%d'% j 659 j +=1 660print output 661return True 662 663classP4RollBack(Command): 664def__init__(self): 665 Command.__init__(self) 666 self.options = [ 667 optparse.make_option("--verbose", dest="verbose", action="store_true"), 668 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 669] 670 self.description ="A tool to debug the multi-branch import. Don't use :)" 671 self.verbose =False 672 self.rollbackLocalBranches =False 673 674defrun(self, args): 675iflen(args) !=1: 676return False 677 maxChange =int(args[0]) 678 679if"p4ExitCode"inp4Cmd("changes -m 1"): 680die("Problems executing p4"); 681 682if self.rollbackLocalBranches: 683 refPrefix ="refs/heads/" 684 lines =read_pipe_lines("git rev-parse --symbolic --branches") 685else: 686 refPrefix ="refs/remotes/" 687 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 688 689for line in lines: 690if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 691 line = line.strip() 692 ref = refPrefix + line 693 log =extractLogMessageFromGitCommit(ref) 694 settings =extractSettingsGitLog(log) 695 696 depotPaths = settings['depot-paths'] 697 change = settings['change'] 698 699 changed =False 700 701iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 702for p in depotPaths]))) ==0: 703print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 704system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 705continue 706 707while change andint(change) > maxChange: 708 changed =True 709if self.verbose: 710print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 711system("git update-ref%s\"%s^\""% (ref, ref)) 712 log =extractLogMessageFromGitCommit(ref) 713 settings =extractSettingsGitLog(log) 714 715 716 depotPaths = settings['depot-paths'] 717 change = settings['change'] 718 719if changed: 720print"%srewound to%s"% (ref, change) 721 722return True 723 724classP4Submit(Command, P4UserMap): 725def__init__(self): 726 Command.__init__(self) 727 P4UserMap.__init__(self) 728 self.options = [ 729 optparse.make_option("--verbose", dest="verbose", action="store_true"), 730 optparse.make_option("--origin", dest="origin"), 731 optparse.make_option("-M", dest="detectRenames", action="store_true"), 732# preserve the user, requires relevant p4 permissions 733 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 734] 735 self.description ="Submit changes from git to the perforce depot." 736 self.usage +=" [name of git branch to submit into perforce depot]" 737 self.interactive =True 738 self.origin ="" 739 self.detectRenames =False 740 self.verbose =False 741 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 742 self.isWindows = (platform.system() =="Windows") 743 self.myP4UserId =None 744 745defcheck(self): 746iflen(p4CmdList("opened ...")) >0: 747die("You have files opened with perforce! Close them before starting the sync.") 748 749# replaces everything between 'Description:' and the next P4 submit template field with the 750# commit message 751defprepareLogMessage(self, template, message): 752 result ="" 753 754 inDescriptionSection =False 755 756for line in template.split("\n"): 757if line.startswith("#"): 758 result += line +"\n" 759continue 760 761if inDescriptionSection: 762if line.startswith("Files:")or line.startswith("Jobs:"): 763 inDescriptionSection =False 764else: 765continue 766else: 767if line.startswith("Description:"): 768 inDescriptionSection =True 769 line +="\n" 770for messageLine in message.split("\n"): 771 line +="\t"+ messageLine +"\n" 772 773 result += line +"\n" 774 775return result 776 777defp4UserForCommit(self,id): 778# Return the tuple (perforce user,git email) for a given git commit id 779 self.getUserMapFromPerforceServer() 780 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id) 781 gitEmail = gitEmail.strip() 782if not self.emails.has_key(gitEmail): 783return(None,gitEmail) 784else: 785return(self.emails[gitEmail],gitEmail) 786 787defcheckValidP4Users(self,commits): 788# check if any git authors cannot be mapped to p4 users 789foridin commits: 790(user,email) = self.p4UserForCommit(id) 791if not user: 792 msg ="Cannot find p4 user for email%sin commit%s."% (email,id) 793ifgitConfig('git-p4.allowMissingP4Users').lower() =="true": 794print"%s"% msg 795else: 796die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg) 797 798deflastP4Changelist(self): 799# Get back the last changelist number submitted in this client spec. This 800# then gets used to patch up the username in the change. If the same 801# client spec is being used by multiple processes then this might go 802# wrong. 803 results =p4CmdList("client -o")# find the current client 804 client =None 805for r in results: 806if r.has_key('Client'): 807 client = r['Client'] 808break 809if not client: 810die("could not get client spec") 811 results =p4CmdList(["changes","-c", client,"-m","1"]) 812for r in results: 813if r.has_key('change'): 814return r['change'] 815die("Could not get changelist number for last submit - cannot patch up user details") 816 817defmodifyChangelistUser(self, changelist, newUser): 818# fixup the user field of a changelist after it has been submitted. 819 changes =p4CmdList("change -o%s"% changelist) 820iflen(changes) !=1: 821die("Bad output from p4 change modifying%sto user%s"% 822(changelist, newUser)) 823 824 c = changes[0] 825if c['User'] == newUser:return# nothing to do 826 c['User'] = newUser 827input= marshal.dumps(c) 828 829 result =p4CmdList("change -f -i", stdin=input) 830for r in result: 831if r.has_key('code'): 832if r['code'] =='error': 833die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data'])) 834if r.has_key('data'): 835print("Updated user field for changelist%sto%s"% (changelist, newUser)) 836return 837die("Could not modify user field of changelist%sto%s"% (changelist, newUser)) 838 839defcanChangeChangelists(self): 840# check to see if we have p4 admin or super-user permissions, either of 841# which are required to modify changelists. 842 results =p4CmdList("protects%s"% self.depotPath) 843for r in results: 844if r.has_key('perm'): 845if r['perm'] =='admin': 846return1 847if r['perm'] =='super': 848return1 849return0 850 851defp4UserId(self): 852if self.myP4UserId: 853return self.myP4UserId 854 855 results =p4CmdList("user -o") 856for r in results: 857if r.has_key('User'): 858 self.myP4UserId = r['User'] 859return r['User'] 860die("Could not find your p4 user id") 861 862defp4UserIsMe(self, p4User): 863# return True if the given p4 user is actually me 864 me = self.p4UserId() 865if not p4User or p4User != me: 866return False 867else: 868return True 869 870defprepareSubmitTemplate(self): 871# remove lines in the Files section that show changes to files outside the depot path we're committing into 872 template ="" 873 inFilesSection =False 874for line inp4_read_pipe_lines(['change','-o']): 875if line.endswith("\r\n"): 876 line = line[:-2] +"\n" 877if inFilesSection: 878if line.startswith("\t"): 879# path starts and ends with a tab 880 path = line[1:] 881 lastTab = path.rfind("\t") 882if lastTab != -1: 883 path = path[:lastTab] 884if notp4PathStartsWith(path, self.depotPath): 885continue 886else: 887 inFilesSection =False 888else: 889if line.startswith("Files:"): 890 inFilesSection =True 891 892 template += line 893 894return template 895 896defedit_template(self, template_file): 897"""Invoke the editor to let the user change the submission 898 message. Return true if okay to continue with the submit.""" 899 900# if configured to skip the editing part, just submit 901ifgitConfig("git-p4.skipSubmitEdit") =="true": 902return True 903 904# look at the modification time, to check later if the user saved 905# the file 906 mtime = os.stat(template_file).st_mtime 907 908# invoke the editor 909if os.environ.has_key("P4EDITOR"): 910 editor = os.environ.get("P4EDITOR") 911else: 912 editor =read_pipe("git var GIT_EDITOR").strip() 913system(editor +" "+ template_file) 914 915# If the file was not saved, prompt to see if this patch should 916# be skipped. But skip this verification step if configured so. 917ifgitConfig("git-p4.skipSubmitEditCheck") =="true": 918return True 919 920# modification time updated means user saved the file 921if os.stat(template_file).st_mtime > mtime: 922return True 923 924while True: 925 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ") 926if response =='y': 927return True 928if response =='n': 929return False 930 931defapplyCommit(self,id): 932print"Applying%s"% (read_pipe("git log --max-count=1 --pretty=oneline%s"%id)) 933 934(p4User, gitEmail) = self.p4UserForCommit(id) 935 936if not self.detectRenames: 937# If not explicitly set check the config variable 938 self.detectRenames =gitConfig("git-p4.detectRenames") 939 940if self.detectRenames.lower() =="false"or self.detectRenames =="": 941 diffOpts ="" 942elif self.detectRenames.lower() =="true": 943 diffOpts ="-M" 944else: 945 diffOpts ="-M%s"% self.detectRenames 946 947 detectCopies =gitConfig("git-p4.detectCopies") 948if detectCopies.lower() =="true": 949 diffOpts +=" -C" 950elif detectCopies !=""and detectCopies.lower() !="false": 951 diffOpts +=" -C%s"% detectCopies 952 953ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true": 954 diffOpts +=" --find-copies-harder" 955 956 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (diffOpts,id,id)) 957 filesToAdd =set() 958 filesToDelete =set() 959 editedFiles =set() 960 filesToChangeExecBit = {} 961for line in diff: 962 diff =parseDiffTreeEntry(line) 963 modifier = diff['status'] 964 path = diff['src'] 965if modifier =="M": 966p4_edit(path) 967ifisModeExecChanged(diff['src_mode'], diff['dst_mode']): 968 filesToChangeExecBit[path] = diff['dst_mode'] 969 editedFiles.add(path) 970elif modifier =="A": 971 filesToAdd.add(path) 972 filesToChangeExecBit[path] = diff['dst_mode'] 973if path in filesToDelete: 974 filesToDelete.remove(path) 975elif modifier =="D": 976 filesToDelete.add(path) 977if path in filesToAdd: 978 filesToAdd.remove(path) 979elif modifier =="C": 980 src, dest = diff['src'], diff['dst'] 981p4_integrate(src, dest) 982if diff['src_sha1'] != diff['dst_sha1']: 983p4_edit(dest) 984ifisModeExecChanged(diff['src_mode'], diff['dst_mode']): 985p4_edit(dest) 986 filesToChangeExecBit[dest] = diff['dst_mode'] 987 os.unlink(dest) 988 editedFiles.add(dest) 989elif modifier =="R": 990 src, dest = diff['src'], diff['dst'] 991p4_integrate(src, dest) 992if diff['src_sha1'] != diff['dst_sha1']: 993p4_edit(dest) 994ifisModeExecChanged(diff['src_mode'], diff['dst_mode']): 995p4_edit(dest) 996 filesToChangeExecBit[dest] = diff['dst_mode'] 997 os.unlink(dest) 998 editedFiles.add(dest) 999 filesToDelete.add(src)1000else:1001die("unknown modifier%sfor%s"% (modifier, path))10021003 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1004 patchcmd = diffcmd +" | git apply "1005 tryPatchCmd = patchcmd +"--check -"1006 applyPatchCmd = patchcmd +"--check --apply -"10071008if os.system(tryPatchCmd) !=0:1009print"Unfortunately applying the change failed!"1010print"What do you want to do?"1011 response ="x"1012while response !="s"and response !="a"and response !="w":1013 response =raw_input("[s]kip this patch / [a]pply the patch forcibly "1014"and with .rej files / [w]rite the patch to a file (patch.txt) ")1015if response =="s":1016print"Skipping! Good luck with the next patches..."1017for f in editedFiles:1018p4_revert(f)1019for f in filesToAdd:1020 os.remove(f)1021return1022elif response =="a":1023 os.system(applyPatchCmd)1024iflen(filesToAdd) >0:1025print"You may also want to call p4 add on the following files:"1026print" ".join(filesToAdd)1027iflen(filesToDelete):1028print"The following files should be scheduled for deletion with p4 delete:"1029print" ".join(filesToDelete)1030die("Please resolve and submit the conflict manually and "1031+"continue afterwards with git-p4 submit --continue")1032elif response =="w":1033system(diffcmd +" > patch.txt")1034print"Patch saved to patch.txt in%s!"% self.clientPath1035die("Please resolve and submit the conflict manually and "1036"continue afterwards with git-p4 submit --continue")10371038system(applyPatchCmd)10391040for f in filesToAdd:1041p4_add(f)1042for f in filesToDelete:1043p4_revert(f)1044p4_delete(f)10451046# Set/clear executable bits1047for f in filesToChangeExecBit.keys():1048 mode = filesToChangeExecBit[f]1049setP4ExecBit(f, mode)10501051 logMessage =extractLogMessageFromGitCommit(id)1052 logMessage = logMessage.strip()10531054 template = self.prepareSubmitTemplate()10551056if self.interactive:1057 submitTemplate = self.prepareLogMessage(template, logMessage)10581059if self.preserveUser:1060 submitTemplate = submitTemplate + ("\n######## Actual user%s, modified after commit\n"% p4User)10611062if os.environ.has_key("P4DIFF"):1063del(os.environ["P4DIFF"])1064 diff =""1065for editedFile in editedFiles:1066 diff +=p4_read_pipe(['diff','-du', editedFile])10671068 newdiff =""1069for newFile in filesToAdd:1070 newdiff +="==== new file ====\n"1071 newdiff +="--- /dev/null\n"1072 newdiff +="+++%s\n"% newFile1073 f =open(newFile,"r")1074for line in f.readlines():1075 newdiff +="+"+ line1076 f.close()10771078if self.checkAuthorship and not self.p4UserIsMe(p4User):1079 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1080 submitTemplate +="######## Use git-p4 option --preserve-user to modify authorship\n"1081 submitTemplate +="######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"10821083 separatorLine ="######## everything below this line is just the diff #######\n"10841085(handle, fileName) = tempfile.mkstemp()1086 tmpFile = os.fdopen(handle,"w+")1087if self.isWindows:1088 submitTemplate = submitTemplate.replace("\n","\r\n")1089 separatorLine = separatorLine.replace("\n","\r\n")1090 newdiff = newdiff.replace("\n","\r\n")1091 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1092 tmpFile.close()10931094if self.edit_template(fileName):1095# read the edited message and submit1096 tmpFile =open(fileName,"rb")1097 message = tmpFile.read()1098 tmpFile.close()1099 submitTemplate = message[:message.index(separatorLine)]1100if self.isWindows:1101 submitTemplate = submitTemplate.replace("\r\n","\n")1102p4_write_pipe(['submit','-i'], submitTemplate)11031104if self.preserveUser:1105if p4User:1106# Get last changelist number. Cannot easily get it from1107# the submit command output as the output is1108# unmarshalled.1109 changelist = self.lastP4Changelist()1110 self.modifyChangelistUser(changelist, p4User)1111else:1112# skip this patch1113print"Submission cancelled, undoing p4 changes."1114for f in editedFiles:1115p4_revert(f)1116for f in filesToAdd:1117p4_revert(f)1118 os.remove(f)11191120 os.remove(fileName)1121else:1122 fileName ="submit.txt"1123file=open(fileName,"w+")1124file.write(self.prepareLogMessage(template, logMessage))1125file.close()1126print("Perforce submit template written as%s. "1127+"Please review/edit and then use p4 submit -i <%sto submit directly!"1128% (fileName, fileName))11291130defrun(self, args):1131iflen(args) ==0:1132 self.master =currentGitBranch()1133iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1134die("Detecting current git branch failed!")1135eliflen(args) ==1:1136 self.master = args[0]1137if notbranchExists(self.master):1138die("Branch%sdoes not exist"% self.master)1139else:1140return False11411142 allowSubmit =gitConfig("git-p4.allowSubmit")1143iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1144die("%sis not in git-p4.allowSubmit"% self.master)11451146[upstream, settings] =findUpstreamBranchPoint()1147 self.depotPath = settings['depot-paths'][0]1148iflen(self.origin) ==0:1149 self.origin = upstream11501151if self.preserveUser:1152if not self.canChangeChangelists():1153die("Cannot preserve user names without p4 super-user or admin permissions")11541155if self.verbose:1156print"Origin branch is "+ self.origin11571158iflen(self.depotPath) ==0:1159print"Internal error: cannot locate perforce depot path from existing branches"1160 sys.exit(128)11611162 self.useClientSpec =False1163ifgitConfig("git-p4.useclientspec","--bool") =="true":1164 self.useClientSpec =True1165if self.useClientSpec:1166 self.clientSpecDirs =getClientSpec()11671168if self.useClientSpec:1169# all files are relative to the client spec1170 self.clientPath =getClientRoot()1171else:1172 self.clientPath =p4Where(self.depotPath)11731174if self.clientPath =="":1175die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)11761177print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1178 self.oldWorkingDirectory = os.getcwd()11791180# ensure the clientPath exists1181if not os.path.exists(self.clientPath):1182 os.makedirs(self.clientPath)11831184chdir(self.clientPath)1185print"Synchronizing p4 checkout..."1186p4_sync("...")1187 self.check()11881189 commits = []1190for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1191 commits.append(line.strip())1192 commits.reverse()11931194if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1195 self.checkAuthorship =False1196else:1197 self.checkAuthorship =True11981199if self.preserveUser:1200 self.checkValidP4Users(commits)12011202whilelen(commits) >0:1203 commit = commits[0]1204 commits = commits[1:]1205 self.applyCommit(commit)1206if not self.interactive:1207break12081209iflen(commits) ==0:1210print"All changes applied!"1211chdir(self.oldWorkingDirectory)12121213 sync =P4Sync()1214 sync.run([])12151216 rebase =P4Rebase()1217 rebase.rebase()12181219return True12201221classView(object):1222"""Represent a p4 view ("p4 help views"), and map files in a1223 repo according to the view."""12241225classPath(object):1226"""A depot or client path, possibly containing wildcards.1227 The only one supported is ... at the end, currently.1228 Initialize with the full path, with //depot or //client."""12291230def__init__(self, path, is_depot):1231 self.path = path1232 self.is_depot = is_depot1233 self.find_wildcards()1234# remember the prefix bit, useful for relative mappings1235 m = re.match("(//[^/]+/)", self.path)1236if not m:1237die("Path%sdoes not start with //prefix/"% self.path)1238 prefix = m.group(1)1239if not self.is_depot:1240# strip //client/ on client paths1241 self.path = self.path[len(prefix):]12421243deffind_wildcards(self):1244"""Make sure wildcards are valid, and set up internal1245 variables."""12461247 self.ends_triple_dot =False1248# There are three wildcards allowed in p4 views1249# (see "p4 help views"). This code knows how to1250# handle "..." (only at the end), but cannot deal with1251# "%%n" or "*". Only check the depot_side, as p4 should1252# validate that the client_side matches too.1253if re.search(r'%%[1-9]', self.path):1254die("Can't handle%%n wildcards in view:%s"% self.path)1255if self.path.find("*") >=0:1256die("Can't handle * wildcards in view:%s"% self.path)1257 triple_dot_index = self.path.find("...")1258if triple_dot_index >=0:1259if not self.path.endswith("..."):1260die("Can handle ... wildcard only at end of path:%s"%1261 self.path)1262 self.ends_triple_dot =True12631264defensure_compatible(self, other_path):1265"""Make sure the wildcards agree."""1266if self.ends_triple_dot != other_path.ends_triple_dot:1267die("Both paths must end with ... if either does;\n"+1268"paths:%s %s"% (self.path, other_path.path))12691270defmatch_wildcards(self, test_path):1271"""See if this test_path matches us, and fill in the value1272 of the wildcards if so. Returns a tuple of1273 (True|False, wildcards[]). For now, only the ... at end1274 is supported, so at most one wildcard."""1275if self.ends_triple_dot:1276 dotless = self.path[:-3]1277if test_path.startswith(dotless):1278 wildcard = test_path[len(dotless):]1279return(True, [ wildcard ])1280else:1281if test_path == self.path:1282return(True, [])1283return(False, [])12841285defmatch(self, test_path):1286"""Just return if it matches; don't bother with the wildcards."""1287 b, _ = self.match_wildcards(test_path)1288return b12891290deffill_in_wildcards(self, wildcards):1291"""Return the relative path, with the wildcards filled in1292 if there are any."""1293if self.ends_triple_dot:1294return self.path[:-3] + wildcards[0]1295else:1296return self.path12971298classMapping(object):1299def__init__(self, depot_side, client_side, overlay, exclude):1300# depot_side is without the trailing /... if it had one1301 self.depot_side = View.Path(depot_side, is_depot=True)1302 self.client_side = View.Path(client_side, is_depot=False)1303 self.overlay = overlay # started with "+"1304 self.exclude = exclude # started with "-"1305assert not(self.overlay and self.exclude)1306 self.depot_side.ensure_compatible(self.client_side)13071308def__str__(self):1309 c =" "1310if self.overlay:1311 c ="+"1312if self.exclude:1313 c ="-"1314return"View.Mapping:%s%s->%s"% \1315(c, self.depot_side, self.client_side)13161317defmap_depot_to_client(self, depot_path):1318"""Calculate the client path if using this mapping on the1319 given depot path; does not consider the effect of other1320 mappings in a view. Even excluded mappings are returned."""1321 matches, wildcards = self.depot_side.match_wildcards(depot_path)1322if not matches:1323return""1324 client_path = self.client_side.fill_in_wildcards(wildcards)1325return client_path13261327#1328# View methods1329#1330def__init__(self):1331 self.mappings = []13321333defappend(self, view_line):1334"""Parse a view line, splitting it into depot and client1335 sides. Append to self.mappings, preserving order."""13361337# Split the view line into exactly two words. P4 enforces1338# structure on these lines that simplifies this quite a bit.1339#1340# Either or both words may be double-quoted.1341# Single quotes do not matter.1342# Double-quote marks cannot occur inside the words.1343# A + or - prefix is also inside the quotes.1344# There are no quotes unless they contain a space.1345# The line is already white-space stripped.1346# The two words are separated by a single space.1347#1348if view_line[0] =='"':1349# First word is double quoted. Find its end.1350 close_quote_index = view_line.find('"',1)1351if close_quote_index <=0:1352die("No first-word closing quote found:%s"% view_line)1353 depot_side = view_line[1:close_quote_index]1354# skip closing quote and space1355 rhs_index = close_quote_index +1+11356else:1357 space_index = view_line.find(" ")1358if space_index <=0:1359die("No word-splitting space found:%s"% view_line)1360 depot_side = view_line[0:space_index]1361 rhs_index = space_index +113621363if view_line[rhs_index] =='"':1364# Second word is double quoted. Make sure there is a1365# double quote at the end too.1366if not view_line.endswith('"'):1367die("View line with rhs quote should end with one:%s"%1368 view_line)1369# skip the quotes1370 client_side = view_line[rhs_index+1:-1]1371else:1372 client_side = view_line[rhs_index:]13731374# prefix + means overlay on previous mapping1375 overlay =False1376if depot_side.startswith("+"):1377 overlay =True1378 depot_side = depot_side[1:]13791380# prefix - means exclude this path1381 exclude =False1382if depot_side.startswith("-"):1383 exclude =True1384 depot_side = depot_side[1:]13851386 m = View.Mapping(depot_side, client_side, overlay, exclude)1387 self.mappings.append(m)13881389defmap_in_client(self, depot_path):1390"""Return the relative location in the client where this1391 depot file should live. Returns "" if the file should1392 not be mapped in the client."""13931394 paths_filled = []1395 client_path =""13961397# look at later entries first1398for m in self.mappings[::-1]:13991400# see where will this path end up in the client1401 p = m.map_depot_to_client(depot_path)14021403if p =="":1404# Depot path does not belong in client. Must remember1405# this, as previous items should not cause files to1406# exist in this path either. Remember that the list is1407# being walked from the end, which has higher precedence.1408# Overlap mappings do not exclude previous mappings.1409if not m.overlay:1410 paths_filled.append(m.client_side)14111412else:1413# This mapping matched; no need to search any further.1414# But, the mapping could be rejected if the client path1415# has already been claimed by an earlier mapping.1416 already_mapped_in_client =False1417for f in paths_filled:1418# this is View.Path.match1419if f.match(p):1420 already_mapped_in_client =True1421break1422if not already_mapped_in_client:1423# Include this file, unless it is from a line that1424# explicitly said to exclude it.1425if not m.exclude:1426 client_path = p14271428# a match, even if rejected, always stops the search1429break14301431return client_path14321433classP4Sync(Command, P4UserMap):1434 delete_actions = ("delete","move/delete","purge")14351436def__init__(self):1437 Command.__init__(self)1438 P4UserMap.__init__(self)1439 self.options = [1440 optparse.make_option("--branch", dest="branch"),1441 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1442 optparse.make_option("--changesfile", dest="changesFile"),1443 optparse.make_option("--silent", dest="silent", action="store_true"),1444 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1445 optparse.make_option("--verbose", dest="verbose", action="store_true"),1446 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1447help="Import into refs/heads/ , not refs/remotes"),1448 optparse.make_option("--max-changes", dest="maxChanges"),1449 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1450help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1451 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1452help="Only sync files that are included in the Perforce Client Spec")1453]1454 self.description ="""Imports from Perforce into a git repository.\n1455 example:1456 //depot/my/project/ -- to import the current head1457 //depot/my/project/@all -- to import everything1458 //depot/my/project/@1,6 -- to import only from revision 1 to 614591460 (a ... is not needed in the path p4 specification, it's added implicitly)"""14611462 self.usage +=" //depot/path[@revRange]"1463 self.silent =False1464 self.createdBranches =set()1465 self.committedChanges =set()1466 self.branch =""1467 self.detectBranches =False1468 self.detectLabels =False1469 self.changesFile =""1470 self.syncWithOrigin =True1471 self.verbose =False1472 self.importIntoRemotes =True1473 self.maxChanges =""1474 self.isWindows = (platform.system() =="Windows")1475 self.keepRepoPath =False1476 self.depotPaths =None1477 self.p4BranchesInGit = []1478 self.cloneExclude = []1479 self.useClientSpec =False1480 self.useClientSpec_from_options =False1481 self.clientSpecDirs =None14821483ifgitConfig("git-p4.syncFromOrigin") =="false":1484 self.syncWithOrigin =False14851486#1487# P4 wildcards are not allowed in filenames. P4 complains1488# if you simply add them, but you can force it with "-f", in1489# which case it translates them into %xx encoding internally.1490# Search for and fix just these four characters. Do % last so1491# that fixing it does not inadvertently create new %-escapes.1492#1493defwildcard_decode(self, path):1494# Cannot have * in a filename in windows; untested as to1495# what p4 would do in such a case.1496if not self.isWindows:1497 path = path.replace("%2A","*")1498 path = path.replace("%23","#") \1499.replace("%40","@") \1500.replace("%25","%")1501return path15021503defextractFilesFromCommit(self, commit):1504 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1505for path in self.cloneExclude]1506 files = []1507 fnum =01508while commit.has_key("depotFile%s"% fnum):1509 path = commit["depotFile%s"% fnum]15101511if[p for p in self.cloneExclude1512ifp4PathStartsWith(path, p)]:1513 found =False1514else:1515 found = [p for p in self.depotPaths1516ifp4PathStartsWith(path, p)]1517if not found:1518 fnum = fnum +11519continue15201521file= {}1522file["path"] = path1523file["rev"] = commit["rev%s"% fnum]1524file["action"] = commit["action%s"% fnum]1525file["type"] = commit["type%s"% fnum]1526 files.append(file)1527 fnum = fnum +11528return files15291530defstripRepoPath(self, path, prefixes):1531if self.useClientSpec:1532return self.clientSpecDirs.map_in_client(path)15331534if self.keepRepoPath:1535 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]15361537for p in prefixes:1538ifp4PathStartsWith(path, p):1539 path = path[len(p):]15401541return path15421543defsplitFilesIntoBranches(self, commit):1544 branches = {}1545 fnum =01546while commit.has_key("depotFile%s"% fnum):1547 path = commit["depotFile%s"% fnum]1548 found = [p for p in self.depotPaths1549ifp4PathStartsWith(path, p)]1550if not found:1551 fnum = fnum +11552continue15531554file= {}1555file["path"] = path1556file["rev"] = commit["rev%s"% fnum]1557file["action"] = commit["action%s"% fnum]1558file["type"] = commit["type%s"% fnum]1559 fnum = fnum +115601561 relPath = self.stripRepoPath(path, self.depotPaths)15621563for branch in self.knownBranches.keys():15641565# add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.21566if relPath.startswith(branch +"/"):1567if branch not in branches:1568 branches[branch] = []1569 branches[branch].append(file)1570break15711572return branches15731574# output one file from the P4 stream1575# - helper for streamP4Files15761577defstreamOneP4File(self,file, contents):1578 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)1579 relPath = self.wildcard_decode(relPath)1580if verbose:1581 sys.stderr.write("%s\n"% relPath)15821583(type_base, type_mods) =split_p4_type(file["type"])15841585 git_mode ="100644"1586if"x"in type_mods:1587 git_mode ="100755"1588if type_base =="symlink":1589 git_mode ="120000"1590# p4 print on a symlink contains "target\n"; remove the newline1591 data =''.join(contents)1592 contents = [data[:-1]]15931594if type_base =="utf16":1595# p4 delivers different text in the python output to -G1596# than it does when using "print -o", or normal p4 client1597# operations. utf16 is converted to ascii or utf8, perhaps.1598# But ascii text saved as -t utf16 is completely mangled.1599# Invoke print -o to get the real contents.1600 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])1601 contents = [ text ]16021603if type_base =="apple":1604# Apple filetype files will be streamed as a concatenation of1605# its appledouble header and the contents. This is useless1606# on both macs and non-macs. If using "print -q -o xx", it1607# will create "xx" with the data, and "%xx" with the header.1608# This is also not very useful.1609#1610# Ideally, someday, this script can learn how to generate1611# appledouble files directly and import those to git, but1612# non-mac machines can never find a use for apple filetype.1613print"\nIgnoring apple filetype file%s"%file['depotFile']1614return16151616# Perhaps windows wants unicode, utf16 newlines translated too;1617# but this is not doing it.1618if self.isWindows and type_base =="text":1619 mangled = []1620for data in contents:1621 data = data.replace("\r\n","\n")1622 mangled.append(data)1623 contents = mangled16241625# Note that we do not try to de-mangle keywords on utf16 files,1626# even though in theory somebody may want that.1627if type_base in("text","unicode","binary"):1628if"ko"in type_mods:1629 text =''.join(contents)1630 text = re.sub(r'\$(Id|Header):[^$]*\$', r'$\1$', text)1631 contents = [ text ]1632elif"k"in type_mods:1633 text =''.join(contents)1634 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$', r'$\1$', text)1635 contents = [ text ]16361637 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))16381639# total length...1640 length =01641for d in contents:1642 length = length +len(d)16431644 self.gitStream.write("data%d\n"% length)1645for d in contents:1646 self.gitStream.write(d)1647 self.gitStream.write("\n")16481649defstreamOneP4Deletion(self,file):1650 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)1651if verbose:1652 sys.stderr.write("delete%s\n"% relPath)1653 self.gitStream.write("D%s\n"% relPath)16541655# handle another chunk of streaming data1656defstreamP4FilesCb(self, marshalled):16571658if marshalled.has_key('depotFile')and self.stream_have_file_info:1659# start of a new file - output the old one first1660 self.streamOneP4File(self.stream_file, self.stream_contents)1661 self.stream_file = {}1662 self.stream_contents = []1663 self.stream_have_file_info =False16641665# pick up the new file information... for the1666# 'data' field we need to append to our array1667for k in marshalled.keys():1668if k =='data':1669 self.stream_contents.append(marshalled['data'])1670else:1671 self.stream_file[k] = marshalled[k]16721673 self.stream_have_file_info =True16741675# Stream directly from "p4 files" into "git fast-import"1676defstreamP4Files(self, files):1677 filesForCommit = []1678 filesToRead = []1679 filesToDelete = []16801681for f in files:1682# if using a client spec, only add the files that have1683# a path in the client1684if self.clientSpecDirs:1685if self.clientSpecDirs.map_in_client(f['path']) =="":1686continue16871688 filesForCommit.append(f)1689if f['action']in self.delete_actions:1690 filesToDelete.append(f)1691else:1692 filesToRead.append(f)16931694# deleted files...1695for f in filesToDelete:1696 self.streamOneP4Deletion(f)16971698iflen(filesToRead) >0:1699 self.stream_file = {}1700 self.stream_contents = []1701 self.stream_have_file_info =False17021703# curry self argument1704defstreamP4FilesCbSelf(entry):1705 self.streamP4FilesCb(entry)17061707 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]17081709p4CmdList(["-x","-","print"],1710 stdin=fileArgs,1711 cb=streamP4FilesCbSelf)17121713# do the last chunk1714if self.stream_file.has_key('depotFile'):1715 self.streamOneP4File(self.stream_file, self.stream_contents)17161717defcommit(self, details, files, branch, branchPrefixes, parent =""):1718 epoch = details["time"]1719 author = details["user"]1720 self.branchPrefixes = branchPrefixes17211722if self.verbose:1723print"commit into%s"% branch17241725# start with reading files; if that fails, we should not1726# create a commit.1727 new_files = []1728for f in files:1729if[p for p in branchPrefixes ifp4PathStartsWith(f['path'], p)]:1730 new_files.append(f)1731else:1732 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])17331734 self.gitStream.write("commit%s\n"% branch)1735# gitStream.write("mark :%s\n" % details["change"])1736 self.committedChanges.add(int(details["change"]))1737 committer =""1738if author not in self.users:1739 self.getUserMapFromPerforceServer()1740if author in self.users:1741 committer ="%s %s %s"% (self.users[author], epoch, self.tz)1742else:1743 committer ="%s<a@b>%s %s"% (author, epoch, self.tz)17441745 self.gitStream.write("committer%s\n"% committer)17461747 self.gitStream.write("data <<EOT\n")1748 self.gitStream.write(details["desc"])1749 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"1750% (','.join(branchPrefixes), details["change"]))1751iflen(details['options']) >0:1752 self.gitStream.write(": options =%s"% details['options'])1753 self.gitStream.write("]\nEOT\n\n")17541755iflen(parent) >0:1756if self.verbose:1757print"parent%s"% parent1758 self.gitStream.write("from%s\n"% parent)17591760 self.streamP4Files(new_files)1761 self.gitStream.write("\n")17621763 change =int(details["change"])17641765if self.labels.has_key(change):1766 label = self.labels[change]1767 labelDetails = label[0]1768 labelRevisions = label[1]1769if self.verbose:1770print"Change%sis labelled%s"% (change, labelDetails)17711772 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)1773for p in branchPrefixes])17741775iflen(files) ==len(labelRevisions):17761777 cleanedFiles = {}1778for info in files:1779if info["action"]in self.delete_actions:1780continue1781 cleanedFiles[info["depotFile"]] = info["rev"]17821783if cleanedFiles == labelRevisions:1784 self.gitStream.write("tag tag_%s\n"% labelDetails["label"])1785 self.gitStream.write("from%s\n"% branch)17861787 owner = labelDetails["Owner"]1788 tagger =""1789if author in self.users:1790 tagger ="%s %s %s"% (self.users[owner], epoch, self.tz)1791else:1792 tagger ="%s<a@b>%s %s"% (owner, epoch, self.tz)1793 self.gitStream.write("tagger%s\n"% tagger)1794 self.gitStream.write("data <<EOT\n")1795 self.gitStream.write(labelDetails["Description"])1796 self.gitStream.write("EOT\n\n")17971798else:1799if not self.silent:1800print("Tag%sdoes not match with change%s: files do not match."1801% (labelDetails["label"], change))18021803else:1804if not self.silent:1805print("Tag%sdoes not match with change%s: file count is different."1806% (labelDetails["label"], change))18071808defgetLabels(self):1809 self.labels = {}18101811 l =p4CmdList("labels%s..."%' '.join(self.depotPaths))1812iflen(l) >0and not self.silent:1813print"Finding files belonging to labels in%s"% `self.depotPaths`18141815for output in l:1816 label = output["label"]1817 revisions = {}1818 newestChange =01819if self.verbose:1820print"Querying files for label%s"% label1821forfileinp4CmdList(["files"] +1822["%s...@%s"% (p, label)1823for p in self.depotPaths]):1824 revisions[file["depotFile"]] =file["rev"]1825 change =int(file["change"])1826if change > newestChange:1827 newestChange = change18281829 self.labels[newestChange] = [output, revisions]18301831if self.verbose:1832print"Label changes:%s"% self.labels.keys()18331834defguessProjectName(self):1835for p in self.depotPaths:1836if p.endswith("/"):1837 p = p[:-1]1838 p = p[p.strip().rfind("/") +1:]1839if not p.endswith("/"):1840 p +="/"1841return p18421843defgetBranchMapping(self):1844 lostAndFoundBranches =set()18451846 user =gitConfig("git-p4.branchUser")1847iflen(user) >0:1848 command ="branches -u%s"% user1849else:1850 command ="branches"18511852for info inp4CmdList(command):1853 details =p4Cmd("branch -o%s"% info["branch"])1854 viewIdx =01855while details.has_key("View%s"% viewIdx):1856 paths = details["View%s"% viewIdx].split(" ")1857 viewIdx = viewIdx +11858# require standard //depot/foo/... //depot/bar/... mapping1859iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):1860continue1861 source = paths[0]1862 destination = paths[1]1863## HACK1864ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):1865 source = source[len(self.depotPaths[0]):-4]1866 destination = destination[len(self.depotPaths[0]):-4]18671868if destination in self.knownBranches:1869if not self.silent:1870print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)1871print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)1872continue18731874 self.knownBranches[destination] = source18751876 lostAndFoundBranches.discard(destination)18771878if source not in self.knownBranches:1879 lostAndFoundBranches.add(source)18801881# Perforce does not strictly require branches to be defined, so we also1882# check git config for a branch list.1883#1884# Example of branch definition in git config file:1885# [git-p4]1886# branchList=main:branchA1887# branchList=main:branchB1888# branchList=branchA:branchC1889 configBranches =gitConfigList("git-p4.branchList")1890for branch in configBranches:1891if branch:1892(source, destination) = branch.split(":")1893 self.knownBranches[destination] = source18941895 lostAndFoundBranches.discard(destination)18961897if source not in self.knownBranches:1898 lostAndFoundBranches.add(source)189919001901for branch in lostAndFoundBranches:1902 self.knownBranches[branch] = branch19031904defgetBranchMappingFromGitBranches(self):1905 branches =p4BranchesInGit(self.importIntoRemotes)1906for branch in branches.keys():1907if branch =="master":1908 branch ="main"1909else:1910 branch = branch[len(self.projectName):]1911 self.knownBranches[branch] = branch19121913deflistExistingP4GitBranches(self):1914# branches holds mapping from name to commit1915 branches =p4BranchesInGit(self.importIntoRemotes)1916 self.p4BranchesInGit = branches.keys()1917for branch in branches.keys():1918 self.initialParents[self.refPrefix + branch] = branches[branch]19191920defupdateOptionDict(self, d):1921 option_keys = {}1922if self.keepRepoPath:1923 option_keys['keepRepoPath'] =119241925 d["options"] =' '.join(sorted(option_keys.keys()))19261927defreadOptions(self, d):1928 self.keepRepoPath = (d.has_key('options')1929and('keepRepoPath'in d['options']))19301931defgitRefForBranch(self, branch):1932if branch =="main":1933return self.refPrefix +"master"19341935iflen(branch) <=0:1936return branch19371938return self.refPrefix + self.projectName + branch19391940defgitCommitByP4Change(self, ref, change):1941if self.verbose:1942print"looking in ref "+ ref +" for change%susing bisect..."% change19431944 earliestCommit =""1945 latestCommit =parseRevision(ref)19461947while True:1948if self.verbose:1949print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)1950 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()1951iflen(next) ==0:1952if self.verbose:1953print"argh"1954return""1955 log =extractLogMessageFromGitCommit(next)1956 settings =extractSettingsGitLog(log)1957 currentChange =int(settings['change'])1958if self.verbose:1959print"current change%s"% currentChange19601961if currentChange == change:1962if self.verbose:1963print"found%s"% next1964return next19651966if currentChange < change:1967 earliestCommit ="^%s"% next1968else:1969 latestCommit ="%s"% next19701971return""19721973defimportNewBranch(self, branch, maxChange):1974# make fast-import flush all changes to disk and update the refs using the checkpoint1975# command so that we can try to find the branch parent in the git history1976 self.gitStream.write("checkpoint\n\n");1977 self.gitStream.flush();1978 branchPrefix = self.depotPaths[0] + branch +"/"1979range="@1,%s"% maxChange1980#print "prefix" + branchPrefix1981 changes =p4ChangesForPaths([branchPrefix],range)1982iflen(changes) <=0:1983return False1984 firstChange = changes[0]1985#print "first change in branch: %s" % firstChange1986 sourceBranch = self.knownBranches[branch]1987 sourceDepotPath = self.depotPaths[0] + sourceBranch1988 sourceRef = self.gitRefForBranch(sourceBranch)1989#print "source " + sourceBranch19901991 branchParentChange =int(p4Cmd("changes -m 1%s...@1,%s"% (sourceDepotPath, firstChange))["change"])1992#print "branch parent: %s" % branchParentChange1993 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)1994iflen(gitParent) >0:1995 self.initialParents[self.gitRefForBranch(branch)] = gitParent1996#print "parent git commit: %s" % gitParent19971998 self.importChanges(changes)1999return True20002001defimportChanges(self, changes):2002 cnt =12003for change in changes:2004 description =p4Cmd("describe%s"% change)2005 self.updateOptionDict(description)20062007if not self.silent:2008 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2009 sys.stdout.flush()2010 cnt = cnt +120112012try:2013if self.detectBranches:2014 branches = self.splitFilesIntoBranches(description)2015for branch in branches.keys():2016## HACK --hwn2017 branchPrefix = self.depotPaths[0] + branch +"/"20182019 parent =""20202021 filesForCommit = branches[branch]20222023if self.verbose:2024print"branch is%s"% branch20252026 self.updatedBranches.add(branch)20272028if branch not in self.createdBranches:2029 self.createdBranches.add(branch)2030 parent = self.knownBranches[branch]2031if parent == branch:2032 parent =""2033else:2034 fullBranch = self.projectName + branch2035if fullBranch not in self.p4BranchesInGit:2036if not self.silent:2037print("\nImporting new branch%s"% fullBranch);2038if self.importNewBranch(branch, change -1):2039 parent =""2040 self.p4BranchesInGit.append(fullBranch)2041if not self.silent:2042print("\nResuming with change%s"% change);20432044if self.verbose:2045print"parent determined through known branches:%s"% parent20462047 branch = self.gitRefForBranch(branch)2048 parent = self.gitRefForBranch(parent)20492050if self.verbose:2051print"looking for initial parent for%s; current parent is%s"% (branch, parent)20522053iflen(parent) ==0and branch in self.initialParents:2054 parent = self.initialParents[branch]2055del self.initialParents[branch]20562057 self.commit(description, filesForCommit, branch, [branchPrefix], parent)2058else:2059 files = self.extractFilesFromCommit(description)2060 self.commit(description, files, self.branch, self.depotPaths,2061 self.initialParent)2062 self.initialParent =""2063exceptIOError:2064print self.gitError.read()2065 sys.exit(1)20662067defimportHeadRevision(self, revision):2068print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)20692070 details = {}2071 details["user"] ="git perforce import user"2072 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2073% (' '.join(self.depotPaths), revision))2074 details["change"] = revision2075 newestRevision =020762077 fileCnt =02078 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]20792080for info inp4CmdList(["files"] + fileArgs):20812082if'code'in info and info['code'] =='error':2083 sys.stderr.write("p4 returned an error:%s\n"2084% info['data'])2085if info['data'].find("must refer to client") >=0:2086 sys.stderr.write("This particular p4 error is misleading.\n")2087 sys.stderr.write("Perhaps the depot path was misspelled.\n");2088 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2089 sys.exit(1)2090if'p4ExitCode'in info:2091 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2092 sys.exit(1)209320942095 change =int(info["change"])2096if change > newestRevision:2097 newestRevision = change20982099if info["action"]in self.delete_actions:2100# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2101#fileCnt = fileCnt + 12102continue21032104for prop in["depotFile","rev","action","type"]:2105 details["%s%s"% (prop, fileCnt)] = info[prop]21062107 fileCnt = fileCnt +121082109 details["change"] = newestRevision21102111# Use time from top-most change so that all git-p4 clones of2112# the same p4 repo have the same commit SHA1s.2113 res =p4CmdList("describe -s%d"% newestRevision)2114 newestTime =None2115for r in res:2116if r.has_key('time'):2117 newestTime =int(r['time'])2118if newestTime is None:2119die("\"describe -s\"on newest change%ddid not give a time")2120 details["time"] = newestTime21212122 self.updateOptionDict(details)2123try:2124 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)2125exceptIOError:2126print"IO error with git fast-import. Is your git version recent enough?"2127print self.gitError.read()212821292130defrun(self, args):2131 self.depotPaths = []2132 self.changeRange =""2133 self.initialParent =""2134 self.previousDepotPaths = []21352136# map from branch depot path to parent branch2137 self.knownBranches = {}2138 self.initialParents = {}2139 self.hasOrigin =originP4BranchesExist()2140if not self.syncWithOrigin:2141 self.hasOrigin =False21422143if self.importIntoRemotes:2144 self.refPrefix ="refs/remotes/p4/"2145else:2146 self.refPrefix ="refs/heads/p4/"21472148if self.syncWithOrigin and self.hasOrigin:2149if not self.silent:2150print"Syncing with origin first by calling git fetch origin"2151system("git fetch origin")21522153iflen(self.branch) ==0:2154 self.branch = self.refPrefix +"master"2155ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2156system("git update-ref%srefs/heads/p4"% self.branch)2157system("git branch -D p4");2158# create it /after/ importing, when master exists2159if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2160system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))21612162# accept either the command-line option, or the configuration variable2163if self.useClientSpec:2164# will use this after clone to set the variable2165 self.useClientSpec_from_options =True2166else:2167ifgitConfig("git-p4.useclientspec","--bool") =="true":2168 self.useClientSpec =True2169if self.useClientSpec:2170 self.clientSpecDirs =getClientSpec()21712172# TODO: should always look at previous commits,2173# merge with previous imports, if possible.2174if args == []:2175if self.hasOrigin:2176createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2177 self.listExistingP4GitBranches()21782179iflen(self.p4BranchesInGit) >1:2180if not self.silent:2181print"Importing from/into multiple branches"2182 self.detectBranches =True21832184if self.verbose:2185print"branches:%s"% self.p4BranchesInGit21862187 p4Change =02188for branch in self.p4BranchesInGit:2189 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)21902191 settings =extractSettingsGitLog(logMsg)21922193 self.readOptions(settings)2194if(settings.has_key('depot-paths')2195and settings.has_key('change')):2196 change =int(settings['change']) +12197 p4Change =max(p4Change, change)21982199 depotPaths =sorted(settings['depot-paths'])2200if self.previousDepotPaths == []:2201 self.previousDepotPaths = depotPaths2202else:2203 paths = []2204for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2205 prev_list = prev.split("/")2206 cur_list = cur.split("/")2207for i inrange(0,min(len(cur_list),len(prev_list))):2208if cur_list[i] <> prev_list[i]:2209 i = i -12210break22112212 paths.append("/".join(cur_list[:i +1]))22132214 self.previousDepotPaths = paths22152216if p4Change >0:2217 self.depotPaths =sorted(self.previousDepotPaths)2218 self.changeRange ="@%s,#head"% p4Change2219if not self.detectBranches:2220 self.initialParent =parseRevision(self.branch)2221if not self.silent and not self.detectBranches:2222print"Performing incremental import into%sgit branch"% self.branch22232224if not self.branch.startswith("refs/"):2225 self.branch ="refs/heads/"+ self.branch22262227iflen(args) ==0and self.depotPaths:2228if not self.silent:2229print"Depot paths:%s"%' '.join(self.depotPaths)2230else:2231if self.depotPaths and self.depotPaths != args:2232print("previous import used depot path%sand now%swas specified. "2233"This doesn't work!"% (' '.join(self.depotPaths),2234' '.join(args)))2235 sys.exit(1)22362237 self.depotPaths =sorted(args)22382239 revision =""2240 self.users = {}22412242# Make sure no revision specifiers are used when --changesfile2243# is specified.2244 bad_changesfile =False2245iflen(self.changesFile) >0:2246for p in self.depotPaths:2247if p.find("@") >=0or p.find("#") >=0:2248 bad_changesfile =True2249break2250if bad_changesfile:2251die("Option --changesfile is incompatible with revision specifiers")22522253 newPaths = []2254for p in self.depotPaths:2255if p.find("@") != -1:2256 atIdx = p.index("@")2257 self.changeRange = p[atIdx:]2258if self.changeRange =="@all":2259 self.changeRange =""2260elif','not in self.changeRange:2261 revision = self.changeRange2262 self.changeRange =""2263 p = p[:atIdx]2264elif p.find("#") != -1:2265 hashIdx = p.index("#")2266 revision = p[hashIdx:]2267 p = p[:hashIdx]2268elif self.previousDepotPaths == []:2269# pay attention to changesfile, if given, else import2270# the entire p4 tree at the head revision2271iflen(self.changesFile) ==0:2272 revision ="#head"22732274 p = re.sub("\.\.\.$","", p)2275if not p.endswith("/"):2276 p +="/"22772278 newPaths.append(p)22792280 self.depotPaths = newPaths228122822283 self.loadUserMapFromCache()2284 self.labels = {}2285if self.detectLabels:2286 self.getLabels();22872288if self.detectBranches:2289## FIXME - what's a P4 projectName ?2290 self.projectName = self.guessProjectName()22912292if self.hasOrigin:2293 self.getBranchMappingFromGitBranches()2294else:2295 self.getBranchMapping()2296if self.verbose:2297print"p4-git branches:%s"% self.p4BranchesInGit2298print"initial parents:%s"% self.initialParents2299for b in self.p4BranchesInGit:2300if b !="master":23012302## FIXME2303 b = b[len(self.projectName):]2304 self.createdBranches.add(b)23052306 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))23072308 importProcess = subprocess.Popen(["git","fast-import"],2309 stdin=subprocess.PIPE, stdout=subprocess.PIPE,2310 stderr=subprocess.PIPE);2311 self.gitOutput = importProcess.stdout2312 self.gitStream = importProcess.stdin2313 self.gitError = importProcess.stderr23142315if revision:2316 self.importHeadRevision(revision)2317else:2318 changes = []23192320iflen(self.changesFile) >0:2321 output =open(self.changesFile).readlines()2322 changeSet =set()2323for line in output:2324 changeSet.add(int(line))23252326for change in changeSet:2327 changes.append(change)23282329 changes.sort()2330else:2331# catch "git-p4 sync" with no new branches, in a repo that2332# does not have any existing git-p4 branches2333iflen(args) ==0and not self.p4BranchesInGit:2334die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2335if self.verbose:2336print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2337 self.changeRange)2338 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)23392340iflen(self.maxChanges) >0:2341 changes = changes[:min(int(self.maxChanges),len(changes))]23422343iflen(changes) ==0:2344if not self.silent:2345print"No changes to import!"2346return True23472348if not self.silent and not self.detectBranches:2349print"Import destination:%s"% self.branch23502351 self.updatedBranches =set()23522353 self.importChanges(changes)23542355if not self.silent:2356print""2357iflen(self.updatedBranches) >0:2358 sys.stdout.write("Updated branches: ")2359for b in self.updatedBranches:2360 sys.stdout.write("%s"% b)2361 sys.stdout.write("\n")23622363 self.gitStream.close()2364if importProcess.wait() !=0:2365die("fast-import failed:%s"% self.gitError.read())2366 self.gitOutput.close()2367 self.gitError.close()23682369return True23702371classP4Rebase(Command):2372def__init__(self):2373 Command.__init__(self)2374 self.options = [ ]2375 self.description = ("Fetches the latest revision from perforce and "2376+"rebases the current work (branch) against it")2377 self.verbose =False23782379defrun(self, args):2380 sync =P4Sync()2381 sync.run([])23822383return self.rebase()23842385defrebase(self):2386if os.system("git update-index --refresh") !=0:2387die("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.");2388iflen(read_pipe("git diff-index HEAD --")) >0:2389die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");23902391[upstream, settings] =findUpstreamBranchPoint()2392iflen(upstream) ==0:2393die("Cannot find upstream branchpoint for rebase")23942395# the branchpoint may be p4/foo~3, so strip off the parent2396 upstream = re.sub("~[0-9]+$","", upstream)23972398print"Rebasing the current branch onto%s"% upstream2399 oldHead =read_pipe("git rev-parse HEAD").strip()2400system("git rebase%s"% upstream)2401system("git diff-tree --stat --summary -M%sHEAD"% oldHead)2402return True24032404classP4Clone(P4Sync):2405def__init__(self):2406 P4Sync.__init__(self)2407 self.description ="Creates a new git repository and imports from Perforce into it"2408 self.usage ="usage: %prog [options] //depot/path[@revRange]"2409 self.options += [2410 optparse.make_option("--destination", dest="cloneDestination",2411 action='store', default=None,2412help="where to leave result of the clone"),2413 optparse.make_option("-/", dest="cloneExclude",2414 action="append",type="string",2415help="exclude depot path"),2416 optparse.make_option("--bare", dest="cloneBare",2417 action="store_true", default=False),2418]2419 self.cloneDestination =None2420 self.needsGit =False2421 self.cloneBare =False24222423# This is required for the "append" cloneExclude action2424defensure_value(self, attr, value):2425if nothasattr(self, attr)orgetattr(self, attr)is None:2426setattr(self, attr, value)2427returngetattr(self, attr)24282429defdefaultDestination(self, args):2430## TODO: use common prefix of args?2431 depotPath = args[0]2432 depotDir = re.sub("(@[^@]*)$","", depotPath)2433 depotDir = re.sub("(#[^#]*)$","", depotDir)2434 depotDir = re.sub(r"\.\.\.$","", depotDir)2435 depotDir = re.sub(r"/$","", depotDir)2436return os.path.split(depotDir)[1]24372438defrun(self, args):2439iflen(args) <1:2440return False24412442if self.keepRepoPath and not self.cloneDestination:2443 sys.stderr.write("Must specify destination for --keep-path\n")2444 sys.exit(1)24452446 depotPaths = args24472448if not self.cloneDestination andlen(depotPaths) >1:2449 self.cloneDestination = depotPaths[-1]2450 depotPaths = depotPaths[:-1]24512452 self.cloneExclude = ["/"+p for p in self.cloneExclude]2453for p in depotPaths:2454if not p.startswith("//"):2455return False24562457if not self.cloneDestination:2458 self.cloneDestination = self.defaultDestination(args)24592460print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)24612462if not os.path.exists(self.cloneDestination):2463 os.makedirs(self.cloneDestination)2464chdir(self.cloneDestination)24652466 init_cmd = ["git","init"]2467if self.cloneBare:2468 init_cmd.append("--bare")2469 subprocess.check_call(init_cmd)24702471if not P4Sync.run(self, depotPaths):2472return False2473if self.branch !="master":2474if self.importIntoRemotes:2475 masterbranch ="refs/remotes/p4/master"2476else:2477 masterbranch ="refs/heads/p4/master"2478ifgitBranchExists(masterbranch):2479system("git branch master%s"% masterbranch)2480if not self.cloneBare:2481system("git checkout -f")2482else:2483print"Could not detect main branch. No checkout/master branch created."24842485# auto-set this variable if invoked with --use-client-spec2486if self.useClientSpec_from_options:2487system("git config --bool git-p4.useclientspec true")24882489return True24902491classP4Branches(Command):2492def__init__(self):2493 Command.__init__(self)2494 self.options = [ ]2495 self.description = ("Shows the git branches that hold imports and their "2496+"corresponding perforce depot paths")2497 self.verbose =False24982499defrun(self, args):2500iforiginP4BranchesExist():2501createOrUpdateBranchesFromOrigin()25022503 cmdline ="git rev-parse --symbolic "2504 cmdline +=" --remotes"25052506for line inread_pipe_lines(cmdline):2507 line = line.strip()25082509if not line.startswith('p4/')or line =="p4/HEAD":2510continue2511 branch = line25122513 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)2514 settings =extractSettingsGitLog(log)25152516print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])2517return True25182519classHelpFormatter(optparse.IndentedHelpFormatter):2520def__init__(self):2521 optparse.IndentedHelpFormatter.__init__(self)25222523defformat_description(self, description):2524if description:2525return description +"\n"2526else:2527return""25282529defprintUsage(commands):2530print"usage:%s<command> [options]"% sys.argv[0]2531print""2532print"valid commands:%s"%", ".join(commands)2533print""2534print"Try%s<command> --help for command specific help."% sys.argv[0]2535print""25362537commands = {2538"debug": P4Debug,2539"submit": P4Submit,2540"commit": P4Submit,2541"sync": P4Sync,2542"rebase": P4Rebase,2543"clone": P4Clone,2544"rollback": P4RollBack,2545"branches": P4Branches2546}254725482549defmain():2550iflen(sys.argv[1:]) ==0:2551printUsage(commands.keys())2552 sys.exit(2)25532554 cmd =""2555 cmdName = sys.argv[1]2556try:2557 klass = commands[cmdName]2558 cmd =klass()2559exceptKeyError:2560print"unknown command%s"% cmdName2561print""2562printUsage(commands.keys())2563 sys.exit(2)25642565 options = cmd.options2566 cmd.gitdir = os.environ.get("GIT_DIR",None)25672568 args = sys.argv[2:]25692570iflen(options) >0:2571if cmd.needsGit:2572 options.append(optparse.make_option("--git-dir", dest="gitdir"))25732574 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),2575 options,2576 description = cmd.description,2577 formatter =HelpFormatter())25782579(cmd, args) = parser.parse_args(sys.argv[2:], cmd);2580global verbose2581 verbose = cmd.verbose2582if cmd.needsGit:2583if cmd.gitdir ==None:2584 cmd.gitdir = os.path.abspath(".git")2585if notisValidGitDir(cmd.gitdir):2586 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()2587if os.path.exists(cmd.gitdir):2588 cdup =read_pipe("git rev-parse --show-cdup").strip()2589iflen(cdup) >0:2590chdir(cdup);25912592if notisValidGitDir(cmd.gitdir):2593ifisValidGitDir(cmd.gitdir +"/.git"):2594 cmd.gitdir +="/.git"2595else:2596die("fatal: cannot locate git repository at%s"% cmd.gitdir)25972598 os.environ["GIT_DIR"] = cmd.gitdir25992600if not cmd.run(args):2601 parser.print_help()2602 sys.exit(2)260326042605if __name__ =='__main__':2606main()