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 857# replaces everything between 'Description:' and the next P4 submit template field with the 858# commit message 859defprepareLogMessage(self, template, message): 860 result ="" 861 862 inDescriptionSection =False 863 864for line in template.split("\n"): 865if line.startswith("#"): 866 result += line +"\n" 867continue 868 869if inDescriptionSection: 870if line.startswith("Files:")or line.startswith("Jobs:"): 871 inDescriptionSection =False 872else: 873continue 874else: 875if line.startswith("Description:"): 876 inDescriptionSection =True 877 line +="\n" 878for messageLine in message.split("\n"): 879 line +="\t"+ messageLine +"\n" 880 881 result += line +"\n" 882 883return result 884 885defpatchRCSKeywords(self,file, pattern): 886# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern 887(handle, outFileName) = tempfile.mkstemp(dir='.') 888try: 889 outFile = os.fdopen(handle,"w+") 890 inFile =open(file,"r") 891 regexp = re.compile(pattern, re.VERBOSE) 892for line in inFile.readlines(): 893 line = regexp.sub(r'$\1$', line) 894 outFile.write(line) 895 inFile.close() 896 outFile.close() 897# Forcibly overwrite the original file 898 os.unlink(file) 899 shutil.move(outFileName,file) 900except: 901# cleanup our temporary file 902 os.unlink(outFileName) 903print"Failed to strip RCS keywords in%s"%file 904raise 905 906print"Patched up RCS keywords in%s"%file 907 908defp4UserForCommit(self,id): 909# Return the tuple (perforce user,git email) for a given git commit id 910 self.getUserMapFromPerforceServer() 911 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id) 912 gitEmail = gitEmail.strip() 913if not self.emails.has_key(gitEmail): 914return(None,gitEmail) 915else: 916return(self.emails[gitEmail],gitEmail) 917 918defcheckValidP4Users(self,commits): 919# check if any git authors cannot be mapped to p4 users 920foridin commits: 921(user,email) = self.p4UserForCommit(id) 922if not user: 923 msg ="Cannot find p4 user for email%sin commit%s."% (email,id) 924ifgitConfig('git-p4.allowMissingP4Users').lower() =="true": 925print"%s"% msg 926else: 927die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg) 928 929deflastP4Changelist(self): 930# Get back the last changelist number submitted in this client spec. This 931# then gets used to patch up the username in the change. If the same 932# client spec is being used by multiple processes then this might go 933# wrong. 934 results =p4CmdList("client -o")# find the current client 935 client =None 936for r in results: 937if r.has_key('Client'): 938 client = r['Client'] 939break 940if not client: 941die("could not get client spec") 942 results =p4CmdList(["changes","-c", client,"-m","1"]) 943for r in results: 944if r.has_key('change'): 945return r['change'] 946die("Could not get changelist number for last submit - cannot patch up user details") 947 948defmodifyChangelistUser(self, changelist, newUser): 949# fixup the user field of a changelist after it has been submitted. 950 changes =p4CmdList("change -o%s"% changelist) 951iflen(changes) !=1: 952die("Bad output from p4 change modifying%sto user%s"% 953(changelist, newUser)) 954 955 c = changes[0] 956if c['User'] == newUser:return# nothing to do 957 c['User'] = newUser 958input= marshal.dumps(c) 959 960 result =p4CmdList("change -f -i", stdin=input) 961for r in result: 962if r.has_key('code'): 963if r['code'] =='error': 964die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data'])) 965if r.has_key('data'): 966print("Updated user field for changelist%sto%s"% (changelist, newUser)) 967return 968die("Could not modify user field of changelist%sto%s"% (changelist, newUser)) 969 970defcanChangeChangelists(self): 971# check to see if we have p4 admin or super-user permissions, either of 972# which are required to modify changelists. 973 results =p4CmdList(["protects", self.depotPath]) 974for r in results: 975if r.has_key('perm'): 976if r['perm'] =='admin': 977return1 978if r['perm'] =='super': 979return1 980return0 981 982defprepareSubmitTemplate(self): 983# remove lines in the Files section that show changes to files outside the depot path we're committing into 984 template ="" 985 inFilesSection =False 986for line inp4_read_pipe_lines(['change','-o']): 987if line.endswith("\r\n"): 988 line = line[:-2] +"\n" 989if inFilesSection: 990if line.startswith("\t"): 991# path starts and ends with a tab 992 path = line[1:] 993 lastTab = path.rfind("\t") 994if lastTab != -1: 995 path = path[:lastTab] 996if notp4PathStartsWith(path, self.depotPath): 997continue 998else: 999 inFilesSection =False1000else:1001if line.startswith("Files:"):1002 inFilesSection =True10031004 template += line10051006return template10071008defedit_template(self, template_file):1009"""Invoke the editor to let the user change the submission1010 message. Return true if okay to continue with the submit."""10111012# if configured to skip the editing part, just submit1013ifgitConfig("git-p4.skipSubmitEdit") =="true":1014return True10151016# look at the modification time, to check later if the user saved1017# the file1018 mtime = os.stat(template_file).st_mtime10191020# invoke the editor1021if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1022 editor = os.environ.get("P4EDITOR")1023else:1024 editor =read_pipe("git var GIT_EDITOR").strip()1025system(editor +" "+ template_file)10261027# If the file was not saved, prompt to see if this patch should1028# be skipped. But skip this verification step if configured so.1029ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1030return True10311032# modification time updated means user saved the file1033if os.stat(template_file).st_mtime > mtime:1034return True10351036while True:1037 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1038if response =='y':1039return True1040if response =='n':1041return False10421043defapplyCommit(self,id):1044print"Applying%s"% (read_pipe("git log --max-count=1 --pretty=oneline%s"%id))10451046(p4User, gitEmail) = self.p4UserForCommit(id)10471048if not self.detectRenames:1049# If not explicitly set check the config variable1050 self.detectRenames =gitConfig("git-p4.detectRenames")10511052if self.detectRenames.lower() =="false"or self.detectRenames =="":1053 diffOpts =""1054elif self.detectRenames.lower() =="true":1055 diffOpts ="-M"1056else:1057 diffOpts ="-M%s"% self.detectRenames10581059 detectCopies =gitConfig("git-p4.detectCopies")1060if detectCopies.lower() =="true":1061 diffOpts +=" -C"1062elif detectCopies !=""and detectCopies.lower() !="false":1063 diffOpts +=" -C%s"% detectCopies10641065ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1066 diffOpts +=" --find-copies-harder"10671068 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (diffOpts,id,id))1069 filesToAdd =set()1070 filesToDelete =set()1071 editedFiles =set()1072 pureRenameCopy =set()1073 filesToChangeExecBit = {}10741075for line in diff:1076 diff =parseDiffTreeEntry(line)1077 modifier = diff['status']1078 path = diff['src']1079if modifier =="M":1080p4_edit(path)1081ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1082 filesToChangeExecBit[path] = diff['dst_mode']1083 editedFiles.add(path)1084elif modifier =="A":1085 filesToAdd.add(path)1086 filesToChangeExecBit[path] = diff['dst_mode']1087if path in filesToDelete:1088 filesToDelete.remove(path)1089elif modifier =="D":1090 filesToDelete.add(path)1091if path in filesToAdd:1092 filesToAdd.remove(path)1093elif modifier =="C":1094 src, dest = diff['src'], diff['dst']1095p4_integrate(src, dest)1096 pureRenameCopy.add(dest)1097if diff['src_sha1'] != diff['dst_sha1']:1098p4_edit(dest)1099 pureRenameCopy.discard(dest)1100ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1101p4_edit(dest)1102 pureRenameCopy.discard(dest)1103 filesToChangeExecBit[dest] = diff['dst_mode']1104 os.unlink(dest)1105 editedFiles.add(dest)1106elif modifier =="R":1107 src, dest = diff['src'], diff['dst']1108p4_integrate(src, dest)1109if diff['src_sha1'] != diff['dst_sha1']:1110p4_edit(dest)1111else:1112 pureRenameCopy.add(dest)1113ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1114p4_edit(dest)1115 filesToChangeExecBit[dest] = diff['dst_mode']1116 os.unlink(dest)1117 editedFiles.add(dest)1118 filesToDelete.add(src)1119else:1120die("unknown modifier%sfor%s"% (modifier, path))11211122 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1123 patchcmd = diffcmd +" | git apply "1124 tryPatchCmd = patchcmd +"--check -"1125 applyPatchCmd = patchcmd +"--check --apply -"1126 patch_succeeded =True11271128if os.system(tryPatchCmd) !=0:1129 fixed_rcs_keywords =False1130 patch_succeeded =False1131print"Unfortunately applying the change failed!"11321133# Patch failed, maybe it's just RCS keyword woes. Look through1134# the patch to see if that's possible.1135ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1136file=None1137 pattern =None1138 kwfiles = {}1139forfilein editedFiles | filesToDelete:1140# did this file's delta contain RCS keywords?1141 pattern =p4_keywords_regexp_for_file(file)11421143if pattern:1144# this file is a possibility...look for RCS keywords.1145 regexp = re.compile(pattern, re.VERBOSE)1146for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1147if regexp.search(line):1148if verbose:1149print"got keyword match on%sin%sin%s"% (pattern, line,file)1150 kwfiles[file] = pattern1151break11521153forfilein kwfiles:1154if verbose:1155print"zapping%swith%s"% (line,pattern)1156 self.patchRCSKeywords(file, kwfiles[file])1157 fixed_rcs_keywords =True11581159if fixed_rcs_keywords:1160print"Retrying the patch with RCS keywords cleaned up"1161if os.system(tryPatchCmd) ==0:1162 patch_succeeded =True11631164if not patch_succeeded:1165print"What do you want to do?"1166 response ="x"1167while response !="s"and response !="a"and response !="w":1168 response =raw_input("[s]kip this patch / [a]pply the patch forcibly "1169"and with .rej files / [w]rite the patch to a file (patch.txt) ")1170if response =="s":1171print"Skipping! Good luck with the next patches..."1172for f in editedFiles:1173p4_revert(f)1174for f in filesToAdd:1175 os.remove(f)1176return1177elif response =="a":1178 os.system(applyPatchCmd)1179iflen(filesToAdd) >0:1180print"You may also want to call p4 add on the following files:"1181print" ".join(filesToAdd)1182iflen(filesToDelete):1183print"The following files should be scheduled for deletion with p4 delete:"1184print" ".join(filesToDelete)1185die("Please resolve and submit the conflict manually and "1186+"continue afterwards with git p4 submit --continue")1187elif response =="w":1188system(diffcmd +" > patch.txt")1189print"Patch saved to patch.txt in%s!"% self.clientPath1190die("Please resolve and submit the conflict manually and "1191"continue afterwards with git p4 submit --continue")11921193system(applyPatchCmd)11941195for f in filesToAdd:1196p4_add(f)1197for f in filesToDelete:1198p4_revert(f)1199p4_delete(f)12001201# Set/clear executable bits1202for f in filesToChangeExecBit.keys():1203 mode = filesToChangeExecBit[f]1204setP4ExecBit(f, mode)12051206 logMessage =extractLogMessageFromGitCommit(id)1207 logMessage = logMessage.strip()12081209 template = self.prepareSubmitTemplate()12101211 submitTemplate = self.prepareLogMessage(template, logMessage)12121213if self.preserveUser:1214 submitTemplate = submitTemplate + ("\n######## Actual user%s, modified after commit\n"% p4User)12151216if os.environ.has_key("P4DIFF"):1217del(os.environ["P4DIFF"])1218 diff =""1219for editedFile in editedFiles:1220 diff +=p4_read_pipe(['diff','-du',1221wildcard_encode(editedFile)])12221223 newdiff =""1224for newFile in filesToAdd:1225 newdiff +="==== new file ====\n"1226 newdiff +="--- /dev/null\n"1227 newdiff +="+++%s\n"% newFile1228 f =open(newFile,"r")1229for line in f.readlines():1230 newdiff +="+"+ line1231 f.close()12321233if self.checkAuthorship and not self.p4UserIsMe(p4User):1234 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1235 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1236 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"12371238 separatorLine ="######## everything below this line is just the diff #######\n"12391240(handle, fileName) = tempfile.mkstemp()1241 tmpFile = os.fdopen(handle,"w+")1242if self.isWindows:1243 submitTemplate = submitTemplate.replace("\n","\r\n")1244 separatorLine = separatorLine.replace("\n","\r\n")1245 newdiff = newdiff.replace("\n","\r\n")1246 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1247 tmpFile.close()12481249if self.edit_template(fileName):1250# read the edited message and submit1251 tmpFile =open(fileName,"rb")1252 message = tmpFile.read()1253 tmpFile.close()1254 submitTemplate = message[:message.index(separatorLine)]1255if self.isWindows:1256 submitTemplate = submitTemplate.replace("\r\n","\n")1257p4_write_pipe(['submit','-i'], submitTemplate)12581259if self.preserveUser:1260if p4User:1261# Get last changelist number. Cannot easily get it from1262# the submit command output as the output is1263# unmarshalled.1264 changelist = self.lastP4Changelist()1265 self.modifyChangelistUser(changelist, p4User)12661267# The rename/copy happened by applying a patch that created a1268# new file. This leaves it writable, which confuses p4.1269for f in pureRenameCopy:1270p4_sync(f,"-f")12711272else:1273# skip this patch1274print"Submission cancelled, undoing p4 changes."1275for f in editedFiles:1276p4_revert(f)1277for f in filesToAdd:1278p4_revert(f)1279 os.remove(f)12801281 os.remove(fileName)12821283# Export git tags as p4 labels. Create a p4 label and then tag1284# with that.1285defexportGitTags(self, gitTags):1286 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1287iflen(validLabelRegexp) ==0:1288 validLabelRegexp = defaultLabelRegexp1289 m = re.compile(validLabelRegexp)12901291for name in gitTags:12921293if not m.match(name):1294if verbose:1295print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1296continue12971298# Get the p4 commit this corresponds to1299 logMessage =extractLogMessageFromGitCommit(name)1300 values =extractSettingsGitLog(logMessage)13011302if not values.has_key('change'):1303# a tag pointing to something not sent to p4; ignore1304if verbose:1305print"git tag%sdoes not give a p4 commit"% name1306continue1307else:1308 changelist = values['change']13091310# Get the tag details.1311 inHeader =True1312 isAnnotated =False1313 body = []1314for l inread_pipe_lines(["git","cat-file","-p", name]):1315 l = l.strip()1316if inHeader:1317if re.match(r'tag\s+', l):1318 isAnnotated =True1319elif re.match(r'\s*$', l):1320 inHeader =False1321continue1322else:1323 body.append(l)13241325if not isAnnotated:1326 body = ["lightweight tag imported by git p4\n"]13271328# Create the label - use the same view as the client spec we are using1329 clientSpec =getClientSpec()13301331 labelTemplate ="Label:%s\n"% name1332 labelTemplate +="Description:\n"1333for b in body:1334 labelTemplate +="\t"+ b +"\n"1335 labelTemplate +="View:\n"1336for mapping in clientSpec.mappings:1337 labelTemplate +="\t%s\n"% mapping.depot_side.path13381339p4_write_pipe(["label","-i"], labelTemplate)13401341# Use the label1342p4_system(["tag","-l", name] +1343["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])13441345if verbose:1346print"created p4 label for tag%s"% name13471348defrun(self, args):1349iflen(args) ==0:1350 self.master =currentGitBranch()1351iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1352die("Detecting current git branch failed!")1353eliflen(args) ==1:1354 self.master = args[0]1355if notbranchExists(self.master):1356die("Branch%sdoes not exist"% self.master)1357else:1358return False13591360 allowSubmit =gitConfig("git-p4.allowSubmit")1361iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1362die("%sis not in git-p4.allowSubmit"% self.master)13631364[upstream, settings] =findUpstreamBranchPoint()1365 self.depotPath = settings['depot-paths'][0]1366iflen(self.origin) ==0:1367 self.origin = upstream13681369if self.preserveUser:1370if not self.canChangeChangelists():1371die("Cannot preserve user names without p4 super-user or admin permissions")13721373if self.verbose:1374print"Origin branch is "+ self.origin13751376iflen(self.depotPath) ==0:1377print"Internal error: cannot locate perforce depot path from existing branches"1378 sys.exit(128)13791380 self.useClientSpec =False1381ifgitConfig("git-p4.useclientspec","--bool") =="true":1382 self.useClientSpec =True1383if self.useClientSpec:1384 self.clientSpecDirs =getClientSpec()13851386if self.useClientSpec:1387# all files are relative to the client spec1388 self.clientPath =getClientRoot()1389else:1390 self.clientPath =p4Where(self.depotPath)13911392if self.clientPath =="":1393die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)13941395print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1396 self.oldWorkingDirectory = os.getcwd()13971398# ensure the clientPath exists1399 new_client_dir =False1400if not os.path.exists(self.clientPath):1401 new_client_dir =True1402 os.makedirs(self.clientPath)14031404chdir(self.clientPath)1405print"Synchronizing p4 checkout..."1406if new_client_dir:1407# old one was destroyed, and maybe nobody told p41408p4_sync("...","-f")1409else:1410p4_sync("...")1411 self.check()14121413 commits = []1414for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1415 commits.append(line.strip())1416 commits.reverse()14171418if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1419 self.checkAuthorship =False1420else:1421 self.checkAuthorship =True14221423if self.preserveUser:1424 self.checkValidP4Users(commits)14251426whilelen(commits) >0:1427 commit = commits[0]1428 commits = commits[1:]1429 self.applyCommit(commit)14301431iflen(commits) ==0:1432print"All changes applied!"1433chdir(self.oldWorkingDirectory)14341435 sync =P4Sync()1436 sync.run([])14371438 rebase =P4Rebase()1439 rebase.rebase()14401441ifgitConfig("git-p4.exportLabels","--bool") =="true":1442 self.exportLabels =True14431444if self.exportLabels:1445 p4Labels =getP4Labels(self.depotPath)1446 gitTags =getGitTags()14471448 missingGitTags = gitTags - p4Labels1449 self.exportGitTags(missingGitTags)14501451return True14521453classView(object):1454"""Represent a p4 view ("p4 help views"), and map files in a1455 repo according to the view."""14561457classPath(object):1458"""A depot or client path, possibly containing wildcards.1459 The only one supported is ... at the end, currently.1460 Initialize with the full path, with //depot or //client."""14611462def__init__(self, path, is_depot):1463 self.path = path1464 self.is_depot = is_depot1465 self.find_wildcards()1466# remember the prefix bit, useful for relative mappings1467 m = re.match("(//[^/]+/)", self.path)1468if not m:1469die("Path%sdoes not start with //prefix/"% self.path)1470 prefix = m.group(1)1471if not self.is_depot:1472# strip //client/ on client paths1473 self.path = self.path[len(prefix):]14741475deffind_wildcards(self):1476"""Make sure wildcards are valid, and set up internal1477 variables."""14781479 self.ends_triple_dot =False1480# There are three wildcards allowed in p4 views1481# (see "p4 help views"). This code knows how to1482# handle "..." (only at the end), but cannot deal with1483# "%%n" or "*". Only check the depot_side, as p4 should1484# validate that the client_side matches too.1485if re.search(r'%%[1-9]', self.path):1486die("Can't handle%%n wildcards in view:%s"% self.path)1487if self.path.find("*") >=0:1488die("Can't handle * wildcards in view:%s"% self.path)1489 triple_dot_index = self.path.find("...")1490if triple_dot_index >=0:1491if triple_dot_index !=len(self.path) -3:1492die("Can handle only single ... wildcard, at end:%s"%1493 self.path)1494 self.ends_triple_dot =True14951496defensure_compatible(self, other_path):1497"""Make sure the wildcards agree."""1498if self.ends_triple_dot != other_path.ends_triple_dot:1499die("Both paths must end with ... if either does;\n"+1500"paths:%s %s"% (self.path, other_path.path))15011502defmatch_wildcards(self, test_path):1503"""See if this test_path matches us, and fill in the value1504 of the wildcards if so. Returns a tuple of1505 (True|False, wildcards[]). For now, only the ... at end1506 is supported, so at most one wildcard."""1507if self.ends_triple_dot:1508 dotless = self.path[:-3]1509if test_path.startswith(dotless):1510 wildcard = test_path[len(dotless):]1511return(True, [ wildcard ])1512else:1513if test_path == self.path:1514return(True, [])1515return(False, [])15161517defmatch(self, test_path):1518"""Just return if it matches; don't bother with the wildcards."""1519 b, _ = self.match_wildcards(test_path)1520return b15211522deffill_in_wildcards(self, wildcards):1523"""Return the relative path, with the wildcards filled in1524 if there are any."""1525if self.ends_triple_dot:1526return self.path[:-3] + wildcards[0]1527else:1528return self.path15291530classMapping(object):1531def__init__(self, depot_side, client_side, overlay, exclude):1532# depot_side is without the trailing /... if it had one1533 self.depot_side = View.Path(depot_side, is_depot=True)1534 self.client_side = View.Path(client_side, is_depot=False)1535 self.overlay = overlay # started with "+"1536 self.exclude = exclude # started with "-"1537assert not(self.overlay and self.exclude)1538 self.depot_side.ensure_compatible(self.client_side)15391540def__str__(self):1541 c =" "1542if self.overlay:1543 c ="+"1544if self.exclude:1545 c ="-"1546return"View.Mapping:%s%s->%s"% \1547(c, self.depot_side.path, self.client_side.path)15481549defmap_depot_to_client(self, depot_path):1550"""Calculate the client path if using this mapping on the1551 given depot path; does not consider the effect of other1552 mappings in a view. Even excluded mappings are returned."""1553 matches, wildcards = self.depot_side.match_wildcards(depot_path)1554if not matches:1555return""1556 client_path = self.client_side.fill_in_wildcards(wildcards)1557return client_path15581559#1560# View methods1561#1562def__init__(self):1563 self.mappings = []15641565defappend(self, view_line):1566"""Parse a view line, splitting it into depot and client1567 sides. Append to self.mappings, preserving order."""15681569# Split the view line into exactly two words. P4 enforces1570# structure on these lines that simplifies this quite a bit.1571#1572# Either or both words may be double-quoted.1573# Single quotes do not matter.1574# Double-quote marks cannot occur inside the words.1575# A + or - prefix is also inside the quotes.1576# There are no quotes unless they contain a space.1577# The line is already white-space stripped.1578# The two words are separated by a single space.1579#1580if view_line[0] =='"':1581# First word is double quoted. Find its end.1582 close_quote_index = view_line.find('"',1)1583if close_quote_index <=0:1584die("No first-word closing quote found:%s"% view_line)1585 depot_side = view_line[1:close_quote_index]1586# skip closing quote and space1587 rhs_index = close_quote_index +1+11588else:1589 space_index = view_line.find(" ")1590if space_index <=0:1591die("No word-splitting space found:%s"% view_line)1592 depot_side = view_line[0:space_index]1593 rhs_index = space_index +115941595if view_line[rhs_index] =='"':1596# Second word is double quoted. Make sure there is a1597# double quote at the end too.1598if not view_line.endswith('"'):1599die("View line with rhs quote should end with one:%s"%1600 view_line)1601# skip the quotes1602 client_side = view_line[rhs_index+1:-1]1603else:1604 client_side = view_line[rhs_index:]16051606# prefix + means overlay on previous mapping1607 overlay =False1608if depot_side.startswith("+"):1609 overlay =True1610 depot_side = depot_side[1:]16111612# prefix - means exclude this path1613 exclude =False1614if depot_side.startswith("-"):1615 exclude =True1616 depot_side = depot_side[1:]16171618 m = View.Mapping(depot_side, client_side, overlay, exclude)1619 self.mappings.append(m)16201621defmap_in_client(self, depot_path):1622"""Return the relative location in the client where this1623 depot file should live. Returns "" if the file should1624 not be mapped in the client."""16251626 paths_filled = []1627 client_path =""16281629# look at later entries first1630for m in self.mappings[::-1]:16311632# see where will this path end up in the client1633 p = m.map_depot_to_client(depot_path)16341635if p =="":1636# Depot path does not belong in client. Must remember1637# this, as previous items should not cause files to1638# exist in this path either. Remember that the list is1639# being walked from the end, which has higher precedence.1640# Overlap mappings do not exclude previous mappings.1641if not m.overlay:1642 paths_filled.append(m.client_side)16431644else:1645# This mapping matched; no need to search any further.1646# But, the mapping could be rejected if the client path1647# has already been claimed by an earlier mapping (i.e.1648# one later in the list, which we are walking backwards).1649 already_mapped_in_client =False1650for f in paths_filled:1651# this is View.Path.match1652if f.match(p):1653 already_mapped_in_client =True1654break1655if not already_mapped_in_client:1656# Include this file, unless it is from a line that1657# explicitly said to exclude it.1658if not m.exclude:1659 client_path = p16601661# a match, even if rejected, always stops the search1662break16631664return client_path16651666classP4Sync(Command, P4UserMap):1667 delete_actions = ("delete","move/delete","purge")16681669def__init__(self):1670 Command.__init__(self)1671 P4UserMap.__init__(self)1672 self.options = [1673 optparse.make_option("--branch", dest="branch"),1674 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1675 optparse.make_option("--changesfile", dest="changesFile"),1676 optparse.make_option("--silent", dest="silent", action="store_true"),1677 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1678 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1679 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1680help="Import into refs/heads/ , not refs/remotes"),1681 optparse.make_option("--max-changes", dest="maxChanges"),1682 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1683help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1684 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1685help="Only sync files that are included in the Perforce Client Spec")1686]1687 self.description ="""Imports from Perforce into a git repository.\n1688 example:1689 //depot/my/project/ -- to import the current head1690 //depot/my/project/@all -- to import everything1691 //depot/my/project/@1,6 -- to import only from revision 1 to 616921693 (a ... is not needed in the path p4 specification, it's added implicitly)"""16941695 self.usage +=" //depot/path[@revRange]"1696 self.silent =False1697 self.createdBranches =set()1698 self.committedChanges =set()1699 self.branch =""1700 self.detectBranches =False1701 self.detectLabels =False1702 self.importLabels =False1703 self.changesFile =""1704 self.syncWithOrigin =True1705 self.importIntoRemotes =True1706 self.maxChanges =""1707 self.isWindows = (platform.system() =="Windows")1708 self.keepRepoPath =False1709 self.depotPaths =None1710 self.p4BranchesInGit = []1711 self.cloneExclude = []1712 self.useClientSpec =False1713 self.useClientSpec_from_options =False1714 self.clientSpecDirs =None1715 self.tempBranches = []1716 self.tempBranchLocation ="git-p4-tmp"17171718ifgitConfig("git-p4.syncFromOrigin") =="false":1719 self.syncWithOrigin =False17201721# Force a checkpoint in fast-import and wait for it to finish1722defcheckpoint(self):1723 self.gitStream.write("checkpoint\n\n")1724 self.gitStream.write("progress checkpoint\n\n")1725 out = self.gitOutput.readline()1726if self.verbose:1727print"checkpoint finished: "+ out17281729defextractFilesFromCommit(self, commit):1730 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1731for path in self.cloneExclude]1732 files = []1733 fnum =01734while commit.has_key("depotFile%s"% fnum):1735 path = commit["depotFile%s"% fnum]17361737if[p for p in self.cloneExclude1738ifp4PathStartsWith(path, p)]:1739 found =False1740else:1741 found = [p for p in self.depotPaths1742ifp4PathStartsWith(path, p)]1743if not found:1744 fnum = fnum +11745continue17461747file= {}1748file["path"] = path1749file["rev"] = commit["rev%s"% fnum]1750file["action"] = commit["action%s"% fnum]1751file["type"] = commit["type%s"% fnum]1752 files.append(file)1753 fnum = fnum +11754return files17551756defstripRepoPath(self, path, prefixes):1757if self.useClientSpec:1758return self.clientSpecDirs.map_in_client(path)17591760if self.keepRepoPath:1761 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]17621763for p in prefixes:1764ifp4PathStartsWith(path, p):1765 path = path[len(p):]17661767return path17681769defsplitFilesIntoBranches(self, commit):1770 branches = {}1771 fnum =01772while commit.has_key("depotFile%s"% fnum):1773 path = commit["depotFile%s"% fnum]1774 found = [p for p in self.depotPaths1775ifp4PathStartsWith(path, p)]1776if not found:1777 fnum = fnum +11778continue17791780file= {}1781file["path"] = path1782file["rev"] = commit["rev%s"% fnum]1783file["action"] = commit["action%s"% fnum]1784file["type"] = commit["type%s"% fnum]1785 fnum = fnum +117861787 relPath = self.stripRepoPath(path, self.depotPaths)1788 relPath =wildcard_decode(relPath)17891790for branch in self.knownBranches.keys():17911792# add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.21793if relPath.startswith(branch +"/"):1794if branch not in branches:1795 branches[branch] = []1796 branches[branch].append(file)1797break17981799return branches18001801# output one file from the P4 stream1802# - helper for streamP4Files18031804defstreamOneP4File(self,file, contents):1805 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)1806 relPath =wildcard_decode(relPath)1807if verbose:1808 sys.stderr.write("%s\n"% relPath)18091810(type_base, type_mods) =split_p4_type(file["type"])18111812 git_mode ="100644"1813if"x"in type_mods:1814 git_mode ="100755"1815if type_base =="symlink":1816 git_mode ="120000"1817# p4 print on a symlink contains "target\n"; remove the newline1818 data =''.join(contents)1819 contents = [data[:-1]]18201821if type_base =="utf16":1822# p4 delivers different text in the python output to -G1823# than it does when using "print -o", or normal p4 client1824# operations. utf16 is converted to ascii or utf8, perhaps.1825# But ascii text saved as -t utf16 is completely mangled.1826# Invoke print -o to get the real contents.1827 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])1828 contents = [ text ]18291830if type_base =="apple":1831# Apple filetype files will be streamed as a concatenation of1832# its appledouble header and the contents. This is useless1833# on both macs and non-macs. If using "print -q -o xx", it1834# will create "xx" with the data, and "%xx" with the header.1835# This is also not very useful.1836#1837# Ideally, someday, this script can learn how to generate1838# appledouble files directly and import those to git, but1839# non-mac machines can never find a use for apple filetype.1840print"\nIgnoring apple filetype file%s"%file['depotFile']1841return18421843# Perhaps windows wants unicode, utf16 newlines translated too;1844# but this is not doing it.1845if self.isWindows and type_base =="text":1846 mangled = []1847for data in contents:1848 data = data.replace("\r\n","\n")1849 mangled.append(data)1850 contents = mangled18511852# Note that we do not try to de-mangle keywords on utf16 files,1853# even though in theory somebody may want that.1854 pattern =p4_keywords_regexp_for_type(type_base, type_mods)1855if pattern:1856 regexp = re.compile(pattern, re.VERBOSE)1857 text =''.join(contents)1858 text = regexp.sub(r'$\1$', text)1859 contents = [ text ]18601861 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))18621863# total length...1864 length =01865for d in contents:1866 length = length +len(d)18671868 self.gitStream.write("data%d\n"% length)1869for d in contents:1870 self.gitStream.write(d)1871 self.gitStream.write("\n")18721873defstreamOneP4Deletion(self,file):1874 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)1875 relPath =wildcard_decode(relPath)1876if verbose:1877 sys.stderr.write("delete%s\n"% relPath)1878 self.gitStream.write("D%s\n"% relPath)18791880# handle another chunk of streaming data1881defstreamP4FilesCb(self, marshalled):18821883if marshalled.has_key('depotFile')and self.stream_have_file_info:1884# start of a new file - output the old one first1885 self.streamOneP4File(self.stream_file, self.stream_contents)1886 self.stream_file = {}1887 self.stream_contents = []1888 self.stream_have_file_info =False18891890# pick up the new file information... for the1891# 'data' field we need to append to our array1892for k in marshalled.keys():1893if k =='data':1894 self.stream_contents.append(marshalled['data'])1895else:1896 self.stream_file[k] = marshalled[k]18971898 self.stream_have_file_info =True18991900# Stream directly from "p4 files" into "git fast-import"1901defstreamP4Files(self, files):1902 filesForCommit = []1903 filesToRead = []1904 filesToDelete = []19051906for f in files:1907# if using a client spec, only add the files that have1908# a path in the client1909if self.clientSpecDirs:1910if self.clientSpecDirs.map_in_client(f['path']) =="":1911continue19121913 filesForCommit.append(f)1914if f['action']in self.delete_actions:1915 filesToDelete.append(f)1916else:1917 filesToRead.append(f)19181919# deleted files...1920for f in filesToDelete:1921 self.streamOneP4Deletion(f)19221923iflen(filesToRead) >0:1924 self.stream_file = {}1925 self.stream_contents = []1926 self.stream_have_file_info =False19271928# curry self argument1929defstreamP4FilesCbSelf(entry):1930 self.streamP4FilesCb(entry)19311932 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]19331934p4CmdList(["-x","-","print"],1935 stdin=fileArgs,1936 cb=streamP4FilesCbSelf)19371938# do the last chunk1939if self.stream_file.has_key('depotFile'):1940 self.streamOneP4File(self.stream_file, self.stream_contents)19411942defmake_email(self, userid):1943if userid in self.users:1944return self.users[userid]1945else:1946return"%s<a@b>"% userid19471948# Stream a p4 tag1949defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):1950if verbose:1951print"writing tag%sfor commit%s"% (labelName, commit)1952 gitStream.write("tag%s\n"% labelName)1953 gitStream.write("from%s\n"% commit)19541955if labelDetails.has_key('Owner'):1956 owner = labelDetails["Owner"]1957else:1958 owner =None19591960# Try to use the owner of the p4 label, or failing that,1961# the current p4 user id.1962if owner:1963 email = self.make_email(owner)1964else:1965 email = self.make_email(self.p4UserId())1966 tagger ="%s %s %s"% (email, epoch, self.tz)19671968 gitStream.write("tagger%s\n"% tagger)19691970print"labelDetails=",labelDetails1971if labelDetails.has_key('Description'):1972 description = labelDetails['Description']1973else:1974 description ='Label from git p4'19751976 gitStream.write("data%d\n"%len(description))1977 gitStream.write(description)1978 gitStream.write("\n")19791980defcommit(self, details, files, branch, branchPrefixes, parent =""):1981 epoch = details["time"]1982 author = details["user"]1983 self.branchPrefixes = branchPrefixes19841985if self.verbose:1986print"commit into%s"% branch19871988# start with reading files; if that fails, we should not1989# create a commit.1990 new_files = []1991for f in files:1992if[p for p in branchPrefixes ifp4PathStartsWith(f['path'], p)]:1993 new_files.append(f)1994else:1995 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])19961997 self.gitStream.write("commit%s\n"% branch)1998# gitStream.write("mark :%s\n" % details["change"])1999 self.committedChanges.add(int(details["change"]))2000 committer =""2001if author not in self.users:2002 self.getUserMapFromPerforceServer()2003 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)20042005 self.gitStream.write("committer%s\n"% committer)20062007 self.gitStream.write("data <<EOT\n")2008 self.gitStream.write(details["desc"])2009 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"2010% (','.join(branchPrefixes), details["change"]))2011iflen(details['options']) >0:2012 self.gitStream.write(": options =%s"% details['options'])2013 self.gitStream.write("]\nEOT\n\n")20142015iflen(parent) >0:2016if self.verbose:2017print"parent%s"% parent2018 self.gitStream.write("from%s\n"% parent)20192020 self.streamP4Files(new_files)2021 self.gitStream.write("\n")20222023 change =int(details["change"])20242025if self.labels.has_key(change):2026 label = self.labels[change]2027 labelDetails = label[0]2028 labelRevisions = label[1]2029if self.verbose:2030print"Change%sis labelled%s"% (change, labelDetails)20312032 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2033for p in branchPrefixes])20342035iflen(files) ==len(labelRevisions):20362037 cleanedFiles = {}2038for info in files:2039if info["action"]in self.delete_actions:2040continue2041 cleanedFiles[info["depotFile"]] = info["rev"]20422043if cleanedFiles == labelRevisions:2044 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)20452046else:2047if not self.silent:2048print("Tag%sdoes not match with change%s: files do not match."2049% (labelDetails["label"], change))20502051else:2052if not self.silent:2053print("Tag%sdoes not match with change%s: file count is different."2054% (labelDetails["label"], change))20552056# Build a dictionary of changelists and labels, for "detect-labels" option.2057defgetLabels(self):2058 self.labels = {}20592060 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2061iflen(l) >0and not self.silent:2062print"Finding files belonging to labels in%s"% `self.depotPaths`20632064for output in l:2065 label = output["label"]2066 revisions = {}2067 newestChange =02068if self.verbose:2069print"Querying files for label%s"% label2070forfileinp4CmdList(["files"] +2071["%s...@%s"% (p, label)2072for p in self.depotPaths]):2073 revisions[file["depotFile"]] =file["rev"]2074 change =int(file["change"])2075if change > newestChange:2076 newestChange = change20772078 self.labels[newestChange] = [output, revisions]20792080if self.verbose:2081print"Label changes:%s"% self.labels.keys()20822083# Import p4 labels as git tags. A direct mapping does not2084# exist, so assume that if all the files are at the same revision2085# then we can use that, or it's something more complicated we should2086# just ignore.2087defimportP4Labels(self, stream, p4Labels):2088if verbose:2089print"import p4 labels: "+' '.join(p4Labels)20902091 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2092 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2093iflen(validLabelRegexp) ==0:2094 validLabelRegexp = defaultLabelRegexp2095 m = re.compile(validLabelRegexp)20962097for name in p4Labels:2098 commitFound =False20992100if not m.match(name):2101if verbose:2102print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2103continue21042105if name in ignoredP4Labels:2106continue21072108 labelDetails =p4CmdList(['label',"-o", name])[0]21092110# get the most recent changelist for each file in this label2111 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2112for p in self.depotPaths])21132114if change.has_key('change'):2115# find the corresponding git commit; take the oldest commit2116 changelist =int(change['change'])2117 gitCommit =read_pipe(["git","rev-list","--max-count=1",2118"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2119iflen(gitCommit) ==0:2120print"could not find git commit for changelist%d"% changelist2121else:2122 gitCommit = gitCommit.strip()2123 commitFound =True2124# Convert from p4 time format2125try:2126 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2127exceptValueError:2128print"Could not convert label time%s"% labelDetail['Update']2129 tmwhen =121302131 when =int(time.mktime(tmwhen))2132 self.streamTag(stream, name, labelDetails, gitCommit, when)2133if verbose:2134print"p4 label%smapped to git commit%s"% (name, gitCommit)2135else:2136if verbose:2137print"Label%shas no changelists - possibly deleted?"% name21382139if not commitFound:2140# We can't import this label; don't try again as it will get very2141# expensive repeatedly fetching all the files for labels that will2142# never be imported. If the label is moved in the future, the2143# ignore will need to be removed manually.2144system(["git","config","--add","git-p4.ignoredP4Labels", name])21452146defguessProjectName(self):2147for p in self.depotPaths:2148if p.endswith("/"):2149 p = p[:-1]2150 p = p[p.strip().rfind("/") +1:]2151if not p.endswith("/"):2152 p +="/"2153return p21542155defgetBranchMapping(self):2156 lostAndFoundBranches =set()21572158 user =gitConfig("git-p4.branchUser")2159iflen(user) >0:2160 command ="branches -u%s"% user2161else:2162 command ="branches"21632164for info inp4CmdList(command):2165 details =p4Cmd(["branch","-o", info["branch"]])2166 viewIdx =02167while details.has_key("View%s"% viewIdx):2168 paths = details["View%s"% viewIdx].split(" ")2169 viewIdx = viewIdx +12170# require standard //depot/foo/... //depot/bar/... mapping2171iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2172continue2173 source = paths[0]2174 destination = paths[1]2175## HACK2176ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2177 source = source[len(self.depotPaths[0]):-4]2178 destination = destination[len(self.depotPaths[0]):-4]21792180if destination in self.knownBranches:2181if not self.silent:2182print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2183print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2184continue21852186 self.knownBranches[destination] = source21872188 lostAndFoundBranches.discard(destination)21892190if source not in self.knownBranches:2191 lostAndFoundBranches.add(source)21922193# Perforce does not strictly require branches to be defined, so we also2194# check git config for a branch list.2195#2196# Example of branch definition in git config file:2197# [git-p4]2198# branchList=main:branchA2199# branchList=main:branchB2200# branchList=branchA:branchC2201 configBranches =gitConfigList("git-p4.branchList")2202for branch in configBranches:2203if branch:2204(source, destination) = branch.split(":")2205 self.knownBranches[destination] = source22062207 lostAndFoundBranches.discard(destination)22082209if source not in self.knownBranches:2210 lostAndFoundBranches.add(source)221122122213for branch in lostAndFoundBranches:2214 self.knownBranches[branch] = branch22152216defgetBranchMappingFromGitBranches(self):2217 branches =p4BranchesInGit(self.importIntoRemotes)2218for branch in branches.keys():2219if branch =="master":2220 branch ="main"2221else:2222 branch = branch[len(self.projectName):]2223 self.knownBranches[branch] = branch22242225deflistExistingP4GitBranches(self):2226# branches holds mapping from name to commit2227 branches =p4BranchesInGit(self.importIntoRemotes)2228 self.p4BranchesInGit = branches.keys()2229for branch in branches.keys():2230 self.initialParents[self.refPrefix + branch] = branches[branch]22312232defupdateOptionDict(self, d):2233 option_keys = {}2234if self.keepRepoPath:2235 option_keys['keepRepoPath'] =122362237 d["options"] =' '.join(sorted(option_keys.keys()))22382239defreadOptions(self, d):2240 self.keepRepoPath = (d.has_key('options')2241and('keepRepoPath'in d['options']))22422243defgitRefForBranch(self, branch):2244if branch =="main":2245return self.refPrefix +"master"22462247iflen(branch) <=0:2248return branch22492250return self.refPrefix + self.projectName + branch22512252defgitCommitByP4Change(self, ref, change):2253if self.verbose:2254print"looking in ref "+ ref +" for change%susing bisect..."% change22552256 earliestCommit =""2257 latestCommit =parseRevision(ref)22582259while True:2260if self.verbose:2261print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2262 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2263iflen(next) ==0:2264if self.verbose:2265print"argh"2266return""2267 log =extractLogMessageFromGitCommit(next)2268 settings =extractSettingsGitLog(log)2269 currentChange =int(settings['change'])2270if self.verbose:2271print"current change%s"% currentChange22722273if currentChange == change:2274if self.verbose:2275print"found%s"% next2276return next22772278if currentChange < change:2279 earliestCommit ="^%s"% next2280else:2281 latestCommit ="%s"% next22822283return""22842285defimportNewBranch(self, branch, maxChange):2286# make fast-import flush all changes to disk and update the refs using the checkpoint2287# command so that we can try to find the branch parent in the git history2288 self.gitStream.write("checkpoint\n\n");2289 self.gitStream.flush();2290 branchPrefix = self.depotPaths[0] + branch +"/"2291range="@1,%s"% maxChange2292#print "prefix" + branchPrefix2293 changes =p4ChangesForPaths([branchPrefix],range)2294iflen(changes) <=0:2295return False2296 firstChange = changes[0]2297#print "first change in branch: %s" % firstChange2298 sourceBranch = self.knownBranches[branch]2299 sourceDepotPath = self.depotPaths[0] + sourceBranch2300 sourceRef = self.gitRefForBranch(sourceBranch)2301#print "source " + sourceBranch23022303 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2304#print "branch parent: %s" % branchParentChange2305 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2306iflen(gitParent) >0:2307 self.initialParents[self.gitRefForBranch(branch)] = gitParent2308#print "parent git commit: %s" % gitParent23092310 self.importChanges(changes)2311return True23122313defsearchParent(self, parent, branch, target):2314 parentFound =False2315for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2316 blob = blob.strip()2317iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2318 parentFound =True2319if self.verbose:2320print"Found parent of%sin commit%s"% (branch, blob)2321break2322if parentFound:2323return blob2324else:2325return None23262327defimportChanges(self, changes):2328 cnt =12329for change in changes:2330 description =p4Cmd(["describe",str(change)])2331 self.updateOptionDict(description)23322333if not self.silent:2334 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2335 sys.stdout.flush()2336 cnt = cnt +123372338try:2339if self.detectBranches:2340 branches = self.splitFilesIntoBranches(description)2341for branch in branches.keys():2342## HACK --hwn2343 branchPrefix = self.depotPaths[0] + branch +"/"23442345 parent =""23462347 filesForCommit = branches[branch]23482349if self.verbose:2350print"branch is%s"% branch23512352 self.updatedBranches.add(branch)23532354if branch not in self.createdBranches:2355 self.createdBranches.add(branch)2356 parent = self.knownBranches[branch]2357if parent == branch:2358 parent =""2359else:2360 fullBranch = self.projectName + branch2361if fullBranch not in self.p4BranchesInGit:2362if not self.silent:2363print("\nImporting new branch%s"% fullBranch);2364if self.importNewBranch(branch, change -1):2365 parent =""2366 self.p4BranchesInGit.append(fullBranch)2367if not self.silent:2368print("\nResuming with change%s"% change);23692370if self.verbose:2371print"parent determined through known branches:%s"% parent23722373 branch = self.gitRefForBranch(branch)2374 parent = self.gitRefForBranch(parent)23752376if self.verbose:2377print"looking for initial parent for%s; current parent is%s"% (branch, parent)23782379iflen(parent) ==0and branch in self.initialParents:2380 parent = self.initialParents[branch]2381del self.initialParents[branch]23822383 blob =None2384iflen(parent) >0:2385 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2386if self.verbose:2387print"Creating temporary branch: "+ tempBranch2388 self.commit(description, filesForCommit, tempBranch, [branchPrefix])2389 self.tempBranches.append(tempBranch)2390 self.checkpoint()2391 blob = self.searchParent(parent, branch, tempBranch)2392if blob:2393 self.commit(description, filesForCommit, branch, [branchPrefix], blob)2394else:2395if self.verbose:2396print"Parent of%snot found. Committing into head of%s"% (branch, parent)2397 self.commit(description, filesForCommit, branch, [branchPrefix], parent)2398else:2399 files = self.extractFilesFromCommit(description)2400 self.commit(description, files, self.branch, self.depotPaths,2401 self.initialParent)2402 self.initialParent =""2403exceptIOError:2404print self.gitError.read()2405 sys.exit(1)24062407defimportHeadRevision(self, revision):2408print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)24092410 details = {}2411 details["user"] ="git perforce import user"2412 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2413% (' '.join(self.depotPaths), revision))2414 details["change"] = revision2415 newestRevision =024162417 fileCnt =02418 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]24192420for info inp4CmdList(["files"] + fileArgs):24212422if'code'in info and info['code'] =='error':2423 sys.stderr.write("p4 returned an error:%s\n"2424% info['data'])2425if info['data'].find("must refer to client") >=0:2426 sys.stderr.write("This particular p4 error is misleading.\n")2427 sys.stderr.write("Perhaps the depot path was misspelled.\n");2428 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2429 sys.exit(1)2430if'p4ExitCode'in info:2431 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2432 sys.exit(1)243324342435 change =int(info["change"])2436if change > newestRevision:2437 newestRevision = change24382439if info["action"]in self.delete_actions:2440# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2441#fileCnt = fileCnt + 12442continue24432444for prop in["depotFile","rev","action","type"]:2445 details["%s%s"% (prop, fileCnt)] = info[prop]24462447 fileCnt = fileCnt +124482449 details["change"] = newestRevision24502451# Use time from top-most change so that all git p4 clones of2452# the same p4 repo have the same commit SHA1s.2453 res =p4CmdList("describe -s%d"% newestRevision)2454 newestTime =None2455for r in res:2456if r.has_key('time'):2457 newestTime =int(r['time'])2458if newestTime is None:2459die("\"describe -s\"on newest change%ddid not give a time")2460 details["time"] = newestTime24612462 self.updateOptionDict(details)2463try:2464 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)2465exceptIOError:2466print"IO error with git fast-import. Is your git version recent enough?"2467print self.gitError.read()246824692470defrun(self, args):2471 self.depotPaths = []2472 self.changeRange =""2473 self.initialParent =""2474 self.previousDepotPaths = []24752476# map from branch depot path to parent branch2477 self.knownBranches = {}2478 self.initialParents = {}2479 self.hasOrigin =originP4BranchesExist()2480if not self.syncWithOrigin:2481 self.hasOrigin =False24822483if self.importIntoRemotes:2484 self.refPrefix ="refs/remotes/p4/"2485else:2486 self.refPrefix ="refs/heads/p4/"24872488if self.syncWithOrigin and self.hasOrigin:2489if not self.silent:2490print"Syncing with origin first by calling git fetch origin"2491system("git fetch origin")24922493iflen(self.branch) ==0:2494 self.branch = self.refPrefix +"master"2495ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2496system("git update-ref%srefs/heads/p4"% self.branch)2497system("git branch -D p4");2498# create it /after/ importing, when master exists2499if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2500system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))25012502# accept either the command-line option, or the configuration variable2503if self.useClientSpec:2504# will use this after clone to set the variable2505 self.useClientSpec_from_options =True2506else:2507ifgitConfig("git-p4.useclientspec","--bool") =="true":2508 self.useClientSpec =True2509if self.useClientSpec:2510 self.clientSpecDirs =getClientSpec()25112512# TODO: should always look at previous commits,2513# merge with previous imports, if possible.2514if args == []:2515if self.hasOrigin:2516createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2517 self.listExistingP4GitBranches()25182519iflen(self.p4BranchesInGit) >1:2520if not self.silent:2521print"Importing from/into multiple branches"2522 self.detectBranches =True25232524if self.verbose:2525print"branches:%s"% self.p4BranchesInGit25262527 p4Change =02528for branch in self.p4BranchesInGit:2529 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)25302531 settings =extractSettingsGitLog(logMsg)25322533 self.readOptions(settings)2534if(settings.has_key('depot-paths')2535and settings.has_key('change')):2536 change =int(settings['change']) +12537 p4Change =max(p4Change, change)25382539 depotPaths =sorted(settings['depot-paths'])2540if self.previousDepotPaths == []:2541 self.previousDepotPaths = depotPaths2542else:2543 paths = []2544for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2545 prev_list = prev.split("/")2546 cur_list = cur.split("/")2547for i inrange(0,min(len(cur_list),len(prev_list))):2548if cur_list[i] <> prev_list[i]:2549 i = i -12550break25512552 paths.append("/".join(cur_list[:i +1]))25532554 self.previousDepotPaths = paths25552556if p4Change >0:2557 self.depotPaths =sorted(self.previousDepotPaths)2558 self.changeRange ="@%s,#head"% p4Change2559if not self.detectBranches:2560 self.initialParent =parseRevision(self.branch)2561if not self.silent and not self.detectBranches:2562print"Performing incremental import into%sgit branch"% self.branch25632564if not self.branch.startswith("refs/"):2565 self.branch ="refs/heads/"+ self.branch25662567iflen(args) ==0and self.depotPaths:2568if not self.silent:2569print"Depot paths:%s"%' '.join(self.depotPaths)2570else:2571if self.depotPaths and self.depotPaths != args:2572print("previous import used depot path%sand now%swas specified. "2573"This doesn't work!"% (' '.join(self.depotPaths),2574' '.join(args)))2575 sys.exit(1)25762577 self.depotPaths =sorted(args)25782579 revision =""2580 self.users = {}25812582# Make sure no revision specifiers are used when --changesfile2583# is specified.2584 bad_changesfile =False2585iflen(self.changesFile) >0:2586for p in self.depotPaths:2587if p.find("@") >=0or p.find("#") >=0:2588 bad_changesfile =True2589break2590if bad_changesfile:2591die("Option --changesfile is incompatible with revision specifiers")25922593 newPaths = []2594for p in self.depotPaths:2595if p.find("@") != -1:2596 atIdx = p.index("@")2597 self.changeRange = p[atIdx:]2598if self.changeRange =="@all":2599 self.changeRange =""2600elif','not in self.changeRange:2601 revision = self.changeRange2602 self.changeRange =""2603 p = p[:atIdx]2604elif p.find("#") != -1:2605 hashIdx = p.index("#")2606 revision = p[hashIdx:]2607 p = p[:hashIdx]2608elif self.previousDepotPaths == []:2609# pay attention to changesfile, if given, else import2610# the entire p4 tree at the head revision2611iflen(self.changesFile) ==0:2612 revision ="#head"26132614 p = re.sub("\.\.\.$","", p)2615if not p.endswith("/"):2616 p +="/"26172618 newPaths.append(p)26192620 self.depotPaths = newPaths26212622 self.loadUserMapFromCache()2623 self.labels = {}2624if self.detectLabels:2625 self.getLabels();26262627if self.detectBranches:2628## FIXME - what's a P4 projectName ?2629 self.projectName = self.guessProjectName()26302631if self.hasOrigin:2632 self.getBranchMappingFromGitBranches()2633else:2634 self.getBranchMapping()2635if self.verbose:2636print"p4-git branches:%s"% self.p4BranchesInGit2637print"initial parents:%s"% self.initialParents2638for b in self.p4BranchesInGit:2639if b !="master":26402641## FIXME2642 b = b[len(self.projectName):]2643 self.createdBranches.add(b)26442645 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))26462647 importProcess = subprocess.Popen(["git","fast-import"],2648 stdin=subprocess.PIPE, stdout=subprocess.PIPE,2649 stderr=subprocess.PIPE);2650 self.gitOutput = importProcess.stdout2651 self.gitStream = importProcess.stdin2652 self.gitError = importProcess.stderr26532654if revision:2655 self.importHeadRevision(revision)2656else:2657 changes = []26582659iflen(self.changesFile) >0:2660 output =open(self.changesFile).readlines()2661 changeSet =set()2662for line in output:2663 changeSet.add(int(line))26642665for change in changeSet:2666 changes.append(change)26672668 changes.sort()2669else:2670# catch "git p4 sync" with no new branches, in a repo that2671# does not have any existing p4 branches2672iflen(args) ==0and not self.p4BranchesInGit:2673die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2674if self.verbose:2675print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2676 self.changeRange)2677 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)26782679iflen(self.maxChanges) >0:2680 changes = changes[:min(int(self.maxChanges),len(changes))]26812682iflen(changes) ==0:2683if not self.silent:2684print"No changes to import!"2685else:2686if not self.silent and not self.detectBranches:2687print"Import destination:%s"% self.branch26882689 self.updatedBranches =set()26902691 self.importChanges(changes)26922693if not self.silent:2694print""2695iflen(self.updatedBranches) >0:2696 sys.stdout.write("Updated branches: ")2697for b in self.updatedBranches:2698 sys.stdout.write("%s"% b)2699 sys.stdout.write("\n")27002701ifgitConfig("git-p4.importLabels","--bool") =="true":2702 self.importLabels =True27032704if self.importLabels:2705 p4Labels =getP4Labels(self.depotPaths)2706 gitTags =getGitTags()27072708 missingP4Labels = p4Labels - gitTags2709 self.importP4Labels(self.gitStream, missingP4Labels)27102711 self.gitStream.close()2712if importProcess.wait() !=0:2713die("fast-import failed:%s"% self.gitError.read())2714 self.gitOutput.close()2715 self.gitError.close()27162717# Cleanup temporary branches created during import2718if self.tempBranches != []:2719for branch in self.tempBranches:2720read_pipe("git update-ref -d%s"% branch)2721 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))27222723return True27242725classP4Rebase(Command):2726def__init__(self):2727 Command.__init__(self)2728 self.options = [2729 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2730]2731 self.importLabels =False2732 self.description = ("Fetches the latest revision from perforce and "2733+"rebases the current work (branch) against it")27342735defrun(self, args):2736 sync =P4Sync()2737 sync.importLabels = self.importLabels2738 sync.run([])27392740return self.rebase()27412742defrebase(self):2743if os.system("git update-index --refresh") !=0:2744die("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.");2745iflen(read_pipe("git diff-index HEAD --")) >0:2746die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");27472748[upstream, settings] =findUpstreamBranchPoint()2749iflen(upstream) ==0:2750die("Cannot find upstream branchpoint for rebase")27512752# the branchpoint may be p4/foo~3, so strip off the parent2753 upstream = re.sub("~[0-9]+$","", upstream)27542755print"Rebasing the current branch onto%s"% upstream2756 oldHead =read_pipe("git rev-parse HEAD").strip()2757system("git rebase%s"% upstream)2758system("git diff-tree --stat --summary -M%sHEAD"% oldHead)2759return True27602761classP4Clone(P4Sync):2762def__init__(self):2763 P4Sync.__init__(self)2764 self.description ="Creates a new git repository and imports from Perforce into it"2765 self.usage ="usage: %prog [options] //depot/path[@revRange]"2766 self.options += [2767 optparse.make_option("--destination", dest="cloneDestination",2768 action='store', default=None,2769help="where to leave result of the clone"),2770 optparse.make_option("-/", dest="cloneExclude",2771 action="append",type="string",2772help="exclude depot path"),2773 optparse.make_option("--bare", dest="cloneBare",2774 action="store_true", default=False),2775]2776 self.cloneDestination =None2777 self.needsGit =False2778 self.cloneBare =False27792780# This is required for the "append" cloneExclude action2781defensure_value(self, attr, value):2782if nothasattr(self, attr)orgetattr(self, attr)is None:2783setattr(self, attr, value)2784returngetattr(self, attr)27852786defdefaultDestination(self, args):2787## TODO: use common prefix of args?2788 depotPath = args[0]2789 depotDir = re.sub("(@[^@]*)$","", depotPath)2790 depotDir = re.sub("(#[^#]*)$","", depotDir)2791 depotDir = re.sub(r"\.\.\.$","", depotDir)2792 depotDir = re.sub(r"/$","", depotDir)2793return os.path.split(depotDir)[1]27942795defrun(self, args):2796iflen(args) <1:2797return False27982799if self.keepRepoPath and not self.cloneDestination:2800 sys.stderr.write("Must specify destination for --keep-path\n")2801 sys.exit(1)28022803 depotPaths = args28042805if not self.cloneDestination andlen(depotPaths) >1:2806 self.cloneDestination = depotPaths[-1]2807 depotPaths = depotPaths[:-1]28082809 self.cloneExclude = ["/"+p for p in self.cloneExclude]2810for p in depotPaths:2811if not p.startswith("//"):2812return False28132814if not self.cloneDestination:2815 self.cloneDestination = self.defaultDestination(args)28162817print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)28182819if not os.path.exists(self.cloneDestination):2820 os.makedirs(self.cloneDestination)2821chdir(self.cloneDestination)28222823 init_cmd = ["git","init"]2824if self.cloneBare:2825 init_cmd.append("--bare")2826 subprocess.check_call(init_cmd)28272828if not P4Sync.run(self, depotPaths):2829return False2830if self.branch !="master":2831if self.importIntoRemotes:2832 masterbranch ="refs/remotes/p4/master"2833else:2834 masterbranch ="refs/heads/p4/master"2835ifgitBranchExists(masterbranch):2836system("git branch master%s"% masterbranch)2837if not self.cloneBare:2838system("git checkout -f")2839else:2840print"Could not detect main branch. No checkout/master branch created."28412842# auto-set this variable if invoked with --use-client-spec2843if self.useClientSpec_from_options:2844system("git config --bool git-p4.useclientspec true")28452846return True28472848classP4Branches(Command):2849def__init__(self):2850 Command.__init__(self)2851 self.options = [ ]2852 self.description = ("Shows the git branches that hold imports and their "2853+"corresponding perforce depot paths")2854 self.verbose =False28552856defrun(self, args):2857iforiginP4BranchesExist():2858createOrUpdateBranchesFromOrigin()28592860 cmdline ="git rev-parse --symbolic "2861 cmdline +=" --remotes"28622863for line inread_pipe_lines(cmdline):2864 line = line.strip()28652866if not line.startswith('p4/')or line =="p4/HEAD":2867continue2868 branch = line28692870 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)2871 settings =extractSettingsGitLog(log)28722873print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])2874return True28752876classHelpFormatter(optparse.IndentedHelpFormatter):2877def__init__(self):2878 optparse.IndentedHelpFormatter.__init__(self)28792880defformat_description(self, description):2881if description:2882return description +"\n"2883else:2884return""28852886defprintUsage(commands):2887print"usage:%s<command> [options]"% sys.argv[0]2888print""2889print"valid commands:%s"%", ".join(commands)2890print""2891print"Try%s<command> --help for command specific help."% sys.argv[0]2892print""28932894commands = {2895"debug": P4Debug,2896"submit": P4Submit,2897"commit": P4Submit,2898"sync": P4Sync,2899"rebase": P4Rebase,2900"clone": P4Clone,2901"rollback": P4RollBack,2902"branches": P4Branches2903}290429052906defmain():2907iflen(sys.argv[1:]) ==0:2908printUsage(commands.keys())2909 sys.exit(2)29102911 cmd =""2912 cmdName = sys.argv[1]2913try:2914 klass = commands[cmdName]2915 cmd =klass()2916exceptKeyError:2917print"unknown command%s"% cmdName2918print""2919printUsage(commands.keys())2920 sys.exit(2)29212922 options = cmd.options2923 cmd.gitdir = os.environ.get("GIT_DIR",None)29242925 args = sys.argv[2:]29262927 options.append(optparse.make_option("--verbose", dest="verbose", action="store_true"))2928if cmd.needsGit:2929 options.append(optparse.make_option("--git-dir", dest="gitdir"))29302931 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),2932 options,2933 description = cmd.description,2934 formatter =HelpFormatter())29352936(cmd, args) = parser.parse_args(sys.argv[2:], cmd);2937global verbose2938 verbose = cmd.verbose2939if cmd.needsGit:2940if cmd.gitdir ==None:2941 cmd.gitdir = os.path.abspath(".git")2942if notisValidGitDir(cmd.gitdir):2943 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()2944if os.path.exists(cmd.gitdir):2945 cdup =read_pipe("git rev-parse --show-cdup").strip()2946iflen(cdup) >0:2947chdir(cdup);29482949if notisValidGitDir(cmd.gitdir):2950ifisValidGitDir(cmd.gitdir +"/.git"):2951 cmd.gitdir +="/.git"2952else:2953die("fatal: cannot locate git repository at%s"% cmd.gitdir)29542955 os.environ["GIT_DIR"] = cmd.gitdir29562957if not cmd.run(args):2958 parser.print_help()2959 sys.exit(2)296029612962if __name__ =='__main__':2963main()