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# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25 26verbose =False 27 28# Only labels/tags matching this will be imported/exported 29defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 30 31defp4_build_cmd(cmd): 32"""Build a suitable p4 command line. 33 34 This consolidates building and returning a p4 command line into one 35 location. It means that hooking into the environment, or other configuration 36 can be done more easily. 37 """ 38 real_cmd = ["p4"] 39 40 user =gitConfig("git-p4.user") 41iflen(user) >0: 42 real_cmd += ["-u",user] 43 44 password =gitConfig("git-p4.password") 45iflen(password) >0: 46 real_cmd += ["-P", password] 47 48 port =gitConfig("git-p4.port") 49iflen(port) >0: 50 real_cmd += ["-p", port] 51 52 host =gitConfig("git-p4.host") 53iflen(host) >0: 54 real_cmd += ["-H", host] 55 56 client =gitConfig("git-p4.client") 57iflen(client) >0: 58 real_cmd += ["-c", client] 59 60 61ifisinstance(cmd,basestring): 62 real_cmd =' '.join(real_cmd) +' '+ cmd 63else: 64 real_cmd += cmd 65return real_cmd 66 67defchdir(dir): 68# P4 uses the PWD environment variable rather than getcwd(). Since we're 69# not using the shell, we have to set it ourselves. This path could 70# be relative, so go there first, then figure out where we ended up. 71 os.chdir(dir) 72 os.environ['PWD'] = os.getcwd() 73 74defdie(msg): 75if verbose: 76raiseException(msg) 77else: 78 sys.stderr.write(msg +"\n") 79 sys.exit(1) 80 81defwrite_pipe(c, stdin): 82if verbose: 83 sys.stderr.write('Writing pipe:%s\n'%str(c)) 84 85 expand =isinstance(c,basestring) 86 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 87 pipe = p.stdin 88 val = pipe.write(stdin) 89 pipe.close() 90if p.wait(): 91die('Command failed:%s'%str(c)) 92 93return val 94 95defp4_write_pipe(c, stdin): 96 real_cmd =p4_build_cmd(c) 97returnwrite_pipe(real_cmd, stdin) 98 99defread_pipe(c, ignore_error=False): 100if verbose: 101 sys.stderr.write('Reading pipe:%s\n'%str(c)) 102 103 expand =isinstance(c,basestring) 104 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 105 pipe = p.stdout 106 val = pipe.read() 107if p.wait()and not ignore_error: 108die('Command failed:%s'%str(c)) 109 110return val 111 112defp4_read_pipe(c, ignore_error=False): 113 real_cmd =p4_build_cmd(c) 114returnread_pipe(real_cmd, ignore_error) 115 116defread_pipe_lines(c): 117if verbose: 118 sys.stderr.write('Reading pipe:%s\n'%str(c)) 119 120 expand =isinstance(c, basestring) 121 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 122 pipe = p.stdout 123 val = pipe.readlines() 124if pipe.close()or p.wait(): 125die('Command failed:%s'%str(c)) 126 127return val 128 129defp4_read_pipe_lines(c): 130"""Specifically invoke p4 on the command supplied. """ 131 real_cmd =p4_build_cmd(c) 132returnread_pipe_lines(real_cmd) 133 134defp4_has_command(cmd): 135"""Ask p4 for help on this command. If it returns an error, the 136 command does not exist in this version of p4.""" 137 real_cmd =p4_build_cmd(["help", cmd]) 138 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 139 stderr=subprocess.PIPE) 140 p.communicate() 141return p.returncode ==0 142 143defp4_has_move_command(): 144"""See if the move command exists, that it supports -k, and that 145 it has not been administratively disabled. The arguments 146 must be correct, but the filenames do not have to exist. Use 147 ones with wildcards so even if they exist, it will fail.""" 148 149if notp4_has_command("move"): 150return False 151 cmd =p4_build_cmd(["move","-k","@from","@to"]) 152 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 153(out, err) = p.communicate() 154# return code will be 1 in either case 155if err.find("Invalid option") >=0: 156return False 157if err.find("disabled") >=0: 158return False 159# assume it failed because @... was invalid changelist 160return True 161 162defsystem(cmd): 163 expand =isinstance(cmd,basestring) 164if verbose: 165 sys.stderr.write("executing%s\n"%str(cmd)) 166 subprocess.check_call(cmd, shell=expand) 167 168defp4_system(cmd): 169"""Specifically invoke p4 as the system command. """ 170 real_cmd =p4_build_cmd(cmd) 171 expand =isinstance(real_cmd, basestring) 172 subprocess.check_call(real_cmd, shell=expand) 173 174_p4_version_string =None 175defp4_version_string(): 176"""Read the version string, showing just the last line, which 177 hopefully is the interesting version bit. 178 179 $ p4 -V 180 Perforce - The Fast Software Configuration Management System. 181 Copyright 1995-2011 Perforce Software. All rights reserved. 182 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 183 """ 184global _p4_version_string 185if not _p4_version_string: 186 a =p4_read_pipe_lines(["-V"]) 187 _p4_version_string = a[-1].rstrip() 188return _p4_version_string 189 190defp4_integrate(src, dest): 191p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 192 193defp4_sync(f, *options): 194p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 195 196defp4_add(f): 197# forcibly add file names with wildcards 198ifwildcard_present(f): 199p4_system(["add","-f", f]) 200else: 201p4_system(["add", f]) 202 203defp4_delete(f): 204p4_system(["delete",wildcard_encode(f)]) 205 206defp4_edit(f): 207p4_system(["edit",wildcard_encode(f)]) 208 209defp4_revert(f): 210p4_system(["revert",wildcard_encode(f)]) 211 212defp4_reopen(type, f): 213p4_system(["reopen","-t",type,wildcard_encode(f)]) 214 215defp4_move(src, dest): 216p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 217 218defp4_describe(change): 219"""Make sure it returns a valid result by checking for 220 the presence of field "time". Return a dict of the 221 results.""" 222 223 ds =p4CmdList(["describe","-s",str(change)]) 224iflen(ds) !=1: 225die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 226 227 d = ds[0] 228 229if"p4ExitCode"in d: 230die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 231str(d))) 232if"code"in d: 233if d["code"] =="error": 234die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 235 236if"time"not in d: 237die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 238 239return d 240 241# 242# Canonicalize the p4 type and return a tuple of the 243# base type, plus any modifiers. See "p4 help filetypes" 244# for a list and explanation. 245# 246defsplit_p4_type(p4type): 247 248 p4_filetypes_historical = { 249"ctempobj":"binary+Sw", 250"ctext":"text+C", 251"cxtext":"text+Cx", 252"ktext":"text+k", 253"kxtext":"text+kx", 254"ltext":"text+F", 255"tempobj":"binary+FSw", 256"ubinary":"binary+F", 257"uresource":"resource+F", 258"uxbinary":"binary+Fx", 259"xbinary":"binary+x", 260"xltext":"text+Fx", 261"xtempobj":"binary+Swx", 262"xtext":"text+x", 263"xunicode":"unicode+x", 264"xutf16":"utf16+x", 265} 266if p4type in p4_filetypes_historical: 267 p4type = p4_filetypes_historical[p4type] 268 mods ="" 269 s = p4type.split("+") 270 base = s[0] 271 mods ="" 272iflen(s) >1: 273 mods = s[1] 274return(base, mods) 275 276# 277# return the raw p4 type of a file (text, text+ko, etc) 278# 279defp4_type(file): 280 results =p4CmdList(["fstat","-T","headType",file]) 281return results[0]['headType'] 282 283# 284# Given a type base and modifier, return a regexp matching 285# the keywords that can be expanded in the file 286# 287defp4_keywords_regexp_for_type(base, type_mods): 288if base in("text","unicode","binary"): 289 kwords =None 290if"ko"in type_mods: 291 kwords ='Id|Header' 292elif"k"in type_mods: 293 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 294else: 295return None 296 pattern = r""" 297 \$ # Starts with a dollar, followed by... 298 (%s) # one of the keywords, followed by... 299 (:[^$\n]+)? # possibly an old expansion, followed by... 300 \$ # another dollar 301 """% kwords 302return pattern 303else: 304return None 305 306# 307# Given a file, return a regexp matching the possible 308# RCS keywords that will be expanded, or None for files 309# with kw expansion turned off. 310# 311defp4_keywords_regexp_for_file(file): 312if not os.path.exists(file): 313return None 314else: 315(type_base, type_mods) =split_p4_type(p4_type(file)) 316returnp4_keywords_regexp_for_type(type_base, type_mods) 317 318defsetP4ExecBit(file, mode): 319# Reopens an already open file and changes the execute bit to match 320# the execute bit setting in the passed in mode. 321 322 p4Type ="+x" 323 324if notisModeExec(mode): 325 p4Type =getP4OpenedType(file) 326 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 327 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 328if p4Type[-1] =="+": 329 p4Type = p4Type[0:-1] 330 331p4_reopen(p4Type,file) 332 333defgetP4OpenedType(file): 334# Returns the perforce file type for the given file. 335 336 result =p4_read_pipe(["opened",wildcard_encode(file)]) 337 match = re.match(".*\((.+)\)\r?$", result) 338if match: 339return match.group(1) 340else: 341die("Could not determine file type for%s(result: '%s')"% (file, result)) 342 343# Return the set of all p4 labels 344defgetP4Labels(depotPaths): 345 labels =set() 346ifisinstance(depotPaths,basestring): 347 depotPaths = [depotPaths] 348 349for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 350 label = l['label'] 351 labels.add(label) 352 353return labels 354 355# Return the set of all git tags 356defgetGitTags(): 357 gitTags =set() 358for line inread_pipe_lines(["git","tag"]): 359 tag = line.strip() 360 gitTags.add(tag) 361return gitTags 362 363defdiffTreePattern(): 364# This is a simple generator for the diff tree regex pattern. This could be 365# a class variable if this and parseDiffTreeEntry were a part of a class. 366 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 367while True: 368yield pattern 369 370defparseDiffTreeEntry(entry): 371"""Parses a single diff tree entry into its component elements. 372 373 See git-diff-tree(1) manpage for details about the format of the diff 374 output. This method returns a dictionary with the following elements: 375 376 src_mode - The mode of the source file 377 dst_mode - The mode of the destination file 378 src_sha1 - The sha1 for the source file 379 dst_sha1 - The sha1 fr the destination file 380 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 381 status_score - The score for the status (applicable for 'C' and 'R' 382 statuses). This is None if there is no score. 383 src - The path for the source file. 384 dst - The path for the destination file. This is only present for 385 copy or renames. If it is not present, this is None. 386 387 If the pattern is not matched, None is returned.""" 388 389 match =diffTreePattern().next().match(entry) 390if match: 391return{ 392'src_mode': match.group(1), 393'dst_mode': match.group(2), 394'src_sha1': match.group(3), 395'dst_sha1': match.group(4), 396'status': match.group(5), 397'status_score': match.group(6), 398'src': match.group(7), 399'dst': match.group(10) 400} 401return None 402 403defisModeExec(mode): 404# Returns True if the given git mode represents an executable file, 405# otherwise False. 406return mode[-3:] =="755" 407 408defisModeExecChanged(src_mode, dst_mode): 409returnisModeExec(src_mode) !=isModeExec(dst_mode) 410 411defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 412 413ifisinstance(cmd,basestring): 414 cmd ="-G "+ cmd 415 expand =True 416else: 417 cmd = ["-G"] + cmd 418 expand =False 419 420 cmd =p4_build_cmd(cmd) 421if verbose: 422 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 423 424# Use a temporary file to avoid deadlocks without 425# subprocess.communicate(), which would put another copy 426# of stdout into memory. 427 stdin_file =None 428if stdin is not None: 429 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 430ifisinstance(stdin,basestring): 431 stdin_file.write(stdin) 432else: 433for i in stdin: 434 stdin_file.write(i +'\n') 435 stdin_file.flush() 436 stdin_file.seek(0) 437 438 p4 = subprocess.Popen(cmd, 439 shell=expand, 440 stdin=stdin_file, 441 stdout=subprocess.PIPE) 442 443 result = [] 444try: 445while True: 446 entry = marshal.load(p4.stdout) 447if cb is not None: 448cb(entry) 449else: 450 result.append(entry) 451exceptEOFError: 452pass 453 exitCode = p4.wait() 454if exitCode !=0: 455 entry = {} 456 entry["p4ExitCode"] = exitCode 457 result.append(entry) 458 459return result 460 461defp4Cmd(cmd): 462list=p4CmdList(cmd) 463 result = {} 464for entry inlist: 465 result.update(entry) 466return result; 467 468defp4Where(depotPath): 469if not depotPath.endswith("/"): 470 depotPath +="/" 471 depotPath = depotPath +"..." 472 outputList =p4CmdList(["where", depotPath]) 473 output =None 474for entry in outputList: 475if"depotFile"in entry: 476if entry["depotFile"] == depotPath: 477 output = entry 478break 479elif"data"in entry: 480 data = entry.get("data") 481 space = data.find(" ") 482if data[:space] == depotPath: 483 output = entry 484break 485if output ==None: 486return"" 487if output["code"] =="error": 488return"" 489 clientPath ="" 490if"path"in output: 491 clientPath = output.get("path") 492elif"data"in output: 493 data = output.get("data") 494 lastSpace = data.rfind(" ") 495 clientPath = data[lastSpace +1:] 496 497if clientPath.endswith("..."): 498 clientPath = clientPath[:-3] 499return clientPath 500 501defcurrentGitBranch(): 502returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 503 504defisValidGitDir(path): 505if(os.path.exists(path +"/HEAD") 506and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 507return True; 508return False 509 510defparseRevision(ref): 511returnread_pipe("git rev-parse%s"% ref).strip() 512 513defbranchExists(ref): 514 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 515 ignore_error=True) 516returnlen(rev) >0 517 518defextractLogMessageFromGitCommit(commit): 519 logMessage ="" 520 521## fixme: title is first line of commit, not 1st paragraph. 522 foundTitle =False 523for log inread_pipe_lines("git cat-file commit%s"% commit): 524if not foundTitle: 525iflen(log) ==1: 526 foundTitle =True 527continue 528 529 logMessage += log 530return logMessage 531 532defextractSettingsGitLog(log): 533 values = {} 534for line in log.split("\n"): 535 line = line.strip() 536 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 537if not m: 538continue 539 540 assignments = m.group(1).split(':') 541for a in assignments: 542 vals = a.split('=') 543 key = vals[0].strip() 544 val = ('='.join(vals[1:])).strip() 545if val.endswith('\"')and val.startswith('"'): 546 val = val[1:-1] 547 548 values[key] = val 549 550 paths = values.get("depot-paths") 551if not paths: 552 paths = values.get("depot-path") 553if paths: 554 values['depot-paths'] = paths.split(',') 555return values 556 557defgitBranchExists(branch): 558 proc = subprocess.Popen(["git","rev-parse", branch], 559 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 560return proc.wait() ==0; 561 562_gitConfig = {} 563defgitConfig(key, args =None):# set args to "--bool", for instance 564if not _gitConfig.has_key(key): 565 argsFilter ="" 566if args !=None: 567 argsFilter ="%s"% args 568 cmd ="git config%s%s"% (argsFilter, key) 569 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 570return _gitConfig[key] 571 572defgitConfigList(key): 573if not _gitConfig.has_key(key): 574 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 575 _gitConfig[key] = s.strip().split(os.linesep) 576return _gitConfig[key] 577 578defp4BranchesInGit(branchesAreInRemotes=True): 579"""Find all the branches whose names start with "p4/", looking 580 in remotes or heads as specified by the argument. Return 581 a dictionary of{ branch: revision }for each one found. 582 The branch names are the short names, without any 583 "p4/" prefix.""" 584 585 branches = {} 586 587 cmdline ="git rev-parse --symbolic " 588if branchesAreInRemotes: 589 cmdline +="--remotes" 590else: 591 cmdline +="--branches" 592 593for line inread_pipe_lines(cmdline): 594 line = line.strip() 595 596# only import to p4/ 597if not line.startswith('p4/'): 598continue 599# special symbolic ref to p4/master 600if line =="p4/HEAD": 601continue 602 603# strip off p4/ prefix 604 branch = line[len("p4/"):] 605 606 branches[branch] =parseRevision(line) 607 608return branches 609 610defbranch_exists(branch): 611"""Make sure that the given ref name really exists.""" 612 613 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 614 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 615 out, _ = p.communicate() 616if p.returncode: 617return False 618# expect exactly one line of output: the branch name 619return out.rstrip() == branch 620 621deffindUpstreamBranchPoint(head ="HEAD"): 622 branches =p4BranchesInGit() 623# map from depot-path to branch name 624 branchByDepotPath = {} 625for branch in branches.keys(): 626 tip = branches[branch] 627 log =extractLogMessageFromGitCommit(tip) 628 settings =extractSettingsGitLog(log) 629if settings.has_key("depot-paths"): 630 paths =",".join(settings["depot-paths"]) 631 branchByDepotPath[paths] ="remotes/p4/"+ branch 632 633 settings =None 634 parent =0 635while parent <65535: 636 commit = head +"~%s"% parent 637 log =extractLogMessageFromGitCommit(commit) 638 settings =extractSettingsGitLog(log) 639if settings.has_key("depot-paths"): 640 paths =",".join(settings["depot-paths"]) 641if branchByDepotPath.has_key(paths): 642return[branchByDepotPath[paths], settings] 643 644 parent = parent +1 645 646return["", settings] 647 648defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 649if not silent: 650print("Creating/updating branch(es) in%sbased on origin branch(es)" 651% localRefPrefix) 652 653 originPrefix ="origin/p4/" 654 655for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 656 line = line.strip() 657if(not line.startswith(originPrefix))or line.endswith("HEAD"): 658continue 659 660 headName = line[len(originPrefix):] 661 remoteHead = localRefPrefix + headName 662 originHead = line 663 664 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 665if(not original.has_key('depot-paths') 666or not original.has_key('change')): 667continue 668 669 update =False 670if notgitBranchExists(remoteHead): 671if verbose: 672print"creating%s"% remoteHead 673 update =True 674else: 675 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 676if settings.has_key('change') >0: 677if settings['depot-paths'] == original['depot-paths']: 678 originP4Change =int(original['change']) 679 p4Change =int(settings['change']) 680if originP4Change > p4Change: 681print("%s(%s) is newer than%s(%s). " 682"Updating p4 branch from origin." 683% (originHead, originP4Change, 684 remoteHead, p4Change)) 685 update =True 686else: 687print("Ignoring:%swas imported from%swhile " 688"%swas imported from%s" 689% (originHead,','.join(original['depot-paths']), 690 remoteHead,','.join(settings['depot-paths']))) 691 692if update: 693system("git update-ref%s %s"% (remoteHead, originHead)) 694 695deforiginP4BranchesExist(): 696returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 697 698defp4ChangesForPaths(depotPaths, changeRange): 699assert depotPaths 700 cmd = ['changes'] 701for p in depotPaths: 702 cmd += ["%s...%s"% (p, changeRange)] 703 output =p4_read_pipe_lines(cmd) 704 705 changes = {} 706for line in output: 707 changeNum =int(line.split(" ")[1]) 708 changes[changeNum] =True 709 710 changelist = changes.keys() 711 changelist.sort() 712return changelist 713 714defp4PathStartsWith(path, prefix): 715# This method tries to remedy a potential mixed-case issue: 716# 717# If UserA adds //depot/DirA/file1 718# and UserB adds //depot/dira/file2 719# 720# we may or may not have a problem. If you have core.ignorecase=true, 721# we treat DirA and dira as the same directory 722 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 723if ignorecase: 724return path.lower().startswith(prefix.lower()) 725return path.startswith(prefix) 726 727defgetClientSpec(): 728"""Look at the p4 client spec, create a View() object that contains 729 all the mappings, and return it.""" 730 731 specList =p4CmdList("client -o") 732iflen(specList) !=1: 733die('Output from "client -o" is%dlines, expecting 1'% 734len(specList)) 735 736# dictionary of all client parameters 737 entry = specList[0] 738 739# just the keys that start with "View" 740 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 741 742# hold this new View 743 view =View() 744 745# append the lines, in order, to the view 746for view_num inrange(len(view_keys)): 747 k ="View%d"% view_num 748if k not in view_keys: 749die("Expected view key%smissing"% k) 750 view.append(entry[k]) 751 752return view 753 754defgetClientRoot(): 755"""Grab the client directory.""" 756 757 output =p4CmdList("client -o") 758iflen(output) !=1: 759die('Output from "client -o" is%dlines, expecting 1'%len(output)) 760 761 entry = output[0] 762if"Root"not in entry: 763die('Client has no "Root"') 764 765return entry["Root"] 766 767# 768# P4 wildcards are not allowed in filenames. P4 complains 769# if you simply add them, but you can force it with "-f", in 770# which case it translates them into %xx encoding internally. 771# 772defwildcard_decode(path): 773# Search for and fix just these four characters. Do % last so 774# that fixing it does not inadvertently create new %-escapes. 775# Cannot have * in a filename in windows; untested as to 776# what p4 would do in such a case. 777if not platform.system() =="Windows": 778 path = path.replace("%2A","*") 779 path = path.replace("%23","#") \ 780.replace("%40","@") \ 781.replace("%25","%") 782return path 783 784defwildcard_encode(path): 785# do % first to avoid double-encoding the %s introduced here 786 path = path.replace("%","%25") \ 787.replace("*","%2A") \ 788.replace("#","%23") \ 789.replace("@","%40") 790return path 791 792defwildcard_present(path): 793return path.translate(None,"*#@%") != path 794 795class Command: 796def__init__(self): 797 self.usage ="usage: %prog [options]" 798 self.needsGit =True 799 self.verbose =False 800 801class P4UserMap: 802def__init__(self): 803 self.userMapFromPerforceServer =False 804 self.myP4UserId =None 805 806defp4UserId(self): 807if self.myP4UserId: 808return self.myP4UserId 809 810 results =p4CmdList("user -o") 811for r in results: 812if r.has_key('User'): 813 self.myP4UserId = r['User'] 814return r['User'] 815die("Could not find your p4 user id") 816 817defp4UserIsMe(self, p4User): 818# return True if the given p4 user is actually me 819 me = self.p4UserId() 820if not p4User or p4User != me: 821return False 822else: 823return True 824 825defgetUserCacheFilename(self): 826 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 827return home +"/.gitp4-usercache.txt" 828 829defgetUserMapFromPerforceServer(self): 830if self.userMapFromPerforceServer: 831return 832 self.users = {} 833 self.emails = {} 834 835for output inp4CmdList("users"): 836if not output.has_key("User"): 837continue 838 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 839 self.emails[output["Email"]] = output["User"] 840 841 842 s ='' 843for(key, val)in self.users.items(): 844 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 845 846open(self.getUserCacheFilename(),"wb").write(s) 847 self.userMapFromPerforceServer =True 848 849defloadUserMapFromCache(self): 850 self.users = {} 851 self.userMapFromPerforceServer =False 852try: 853 cache =open(self.getUserCacheFilename(),"rb") 854 lines = cache.readlines() 855 cache.close() 856for line in lines: 857 entry = line.strip().split("\t") 858 self.users[entry[0]] = entry[1] 859exceptIOError: 860 self.getUserMapFromPerforceServer() 861 862classP4Debug(Command): 863def__init__(self): 864 Command.__init__(self) 865 self.options = [] 866 self.description ="A tool to debug the output of p4 -G." 867 self.needsGit =False 868 869defrun(self, args): 870 j =0 871for output inp4CmdList(args): 872print'Element:%d'% j 873 j +=1 874print output 875return True 876 877classP4RollBack(Command): 878def__init__(self): 879 Command.__init__(self) 880 self.options = [ 881 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 882] 883 self.description ="A tool to debug the multi-branch import. Don't use :)" 884 self.rollbackLocalBranches =False 885 886defrun(self, args): 887iflen(args) !=1: 888return False 889 maxChange =int(args[0]) 890 891if"p4ExitCode"inp4Cmd("changes -m 1"): 892die("Problems executing p4"); 893 894if self.rollbackLocalBranches: 895 refPrefix ="refs/heads/" 896 lines =read_pipe_lines("git rev-parse --symbolic --branches") 897else: 898 refPrefix ="refs/remotes/" 899 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 900 901for line in lines: 902if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 903 line = line.strip() 904 ref = refPrefix + line 905 log =extractLogMessageFromGitCommit(ref) 906 settings =extractSettingsGitLog(log) 907 908 depotPaths = settings['depot-paths'] 909 change = settings['change'] 910 911 changed =False 912 913iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 914for p in depotPaths]))) ==0: 915print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 916system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 917continue 918 919while change andint(change) > maxChange: 920 changed =True 921if self.verbose: 922print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 923system("git update-ref%s\"%s^\""% (ref, ref)) 924 log =extractLogMessageFromGitCommit(ref) 925 settings =extractSettingsGitLog(log) 926 927 928 depotPaths = settings['depot-paths'] 929 change = settings['change'] 930 931if changed: 932print"%srewound to%s"% (ref, change) 933 934return True 935 936classP4Submit(Command, P4UserMap): 937 938 conflict_behavior_choices = ("ask","skip","quit") 939 940def__init__(self): 941 Command.__init__(self) 942 P4UserMap.__init__(self) 943 self.options = [ 944 optparse.make_option("--origin", dest="origin"), 945 optparse.make_option("-M", dest="detectRenames", action="store_true"), 946# preserve the user, requires relevant p4 permissions 947 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 948 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 949 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 950 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 951 optparse.make_option("--conflict", dest="conflict_behavior", 952 choices=self.conflict_behavior_choices), 953 optparse.make_option("--branch", dest="branch"), 954] 955 self.description ="Submit changes from git to the perforce depot." 956 self.usage +=" [name of git branch to submit into perforce depot]" 957 self.origin ="" 958 self.detectRenames =False 959 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 960 self.dry_run =False 961 self.prepare_p4_only =False 962 self.conflict_behavior =None 963 self.isWindows = (platform.system() =="Windows") 964 self.exportLabels =False 965 self.p4HasMoveCommand =p4_has_move_command() 966 self.branch =None 967 968defcheck(self): 969iflen(p4CmdList("opened ...")) >0: 970die("You have files opened with perforce! Close them before starting the sync.") 971 972defseparate_jobs_from_description(self, message): 973"""Extract and return a possible Jobs field in the commit 974 message. It goes into a separate section in the p4 change 975 specification. 976 977 A jobs line starts with "Jobs:" and looks like a new field 978 in a form. Values are white-space separated on the same 979 line or on following lines that start with a tab. 980 981 This does not parse and extract the full git commit message 982 like a p4 form. It just sees the Jobs: line as a marker 983 to pass everything from then on directly into the p4 form, 984 but outside the description section. 985 986 Return a tuple (stripped log message, jobs string).""" 987 988 m = re.search(r'^Jobs:', message, re.MULTILINE) 989if m is None: 990return(message,None) 991 992 jobtext = message[m.start():] 993 stripped_message = message[:m.start()].rstrip() 994return(stripped_message, jobtext) 995 996defprepareLogMessage(self, template, message, jobs): 997"""Edits the template returned from "p4 change -o" to insert 998 the message in the Description field, and the jobs text in 999 the Jobs field."""1000 result =""10011002 inDescriptionSection =False10031004for line in template.split("\n"):1005if line.startswith("#"):1006 result += line +"\n"1007continue10081009if inDescriptionSection:1010if line.startswith("Files:")or line.startswith("Jobs:"):1011 inDescriptionSection =False1012# insert Jobs section1013if jobs:1014 result += jobs +"\n"1015else:1016continue1017else:1018if line.startswith("Description:"):1019 inDescriptionSection =True1020 line +="\n"1021for messageLine in message.split("\n"):1022 line +="\t"+ messageLine +"\n"10231024 result += line +"\n"10251026return result10271028defpatchRCSKeywords(self,file, pattern):1029# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1030(handle, outFileName) = tempfile.mkstemp(dir='.')1031try:1032 outFile = os.fdopen(handle,"w+")1033 inFile =open(file,"r")1034 regexp = re.compile(pattern, re.VERBOSE)1035for line in inFile.readlines():1036 line = regexp.sub(r'$\1$', line)1037 outFile.write(line)1038 inFile.close()1039 outFile.close()1040# Forcibly overwrite the original file1041 os.unlink(file)1042 shutil.move(outFileName,file)1043except:1044# cleanup our temporary file1045 os.unlink(outFileName)1046print"Failed to strip RCS keywords in%s"%file1047raise10481049print"Patched up RCS keywords in%s"%file10501051defp4UserForCommit(self,id):1052# Return the tuple (perforce user,git email) for a given git commit id1053 self.getUserMapFromPerforceServer()1054 gitEmail =read_pipe(["git","log","--max-count=1",1055"--format=%ae",id])1056 gitEmail = gitEmail.strip()1057if not self.emails.has_key(gitEmail):1058return(None,gitEmail)1059else:1060return(self.emails[gitEmail],gitEmail)10611062defcheckValidP4Users(self,commits):1063# check if any git authors cannot be mapped to p4 users1064foridin commits:1065(user,email) = self.p4UserForCommit(id)1066if not user:1067 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1068ifgitConfig('git-p4.allowMissingP4Users').lower() =="true":1069print"%s"% msg1070else:1071die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10721073deflastP4Changelist(self):1074# Get back the last changelist number submitted in this client spec. This1075# then gets used to patch up the username in the change. If the same1076# client spec is being used by multiple processes then this might go1077# wrong.1078 results =p4CmdList("client -o")# find the current client1079 client =None1080for r in results:1081if r.has_key('Client'):1082 client = r['Client']1083break1084if not client:1085die("could not get client spec")1086 results =p4CmdList(["changes","-c", client,"-m","1"])1087for r in results:1088if r.has_key('change'):1089return r['change']1090die("Could not get changelist number for last submit - cannot patch up user details")10911092defmodifyChangelistUser(self, changelist, newUser):1093# fixup the user field of a changelist after it has been submitted.1094 changes =p4CmdList("change -o%s"% changelist)1095iflen(changes) !=1:1096die("Bad output from p4 change modifying%sto user%s"%1097(changelist, newUser))10981099 c = changes[0]1100if c['User'] == newUser:return# nothing to do1101 c['User'] = newUser1102input= marshal.dumps(c)11031104 result =p4CmdList("change -f -i", stdin=input)1105for r in result:1106if r.has_key('code'):1107if r['code'] =='error':1108die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1109if r.has_key('data'):1110print("Updated user field for changelist%sto%s"% (changelist, newUser))1111return1112die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11131114defcanChangeChangelists(self):1115# check to see if we have p4 admin or super-user permissions, either of1116# which are required to modify changelists.1117 results =p4CmdList(["protects", self.depotPath])1118for r in results:1119if r.has_key('perm'):1120if r['perm'] =='admin':1121return11122if r['perm'] =='super':1123return11124return011251126defprepareSubmitTemplate(self):1127"""Run "p4 change -o" to grab a change specification template.1128 This does not use "p4 -G", as it is nice to keep the submission1129 template in original order, since a human might edit it.11301131 Remove lines in the Files section that show changes to files1132 outside the depot path we're committing into."""11331134 template =""1135 inFilesSection =False1136for line inp4_read_pipe_lines(['change','-o']):1137if line.endswith("\r\n"):1138 line = line[:-2] +"\n"1139if inFilesSection:1140if line.startswith("\t"):1141# path starts and ends with a tab1142 path = line[1:]1143 lastTab = path.rfind("\t")1144if lastTab != -1:1145 path = path[:lastTab]1146if notp4PathStartsWith(path, self.depotPath):1147continue1148else:1149 inFilesSection =False1150else:1151if line.startswith("Files:"):1152 inFilesSection =True11531154 template += line11551156return template11571158defedit_template(self, template_file):1159"""Invoke the editor to let the user change the submission1160 message. Return true if okay to continue with the submit."""11611162# if configured to skip the editing part, just submit1163ifgitConfig("git-p4.skipSubmitEdit") =="true":1164return True11651166# look at the modification time, to check later if the user saved1167# the file1168 mtime = os.stat(template_file).st_mtime11691170# invoke the editor1171if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1172 editor = os.environ.get("P4EDITOR")1173else:1174 editor =read_pipe("git var GIT_EDITOR").strip()1175system(editor +" "+ template_file)11761177# If the file was not saved, prompt to see if this patch should1178# be skipped. But skip this verification step if configured so.1179ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1180return True11811182# modification time updated means user saved the file1183if os.stat(template_file).st_mtime > mtime:1184return True11851186while True:1187 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1188if response =='y':1189return True1190if response =='n':1191return False11921193defapplyCommit(self,id):1194"""Apply one commit, return True if it succeeded."""11951196print"Applying",read_pipe(["git","show","-s",1197"--format=format:%h%s",id])11981199(p4User, gitEmail) = self.p4UserForCommit(id)12001201 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1202 filesToAdd =set()1203 filesToDelete =set()1204 editedFiles =set()1205 pureRenameCopy =set()1206 filesToChangeExecBit = {}12071208for line in diff:1209 diff =parseDiffTreeEntry(line)1210 modifier = diff['status']1211 path = diff['src']1212if modifier =="M":1213p4_edit(path)1214ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1215 filesToChangeExecBit[path] = diff['dst_mode']1216 editedFiles.add(path)1217elif modifier =="A":1218 filesToAdd.add(path)1219 filesToChangeExecBit[path] = diff['dst_mode']1220if path in filesToDelete:1221 filesToDelete.remove(path)1222elif modifier =="D":1223 filesToDelete.add(path)1224if path in filesToAdd:1225 filesToAdd.remove(path)1226elif modifier =="C":1227 src, dest = diff['src'], diff['dst']1228p4_integrate(src, dest)1229 pureRenameCopy.add(dest)1230if diff['src_sha1'] != diff['dst_sha1']:1231p4_edit(dest)1232 pureRenameCopy.discard(dest)1233ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1234p4_edit(dest)1235 pureRenameCopy.discard(dest)1236 filesToChangeExecBit[dest] = diff['dst_mode']1237if self.isWindows:1238# turn off read-only attribute1239 os.chmod(dest, stat.S_IWRITE)1240 os.unlink(dest)1241 editedFiles.add(dest)1242elif modifier =="R":1243 src, dest = diff['src'], diff['dst']1244if self.p4HasMoveCommand:1245p4_edit(src)# src must be open before move1246p4_move(src, dest)# opens for (move/delete, move/add)1247else:1248p4_integrate(src, dest)1249if diff['src_sha1'] != diff['dst_sha1']:1250p4_edit(dest)1251else:1252 pureRenameCopy.add(dest)1253ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1254if not self.p4HasMoveCommand:1255p4_edit(dest)# with move: already open, writable1256 filesToChangeExecBit[dest] = diff['dst_mode']1257if not self.p4HasMoveCommand:1258if self.isWindows:1259 os.chmod(dest, stat.S_IWRITE)1260 os.unlink(dest)1261 filesToDelete.add(src)1262 editedFiles.add(dest)1263else:1264die("unknown modifier%sfor%s"% (modifier, path))12651266 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1267 patchcmd = diffcmd +" | git apply "1268 tryPatchCmd = patchcmd +"--check -"1269 applyPatchCmd = patchcmd +"--check --apply -"1270 patch_succeeded =True12711272if os.system(tryPatchCmd) !=0:1273 fixed_rcs_keywords =False1274 patch_succeeded =False1275print"Unfortunately applying the change failed!"12761277# Patch failed, maybe it's just RCS keyword woes. Look through1278# the patch to see if that's possible.1279ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1280file=None1281 pattern =None1282 kwfiles = {}1283forfilein editedFiles | filesToDelete:1284# did this file's delta contain RCS keywords?1285 pattern =p4_keywords_regexp_for_file(file)12861287if pattern:1288# this file is a possibility...look for RCS keywords.1289 regexp = re.compile(pattern, re.VERBOSE)1290for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1291if regexp.search(line):1292if verbose:1293print"got keyword match on%sin%sin%s"% (pattern, line,file)1294 kwfiles[file] = pattern1295break12961297forfilein kwfiles:1298if verbose:1299print"zapping%swith%s"% (line,pattern)1300# File is being deleted, so not open in p4. Must1301# disable the read-only bit on windows.1302if self.isWindows andfilenot in editedFiles:1303 os.chmod(file, stat.S_IWRITE)1304 self.patchRCSKeywords(file, kwfiles[file])1305 fixed_rcs_keywords =True13061307if fixed_rcs_keywords:1308print"Retrying the patch with RCS keywords cleaned up"1309if os.system(tryPatchCmd) ==0:1310 patch_succeeded =True13111312if not patch_succeeded:1313for f in editedFiles:1314p4_revert(f)1315return False13161317#1318# Apply the patch for real, and do add/delete/+x handling.1319#1320system(applyPatchCmd)13211322for f in filesToAdd:1323p4_add(f)1324for f in filesToDelete:1325p4_revert(f)1326p4_delete(f)13271328# Set/clear executable bits1329for f in filesToChangeExecBit.keys():1330 mode = filesToChangeExecBit[f]1331setP4ExecBit(f, mode)13321333#1334# Build p4 change description, starting with the contents1335# of the git commit message.1336#1337 logMessage =extractLogMessageFromGitCommit(id)1338 logMessage = logMessage.strip()1339(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13401341 template = self.prepareSubmitTemplate()1342 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13431344if self.preserveUser:1345 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13461347if self.checkAuthorship and not self.p4UserIsMe(p4User):1348 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1349 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1350 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13511352 separatorLine ="######## everything below this line is just the diff #######\n"13531354# diff1355if os.environ.has_key("P4DIFF"):1356del(os.environ["P4DIFF"])1357 diff =""1358for editedFile in editedFiles:1359 diff +=p4_read_pipe(['diff','-du',1360wildcard_encode(editedFile)])13611362# new file diff1363 newdiff =""1364for newFile in filesToAdd:1365 newdiff +="==== new file ====\n"1366 newdiff +="--- /dev/null\n"1367 newdiff +="+++%s\n"% newFile1368 f =open(newFile,"r")1369for line in f.readlines():1370 newdiff +="+"+ line1371 f.close()13721373# change description file: submitTemplate, separatorLine, diff, newdiff1374(handle, fileName) = tempfile.mkstemp()1375 tmpFile = os.fdopen(handle,"w+")1376if self.isWindows:1377 submitTemplate = submitTemplate.replace("\n","\r\n")1378 separatorLine = separatorLine.replace("\n","\r\n")1379 newdiff = newdiff.replace("\n","\r\n")1380 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1381 tmpFile.close()13821383if self.prepare_p4_only:1384#1385# Leave the p4 tree prepared, and the submit template around1386# and let the user decide what to do next1387#1388print1389print"P4 workspace prepared for submission."1390print"To submit or revert, go to client workspace"1391print" "+ self.clientPath1392print1393print"To submit, use\"p4 submit\"to write a new description,"1394print"or\"p4 submit -i%s\"to use the one prepared by" \1395"\"git p4\"."% fileName1396print"You can delete the file\"%s\"when finished."% fileName13971398if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1399print"To preserve change ownership by user%s, you must\n" \1400"do\"p4 change -f <change>\"after submitting and\n" \1401"edit the User field."1402if pureRenameCopy:1403print"After submitting, renamed files must be re-synced."1404print"Invoke\"p4 sync -f\"on each of these files:"1405for f in pureRenameCopy:1406print" "+ f14071408print1409print"To revert the changes, use\"p4 revert ...\", and delete"1410print"the submit template file\"%s\""% fileName1411if filesToAdd:1412print"Since the commit adds new files, they must be deleted:"1413for f in filesToAdd:1414print" "+ f1415print1416return True14171418#1419# Let the user edit the change description, then submit it.1420#1421if self.edit_template(fileName):1422# read the edited message and submit1423 ret =True1424 tmpFile =open(fileName,"rb")1425 message = tmpFile.read()1426 tmpFile.close()1427 submitTemplate = message[:message.index(separatorLine)]1428if self.isWindows:1429 submitTemplate = submitTemplate.replace("\r\n","\n")1430p4_write_pipe(['submit','-i'], submitTemplate)14311432if self.preserveUser:1433if p4User:1434# Get last changelist number. Cannot easily get it from1435# the submit command output as the output is1436# unmarshalled.1437 changelist = self.lastP4Changelist()1438 self.modifyChangelistUser(changelist, p4User)14391440# The rename/copy happened by applying a patch that created a1441# new file. This leaves it writable, which confuses p4.1442for f in pureRenameCopy:1443p4_sync(f,"-f")14441445else:1446# skip this patch1447 ret =False1448print"Submission cancelled, undoing p4 changes."1449for f in editedFiles:1450p4_revert(f)1451for f in filesToAdd:1452p4_revert(f)1453 os.remove(f)1454for f in filesToDelete:1455p4_revert(f)14561457 os.remove(fileName)1458return ret14591460# Export git tags as p4 labels. Create a p4 label and then tag1461# with that.1462defexportGitTags(self, gitTags):1463 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1464iflen(validLabelRegexp) ==0:1465 validLabelRegexp = defaultLabelRegexp1466 m = re.compile(validLabelRegexp)14671468for name in gitTags:14691470if not m.match(name):1471if verbose:1472print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1473continue14741475# Get the p4 commit this corresponds to1476 logMessage =extractLogMessageFromGitCommit(name)1477 values =extractSettingsGitLog(logMessage)14781479if not values.has_key('change'):1480# a tag pointing to something not sent to p4; ignore1481if verbose:1482print"git tag%sdoes not give a p4 commit"% name1483continue1484else:1485 changelist = values['change']14861487# Get the tag details.1488 inHeader =True1489 isAnnotated =False1490 body = []1491for l inread_pipe_lines(["git","cat-file","-p", name]):1492 l = l.strip()1493if inHeader:1494if re.match(r'tag\s+', l):1495 isAnnotated =True1496elif re.match(r'\s*$', l):1497 inHeader =False1498continue1499else:1500 body.append(l)15011502if not isAnnotated:1503 body = ["lightweight tag imported by git p4\n"]15041505# Create the label - use the same view as the client spec we are using1506 clientSpec =getClientSpec()15071508 labelTemplate ="Label:%s\n"% name1509 labelTemplate +="Description:\n"1510for b in body:1511 labelTemplate +="\t"+ b +"\n"1512 labelTemplate +="View:\n"1513for mapping in clientSpec.mappings:1514 labelTemplate +="\t%s\n"% mapping.depot_side.path15151516if self.dry_run:1517print"Would create p4 label%sfor tag"% name1518elif self.prepare_p4_only:1519print"Not creating p4 label%sfor tag due to option" \1520" --prepare-p4-only"% name1521else:1522p4_write_pipe(["label","-i"], labelTemplate)15231524# Use the label1525p4_system(["tag","-l", name] +1526["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])15271528if verbose:1529print"created p4 label for tag%s"% name15301531defrun(self, args):1532iflen(args) ==0:1533 self.master =currentGitBranch()1534iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1535die("Detecting current git branch failed!")1536eliflen(args) ==1:1537 self.master = args[0]1538if notbranchExists(self.master):1539die("Branch%sdoes not exist"% self.master)1540else:1541return False15421543 allowSubmit =gitConfig("git-p4.allowSubmit")1544iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1545die("%sis not in git-p4.allowSubmit"% self.master)15461547[upstream, settings] =findUpstreamBranchPoint()1548 self.depotPath = settings['depot-paths'][0]1549iflen(self.origin) ==0:1550 self.origin = upstream15511552if self.preserveUser:1553if not self.canChangeChangelists():1554die("Cannot preserve user names without p4 super-user or admin permissions")15551556# if not set from the command line, try the config file1557if self.conflict_behavior is None:1558 val =gitConfig("git-p4.conflict")1559if val:1560if val not in self.conflict_behavior_choices:1561die("Invalid value '%s' for config git-p4.conflict"% val)1562else:1563 val ="ask"1564 self.conflict_behavior = val15651566if self.verbose:1567print"Origin branch is "+ self.origin15681569iflen(self.depotPath) ==0:1570print"Internal error: cannot locate perforce depot path from existing branches"1571 sys.exit(128)15721573 self.useClientSpec =False1574ifgitConfig("git-p4.useclientspec","--bool") =="true":1575 self.useClientSpec =True1576if self.useClientSpec:1577 self.clientSpecDirs =getClientSpec()15781579if self.useClientSpec:1580# all files are relative to the client spec1581 self.clientPath =getClientRoot()1582else:1583 self.clientPath =p4Where(self.depotPath)15841585if self.clientPath =="":1586die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15871588print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1589 self.oldWorkingDirectory = os.getcwd()15901591# ensure the clientPath exists1592 new_client_dir =False1593if not os.path.exists(self.clientPath):1594 new_client_dir =True1595 os.makedirs(self.clientPath)15961597chdir(self.clientPath)1598if self.dry_run:1599print"Would synchronize p4 checkout in%s"% self.clientPath1600else:1601print"Synchronizing p4 checkout..."1602if new_client_dir:1603# old one was destroyed, and maybe nobody told p41604p4_sync("...","-f")1605else:1606p4_sync("...")1607 self.check()16081609 commits = []1610for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1611 commits.append(line.strip())1612 commits.reverse()16131614if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1615 self.checkAuthorship =False1616else:1617 self.checkAuthorship =True16181619if self.preserveUser:1620 self.checkValidP4Users(commits)16211622#1623# Build up a set of options to be passed to diff when1624# submitting each commit to p4.1625#1626if self.detectRenames:1627# command-line -M arg1628 self.diffOpts ="-M"1629else:1630# If not explicitly set check the config variable1631 detectRenames =gitConfig("git-p4.detectRenames")16321633if detectRenames.lower() =="false"or detectRenames =="":1634 self.diffOpts =""1635elif detectRenames.lower() =="true":1636 self.diffOpts ="-M"1637else:1638 self.diffOpts ="-M%s"% detectRenames16391640# no command-line arg for -C or --find-copies-harder, just1641# config variables1642 detectCopies =gitConfig("git-p4.detectCopies")1643if detectCopies.lower() =="false"or detectCopies =="":1644pass1645elif detectCopies.lower() =="true":1646 self.diffOpts +=" -C"1647else:1648 self.diffOpts +=" -C%s"% detectCopies16491650ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1651 self.diffOpts +=" --find-copies-harder"16521653#1654# Apply the commits, one at a time. On failure, ask if should1655# continue to try the rest of the patches, or quit.1656#1657if self.dry_run:1658print"Would apply"1659 applied = []1660 last =len(commits) -11661for i, commit inenumerate(commits):1662if self.dry_run:1663print" ",read_pipe(["git","show","-s",1664"--format=format:%h%s", commit])1665 ok =True1666else:1667 ok = self.applyCommit(commit)1668if ok:1669 applied.append(commit)1670else:1671if self.prepare_p4_only and i < last:1672print"Processing only the first commit due to option" \1673" --prepare-p4-only"1674break1675if i < last:1676 quit =False1677while True:1678# prompt for what to do, or use the option/variable1679if self.conflict_behavior =="ask":1680print"What do you want to do?"1681 response =raw_input("[s]kip this commit but apply"1682" the rest, or [q]uit? ")1683if not response:1684continue1685elif self.conflict_behavior =="skip":1686 response ="s"1687elif self.conflict_behavior =="quit":1688 response ="q"1689else:1690die("Unknown conflict_behavior '%s'"%1691 self.conflict_behavior)16921693if response[0] =="s":1694print"Skipping this commit, but applying the rest"1695break1696if response[0] =="q":1697print"Quitting"1698 quit =True1699break1700if quit:1701break17021703chdir(self.oldWorkingDirectory)17041705if self.dry_run:1706pass1707elif self.prepare_p4_only:1708pass1709eliflen(commits) ==len(applied):1710print"All commits applied!"17111712 sync =P4Sync()1713if self.branch:1714 sync.branch = self.branch1715 sync.run([])17161717 rebase =P4Rebase()1718 rebase.rebase()17191720else:1721iflen(applied) ==0:1722print"No commits applied."1723else:1724print"Applied only the commits marked with '*':"1725for c in commits:1726if c in applied:1727 star ="*"1728else:1729 star =" "1730print star,read_pipe(["git","show","-s",1731"--format=format:%h%s", c])1732print"You will have to do 'git p4 sync' and rebase."17331734ifgitConfig("git-p4.exportLabels","--bool") =="true":1735 self.exportLabels =True17361737if self.exportLabels:1738 p4Labels =getP4Labels(self.depotPath)1739 gitTags =getGitTags()17401741 missingGitTags = gitTags - p4Labels1742 self.exportGitTags(missingGitTags)17431744# exit with error unless everything applied perfecly1745iflen(commits) !=len(applied):1746 sys.exit(1)17471748return True17491750classView(object):1751"""Represent a p4 view ("p4 help views"), and map files in a1752 repo according to the view."""17531754classPath(object):1755"""A depot or client path, possibly containing wildcards.1756 The only one supported is ... at the end, currently.1757 Initialize with the full path, with //depot or //client."""17581759def__init__(self, path, is_depot):1760 self.path = path1761 self.is_depot = is_depot1762 self.find_wildcards()1763# remember the prefix bit, useful for relative mappings1764 m = re.match("(//[^/]+/)", self.path)1765if not m:1766die("Path%sdoes not start with //prefix/"% self.path)1767 prefix = m.group(1)1768if not self.is_depot:1769# strip //client/ on client paths1770 self.path = self.path[len(prefix):]17711772deffind_wildcards(self):1773"""Make sure wildcards are valid, and set up internal1774 variables."""17751776 self.ends_triple_dot =False1777# There are three wildcards allowed in p4 views1778# (see "p4 help views"). This code knows how to1779# handle "..." (only at the end), but cannot deal with1780# "%%n" or "*". Only check the depot_side, as p4 should1781# validate that the client_side matches too.1782if re.search(r'%%[1-9]', self.path):1783die("Can't handle%%n wildcards in view:%s"% self.path)1784if self.path.find("*") >=0:1785die("Can't handle * wildcards in view:%s"% self.path)1786 triple_dot_index = self.path.find("...")1787if triple_dot_index >=0:1788if triple_dot_index !=len(self.path) -3:1789die("Can handle only single ... wildcard, at end:%s"%1790 self.path)1791 self.ends_triple_dot =True17921793defensure_compatible(self, other_path):1794"""Make sure the wildcards agree."""1795if self.ends_triple_dot != other_path.ends_triple_dot:1796die("Both paths must end with ... if either does;\n"+1797"paths:%s %s"% (self.path, other_path.path))17981799defmatch_wildcards(self, test_path):1800"""See if this test_path matches us, and fill in the value1801 of the wildcards if so. Returns a tuple of1802 (True|False, wildcards[]). For now, only the ... at end1803 is supported, so at most one wildcard."""1804if self.ends_triple_dot:1805 dotless = self.path[:-3]1806if test_path.startswith(dotless):1807 wildcard = test_path[len(dotless):]1808return(True, [ wildcard ])1809else:1810if test_path == self.path:1811return(True, [])1812return(False, [])18131814defmatch(self, test_path):1815"""Just return if it matches; don't bother with the wildcards."""1816 b, _ = self.match_wildcards(test_path)1817return b18181819deffill_in_wildcards(self, wildcards):1820"""Return the relative path, with the wildcards filled in1821 if there are any."""1822if self.ends_triple_dot:1823return self.path[:-3] + wildcards[0]1824else:1825return self.path18261827classMapping(object):1828def__init__(self, depot_side, client_side, overlay, exclude):1829# depot_side is without the trailing /... if it had one1830 self.depot_side = View.Path(depot_side, is_depot=True)1831 self.client_side = View.Path(client_side, is_depot=False)1832 self.overlay = overlay # started with "+"1833 self.exclude = exclude # started with "-"1834assert not(self.overlay and self.exclude)1835 self.depot_side.ensure_compatible(self.client_side)18361837def__str__(self):1838 c =" "1839if self.overlay:1840 c ="+"1841if self.exclude:1842 c ="-"1843return"View.Mapping:%s%s->%s"% \1844(c, self.depot_side.path, self.client_side.path)18451846defmap_depot_to_client(self, depot_path):1847"""Calculate the client path if using this mapping on the1848 given depot path; does not consider the effect of other1849 mappings in a view. Even excluded mappings are returned."""1850 matches, wildcards = self.depot_side.match_wildcards(depot_path)1851if not matches:1852return""1853 client_path = self.client_side.fill_in_wildcards(wildcards)1854return client_path18551856#1857# View methods1858#1859def__init__(self):1860 self.mappings = []18611862defappend(self, view_line):1863"""Parse a view line, splitting it into depot and client1864 sides. Append to self.mappings, preserving order."""18651866# Split the view line into exactly two words. P4 enforces1867# structure on these lines that simplifies this quite a bit.1868#1869# Either or both words may be double-quoted.1870# Single quotes do not matter.1871# Double-quote marks cannot occur inside the words.1872# A + or - prefix is also inside the quotes.1873# There are no quotes unless they contain a space.1874# The line is already white-space stripped.1875# The two words are separated by a single space.1876#1877if view_line[0] =='"':1878# First word is double quoted. Find its end.1879 close_quote_index = view_line.find('"',1)1880if close_quote_index <=0:1881die("No first-word closing quote found:%s"% view_line)1882 depot_side = view_line[1:close_quote_index]1883# skip closing quote and space1884 rhs_index = close_quote_index +1+11885else:1886 space_index = view_line.find(" ")1887if space_index <=0:1888die("No word-splitting space found:%s"% view_line)1889 depot_side = view_line[0:space_index]1890 rhs_index = space_index +118911892if view_line[rhs_index] =='"':1893# Second word is double quoted. Make sure there is a1894# double quote at the end too.1895if not view_line.endswith('"'):1896die("View line with rhs quote should end with one:%s"%1897 view_line)1898# skip the quotes1899 client_side = view_line[rhs_index+1:-1]1900else:1901 client_side = view_line[rhs_index:]19021903# prefix + means overlay on previous mapping1904 overlay =False1905if depot_side.startswith("+"):1906 overlay =True1907 depot_side = depot_side[1:]19081909# prefix - means exclude this path1910 exclude =False1911if depot_side.startswith("-"):1912 exclude =True1913 depot_side = depot_side[1:]19141915 m = View.Mapping(depot_side, client_side, overlay, exclude)1916 self.mappings.append(m)19171918defmap_in_client(self, depot_path):1919"""Return the relative location in the client where this1920 depot file should live. Returns "" if the file should1921 not be mapped in the client."""19221923 paths_filled = []1924 client_path =""19251926# look at later entries first1927for m in self.mappings[::-1]:19281929# see where will this path end up in the client1930 p = m.map_depot_to_client(depot_path)19311932if p =="":1933# Depot path does not belong in client. Must remember1934# this, as previous items should not cause files to1935# exist in this path either. Remember that the list is1936# being walked from the end, which has higher precedence.1937# Overlap mappings do not exclude previous mappings.1938if not m.overlay:1939 paths_filled.append(m.client_side)19401941else:1942# This mapping matched; no need to search any further.1943# But, the mapping could be rejected if the client path1944# has already been claimed by an earlier mapping (i.e.1945# one later in the list, which we are walking backwards).1946 already_mapped_in_client =False1947for f in paths_filled:1948# this is View.Path.match1949if f.match(p):1950 already_mapped_in_client =True1951break1952if not already_mapped_in_client:1953# Include this file, unless it is from a line that1954# explicitly said to exclude it.1955if not m.exclude:1956 client_path = p19571958# a match, even if rejected, always stops the search1959break19601961return client_path19621963classP4Sync(Command, P4UserMap):1964 delete_actions = ("delete","move/delete","purge")19651966def__init__(self):1967 Command.__init__(self)1968 P4UserMap.__init__(self)1969 self.options = [1970 optparse.make_option("--branch", dest="branch"),1971 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1972 optparse.make_option("--changesfile", dest="changesFile"),1973 optparse.make_option("--silent", dest="silent", action="store_true"),1974 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1975 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1976 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1977help="Import into refs/heads/ , not refs/remotes"),1978 optparse.make_option("--max-changes", dest="maxChanges"),1979 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1980help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1981 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1982help="Only sync files that are included in the Perforce Client Spec")1983]1984 self.description ="""Imports from Perforce into a git repository.\n1985 example:1986 //depot/my/project/ -- to import the current head1987 //depot/my/project/@all -- to import everything1988 //depot/my/project/@1,6 -- to import only from revision 1 to 619891990 (a ... is not needed in the path p4 specification, it's added implicitly)"""19911992 self.usage +=" //depot/path[@revRange]"1993 self.silent =False1994 self.createdBranches =set()1995 self.committedChanges =set()1996 self.branch =""1997 self.detectBranches =False1998 self.detectLabels =False1999 self.importLabels =False2000 self.changesFile =""2001 self.syncWithOrigin =True2002 self.importIntoRemotes =True2003 self.maxChanges =""2004 self.keepRepoPath =False2005 self.depotPaths =None2006 self.p4BranchesInGit = []2007 self.cloneExclude = []2008 self.useClientSpec =False2009 self.useClientSpec_from_options =False2010 self.clientSpecDirs =None2011 self.tempBranches = []2012 self.tempBranchLocation ="git-p4-tmp"20132014ifgitConfig("git-p4.syncFromOrigin") =="false":2015 self.syncWithOrigin =False20162017# Force a checkpoint in fast-import and wait for it to finish2018defcheckpoint(self):2019 self.gitStream.write("checkpoint\n\n")2020 self.gitStream.write("progress checkpoint\n\n")2021 out = self.gitOutput.readline()2022if self.verbose:2023print"checkpoint finished: "+ out20242025defextractFilesFromCommit(self, commit):2026 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2027for path in self.cloneExclude]2028 files = []2029 fnum =02030while commit.has_key("depotFile%s"% fnum):2031 path = commit["depotFile%s"% fnum]20322033if[p for p in self.cloneExclude2034ifp4PathStartsWith(path, p)]:2035 found =False2036else:2037 found = [p for p in self.depotPaths2038ifp4PathStartsWith(path, p)]2039if not found:2040 fnum = fnum +12041continue20422043file= {}2044file["path"] = path2045file["rev"] = commit["rev%s"% fnum]2046file["action"] = commit["action%s"% fnum]2047file["type"] = commit["type%s"% fnum]2048 files.append(file)2049 fnum = fnum +12050return files20512052defstripRepoPath(self, path, prefixes):2053"""When streaming files, this is called to map a p4 depot path2054 to where it should go in git. The prefixes are either2055 self.depotPaths, or self.branchPrefixes in the case of2056 branch detection."""20572058if self.useClientSpec:2059# branch detection moves files up a level (the branch name)2060# from what client spec interpretation gives2061 path = self.clientSpecDirs.map_in_client(path)2062if self.detectBranches:2063for b in self.knownBranches:2064if path.startswith(b +"/"):2065 path = path[len(b)+1:]20662067elif self.keepRepoPath:2068# Preserve everything in relative path name except leading2069# //depot/; just look at first prefix as they all should2070# be in the same depot.2071 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2072ifp4PathStartsWith(path, depot):2073 path = path[len(depot):]20742075else:2076for p in prefixes:2077ifp4PathStartsWith(path, p):2078 path = path[len(p):]2079break20802081 path =wildcard_decode(path)2082return path20832084defsplitFilesIntoBranches(self, commit):2085"""Look at each depotFile in the commit to figure out to what2086 branch it belongs."""20872088 branches = {}2089 fnum =02090while commit.has_key("depotFile%s"% fnum):2091 path = commit["depotFile%s"% fnum]2092 found = [p for p in self.depotPaths2093ifp4PathStartsWith(path, p)]2094if not found:2095 fnum = fnum +12096continue20972098file= {}2099file["path"] = path2100file["rev"] = commit["rev%s"% fnum]2101file["action"] = commit["action%s"% fnum]2102file["type"] = commit["type%s"% fnum]2103 fnum = fnum +121042105# start with the full relative path where this file would2106# go in a p4 client2107if self.useClientSpec:2108 relPath = self.clientSpecDirs.map_in_client(path)2109else:2110 relPath = self.stripRepoPath(path, self.depotPaths)21112112for branch in self.knownBranches.keys():2113# add a trailing slash so that a commit into qt/4.2foo2114# doesn't end up in qt/4.2, e.g.2115if relPath.startswith(branch +"/"):2116if branch not in branches:2117 branches[branch] = []2118 branches[branch].append(file)2119break21202121return branches21222123# output one file from the P4 stream2124# - helper for streamP4Files21252126defstreamOneP4File(self,file, contents):2127 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2128if verbose:2129 sys.stderr.write("%s\n"% relPath)21302131(type_base, type_mods) =split_p4_type(file["type"])21322133 git_mode ="100644"2134if"x"in type_mods:2135 git_mode ="100755"2136if type_base =="symlink":2137 git_mode ="120000"2138# p4 print on a symlink contains "target\n"; remove the newline2139 data =''.join(contents)2140 contents = [data[:-1]]21412142if type_base =="utf16":2143# p4 delivers different text in the python output to -G2144# than it does when using "print -o", or normal p4 client2145# operations. utf16 is converted to ascii or utf8, perhaps.2146# But ascii text saved as -t utf16 is completely mangled.2147# Invoke print -o to get the real contents.2148#2149# On windows, the newlines will always be mangled by print, so put2150# them back too. This is not needed to the cygwin windows version,2151# just the native "NT" type.2152#2153 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2154ifp4_version_string().find("/NT") >=0:2155 text = text.replace("\r\n","\n")2156 contents = [ text ]21572158if type_base =="apple":2159# Apple filetype files will be streamed as a concatenation of2160# its appledouble header and the contents. This is useless2161# on both macs and non-macs. If using "print -q -o xx", it2162# will create "xx" with the data, and "%xx" with the header.2163# This is also not very useful.2164#2165# Ideally, someday, this script can learn how to generate2166# appledouble files directly and import those to git, but2167# non-mac machines can never find a use for apple filetype.2168print"\nIgnoring apple filetype file%s"%file['depotFile']2169return21702171# Note that we do not try to de-mangle keywords on utf16 files,2172# even though in theory somebody may want that.2173 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2174if pattern:2175 regexp = re.compile(pattern, re.VERBOSE)2176 text =''.join(contents)2177 text = regexp.sub(r'$\1$', text)2178 contents = [ text ]21792180 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21812182# total length...2183 length =02184for d in contents:2185 length = length +len(d)21862187 self.gitStream.write("data%d\n"% length)2188for d in contents:2189 self.gitStream.write(d)2190 self.gitStream.write("\n")21912192defstreamOneP4Deletion(self,file):2193 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2194if verbose:2195 sys.stderr.write("delete%s\n"% relPath)2196 self.gitStream.write("D%s\n"% relPath)21972198# handle another chunk of streaming data2199defstreamP4FilesCb(self, marshalled):22002201# catch p4 errors and complain2202 err =None2203if"code"in marshalled:2204if marshalled["code"] =="error":2205if"data"in marshalled:2206 err = marshalled["data"].rstrip()2207if err:2208 f =None2209if self.stream_have_file_info:2210if"depotFile"in self.stream_file:2211 f = self.stream_file["depotFile"]2212# force a failure in fast-import, else an empty2213# commit will be made2214 self.gitStream.write("\n")2215 self.gitStream.write("die-now\n")2216 self.gitStream.close()2217# ignore errors, but make sure it exits first2218 self.importProcess.wait()2219if f:2220die("Error from p4 print for%s:%s"% (f, err))2221else:2222die("Error from p4 print:%s"% err)22232224if marshalled.has_key('depotFile')and self.stream_have_file_info:2225# start of a new file - output the old one first2226 self.streamOneP4File(self.stream_file, self.stream_contents)2227 self.stream_file = {}2228 self.stream_contents = []2229 self.stream_have_file_info =False22302231# pick up the new file information... for the2232# 'data' field we need to append to our array2233for k in marshalled.keys():2234if k =='data':2235 self.stream_contents.append(marshalled['data'])2236else:2237 self.stream_file[k] = marshalled[k]22382239 self.stream_have_file_info =True22402241# Stream directly from "p4 files" into "git fast-import"2242defstreamP4Files(self, files):2243 filesForCommit = []2244 filesToRead = []2245 filesToDelete = []22462247for f in files:2248# if using a client spec, only add the files that have2249# a path in the client2250if self.clientSpecDirs:2251if self.clientSpecDirs.map_in_client(f['path']) =="":2252continue22532254 filesForCommit.append(f)2255if f['action']in self.delete_actions:2256 filesToDelete.append(f)2257else:2258 filesToRead.append(f)22592260# deleted files...2261for f in filesToDelete:2262 self.streamOneP4Deletion(f)22632264iflen(filesToRead) >0:2265 self.stream_file = {}2266 self.stream_contents = []2267 self.stream_have_file_info =False22682269# curry self argument2270defstreamP4FilesCbSelf(entry):2271 self.streamP4FilesCb(entry)22722273 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22742275p4CmdList(["-x","-","print"],2276 stdin=fileArgs,2277 cb=streamP4FilesCbSelf)22782279# do the last chunk2280if self.stream_file.has_key('depotFile'):2281 self.streamOneP4File(self.stream_file, self.stream_contents)22822283defmake_email(self, userid):2284if userid in self.users:2285return self.users[userid]2286else:2287return"%s<a@b>"% userid22882289# Stream a p4 tag2290defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2291if verbose:2292print"writing tag%sfor commit%s"% (labelName, commit)2293 gitStream.write("tag%s\n"% labelName)2294 gitStream.write("from%s\n"% commit)22952296if labelDetails.has_key('Owner'):2297 owner = labelDetails["Owner"]2298else:2299 owner =None23002301# Try to use the owner of the p4 label, or failing that,2302# the current p4 user id.2303if owner:2304 email = self.make_email(owner)2305else:2306 email = self.make_email(self.p4UserId())2307 tagger ="%s %s %s"% (email, epoch, self.tz)23082309 gitStream.write("tagger%s\n"% tagger)23102311print"labelDetails=",labelDetails2312if labelDetails.has_key('Description'):2313 description = labelDetails['Description']2314else:2315 description ='Label from git p4'23162317 gitStream.write("data%d\n"%len(description))2318 gitStream.write(description)2319 gitStream.write("\n")23202321defcommit(self, details, files, branch, parent =""):2322 epoch = details["time"]2323 author = details["user"]23242325if self.verbose:2326print"commit into%s"% branch23272328# start with reading files; if that fails, we should not2329# create a commit.2330 new_files = []2331for f in files:2332if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2333 new_files.append(f)2334else:2335 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23362337 self.gitStream.write("commit%s\n"% branch)2338# gitStream.write("mark :%s\n" % details["change"])2339 self.committedChanges.add(int(details["change"]))2340 committer =""2341if author not in self.users:2342 self.getUserMapFromPerforceServer()2343 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23442345 self.gitStream.write("committer%s\n"% committer)23462347 self.gitStream.write("data <<EOT\n")2348 self.gitStream.write(details["desc"])2349 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2350(','.join(self.branchPrefixes), details["change"]))2351iflen(details['options']) >0:2352 self.gitStream.write(": options =%s"% details['options'])2353 self.gitStream.write("]\nEOT\n\n")23542355iflen(parent) >0:2356if self.verbose:2357print"parent%s"% parent2358 self.gitStream.write("from%s\n"% parent)23592360 self.streamP4Files(new_files)2361 self.gitStream.write("\n")23622363 change =int(details["change"])23642365if self.labels.has_key(change):2366 label = self.labels[change]2367 labelDetails = label[0]2368 labelRevisions = label[1]2369if self.verbose:2370print"Change%sis labelled%s"% (change, labelDetails)23712372 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2373for p in self.branchPrefixes])23742375iflen(files) ==len(labelRevisions):23762377 cleanedFiles = {}2378for info in files:2379if info["action"]in self.delete_actions:2380continue2381 cleanedFiles[info["depotFile"]] = info["rev"]23822383if cleanedFiles == labelRevisions:2384 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23852386else:2387if not self.silent:2388print("Tag%sdoes not match with change%s: files do not match."2389% (labelDetails["label"], change))23902391else:2392if not self.silent:2393print("Tag%sdoes not match with change%s: file count is different."2394% (labelDetails["label"], change))23952396# Build a dictionary of changelists and labels, for "detect-labels" option.2397defgetLabels(self):2398 self.labels = {}23992400 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2401iflen(l) >0and not self.silent:2402print"Finding files belonging to labels in%s"% `self.depotPaths`24032404for output in l:2405 label = output["label"]2406 revisions = {}2407 newestChange =02408if self.verbose:2409print"Querying files for label%s"% label2410forfileinp4CmdList(["files"] +2411["%s...@%s"% (p, label)2412for p in self.depotPaths]):2413 revisions[file["depotFile"]] =file["rev"]2414 change =int(file["change"])2415if change > newestChange:2416 newestChange = change24172418 self.labels[newestChange] = [output, revisions]24192420if self.verbose:2421print"Label changes:%s"% self.labels.keys()24222423# Import p4 labels as git tags. A direct mapping does not2424# exist, so assume that if all the files are at the same revision2425# then we can use that, or it's something more complicated we should2426# just ignore.2427defimportP4Labels(self, stream, p4Labels):2428if verbose:2429print"import p4 labels: "+' '.join(p4Labels)24302431 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2432 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2433iflen(validLabelRegexp) ==0:2434 validLabelRegexp = defaultLabelRegexp2435 m = re.compile(validLabelRegexp)24362437for name in p4Labels:2438 commitFound =False24392440if not m.match(name):2441if verbose:2442print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2443continue24442445if name in ignoredP4Labels:2446continue24472448 labelDetails =p4CmdList(['label',"-o", name])[0]24492450# get the most recent changelist for each file in this label2451 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2452for p in self.depotPaths])24532454if change.has_key('change'):2455# find the corresponding git commit; take the oldest commit2456 changelist =int(change['change'])2457 gitCommit =read_pipe(["git","rev-list","--max-count=1",2458"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2459iflen(gitCommit) ==0:2460print"could not find git commit for changelist%d"% changelist2461else:2462 gitCommit = gitCommit.strip()2463 commitFound =True2464# Convert from p4 time format2465try:2466 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2467exceptValueError:2468print"Could not convert label time%s"% labelDetails['Update']2469 tmwhen =124702471 when =int(time.mktime(tmwhen))2472 self.streamTag(stream, name, labelDetails, gitCommit, when)2473if verbose:2474print"p4 label%smapped to git commit%s"% (name, gitCommit)2475else:2476if verbose:2477print"Label%shas no changelists - possibly deleted?"% name24782479if not commitFound:2480# We can't import this label; don't try again as it will get very2481# expensive repeatedly fetching all the files for labels that will2482# never be imported. If the label is moved in the future, the2483# ignore will need to be removed manually.2484system(["git","config","--add","git-p4.ignoredP4Labels", name])24852486defguessProjectName(self):2487for p in self.depotPaths:2488if p.endswith("/"):2489 p = p[:-1]2490 p = p[p.strip().rfind("/") +1:]2491if not p.endswith("/"):2492 p +="/"2493return p24942495defgetBranchMapping(self):2496 lostAndFoundBranches =set()24972498 user =gitConfig("git-p4.branchUser")2499iflen(user) >0:2500 command ="branches -u%s"% user2501else:2502 command ="branches"25032504for info inp4CmdList(command):2505 details =p4Cmd(["branch","-o", info["branch"]])2506 viewIdx =02507while details.has_key("View%s"% viewIdx):2508 paths = details["View%s"% viewIdx].split(" ")2509 viewIdx = viewIdx +12510# require standard //depot/foo/... //depot/bar/... mapping2511iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2512continue2513 source = paths[0]2514 destination = paths[1]2515## HACK2516ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2517 source = source[len(self.depotPaths[0]):-4]2518 destination = destination[len(self.depotPaths[0]):-4]25192520if destination in self.knownBranches:2521if not self.silent:2522print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2523print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2524continue25252526 self.knownBranches[destination] = source25272528 lostAndFoundBranches.discard(destination)25292530if source not in self.knownBranches:2531 lostAndFoundBranches.add(source)25322533# Perforce does not strictly require branches to be defined, so we also2534# check git config for a branch list.2535#2536# Example of branch definition in git config file:2537# [git-p4]2538# branchList=main:branchA2539# branchList=main:branchB2540# branchList=branchA:branchC2541 configBranches =gitConfigList("git-p4.branchList")2542for branch in configBranches:2543if branch:2544(source, destination) = branch.split(":")2545 self.knownBranches[destination] = source25462547 lostAndFoundBranches.discard(destination)25482549if source not in self.knownBranches:2550 lostAndFoundBranches.add(source)255125522553for branch in lostAndFoundBranches:2554 self.knownBranches[branch] = branch25552556defgetBranchMappingFromGitBranches(self):2557 branches =p4BranchesInGit(self.importIntoRemotes)2558for branch in branches.keys():2559if branch =="master":2560 branch ="main"2561else:2562 branch = branch[len(self.projectName):]2563 self.knownBranches[branch] = branch25642565defupdateOptionDict(self, d):2566 option_keys = {}2567if self.keepRepoPath:2568 option_keys['keepRepoPath'] =125692570 d["options"] =' '.join(sorted(option_keys.keys()))25712572defreadOptions(self, d):2573 self.keepRepoPath = (d.has_key('options')2574and('keepRepoPath'in d['options']))25752576defgitRefForBranch(self, branch):2577if branch =="main":2578return self.refPrefix +"master"25792580iflen(branch) <=0:2581return branch25822583return self.refPrefix + self.projectName + branch25842585defgitCommitByP4Change(self, ref, change):2586if self.verbose:2587print"looking in ref "+ ref +" for change%susing bisect..."% change25882589 earliestCommit =""2590 latestCommit =parseRevision(ref)25912592while True:2593if self.verbose:2594print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2595 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2596iflen(next) ==0:2597if self.verbose:2598print"argh"2599return""2600 log =extractLogMessageFromGitCommit(next)2601 settings =extractSettingsGitLog(log)2602 currentChange =int(settings['change'])2603if self.verbose:2604print"current change%s"% currentChange26052606if currentChange == change:2607if self.verbose:2608print"found%s"% next2609return next26102611if currentChange < change:2612 earliestCommit ="^%s"% next2613else:2614 latestCommit ="%s"% next26152616return""26172618defimportNewBranch(self, branch, maxChange):2619# make fast-import flush all changes to disk and update the refs using the checkpoint2620# command so that we can try to find the branch parent in the git history2621 self.gitStream.write("checkpoint\n\n");2622 self.gitStream.flush();2623 branchPrefix = self.depotPaths[0] + branch +"/"2624range="@1,%s"% maxChange2625#print "prefix" + branchPrefix2626 changes =p4ChangesForPaths([branchPrefix],range)2627iflen(changes) <=0:2628return False2629 firstChange = changes[0]2630#print "first change in branch: %s" % firstChange2631 sourceBranch = self.knownBranches[branch]2632 sourceDepotPath = self.depotPaths[0] + sourceBranch2633 sourceRef = self.gitRefForBranch(sourceBranch)2634#print "source " + sourceBranch26352636 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2637#print "branch parent: %s" % branchParentChange2638 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2639iflen(gitParent) >0:2640 self.initialParents[self.gitRefForBranch(branch)] = gitParent2641#print "parent git commit: %s" % gitParent26422643 self.importChanges(changes)2644return True26452646defsearchParent(self, parent, branch, target):2647 parentFound =False2648for blob inread_pipe_lines(["git","rev-list","--reverse",2649"--no-merges", parent]):2650 blob = blob.strip()2651iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2652 parentFound =True2653if self.verbose:2654print"Found parent of%sin commit%s"% (branch, blob)2655break2656if parentFound:2657return blob2658else:2659return None26602661defimportChanges(self, changes):2662 cnt =12663for change in changes:2664 description =p4_describe(change)2665 self.updateOptionDict(description)26662667if not self.silent:2668 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2669 sys.stdout.flush()2670 cnt = cnt +126712672try:2673if self.detectBranches:2674 branches = self.splitFilesIntoBranches(description)2675for branch in branches.keys():2676## HACK --hwn2677 branchPrefix = self.depotPaths[0] + branch +"/"2678 self.branchPrefixes = [ branchPrefix ]26792680 parent =""26812682 filesForCommit = branches[branch]26832684if self.verbose:2685print"branch is%s"% branch26862687 self.updatedBranches.add(branch)26882689if branch not in self.createdBranches:2690 self.createdBranches.add(branch)2691 parent = self.knownBranches[branch]2692if parent == branch:2693 parent =""2694else:2695 fullBranch = self.projectName + branch2696if fullBranch not in self.p4BranchesInGit:2697if not self.silent:2698print("\nImporting new branch%s"% fullBranch);2699if self.importNewBranch(branch, change -1):2700 parent =""2701 self.p4BranchesInGit.append(fullBranch)2702if not self.silent:2703print("\nResuming with change%s"% change);27042705if self.verbose:2706print"parent determined through known branches:%s"% parent27072708 branch = self.gitRefForBranch(branch)2709 parent = self.gitRefForBranch(parent)27102711if self.verbose:2712print"looking for initial parent for%s; current parent is%s"% (branch, parent)27132714iflen(parent) ==0and branch in self.initialParents:2715 parent = self.initialParents[branch]2716del self.initialParents[branch]27172718 blob =None2719iflen(parent) >0:2720 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2721if self.verbose:2722print"Creating temporary branch: "+ tempBranch2723 self.commit(description, filesForCommit, tempBranch)2724 self.tempBranches.append(tempBranch)2725 self.checkpoint()2726 blob = self.searchParent(parent, branch, tempBranch)2727if blob:2728 self.commit(description, filesForCommit, branch, blob)2729else:2730if self.verbose:2731print"Parent of%snot found. Committing into head of%s"% (branch, parent)2732 self.commit(description, filesForCommit, branch, parent)2733else:2734 files = self.extractFilesFromCommit(description)2735 self.commit(description, files, self.branch,2736 self.initialParent)2737# only needed once, to connect to the previous commit2738 self.initialParent =""2739exceptIOError:2740print self.gitError.read()2741 sys.exit(1)27422743defimportHeadRevision(self, revision):2744print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27452746 details = {}2747 details["user"] ="git perforce import user"2748 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2749% (' '.join(self.depotPaths), revision))2750 details["change"] = revision2751 newestRevision =027522753 fileCnt =02754 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27552756for info inp4CmdList(["files"] + fileArgs):27572758if'code'in info and info['code'] =='error':2759 sys.stderr.write("p4 returned an error:%s\n"2760% info['data'])2761if info['data'].find("must refer to client") >=0:2762 sys.stderr.write("This particular p4 error is misleading.\n")2763 sys.stderr.write("Perhaps the depot path was misspelled.\n");2764 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2765 sys.exit(1)2766if'p4ExitCode'in info:2767 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2768 sys.exit(1)276927702771 change =int(info["change"])2772if change > newestRevision:2773 newestRevision = change27742775if info["action"]in self.delete_actions:2776# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2777#fileCnt = fileCnt + 12778continue27792780for prop in["depotFile","rev","action","type"]:2781 details["%s%s"% (prop, fileCnt)] = info[prop]27822783 fileCnt = fileCnt +127842785 details["change"] = newestRevision27862787# Use time from top-most change so that all git p4 clones of2788# the same p4 repo have the same commit SHA1s.2789 res =p4_describe(newestRevision)2790 details["time"] = res["time"]27912792 self.updateOptionDict(details)2793try:2794 self.commit(details, self.extractFilesFromCommit(details), self.branch)2795exceptIOError:2796print"IO error with git fast-import. Is your git version recent enough?"2797print self.gitError.read()279827992800defrun(self, args):2801 self.depotPaths = []2802 self.changeRange =""2803 self.previousDepotPaths = []2804 self.hasOrigin =False28052806# map from branch depot path to parent branch2807 self.knownBranches = {}2808 self.initialParents = {}28092810if self.importIntoRemotes:2811 self.refPrefix ="refs/remotes/p4/"2812else:2813 self.refPrefix ="refs/heads/p4/"28142815if self.syncWithOrigin:2816 self.hasOrigin =originP4BranchesExist()2817if self.hasOrigin:2818if not self.silent:2819print'Syncing with origin first, using "git fetch origin"'2820system("git fetch origin")28212822 branch_arg_given =bool(self.branch)2823iflen(self.branch) ==0:2824 self.branch = self.refPrefix +"master"2825ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2826system("git update-ref%srefs/heads/p4"% self.branch)2827system("git branch -D p4")28282829# accept either the command-line option, or the configuration variable2830if self.useClientSpec:2831# will use this after clone to set the variable2832 self.useClientSpec_from_options =True2833else:2834ifgitConfig("git-p4.useclientspec","--bool") =="true":2835 self.useClientSpec =True2836if self.useClientSpec:2837 self.clientSpecDirs =getClientSpec()28382839# TODO: should always look at previous commits,2840# merge with previous imports, if possible.2841if args == []:2842if self.hasOrigin:2843createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28442845# branches holds mapping from branch name to sha12846 branches =p4BranchesInGit(self.importIntoRemotes)28472848# restrict to just this one, disabling detect-branches2849if branch_arg_given:2850 short = self.branch.split("/")[-1]2851if short in branches:2852 self.p4BranchesInGit = [ short ]2853else:2854 self.p4BranchesInGit = branches.keys()28552856iflen(self.p4BranchesInGit) >1:2857if not self.silent:2858print"Importing from/into multiple branches"2859 self.detectBranches =True2860for branch in branches.keys():2861 self.initialParents[self.refPrefix + branch] = \2862 branches[branch]28632864if self.verbose:2865print"branches:%s"% self.p4BranchesInGit28662867 p4Change =02868for branch in self.p4BranchesInGit:2869 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28702871 settings =extractSettingsGitLog(logMsg)28722873 self.readOptions(settings)2874if(settings.has_key('depot-paths')2875and settings.has_key('change')):2876 change =int(settings['change']) +12877 p4Change =max(p4Change, change)28782879 depotPaths =sorted(settings['depot-paths'])2880if self.previousDepotPaths == []:2881 self.previousDepotPaths = depotPaths2882else:2883 paths = []2884for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2885 prev_list = prev.split("/")2886 cur_list = cur.split("/")2887for i inrange(0,min(len(cur_list),len(prev_list))):2888if cur_list[i] <> prev_list[i]:2889 i = i -12890break28912892 paths.append("/".join(cur_list[:i +1]))28932894 self.previousDepotPaths = paths28952896if p4Change >0:2897 self.depotPaths =sorted(self.previousDepotPaths)2898 self.changeRange ="@%s,#head"% p4Change2899if not self.silent and not self.detectBranches:2900print"Performing incremental import into%sgit branch"% self.branch29012902# accept multiple ref name abbreviations:2903# refs/foo/bar/branch -> use it exactly2904# p4/branch -> prepend refs/remotes/ or refs/heads/2905# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2906if not self.branch.startswith("refs/"):2907if self.importIntoRemotes:2908 prepend ="refs/remotes/"2909else:2910 prepend ="refs/heads/"2911if not self.branch.startswith("p4/"):2912 prepend +="p4/"2913 self.branch = prepend + self.branch29142915iflen(args) ==0and self.depotPaths:2916if not self.silent:2917print"Depot paths:%s"%' '.join(self.depotPaths)2918else:2919if self.depotPaths and self.depotPaths != args:2920print("previous import used depot path%sand now%swas specified. "2921"This doesn't work!"% (' '.join(self.depotPaths),2922' '.join(args)))2923 sys.exit(1)29242925 self.depotPaths =sorted(args)29262927 revision =""2928 self.users = {}29292930# Make sure no revision specifiers are used when --changesfile2931# is specified.2932 bad_changesfile =False2933iflen(self.changesFile) >0:2934for p in self.depotPaths:2935if p.find("@") >=0or p.find("#") >=0:2936 bad_changesfile =True2937break2938if bad_changesfile:2939die("Option --changesfile is incompatible with revision specifiers")29402941 newPaths = []2942for p in self.depotPaths:2943if p.find("@") != -1:2944 atIdx = p.index("@")2945 self.changeRange = p[atIdx:]2946if self.changeRange =="@all":2947 self.changeRange =""2948elif','not in self.changeRange:2949 revision = self.changeRange2950 self.changeRange =""2951 p = p[:atIdx]2952elif p.find("#") != -1:2953 hashIdx = p.index("#")2954 revision = p[hashIdx:]2955 p = p[:hashIdx]2956elif self.previousDepotPaths == []:2957# pay attention to changesfile, if given, else import2958# the entire p4 tree at the head revision2959iflen(self.changesFile) ==0:2960 revision ="#head"29612962 p = re.sub("\.\.\.$","", p)2963if not p.endswith("/"):2964 p +="/"29652966 newPaths.append(p)29672968 self.depotPaths = newPaths29692970# --detect-branches may change this for each branch2971 self.branchPrefixes = self.depotPaths29722973 self.loadUserMapFromCache()2974 self.labels = {}2975if self.detectLabels:2976 self.getLabels();29772978if self.detectBranches:2979## FIXME - what's a P4 projectName ?2980 self.projectName = self.guessProjectName()29812982if self.hasOrigin:2983 self.getBranchMappingFromGitBranches()2984else:2985 self.getBranchMapping()2986if self.verbose:2987print"p4-git branches:%s"% self.p4BranchesInGit2988print"initial parents:%s"% self.initialParents2989for b in self.p4BranchesInGit:2990if b !="master":29912992## FIXME2993 b = b[len(self.projectName):]2994 self.createdBranches.add(b)29952996 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29972998 self.importProcess = subprocess.Popen(["git","fast-import"],2999 stdin=subprocess.PIPE,3000 stdout=subprocess.PIPE,3001 stderr=subprocess.PIPE);3002 self.gitOutput = self.importProcess.stdout3003 self.gitStream = self.importProcess.stdin3004 self.gitError = self.importProcess.stderr30053006if revision:3007 self.importHeadRevision(revision)3008else:3009 changes = []30103011iflen(self.changesFile) >0:3012 output =open(self.changesFile).readlines()3013 changeSet =set()3014for line in output:3015 changeSet.add(int(line))30163017for change in changeSet:3018 changes.append(change)30193020 changes.sort()3021else:3022# catch "git p4 sync" with no new branches, in a repo that3023# does not have any existing p4 branches3024iflen(args) ==0:3025if not self.p4BranchesInGit:3026die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30273028# The default branch is master, unless --branch is used to3029# specify something else. Make sure it exists, or complain3030# nicely about how to use --branch.3031if not self.detectBranches:3032if notbranch_exists(self.branch):3033if branch_arg_given:3034die("Error: branch%sdoes not exist."% self.branch)3035else:3036die("Error: no branch%s; perhaps specify one with --branch."%3037 self.branch)30383039if self.verbose:3040print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3041 self.changeRange)3042 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30433044iflen(self.maxChanges) >0:3045 changes = changes[:min(int(self.maxChanges),len(changes))]30463047iflen(changes) ==0:3048if not self.silent:3049print"No changes to import!"3050else:3051if not self.silent and not self.detectBranches:3052print"Import destination:%s"% self.branch30533054 self.updatedBranches =set()30553056if not self.detectBranches:3057if args:3058# start a new branch3059 self.initialParent =""3060else:3061# build on a previous revision3062 self.initialParent =parseRevision(self.branch)30633064 self.importChanges(changes)30653066if not self.silent:3067print""3068iflen(self.updatedBranches) >0:3069 sys.stdout.write("Updated branches: ")3070for b in self.updatedBranches:3071 sys.stdout.write("%s"% b)3072 sys.stdout.write("\n")30733074ifgitConfig("git-p4.importLabels","--bool") =="true":3075 self.importLabels =True30763077if self.importLabels:3078 p4Labels =getP4Labels(self.depotPaths)3079 gitTags =getGitTags()30803081 missingP4Labels = p4Labels - gitTags3082 self.importP4Labels(self.gitStream, missingP4Labels)30833084 self.gitStream.close()3085if self.importProcess.wait() !=0:3086die("fast-import failed:%s"% self.gitError.read())3087 self.gitOutput.close()3088 self.gitError.close()30893090# Cleanup temporary branches created during import3091if self.tempBranches != []:3092for branch in self.tempBranches:3093read_pipe("git update-ref -d%s"% branch)3094 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30953096# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3097# a convenient shortcut refname "p4".3098if self.importIntoRemotes:3099 head_ref = self.refPrefix +"HEAD"3100if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3101system(["git","symbolic-ref", head_ref, self.branch])31023103return True31043105classP4Rebase(Command):3106def__init__(self):3107 Command.__init__(self)3108 self.options = [3109 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3110]3111 self.importLabels =False3112 self.description = ("Fetches the latest revision from perforce and "3113+"rebases the current work (branch) against it")31143115defrun(self, args):3116 sync =P4Sync()3117 sync.importLabels = self.importLabels3118 sync.run([])31193120return self.rebase()31213122defrebase(self):3123if os.system("git update-index --refresh") !=0:3124die("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.");3125iflen(read_pipe("git diff-index HEAD --")) >0:3126die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");31273128[upstream, settings] =findUpstreamBranchPoint()3129iflen(upstream) ==0:3130die("Cannot find upstream branchpoint for rebase")31313132# the branchpoint may be p4/foo~3, so strip off the parent3133 upstream = re.sub("~[0-9]+$","", upstream)31343135print"Rebasing the current branch onto%s"% upstream3136 oldHead =read_pipe("git rev-parse HEAD").strip()3137system("git rebase%s"% upstream)3138system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3139return True31403141classP4Clone(P4Sync):3142def__init__(self):3143 P4Sync.__init__(self)3144 self.description ="Creates a new git repository and imports from Perforce into it"3145 self.usage ="usage: %prog [options] //depot/path[@revRange]"3146 self.options += [3147 optparse.make_option("--destination", dest="cloneDestination",3148 action='store', default=None,3149help="where to leave result of the clone"),3150 optparse.make_option("-/", dest="cloneExclude",3151 action="append",type="string",3152help="exclude depot path"),3153 optparse.make_option("--bare", dest="cloneBare",3154 action="store_true", default=False),3155]3156 self.cloneDestination =None3157 self.needsGit =False3158 self.cloneBare =False31593160# This is required for the "append" cloneExclude action3161defensure_value(self, attr, value):3162if nothasattr(self, attr)orgetattr(self, attr)is None:3163setattr(self, attr, value)3164returngetattr(self, attr)31653166defdefaultDestination(self, args):3167## TODO: use common prefix of args?3168 depotPath = args[0]3169 depotDir = re.sub("(@[^@]*)$","", depotPath)3170 depotDir = re.sub("(#[^#]*)$","", depotDir)3171 depotDir = re.sub(r"\.\.\.$","", depotDir)3172 depotDir = re.sub(r"/$","", depotDir)3173return os.path.split(depotDir)[1]31743175defrun(self, args):3176iflen(args) <1:3177return False31783179if self.keepRepoPath and not self.cloneDestination:3180 sys.stderr.write("Must specify destination for --keep-path\n")3181 sys.exit(1)31823183 depotPaths = args31843185if not self.cloneDestination andlen(depotPaths) >1:3186 self.cloneDestination = depotPaths[-1]3187 depotPaths = depotPaths[:-1]31883189 self.cloneExclude = ["/"+p for p in self.cloneExclude]3190for p in depotPaths:3191if not p.startswith("//"):3192 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3193return False31943195if not self.cloneDestination:3196 self.cloneDestination = self.defaultDestination(args)31973198print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31993200if not os.path.exists(self.cloneDestination):3201 os.makedirs(self.cloneDestination)3202chdir(self.cloneDestination)32033204 init_cmd = ["git","init"]3205if self.cloneBare:3206 init_cmd.append("--bare")3207 subprocess.check_call(init_cmd)32083209if not P4Sync.run(self, depotPaths):3210return False32113212# create a master branch and check out a work tree3213ifgitBranchExists(self.branch):3214system(["git","branch","master", self.branch ])3215if not self.cloneBare:3216system(["git","checkout","-f"])3217else:3218print'Not checking out any branch, use ' \3219'"git checkout -q -b master <branch>"'32203221# auto-set this variable if invoked with --use-client-spec3222if self.useClientSpec_from_options:3223system("git config --bool git-p4.useclientspec true")32243225return True32263227classP4Branches(Command):3228def__init__(self):3229 Command.__init__(self)3230 self.options = [ ]3231 self.description = ("Shows the git branches that hold imports and their "3232+"corresponding perforce depot paths")3233 self.verbose =False32343235defrun(self, args):3236iforiginP4BranchesExist():3237createOrUpdateBranchesFromOrigin()32383239 cmdline ="git rev-parse --symbolic "3240 cmdline +=" --remotes"32413242for line inread_pipe_lines(cmdline):3243 line = line.strip()32443245if not line.startswith('p4/')or line =="p4/HEAD":3246continue3247 branch = line32483249 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3250 settings =extractSettingsGitLog(log)32513252print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3253return True32543255classHelpFormatter(optparse.IndentedHelpFormatter):3256def__init__(self):3257 optparse.IndentedHelpFormatter.__init__(self)32583259defformat_description(self, description):3260if description:3261return description +"\n"3262else:3263return""32643265defprintUsage(commands):3266print"usage:%s<command> [options]"% sys.argv[0]3267print""3268print"valid commands:%s"%", ".join(commands)3269print""3270print"Try%s<command> --help for command specific help."% sys.argv[0]3271print""32723273commands = {3274"debug": P4Debug,3275"submit": P4Submit,3276"commit": P4Submit,3277"sync": P4Sync,3278"rebase": P4Rebase,3279"clone": P4Clone,3280"rollback": P4RollBack,3281"branches": P4Branches3282}328332843285defmain():3286iflen(sys.argv[1:]) ==0:3287printUsage(commands.keys())3288 sys.exit(2)32893290 cmdName = sys.argv[1]3291try:3292 klass = commands[cmdName]3293 cmd =klass()3294exceptKeyError:3295print"unknown command%s"% cmdName3296print""3297printUsage(commands.keys())3298 sys.exit(2)32993300 options = cmd.options3301 cmd.gitdir = os.environ.get("GIT_DIR",None)33023303 args = sys.argv[2:]33043305 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3306if cmd.needsGit:3307 options.append(optparse.make_option("--git-dir", dest="gitdir"))33083309 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3310 options,3311 description = cmd.description,3312 formatter =HelpFormatter())33133314(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3315global verbose3316 verbose = cmd.verbose3317if cmd.needsGit:3318if cmd.gitdir ==None:3319 cmd.gitdir = os.path.abspath(".git")3320if notisValidGitDir(cmd.gitdir):3321 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3322if os.path.exists(cmd.gitdir):3323 cdup =read_pipe("git rev-parse --show-cdup").strip()3324iflen(cdup) >0:3325chdir(cdup);33263327if notisValidGitDir(cmd.gitdir):3328ifisValidGitDir(cmd.gitdir +"/.git"):3329 cmd.gitdir +="/.git"3330else:3331die("fatal: cannot locate git repository at%s"% cmd.gitdir)33323333 os.environ["GIT_DIR"] = cmd.gitdir33343335if not cmd.run(args):3336 parser.print_help()3337 sys.exit(2)333833393340if __name__ =='__main__':3341main()