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.origin ="" 848 self.detectRenames =False 849 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 850 self.isWindows = (platform.system() =="Windows") 851 self.exportLabels =False 852 853defcheck(self): 854iflen(p4CmdList("opened ...")) >0: 855die("You have files opened with perforce! Close them before starting the sync.") 856 857defseparate_jobs_from_description(self, message): 858"""Extract and return a possible Jobs field in the commit 859 message. It goes into a separate section in the p4 change 860 specification. 861 862 A jobs line starts with "Jobs:" and looks like a new field 863 in a form. Values are white-space separated on the same 864 line or on following lines that start with a tab. 865 866 This does not parse and extract the full git commit message 867 like a p4 form. It just sees the Jobs: line as a marker 868 to pass everything from then on directly into the p4 form, 869 but outside the description section. 870 871 Return a tuple (stripped log message, jobs string).""" 872 873 m = re.search(r'^Jobs:', message, re.MULTILINE) 874if m is None: 875return(message,None) 876 877 jobtext = message[m.start():] 878 stripped_message = message[:m.start()].rstrip() 879return(stripped_message, jobtext) 880 881defprepareLogMessage(self, template, message, jobs): 882"""Edits the template returned from "p4 change -o" to insert 883 the message in the Description field, and the jobs text in 884 the Jobs field.""" 885 result ="" 886 887 inDescriptionSection =False 888 889for line in template.split("\n"): 890if line.startswith("#"): 891 result += line +"\n" 892continue 893 894if inDescriptionSection: 895if line.startswith("Files:")or line.startswith("Jobs:"): 896 inDescriptionSection =False 897# insert Jobs section 898if jobs: 899 result += jobs +"\n" 900else: 901continue 902else: 903if line.startswith("Description:"): 904 inDescriptionSection =True 905 line +="\n" 906for messageLine in message.split("\n"): 907 line +="\t"+ messageLine +"\n" 908 909 result += line +"\n" 910 911return result 912 913defpatchRCSKeywords(self,file, pattern): 914# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern 915(handle, outFileName) = tempfile.mkstemp(dir='.') 916try: 917 outFile = os.fdopen(handle,"w+") 918 inFile =open(file,"r") 919 regexp = re.compile(pattern, re.VERBOSE) 920for line in inFile.readlines(): 921 line = regexp.sub(r'$\1$', line) 922 outFile.write(line) 923 inFile.close() 924 outFile.close() 925# Forcibly overwrite the original file 926 os.unlink(file) 927 shutil.move(outFileName,file) 928except: 929# cleanup our temporary file 930 os.unlink(outFileName) 931print"Failed to strip RCS keywords in%s"%file 932raise 933 934print"Patched up RCS keywords in%s"%file 935 936defp4UserForCommit(self,id): 937# Return the tuple (perforce user,git email) for a given git commit id 938 self.getUserMapFromPerforceServer() 939 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id) 940 gitEmail = gitEmail.strip() 941if not self.emails.has_key(gitEmail): 942return(None,gitEmail) 943else: 944return(self.emails[gitEmail],gitEmail) 945 946defcheckValidP4Users(self,commits): 947# check if any git authors cannot be mapped to p4 users 948foridin commits: 949(user,email) = self.p4UserForCommit(id) 950if not user: 951 msg ="Cannot find p4 user for email%sin commit%s."% (email,id) 952ifgitConfig('git-p4.allowMissingP4Users').lower() =="true": 953print"%s"% msg 954else: 955die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg) 956 957deflastP4Changelist(self): 958# Get back the last changelist number submitted in this client spec. This 959# then gets used to patch up the username in the change. If the same 960# client spec is being used by multiple processes then this might go 961# wrong. 962 results =p4CmdList("client -o")# find the current client 963 client =None 964for r in results: 965if r.has_key('Client'): 966 client = r['Client'] 967break 968if not client: 969die("could not get client spec") 970 results =p4CmdList(["changes","-c", client,"-m","1"]) 971for r in results: 972if r.has_key('change'): 973return r['change'] 974die("Could not get changelist number for last submit - cannot patch up user details") 975 976defmodifyChangelistUser(self, changelist, newUser): 977# fixup the user field of a changelist after it has been submitted. 978 changes =p4CmdList("change -o%s"% changelist) 979iflen(changes) !=1: 980die("Bad output from p4 change modifying%sto user%s"% 981(changelist, newUser)) 982 983 c = changes[0] 984if c['User'] == newUser:return# nothing to do 985 c['User'] = newUser 986input= marshal.dumps(c) 987 988 result =p4CmdList("change -f -i", stdin=input) 989for r in result: 990if r.has_key('code'): 991if r['code'] =='error': 992die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data'])) 993if r.has_key('data'): 994print("Updated user field for changelist%sto%s"% (changelist, newUser)) 995return 996die("Could not modify user field of changelist%sto%s"% (changelist, newUser)) 997 998defcanChangeChangelists(self): 999# check to see if we have p4 admin or super-user permissions, either of1000# which are required to modify changelists.1001 results =p4CmdList(["protects", self.depotPath])1002for r in results:1003if r.has_key('perm'):1004if r['perm'] =='admin':1005return11006if r['perm'] =='super':1007return11008return010091010defprepareSubmitTemplate(self):1011"""Run "p4 change -o" to grab a change specification template.1012 This does not use "p4 -G", as it is nice to keep the submission1013 template in original order, since a human might edit it.10141015 Remove lines in the Files section that show changes to files1016 outside the depot path we're committing into."""10171018 template =""1019 inFilesSection =False1020for line inp4_read_pipe_lines(['change','-o']):1021if line.endswith("\r\n"):1022 line = line[:-2] +"\n"1023if inFilesSection:1024if line.startswith("\t"):1025# path starts and ends with a tab1026 path = line[1:]1027 lastTab = path.rfind("\t")1028if lastTab != -1:1029 path = path[:lastTab]1030if notp4PathStartsWith(path, self.depotPath):1031continue1032else:1033 inFilesSection =False1034else:1035if line.startswith("Files:"):1036 inFilesSection =True10371038 template += line10391040return template10411042defedit_template(self, template_file):1043"""Invoke the editor to let the user change the submission1044 message. Return true if okay to continue with the submit."""10451046# if configured to skip the editing part, just submit1047ifgitConfig("git-p4.skipSubmitEdit") =="true":1048return True10491050# look at the modification time, to check later if the user saved1051# the file1052 mtime = os.stat(template_file).st_mtime10531054# invoke the editor1055if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1056 editor = os.environ.get("P4EDITOR")1057else:1058 editor =read_pipe("git var GIT_EDITOR").strip()1059system(editor +" "+ template_file)10601061# If the file was not saved, prompt to see if this patch should1062# be skipped. But skip this verification step if configured so.1063ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1064return True10651066# modification time updated means user saved the file1067if os.stat(template_file).st_mtime > mtime:1068return True10691070while True:1071 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1072if response =='y':1073return True1074if response =='n':1075return False10761077defapplyCommit(self,id):1078print"Applying%s"% (read_pipe("git log --max-count=1 --pretty=oneline%s"%id))10791080(p4User, gitEmail) = self.p4UserForCommit(id)10811082if not self.detectRenames:1083# If not explicitly set check the config variable1084 self.detectRenames =gitConfig("git-p4.detectRenames")10851086if self.detectRenames.lower() =="false"or self.detectRenames =="":1087 diffOpts =""1088elif self.detectRenames.lower() =="true":1089 diffOpts ="-M"1090else:1091 diffOpts ="-M%s"% self.detectRenames10921093 detectCopies =gitConfig("git-p4.detectCopies")1094if detectCopies.lower() =="true":1095 diffOpts +=" -C"1096elif detectCopies !=""and detectCopies.lower() !="false":1097 diffOpts +=" -C%s"% detectCopies10981099ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1100 diffOpts +=" --find-copies-harder"11011102 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (diffOpts,id,id))1103 filesToAdd =set()1104 filesToDelete =set()1105 editedFiles =set()1106 pureRenameCopy =set()1107 filesToChangeExecBit = {}11081109for line in diff:1110 diff =parseDiffTreeEntry(line)1111 modifier = diff['status']1112 path = diff['src']1113if modifier =="M":1114p4_edit(path)1115ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1116 filesToChangeExecBit[path] = diff['dst_mode']1117 editedFiles.add(path)1118elif modifier =="A":1119 filesToAdd.add(path)1120 filesToChangeExecBit[path] = diff['dst_mode']1121if path in filesToDelete:1122 filesToDelete.remove(path)1123elif modifier =="D":1124 filesToDelete.add(path)1125if path in filesToAdd:1126 filesToAdd.remove(path)1127elif modifier =="C":1128 src, dest = diff['src'], diff['dst']1129p4_integrate(src, dest)1130 pureRenameCopy.add(dest)1131if diff['src_sha1'] != diff['dst_sha1']:1132p4_edit(dest)1133 pureRenameCopy.discard(dest)1134ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1135p4_edit(dest)1136 pureRenameCopy.discard(dest)1137 filesToChangeExecBit[dest] = diff['dst_mode']1138 os.unlink(dest)1139 editedFiles.add(dest)1140elif modifier =="R":1141 src, dest = diff['src'], diff['dst']1142p4_integrate(src, dest)1143if diff['src_sha1'] != diff['dst_sha1']:1144p4_edit(dest)1145else:1146 pureRenameCopy.add(dest)1147ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1148p4_edit(dest)1149 filesToChangeExecBit[dest] = diff['dst_mode']1150 os.unlink(dest)1151 editedFiles.add(dest)1152 filesToDelete.add(src)1153else:1154die("unknown modifier%sfor%s"% (modifier, path))11551156 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1157 patchcmd = diffcmd +" | git apply "1158 tryPatchCmd = patchcmd +"--check -"1159 applyPatchCmd = patchcmd +"--check --apply -"1160 patch_succeeded =True11611162if os.system(tryPatchCmd) !=0:1163 fixed_rcs_keywords =False1164 patch_succeeded =False1165print"Unfortunately applying the change failed!"11661167# Patch failed, maybe it's just RCS keyword woes. Look through1168# the patch to see if that's possible.1169ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1170file=None1171 pattern =None1172 kwfiles = {}1173forfilein editedFiles | filesToDelete:1174# did this file's delta contain RCS keywords?1175 pattern =p4_keywords_regexp_for_file(file)11761177if pattern:1178# this file is a possibility...look for RCS keywords.1179 regexp = re.compile(pattern, re.VERBOSE)1180for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1181if regexp.search(line):1182if verbose:1183print"got keyword match on%sin%sin%s"% (pattern, line,file)1184 kwfiles[file] = pattern1185break11861187forfilein kwfiles:1188if verbose:1189print"zapping%swith%s"% (line,pattern)1190 self.patchRCSKeywords(file, kwfiles[file])1191 fixed_rcs_keywords =True11921193if fixed_rcs_keywords:1194print"Retrying the patch with RCS keywords cleaned up"1195if os.system(tryPatchCmd) ==0:1196 patch_succeeded =True11971198if not patch_succeeded:1199print"What do you want to do?"1200 response ="x"1201while response !="s"and response !="a"and response !="w":1202 response =raw_input("[s]kip this patch / [a]pply the patch forcibly "1203"and with .rej files / [w]rite the patch to a file (patch.txt) ")1204if response =="s":1205print"Skipping! Good luck with the next patches..."1206for f in editedFiles:1207p4_revert(f)1208for f in filesToAdd:1209 os.remove(f)1210return1211elif response =="a":1212 os.system(applyPatchCmd)1213iflen(filesToAdd) >0:1214print"You may also want to call p4 add on the following files:"1215print" ".join(filesToAdd)1216iflen(filesToDelete):1217print"The following files should be scheduled for deletion with p4 delete:"1218print" ".join(filesToDelete)1219die("Please resolve and submit the conflict manually and "1220+"continue afterwards with git p4 submit --continue")1221elif response =="w":1222system(diffcmd +" > patch.txt")1223print"Patch saved to patch.txt in%s!"% self.clientPath1224die("Please resolve and submit the conflict manually and "1225"continue afterwards with git p4 submit --continue")12261227system(applyPatchCmd)12281229for f in filesToAdd:1230p4_add(f)1231for f in filesToDelete:1232p4_revert(f)1233p4_delete(f)12341235# Set/clear executable bits1236for f in filesToChangeExecBit.keys():1237 mode = filesToChangeExecBit[f]1238setP4ExecBit(f, mode)12391240 logMessage =extractLogMessageFromGitCommit(id)1241 logMessage = logMessage.strip()1242(logMessage, jobs) = self.separate_jobs_from_description(logMessage)12431244 template = self.prepareSubmitTemplate()1245 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)12461247if self.preserveUser:1248 submitTemplate = submitTemplate + ("\n######## Actual user%s, modified after commit\n"% p4User)12491250if os.environ.has_key("P4DIFF"):1251del(os.environ["P4DIFF"])1252 diff =""1253for editedFile in editedFiles:1254 diff +=p4_read_pipe(['diff','-du',1255wildcard_encode(editedFile)])12561257 newdiff =""1258for newFile in filesToAdd:1259 newdiff +="==== new file ====\n"1260 newdiff +="--- /dev/null\n"1261 newdiff +="+++%s\n"% newFile1262 f =open(newFile,"r")1263for line in f.readlines():1264 newdiff +="+"+ line1265 f.close()12661267if self.checkAuthorship and not self.p4UserIsMe(p4User):1268 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1269 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1270 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"12711272 separatorLine ="######## everything below this line is just the diff #######\n"12731274(handle, fileName) = tempfile.mkstemp()1275 tmpFile = os.fdopen(handle,"w+")1276if self.isWindows:1277 submitTemplate = submitTemplate.replace("\n","\r\n")1278 separatorLine = separatorLine.replace("\n","\r\n")1279 newdiff = newdiff.replace("\n","\r\n")1280 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1281 tmpFile.close()12821283if self.edit_template(fileName):1284# read the edited message and submit1285 tmpFile =open(fileName,"rb")1286 message = tmpFile.read()1287 tmpFile.close()1288 submitTemplate = message[:message.index(separatorLine)]1289if self.isWindows:1290 submitTemplate = submitTemplate.replace("\r\n","\n")1291p4_write_pipe(['submit','-i'], submitTemplate)12921293if self.preserveUser:1294if p4User:1295# Get last changelist number. Cannot easily get it from1296# the submit command output as the output is1297# unmarshalled.1298 changelist = self.lastP4Changelist()1299 self.modifyChangelistUser(changelist, p4User)13001301# The rename/copy happened by applying a patch that created a1302# new file. This leaves it writable, which confuses p4.1303for f in pureRenameCopy:1304p4_sync(f,"-f")13051306else:1307# skip this patch1308print"Submission cancelled, undoing p4 changes."1309for f in editedFiles:1310p4_revert(f)1311for f in filesToAdd:1312p4_revert(f)1313 os.remove(f)13141315 os.remove(fileName)13161317# Export git tags as p4 labels. Create a p4 label and then tag1318# with that.1319defexportGitTags(self, gitTags):1320 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1321iflen(validLabelRegexp) ==0:1322 validLabelRegexp = defaultLabelRegexp1323 m = re.compile(validLabelRegexp)13241325for name in gitTags:13261327if not m.match(name):1328if verbose:1329print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1330continue13311332# Get the p4 commit this corresponds to1333 logMessage =extractLogMessageFromGitCommit(name)1334 values =extractSettingsGitLog(logMessage)13351336if not values.has_key('change'):1337# a tag pointing to something not sent to p4; ignore1338if verbose:1339print"git tag%sdoes not give a p4 commit"% name1340continue1341else:1342 changelist = values['change']13431344# Get the tag details.1345 inHeader =True1346 isAnnotated =False1347 body = []1348for l inread_pipe_lines(["git","cat-file","-p", name]):1349 l = l.strip()1350if inHeader:1351if re.match(r'tag\s+', l):1352 isAnnotated =True1353elif re.match(r'\s*$', l):1354 inHeader =False1355continue1356else:1357 body.append(l)13581359if not isAnnotated:1360 body = ["lightweight tag imported by git p4\n"]13611362# Create the label - use the same view as the client spec we are using1363 clientSpec =getClientSpec()13641365 labelTemplate ="Label:%s\n"% name1366 labelTemplate +="Description:\n"1367for b in body:1368 labelTemplate +="\t"+ b +"\n"1369 labelTemplate +="View:\n"1370for mapping in clientSpec.mappings:1371 labelTemplate +="\t%s\n"% mapping.depot_side.path13721373p4_write_pipe(["label","-i"], labelTemplate)13741375# Use the label1376p4_system(["tag","-l", name] +1377["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])13781379if verbose:1380print"created p4 label for tag%s"% name13811382defrun(self, args):1383iflen(args) ==0:1384 self.master =currentGitBranch()1385iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1386die("Detecting current git branch failed!")1387eliflen(args) ==1:1388 self.master = args[0]1389if notbranchExists(self.master):1390die("Branch%sdoes not exist"% self.master)1391else:1392return False13931394 allowSubmit =gitConfig("git-p4.allowSubmit")1395iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1396die("%sis not in git-p4.allowSubmit"% self.master)13971398[upstream, settings] =findUpstreamBranchPoint()1399 self.depotPath = settings['depot-paths'][0]1400iflen(self.origin) ==0:1401 self.origin = upstream14021403if self.preserveUser:1404if not self.canChangeChangelists():1405die("Cannot preserve user names without p4 super-user or admin permissions")14061407if self.verbose:1408print"Origin branch is "+ self.origin14091410iflen(self.depotPath) ==0:1411print"Internal error: cannot locate perforce depot path from existing branches"1412 sys.exit(128)14131414 self.useClientSpec =False1415ifgitConfig("git-p4.useclientspec","--bool") =="true":1416 self.useClientSpec =True1417if self.useClientSpec:1418 self.clientSpecDirs =getClientSpec()14191420if self.useClientSpec:1421# all files are relative to the client spec1422 self.clientPath =getClientRoot()1423else:1424 self.clientPath =p4Where(self.depotPath)14251426if self.clientPath =="":1427die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)14281429print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1430 self.oldWorkingDirectory = os.getcwd()14311432# ensure the clientPath exists1433 new_client_dir =False1434if not os.path.exists(self.clientPath):1435 new_client_dir =True1436 os.makedirs(self.clientPath)14371438chdir(self.clientPath)1439print"Synchronizing p4 checkout..."1440if new_client_dir:1441# old one was destroyed, and maybe nobody told p41442p4_sync("...","-f")1443else:1444p4_sync("...")1445 self.check()14461447 commits = []1448for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1449 commits.append(line.strip())1450 commits.reverse()14511452if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1453 self.checkAuthorship =False1454else:1455 self.checkAuthorship =True14561457if self.preserveUser:1458 self.checkValidP4Users(commits)14591460whilelen(commits) >0:1461 commit = commits[0]1462 commits = commits[1:]1463 self.applyCommit(commit)14641465iflen(commits) ==0:1466print"All changes applied!"1467chdir(self.oldWorkingDirectory)14681469 sync =P4Sync()1470 sync.run([])14711472 rebase =P4Rebase()1473 rebase.rebase()14741475ifgitConfig("git-p4.exportLabels","--bool") =="true":1476 self.exportLabels =True14771478if self.exportLabels:1479 p4Labels =getP4Labels(self.depotPath)1480 gitTags =getGitTags()14811482 missingGitTags = gitTags - p4Labels1483 self.exportGitTags(missingGitTags)14841485return True14861487classView(object):1488"""Represent a p4 view ("p4 help views"), and map files in a1489 repo according to the view."""14901491classPath(object):1492"""A depot or client path, possibly containing wildcards.1493 The only one supported is ... at the end, currently.1494 Initialize with the full path, with //depot or //client."""14951496def__init__(self, path, is_depot):1497 self.path = path1498 self.is_depot = is_depot1499 self.find_wildcards()1500# remember the prefix bit, useful for relative mappings1501 m = re.match("(//[^/]+/)", self.path)1502if not m:1503die("Path%sdoes not start with //prefix/"% self.path)1504 prefix = m.group(1)1505if not self.is_depot:1506# strip //client/ on client paths1507 self.path = self.path[len(prefix):]15081509deffind_wildcards(self):1510"""Make sure wildcards are valid, and set up internal1511 variables."""15121513 self.ends_triple_dot =False1514# There are three wildcards allowed in p4 views1515# (see "p4 help views"). This code knows how to1516# handle "..." (only at the end), but cannot deal with1517# "%%n" or "*". Only check the depot_side, as p4 should1518# validate that the client_side matches too.1519if re.search(r'%%[1-9]', self.path):1520die("Can't handle%%n wildcards in view:%s"% self.path)1521if self.path.find("*") >=0:1522die("Can't handle * wildcards in view:%s"% self.path)1523 triple_dot_index = self.path.find("...")1524if triple_dot_index >=0:1525if triple_dot_index !=len(self.path) -3:1526die("Can handle only single ... wildcard, at end:%s"%1527 self.path)1528 self.ends_triple_dot =True15291530defensure_compatible(self, other_path):1531"""Make sure the wildcards agree."""1532if self.ends_triple_dot != other_path.ends_triple_dot:1533die("Both paths must end with ... if either does;\n"+1534"paths:%s %s"% (self.path, other_path.path))15351536defmatch_wildcards(self, test_path):1537"""See if this test_path matches us, and fill in the value1538 of the wildcards if so. Returns a tuple of1539 (True|False, wildcards[]). For now, only the ... at end1540 is supported, so at most one wildcard."""1541if self.ends_triple_dot:1542 dotless = self.path[:-3]1543if test_path.startswith(dotless):1544 wildcard = test_path[len(dotless):]1545return(True, [ wildcard ])1546else:1547if test_path == self.path:1548return(True, [])1549return(False, [])15501551defmatch(self, test_path):1552"""Just return if it matches; don't bother with the wildcards."""1553 b, _ = self.match_wildcards(test_path)1554return b15551556deffill_in_wildcards(self, wildcards):1557"""Return the relative path, with the wildcards filled in1558 if there are any."""1559if self.ends_triple_dot:1560return self.path[:-3] + wildcards[0]1561else:1562return self.path15631564classMapping(object):1565def__init__(self, depot_side, client_side, overlay, exclude):1566# depot_side is without the trailing /... if it had one1567 self.depot_side = View.Path(depot_side, is_depot=True)1568 self.client_side = View.Path(client_side, is_depot=False)1569 self.overlay = overlay # started with "+"1570 self.exclude = exclude # started with "-"1571assert not(self.overlay and self.exclude)1572 self.depot_side.ensure_compatible(self.client_side)15731574def__str__(self):1575 c =" "1576if self.overlay:1577 c ="+"1578if self.exclude:1579 c ="-"1580return"View.Mapping:%s%s->%s"% \1581(c, self.depot_side.path, self.client_side.path)15821583defmap_depot_to_client(self, depot_path):1584"""Calculate the client path if using this mapping on the1585 given depot path; does not consider the effect of other1586 mappings in a view. Even excluded mappings are returned."""1587 matches, wildcards = self.depot_side.match_wildcards(depot_path)1588if not matches:1589return""1590 client_path = self.client_side.fill_in_wildcards(wildcards)1591return client_path15921593#1594# View methods1595#1596def__init__(self):1597 self.mappings = []15981599defappend(self, view_line):1600"""Parse a view line, splitting it into depot and client1601 sides. Append to self.mappings, preserving order."""16021603# Split the view line into exactly two words. P4 enforces1604# structure on these lines that simplifies this quite a bit.1605#1606# Either or both words may be double-quoted.1607# Single quotes do not matter.1608# Double-quote marks cannot occur inside the words.1609# A + or - prefix is also inside the quotes.1610# There are no quotes unless they contain a space.1611# The line is already white-space stripped.1612# The two words are separated by a single space.1613#1614if view_line[0] =='"':1615# First word is double quoted. Find its end.1616 close_quote_index = view_line.find('"',1)1617if close_quote_index <=0:1618die("No first-word closing quote found:%s"% view_line)1619 depot_side = view_line[1:close_quote_index]1620# skip closing quote and space1621 rhs_index = close_quote_index +1+11622else:1623 space_index = view_line.find(" ")1624if space_index <=0:1625die("No word-splitting space found:%s"% view_line)1626 depot_side = view_line[0:space_index]1627 rhs_index = space_index +116281629if view_line[rhs_index] =='"':1630# Second word is double quoted. Make sure there is a1631# double quote at the end too.1632if not view_line.endswith('"'):1633die("View line with rhs quote should end with one:%s"%1634 view_line)1635# skip the quotes1636 client_side = view_line[rhs_index+1:-1]1637else:1638 client_side = view_line[rhs_index:]16391640# prefix + means overlay on previous mapping1641 overlay =False1642if depot_side.startswith("+"):1643 overlay =True1644 depot_side = depot_side[1:]16451646# prefix - means exclude this path1647 exclude =False1648if depot_side.startswith("-"):1649 exclude =True1650 depot_side = depot_side[1:]16511652 m = View.Mapping(depot_side, client_side, overlay, exclude)1653 self.mappings.append(m)16541655defmap_in_client(self, depot_path):1656"""Return the relative location in the client where this1657 depot file should live. Returns "" if the file should1658 not be mapped in the client."""16591660 paths_filled = []1661 client_path =""16621663# look at later entries first1664for m in self.mappings[::-1]:16651666# see where will this path end up in the client1667 p = m.map_depot_to_client(depot_path)16681669if p =="":1670# Depot path does not belong in client. Must remember1671# this, as previous items should not cause files to1672# exist in this path either. Remember that the list is1673# being walked from the end, which has higher precedence.1674# Overlap mappings do not exclude previous mappings.1675if not m.overlay:1676 paths_filled.append(m.client_side)16771678else:1679# This mapping matched; no need to search any further.1680# But, the mapping could be rejected if the client path1681# has already been claimed by an earlier mapping (i.e.1682# one later in the list, which we are walking backwards).1683 already_mapped_in_client =False1684for f in paths_filled:1685# this is View.Path.match1686if f.match(p):1687 already_mapped_in_client =True1688break1689if not already_mapped_in_client:1690# Include this file, unless it is from a line that1691# explicitly said to exclude it.1692if not m.exclude:1693 client_path = p16941695# a match, even if rejected, always stops the search1696break16971698return client_path16991700classP4Sync(Command, P4UserMap):1701 delete_actions = ("delete","move/delete","purge")17021703def__init__(self):1704 Command.__init__(self)1705 P4UserMap.__init__(self)1706 self.options = [1707 optparse.make_option("--branch", dest="branch"),1708 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1709 optparse.make_option("--changesfile", dest="changesFile"),1710 optparse.make_option("--silent", dest="silent", action="store_true"),1711 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1712 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1713 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1714help="Import into refs/heads/ , not refs/remotes"),1715 optparse.make_option("--max-changes", dest="maxChanges"),1716 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1717help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1718 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1719help="Only sync files that are included in the Perforce Client Spec")1720]1721 self.description ="""Imports from Perforce into a git repository.\n1722 example:1723 //depot/my/project/ -- to import the current head1724 //depot/my/project/@all -- to import everything1725 //depot/my/project/@1,6 -- to import only from revision 1 to 617261727 (a ... is not needed in the path p4 specification, it's added implicitly)"""17281729 self.usage +=" //depot/path[@revRange]"1730 self.silent =False1731 self.createdBranches =set()1732 self.committedChanges =set()1733 self.branch =""1734 self.detectBranches =False1735 self.detectLabels =False1736 self.importLabels =False1737 self.changesFile =""1738 self.syncWithOrigin =True1739 self.importIntoRemotes =True1740 self.maxChanges =""1741 self.isWindows = (platform.system() =="Windows")1742 self.keepRepoPath =False1743 self.depotPaths =None1744 self.p4BranchesInGit = []1745 self.cloneExclude = []1746 self.useClientSpec =False1747 self.useClientSpec_from_options =False1748 self.clientSpecDirs =None1749 self.tempBranches = []1750 self.tempBranchLocation ="git-p4-tmp"17511752ifgitConfig("git-p4.syncFromOrigin") =="false":1753 self.syncWithOrigin =False17541755# Force a checkpoint in fast-import and wait for it to finish1756defcheckpoint(self):1757 self.gitStream.write("checkpoint\n\n")1758 self.gitStream.write("progress checkpoint\n\n")1759 out = self.gitOutput.readline()1760if self.verbose:1761print"checkpoint finished: "+ out17621763defextractFilesFromCommit(self, commit):1764 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1765for path in self.cloneExclude]1766 files = []1767 fnum =01768while commit.has_key("depotFile%s"% fnum):1769 path = commit["depotFile%s"% fnum]17701771if[p for p in self.cloneExclude1772ifp4PathStartsWith(path, p)]:1773 found =False1774else:1775 found = [p for p in self.depotPaths1776ifp4PathStartsWith(path, p)]1777if not found:1778 fnum = fnum +11779continue17801781file= {}1782file["path"] = path1783file["rev"] = commit["rev%s"% fnum]1784file["action"] = commit["action%s"% fnum]1785file["type"] = commit["type%s"% fnum]1786 files.append(file)1787 fnum = fnum +11788return files17891790defstripRepoPath(self, path, prefixes):1791if self.useClientSpec:1792return self.clientSpecDirs.map_in_client(path)17931794if self.keepRepoPath:1795 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]17961797for p in prefixes:1798ifp4PathStartsWith(path, p):1799 path = path[len(p):]18001801return path18021803defsplitFilesIntoBranches(self, commit):1804 branches = {}1805 fnum =01806while commit.has_key("depotFile%s"% fnum):1807 path = commit["depotFile%s"% fnum]1808 found = [p for p in self.depotPaths1809ifp4PathStartsWith(path, p)]1810if not found:1811 fnum = fnum +11812continue18131814file= {}1815file["path"] = path1816file["rev"] = commit["rev%s"% fnum]1817file["action"] = commit["action%s"% fnum]1818file["type"] = commit["type%s"% fnum]1819 fnum = fnum +118201821 relPath = self.stripRepoPath(path, self.depotPaths)1822 relPath =wildcard_decode(relPath)18231824for branch in self.knownBranches.keys():18251826# add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.21827if relPath.startswith(branch +"/"):1828if branch not in branches:1829 branches[branch] = []1830 branches[branch].append(file)1831break18321833return branches18341835# output one file from the P4 stream1836# - helper for streamP4Files18371838defstreamOneP4File(self,file, contents):1839 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)1840 relPath =wildcard_decode(relPath)1841if verbose:1842 sys.stderr.write("%s\n"% relPath)18431844(type_base, type_mods) =split_p4_type(file["type"])18451846 git_mode ="100644"1847if"x"in type_mods:1848 git_mode ="100755"1849if type_base =="symlink":1850 git_mode ="120000"1851# p4 print on a symlink contains "target\n"; remove the newline1852 data =''.join(contents)1853 contents = [data[:-1]]18541855if type_base =="utf16":1856# p4 delivers different text in the python output to -G1857# than it does when using "print -o", or normal p4 client1858# operations. utf16 is converted to ascii or utf8, perhaps.1859# But ascii text saved as -t utf16 is completely mangled.1860# Invoke print -o to get the real contents.1861 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])1862 contents = [ text ]18631864if type_base =="apple":1865# Apple filetype files will be streamed as a concatenation of1866# its appledouble header and the contents. This is useless1867# on both macs and non-macs. If using "print -q -o xx", it1868# will create "xx" with the data, and "%xx" with the header.1869# This is also not very useful.1870#1871# Ideally, someday, this script can learn how to generate1872# appledouble files directly and import those to git, but1873# non-mac machines can never find a use for apple filetype.1874print"\nIgnoring apple filetype file%s"%file['depotFile']1875return18761877# Perhaps windows wants unicode, utf16 newlines translated too;1878# but this is not doing it.1879if self.isWindows and type_base =="text":1880 mangled = []1881for data in contents:1882 data = data.replace("\r\n","\n")1883 mangled.append(data)1884 contents = mangled18851886# Note that we do not try to de-mangle keywords on utf16 files,1887# even though in theory somebody may want that.1888 pattern =p4_keywords_regexp_for_type(type_base, type_mods)1889if pattern:1890 regexp = re.compile(pattern, re.VERBOSE)1891 text =''.join(contents)1892 text = regexp.sub(r'$\1$', text)1893 contents = [ text ]18941895 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))18961897# total length...1898 length =01899for d in contents:1900 length = length +len(d)19011902 self.gitStream.write("data%d\n"% length)1903for d in contents:1904 self.gitStream.write(d)1905 self.gitStream.write("\n")19061907defstreamOneP4Deletion(self,file):1908 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)1909 relPath =wildcard_decode(relPath)1910if verbose:1911 sys.stderr.write("delete%s\n"% relPath)1912 self.gitStream.write("D%s\n"% relPath)19131914# handle another chunk of streaming data1915defstreamP4FilesCb(self, marshalled):19161917if marshalled.has_key('depotFile')and self.stream_have_file_info:1918# start of a new file - output the old one first1919 self.streamOneP4File(self.stream_file, self.stream_contents)1920 self.stream_file = {}1921 self.stream_contents = []1922 self.stream_have_file_info =False19231924# pick up the new file information... for the1925# 'data' field we need to append to our array1926for k in marshalled.keys():1927if k =='data':1928 self.stream_contents.append(marshalled['data'])1929else:1930 self.stream_file[k] = marshalled[k]19311932 self.stream_have_file_info =True19331934# Stream directly from "p4 files" into "git fast-import"1935defstreamP4Files(self, files):1936 filesForCommit = []1937 filesToRead = []1938 filesToDelete = []19391940for f in files:1941# if using a client spec, only add the files that have1942# a path in the client1943if self.clientSpecDirs:1944if self.clientSpecDirs.map_in_client(f['path']) =="":1945continue19461947 filesForCommit.append(f)1948if f['action']in self.delete_actions:1949 filesToDelete.append(f)1950else:1951 filesToRead.append(f)19521953# deleted files...1954for f in filesToDelete:1955 self.streamOneP4Deletion(f)19561957iflen(filesToRead) >0:1958 self.stream_file = {}1959 self.stream_contents = []1960 self.stream_have_file_info =False19611962# curry self argument1963defstreamP4FilesCbSelf(entry):1964 self.streamP4FilesCb(entry)19651966 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]19671968p4CmdList(["-x","-","print"],1969 stdin=fileArgs,1970 cb=streamP4FilesCbSelf)19711972# do the last chunk1973if self.stream_file.has_key('depotFile'):1974 self.streamOneP4File(self.stream_file, self.stream_contents)19751976defmake_email(self, userid):1977if userid in self.users:1978return self.users[userid]1979else:1980return"%s<a@b>"% userid19811982# Stream a p4 tag1983defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):1984if verbose:1985print"writing tag%sfor commit%s"% (labelName, commit)1986 gitStream.write("tag%s\n"% labelName)1987 gitStream.write("from%s\n"% commit)19881989if labelDetails.has_key('Owner'):1990 owner = labelDetails["Owner"]1991else:1992 owner =None19931994# Try to use the owner of the p4 label, or failing that,1995# the current p4 user id.1996if owner:1997 email = self.make_email(owner)1998else:1999 email = self.make_email(self.p4UserId())2000 tagger ="%s %s %s"% (email, epoch, self.tz)20012002 gitStream.write("tagger%s\n"% tagger)20032004print"labelDetails=",labelDetails2005if labelDetails.has_key('Description'):2006 description = labelDetails['Description']2007else:2008 description ='Label from git p4'20092010 gitStream.write("data%d\n"%len(description))2011 gitStream.write(description)2012 gitStream.write("\n")20132014defcommit(self, details, files, branch, branchPrefixes, parent =""):2015 epoch = details["time"]2016 author = details["user"]2017 self.branchPrefixes = branchPrefixes20182019if self.verbose:2020print"commit into%s"% branch20212022# start with reading files; if that fails, we should not2023# create a commit.2024 new_files = []2025for f in files:2026if[p for p in branchPrefixes ifp4PathStartsWith(f['path'], p)]:2027 new_files.append(f)2028else:2029 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])20302031 self.gitStream.write("commit%s\n"% branch)2032# gitStream.write("mark :%s\n" % details["change"])2033 self.committedChanges.add(int(details["change"]))2034 committer =""2035if author not in self.users:2036 self.getUserMapFromPerforceServer()2037 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)20382039 self.gitStream.write("committer%s\n"% committer)20402041 self.gitStream.write("data <<EOT\n")2042 self.gitStream.write(details["desc"])2043 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"2044% (','.join(branchPrefixes), details["change"]))2045iflen(details['options']) >0:2046 self.gitStream.write(": options =%s"% details['options'])2047 self.gitStream.write("]\nEOT\n\n")20482049iflen(parent) >0:2050if self.verbose:2051print"parent%s"% parent2052 self.gitStream.write("from%s\n"% parent)20532054 self.streamP4Files(new_files)2055 self.gitStream.write("\n")20562057 change =int(details["change"])20582059if self.labels.has_key(change):2060 label = self.labels[change]2061 labelDetails = label[0]2062 labelRevisions = label[1]2063if self.verbose:2064print"Change%sis labelled%s"% (change, labelDetails)20652066 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2067for p in branchPrefixes])20682069iflen(files) ==len(labelRevisions):20702071 cleanedFiles = {}2072for info in files:2073if info["action"]in self.delete_actions:2074continue2075 cleanedFiles[info["depotFile"]] = info["rev"]20762077if cleanedFiles == labelRevisions:2078 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)20792080else:2081if not self.silent:2082print("Tag%sdoes not match with change%s: files do not match."2083% (labelDetails["label"], change))20842085else:2086if not self.silent:2087print("Tag%sdoes not match with change%s: file count is different."2088% (labelDetails["label"], change))20892090# Build a dictionary of changelists and labels, for "detect-labels" option.2091defgetLabels(self):2092 self.labels = {}20932094 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2095iflen(l) >0and not self.silent:2096print"Finding files belonging to labels in%s"% `self.depotPaths`20972098for output in l:2099 label = output["label"]2100 revisions = {}2101 newestChange =02102if self.verbose:2103print"Querying files for label%s"% label2104forfileinp4CmdList(["files"] +2105["%s...@%s"% (p, label)2106for p in self.depotPaths]):2107 revisions[file["depotFile"]] =file["rev"]2108 change =int(file["change"])2109if change > newestChange:2110 newestChange = change21112112 self.labels[newestChange] = [output, revisions]21132114if self.verbose:2115print"Label changes:%s"% self.labels.keys()21162117# Import p4 labels as git tags. A direct mapping does not2118# exist, so assume that if all the files are at the same revision2119# then we can use that, or it's something more complicated we should2120# just ignore.2121defimportP4Labels(self, stream, p4Labels):2122if verbose:2123print"import p4 labels: "+' '.join(p4Labels)21242125 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2126 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2127iflen(validLabelRegexp) ==0:2128 validLabelRegexp = defaultLabelRegexp2129 m = re.compile(validLabelRegexp)21302131for name in p4Labels:2132 commitFound =False21332134if not m.match(name):2135if verbose:2136print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2137continue21382139if name in ignoredP4Labels:2140continue21412142 labelDetails =p4CmdList(['label',"-o", name])[0]21432144# get the most recent changelist for each file in this label2145 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2146for p in self.depotPaths])21472148if change.has_key('change'):2149# find the corresponding git commit; take the oldest commit2150 changelist =int(change['change'])2151 gitCommit =read_pipe(["git","rev-list","--max-count=1",2152"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2153iflen(gitCommit) ==0:2154print"could not find git commit for changelist%d"% changelist2155else:2156 gitCommit = gitCommit.strip()2157 commitFound =True2158# Convert from p4 time format2159try:2160 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2161exceptValueError:2162print"Could not convert label time%s"% labelDetail['Update']2163 tmwhen =121642165 when =int(time.mktime(tmwhen))2166 self.streamTag(stream, name, labelDetails, gitCommit, when)2167if verbose:2168print"p4 label%smapped to git commit%s"% (name, gitCommit)2169else:2170if verbose:2171print"Label%shas no changelists - possibly deleted?"% name21722173if not commitFound:2174# We can't import this label; don't try again as it will get very2175# expensive repeatedly fetching all the files for labels that will2176# never be imported. If the label is moved in the future, the2177# ignore will need to be removed manually.2178system(["git","config","--add","git-p4.ignoredP4Labels", name])21792180defguessProjectName(self):2181for p in self.depotPaths:2182if p.endswith("/"):2183 p = p[:-1]2184 p = p[p.strip().rfind("/") +1:]2185if not p.endswith("/"):2186 p +="/"2187return p21882189defgetBranchMapping(self):2190 lostAndFoundBranches =set()21912192 user =gitConfig("git-p4.branchUser")2193iflen(user) >0:2194 command ="branches -u%s"% user2195else:2196 command ="branches"21972198for info inp4CmdList(command):2199 details =p4Cmd(["branch","-o", info["branch"]])2200 viewIdx =02201while details.has_key("View%s"% viewIdx):2202 paths = details["View%s"% viewIdx].split(" ")2203 viewIdx = viewIdx +12204# require standard //depot/foo/... //depot/bar/... mapping2205iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2206continue2207 source = paths[0]2208 destination = paths[1]2209## HACK2210ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2211 source = source[len(self.depotPaths[0]):-4]2212 destination = destination[len(self.depotPaths[0]):-4]22132214if destination in self.knownBranches:2215if not self.silent:2216print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2217print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2218continue22192220 self.knownBranches[destination] = source22212222 lostAndFoundBranches.discard(destination)22232224if source not in self.knownBranches:2225 lostAndFoundBranches.add(source)22262227# Perforce does not strictly require branches to be defined, so we also2228# check git config for a branch list.2229#2230# Example of branch definition in git config file:2231# [git-p4]2232# branchList=main:branchA2233# branchList=main:branchB2234# branchList=branchA:branchC2235 configBranches =gitConfigList("git-p4.branchList")2236for branch in configBranches:2237if branch:2238(source, destination) = branch.split(":")2239 self.knownBranches[destination] = source22402241 lostAndFoundBranches.discard(destination)22422243if source not in self.knownBranches:2244 lostAndFoundBranches.add(source)224522462247for branch in lostAndFoundBranches:2248 self.knownBranches[branch] = branch22492250defgetBranchMappingFromGitBranches(self):2251 branches =p4BranchesInGit(self.importIntoRemotes)2252for branch in branches.keys():2253if branch =="master":2254 branch ="main"2255else:2256 branch = branch[len(self.projectName):]2257 self.knownBranches[branch] = branch22582259deflistExistingP4GitBranches(self):2260# branches holds mapping from name to commit2261 branches =p4BranchesInGit(self.importIntoRemotes)2262 self.p4BranchesInGit = branches.keys()2263for branch in branches.keys():2264 self.initialParents[self.refPrefix + branch] = branches[branch]22652266defupdateOptionDict(self, d):2267 option_keys = {}2268if self.keepRepoPath:2269 option_keys['keepRepoPath'] =122702271 d["options"] =' '.join(sorted(option_keys.keys()))22722273defreadOptions(self, d):2274 self.keepRepoPath = (d.has_key('options')2275and('keepRepoPath'in d['options']))22762277defgitRefForBranch(self, branch):2278if branch =="main":2279return self.refPrefix +"master"22802281iflen(branch) <=0:2282return branch22832284return self.refPrefix + self.projectName + branch22852286defgitCommitByP4Change(self, ref, change):2287if self.verbose:2288print"looking in ref "+ ref +" for change%susing bisect..."% change22892290 earliestCommit =""2291 latestCommit =parseRevision(ref)22922293while True:2294if self.verbose:2295print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2296 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2297iflen(next) ==0:2298if self.verbose:2299print"argh"2300return""2301 log =extractLogMessageFromGitCommit(next)2302 settings =extractSettingsGitLog(log)2303 currentChange =int(settings['change'])2304if self.verbose:2305print"current change%s"% currentChange23062307if currentChange == change:2308if self.verbose:2309print"found%s"% next2310return next23112312if currentChange < change:2313 earliestCommit ="^%s"% next2314else:2315 latestCommit ="%s"% next23162317return""23182319defimportNewBranch(self, branch, maxChange):2320# make fast-import flush all changes to disk and update the refs using the checkpoint2321# command so that we can try to find the branch parent in the git history2322 self.gitStream.write("checkpoint\n\n");2323 self.gitStream.flush();2324 branchPrefix = self.depotPaths[0] + branch +"/"2325range="@1,%s"% maxChange2326#print "prefix" + branchPrefix2327 changes =p4ChangesForPaths([branchPrefix],range)2328iflen(changes) <=0:2329return False2330 firstChange = changes[0]2331#print "first change in branch: %s" % firstChange2332 sourceBranch = self.knownBranches[branch]2333 sourceDepotPath = self.depotPaths[0] + sourceBranch2334 sourceRef = self.gitRefForBranch(sourceBranch)2335#print "source " + sourceBranch23362337 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2338#print "branch parent: %s" % branchParentChange2339 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2340iflen(gitParent) >0:2341 self.initialParents[self.gitRefForBranch(branch)] = gitParent2342#print "parent git commit: %s" % gitParent23432344 self.importChanges(changes)2345return True23462347defsearchParent(self, parent, branch, target):2348 parentFound =False2349for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2350 blob = blob.strip()2351iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2352 parentFound =True2353if self.verbose:2354print"Found parent of%sin commit%s"% (branch, blob)2355break2356if parentFound:2357return blob2358else:2359return None23602361defimportChanges(self, changes):2362 cnt =12363for change in changes:2364 description =p4Cmd(["describe",str(change)])2365 self.updateOptionDict(description)23662367if not self.silent:2368 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2369 sys.stdout.flush()2370 cnt = cnt +123712372try:2373if self.detectBranches:2374 branches = self.splitFilesIntoBranches(description)2375for branch in branches.keys():2376## HACK --hwn2377 branchPrefix = self.depotPaths[0] + branch +"/"23782379 parent =""23802381 filesForCommit = branches[branch]23822383if self.verbose:2384print"branch is%s"% branch23852386 self.updatedBranches.add(branch)23872388if branch not in self.createdBranches:2389 self.createdBranches.add(branch)2390 parent = self.knownBranches[branch]2391if parent == branch:2392 parent =""2393else:2394 fullBranch = self.projectName + branch2395if fullBranch not in self.p4BranchesInGit:2396if not self.silent:2397print("\nImporting new branch%s"% fullBranch);2398if self.importNewBranch(branch, change -1):2399 parent =""2400 self.p4BranchesInGit.append(fullBranch)2401if not self.silent:2402print("\nResuming with change%s"% change);24032404if self.verbose:2405print"parent determined through known branches:%s"% parent24062407 branch = self.gitRefForBranch(branch)2408 parent = self.gitRefForBranch(parent)24092410if self.verbose:2411print"looking for initial parent for%s; current parent is%s"% (branch, parent)24122413iflen(parent) ==0and branch in self.initialParents:2414 parent = self.initialParents[branch]2415del self.initialParents[branch]24162417 blob =None2418iflen(parent) >0:2419 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2420if self.verbose:2421print"Creating temporary branch: "+ tempBranch2422 self.commit(description, filesForCommit, tempBranch, [branchPrefix])2423 self.tempBranches.append(tempBranch)2424 self.checkpoint()2425 blob = self.searchParent(parent, branch, tempBranch)2426if blob:2427 self.commit(description, filesForCommit, branch, [branchPrefix], blob)2428else:2429if self.verbose:2430print"Parent of%snot found. Committing into head of%s"% (branch, parent)2431 self.commit(description, filesForCommit, branch, [branchPrefix], parent)2432else:2433 files = self.extractFilesFromCommit(description)2434 self.commit(description, files, self.branch, self.depotPaths,2435 self.initialParent)2436 self.initialParent =""2437exceptIOError:2438print self.gitError.read()2439 sys.exit(1)24402441defimportHeadRevision(self, revision):2442print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)24432444 details = {}2445 details["user"] ="git perforce import user"2446 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2447% (' '.join(self.depotPaths), revision))2448 details["change"] = revision2449 newestRevision =024502451 fileCnt =02452 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]24532454for info inp4CmdList(["files"] + fileArgs):24552456if'code'in info and info['code'] =='error':2457 sys.stderr.write("p4 returned an error:%s\n"2458% info['data'])2459if info['data'].find("must refer to client") >=0:2460 sys.stderr.write("This particular p4 error is misleading.\n")2461 sys.stderr.write("Perhaps the depot path was misspelled.\n");2462 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2463 sys.exit(1)2464if'p4ExitCode'in info:2465 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2466 sys.exit(1)246724682469 change =int(info["change"])2470if change > newestRevision:2471 newestRevision = change24722473if info["action"]in self.delete_actions:2474# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2475#fileCnt = fileCnt + 12476continue24772478for prop in["depotFile","rev","action","type"]:2479 details["%s%s"% (prop, fileCnt)] = info[prop]24802481 fileCnt = fileCnt +124822483 details["change"] = newestRevision24842485# Use time from top-most change so that all git p4 clones of2486# the same p4 repo have the same commit SHA1s.2487 res =p4CmdList("describe -s%d"% newestRevision)2488 newestTime =None2489for r in res:2490if r.has_key('time'):2491 newestTime =int(r['time'])2492if newestTime is None:2493die("\"describe -s\"on newest change%ddid not give a time")2494 details["time"] = newestTime24952496 self.updateOptionDict(details)2497try:2498 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)2499exceptIOError:2500print"IO error with git fast-import. Is your git version recent enough?"2501print self.gitError.read()250225032504defrun(self, args):2505 self.depotPaths = []2506 self.changeRange =""2507 self.initialParent =""2508 self.previousDepotPaths = []25092510# map from branch depot path to parent branch2511 self.knownBranches = {}2512 self.initialParents = {}2513 self.hasOrigin =originP4BranchesExist()2514if not self.syncWithOrigin:2515 self.hasOrigin =False25162517if self.importIntoRemotes:2518 self.refPrefix ="refs/remotes/p4/"2519else:2520 self.refPrefix ="refs/heads/p4/"25212522if self.syncWithOrigin and self.hasOrigin:2523if not self.silent:2524print"Syncing with origin first by calling git fetch origin"2525system("git fetch origin")25262527iflen(self.branch) ==0:2528 self.branch = self.refPrefix +"master"2529ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2530system("git update-ref%srefs/heads/p4"% self.branch)2531system("git branch -D p4");2532# create it /after/ importing, when master exists2533if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2534system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))25352536# accept either the command-line option, or the configuration variable2537if self.useClientSpec:2538# will use this after clone to set the variable2539 self.useClientSpec_from_options =True2540else:2541ifgitConfig("git-p4.useclientspec","--bool") =="true":2542 self.useClientSpec =True2543if self.useClientSpec:2544 self.clientSpecDirs =getClientSpec()25452546# TODO: should always look at previous commits,2547# merge with previous imports, if possible.2548if args == []:2549if self.hasOrigin:2550createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2551 self.listExistingP4GitBranches()25522553iflen(self.p4BranchesInGit) >1:2554if not self.silent:2555print"Importing from/into multiple branches"2556 self.detectBranches =True25572558if self.verbose:2559print"branches:%s"% self.p4BranchesInGit25602561 p4Change =02562for branch in self.p4BranchesInGit:2563 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)25642565 settings =extractSettingsGitLog(logMsg)25662567 self.readOptions(settings)2568if(settings.has_key('depot-paths')2569and settings.has_key('change')):2570 change =int(settings['change']) +12571 p4Change =max(p4Change, change)25722573 depotPaths =sorted(settings['depot-paths'])2574if self.previousDepotPaths == []:2575 self.previousDepotPaths = depotPaths2576else:2577 paths = []2578for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2579 prev_list = prev.split("/")2580 cur_list = cur.split("/")2581for i inrange(0,min(len(cur_list),len(prev_list))):2582if cur_list[i] <> prev_list[i]:2583 i = i -12584break25852586 paths.append("/".join(cur_list[:i +1]))25872588 self.previousDepotPaths = paths25892590if p4Change >0:2591 self.depotPaths =sorted(self.previousDepotPaths)2592 self.changeRange ="@%s,#head"% p4Change2593if not self.detectBranches:2594 self.initialParent =parseRevision(self.branch)2595if not self.silent and not self.detectBranches:2596print"Performing incremental import into%sgit branch"% self.branch25972598if not self.branch.startswith("refs/"):2599 self.branch ="refs/heads/"+ self.branch26002601iflen(args) ==0and self.depotPaths:2602if not self.silent:2603print"Depot paths:%s"%' '.join(self.depotPaths)2604else:2605if self.depotPaths and self.depotPaths != args:2606print("previous import used depot path%sand now%swas specified. "2607"This doesn't work!"% (' '.join(self.depotPaths),2608' '.join(args)))2609 sys.exit(1)26102611 self.depotPaths =sorted(args)26122613 revision =""2614 self.users = {}26152616# Make sure no revision specifiers are used when --changesfile2617# is specified.2618 bad_changesfile =False2619iflen(self.changesFile) >0:2620for p in self.depotPaths:2621if p.find("@") >=0or p.find("#") >=0:2622 bad_changesfile =True2623break2624if bad_changesfile:2625die("Option --changesfile is incompatible with revision specifiers")26262627 newPaths = []2628for p in self.depotPaths:2629if p.find("@") != -1:2630 atIdx = p.index("@")2631 self.changeRange = p[atIdx:]2632if self.changeRange =="@all":2633 self.changeRange =""2634elif','not in self.changeRange:2635 revision = self.changeRange2636 self.changeRange =""2637 p = p[:atIdx]2638elif p.find("#") != -1:2639 hashIdx = p.index("#")2640 revision = p[hashIdx:]2641 p = p[:hashIdx]2642elif self.previousDepotPaths == []:2643# pay attention to changesfile, if given, else import2644# the entire p4 tree at the head revision2645iflen(self.changesFile) ==0:2646 revision ="#head"26472648 p = re.sub("\.\.\.$","", p)2649if not p.endswith("/"):2650 p +="/"26512652 newPaths.append(p)26532654 self.depotPaths = newPaths26552656 self.loadUserMapFromCache()2657 self.labels = {}2658if self.detectLabels:2659 self.getLabels();26602661if self.detectBranches:2662## FIXME - what's a P4 projectName ?2663 self.projectName = self.guessProjectName()26642665if self.hasOrigin:2666 self.getBranchMappingFromGitBranches()2667else:2668 self.getBranchMapping()2669if self.verbose:2670print"p4-git branches:%s"% self.p4BranchesInGit2671print"initial parents:%s"% self.initialParents2672for b in self.p4BranchesInGit:2673if b !="master":26742675## FIXME2676 b = b[len(self.projectName):]2677 self.createdBranches.add(b)26782679 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))26802681 importProcess = subprocess.Popen(["git","fast-import"],2682 stdin=subprocess.PIPE, stdout=subprocess.PIPE,2683 stderr=subprocess.PIPE);2684 self.gitOutput = importProcess.stdout2685 self.gitStream = importProcess.stdin2686 self.gitError = importProcess.stderr26872688if revision:2689 self.importHeadRevision(revision)2690else:2691 changes = []26922693iflen(self.changesFile) >0:2694 output =open(self.changesFile).readlines()2695 changeSet =set()2696for line in output:2697 changeSet.add(int(line))26982699for change in changeSet:2700 changes.append(change)27012702 changes.sort()2703else:2704# catch "git p4 sync" with no new branches, in a repo that2705# does not have any existing p4 branches2706iflen(args) ==0and not self.p4BranchesInGit:2707die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2708if self.verbose:2709print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2710 self.changeRange)2711 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)27122713iflen(self.maxChanges) >0:2714 changes = changes[:min(int(self.maxChanges),len(changes))]27152716iflen(changes) ==0:2717if not self.silent:2718print"No changes to import!"2719else:2720if not self.silent and not self.detectBranches:2721print"Import destination:%s"% self.branch27222723 self.updatedBranches =set()27242725 self.importChanges(changes)27262727if not self.silent:2728print""2729iflen(self.updatedBranches) >0:2730 sys.stdout.write("Updated branches: ")2731for b in self.updatedBranches:2732 sys.stdout.write("%s"% b)2733 sys.stdout.write("\n")27342735ifgitConfig("git-p4.importLabels","--bool") =="true":2736 self.importLabels =True27372738if self.importLabels:2739 p4Labels =getP4Labels(self.depotPaths)2740 gitTags =getGitTags()27412742 missingP4Labels = p4Labels - gitTags2743 self.importP4Labels(self.gitStream, missingP4Labels)27442745 self.gitStream.close()2746if importProcess.wait() !=0:2747die("fast-import failed:%s"% self.gitError.read())2748 self.gitOutput.close()2749 self.gitError.close()27502751# Cleanup temporary branches created during import2752if self.tempBranches != []:2753for branch in self.tempBranches:2754read_pipe("git update-ref -d%s"% branch)2755 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))27562757return True27582759classP4Rebase(Command):2760def__init__(self):2761 Command.__init__(self)2762 self.options = [2763 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2764]2765 self.importLabels =False2766 self.description = ("Fetches the latest revision from perforce and "2767+"rebases the current work (branch) against it")27682769defrun(self, args):2770 sync =P4Sync()2771 sync.importLabels = self.importLabels2772 sync.run([])27732774return self.rebase()27752776defrebase(self):2777if os.system("git update-index --refresh") !=0:2778die("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.");2779iflen(read_pipe("git diff-index HEAD --")) >0:2780die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");27812782[upstream, settings] =findUpstreamBranchPoint()2783iflen(upstream) ==0:2784die("Cannot find upstream branchpoint for rebase")27852786# the branchpoint may be p4/foo~3, so strip off the parent2787 upstream = re.sub("~[0-9]+$","", upstream)27882789print"Rebasing the current branch onto%s"% upstream2790 oldHead =read_pipe("git rev-parse HEAD").strip()2791system("git rebase%s"% upstream)2792system("git diff-tree --stat --summary -M%sHEAD"% oldHead)2793return True27942795classP4Clone(P4Sync):2796def__init__(self):2797 P4Sync.__init__(self)2798 self.description ="Creates a new git repository and imports from Perforce into it"2799 self.usage ="usage: %prog [options] //depot/path[@revRange]"2800 self.options += [2801 optparse.make_option("--destination", dest="cloneDestination",2802 action='store', default=None,2803help="where to leave result of the clone"),2804 optparse.make_option("-/", dest="cloneExclude",2805 action="append",type="string",2806help="exclude depot path"),2807 optparse.make_option("--bare", dest="cloneBare",2808 action="store_true", default=False),2809]2810 self.cloneDestination =None2811 self.needsGit =False2812 self.cloneBare =False28132814# This is required for the "append" cloneExclude action2815defensure_value(self, attr, value):2816if nothasattr(self, attr)orgetattr(self, attr)is None:2817setattr(self, attr, value)2818returngetattr(self, attr)28192820defdefaultDestination(self, args):2821## TODO: use common prefix of args?2822 depotPath = args[0]2823 depotDir = re.sub("(@[^@]*)$","", depotPath)2824 depotDir = re.sub("(#[^#]*)$","", depotDir)2825 depotDir = re.sub(r"\.\.\.$","", depotDir)2826 depotDir = re.sub(r"/$","", depotDir)2827return os.path.split(depotDir)[1]28282829defrun(self, args):2830iflen(args) <1:2831return False28322833if self.keepRepoPath and not self.cloneDestination:2834 sys.stderr.write("Must specify destination for --keep-path\n")2835 sys.exit(1)28362837 depotPaths = args28382839if not self.cloneDestination andlen(depotPaths) >1:2840 self.cloneDestination = depotPaths[-1]2841 depotPaths = depotPaths[:-1]28422843 self.cloneExclude = ["/"+p for p in self.cloneExclude]2844for p in depotPaths:2845if not p.startswith("//"):2846return False28472848if not self.cloneDestination:2849 self.cloneDestination = self.defaultDestination(args)28502851print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)28522853if not os.path.exists(self.cloneDestination):2854 os.makedirs(self.cloneDestination)2855chdir(self.cloneDestination)28562857 init_cmd = ["git","init"]2858if self.cloneBare:2859 init_cmd.append("--bare")2860 subprocess.check_call(init_cmd)28612862if not P4Sync.run(self, depotPaths):2863return False2864if self.branch !="master":2865if self.importIntoRemotes:2866 masterbranch ="refs/remotes/p4/master"2867else:2868 masterbranch ="refs/heads/p4/master"2869ifgitBranchExists(masterbranch):2870system("git branch master%s"% masterbranch)2871if not self.cloneBare:2872system("git checkout -f")2873else:2874print"Could not detect main branch. No checkout/master branch created."28752876# auto-set this variable if invoked with --use-client-spec2877if self.useClientSpec_from_options:2878system("git config --bool git-p4.useclientspec true")28792880return True28812882classP4Branches(Command):2883def__init__(self):2884 Command.__init__(self)2885 self.options = [ ]2886 self.description = ("Shows the git branches that hold imports and their "2887+"corresponding perforce depot paths")2888 self.verbose =False28892890defrun(self, args):2891iforiginP4BranchesExist():2892createOrUpdateBranchesFromOrigin()28932894 cmdline ="git rev-parse --symbolic "2895 cmdline +=" --remotes"28962897for line inread_pipe_lines(cmdline):2898 line = line.strip()28992900if not line.startswith('p4/')or line =="p4/HEAD":2901continue2902 branch = line29032904 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)2905 settings =extractSettingsGitLog(log)29062907print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])2908return True29092910classHelpFormatter(optparse.IndentedHelpFormatter):2911def__init__(self):2912 optparse.IndentedHelpFormatter.__init__(self)29132914defformat_description(self, description):2915if description:2916return description +"\n"2917else:2918return""29192920defprintUsage(commands):2921print"usage:%s<command> [options]"% sys.argv[0]2922print""2923print"valid commands:%s"%", ".join(commands)2924print""2925print"Try%s<command> --help for command specific help."% sys.argv[0]2926print""29272928commands = {2929"debug": P4Debug,2930"submit": P4Submit,2931"commit": P4Submit,2932"sync": P4Sync,2933"rebase": P4Rebase,2934"clone": P4Clone,2935"rollback": P4RollBack,2936"branches": P4Branches2937}293829392940defmain():2941iflen(sys.argv[1:]) ==0:2942printUsage(commands.keys())2943 sys.exit(2)29442945 cmd =""2946 cmdName = sys.argv[1]2947try:2948 klass = commands[cmdName]2949 cmd =klass()2950exceptKeyError:2951print"unknown command%s"% cmdName2952print""2953printUsage(commands.keys())2954 sys.exit(2)29552956 options = cmd.options2957 cmd.gitdir = os.environ.get("GIT_DIR",None)29582959 args = sys.argv[2:]29602961 options.append(optparse.make_option("--verbose", dest="verbose", action="store_true"))2962if cmd.needsGit:2963 options.append(optparse.make_option("--git-dir", dest="gitdir"))29642965 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),2966 options,2967 description = cmd.description,2968 formatter =HelpFormatter())29692970(cmd, args) = parser.parse_args(sys.argv[2:], cmd);2971global verbose2972 verbose = cmd.verbose2973if cmd.needsGit:2974if cmd.gitdir ==None:2975 cmd.gitdir = os.path.abspath(".git")2976if notisValidGitDir(cmd.gitdir):2977 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()2978if os.path.exists(cmd.gitdir):2979 cdup =read_pipe("git rev-parse --show-cdup").strip()2980iflen(cdup) >0:2981chdir(cdup);29822983if notisValidGitDir(cmd.gitdir):2984ifisValidGitDir(cmd.gitdir +"/.git"):2985 cmd.gitdir +="/.git"2986else:2987die("fatal: cannot locate git repository at%s"% cmd.gitdir)29882989 os.environ["GIT_DIR"] = cmd.gitdir29902991if not cmd.run(args):2992 parser.print_help()2993 sys.exit(2)299429952996if __name__ =='__main__':2997main()