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 132defp4_has_move_command(): 133"""See if the move command exists, that it supports -k, and that 134 it has not been administratively disabled. The arguments 135 must be correct, but the filenames do not have to exist. Use 136 ones with wildcards so even if they exist, it will fail.""" 137 138if notp4_has_command("move"): 139return False 140 cmd =p4_build_cmd(["move","-k","@from","@to"]) 141 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 142(out, err) = p.communicate() 143# return code will be 1 in either case 144if err.find("Invalid option") >=0: 145return False 146if err.find("disabled") >=0: 147return False 148# assume it failed because @... was invalid changelist 149return True 150 151defsystem(cmd): 152 expand =isinstance(cmd,basestring) 153if verbose: 154 sys.stderr.write("executing%s\n"%str(cmd)) 155 subprocess.check_call(cmd, shell=expand) 156 157defp4_system(cmd): 158"""Specifically invoke p4 as the system command. """ 159 real_cmd =p4_build_cmd(cmd) 160 expand =isinstance(real_cmd, basestring) 161 subprocess.check_call(real_cmd, shell=expand) 162 163defp4_integrate(src, dest): 164p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 165 166defp4_sync(f, *options): 167p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 168 169defp4_add(f): 170# forcibly add file names with wildcards 171ifwildcard_present(f): 172p4_system(["add","-f", f]) 173else: 174p4_system(["add", f]) 175 176defp4_delete(f): 177p4_system(["delete",wildcard_encode(f)]) 178 179defp4_edit(f): 180p4_system(["edit",wildcard_encode(f)]) 181 182defp4_revert(f): 183p4_system(["revert",wildcard_encode(f)]) 184 185defp4_reopen(type, f): 186p4_system(["reopen","-t",type,wildcard_encode(f)]) 187 188defp4_move(src, dest): 189p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 190 191defp4_describe(change): 192"""Make sure it returns a valid result by checking for 193 the presence of field "time". Return a dict of the 194 results.""" 195 196 ds =p4CmdList(["describe","-s",str(change)]) 197iflen(ds) !=1: 198die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 199 200 d = ds[0] 201 202if"p4ExitCode"in d: 203die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 204str(d))) 205if"code"in d: 206if d["code"] =="error": 207die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 208 209if"time"not in d: 210die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 211 212return d 213 214# 215# Canonicalize the p4 type and return a tuple of the 216# base type, plus any modifiers. See "p4 help filetypes" 217# for a list and explanation. 218# 219defsplit_p4_type(p4type): 220 221 p4_filetypes_historical = { 222"ctempobj":"binary+Sw", 223"ctext":"text+C", 224"cxtext":"text+Cx", 225"ktext":"text+k", 226"kxtext":"text+kx", 227"ltext":"text+F", 228"tempobj":"binary+FSw", 229"ubinary":"binary+F", 230"uresource":"resource+F", 231"uxbinary":"binary+Fx", 232"xbinary":"binary+x", 233"xltext":"text+Fx", 234"xtempobj":"binary+Swx", 235"xtext":"text+x", 236"xunicode":"unicode+x", 237"xutf16":"utf16+x", 238} 239if p4type in p4_filetypes_historical: 240 p4type = p4_filetypes_historical[p4type] 241 mods ="" 242 s = p4type.split("+") 243 base = s[0] 244 mods ="" 245iflen(s) >1: 246 mods = s[1] 247return(base, mods) 248 249# 250# return the raw p4 type of a file (text, text+ko, etc) 251# 252defp4_type(file): 253 results =p4CmdList(["fstat","-T","headType",file]) 254return results[0]['headType'] 255 256# 257# Given a type base and modifier, return a regexp matching 258# the keywords that can be expanded in the file 259# 260defp4_keywords_regexp_for_type(base, type_mods): 261if base in("text","unicode","binary"): 262 kwords =None 263if"ko"in type_mods: 264 kwords ='Id|Header' 265elif"k"in type_mods: 266 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 267else: 268return None 269 pattern = r""" 270 \$ # Starts with a dollar, followed by... 271 (%s) # one of the keywords, followed by... 272 (:[^$\n]+)? # possibly an old expansion, followed by... 273 \$ # another dollar 274 """% kwords 275return pattern 276else: 277return None 278 279# 280# Given a file, return a regexp matching the possible 281# RCS keywords that will be expanded, or None for files 282# with kw expansion turned off. 283# 284defp4_keywords_regexp_for_file(file): 285if not os.path.exists(file): 286return None 287else: 288(type_base, type_mods) =split_p4_type(p4_type(file)) 289returnp4_keywords_regexp_for_type(type_base, type_mods) 290 291defsetP4ExecBit(file, mode): 292# Reopens an already open file and changes the execute bit to match 293# the execute bit setting in the passed in mode. 294 295 p4Type ="+x" 296 297if notisModeExec(mode): 298 p4Type =getP4OpenedType(file) 299 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 300 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 301if p4Type[-1] =="+": 302 p4Type = p4Type[0:-1] 303 304p4_reopen(p4Type,file) 305 306defgetP4OpenedType(file): 307# Returns the perforce file type for the given file. 308 309 result =p4_read_pipe(["opened",wildcard_encode(file)]) 310 match = re.match(".*\((.+)\)\r?$", result) 311if match: 312return match.group(1) 313else: 314die("Could not determine file type for%s(result: '%s')"% (file, result)) 315 316# Return the set of all p4 labels 317defgetP4Labels(depotPaths): 318 labels =set() 319ifisinstance(depotPaths,basestring): 320 depotPaths = [depotPaths] 321 322for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 323 label = l['label'] 324 labels.add(label) 325 326return labels 327 328# Return the set of all git tags 329defgetGitTags(): 330 gitTags =set() 331for line inread_pipe_lines(["git","tag"]): 332 tag = line.strip() 333 gitTags.add(tag) 334return gitTags 335 336defdiffTreePattern(): 337# This is a simple generator for the diff tree regex pattern. This could be 338# a class variable if this and parseDiffTreeEntry were a part of a class. 339 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 340while True: 341yield pattern 342 343defparseDiffTreeEntry(entry): 344"""Parses a single diff tree entry into its component elements. 345 346 See git-diff-tree(1) manpage for details about the format of the diff 347 output. This method returns a dictionary with the following elements: 348 349 src_mode - The mode of the source file 350 dst_mode - The mode of the destination file 351 src_sha1 - The sha1 for the source file 352 dst_sha1 - The sha1 fr the destination file 353 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 354 status_score - The score for the status (applicable for 'C' and 'R' 355 statuses). This is None if there is no score. 356 src - The path for the source file. 357 dst - The path for the destination file. This is only present for 358 copy or renames. If it is not present, this is None. 359 360 If the pattern is not matched, None is returned.""" 361 362 match =diffTreePattern().next().match(entry) 363if match: 364return{ 365'src_mode': match.group(1), 366'dst_mode': match.group(2), 367'src_sha1': match.group(3), 368'dst_sha1': match.group(4), 369'status': match.group(5), 370'status_score': match.group(6), 371'src': match.group(7), 372'dst': match.group(10) 373} 374return None 375 376defisModeExec(mode): 377# Returns True if the given git mode represents an executable file, 378# otherwise False. 379return mode[-3:] =="755" 380 381defisModeExecChanged(src_mode, dst_mode): 382returnisModeExec(src_mode) !=isModeExec(dst_mode) 383 384defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 385 386ifisinstance(cmd,basestring): 387 cmd ="-G "+ cmd 388 expand =True 389else: 390 cmd = ["-G"] + cmd 391 expand =False 392 393 cmd =p4_build_cmd(cmd) 394if verbose: 395 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 396 397# Use a temporary file to avoid deadlocks without 398# subprocess.communicate(), which would put another copy 399# of stdout into memory. 400 stdin_file =None 401if stdin is not None: 402 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 403ifisinstance(stdin,basestring): 404 stdin_file.write(stdin) 405else: 406for i in stdin: 407 stdin_file.write(i +'\n') 408 stdin_file.flush() 409 stdin_file.seek(0) 410 411 p4 = subprocess.Popen(cmd, 412 shell=expand, 413 stdin=stdin_file, 414 stdout=subprocess.PIPE) 415 416 result = [] 417try: 418while True: 419 entry = marshal.load(p4.stdout) 420if cb is not None: 421cb(entry) 422else: 423 result.append(entry) 424exceptEOFError: 425pass 426 exitCode = p4.wait() 427if exitCode !=0: 428 entry = {} 429 entry["p4ExitCode"] = exitCode 430 result.append(entry) 431 432return result 433 434defp4Cmd(cmd): 435list=p4CmdList(cmd) 436 result = {} 437for entry inlist: 438 result.update(entry) 439return result; 440 441defp4Where(depotPath): 442if not depotPath.endswith("/"): 443 depotPath +="/" 444 depotPath = depotPath +"..." 445 outputList =p4CmdList(["where", depotPath]) 446 output =None 447for entry in outputList: 448if"depotFile"in entry: 449if entry["depotFile"] == depotPath: 450 output = entry 451break 452elif"data"in entry: 453 data = entry.get("data") 454 space = data.find(" ") 455if data[:space] == depotPath: 456 output = entry 457break 458if output ==None: 459return"" 460if output["code"] =="error": 461return"" 462 clientPath ="" 463if"path"in output: 464 clientPath = output.get("path") 465elif"data"in output: 466 data = output.get("data") 467 lastSpace = data.rfind(" ") 468 clientPath = data[lastSpace +1:] 469 470if clientPath.endswith("..."): 471 clientPath = clientPath[:-3] 472return clientPath 473 474defcurrentGitBranch(): 475returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 476 477defisValidGitDir(path): 478if(os.path.exists(path +"/HEAD") 479and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 480return True; 481return False 482 483defparseRevision(ref): 484returnread_pipe("git rev-parse%s"% ref).strip() 485 486defbranchExists(ref): 487 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 488 ignore_error=True) 489returnlen(rev) >0 490 491defextractLogMessageFromGitCommit(commit): 492 logMessage ="" 493 494## fixme: title is first line of commit, not 1st paragraph. 495 foundTitle =False 496for log inread_pipe_lines("git cat-file commit%s"% commit): 497if not foundTitle: 498iflen(log) ==1: 499 foundTitle =True 500continue 501 502 logMessage += log 503return logMessage 504 505defextractSettingsGitLog(log): 506 values = {} 507for line in log.split("\n"): 508 line = line.strip() 509 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 510if not m: 511continue 512 513 assignments = m.group(1).split(':') 514for a in assignments: 515 vals = a.split('=') 516 key = vals[0].strip() 517 val = ('='.join(vals[1:])).strip() 518if val.endswith('\"')and val.startswith('"'): 519 val = val[1:-1] 520 521 values[key] = val 522 523 paths = values.get("depot-paths") 524if not paths: 525 paths = values.get("depot-path") 526if paths: 527 values['depot-paths'] = paths.split(',') 528return values 529 530defgitBranchExists(branch): 531 proc = subprocess.Popen(["git","rev-parse", branch], 532 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 533return proc.wait() ==0; 534 535_gitConfig = {} 536defgitConfig(key, args =None):# set args to "--bool", for instance 537if not _gitConfig.has_key(key): 538 argsFilter ="" 539if args !=None: 540 argsFilter ="%s"% args 541 cmd ="git config%s%s"% (argsFilter, key) 542 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 543return _gitConfig[key] 544 545defgitConfigList(key): 546if not _gitConfig.has_key(key): 547 _gitConfig[key] =read_pipe("git config --get-all%s"% key, ignore_error=True).strip().split(os.linesep) 548return _gitConfig[key] 549 550defp4BranchesInGit(branchesAreInRemotes=True): 551"""Find all the branches whose names start with "p4/", looking 552 in remotes or heads as specified by the argument. Return 553 a dictionary of{ branch: revision }for each one found. 554 The branch names are the short names, without any 555 "p4/" prefix.""" 556 557 branches = {} 558 559 cmdline ="git rev-parse --symbolic " 560if branchesAreInRemotes: 561 cmdline +="--remotes" 562else: 563 cmdline +="--branches" 564 565for line inread_pipe_lines(cmdline): 566 line = line.strip() 567 568# only import to p4/ 569if not line.startswith('p4/'): 570continue 571# special symbolic ref to p4/master 572if line =="p4/HEAD": 573continue 574 575# strip off p4/ prefix 576 branch = line[len("p4/"):] 577 578 branches[branch] =parseRevision(line) 579 580return branches 581 582deffindUpstreamBranchPoint(head ="HEAD"): 583 branches =p4BranchesInGit() 584# map from depot-path to branch name 585 branchByDepotPath = {} 586for branch in branches.keys(): 587 tip = branches[branch] 588 log =extractLogMessageFromGitCommit(tip) 589 settings =extractSettingsGitLog(log) 590if settings.has_key("depot-paths"): 591 paths =",".join(settings["depot-paths"]) 592 branchByDepotPath[paths] ="remotes/p4/"+ branch 593 594 settings =None 595 parent =0 596while parent <65535: 597 commit = head +"~%s"% parent 598 log =extractLogMessageFromGitCommit(commit) 599 settings =extractSettingsGitLog(log) 600if settings.has_key("depot-paths"): 601 paths =",".join(settings["depot-paths"]) 602if branchByDepotPath.has_key(paths): 603return[branchByDepotPath[paths], settings] 604 605 parent = parent +1 606 607return["", settings] 608 609defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 610if not silent: 611print("Creating/updating branch(es) in%sbased on origin branch(es)" 612% localRefPrefix) 613 614 originPrefix ="origin/p4/" 615 616for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 617 line = line.strip() 618if(not line.startswith(originPrefix))or line.endswith("HEAD"): 619continue 620 621 headName = line[len(originPrefix):] 622 remoteHead = localRefPrefix + headName 623 originHead = line 624 625 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 626if(not original.has_key('depot-paths') 627or not original.has_key('change')): 628continue 629 630 update =False 631if notgitBranchExists(remoteHead): 632if verbose: 633print"creating%s"% remoteHead 634 update =True 635else: 636 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 637if settings.has_key('change') >0: 638if settings['depot-paths'] == original['depot-paths']: 639 originP4Change =int(original['change']) 640 p4Change =int(settings['change']) 641if originP4Change > p4Change: 642print("%s(%s) is newer than%s(%s). " 643"Updating p4 branch from origin." 644% (originHead, originP4Change, 645 remoteHead, p4Change)) 646 update =True 647else: 648print("Ignoring:%swas imported from%swhile " 649"%swas imported from%s" 650% (originHead,','.join(original['depot-paths']), 651 remoteHead,','.join(settings['depot-paths']))) 652 653if update: 654system("git update-ref%s %s"% (remoteHead, originHead)) 655 656deforiginP4BranchesExist(): 657returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 658 659defp4ChangesForPaths(depotPaths, changeRange): 660assert depotPaths 661 cmd = ['changes'] 662for p in depotPaths: 663 cmd += ["%s...%s"% (p, changeRange)] 664 output =p4_read_pipe_lines(cmd) 665 666 changes = {} 667for line in output: 668 changeNum =int(line.split(" ")[1]) 669 changes[changeNum] =True 670 671 changelist = changes.keys() 672 changelist.sort() 673return changelist 674 675defp4PathStartsWith(path, prefix): 676# This method tries to remedy a potential mixed-case issue: 677# 678# If UserA adds //depot/DirA/file1 679# and UserB adds //depot/dira/file2 680# 681# we may or may not have a problem. If you have core.ignorecase=true, 682# we treat DirA and dira as the same directory 683 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 684if ignorecase: 685return path.lower().startswith(prefix.lower()) 686return path.startswith(prefix) 687 688defgetClientSpec(): 689"""Look at the p4 client spec, create a View() object that contains 690 all the mappings, and return it.""" 691 692 specList =p4CmdList("client -o") 693iflen(specList) !=1: 694die('Output from "client -o" is%dlines, expecting 1'% 695len(specList)) 696 697# dictionary of all client parameters 698 entry = specList[0] 699 700# just the keys that start with "View" 701 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 702 703# hold this new View 704 view =View() 705 706# append the lines, in order, to the view 707for view_num inrange(len(view_keys)): 708 k ="View%d"% view_num 709if k not in view_keys: 710die("Expected view key%smissing"% k) 711 view.append(entry[k]) 712 713return view 714 715defgetClientRoot(): 716"""Grab the client directory.""" 717 718 output =p4CmdList("client -o") 719iflen(output) !=1: 720die('Output from "client -o" is%dlines, expecting 1'%len(output)) 721 722 entry = output[0] 723if"Root"not in entry: 724die('Client has no "Root"') 725 726return entry["Root"] 727 728# 729# P4 wildcards are not allowed in filenames. P4 complains 730# if you simply add them, but you can force it with "-f", in 731# which case it translates them into %xx encoding internally. 732# 733defwildcard_decode(path): 734# Search for and fix just these four characters. Do % last so 735# that fixing it does not inadvertently create new %-escapes. 736# Cannot have * in a filename in windows; untested as to 737# what p4 would do in such a case. 738if not platform.system() =="Windows": 739 path = path.replace("%2A","*") 740 path = path.replace("%23","#") \ 741.replace("%40","@") \ 742.replace("%25","%") 743return path 744 745defwildcard_encode(path): 746# do % first to avoid double-encoding the %s introduced here 747 path = path.replace("%","%25") \ 748.replace("*","%2A") \ 749.replace("#","%23") \ 750.replace("@","%40") 751return path 752 753defwildcard_present(path): 754return path.translate(None,"*#@%") != path 755 756class Command: 757def__init__(self): 758 self.usage ="usage: %prog [options]" 759 self.needsGit =True 760 self.verbose =False 761 762class P4UserMap: 763def__init__(self): 764 self.userMapFromPerforceServer =False 765 self.myP4UserId =None 766 767defp4UserId(self): 768if self.myP4UserId: 769return self.myP4UserId 770 771 results =p4CmdList("user -o") 772for r in results: 773if r.has_key('User'): 774 self.myP4UserId = r['User'] 775return r['User'] 776die("Could not find your p4 user id") 777 778defp4UserIsMe(self, p4User): 779# return True if the given p4 user is actually me 780 me = self.p4UserId() 781if not p4User or p4User != me: 782return False 783else: 784return True 785 786defgetUserCacheFilename(self): 787 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 788return home +"/.gitp4-usercache.txt" 789 790defgetUserMapFromPerforceServer(self): 791if self.userMapFromPerforceServer: 792return 793 self.users = {} 794 self.emails = {} 795 796for output inp4CmdList("users"): 797if not output.has_key("User"): 798continue 799 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 800 self.emails[output["Email"]] = output["User"] 801 802 803 s ='' 804for(key, val)in self.users.items(): 805 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 806 807open(self.getUserCacheFilename(),"wb").write(s) 808 self.userMapFromPerforceServer =True 809 810defloadUserMapFromCache(self): 811 self.users = {} 812 self.userMapFromPerforceServer =False 813try: 814 cache =open(self.getUserCacheFilename(),"rb") 815 lines = cache.readlines() 816 cache.close() 817for line in lines: 818 entry = line.strip().split("\t") 819 self.users[entry[0]] = entry[1] 820exceptIOError: 821 self.getUserMapFromPerforceServer() 822 823classP4Debug(Command): 824def__init__(self): 825 Command.__init__(self) 826 self.options = [] 827 self.description ="A tool to debug the output of p4 -G." 828 self.needsGit =False 829 830defrun(self, args): 831 j =0 832for output inp4CmdList(args): 833print'Element:%d'% j 834 j +=1 835print output 836return True 837 838classP4RollBack(Command): 839def__init__(self): 840 Command.__init__(self) 841 self.options = [ 842 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 843] 844 self.description ="A tool to debug the multi-branch import. Don't use :)" 845 self.rollbackLocalBranches =False 846 847defrun(self, args): 848iflen(args) !=1: 849return False 850 maxChange =int(args[0]) 851 852if"p4ExitCode"inp4Cmd("changes -m 1"): 853die("Problems executing p4"); 854 855if self.rollbackLocalBranches: 856 refPrefix ="refs/heads/" 857 lines =read_pipe_lines("git rev-parse --symbolic --branches") 858else: 859 refPrefix ="refs/remotes/" 860 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 861 862for line in lines: 863if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 864 line = line.strip() 865 ref = refPrefix + line 866 log =extractLogMessageFromGitCommit(ref) 867 settings =extractSettingsGitLog(log) 868 869 depotPaths = settings['depot-paths'] 870 change = settings['change'] 871 872 changed =False 873 874iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 875for p in depotPaths]))) ==0: 876print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 877system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 878continue 879 880while change andint(change) > maxChange: 881 changed =True 882if self.verbose: 883print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 884system("git update-ref%s\"%s^\""% (ref, ref)) 885 log =extractLogMessageFromGitCommit(ref) 886 settings =extractSettingsGitLog(log) 887 888 889 depotPaths = settings['depot-paths'] 890 change = settings['change'] 891 892if changed: 893print"%srewound to%s"% (ref, change) 894 895return True 896 897classP4Submit(Command, P4UserMap): 898 899 conflict_behavior_choices = ("ask","skip","quit") 900 901def__init__(self): 902 Command.__init__(self) 903 P4UserMap.__init__(self) 904 self.options = [ 905 optparse.make_option("--origin", dest="origin"), 906 optparse.make_option("-M", dest="detectRenames", action="store_true"), 907# preserve the user, requires relevant p4 permissions 908 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 909 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 910 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 911 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 912 optparse.make_option("--conflict", dest="conflict_behavior", 913 choices=self.conflict_behavior_choices) 914] 915 self.description ="Submit changes from git to the perforce depot." 916 self.usage +=" [name of git branch to submit into perforce depot]" 917 self.origin ="" 918 self.detectRenames =False 919 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 920 self.dry_run =False 921 self.prepare_p4_only =False 922 self.conflict_behavior =None 923 self.isWindows = (platform.system() =="Windows") 924 self.exportLabels =False 925 self.p4HasMoveCommand =p4_has_move_command() 926 927defcheck(self): 928iflen(p4CmdList("opened ...")) >0: 929die("You have files opened with perforce! Close them before starting the sync.") 930 931defseparate_jobs_from_description(self, message): 932"""Extract and return a possible Jobs field in the commit 933 message. It goes into a separate section in the p4 change 934 specification. 935 936 A jobs line starts with "Jobs:" and looks like a new field 937 in a form. Values are white-space separated on the same 938 line or on following lines that start with a tab. 939 940 This does not parse and extract the full git commit message 941 like a p4 form. It just sees the Jobs: line as a marker 942 to pass everything from then on directly into the p4 form, 943 but outside the description section. 944 945 Return a tuple (stripped log message, jobs string).""" 946 947 m = re.search(r'^Jobs:', message, re.MULTILINE) 948if m is None: 949return(message,None) 950 951 jobtext = message[m.start():] 952 stripped_message = message[:m.start()].rstrip() 953return(stripped_message, jobtext) 954 955defprepareLogMessage(self, template, message, jobs): 956"""Edits the template returned from "p4 change -o" to insert 957 the message in the Description field, and the jobs text in 958 the Jobs field.""" 959 result ="" 960 961 inDescriptionSection =False 962 963for line in template.split("\n"): 964if line.startswith("#"): 965 result += line +"\n" 966continue 967 968if inDescriptionSection: 969if line.startswith("Files:")or line.startswith("Jobs:"): 970 inDescriptionSection =False 971# insert Jobs section 972if jobs: 973 result += jobs +"\n" 974else: 975continue 976else: 977if line.startswith("Description:"): 978 inDescriptionSection =True 979 line +="\n" 980for messageLine in message.split("\n"): 981 line +="\t"+ messageLine +"\n" 982 983 result += line +"\n" 984 985return result 986 987defpatchRCSKeywords(self,file, pattern): 988# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern 989(handle, outFileName) = tempfile.mkstemp(dir='.') 990try: 991 outFile = os.fdopen(handle,"w+") 992 inFile =open(file,"r") 993 regexp = re.compile(pattern, re.VERBOSE) 994for line in inFile.readlines(): 995 line = regexp.sub(r'$\1$', line) 996 outFile.write(line) 997 inFile.close() 998 outFile.close() 999# Forcibly overwrite the original file1000 os.unlink(file)1001 shutil.move(outFileName,file)1002except:1003# cleanup our temporary file1004 os.unlink(outFileName)1005print"Failed to strip RCS keywords in%s"%file1006raise10071008print"Patched up RCS keywords in%s"%file10091010defp4UserForCommit(self,id):1011# Return the tuple (perforce user,git email) for a given git commit id1012 self.getUserMapFromPerforceServer()1013 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id)1014 gitEmail = gitEmail.strip()1015if not self.emails.has_key(gitEmail):1016return(None,gitEmail)1017else:1018return(self.emails[gitEmail],gitEmail)10191020defcheckValidP4Users(self,commits):1021# check if any git authors cannot be mapped to p4 users1022foridin commits:1023(user,email) = self.p4UserForCommit(id)1024if not user:1025 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1026ifgitConfig('git-p4.allowMissingP4Users').lower() =="true":1027print"%s"% msg1028else:1029die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10301031deflastP4Changelist(self):1032# Get back the last changelist number submitted in this client spec. This1033# then gets used to patch up the username in the change. If the same1034# client spec is being used by multiple processes then this might go1035# wrong.1036 results =p4CmdList("client -o")# find the current client1037 client =None1038for r in results:1039if r.has_key('Client'):1040 client = r['Client']1041break1042if not client:1043die("could not get client spec")1044 results =p4CmdList(["changes","-c", client,"-m","1"])1045for r in results:1046if r.has_key('change'):1047return r['change']1048die("Could not get changelist number for last submit - cannot patch up user details")10491050defmodifyChangelistUser(self, changelist, newUser):1051# fixup the user field of a changelist after it has been submitted.1052 changes =p4CmdList("change -o%s"% changelist)1053iflen(changes) !=1:1054die("Bad output from p4 change modifying%sto user%s"%1055(changelist, newUser))10561057 c = changes[0]1058if c['User'] == newUser:return# nothing to do1059 c['User'] = newUser1060input= marshal.dumps(c)10611062 result =p4CmdList("change -f -i", stdin=input)1063for r in result:1064if r.has_key('code'):1065if r['code'] =='error':1066die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1067if r.has_key('data'):1068print("Updated user field for changelist%sto%s"% (changelist, newUser))1069return1070die("Could not modify user field of changelist%sto%s"% (changelist, newUser))10711072defcanChangeChangelists(self):1073# check to see if we have p4 admin or super-user permissions, either of1074# which are required to modify changelists.1075 results =p4CmdList(["protects", self.depotPath])1076for r in results:1077if r.has_key('perm'):1078if r['perm'] =='admin':1079return11080if r['perm'] =='super':1081return11082return010831084defprepareSubmitTemplate(self):1085"""Run "p4 change -o" to grab a change specification template.1086 This does not use "p4 -G", as it is nice to keep the submission1087 template in original order, since a human might edit it.10881089 Remove lines in the Files section that show changes to files1090 outside the depot path we're committing into."""10911092 template =""1093 inFilesSection =False1094for line inp4_read_pipe_lines(['change','-o']):1095if line.endswith("\r\n"):1096 line = line[:-2] +"\n"1097if inFilesSection:1098if line.startswith("\t"):1099# path starts and ends with a tab1100 path = line[1:]1101 lastTab = path.rfind("\t")1102if lastTab != -1:1103 path = path[:lastTab]1104if notp4PathStartsWith(path, self.depotPath):1105continue1106else:1107 inFilesSection =False1108else:1109if line.startswith("Files:"):1110 inFilesSection =True11111112 template += line11131114return template11151116defedit_template(self, template_file):1117"""Invoke the editor to let the user change the submission1118 message. Return true if okay to continue with the submit."""11191120# if configured to skip the editing part, just submit1121ifgitConfig("git-p4.skipSubmitEdit") =="true":1122return True11231124# look at the modification time, to check later if the user saved1125# the file1126 mtime = os.stat(template_file).st_mtime11271128# invoke the editor1129if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1130 editor = os.environ.get("P4EDITOR")1131else:1132 editor =read_pipe("git var GIT_EDITOR").strip()1133system(editor +" "+ template_file)11341135# If the file was not saved, prompt to see if this patch should1136# be skipped. But skip this verification step if configured so.1137ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1138return True11391140# modification time updated means user saved the file1141if os.stat(template_file).st_mtime > mtime:1142return True11431144while True:1145 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1146if response =='y':1147return True1148if response =='n':1149return False11501151defapplyCommit(self,id):1152"""Apply one commit, return True if it succeeded."""11531154print"Applying",read_pipe(["git","show","-s",1155"--format=format:%h%s",id])11561157(p4User, gitEmail) = self.p4UserForCommit(id)11581159 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1160 filesToAdd =set()1161 filesToDelete =set()1162 editedFiles =set()1163 pureRenameCopy =set()1164 filesToChangeExecBit = {}11651166for line in diff:1167 diff =parseDiffTreeEntry(line)1168 modifier = diff['status']1169 path = diff['src']1170if modifier =="M":1171p4_edit(path)1172ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1173 filesToChangeExecBit[path] = diff['dst_mode']1174 editedFiles.add(path)1175elif modifier =="A":1176 filesToAdd.add(path)1177 filesToChangeExecBit[path] = diff['dst_mode']1178if path in filesToDelete:1179 filesToDelete.remove(path)1180elif modifier =="D":1181 filesToDelete.add(path)1182if path in filesToAdd:1183 filesToAdd.remove(path)1184elif modifier =="C":1185 src, dest = diff['src'], diff['dst']1186p4_integrate(src, dest)1187 pureRenameCopy.add(dest)1188if diff['src_sha1'] != diff['dst_sha1']:1189p4_edit(dest)1190 pureRenameCopy.discard(dest)1191ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1192p4_edit(dest)1193 pureRenameCopy.discard(dest)1194 filesToChangeExecBit[dest] = diff['dst_mode']1195 os.unlink(dest)1196 editedFiles.add(dest)1197elif modifier =="R":1198 src, dest = diff['src'], diff['dst']1199if self.p4HasMoveCommand:1200p4_edit(src)# src must be open before move1201p4_move(src, dest)# opens for (move/delete, move/add)1202else:1203p4_integrate(src, dest)1204if diff['src_sha1'] != diff['dst_sha1']:1205p4_edit(dest)1206else:1207 pureRenameCopy.add(dest)1208ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1209if not self.p4HasMoveCommand:1210p4_edit(dest)# with move: already open, writable1211 filesToChangeExecBit[dest] = diff['dst_mode']1212if not self.p4HasMoveCommand:1213 os.unlink(dest)1214 filesToDelete.add(src)1215 editedFiles.add(dest)1216else:1217die("unknown modifier%sfor%s"% (modifier, path))12181219 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1220 patchcmd = diffcmd +" | git apply "1221 tryPatchCmd = patchcmd +"--check -"1222 applyPatchCmd = patchcmd +"--check --apply -"1223 patch_succeeded =True12241225if os.system(tryPatchCmd) !=0:1226 fixed_rcs_keywords =False1227 patch_succeeded =False1228print"Unfortunately applying the change failed!"12291230# Patch failed, maybe it's just RCS keyword woes. Look through1231# the patch to see if that's possible.1232ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1233file=None1234 pattern =None1235 kwfiles = {}1236forfilein editedFiles | filesToDelete:1237# did this file's delta contain RCS keywords?1238 pattern =p4_keywords_regexp_for_file(file)12391240if pattern:1241# this file is a possibility...look for RCS keywords.1242 regexp = re.compile(pattern, re.VERBOSE)1243for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1244if regexp.search(line):1245if verbose:1246print"got keyword match on%sin%sin%s"% (pattern, line,file)1247 kwfiles[file] = pattern1248break12491250forfilein kwfiles:1251if verbose:1252print"zapping%swith%s"% (line,pattern)1253 self.patchRCSKeywords(file, kwfiles[file])1254 fixed_rcs_keywords =True12551256if fixed_rcs_keywords:1257print"Retrying the patch with RCS keywords cleaned up"1258if os.system(tryPatchCmd) ==0:1259 patch_succeeded =True12601261if not patch_succeeded:1262for f in editedFiles:1263p4_revert(f)1264return False12651266#1267# Apply the patch for real, and do add/delete/+x handling.1268#1269system(applyPatchCmd)12701271for f in filesToAdd:1272p4_add(f)1273for f in filesToDelete:1274p4_revert(f)1275p4_delete(f)12761277# Set/clear executable bits1278for f in filesToChangeExecBit.keys():1279 mode = filesToChangeExecBit[f]1280setP4ExecBit(f, mode)12811282#1283# Build p4 change description, starting with the contents1284# of the git commit message.1285#1286 logMessage =extractLogMessageFromGitCommit(id)1287 logMessage = logMessage.strip()1288(logMessage, jobs) = self.separate_jobs_from_description(logMessage)12891290 template = self.prepareSubmitTemplate()1291 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)12921293if self.preserveUser:1294 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User12951296if self.checkAuthorship and not self.p4UserIsMe(p4User):1297 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1298 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1299 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13001301 separatorLine ="######## everything below this line is just the diff #######\n"13021303# diff1304if os.environ.has_key("P4DIFF"):1305del(os.environ["P4DIFF"])1306 diff =""1307for editedFile in editedFiles:1308 diff +=p4_read_pipe(['diff','-du',1309wildcard_encode(editedFile)])13101311# new file diff1312 newdiff =""1313for newFile in filesToAdd:1314 newdiff +="==== new file ====\n"1315 newdiff +="--- /dev/null\n"1316 newdiff +="+++%s\n"% newFile1317 f =open(newFile,"r")1318for line in f.readlines():1319 newdiff +="+"+ line1320 f.close()13211322# change description file: submitTemplate, separatorLine, diff, newdiff1323(handle, fileName) = tempfile.mkstemp()1324 tmpFile = os.fdopen(handle,"w+")1325if self.isWindows:1326 submitTemplate = submitTemplate.replace("\n","\r\n")1327 separatorLine = separatorLine.replace("\n","\r\n")1328 newdiff = newdiff.replace("\n","\r\n")1329 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1330 tmpFile.close()13311332if self.prepare_p4_only:1333#1334# Leave the p4 tree prepared, and the submit template around1335# and let the user decide what to do next1336#1337print1338print"P4 workspace prepared for submission."1339print"To submit or revert, go to client workspace"1340print" "+ self.clientPath1341print1342print"To submit, use\"p4 submit\"to write a new description,"1343print"or\"p4 submit -i%s\"to use the one prepared by" \1344"\"git p4\"."% fileName1345print"You can delete the file\"%s\"when finished."% fileName13461347if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1348print"To preserve change ownership by user%s, you must\n" \1349"do\"p4 change -f <change>\"after submitting and\n" \1350"edit the User field."1351if pureRenameCopy:1352print"After submitting, renamed files must be re-synced."1353print"Invoke\"p4 sync -f\"on each of these files:"1354for f in pureRenameCopy:1355print" "+ f13561357print1358print"To revert the changes, use\"p4 revert ...\", and delete"1359print"the submit template file\"%s\""% fileName1360if filesToAdd:1361print"Since the commit adds new files, they must be deleted:"1362for f in filesToAdd:1363print" "+ f1364print1365return True13661367#1368# Let the user edit the change description, then submit it.1369#1370if self.edit_template(fileName):1371# read the edited message and submit1372 ret =True1373 tmpFile =open(fileName,"rb")1374 message = tmpFile.read()1375 tmpFile.close()1376 submitTemplate = message[:message.index(separatorLine)]1377if self.isWindows:1378 submitTemplate = submitTemplate.replace("\r\n","\n")1379p4_write_pipe(['submit','-i'], submitTemplate)13801381if self.preserveUser:1382if p4User:1383# Get last changelist number. Cannot easily get it from1384# the submit command output as the output is1385# unmarshalled.1386 changelist = self.lastP4Changelist()1387 self.modifyChangelistUser(changelist, p4User)13881389# The rename/copy happened by applying a patch that created a1390# new file. This leaves it writable, which confuses p4.1391for f in pureRenameCopy:1392p4_sync(f,"-f")13931394else:1395# skip this patch1396 ret =False1397print"Submission cancelled, undoing p4 changes."1398for f in editedFiles:1399p4_revert(f)1400for f in filesToAdd:1401p4_revert(f)1402 os.remove(f)1403for f in filesToDelete:1404p4_revert(f)14051406 os.remove(fileName)1407return ret14081409# Export git tags as p4 labels. Create a p4 label and then tag1410# with that.1411defexportGitTags(self, gitTags):1412 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1413iflen(validLabelRegexp) ==0:1414 validLabelRegexp = defaultLabelRegexp1415 m = re.compile(validLabelRegexp)14161417for name in gitTags:14181419if not m.match(name):1420if verbose:1421print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1422continue14231424# Get the p4 commit this corresponds to1425 logMessage =extractLogMessageFromGitCommit(name)1426 values =extractSettingsGitLog(logMessage)14271428if not values.has_key('change'):1429# a tag pointing to something not sent to p4; ignore1430if verbose:1431print"git tag%sdoes not give a p4 commit"% name1432continue1433else:1434 changelist = values['change']14351436# Get the tag details.1437 inHeader =True1438 isAnnotated =False1439 body = []1440for l inread_pipe_lines(["git","cat-file","-p", name]):1441 l = l.strip()1442if inHeader:1443if re.match(r'tag\s+', l):1444 isAnnotated =True1445elif re.match(r'\s*$', l):1446 inHeader =False1447continue1448else:1449 body.append(l)14501451if not isAnnotated:1452 body = ["lightweight tag imported by git p4\n"]14531454# Create the label - use the same view as the client spec we are using1455 clientSpec =getClientSpec()14561457 labelTemplate ="Label:%s\n"% name1458 labelTemplate +="Description:\n"1459for b in body:1460 labelTemplate +="\t"+ b +"\n"1461 labelTemplate +="View:\n"1462for mapping in clientSpec.mappings:1463 labelTemplate +="\t%s\n"% mapping.depot_side.path14641465if self.dry_run:1466print"Would create p4 label%sfor tag"% name1467elif self.prepare_p4_only:1468print"Not creating p4 label%sfor tag due to option" \1469" --prepare-p4-only"% name1470else:1471p4_write_pipe(["label","-i"], labelTemplate)14721473# Use the label1474p4_system(["tag","-l", name] +1475["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])14761477if verbose:1478print"created p4 label for tag%s"% name14791480defrun(self, args):1481iflen(args) ==0:1482 self.master =currentGitBranch()1483iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1484die("Detecting current git branch failed!")1485eliflen(args) ==1:1486 self.master = args[0]1487if notbranchExists(self.master):1488die("Branch%sdoes not exist"% self.master)1489else:1490return False14911492 allowSubmit =gitConfig("git-p4.allowSubmit")1493iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1494die("%sis not in git-p4.allowSubmit"% self.master)14951496[upstream, settings] =findUpstreamBranchPoint()1497 self.depotPath = settings['depot-paths'][0]1498iflen(self.origin) ==0:1499 self.origin = upstream15001501if self.preserveUser:1502if not self.canChangeChangelists():1503die("Cannot preserve user names without p4 super-user or admin permissions")15041505# if not set from the command line, try the config file1506if self.conflict_behavior is None:1507 val =gitConfig("git-p4.conflict")1508if val:1509if val not in self.conflict_behavior_choices:1510die("Invalid value '%s' for config git-p4.conflict"% val)1511else:1512 val ="ask"1513 self.conflict_behavior = val15141515if self.verbose:1516print"Origin branch is "+ self.origin15171518iflen(self.depotPath) ==0:1519print"Internal error: cannot locate perforce depot path from existing branches"1520 sys.exit(128)15211522 self.useClientSpec =False1523ifgitConfig("git-p4.useclientspec","--bool") =="true":1524 self.useClientSpec =True1525if self.useClientSpec:1526 self.clientSpecDirs =getClientSpec()15271528if self.useClientSpec:1529# all files are relative to the client spec1530 self.clientPath =getClientRoot()1531else:1532 self.clientPath =p4Where(self.depotPath)15331534if self.clientPath =="":1535die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15361537print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1538 self.oldWorkingDirectory = os.getcwd()15391540# ensure the clientPath exists1541 new_client_dir =False1542if not os.path.exists(self.clientPath):1543 new_client_dir =True1544 os.makedirs(self.clientPath)15451546chdir(self.clientPath)1547if self.dry_run:1548print"Would synchronize p4 checkout in%s"% self.clientPath1549else:1550print"Synchronizing p4 checkout..."1551if new_client_dir:1552# old one was destroyed, and maybe nobody told p41553p4_sync("...","-f")1554else:1555p4_sync("...")1556 self.check()15571558 commits = []1559for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1560 commits.append(line.strip())1561 commits.reverse()15621563if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1564 self.checkAuthorship =False1565else:1566 self.checkAuthorship =True15671568if self.preserveUser:1569 self.checkValidP4Users(commits)15701571#1572# Build up a set of options to be passed to diff when1573# submitting each commit to p4.1574#1575if self.detectRenames:1576# command-line -M arg1577 self.diffOpts ="-M"1578else:1579# If not explicitly set check the config variable1580 detectRenames =gitConfig("git-p4.detectRenames")15811582if detectRenames.lower() =="false"or detectRenames =="":1583 self.diffOpts =""1584elif detectRenames.lower() =="true":1585 self.diffOpts ="-M"1586else:1587 self.diffOpts ="-M%s"% detectRenames15881589# no command-line arg for -C or --find-copies-harder, just1590# config variables1591 detectCopies =gitConfig("git-p4.detectCopies")1592if detectCopies.lower() =="false"or detectCopies =="":1593pass1594elif detectCopies.lower() =="true":1595 self.diffOpts +=" -C"1596else:1597 self.diffOpts +=" -C%s"% detectCopies15981599ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1600 self.diffOpts +=" --find-copies-harder"16011602#1603# Apply the commits, one at a time. On failure, ask if should1604# continue to try the rest of the patches, or quit.1605#1606if self.dry_run:1607print"Would apply"1608 applied = []1609 last =len(commits) -11610for i, commit inenumerate(commits):1611if self.dry_run:1612print" ",read_pipe(["git","show","-s",1613"--format=format:%h%s", commit])1614 ok =True1615else:1616 ok = self.applyCommit(commit)1617if ok:1618 applied.append(commit)1619else:1620if self.prepare_p4_only and i < last:1621print"Processing only the first commit due to option" \1622" --prepare-p4-only"1623break1624if i < last:1625 quit =False1626while True:1627# prompt for what to do, or use the option/variable1628if self.conflict_behavior =="ask":1629print"What do you want to do?"1630 response =raw_input("[s]kip this commit but apply"1631" the rest, or [q]uit? ")1632if not response:1633continue1634elif self.conflict_behavior =="skip":1635 response ="s"1636elif self.conflict_behavior =="quit":1637 response ="q"1638else:1639die("Unknown conflict_behavior '%s'"%1640 self.conflict_behavior)16411642if response[0] =="s":1643print"Skipping this commit, but applying the rest"1644break1645if response[0] =="q":1646print"Quitting"1647 quit =True1648break1649if quit:1650break16511652chdir(self.oldWorkingDirectory)16531654if self.dry_run:1655pass1656elif self.prepare_p4_only:1657pass1658eliflen(commits) ==len(applied):1659print"All commits applied!"16601661 sync =P4Sync()1662 sync.run([])16631664 rebase =P4Rebase()1665 rebase.rebase()16661667else:1668iflen(applied) ==0:1669print"No commits applied."1670else:1671print"Applied only the commits marked with '*':"1672for c in commits:1673if c in applied:1674 star ="*"1675else:1676 star =" "1677print star,read_pipe(["git","show","-s",1678"--format=format:%h%s", c])1679print"You will have to do 'git p4 sync' and rebase."16801681ifgitConfig("git-p4.exportLabels","--bool") =="true":1682 self.exportLabels =True16831684if self.exportLabels:1685 p4Labels =getP4Labels(self.depotPath)1686 gitTags =getGitTags()16871688 missingGitTags = gitTags - p4Labels1689 self.exportGitTags(missingGitTags)16901691# exit with error unless everything applied perfecly1692iflen(commits) !=len(applied):1693 sys.exit(1)16941695return True16961697classView(object):1698"""Represent a p4 view ("p4 help views"), and map files in a1699 repo according to the view."""17001701classPath(object):1702"""A depot or client path, possibly containing wildcards.1703 The only one supported is ... at the end, currently.1704 Initialize with the full path, with //depot or //client."""17051706def__init__(self, path, is_depot):1707 self.path = path1708 self.is_depot = is_depot1709 self.find_wildcards()1710# remember the prefix bit, useful for relative mappings1711 m = re.match("(//[^/]+/)", self.path)1712if not m:1713die("Path%sdoes not start with //prefix/"% self.path)1714 prefix = m.group(1)1715if not self.is_depot:1716# strip //client/ on client paths1717 self.path = self.path[len(prefix):]17181719deffind_wildcards(self):1720"""Make sure wildcards are valid, and set up internal1721 variables."""17221723 self.ends_triple_dot =False1724# There are three wildcards allowed in p4 views1725# (see "p4 help views"). This code knows how to1726# handle "..." (only at the end), but cannot deal with1727# "%%n" or "*". Only check the depot_side, as p4 should1728# validate that the client_side matches too.1729if re.search(r'%%[1-9]', self.path):1730die("Can't handle%%n wildcards in view:%s"% self.path)1731if self.path.find("*") >=0:1732die("Can't handle * wildcards in view:%s"% self.path)1733 triple_dot_index = self.path.find("...")1734if triple_dot_index >=0:1735if triple_dot_index !=len(self.path) -3:1736die("Can handle only single ... wildcard, at end:%s"%1737 self.path)1738 self.ends_triple_dot =True17391740defensure_compatible(self, other_path):1741"""Make sure the wildcards agree."""1742if self.ends_triple_dot != other_path.ends_triple_dot:1743die("Both paths must end with ... if either does;\n"+1744"paths:%s %s"% (self.path, other_path.path))17451746defmatch_wildcards(self, test_path):1747"""See if this test_path matches us, and fill in the value1748 of the wildcards if so. Returns a tuple of1749 (True|False, wildcards[]). For now, only the ... at end1750 is supported, so at most one wildcard."""1751if self.ends_triple_dot:1752 dotless = self.path[:-3]1753if test_path.startswith(dotless):1754 wildcard = test_path[len(dotless):]1755return(True, [ wildcard ])1756else:1757if test_path == self.path:1758return(True, [])1759return(False, [])17601761defmatch(self, test_path):1762"""Just return if it matches; don't bother with the wildcards."""1763 b, _ = self.match_wildcards(test_path)1764return b17651766deffill_in_wildcards(self, wildcards):1767"""Return the relative path, with the wildcards filled in1768 if there are any."""1769if self.ends_triple_dot:1770return self.path[:-3] + wildcards[0]1771else:1772return self.path17731774classMapping(object):1775def__init__(self, depot_side, client_side, overlay, exclude):1776# depot_side is without the trailing /... if it had one1777 self.depot_side = View.Path(depot_side, is_depot=True)1778 self.client_side = View.Path(client_side, is_depot=False)1779 self.overlay = overlay # started with "+"1780 self.exclude = exclude # started with "-"1781assert not(self.overlay and self.exclude)1782 self.depot_side.ensure_compatible(self.client_side)17831784def__str__(self):1785 c =" "1786if self.overlay:1787 c ="+"1788if self.exclude:1789 c ="-"1790return"View.Mapping:%s%s->%s"% \1791(c, self.depot_side.path, self.client_side.path)17921793defmap_depot_to_client(self, depot_path):1794"""Calculate the client path if using this mapping on the1795 given depot path; does not consider the effect of other1796 mappings in a view. Even excluded mappings are returned."""1797 matches, wildcards = self.depot_side.match_wildcards(depot_path)1798if not matches:1799return""1800 client_path = self.client_side.fill_in_wildcards(wildcards)1801return client_path18021803#1804# View methods1805#1806def__init__(self):1807 self.mappings = []18081809defappend(self, view_line):1810"""Parse a view line, splitting it into depot and client1811 sides. Append to self.mappings, preserving order."""18121813# Split the view line into exactly two words. P4 enforces1814# structure on these lines that simplifies this quite a bit.1815#1816# Either or both words may be double-quoted.1817# Single quotes do not matter.1818# Double-quote marks cannot occur inside the words.1819# A + or - prefix is also inside the quotes.1820# There are no quotes unless they contain a space.1821# The line is already white-space stripped.1822# The two words are separated by a single space.1823#1824if view_line[0] =='"':1825# First word is double quoted. Find its end.1826 close_quote_index = view_line.find('"',1)1827if close_quote_index <=0:1828die("No first-word closing quote found:%s"% view_line)1829 depot_side = view_line[1:close_quote_index]1830# skip closing quote and space1831 rhs_index = close_quote_index +1+11832else:1833 space_index = view_line.find(" ")1834if space_index <=0:1835die("No word-splitting space found:%s"% view_line)1836 depot_side = view_line[0:space_index]1837 rhs_index = space_index +118381839if view_line[rhs_index] =='"':1840# Second word is double quoted. Make sure there is a1841# double quote at the end too.1842if not view_line.endswith('"'):1843die("View line with rhs quote should end with one:%s"%1844 view_line)1845# skip the quotes1846 client_side = view_line[rhs_index+1:-1]1847else:1848 client_side = view_line[rhs_index:]18491850# prefix + means overlay on previous mapping1851 overlay =False1852if depot_side.startswith("+"):1853 overlay =True1854 depot_side = depot_side[1:]18551856# prefix - means exclude this path1857 exclude =False1858if depot_side.startswith("-"):1859 exclude =True1860 depot_side = depot_side[1:]18611862 m = View.Mapping(depot_side, client_side, overlay, exclude)1863 self.mappings.append(m)18641865defmap_in_client(self, depot_path):1866"""Return the relative location in the client where this1867 depot file should live. Returns "" if the file should1868 not be mapped in the client."""18691870 paths_filled = []1871 client_path =""18721873# look at later entries first1874for m in self.mappings[::-1]:18751876# see where will this path end up in the client1877 p = m.map_depot_to_client(depot_path)18781879if p =="":1880# Depot path does not belong in client. Must remember1881# this, as previous items should not cause files to1882# exist in this path either. Remember that the list is1883# being walked from the end, which has higher precedence.1884# Overlap mappings do not exclude previous mappings.1885if not m.overlay:1886 paths_filled.append(m.client_side)18871888else:1889# This mapping matched; no need to search any further.1890# But, the mapping could be rejected if the client path1891# has already been claimed by an earlier mapping (i.e.1892# one later in the list, which we are walking backwards).1893 already_mapped_in_client =False1894for f in paths_filled:1895# this is View.Path.match1896if f.match(p):1897 already_mapped_in_client =True1898break1899if not already_mapped_in_client:1900# Include this file, unless it is from a line that1901# explicitly said to exclude it.1902if not m.exclude:1903 client_path = p19041905# a match, even if rejected, always stops the search1906break19071908return client_path19091910classP4Sync(Command, P4UserMap):1911 delete_actions = ("delete","move/delete","purge")19121913def__init__(self):1914 Command.__init__(self)1915 P4UserMap.__init__(self)1916 self.options = [1917 optparse.make_option("--branch", dest="branch"),1918 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1919 optparse.make_option("--changesfile", dest="changesFile"),1920 optparse.make_option("--silent", dest="silent", action="store_true"),1921 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1922 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1923 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1924help="Import into refs/heads/ , not refs/remotes"),1925 optparse.make_option("--max-changes", dest="maxChanges"),1926 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1927help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1928 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1929help="Only sync files that are included in the Perforce Client Spec")1930]1931 self.description ="""Imports from Perforce into a git repository.\n1932 example:1933 //depot/my/project/ -- to import the current head1934 //depot/my/project/@all -- to import everything1935 //depot/my/project/@1,6 -- to import only from revision 1 to 619361937 (a ... is not needed in the path p4 specification, it's added implicitly)"""19381939 self.usage +=" //depot/path[@revRange]"1940 self.silent =False1941 self.createdBranches =set()1942 self.committedChanges =set()1943 self.branch =""1944 self.detectBranches =False1945 self.detectLabels =False1946 self.importLabels =False1947 self.changesFile =""1948 self.syncWithOrigin =True1949 self.importIntoRemotes =True1950 self.maxChanges =""1951 self.isWindows = (platform.system() =="Windows")1952 self.keepRepoPath =False1953 self.depotPaths =None1954 self.p4BranchesInGit = []1955 self.cloneExclude = []1956 self.useClientSpec =False1957 self.useClientSpec_from_options =False1958 self.clientSpecDirs =None1959 self.tempBranches = []1960 self.tempBranchLocation ="git-p4-tmp"19611962ifgitConfig("git-p4.syncFromOrigin") =="false":1963 self.syncWithOrigin =False19641965# Force a checkpoint in fast-import and wait for it to finish1966defcheckpoint(self):1967 self.gitStream.write("checkpoint\n\n")1968 self.gitStream.write("progress checkpoint\n\n")1969 out = self.gitOutput.readline()1970if self.verbose:1971print"checkpoint finished: "+ out19721973defextractFilesFromCommit(self, commit):1974 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1975for path in self.cloneExclude]1976 files = []1977 fnum =01978while commit.has_key("depotFile%s"% fnum):1979 path = commit["depotFile%s"% fnum]19801981if[p for p in self.cloneExclude1982ifp4PathStartsWith(path, p)]:1983 found =False1984else:1985 found = [p for p in self.depotPaths1986ifp4PathStartsWith(path, p)]1987if not found:1988 fnum = fnum +11989continue19901991file= {}1992file["path"] = path1993file["rev"] = commit["rev%s"% fnum]1994file["action"] = commit["action%s"% fnum]1995file["type"] = commit["type%s"% fnum]1996 files.append(file)1997 fnum = fnum +11998return files19992000defstripRepoPath(self, path, prefixes):2001"""When streaming files, this is called to map a p4 depot path2002 to where it should go in git. The prefixes are either2003 self.depotPaths, or self.branchPrefixes in the case of2004 branch detection."""20052006if self.useClientSpec:2007# branch detection moves files up a level (the branch name)2008# from what client spec interpretation gives2009 path = self.clientSpecDirs.map_in_client(path)2010if self.detectBranches:2011for b in self.knownBranches:2012if path.startswith(b +"/"):2013 path = path[len(b)+1:]20142015elif self.keepRepoPath:2016# Preserve everything in relative path name except leading2017# //depot/; just look at first prefix as they all should2018# be in the same depot.2019 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2020ifp4PathStartsWith(path, depot):2021 path = path[len(depot):]20222023else:2024for p in prefixes:2025ifp4PathStartsWith(path, p):2026 path = path[len(p):]2027break20282029 path =wildcard_decode(path)2030return path20312032defsplitFilesIntoBranches(self, commit):2033"""Look at each depotFile in the commit to figure out to what2034 branch it belongs."""20352036 branches = {}2037 fnum =02038while commit.has_key("depotFile%s"% fnum):2039 path = commit["depotFile%s"% fnum]2040 found = [p for p in self.depotPaths2041ifp4PathStartsWith(path, p)]2042if not found:2043 fnum = fnum +12044continue20452046file= {}2047file["path"] = path2048file["rev"] = commit["rev%s"% fnum]2049file["action"] = commit["action%s"% fnum]2050file["type"] = commit["type%s"% fnum]2051 fnum = fnum +120522053# start with the full relative path where this file would2054# go in a p4 client2055if self.useClientSpec:2056 relPath = self.clientSpecDirs.map_in_client(path)2057else:2058 relPath = self.stripRepoPath(path, self.depotPaths)20592060for branch in self.knownBranches.keys():2061# add a trailing slash so that a commit into qt/4.2foo2062# doesn't end up in qt/4.2, e.g.2063if relPath.startswith(branch +"/"):2064if branch not in branches:2065 branches[branch] = []2066 branches[branch].append(file)2067break20682069return branches20702071# output one file from the P4 stream2072# - helper for streamP4Files20732074defstreamOneP4File(self,file, contents):2075 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2076if verbose:2077 sys.stderr.write("%s\n"% relPath)20782079(type_base, type_mods) =split_p4_type(file["type"])20802081 git_mode ="100644"2082if"x"in type_mods:2083 git_mode ="100755"2084if type_base =="symlink":2085 git_mode ="120000"2086# p4 print on a symlink contains "target\n"; remove the newline2087 data =''.join(contents)2088 contents = [data[:-1]]20892090if type_base =="utf16":2091# p4 delivers different text in the python output to -G2092# than it does when using "print -o", or normal p4 client2093# operations. utf16 is converted to ascii or utf8, perhaps.2094# But ascii text saved as -t utf16 is completely mangled.2095# Invoke print -o to get the real contents.2096 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2097 contents = [ text ]20982099if type_base =="apple":2100# Apple filetype files will be streamed as a concatenation of2101# its appledouble header and the contents. This is useless2102# on both macs and non-macs. If using "print -q -o xx", it2103# will create "xx" with the data, and "%xx" with the header.2104# This is also not very useful.2105#2106# Ideally, someday, this script can learn how to generate2107# appledouble files directly and import those to git, but2108# non-mac machines can never find a use for apple filetype.2109print"\nIgnoring apple filetype file%s"%file['depotFile']2110return21112112# Perhaps windows wants unicode, utf16 newlines translated too;2113# but this is not doing it.2114if self.isWindows and type_base =="text":2115 mangled = []2116for data in contents:2117 data = data.replace("\r\n","\n")2118 mangled.append(data)2119 contents = mangled21202121# Note that we do not try to de-mangle keywords on utf16 files,2122# even though in theory somebody may want that.2123 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2124if pattern:2125 regexp = re.compile(pattern, re.VERBOSE)2126 text =''.join(contents)2127 text = regexp.sub(r'$\1$', text)2128 contents = [ text ]21292130 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21312132# total length...2133 length =02134for d in contents:2135 length = length +len(d)21362137 self.gitStream.write("data%d\n"% length)2138for d in contents:2139 self.gitStream.write(d)2140 self.gitStream.write("\n")21412142defstreamOneP4Deletion(self,file):2143 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2144if verbose:2145 sys.stderr.write("delete%s\n"% relPath)2146 self.gitStream.write("D%s\n"% relPath)21472148# handle another chunk of streaming data2149defstreamP4FilesCb(self, marshalled):21502151# catch p4 errors and complain2152 err =None2153if"code"in marshalled:2154if marshalled["code"] =="error":2155if"data"in marshalled:2156 err = marshalled["data"].rstrip()2157if err:2158 f =None2159if self.stream_have_file_info:2160if"depotFile"in self.stream_file:2161 f = self.stream_file["depotFile"]2162# force a failure in fast-import, else an empty2163# commit will be made2164 self.gitStream.write("\n")2165 self.gitStream.write("die-now\n")2166 self.gitStream.close()2167# ignore errors, but make sure it exits first2168 self.importProcess.wait()2169if f:2170die("Error from p4 print for%s:%s"% (f, err))2171else:2172die("Error from p4 print:%s"% err)21732174if marshalled.has_key('depotFile')and self.stream_have_file_info:2175# start of a new file - output the old one first2176 self.streamOneP4File(self.stream_file, self.stream_contents)2177 self.stream_file = {}2178 self.stream_contents = []2179 self.stream_have_file_info =False21802181# pick up the new file information... for the2182# 'data' field we need to append to our array2183for k in marshalled.keys():2184if k =='data':2185 self.stream_contents.append(marshalled['data'])2186else:2187 self.stream_file[k] = marshalled[k]21882189 self.stream_have_file_info =True21902191# Stream directly from "p4 files" into "git fast-import"2192defstreamP4Files(self, files):2193 filesForCommit = []2194 filesToRead = []2195 filesToDelete = []21962197for f in files:2198# if using a client spec, only add the files that have2199# a path in the client2200if self.clientSpecDirs:2201if self.clientSpecDirs.map_in_client(f['path']) =="":2202continue22032204 filesForCommit.append(f)2205if f['action']in self.delete_actions:2206 filesToDelete.append(f)2207else:2208 filesToRead.append(f)22092210# deleted files...2211for f in filesToDelete:2212 self.streamOneP4Deletion(f)22132214iflen(filesToRead) >0:2215 self.stream_file = {}2216 self.stream_contents = []2217 self.stream_have_file_info =False22182219# curry self argument2220defstreamP4FilesCbSelf(entry):2221 self.streamP4FilesCb(entry)22222223 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22242225p4CmdList(["-x","-","print"],2226 stdin=fileArgs,2227 cb=streamP4FilesCbSelf)22282229# do the last chunk2230if self.stream_file.has_key('depotFile'):2231 self.streamOneP4File(self.stream_file, self.stream_contents)22322233defmake_email(self, userid):2234if userid in self.users:2235return self.users[userid]2236else:2237return"%s<a@b>"% userid22382239# Stream a p4 tag2240defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2241if verbose:2242print"writing tag%sfor commit%s"% (labelName, commit)2243 gitStream.write("tag%s\n"% labelName)2244 gitStream.write("from%s\n"% commit)22452246if labelDetails.has_key('Owner'):2247 owner = labelDetails["Owner"]2248else:2249 owner =None22502251# Try to use the owner of the p4 label, or failing that,2252# the current p4 user id.2253if owner:2254 email = self.make_email(owner)2255else:2256 email = self.make_email(self.p4UserId())2257 tagger ="%s %s %s"% (email, epoch, self.tz)22582259 gitStream.write("tagger%s\n"% tagger)22602261print"labelDetails=",labelDetails2262if labelDetails.has_key('Description'):2263 description = labelDetails['Description']2264else:2265 description ='Label from git p4'22662267 gitStream.write("data%d\n"%len(description))2268 gitStream.write(description)2269 gitStream.write("\n")22702271defcommit(self, details, files, branch, parent =""):2272 epoch = details["time"]2273 author = details["user"]22742275if self.verbose:2276print"commit into%s"% branch22772278# start with reading files; if that fails, we should not2279# create a commit.2280 new_files = []2281for f in files:2282if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2283 new_files.append(f)2284else:2285 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])22862287 self.gitStream.write("commit%s\n"% branch)2288# gitStream.write("mark :%s\n" % details["change"])2289 self.committedChanges.add(int(details["change"]))2290 committer =""2291if author not in self.users:2292 self.getUserMapFromPerforceServer()2293 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)22942295 self.gitStream.write("committer%s\n"% committer)22962297 self.gitStream.write("data <<EOT\n")2298 self.gitStream.write(details["desc"])2299 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2300(','.join(self.branchPrefixes), details["change"]))2301iflen(details['options']) >0:2302 self.gitStream.write(": options =%s"% details['options'])2303 self.gitStream.write("]\nEOT\n\n")23042305iflen(parent) >0:2306if self.verbose:2307print"parent%s"% parent2308 self.gitStream.write("from%s\n"% parent)23092310 self.streamP4Files(new_files)2311 self.gitStream.write("\n")23122313 change =int(details["change"])23142315if self.labels.has_key(change):2316 label = self.labels[change]2317 labelDetails = label[0]2318 labelRevisions = label[1]2319if self.verbose:2320print"Change%sis labelled%s"% (change, labelDetails)23212322 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2323for p in self.branchPrefixes])23242325iflen(files) ==len(labelRevisions):23262327 cleanedFiles = {}2328for info in files:2329if info["action"]in self.delete_actions:2330continue2331 cleanedFiles[info["depotFile"]] = info["rev"]23322333if cleanedFiles == labelRevisions:2334 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23352336else:2337if not self.silent:2338print("Tag%sdoes not match with change%s: files do not match."2339% (labelDetails["label"], change))23402341else:2342if not self.silent:2343print("Tag%sdoes not match with change%s: file count is different."2344% (labelDetails["label"], change))23452346# Build a dictionary of changelists and labels, for "detect-labels" option.2347defgetLabels(self):2348 self.labels = {}23492350 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2351iflen(l) >0and not self.silent:2352print"Finding files belonging to labels in%s"% `self.depotPaths`23532354for output in l:2355 label = output["label"]2356 revisions = {}2357 newestChange =02358if self.verbose:2359print"Querying files for label%s"% label2360forfileinp4CmdList(["files"] +2361["%s...@%s"% (p, label)2362for p in self.depotPaths]):2363 revisions[file["depotFile"]] =file["rev"]2364 change =int(file["change"])2365if change > newestChange:2366 newestChange = change23672368 self.labels[newestChange] = [output, revisions]23692370if self.verbose:2371print"Label changes:%s"% self.labels.keys()23722373# Import p4 labels as git tags. A direct mapping does not2374# exist, so assume that if all the files are at the same revision2375# then we can use that, or it's something more complicated we should2376# just ignore.2377defimportP4Labels(self, stream, p4Labels):2378if verbose:2379print"import p4 labels: "+' '.join(p4Labels)23802381 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2382 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2383iflen(validLabelRegexp) ==0:2384 validLabelRegexp = defaultLabelRegexp2385 m = re.compile(validLabelRegexp)23862387for name in p4Labels:2388 commitFound =False23892390if not m.match(name):2391if verbose:2392print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2393continue23942395if name in ignoredP4Labels:2396continue23972398 labelDetails =p4CmdList(['label',"-o", name])[0]23992400# get the most recent changelist for each file in this label2401 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2402for p in self.depotPaths])24032404if change.has_key('change'):2405# find the corresponding git commit; take the oldest commit2406 changelist =int(change['change'])2407 gitCommit =read_pipe(["git","rev-list","--max-count=1",2408"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2409iflen(gitCommit) ==0:2410print"could not find git commit for changelist%d"% changelist2411else:2412 gitCommit = gitCommit.strip()2413 commitFound =True2414# Convert from p4 time format2415try:2416 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2417exceptValueError:2418print"Could not convert label time%s"% labelDetails['Update']2419 tmwhen =124202421 when =int(time.mktime(tmwhen))2422 self.streamTag(stream, name, labelDetails, gitCommit, when)2423if verbose:2424print"p4 label%smapped to git commit%s"% (name, gitCommit)2425else:2426if verbose:2427print"Label%shas no changelists - possibly deleted?"% name24282429if not commitFound:2430# We can't import this label; don't try again as it will get very2431# expensive repeatedly fetching all the files for labels that will2432# never be imported. If the label is moved in the future, the2433# ignore will need to be removed manually.2434system(["git","config","--add","git-p4.ignoredP4Labels", name])24352436defguessProjectName(self):2437for p in self.depotPaths:2438if p.endswith("/"):2439 p = p[:-1]2440 p = p[p.strip().rfind("/") +1:]2441if not p.endswith("/"):2442 p +="/"2443return p24442445defgetBranchMapping(self):2446 lostAndFoundBranches =set()24472448 user =gitConfig("git-p4.branchUser")2449iflen(user) >0:2450 command ="branches -u%s"% user2451else:2452 command ="branches"24532454for info inp4CmdList(command):2455 details =p4Cmd(["branch","-o", info["branch"]])2456 viewIdx =02457while details.has_key("View%s"% viewIdx):2458 paths = details["View%s"% viewIdx].split(" ")2459 viewIdx = viewIdx +12460# require standard //depot/foo/... //depot/bar/... mapping2461iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2462continue2463 source = paths[0]2464 destination = paths[1]2465## HACK2466ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2467 source = source[len(self.depotPaths[0]):-4]2468 destination = destination[len(self.depotPaths[0]):-4]24692470if destination in self.knownBranches:2471if not self.silent:2472print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2473print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2474continue24752476 self.knownBranches[destination] = source24772478 lostAndFoundBranches.discard(destination)24792480if source not in self.knownBranches:2481 lostAndFoundBranches.add(source)24822483# Perforce does not strictly require branches to be defined, so we also2484# check git config for a branch list.2485#2486# Example of branch definition in git config file:2487# [git-p4]2488# branchList=main:branchA2489# branchList=main:branchB2490# branchList=branchA:branchC2491 configBranches =gitConfigList("git-p4.branchList")2492for branch in configBranches:2493if branch:2494(source, destination) = branch.split(":")2495 self.knownBranches[destination] = source24962497 lostAndFoundBranches.discard(destination)24982499if source not in self.knownBranches:2500 lostAndFoundBranches.add(source)250125022503for branch in lostAndFoundBranches:2504 self.knownBranches[branch] = branch25052506defgetBranchMappingFromGitBranches(self):2507 branches =p4BranchesInGit(self.importIntoRemotes)2508for branch in branches.keys():2509if branch =="master":2510 branch ="main"2511else:2512 branch = branch[len(self.projectName):]2513 self.knownBranches[branch] = branch25142515defupdateOptionDict(self, d):2516 option_keys = {}2517if self.keepRepoPath:2518 option_keys['keepRepoPath'] =125192520 d["options"] =' '.join(sorted(option_keys.keys()))25212522defreadOptions(self, d):2523 self.keepRepoPath = (d.has_key('options')2524and('keepRepoPath'in d['options']))25252526defgitRefForBranch(self, branch):2527if branch =="main":2528return self.refPrefix +"master"25292530iflen(branch) <=0:2531return branch25322533return self.refPrefix + self.projectName + branch25342535defgitCommitByP4Change(self, ref, change):2536if self.verbose:2537print"looking in ref "+ ref +" for change%susing bisect..."% change25382539 earliestCommit =""2540 latestCommit =parseRevision(ref)25412542while True:2543if self.verbose:2544print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2545 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2546iflen(next) ==0:2547if self.verbose:2548print"argh"2549return""2550 log =extractLogMessageFromGitCommit(next)2551 settings =extractSettingsGitLog(log)2552 currentChange =int(settings['change'])2553if self.verbose:2554print"current change%s"% currentChange25552556if currentChange == change:2557if self.verbose:2558print"found%s"% next2559return next25602561if currentChange < change:2562 earliestCommit ="^%s"% next2563else:2564 latestCommit ="%s"% next25652566return""25672568defimportNewBranch(self, branch, maxChange):2569# make fast-import flush all changes to disk and update the refs using the checkpoint2570# command so that we can try to find the branch parent in the git history2571 self.gitStream.write("checkpoint\n\n");2572 self.gitStream.flush();2573 branchPrefix = self.depotPaths[0] + branch +"/"2574range="@1,%s"% maxChange2575#print "prefix" + branchPrefix2576 changes =p4ChangesForPaths([branchPrefix],range)2577iflen(changes) <=0:2578return False2579 firstChange = changes[0]2580#print "first change in branch: %s" % firstChange2581 sourceBranch = self.knownBranches[branch]2582 sourceDepotPath = self.depotPaths[0] + sourceBranch2583 sourceRef = self.gitRefForBranch(sourceBranch)2584#print "source " + sourceBranch25852586 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2587#print "branch parent: %s" % branchParentChange2588 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2589iflen(gitParent) >0:2590 self.initialParents[self.gitRefForBranch(branch)] = gitParent2591#print "parent git commit: %s" % gitParent25922593 self.importChanges(changes)2594return True25952596defsearchParent(self, parent, branch, target):2597 parentFound =False2598for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2599 blob = blob.strip()2600iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2601 parentFound =True2602if self.verbose:2603print"Found parent of%sin commit%s"% (branch, blob)2604break2605if parentFound:2606return blob2607else:2608return None26092610defimportChanges(self, changes):2611 cnt =12612for change in changes:2613 description =p4_describe(change)2614 self.updateOptionDict(description)26152616if not self.silent:2617 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2618 sys.stdout.flush()2619 cnt = cnt +126202621try:2622if self.detectBranches:2623 branches = self.splitFilesIntoBranches(description)2624for branch in branches.keys():2625## HACK --hwn2626 branchPrefix = self.depotPaths[0] + branch +"/"2627 self.branchPrefixes = [ branchPrefix ]26282629 parent =""26302631 filesForCommit = branches[branch]26322633if self.verbose:2634print"branch is%s"% branch26352636 self.updatedBranches.add(branch)26372638if branch not in self.createdBranches:2639 self.createdBranches.add(branch)2640 parent = self.knownBranches[branch]2641if parent == branch:2642 parent =""2643else:2644 fullBranch = self.projectName + branch2645if fullBranch not in self.p4BranchesInGit:2646if not self.silent:2647print("\nImporting new branch%s"% fullBranch);2648if self.importNewBranch(branch, change -1):2649 parent =""2650 self.p4BranchesInGit.append(fullBranch)2651if not self.silent:2652print("\nResuming with change%s"% change);26532654if self.verbose:2655print"parent determined through known branches:%s"% parent26562657 branch = self.gitRefForBranch(branch)2658 parent = self.gitRefForBranch(parent)26592660if self.verbose:2661print"looking for initial parent for%s; current parent is%s"% (branch, parent)26622663iflen(parent) ==0and branch in self.initialParents:2664 parent = self.initialParents[branch]2665del self.initialParents[branch]26662667 blob =None2668iflen(parent) >0:2669 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2670if self.verbose:2671print"Creating temporary branch: "+ tempBranch2672 self.commit(description, filesForCommit, tempBranch)2673 self.tempBranches.append(tempBranch)2674 self.checkpoint()2675 blob = self.searchParent(parent, branch, tempBranch)2676if blob:2677 self.commit(description, filesForCommit, branch, blob)2678else:2679if self.verbose:2680print"Parent of%snot found. Committing into head of%s"% (branch, parent)2681 self.commit(description, filesForCommit, branch, parent)2682else:2683 files = self.extractFilesFromCommit(description)2684 self.commit(description, files, self.branch,2685 self.initialParent)2686 self.initialParent =""2687exceptIOError:2688print self.gitError.read()2689 sys.exit(1)26902691defimportHeadRevision(self, revision):2692print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)26932694 details = {}2695 details["user"] ="git perforce import user"2696 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2697% (' '.join(self.depotPaths), revision))2698 details["change"] = revision2699 newestRevision =027002701 fileCnt =02702 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27032704for info inp4CmdList(["files"] + fileArgs):27052706if'code'in info and info['code'] =='error':2707 sys.stderr.write("p4 returned an error:%s\n"2708% info['data'])2709if info['data'].find("must refer to client") >=0:2710 sys.stderr.write("This particular p4 error is misleading.\n")2711 sys.stderr.write("Perhaps the depot path was misspelled.\n");2712 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2713 sys.exit(1)2714if'p4ExitCode'in info:2715 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2716 sys.exit(1)271727182719 change =int(info["change"])2720if change > newestRevision:2721 newestRevision = change27222723if info["action"]in self.delete_actions:2724# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2725#fileCnt = fileCnt + 12726continue27272728for prop in["depotFile","rev","action","type"]:2729 details["%s%s"% (prop, fileCnt)] = info[prop]27302731 fileCnt = fileCnt +127322733 details["change"] = newestRevision27342735# Use time from top-most change so that all git p4 clones of2736# the same p4 repo have the same commit SHA1s.2737 res =p4_describe(newestRevision)2738 details["time"] = res["time"]27392740 self.updateOptionDict(details)2741try:2742 self.commit(details, self.extractFilesFromCommit(details), self.branch)2743exceptIOError:2744print"IO error with git fast-import. Is your git version recent enough?"2745print self.gitError.read()274627472748defrun(self, args):2749 self.depotPaths = []2750 self.changeRange =""2751 self.initialParent =""2752 self.previousDepotPaths = []2753 self.hasOrigin =False27542755# map from branch depot path to parent branch2756 self.knownBranches = {}2757 self.initialParents = {}27582759if self.importIntoRemotes:2760 self.refPrefix ="refs/remotes/p4/"2761else:2762 self.refPrefix ="refs/heads/p4/"27632764if self.syncWithOrigin:2765 self.hasOrigin =originP4BranchesExist()2766if self.hasOrigin:2767if not self.silent:2768print'Syncing with origin first, using "git fetch origin"'2769system("git fetch origin")27702771iflen(self.branch) ==0:2772 self.branch = self.refPrefix +"master"2773ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2774system("git update-ref%srefs/heads/p4"% self.branch)2775system("git branch -D p4")27762777# accept either the command-line option, or the configuration variable2778if self.useClientSpec:2779# will use this after clone to set the variable2780 self.useClientSpec_from_options =True2781else:2782ifgitConfig("git-p4.useclientspec","--bool") =="true":2783 self.useClientSpec =True2784if self.useClientSpec:2785 self.clientSpecDirs =getClientSpec()27862787# TODO: should always look at previous commits,2788# merge with previous imports, if possible.2789if args == []:2790if self.hasOrigin:2791createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)27922793# branches holds mapping from branch name to sha12794 branches =p4BranchesInGit(self.importIntoRemotes)2795 self.p4BranchesInGit = branches.keys()2796for branch in branches.keys():2797 self.initialParents[self.refPrefix + branch] = branches[branch]27982799iflen(self.p4BranchesInGit) >1:2800if not self.silent:2801print"Importing from/into multiple branches"2802 self.detectBranches =True28032804if self.verbose:2805print"branches:%s"% self.p4BranchesInGit28062807 p4Change =02808for branch in self.p4BranchesInGit:2809 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28102811 settings =extractSettingsGitLog(logMsg)28122813 self.readOptions(settings)2814if(settings.has_key('depot-paths')2815and settings.has_key('change')):2816 change =int(settings['change']) +12817 p4Change =max(p4Change, change)28182819 depotPaths =sorted(settings['depot-paths'])2820if self.previousDepotPaths == []:2821 self.previousDepotPaths = depotPaths2822else:2823 paths = []2824for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2825 prev_list = prev.split("/")2826 cur_list = cur.split("/")2827for i inrange(0,min(len(cur_list),len(prev_list))):2828if cur_list[i] <> prev_list[i]:2829 i = i -12830break28312832 paths.append("/".join(cur_list[:i +1]))28332834 self.previousDepotPaths = paths28352836if p4Change >0:2837 self.depotPaths =sorted(self.previousDepotPaths)2838 self.changeRange ="@%s,#head"% p4Change2839if not self.detectBranches:2840 self.initialParent =parseRevision(self.branch)2841if not self.silent and not self.detectBranches:2842print"Performing incremental import into%sgit branch"% self.branch28432844if not self.branch.startswith("refs/"):2845 self.branch ="refs/heads/"+ self.branch28462847iflen(args) ==0and self.depotPaths:2848if not self.silent:2849print"Depot paths:%s"%' '.join(self.depotPaths)2850else:2851if self.depotPaths and self.depotPaths != args:2852print("previous import used depot path%sand now%swas specified. "2853"This doesn't work!"% (' '.join(self.depotPaths),2854' '.join(args)))2855 sys.exit(1)28562857 self.depotPaths =sorted(args)28582859 revision =""2860 self.users = {}28612862# Make sure no revision specifiers are used when --changesfile2863# is specified.2864 bad_changesfile =False2865iflen(self.changesFile) >0:2866for p in self.depotPaths:2867if p.find("@") >=0or p.find("#") >=0:2868 bad_changesfile =True2869break2870if bad_changesfile:2871die("Option --changesfile is incompatible with revision specifiers")28722873 newPaths = []2874for p in self.depotPaths:2875if p.find("@") != -1:2876 atIdx = p.index("@")2877 self.changeRange = p[atIdx:]2878if self.changeRange =="@all":2879 self.changeRange =""2880elif','not in self.changeRange:2881 revision = self.changeRange2882 self.changeRange =""2883 p = p[:atIdx]2884elif p.find("#") != -1:2885 hashIdx = p.index("#")2886 revision = p[hashIdx:]2887 p = p[:hashIdx]2888elif self.previousDepotPaths == []:2889# pay attention to changesfile, if given, else import2890# the entire p4 tree at the head revision2891iflen(self.changesFile) ==0:2892 revision ="#head"28932894 p = re.sub("\.\.\.$","", p)2895if not p.endswith("/"):2896 p +="/"28972898 newPaths.append(p)28992900 self.depotPaths = newPaths29012902# --detect-branches may change this for each branch2903 self.branchPrefixes = self.depotPaths29042905 self.loadUserMapFromCache()2906 self.labels = {}2907if self.detectLabels:2908 self.getLabels();29092910if self.detectBranches:2911## FIXME - what's a P4 projectName ?2912 self.projectName = self.guessProjectName()29132914if self.hasOrigin:2915 self.getBranchMappingFromGitBranches()2916else:2917 self.getBranchMapping()2918if self.verbose:2919print"p4-git branches:%s"% self.p4BranchesInGit2920print"initial parents:%s"% self.initialParents2921for b in self.p4BranchesInGit:2922if b !="master":29232924## FIXME2925 b = b[len(self.projectName):]2926 self.createdBranches.add(b)29272928 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29292930 self.importProcess = subprocess.Popen(["git","fast-import"],2931 stdin=subprocess.PIPE,2932 stdout=subprocess.PIPE,2933 stderr=subprocess.PIPE);2934 self.gitOutput = self.importProcess.stdout2935 self.gitStream = self.importProcess.stdin2936 self.gitError = self.importProcess.stderr29372938if revision:2939 self.importHeadRevision(revision)2940else:2941 changes = []29422943iflen(self.changesFile) >0:2944 output =open(self.changesFile).readlines()2945 changeSet =set()2946for line in output:2947 changeSet.add(int(line))29482949for change in changeSet:2950 changes.append(change)29512952 changes.sort()2953else:2954# catch "git p4 sync" with no new branches, in a repo that2955# does not have any existing p4 branches2956iflen(args) ==0and not self.p4BranchesInGit:2957die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2958if self.verbose:2959print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2960 self.changeRange)2961 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)29622963iflen(self.maxChanges) >0:2964 changes = changes[:min(int(self.maxChanges),len(changes))]29652966iflen(changes) ==0:2967if not self.silent:2968print"No changes to import!"2969else:2970if not self.silent and not self.detectBranches:2971print"Import destination:%s"% self.branch29722973 self.updatedBranches =set()29742975 self.importChanges(changes)29762977if not self.silent:2978print""2979iflen(self.updatedBranches) >0:2980 sys.stdout.write("Updated branches: ")2981for b in self.updatedBranches:2982 sys.stdout.write("%s"% b)2983 sys.stdout.write("\n")29842985ifgitConfig("git-p4.importLabels","--bool") =="true":2986 self.importLabels =True29872988if self.importLabels:2989 p4Labels =getP4Labels(self.depotPaths)2990 gitTags =getGitTags()29912992 missingP4Labels = p4Labels - gitTags2993 self.importP4Labels(self.gitStream, missingP4Labels)29942995 self.gitStream.close()2996if self.importProcess.wait() !=0:2997die("fast-import failed:%s"% self.gitError.read())2998 self.gitOutput.close()2999 self.gitError.close()30003001# Cleanup temporary branches created during import3002if self.tempBranches != []:3003for branch in self.tempBranches:3004read_pipe("git update-ref -d%s"% branch)3005 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30063007# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3008# a convenient shortcut refname "p4".3009if self.importIntoRemotes:3010 head_ref = self.refPrefix +"HEAD"3011if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3012system(["git","symbolic-ref", head_ref, self.branch])30133014return True30153016classP4Rebase(Command):3017def__init__(self):3018 Command.__init__(self)3019 self.options = [3020 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3021]3022 self.importLabels =False3023 self.description = ("Fetches the latest revision from perforce and "3024+"rebases the current work (branch) against it")30253026defrun(self, args):3027 sync =P4Sync()3028 sync.importLabels = self.importLabels3029 sync.run([])30303031return self.rebase()30323033defrebase(self):3034if os.system("git update-index --refresh") !=0:3035die("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.");3036iflen(read_pipe("git diff-index HEAD --")) >0:3037die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");30383039[upstream, settings] =findUpstreamBranchPoint()3040iflen(upstream) ==0:3041die("Cannot find upstream branchpoint for rebase")30423043# the branchpoint may be p4/foo~3, so strip off the parent3044 upstream = re.sub("~[0-9]+$","", upstream)30453046print"Rebasing the current branch onto%s"% upstream3047 oldHead =read_pipe("git rev-parse HEAD").strip()3048system("git rebase%s"% upstream)3049system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3050return True30513052classP4Clone(P4Sync):3053def__init__(self):3054 P4Sync.__init__(self)3055 self.description ="Creates a new git repository and imports from Perforce into it"3056 self.usage ="usage: %prog [options] //depot/path[@revRange]"3057 self.options += [3058 optparse.make_option("--destination", dest="cloneDestination",3059 action='store', default=None,3060help="where to leave result of the clone"),3061 optparse.make_option("-/", dest="cloneExclude",3062 action="append",type="string",3063help="exclude depot path"),3064 optparse.make_option("--bare", dest="cloneBare",3065 action="store_true", default=False),3066]3067 self.cloneDestination =None3068 self.needsGit =False3069 self.cloneBare =False30703071# This is required for the "append" cloneExclude action3072defensure_value(self, attr, value):3073if nothasattr(self, attr)orgetattr(self, attr)is None:3074setattr(self, attr, value)3075returngetattr(self, attr)30763077defdefaultDestination(self, args):3078## TODO: use common prefix of args?3079 depotPath = args[0]3080 depotDir = re.sub("(@[^@]*)$","", depotPath)3081 depotDir = re.sub("(#[^#]*)$","", depotDir)3082 depotDir = re.sub(r"\.\.\.$","", depotDir)3083 depotDir = re.sub(r"/$","", depotDir)3084return os.path.split(depotDir)[1]30853086defrun(self, args):3087iflen(args) <1:3088return False30893090if self.keepRepoPath and not self.cloneDestination:3091 sys.stderr.write("Must specify destination for --keep-path\n")3092 sys.exit(1)30933094 depotPaths = args30953096if not self.cloneDestination andlen(depotPaths) >1:3097 self.cloneDestination = depotPaths[-1]3098 depotPaths = depotPaths[:-1]30993100 self.cloneExclude = ["/"+p for p in self.cloneExclude]3101for p in depotPaths:3102if not p.startswith("//"):3103return False31043105if not self.cloneDestination:3106 self.cloneDestination = self.defaultDestination(args)31073108print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31093110if not os.path.exists(self.cloneDestination):3111 os.makedirs(self.cloneDestination)3112chdir(self.cloneDestination)31133114 init_cmd = ["git","init"]3115if self.cloneBare:3116 init_cmd.append("--bare")3117 subprocess.check_call(init_cmd)31183119if not P4Sync.run(self, depotPaths):3120return False3121if self.branch !="master":3122if self.importIntoRemotes:3123 masterbranch ="refs/remotes/p4/master"3124else:3125 masterbranch ="refs/heads/p4/master"3126ifgitBranchExists(masterbranch):3127system("git branch master%s"% masterbranch)3128if not self.cloneBare:3129system("git checkout -f")3130else:3131print"Could not detect main branch. No checkout/master branch created."31323133# auto-set this variable if invoked with --use-client-spec3134if self.useClientSpec_from_options:3135system("git config --bool git-p4.useclientspec true")31363137return True31383139classP4Branches(Command):3140def__init__(self):3141 Command.__init__(self)3142 self.options = [ ]3143 self.description = ("Shows the git branches that hold imports and their "3144+"corresponding perforce depot paths")3145 self.verbose =False31463147defrun(self, args):3148iforiginP4BranchesExist():3149createOrUpdateBranchesFromOrigin()31503151 cmdline ="git rev-parse --symbolic "3152 cmdline +=" --remotes"31533154for line inread_pipe_lines(cmdline):3155 line = line.strip()31563157if not line.startswith('p4/')or line =="p4/HEAD":3158continue3159 branch = line31603161 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3162 settings =extractSettingsGitLog(log)31633164print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3165return True31663167classHelpFormatter(optparse.IndentedHelpFormatter):3168def__init__(self):3169 optparse.IndentedHelpFormatter.__init__(self)31703171defformat_description(self, description):3172if description:3173return description +"\n"3174else:3175return""31763177defprintUsage(commands):3178print"usage:%s<command> [options]"% sys.argv[0]3179print""3180print"valid commands:%s"%", ".join(commands)3181print""3182print"Try%s<command> --help for command specific help."% sys.argv[0]3183print""31843185commands = {3186"debug": P4Debug,3187"submit": P4Submit,3188"commit": P4Submit,3189"sync": P4Sync,3190"rebase": P4Rebase,3191"clone": P4Clone,3192"rollback": P4RollBack,3193"branches": P4Branches3194}319531963197defmain():3198iflen(sys.argv[1:]) ==0:3199printUsage(commands.keys())3200 sys.exit(2)32013202 cmdName = sys.argv[1]3203try:3204 klass = commands[cmdName]3205 cmd =klass()3206exceptKeyError:3207print"unknown command%s"% cmdName3208print""3209printUsage(commands.keys())3210 sys.exit(2)32113212 options = cmd.options3213 cmd.gitdir = os.environ.get("GIT_DIR",None)32143215 args = sys.argv[2:]32163217 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3218if cmd.needsGit:3219 options.append(optparse.make_option("--git-dir", dest="gitdir"))32203221 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3222 options,3223 description = cmd.description,3224 formatter =HelpFormatter())32253226(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3227global verbose3228 verbose = cmd.verbose3229if cmd.needsGit:3230if cmd.gitdir ==None:3231 cmd.gitdir = os.path.abspath(".git")3232if notisValidGitDir(cmd.gitdir):3233 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3234if os.path.exists(cmd.gitdir):3235 cdup =read_pipe("git rev-parse --show-cdup").strip()3236iflen(cdup) >0:3237chdir(cdup);32383239if notisValidGitDir(cmd.gitdir):3240ifisValidGitDir(cmd.gitdir +"/.git"):3241 cmd.gitdir +="/.git"3242else:3243die("fatal: cannot locate git repository at%s"% cmd.gitdir)32443245 os.environ["GIT_DIR"] = cmd.gitdir32463247if not cmd.run(args):3248 parser.print_help()3249 sys.exit(2)325032513252if __name__ =='__main__':3253main()