1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10 11import optparse, sys, os, marshal, subprocess, shelve 12import tempfile, getopt, os.path, time, platform 13import re, shutil 14 15verbose =False 16 17# Only labels/tags matching this will be imported/exported 18defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 19 20defp4_build_cmd(cmd): 21"""Build a suitable p4 command line. 22 23 This consolidates building and returning a p4 command line into one 24 location. It means that hooking into the environment, or other configuration 25 can be done more easily. 26 """ 27 real_cmd = ["p4"] 28 29 user =gitConfig("git-p4.user") 30iflen(user) >0: 31 real_cmd += ["-u",user] 32 33 password =gitConfig("git-p4.password") 34iflen(password) >0: 35 real_cmd += ["-P", password] 36 37 port =gitConfig("git-p4.port") 38iflen(port) >0: 39 real_cmd += ["-p", port] 40 41 host =gitConfig("git-p4.host") 42iflen(host) >0: 43 real_cmd += ["-H", host] 44 45 client =gitConfig("git-p4.client") 46iflen(client) >0: 47 real_cmd += ["-c", client] 48 49 50ifisinstance(cmd,basestring): 51 real_cmd =' '.join(real_cmd) +' '+ cmd 52else: 53 real_cmd += cmd 54return real_cmd 55 56defchdir(dir): 57# P4 uses the PWD environment variable rather than getcwd(). Since we're 58# not using the shell, we have to set it ourselves. This path could 59# be relative, so go there first, then figure out where we ended up. 60 os.chdir(dir) 61 os.environ['PWD'] = os.getcwd() 62 63defdie(msg): 64if verbose: 65raiseException(msg) 66else: 67 sys.stderr.write(msg +"\n") 68 sys.exit(1) 69 70defwrite_pipe(c, stdin): 71if verbose: 72 sys.stderr.write('Writing pipe:%s\n'%str(c)) 73 74 expand =isinstance(c,basestring) 75 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 76 pipe = p.stdin 77 val = pipe.write(stdin) 78 pipe.close() 79if p.wait(): 80die('Command failed:%s'%str(c)) 81 82return val 83 84defp4_write_pipe(c, stdin): 85 real_cmd =p4_build_cmd(c) 86returnwrite_pipe(real_cmd, stdin) 87 88defread_pipe(c, ignore_error=False): 89if verbose: 90 sys.stderr.write('Reading pipe:%s\n'%str(c)) 91 92 expand =isinstance(c,basestring) 93 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 94 pipe = p.stdout 95 val = pipe.read() 96if p.wait()and not ignore_error: 97die('Command failed:%s'%str(c)) 98 99return val 100 101defp4_read_pipe(c, ignore_error=False): 102 real_cmd =p4_build_cmd(c) 103returnread_pipe(real_cmd, ignore_error) 104 105defread_pipe_lines(c): 106if verbose: 107 sys.stderr.write('Reading pipe:%s\n'%str(c)) 108 109 expand =isinstance(c, basestring) 110 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 111 pipe = p.stdout 112 val = pipe.readlines() 113if pipe.close()or p.wait(): 114die('Command failed:%s'%str(c)) 115 116return val 117 118defp4_read_pipe_lines(c): 119"""Specifically invoke p4 on the command supplied. """ 120 real_cmd =p4_build_cmd(c) 121returnread_pipe_lines(real_cmd) 122 123defsystem(cmd): 124 expand =isinstance(cmd,basestring) 125if verbose: 126 sys.stderr.write("executing%s\n"%str(cmd)) 127 subprocess.check_call(cmd, shell=expand) 128 129defp4_system(cmd): 130"""Specifically invoke p4 as the system command. """ 131 real_cmd =p4_build_cmd(cmd) 132 expand =isinstance(real_cmd, basestring) 133 subprocess.check_call(real_cmd, shell=expand) 134 135defp4_integrate(src, dest): 136p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 137 138defp4_sync(f, *options): 139p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 140 141defp4_add(f): 142# forcibly add file names with wildcards 143ifwildcard_present(f): 144p4_system(["add","-f", f]) 145else: 146p4_system(["add", f]) 147 148defp4_delete(f): 149p4_system(["delete",wildcard_encode(f)]) 150 151defp4_edit(f): 152p4_system(["edit",wildcard_encode(f)]) 153 154defp4_revert(f): 155p4_system(["revert",wildcard_encode(f)]) 156 157defp4_reopen(type, f): 158p4_system(["reopen","-t",type,wildcard_encode(f)]) 159 160# 161# Canonicalize the p4 type and return a tuple of the 162# base type, plus any modifiers. See "p4 help filetypes" 163# for a list and explanation. 164# 165defsplit_p4_type(p4type): 166 167 p4_filetypes_historical = { 168"ctempobj":"binary+Sw", 169"ctext":"text+C", 170"cxtext":"text+Cx", 171"ktext":"text+k", 172"kxtext":"text+kx", 173"ltext":"text+F", 174"tempobj":"binary+FSw", 175"ubinary":"binary+F", 176"uresource":"resource+F", 177"uxbinary":"binary+Fx", 178"xbinary":"binary+x", 179"xltext":"text+Fx", 180"xtempobj":"binary+Swx", 181"xtext":"text+x", 182"xunicode":"unicode+x", 183"xutf16":"utf16+x", 184} 185if p4type in p4_filetypes_historical: 186 p4type = p4_filetypes_historical[p4type] 187 mods ="" 188 s = p4type.split("+") 189 base = s[0] 190 mods ="" 191iflen(s) >1: 192 mods = s[1] 193return(base, mods) 194 195# 196# return the raw p4 type of a file (text, text+ko, etc) 197# 198defp4_type(file): 199 results =p4CmdList(["fstat","-T","headType",file]) 200return results[0]['headType'] 201 202# 203# Given a type base and modifier, return a regexp matching 204# the keywords that can be expanded in the file 205# 206defp4_keywords_regexp_for_type(base, type_mods): 207if base in("text","unicode","binary"): 208 kwords =None 209if"ko"in type_mods: 210 kwords ='Id|Header' 211elif"k"in type_mods: 212 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 213else: 214return None 215 pattern = r""" 216 \$ # Starts with a dollar, followed by... 217 (%s) # one of the keywords, followed by... 218 (:[^$]+)? # possibly an old expansion, followed by... 219 \$ # another dollar 220 """% kwords 221return pattern 222else: 223return None 224 225# 226# Given a file, return a regexp matching the possible 227# RCS keywords that will be expanded, or None for files 228# with kw expansion turned off. 229# 230defp4_keywords_regexp_for_file(file): 231if not os.path.exists(file): 232return None 233else: 234(type_base, type_mods) =split_p4_type(p4_type(file)) 235returnp4_keywords_regexp_for_type(type_base, type_mods) 236 237defsetP4ExecBit(file, mode): 238# Reopens an already open file and changes the execute bit to match 239# the execute bit setting in the passed in mode. 240 241 p4Type ="+x" 242 243if notisModeExec(mode): 244 p4Type =getP4OpenedType(file) 245 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 246 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 247if p4Type[-1] =="+": 248 p4Type = p4Type[0:-1] 249 250p4_reopen(p4Type,file) 251 252defgetP4OpenedType(file): 253# Returns the perforce file type for the given file. 254 255 result =p4_read_pipe(["opened",wildcard_encode(file)]) 256 match = re.match(".*\((.+)\)\r?$", result) 257if match: 258return match.group(1) 259else: 260die("Could not determine file type for%s(result: '%s')"% (file, result)) 261 262# Return the set of all p4 labels 263defgetP4Labels(depotPaths): 264 labels =set() 265ifisinstance(depotPaths,basestring): 266 depotPaths = [depotPaths] 267 268for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 269 label = l['label'] 270 labels.add(label) 271 272return labels 273 274# Return the set of all git tags 275defgetGitTags(): 276 gitTags =set() 277for line inread_pipe_lines(["git","tag"]): 278 tag = line.strip() 279 gitTags.add(tag) 280return gitTags 281 282defdiffTreePattern(): 283# This is a simple generator for the diff tree regex pattern. This could be 284# a class variable if this and parseDiffTreeEntry were a part of a class. 285 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 286while True: 287yield pattern 288 289defparseDiffTreeEntry(entry): 290"""Parses a single diff tree entry into its component elements. 291 292 See git-diff-tree(1) manpage for details about the format of the diff 293 output. This method returns a dictionary with the following elements: 294 295 src_mode - The mode of the source file 296 dst_mode - The mode of the destination file 297 src_sha1 - The sha1 for the source file 298 dst_sha1 - The sha1 fr the destination file 299 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 300 status_score - The score for the status (applicable for 'C' and 'R' 301 statuses). This is None if there is no score. 302 src - The path for the source file. 303 dst - The path for the destination file. This is only present for 304 copy or renames. If it is not present, this is None. 305 306 If the pattern is not matched, None is returned.""" 307 308 match =diffTreePattern().next().match(entry) 309if match: 310return{ 311'src_mode': match.group(1), 312'dst_mode': match.group(2), 313'src_sha1': match.group(3), 314'dst_sha1': match.group(4), 315'status': match.group(5), 316'status_score': match.group(6), 317'src': match.group(7), 318'dst': match.group(10) 319} 320return None 321 322defisModeExec(mode): 323# Returns True if the given git mode represents an executable file, 324# otherwise False. 325return mode[-3:] =="755" 326 327defisModeExecChanged(src_mode, dst_mode): 328returnisModeExec(src_mode) !=isModeExec(dst_mode) 329 330defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 331 332ifisinstance(cmd,basestring): 333 cmd ="-G "+ cmd 334 expand =True 335else: 336 cmd = ["-G"] + cmd 337 expand =False 338 339 cmd =p4_build_cmd(cmd) 340if verbose: 341 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 342 343# Use a temporary file to avoid deadlocks without 344# subprocess.communicate(), which would put another copy 345# of stdout into memory. 346 stdin_file =None 347if stdin is not None: 348 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 349ifisinstance(stdin,basestring): 350 stdin_file.write(stdin) 351else: 352for i in stdin: 353 stdin_file.write(i +'\n') 354 stdin_file.flush() 355 stdin_file.seek(0) 356 357 p4 = subprocess.Popen(cmd, 358 shell=expand, 359 stdin=stdin_file, 360 stdout=subprocess.PIPE) 361 362 result = [] 363try: 364while True: 365 entry = marshal.load(p4.stdout) 366if cb is not None: 367cb(entry) 368else: 369 result.append(entry) 370exceptEOFError: 371pass 372 exitCode = p4.wait() 373if exitCode !=0: 374 entry = {} 375 entry["p4ExitCode"] = exitCode 376 result.append(entry) 377 378return result 379 380defp4Cmd(cmd): 381list=p4CmdList(cmd) 382 result = {} 383for entry inlist: 384 result.update(entry) 385return result; 386 387defp4Where(depotPath): 388if not depotPath.endswith("/"): 389 depotPath +="/" 390 depotPath = depotPath +"..." 391 outputList =p4CmdList(["where", depotPath]) 392 output =None 393for entry in outputList: 394if"depotFile"in entry: 395if entry["depotFile"] == depotPath: 396 output = entry 397break 398elif"data"in entry: 399 data = entry.get("data") 400 space = data.find(" ") 401if data[:space] == depotPath: 402 output = entry 403break 404if output ==None: 405return"" 406if output["code"] =="error": 407return"" 408 clientPath ="" 409if"path"in output: 410 clientPath = output.get("path") 411elif"data"in output: 412 data = output.get("data") 413 lastSpace = data.rfind(" ") 414 clientPath = data[lastSpace +1:] 415 416if clientPath.endswith("..."): 417 clientPath = clientPath[:-3] 418return clientPath 419 420defcurrentGitBranch(): 421returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 422 423defisValidGitDir(path): 424if(os.path.exists(path +"/HEAD") 425and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 426return True; 427return False 428 429defparseRevision(ref): 430returnread_pipe("git rev-parse%s"% ref).strip() 431 432defbranchExists(ref): 433 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 434 ignore_error=True) 435returnlen(rev) >0 436 437defextractLogMessageFromGitCommit(commit): 438 logMessage ="" 439 440## fixme: title is first line of commit, not 1st paragraph. 441 foundTitle =False 442for log inread_pipe_lines("git cat-file commit%s"% commit): 443if not foundTitle: 444iflen(log) ==1: 445 foundTitle =True 446continue 447 448 logMessage += log 449return logMessage 450 451defextractSettingsGitLog(log): 452 values = {} 453for line in log.split("\n"): 454 line = line.strip() 455 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 456if not m: 457continue 458 459 assignments = m.group(1).split(':') 460for a in assignments: 461 vals = a.split('=') 462 key = vals[0].strip() 463 val = ('='.join(vals[1:])).strip() 464if val.endswith('\"')and val.startswith('"'): 465 val = val[1:-1] 466 467 values[key] = val 468 469 paths = values.get("depot-paths") 470if not paths: 471 paths = values.get("depot-path") 472if paths: 473 values['depot-paths'] = paths.split(',') 474return values 475 476defgitBranchExists(branch): 477 proc = subprocess.Popen(["git","rev-parse", branch], 478 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 479return proc.wait() ==0; 480 481_gitConfig = {} 482defgitConfig(key, args =None):# set args to "--bool", for instance 483if not _gitConfig.has_key(key): 484 argsFilter ="" 485if args !=None: 486 argsFilter ="%s"% args 487 cmd ="git config%s%s"% (argsFilter, key) 488 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 489return _gitConfig[key] 490 491defgitConfigList(key): 492if not _gitConfig.has_key(key): 493 _gitConfig[key] =read_pipe("git config --get-all%s"% key, ignore_error=True).strip().split(os.linesep) 494return _gitConfig[key] 495 496defp4BranchesInGit(branchesAreInRemotes =True): 497 branches = {} 498 499 cmdline ="git rev-parse --symbolic " 500if branchesAreInRemotes: 501 cmdline +=" --remotes" 502else: 503 cmdline +=" --branches" 504 505for line inread_pipe_lines(cmdline): 506 line = line.strip() 507 508## only import to p4/ 509if not line.startswith('p4/')or line =="p4/HEAD": 510continue 511 branch = line 512 513# strip off p4 514 branch = re.sub("^p4/","", line) 515 516 branches[branch] =parseRevision(line) 517return branches 518 519deffindUpstreamBranchPoint(head ="HEAD"): 520 branches =p4BranchesInGit() 521# map from depot-path to branch name 522 branchByDepotPath = {} 523for branch in branches.keys(): 524 tip = branches[branch] 525 log =extractLogMessageFromGitCommit(tip) 526 settings =extractSettingsGitLog(log) 527if settings.has_key("depot-paths"): 528 paths =",".join(settings["depot-paths"]) 529 branchByDepotPath[paths] ="remotes/p4/"+ branch 530 531 settings =None 532 parent =0 533while parent <65535: 534 commit = head +"~%s"% parent 535 log =extractLogMessageFromGitCommit(commit) 536 settings =extractSettingsGitLog(log) 537if settings.has_key("depot-paths"): 538 paths =",".join(settings["depot-paths"]) 539if branchByDepotPath.has_key(paths): 540return[branchByDepotPath[paths], settings] 541 542 parent = parent +1 543 544return["", settings] 545 546defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 547if not silent: 548print("Creating/updating branch(es) in%sbased on origin branch(es)" 549% localRefPrefix) 550 551 originPrefix ="origin/p4/" 552 553for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 554 line = line.strip() 555if(not line.startswith(originPrefix))or line.endswith("HEAD"): 556continue 557 558 headName = line[len(originPrefix):] 559 remoteHead = localRefPrefix + headName 560 originHead = line 561 562 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 563if(not original.has_key('depot-paths') 564or not original.has_key('change')): 565continue 566 567 update =False 568if notgitBranchExists(remoteHead): 569if verbose: 570print"creating%s"% remoteHead 571 update =True 572else: 573 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 574if settings.has_key('change') >0: 575if settings['depot-paths'] == original['depot-paths']: 576 originP4Change =int(original['change']) 577 p4Change =int(settings['change']) 578if originP4Change > p4Change: 579print("%s(%s) is newer than%s(%s). " 580"Updating p4 branch from origin." 581% (originHead, originP4Change, 582 remoteHead, p4Change)) 583 update =True 584else: 585print("Ignoring:%swas imported from%swhile " 586"%swas imported from%s" 587% (originHead,','.join(original['depot-paths']), 588 remoteHead,','.join(settings['depot-paths']))) 589 590if update: 591system("git update-ref%s %s"% (remoteHead, originHead)) 592 593deforiginP4BranchesExist(): 594returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 595 596defp4ChangesForPaths(depotPaths, changeRange): 597assert depotPaths 598 cmd = ['changes'] 599for p in depotPaths: 600 cmd += ["%s...%s"% (p, changeRange)] 601 output =p4_read_pipe_lines(cmd) 602 603 changes = {} 604for line in output: 605 changeNum =int(line.split(" ")[1]) 606 changes[changeNum] =True 607 608 changelist = changes.keys() 609 changelist.sort() 610return changelist 611 612defp4PathStartsWith(path, prefix): 613# This method tries to remedy a potential mixed-case issue: 614# 615# If UserA adds //depot/DirA/file1 616# and UserB adds //depot/dira/file2 617# 618# we may or may not have a problem. If you have core.ignorecase=true, 619# we treat DirA and dira as the same directory 620 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 621if ignorecase: 622return path.lower().startswith(prefix.lower()) 623return path.startswith(prefix) 624 625defgetClientSpec(): 626"""Look at the p4 client spec, create a View() object that contains 627 all the mappings, and return it.""" 628 629 specList =p4CmdList("client -o") 630iflen(specList) !=1: 631die('Output from "client -o" is%dlines, expecting 1'% 632len(specList)) 633 634# dictionary of all client parameters 635 entry = specList[0] 636 637# just the keys that start with "View" 638 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 639 640# hold this new View 641 view =View() 642 643# append the lines, in order, to the view 644for view_num inrange(len(view_keys)): 645 k ="View%d"% view_num 646if k not in view_keys: 647die("Expected view key%smissing"% k) 648 view.append(entry[k]) 649 650return view 651 652defgetClientRoot(): 653"""Grab the client directory.""" 654 655 output =p4CmdList("client -o") 656iflen(output) !=1: 657die('Output from "client -o" is%dlines, expecting 1'%len(output)) 658 659 entry = output[0] 660if"Root"not in entry: 661die('Client has no "Root"') 662 663return entry["Root"] 664 665# 666# P4 wildcards are not allowed in filenames. P4 complains 667# if you simply add them, but you can force it with "-f", in 668# which case it translates them into %xx encoding internally. 669# 670defwildcard_decode(path): 671# Search for and fix just these four characters. Do % last so 672# that fixing it does not inadvertently create new %-escapes. 673# Cannot have * in a filename in windows; untested as to 674# what p4 would do in such a case. 675if not platform.system() =="Windows": 676 path = path.replace("%2A","*") 677 path = path.replace("%23","#") \ 678.replace("%40","@") \ 679.replace("%25","%") 680return path 681 682defwildcard_encode(path): 683# do % first to avoid double-encoding the %s introduced here 684 path = path.replace("%","%25") \ 685.replace("*","%2A") \ 686.replace("#","%23") \ 687.replace("@","%40") 688return path 689 690defwildcard_present(path): 691return path.translate(None,"*#@%") != path 692 693class Command: 694def__init__(self): 695 self.usage ="usage: %prog [options]" 696 self.needsGit =True 697 self.verbose =False 698 699class P4UserMap: 700def__init__(self): 701 self.userMapFromPerforceServer =False 702 self.myP4UserId =None 703 704defp4UserId(self): 705if self.myP4UserId: 706return self.myP4UserId 707 708 results =p4CmdList("user -o") 709for r in results: 710if r.has_key('User'): 711 self.myP4UserId = r['User'] 712return r['User'] 713die("Could not find your p4 user id") 714 715defp4UserIsMe(self, p4User): 716# return True if the given p4 user is actually me 717 me = self.p4UserId() 718if not p4User or p4User != me: 719return False 720else: 721return True 722 723defgetUserCacheFilename(self): 724 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 725return home +"/.gitp4-usercache.txt" 726 727defgetUserMapFromPerforceServer(self): 728if self.userMapFromPerforceServer: 729return 730 self.users = {} 731 self.emails = {} 732 733for output inp4CmdList("users"): 734if not output.has_key("User"): 735continue 736 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 737 self.emails[output["Email"]] = output["User"] 738 739 740 s ='' 741for(key, val)in self.users.items(): 742 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 743 744open(self.getUserCacheFilename(),"wb").write(s) 745 self.userMapFromPerforceServer =True 746 747defloadUserMapFromCache(self): 748 self.users = {} 749 self.userMapFromPerforceServer =False 750try: 751 cache =open(self.getUserCacheFilename(),"rb") 752 lines = cache.readlines() 753 cache.close() 754for line in lines: 755 entry = line.strip().split("\t") 756 self.users[entry[0]] = entry[1] 757exceptIOError: 758 self.getUserMapFromPerforceServer() 759 760classP4Debug(Command): 761def__init__(self): 762 Command.__init__(self) 763 self.options = [] 764 self.description ="A tool to debug the output of p4 -G." 765 self.needsGit =False 766 767defrun(self, args): 768 j =0 769for output inp4CmdList(args): 770print'Element:%d'% j 771 j +=1 772print output 773return True 774 775classP4RollBack(Command): 776def__init__(self): 777 Command.__init__(self) 778 self.options = [ 779 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 780] 781 self.description ="A tool to debug the multi-branch import. Don't use :)" 782 self.rollbackLocalBranches =False 783 784defrun(self, args): 785iflen(args) !=1: 786return False 787 maxChange =int(args[0]) 788 789if"p4ExitCode"inp4Cmd("changes -m 1"): 790die("Problems executing p4"); 791 792if self.rollbackLocalBranches: 793 refPrefix ="refs/heads/" 794 lines =read_pipe_lines("git rev-parse --symbolic --branches") 795else: 796 refPrefix ="refs/remotes/" 797 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 798 799for line in lines: 800if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 801 line = line.strip() 802 ref = refPrefix + line 803 log =extractLogMessageFromGitCommit(ref) 804 settings =extractSettingsGitLog(log) 805 806 depotPaths = settings['depot-paths'] 807 change = settings['change'] 808 809 changed =False 810 811iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 812for p in depotPaths]))) ==0: 813print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 814system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 815continue 816 817while change andint(change) > maxChange: 818 changed =True 819if self.verbose: 820print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 821system("git update-ref%s\"%s^\""% (ref, ref)) 822 log =extractLogMessageFromGitCommit(ref) 823 settings =extractSettingsGitLog(log) 824 825 826 depotPaths = settings['depot-paths'] 827 change = settings['change'] 828 829if changed: 830print"%srewound to%s"% (ref, change) 831 832return True 833 834classP4Submit(Command, P4UserMap): 835def__init__(self): 836 Command.__init__(self) 837 P4UserMap.__init__(self) 838 self.options = [ 839 optparse.make_option("--origin", dest="origin"), 840 optparse.make_option("-M", dest="detectRenames", action="store_true"), 841# preserve the user, requires relevant p4 permissions 842 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 843 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 844] 845 self.description ="Submit changes from git to the perforce depot." 846 self.usage +=" [name of git branch to submit into perforce depot]" 847 self.interactive =True 848 self.origin ="" 849 self.detectRenames =False 850 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 851 self.isWindows = (platform.system() =="Windows") 852 self.exportLabels =False 853 854defcheck(self): 855iflen(p4CmdList("opened ...")) >0: 856die("You have files opened with perforce! Close them before starting the sync.") 857 858# replaces everything between 'Description:' and the next P4 submit template field with the 859# commit message 860defprepareLogMessage(self, template, message): 861 result ="" 862 863 inDescriptionSection =False 864 865for line in template.split("\n"): 866if line.startswith("#"): 867 result += line +"\n" 868continue 869 870if inDescriptionSection: 871if line.startswith("Files:")or line.startswith("Jobs:"): 872 inDescriptionSection =False 873else: 874continue 875else: 876if line.startswith("Description:"): 877 inDescriptionSection =True 878 line +="\n" 879for messageLine in message.split("\n"): 880 line +="\t"+ messageLine +"\n" 881 882 result += line +"\n" 883 884return result 885 886defpatchRCSKeywords(self,file, pattern): 887# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern 888(handle, outFileName) = tempfile.mkstemp(dir='.') 889try: 890 outFile = os.fdopen(handle,"w+") 891 inFile =open(file,"r") 892 regexp = re.compile(pattern, re.VERBOSE) 893for line in inFile.readlines(): 894 line = regexp.sub(r'$\1$', line) 895 outFile.write(line) 896 inFile.close() 897 outFile.close() 898# Forcibly overwrite the original file 899 os.unlink(file) 900 shutil.move(outFileName,file) 901except: 902# cleanup our temporary file 903 os.unlink(outFileName) 904print"Failed to strip RCS keywords in%s"%file 905raise 906 907print"Patched up RCS keywords in%s"%file 908 909defp4UserForCommit(self,id): 910# Return the tuple (perforce user,git email) for a given git commit id 911 self.getUserMapFromPerforceServer() 912 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id) 913 gitEmail = gitEmail.strip() 914if not self.emails.has_key(gitEmail): 915return(None,gitEmail) 916else: 917return(self.emails[gitEmail],gitEmail) 918 919defcheckValidP4Users(self,commits): 920# check if any git authors cannot be mapped to p4 users 921foridin commits: 922(user,email) = self.p4UserForCommit(id) 923if not user: 924 msg ="Cannot find p4 user for email%sin commit%s."% (email,id) 925ifgitConfig('git-p4.allowMissingP4Users').lower() =="true": 926print"%s"% msg 927else: 928die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg) 929 930deflastP4Changelist(self): 931# Get back the last changelist number submitted in this client spec. This 932# then gets used to patch up the username in the change. If the same 933# client spec is being used by multiple processes then this might go 934# wrong. 935 results =p4CmdList("client -o")# find the current client 936 client =None 937for r in results: 938if r.has_key('Client'): 939 client = r['Client'] 940break 941if not client: 942die("could not get client spec") 943 results =p4CmdList(["changes","-c", client,"-m","1"]) 944for r in results: 945if r.has_key('change'): 946return r['change'] 947die("Could not get changelist number for last submit - cannot patch up user details") 948 949defmodifyChangelistUser(self, changelist, newUser): 950# fixup the user field of a changelist after it has been submitted. 951 changes =p4CmdList("change -o%s"% changelist) 952iflen(changes) !=1: 953die("Bad output from p4 change modifying%sto user%s"% 954(changelist, newUser)) 955 956 c = changes[0] 957if c['User'] == newUser:return# nothing to do 958 c['User'] = newUser 959input= marshal.dumps(c) 960 961 result =p4CmdList("change -f -i", stdin=input) 962for r in result: 963if r.has_key('code'): 964if r['code'] =='error': 965die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data'])) 966if r.has_key('data'): 967print("Updated user field for changelist%sto%s"% (changelist, newUser)) 968return 969die("Could not modify user field of changelist%sto%s"% (changelist, newUser)) 970 971defcanChangeChangelists(self): 972# check to see if we have p4 admin or super-user permissions, either of 973# which are required to modify changelists. 974 results =p4CmdList(["protects", self.depotPath]) 975for r in results: 976if r.has_key('perm'): 977if r['perm'] =='admin': 978return1 979if r['perm'] =='super': 980return1 981return0 982 983defprepareSubmitTemplate(self): 984# remove lines in the Files section that show changes to files outside the depot path we're committing into 985 template ="" 986 inFilesSection =False 987for line inp4_read_pipe_lines(['change','-o']): 988if line.endswith("\r\n"): 989 line = line[:-2] +"\n" 990if inFilesSection: 991if line.startswith("\t"): 992# path starts and ends with a tab 993 path = line[1:] 994 lastTab = path.rfind("\t") 995if lastTab != -1: 996 path = path[:lastTab] 997if notp4PathStartsWith(path, self.depotPath): 998continue 999else:1000 inFilesSection =False1001else:1002if line.startswith("Files:"):1003 inFilesSection =True10041005 template += line10061007return template10081009defedit_template(self, template_file):1010"""Invoke the editor to let the user change the submission1011 message. Return true if okay to continue with the submit."""10121013# if configured to skip the editing part, just submit1014ifgitConfig("git-p4.skipSubmitEdit") =="true":1015return True10161017# look at the modification time, to check later if the user saved1018# the file1019 mtime = os.stat(template_file).st_mtime10201021# invoke the editor1022if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1023 editor = os.environ.get("P4EDITOR")1024else:1025 editor =read_pipe("git var GIT_EDITOR").strip()1026system(editor +" "+ template_file)10271028# If the file was not saved, prompt to see if this patch should1029# be skipped. But skip this verification step if configured so.1030ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1031return True10321033# modification time updated means user saved the file1034if os.stat(template_file).st_mtime > mtime:1035return True10361037while True:1038 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1039if response =='y':1040return True1041if response =='n':1042return False10431044defapplyCommit(self,id):1045print"Applying%s"% (read_pipe("git log --max-count=1 --pretty=oneline%s"%id))10461047(p4User, gitEmail) = self.p4UserForCommit(id)10481049if not self.detectRenames:1050# If not explicitly set check the config variable1051 self.detectRenames =gitConfig("git-p4.detectRenames")10521053if self.detectRenames.lower() =="false"or self.detectRenames =="":1054 diffOpts =""1055elif self.detectRenames.lower() =="true":1056 diffOpts ="-M"1057else:1058 diffOpts ="-M%s"% self.detectRenames10591060 detectCopies =gitConfig("git-p4.detectCopies")1061if detectCopies.lower() =="true":1062 diffOpts +=" -C"1063elif detectCopies !=""and detectCopies.lower() !="false":1064 diffOpts +=" -C%s"% detectCopies10651066ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1067 diffOpts +=" --find-copies-harder"10681069 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (diffOpts,id,id))1070 filesToAdd =set()1071 filesToDelete =set()1072 editedFiles =set()1073 pureRenameCopy =set()1074 filesToChangeExecBit = {}10751076for line in diff:1077 diff =parseDiffTreeEntry(line)1078 modifier = diff['status']1079 path = diff['src']1080if modifier =="M":1081p4_edit(path)1082ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1083 filesToChangeExecBit[path] = diff['dst_mode']1084 editedFiles.add(path)1085elif modifier =="A":1086 filesToAdd.add(path)1087 filesToChangeExecBit[path] = diff['dst_mode']1088if path in filesToDelete:1089 filesToDelete.remove(path)1090elif modifier =="D":1091 filesToDelete.add(path)1092if path in filesToAdd:1093 filesToAdd.remove(path)1094elif modifier =="C":1095 src, dest = diff['src'], diff['dst']1096p4_integrate(src, dest)1097 pureRenameCopy.add(dest)1098if diff['src_sha1'] != diff['dst_sha1']:1099p4_edit(dest)1100 pureRenameCopy.discard(dest)1101ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1102p4_edit(dest)1103 pureRenameCopy.discard(dest)1104 filesToChangeExecBit[dest] = diff['dst_mode']1105 os.unlink(dest)1106 editedFiles.add(dest)1107elif modifier =="R":1108 src, dest = diff['src'], diff['dst']1109p4_integrate(src, dest)1110if diff['src_sha1'] != diff['dst_sha1']:1111p4_edit(dest)1112else:1113 pureRenameCopy.add(dest)1114ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1115p4_edit(dest)1116 filesToChangeExecBit[dest] = diff['dst_mode']1117 os.unlink(dest)1118 editedFiles.add(dest)1119 filesToDelete.add(src)1120else:1121die("unknown modifier%sfor%s"% (modifier, path))11221123 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1124 patchcmd = diffcmd +" | git apply "1125 tryPatchCmd = patchcmd +"--check -"1126 applyPatchCmd = patchcmd +"--check --apply -"1127 patch_succeeded =True11281129if os.system(tryPatchCmd) !=0:1130 fixed_rcs_keywords =False1131 patch_succeeded =False1132print"Unfortunately applying the change failed!"11331134# Patch failed, maybe it's just RCS keyword woes. Look through1135# the patch to see if that's possible.1136ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1137file=None1138 pattern =None1139 kwfiles = {}1140forfilein editedFiles | filesToDelete:1141# did this file's delta contain RCS keywords?1142 pattern =p4_keywords_regexp_for_file(file)11431144if pattern:1145# this file is a possibility...look for RCS keywords.1146 regexp = re.compile(pattern, re.VERBOSE)1147for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1148if regexp.search(line):1149if verbose:1150print"got keyword match on%sin%sin%s"% (pattern, line,file)1151 kwfiles[file] = pattern1152break11531154forfilein kwfiles:1155if verbose:1156print"zapping%swith%s"% (line,pattern)1157 self.patchRCSKeywords(file, kwfiles[file])1158 fixed_rcs_keywords =True11591160if fixed_rcs_keywords:1161print"Retrying the patch with RCS keywords cleaned up"1162if os.system(tryPatchCmd) ==0:1163 patch_succeeded =True11641165if not patch_succeeded:1166print"What do you want to do?"1167 response ="x"1168while response !="s"and response !="a"and response !="w":1169 response =raw_input("[s]kip this patch / [a]pply the patch forcibly "1170"and with .rej files / [w]rite the patch to a file (patch.txt) ")1171if response =="s":1172print"Skipping! Good luck with the next patches..."1173for f in editedFiles:1174p4_revert(f)1175for f in filesToAdd:1176 os.remove(f)1177return1178elif response =="a":1179 os.system(applyPatchCmd)1180iflen(filesToAdd) >0:1181print"You may also want to call p4 add on the following files:"1182print" ".join(filesToAdd)1183iflen(filesToDelete):1184print"The following files should be scheduled for deletion with p4 delete:"1185print" ".join(filesToDelete)1186die("Please resolve and submit the conflict manually and "1187+"continue afterwards with git p4 submit --continue")1188elif response =="w":1189system(diffcmd +" > patch.txt")1190print"Patch saved to patch.txt in%s!"% self.clientPath1191die("Please resolve and submit the conflict manually and "1192"continue afterwards with git p4 submit --continue")11931194system(applyPatchCmd)11951196for f in filesToAdd:1197p4_add(f)1198for f in filesToDelete:1199p4_revert(f)1200p4_delete(f)12011202# Set/clear executable bits1203for f in filesToChangeExecBit.keys():1204 mode = filesToChangeExecBit[f]1205setP4ExecBit(f, mode)12061207 logMessage =extractLogMessageFromGitCommit(id)1208 logMessage = logMessage.strip()12091210 template = self.prepareSubmitTemplate()12111212if self.interactive:1213 submitTemplate = self.prepareLogMessage(template, logMessage)12141215if self.preserveUser:1216 submitTemplate = submitTemplate + ("\n######## Actual user%s, modified after commit\n"% p4User)12171218if os.environ.has_key("P4DIFF"):1219del(os.environ["P4DIFF"])1220 diff =""1221for editedFile in editedFiles:1222 diff +=p4_read_pipe(['diff','-du',1223wildcard_encode(editedFile)])12241225 newdiff =""1226for newFile in filesToAdd:1227 newdiff +="==== new file ====\n"1228 newdiff +="--- /dev/null\n"1229 newdiff +="+++%s\n"% newFile1230 f =open(newFile,"r")1231for line in f.readlines():1232 newdiff +="+"+ line1233 f.close()12341235if self.checkAuthorship and not self.p4UserIsMe(p4User):1236 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1237 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1238 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"12391240 separatorLine ="######## everything below this line is just the diff #######\n"12411242(handle, fileName) = tempfile.mkstemp()1243 tmpFile = os.fdopen(handle,"w+")1244if self.isWindows:1245 submitTemplate = submitTemplate.replace("\n","\r\n")1246 separatorLine = separatorLine.replace("\n","\r\n")1247 newdiff = newdiff.replace("\n","\r\n")1248 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1249 tmpFile.close()12501251if self.edit_template(fileName):1252# read the edited message and submit1253 tmpFile =open(fileName,"rb")1254 message = tmpFile.read()1255 tmpFile.close()1256 submitTemplate = message[:message.index(separatorLine)]1257if self.isWindows:1258 submitTemplate = submitTemplate.replace("\r\n","\n")1259p4_write_pipe(['submit','-i'], submitTemplate)12601261if self.preserveUser:1262if p4User:1263# Get last changelist number. Cannot easily get it from1264# the submit command output as the output is1265# unmarshalled.1266 changelist = self.lastP4Changelist()1267 self.modifyChangelistUser(changelist, p4User)12681269# The rename/copy happened by applying a patch that created a1270# new file. This leaves it writable, which confuses p4.1271for f in pureRenameCopy:1272p4_sync(f,"-f")12731274else:1275# skip this patch1276print"Submission cancelled, undoing p4 changes."1277for f in editedFiles:1278p4_revert(f)1279for f in filesToAdd:1280p4_revert(f)1281 os.remove(f)12821283 os.remove(fileName)1284else:1285 fileName ="submit.txt"1286file=open(fileName,"w+")1287file.write(self.prepareLogMessage(template, logMessage))1288file.close()1289print("Perforce submit template written as%s. "1290+"Please review/edit and then use p4 submit -i <%sto submit directly!"1291% (fileName, fileName))12921293# Export git tags as p4 labels. Create a p4 label and then tag1294# with that.1295defexportGitTags(self, gitTags):1296 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1297iflen(validLabelRegexp) ==0:1298 validLabelRegexp = defaultLabelRegexp1299 m = re.compile(validLabelRegexp)13001301for name in gitTags:13021303if not m.match(name):1304if verbose:1305print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1306continue13071308# Get the p4 commit this corresponds to1309 logMessage =extractLogMessageFromGitCommit(name)1310 values =extractSettingsGitLog(logMessage)13111312if not values.has_key('change'):1313# a tag pointing to something not sent to p4; ignore1314if verbose:1315print"git tag%sdoes not give a p4 commit"% name1316continue1317else:1318 changelist = values['change']13191320# Get the tag details.1321 inHeader =True1322 isAnnotated =False1323 body = []1324for l inread_pipe_lines(["git","cat-file","-p", name]):1325 l = l.strip()1326if inHeader:1327if re.match(r'tag\s+', l):1328 isAnnotated =True1329elif re.match(r'\s*$', l):1330 inHeader =False1331continue1332else:1333 body.append(l)13341335if not isAnnotated:1336 body = ["lightweight tag imported by git p4\n"]13371338# Create the label - use the same view as the client spec we are using1339 clientSpec =getClientSpec()13401341 labelTemplate ="Label:%s\n"% name1342 labelTemplate +="Description:\n"1343for b in body:1344 labelTemplate +="\t"+ b +"\n"1345 labelTemplate +="View:\n"1346for mapping in clientSpec.mappings:1347 labelTemplate +="\t%s\n"% mapping.depot_side.path13481349p4_write_pipe(["label","-i"], labelTemplate)13501351# Use the label1352p4_system(["tag","-l", name] +1353["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])13541355if verbose:1356print"created p4 label for tag%s"% name13571358defrun(self, args):1359iflen(args) ==0:1360 self.master =currentGitBranch()1361iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1362die("Detecting current git branch failed!")1363eliflen(args) ==1:1364 self.master = args[0]1365if notbranchExists(self.master):1366die("Branch%sdoes not exist"% self.master)1367else:1368return False13691370 allowSubmit =gitConfig("git-p4.allowSubmit")1371iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1372die("%sis not in git-p4.allowSubmit"% self.master)13731374[upstream, settings] =findUpstreamBranchPoint()1375 self.depotPath = settings['depot-paths'][0]1376iflen(self.origin) ==0:1377 self.origin = upstream13781379if self.preserveUser:1380if not self.canChangeChangelists():1381die("Cannot preserve user names without p4 super-user or admin permissions")13821383if self.verbose:1384print"Origin branch is "+ self.origin13851386iflen(self.depotPath) ==0:1387print"Internal error: cannot locate perforce depot path from existing branches"1388 sys.exit(128)13891390 self.useClientSpec =False1391ifgitConfig("git-p4.useclientspec","--bool") =="true":1392 self.useClientSpec =True1393if self.useClientSpec:1394 self.clientSpecDirs =getClientSpec()13951396if self.useClientSpec:1397# all files are relative to the client spec1398 self.clientPath =getClientRoot()1399else:1400 self.clientPath =p4Where(self.depotPath)14011402if self.clientPath =="":1403die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)14041405print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1406 self.oldWorkingDirectory = os.getcwd()14071408# ensure the clientPath exists1409 new_client_dir =False1410if not os.path.exists(self.clientPath):1411 new_client_dir =True1412 os.makedirs(self.clientPath)14131414chdir(self.clientPath)1415print"Synchronizing p4 checkout..."1416if new_client_dir:1417# old one was destroyed, and maybe nobody told p41418p4_sync("...","-f")1419else:1420p4_sync("...")1421 self.check()14221423 commits = []1424for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1425 commits.append(line.strip())1426 commits.reverse()14271428if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1429 self.checkAuthorship =False1430else:1431 self.checkAuthorship =True14321433if self.preserveUser:1434 self.checkValidP4Users(commits)14351436whilelen(commits) >0:1437 commit = commits[0]1438 commits = commits[1:]1439 self.applyCommit(commit)1440if not self.interactive:1441break14421443iflen(commits) ==0:1444print"All changes applied!"1445chdir(self.oldWorkingDirectory)14461447 sync =P4Sync()1448 sync.run([])14491450 rebase =P4Rebase()1451 rebase.rebase()14521453ifgitConfig("git-p4.exportLabels","--bool") =="true":1454 self.exportLabels =True14551456if self.exportLabels:1457 p4Labels =getP4Labels(self.depotPath)1458 gitTags =getGitTags()14591460 missingGitTags = gitTags - p4Labels1461 self.exportGitTags(missingGitTags)14621463return True14641465classView(object):1466"""Represent a p4 view ("p4 help views"), and map files in a1467 repo according to the view."""14681469classPath(object):1470"""A depot or client path, possibly containing wildcards.1471 The only one supported is ... at the end, currently.1472 Initialize with the full path, with //depot or //client."""14731474def__init__(self, path, is_depot):1475 self.path = path1476 self.is_depot = is_depot1477 self.find_wildcards()1478# remember the prefix bit, useful for relative mappings1479 m = re.match("(//[^/]+/)", self.path)1480if not m:1481die("Path%sdoes not start with //prefix/"% self.path)1482 prefix = m.group(1)1483if not self.is_depot:1484# strip //client/ on client paths1485 self.path = self.path[len(prefix):]14861487deffind_wildcards(self):1488"""Make sure wildcards are valid, and set up internal1489 variables."""14901491 self.ends_triple_dot =False1492# There are three wildcards allowed in p4 views1493# (see "p4 help views"). This code knows how to1494# handle "..." (only at the end), but cannot deal with1495# "%%n" or "*". Only check the depot_side, as p4 should1496# validate that the client_side matches too.1497if re.search(r'%%[1-9]', self.path):1498die("Can't handle%%n wildcards in view:%s"% self.path)1499if self.path.find("*") >=0:1500die("Can't handle * wildcards in view:%s"% self.path)1501 triple_dot_index = self.path.find("...")1502if triple_dot_index >=0:1503if triple_dot_index !=len(self.path) -3:1504die("Can handle only single ... wildcard, at end:%s"%1505 self.path)1506 self.ends_triple_dot =True15071508defensure_compatible(self, other_path):1509"""Make sure the wildcards agree."""1510if self.ends_triple_dot != other_path.ends_triple_dot:1511die("Both paths must end with ... if either does;\n"+1512"paths:%s %s"% (self.path, other_path.path))15131514defmatch_wildcards(self, test_path):1515"""See if this test_path matches us, and fill in the value1516 of the wildcards if so. Returns a tuple of1517 (True|False, wildcards[]). For now, only the ... at end1518 is supported, so at most one wildcard."""1519if self.ends_triple_dot:1520 dotless = self.path[:-3]1521if test_path.startswith(dotless):1522 wildcard = test_path[len(dotless):]1523return(True, [ wildcard ])1524else:1525if test_path == self.path:1526return(True, [])1527return(False, [])15281529defmatch(self, test_path):1530"""Just return if it matches; don't bother with the wildcards."""1531 b, _ = self.match_wildcards(test_path)1532return b15331534deffill_in_wildcards(self, wildcards):1535"""Return the relative path, with the wildcards filled in1536 if there are any."""1537if self.ends_triple_dot:1538return self.path[:-3] + wildcards[0]1539else:1540return self.path15411542classMapping(object):1543def__init__(self, depot_side, client_side, overlay, exclude):1544# depot_side is without the trailing /... if it had one1545 self.depot_side = View.Path(depot_side, is_depot=True)1546 self.client_side = View.Path(client_side, is_depot=False)1547 self.overlay = overlay # started with "+"1548 self.exclude = exclude # started with "-"1549assert not(self.overlay and self.exclude)1550 self.depot_side.ensure_compatible(self.client_side)15511552def__str__(self):1553 c =" "1554if self.overlay:1555 c ="+"1556if self.exclude:1557 c ="-"1558return"View.Mapping:%s%s->%s"% \1559(c, self.depot_side.path, self.client_side.path)15601561defmap_depot_to_client(self, depot_path):1562"""Calculate the client path if using this mapping on the1563 given depot path; does not consider the effect of other1564 mappings in a view. Even excluded mappings are returned."""1565 matches, wildcards = self.depot_side.match_wildcards(depot_path)1566if not matches:1567return""1568 client_path = self.client_side.fill_in_wildcards(wildcards)1569return client_path15701571#1572# View methods1573#1574def__init__(self):1575 self.mappings = []15761577defappend(self, view_line):1578"""Parse a view line, splitting it into depot and client1579 sides. Append to self.mappings, preserving order."""15801581# Split the view line into exactly two words. P4 enforces1582# structure on these lines that simplifies this quite a bit.1583#1584# Either or both words may be double-quoted.1585# Single quotes do not matter.1586# Double-quote marks cannot occur inside the words.1587# A + or - prefix is also inside the quotes.1588# There are no quotes unless they contain a space.1589# The line is already white-space stripped.1590# The two words are separated by a single space.1591#1592if view_line[0] =='"':1593# First word is double quoted. Find its end.1594 close_quote_index = view_line.find('"',1)1595if close_quote_index <=0:1596die("No first-word closing quote found:%s"% view_line)1597 depot_side = view_line[1:close_quote_index]1598# skip closing quote and space1599 rhs_index = close_quote_index +1+11600else:1601 space_index = view_line.find(" ")1602if space_index <=0:1603die("No word-splitting space found:%s"% view_line)1604 depot_side = view_line[0:space_index]1605 rhs_index = space_index +116061607if view_line[rhs_index] =='"':1608# Second word is double quoted. Make sure there is a1609# double quote at the end too.1610if not view_line.endswith('"'):1611die("View line with rhs quote should end with one:%s"%1612 view_line)1613# skip the quotes1614 client_side = view_line[rhs_index+1:-1]1615else:1616 client_side = view_line[rhs_index:]16171618# prefix + means overlay on previous mapping1619 overlay =False1620if depot_side.startswith("+"):1621 overlay =True1622 depot_side = depot_side[1:]16231624# prefix - means exclude this path1625 exclude =False1626if depot_side.startswith("-"):1627 exclude =True1628 depot_side = depot_side[1:]16291630 m = View.Mapping(depot_side, client_side, overlay, exclude)1631 self.mappings.append(m)16321633defmap_in_client(self, depot_path):1634"""Return the relative location in the client where this1635 depot file should live. Returns "" if the file should1636 not be mapped in the client."""16371638 paths_filled = []1639 client_path =""16401641# look at later entries first1642for m in self.mappings[::-1]:16431644# see where will this path end up in the client1645 p = m.map_depot_to_client(depot_path)16461647if p =="":1648# Depot path does not belong in client. Must remember1649# this, as previous items should not cause files to1650# exist in this path either. Remember that the list is1651# being walked from the end, which has higher precedence.1652# Overlap mappings do not exclude previous mappings.1653if not m.overlay:1654 paths_filled.append(m.client_side)16551656else:1657# This mapping matched; no need to search any further.1658# But, the mapping could be rejected if the client path1659# has already been claimed by an earlier mapping (i.e.1660# one later in the list, which we are walking backwards).1661 already_mapped_in_client =False1662for f in paths_filled:1663# this is View.Path.match1664if f.match(p):1665 already_mapped_in_client =True1666break1667if not already_mapped_in_client:1668# Include this file, unless it is from a line that1669# explicitly said to exclude it.1670if not m.exclude:1671 client_path = p16721673# a match, even if rejected, always stops the search1674break16751676return client_path16771678classP4Sync(Command, P4UserMap):1679 delete_actions = ("delete","move/delete","purge")16801681def__init__(self):1682 Command.__init__(self)1683 P4UserMap.__init__(self)1684 self.options = [1685 optparse.make_option("--branch", dest="branch"),1686 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1687 optparse.make_option("--changesfile", dest="changesFile"),1688 optparse.make_option("--silent", dest="silent", action="store_true"),1689 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1690 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1691 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1692help="Import into refs/heads/ , not refs/remotes"),1693 optparse.make_option("--max-changes", dest="maxChanges"),1694 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1695help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1696 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1697help="Only sync files that are included in the Perforce Client Spec")1698]1699 self.description ="""Imports from Perforce into a git repository.\n1700 example:1701 //depot/my/project/ -- to import the current head1702 //depot/my/project/@all -- to import everything1703 //depot/my/project/@1,6 -- to import only from revision 1 to 617041705 (a ... is not needed in the path p4 specification, it's added implicitly)"""17061707 self.usage +=" //depot/path[@revRange]"1708 self.silent =False1709 self.createdBranches =set()1710 self.committedChanges =set()1711 self.branch =""1712 self.detectBranches =False1713 self.detectLabels =False1714 self.importLabels =False1715 self.changesFile =""1716 self.syncWithOrigin =True1717 self.importIntoRemotes =True1718 self.maxChanges =""1719 self.isWindows = (platform.system() =="Windows")1720 self.keepRepoPath =False1721 self.depotPaths =None1722 self.p4BranchesInGit = []1723 self.cloneExclude = []1724 self.useClientSpec =False1725 self.useClientSpec_from_options =False1726 self.clientSpecDirs =None1727 self.tempBranches = []1728 self.tempBranchLocation ="git-p4-tmp"17291730ifgitConfig("git-p4.syncFromOrigin") =="false":1731 self.syncWithOrigin =False17321733# Force a checkpoint in fast-import and wait for it to finish1734defcheckpoint(self):1735 self.gitStream.write("checkpoint\n\n")1736 self.gitStream.write("progress checkpoint\n\n")1737 out = self.gitOutput.readline()1738if self.verbose:1739print"checkpoint finished: "+ out17401741defextractFilesFromCommit(self, commit):1742 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1743for path in self.cloneExclude]1744 files = []1745 fnum =01746while commit.has_key("depotFile%s"% fnum):1747 path = commit["depotFile%s"% fnum]17481749if[p for p in self.cloneExclude1750ifp4PathStartsWith(path, p)]:1751 found =False1752else:1753 found = [p for p in self.depotPaths1754ifp4PathStartsWith(path, p)]1755if not found:1756 fnum = fnum +11757continue17581759file= {}1760file["path"] = path1761file["rev"] = commit["rev%s"% fnum]1762file["action"] = commit["action%s"% fnum]1763file["type"] = commit["type%s"% fnum]1764 files.append(file)1765 fnum = fnum +11766return files17671768defstripRepoPath(self, path, prefixes):1769if self.useClientSpec:1770return self.clientSpecDirs.map_in_client(path)17711772if self.keepRepoPath:1773 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]17741775for p in prefixes:1776ifp4PathStartsWith(path, p):1777 path = path[len(p):]17781779return path17801781defsplitFilesIntoBranches(self, commit):1782 branches = {}1783 fnum =01784while commit.has_key("depotFile%s"% fnum):1785 path = commit["depotFile%s"% fnum]1786 found = [p for p in self.depotPaths1787ifp4PathStartsWith(path, p)]1788if not found:1789 fnum = fnum +11790continue17911792file= {}1793file["path"] = path1794file["rev"] = commit["rev%s"% fnum]1795file["action"] = commit["action%s"% fnum]1796file["type"] = commit["type%s"% fnum]1797 fnum = fnum +117981799 relPath = self.stripRepoPath(path, self.depotPaths)1800 relPath =wildcard_decode(relPath)18011802for branch in self.knownBranches.keys():18031804# add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.21805if relPath.startswith(branch +"/"):1806if branch not in branches:1807 branches[branch] = []1808 branches[branch].append(file)1809break18101811return branches18121813# output one file from the P4 stream1814# - helper for streamP4Files18151816defstreamOneP4File(self,file, contents):1817 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)1818 relPath =wildcard_decode(relPath)1819if verbose:1820 sys.stderr.write("%s\n"% relPath)18211822(type_base, type_mods) =split_p4_type(file["type"])18231824 git_mode ="100644"1825if"x"in type_mods:1826 git_mode ="100755"1827if type_base =="symlink":1828 git_mode ="120000"1829# p4 print on a symlink contains "target\n"; remove the newline1830 data =''.join(contents)1831 contents = [data[:-1]]18321833if type_base =="utf16":1834# p4 delivers different text in the python output to -G1835# than it does when using "print -o", or normal p4 client1836# operations. utf16 is converted to ascii or utf8, perhaps.1837# But ascii text saved as -t utf16 is completely mangled.1838# Invoke print -o to get the real contents.1839 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])1840 contents = [ text ]18411842if type_base =="apple":1843# Apple filetype files will be streamed as a concatenation of1844# its appledouble header and the contents. This is useless1845# on both macs and non-macs. If using "print -q -o xx", it1846# will create "xx" with the data, and "%xx" with the header.1847# This is also not very useful.1848#1849# Ideally, someday, this script can learn how to generate1850# appledouble files directly and import those to git, but1851# non-mac machines can never find a use for apple filetype.1852print"\nIgnoring apple filetype file%s"%file['depotFile']1853return18541855# Perhaps windows wants unicode, utf16 newlines translated too;1856# but this is not doing it.1857if self.isWindows and type_base =="text":1858 mangled = []1859for data in contents:1860 data = data.replace("\r\n","\n")1861 mangled.append(data)1862 contents = mangled18631864# Note that we do not try to de-mangle keywords on utf16 files,1865# even though in theory somebody may want that.1866 pattern =p4_keywords_regexp_for_type(type_base, type_mods)1867if pattern:1868 regexp = re.compile(pattern, re.VERBOSE)1869 text =''.join(contents)1870 text = regexp.sub(r'$\1$', text)1871 contents = [ text ]18721873 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))18741875# total length...1876 length =01877for d in contents:1878 length = length +len(d)18791880 self.gitStream.write("data%d\n"% length)1881for d in contents:1882 self.gitStream.write(d)1883 self.gitStream.write("\n")18841885defstreamOneP4Deletion(self,file):1886 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)1887 relPath =wildcard_decode(relPath)1888if verbose:1889 sys.stderr.write("delete%s\n"% relPath)1890 self.gitStream.write("D%s\n"% relPath)18911892# handle another chunk of streaming data1893defstreamP4FilesCb(self, marshalled):18941895if marshalled.has_key('depotFile')and self.stream_have_file_info:1896# start of a new file - output the old one first1897 self.streamOneP4File(self.stream_file, self.stream_contents)1898 self.stream_file = {}1899 self.stream_contents = []1900 self.stream_have_file_info =False19011902# pick up the new file information... for the1903# 'data' field we need to append to our array1904for k in marshalled.keys():1905if k =='data':1906 self.stream_contents.append(marshalled['data'])1907else:1908 self.stream_file[k] = marshalled[k]19091910 self.stream_have_file_info =True19111912# Stream directly from "p4 files" into "git fast-import"1913defstreamP4Files(self, files):1914 filesForCommit = []1915 filesToRead = []1916 filesToDelete = []19171918for f in files:1919# if using a client spec, only add the files that have1920# a path in the client1921if self.clientSpecDirs:1922if self.clientSpecDirs.map_in_client(f['path']) =="":1923continue19241925 filesForCommit.append(f)1926if f['action']in self.delete_actions:1927 filesToDelete.append(f)1928else:1929 filesToRead.append(f)19301931# deleted files...1932for f in filesToDelete:1933 self.streamOneP4Deletion(f)19341935iflen(filesToRead) >0:1936 self.stream_file = {}1937 self.stream_contents = []1938 self.stream_have_file_info =False19391940# curry self argument1941defstreamP4FilesCbSelf(entry):1942 self.streamP4FilesCb(entry)19431944 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]19451946p4CmdList(["-x","-","print"],1947 stdin=fileArgs,1948 cb=streamP4FilesCbSelf)19491950# do the last chunk1951if self.stream_file.has_key('depotFile'):1952 self.streamOneP4File(self.stream_file, self.stream_contents)19531954defmake_email(self, userid):1955if userid in self.users:1956return self.users[userid]1957else:1958return"%s<a@b>"% userid19591960# Stream a p4 tag1961defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):1962if verbose:1963print"writing tag%sfor commit%s"% (labelName, commit)1964 gitStream.write("tag%s\n"% labelName)1965 gitStream.write("from%s\n"% commit)19661967if labelDetails.has_key('Owner'):1968 owner = labelDetails["Owner"]1969else:1970 owner =None19711972# Try to use the owner of the p4 label, or failing that,1973# the current p4 user id.1974if owner:1975 email = self.make_email(owner)1976else:1977 email = self.make_email(self.p4UserId())1978 tagger ="%s %s %s"% (email, epoch, self.tz)19791980 gitStream.write("tagger%s\n"% tagger)19811982print"labelDetails=",labelDetails1983if labelDetails.has_key('Description'):1984 description = labelDetails['Description']1985else:1986 description ='Label from git p4'19871988 gitStream.write("data%d\n"%len(description))1989 gitStream.write(description)1990 gitStream.write("\n")19911992defcommit(self, details, files, branch, branchPrefixes, parent =""):1993 epoch = details["time"]1994 author = details["user"]1995 self.branchPrefixes = branchPrefixes19961997if self.verbose:1998print"commit into%s"% branch19992000# start with reading files; if that fails, we should not2001# create a commit.2002 new_files = []2003for f in files:2004if[p for p in branchPrefixes ifp4PathStartsWith(f['path'], p)]:2005 new_files.append(f)2006else:2007 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])20082009 self.gitStream.write("commit%s\n"% branch)2010# gitStream.write("mark :%s\n" % details["change"])2011 self.committedChanges.add(int(details["change"]))2012 committer =""2013if author not in self.users:2014 self.getUserMapFromPerforceServer()2015 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)20162017 self.gitStream.write("committer%s\n"% committer)20182019 self.gitStream.write("data <<EOT\n")2020 self.gitStream.write(details["desc"])2021 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"2022% (','.join(branchPrefixes), details["change"]))2023iflen(details['options']) >0:2024 self.gitStream.write(": options =%s"% details['options'])2025 self.gitStream.write("]\nEOT\n\n")20262027iflen(parent) >0:2028if self.verbose:2029print"parent%s"% parent2030 self.gitStream.write("from%s\n"% parent)20312032 self.streamP4Files(new_files)2033 self.gitStream.write("\n")20342035 change =int(details["change"])20362037if self.labels.has_key(change):2038 label = self.labels[change]2039 labelDetails = label[0]2040 labelRevisions = label[1]2041if self.verbose:2042print"Change%sis labelled%s"% (change, labelDetails)20432044 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2045for p in branchPrefixes])20462047iflen(files) ==len(labelRevisions):20482049 cleanedFiles = {}2050for info in files:2051if info["action"]in self.delete_actions:2052continue2053 cleanedFiles[info["depotFile"]] = info["rev"]20542055if cleanedFiles == labelRevisions:2056 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)20572058else:2059if not self.silent:2060print("Tag%sdoes not match with change%s: files do not match."2061% (labelDetails["label"], change))20622063else:2064if not self.silent:2065print("Tag%sdoes not match with change%s: file count is different."2066% (labelDetails["label"], change))20672068# Build a dictionary of changelists and labels, for "detect-labels" option.2069defgetLabels(self):2070 self.labels = {}20712072 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2073iflen(l) >0and not self.silent:2074print"Finding files belonging to labels in%s"% `self.depotPaths`20752076for output in l:2077 label = output["label"]2078 revisions = {}2079 newestChange =02080if self.verbose:2081print"Querying files for label%s"% label2082forfileinp4CmdList(["files"] +2083["%s...@%s"% (p, label)2084for p in self.depotPaths]):2085 revisions[file["depotFile"]] =file["rev"]2086 change =int(file["change"])2087if change > newestChange:2088 newestChange = change20892090 self.labels[newestChange] = [output, revisions]20912092if self.verbose:2093print"Label changes:%s"% self.labels.keys()20942095# Import p4 labels as git tags. A direct mapping does not2096# exist, so assume that if all the files are at the same revision2097# then we can use that, or it's something more complicated we should2098# just ignore.2099defimportP4Labels(self, stream, p4Labels):2100if verbose:2101print"import p4 labels: "+' '.join(p4Labels)21022103 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2104 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2105iflen(validLabelRegexp) ==0:2106 validLabelRegexp = defaultLabelRegexp2107 m = re.compile(validLabelRegexp)21082109for name in p4Labels:2110 commitFound =False21112112if not m.match(name):2113if verbose:2114print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2115continue21162117if name in ignoredP4Labels:2118continue21192120 labelDetails =p4CmdList(['label',"-o", name])[0]21212122# get the most recent changelist for each file in this label2123 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2124for p in self.depotPaths])21252126if change.has_key('change'):2127# find the corresponding git commit; take the oldest commit2128 changelist =int(change['change'])2129 gitCommit =read_pipe(["git","rev-list","--max-count=1",2130"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2131iflen(gitCommit) ==0:2132print"could not find git commit for changelist%d"% changelist2133else:2134 gitCommit = gitCommit.strip()2135 commitFound =True2136# Convert from p4 time format2137try:2138 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2139exceptValueError:2140print"Could not convert label time%s"% labelDetail['Update']2141 tmwhen =121422143 when =int(time.mktime(tmwhen))2144 self.streamTag(stream, name, labelDetails, gitCommit, when)2145if verbose:2146print"p4 label%smapped to git commit%s"% (name, gitCommit)2147else:2148if verbose:2149print"Label%shas no changelists - possibly deleted?"% name21502151if not commitFound:2152# We can't import this label; don't try again as it will get very2153# expensive repeatedly fetching all the files for labels that will2154# never be imported. If the label is moved in the future, the2155# ignore will need to be removed manually.2156system(["git","config","--add","git-p4.ignoredP4Labels", name])21572158defguessProjectName(self):2159for p in self.depotPaths:2160if p.endswith("/"):2161 p = p[:-1]2162 p = p[p.strip().rfind("/") +1:]2163if not p.endswith("/"):2164 p +="/"2165return p21662167defgetBranchMapping(self):2168 lostAndFoundBranches =set()21692170 user =gitConfig("git-p4.branchUser")2171iflen(user) >0:2172 command ="branches -u%s"% user2173else:2174 command ="branches"21752176for info inp4CmdList(command):2177 details =p4Cmd(["branch","-o", info["branch"]])2178 viewIdx =02179while details.has_key("View%s"% viewIdx):2180 paths = details["View%s"% viewIdx].split(" ")2181 viewIdx = viewIdx +12182# require standard //depot/foo/... //depot/bar/... mapping2183iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2184continue2185 source = paths[0]2186 destination = paths[1]2187## HACK2188ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2189 source = source[len(self.depotPaths[0]):-4]2190 destination = destination[len(self.depotPaths[0]):-4]21912192if destination in self.knownBranches:2193if not self.silent:2194print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2195print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2196continue21972198 self.knownBranches[destination] = source21992200 lostAndFoundBranches.discard(destination)22012202if source not in self.knownBranches:2203 lostAndFoundBranches.add(source)22042205# Perforce does not strictly require branches to be defined, so we also2206# check git config for a branch list.2207#2208# Example of branch definition in git config file:2209# [git-p4]2210# branchList=main:branchA2211# branchList=main:branchB2212# branchList=branchA:branchC2213 configBranches =gitConfigList("git-p4.branchList")2214for branch in configBranches:2215if branch:2216(source, destination) = branch.split(":")2217 self.knownBranches[destination] = source22182219 lostAndFoundBranches.discard(destination)22202221if source not in self.knownBranches:2222 lostAndFoundBranches.add(source)222322242225for branch in lostAndFoundBranches:2226 self.knownBranches[branch] = branch22272228defgetBranchMappingFromGitBranches(self):2229 branches =p4BranchesInGit(self.importIntoRemotes)2230for branch in branches.keys():2231if branch =="master":2232 branch ="main"2233else:2234 branch = branch[len(self.projectName):]2235 self.knownBranches[branch] = branch22362237deflistExistingP4GitBranches(self):2238# branches holds mapping from name to commit2239 branches =p4BranchesInGit(self.importIntoRemotes)2240 self.p4BranchesInGit = branches.keys()2241for branch in branches.keys():2242 self.initialParents[self.refPrefix + branch] = branches[branch]22432244defupdateOptionDict(self, d):2245 option_keys = {}2246if self.keepRepoPath:2247 option_keys['keepRepoPath'] =122482249 d["options"] =' '.join(sorted(option_keys.keys()))22502251defreadOptions(self, d):2252 self.keepRepoPath = (d.has_key('options')2253and('keepRepoPath'in d['options']))22542255defgitRefForBranch(self, branch):2256if branch =="main":2257return self.refPrefix +"master"22582259iflen(branch) <=0:2260return branch22612262return self.refPrefix + self.projectName + branch22632264defgitCommitByP4Change(self, ref, change):2265if self.verbose:2266print"looking in ref "+ ref +" for change%susing bisect..."% change22672268 earliestCommit =""2269 latestCommit =parseRevision(ref)22702271while True:2272if self.verbose:2273print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2274 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2275iflen(next) ==0:2276if self.verbose:2277print"argh"2278return""2279 log =extractLogMessageFromGitCommit(next)2280 settings =extractSettingsGitLog(log)2281 currentChange =int(settings['change'])2282if self.verbose:2283print"current change%s"% currentChange22842285if currentChange == change:2286if self.verbose:2287print"found%s"% next2288return next22892290if currentChange < change:2291 earliestCommit ="^%s"% next2292else:2293 latestCommit ="%s"% next22942295return""22962297defimportNewBranch(self, branch, maxChange):2298# make fast-import flush all changes to disk and update the refs using the checkpoint2299# command so that we can try to find the branch parent in the git history2300 self.gitStream.write("checkpoint\n\n");2301 self.gitStream.flush();2302 branchPrefix = self.depotPaths[0] + branch +"/"2303range="@1,%s"% maxChange2304#print "prefix" + branchPrefix2305 changes =p4ChangesForPaths([branchPrefix],range)2306iflen(changes) <=0:2307return False2308 firstChange = changes[0]2309#print "first change in branch: %s" % firstChange2310 sourceBranch = self.knownBranches[branch]2311 sourceDepotPath = self.depotPaths[0] + sourceBranch2312 sourceRef = self.gitRefForBranch(sourceBranch)2313#print "source " + sourceBranch23142315 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2316#print "branch parent: %s" % branchParentChange2317 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2318iflen(gitParent) >0:2319 self.initialParents[self.gitRefForBranch(branch)] = gitParent2320#print "parent git commit: %s" % gitParent23212322 self.importChanges(changes)2323return True23242325defsearchParent(self, parent, branch, target):2326 parentFound =False2327for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2328 blob = blob.strip()2329iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2330 parentFound =True2331if self.verbose:2332print"Found parent of%sin commit%s"% (branch, blob)2333break2334if parentFound:2335return blob2336else:2337return None23382339defimportChanges(self, changes):2340 cnt =12341for change in changes:2342 description =p4Cmd(["describe",str(change)])2343 self.updateOptionDict(description)23442345if not self.silent:2346 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2347 sys.stdout.flush()2348 cnt = cnt +123492350try:2351if self.detectBranches:2352 branches = self.splitFilesIntoBranches(description)2353for branch in branches.keys():2354## HACK --hwn2355 branchPrefix = self.depotPaths[0] + branch +"/"23562357 parent =""23582359 filesForCommit = branches[branch]23602361if self.verbose:2362print"branch is%s"% branch23632364 self.updatedBranches.add(branch)23652366if branch not in self.createdBranches:2367 self.createdBranches.add(branch)2368 parent = self.knownBranches[branch]2369if parent == branch:2370 parent =""2371else:2372 fullBranch = self.projectName + branch2373if fullBranch not in self.p4BranchesInGit:2374if not self.silent:2375print("\nImporting new branch%s"% fullBranch);2376if self.importNewBranch(branch, change -1):2377 parent =""2378 self.p4BranchesInGit.append(fullBranch)2379if not self.silent:2380print("\nResuming with change%s"% change);23812382if self.verbose:2383print"parent determined through known branches:%s"% parent23842385 branch = self.gitRefForBranch(branch)2386 parent = self.gitRefForBranch(parent)23872388if self.verbose:2389print"looking for initial parent for%s; current parent is%s"% (branch, parent)23902391iflen(parent) ==0and branch in self.initialParents:2392 parent = self.initialParents[branch]2393del self.initialParents[branch]23942395 blob =None2396iflen(parent) >0:2397 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2398if self.verbose:2399print"Creating temporary branch: "+ tempBranch2400 self.commit(description, filesForCommit, tempBranch, [branchPrefix])2401 self.tempBranches.append(tempBranch)2402 self.checkpoint()2403 blob = self.searchParent(parent, branch, tempBranch)2404if blob:2405 self.commit(description, filesForCommit, branch, [branchPrefix], blob)2406else:2407if self.verbose:2408print"Parent of%snot found. Committing into head of%s"% (branch, parent)2409 self.commit(description, filesForCommit, branch, [branchPrefix], parent)2410else:2411 files = self.extractFilesFromCommit(description)2412 self.commit(description, files, self.branch, self.depotPaths,2413 self.initialParent)2414 self.initialParent =""2415exceptIOError:2416print self.gitError.read()2417 sys.exit(1)24182419defimportHeadRevision(self, revision):2420print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)24212422 details = {}2423 details["user"] ="git perforce import user"2424 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2425% (' '.join(self.depotPaths), revision))2426 details["change"] = revision2427 newestRevision =024282429 fileCnt =02430 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]24312432for info inp4CmdList(["files"] + fileArgs):24332434if'code'in info and info['code'] =='error':2435 sys.stderr.write("p4 returned an error:%s\n"2436% info['data'])2437if info['data'].find("must refer to client") >=0:2438 sys.stderr.write("This particular p4 error is misleading.\n")2439 sys.stderr.write("Perhaps the depot path was misspelled.\n");2440 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2441 sys.exit(1)2442if'p4ExitCode'in info:2443 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2444 sys.exit(1)244524462447 change =int(info["change"])2448if change > newestRevision:2449 newestRevision = change24502451if info["action"]in self.delete_actions:2452# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2453#fileCnt = fileCnt + 12454continue24552456for prop in["depotFile","rev","action","type"]:2457 details["%s%s"% (prop, fileCnt)] = info[prop]24582459 fileCnt = fileCnt +124602461 details["change"] = newestRevision24622463# Use time from top-most change so that all git p4 clones of2464# the same p4 repo have the same commit SHA1s.2465 res =p4CmdList("describe -s%d"% newestRevision)2466 newestTime =None2467for r in res:2468if r.has_key('time'):2469 newestTime =int(r['time'])2470if newestTime is None:2471die("\"describe -s\"on newest change%ddid not give a time")2472 details["time"] = newestTime24732474 self.updateOptionDict(details)2475try:2476 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)2477exceptIOError:2478print"IO error with git fast-import. Is your git version recent enough?"2479print self.gitError.read()248024812482defrun(self, args):2483 self.depotPaths = []2484 self.changeRange =""2485 self.initialParent =""2486 self.previousDepotPaths = []24872488# map from branch depot path to parent branch2489 self.knownBranches = {}2490 self.initialParents = {}2491 self.hasOrigin =originP4BranchesExist()2492if not self.syncWithOrigin:2493 self.hasOrigin =False24942495if self.importIntoRemotes:2496 self.refPrefix ="refs/remotes/p4/"2497else:2498 self.refPrefix ="refs/heads/p4/"24992500if self.syncWithOrigin and self.hasOrigin:2501if not self.silent:2502print"Syncing with origin first by calling git fetch origin"2503system("git fetch origin")25042505iflen(self.branch) ==0:2506 self.branch = self.refPrefix +"master"2507ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2508system("git update-ref%srefs/heads/p4"% self.branch)2509system("git branch -D p4");2510# create it /after/ importing, when master exists2511if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2512system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))25132514# accept either the command-line option, or the configuration variable2515if self.useClientSpec:2516# will use this after clone to set the variable2517 self.useClientSpec_from_options =True2518else:2519ifgitConfig("git-p4.useclientspec","--bool") =="true":2520 self.useClientSpec =True2521if self.useClientSpec:2522 self.clientSpecDirs =getClientSpec()25232524# TODO: should always look at previous commits,2525# merge with previous imports, if possible.2526if args == []:2527if self.hasOrigin:2528createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2529 self.listExistingP4GitBranches()25302531iflen(self.p4BranchesInGit) >1:2532if not self.silent:2533print"Importing from/into multiple branches"2534 self.detectBranches =True25352536if self.verbose:2537print"branches:%s"% self.p4BranchesInGit25382539 p4Change =02540for branch in self.p4BranchesInGit:2541 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)25422543 settings =extractSettingsGitLog(logMsg)25442545 self.readOptions(settings)2546if(settings.has_key('depot-paths')2547and settings.has_key('change')):2548 change =int(settings['change']) +12549 p4Change =max(p4Change, change)25502551 depotPaths =sorted(settings['depot-paths'])2552if self.previousDepotPaths == []:2553 self.previousDepotPaths = depotPaths2554else:2555 paths = []2556for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2557 prev_list = prev.split("/")2558 cur_list = cur.split("/")2559for i inrange(0,min(len(cur_list),len(prev_list))):2560if cur_list[i] <> prev_list[i]:2561 i = i -12562break25632564 paths.append("/".join(cur_list[:i +1]))25652566 self.previousDepotPaths = paths25672568if p4Change >0:2569 self.depotPaths =sorted(self.previousDepotPaths)2570 self.changeRange ="@%s,#head"% p4Change2571if not self.detectBranches:2572 self.initialParent =parseRevision(self.branch)2573if not self.silent and not self.detectBranches:2574print"Performing incremental import into%sgit branch"% self.branch25752576if not self.branch.startswith("refs/"):2577 self.branch ="refs/heads/"+ self.branch25782579iflen(args) ==0and self.depotPaths:2580if not self.silent:2581print"Depot paths:%s"%' '.join(self.depotPaths)2582else:2583if self.depotPaths and self.depotPaths != args:2584print("previous import used depot path%sand now%swas specified. "2585"This doesn't work!"% (' '.join(self.depotPaths),2586' '.join(args)))2587 sys.exit(1)25882589 self.depotPaths =sorted(args)25902591 revision =""2592 self.users = {}25932594# Make sure no revision specifiers are used when --changesfile2595# is specified.2596 bad_changesfile =False2597iflen(self.changesFile) >0:2598for p in self.depotPaths:2599if p.find("@") >=0or p.find("#") >=0:2600 bad_changesfile =True2601break2602if bad_changesfile:2603die("Option --changesfile is incompatible with revision specifiers")26042605 newPaths = []2606for p in self.depotPaths:2607if p.find("@") != -1:2608 atIdx = p.index("@")2609 self.changeRange = p[atIdx:]2610if self.changeRange =="@all":2611 self.changeRange =""2612elif','not in self.changeRange:2613 revision = self.changeRange2614 self.changeRange =""2615 p = p[:atIdx]2616elif p.find("#") != -1:2617 hashIdx = p.index("#")2618 revision = p[hashIdx:]2619 p = p[:hashIdx]2620elif self.previousDepotPaths == []:2621# pay attention to changesfile, if given, else import2622# the entire p4 tree at the head revision2623iflen(self.changesFile) ==0:2624 revision ="#head"26252626 p = re.sub("\.\.\.$","", p)2627if not p.endswith("/"):2628 p +="/"26292630 newPaths.append(p)26312632 self.depotPaths = newPaths26332634 self.loadUserMapFromCache()2635 self.labels = {}2636if self.detectLabels:2637 self.getLabels();26382639if self.detectBranches:2640## FIXME - what's a P4 projectName ?2641 self.projectName = self.guessProjectName()26422643if self.hasOrigin:2644 self.getBranchMappingFromGitBranches()2645else:2646 self.getBranchMapping()2647if self.verbose:2648print"p4-git branches:%s"% self.p4BranchesInGit2649print"initial parents:%s"% self.initialParents2650for b in self.p4BranchesInGit:2651if b !="master":26522653## FIXME2654 b = b[len(self.projectName):]2655 self.createdBranches.add(b)26562657 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))26582659 importProcess = subprocess.Popen(["git","fast-import"],2660 stdin=subprocess.PIPE, stdout=subprocess.PIPE,2661 stderr=subprocess.PIPE);2662 self.gitOutput = importProcess.stdout2663 self.gitStream = importProcess.stdin2664 self.gitError = importProcess.stderr26652666if revision:2667 self.importHeadRevision(revision)2668else:2669 changes = []26702671iflen(self.changesFile) >0:2672 output =open(self.changesFile).readlines()2673 changeSet =set()2674for line in output:2675 changeSet.add(int(line))26762677for change in changeSet:2678 changes.append(change)26792680 changes.sort()2681else:2682# catch "git p4 sync" with no new branches, in a repo that2683# does not have any existing p4 branches2684iflen(args) ==0and not self.p4BranchesInGit:2685die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2686if self.verbose:2687print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2688 self.changeRange)2689 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)26902691iflen(self.maxChanges) >0:2692 changes = changes[:min(int(self.maxChanges),len(changes))]26932694iflen(changes) ==0:2695if not self.silent:2696print"No changes to import!"2697else:2698if not self.silent and not self.detectBranches:2699print"Import destination:%s"% self.branch27002701 self.updatedBranches =set()27022703 self.importChanges(changes)27042705if not self.silent:2706print""2707iflen(self.updatedBranches) >0:2708 sys.stdout.write("Updated branches: ")2709for b in self.updatedBranches:2710 sys.stdout.write("%s"% b)2711 sys.stdout.write("\n")27122713ifgitConfig("git-p4.importLabels","--bool") =="true":2714 self.importLabels =True27152716if self.importLabels:2717 p4Labels =getP4Labels(self.depotPaths)2718 gitTags =getGitTags()27192720 missingP4Labels = p4Labels - gitTags2721 self.importP4Labels(self.gitStream, missingP4Labels)27222723 self.gitStream.close()2724if importProcess.wait() !=0:2725die("fast-import failed:%s"% self.gitError.read())2726 self.gitOutput.close()2727 self.gitError.close()27282729# Cleanup temporary branches created during import2730if self.tempBranches != []:2731for branch in self.tempBranches:2732read_pipe("git update-ref -d%s"% branch)2733 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))27342735return True27362737classP4Rebase(Command):2738def__init__(self):2739 Command.__init__(self)2740 self.options = [2741 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2742]2743 self.importLabels =False2744 self.description = ("Fetches the latest revision from perforce and "2745+"rebases the current work (branch) against it")27462747defrun(self, args):2748 sync =P4Sync()2749 sync.importLabels = self.importLabels2750 sync.run([])27512752return self.rebase()27532754defrebase(self):2755if os.system("git update-index --refresh") !=0:2756die("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.");2757iflen(read_pipe("git diff-index HEAD --")) >0:2758die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");27592760[upstream, settings] =findUpstreamBranchPoint()2761iflen(upstream) ==0:2762die("Cannot find upstream branchpoint for rebase")27632764# the branchpoint may be p4/foo~3, so strip off the parent2765 upstream = re.sub("~[0-9]+$","", upstream)27662767print"Rebasing the current branch onto%s"% upstream2768 oldHead =read_pipe("git rev-parse HEAD").strip()2769system("git rebase%s"% upstream)2770system("git diff-tree --stat --summary -M%sHEAD"% oldHead)2771return True27722773classP4Clone(P4Sync):2774def__init__(self):2775 P4Sync.__init__(self)2776 self.description ="Creates a new git repository and imports from Perforce into it"2777 self.usage ="usage: %prog [options] //depot/path[@revRange]"2778 self.options += [2779 optparse.make_option("--destination", dest="cloneDestination",2780 action='store', default=None,2781help="where to leave result of the clone"),2782 optparse.make_option("-/", dest="cloneExclude",2783 action="append",type="string",2784help="exclude depot path"),2785 optparse.make_option("--bare", dest="cloneBare",2786 action="store_true", default=False),2787]2788 self.cloneDestination =None2789 self.needsGit =False2790 self.cloneBare =False27912792# This is required for the "append" cloneExclude action2793defensure_value(self, attr, value):2794if nothasattr(self, attr)orgetattr(self, attr)is None:2795setattr(self, attr, value)2796returngetattr(self, attr)27972798defdefaultDestination(self, args):2799## TODO: use common prefix of args?2800 depotPath = args[0]2801 depotDir = re.sub("(@[^@]*)$","", depotPath)2802 depotDir = re.sub("(#[^#]*)$","", depotDir)2803 depotDir = re.sub(r"\.\.\.$","", depotDir)2804 depotDir = re.sub(r"/$","", depotDir)2805return os.path.split(depotDir)[1]28062807defrun(self, args):2808iflen(args) <1:2809return False28102811if self.keepRepoPath and not self.cloneDestination:2812 sys.stderr.write("Must specify destination for --keep-path\n")2813 sys.exit(1)28142815 depotPaths = args28162817if not self.cloneDestination andlen(depotPaths) >1:2818 self.cloneDestination = depotPaths[-1]2819 depotPaths = depotPaths[:-1]28202821 self.cloneExclude = ["/"+p for p in self.cloneExclude]2822for p in depotPaths:2823if not p.startswith("//"):2824return False28252826if not self.cloneDestination:2827 self.cloneDestination = self.defaultDestination(args)28282829print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)28302831if not os.path.exists(self.cloneDestination):2832 os.makedirs(self.cloneDestination)2833chdir(self.cloneDestination)28342835 init_cmd = ["git","init"]2836if self.cloneBare:2837 init_cmd.append("--bare")2838 subprocess.check_call(init_cmd)28392840if not P4Sync.run(self, depotPaths):2841return False2842if self.branch !="master":2843if self.importIntoRemotes:2844 masterbranch ="refs/remotes/p4/master"2845else:2846 masterbranch ="refs/heads/p4/master"2847ifgitBranchExists(masterbranch):2848system("git branch master%s"% masterbranch)2849if not self.cloneBare:2850system("git checkout -f")2851else:2852print"Could not detect main branch. No checkout/master branch created."28532854# auto-set this variable if invoked with --use-client-spec2855if self.useClientSpec_from_options:2856system("git config --bool git-p4.useclientspec true")28572858return True28592860classP4Branches(Command):2861def__init__(self):2862 Command.__init__(self)2863 self.options = [ ]2864 self.description = ("Shows the git branches that hold imports and their "2865+"corresponding perforce depot paths")2866 self.verbose =False28672868defrun(self, args):2869iforiginP4BranchesExist():2870createOrUpdateBranchesFromOrigin()28712872 cmdline ="git rev-parse --symbolic "2873 cmdline +=" --remotes"28742875for line inread_pipe_lines(cmdline):2876 line = line.strip()28772878if not line.startswith('p4/')or line =="p4/HEAD":2879continue2880 branch = line28812882 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)2883 settings =extractSettingsGitLog(log)28842885print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])2886return True28872888classHelpFormatter(optparse.IndentedHelpFormatter):2889def__init__(self):2890 optparse.IndentedHelpFormatter.__init__(self)28912892defformat_description(self, description):2893if description:2894return description +"\n"2895else:2896return""28972898defprintUsage(commands):2899print"usage:%s<command> [options]"% sys.argv[0]2900print""2901print"valid commands:%s"%", ".join(commands)2902print""2903print"Try%s<command> --help for command specific help."% sys.argv[0]2904print""29052906commands = {2907"debug": P4Debug,2908"submit": P4Submit,2909"commit": P4Submit,2910"sync": P4Sync,2911"rebase": P4Rebase,2912"clone": P4Clone,2913"rollback": P4RollBack,2914"branches": P4Branches2915}291629172918defmain():2919iflen(sys.argv[1:]) ==0:2920printUsage(commands.keys())2921 sys.exit(2)29222923 cmd =""2924 cmdName = sys.argv[1]2925try:2926 klass = commands[cmdName]2927 cmd =klass()2928exceptKeyError:2929print"unknown command%s"% cmdName2930print""2931printUsage(commands.keys())2932 sys.exit(2)29332934 options = cmd.options2935 cmd.gitdir = os.environ.get("GIT_DIR",None)29362937 args = sys.argv[2:]29382939 options.append(optparse.make_option("--verbose", dest="verbose", action="store_true"))2940if cmd.needsGit:2941 options.append(optparse.make_option("--git-dir", dest="gitdir"))29422943 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),2944 options,2945 description = cmd.description,2946 formatter =HelpFormatter())29472948(cmd, args) = parser.parse_args(sys.argv[2:], cmd);2949global verbose2950 verbose = cmd.verbose2951if cmd.needsGit:2952if cmd.gitdir ==None:2953 cmd.gitdir = os.path.abspath(".git")2954if notisValidGitDir(cmd.gitdir):2955 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()2956if os.path.exists(cmd.gitdir):2957 cdup =read_pipe("git rev-parse --show-cdup").strip()2958iflen(cdup) >0:2959chdir(cdup);29602961if notisValidGitDir(cmd.gitdir):2962ifisValidGitDir(cmd.gitdir +"/.git"):2963 cmd.gitdir +="/.git"2964else:2965die("fatal: cannot locate git repository at%s"% cmd.gitdir)29662967 os.environ["GIT_DIR"] = cmd.gitdir29682969if not cmd.run(args):2970 parser.print_help()2971 sys.exit(2)297229732974if __name__ =='__main__':2975main()