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 582defbranch_exists(branch): 583"""Make sure that the given ref name really exists.""" 584 585 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 586 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 587 out, _ = p.communicate() 588if p.returncode: 589return False 590# expect exactly one line of output: the branch name 591return out.rstrip() == branch 592 593deffindUpstreamBranchPoint(head ="HEAD"): 594 branches =p4BranchesInGit() 595# map from depot-path to branch name 596 branchByDepotPath = {} 597for branch in branches.keys(): 598 tip = branches[branch] 599 log =extractLogMessageFromGitCommit(tip) 600 settings =extractSettingsGitLog(log) 601if settings.has_key("depot-paths"): 602 paths =",".join(settings["depot-paths"]) 603 branchByDepotPath[paths] ="remotes/p4/"+ branch 604 605 settings =None 606 parent =0 607while parent <65535: 608 commit = head +"~%s"% parent 609 log =extractLogMessageFromGitCommit(commit) 610 settings =extractSettingsGitLog(log) 611if settings.has_key("depot-paths"): 612 paths =",".join(settings["depot-paths"]) 613if branchByDepotPath.has_key(paths): 614return[branchByDepotPath[paths], settings] 615 616 parent = parent +1 617 618return["", settings] 619 620defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 621if not silent: 622print("Creating/updating branch(es) in%sbased on origin branch(es)" 623% localRefPrefix) 624 625 originPrefix ="origin/p4/" 626 627for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 628 line = line.strip() 629if(not line.startswith(originPrefix))or line.endswith("HEAD"): 630continue 631 632 headName = line[len(originPrefix):] 633 remoteHead = localRefPrefix + headName 634 originHead = line 635 636 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 637if(not original.has_key('depot-paths') 638or not original.has_key('change')): 639continue 640 641 update =False 642if notgitBranchExists(remoteHead): 643if verbose: 644print"creating%s"% remoteHead 645 update =True 646else: 647 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 648if settings.has_key('change') >0: 649if settings['depot-paths'] == original['depot-paths']: 650 originP4Change =int(original['change']) 651 p4Change =int(settings['change']) 652if originP4Change > p4Change: 653print("%s(%s) is newer than%s(%s). " 654"Updating p4 branch from origin." 655% (originHead, originP4Change, 656 remoteHead, p4Change)) 657 update =True 658else: 659print("Ignoring:%swas imported from%swhile " 660"%swas imported from%s" 661% (originHead,','.join(original['depot-paths']), 662 remoteHead,','.join(settings['depot-paths']))) 663 664if update: 665system("git update-ref%s %s"% (remoteHead, originHead)) 666 667deforiginP4BranchesExist(): 668returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 669 670defp4ChangesForPaths(depotPaths, changeRange): 671assert depotPaths 672 cmd = ['changes'] 673for p in depotPaths: 674 cmd += ["%s...%s"% (p, changeRange)] 675 output =p4_read_pipe_lines(cmd) 676 677 changes = {} 678for line in output: 679 changeNum =int(line.split(" ")[1]) 680 changes[changeNum] =True 681 682 changelist = changes.keys() 683 changelist.sort() 684return changelist 685 686defp4PathStartsWith(path, prefix): 687# This method tries to remedy a potential mixed-case issue: 688# 689# If UserA adds //depot/DirA/file1 690# and UserB adds //depot/dira/file2 691# 692# we may or may not have a problem. If you have core.ignorecase=true, 693# we treat DirA and dira as the same directory 694 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 695if ignorecase: 696return path.lower().startswith(prefix.lower()) 697return path.startswith(prefix) 698 699defgetClientSpec(): 700"""Look at the p4 client spec, create a View() object that contains 701 all the mappings, and return it.""" 702 703 specList =p4CmdList("client -o") 704iflen(specList) !=1: 705die('Output from "client -o" is%dlines, expecting 1'% 706len(specList)) 707 708# dictionary of all client parameters 709 entry = specList[0] 710 711# just the keys that start with "View" 712 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 713 714# hold this new View 715 view =View() 716 717# append the lines, in order, to the view 718for view_num inrange(len(view_keys)): 719 k ="View%d"% view_num 720if k not in view_keys: 721die("Expected view key%smissing"% k) 722 view.append(entry[k]) 723 724return view 725 726defgetClientRoot(): 727"""Grab the client directory.""" 728 729 output =p4CmdList("client -o") 730iflen(output) !=1: 731die('Output from "client -o" is%dlines, expecting 1'%len(output)) 732 733 entry = output[0] 734if"Root"not in entry: 735die('Client has no "Root"') 736 737return entry["Root"] 738 739# 740# P4 wildcards are not allowed in filenames. P4 complains 741# if you simply add them, but you can force it with "-f", in 742# which case it translates them into %xx encoding internally. 743# 744defwildcard_decode(path): 745# Search for and fix just these four characters. Do % last so 746# that fixing it does not inadvertently create new %-escapes. 747# Cannot have * in a filename in windows; untested as to 748# what p4 would do in such a case. 749if not platform.system() =="Windows": 750 path = path.replace("%2A","*") 751 path = path.replace("%23","#") \ 752.replace("%40","@") \ 753.replace("%25","%") 754return path 755 756defwildcard_encode(path): 757# do % first to avoid double-encoding the %s introduced here 758 path = path.replace("%","%25") \ 759.replace("*","%2A") \ 760.replace("#","%23") \ 761.replace("@","%40") 762return path 763 764defwildcard_present(path): 765return path.translate(None,"*#@%") != path 766 767class Command: 768def__init__(self): 769 self.usage ="usage: %prog [options]" 770 self.needsGit =True 771 self.verbose =False 772 773class P4UserMap: 774def__init__(self): 775 self.userMapFromPerforceServer =False 776 self.myP4UserId =None 777 778defp4UserId(self): 779if self.myP4UserId: 780return self.myP4UserId 781 782 results =p4CmdList("user -o") 783for r in results: 784if r.has_key('User'): 785 self.myP4UserId = r['User'] 786return r['User'] 787die("Could not find your p4 user id") 788 789defp4UserIsMe(self, p4User): 790# return True if the given p4 user is actually me 791 me = self.p4UserId() 792if not p4User or p4User != me: 793return False 794else: 795return True 796 797defgetUserCacheFilename(self): 798 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 799return home +"/.gitp4-usercache.txt" 800 801defgetUserMapFromPerforceServer(self): 802if self.userMapFromPerforceServer: 803return 804 self.users = {} 805 self.emails = {} 806 807for output inp4CmdList("users"): 808if not output.has_key("User"): 809continue 810 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 811 self.emails[output["Email"]] = output["User"] 812 813 814 s ='' 815for(key, val)in self.users.items(): 816 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 817 818open(self.getUserCacheFilename(),"wb").write(s) 819 self.userMapFromPerforceServer =True 820 821defloadUserMapFromCache(self): 822 self.users = {} 823 self.userMapFromPerforceServer =False 824try: 825 cache =open(self.getUserCacheFilename(),"rb") 826 lines = cache.readlines() 827 cache.close() 828for line in lines: 829 entry = line.strip().split("\t") 830 self.users[entry[0]] = entry[1] 831exceptIOError: 832 self.getUserMapFromPerforceServer() 833 834classP4Debug(Command): 835def__init__(self): 836 Command.__init__(self) 837 self.options = [] 838 self.description ="A tool to debug the output of p4 -G." 839 self.needsGit =False 840 841defrun(self, args): 842 j =0 843for output inp4CmdList(args): 844print'Element:%d'% j 845 j +=1 846print output 847return True 848 849classP4RollBack(Command): 850def__init__(self): 851 Command.__init__(self) 852 self.options = [ 853 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 854] 855 self.description ="A tool to debug the multi-branch import. Don't use :)" 856 self.rollbackLocalBranches =False 857 858defrun(self, args): 859iflen(args) !=1: 860return False 861 maxChange =int(args[0]) 862 863if"p4ExitCode"inp4Cmd("changes -m 1"): 864die("Problems executing p4"); 865 866if self.rollbackLocalBranches: 867 refPrefix ="refs/heads/" 868 lines =read_pipe_lines("git rev-parse --symbolic --branches") 869else: 870 refPrefix ="refs/remotes/" 871 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 872 873for line in lines: 874if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 875 line = line.strip() 876 ref = refPrefix + line 877 log =extractLogMessageFromGitCommit(ref) 878 settings =extractSettingsGitLog(log) 879 880 depotPaths = settings['depot-paths'] 881 change = settings['change'] 882 883 changed =False 884 885iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 886for p in depotPaths]))) ==0: 887print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 888system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 889continue 890 891while change andint(change) > maxChange: 892 changed =True 893if self.verbose: 894print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 895system("git update-ref%s\"%s^\""% (ref, ref)) 896 log =extractLogMessageFromGitCommit(ref) 897 settings =extractSettingsGitLog(log) 898 899 900 depotPaths = settings['depot-paths'] 901 change = settings['change'] 902 903if changed: 904print"%srewound to%s"% (ref, change) 905 906return True 907 908classP4Submit(Command, P4UserMap): 909 910 conflict_behavior_choices = ("ask","skip","quit") 911 912def__init__(self): 913 Command.__init__(self) 914 P4UserMap.__init__(self) 915 self.options = [ 916 optparse.make_option("--origin", dest="origin"), 917 optparse.make_option("-M", dest="detectRenames", action="store_true"), 918# preserve the user, requires relevant p4 permissions 919 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 920 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 921 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 922 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 923 optparse.make_option("--conflict", dest="conflict_behavior", 924 choices=self.conflict_behavior_choices), 925 optparse.make_option("--branch", dest="branch"), 926] 927 self.description ="Submit changes from git to the perforce depot." 928 self.usage +=" [name of git branch to submit into perforce depot]" 929 self.origin ="" 930 self.detectRenames =False 931 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 932 self.dry_run =False 933 self.prepare_p4_only =False 934 self.conflict_behavior =None 935 self.isWindows = (platform.system() =="Windows") 936 self.exportLabels =False 937 self.p4HasMoveCommand =p4_has_move_command() 938 self.branch =None 939 940defcheck(self): 941iflen(p4CmdList("opened ...")) >0: 942die("You have files opened with perforce! Close them before starting the sync.") 943 944defseparate_jobs_from_description(self, message): 945"""Extract and return a possible Jobs field in the commit 946 message. It goes into a separate section in the p4 change 947 specification. 948 949 A jobs line starts with "Jobs:" and looks like a new field 950 in a form. Values are white-space separated on the same 951 line or on following lines that start with a tab. 952 953 This does not parse and extract the full git commit message 954 like a p4 form. It just sees the Jobs: line as a marker 955 to pass everything from then on directly into the p4 form, 956 but outside the description section. 957 958 Return a tuple (stripped log message, jobs string).""" 959 960 m = re.search(r'^Jobs:', message, re.MULTILINE) 961if m is None: 962return(message,None) 963 964 jobtext = message[m.start():] 965 stripped_message = message[:m.start()].rstrip() 966return(stripped_message, jobtext) 967 968defprepareLogMessage(self, template, message, jobs): 969"""Edits the template returned from "p4 change -o" to insert 970 the message in the Description field, and the jobs text in 971 the Jobs field.""" 972 result ="" 973 974 inDescriptionSection =False 975 976for line in template.split("\n"): 977if line.startswith("#"): 978 result += line +"\n" 979continue 980 981if inDescriptionSection: 982if line.startswith("Files:")or line.startswith("Jobs:"): 983 inDescriptionSection =False 984# insert Jobs section 985if jobs: 986 result += jobs +"\n" 987else: 988continue 989else: 990if line.startswith("Description:"): 991 inDescriptionSection =True 992 line +="\n" 993for messageLine in message.split("\n"): 994 line +="\t"+ messageLine +"\n" 995 996 result += line +"\n" 997 998return result 9991000defpatchRCSKeywords(self,file, pattern):1001# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1002(handle, outFileName) = tempfile.mkstemp(dir='.')1003try:1004 outFile = os.fdopen(handle,"w+")1005 inFile =open(file,"r")1006 regexp = re.compile(pattern, re.VERBOSE)1007for line in inFile.readlines():1008 line = regexp.sub(r'$\1$', line)1009 outFile.write(line)1010 inFile.close()1011 outFile.close()1012# Forcibly overwrite the original file1013 os.unlink(file)1014 shutil.move(outFileName,file)1015except:1016# cleanup our temporary file1017 os.unlink(outFileName)1018print"Failed to strip RCS keywords in%s"%file1019raise10201021print"Patched up RCS keywords in%s"%file10221023defp4UserForCommit(self,id):1024# Return the tuple (perforce user,git email) for a given git commit id1025 self.getUserMapFromPerforceServer()1026 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id)1027 gitEmail = gitEmail.strip()1028if not self.emails.has_key(gitEmail):1029return(None,gitEmail)1030else:1031return(self.emails[gitEmail],gitEmail)10321033defcheckValidP4Users(self,commits):1034# check if any git authors cannot be mapped to p4 users1035foridin commits:1036(user,email) = self.p4UserForCommit(id)1037if not user:1038 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1039ifgitConfig('git-p4.allowMissingP4Users').lower() =="true":1040print"%s"% msg1041else:1042die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10431044deflastP4Changelist(self):1045# Get back the last changelist number submitted in this client spec. This1046# then gets used to patch up the username in the change. If the same1047# client spec is being used by multiple processes then this might go1048# wrong.1049 results =p4CmdList("client -o")# find the current client1050 client =None1051for r in results:1052if r.has_key('Client'):1053 client = r['Client']1054break1055if not client:1056die("could not get client spec")1057 results =p4CmdList(["changes","-c", client,"-m","1"])1058for r in results:1059if r.has_key('change'):1060return r['change']1061die("Could not get changelist number for last submit - cannot patch up user details")10621063defmodifyChangelistUser(self, changelist, newUser):1064# fixup the user field of a changelist after it has been submitted.1065 changes =p4CmdList("change -o%s"% changelist)1066iflen(changes) !=1:1067die("Bad output from p4 change modifying%sto user%s"%1068(changelist, newUser))10691070 c = changes[0]1071if c['User'] == newUser:return# nothing to do1072 c['User'] = newUser1073input= marshal.dumps(c)10741075 result =p4CmdList("change -f -i", stdin=input)1076for r in result:1077if r.has_key('code'):1078if r['code'] =='error':1079die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1080if r.has_key('data'):1081print("Updated user field for changelist%sto%s"% (changelist, newUser))1082return1083die("Could not modify user field of changelist%sto%s"% (changelist, newUser))10841085defcanChangeChangelists(self):1086# check to see if we have p4 admin or super-user permissions, either of1087# which are required to modify changelists.1088 results =p4CmdList(["protects", self.depotPath])1089for r in results:1090if r.has_key('perm'):1091if r['perm'] =='admin':1092return11093if r['perm'] =='super':1094return11095return010961097defprepareSubmitTemplate(self):1098"""Run "p4 change -o" to grab a change specification template.1099 This does not use "p4 -G", as it is nice to keep the submission1100 template in original order, since a human might edit it.11011102 Remove lines in the Files section that show changes to files1103 outside the depot path we're committing into."""11041105 template =""1106 inFilesSection =False1107for line inp4_read_pipe_lines(['change','-o']):1108if line.endswith("\r\n"):1109 line = line[:-2] +"\n"1110if inFilesSection:1111if line.startswith("\t"):1112# path starts and ends with a tab1113 path = line[1:]1114 lastTab = path.rfind("\t")1115if lastTab != -1:1116 path = path[:lastTab]1117if notp4PathStartsWith(path, self.depotPath):1118continue1119else:1120 inFilesSection =False1121else:1122if line.startswith("Files:"):1123 inFilesSection =True11241125 template += line11261127return template11281129defedit_template(self, template_file):1130"""Invoke the editor to let the user change the submission1131 message. Return true if okay to continue with the submit."""11321133# if configured to skip the editing part, just submit1134ifgitConfig("git-p4.skipSubmitEdit") =="true":1135return True11361137# look at the modification time, to check later if the user saved1138# the file1139 mtime = os.stat(template_file).st_mtime11401141# invoke the editor1142if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1143 editor = os.environ.get("P4EDITOR")1144else:1145 editor =read_pipe("git var GIT_EDITOR").strip()1146system(editor +" "+ template_file)11471148# If the file was not saved, prompt to see if this patch should1149# be skipped. But skip this verification step if configured so.1150ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1151return True11521153# modification time updated means user saved the file1154if os.stat(template_file).st_mtime > mtime:1155return True11561157while True:1158 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1159if response =='y':1160return True1161if response =='n':1162return False11631164defapplyCommit(self,id):1165"""Apply one commit, return True if it succeeded."""11661167print"Applying",read_pipe(["git","show","-s",1168"--format=format:%h%s",id])11691170(p4User, gitEmail) = self.p4UserForCommit(id)11711172 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1173 filesToAdd =set()1174 filesToDelete =set()1175 editedFiles =set()1176 pureRenameCopy =set()1177 filesToChangeExecBit = {}11781179for line in diff:1180 diff =parseDiffTreeEntry(line)1181 modifier = diff['status']1182 path = diff['src']1183if modifier =="M":1184p4_edit(path)1185ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1186 filesToChangeExecBit[path] = diff['dst_mode']1187 editedFiles.add(path)1188elif modifier =="A":1189 filesToAdd.add(path)1190 filesToChangeExecBit[path] = diff['dst_mode']1191if path in filesToDelete:1192 filesToDelete.remove(path)1193elif modifier =="D":1194 filesToDelete.add(path)1195if path in filesToAdd:1196 filesToAdd.remove(path)1197elif modifier =="C":1198 src, dest = diff['src'], diff['dst']1199p4_integrate(src, dest)1200 pureRenameCopy.add(dest)1201if diff['src_sha1'] != diff['dst_sha1']:1202p4_edit(dest)1203 pureRenameCopy.discard(dest)1204ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1205p4_edit(dest)1206 pureRenameCopy.discard(dest)1207 filesToChangeExecBit[dest] = diff['dst_mode']1208 os.unlink(dest)1209 editedFiles.add(dest)1210elif modifier =="R":1211 src, dest = diff['src'], diff['dst']1212if self.p4HasMoveCommand:1213p4_edit(src)# src must be open before move1214p4_move(src, dest)# opens for (move/delete, move/add)1215else:1216p4_integrate(src, dest)1217if diff['src_sha1'] != diff['dst_sha1']:1218p4_edit(dest)1219else:1220 pureRenameCopy.add(dest)1221ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1222if not self.p4HasMoveCommand:1223p4_edit(dest)# with move: already open, writable1224 filesToChangeExecBit[dest] = diff['dst_mode']1225if not self.p4HasMoveCommand:1226 os.unlink(dest)1227 filesToDelete.add(src)1228 editedFiles.add(dest)1229else:1230die("unknown modifier%sfor%s"% (modifier, path))12311232 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1233 patchcmd = diffcmd +" | git apply "1234 tryPatchCmd = patchcmd +"--check -"1235 applyPatchCmd = patchcmd +"--check --apply -"1236 patch_succeeded =True12371238if os.system(tryPatchCmd) !=0:1239 fixed_rcs_keywords =False1240 patch_succeeded =False1241print"Unfortunately applying the change failed!"12421243# Patch failed, maybe it's just RCS keyword woes. Look through1244# the patch to see if that's possible.1245ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1246file=None1247 pattern =None1248 kwfiles = {}1249forfilein editedFiles | filesToDelete:1250# did this file's delta contain RCS keywords?1251 pattern =p4_keywords_regexp_for_file(file)12521253if pattern:1254# this file is a possibility...look for RCS keywords.1255 regexp = re.compile(pattern, re.VERBOSE)1256for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1257if regexp.search(line):1258if verbose:1259print"got keyword match on%sin%sin%s"% (pattern, line,file)1260 kwfiles[file] = pattern1261break12621263forfilein kwfiles:1264if verbose:1265print"zapping%swith%s"% (line,pattern)1266 self.patchRCSKeywords(file, kwfiles[file])1267 fixed_rcs_keywords =True12681269if fixed_rcs_keywords:1270print"Retrying the patch with RCS keywords cleaned up"1271if os.system(tryPatchCmd) ==0:1272 patch_succeeded =True12731274if not patch_succeeded:1275for f in editedFiles:1276p4_revert(f)1277return False12781279#1280# Apply the patch for real, and do add/delete/+x handling.1281#1282system(applyPatchCmd)12831284for f in filesToAdd:1285p4_add(f)1286for f in filesToDelete:1287p4_revert(f)1288p4_delete(f)12891290# Set/clear executable bits1291for f in filesToChangeExecBit.keys():1292 mode = filesToChangeExecBit[f]1293setP4ExecBit(f, mode)12941295#1296# Build p4 change description, starting with the contents1297# of the git commit message.1298#1299 logMessage =extractLogMessageFromGitCommit(id)1300 logMessage = logMessage.strip()1301(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13021303 template = self.prepareSubmitTemplate()1304 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13051306if self.preserveUser:1307 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13081309if self.checkAuthorship and not self.p4UserIsMe(p4User):1310 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1311 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1312 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13131314 separatorLine ="######## everything below this line is just the diff #######\n"13151316# diff1317if os.environ.has_key("P4DIFF"):1318del(os.environ["P4DIFF"])1319 diff =""1320for editedFile in editedFiles:1321 diff +=p4_read_pipe(['diff','-du',1322wildcard_encode(editedFile)])13231324# new file diff1325 newdiff =""1326for newFile in filesToAdd:1327 newdiff +="==== new file ====\n"1328 newdiff +="--- /dev/null\n"1329 newdiff +="+++%s\n"% newFile1330 f =open(newFile,"r")1331for line in f.readlines():1332 newdiff +="+"+ line1333 f.close()13341335# change description file: submitTemplate, separatorLine, diff, newdiff1336(handle, fileName) = tempfile.mkstemp()1337 tmpFile = os.fdopen(handle,"w+")1338if self.isWindows:1339 submitTemplate = submitTemplate.replace("\n","\r\n")1340 separatorLine = separatorLine.replace("\n","\r\n")1341 newdiff = newdiff.replace("\n","\r\n")1342 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1343 tmpFile.close()13441345if self.prepare_p4_only:1346#1347# Leave the p4 tree prepared, and the submit template around1348# and let the user decide what to do next1349#1350print1351print"P4 workspace prepared for submission."1352print"To submit or revert, go to client workspace"1353print" "+ self.clientPath1354print1355print"To submit, use\"p4 submit\"to write a new description,"1356print"or\"p4 submit -i%s\"to use the one prepared by" \1357"\"git p4\"."% fileName1358print"You can delete the file\"%s\"when finished."% fileName13591360if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1361print"To preserve change ownership by user%s, you must\n" \1362"do\"p4 change -f <change>\"after submitting and\n" \1363"edit the User field."1364if pureRenameCopy:1365print"After submitting, renamed files must be re-synced."1366print"Invoke\"p4 sync -f\"on each of these files:"1367for f in pureRenameCopy:1368print" "+ f13691370print1371print"To revert the changes, use\"p4 revert ...\", and delete"1372print"the submit template file\"%s\""% fileName1373if filesToAdd:1374print"Since the commit adds new files, they must be deleted:"1375for f in filesToAdd:1376print" "+ f1377print1378return True13791380#1381# Let the user edit the change description, then submit it.1382#1383if self.edit_template(fileName):1384# read the edited message and submit1385 ret =True1386 tmpFile =open(fileName,"rb")1387 message = tmpFile.read()1388 tmpFile.close()1389 submitTemplate = message[:message.index(separatorLine)]1390if self.isWindows:1391 submitTemplate = submitTemplate.replace("\r\n","\n")1392p4_write_pipe(['submit','-i'], submitTemplate)13931394if self.preserveUser:1395if p4User:1396# Get last changelist number. Cannot easily get it from1397# the submit command output as the output is1398# unmarshalled.1399 changelist = self.lastP4Changelist()1400 self.modifyChangelistUser(changelist, p4User)14011402# The rename/copy happened by applying a patch that created a1403# new file. This leaves it writable, which confuses p4.1404for f in pureRenameCopy:1405p4_sync(f,"-f")14061407else:1408# skip this patch1409 ret =False1410print"Submission cancelled, undoing p4 changes."1411for f in editedFiles:1412p4_revert(f)1413for f in filesToAdd:1414p4_revert(f)1415 os.remove(f)1416for f in filesToDelete:1417p4_revert(f)14181419 os.remove(fileName)1420return ret14211422# Export git tags as p4 labels. Create a p4 label and then tag1423# with that.1424defexportGitTags(self, gitTags):1425 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1426iflen(validLabelRegexp) ==0:1427 validLabelRegexp = defaultLabelRegexp1428 m = re.compile(validLabelRegexp)14291430for name in gitTags:14311432if not m.match(name):1433if verbose:1434print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1435continue14361437# Get the p4 commit this corresponds to1438 logMessage =extractLogMessageFromGitCommit(name)1439 values =extractSettingsGitLog(logMessage)14401441if not values.has_key('change'):1442# a tag pointing to something not sent to p4; ignore1443if verbose:1444print"git tag%sdoes not give a p4 commit"% name1445continue1446else:1447 changelist = values['change']14481449# Get the tag details.1450 inHeader =True1451 isAnnotated =False1452 body = []1453for l inread_pipe_lines(["git","cat-file","-p", name]):1454 l = l.strip()1455if inHeader:1456if re.match(r'tag\s+', l):1457 isAnnotated =True1458elif re.match(r'\s*$', l):1459 inHeader =False1460continue1461else:1462 body.append(l)14631464if not isAnnotated:1465 body = ["lightweight tag imported by git p4\n"]14661467# Create the label - use the same view as the client spec we are using1468 clientSpec =getClientSpec()14691470 labelTemplate ="Label:%s\n"% name1471 labelTemplate +="Description:\n"1472for b in body:1473 labelTemplate +="\t"+ b +"\n"1474 labelTemplate +="View:\n"1475for mapping in clientSpec.mappings:1476 labelTemplate +="\t%s\n"% mapping.depot_side.path14771478if self.dry_run:1479print"Would create p4 label%sfor tag"% name1480elif self.prepare_p4_only:1481print"Not creating p4 label%sfor tag due to option" \1482" --prepare-p4-only"% name1483else:1484p4_write_pipe(["label","-i"], labelTemplate)14851486# Use the label1487p4_system(["tag","-l", name] +1488["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])14891490if verbose:1491print"created p4 label for tag%s"% name14921493defrun(self, args):1494iflen(args) ==0:1495 self.master =currentGitBranch()1496iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1497die("Detecting current git branch failed!")1498eliflen(args) ==1:1499 self.master = args[0]1500if notbranchExists(self.master):1501die("Branch%sdoes not exist"% self.master)1502else:1503return False15041505 allowSubmit =gitConfig("git-p4.allowSubmit")1506iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1507die("%sis not in git-p4.allowSubmit"% self.master)15081509[upstream, settings] =findUpstreamBranchPoint()1510 self.depotPath = settings['depot-paths'][0]1511iflen(self.origin) ==0:1512 self.origin = upstream15131514if self.preserveUser:1515if not self.canChangeChangelists():1516die("Cannot preserve user names without p4 super-user or admin permissions")15171518# if not set from the command line, try the config file1519if self.conflict_behavior is None:1520 val =gitConfig("git-p4.conflict")1521if val:1522if val not in self.conflict_behavior_choices:1523die("Invalid value '%s' for config git-p4.conflict"% val)1524else:1525 val ="ask"1526 self.conflict_behavior = val15271528if self.verbose:1529print"Origin branch is "+ self.origin15301531iflen(self.depotPath) ==0:1532print"Internal error: cannot locate perforce depot path from existing branches"1533 sys.exit(128)15341535 self.useClientSpec =False1536ifgitConfig("git-p4.useclientspec","--bool") =="true":1537 self.useClientSpec =True1538if self.useClientSpec:1539 self.clientSpecDirs =getClientSpec()15401541if self.useClientSpec:1542# all files are relative to the client spec1543 self.clientPath =getClientRoot()1544else:1545 self.clientPath =p4Where(self.depotPath)15461547if self.clientPath =="":1548die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15491550print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1551 self.oldWorkingDirectory = os.getcwd()15521553# ensure the clientPath exists1554 new_client_dir =False1555if not os.path.exists(self.clientPath):1556 new_client_dir =True1557 os.makedirs(self.clientPath)15581559chdir(self.clientPath)1560if self.dry_run:1561print"Would synchronize p4 checkout in%s"% self.clientPath1562else:1563print"Synchronizing p4 checkout..."1564if new_client_dir:1565# old one was destroyed, and maybe nobody told p41566p4_sync("...","-f")1567else:1568p4_sync("...")1569 self.check()15701571 commits = []1572for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1573 commits.append(line.strip())1574 commits.reverse()15751576if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1577 self.checkAuthorship =False1578else:1579 self.checkAuthorship =True15801581if self.preserveUser:1582 self.checkValidP4Users(commits)15831584#1585# Build up a set of options to be passed to diff when1586# submitting each commit to p4.1587#1588if self.detectRenames:1589# command-line -M arg1590 self.diffOpts ="-M"1591else:1592# If not explicitly set check the config variable1593 detectRenames =gitConfig("git-p4.detectRenames")15941595if detectRenames.lower() =="false"or detectRenames =="":1596 self.diffOpts =""1597elif detectRenames.lower() =="true":1598 self.diffOpts ="-M"1599else:1600 self.diffOpts ="-M%s"% detectRenames16011602# no command-line arg for -C or --find-copies-harder, just1603# config variables1604 detectCopies =gitConfig("git-p4.detectCopies")1605if detectCopies.lower() =="false"or detectCopies =="":1606pass1607elif detectCopies.lower() =="true":1608 self.diffOpts +=" -C"1609else:1610 self.diffOpts +=" -C%s"% detectCopies16111612ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1613 self.diffOpts +=" --find-copies-harder"16141615#1616# Apply the commits, one at a time. On failure, ask if should1617# continue to try the rest of the patches, or quit.1618#1619if self.dry_run:1620print"Would apply"1621 applied = []1622 last =len(commits) -11623for i, commit inenumerate(commits):1624if self.dry_run:1625print" ",read_pipe(["git","show","-s",1626"--format=format:%h%s", commit])1627 ok =True1628else:1629 ok = self.applyCommit(commit)1630if ok:1631 applied.append(commit)1632else:1633if self.prepare_p4_only and i < last:1634print"Processing only the first commit due to option" \1635" --prepare-p4-only"1636break1637if i < last:1638 quit =False1639while True:1640# prompt for what to do, or use the option/variable1641if self.conflict_behavior =="ask":1642print"What do you want to do?"1643 response =raw_input("[s]kip this commit but apply"1644" the rest, or [q]uit? ")1645if not response:1646continue1647elif self.conflict_behavior =="skip":1648 response ="s"1649elif self.conflict_behavior =="quit":1650 response ="q"1651else:1652die("Unknown conflict_behavior '%s'"%1653 self.conflict_behavior)16541655if response[0] =="s":1656print"Skipping this commit, but applying the rest"1657break1658if response[0] =="q":1659print"Quitting"1660 quit =True1661break1662if quit:1663break16641665chdir(self.oldWorkingDirectory)16661667if self.dry_run:1668pass1669elif self.prepare_p4_only:1670pass1671eliflen(commits) ==len(applied):1672print"All commits applied!"16731674 sync =P4Sync()1675if self.branch:1676 sync.branch = self.branch1677 sync.run([])16781679 rebase =P4Rebase()1680 rebase.rebase()16811682else:1683iflen(applied) ==0:1684print"No commits applied."1685else:1686print"Applied only the commits marked with '*':"1687for c in commits:1688if c in applied:1689 star ="*"1690else:1691 star =" "1692print star,read_pipe(["git","show","-s",1693"--format=format:%h%s", c])1694print"You will have to do 'git p4 sync' and rebase."16951696ifgitConfig("git-p4.exportLabels","--bool") =="true":1697 self.exportLabels =True16981699if self.exportLabels:1700 p4Labels =getP4Labels(self.depotPath)1701 gitTags =getGitTags()17021703 missingGitTags = gitTags - p4Labels1704 self.exportGitTags(missingGitTags)17051706# exit with error unless everything applied perfecly1707iflen(commits) !=len(applied):1708 sys.exit(1)17091710return True17111712classView(object):1713"""Represent a p4 view ("p4 help views"), and map files in a1714 repo according to the view."""17151716classPath(object):1717"""A depot or client path, possibly containing wildcards.1718 The only one supported is ... at the end, currently.1719 Initialize with the full path, with //depot or //client."""17201721def__init__(self, path, is_depot):1722 self.path = path1723 self.is_depot = is_depot1724 self.find_wildcards()1725# remember the prefix bit, useful for relative mappings1726 m = re.match("(//[^/]+/)", self.path)1727if not m:1728die("Path%sdoes not start with //prefix/"% self.path)1729 prefix = m.group(1)1730if not self.is_depot:1731# strip //client/ on client paths1732 self.path = self.path[len(prefix):]17331734deffind_wildcards(self):1735"""Make sure wildcards are valid, and set up internal1736 variables."""17371738 self.ends_triple_dot =False1739# There are three wildcards allowed in p4 views1740# (see "p4 help views"). This code knows how to1741# handle "..." (only at the end), but cannot deal with1742# "%%n" or "*". Only check the depot_side, as p4 should1743# validate that the client_side matches too.1744if re.search(r'%%[1-9]', self.path):1745die("Can't handle%%n wildcards in view:%s"% self.path)1746if self.path.find("*") >=0:1747die("Can't handle * wildcards in view:%s"% self.path)1748 triple_dot_index = self.path.find("...")1749if triple_dot_index >=0:1750if triple_dot_index !=len(self.path) -3:1751die("Can handle only single ... wildcard, at end:%s"%1752 self.path)1753 self.ends_triple_dot =True17541755defensure_compatible(self, other_path):1756"""Make sure the wildcards agree."""1757if self.ends_triple_dot != other_path.ends_triple_dot:1758die("Both paths must end with ... if either does;\n"+1759"paths:%s %s"% (self.path, other_path.path))17601761defmatch_wildcards(self, test_path):1762"""See if this test_path matches us, and fill in the value1763 of the wildcards if so. Returns a tuple of1764 (True|False, wildcards[]). For now, only the ... at end1765 is supported, so at most one wildcard."""1766if self.ends_triple_dot:1767 dotless = self.path[:-3]1768if test_path.startswith(dotless):1769 wildcard = test_path[len(dotless):]1770return(True, [ wildcard ])1771else:1772if test_path == self.path:1773return(True, [])1774return(False, [])17751776defmatch(self, test_path):1777"""Just return if it matches; don't bother with the wildcards."""1778 b, _ = self.match_wildcards(test_path)1779return b17801781deffill_in_wildcards(self, wildcards):1782"""Return the relative path, with the wildcards filled in1783 if there are any."""1784if self.ends_triple_dot:1785return self.path[:-3] + wildcards[0]1786else:1787return self.path17881789classMapping(object):1790def__init__(self, depot_side, client_side, overlay, exclude):1791# depot_side is without the trailing /... if it had one1792 self.depot_side = View.Path(depot_side, is_depot=True)1793 self.client_side = View.Path(client_side, is_depot=False)1794 self.overlay = overlay # started with "+"1795 self.exclude = exclude # started with "-"1796assert not(self.overlay and self.exclude)1797 self.depot_side.ensure_compatible(self.client_side)17981799def__str__(self):1800 c =" "1801if self.overlay:1802 c ="+"1803if self.exclude:1804 c ="-"1805return"View.Mapping:%s%s->%s"% \1806(c, self.depot_side.path, self.client_side.path)18071808defmap_depot_to_client(self, depot_path):1809"""Calculate the client path if using this mapping on the1810 given depot path; does not consider the effect of other1811 mappings in a view. Even excluded mappings are returned."""1812 matches, wildcards = self.depot_side.match_wildcards(depot_path)1813if not matches:1814return""1815 client_path = self.client_side.fill_in_wildcards(wildcards)1816return client_path18171818#1819# View methods1820#1821def__init__(self):1822 self.mappings = []18231824defappend(self, view_line):1825"""Parse a view line, splitting it into depot and client1826 sides. Append to self.mappings, preserving order."""18271828# Split the view line into exactly two words. P4 enforces1829# structure on these lines that simplifies this quite a bit.1830#1831# Either or both words may be double-quoted.1832# Single quotes do not matter.1833# Double-quote marks cannot occur inside the words.1834# A + or - prefix is also inside the quotes.1835# There are no quotes unless they contain a space.1836# The line is already white-space stripped.1837# The two words are separated by a single space.1838#1839if view_line[0] =='"':1840# First word is double quoted. Find its end.1841 close_quote_index = view_line.find('"',1)1842if close_quote_index <=0:1843die("No first-word closing quote found:%s"% view_line)1844 depot_side = view_line[1:close_quote_index]1845# skip closing quote and space1846 rhs_index = close_quote_index +1+11847else:1848 space_index = view_line.find(" ")1849if space_index <=0:1850die("No word-splitting space found:%s"% view_line)1851 depot_side = view_line[0:space_index]1852 rhs_index = space_index +118531854if view_line[rhs_index] =='"':1855# Second word is double quoted. Make sure there is a1856# double quote at the end too.1857if not view_line.endswith('"'):1858die("View line with rhs quote should end with one:%s"%1859 view_line)1860# skip the quotes1861 client_side = view_line[rhs_index+1:-1]1862else:1863 client_side = view_line[rhs_index:]18641865# prefix + means overlay on previous mapping1866 overlay =False1867if depot_side.startswith("+"):1868 overlay =True1869 depot_side = depot_side[1:]18701871# prefix - means exclude this path1872 exclude =False1873if depot_side.startswith("-"):1874 exclude =True1875 depot_side = depot_side[1:]18761877 m = View.Mapping(depot_side, client_side, overlay, exclude)1878 self.mappings.append(m)18791880defmap_in_client(self, depot_path):1881"""Return the relative location in the client where this1882 depot file should live. Returns "" if the file should1883 not be mapped in the client."""18841885 paths_filled = []1886 client_path =""18871888# look at later entries first1889for m in self.mappings[::-1]:18901891# see where will this path end up in the client1892 p = m.map_depot_to_client(depot_path)18931894if p =="":1895# Depot path does not belong in client. Must remember1896# this, as previous items should not cause files to1897# exist in this path either. Remember that the list is1898# being walked from the end, which has higher precedence.1899# Overlap mappings do not exclude previous mappings.1900if not m.overlay:1901 paths_filled.append(m.client_side)19021903else:1904# This mapping matched; no need to search any further.1905# But, the mapping could be rejected if the client path1906# has already been claimed by an earlier mapping (i.e.1907# one later in the list, which we are walking backwards).1908 already_mapped_in_client =False1909for f in paths_filled:1910# this is View.Path.match1911if f.match(p):1912 already_mapped_in_client =True1913break1914if not already_mapped_in_client:1915# Include this file, unless it is from a line that1916# explicitly said to exclude it.1917if not m.exclude:1918 client_path = p19191920# a match, even if rejected, always stops the search1921break19221923return client_path19241925classP4Sync(Command, P4UserMap):1926 delete_actions = ("delete","move/delete","purge")19271928def__init__(self):1929 Command.__init__(self)1930 P4UserMap.__init__(self)1931 self.options = [1932 optparse.make_option("--branch", dest="branch"),1933 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1934 optparse.make_option("--changesfile", dest="changesFile"),1935 optparse.make_option("--silent", dest="silent", action="store_true"),1936 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1937 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1938 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1939help="Import into refs/heads/ , not refs/remotes"),1940 optparse.make_option("--max-changes", dest="maxChanges"),1941 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1942help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1943 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1944help="Only sync files that are included in the Perforce Client Spec")1945]1946 self.description ="""Imports from Perforce into a git repository.\n1947 example:1948 //depot/my/project/ -- to import the current head1949 //depot/my/project/@all -- to import everything1950 //depot/my/project/@1,6 -- to import only from revision 1 to 619511952 (a ... is not needed in the path p4 specification, it's added implicitly)"""19531954 self.usage +=" //depot/path[@revRange]"1955 self.silent =False1956 self.createdBranches =set()1957 self.committedChanges =set()1958 self.branch =""1959 self.detectBranches =False1960 self.detectLabels =False1961 self.importLabels =False1962 self.changesFile =""1963 self.syncWithOrigin =True1964 self.importIntoRemotes =True1965 self.maxChanges =""1966 self.isWindows = (platform.system() =="Windows")1967 self.keepRepoPath =False1968 self.depotPaths =None1969 self.p4BranchesInGit = []1970 self.cloneExclude = []1971 self.useClientSpec =False1972 self.useClientSpec_from_options =False1973 self.clientSpecDirs =None1974 self.tempBranches = []1975 self.tempBranchLocation ="git-p4-tmp"19761977ifgitConfig("git-p4.syncFromOrigin") =="false":1978 self.syncWithOrigin =False19791980# Force a checkpoint in fast-import and wait for it to finish1981defcheckpoint(self):1982 self.gitStream.write("checkpoint\n\n")1983 self.gitStream.write("progress checkpoint\n\n")1984 out = self.gitOutput.readline()1985if self.verbose:1986print"checkpoint finished: "+ out19871988defextractFilesFromCommit(self, commit):1989 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1990for path in self.cloneExclude]1991 files = []1992 fnum =01993while commit.has_key("depotFile%s"% fnum):1994 path = commit["depotFile%s"% fnum]19951996if[p for p in self.cloneExclude1997ifp4PathStartsWith(path, p)]:1998 found =False1999else:2000 found = [p for p in self.depotPaths2001ifp4PathStartsWith(path, p)]2002if not found:2003 fnum = fnum +12004continue20052006file= {}2007file["path"] = path2008file["rev"] = commit["rev%s"% fnum]2009file["action"] = commit["action%s"% fnum]2010file["type"] = commit["type%s"% fnum]2011 files.append(file)2012 fnum = fnum +12013return files20142015defstripRepoPath(self, path, prefixes):2016"""When streaming files, this is called to map a p4 depot path2017 to where it should go in git. The prefixes are either2018 self.depotPaths, or self.branchPrefixes in the case of2019 branch detection."""20202021if self.useClientSpec:2022# branch detection moves files up a level (the branch name)2023# from what client spec interpretation gives2024 path = self.clientSpecDirs.map_in_client(path)2025if self.detectBranches:2026for b in self.knownBranches:2027if path.startswith(b +"/"):2028 path = path[len(b)+1:]20292030elif self.keepRepoPath:2031# Preserve everything in relative path name except leading2032# //depot/; just look at first prefix as they all should2033# be in the same depot.2034 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2035ifp4PathStartsWith(path, depot):2036 path = path[len(depot):]20372038else:2039for p in prefixes:2040ifp4PathStartsWith(path, p):2041 path = path[len(p):]2042break20432044 path =wildcard_decode(path)2045return path20462047defsplitFilesIntoBranches(self, commit):2048"""Look at each depotFile in the commit to figure out to what2049 branch it belongs."""20502051 branches = {}2052 fnum =02053while commit.has_key("depotFile%s"% fnum):2054 path = commit["depotFile%s"% fnum]2055 found = [p for p in self.depotPaths2056ifp4PathStartsWith(path, p)]2057if not found:2058 fnum = fnum +12059continue20602061file= {}2062file["path"] = path2063file["rev"] = commit["rev%s"% fnum]2064file["action"] = commit["action%s"% fnum]2065file["type"] = commit["type%s"% fnum]2066 fnum = fnum +120672068# start with the full relative path where this file would2069# go in a p4 client2070if self.useClientSpec:2071 relPath = self.clientSpecDirs.map_in_client(path)2072else:2073 relPath = self.stripRepoPath(path, self.depotPaths)20742075for branch in self.knownBranches.keys():2076# add a trailing slash so that a commit into qt/4.2foo2077# doesn't end up in qt/4.2, e.g.2078if relPath.startswith(branch +"/"):2079if branch not in branches:2080 branches[branch] = []2081 branches[branch].append(file)2082break20832084return branches20852086# output one file from the P4 stream2087# - helper for streamP4Files20882089defstreamOneP4File(self,file, contents):2090 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2091if verbose:2092 sys.stderr.write("%s\n"% relPath)20932094(type_base, type_mods) =split_p4_type(file["type"])20952096 git_mode ="100644"2097if"x"in type_mods:2098 git_mode ="100755"2099if type_base =="symlink":2100 git_mode ="120000"2101# p4 print on a symlink contains "target\n"; remove the newline2102 data =''.join(contents)2103 contents = [data[:-1]]21042105if type_base =="utf16":2106# p4 delivers different text in the python output to -G2107# than it does when using "print -o", or normal p4 client2108# operations. utf16 is converted to ascii or utf8, perhaps.2109# But ascii text saved as -t utf16 is completely mangled.2110# Invoke print -o to get the real contents.2111 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2112 contents = [ text ]21132114if type_base =="apple":2115# Apple filetype files will be streamed as a concatenation of2116# its appledouble header and the contents. This is useless2117# on both macs and non-macs. If using "print -q -o xx", it2118# will create "xx" with the data, and "%xx" with the header.2119# This is also not very useful.2120#2121# Ideally, someday, this script can learn how to generate2122# appledouble files directly and import those to git, but2123# non-mac machines can never find a use for apple filetype.2124print"\nIgnoring apple filetype file%s"%file['depotFile']2125return21262127# Perhaps windows wants unicode, utf16 newlines translated too;2128# but this is not doing it.2129if self.isWindows and type_base =="text":2130 mangled = []2131for data in contents:2132 data = data.replace("\r\n","\n")2133 mangled.append(data)2134 contents = mangled21352136# Note that we do not try to de-mangle keywords on utf16 files,2137# even though in theory somebody may want that.2138 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2139if pattern:2140 regexp = re.compile(pattern, re.VERBOSE)2141 text =''.join(contents)2142 text = regexp.sub(r'$\1$', text)2143 contents = [ text ]21442145 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21462147# total length...2148 length =02149for d in contents:2150 length = length +len(d)21512152 self.gitStream.write("data%d\n"% length)2153for d in contents:2154 self.gitStream.write(d)2155 self.gitStream.write("\n")21562157defstreamOneP4Deletion(self,file):2158 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2159if verbose:2160 sys.stderr.write("delete%s\n"% relPath)2161 self.gitStream.write("D%s\n"% relPath)21622163# handle another chunk of streaming data2164defstreamP4FilesCb(self, marshalled):21652166# catch p4 errors and complain2167 err =None2168if"code"in marshalled:2169if marshalled["code"] =="error":2170if"data"in marshalled:2171 err = marshalled["data"].rstrip()2172if err:2173 f =None2174if self.stream_have_file_info:2175if"depotFile"in self.stream_file:2176 f = self.stream_file["depotFile"]2177# force a failure in fast-import, else an empty2178# commit will be made2179 self.gitStream.write("\n")2180 self.gitStream.write("die-now\n")2181 self.gitStream.close()2182# ignore errors, but make sure it exits first2183 self.importProcess.wait()2184if f:2185die("Error from p4 print for%s:%s"% (f, err))2186else:2187die("Error from p4 print:%s"% err)21882189if marshalled.has_key('depotFile')and self.stream_have_file_info:2190# start of a new file - output the old one first2191 self.streamOneP4File(self.stream_file, self.stream_contents)2192 self.stream_file = {}2193 self.stream_contents = []2194 self.stream_have_file_info =False21952196# pick up the new file information... for the2197# 'data' field we need to append to our array2198for k in marshalled.keys():2199if k =='data':2200 self.stream_contents.append(marshalled['data'])2201else:2202 self.stream_file[k] = marshalled[k]22032204 self.stream_have_file_info =True22052206# Stream directly from "p4 files" into "git fast-import"2207defstreamP4Files(self, files):2208 filesForCommit = []2209 filesToRead = []2210 filesToDelete = []22112212for f in files:2213# if using a client spec, only add the files that have2214# a path in the client2215if self.clientSpecDirs:2216if self.clientSpecDirs.map_in_client(f['path']) =="":2217continue22182219 filesForCommit.append(f)2220if f['action']in self.delete_actions:2221 filesToDelete.append(f)2222else:2223 filesToRead.append(f)22242225# deleted files...2226for f in filesToDelete:2227 self.streamOneP4Deletion(f)22282229iflen(filesToRead) >0:2230 self.stream_file = {}2231 self.stream_contents = []2232 self.stream_have_file_info =False22332234# curry self argument2235defstreamP4FilesCbSelf(entry):2236 self.streamP4FilesCb(entry)22372238 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22392240p4CmdList(["-x","-","print"],2241 stdin=fileArgs,2242 cb=streamP4FilesCbSelf)22432244# do the last chunk2245if self.stream_file.has_key('depotFile'):2246 self.streamOneP4File(self.stream_file, self.stream_contents)22472248defmake_email(self, userid):2249if userid in self.users:2250return self.users[userid]2251else:2252return"%s<a@b>"% userid22532254# Stream a p4 tag2255defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2256if verbose:2257print"writing tag%sfor commit%s"% (labelName, commit)2258 gitStream.write("tag%s\n"% labelName)2259 gitStream.write("from%s\n"% commit)22602261if labelDetails.has_key('Owner'):2262 owner = labelDetails["Owner"]2263else:2264 owner =None22652266# Try to use the owner of the p4 label, or failing that,2267# the current p4 user id.2268if owner:2269 email = self.make_email(owner)2270else:2271 email = self.make_email(self.p4UserId())2272 tagger ="%s %s %s"% (email, epoch, self.tz)22732274 gitStream.write("tagger%s\n"% tagger)22752276print"labelDetails=",labelDetails2277if labelDetails.has_key('Description'):2278 description = labelDetails['Description']2279else:2280 description ='Label from git p4'22812282 gitStream.write("data%d\n"%len(description))2283 gitStream.write(description)2284 gitStream.write("\n")22852286defcommit(self, details, files, branch, parent =""):2287 epoch = details["time"]2288 author = details["user"]22892290if self.verbose:2291print"commit into%s"% branch22922293# start with reading files; if that fails, we should not2294# create a commit.2295 new_files = []2296for f in files:2297if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2298 new_files.append(f)2299else:2300 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23012302 self.gitStream.write("commit%s\n"% branch)2303# gitStream.write("mark :%s\n" % details["change"])2304 self.committedChanges.add(int(details["change"]))2305 committer =""2306if author not in self.users:2307 self.getUserMapFromPerforceServer()2308 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23092310 self.gitStream.write("committer%s\n"% committer)23112312 self.gitStream.write("data <<EOT\n")2313 self.gitStream.write(details["desc"])2314 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2315(','.join(self.branchPrefixes), details["change"]))2316iflen(details['options']) >0:2317 self.gitStream.write(": options =%s"% details['options'])2318 self.gitStream.write("]\nEOT\n\n")23192320iflen(parent) >0:2321if self.verbose:2322print"parent%s"% parent2323 self.gitStream.write("from%s\n"% parent)23242325 self.streamP4Files(new_files)2326 self.gitStream.write("\n")23272328 change =int(details["change"])23292330if self.labels.has_key(change):2331 label = self.labels[change]2332 labelDetails = label[0]2333 labelRevisions = label[1]2334if self.verbose:2335print"Change%sis labelled%s"% (change, labelDetails)23362337 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2338for p in self.branchPrefixes])23392340iflen(files) ==len(labelRevisions):23412342 cleanedFiles = {}2343for info in files:2344if info["action"]in self.delete_actions:2345continue2346 cleanedFiles[info["depotFile"]] = info["rev"]23472348if cleanedFiles == labelRevisions:2349 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23502351else:2352if not self.silent:2353print("Tag%sdoes not match with change%s: files do not match."2354% (labelDetails["label"], change))23552356else:2357if not self.silent:2358print("Tag%sdoes not match with change%s: file count is different."2359% (labelDetails["label"], change))23602361# Build a dictionary of changelists and labels, for "detect-labels" option.2362defgetLabels(self):2363 self.labels = {}23642365 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2366iflen(l) >0and not self.silent:2367print"Finding files belonging to labels in%s"% `self.depotPaths`23682369for output in l:2370 label = output["label"]2371 revisions = {}2372 newestChange =02373if self.verbose:2374print"Querying files for label%s"% label2375forfileinp4CmdList(["files"] +2376["%s...@%s"% (p, label)2377for p in self.depotPaths]):2378 revisions[file["depotFile"]] =file["rev"]2379 change =int(file["change"])2380if change > newestChange:2381 newestChange = change23822383 self.labels[newestChange] = [output, revisions]23842385if self.verbose:2386print"Label changes:%s"% self.labels.keys()23872388# Import p4 labels as git tags. A direct mapping does not2389# exist, so assume that if all the files are at the same revision2390# then we can use that, or it's something more complicated we should2391# just ignore.2392defimportP4Labels(self, stream, p4Labels):2393if verbose:2394print"import p4 labels: "+' '.join(p4Labels)23952396 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2397 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2398iflen(validLabelRegexp) ==0:2399 validLabelRegexp = defaultLabelRegexp2400 m = re.compile(validLabelRegexp)24012402for name in p4Labels:2403 commitFound =False24042405if not m.match(name):2406if verbose:2407print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2408continue24092410if name in ignoredP4Labels:2411continue24122413 labelDetails =p4CmdList(['label',"-o", name])[0]24142415# get the most recent changelist for each file in this label2416 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2417for p in self.depotPaths])24182419if change.has_key('change'):2420# find the corresponding git commit; take the oldest commit2421 changelist =int(change['change'])2422 gitCommit =read_pipe(["git","rev-list","--max-count=1",2423"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2424iflen(gitCommit) ==0:2425print"could not find git commit for changelist%d"% changelist2426else:2427 gitCommit = gitCommit.strip()2428 commitFound =True2429# Convert from p4 time format2430try:2431 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2432exceptValueError:2433print"Could not convert label time%s"% labelDetails['Update']2434 tmwhen =124352436 when =int(time.mktime(tmwhen))2437 self.streamTag(stream, name, labelDetails, gitCommit, when)2438if verbose:2439print"p4 label%smapped to git commit%s"% (name, gitCommit)2440else:2441if verbose:2442print"Label%shas no changelists - possibly deleted?"% name24432444if not commitFound:2445# We can't import this label; don't try again as it will get very2446# expensive repeatedly fetching all the files for labels that will2447# never be imported. If the label is moved in the future, the2448# ignore will need to be removed manually.2449system(["git","config","--add","git-p4.ignoredP4Labels", name])24502451defguessProjectName(self):2452for p in self.depotPaths:2453if p.endswith("/"):2454 p = p[:-1]2455 p = p[p.strip().rfind("/") +1:]2456if not p.endswith("/"):2457 p +="/"2458return p24592460defgetBranchMapping(self):2461 lostAndFoundBranches =set()24622463 user =gitConfig("git-p4.branchUser")2464iflen(user) >0:2465 command ="branches -u%s"% user2466else:2467 command ="branches"24682469for info inp4CmdList(command):2470 details =p4Cmd(["branch","-o", info["branch"]])2471 viewIdx =02472while details.has_key("View%s"% viewIdx):2473 paths = details["View%s"% viewIdx].split(" ")2474 viewIdx = viewIdx +12475# require standard //depot/foo/... //depot/bar/... mapping2476iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2477continue2478 source = paths[0]2479 destination = paths[1]2480## HACK2481ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2482 source = source[len(self.depotPaths[0]):-4]2483 destination = destination[len(self.depotPaths[0]):-4]24842485if destination in self.knownBranches:2486if not self.silent:2487print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2488print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2489continue24902491 self.knownBranches[destination] = source24922493 lostAndFoundBranches.discard(destination)24942495if source not in self.knownBranches:2496 lostAndFoundBranches.add(source)24972498# Perforce does not strictly require branches to be defined, so we also2499# check git config for a branch list.2500#2501# Example of branch definition in git config file:2502# [git-p4]2503# branchList=main:branchA2504# branchList=main:branchB2505# branchList=branchA:branchC2506 configBranches =gitConfigList("git-p4.branchList")2507for branch in configBranches:2508if branch:2509(source, destination) = branch.split(":")2510 self.knownBranches[destination] = source25112512 lostAndFoundBranches.discard(destination)25132514if source not in self.knownBranches:2515 lostAndFoundBranches.add(source)251625172518for branch in lostAndFoundBranches:2519 self.knownBranches[branch] = branch25202521defgetBranchMappingFromGitBranches(self):2522 branches =p4BranchesInGit(self.importIntoRemotes)2523for branch in branches.keys():2524if branch =="master":2525 branch ="main"2526else:2527 branch = branch[len(self.projectName):]2528 self.knownBranches[branch] = branch25292530defupdateOptionDict(self, d):2531 option_keys = {}2532if self.keepRepoPath:2533 option_keys['keepRepoPath'] =125342535 d["options"] =' '.join(sorted(option_keys.keys()))25362537defreadOptions(self, d):2538 self.keepRepoPath = (d.has_key('options')2539and('keepRepoPath'in d['options']))25402541defgitRefForBranch(self, branch):2542if branch =="main":2543return self.refPrefix +"master"25442545iflen(branch) <=0:2546return branch25472548return self.refPrefix + self.projectName + branch25492550defgitCommitByP4Change(self, ref, change):2551if self.verbose:2552print"looking in ref "+ ref +" for change%susing bisect..."% change25532554 earliestCommit =""2555 latestCommit =parseRevision(ref)25562557while True:2558if self.verbose:2559print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2560 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2561iflen(next) ==0:2562if self.verbose:2563print"argh"2564return""2565 log =extractLogMessageFromGitCommit(next)2566 settings =extractSettingsGitLog(log)2567 currentChange =int(settings['change'])2568if self.verbose:2569print"current change%s"% currentChange25702571if currentChange == change:2572if self.verbose:2573print"found%s"% next2574return next25752576if currentChange < change:2577 earliestCommit ="^%s"% next2578else:2579 latestCommit ="%s"% next25802581return""25822583defimportNewBranch(self, branch, maxChange):2584# make fast-import flush all changes to disk and update the refs using the checkpoint2585# command so that we can try to find the branch parent in the git history2586 self.gitStream.write("checkpoint\n\n");2587 self.gitStream.flush();2588 branchPrefix = self.depotPaths[0] + branch +"/"2589range="@1,%s"% maxChange2590#print "prefix" + branchPrefix2591 changes =p4ChangesForPaths([branchPrefix],range)2592iflen(changes) <=0:2593return False2594 firstChange = changes[0]2595#print "first change in branch: %s" % firstChange2596 sourceBranch = self.knownBranches[branch]2597 sourceDepotPath = self.depotPaths[0] + sourceBranch2598 sourceRef = self.gitRefForBranch(sourceBranch)2599#print "source " + sourceBranch26002601 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2602#print "branch parent: %s" % branchParentChange2603 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2604iflen(gitParent) >0:2605 self.initialParents[self.gitRefForBranch(branch)] = gitParent2606#print "parent git commit: %s" % gitParent26072608 self.importChanges(changes)2609return True26102611defsearchParent(self, parent, branch, target):2612 parentFound =False2613for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2614 blob = blob.strip()2615iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2616 parentFound =True2617if self.verbose:2618print"Found parent of%sin commit%s"% (branch, blob)2619break2620if parentFound:2621return blob2622else:2623return None26242625defimportChanges(self, changes):2626 cnt =12627for change in changes:2628 description =p4_describe(change)2629 self.updateOptionDict(description)26302631if not self.silent:2632 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2633 sys.stdout.flush()2634 cnt = cnt +126352636try:2637if self.detectBranches:2638 branches = self.splitFilesIntoBranches(description)2639for branch in branches.keys():2640## HACK --hwn2641 branchPrefix = self.depotPaths[0] + branch +"/"2642 self.branchPrefixes = [ branchPrefix ]26432644 parent =""26452646 filesForCommit = branches[branch]26472648if self.verbose:2649print"branch is%s"% branch26502651 self.updatedBranches.add(branch)26522653if branch not in self.createdBranches:2654 self.createdBranches.add(branch)2655 parent = self.knownBranches[branch]2656if parent == branch:2657 parent =""2658else:2659 fullBranch = self.projectName + branch2660if fullBranch not in self.p4BranchesInGit:2661if not self.silent:2662print("\nImporting new branch%s"% fullBranch);2663if self.importNewBranch(branch, change -1):2664 parent =""2665 self.p4BranchesInGit.append(fullBranch)2666if not self.silent:2667print("\nResuming with change%s"% change);26682669if self.verbose:2670print"parent determined through known branches:%s"% parent26712672 branch = self.gitRefForBranch(branch)2673 parent = self.gitRefForBranch(parent)26742675if self.verbose:2676print"looking for initial parent for%s; current parent is%s"% (branch, parent)26772678iflen(parent) ==0and branch in self.initialParents:2679 parent = self.initialParents[branch]2680del self.initialParents[branch]26812682 blob =None2683iflen(parent) >0:2684 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2685if self.verbose:2686print"Creating temporary branch: "+ tempBranch2687 self.commit(description, filesForCommit, tempBranch)2688 self.tempBranches.append(tempBranch)2689 self.checkpoint()2690 blob = self.searchParent(parent, branch, tempBranch)2691if blob:2692 self.commit(description, filesForCommit, branch, blob)2693else:2694if self.verbose:2695print"Parent of%snot found. Committing into head of%s"% (branch, parent)2696 self.commit(description, filesForCommit, branch, parent)2697else:2698 files = self.extractFilesFromCommit(description)2699 self.commit(description, files, self.branch,2700 self.initialParent)2701# only needed once, to connect to the previous commit2702 self.initialParent =""2703exceptIOError:2704print self.gitError.read()2705 sys.exit(1)27062707defimportHeadRevision(self, revision):2708print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27092710 details = {}2711 details["user"] ="git perforce import user"2712 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2713% (' '.join(self.depotPaths), revision))2714 details["change"] = revision2715 newestRevision =027162717 fileCnt =02718 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27192720for info inp4CmdList(["files"] + fileArgs):27212722if'code'in info and info['code'] =='error':2723 sys.stderr.write("p4 returned an error:%s\n"2724% info['data'])2725if info['data'].find("must refer to client") >=0:2726 sys.stderr.write("This particular p4 error is misleading.\n")2727 sys.stderr.write("Perhaps the depot path was misspelled.\n");2728 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2729 sys.exit(1)2730if'p4ExitCode'in info:2731 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2732 sys.exit(1)273327342735 change =int(info["change"])2736if change > newestRevision:2737 newestRevision = change27382739if info["action"]in self.delete_actions:2740# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2741#fileCnt = fileCnt + 12742continue27432744for prop in["depotFile","rev","action","type"]:2745 details["%s%s"% (prop, fileCnt)] = info[prop]27462747 fileCnt = fileCnt +127482749 details["change"] = newestRevision27502751# Use time from top-most change so that all git p4 clones of2752# the same p4 repo have the same commit SHA1s.2753 res =p4_describe(newestRevision)2754 details["time"] = res["time"]27552756 self.updateOptionDict(details)2757try:2758 self.commit(details, self.extractFilesFromCommit(details), self.branch)2759exceptIOError:2760print"IO error with git fast-import. Is your git version recent enough?"2761print self.gitError.read()276227632764defrun(self, args):2765 self.depotPaths = []2766 self.changeRange =""2767 self.previousDepotPaths = []2768 self.hasOrigin =False27692770# map from branch depot path to parent branch2771 self.knownBranches = {}2772 self.initialParents = {}27732774if self.importIntoRemotes:2775 self.refPrefix ="refs/remotes/p4/"2776else:2777 self.refPrefix ="refs/heads/p4/"27782779if self.syncWithOrigin:2780 self.hasOrigin =originP4BranchesExist()2781if self.hasOrigin:2782if not self.silent:2783print'Syncing with origin first, using "git fetch origin"'2784system("git fetch origin")27852786 branch_arg_given =bool(self.branch)2787iflen(self.branch) ==0:2788 self.branch = self.refPrefix +"master"2789ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2790system("git update-ref%srefs/heads/p4"% self.branch)2791system("git branch -D p4")27922793# accept either the command-line option, or the configuration variable2794if self.useClientSpec:2795# will use this after clone to set the variable2796 self.useClientSpec_from_options =True2797else:2798ifgitConfig("git-p4.useclientspec","--bool") =="true":2799 self.useClientSpec =True2800if self.useClientSpec:2801 self.clientSpecDirs =getClientSpec()28022803# TODO: should always look at previous commits,2804# merge with previous imports, if possible.2805if args == []:2806if self.hasOrigin:2807createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28082809# branches holds mapping from branch name to sha12810 branches =p4BranchesInGit(self.importIntoRemotes)28112812# restrict to just this one, disabling detect-branches2813if branch_arg_given:2814 short = self.branch.split("/")[-1]2815if short in branches:2816 self.p4BranchesInGit = [ short ]2817else:2818 self.p4BranchesInGit = branches.keys()28192820iflen(self.p4BranchesInGit) >1:2821if not self.silent:2822print"Importing from/into multiple branches"2823 self.detectBranches =True2824for branch in branches.keys():2825 self.initialParents[self.refPrefix + branch] = \2826 branches[branch]28272828if self.verbose:2829print"branches:%s"% self.p4BranchesInGit28302831 p4Change =02832for branch in self.p4BranchesInGit:2833 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28342835 settings =extractSettingsGitLog(logMsg)28362837 self.readOptions(settings)2838if(settings.has_key('depot-paths')2839and settings.has_key('change')):2840 change =int(settings['change']) +12841 p4Change =max(p4Change, change)28422843 depotPaths =sorted(settings['depot-paths'])2844if self.previousDepotPaths == []:2845 self.previousDepotPaths = depotPaths2846else:2847 paths = []2848for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2849 prev_list = prev.split("/")2850 cur_list = cur.split("/")2851for i inrange(0,min(len(cur_list),len(prev_list))):2852if cur_list[i] <> prev_list[i]:2853 i = i -12854break28552856 paths.append("/".join(cur_list[:i +1]))28572858 self.previousDepotPaths = paths28592860if p4Change >0:2861 self.depotPaths =sorted(self.previousDepotPaths)2862 self.changeRange ="@%s,#head"% p4Change2863if not self.silent and not self.detectBranches:2864print"Performing incremental import into%sgit branch"% self.branch28652866# accept multiple ref name abbreviations:2867# refs/foo/bar/branch -> use it exactly2868# p4/branch -> prepend refs/remotes/ or refs/heads/2869# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2870if not self.branch.startswith("refs/"):2871if self.importIntoRemotes:2872 prepend ="refs/remotes/"2873else:2874 prepend ="refs/heads/"2875if not self.branch.startswith("p4/"):2876 prepend +="p4/"2877 self.branch = prepend + self.branch28782879iflen(args) ==0and self.depotPaths:2880if not self.silent:2881print"Depot paths:%s"%' '.join(self.depotPaths)2882else:2883if self.depotPaths and self.depotPaths != args:2884print("previous import used depot path%sand now%swas specified. "2885"This doesn't work!"% (' '.join(self.depotPaths),2886' '.join(args)))2887 sys.exit(1)28882889 self.depotPaths =sorted(args)28902891 revision =""2892 self.users = {}28932894# Make sure no revision specifiers are used when --changesfile2895# is specified.2896 bad_changesfile =False2897iflen(self.changesFile) >0:2898for p in self.depotPaths:2899if p.find("@") >=0or p.find("#") >=0:2900 bad_changesfile =True2901break2902if bad_changesfile:2903die("Option --changesfile is incompatible with revision specifiers")29042905 newPaths = []2906for p in self.depotPaths:2907if p.find("@") != -1:2908 atIdx = p.index("@")2909 self.changeRange = p[atIdx:]2910if self.changeRange =="@all":2911 self.changeRange =""2912elif','not in self.changeRange:2913 revision = self.changeRange2914 self.changeRange =""2915 p = p[:atIdx]2916elif p.find("#") != -1:2917 hashIdx = p.index("#")2918 revision = p[hashIdx:]2919 p = p[:hashIdx]2920elif self.previousDepotPaths == []:2921# pay attention to changesfile, if given, else import2922# the entire p4 tree at the head revision2923iflen(self.changesFile) ==0:2924 revision ="#head"29252926 p = re.sub("\.\.\.$","", p)2927if not p.endswith("/"):2928 p +="/"29292930 newPaths.append(p)29312932 self.depotPaths = newPaths29332934# --detect-branches may change this for each branch2935 self.branchPrefixes = self.depotPaths29362937 self.loadUserMapFromCache()2938 self.labels = {}2939if self.detectLabels:2940 self.getLabels();29412942if self.detectBranches:2943## FIXME - what's a P4 projectName ?2944 self.projectName = self.guessProjectName()29452946if self.hasOrigin:2947 self.getBranchMappingFromGitBranches()2948else:2949 self.getBranchMapping()2950if self.verbose:2951print"p4-git branches:%s"% self.p4BranchesInGit2952print"initial parents:%s"% self.initialParents2953for b in self.p4BranchesInGit:2954if b !="master":29552956## FIXME2957 b = b[len(self.projectName):]2958 self.createdBranches.add(b)29592960 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29612962 self.importProcess = subprocess.Popen(["git","fast-import"],2963 stdin=subprocess.PIPE,2964 stdout=subprocess.PIPE,2965 stderr=subprocess.PIPE);2966 self.gitOutput = self.importProcess.stdout2967 self.gitStream = self.importProcess.stdin2968 self.gitError = self.importProcess.stderr29692970if revision:2971 self.importHeadRevision(revision)2972else:2973 changes = []29742975iflen(self.changesFile) >0:2976 output =open(self.changesFile).readlines()2977 changeSet =set()2978for line in output:2979 changeSet.add(int(line))29802981for change in changeSet:2982 changes.append(change)29832984 changes.sort()2985else:2986# catch "git p4 sync" with no new branches, in a repo that2987# does not have any existing p4 branches2988iflen(args) ==0:2989if not self.p4BranchesInGit:2990die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")29912992# The default branch is master, unless --branch is used to2993# specify something else. Make sure it exists, or complain2994# nicely about how to use --branch.2995if not self.detectBranches:2996if notbranch_exists(self.branch):2997if branch_arg_given:2998die("Error: branch%sdoes not exist."% self.branch)2999else:3000die("Error: no branch%s; perhaps specify one with --branch."%3001 self.branch)30023003if self.verbose:3004print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3005 self.changeRange)3006 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30073008iflen(self.maxChanges) >0:3009 changes = changes[:min(int(self.maxChanges),len(changes))]30103011iflen(changes) ==0:3012if not self.silent:3013print"No changes to import!"3014else:3015if not self.silent and not self.detectBranches:3016print"Import destination:%s"% self.branch30173018 self.updatedBranches =set()30193020if not self.detectBranches:3021if args:3022# start a new branch3023 self.initialParent =""3024else:3025# build on a previous revision3026 self.initialParent =parseRevision(self.branch)30273028 self.importChanges(changes)30293030if not self.silent:3031print""3032iflen(self.updatedBranches) >0:3033 sys.stdout.write("Updated branches: ")3034for b in self.updatedBranches:3035 sys.stdout.write("%s"% b)3036 sys.stdout.write("\n")30373038ifgitConfig("git-p4.importLabels","--bool") =="true":3039 self.importLabels =True30403041if self.importLabels:3042 p4Labels =getP4Labels(self.depotPaths)3043 gitTags =getGitTags()30443045 missingP4Labels = p4Labels - gitTags3046 self.importP4Labels(self.gitStream, missingP4Labels)30473048 self.gitStream.close()3049if self.importProcess.wait() !=0:3050die("fast-import failed:%s"% self.gitError.read())3051 self.gitOutput.close()3052 self.gitError.close()30533054# Cleanup temporary branches created during import3055if self.tempBranches != []:3056for branch in self.tempBranches:3057read_pipe("git update-ref -d%s"% branch)3058 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30593060# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3061# a convenient shortcut refname "p4".3062if self.importIntoRemotes:3063 head_ref = self.refPrefix +"HEAD"3064if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3065system(["git","symbolic-ref", head_ref, self.branch])30663067return True30683069classP4Rebase(Command):3070def__init__(self):3071 Command.__init__(self)3072 self.options = [3073 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3074]3075 self.importLabels =False3076 self.description = ("Fetches the latest revision from perforce and "3077+"rebases the current work (branch) against it")30783079defrun(self, args):3080 sync =P4Sync()3081 sync.importLabels = self.importLabels3082 sync.run([])30833084return self.rebase()30853086defrebase(self):3087if os.system("git update-index --refresh") !=0:3088die("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.");3089iflen(read_pipe("git diff-index HEAD --")) >0:3090die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");30913092[upstream, settings] =findUpstreamBranchPoint()3093iflen(upstream) ==0:3094die("Cannot find upstream branchpoint for rebase")30953096# the branchpoint may be p4/foo~3, so strip off the parent3097 upstream = re.sub("~[0-9]+$","", upstream)30983099print"Rebasing the current branch onto%s"% upstream3100 oldHead =read_pipe("git rev-parse HEAD").strip()3101system("git rebase%s"% upstream)3102system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3103return True31043105classP4Clone(P4Sync):3106def__init__(self):3107 P4Sync.__init__(self)3108 self.description ="Creates a new git repository and imports from Perforce into it"3109 self.usage ="usage: %prog [options] //depot/path[@revRange]"3110 self.options += [3111 optparse.make_option("--destination", dest="cloneDestination",3112 action='store', default=None,3113help="where to leave result of the clone"),3114 optparse.make_option("-/", dest="cloneExclude",3115 action="append",type="string",3116help="exclude depot path"),3117 optparse.make_option("--bare", dest="cloneBare",3118 action="store_true", default=False),3119]3120 self.cloneDestination =None3121 self.needsGit =False3122 self.cloneBare =False31233124# This is required for the "append" cloneExclude action3125defensure_value(self, attr, value):3126if nothasattr(self, attr)orgetattr(self, attr)is None:3127setattr(self, attr, value)3128returngetattr(self, attr)31293130defdefaultDestination(self, args):3131## TODO: use common prefix of args?3132 depotPath = args[0]3133 depotDir = re.sub("(@[^@]*)$","", depotPath)3134 depotDir = re.sub("(#[^#]*)$","", depotDir)3135 depotDir = re.sub(r"\.\.\.$","", depotDir)3136 depotDir = re.sub(r"/$","", depotDir)3137return os.path.split(depotDir)[1]31383139defrun(self, args):3140iflen(args) <1:3141return False31423143if self.keepRepoPath and not self.cloneDestination:3144 sys.stderr.write("Must specify destination for --keep-path\n")3145 sys.exit(1)31463147 depotPaths = args31483149if not self.cloneDestination andlen(depotPaths) >1:3150 self.cloneDestination = depotPaths[-1]3151 depotPaths = depotPaths[:-1]31523153 self.cloneExclude = ["/"+p for p in self.cloneExclude]3154for p in depotPaths:3155if not p.startswith("//"):3156return False31573158if not self.cloneDestination:3159 self.cloneDestination = self.defaultDestination(args)31603161print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31623163if not os.path.exists(self.cloneDestination):3164 os.makedirs(self.cloneDestination)3165chdir(self.cloneDestination)31663167 init_cmd = ["git","init"]3168if self.cloneBare:3169 init_cmd.append("--bare")3170 subprocess.check_call(init_cmd)31713172if not P4Sync.run(self, depotPaths):3173return False31743175# create a master branch and check out a work tree3176ifgitBranchExists(self.branch):3177system(["git","branch","master", self.branch ])3178if not self.cloneBare:3179system(["git","checkout","-f"])3180else:3181print'Not checking out any branch, use ' \3182'"git checkout -q -b master <branch>"'31833184# auto-set this variable if invoked with --use-client-spec3185if self.useClientSpec_from_options:3186system("git config --bool git-p4.useclientspec true")31873188return True31893190classP4Branches(Command):3191def__init__(self):3192 Command.__init__(self)3193 self.options = [ ]3194 self.description = ("Shows the git branches that hold imports and their "3195+"corresponding perforce depot paths")3196 self.verbose =False31973198defrun(self, args):3199iforiginP4BranchesExist():3200createOrUpdateBranchesFromOrigin()32013202 cmdline ="git rev-parse --symbolic "3203 cmdline +=" --remotes"32043205for line inread_pipe_lines(cmdline):3206 line = line.strip()32073208if not line.startswith('p4/')or line =="p4/HEAD":3209continue3210 branch = line32113212 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3213 settings =extractSettingsGitLog(log)32143215print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3216return True32173218classHelpFormatter(optparse.IndentedHelpFormatter):3219def__init__(self):3220 optparse.IndentedHelpFormatter.__init__(self)32213222defformat_description(self, description):3223if description:3224return description +"\n"3225else:3226return""32273228defprintUsage(commands):3229print"usage:%s<command> [options]"% sys.argv[0]3230print""3231print"valid commands:%s"%", ".join(commands)3232print""3233print"Try%s<command> --help for command specific help."% sys.argv[0]3234print""32353236commands = {3237"debug": P4Debug,3238"submit": P4Submit,3239"commit": P4Submit,3240"sync": P4Sync,3241"rebase": P4Rebase,3242"clone": P4Clone,3243"rollback": P4RollBack,3244"branches": P4Branches3245}324632473248defmain():3249iflen(sys.argv[1:]) ==0:3250printUsage(commands.keys())3251 sys.exit(2)32523253 cmdName = sys.argv[1]3254try:3255 klass = commands[cmdName]3256 cmd =klass()3257exceptKeyError:3258print"unknown command%s"% cmdName3259print""3260printUsage(commands.keys())3261 sys.exit(2)32623263 options = cmd.options3264 cmd.gitdir = os.environ.get("GIT_DIR",None)32653266 args = sys.argv[2:]32673268 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3269if cmd.needsGit:3270 options.append(optparse.make_option("--git-dir", dest="gitdir"))32713272 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3273 options,3274 description = cmd.description,3275 formatter =HelpFormatter())32763277(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3278global verbose3279 verbose = cmd.verbose3280if cmd.needsGit:3281if cmd.gitdir ==None:3282 cmd.gitdir = os.path.abspath(".git")3283if notisValidGitDir(cmd.gitdir):3284 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3285if os.path.exists(cmd.gitdir):3286 cdup =read_pipe("git rev-parse --show-cdup").strip()3287iflen(cdup) >0:3288chdir(cdup);32893290if notisValidGitDir(cmd.gitdir):3291ifisValidGitDir(cmd.gitdir +"/.git"):3292 cmd.gitdir +="/.git"3293else:3294die("fatal: cannot locate git repository at%s"% cmd.gitdir)32953296 os.environ["GIT_DIR"] = cmd.gitdir32973298if not cmd.run(args):3299 parser.print_help()3300 sys.exit(2)330133023303if __name__ =='__main__':3304main()