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 558class Command: 559def__init__(self): 560 self.usage ="usage: %prog [options]" 561 self.needsGit =True 562 563class P4UserMap: 564def__init__(self): 565 self.userMapFromPerforceServer =False 566 self.myP4UserId =None 567 568defp4UserId(self): 569if self.myP4UserId: 570return self.myP4UserId 571 572 results =p4CmdList("user -o") 573for r in results: 574if r.has_key('User'): 575 self.myP4UserId = r['User'] 576return r['User'] 577die("Could not find your p4 user id") 578 579defp4UserIsMe(self, p4User): 580# return True if the given p4 user is actually me 581 me = self.p4UserId() 582if not p4User or p4User != me: 583return False 584else: 585return True 586 587defgetUserCacheFilename(self): 588 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 589return home +"/.gitp4-usercache.txt" 590 591defgetUserMapFromPerforceServer(self): 592if self.userMapFromPerforceServer: 593return 594 self.users = {} 595 self.emails = {} 596 597for output inp4CmdList("users"): 598if not output.has_key("User"): 599continue 600 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 601 self.emails[output["Email"]] = output["User"] 602 603 604 s ='' 605for(key, val)in self.users.items(): 606 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 607 608open(self.getUserCacheFilename(),"wb").write(s) 609 self.userMapFromPerforceServer =True 610 611defloadUserMapFromCache(self): 612 self.users = {} 613 self.userMapFromPerforceServer =False 614try: 615 cache =open(self.getUserCacheFilename(),"rb") 616 lines = cache.readlines() 617 cache.close() 618for line in lines: 619 entry = line.strip().split("\t") 620 self.users[entry[0]] = entry[1] 621exceptIOError: 622 self.getUserMapFromPerforceServer() 623 624classP4Debug(Command): 625def__init__(self): 626 Command.__init__(self) 627 self.options = [ 628 optparse.make_option("--verbose", dest="verbose", action="store_true", 629 default=False), 630] 631 self.description ="A tool to debug the output of p4 -G." 632 self.needsGit =False 633 self.verbose =False 634 635defrun(self, args): 636 j =0 637for output inp4CmdList(args): 638print'Element:%d'% j 639 j +=1 640print output 641return True 642 643classP4RollBack(Command): 644def__init__(self): 645 Command.__init__(self) 646 self.options = [ 647 optparse.make_option("--verbose", dest="verbose", action="store_true"), 648 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 649] 650 self.description ="A tool to debug the multi-branch import. Don't use :)" 651 self.verbose =False 652 self.rollbackLocalBranches =False 653 654defrun(self, args): 655iflen(args) !=1: 656return False 657 maxChange =int(args[0]) 658 659if"p4ExitCode"inp4Cmd("changes -m 1"): 660die("Problems executing p4"); 661 662if self.rollbackLocalBranches: 663 refPrefix ="refs/heads/" 664 lines =read_pipe_lines("git rev-parse --symbolic --branches") 665else: 666 refPrefix ="refs/remotes/" 667 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 668 669for line in lines: 670if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 671 line = line.strip() 672 ref = refPrefix + line 673 log =extractLogMessageFromGitCommit(ref) 674 settings =extractSettingsGitLog(log) 675 676 depotPaths = settings['depot-paths'] 677 change = settings['change'] 678 679 changed =False 680 681iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 682for p in depotPaths]))) ==0: 683print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 684system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 685continue 686 687while change andint(change) > maxChange: 688 changed =True 689if self.verbose: 690print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 691system("git update-ref%s\"%s^\""% (ref, ref)) 692 log =extractLogMessageFromGitCommit(ref) 693 settings =extractSettingsGitLog(log) 694 695 696 depotPaths = settings['depot-paths'] 697 change = settings['change'] 698 699if changed: 700print"%srewound to%s"% (ref, change) 701 702return True 703 704classP4Submit(Command, P4UserMap): 705def__init__(self): 706 Command.__init__(self) 707 P4UserMap.__init__(self) 708 self.options = [ 709 optparse.make_option("--verbose", dest="verbose", action="store_true"), 710 optparse.make_option("--origin", dest="origin"), 711 optparse.make_option("-M", dest="detectRenames", action="store_true"), 712# preserve the user, requires relevant p4 permissions 713 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 714] 715 self.description ="Submit changes from git to the perforce depot." 716 self.usage +=" [name of git branch to submit into perforce depot]" 717 self.interactive =True 718 self.origin ="" 719 self.detectRenames =False 720 self.verbose =False 721 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 722 self.isWindows = (platform.system() =="Windows") 723 724defcheck(self): 725iflen(p4CmdList("opened ...")) >0: 726die("You have files opened with perforce! Close them before starting the sync.") 727 728# replaces everything between 'Description:' and the next P4 submit template field with the 729# commit message 730defprepareLogMessage(self, template, message): 731 result ="" 732 733 inDescriptionSection =False 734 735for line in template.split("\n"): 736if line.startswith("#"): 737 result += line +"\n" 738continue 739 740if inDescriptionSection: 741if line.startswith("Files:")or line.startswith("Jobs:"): 742 inDescriptionSection =False 743else: 744continue 745else: 746if line.startswith("Description:"): 747 inDescriptionSection =True 748 line +="\n" 749for messageLine in message.split("\n"): 750 line +="\t"+ messageLine +"\n" 751 752 result += line +"\n" 753 754return result 755 756defp4UserForCommit(self,id): 757# Return the tuple (perforce user,git email) for a given git commit id 758 self.getUserMapFromPerforceServer() 759 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id) 760 gitEmail = gitEmail.strip() 761if not self.emails.has_key(gitEmail): 762return(None,gitEmail) 763else: 764return(self.emails[gitEmail],gitEmail) 765 766defcheckValidP4Users(self,commits): 767# check if any git authors cannot be mapped to p4 users 768foridin commits: 769(user,email) = self.p4UserForCommit(id) 770if not user: 771 msg ="Cannot find p4 user for email%sin commit%s."% (email,id) 772ifgitConfig('git-p4.allowMissingP4Users').lower() =="true": 773print"%s"% msg 774else: 775die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg) 776 777deflastP4Changelist(self): 778# Get back the last changelist number submitted in this client spec. This 779# then gets used to patch up the username in the change. If the same 780# client spec is being used by multiple processes then this might go 781# wrong. 782 results =p4CmdList("client -o")# find the current client 783 client =None 784for r in results: 785if r.has_key('Client'): 786 client = r['Client'] 787break 788if not client: 789die("could not get client spec") 790 results =p4CmdList(["changes","-c", client,"-m","1"]) 791for r in results: 792if r.has_key('change'): 793return r['change'] 794die("Could not get changelist number for last submit - cannot patch up user details") 795 796defmodifyChangelistUser(self, changelist, newUser): 797# fixup the user field of a changelist after it has been submitted. 798 changes =p4CmdList("change -o%s"% changelist) 799iflen(changes) !=1: 800die("Bad output from p4 change modifying%sto user%s"% 801(changelist, newUser)) 802 803 c = changes[0] 804if c['User'] == newUser:return# nothing to do 805 c['User'] = newUser 806input= marshal.dumps(c) 807 808 result =p4CmdList("change -f -i", stdin=input) 809for r in result: 810if r.has_key('code'): 811if r['code'] =='error': 812die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data'])) 813if r.has_key('data'): 814print("Updated user field for changelist%sto%s"% (changelist, newUser)) 815return 816die("Could not modify user field of changelist%sto%s"% (changelist, newUser)) 817 818defcanChangeChangelists(self): 819# check to see if we have p4 admin or super-user permissions, either of 820# which are required to modify changelists. 821 results =p4CmdList(["protects", self.depotPath]) 822for r in results: 823if r.has_key('perm'): 824if r['perm'] =='admin': 825return1 826if r['perm'] =='super': 827return1 828return0 829 830defprepareSubmitTemplate(self): 831# remove lines in the Files section that show changes to files outside the depot path we're committing into 832 template ="" 833 inFilesSection =False 834for line inp4_read_pipe_lines(['change','-o']): 835if line.endswith("\r\n"): 836 line = line[:-2] +"\n" 837if inFilesSection: 838if line.startswith("\t"): 839# path starts and ends with a tab 840 path = line[1:] 841 lastTab = path.rfind("\t") 842if lastTab != -1: 843 path = path[:lastTab] 844if notp4PathStartsWith(path, self.depotPath): 845continue 846else: 847 inFilesSection =False 848else: 849if line.startswith("Files:"): 850 inFilesSection =True 851 852 template += line 853 854return template 855 856defedit_template(self, template_file): 857"""Invoke the editor to let the user change the submission 858 message. Return true if okay to continue with the submit.""" 859 860# if configured to skip the editing part, just submit 861ifgitConfig("git-p4.skipSubmitEdit") =="true": 862return True 863 864# look at the modification time, to check later if the user saved 865# the file 866 mtime = os.stat(template_file).st_mtime 867 868# invoke the editor 869if os.environ.has_key("P4EDITOR"): 870 editor = os.environ.get("P4EDITOR") 871else: 872 editor =read_pipe("git var GIT_EDITOR").strip() 873system(editor +" "+ template_file) 874 875# If the file was not saved, prompt to see if this patch should 876# be skipped. But skip this verification step if configured so. 877ifgitConfig("git-p4.skipSubmitEditCheck") =="true": 878return True 879 880# modification time updated means user saved the file 881if os.stat(template_file).st_mtime > mtime: 882return True 883 884while True: 885 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ") 886if response =='y': 887return True 888if response =='n': 889return False 890 891defapplyCommit(self,id): 892print"Applying%s"% (read_pipe("git log --max-count=1 --pretty=oneline%s"%id)) 893 894(p4User, gitEmail) = self.p4UserForCommit(id) 895 896if not self.detectRenames: 897# If not explicitly set check the config variable 898 self.detectRenames =gitConfig("git-p4.detectRenames") 899 900if self.detectRenames.lower() =="false"or self.detectRenames =="": 901 diffOpts ="" 902elif self.detectRenames.lower() =="true": 903 diffOpts ="-M" 904else: 905 diffOpts ="-M%s"% self.detectRenames 906 907 detectCopies =gitConfig("git-p4.detectCopies") 908if detectCopies.lower() =="true": 909 diffOpts +=" -C" 910elif detectCopies !=""and detectCopies.lower() !="false": 911 diffOpts +=" -C%s"% detectCopies 912 913ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true": 914 diffOpts +=" --find-copies-harder" 915 916 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (diffOpts,id,id)) 917 filesToAdd =set() 918 filesToDelete =set() 919 editedFiles =set() 920 filesToChangeExecBit = {} 921for line in diff: 922 diff =parseDiffTreeEntry(line) 923 modifier = diff['status'] 924 path = diff['src'] 925if modifier =="M": 926p4_edit(path) 927ifisModeExecChanged(diff['src_mode'], diff['dst_mode']): 928 filesToChangeExecBit[path] = diff['dst_mode'] 929 editedFiles.add(path) 930elif modifier =="A": 931 filesToAdd.add(path) 932 filesToChangeExecBit[path] = diff['dst_mode'] 933if path in filesToDelete: 934 filesToDelete.remove(path) 935elif modifier =="D": 936 filesToDelete.add(path) 937if path in filesToAdd: 938 filesToAdd.remove(path) 939elif modifier =="C": 940 src, dest = diff['src'], diff['dst'] 941p4_integrate(src, dest) 942if diff['src_sha1'] != diff['dst_sha1']: 943p4_edit(dest) 944ifisModeExecChanged(diff['src_mode'], diff['dst_mode']): 945p4_edit(dest) 946 filesToChangeExecBit[dest] = diff['dst_mode'] 947 os.unlink(dest) 948 editedFiles.add(dest) 949elif modifier =="R": 950 src, dest = diff['src'], diff['dst'] 951p4_integrate(src, dest) 952if diff['src_sha1'] != diff['dst_sha1']: 953p4_edit(dest) 954ifisModeExecChanged(diff['src_mode'], diff['dst_mode']): 955p4_edit(dest) 956 filesToChangeExecBit[dest] = diff['dst_mode'] 957 os.unlink(dest) 958 editedFiles.add(dest) 959 filesToDelete.add(src) 960else: 961die("unknown modifier%sfor%s"% (modifier, path)) 962 963 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id) 964 patchcmd = diffcmd +" | git apply " 965 tryPatchCmd = patchcmd +"--check -" 966 applyPatchCmd = patchcmd +"--check --apply -" 967 968if os.system(tryPatchCmd) !=0: 969print"Unfortunately applying the change failed!" 970print"What do you want to do?" 971 response ="x" 972while response !="s"and response !="a"and response !="w": 973 response =raw_input("[s]kip this patch / [a]pply the patch forcibly " 974"and with .rej files / [w]rite the patch to a file (patch.txt) ") 975if response =="s": 976print"Skipping! Good luck with the next patches..." 977for f in editedFiles: 978p4_revert(f) 979for f in filesToAdd: 980 os.remove(f) 981return 982elif response =="a": 983 os.system(applyPatchCmd) 984iflen(filesToAdd) >0: 985print"You may also want to call p4 add on the following files:" 986print" ".join(filesToAdd) 987iflen(filesToDelete): 988print"The following files should be scheduled for deletion with p4 delete:" 989print" ".join(filesToDelete) 990die("Please resolve and submit the conflict manually and " 991+"continue afterwards with git-p4 submit --continue") 992elif response =="w": 993system(diffcmd +" > patch.txt") 994print"Patch saved to patch.txt in%s!"% self.clientPath 995die("Please resolve and submit the conflict manually and " 996"continue afterwards with git-p4 submit --continue") 997 998system(applyPatchCmd) 9991000for f in filesToAdd:1001p4_add(f)1002for f in filesToDelete:1003p4_revert(f)1004p4_delete(f)10051006# Set/clear executable bits1007for f in filesToChangeExecBit.keys():1008 mode = filesToChangeExecBit[f]1009setP4ExecBit(f, mode)10101011 logMessage =extractLogMessageFromGitCommit(id)1012 logMessage = logMessage.strip()10131014 template = self.prepareSubmitTemplate()10151016if self.interactive:1017 submitTemplate = self.prepareLogMessage(template, logMessage)10181019if self.preserveUser:1020 submitTemplate = submitTemplate + ("\n######## Actual user%s, modified after commit\n"% p4User)10211022if os.environ.has_key("P4DIFF"):1023del(os.environ["P4DIFF"])1024 diff =""1025for editedFile in editedFiles:1026 diff +=p4_read_pipe(['diff','-du', editedFile])10271028 newdiff =""1029for newFile in filesToAdd:1030 newdiff +="==== new file ====\n"1031 newdiff +="--- /dev/null\n"1032 newdiff +="+++%s\n"% newFile1033 f =open(newFile,"r")1034for line in f.readlines():1035 newdiff +="+"+ line1036 f.close()10371038if self.checkAuthorship and not self.p4UserIsMe(p4User):1039 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1040 submitTemplate +="######## Use git-p4 option --preserve-user to modify authorship\n"1041 submitTemplate +="######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"10421043 separatorLine ="######## everything below this line is just the diff #######\n"10441045(handle, fileName) = tempfile.mkstemp()1046 tmpFile = os.fdopen(handle,"w+")1047if self.isWindows:1048 submitTemplate = submitTemplate.replace("\n","\r\n")1049 separatorLine = separatorLine.replace("\n","\r\n")1050 newdiff = newdiff.replace("\n","\r\n")1051 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1052 tmpFile.close()10531054if self.edit_template(fileName):1055# read the edited message and submit1056 tmpFile =open(fileName,"rb")1057 message = tmpFile.read()1058 tmpFile.close()1059 submitTemplate = message[:message.index(separatorLine)]1060if self.isWindows:1061 submitTemplate = submitTemplate.replace("\r\n","\n")1062p4_write_pipe(['submit','-i'], submitTemplate)10631064if self.preserveUser:1065if p4User:1066# Get last changelist number. Cannot easily get it from1067# the submit command output as the output is1068# unmarshalled.1069 changelist = self.lastP4Changelist()1070 self.modifyChangelistUser(changelist, p4User)1071else:1072# skip this patch1073print"Submission cancelled, undoing p4 changes."1074for f in editedFiles:1075p4_revert(f)1076for f in filesToAdd:1077p4_revert(f)1078 os.remove(f)10791080 os.remove(fileName)1081else:1082 fileName ="submit.txt"1083file=open(fileName,"w+")1084file.write(self.prepareLogMessage(template, logMessage))1085file.close()1086print("Perforce submit template written as%s. "1087+"Please review/edit and then use p4 submit -i <%sto submit directly!"1088% (fileName, fileName))10891090defrun(self, args):1091iflen(args) ==0:1092 self.master =currentGitBranch()1093iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1094die("Detecting current git branch failed!")1095eliflen(args) ==1:1096 self.master = args[0]1097if notbranchExists(self.master):1098die("Branch%sdoes not exist"% self.master)1099else:1100return False11011102 allowSubmit =gitConfig("git-p4.allowSubmit")1103iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1104die("%sis not in git-p4.allowSubmit"% self.master)11051106[upstream, settings] =findUpstreamBranchPoint()1107 self.depotPath = settings['depot-paths'][0]1108iflen(self.origin) ==0:1109 self.origin = upstream11101111if self.preserveUser:1112if not self.canChangeChangelists():1113die("Cannot preserve user names without p4 super-user or admin permissions")11141115if self.verbose:1116print"Origin branch is "+ self.origin11171118iflen(self.depotPath) ==0:1119print"Internal error: cannot locate perforce depot path from existing branches"1120 sys.exit(128)11211122 self.clientPath =p4Where(self.depotPath)11231124iflen(self.clientPath) ==0:1125print"Error: Cannot locate perforce checkout of%sin client view"% self.depotPath1126 sys.exit(128)11271128print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1129 self.oldWorkingDirectory = os.getcwd()11301131# ensure the clientPath exists1132if not os.path.exists(self.clientPath):1133 os.makedirs(self.clientPath)11341135chdir(self.clientPath)1136print"Synchronizing p4 checkout..."1137p4_sync("...")1138 self.check()11391140 commits = []1141for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1142 commits.append(line.strip())1143 commits.reverse()11441145if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1146 self.checkAuthorship =False1147else:1148 self.checkAuthorship =True11491150if self.preserveUser:1151 self.checkValidP4Users(commits)11521153whilelen(commits) >0:1154 commit = commits[0]1155 commits = commits[1:]1156 self.applyCommit(commit)1157if not self.interactive:1158break11591160iflen(commits) ==0:1161print"All changes applied!"1162chdir(self.oldWorkingDirectory)11631164 sync =P4Sync()1165 sync.run([])11661167 rebase =P4Rebase()1168 rebase.rebase()11691170return True11711172classView(object):1173"""Represent a p4 view ("p4 help views"), and map files in a1174 repo according to the view."""11751176classPath(object):1177"""A depot or client path, possibly containing wildcards.1178 The only one supported is ... at the end, currently.1179 Initialize with the full path, with //depot or //client."""11801181def__init__(self, path, is_depot):1182 self.path = path1183 self.is_depot = is_depot1184 self.find_wildcards()1185# remember the prefix bit, useful for relative mappings1186 m = re.match("(//[^/]+/)", self.path)1187if not m:1188die("Path%sdoes not start with //prefix/"% self.path)1189 prefix = m.group(1)1190if not self.is_depot:1191# strip //client/ on client paths1192 self.path = self.path[len(prefix):]11931194deffind_wildcards(self):1195"""Make sure wildcards are valid, and set up internal1196 variables."""11971198 self.ends_triple_dot =False1199# There are three wildcards allowed in p4 views1200# (see "p4 help views"). This code knows how to1201# handle "..." (only at the end), but cannot deal with1202# "%%n" or "*". Only check the depot_side, as p4 should1203# validate that the client_side matches too.1204if re.search(r'%%[1-9]', self.path):1205die("Can't handle%%n wildcards in view:%s"% self.path)1206if self.path.find("*") >=0:1207die("Can't handle * wildcards in view:%s"% self.path)1208 triple_dot_index = self.path.find("...")1209if triple_dot_index >=0:1210if triple_dot_index !=len(self.path) -3:1211die("Can handle only single ... wildcard, at end:%s"%1212 self.path)1213 self.ends_triple_dot =True12141215defensure_compatible(self, other_path):1216"""Make sure the wildcards agree."""1217if self.ends_triple_dot != other_path.ends_triple_dot:1218die("Both paths must end with ... if either does;\n"+1219"paths:%s %s"% (self.path, other_path.path))12201221defmatch_wildcards(self, test_path):1222"""See if this test_path matches us, and fill in the value1223 of the wildcards if so. Returns a tuple of1224 (True|False, wildcards[]). For now, only the ... at end1225 is supported, so at most one wildcard."""1226if self.ends_triple_dot:1227 dotless = self.path[:-3]1228if test_path.startswith(dotless):1229 wildcard = test_path[len(dotless):]1230return(True, [ wildcard ])1231else:1232if test_path == self.path:1233return(True, [])1234return(False, [])12351236defmatch(self, test_path):1237"""Just return if it matches; don't bother with the wildcards."""1238 b, _ = self.match_wildcards(test_path)1239return b12401241deffill_in_wildcards(self, wildcards):1242"""Return the relative path, with the wildcards filled in1243 if there are any."""1244if self.ends_triple_dot:1245return self.path[:-3] + wildcards[0]1246else:1247return self.path12481249classMapping(object):1250def__init__(self, depot_side, client_side, overlay, exclude):1251# depot_side is without the trailing /... if it had one1252 self.depot_side = View.Path(depot_side, is_depot=True)1253 self.client_side = View.Path(client_side, is_depot=False)1254 self.overlay = overlay # started with "+"1255 self.exclude = exclude # started with "-"1256assert not(self.overlay and self.exclude)1257 self.depot_side.ensure_compatible(self.client_side)12581259def__str__(self):1260 c =" "1261if self.overlay:1262 c ="+"1263if self.exclude:1264 c ="-"1265return"View.Mapping:%s%s->%s"% \1266(c, self.depot_side.path, self.client_side.path)12671268defmap_depot_to_client(self, depot_path):1269"""Calculate the client path if using this mapping on the1270 given depot path; does not consider the effect of other1271 mappings in a view. Even excluded mappings are returned."""1272 matches, wildcards = self.depot_side.match_wildcards(depot_path)1273if not matches:1274return""1275 client_path = self.client_side.fill_in_wildcards(wildcards)1276return client_path12771278#1279# View methods1280#1281def__init__(self):1282 self.mappings = []12831284defappend(self, view_line):1285"""Parse a view line, splitting it into depot and client1286 sides. Append to self.mappings, preserving order."""12871288# Split the view line into exactly two words. P4 enforces1289# structure on these lines that simplifies this quite a bit.1290#1291# Either or both words may be double-quoted.1292# Single quotes do not matter.1293# Double-quote marks cannot occur inside the words.1294# A + or - prefix is also inside the quotes.1295# There are no quotes unless they contain a space.1296# The line is already white-space stripped.1297# The two words are separated by a single space.1298#1299if view_line[0] =='"':1300# First word is double quoted. Find its end.1301 close_quote_index = view_line.find('"',1)1302if close_quote_index <=0:1303die("No first-word closing quote found:%s"% view_line)1304 depot_side = view_line[1:close_quote_index]1305# skip closing quote and space1306 rhs_index = close_quote_index +1+11307else:1308 space_index = view_line.find(" ")1309if space_index <=0:1310die("No word-splitting space found:%s"% view_line)1311 depot_side = view_line[0:space_index]1312 rhs_index = space_index +113131314if view_line[rhs_index] =='"':1315# Second word is double quoted. Make sure there is a1316# double quote at the end too.1317if not view_line.endswith('"'):1318die("View line with rhs quote should end with one:%s"%1319 view_line)1320# skip the quotes1321 client_side = view_line[rhs_index+1:-1]1322else:1323 client_side = view_line[rhs_index:]13241325# prefix + means overlay on previous mapping1326 overlay =False1327if depot_side.startswith("+"):1328 overlay =True1329 depot_side = depot_side[1:]13301331# prefix - means exclude this path1332 exclude =False1333if depot_side.startswith("-"):1334 exclude =True1335 depot_side = depot_side[1:]13361337 m = View.Mapping(depot_side, client_side, overlay, exclude)1338 self.mappings.append(m)13391340defmap_in_client(self, depot_path):1341"""Return the relative location in the client where this1342 depot file should live. Returns "" if the file should1343 not be mapped in the client."""13441345 paths_filled = []1346 client_path =""13471348# look at later entries first1349for m in self.mappings[::-1]:13501351# see where will this path end up in the client1352 p = m.map_depot_to_client(depot_path)13531354if p =="":1355# Depot path does not belong in client. Must remember1356# this, as previous items should not cause files to1357# exist in this path either. Remember that the list is1358# being walked from the end, which has higher precedence.1359# Overlap mappings do not exclude previous mappings.1360if not m.overlay:1361 paths_filled.append(m.client_side)13621363else:1364# This mapping matched; no need to search any further.1365# But, the mapping could be rejected if the client path1366# has already been claimed by an earlier mapping (i.e.1367# one later in the list, which we are walking backwards).1368 already_mapped_in_client =False1369for f in paths_filled:1370# this is View.Path.match1371if f.match(p):1372 already_mapped_in_client =True1373break1374if not already_mapped_in_client:1375# Include this file, unless it is from a line that1376# explicitly said to exclude it.1377if not m.exclude:1378 client_path = p13791380# a match, even if rejected, always stops the search1381break13821383return client_path13841385classP4Sync(Command, P4UserMap):1386 delete_actions = ("delete","move/delete","purge")13871388def__init__(self):1389 Command.__init__(self)1390 P4UserMap.__init__(self)1391 self.options = [1392 optparse.make_option("--branch", dest="branch"),1393 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1394 optparse.make_option("--changesfile", dest="changesFile"),1395 optparse.make_option("--silent", dest="silent", action="store_true"),1396 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1397 optparse.make_option("--verbose", dest="verbose", action="store_true"),1398 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1399help="Import into refs/heads/ , not refs/remotes"),1400 optparse.make_option("--max-changes", dest="maxChanges"),1401 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1402help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1403 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1404help="Only sync files that are included in the Perforce Client Spec")1405]1406 self.description ="""Imports from Perforce into a git repository.\n1407 example:1408 //depot/my/project/ -- to import the current head1409 //depot/my/project/@all -- to import everything1410 //depot/my/project/@1,6 -- to import only from revision 1 to 614111412 (a ... is not needed in the path p4 specification, it's added implicitly)"""14131414 self.usage +=" //depot/path[@revRange]"1415 self.silent =False1416 self.createdBranches =set()1417 self.committedChanges =set()1418 self.branch =""1419 self.detectBranches =False1420 self.detectLabels =False1421 self.changesFile =""1422 self.syncWithOrigin =True1423 self.verbose =False1424 self.importIntoRemotes =True1425 self.maxChanges =""1426 self.isWindows = (platform.system() =="Windows")1427 self.keepRepoPath =False1428 self.depotPaths =None1429 self.p4BranchesInGit = []1430 self.cloneExclude = []1431 self.useClientSpec =False1432 self.clientSpecDirs =None1433 self.tempBranches = []1434 self.tempBranchLocation ="git-p4-tmp"14351436ifgitConfig("git-p4.syncFromOrigin") =="false":1437 self.syncWithOrigin =False14381439#1440# P4 wildcards are not allowed in filenames. P4 complains1441# if you simply add them, but you can force it with "-f", in1442# which case it translates them into %xx encoding internally.1443# Search for and fix just these four characters. Do % last so1444# that fixing it does not inadvertently create new %-escapes.1445#1446defwildcard_decode(self, path):1447# Cannot have * in a filename in windows; untested as to1448# what p4 would do in such a case.1449if not self.isWindows:1450 path = path.replace("%2A","*")1451 path = path.replace("%23","#") \1452.replace("%40","@") \1453.replace("%25","%")1454return path14551456# Force a checkpoint in fast-import and wait for it to finish1457defcheckpoint(self):1458 self.gitStream.write("checkpoint\n\n")1459 self.gitStream.write("progress checkpoint\n\n")1460 out = self.gitOutput.readline()1461if self.verbose:1462print"checkpoint finished: "+ out14631464defextractFilesFromCommit(self, commit):1465 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1466for path in self.cloneExclude]1467 files = []1468 fnum =01469while commit.has_key("depotFile%s"% fnum):1470 path = commit["depotFile%s"% fnum]14711472if[p for p in self.cloneExclude1473ifp4PathStartsWith(path, p)]:1474 found =False1475else:1476 found = [p for p in self.depotPaths1477ifp4PathStartsWith(path, p)]1478if not found:1479 fnum = fnum +11480continue14811482file= {}1483file["path"] = path1484file["rev"] = commit["rev%s"% fnum]1485file["action"] = commit["action%s"% fnum]1486file["type"] = commit["type%s"% fnum]1487 files.append(file)1488 fnum = fnum +11489return files14901491defstripRepoPath(self, path, prefixes):1492if self.useClientSpec:1493return self.clientSpecDirs.map_in_client(path)14941495if self.keepRepoPath:1496 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]14971498for p in prefixes:1499ifp4PathStartsWith(path, p):1500 path = path[len(p):]15011502return path15031504defsplitFilesIntoBranches(self, commit):1505 branches = {}1506 fnum =01507while commit.has_key("depotFile%s"% fnum):1508 path = commit["depotFile%s"% fnum]1509 found = [p for p in self.depotPaths1510ifp4PathStartsWith(path, p)]1511if not found:1512 fnum = fnum +11513continue15141515file= {}1516file["path"] = path1517file["rev"] = commit["rev%s"% fnum]1518file["action"] = commit["action%s"% fnum]1519file["type"] = commit["type%s"% fnum]1520 fnum = fnum +115211522 relPath = self.stripRepoPath(path, self.depotPaths)15231524for branch in self.knownBranches.keys():15251526# add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.21527if relPath.startswith(branch +"/"):1528if branch not in branches:1529 branches[branch] = []1530 branches[branch].append(file)1531break15321533return branches15341535# output one file from the P4 stream1536# - helper for streamP4Files15371538defstreamOneP4File(self,file, contents):1539 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)1540 relPath = self.wildcard_decode(relPath)1541if verbose:1542 sys.stderr.write("%s\n"% relPath)15431544(type_base, type_mods) =split_p4_type(file["type"])15451546 git_mode ="100644"1547if"x"in type_mods:1548 git_mode ="100755"1549if type_base =="symlink":1550 git_mode ="120000"1551# p4 print on a symlink contains "target\n"; remove the newline1552 data =''.join(contents)1553 contents = [data[:-1]]15541555if type_base =="utf16":1556# p4 delivers different text in the python output to -G1557# than it does when using "print -o", or normal p4 client1558# operations. utf16 is converted to ascii or utf8, perhaps.1559# But ascii text saved as -t utf16 is completely mangled.1560# Invoke print -o to get the real contents.1561 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])1562 contents = [ text ]15631564if type_base =="apple":1565# Apple filetype files will be streamed as a concatenation of1566# its appledouble header and the contents. This is useless1567# on both macs and non-macs. If using "print -q -o xx", it1568# will create "xx" with the data, and "%xx" with the header.1569# This is also not very useful.1570#1571# Ideally, someday, this script can learn how to generate1572# appledouble files directly and import those to git, but1573# non-mac machines can never find a use for apple filetype.1574print"\nIgnoring apple filetype file%s"%file['depotFile']1575return15761577# Perhaps windows wants unicode, utf16 newlines translated too;1578# but this is not doing it.1579if self.isWindows and type_base =="text":1580 mangled = []1581for data in contents:1582 data = data.replace("\r\n","\n")1583 mangled.append(data)1584 contents = mangled15851586# Note that we do not try to de-mangle keywords on utf16 files,1587# even though in theory somebody may want that.1588if type_base in("text","unicode","binary"):1589if"ko"in type_mods:1590 text =''.join(contents)1591 text = re.sub(r'\$(Id|Header):[^$]*\$', r'$\1$', text)1592 contents = [ text ]1593elif"k"in type_mods:1594 text =''.join(contents)1595 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$', r'$\1$', text)1596 contents = [ text ]15971598 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))15991600# total length...1601 length =01602for d in contents:1603 length = length +len(d)16041605 self.gitStream.write("data%d\n"% length)1606for d in contents:1607 self.gitStream.write(d)1608 self.gitStream.write("\n")16091610defstreamOneP4Deletion(self,file):1611 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)1612if verbose:1613 sys.stderr.write("delete%s\n"% relPath)1614 self.gitStream.write("D%s\n"% relPath)16151616# handle another chunk of streaming data1617defstreamP4FilesCb(self, marshalled):16181619if marshalled.has_key('depotFile')and self.stream_have_file_info:1620# start of a new file - output the old one first1621 self.streamOneP4File(self.stream_file, self.stream_contents)1622 self.stream_file = {}1623 self.stream_contents = []1624 self.stream_have_file_info =False16251626# pick up the new file information... for the1627# 'data' field we need to append to our array1628for k in marshalled.keys():1629if k =='data':1630 self.stream_contents.append(marshalled['data'])1631else:1632 self.stream_file[k] = marshalled[k]16331634 self.stream_have_file_info =True16351636# Stream directly from "p4 files" into "git fast-import"1637defstreamP4Files(self, files):1638 filesForCommit = []1639 filesToRead = []1640 filesToDelete = []16411642for f in files:1643# if using a client spec, only add the files that have1644# a path in the client1645if self.clientSpecDirs:1646if self.clientSpecDirs.map_in_client(f['path']) =="":1647continue16481649 filesForCommit.append(f)1650if f['action']in self.delete_actions:1651 filesToDelete.append(f)1652else:1653 filesToRead.append(f)16541655# deleted files...1656for f in filesToDelete:1657 self.streamOneP4Deletion(f)16581659iflen(filesToRead) >0:1660 self.stream_file = {}1661 self.stream_contents = []1662 self.stream_have_file_info =False16631664# curry self argument1665defstreamP4FilesCbSelf(entry):1666 self.streamP4FilesCb(entry)16671668 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]16691670p4CmdList(["-x","-","print"],1671 stdin=fileArgs,1672 cb=streamP4FilesCbSelf)16731674# do the last chunk1675if self.stream_file.has_key('depotFile'):1676 self.streamOneP4File(self.stream_file, self.stream_contents)16771678defmake_email(self, userid):1679if userid in self.users:1680return self.users[userid]1681else:1682return"%s<a@b>"% userid16831684defcommit(self, details, files, branch, branchPrefixes, parent =""):1685 epoch = details["time"]1686 author = details["user"]1687 self.branchPrefixes = branchPrefixes16881689if self.verbose:1690print"commit into%s"% branch16911692# start with reading files; if that fails, we should not1693# create a commit.1694 new_files = []1695for f in files:1696if[p for p in branchPrefixes ifp4PathStartsWith(f['path'], p)]:1697 new_files.append(f)1698else:1699 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])17001701 self.gitStream.write("commit%s\n"% branch)1702# gitStream.write("mark :%s\n" % details["change"])1703 self.committedChanges.add(int(details["change"]))1704 committer =""1705if author not in self.users:1706 self.getUserMapFromPerforceServer()1707 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)17081709 self.gitStream.write("committer%s\n"% committer)17101711 self.gitStream.write("data <<EOT\n")1712 self.gitStream.write(details["desc"])1713 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"1714% (','.join(branchPrefixes), details["change"]))1715iflen(details['options']) >0:1716 self.gitStream.write(": options =%s"% details['options'])1717 self.gitStream.write("]\nEOT\n\n")17181719iflen(parent) >0:1720if self.verbose:1721print"parent%s"% parent1722 self.gitStream.write("from%s\n"% parent)17231724 self.streamP4Files(new_files)1725 self.gitStream.write("\n")17261727 change =int(details["change"])17281729if self.labels.has_key(change):1730 label = self.labels[change]1731 labelDetails = label[0]1732 labelRevisions = label[1]1733if self.verbose:1734print"Change%sis labelled%s"% (change, labelDetails)17351736 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)1737for p in branchPrefixes])17381739iflen(files) ==len(labelRevisions):17401741 cleanedFiles = {}1742for info in files:1743if info["action"]in self.delete_actions:1744continue1745 cleanedFiles[info["depotFile"]] = info["rev"]17461747if cleanedFiles == labelRevisions:1748 self.gitStream.write("tag tag_%s\n"% labelDetails["label"])1749 self.gitStream.write("from%s\n"% branch)17501751 owner = labelDetails["Owner"]17521753# Try to use the owner of the p4 label, or failing that,1754# the current p4 user id.1755if owner:1756 email = self.make_email(owner)1757else:1758 email = self.make_email(self.p4UserId())1759 tagger ="%s %s %s"% (email, epoch, self.tz)17601761 self.gitStream.write("tagger%s\n"% tagger)17621763 description = labelDetails["Description"]1764 self.gitStream.write("data%d\n"%len(description))1765 self.gitStream.write(description)1766 self.gitStream.write("\n")17671768else:1769if not self.silent:1770print("Tag%sdoes not match with change%s: files do not match."1771% (labelDetails["label"], change))17721773else:1774if not self.silent:1775print("Tag%sdoes not match with change%s: file count is different."1776% (labelDetails["label"], change))17771778defgetLabels(self):1779 self.labels = {}17801781 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])1782iflen(l) >0and not self.silent:1783print"Finding files belonging to labels in%s"% `self.depotPaths`17841785for output in l:1786 label = output["label"]1787 revisions = {}1788 newestChange =01789if self.verbose:1790print"Querying files for label%s"% label1791forfileinp4CmdList(["files"] +1792["%s...@%s"% (p, label)1793for p in self.depotPaths]):1794 revisions[file["depotFile"]] =file["rev"]1795 change =int(file["change"])1796if change > newestChange:1797 newestChange = change17981799 self.labels[newestChange] = [output, revisions]18001801if self.verbose:1802print"Label changes:%s"% self.labels.keys()18031804defguessProjectName(self):1805for p in self.depotPaths:1806if p.endswith("/"):1807 p = p[:-1]1808 p = p[p.strip().rfind("/") +1:]1809if not p.endswith("/"):1810 p +="/"1811return p18121813defgetBranchMapping(self):1814 lostAndFoundBranches =set()18151816 user =gitConfig("git-p4.branchUser")1817iflen(user) >0:1818 command ="branches -u%s"% user1819else:1820 command ="branches"18211822for info inp4CmdList(command):1823 details =p4Cmd(["branch","-o", info["branch"]])1824 viewIdx =01825while details.has_key("View%s"% viewIdx):1826 paths = details["View%s"% viewIdx].split(" ")1827 viewIdx = viewIdx +11828# require standard //depot/foo/... //depot/bar/... mapping1829iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):1830continue1831 source = paths[0]1832 destination = paths[1]1833## HACK1834ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):1835 source = source[len(self.depotPaths[0]):-4]1836 destination = destination[len(self.depotPaths[0]):-4]18371838if destination in self.knownBranches:1839if not self.silent:1840print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)1841print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)1842continue18431844 self.knownBranches[destination] = source18451846 lostAndFoundBranches.discard(destination)18471848if source not in self.knownBranches:1849 lostAndFoundBranches.add(source)18501851# Perforce does not strictly require branches to be defined, so we also1852# check git config for a branch list.1853#1854# Example of branch definition in git config file:1855# [git-p4]1856# branchList=main:branchA1857# branchList=main:branchB1858# branchList=branchA:branchC1859 configBranches =gitConfigList("git-p4.branchList")1860for branch in configBranches:1861if branch:1862(source, destination) = branch.split(":")1863 self.knownBranches[destination] = source18641865 lostAndFoundBranches.discard(destination)18661867if source not in self.knownBranches:1868 lostAndFoundBranches.add(source)186918701871for branch in lostAndFoundBranches:1872 self.knownBranches[branch] = branch18731874defgetBranchMappingFromGitBranches(self):1875 branches =p4BranchesInGit(self.importIntoRemotes)1876for branch in branches.keys():1877if branch =="master":1878 branch ="main"1879else:1880 branch = branch[len(self.projectName):]1881 self.knownBranches[branch] = branch18821883deflistExistingP4GitBranches(self):1884# branches holds mapping from name to commit1885 branches =p4BranchesInGit(self.importIntoRemotes)1886 self.p4BranchesInGit = branches.keys()1887for branch in branches.keys():1888 self.initialParents[self.refPrefix + branch] = branches[branch]18891890defupdateOptionDict(self, d):1891 option_keys = {}1892if self.keepRepoPath:1893 option_keys['keepRepoPath'] =118941895 d["options"] =' '.join(sorted(option_keys.keys()))18961897defreadOptions(self, d):1898 self.keepRepoPath = (d.has_key('options')1899and('keepRepoPath'in d['options']))19001901defgitRefForBranch(self, branch):1902if branch =="main":1903return self.refPrefix +"master"19041905iflen(branch) <=0:1906return branch19071908return self.refPrefix + self.projectName + branch19091910defgitCommitByP4Change(self, ref, change):1911if self.verbose:1912print"looking in ref "+ ref +" for change%susing bisect..."% change19131914 earliestCommit =""1915 latestCommit =parseRevision(ref)19161917while True:1918if self.verbose:1919print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)1920 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()1921iflen(next) ==0:1922if self.verbose:1923print"argh"1924return""1925 log =extractLogMessageFromGitCommit(next)1926 settings =extractSettingsGitLog(log)1927 currentChange =int(settings['change'])1928if self.verbose:1929print"current change%s"% currentChange19301931if currentChange == change:1932if self.verbose:1933print"found%s"% next1934return next19351936if currentChange < change:1937 earliestCommit ="^%s"% next1938else:1939 latestCommit ="%s"% next19401941return""19421943defimportNewBranch(self, branch, maxChange):1944# make fast-import flush all changes to disk and update the refs using the checkpoint1945# command so that we can try to find the branch parent in the git history1946 self.gitStream.write("checkpoint\n\n");1947 self.gitStream.flush();1948 branchPrefix = self.depotPaths[0] + branch +"/"1949range="@1,%s"% maxChange1950#print "prefix" + branchPrefix1951 changes =p4ChangesForPaths([branchPrefix],range)1952iflen(changes) <=0:1953return False1954 firstChange = changes[0]1955#print "first change in branch: %s" % firstChange1956 sourceBranch = self.knownBranches[branch]1957 sourceDepotPath = self.depotPaths[0] + sourceBranch1958 sourceRef = self.gitRefForBranch(sourceBranch)1959#print "source " + sourceBranch19601961 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])1962#print "branch parent: %s" % branchParentChange1963 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)1964iflen(gitParent) >0:1965 self.initialParents[self.gitRefForBranch(branch)] = gitParent1966#print "parent git commit: %s" % gitParent19671968 self.importChanges(changes)1969return True19701971defsearchParent(self, parent, branch, target):1972 parentFound =False1973for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):1974 blob = blob.strip()1975iflen(read_pipe(["git","diff-tree", blob, target])) ==0:1976 parentFound =True1977if self.verbose:1978print"Found parent of%sin commit%s"% (branch, blob)1979break1980if parentFound:1981return blob1982else:1983return None19841985defimportChanges(self, changes):1986 cnt =11987for change in changes:1988 description =p4Cmd(["describe",str(change)])1989 self.updateOptionDict(description)19901991if not self.silent:1992 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))1993 sys.stdout.flush()1994 cnt = cnt +119951996try:1997if self.detectBranches:1998 branches = self.splitFilesIntoBranches(description)1999for branch in branches.keys():2000## HACK --hwn2001 branchPrefix = self.depotPaths[0] + branch +"/"20022003 parent =""20042005 filesForCommit = branches[branch]20062007if self.verbose:2008print"branch is%s"% branch20092010 self.updatedBranches.add(branch)20112012if branch not in self.createdBranches:2013 self.createdBranches.add(branch)2014 parent = self.knownBranches[branch]2015if parent == branch:2016 parent =""2017else:2018 fullBranch = self.projectName + branch2019if fullBranch not in self.p4BranchesInGit:2020if not self.silent:2021print("\nImporting new branch%s"% fullBranch);2022if self.importNewBranch(branch, change -1):2023 parent =""2024 self.p4BranchesInGit.append(fullBranch)2025if not self.silent:2026print("\nResuming with change%s"% change);20272028if self.verbose:2029print"parent determined through known branches:%s"% parent20302031 branch = self.gitRefForBranch(branch)2032 parent = self.gitRefForBranch(parent)20332034if self.verbose:2035print"looking for initial parent for%s; current parent is%s"% (branch, parent)20362037iflen(parent) ==0and branch in self.initialParents:2038 parent = self.initialParents[branch]2039del self.initialParents[branch]20402041 blob =None2042iflen(parent) >0:2043 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2044if self.verbose:2045print"Creating temporary branch: "+ tempBranch2046 self.commit(description, filesForCommit, tempBranch, [branchPrefix])2047 self.tempBranches.append(tempBranch)2048 self.checkpoint()2049 blob = self.searchParent(parent, branch, tempBranch)2050if blob:2051 self.commit(description, filesForCommit, branch, [branchPrefix], blob)2052else:2053if self.verbose:2054print"Parent of%snot found. Committing into head of%s"% (branch, parent)2055 self.commit(description, filesForCommit, branch, [branchPrefix], parent)2056else:2057 files = self.extractFilesFromCommit(description)2058 self.commit(description, files, self.branch, self.depotPaths,2059 self.initialParent)2060 self.initialParent =""2061exceptIOError:2062print self.gitError.read()2063 sys.exit(1)20642065defimportHeadRevision(self, revision):2066print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)20672068 details = {}2069 details["user"] ="git perforce import user"2070 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2071% (' '.join(self.depotPaths), revision))2072 details["change"] = revision2073 newestRevision =020742075 fileCnt =02076 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]20772078for info inp4CmdList(["files"] + fileArgs):20792080if'code'in info and info['code'] =='error':2081 sys.stderr.write("p4 returned an error:%s\n"2082% info['data'])2083if info['data'].find("must refer to client") >=0:2084 sys.stderr.write("This particular p4 error is misleading.\n")2085 sys.stderr.write("Perhaps the depot path was misspelled.\n");2086 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2087 sys.exit(1)2088if'p4ExitCode'in info:2089 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2090 sys.exit(1)209120922093 change =int(info["change"])2094if change > newestRevision:2095 newestRevision = change20962097if info["action"]in self.delete_actions:2098# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2099#fileCnt = fileCnt + 12100continue21012102for prop in["depotFile","rev","action","type"]:2103 details["%s%s"% (prop, fileCnt)] = info[prop]21042105 fileCnt = fileCnt +121062107 details["change"] = newestRevision21082109# Use time from top-most change so that all git-p4 clones of2110# the same p4 repo have the same commit SHA1s.2111 res =p4CmdList("describe -s%d"% newestRevision)2112 newestTime =None2113for r in res:2114if r.has_key('time'):2115 newestTime =int(r['time'])2116if newestTime is None:2117die("\"describe -s\"on newest change%ddid not give a time")2118 details["time"] = newestTime21192120 self.updateOptionDict(details)2121try:2122 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)2123exceptIOError:2124print"IO error with git fast-import. Is your git version recent enough?"2125print self.gitError.read()212621272128defgetClientSpec(self):2129 specList =p4CmdList("client -o")2130iflen(specList) !=1:2131die('Output from "client -o" is%dlines, expecting 1'%2132len(specList))21332134# dictionary of all client parameters2135 entry = specList[0]21362137# just the keys that start with "View"2138 view_keys = [ k for k in entry.keys()if k.startswith("View") ]21392140# hold this new View2141 view =View()21422143# append the lines, in order, to the view2144for view_num inrange(len(view_keys)):2145 k ="View%d"% view_num2146if k not in view_keys:2147die("Expected view key%smissing"% k)2148 view.append(entry[k])21492150 self.clientSpecDirs = view2151if self.verbose:2152for i, m inenumerate(self.clientSpecDirs.mappings):2153print"clientSpecDirs%d:%s"% (i,str(m))21542155defrun(self, args):2156 self.depotPaths = []2157 self.changeRange =""2158 self.initialParent =""2159 self.previousDepotPaths = []21602161# map from branch depot path to parent branch2162 self.knownBranches = {}2163 self.initialParents = {}2164 self.hasOrigin =originP4BranchesExist()2165if not self.syncWithOrigin:2166 self.hasOrigin =False21672168if self.importIntoRemotes:2169 self.refPrefix ="refs/remotes/p4/"2170else:2171 self.refPrefix ="refs/heads/p4/"21722173if self.syncWithOrigin and self.hasOrigin:2174if not self.silent:2175print"Syncing with origin first by calling git fetch origin"2176system("git fetch origin")21772178iflen(self.branch) ==0:2179 self.branch = self.refPrefix +"master"2180ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2181system("git update-ref%srefs/heads/p4"% self.branch)2182system("git branch -D p4");2183# create it /after/ importing, when master exists2184if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2185system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))21862187if not self.useClientSpec:2188ifgitConfig("git-p4.useclientspec","--bool") =="true":2189 self.useClientSpec =True2190if self.useClientSpec:2191 self.getClientSpec()21922193# TODO: should always look at previous commits,2194# merge with previous imports, if possible.2195if args == []:2196if self.hasOrigin:2197createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2198 self.listExistingP4GitBranches()21992200iflen(self.p4BranchesInGit) >1:2201if not self.silent:2202print"Importing from/into multiple branches"2203 self.detectBranches =True22042205if self.verbose:2206print"branches:%s"% self.p4BranchesInGit22072208 p4Change =02209for branch in self.p4BranchesInGit:2210 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)22112212 settings =extractSettingsGitLog(logMsg)22132214 self.readOptions(settings)2215if(settings.has_key('depot-paths')2216and settings.has_key('change')):2217 change =int(settings['change']) +12218 p4Change =max(p4Change, change)22192220 depotPaths =sorted(settings['depot-paths'])2221if self.previousDepotPaths == []:2222 self.previousDepotPaths = depotPaths2223else:2224 paths = []2225for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2226 prev_list = prev.split("/")2227 cur_list = cur.split("/")2228for i inrange(0,min(len(cur_list),len(prev_list))):2229if cur_list[i] <> prev_list[i]:2230 i = i -12231break22322233 paths.append("/".join(cur_list[:i +1]))22342235 self.previousDepotPaths = paths22362237if p4Change >0:2238 self.depotPaths =sorted(self.previousDepotPaths)2239 self.changeRange ="@%s,#head"% p4Change2240if not self.detectBranches:2241 self.initialParent =parseRevision(self.branch)2242if not self.silent and not self.detectBranches:2243print"Performing incremental import into%sgit branch"% self.branch22442245if not self.branch.startswith("refs/"):2246 self.branch ="refs/heads/"+ self.branch22472248iflen(args) ==0and self.depotPaths:2249if not self.silent:2250print"Depot paths:%s"%' '.join(self.depotPaths)2251else:2252if self.depotPaths and self.depotPaths != args:2253print("previous import used depot path%sand now%swas specified. "2254"This doesn't work!"% (' '.join(self.depotPaths),2255' '.join(args)))2256 sys.exit(1)22572258 self.depotPaths =sorted(args)22592260 revision =""2261 self.users = {}22622263# Make sure no revision specifiers are used when --changesfile2264# is specified.2265 bad_changesfile =False2266iflen(self.changesFile) >0:2267for p in self.depotPaths:2268if p.find("@") >=0or p.find("#") >=0:2269 bad_changesfile =True2270break2271if bad_changesfile:2272die("Option --changesfile is incompatible with revision specifiers")22732274 newPaths = []2275for p in self.depotPaths:2276if p.find("@") != -1:2277 atIdx = p.index("@")2278 self.changeRange = p[atIdx:]2279if self.changeRange =="@all":2280 self.changeRange =""2281elif','not in self.changeRange:2282 revision = self.changeRange2283 self.changeRange =""2284 p = p[:atIdx]2285elif p.find("#") != -1:2286 hashIdx = p.index("#")2287 revision = p[hashIdx:]2288 p = p[:hashIdx]2289elif self.previousDepotPaths == []:2290# pay attention to changesfile, if given, else import2291# the entire p4 tree at the head revision2292iflen(self.changesFile) ==0:2293 revision ="#head"22942295 p = re.sub("\.\.\.$","", p)2296if not p.endswith("/"):2297 p +="/"22982299 newPaths.append(p)23002301 self.depotPaths = newPaths230223032304 self.loadUserMapFromCache()2305 self.labels = {}2306if self.detectLabels:2307 self.getLabels();23082309if self.detectBranches:2310## FIXME - what's a P4 projectName ?2311 self.projectName = self.guessProjectName()23122313if self.hasOrigin:2314 self.getBranchMappingFromGitBranches()2315else:2316 self.getBranchMapping()2317if self.verbose:2318print"p4-git branches:%s"% self.p4BranchesInGit2319print"initial parents:%s"% self.initialParents2320for b in self.p4BranchesInGit:2321if b !="master":23222323## FIXME2324 b = b[len(self.projectName):]2325 self.createdBranches.add(b)23262327 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))23282329 importProcess = subprocess.Popen(["git","fast-import"],2330 stdin=subprocess.PIPE, stdout=subprocess.PIPE,2331 stderr=subprocess.PIPE);2332 self.gitOutput = importProcess.stdout2333 self.gitStream = importProcess.stdin2334 self.gitError = importProcess.stderr23352336if revision:2337 self.importHeadRevision(revision)2338else:2339 changes = []23402341iflen(self.changesFile) >0:2342 output =open(self.changesFile).readlines()2343 changeSet =set()2344for line in output:2345 changeSet.add(int(line))23462347for change in changeSet:2348 changes.append(change)23492350 changes.sort()2351else:2352# catch "git-p4 sync" with no new branches, in a repo that2353# does not have any existing git-p4 branches2354iflen(args) ==0and not self.p4BranchesInGit:2355die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2356if self.verbose:2357print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2358 self.changeRange)2359 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)23602361iflen(self.maxChanges) >0:2362 changes = changes[:min(int(self.maxChanges),len(changes))]23632364iflen(changes) ==0:2365if not self.silent:2366print"No changes to import!"2367return True23682369if not self.silent and not self.detectBranches:2370print"Import destination:%s"% self.branch23712372 self.updatedBranches =set()23732374 self.importChanges(changes)23752376if not self.silent:2377print""2378iflen(self.updatedBranches) >0:2379 sys.stdout.write("Updated branches: ")2380for b in self.updatedBranches:2381 sys.stdout.write("%s"% b)2382 sys.stdout.write("\n")23832384 self.gitStream.close()2385if importProcess.wait() !=0:2386die("fast-import failed:%s"% self.gitError.read())2387 self.gitOutput.close()2388 self.gitError.close()23892390# Cleanup temporary branches created during import2391if self.tempBranches != []:2392for branch in self.tempBranches:2393read_pipe("git update-ref -d%s"% branch)2394 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))23952396return True23972398classP4Rebase(Command):2399def__init__(self):2400 Command.__init__(self)2401 self.options = [ ]2402 self.description = ("Fetches the latest revision from perforce and "2403+"rebases the current work (branch) against it")2404 self.verbose =False24052406defrun(self, args):2407 sync =P4Sync()2408 sync.run([])24092410return self.rebase()24112412defrebase(self):2413if os.system("git update-index --refresh") !=0:2414die("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.");2415iflen(read_pipe("git diff-index HEAD --")) >0:2416die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");24172418[upstream, settings] =findUpstreamBranchPoint()2419iflen(upstream) ==0:2420die("Cannot find upstream branchpoint for rebase")24212422# the branchpoint may be p4/foo~3, so strip off the parent2423 upstream = re.sub("~[0-9]+$","", upstream)24242425print"Rebasing the current branch onto%s"% upstream2426 oldHead =read_pipe("git rev-parse HEAD").strip()2427system("git rebase%s"% upstream)2428system("git diff-tree --stat --summary -M%sHEAD"% oldHead)2429return True24302431classP4Clone(P4Sync):2432def__init__(self):2433 P4Sync.__init__(self)2434 self.description ="Creates a new git repository and imports from Perforce into it"2435 self.usage ="usage: %prog [options] //depot/path[@revRange]"2436 self.options += [2437 optparse.make_option("--destination", dest="cloneDestination",2438 action='store', default=None,2439help="where to leave result of the clone"),2440 optparse.make_option("-/", dest="cloneExclude",2441 action="append",type="string",2442help="exclude depot path"),2443 optparse.make_option("--bare", dest="cloneBare",2444 action="store_true", default=False),2445]2446 self.cloneDestination =None2447 self.needsGit =False2448 self.cloneBare =False24492450# This is required for the "append" cloneExclude action2451defensure_value(self, attr, value):2452if nothasattr(self, attr)orgetattr(self, attr)is None:2453setattr(self, attr, value)2454returngetattr(self, attr)24552456defdefaultDestination(self, args):2457## TODO: use common prefix of args?2458 depotPath = args[0]2459 depotDir = re.sub("(@[^@]*)$","", depotPath)2460 depotDir = re.sub("(#[^#]*)$","", depotDir)2461 depotDir = re.sub(r"\.\.\.$","", depotDir)2462 depotDir = re.sub(r"/$","", depotDir)2463return os.path.split(depotDir)[1]24642465defrun(self, args):2466iflen(args) <1:2467return False24682469if self.keepRepoPath and not self.cloneDestination:2470 sys.stderr.write("Must specify destination for --keep-path\n")2471 sys.exit(1)24722473 depotPaths = args24742475if not self.cloneDestination andlen(depotPaths) >1:2476 self.cloneDestination = depotPaths[-1]2477 depotPaths = depotPaths[:-1]24782479 self.cloneExclude = ["/"+p for p in self.cloneExclude]2480for p in depotPaths:2481if not p.startswith("//"):2482return False24832484if not self.cloneDestination:2485 self.cloneDestination = self.defaultDestination(args)24862487print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)24882489if not os.path.exists(self.cloneDestination):2490 os.makedirs(self.cloneDestination)2491chdir(self.cloneDestination)24922493 init_cmd = ["git","init"]2494if self.cloneBare:2495 init_cmd.append("--bare")2496 subprocess.check_call(init_cmd)24972498if not P4Sync.run(self, depotPaths):2499return False2500if self.branch !="master":2501if self.importIntoRemotes:2502 masterbranch ="refs/remotes/p4/master"2503else:2504 masterbranch ="refs/heads/p4/master"2505ifgitBranchExists(masterbranch):2506system("git branch master%s"% masterbranch)2507if not self.cloneBare:2508system("git checkout -f")2509else:2510print"Could not detect main branch. No checkout/master branch created."25112512return True25132514classP4Branches(Command):2515def__init__(self):2516 Command.__init__(self)2517 self.options = [ ]2518 self.description = ("Shows the git branches that hold imports and their "2519+"corresponding perforce depot paths")2520 self.verbose =False25212522defrun(self, args):2523iforiginP4BranchesExist():2524createOrUpdateBranchesFromOrigin()25252526 cmdline ="git rev-parse --symbolic "2527 cmdline +=" --remotes"25282529for line inread_pipe_lines(cmdline):2530 line = line.strip()25312532if not line.startswith('p4/')or line =="p4/HEAD":2533continue2534 branch = line25352536 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)2537 settings =extractSettingsGitLog(log)25382539print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])2540return True25412542classHelpFormatter(optparse.IndentedHelpFormatter):2543def__init__(self):2544 optparse.IndentedHelpFormatter.__init__(self)25452546defformat_description(self, description):2547if description:2548return description +"\n"2549else:2550return""25512552defprintUsage(commands):2553print"usage:%s<command> [options]"% sys.argv[0]2554print""2555print"valid commands:%s"%", ".join(commands)2556print""2557print"Try%s<command> --help for command specific help."% sys.argv[0]2558print""25592560commands = {2561"debug": P4Debug,2562"submit": P4Submit,2563"commit": P4Submit,2564"sync": P4Sync,2565"rebase": P4Rebase,2566"clone": P4Clone,2567"rollback": P4RollBack,2568"branches": P4Branches2569}257025712572defmain():2573iflen(sys.argv[1:]) ==0:2574printUsage(commands.keys())2575 sys.exit(2)25762577 cmd =""2578 cmdName = sys.argv[1]2579try:2580 klass = commands[cmdName]2581 cmd =klass()2582exceptKeyError:2583print"unknown command%s"% cmdName2584print""2585printUsage(commands.keys())2586 sys.exit(2)25872588 options = cmd.options2589 cmd.gitdir = os.environ.get("GIT_DIR",None)25902591 args = sys.argv[2:]25922593iflen(options) >0:2594if cmd.needsGit:2595 options.append(optparse.make_option("--git-dir", dest="gitdir"))25962597 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),2598 options,2599 description = cmd.description,2600 formatter =HelpFormatter())26012602(cmd, args) = parser.parse_args(sys.argv[2:], cmd);2603global verbose2604 verbose = cmd.verbose2605if cmd.needsGit:2606if cmd.gitdir ==None:2607 cmd.gitdir = os.path.abspath(".git")2608if notisValidGitDir(cmd.gitdir):2609 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()2610if os.path.exists(cmd.gitdir):2611 cdup =read_pipe("git rev-parse --show-cdup").strip()2612iflen(cdup) >0:2613chdir(cdup);26142615if notisValidGitDir(cmd.gitdir):2616ifisValidGitDir(cmd.gitdir +"/.git"):2617 cmd.gitdir +="/.git"2618else:2619die("fatal: cannot locate git repository at%s"% cmd.gitdir)26202621 os.environ["GIT_DIR"] = cmd.gitdir26222623if not cmd.run(args):2624 parser.print_help()2625 sys.exit(2)262626272628if __name__ =='__main__':2629main()