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 123defp4_has_command(cmd): 124"""Ask p4 for help on this command. If it returns an error, the 125 command does not exist in this version of p4.""" 126 real_cmd =p4_build_cmd(["help", cmd]) 127 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 128 stderr=subprocess.PIPE) 129 p.communicate() 130return p.returncode ==0 131 132defsystem(cmd): 133 expand =isinstance(cmd,basestring) 134if verbose: 135 sys.stderr.write("executing%s\n"%str(cmd)) 136 subprocess.check_call(cmd, shell=expand) 137 138defp4_system(cmd): 139"""Specifically invoke p4 as the system command. """ 140 real_cmd =p4_build_cmd(cmd) 141 expand =isinstance(real_cmd, basestring) 142 subprocess.check_call(real_cmd, shell=expand) 143 144defp4_integrate(src, dest): 145p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 146 147defp4_sync(f, *options): 148p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 149 150defp4_add(f): 151# forcibly add file names with wildcards 152ifwildcard_present(f): 153p4_system(["add","-f", f]) 154else: 155p4_system(["add", f]) 156 157defp4_delete(f): 158p4_system(["delete",wildcard_encode(f)]) 159 160defp4_edit(f): 161p4_system(["edit",wildcard_encode(f)]) 162 163defp4_revert(f): 164p4_system(["revert",wildcard_encode(f)]) 165 166defp4_reopen(type, f): 167p4_system(["reopen","-t",type,wildcard_encode(f)]) 168 169defp4_move(src, dest): 170p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 171 172defp4_describe(change): 173"""Make sure it returns a valid result by checking for 174 the presence of field "time". Return a dict of the 175 results.""" 176 177 ds =p4CmdList(["describe","-s",str(change)]) 178iflen(ds) !=1: 179die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 180 181 d = ds[0] 182 183if"p4ExitCode"in d: 184die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 185str(d))) 186if"code"in d: 187if d["code"] =="error": 188die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 189 190if"time"not in d: 191die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 192 193return d 194 195# 196# Canonicalize the p4 type and return a tuple of the 197# base type, plus any modifiers. See "p4 help filetypes" 198# for a list and explanation. 199# 200defsplit_p4_type(p4type): 201 202 p4_filetypes_historical = { 203"ctempobj":"binary+Sw", 204"ctext":"text+C", 205"cxtext":"text+Cx", 206"ktext":"text+k", 207"kxtext":"text+kx", 208"ltext":"text+F", 209"tempobj":"binary+FSw", 210"ubinary":"binary+F", 211"uresource":"resource+F", 212"uxbinary":"binary+Fx", 213"xbinary":"binary+x", 214"xltext":"text+Fx", 215"xtempobj":"binary+Swx", 216"xtext":"text+x", 217"xunicode":"unicode+x", 218"xutf16":"utf16+x", 219} 220if p4type in p4_filetypes_historical: 221 p4type = p4_filetypes_historical[p4type] 222 mods ="" 223 s = p4type.split("+") 224 base = s[0] 225 mods ="" 226iflen(s) >1: 227 mods = s[1] 228return(base, mods) 229 230# 231# return the raw p4 type of a file (text, text+ko, etc) 232# 233defp4_type(file): 234 results =p4CmdList(["fstat","-T","headType",file]) 235return results[0]['headType'] 236 237# 238# Given a type base and modifier, return a regexp matching 239# the keywords that can be expanded in the file 240# 241defp4_keywords_regexp_for_type(base, type_mods): 242if base in("text","unicode","binary"): 243 kwords =None 244if"ko"in type_mods: 245 kwords ='Id|Header' 246elif"k"in type_mods: 247 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 248else: 249return None 250 pattern = r""" 251 \$ # Starts with a dollar, followed by... 252 (%s) # one of the keywords, followed by... 253 (:[^$\n]+)? # possibly an old expansion, followed by... 254 \$ # another dollar 255 """% kwords 256return pattern 257else: 258return None 259 260# 261# Given a file, return a regexp matching the possible 262# RCS keywords that will be expanded, or None for files 263# with kw expansion turned off. 264# 265defp4_keywords_regexp_for_file(file): 266if not os.path.exists(file): 267return None 268else: 269(type_base, type_mods) =split_p4_type(p4_type(file)) 270returnp4_keywords_regexp_for_type(type_base, type_mods) 271 272defsetP4ExecBit(file, mode): 273# Reopens an already open file and changes the execute bit to match 274# the execute bit setting in the passed in mode. 275 276 p4Type ="+x" 277 278if notisModeExec(mode): 279 p4Type =getP4OpenedType(file) 280 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 281 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 282if p4Type[-1] =="+": 283 p4Type = p4Type[0:-1] 284 285p4_reopen(p4Type,file) 286 287defgetP4OpenedType(file): 288# Returns the perforce file type for the given file. 289 290 result =p4_read_pipe(["opened",wildcard_encode(file)]) 291 match = re.match(".*\((.+)\)\r?$", result) 292if match: 293return match.group(1) 294else: 295die("Could not determine file type for%s(result: '%s')"% (file, result)) 296 297# Return the set of all p4 labels 298defgetP4Labels(depotPaths): 299 labels =set() 300ifisinstance(depotPaths,basestring): 301 depotPaths = [depotPaths] 302 303for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 304 label = l['label'] 305 labels.add(label) 306 307return labels 308 309# Return the set of all git tags 310defgetGitTags(): 311 gitTags =set() 312for line inread_pipe_lines(["git","tag"]): 313 tag = line.strip() 314 gitTags.add(tag) 315return gitTags 316 317defdiffTreePattern(): 318# This is a simple generator for the diff tree regex pattern. This could be 319# a class variable if this and parseDiffTreeEntry were a part of a class. 320 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 321while True: 322yield pattern 323 324defparseDiffTreeEntry(entry): 325"""Parses a single diff tree entry into its component elements. 326 327 See git-diff-tree(1) manpage for details about the format of the diff 328 output. This method returns a dictionary with the following elements: 329 330 src_mode - The mode of the source file 331 dst_mode - The mode of the destination file 332 src_sha1 - The sha1 for the source file 333 dst_sha1 - The sha1 fr the destination file 334 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 335 status_score - The score for the status (applicable for 'C' and 'R' 336 statuses). This is None if there is no score. 337 src - The path for the source file. 338 dst - The path for the destination file. This is only present for 339 copy or renames. If it is not present, this is None. 340 341 If the pattern is not matched, None is returned.""" 342 343 match =diffTreePattern().next().match(entry) 344if match: 345return{ 346'src_mode': match.group(1), 347'dst_mode': match.group(2), 348'src_sha1': match.group(3), 349'dst_sha1': match.group(4), 350'status': match.group(5), 351'status_score': match.group(6), 352'src': match.group(7), 353'dst': match.group(10) 354} 355return None 356 357defisModeExec(mode): 358# Returns True if the given git mode represents an executable file, 359# otherwise False. 360return mode[-3:] =="755" 361 362defisModeExecChanged(src_mode, dst_mode): 363returnisModeExec(src_mode) !=isModeExec(dst_mode) 364 365defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 366 367ifisinstance(cmd,basestring): 368 cmd ="-G "+ cmd 369 expand =True 370else: 371 cmd = ["-G"] + cmd 372 expand =False 373 374 cmd =p4_build_cmd(cmd) 375if verbose: 376 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 377 378# Use a temporary file to avoid deadlocks without 379# subprocess.communicate(), which would put another copy 380# of stdout into memory. 381 stdin_file =None 382if stdin is not None: 383 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 384ifisinstance(stdin,basestring): 385 stdin_file.write(stdin) 386else: 387for i in stdin: 388 stdin_file.write(i +'\n') 389 stdin_file.flush() 390 stdin_file.seek(0) 391 392 p4 = subprocess.Popen(cmd, 393 shell=expand, 394 stdin=stdin_file, 395 stdout=subprocess.PIPE) 396 397 result = [] 398try: 399while True: 400 entry = marshal.load(p4.stdout) 401if cb is not None: 402cb(entry) 403else: 404 result.append(entry) 405exceptEOFError: 406pass 407 exitCode = p4.wait() 408if exitCode !=0: 409 entry = {} 410 entry["p4ExitCode"] = exitCode 411 result.append(entry) 412 413return result 414 415defp4Cmd(cmd): 416list=p4CmdList(cmd) 417 result = {} 418for entry inlist: 419 result.update(entry) 420return result; 421 422defp4Where(depotPath): 423if not depotPath.endswith("/"): 424 depotPath +="/" 425 depotPath = depotPath +"..." 426 outputList =p4CmdList(["where", depotPath]) 427 output =None 428for entry in outputList: 429if"depotFile"in entry: 430if entry["depotFile"] == depotPath: 431 output = entry 432break 433elif"data"in entry: 434 data = entry.get("data") 435 space = data.find(" ") 436if data[:space] == depotPath: 437 output = entry 438break 439if output ==None: 440return"" 441if output["code"] =="error": 442return"" 443 clientPath ="" 444if"path"in output: 445 clientPath = output.get("path") 446elif"data"in output: 447 data = output.get("data") 448 lastSpace = data.rfind(" ") 449 clientPath = data[lastSpace +1:] 450 451if clientPath.endswith("..."): 452 clientPath = clientPath[:-3] 453return clientPath 454 455defcurrentGitBranch(): 456returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 457 458defisValidGitDir(path): 459if(os.path.exists(path +"/HEAD") 460and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 461return True; 462return False 463 464defparseRevision(ref): 465returnread_pipe("git rev-parse%s"% ref).strip() 466 467defbranchExists(ref): 468 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 469 ignore_error=True) 470returnlen(rev) >0 471 472defextractLogMessageFromGitCommit(commit): 473 logMessage ="" 474 475## fixme: title is first line of commit, not 1st paragraph. 476 foundTitle =False 477for log inread_pipe_lines("git cat-file commit%s"% commit): 478if not foundTitle: 479iflen(log) ==1: 480 foundTitle =True 481continue 482 483 logMessage += log 484return logMessage 485 486defextractSettingsGitLog(log): 487 values = {} 488for line in log.split("\n"): 489 line = line.strip() 490 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 491if not m: 492continue 493 494 assignments = m.group(1).split(':') 495for a in assignments: 496 vals = a.split('=') 497 key = vals[0].strip() 498 val = ('='.join(vals[1:])).strip() 499if val.endswith('\"')and val.startswith('"'): 500 val = val[1:-1] 501 502 values[key] = val 503 504 paths = values.get("depot-paths") 505if not paths: 506 paths = values.get("depot-path") 507if paths: 508 values['depot-paths'] = paths.split(',') 509return values 510 511defgitBranchExists(branch): 512 proc = subprocess.Popen(["git","rev-parse", branch], 513 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 514return proc.wait() ==0; 515 516_gitConfig = {} 517defgitConfig(key, args =None):# set args to "--bool", for instance 518if not _gitConfig.has_key(key): 519 argsFilter ="" 520if args !=None: 521 argsFilter ="%s"% args 522 cmd ="git config%s%s"% (argsFilter, key) 523 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 524return _gitConfig[key] 525 526defgitConfigList(key): 527if not _gitConfig.has_key(key): 528 _gitConfig[key] =read_pipe("git config --get-all%s"% key, ignore_error=True).strip().split(os.linesep) 529return _gitConfig[key] 530 531defp4BranchesInGit(branchesAreInRemotes =True): 532 branches = {} 533 534 cmdline ="git rev-parse --symbolic " 535if branchesAreInRemotes: 536 cmdline +=" --remotes" 537else: 538 cmdline +=" --branches" 539 540for line inread_pipe_lines(cmdline): 541 line = line.strip() 542 543## only import to p4/ 544if not line.startswith('p4/')or line =="p4/HEAD": 545continue 546 branch = line 547 548# strip off p4 549 branch = re.sub("^p4/","", line) 550 551 branches[branch] =parseRevision(line) 552return branches 553 554deffindUpstreamBranchPoint(head ="HEAD"): 555 branches =p4BranchesInGit() 556# map from depot-path to branch name 557 branchByDepotPath = {} 558for branch in branches.keys(): 559 tip = branches[branch] 560 log =extractLogMessageFromGitCommit(tip) 561 settings =extractSettingsGitLog(log) 562if settings.has_key("depot-paths"): 563 paths =",".join(settings["depot-paths"]) 564 branchByDepotPath[paths] ="remotes/p4/"+ branch 565 566 settings =None 567 parent =0 568while parent <65535: 569 commit = head +"~%s"% parent 570 log =extractLogMessageFromGitCommit(commit) 571 settings =extractSettingsGitLog(log) 572if settings.has_key("depot-paths"): 573 paths =",".join(settings["depot-paths"]) 574if branchByDepotPath.has_key(paths): 575return[branchByDepotPath[paths], settings] 576 577 parent = parent +1 578 579return["", settings] 580 581defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 582if not silent: 583print("Creating/updating branch(es) in%sbased on origin branch(es)" 584% localRefPrefix) 585 586 originPrefix ="origin/p4/" 587 588for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 589 line = line.strip() 590if(not line.startswith(originPrefix))or line.endswith("HEAD"): 591continue 592 593 headName = line[len(originPrefix):] 594 remoteHead = localRefPrefix + headName 595 originHead = line 596 597 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 598if(not original.has_key('depot-paths') 599or not original.has_key('change')): 600continue 601 602 update =False 603if notgitBranchExists(remoteHead): 604if verbose: 605print"creating%s"% remoteHead 606 update =True 607else: 608 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 609if settings.has_key('change') >0: 610if settings['depot-paths'] == original['depot-paths']: 611 originP4Change =int(original['change']) 612 p4Change =int(settings['change']) 613if originP4Change > p4Change: 614print("%s(%s) is newer than%s(%s). " 615"Updating p4 branch from origin." 616% (originHead, originP4Change, 617 remoteHead, p4Change)) 618 update =True 619else: 620print("Ignoring:%swas imported from%swhile " 621"%swas imported from%s" 622% (originHead,','.join(original['depot-paths']), 623 remoteHead,','.join(settings['depot-paths']))) 624 625if update: 626system("git update-ref%s %s"% (remoteHead, originHead)) 627 628deforiginP4BranchesExist(): 629returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 630 631defp4ChangesForPaths(depotPaths, changeRange): 632assert depotPaths 633 cmd = ['changes'] 634for p in depotPaths: 635 cmd += ["%s...%s"% (p, changeRange)] 636 output =p4_read_pipe_lines(cmd) 637 638 changes = {} 639for line in output: 640 changeNum =int(line.split(" ")[1]) 641 changes[changeNum] =True 642 643 changelist = changes.keys() 644 changelist.sort() 645return changelist 646 647defp4PathStartsWith(path, prefix): 648# This method tries to remedy a potential mixed-case issue: 649# 650# If UserA adds //depot/DirA/file1 651# and UserB adds //depot/dira/file2 652# 653# we may or may not have a problem. If you have core.ignorecase=true, 654# we treat DirA and dira as the same directory 655 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 656if ignorecase: 657return path.lower().startswith(prefix.lower()) 658return path.startswith(prefix) 659 660defgetClientSpec(): 661"""Look at the p4 client spec, create a View() object that contains 662 all the mappings, and return it.""" 663 664 specList =p4CmdList("client -o") 665iflen(specList) !=1: 666die('Output from "client -o" is%dlines, expecting 1'% 667len(specList)) 668 669# dictionary of all client parameters 670 entry = specList[0] 671 672# just the keys that start with "View" 673 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 674 675# hold this new View 676 view =View() 677 678# append the lines, in order, to the view 679for view_num inrange(len(view_keys)): 680 k ="View%d"% view_num 681if k not in view_keys: 682die("Expected view key%smissing"% k) 683 view.append(entry[k]) 684 685return view 686 687defgetClientRoot(): 688"""Grab the client directory.""" 689 690 output =p4CmdList("client -o") 691iflen(output) !=1: 692die('Output from "client -o" is%dlines, expecting 1'%len(output)) 693 694 entry = output[0] 695if"Root"not in entry: 696die('Client has no "Root"') 697 698return entry["Root"] 699 700# 701# P4 wildcards are not allowed in filenames. P4 complains 702# if you simply add them, but you can force it with "-f", in 703# which case it translates them into %xx encoding internally. 704# 705defwildcard_decode(path): 706# Search for and fix just these four characters. Do % last so 707# that fixing it does not inadvertently create new %-escapes. 708# Cannot have * in a filename in windows; untested as to 709# what p4 would do in such a case. 710if not platform.system() =="Windows": 711 path = path.replace("%2A","*") 712 path = path.replace("%23","#") \ 713.replace("%40","@") \ 714.replace("%25","%") 715return path 716 717defwildcard_encode(path): 718# do % first to avoid double-encoding the %s introduced here 719 path = path.replace("%","%25") \ 720.replace("*","%2A") \ 721.replace("#","%23") \ 722.replace("@","%40") 723return path 724 725defwildcard_present(path): 726return path.translate(None,"*#@%") != path 727 728class Command: 729def__init__(self): 730 self.usage ="usage: %prog [options]" 731 self.needsGit =True 732 self.verbose =False 733 734class P4UserMap: 735def__init__(self): 736 self.userMapFromPerforceServer =False 737 self.myP4UserId =None 738 739defp4UserId(self): 740if self.myP4UserId: 741return self.myP4UserId 742 743 results =p4CmdList("user -o") 744for r in results: 745if r.has_key('User'): 746 self.myP4UserId = r['User'] 747return r['User'] 748die("Could not find your p4 user id") 749 750defp4UserIsMe(self, p4User): 751# return True if the given p4 user is actually me 752 me = self.p4UserId() 753if not p4User or p4User != me: 754return False 755else: 756return True 757 758defgetUserCacheFilename(self): 759 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 760return home +"/.gitp4-usercache.txt" 761 762defgetUserMapFromPerforceServer(self): 763if self.userMapFromPerforceServer: 764return 765 self.users = {} 766 self.emails = {} 767 768for output inp4CmdList("users"): 769if not output.has_key("User"): 770continue 771 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 772 self.emails[output["Email"]] = output["User"] 773 774 775 s ='' 776for(key, val)in self.users.items(): 777 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 778 779open(self.getUserCacheFilename(),"wb").write(s) 780 self.userMapFromPerforceServer =True 781 782defloadUserMapFromCache(self): 783 self.users = {} 784 self.userMapFromPerforceServer =False 785try: 786 cache =open(self.getUserCacheFilename(),"rb") 787 lines = cache.readlines() 788 cache.close() 789for line in lines: 790 entry = line.strip().split("\t") 791 self.users[entry[0]] = entry[1] 792exceptIOError: 793 self.getUserMapFromPerforceServer() 794 795classP4Debug(Command): 796def__init__(self): 797 Command.__init__(self) 798 self.options = [] 799 self.description ="A tool to debug the output of p4 -G." 800 self.needsGit =False 801 802defrun(self, args): 803 j =0 804for output inp4CmdList(args): 805print'Element:%d'% j 806 j +=1 807print output 808return True 809 810classP4RollBack(Command): 811def__init__(self): 812 Command.__init__(self) 813 self.options = [ 814 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 815] 816 self.description ="A tool to debug the multi-branch import. Don't use :)" 817 self.rollbackLocalBranches =False 818 819defrun(self, args): 820iflen(args) !=1: 821return False 822 maxChange =int(args[0]) 823 824if"p4ExitCode"inp4Cmd("changes -m 1"): 825die("Problems executing p4"); 826 827if self.rollbackLocalBranches: 828 refPrefix ="refs/heads/" 829 lines =read_pipe_lines("git rev-parse --symbolic --branches") 830else: 831 refPrefix ="refs/remotes/" 832 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 833 834for line in lines: 835if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 836 line = line.strip() 837 ref = refPrefix + line 838 log =extractLogMessageFromGitCommit(ref) 839 settings =extractSettingsGitLog(log) 840 841 depotPaths = settings['depot-paths'] 842 change = settings['change'] 843 844 changed =False 845 846iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 847for p in depotPaths]))) ==0: 848print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 849system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 850continue 851 852while change andint(change) > maxChange: 853 changed =True 854if self.verbose: 855print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 856system("git update-ref%s\"%s^\""% (ref, ref)) 857 log =extractLogMessageFromGitCommit(ref) 858 settings =extractSettingsGitLog(log) 859 860 861 depotPaths = settings['depot-paths'] 862 change = settings['change'] 863 864if changed: 865print"%srewound to%s"% (ref, change) 866 867return True 868 869classP4Submit(Command, P4UserMap): 870 871 conflict_behavior_choices = ("ask","skip","quit") 872 873def__init__(self): 874 Command.__init__(self) 875 P4UserMap.__init__(self) 876 self.options = [ 877 optparse.make_option("--origin", dest="origin"), 878 optparse.make_option("-M", dest="detectRenames", action="store_true"), 879# preserve the user, requires relevant p4 permissions 880 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 881 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 882 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 883 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 884 optparse.make_option("--conflict", dest="conflict_behavior", 885 choices=self.conflict_behavior_choices) 886] 887 self.description ="Submit changes from git to the perforce depot." 888 self.usage +=" [name of git branch to submit into perforce depot]" 889 self.origin ="" 890 self.detectRenames =False 891 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 892 self.dry_run =False 893 self.prepare_p4_only =False 894 self.conflict_behavior =None 895 self.isWindows = (platform.system() =="Windows") 896 self.exportLabels =False 897 self.p4HasMoveCommand =p4_has_command("move") 898 899defcheck(self): 900iflen(p4CmdList("opened ...")) >0: 901die("You have files opened with perforce! Close them before starting the sync.") 902 903defseparate_jobs_from_description(self, message): 904"""Extract and return a possible Jobs field in the commit 905 message. It goes into a separate section in the p4 change 906 specification. 907 908 A jobs line starts with "Jobs:" and looks like a new field 909 in a form. Values are white-space separated on the same 910 line or on following lines that start with a tab. 911 912 This does not parse and extract the full git commit message 913 like a p4 form. It just sees the Jobs: line as a marker 914 to pass everything from then on directly into the p4 form, 915 but outside the description section. 916 917 Return a tuple (stripped log message, jobs string).""" 918 919 m = re.search(r'^Jobs:', message, re.MULTILINE) 920if m is None: 921return(message,None) 922 923 jobtext = message[m.start():] 924 stripped_message = message[:m.start()].rstrip() 925return(stripped_message, jobtext) 926 927defprepareLogMessage(self, template, message, jobs): 928"""Edits the template returned from "p4 change -o" to insert 929 the message in the Description field, and the jobs text in 930 the Jobs field.""" 931 result ="" 932 933 inDescriptionSection =False 934 935for line in template.split("\n"): 936if line.startswith("#"): 937 result += line +"\n" 938continue 939 940if inDescriptionSection: 941if line.startswith("Files:")or line.startswith("Jobs:"): 942 inDescriptionSection =False 943# insert Jobs section 944if jobs: 945 result += jobs +"\n" 946else: 947continue 948else: 949if line.startswith("Description:"): 950 inDescriptionSection =True 951 line +="\n" 952for messageLine in message.split("\n"): 953 line +="\t"+ messageLine +"\n" 954 955 result += line +"\n" 956 957return result 958 959defpatchRCSKeywords(self,file, pattern): 960# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern 961(handle, outFileName) = tempfile.mkstemp(dir='.') 962try: 963 outFile = os.fdopen(handle,"w+") 964 inFile =open(file,"r") 965 regexp = re.compile(pattern, re.VERBOSE) 966for line in inFile.readlines(): 967 line = regexp.sub(r'$\1$', line) 968 outFile.write(line) 969 inFile.close() 970 outFile.close() 971# Forcibly overwrite the original file 972 os.unlink(file) 973 shutil.move(outFileName,file) 974except: 975# cleanup our temporary file 976 os.unlink(outFileName) 977print"Failed to strip RCS keywords in%s"%file 978raise 979 980print"Patched up RCS keywords in%s"%file 981 982defp4UserForCommit(self,id): 983# Return the tuple (perforce user,git email) for a given git commit id 984 self.getUserMapFromPerforceServer() 985 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id) 986 gitEmail = gitEmail.strip() 987if not self.emails.has_key(gitEmail): 988return(None,gitEmail) 989else: 990return(self.emails[gitEmail],gitEmail) 991 992defcheckValidP4Users(self,commits): 993# check if any git authors cannot be mapped to p4 users 994foridin commits: 995(user,email) = self.p4UserForCommit(id) 996if not user: 997 msg ="Cannot find p4 user for email%sin commit%s."% (email,id) 998ifgitConfig('git-p4.allowMissingP4Users').lower() =="true": 999print"%s"% msg1000else:1001die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10021003deflastP4Changelist(self):1004# Get back the last changelist number submitted in this client spec. This1005# then gets used to patch up the username in the change. If the same1006# client spec is being used by multiple processes then this might go1007# wrong.1008 results =p4CmdList("client -o")# find the current client1009 client =None1010for r in results:1011if r.has_key('Client'):1012 client = r['Client']1013break1014if not client:1015die("could not get client spec")1016 results =p4CmdList(["changes","-c", client,"-m","1"])1017for r in results:1018if r.has_key('change'):1019return r['change']1020die("Could not get changelist number for last submit - cannot patch up user details")10211022defmodifyChangelistUser(self, changelist, newUser):1023# fixup the user field of a changelist after it has been submitted.1024 changes =p4CmdList("change -o%s"% changelist)1025iflen(changes) !=1:1026die("Bad output from p4 change modifying%sto user%s"%1027(changelist, newUser))10281029 c = changes[0]1030if c['User'] == newUser:return# nothing to do1031 c['User'] = newUser1032input= marshal.dumps(c)10331034 result =p4CmdList("change -f -i", stdin=input)1035for r in result:1036if r.has_key('code'):1037if r['code'] =='error':1038die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1039if r.has_key('data'):1040print("Updated user field for changelist%sto%s"% (changelist, newUser))1041return1042die("Could not modify user field of changelist%sto%s"% (changelist, newUser))10431044defcanChangeChangelists(self):1045# check to see if we have p4 admin or super-user permissions, either of1046# which are required to modify changelists.1047 results =p4CmdList(["protects", self.depotPath])1048for r in results:1049if r.has_key('perm'):1050if r['perm'] =='admin':1051return11052if r['perm'] =='super':1053return11054return010551056defprepareSubmitTemplate(self):1057"""Run "p4 change -o" to grab a change specification template.1058 This does not use "p4 -G", as it is nice to keep the submission1059 template in original order, since a human might edit it.10601061 Remove lines in the Files section that show changes to files1062 outside the depot path we're committing into."""10631064 template =""1065 inFilesSection =False1066for line inp4_read_pipe_lines(['change','-o']):1067if line.endswith("\r\n"):1068 line = line[:-2] +"\n"1069if inFilesSection:1070if line.startswith("\t"):1071# path starts and ends with a tab1072 path = line[1:]1073 lastTab = path.rfind("\t")1074if lastTab != -1:1075 path = path[:lastTab]1076if notp4PathStartsWith(path, self.depotPath):1077continue1078else:1079 inFilesSection =False1080else:1081if line.startswith("Files:"):1082 inFilesSection =True10831084 template += line10851086return template10871088defedit_template(self, template_file):1089"""Invoke the editor to let the user change the submission1090 message. Return true if okay to continue with the submit."""10911092# if configured to skip the editing part, just submit1093ifgitConfig("git-p4.skipSubmitEdit") =="true":1094return True10951096# look at the modification time, to check later if the user saved1097# the file1098 mtime = os.stat(template_file).st_mtime10991100# invoke the editor1101if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1102 editor = os.environ.get("P4EDITOR")1103else:1104 editor =read_pipe("git var GIT_EDITOR").strip()1105system(editor +" "+ template_file)11061107# If the file was not saved, prompt to see if this patch should1108# be skipped. But skip this verification step if configured so.1109ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1110return True11111112# modification time updated means user saved the file1113if os.stat(template_file).st_mtime > mtime:1114return True11151116while True:1117 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1118if response =='y':1119return True1120if response =='n':1121return False11221123defapplyCommit(self,id):1124"""Apply one commit, return True if it succeeded."""11251126print"Applying",read_pipe(["git","show","-s",1127"--format=format:%h%s",id])11281129(p4User, gitEmail) = self.p4UserForCommit(id)11301131 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1132 filesToAdd =set()1133 filesToDelete =set()1134 editedFiles =set()1135 pureRenameCopy =set()1136 filesToChangeExecBit = {}11371138for line in diff:1139 diff =parseDiffTreeEntry(line)1140 modifier = diff['status']1141 path = diff['src']1142if modifier =="M":1143p4_edit(path)1144ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1145 filesToChangeExecBit[path] = diff['dst_mode']1146 editedFiles.add(path)1147elif modifier =="A":1148 filesToAdd.add(path)1149 filesToChangeExecBit[path] = diff['dst_mode']1150if path in filesToDelete:1151 filesToDelete.remove(path)1152elif modifier =="D":1153 filesToDelete.add(path)1154if path in filesToAdd:1155 filesToAdd.remove(path)1156elif modifier =="C":1157 src, dest = diff['src'], diff['dst']1158p4_integrate(src, dest)1159 pureRenameCopy.add(dest)1160if diff['src_sha1'] != diff['dst_sha1']:1161p4_edit(dest)1162 pureRenameCopy.discard(dest)1163ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1164p4_edit(dest)1165 pureRenameCopy.discard(dest)1166 filesToChangeExecBit[dest] = diff['dst_mode']1167 os.unlink(dest)1168 editedFiles.add(dest)1169elif modifier =="R":1170 src, dest = diff['src'], diff['dst']1171if self.p4HasMoveCommand:1172p4_edit(src)# src must be open before move1173p4_move(src, dest)# opens for (move/delete, move/add)1174else:1175p4_integrate(src, dest)1176if diff['src_sha1'] != diff['dst_sha1']:1177p4_edit(dest)1178else:1179 pureRenameCopy.add(dest)1180ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1181if not self.p4HasMoveCommand:1182p4_edit(dest)# with move: already open, writable1183 filesToChangeExecBit[dest] = diff['dst_mode']1184if not self.p4HasMoveCommand:1185 os.unlink(dest)1186 filesToDelete.add(src)1187 editedFiles.add(dest)1188else:1189die("unknown modifier%sfor%s"% (modifier, path))11901191 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1192 patchcmd = diffcmd +" | git apply "1193 tryPatchCmd = patchcmd +"--check -"1194 applyPatchCmd = patchcmd +"--check --apply -"1195 patch_succeeded =True11961197if os.system(tryPatchCmd) !=0:1198 fixed_rcs_keywords =False1199 patch_succeeded =False1200print"Unfortunately applying the change failed!"12011202# Patch failed, maybe it's just RCS keyword woes. Look through1203# the patch to see if that's possible.1204ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1205file=None1206 pattern =None1207 kwfiles = {}1208forfilein editedFiles | filesToDelete:1209# did this file's delta contain RCS keywords?1210 pattern =p4_keywords_regexp_for_file(file)12111212if pattern:1213# this file is a possibility...look for RCS keywords.1214 regexp = re.compile(pattern, re.VERBOSE)1215for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1216if regexp.search(line):1217if verbose:1218print"got keyword match on%sin%sin%s"% (pattern, line,file)1219 kwfiles[file] = pattern1220break12211222forfilein kwfiles:1223if verbose:1224print"zapping%swith%s"% (line,pattern)1225 self.patchRCSKeywords(file, kwfiles[file])1226 fixed_rcs_keywords =True12271228if fixed_rcs_keywords:1229print"Retrying the patch with RCS keywords cleaned up"1230if os.system(tryPatchCmd) ==0:1231 patch_succeeded =True12321233if not patch_succeeded:1234for f in editedFiles:1235p4_revert(f)1236return False12371238#1239# Apply the patch for real, and do add/delete/+x handling.1240#1241system(applyPatchCmd)12421243for f in filesToAdd:1244p4_add(f)1245for f in filesToDelete:1246p4_revert(f)1247p4_delete(f)12481249# Set/clear executable bits1250for f in filesToChangeExecBit.keys():1251 mode = filesToChangeExecBit[f]1252setP4ExecBit(f, mode)12531254#1255# Build p4 change description, starting with the contents1256# of the git commit message.1257#1258 logMessage =extractLogMessageFromGitCommit(id)1259 logMessage = logMessage.strip()1260(logMessage, jobs) = self.separate_jobs_from_description(logMessage)12611262 template = self.prepareSubmitTemplate()1263 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)12641265if self.preserveUser:1266 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User12671268if self.checkAuthorship and not self.p4UserIsMe(p4User):1269 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1270 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1271 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"12721273 separatorLine ="######## everything below this line is just the diff #######\n"12741275# diff1276if os.environ.has_key("P4DIFF"):1277del(os.environ["P4DIFF"])1278 diff =""1279for editedFile in editedFiles:1280 diff +=p4_read_pipe(['diff','-du',1281wildcard_encode(editedFile)])12821283# new file diff1284 newdiff =""1285for newFile in filesToAdd:1286 newdiff +="==== new file ====\n"1287 newdiff +="--- /dev/null\n"1288 newdiff +="+++%s\n"% newFile1289 f =open(newFile,"r")1290for line in f.readlines():1291 newdiff +="+"+ line1292 f.close()12931294# change description file: submitTemplate, separatorLine, diff, newdiff1295(handle, fileName) = tempfile.mkstemp()1296 tmpFile = os.fdopen(handle,"w+")1297if self.isWindows:1298 submitTemplate = submitTemplate.replace("\n","\r\n")1299 separatorLine = separatorLine.replace("\n","\r\n")1300 newdiff = newdiff.replace("\n","\r\n")1301 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1302 tmpFile.close()13031304if self.prepare_p4_only:1305#1306# Leave the p4 tree prepared, and the submit template around1307# and let the user decide what to do next1308#1309print1310print"P4 workspace prepared for submission."1311print"To submit or revert, go to client workspace"1312print" "+ self.clientPath1313print1314print"To submit, use\"p4 submit\"to write a new description,"1315print"or\"p4 submit -i%s\"to use the one prepared by" \1316"\"git p4\"."% fileName1317print"You can delete the file\"%s\"when finished."% fileName13181319if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1320print"To preserve change ownership by user%s, you must\n" \1321"do\"p4 change -f <change>\"after submitting and\n" \1322"edit the User field."1323if pureRenameCopy:1324print"After submitting, renamed files must be re-synced."1325print"Invoke\"p4 sync -f\"on each of these files:"1326for f in pureRenameCopy:1327print" "+ f13281329print1330print"To revert the changes, use\"p4 revert ...\", and delete"1331print"the submit template file\"%s\""% fileName1332if filesToAdd:1333print"Since the commit adds new files, they must be deleted:"1334for f in filesToAdd:1335print" "+ f1336print1337return True13381339#1340# Let the user edit the change description, then submit it.1341#1342if self.edit_template(fileName):1343# read the edited message and submit1344 ret =True1345 tmpFile =open(fileName,"rb")1346 message = tmpFile.read()1347 tmpFile.close()1348 submitTemplate = message[:message.index(separatorLine)]1349if self.isWindows:1350 submitTemplate = submitTemplate.replace("\r\n","\n")1351p4_write_pipe(['submit','-i'], submitTemplate)13521353if self.preserveUser:1354if p4User:1355# Get last changelist number. Cannot easily get it from1356# the submit command output as the output is1357# unmarshalled.1358 changelist = self.lastP4Changelist()1359 self.modifyChangelistUser(changelist, p4User)13601361# The rename/copy happened by applying a patch that created a1362# new file. This leaves it writable, which confuses p4.1363for f in pureRenameCopy:1364p4_sync(f,"-f")13651366else:1367# skip this patch1368 ret =False1369print"Submission cancelled, undoing p4 changes."1370for f in editedFiles:1371p4_revert(f)1372for f in filesToAdd:1373p4_revert(f)1374 os.remove(f)1375for f in filesToDelete:1376p4_revert(f)13771378 os.remove(fileName)1379return ret13801381# Export git tags as p4 labels. Create a p4 label and then tag1382# with that.1383defexportGitTags(self, gitTags):1384 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1385iflen(validLabelRegexp) ==0:1386 validLabelRegexp = defaultLabelRegexp1387 m = re.compile(validLabelRegexp)13881389for name in gitTags:13901391if not m.match(name):1392if verbose:1393print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1394continue13951396# Get the p4 commit this corresponds to1397 logMessage =extractLogMessageFromGitCommit(name)1398 values =extractSettingsGitLog(logMessage)13991400if not values.has_key('change'):1401# a tag pointing to something not sent to p4; ignore1402if verbose:1403print"git tag%sdoes not give a p4 commit"% name1404continue1405else:1406 changelist = values['change']14071408# Get the tag details.1409 inHeader =True1410 isAnnotated =False1411 body = []1412for l inread_pipe_lines(["git","cat-file","-p", name]):1413 l = l.strip()1414if inHeader:1415if re.match(r'tag\s+', l):1416 isAnnotated =True1417elif re.match(r'\s*$', l):1418 inHeader =False1419continue1420else:1421 body.append(l)14221423if not isAnnotated:1424 body = ["lightweight tag imported by git p4\n"]14251426# Create the label - use the same view as the client spec we are using1427 clientSpec =getClientSpec()14281429 labelTemplate ="Label:%s\n"% name1430 labelTemplate +="Description:\n"1431for b in body:1432 labelTemplate +="\t"+ b +"\n"1433 labelTemplate +="View:\n"1434for mapping in clientSpec.mappings:1435 labelTemplate +="\t%s\n"% mapping.depot_side.path14361437if self.dry_run:1438print"Would create p4 label%sfor tag"% name1439elif self.prepare_p4_only:1440print"Not creating p4 label%sfor tag due to option" \1441" --prepare-p4-only"% name1442else:1443p4_write_pipe(["label","-i"], labelTemplate)14441445# Use the label1446p4_system(["tag","-l", name] +1447["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])14481449if verbose:1450print"created p4 label for tag%s"% name14511452defrun(self, args):1453iflen(args) ==0:1454 self.master =currentGitBranch()1455iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1456die("Detecting current git branch failed!")1457eliflen(args) ==1:1458 self.master = args[0]1459if notbranchExists(self.master):1460die("Branch%sdoes not exist"% self.master)1461else:1462return False14631464 allowSubmit =gitConfig("git-p4.allowSubmit")1465iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1466die("%sis not in git-p4.allowSubmit"% self.master)14671468[upstream, settings] =findUpstreamBranchPoint()1469 self.depotPath = settings['depot-paths'][0]1470iflen(self.origin) ==0:1471 self.origin = upstream14721473if self.preserveUser:1474if not self.canChangeChangelists():1475die("Cannot preserve user names without p4 super-user or admin permissions")14761477# if not set from the command line, try the config file1478if self.conflict_behavior is None:1479 val =gitConfig("git-p4.conflict")1480if val:1481if val not in self.conflict_behavior_choices:1482die("Invalid value '%s' for config git-p4.conflict"% val)1483else:1484 val ="ask"1485 self.conflict_behavior = val14861487if self.verbose:1488print"Origin branch is "+ self.origin14891490iflen(self.depotPath) ==0:1491print"Internal error: cannot locate perforce depot path from existing branches"1492 sys.exit(128)14931494 self.useClientSpec =False1495ifgitConfig("git-p4.useclientspec","--bool") =="true":1496 self.useClientSpec =True1497if self.useClientSpec:1498 self.clientSpecDirs =getClientSpec()14991500if self.useClientSpec:1501# all files are relative to the client spec1502 self.clientPath =getClientRoot()1503else:1504 self.clientPath =p4Where(self.depotPath)15051506if self.clientPath =="":1507die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15081509print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1510 self.oldWorkingDirectory = os.getcwd()15111512# ensure the clientPath exists1513 new_client_dir =False1514if not os.path.exists(self.clientPath):1515 new_client_dir =True1516 os.makedirs(self.clientPath)15171518chdir(self.clientPath)1519if self.dry_run:1520print"Would synchronize p4 checkout in%s"% self.clientPath1521else:1522print"Synchronizing p4 checkout..."1523if new_client_dir:1524# old one was destroyed, and maybe nobody told p41525p4_sync("...","-f")1526else:1527p4_sync("...")1528 self.check()15291530 commits = []1531for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1532 commits.append(line.strip())1533 commits.reverse()15341535if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1536 self.checkAuthorship =False1537else:1538 self.checkAuthorship =True15391540if self.preserveUser:1541 self.checkValidP4Users(commits)15421543#1544# Build up a set of options to be passed to diff when1545# submitting each commit to p4.1546#1547if self.detectRenames:1548# command-line -M arg1549 self.diffOpts ="-M"1550else:1551# If not explicitly set check the config variable1552 detectRenames =gitConfig("git-p4.detectRenames")15531554if detectRenames.lower() =="false"or detectRenames =="":1555 self.diffOpts =""1556elif detectRenames.lower() =="true":1557 self.diffOpts ="-M"1558else:1559 self.diffOpts ="-M%s"% detectRenames15601561# no command-line arg for -C or --find-copies-harder, just1562# config variables1563 detectCopies =gitConfig("git-p4.detectCopies")1564if detectCopies.lower() =="false"or detectCopies =="":1565pass1566elif detectCopies.lower() =="true":1567 self.diffOpts +=" -C"1568else:1569 self.diffOpts +=" -C%s"% detectCopies15701571ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1572 self.diffOpts +=" --find-copies-harder"15731574#1575# Apply the commits, one at a time. On failure, ask if should1576# continue to try the rest of the patches, or quit.1577#1578if self.dry_run:1579print"Would apply"1580 applied = []1581 last =len(commits) -11582for i, commit inenumerate(commits):1583if self.dry_run:1584print" ",read_pipe(["git","show","-s",1585"--format=format:%h%s", commit])1586 ok =True1587else:1588 ok = self.applyCommit(commit)1589if ok:1590 applied.append(commit)1591else:1592if self.prepare_p4_only and i < last:1593print"Processing only the first commit due to option" \1594" --prepare-p4-only"1595break1596if i < last:1597 quit =False1598while True:1599# prompt for what to do, or use the option/variable1600if self.conflict_behavior =="ask":1601print"What do you want to do?"1602 response =raw_input("[s]kip this commit but apply"1603" the rest, or [q]uit? ")1604if not response:1605continue1606elif self.conflict_behavior =="skip":1607 response ="s"1608elif self.conflict_behavior =="quit":1609 response ="q"1610else:1611die("Unknown conflict_behavior '%s'"%1612 self.conflict_behavior)16131614if response[0] =="s":1615print"Skipping this commit, but applying the rest"1616break1617if response[0] =="q":1618print"Quitting"1619 quit =True1620break1621if quit:1622break16231624chdir(self.oldWorkingDirectory)16251626if self.dry_run:1627pass1628elif self.prepare_p4_only:1629pass1630eliflen(commits) ==len(applied):1631print"All commits applied!"16321633 sync =P4Sync()1634 sync.run([])16351636 rebase =P4Rebase()1637 rebase.rebase()16381639else:1640iflen(applied) ==0:1641print"No commits applied."1642else:1643print"Applied only the commits marked with '*':"1644for c in commits:1645if c in applied:1646 star ="*"1647else:1648 star =" "1649print star,read_pipe(["git","show","-s",1650"--format=format:%h%s", c])1651print"You will have to do 'git p4 sync' and rebase."16521653ifgitConfig("git-p4.exportLabels","--bool") =="true":1654 self.exportLabels =True16551656if self.exportLabels:1657 p4Labels =getP4Labels(self.depotPath)1658 gitTags =getGitTags()16591660 missingGitTags = gitTags - p4Labels1661 self.exportGitTags(missingGitTags)16621663# exit with error unless everything applied perfecly1664iflen(commits) !=len(applied):1665 sys.exit(1)16661667return True16681669classView(object):1670"""Represent a p4 view ("p4 help views"), and map files in a1671 repo according to the view."""16721673classPath(object):1674"""A depot or client path, possibly containing wildcards.1675 The only one supported is ... at the end, currently.1676 Initialize with the full path, with //depot or //client."""16771678def__init__(self, path, is_depot):1679 self.path = path1680 self.is_depot = is_depot1681 self.find_wildcards()1682# remember the prefix bit, useful for relative mappings1683 m = re.match("(//[^/]+/)", self.path)1684if not m:1685die("Path%sdoes not start with //prefix/"% self.path)1686 prefix = m.group(1)1687if not self.is_depot:1688# strip //client/ on client paths1689 self.path = self.path[len(prefix):]16901691deffind_wildcards(self):1692"""Make sure wildcards are valid, and set up internal1693 variables."""16941695 self.ends_triple_dot =False1696# There are three wildcards allowed in p4 views1697# (see "p4 help views"). This code knows how to1698# handle "..." (only at the end), but cannot deal with1699# "%%n" or "*". Only check the depot_side, as p4 should1700# validate that the client_side matches too.1701if re.search(r'%%[1-9]', self.path):1702die("Can't handle%%n wildcards in view:%s"% self.path)1703if self.path.find("*") >=0:1704die("Can't handle * wildcards in view:%s"% self.path)1705 triple_dot_index = self.path.find("...")1706if triple_dot_index >=0:1707if triple_dot_index !=len(self.path) -3:1708die("Can handle only single ... wildcard, at end:%s"%1709 self.path)1710 self.ends_triple_dot =True17111712defensure_compatible(self, other_path):1713"""Make sure the wildcards agree."""1714if self.ends_triple_dot != other_path.ends_triple_dot:1715die("Both paths must end with ... if either does;\n"+1716"paths:%s %s"% (self.path, other_path.path))17171718defmatch_wildcards(self, test_path):1719"""See if this test_path matches us, and fill in the value1720 of the wildcards if so. Returns a tuple of1721 (True|False, wildcards[]). For now, only the ... at end1722 is supported, so at most one wildcard."""1723if self.ends_triple_dot:1724 dotless = self.path[:-3]1725if test_path.startswith(dotless):1726 wildcard = test_path[len(dotless):]1727return(True, [ wildcard ])1728else:1729if test_path == self.path:1730return(True, [])1731return(False, [])17321733defmatch(self, test_path):1734"""Just return if it matches; don't bother with the wildcards."""1735 b, _ = self.match_wildcards(test_path)1736return b17371738deffill_in_wildcards(self, wildcards):1739"""Return the relative path, with the wildcards filled in1740 if there are any."""1741if self.ends_triple_dot:1742return self.path[:-3] + wildcards[0]1743else:1744return self.path17451746classMapping(object):1747def__init__(self, depot_side, client_side, overlay, exclude):1748# depot_side is without the trailing /... if it had one1749 self.depot_side = View.Path(depot_side, is_depot=True)1750 self.client_side = View.Path(client_side, is_depot=False)1751 self.overlay = overlay # started with "+"1752 self.exclude = exclude # started with "-"1753assert not(self.overlay and self.exclude)1754 self.depot_side.ensure_compatible(self.client_side)17551756def__str__(self):1757 c =" "1758if self.overlay:1759 c ="+"1760if self.exclude:1761 c ="-"1762return"View.Mapping:%s%s->%s"% \1763(c, self.depot_side.path, self.client_side.path)17641765defmap_depot_to_client(self, depot_path):1766"""Calculate the client path if using this mapping on the1767 given depot path; does not consider the effect of other1768 mappings in a view. Even excluded mappings are returned."""1769 matches, wildcards = self.depot_side.match_wildcards(depot_path)1770if not matches:1771return""1772 client_path = self.client_side.fill_in_wildcards(wildcards)1773return client_path17741775#1776# View methods1777#1778def__init__(self):1779 self.mappings = []17801781defappend(self, view_line):1782"""Parse a view line, splitting it into depot and client1783 sides. Append to self.mappings, preserving order."""17841785# Split the view line into exactly two words. P4 enforces1786# structure on these lines that simplifies this quite a bit.1787#1788# Either or both words may be double-quoted.1789# Single quotes do not matter.1790# Double-quote marks cannot occur inside the words.1791# A + or - prefix is also inside the quotes.1792# There are no quotes unless they contain a space.1793# The line is already white-space stripped.1794# The two words are separated by a single space.1795#1796if view_line[0] =='"':1797# First word is double quoted. Find its end.1798 close_quote_index = view_line.find('"',1)1799if close_quote_index <=0:1800die("No first-word closing quote found:%s"% view_line)1801 depot_side = view_line[1:close_quote_index]1802# skip closing quote and space1803 rhs_index = close_quote_index +1+11804else:1805 space_index = view_line.find(" ")1806if space_index <=0:1807die("No word-splitting space found:%s"% view_line)1808 depot_side = view_line[0:space_index]1809 rhs_index = space_index +118101811if view_line[rhs_index] =='"':1812# Second word is double quoted. Make sure there is a1813# double quote at the end too.1814if not view_line.endswith('"'):1815die("View line with rhs quote should end with one:%s"%1816 view_line)1817# skip the quotes1818 client_side = view_line[rhs_index+1:-1]1819else:1820 client_side = view_line[rhs_index:]18211822# prefix + means overlay on previous mapping1823 overlay =False1824if depot_side.startswith("+"):1825 overlay =True1826 depot_side = depot_side[1:]18271828# prefix - means exclude this path1829 exclude =False1830if depot_side.startswith("-"):1831 exclude =True1832 depot_side = depot_side[1:]18331834 m = View.Mapping(depot_side, client_side, overlay, exclude)1835 self.mappings.append(m)18361837defmap_in_client(self, depot_path):1838"""Return the relative location in the client where this1839 depot file should live. Returns "" if the file should1840 not be mapped in the client."""18411842 paths_filled = []1843 client_path =""18441845# look at later entries first1846for m in self.mappings[::-1]:18471848# see where will this path end up in the client1849 p = m.map_depot_to_client(depot_path)18501851if p =="":1852# Depot path does not belong in client. Must remember1853# this, as previous items should not cause files to1854# exist in this path either. Remember that the list is1855# being walked from the end, which has higher precedence.1856# Overlap mappings do not exclude previous mappings.1857if not m.overlay:1858 paths_filled.append(m.client_side)18591860else:1861# This mapping matched; no need to search any further.1862# But, the mapping could be rejected if the client path1863# has already been claimed by an earlier mapping (i.e.1864# one later in the list, which we are walking backwards).1865 already_mapped_in_client =False1866for f in paths_filled:1867# this is View.Path.match1868if f.match(p):1869 already_mapped_in_client =True1870break1871if not already_mapped_in_client:1872# Include this file, unless it is from a line that1873# explicitly said to exclude it.1874if not m.exclude:1875 client_path = p18761877# a match, even if rejected, always stops the search1878break18791880return client_path18811882classP4Sync(Command, P4UserMap):1883 delete_actions = ("delete","move/delete","purge")18841885def__init__(self):1886 Command.__init__(self)1887 P4UserMap.__init__(self)1888 self.options = [1889 optparse.make_option("--branch", dest="branch"),1890 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1891 optparse.make_option("--changesfile", dest="changesFile"),1892 optparse.make_option("--silent", dest="silent", action="store_true"),1893 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1894 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1895 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1896help="Import into refs/heads/ , not refs/remotes"),1897 optparse.make_option("--max-changes", dest="maxChanges"),1898 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1899help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1900 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1901help="Only sync files that are included in the Perforce Client Spec")1902]1903 self.description ="""Imports from Perforce into a git repository.\n1904 example:1905 //depot/my/project/ -- to import the current head1906 //depot/my/project/@all -- to import everything1907 //depot/my/project/@1,6 -- to import only from revision 1 to 619081909 (a ... is not needed in the path p4 specification, it's added implicitly)"""19101911 self.usage +=" //depot/path[@revRange]"1912 self.silent =False1913 self.createdBranches =set()1914 self.committedChanges =set()1915 self.branch =""1916 self.detectBranches =False1917 self.detectLabels =False1918 self.importLabels =False1919 self.changesFile =""1920 self.syncWithOrigin =True1921 self.importIntoRemotes =True1922 self.maxChanges =""1923 self.isWindows = (platform.system() =="Windows")1924 self.keepRepoPath =False1925 self.depotPaths =None1926 self.p4BranchesInGit = []1927 self.cloneExclude = []1928 self.useClientSpec =False1929 self.useClientSpec_from_options =False1930 self.clientSpecDirs =None1931 self.tempBranches = []1932 self.tempBranchLocation ="git-p4-tmp"19331934ifgitConfig("git-p4.syncFromOrigin") =="false":1935 self.syncWithOrigin =False19361937# Force a checkpoint in fast-import and wait for it to finish1938defcheckpoint(self):1939 self.gitStream.write("checkpoint\n\n")1940 self.gitStream.write("progress checkpoint\n\n")1941 out = self.gitOutput.readline()1942if self.verbose:1943print"checkpoint finished: "+ out19441945defextractFilesFromCommit(self, commit):1946 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1947for path in self.cloneExclude]1948 files = []1949 fnum =01950while commit.has_key("depotFile%s"% fnum):1951 path = commit["depotFile%s"% fnum]19521953if[p for p in self.cloneExclude1954ifp4PathStartsWith(path, p)]:1955 found =False1956else:1957 found = [p for p in self.depotPaths1958ifp4PathStartsWith(path, p)]1959if not found:1960 fnum = fnum +11961continue19621963file= {}1964file["path"] = path1965file["rev"] = commit["rev%s"% fnum]1966file["action"] = commit["action%s"% fnum]1967file["type"] = commit["type%s"% fnum]1968 files.append(file)1969 fnum = fnum +11970return files19711972defstripRepoPath(self, path, prefixes):1973"""When streaming files, this is called to map a p4 depot path1974 to where it should go in git. The prefixes are either1975 self.depotPaths, or self.branchPrefixes in the case of1976 branch detection."""19771978if self.useClientSpec:1979# branch detection moves files up a level (the branch name)1980# from what client spec interpretation gives1981 path = self.clientSpecDirs.map_in_client(path)1982if self.detectBranches:1983for b in self.knownBranches:1984if path.startswith(b +"/"):1985 path = path[len(b)+1:]19861987elif self.keepRepoPath:1988# Preserve everything in relative path name except leading1989# //depot/; just look at first prefix as they all should1990# be in the same depot.1991 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])1992ifp4PathStartsWith(path, depot):1993 path = path[len(depot):]19941995else:1996for p in prefixes:1997ifp4PathStartsWith(path, p):1998 path = path[len(p):]1999break20002001 path =wildcard_decode(path)2002return path20032004defsplitFilesIntoBranches(self, commit):2005"""Look at each depotFile in the commit to figure out to what2006 branch it belongs."""20072008 branches = {}2009 fnum =02010while commit.has_key("depotFile%s"% fnum):2011 path = commit["depotFile%s"% fnum]2012 found = [p for p in self.depotPaths2013ifp4PathStartsWith(path, p)]2014if not found:2015 fnum = fnum +12016continue20172018file= {}2019file["path"] = path2020file["rev"] = commit["rev%s"% fnum]2021file["action"] = commit["action%s"% fnum]2022file["type"] = commit["type%s"% fnum]2023 fnum = fnum +120242025# start with the full relative path where this file would2026# go in a p4 client2027if self.useClientSpec:2028 relPath = self.clientSpecDirs.map_in_client(path)2029else:2030 relPath = self.stripRepoPath(path, self.depotPaths)20312032for branch in self.knownBranches.keys():2033# add a trailing slash so that a commit into qt/4.2foo2034# doesn't end up in qt/4.2, e.g.2035if relPath.startswith(branch +"/"):2036if branch not in branches:2037 branches[branch] = []2038 branches[branch].append(file)2039break20402041return branches20422043# output one file from the P4 stream2044# - helper for streamP4Files20452046defstreamOneP4File(self,file, contents):2047 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2048if verbose:2049 sys.stderr.write("%s\n"% relPath)20502051(type_base, type_mods) =split_p4_type(file["type"])20522053 git_mode ="100644"2054if"x"in type_mods:2055 git_mode ="100755"2056if type_base =="symlink":2057 git_mode ="120000"2058# p4 print on a symlink contains "target\n"; remove the newline2059 data =''.join(contents)2060 contents = [data[:-1]]20612062if type_base =="utf16":2063# p4 delivers different text in the python output to -G2064# than it does when using "print -o", or normal p4 client2065# operations. utf16 is converted to ascii or utf8, perhaps.2066# But ascii text saved as -t utf16 is completely mangled.2067# Invoke print -o to get the real contents.2068 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2069 contents = [ text ]20702071if type_base =="apple":2072# Apple filetype files will be streamed as a concatenation of2073# its appledouble header and the contents. This is useless2074# on both macs and non-macs. If using "print -q -o xx", it2075# will create "xx" with the data, and "%xx" with the header.2076# This is also not very useful.2077#2078# Ideally, someday, this script can learn how to generate2079# appledouble files directly and import those to git, but2080# non-mac machines can never find a use for apple filetype.2081print"\nIgnoring apple filetype file%s"%file['depotFile']2082return20832084# Perhaps windows wants unicode, utf16 newlines translated too;2085# but this is not doing it.2086if self.isWindows and type_base =="text":2087 mangled = []2088for data in contents:2089 data = data.replace("\r\n","\n")2090 mangled.append(data)2091 contents = mangled20922093# Note that we do not try to de-mangle keywords on utf16 files,2094# even though in theory somebody may want that.2095 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2096if pattern:2097 regexp = re.compile(pattern, re.VERBOSE)2098 text =''.join(contents)2099 text = regexp.sub(r'$\1$', text)2100 contents = [ text ]21012102 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21032104# total length...2105 length =02106for d in contents:2107 length = length +len(d)21082109 self.gitStream.write("data%d\n"% length)2110for d in contents:2111 self.gitStream.write(d)2112 self.gitStream.write("\n")21132114defstreamOneP4Deletion(self,file):2115 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2116if verbose:2117 sys.stderr.write("delete%s\n"% relPath)2118 self.gitStream.write("D%s\n"% relPath)21192120# handle another chunk of streaming data2121defstreamP4FilesCb(self, marshalled):21222123if marshalled.has_key('depotFile')and self.stream_have_file_info:2124# start of a new file - output the old one first2125 self.streamOneP4File(self.stream_file, self.stream_contents)2126 self.stream_file = {}2127 self.stream_contents = []2128 self.stream_have_file_info =False21292130# pick up the new file information... for the2131# 'data' field we need to append to our array2132for k in marshalled.keys():2133if k =='data':2134 self.stream_contents.append(marshalled['data'])2135else:2136 self.stream_file[k] = marshalled[k]21372138 self.stream_have_file_info =True21392140# Stream directly from "p4 files" into "git fast-import"2141defstreamP4Files(self, files):2142 filesForCommit = []2143 filesToRead = []2144 filesToDelete = []21452146for f in files:2147# if using a client spec, only add the files that have2148# a path in the client2149if self.clientSpecDirs:2150if self.clientSpecDirs.map_in_client(f['path']) =="":2151continue21522153 filesForCommit.append(f)2154if f['action']in self.delete_actions:2155 filesToDelete.append(f)2156else:2157 filesToRead.append(f)21582159# deleted files...2160for f in filesToDelete:2161 self.streamOneP4Deletion(f)21622163iflen(filesToRead) >0:2164 self.stream_file = {}2165 self.stream_contents = []2166 self.stream_have_file_info =False21672168# curry self argument2169defstreamP4FilesCbSelf(entry):2170 self.streamP4FilesCb(entry)21712172 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]21732174p4CmdList(["-x","-","print"],2175 stdin=fileArgs,2176 cb=streamP4FilesCbSelf)21772178# do the last chunk2179if self.stream_file.has_key('depotFile'):2180 self.streamOneP4File(self.stream_file, self.stream_contents)21812182defmake_email(self, userid):2183if userid in self.users:2184return self.users[userid]2185else:2186return"%s<a@b>"% userid21872188# Stream a p4 tag2189defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2190if verbose:2191print"writing tag%sfor commit%s"% (labelName, commit)2192 gitStream.write("tag%s\n"% labelName)2193 gitStream.write("from%s\n"% commit)21942195if labelDetails.has_key('Owner'):2196 owner = labelDetails["Owner"]2197else:2198 owner =None21992200# Try to use the owner of the p4 label, or failing that,2201# the current p4 user id.2202if owner:2203 email = self.make_email(owner)2204else:2205 email = self.make_email(self.p4UserId())2206 tagger ="%s %s %s"% (email, epoch, self.tz)22072208 gitStream.write("tagger%s\n"% tagger)22092210print"labelDetails=",labelDetails2211if labelDetails.has_key('Description'):2212 description = labelDetails['Description']2213else:2214 description ='Label from git p4'22152216 gitStream.write("data%d\n"%len(description))2217 gitStream.write(description)2218 gitStream.write("\n")22192220defcommit(self, details, files, branch, parent =""):2221 epoch = details["time"]2222 author = details["user"]22232224if self.verbose:2225print"commit into%s"% branch22262227# start with reading files; if that fails, we should not2228# create a commit.2229 new_files = []2230for f in files:2231if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2232 new_files.append(f)2233else:2234 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])22352236 self.gitStream.write("commit%s\n"% branch)2237# gitStream.write("mark :%s\n" % details["change"])2238 self.committedChanges.add(int(details["change"]))2239 committer =""2240if author not in self.users:2241 self.getUserMapFromPerforceServer()2242 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)22432244 self.gitStream.write("committer%s\n"% committer)22452246 self.gitStream.write("data <<EOT\n")2247 self.gitStream.write(details["desc"])2248 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2249(','.join(self.branchPrefixes), details["change"]))2250iflen(details['options']) >0:2251 self.gitStream.write(": options =%s"% details['options'])2252 self.gitStream.write("]\nEOT\n\n")22532254iflen(parent) >0:2255if self.verbose:2256print"parent%s"% parent2257 self.gitStream.write("from%s\n"% parent)22582259 self.streamP4Files(new_files)2260 self.gitStream.write("\n")22612262 change =int(details["change"])22632264if self.labels.has_key(change):2265 label = self.labels[change]2266 labelDetails = label[0]2267 labelRevisions = label[1]2268if self.verbose:2269print"Change%sis labelled%s"% (change, labelDetails)22702271 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2272for p in self.branchPrefixes])22732274iflen(files) ==len(labelRevisions):22752276 cleanedFiles = {}2277for info in files:2278if info["action"]in self.delete_actions:2279continue2280 cleanedFiles[info["depotFile"]] = info["rev"]22812282if cleanedFiles == labelRevisions:2283 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)22842285else:2286if not self.silent:2287print("Tag%sdoes not match with change%s: files do not match."2288% (labelDetails["label"], change))22892290else:2291if not self.silent:2292print("Tag%sdoes not match with change%s: file count is different."2293% (labelDetails["label"], change))22942295# Build a dictionary of changelists and labels, for "detect-labels" option.2296defgetLabels(self):2297 self.labels = {}22982299 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2300iflen(l) >0and not self.silent:2301print"Finding files belonging to labels in%s"% `self.depotPaths`23022303for output in l:2304 label = output["label"]2305 revisions = {}2306 newestChange =02307if self.verbose:2308print"Querying files for label%s"% label2309forfileinp4CmdList(["files"] +2310["%s...@%s"% (p, label)2311for p in self.depotPaths]):2312 revisions[file["depotFile"]] =file["rev"]2313 change =int(file["change"])2314if change > newestChange:2315 newestChange = change23162317 self.labels[newestChange] = [output, revisions]23182319if self.verbose:2320print"Label changes:%s"% self.labels.keys()23212322# Import p4 labels as git tags. A direct mapping does not2323# exist, so assume that if all the files are at the same revision2324# then we can use that, or it's something more complicated we should2325# just ignore.2326defimportP4Labels(self, stream, p4Labels):2327if verbose:2328print"import p4 labels: "+' '.join(p4Labels)23292330 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2331 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2332iflen(validLabelRegexp) ==0:2333 validLabelRegexp = defaultLabelRegexp2334 m = re.compile(validLabelRegexp)23352336for name in p4Labels:2337 commitFound =False23382339if not m.match(name):2340if verbose:2341print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2342continue23432344if name in ignoredP4Labels:2345continue23462347 labelDetails =p4CmdList(['label',"-o", name])[0]23482349# get the most recent changelist for each file in this label2350 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2351for p in self.depotPaths])23522353if change.has_key('change'):2354# find the corresponding git commit; take the oldest commit2355 changelist =int(change['change'])2356 gitCommit =read_pipe(["git","rev-list","--max-count=1",2357"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2358iflen(gitCommit) ==0:2359print"could not find git commit for changelist%d"% changelist2360else:2361 gitCommit = gitCommit.strip()2362 commitFound =True2363# Convert from p4 time format2364try:2365 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2366exceptValueError:2367print"Could not convert label time%s"% labelDetail['Update']2368 tmwhen =123692370 when =int(time.mktime(tmwhen))2371 self.streamTag(stream, name, labelDetails, gitCommit, when)2372if verbose:2373print"p4 label%smapped to git commit%s"% (name, gitCommit)2374else:2375if verbose:2376print"Label%shas no changelists - possibly deleted?"% name23772378if not commitFound:2379# We can't import this label; don't try again as it will get very2380# expensive repeatedly fetching all the files for labels that will2381# never be imported. If the label is moved in the future, the2382# ignore will need to be removed manually.2383system(["git","config","--add","git-p4.ignoredP4Labels", name])23842385defguessProjectName(self):2386for p in self.depotPaths:2387if p.endswith("/"):2388 p = p[:-1]2389 p = p[p.strip().rfind("/") +1:]2390if not p.endswith("/"):2391 p +="/"2392return p23932394defgetBranchMapping(self):2395 lostAndFoundBranches =set()23962397 user =gitConfig("git-p4.branchUser")2398iflen(user) >0:2399 command ="branches -u%s"% user2400else:2401 command ="branches"24022403for info inp4CmdList(command):2404 details =p4Cmd(["branch","-o", info["branch"]])2405 viewIdx =02406while details.has_key("View%s"% viewIdx):2407 paths = details["View%s"% viewIdx].split(" ")2408 viewIdx = viewIdx +12409# require standard //depot/foo/... //depot/bar/... mapping2410iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2411continue2412 source = paths[0]2413 destination = paths[1]2414## HACK2415ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2416 source = source[len(self.depotPaths[0]):-4]2417 destination = destination[len(self.depotPaths[0]):-4]24182419if destination in self.knownBranches:2420if not self.silent:2421print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2422print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2423continue24242425 self.knownBranches[destination] = source24262427 lostAndFoundBranches.discard(destination)24282429if source not in self.knownBranches:2430 lostAndFoundBranches.add(source)24312432# Perforce does not strictly require branches to be defined, so we also2433# check git config for a branch list.2434#2435# Example of branch definition in git config file:2436# [git-p4]2437# branchList=main:branchA2438# branchList=main:branchB2439# branchList=branchA:branchC2440 configBranches =gitConfigList("git-p4.branchList")2441for branch in configBranches:2442if branch:2443(source, destination) = branch.split(":")2444 self.knownBranches[destination] = source24452446 lostAndFoundBranches.discard(destination)24472448if source not in self.knownBranches:2449 lostAndFoundBranches.add(source)245024512452for branch in lostAndFoundBranches:2453 self.knownBranches[branch] = branch24542455defgetBranchMappingFromGitBranches(self):2456 branches =p4BranchesInGit(self.importIntoRemotes)2457for branch in branches.keys():2458if branch =="master":2459 branch ="main"2460else:2461 branch = branch[len(self.projectName):]2462 self.knownBranches[branch] = branch24632464deflistExistingP4GitBranches(self):2465# branches holds mapping from name to commit2466 branches =p4BranchesInGit(self.importIntoRemotes)2467 self.p4BranchesInGit = branches.keys()2468for branch in branches.keys():2469 self.initialParents[self.refPrefix + branch] = branches[branch]24702471defupdateOptionDict(self, d):2472 option_keys = {}2473if self.keepRepoPath:2474 option_keys['keepRepoPath'] =124752476 d["options"] =' '.join(sorted(option_keys.keys()))24772478defreadOptions(self, d):2479 self.keepRepoPath = (d.has_key('options')2480and('keepRepoPath'in d['options']))24812482defgitRefForBranch(self, branch):2483if branch =="main":2484return self.refPrefix +"master"24852486iflen(branch) <=0:2487return branch24882489return self.refPrefix + self.projectName + branch24902491defgitCommitByP4Change(self, ref, change):2492if self.verbose:2493print"looking in ref "+ ref +" for change%susing bisect..."% change24942495 earliestCommit =""2496 latestCommit =parseRevision(ref)24972498while True:2499if self.verbose:2500print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2501 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2502iflen(next) ==0:2503if self.verbose:2504print"argh"2505return""2506 log =extractLogMessageFromGitCommit(next)2507 settings =extractSettingsGitLog(log)2508 currentChange =int(settings['change'])2509if self.verbose:2510print"current change%s"% currentChange25112512if currentChange == change:2513if self.verbose:2514print"found%s"% next2515return next25162517if currentChange < change:2518 earliestCommit ="^%s"% next2519else:2520 latestCommit ="%s"% next25212522return""25232524defimportNewBranch(self, branch, maxChange):2525# make fast-import flush all changes to disk and update the refs using the checkpoint2526# command so that we can try to find the branch parent in the git history2527 self.gitStream.write("checkpoint\n\n");2528 self.gitStream.flush();2529 branchPrefix = self.depotPaths[0] + branch +"/"2530range="@1,%s"% maxChange2531#print "prefix" + branchPrefix2532 changes =p4ChangesForPaths([branchPrefix],range)2533iflen(changes) <=0:2534return False2535 firstChange = changes[0]2536#print "first change in branch: %s" % firstChange2537 sourceBranch = self.knownBranches[branch]2538 sourceDepotPath = self.depotPaths[0] + sourceBranch2539 sourceRef = self.gitRefForBranch(sourceBranch)2540#print "source " + sourceBranch25412542 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2543#print "branch parent: %s" % branchParentChange2544 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2545iflen(gitParent) >0:2546 self.initialParents[self.gitRefForBranch(branch)] = gitParent2547#print "parent git commit: %s" % gitParent25482549 self.importChanges(changes)2550return True25512552defsearchParent(self, parent, branch, target):2553 parentFound =False2554for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2555 blob = blob.strip()2556iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2557 parentFound =True2558if self.verbose:2559print"Found parent of%sin commit%s"% (branch, blob)2560break2561if parentFound:2562return blob2563else:2564return None25652566defimportChanges(self, changes):2567 cnt =12568for change in changes:2569 description =p4_describe(change)2570 self.updateOptionDict(description)25712572if not self.silent:2573 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2574 sys.stdout.flush()2575 cnt = cnt +125762577try:2578if self.detectBranches:2579 branches = self.splitFilesIntoBranches(description)2580for branch in branches.keys():2581## HACK --hwn2582 branchPrefix = self.depotPaths[0] + branch +"/"2583 self.branchPrefixes = [ branchPrefix ]25842585 parent =""25862587 filesForCommit = branches[branch]25882589if self.verbose:2590print"branch is%s"% branch25912592 self.updatedBranches.add(branch)25932594if branch not in self.createdBranches:2595 self.createdBranches.add(branch)2596 parent = self.knownBranches[branch]2597if parent == branch:2598 parent =""2599else:2600 fullBranch = self.projectName + branch2601if fullBranch not in self.p4BranchesInGit:2602if not self.silent:2603print("\nImporting new branch%s"% fullBranch);2604if self.importNewBranch(branch, change -1):2605 parent =""2606 self.p4BranchesInGit.append(fullBranch)2607if not self.silent:2608print("\nResuming with change%s"% change);26092610if self.verbose:2611print"parent determined through known branches:%s"% parent26122613 branch = self.gitRefForBranch(branch)2614 parent = self.gitRefForBranch(parent)26152616if self.verbose:2617print"looking for initial parent for%s; current parent is%s"% (branch, parent)26182619iflen(parent) ==0and branch in self.initialParents:2620 parent = self.initialParents[branch]2621del self.initialParents[branch]26222623 blob =None2624iflen(parent) >0:2625 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2626if self.verbose:2627print"Creating temporary branch: "+ tempBranch2628 self.commit(description, filesForCommit, tempBranch)2629 self.tempBranches.append(tempBranch)2630 self.checkpoint()2631 blob = self.searchParent(parent, branch, tempBranch)2632if blob:2633 self.commit(description, filesForCommit, branch, blob)2634else:2635if self.verbose:2636print"Parent of%snot found. Committing into head of%s"% (branch, parent)2637 self.commit(description, filesForCommit, branch, parent)2638else:2639 files = self.extractFilesFromCommit(description)2640 self.commit(description, files, self.branch,2641 self.initialParent)2642 self.initialParent =""2643exceptIOError:2644print self.gitError.read()2645 sys.exit(1)26462647defimportHeadRevision(self, revision):2648print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)26492650 details = {}2651 details["user"] ="git perforce import user"2652 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2653% (' '.join(self.depotPaths), revision))2654 details["change"] = revision2655 newestRevision =026562657 fileCnt =02658 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]26592660for info inp4CmdList(["files"] + fileArgs):26612662if'code'in info and info['code'] =='error':2663 sys.stderr.write("p4 returned an error:%s\n"2664% info['data'])2665if info['data'].find("must refer to client") >=0:2666 sys.stderr.write("This particular p4 error is misleading.\n")2667 sys.stderr.write("Perhaps the depot path was misspelled.\n");2668 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2669 sys.exit(1)2670if'p4ExitCode'in info:2671 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2672 sys.exit(1)267326742675 change =int(info["change"])2676if change > newestRevision:2677 newestRevision = change26782679if info["action"]in self.delete_actions:2680# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2681#fileCnt = fileCnt + 12682continue26832684for prop in["depotFile","rev","action","type"]:2685 details["%s%s"% (prop, fileCnt)] = info[prop]26862687 fileCnt = fileCnt +126882689 details["change"] = newestRevision26902691# Use time from top-most change so that all git p4 clones of2692# the same p4 repo have the same commit SHA1s.2693 res =p4_describe(newestRevision)2694 details["time"] = res["time"]26952696 self.updateOptionDict(details)2697try:2698 self.commit(details, self.extractFilesFromCommit(details), self.branch)2699exceptIOError:2700print"IO error with git fast-import. Is your git version recent enough?"2701print self.gitError.read()270227032704defrun(self, args):2705 self.depotPaths = []2706 self.changeRange =""2707 self.initialParent =""2708 self.previousDepotPaths = []27092710# map from branch depot path to parent branch2711 self.knownBranches = {}2712 self.initialParents = {}2713 self.hasOrigin =originP4BranchesExist()2714if not self.syncWithOrigin:2715 self.hasOrigin =False27162717if self.importIntoRemotes:2718 self.refPrefix ="refs/remotes/p4/"2719else:2720 self.refPrefix ="refs/heads/p4/"27212722if self.syncWithOrigin and self.hasOrigin:2723if not self.silent:2724print"Syncing with origin first by calling git fetch origin"2725system("git fetch origin")27262727iflen(self.branch) ==0:2728 self.branch = self.refPrefix +"master"2729ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2730system("git update-ref%srefs/heads/p4"% self.branch)2731system("git branch -D p4");2732# create it /after/ importing, when master exists2733if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2734system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))27352736# accept either the command-line option, or the configuration variable2737if self.useClientSpec:2738# will use this after clone to set the variable2739 self.useClientSpec_from_options =True2740else:2741ifgitConfig("git-p4.useclientspec","--bool") =="true":2742 self.useClientSpec =True2743if self.useClientSpec:2744 self.clientSpecDirs =getClientSpec()27452746# TODO: should always look at previous commits,2747# merge with previous imports, if possible.2748if args == []:2749if self.hasOrigin:2750createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2751 self.listExistingP4GitBranches()27522753iflen(self.p4BranchesInGit) >1:2754if not self.silent:2755print"Importing from/into multiple branches"2756 self.detectBranches =True27572758if self.verbose:2759print"branches:%s"% self.p4BranchesInGit27602761 p4Change =02762for branch in self.p4BranchesInGit:2763 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)27642765 settings =extractSettingsGitLog(logMsg)27662767 self.readOptions(settings)2768if(settings.has_key('depot-paths')2769and settings.has_key('change')):2770 change =int(settings['change']) +12771 p4Change =max(p4Change, change)27722773 depotPaths =sorted(settings['depot-paths'])2774if self.previousDepotPaths == []:2775 self.previousDepotPaths = depotPaths2776else:2777 paths = []2778for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2779 prev_list = prev.split("/")2780 cur_list = cur.split("/")2781for i inrange(0,min(len(cur_list),len(prev_list))):2782if cur_list[i] <> prev_list[i]:2783 i = i -12784break27852786 paths.append("/".join(cur_list[:i +1]))27872788 self.previousDepotPaths = paths27892790if p4Change >0:2791 self.depotPaths =sorted(self.previousDepotPaths)2792 self.changeRange ="@%s,#head"% p4Change2793if not self.detectBranches:2794 self.initialParent =parseRevision(self.branch)2795if not self.silent and not self.detectBranches:2796print"Performing incremental import into%sgit branch"% self.branch27972798if not self.branch.startswith("refs/"):2799 self.branch ="refs/heads/"+ self.branch28002801iflen(args) ==0and self.depotPaths:2802if not self.silent:2803print"Depot paths:%s"%' '.join(self.depotPaths)2804else:2805if self.depotPaths and self.depotPaths != args:2806print("previous import used depot path%sand now%swas specified. "2807"This doesn't work!"% (' '.join(self.depotPaths),2808' '.join(args)))2809 sys.exit(1)28102811 self.depotPaths =sorted(args)28122813 revision =""2814 self.users = {}28152816# Make sure no revision specifiers are used when --changesfile2817# is specified.2818 bad_changesfile =False2819iflen(self.changesFile) >0:2820for p in self.depotPaths:2821if p.find("@") >=0or p.find("#") >=0:2822 bad_changesfile =True2823break2824if bad_changesfile:2825die("Option --changesfile is incompatible with revision specifiers")28262827 newPaths = []2828for p in self.depotPaths:2829if p.find("@") != -1:2830 atIdx = p.index("@")2831 self.changeRange = p[atIdx:]2832if self.changeRange =="@all":2833 self.changeRange =""2834elif','not in self.changeRange:2835 revision = self.changeRange2836 self.changeRange =""2837 p = p[:atIdx]2838elif p.find("#") != -1:2839 hashIdx = p.index("#")2840 revision = p[hashIdx:]2841 p = p[:hashIdx]2842elif self.previousDepotPaths == []:2843# pay attention to changesfile, if given, else import2844# the entire p4 tree at the head revision2845iflen(self.changesFile) ==0:2846 revision ="#head"28472848 p = re.sub("\.\.\.$","", p)2849if not p.endswith("/"):2850 p +="/"28512852 newPaths.append(p)28532854 self.depotPaths = newPaths28552856# --detect-branches may change this for each branch2857 self.branchPrefixes = self.depotPaths28582859 self.loadUserMapFromCache()2860 self.labels = {}2861if self.detectLabels:2862 self.getLabels();28632864if self.detectBranches:2865## FIXME - what's a P4 projectName ?2866 self.projectName = self.guessProjectName()28672868if self.hasOrigin:2869 self.getBranchMappingFromGitBranches()2870else:2871 self.getBranchMapping()2872if self.verbose:2873print"p4-git branches:%s"% self.p4BranchesInGit2874print"initial parents:%s"% self.initialParents2875for b in self.p4BranchesInGit:2876if b !="master":28772878## FIXME2879 b = b[len(self.projectName):]2880 self.createdBranches.add(b)28812882 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))28832884 importProcess = subprocess.Popen(["git","fast-import"],2885 stdin=subprocess.PIPE, stdout=subprocess.PIPE,2886 stderr=subprocess.PIPE);2887 self.gitOutput = importProcess.stdout2888 self.gitStream = importProcess.stdin2889 self.gitError = importProcess.stderr28902891if revision:2892 self.importHeadRevision(revision)2893else:2894 changes = []28952896iflen(self.changesFile) >0:2897 output =open(self.changesFile).readlines()2898 changeSet =set()2899for line in output:2900 changeSet.add(int(line))29012902for change in changeSet:2903 changes.append(change)29042905 changes.sort()2906else:2907# catch "git p4 sync" with no new branches, in a repo that2908# does not have any existing p4 branches2909iflen(args) ==0and not self.p4BranchesInGit:2910die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2911if self.verbose:2912print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2913 self.changeRange)2914 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)29152916iflen(self.maxChanges) >0:2917 changes = changes[:min(int(self.maxChanges),len(changes))]29182919iflen(changes) ==0:2920if not self.silent:2921print"No changes to import!"2922else:2923if not self.silent and not self.detectBranches:2924print"Import destination:%s"% self.branch29252926 self.updatedBranches =set()29272928 self.importChanges(changes)29292930if not self.silent:2931print""2932iflen(self.updatedBranches) >0:2933 sys.stdout.write("Updated branches: ")2934for b in self.updatedBranches:2935 sys.stdout.write("%s"% b)2936 sys.stdout.write("\n")29372938ifgitConfig("git-p4.importLabels","--bool") =="true":2939 self.importLabels =True29402941if self.importLabels:2942 p4Labels =getP4Labels(self.depotPaths)2943 gitTags =getGitTags()29442945 missingP4Labels = p4Labels - gitTags2946 self.importP4Labels(self.gitStream, missingP4Labels)29472948 self.gitStream.close()2949if importProcess.wait() !=0:2950die("fast-import failed:%s"% self.gitError.read())2951 self.gitOutput.close()2952 self.gitError.close()29532954# Cleanup temporary branches created during import2955if self.tempBranches != []:2956for branch in self.tempBranches:2957read_pipe("git update-ref -d%s"% branch)2958 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))29592960return True29612962classP4Rebase(Command):2963def__init__(self):2964 Command.__init__(self)2965 self.options = [2966 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2967]2968 self.importLabels =False2969 self.description = ("Fetches the latest revision from perforce and "2970+"rebases the current work (branch) against it")29712972defrun(self, args):2973 sync =P4Sync()2974 sync.importLabels = self.importLabels2975 sync.run([])29762977return self.rebase()29782979defrebase(self):2980if os.system("git update-index --refresh") !=0:2981die("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.");2982iflen(read_pipe("git diff-index HEAD --")) >0:2983die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");29842985[upstream, settings] =findUpstreamBranchPoint()2986iflen(upstream) ==0:2987die("Cannot find upstream branchpoint for rebase")29882989# the branchpoint may be p4/foo~3, so strip off the parent2990 upstream = re.sub("~[0-9]+$","", upstream)29912992print"Rebasing the current branch onto%s"% upstream2993 oldHead =read_pipe("git rev-parse HEAD").strip()2994system("git rebase%s"% upstream)2995system("git diff-tree --stat --summary -M%sHEAD"% oldHead)2996return True29972998classP4Clone(P4Sync):2999def__init__(self):3000 P4Sync.__init__(self)3001 self.description ="Creates a new git repository and imports from Perforce into it"3002 self.usage ="usage: %prog [options] //depot/path[@revRange]"3003 self.options += [3004 optparse.make_option("--destination", dest="cloneDestination",3005 action='store', default=None,3006help="where to leave result of the clone"),3007 optparse.make_option("-/", dest="cloneExclude",3008 action="append",type="string",3009help="exclude depot path"),3010 optparse.make_option("--bare", dest="cloneBare",3011 action="store_true", default=False),3012]3013 self.cloneDestination =None3014 self.needsGit =False3015 self.cloneBare =False30163017# This is required for the "append" cloneExclude action3018defensure_value(self, attr, value):3019if nothasattr(self, attr)orgetattr(self, attr)is None:3020setattr(self, attr, value)3021returngetattr(self, attr)30223023defdefaultDestination(self, args):3024## TODO: use common prefix of args?3025 depotPath = args[0]3026 depotDir = re.sub("(@[^@]*)$","", depotPath)3027 depotDir = re.sub("(#[^#]*)$","", depotDir)3028 depotDir = re.sub(r"\.\.\.$","", depotDir)3029 depotDir = re.sub(r"/$","", depotDir)3030return os.path.split(depotDir)[1]30313032defrun(self, args):3033iflen(args) <1:3034return False30353036if self.keepRepoPath and not self.cloneDestination:3037 sys.stderr.write("Must specify destination for --keep-path\n")3038 sys.exit(1)30393040 depotPaths = args30413042if not self.cloneDestination andlen(depotPaths) >1:3043 self.cloneDestination = depotPaths[-1]3044 depotPaths = depotPaths[:-1]30453046 self.cloneExclude = ["/"+p for p in self.cloneExclude]3047for p in depotPaths:3048if not p.startswith("//"):3049return False30503051if not self.cloneDestination:3052 self.cloneDestination = self.defaultDestination(args)30533054print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)30553056if not os.path.exists(self.cloneDestination):3057 os.makedirs(self.cloneDestination)3058chdir(self.cloneDestination)30593060 init_cmd = ["git","init"]3061if self.cloneBare:3062 init_cmd.append("--bare")3063 subprocess.check_call(init_cmd)30643065if not P4Sync.run(self, depotPaths):3066return False3067if self.branch !="master":3068if self.importIntoRemotes:3069 masterbranch ="refs/remotes/p4/master"3070else:3071 masterbranch ="refs/heads/p4/master"3072ifgitBranchExists(masterbranch):3073system("git branch master%s"% masterbranch)3074if not self.cloneBare:3075system("git checkout -f")3076else:3077print"Could not detect main branch. No checkout/master branch created."30783079# auto-set this variable if invoked with --use-client-spec3080if self.useClientSpec_from_options:3081system("git config --bool git-p4.useclientspec true")30823083return True30843085classP4Branches(Command):3086def__init__(self):3087 Command.__init__(self)3088 self.options = [ ]3089 self.description = ("Shows the git branches that hold imports and their "3090+"corresponding perforce depot paths")3091 self.verbose =False30923093defrun(self, args):3094iforiginP4BranchesExist():3095createOrUpdateBranchesFromOrigin()30963097 cmdline ="git rev-parse --symbolic "3098 cmdline +=" --remotes"30993100for line inread_pipe_lines(cmdline):3101 line = line.strip()31023103if not line.startswith('p4/')or line =="p4/HEAD":3104continue3105 branch = line31063107 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3108 settings =extractSettingsGitLog(log)31093110print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3111return True31123113classHelpFormatter(optparse.IndentedHelpFormatter):3114def__init__(self):3115 optparse.IndentedHelpFormatter.__init__(self)31163117defformat_description(self, description):3118if description:3119return description +"\n"3120else:3121return""31223123defprintUsage(commands):3124print"usage:%s<command> [options]"% sys.argv[0]3125print""3126print"valid commands:%s"%", ".join(commands)3127print""3128print"Try%s<command> --help for command specific help."% sys.argv[0]3129print""31303131commands = {3132"debug": P4Debug,3133"submit": P4Submit,3134"commit": P4Submit,3135"sync": P4Sync,3136"rebase": P4Rebase,3137"clone": P4Clone,3138"rollback": P4RollBack,3139"branches": P4Branches3140}314131423143defmain():3144iflen(sys.argv[1:]) ==0:3145printUsage(commands.keys())3146 sys.exit(2)31473148 cmd =""3149 cmdName = sys.argv[1]3150try:3151 klass = commands[cmdName]3152 cmd =klass()3153exceptKeyError:3154print"unknown command%s"% cmdName3155print""3156printUsage(commands.keys())3157 sys.exit(2)31583159 options = cmd.options3160 cmd.gitdir = os.environ.get("GIT_DIR",None)31613162 args = sys.argv[2:]31633164 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3165if cmd.needsGit:3166 options.append(optparse.make_option("--git-dir", dest="gitdir"))31673168 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3169 options,3170 description = cmd.description,3171 formatter =HelpFormatter())31723173(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3174global verbose3175 verbose = cmd.verbose3176if cmd.needsGit:3177if cmd.gitdir ==None:3178 cmd.gitdir = os.path.abspath(".git")3179if notisValidGitDir(cmd.gitdir):3180 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3181if os.path.exists(cmd.gitdir):3182 cdup =read_pipe("git rev-parse --show-cdup").strip()3183iflen(cdup) >0:3184chdir(cdup);31853186if notisValidGitDir(cmd.gitdir):3187ifisValidGitDir(cmd.gitdir +"/.git"):3188 cmd.gitdir +="/.git"3189else:3190die("fatal: cannot locate git repository at%s"% cmd.gitdir)31913192 os.environ["GIT_DIR"] = cmd.gitdir31933194if not cmd.run(args):3195 parser.print_help()3196 sys.exit(2)319731983199if __name__ =='__main__':3200main()