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 = {} 563 564defgitConfig(key, args=None):# set args to "--bool", for instance 565if not _gitConfig.has_key(key): 566 cmd = ["git","config"] 567if args: 568assert(args =="--bool") 569 cmd.append(args) 570 cmd.append(key) 571 s =read_pipe(cmd, ignore_error=True) 572 _gitConfig[key] = s.strip() 573return _gitConfig[key] 574 575defgitConfigList(key): 576if not _gitConfig.has_key(key): 577 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 578 _gitConfig[key] = s.strip().split(os.linesep) 579return _gitConfig[key] 580 581defp4BranchesInGit(branchesAreInRemotes=True): 582"""Find all the branches whose names start with "p4/", looking 583 in remotes or heads as specified by the argument. Return 584 a dictionary of{ branch: revision }for each one found. 585 The branch names are the short names, without any 586 "p4/" prefix.""" 587 588 branches = {} 589 590 cmdline ="git rev-parse --symbolic " 591if branchesAreInRemotes: 592 cmdline +="--remotes" 593else: 594 cmdline +="--branches" 595 596for line inread_pipe_lines(cmdline): 597 line = line.strip() 598 599# only import to p4/ 600if not line.startswith('p4/'): 601continue 602# special symbolic ref to p4/master 603if line =="p4/HEAD": 604continue 605 606# strip off p4/ prefix 607 branch = line[len("p4/"):] 608 609 branches[branch] =parseRevision(line) 610 611return branches 612 613defbranch_exists(branch): 614"""Make sure that the given ref name really exists.""" 615 616 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 617 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 618 out, _ = p.communicate() 619if p.returncode: 620return False 621# expect exactly one line of output: the branch name 622return out.rstrip() == branch 623 624deffindUpstreamBranchPoint(head ="HEAD"): 625 branches =p4BranchesInGit() 626# map from depot-path to branch name 627 branchByDepotPath = {} 628for branch in branches.keys(): 629 tip = branches[branch] 630 log =extractLogMessageFromGitCommit(tip) 631 settings =extractSettingsGitLog(log) 632if settings.has_key("depot-paths"): 633 paths =",".join(settings["depot-paths"]) 634 branchByDepotPath[paths] ="remotes/p4/"+ branch 635 636 settings =None 637 parent =0 638while parent <65535: 639 commit = head +"~%s"% parent 640 log =extractLogMessageFromGitCommit(commit) 641 settings =extractSettingsGitLog(log) 642if settings.has_key("depot-paths"): 643 paths =",".join(settings["depot-paths"]) 644if branchByDepotPath.has_key(paths): 645return[branchByDepotPath[paths], settings] 646 647 parent = parent +1 648 649return["", settings] 650 651defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 652if not silent: 653print("Creating/updating branch(es) in%sbased on origin branch(es)" 654% localRefPrefix) 655 656 originPrefix ="origin/p4/" 657 658for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 659 line = line.strip() 660if(not line.startswith(originPrefix))or line.endswith("HEAD"): 661continue 662 663 headName = line[len(originPrefix):] 664 remoteHead = localRefPrefix + headName 665 originHead = line 666 667 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 668if(not original.has_key('depot-paths') 669or not original.has_key('change')): 670continue 671 672 update =False 673if notgitBranchExists(remoteHead): 674if verbose: 675print"creating%s"% remoteHead 676 update =True 677else: 678 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 679if settings.has_key('change') >0: 680if settings['depot-paths'] == original['depot-paths']: 681 originP4Change =int(original['change']) 682 p4Change =int(settings['change']) 683if originP4Change > p4Change: 684print("%s(%s) is newer than%s(%s). " 685"Updating p4 branch from origin." 686% (originHead, originP4Change, 687 remoteHead, p4Change)) 688 update =True 689else: 690print("Ignoring:%swas imported from%swhile " 691"%swas imported from%s" 692% (originHead,','.join(original['depot-paths']), 693 remoteHead,','.join(settings['depot-paths']))) 694 695if update: 696system("git update-ref%s %s"% (remoteHead, originHead)) 697 698deforiginP4BranchesExist(): 699returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 700 701defp4ChangesForPaths(depotPaths, changeRange): 702assert depotPaths 703 cmd = ['changes'] 704for p in depotPaths: 705 cmd += ["%s...%s"% (p, changeRange)] 706 output =p4_read_pipe_lines(cmd) 707 708 changes = {} 709for line in output: 710 changeNum =int(line.split(" ")[1]) 711 changes[changeNum] =True 712 713 changelist = changes.keys() 714 changelist.sort() 715return changelist 716 717defp4PathStartsWith(path, prefix): 718# This method tries to remedy a potential mixed-case issue: 719# 720# If UserA adds //depot/DirA/file1 721# and UserB adds //depot/dira/file2 722# 723# we may or may not have a problem. If you have core.ignorecase=true, 724# we treat DirA and dira as the same directory 725 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 726if ignorecase: 727return path.lower().startswith(prefix.lower()) 728return path.startswith(prefix) 729 730defgetClientSpec(): 731"""Look at the p4 client spec, create a View() object that contains 732 all the mappings, and return it.""" 733 734 specList =p4CmdList("client -o") 735iflen(specList) !=1: 736die('Output from "client -o" is%dlines, expecting 1'% 737len(specList)) 738 739# dictionary of all client parameters 740 entry = specList[0] 741 742# just the keys that start with "View" 743 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 744 745# hold this new View 746 view =View() 747 748# append the lines, in order, to the view 749for view_num inrange(len(view_keys)): 750 k ="View%d"% view_num 751if k not in view_keys: 752die("Expected view key%smissing"% k) 753 view.append(entry[k]) 754 755return view 756 757defgetClientRoot(): 758"""Grab the client directory.""" 759 760 output =p4CmdList("client -o") 761iflen(output) !=1: 762die('Output from "client -o" is%dlines, expecting 1'%len(output)) 763 764 entry = output[0] 765if"Root"not in entry: 766die('Client has no "Root"') 767 768return entry["Root"] 769 770# 771# P4 wildcards are not allowed in filenames. P4 complains 772# if you simply add them, but you can force it with "-f", in 773# which case it translates them into %xx encoding internally. 774# 775defwildcard_decode(path): 776# Search for and fix just these four characters. Do % last so 777# that fixing it does not inadvertently create new %-escapes. 778# Cannot have * in a filename in windows; untested as to 779# what p4 would do in such a case. 780if not platform.system() =="Windows": 781 path = path.replace("%2A","*") 782 path = path.replace("%23","#") \ 783.replace("%40","@") \ 784.replace("%25","%") 785return path 786 787defwildcard_encode(path): 788# do % first to avoid double-encoding the %s introduced here 789 path = path.replace("%","%25") \ 790.replace("*","%2A") \ 791.replace("#","%23") \ 792.replace("@","%40") 793return path 794 795defwildcard_present(path): 796return path.translate(None,"*#@%") != path 797 798class Command: 799def__init__(self): 800 self.usage ="usage: %prog [options]" 801 self.needsGit =True 802 self.verbose =False 803 804class P4UserMap: 805def__init__(self): 806 self.userMapFromPerforceServer =False 807 self.myP4UserId =None 808 809defp4UserId(self): 810if self.myP4UserId: 811return self.myP4UserId 812 813 results =p4CmdList("user -o") 814for r in results: 815if r.has_key('User'): 816 self.myP4UserId = r['User'] 817return r['User'] 818die("Could not find your p4 user id") 819 820defp4UserIsMe(self, p4User): 821# return True if the given p4 user is actually me 822 me = self.p4UserId() 823if not p4User or p4User != me: 824return False 825else: 826return True 827 828defgetUserCacheFilename(self): 829 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 830return home +"/.gitp4-usercache.txt" 831 832defgetUserMapFromPerforceServer(self): 833if self.userMapFromPerforceServer: 834return 835 self.users = {} 836 self.emails = {} 837 838for output inp4CmdList("users"): 839if not output.has_key("User"): 840continue 841 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 842 self.emails[output["Email"]] = output["User"] 843 844 845 s ='' 846for(key, val)in self.users.items(): 847 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 848 849open(self.getUserCacheFilename(),"wb").write(s) 850 self.userMapFromPerforceServer =True 851 852defloadUserMapFromCache(self): 853 self.users = {} 854 self.userMapFromPerforceServer =False 855try: 856 cache =open(self.getUserCacheFilename(),"rb") 857 lines = cache.readlines() 858 cache.close() 859for line in lines: 860 entry = line.strip().split("\t") 861 self.users[entry[0]] = entry[1] 862exceptIOError: 863 self.getUserMapFromPerforceServer() 864 865classP4Debug(Command): 866def__init__(self): 867 Command.__init__(self) 868 self.options = [] 869 self.description ="A tool to debug the output of p4 -G." 870 self.needsGit =False 871 872defrun(self, args): 873 j =0 874for output inp4CmdList(args): 875print'Element:%d'% j 876 j +=1 877print output 878return True 879 880classP4RollBack(Command): 881def__init__(self): 882 Command.__init__(self) 883 self.options = [ 884 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 885] 886 self.description ="A tool to debug the multi-branch import. Don't use :)" 887 self.rollbackLocalBranches =False 888 889defrun(self, args): 890iflen(args) !=1: 891return False 892 maxChange =int(args[0]) 893 894if"p4ExitCode"inp4Cmd("changes -m 1"): 895die("Problems executing p4"); 896 897if self.rollbackLocalBranches: 898 refPrefix ="refs/heads/" 899 lines =read_pipe_lines("git rev-parse --symbolic --branches") 900else: 901 refPrefix ="refs/remotes/" 902 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 903 904for line in lines: 905if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 906 line = line.strip() 907 ref = refPrefix + line 908 log =extractLogMessageFromGitCommit(ref) 909 settings =extractSettingsGitLog(log) 910 911 depotPaths = settings['depot-paths'] 912 change = settings['change'] 913 914 changed =False 915 916iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 917for p in depotPaths]))) ==0: 918print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 919system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 920continue 921 922while change andint(change) > maxChange: 923 changed =True 924if self.verbose: 925print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 926system("git update-ref%s\"%s^\""% (ref, ref)) 927 log =extractLogMessageFromGitCommit(ref) 928 settings =extractSettingsGitLog(log) 929 930 931 depotPaths = settings['depot-paths'] 932 change = settings['change'] 933 934if changed: 935print"%srewound to%s"% (ref, change) 936 937return True 938 939classP4Submit(Command, P4UserMap): 940 941 conflict_behavior_choices = ("ask","skip","quit") 942 943def__init__(self): 944 Command.__init__(self) 945 P4UserMap.__init__(self) 946 self.options = [ 947 optparse.make_option("--origin", dest="origin"), 948 optparse.make_option("-M", dest="detectRenames", action="store_true"), 949# preserve the user, requires relevant p4 permissions 950 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 951 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 952 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 953 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 954 optparse.make_option("--conflict", dest="conflict_behavior", 955 choices=self.conflict_behavior_choices), 956 optparse.make_option("--branch", dest="branch"), 957] 958 self.description ="Submit changes from git to the perforce depot." 959 self.usage +=" [name of git branch to submit into perforce depot]" 960 self.origin ="" 961 self.detectRenames =False 962 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 963 self.dry_run =False 964 self.prepare_p4_only =False 965 self.conflict_behavior =None 966 self.isWindows = (platform.system() =="Windows") 967 self.exportLabels =False 968 self.p4HasMoveCommand =p4_has_move_command() 969 self.branch =None 970 971defcheck(self): 972iflen(p4CmdList("opened ...")) >0: 973die("You have files opened with perforce! Close them before starting the sync.") 974 975defseparate_jobs_from_description(self, message): 976"""Extract and return a possible Jobs field in the commit 977 message. It goes into a separate section in the p4 change 978 specification. 979 980 A jobs line starts with "Jobs:" and looks like a new field 981 in a form. Values are white-space separated on the same 982 line or on following lines that start with a tab. 983 984 This does not parse and extract the full git commit message 985 like a p4 form. It just sees the Jobs: line as a marker 986 to pass everything from then on directly into the p4 form, 987 but outside the description section. 988 989 Return a tuple (stripped log message, jobs string).""" 990 991 m = re.search(r'^Jobs:', message, re.MULTILINE) 992if m is None: 993return(message,None) 994 995 jobtext = message[m.start():] 996 stripped_message = message[:m.start()].rstrip() 997return(stripped_message, jobtext) 998 999defprepareLogMessage(self, template, message, jobs):1000"""Edits the template returned from "p4 change -o" to insert1001 the message in the Description field, and the jobs text in1002 the Jobs field."""1003 result =""10041005 inDescriptionSection =False10061007for line in template.split("\n"):1008if line.startswith("#"):1009 result += line +"\n"1010continue10111012if inDescriptionSection:1013if line.startswith("Files:")or line.startswith("Jobs:"):1014 inDescriptionSection =False1015# insert Jobs section1016if jobs:1017 result += jobs +"\n"1018else:1019continue1020else:1021if line.startswith("Description:"):1022 inDescriptionSection =True1023 line +="\n"1024for messageLine in message.split("\n"):1025 line +="\t"+ messageLine +"\n"10261027 result += line +"\n"10281029return result10301031defpatchRCSKeywords(self,file, pattern):1032# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1033(handle, outFileName) = tempfile.mkstemp(dir='.')1034try:1035 outFile = os.fdopen(handle,"w+")1036 inFile =open(file,"r")1037 regexp = re.compile(pattern, re.VERBOSE)1038for line in inFile.readlines():1039 line = regexp.sub(r'$\1$', line)1040 outFile.write(line)1041 inFile.close()1042 outFile.close()1043# Forcibly overwrite the original file1044 os.unlink(file)1045 shutil.move(outFileName,file)1046except:1047# cleanup our temporary file1048 os.unlink(outFileName)1049print"Failed to strip RCS keywords in%s"%file1050raise10511052print"Patched up RCS keywords in%s"%file10531054defp4UserForCommit(self,id):1055# Return the tuple (perforce user,git email) for a given git commit id1056 self.getUserMapFromPerforceServer()1057 gitEmail =read_pipe(["git","log","--max-count=1",1058"--format=%ae",id])1059 gitEmail = gitEmail.strip()1060if not self.emails.has_key(gitEmail):1061return(None,gitEmail)1062else:1063return(self.emails[gitEmail],gitEmail)10641065defcheckValidP4Users(self,commits):1066# check if any git authors cannot be mapped to p4 users1067foridin commits:1068(user,email) = self.p4UserForCommit(id)1069if not user:1070 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1071ifgitConfig('git-p4.allowMissingP4Users').lower() =="true":1072print"%s"% msg1073else:1074die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10751076deflastP4Changelist(self):1077# Get back the last changelist number submitted in this client spec. This1078# then gets used to patch up the username in the change. If the same1079# client spec is being used by multiple processes then this might go1080# wrong.1081 results =p4CmdList("client -o")# find the current client1082 client =None1083for r in results:1084if r.has_key('Client'):1085 client = r['Client']1086break1087if not client:1088die("could not get client spec")1089 results =p4CmdList(["changes","-c", client,"-m","1"])1090for r in results:1091if r.has_key('change'):1092return r['change']1093die("Could not get changelist number for last submit - cannot patch up user details")10941095defmodifyChangelistUser(self, changelist, newUser):1096# fixup the user field of a changelist after it has been submitted.1097 changes =p4CmdList("change -o%s"% changelist)1098iflen(changes) !=1:1099die("Bad output from p4 change modifying%sto user%s"%1100(changelist, newUser))11011102 c = changes[0]1103if c['User'] == newUser:return# nothing to do1104 c['User'] = newUser1105input= marshal.dumps(c)11061107 result =p4CmdList("change -f -i", stdin=input)1108for r in result:1109if r.has_key('code'):1110if r['code'] =='error':1111die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1112if r.has_key('data'):1113print("Updated user field for changelist%sto%s"% (changelist, newUser))1114return1115die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11161117defcanChangeChangelists(self):1118# check to see if we have p4 admin or super-user permissions, either of1119# which are required to modify changelists.1120 results =p4CmdList(["protects", self.depotPath])1121for r in results:1122if r.has_key('perm'):1123if r['perm'] =='admin':1124return11125if r['perm'] =='super':1126return11127return011281129defprepareSubmitTemplate(self):1130"""Run "p4 change -o" to grab a change specification template.1131 This does not use "p4 -G", as it is nice to keep the submission1132 template in original order, since a human might edit it.11331134 Remove lines in the Files section that show changes to files1135 outside the depot path we're committing into."""11361137 template =""1138 inFilesSection =False1139for line inp4_read_pipe_lines(['change','-o']):1140if line.endswith("\r\n"):1141 line = line[:-2] +"\n"1142if inFilesSection:1143if line.startswith("\t"):1144# path starts and ends with a tab1145 path = line[1:]1146 lastTab = path.rfind("\t")1147if lastTab != -1:1148 path = path[:lastTab]1149if notp4PathStartsWith(path, self.depotPath):1150continue1151else:1152 inFilesSection =False1153else:1154if line.startswith("Files:"):1155 inFilesSection =True11561157 template += line11581159return template11601161defedit_template(self, template_file):1162"""Invoke the editor to let the user change the submission1163 message. Return true if okay to continue with the submit."""11641165# if configured to skip the editing part, just submit1166ifgitConfig("git-p4.skipSubmitEdit") =="true":1167return True11681169# look at the modification time, to check later if the user saved1170# the file1171 mtime = os.stat(template_file).st_mtime11721173# invoke the editor1174if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1175 editor = os.environ.get("P4EDITOR")1176else:1177 editor =read_pipe("git var GIT_EDITOR").strip()1178system(editor +" "+ template_file)11791180# If the file was not saved, prompt to see if this patch should1181# be skipped. But skip this verification step if configured so.1182ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1183return True11841185# modification time updated means user saved the file1186if os.stat(template_file).st_mtime > mtime:1187return True11881189while True:1190 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1191if response =='y':1192return True1193if response =='n':1194return False11951196defapplyCommit(self,id):1197"""Apply one commit, return True if it succeeded."""11981199print"Applying",read_pipe(["git","show","-s",1200"--format=format:%h%s",id])12011202(p4User, gitEmail) = self.p4UserForCommit(id)12031204 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1205 filesToAdd =set()1206 filesToDelete =set()1207 editedFiles =set()1208 pureRenameCopy =set()1209 filesToChangeExecBit = {}12101211for line in diff:1212 diff =parseDiffTreeEntry(line)1213 modifier = diff['status']1214 path = diff['src']1215if modifier =="M":1216p4_edit(path)1217ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1218 filesToChangeExecBit[path] = diff['dst_mode']1219 editedFiles.add(path)1220elif modifier =="A":1221 filesToAdd.add(path)1222 filesToChangeExecBit[path] = diff['dst_mode']1223if path in filesToDelete:1224 filesToDelete.remove(path)1225elif modifier =="D":1226 filesToDelete.add(path)1227if path in filesToAdd:1228 filesToAdd.remove(path)1229elif modifier =="C":1230 src, dest = diff['src'], diff['dst']1231p4_integrate(src, dest)1232 pureRenameCopy.add(dest)1233if diff['src_sha1'] != diff['dst_sha1']:1234p4_edit(dest)1235 pureRenameCopy.discard(dest)1236ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1237p4_edit(dest)1238 pureRenameCopy.discard(dest)1239 filesToChangeExecBit[dest] = diff['dst_mode']1240if self.isWindows:1241# turn off read-only attribute1242 os.chmod(dest, stat.S_IWRITE)1243 os.unlink(dest)1244 editedFiles.add(dest)1245elif modifier =="R":1246 src, dest = diff['src'], diff['dst']1247if self.p4HasMoveCommand:1248p4_edit(src)# src must be open before move1249p4_move(src, dest)# opens for (move/delete, move/add)1250else:1251p4_integrate(src, dest)1252if diff['src_sha1'] != diff['dst_sha1']:1253p4_edit(dest)1254else:1255 pureRenameCopy.add(dest)1256ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1257if not self.p4HasMoveCommand:1258p4_edit(dest)# with move: already open, writable1259 filesToChangeExecBit[dest] = diff['dst_mode']1260if not self.p4HasMoveCommand:1261if self.isWindows:1262 os.chmod(dest, stat.S_IWRITE)1263 os.unlink(dest)1264 filesToDelete.add(src)1265 editedFiles.add(dest)1266else:1267die("unknown modifier%sfor%s"% (modifier, path))12681269 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1270 patchcmd = diffcmd +" | git apply "1271 tryPatchCmd = patchcmd +"--check -"1272 applyPatchCmd = patchcmd +"--check --apply -"1273 patch_succeeded =True12741275if os.system(tryPatchCmd) !=0:1276 fixed_rcs_keywords =False1277 patch_succeeded =False1278print"Unfortunately applying the change failed!"12791280# Patch failed, maybe it's just RCS keyword woes. Look through1281# the patch to see if that's possible.1282ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1283file=None1284 pattern =None1285 kwfiles = {}1286forfilein editedFiles | filesToDelete:1287# did this file's delta contain RCS keywords?1288 pattern =p4_keywords_regexp_for_file(file)12891290if pattern:1291# this file is a possibility...look for RCS keywords.1292 regexp = re.compile(pattern, re.VERBOSE)1293for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1294if regexp.search(line):1295if verbose:1296print"got keyword match on%sin%sin%s"% (pattern, line,file)1297 kwfiles[file] = pattern1298break12991300forfilein kwfiles:1301if verbose:1302print"zapping%swith%s"% (line,pattern)1303# File is being deleted, so not open in p4. Must1304# disable the read-only bit on windows.1305if self.isWindows andfilenot in editedFiles:1306 os.chmod(file, stat.S_IWRITE)1307 self.patchRCSKeywords(file, kwfiles[file])1308 fixed_rcs_keywords =True13091310if fixed_rcs_keywords:1311print"Retrying the patch with RCS keywords cleaned up"1312if os.system(tryPatchCmd) ==0:1313 patch_succeeded =True13141315if not patch_succeeded:1316for f in editedFiles:1317p4_revert(f)1318return False13191320#1321# Apply the patch for real, and do add/delete/+x handling.1322#1323system(applyPatchCmd)13241325for f in filesToAdd:1326p4_add(f)1327for f in filesToDelete:1328p4_revert(f)1329p4_delete(f)13301331# Set/clear executable bits1332for f in filesToChangeExecBit.keys():1333 mode = filesToChangeExecBit[f]1334setP4ExecBit(f, mode)13351336#1337# Build p4 change description, starting with the contents1338# of the git commit message.1339#1340 logMessage =extractLogMessageFromGitCommit(id)1341 logMessage = logMessage.strip()1342(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13431344 template = self.prepareSubmitTemplate()1345 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13461347if self.preserveUser:1348 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13491350if self.checkAuthorship and not self.p4UserIsMe(p4User):1351 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1352 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1353 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13541355 separatorLine ="######## everything below this line is just the diff #######\n"13561357# diff1358if os.environ.has_key("P4DIFF"):1359del(os.environ["P4DIFF"])1360 diff =""1361for editedFile in editedFiles:1362 diff +=p4_read_pipe(['diff','-du',1363wildcard_encode(editedFile)])13641365# new file diff1366 newdiff =""1367for newFile in filesToAdd:1368 newdiff +="==== new file ====\n"1369 newdiff +="--- /dev/null\n"1370 newdiff +="+++%s\n"% newFile1371 f =open(newFile,"r")1372for line in f.readlines():1373 newdiff +="+"+ line1374 f.close()13751376# change description file: submitTemplate, separatorLine, diff, newdiff1377(handle, fileName) = tempfile.mkstemp()1378 tmpFile = os.fdopen(handle,"w+")1379if self.isWindows:1380 submitTemplate = submitTemplate.replace("\n","\r\n")1381 separatorLine = separatorLine.replace("\n","\r\n")1382 newdiff = newdiff.replace("\n","\r\n")1383 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1384 tmpFile.close()13851386if self.prepare_p4_only:1387#1388# Leave the p4 tree prepared, and the submit template around1389# and let the user decide what to do next1390#1391print1392print"P4 workspace prepared for submission."1393print"To submit or revert, go to client workspace"1394print" "+ self.clientPath1395print1396print"To submit, use\"p4 submit\"to write a new description,"1397print"or\"p4 submit -i%s\"to use the one prepared by" \1398"\"git p4\"."% fileName1399print"You can delete the file\"%s\"when finished."% fileName14001401if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1402print"To preserve change ownership by user%s, you must\n" \1403"do\"p4 change -f <change>\"after submitting and\n" \1404"edit the User field."1405if pureRenameCopy:1406print"After submitting, renamed files must be re-synced."1407print"Invoke\"p4 sync -f\"on each of these files:"1408for f in pureRenameCopy:1409print" "+ f14101411print1412print"To revert the changes, use\"p4 revert ...\", and delete"1413print"the submit template file\"%s\""% fileName1414if filesToAdd:1415print"Since the commit adds new files, they must be deleted:"1416for f in filesToAdd:1417print" "+ f1418print1419return True14201421#1422# Let the user edit the change description, then submit it.1423#1424if self.edit_template(fileName):1425# read the edited message and submit1426 ret =True1427 tmpFile =open(fileName,"rb")1428 message = tmpFile.read()1429 tmpFile.close()1430 submitTemplate = message[:message.index(separatorLine)]1431if self.isWindows:1432 submitTemplate = submitTemplate.replace("\r\n","\n")1433p4_write_pipe(['submit','-i'], submitTemplate)14341435if self.preserveUser:1436if p4User:1437# Get last changelist number. Cannot easily get it from1438# the submit command output as the output is1439# unmarshalled.1440 changelist = self.lastP4Changelist()1441 self.modifyChangelistUser(changelist, p4User)14421443# The rename/copy happened by applying a patch that created a1444# new file. This leaves it writable, which confuses p4.1445for f in pureRenameCopy:1446p4_sync(f,"-f")14471448else:1449# skip this patch1450 ret =False1451print"Submission cancelled, undoing p4 changes."1452for f in editedFiles:1453p4_revert(f)1454for f in filesToAdd:1455p4_revert(f)1456 os.remove(f)1457for f in filesToDelete:1458p4_revert(f)14591460 os.remove(fileName)1461return ret14621463# Export git tags as p4 labels. Create a p4 label and then tag1464# with that.1465defexportGitTags(self, gitTags):1466 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1467iflen(validLabelRegexp) ==0:1468 validLabelRegexp = defaultLabelRegexp1469 m = re.compile(validLabelRegexp)14701471for name in gitTags:14721473if not m.match(name):1474if verbose:1475print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1476continue14771478# Get the p4 commit this corresponds to1479 logMessage =extractLogMessageFromGitCommit(name)1480 values =extractSettingsGitLog(logMessage)14811482if not values.has_key('change'):1483# a tag pointing to something not sent to p4; ignore1484if verbose:1485print"git tag%sdoes not give a p4 commit"% name1486continue1487else:1488 changelist = values['change']14891490# Get the tag details.1491 inHeader =True1492 isAnnotated =False1493 body = []1494for l inread_pipe_lines(["git","cat-file","-p", name]):1495 l = l.strip()1496if inHeader:1497if re.match(r'tag\s+', l):1498 isAnnotated =True1499elif re.match(r'\s*$', l):1500 inHeader =False1501continue1502else:1503 body.append(l)15041505if not isAnnotated:1506 body = ["lightweight tag imported by git p4\n"]15071508# Create the label - use the same view as the client spec we are using1509 clientSpec =getClientSpec()15101511 labelTemplate ="Label:%s\n"% name1512 labelTemplate +="Description:\n"1513for b in body:1514 labelTemplate +="\t"+ b +"\n"1515 labelTemplate +="View:\n"1516for mapping in clientSpec.mappings:1517 labelTemplate +="\t%s\n"% mapping.depot_side.path15181519if self.dry_run:1520print"Would create p4 label%sfor tag"% name1521elif self.prepare_p4_only:1522print"Not creating p4 label%sfor tag due to option" \1523" --prepare-p4-only"% name1524else:1525p4_write_pipe(["label","-i"], labelTemplate)15261527# Use the label1528p4_system(["tag","-l", name] +1529["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])15301531if verbose:1532print"created p4 label for tag%s"% name15331534defrun(self, args):1535iflen(args) ==0:1536 self.master =currentGitBranch()1537iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1538die("Detecting current git branch failed!")1539eliflen(args) ==1:1540 self.master = args[0]1541if notbranchExists(self.master):1542die("Branch%sdoes not exist"% self.master)1543else:1544return False15451546 allowSubmit =gitConfig("git-p4.allowSubmit")1547iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1548die("%sis not in git-p4.allowSubmit"% self.master)15491550[upstream, settings] =findUpstreamBranchPoint()1551 self.depotPath = settings['depot-paths'][0]1552iflen(self.origin) ==0:1553 self.origin = upstream15541555if self.preserveUser:1556if not self.canChangeChangelists():1557die("Cannot preserve user names without p4 super-user or admin permissions")15581559# if not set from the command line, try the config file1560if self.conflict_behavior is None:1561 val =gitConfig("git-p4.conflict")1562if val:1563if val not in self.conflict_behavior_choices:1564die("Invalid value '%s' for config git-p4.conflict"% val)1565else:1566 val ="ask"1567 self.conflict_behavior = val15681569if self.verbose:1570print"Origin branch is "+ self.origin15711572iflen(self.depotPath) ==0:1573print"Internal error: cannot locate perforce depot path from existing branches"1574 sys.exit(128)15751576 self.useClientSpec =False1577ifgitConfig("git-p4.useclientspec","--bool") =="true":1578 self.useClientSpec =True1579if self.useClientSpec:1580 self.clientSpecDirs =getClientSpec()15811582if self.useClientSpec:1583# all files are relative to the client spec1584 self.clientPath =getClientRoot()1585else:1586 self.clientPath =p4Where(self.depotPath)15871588if self.clientPath =="":1589die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15901591print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1592 self.oldWorkingDirectory = os.getcwd()15931594# ensure the clientPath exists1595 new_client_dir =False1596if not os.path.exists(self.clientPath):1597 new_client_dir =True1598 os.makedirs(self.clientPath)15991600chdir(self.clientPath)1601if self.dry_run:1602print"Would synchronize p4 checkout in%s"% self.clientPath1603else:1604print"Synchronizing p4 checkout..."1605if new_client_dir:1606# old one was destroyed, and maybe nobody told p41607p4_sync("...","-f")1608else:1609p4_sync("...")1610 self.check()16111612 commits = []1613for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1614 commits.append(line.strip())1615 commits.reverse()16161617if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1618 self.checkAuthorship =False1619else:1620 self.checkAuthorship =True16211622if self.preserveUser:1623 self.checkValidP4Users(commits)16241625#1626# Build up a set of options to be passed to diff when1627# submitting each commit to p4.1628#1629if self.detectRenames:1630# command-line -M arg1631 self.diffOpts ="-M"1632else:1633# If not explicitly set check the config variable1634 detectRenames =gitConfig("git-p4.detectRenames")16351636if detectRenames.lower() =="false"or detectRenames =="":1637 self.diffOpts =""1638elif detectRenames.lower() =="true":1639 self.diffOpts ="-M"1640else:1641 self.diffOpts ="-M%s"% detectRenames16421643# no command-line arg for -C or --find-copies-harder, just1644# config variables1645 detectCopies =gitConfig("git-p4.detectCopies")1646if detectCopies.lower() =="false"or detectCopies =="":1647pass1648elif detectCopies.lower() =="true":1649 self.diffOpts +=" -C"1650else:1651 self.diffOpts +=" -C%s"% detectCopies16521653ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1654 self.diffOpts +=" --find-copies-harder"16551656#1657# Apply the commits, one at a time. On failure, ask if should1658# continue to try the rest of the patches, or quit.1659#1660if self.dry_run:1661print"Would apply"1662 applied = []1663 last =len(commits) -11664for i, commit inenumerate(commits):1665if self.dry_run:1666print" ",read_pipe(["git","show","-s",1667"--format=format:%h%s", commit])1668 ok =True1669else:1670 ok = self.applyCommit(commit)1671if ok:1672 applied.append(commit)1673else:1674if self.prepare_p4_only and i < last:1675print"Processing only the first commit due to option" \1676" --prepare-p4-only"1677break1678if i < last:1679 quit =False1680while True:1681# prompt for what to do, or use the option/variable1682if self.conflict_behavior =="ask":1683print"What do you want to do?"1684 response =raw_input("[s]kip this commit but apply"1685" the rest, or [q]uit? ")1686if not response:1687continue1688elif self.conflict_behavior =="skip":1689 response ="s"1690elif self.conflict_behavior =="quit":1691 response ="q"1692else:1693die("Unknown conflict_behavior '%s'"%1694 self.conflict_behavior)16951696if response[0] =="s":1697print"Skipping this commit, but applying the rest"1698break1699if response[0] =="q":1700print"Quitting"1701 quit =True1702break1703if quit:1704break17051706chdir(self.oldWorkingDirectory)17071708if self.dry_run:1709pass1710elif self.prepare_p4_only:1711pass1712eliflen(commits) ==len(applied):1713print"All commits applied!"17141715 sync =P4Sync()1716if self.branch:1717 sync.branch = self.branch1718 sync.run([])17191720 rebase =P4Rebase()1721 rebase.rebase()17221723else:1724iflen(applied) ==0:1725print"No commits applied."1726else:1727print"Applied only the commits marked with '*':"1728for c in commits:1729if c in applied:1730 star ="*"1731else:1732 star =" "1733print star,read_pipe(["git","show","-s",1734"--format=format:%h%s", c])1735print"You will have to do 'git p4 sync' and rebase."17361737ifgitConfig("git-p4.exportLabels","--bool") =="true":1738 self.exportLabels =True17391740if self.exportLabels:1741 p4Labels =getP4Labels(self.depotPath)1742 gitTags =getGitTags()17431744 missingGitTags = gitTags - p4Labels1745 self.exportGitTags(missingGitTags)17461747# exit with error unless everything applied perfecly1748iflen(commits) !=len(applied):1749 sys.exit(1)17501751return True17521753classView(object):1754"""Represent a p4 view ("p4 help views"), and map files in a1755 repo according to the view."""17561757classPath(object):1758"""A depot or client path, possibly containing wildcards.1759 The only one supported is ... at the end, currently.1760 Initialize with the full path, with //depot or //client."""17611762def__init__(self, path, is_depot):1763 self.path = path1764 self.is_depot = is_depot1765 self.find_wildcards()1766# remember the prefix bit, useful for relative mappings1767 m = re.match("(//[^/]+/)", self.path)1768if not m:1769die("Path%sdoes not start with //prefix/"% self.path)1770 prefix = m.group(1)1771if not self.is_depot:1772# strip //client/ on client paths1773 self.path = self.path[len(prefix):]17741775deffind_wildcards(self):1776"""Make sure wildcards are valid, and set up internal1777 variables."""17781779 self.ends_triple_dot =False1780# There are three wildcards allowed in p4 views1781# (see "p4 help views"). This code knows how to1782# handle "..." (only at the end), but cannot deal with1783# "%%n" or "*". Only check the depot_side, as p4 should1784# validate that the client_side matches too.1785if re.search(r'%%[1-9]', self.path):1786die("Can't handle%%n wildcards in view:%s"% self.path)1787if self.path.find("*") >=0:1788die("Can't handle * wildcards in view:%s"% self.path)1789 triple_dot_index = self.path.find("...")1790if triple_dot_index >=0:1791if triple_dot_index !=len(self.path) -3:1792die("Can handle only single ... wildcard, at end:%s"%1793 self.path)1794 self.ends_triple_dot =True17951796defensure_compatible(self, other_path):1797"""Make sure the wildcards agree."""1798if self.ends_triple_dot != other_path.ends_triple_dot:1799die("Both paths must end with ... if either does;\n"+1800"paths:%s %s"% (self.path, other_path.path))18011802defmatch_wildcards(self, test_path):1803"""See if this test_path matches us, and fill in the value1804 of the wildcards if so. Returns a tuple of1805 (True|False, wildcards[]). For now, only the ... at end1806 is supported, so at most one wildcard."""1807if self.ends_triple_dot:1808 dotless = self.path[:-3]1809if test_path.startswith(dotless):1810 wildcard = test_path[len(dotless):]1811return(True, [ wildcard ])1812else:1813if test_path == self.path:1814return(True, [])1815return(False, [])18161817defmatch(self, test_path):1818"""Just return if it matches; don't bother with the wildcards."""1819 b, _ = self.match_wildcards(test_path)1820return b18211822deffill_in_wildcards(self, wildcards):1823"""Return the relative path, with the wildcards filled in1824 if there are any."""1825if self.ends_triple_dot:1826return self.path[:-3] + wildcards[0]1827else:1828return self.path18291830classMapping(object):1831def__init__(self, depot_side, client_side, overlay, exclude):1832# depot_side is without the trailing /... if it had one1833 self.depot_side = View.Path(depot_side, is_depot=True)1834 self.client_side = View.Path(client_side, is_depot=False)1835 self.overlay = overlay # started with "+"1836 self.exclude = exclude # started with "-"1837assert not(self.overlay and self.exclude)1838 self.depot_side.ensure_compatible(self.client_side)18391840def__str__(self):1841 c =" "1842if self.overlay:1843 c ="+"1844if self.exclude:1845 c ="-"1846return"View.Mapping:%s%s->%s"% \1847(c, self.depot_side.path, self.client_side.path)18481849defmap_depot_to_client(self, depot_path):1850"""Calculate the client path if using this mapping on the1851 given depot path; does not consider the effect of other1852 mappings in a view. Even excluded mappings are returned."""1853 matches, wildcards = self.depot_side.match_wildcards(depot_path)1854if not matches:1855return""1856 client_path = self.client_side.fill_in_wildcards(wildcards)1857return client_path18581859#1860# View methods1861#1862def__init__(self):1863 self.mappings = []18641865defappend(self, view_line):1866"""Parse a view line, splitting it into depot and client1867 sides. Append to self.mappings, preserving order."""18681869# Split the view line into exactly two words. P4 enforces1870# structure on these lines that simplifies this quite a bit.1871#1872# Either or both words may be double-quoted.1873# Single quotes do not matter.1874# Double-quote marks cannot occur inside the words.1875# A + or - prefix is also inside the quotes.1876# There are no quotes unless they contain a space.1877# The line is already white-space stripped.1878# The two words are separated by a single space.1879#1880if view_line[0] =='"':1881# First word is double quoted. Find its end.1882 close_quote_index = view_line.find('"',1)1883if close_quote_index <=0:1884die("No first-word closing quote found:%s"% view_line)1885 depot_side = view_line[1:close_quote_index]1886# skip closing quote and space1887 rhs_index = close_quote_index +1+11888else:1889 space_index = view_line.find(" ")1890if space_index <=0:1891die("No word-splitting space found:%s"% view_line)1892 depot_side = view_line[0:space_index]1893 rhs_index = space_index +118941895if view_line[rhs_index] =='"':1896# Second word is double quoted. Make sure there is a1897# double quote at the end too.1898if not view_line.endswith('"'):1899die("View line with rhs quote should end with one:%s"%1900 view_line)1901# skip the quotes1902 client_side = view_line[rhs_index+1:-1]1903else:1904 client_side = view_line[rhs_index:]19051906# prefix + means overlay on previous mapping1907 overlay =False1908if depot_side.startswith("+"):1909 overlay =True1910 depot_side = depot_side[1:]19111912# prefix - means exclude this path1913 exclude =False1914if depot_side.startswith("-"):1915 exclude =True1916 depot_side = depot_side[1:]19171918 m = View.Mapping(depot_side, client_side, overlay, exclude)1919 self.mappings.append(m)19201921defmap_in_client(self, depot_path):1922"""Return the relative location in the client where this1923 depot file should live. Returns "" if the file should1924 not be mapped in the client."""19251926 paths_filled = []1927 client_path =""19281929# look at later entries first1930for m in self.mappings[::-1]:19311932# see where will this path end up in the client1933 p = m.map_depot_to_client(depot_path)19341935if p =="":1936# Depot path does not belong in client. Must remember1937# this, as previous items should not cause files to1938# exist in this path either. Remember that the list is1939# being walked from the end, which has higher precedence.1940# Overlap mappings do not exclude previous mappings.1941if not m.overlay:1942 paths_filled.append(m.client_side)19431944else:1945# This mapping matched; no need to search any further.1946# But, the mapping could be rejected if the client path1947# has already been claimed by an earlier mapping (i.e.1948# one later in the list, which we are walking backwards).1949 already_mapped_in_client =False1950for f in paths_filled:1951# this is View.Path.match1952if f.match(p):1953 already_mapped_in_client =True1954break1955if not already_mapped_in_client:1956# Include this file, unless it is from a line that1957# explicitly said to exclude it.1958if not m.exclude:1959 client_path = p19601961# a match, even if rejected, always stops the search1962break19631964return client_path19651966classP4Sync(Command, P4UserMap):1967 delete_actions = ("delete","move/delete","purge")19681969def__init__(self):1970 Command.__init__(self)1971 P4UserMap.__init__(self)1972 self.options = [1973 optparse.make_option("--branch", dest="branch"),1974 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1975 optparse.make_option("--changesfile", dest="changesFile"),1976 optparse.make_option("--silent", dest="silent", action="store_true"),1977 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1978 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1979 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1980help="Import into refs/heads/ , not refs/remotes"),1981 optparse.make_option("--max-changes", dest="maxChanges"),1982 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1983help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1984 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1985help="Only sync files that are included in the Perforce Client Spec")1986]1987 self.description ="""Imports from Perforce into a git repository.\n1988 example:1989 //depot/my/project/ -- to import the current head1990 //depot/my/project/@all -- to import everything1991 //depot/my/project/@1,6 -- to import only from revision 1 to 619921993 (a ... is not needed in the path p4 specification, it's added implicitly)"""19941995 self.usage +=" //depot/path[@revRange]"1996 self.silent =False1997 self.createdBranches =set()1998 self.committedChanges =set()1999 self.branch =""2000 self.detectBranches =False2001 self.detectLabels =False2002 self.importLabels =False2003 self.changesFile =""2004 self.syncWithOrigin =True2005 self.importIntoRemotes =True2006 self.maxChanges =""2007 self.keepRepoPath =False2008 self.depotPaths =None2009 self.p4BranchesInGit = []2010 self.cloneExclude = []2011 self.useClientSpec =False2012 self.useClientSpec_from_options =False2013 self.clientSpecDirs =None2014 self.tempBranches = []2015 self.tempBranchLocation ="git-p4-tmp"20162017ifgitConfig("git-p4.syncFromOrigin") =="false":2018 self.syncWithOrigin =False20192020# Force a checkpoint in fast-import and wait for it to finish2021defcheckpoint(self):2022 self.gitStream.write("checkpoint\n\n")2023 self.gitStream.write("progress checkpoint\n\n")2024 out = self.gitOutput.readline()2025if self.verbose:2026print"checkpoint finished: "+ out20272028defextractFilesFromCommit(self, commit):2029 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2030for path in self.cloneExclude]2031 files = []2032 fnum =02033while commit.has_key("depotFile%s"% fnum):2034 path = commit["depotFile%s"% fnum]20352036if[p for p in self.cloneExclude2037ifp4PathStartsWith(path, p)]:2038 found =False2039else:2040 found = [p for p in self.depotPaths2041ifp4PathStartsWith(path, p)]2042if not found:2043 fnum = fnum +12044continue20452046file= {}2047file["path"] = path2048file["rev"] = commit["rev%s"% fnum]2049file["action"] = commit["action%s"% fnum]2050file["type"] = commit["type%s"% fnum]2051 files.append(file)2052 fnum = fnum +12053return files20542055defstripRepoPath(self, path, prefixes):2056"""When streaming files, this is called to map a p4 depot path2057 to where it should go in git. The prefixes are either2058 self.depotPaths, or self.branchPrefixes in the case of2059 branch detection."""20602061if self.useClientSpec:2062# branch detection moves files up a level (the branch name)2063# from what client spec interpretation gives2064 path = self.clientSpecDirs.map_in_client(path)2065if self.detectBranches:2066for b in self.knownBranches:2067if path.startswith(b +"/"):2068 path = path[len(b)+1:]20692070elif self.keepRepoPath:2071# Preserve everything in relative path name except leading2072# //depot/; just look at first prefix as they all should2073# be in the same depot.2074 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2075ifp4PathStartsWith(path, depot):2076 path = path[len(depot):]20772078else:2079for p in prefixes:2080ifp4PathStartsWith(path, p):2081 path = path[len(p):]2082break20832084 path =wildcard_decode(path)2085return path20862087defsplitFilesIntoBranches(self, commit):2088"""Look at each depotFile in the commit to figure out to what2089 branch it belongs."""20902091 branches = {}2092 fnum =02093while commit.has_key("depotFile%s"% fnum):2094 path = commit["depotFile%s"% fnum]2095 found = [p for p in self.depotPaths2096ifp4PathStartsWith(path, p)]2097if not found:2098 fnum = fnum +12099continue21002101file= {}2102file["path"] = path2103file["rev"] = commit["rev%s"% fnum]2104file["action"] = commit["action%s"% fnum]2105file["type"] = commit["type%s"% fnum]2106 fnum = fnum +121072108# start with the full relative path where this file would2109# go in a p4 client2110if self.useClientSpec:2111 relPath = self.clientSpecDirs.map_in_client(path)2112else:2113 relPath = self.stripRepoPath(path, self.depotPaths)21142115for branch in self.knownBranches.keys():2116# add a trailing slash so that a commit into qt/4.2foo2117# doesn't end up in qt/4.2, e.g.2118if relPath.startswith(branch +"/"):2119if branch not in branches:2120 branches[branch] = []2121 branches[branch].append(file)2122break21232124return branches21252126# output one file from the P4 stream2127# - helper for streamP4Files21282129defstreamOneP4File(self,file, contents):2130 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2131if verbose:2132 sys.stderr.write("%s\n"% relPath)21332134(type_base, type_mods) =split_p4_type(file["type"])21352136 git_mode ="100644"2137if"x"in type_mods:2138 git_mode ="100755"2139if type_base =="symlink":2140 git_mode ="120000"2141# p4 print on a symlink contains "target\n"; remove the newline2142 data =''.join(contents)2143 contents = [data[:-1]]21442145if type_base =="utf16":2146# p4 delivers different text in the python output to -G2147# than it does when using "print -o", or normal p4 client2148# operations. utf16 is converted to ascii or utf8, perhaps.2149# But ascii text saved as -t utf16 is completely mangled.2150# Invoke print -o to get the real contents.2151#2152# On windows, the newlines will always be mangled by print, so put2153# them back too. This is not needed to the cygwin windows version,2154# just the native "NT" type.2155#2156 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2157ifp4_version_string().find("/NT") >=0:2158 text = text.replace("\r\n","\n")2159 contents = [ text ]21602161if type_base =="apple":2162# Apple filetype files will be streamed as a concatenation of2163# its appledouble header and the contents. This is useless2164# on both macs and non-macs. If using "print -q -o xx", it2165# will create "xx" with the data, and "%xx" with the header.2166# This is also not very useful.2167#2168# Ideally, someday, this script can learn how to generate2169# appledouble files directly and import those to git, but2170# non-mac machines can never find a use for apple filetype.2171print"\nIgnoring apple filetype file%s"%file['depotFile']2172return21732174# Note that we do not try to de-mangle keywords on utf16 files,2175# even though in theory somebody may want that.2176 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2177if pattern:2178 regexp = re.compile(pattern, re.VERBOSE)2179 text =''.join(contents)2180 text = regexp.sub(r'$\1$', text)2181 contents = [ text ]21822183 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21842185# total length...2186 length =02187for d in contents:2188 length = length +len(d)21892190 self.gitStream.write("data%d\n"% length)2191for d in contents:2192 self.gitStream.write(d)2193 self.gitStream.write("\n")21942195defstreamOneP4Deletion(self,file):2196 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2197if verbose:2198 sys.stderr.write("delete%s\n"% relPath)2199 self.gitStream.write("D%s\n"% relPath)22002201# handle another chunk of streaming data2202defstreamP4FilesCb(self, marshalled):22032204# catch p4 errors and complain2205 err =None2206if"code"in marshalled:2207if marshalled["code"] =="error":2208if"data"in marshalled:2209 err = marshalled["data"].rstrip()2210if err:2211 f =None2212if self.stream_have_file_info:2213if"depotFile"in self.stream_file:2214 f = self.stream_file["depotFile"]2215# force a failure in fast-import, else an empty2216# commit will be made2217 self.gitStream.write("\n")2218 self.gitStream.write("die-now\n")2219 self.gitStream.close()2220# ignore errors, but make sure it exits first2221 self.importProcess.wait()2222if f:2223die("Error from p4 print for%s:%s"% (f, err))2224else:2225die("Error from p4 print:%s"% err)22262227if marshalled.has_key('depotFile')and self.stream_have_file_info:2228# start of a new file - output the old one first2229 self.streamOneP4File(self.stream_file, self.stream_contents)2230 self.stream_file = {}2231 self.stream_contents = []2232 self.stream_have_file_info =False22332234# pick up the new file information... for the2235# 'data' field we need to append to our array2236for k in marshalled.keys():2237if k =='data':2238 self.stream_contents.append(marshalled['data'])2239else:2240 self.stream_file[k] = marshalled[k]22412242 self.stream_have_file_info =True22432244# Stream directly from "p4 files" into "git fast-import"2245defstreamP4Files(self, files):2246 filesForCommit = []2247 filesToRead = []2248 filesToDelete = []22492250for f in files:2251# if using a client spec, only add the files that have2252# a path in the client2253if self.clientSpecDirs:2254if self.clientSpecDirs.map_in_client(f['path']) =="":2255continue22562257 filesForCommit.append(f)2258if f['action']in self.delete_actions:2259 filesToDelete.append(f)2260else:2261 filesToRead.append(f)22622263# deleted files...2264for f in filesToDelete:2265 self.streamOneP4Deletion(f)22662267iflen(filesToRead) >0:2268 self.stream_file = {}2269 self.stream_contents = []2270 self.stream_have_file_info =False22712272# curry self argument2273defstreamP4FilesCbSelf(entry):2274 self.streamP4FilesCb(entry)22752276 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22772278p4CmdList(["-x","-","print"],2279 stdin=fileArgs,2280 cb=streamP4FilesCbSelf)22812282# do the last chunk2283if self.stream_file.has_key('depotFile'):2284 self.streamOneP4File(self.stream_file, self.stream_contents)22852286defmake_email(self, userid):2287if userid in self.users:2288return self.users[userid]2289else:2290return"%s<a@b>"% userid22912292# Stream a p4 tag2293defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2294if verbose:2295print"writing tag%sfor commit%s"% (labelName, commit)2296 gitStream.write("tag%s\n"% labelName)2297 gitStream.write("from%s\n"% commit)22982299if labelDetails.has_key('Owner'):2300 owner = labelDetails["Owner"]2301else:2302 owner =None23032304# Try to use the owner of the p4 label, or failing that,2305# the current p4 user id.2306if owner:2307 email = self.make_email(owner)2308else:2309 email = self.make_email(self.p4UserId())2310 tagger ="%s %s %s"% (email, epoch, self.tz)23112312 gitStream.write("tagger%s\n"% tagger)23132314print"labelDetails=",labelDetails2315if labelDetails.has_key('Description'):2316 description = labelDetails['Description']2317else:2318 description ='Label from git p4'23192320 gitStream.write("data%d\n"%len(description))2321 gitStream.write(description)2322 gitStream.write("\n")23232324defcommit(self, details, files, branch, parent =""):2325 epoch = details["time"]2326 author = details["user"]23272328if self.verbose:2329print"commit into%s"% branch23302331# start with reading files; if that fails, we should not2332# create a commit.2333 new_files = []2334for f in files:2335if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2336 new_files.append(f)2337else:2338 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23392340 self.gitStream.write("commit%s\n"% branch)2341# gitStream.write("mark :%s\n" % details["change"])2342 self.committedChanges.add(int(details["change"]))2343 committer =""2344if author not in self.users:2345 self.getUserMapFromPerforceServer()2346 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23472348 self.gitStream.write("committer%s\n"% committer)23492350 self.gitStream.write("data <<EOT\n")2351 self.gitStream.write(details["desc"])2352 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2353(','.join(self.branchPrefixes), details["change"]))2354iflen(details['options']) >0:2355 self.gitStream.write(": options =%s"% details['options'])2356 self.gitStream.write("]\nEOT\n\n")23572358iflen(parent) >0:2359if self.verbose:2360print"parent%s"% parent2361 self.gitStream.write("from%s\n"% parent)23622363 self.streamP4Files(new_files)2364 self.gitStream.write("\n")23652366 change =int(details["change"])23672368if self.labels.has_key(change):2369 label = self.labels[change]2370 labelDetails = label[0]2371 labelRevisions = label[1]2372if self.verbose:2373print"Change%sis labelled%s"% (change, labelDetails)23742375 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2376for p in self.branchPrefixes])23772378iflen(files) ==len(labelRevisions):23792380 cleanedFiles = {}2381for info in files:2382if info["action"]in self.delete_actions:2383continue2384 cleanedFiles[info["depotFile"]] = info["rev"]23852386if cleanedFiles == labelRevisions:2387 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23882389else:2390if not self.silent:2391print("Tag%sdoes not match with change%s: files do not match."2392% (labelDetails["label"], change))23932394else:2395if not self.silent:2396print("Tag%sdoes not match with change%s: file count is different."2397% (labelDetails["label"], change))23982399# Build a dictionary of changelists and labels, for "detect-labels" option.2400defgetLabels(self):2401 self.labels = {}24022403 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2404iflen(l) >0and not self.silent:2405print"Finding files belonging to labels in%s"% `self.depotPaths`24062407for output in l:2408 label = output["label"]2409 revisions = {}2410 newestChange =02411if self.verbose:2412print"Querying files for label%s"% label2413forfileinp4CmdList(["files"] +2414["%s...@%s"% (p, label)2415for p in self.depotPaths]):2416 revisions[file["depotFile"]] =file["rev"]2417 change =int(file["change"])2418if change > newestChange:2419 newestChange = change24202421 self.labels[newestChange] = [output, revisions]24222423if self.verbose:2424print"Label changes:%s"% self.labels.keys()24252426# Import p4 labels as git tags. A direct mapping does not2427# exist, so assume that if all the files are at the same revision2428# then we can use that, or it's something more complicated we should2429# just ignore.2430defimportP4Labels(self, stream, p4Labels):2431if verbose:2432print"import p4 labels: "+' '.join(p4Labels)24332434 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2435 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2436iflen(validLabelRegexp) ==0:2437 validLabelRegexp = defaultLabelRegexp2438 m = re.compile(validLabelRegexp)24392440for name in p4Labels:2441 commitFound =False24422443if not m.match(name):2444if verbose:2445print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2446continue24472448if name in ignoredP4Labels:2449continue24502451 labelDetails =p4CmdList(['label',"-o", name])[0]24522453# get the most recent changelist for each file in this label2454 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2455for p in self.depotPaths])24562457if change.has_key('change'):2458# find the corresponding git commit; take the oldest commit2459 changelist =int(change['change'])2460 gitCommit =read_pipe(["git","rev-list","--max-count=1",2461"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2462iflen(gitCommit) ==0:2463print"could not find git commit for changelist%d"% changelist2464else:2465 gitCommit = gitCommit.strip()2466 commitFound =True2467# Convert from p4 time format2468try:2469 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2470exceptValueError:2471print"Could not convert label time%s"% labelDetails['Update']2472 tmwhen =124732474 when =int(time.mktime(tmwhen))2475 self.streamTag(stream, name, labelDetails, gitCommit, when)2476if verbose:2477print"p4 label%smapped to git commit%s"% (name, gitCommit)2478else:2479if verbose:2480print"Label%shas no changelists - possibly deleted?"% name24812482if not commitFound:2483# We can't import this label; don't try again as it will get very2484# expensive repeatedly fetching all the files for labels that will2485# never be imported. If the label is moved in the future, the2486# ignore will need to be removed manually.2487system(["git","config","--add","git-p4.ignoredP4Labels", name])24882489defguessProjectName(self):2490for p in self.depotPaths:2491if p.endswith("/"):2492 p = p[:-1]2493 p = p[p.strip().rfind("/") +1:]2494if not p.endswith("/"):2495 p +="/"2496return p24972498defgetBranchMapping(self):2499 lostAndFoundBranches =set()25002501 user =gitConfig("git-p4.branchUser")2502iflen(user) >0:2503 command ="branches -u%s"% user2504else:2505 command ="branches"25062507for info inp4CmdList(command):2508 details =p4Cmd(["branch","-o", info["branch"]])2509 viewIdx =02510while details.has_key("View%s"% viewIdx):2511 paths = details["View%s"% viewIdx].split(" ")2512 viewIdx = viewIdx +12513# require standard //depot/foo/... //depot/bar/... mapping2514iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2515continue2516 source = paths[0]2517 destination = paths[1]2518## HACK2519ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2520 source = source[len(self.depotPaths[0]):-4]2521 destination = destination[len(self.depotPaths[0]):-4]25222523if destination in self.knownBranches:2524if not self.silent:2525print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2526print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2527continue25282529 self.knownBranches[destination] = source25302531 lostAndFoundBranches.discard(destination)25322533if source not in self.knownBranches:2534 lostAndFoundBranches.add(source)25352536# Perforce does not strictly require branches to be defined, so we also2537# check git config for a branch list.2538#2539# Example of branch definition in git config file:2540# [git-p4]2541# branchList=main:branchA2542# branchList=main:branchB2543# branchList=branchA:branchC2544 configBranches =gitConfigList("git-p4.branchList")2545for branch in configBranches:2546if branch:2547(source, destination) = branch.split(":")2548 self.knownBranches[destination] = source25492550 lostAndFoundBranches.discard(destination)25512552if source not in self.knownBranches:2553 lostAndFoundBranches.add(source)255425552556for branch in lostAndFoundBranches:2557 self.knownBranches[branch] = branch25582559defgetBranchMappingFromGitBranches(self):2560 branches =p4BranchesInGit(self.importIntoRemotes)2561for branch in branches.keys():2562if branch =="master":2563 branch ="main"2564else:2565 branch = branch[len(self.projectName):]2566 self.knownBranches[branch] = branch25672568defupdateOptionDict(self, d):2569 option_keys = {}2570if self.keepRepoPath:2571 option_keys['keepRepoPath'] =125722573 d["options"] =' '.join(sorted(option_keys.keys()))25742575defreadOptions(self, d):2576 self.keepRepoPath = (d.has_key('options')2577and('keepRepoPath'in d['options']))25782579defgitRefForBranch(self, branch):2580if branch =="main":2581return self.refPrefix +"master"25822583iflen(branch) <=0:2584return branch25852586return self.refPrefix + self.projectName + branch25872588defgitCommitByP4Change(self, ref, change):2589if self.verbose:2590print"looking in ref "+ ref +" for change%susing bisect..."% change25912592 earliestCommit =""2593 latestCommit =parseRevision(ref)25942595while True:2596if self.verbose:2597print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2598 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2599iflen(next) ==0:2600if self.verbose:2601print"argh"2602return""2603 log =extractLogMessageFromGitCommit(next)2604 settings =extractSettingsGitLog(log)2605 currentChange =int(settings['change'])2606if self.verbose:2607print"current change%s"% currentChange26082609if currentChange == change:2610if self.verbose:2611print"found%s"% next2612return next26132614if currentChange < change:2615 earliestCommit ="^%s"% next2616else:2617 latestCommit ="%s"% next26182619return""26202621defimportNewBranch(self, branch, maxChange):2622# make fast-import flush all changes to disk and update the refs using the checkpoint2623# command so that we can try to find the branch parent in the git history2624 self.gitStream.write("checkpoint\n\n");2625 self.gitStream.flush();2626 branchPrefix = self.depotPaths[0] + branch +"/"2627range="@1,%s"% maxChange2628#print "prefix" + branchPrefix2629 changes =p4ChangesForPaths([branchPrefix],range)2630iflen(changes) <=0:2631return False2632 firstChange = changes[0]2633#print "first change in branch: %s" % firstChange2634 sourceBranch = self.knownBranches[branch]2635 sourceDepotPath = self.depotPaths[0] + sourceBranch2636 sourceRef = self.gitRefForBranch(sourceBranch)2637#print "source " + sourceBranch26382639 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2640#print "branch parent: %s" % branchParentChange2641 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2642iflen(gitParent) >0:2643 self.initialParents[self.gitRefForBranch(branch)] = gitParent2644#print "parent git commit: %s" % gitParent26452646 self.importChanges(changes)2647return True26482649defsearchParent(self, parent, branch, target):2650 parentFound =False2651for blob inread_pipe_lines(["git","rev-list","--reverse",2652"--no-merges", parent]):2653 blob = blob.strip()2654iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2655 parentFound =True2656if self.verbose:2657print"Found parent of%sin commit%s"% (branch, blob)2658break2659if parentFound:2660return blob2661else:2662return None26632664defimportChanges(self, changes):2665 cnt =12666for change in changes:2667 description =p4_describe(change)2668 self.updateOptionDict(description)26692670if not self.silent:2671 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2672 sys.stdout.flush()2673 cnt = cnt +126742675try:2676if self.detectBranches:2677 branches = self.splitFilesIntoBranches(description)2678for branch in branches.keys():2679## HACK --hwn2680 branchPrefix = self.depotPaths[0] + branch +"/"2681 self.branchPrefixes = [ branchPrefix ]26822683 parent =""26842685 filesForCommit = branches[branch]26862687if self.verbose:2688print"branch is%s"% branch26892690 self.updatedBranches.add(branch)26912692if branch not in self.createdBranches:2693 self.createdBranches.add(branch)2694 parent = self.knownBranches[branch]2695if parent == branch:2696 parent =""2697else:2698 fullBranch = self.projectName + branch2699if fullBranch not in self.p4BranchesInGit:2700if not self.silent:2701print("\nImporting new branch%s"% fullBranch);2702if self.importNewBranch(branch, change -1):2703 parent =""2704 self.p4BranchesInGit.append(fullBranch)2705if not self.silent:2706print("\nResuming with change%s"% change);27072708if self.verbose:2709print"parent determined through known branches:%s"% parent27102711 branch = self.gitRefForBranch(branch)2712 parent = self.gitRefForBranch(parent)27132714if self.verbose:2715print"looking for initial parent for%s; current parent is%s"% (branch, parent)27162717iflen(parent) ==0and branch in self.initialParents:2718 parent = self.initialParents[branch]2719del self.initialParents[branch]27202721 blob =None2722iflen(parent) >0:2723 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2724if self.verbose:2725print"Creating temporary branch: "+ tempBranch2726 self.commit(description, filesForCommit, tempBranch)2727 self.tempBranches.append(tempBranch)2728 self.checkpoint()2729 blob = self.searchParent(parent, branch, tempBranch)2730if blob:2731 self.commit(description, filesForCommit, branch, blob)2732else:2733if self.verbose:2734print"Parent of%snot found. Committing into head of%s"% (branch, parent)2735 self.commit(description, filesForCommit, branch, parent)2736else:2737 files = self.extractFilesFromCommit(description)2738 self.commit(description, files, self.branch,2739 self.initialParent)2740# only needed once, to connect to the previous commit2741 self.initialParent =""2742exceptIOError:2743print self.gitError.read()2744 sys.exit(1)27452746defimportHeadRevision(self, revision):2747print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27482749 details = {}2750 details["user"] ="git perforce import user"2751 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2752% (' '.join(self.depotPaths), revision))2753 details["change"] = revision2754 newestRevision =027552756 fileCnt =02757 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27582759for info inp4CmdList(["files"] + fileArgs):27602761if'code'in info and info['code'] =='error':2762 sys.stderr.write("p4 returned an error:%s\n"2763% info['data'])2764if info['data'].find("must refer to client") >=0:2765 sys.stderr.write("This particular p4 error is misleading.\n")2766 sys.stderr.write("Perhaps the depot path was misspelled.\n");2767 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2768 sys.exit(1)2769if'p4ExitCode'in info:2770 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2771 sys.exit(1)277227732774 change =int(info["change"])2775if change > newestRevision:2776 newestRevision = change27772778if info["action"]in self.delete_actions:2779# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2780#fileCnt = fileCnt + 12781continue27822783for prop in["depotFile","rev","action","type"]:2784 details["%s%s"% (prop, fileCnt)] = info[prop]27852786 fileCnt = fileCnt +127872788 details["change"] = newestRevision27892790# Use time from top-most change so that all git p4 clones of2791# the same p4 repo have the same commit SHA1s.2792 res =p4_describe(newestRevision)2793 details["time"] = res["time"]27942795 self.updateOptionDict(details)2796try:2797 self.commit(details, self.extractFilesFromCommit(details), self.branch)2798exceptIOError:2799print"IO error with git fast-import. Is your git version recent enough?"2800print self.gitError.read()280128022803defrun(self, args):2804 self.depotPaths = []2805 self.changeRange =""2806 self.previousDepotPaths = []2807 self.hasOrigin =False28082809# map from branch depot path to parent branch2810 self.knownBranches = {}2811 self.initialParents = {}28122813if self.importIntoRemotes:2814 self.refPrefix ="refs/remotes/p4/"2815else:2816 self.refPrefix ="refs/heads/p4/"28172818if self.syncWithOrigin:2819 self.hasOrigin =originP4BranchesExist()2820if self.hasOrigin:2821if not self.silent:2822print'Syncing with origin first, using "git fetch origin"'2823system("git fetch origin")28242825 branch_arg_given =bool(self.branch)2826iflen(self.branch) ==0:2827 self.branch = self.refPrefix +"master"2828ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2829system("git update-ref%srefs/heads/p4"% self.branch)2830system("git branch -D p4")28312832# accept either the command-line option, or the configuration variable2833if self.useClientSpec:2834# will use this after clone to set the variable2835 self.useClientSpec_from_options =True2836else:2837ifgitConfig("git-p4.useclientspec","--bool") =="true":2838 self.useClientSpec =True2839if self.useClientSpec:2840 self.clientSpecDirs =getClientSpec()28412842# TODO: should always look at previous commits,2843# merge with previous imports, if possible.2844if args == []:2845if self.hasOrigin:2846createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28472848# branches holds mapping from branch name to sha12849 branches =p4BranchesInGit(self.importIntoRemotes)28502851# restrict to just this one, disabling detect-branches2852if branch_arg_given:2853 short = self.branch.split("/")[-1]2854if short in branches:2855 self.p4BranchesInGit = [ short ]2856else:2857 self.p4BranchesInGit = branches.keys()28582859iflen(self.p4BranchesInGit) >1:2860if not self.silent:2861print"Importing from/into multiple branches"2862 self.detectBranches =True2863for branch in branches.keys():2864 self.initialParents[self.refPrefix + branch] = \2865 branches[branch]28662867if self.verbose:2868print"branches:%s"% self.p4BranchesInGit28692870 p4Change =02871for branch in self.p4BranchesInGit:2872 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28732874 settings =extractSettingsGitLog(logMsg)28752876 self.readOptions(settings)2877if(settings.has_key('depot-paths')2878and settings.has_key('change')):2879 change =int(settings['change']) +12880 p4Change =max(p4Change, change)28812882 depotPaths =sorted(settings['depot-paths'])2883if self.previousDepotPaths == []:2884 self.previousDepotPaths = depotPaths2885else:2886 paths = []2887for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2888 prev_list = prev.split("/")2889 cur_list = cur.split("/")2890for i inrange(0,min(len(cur_list),len(prev_list))):2891if cur_list[i] <> prev_list[i]:2892 i = i -12893break28942895 paths.append("/".join(cur_list[:i +1]))28962897 self.previousDepotPaths = paths28982899if p4Change >0:2900 self.depotPaths =sorted(self.previousDepotPaths)2901 self.changeRange ="@%s,#head"% p4Change2902if not self.silent and not self.detectBranches:2903print"Performing incremental import into%sgit branch"% self.branch29042905# accept multiple ref name abbreviations:2906# refs/foo/bar/branch -> use it exactly2907# p4/branch -> prepend refs/remotes/ or refs/heads/2908# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2909if not self.branch.startswith("refs/"):2910if self.importIntoRemotes:2911 prepend ="refs/remotes/"2912else:2913 prepend ="refs/heads/"2914if not self.branch.startswith("p4/"):2915 prepend +="p4/"2916 self.branch = prepend + self.branch29172918iflen(args) ==0and self.depotPaths:2919if not self.silent:2920print"Depot paths:%s"%' '.join(self.depotPaths)2921else:2922if self.depotPaths and self.depotPaths != args:2923print("previous import used depot path%sand now%swas specified. "2924"This doesn't work!"% (' '.join(self.depotPaths),2925' '.join(args)))2926 sys.exit(1)29272928 self.depotPaths =sorted(args)29292930 revision =""2931 self.users = {}29322933# Make sure no revision specifiers are used when --changesfile2934# is specified.2935 bad_changesfile =False2936iflen(self.changesFile) >0:2937for p in self.depotPaths:2938if p.find("@") >=0or p.find("#") >=0:2939 bad_changesfile =True2940break2941if bad_changesfile:2942die("Option --changesfile is incompatible with revision specifiers")29432944 newPaths = []2945for p in self.depotPaths:2946if p.find("@") != -1:2947 atIdx = p.index("@")2948 self.changeRange = p[atIdx:]2949if self.changeRange =="@all":2950 self.changeRange =""2951elif','not in self.changeRange:2952 revision = self.changeRange2953 self.changeRange =""2954 p = p[:atIdx]2955elif p.find("#") != -1:2956 hashIdx = p.index("#")2957 revision = p[hashIdx:]2958 p = p[:hashIdx]2959elif self.previousDepotPaths == []:2960# pay attention to changesfile, if given, else import2961# the entire p4 tree at the head revision2962iflen(self.changesFile) ==0:2963 revision ="#head"29642965 p = re.sub("\.\.\.$","", p)2966if not p.endswith("/"):2967 p +="/"29682969 newPaths.append(p)29702971 self.depotPaths = newPaths29722973# --detect-branches may change this for each branch2974 self.branchPrefixes = self.depotPaths29752976 self.loadUserMapFromCache()2977 self.labels = {}2978if self.detectLabels:2979 self.getLabels();29802981if self.detectBranches:2982## FIXME - what's a P4 projectName ?2983 self.projectName = self.guessProjectName()29842985if self.hasOrigin:2986 self.getBranchMappingFromGitBranches()2987else:2988 self.getBranchMapping()2989if self.verbose:2990print"p4-git branches:%s"% self.p4BranchesInGit2991print"initial parents:%s"% self.initialParents2992for b in self.p4BranchesInGit:2993if b !="master":29942995## FIXME2996 b = b[len(self.projectName):]2997 self.createdBranches.add(b)29982999 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30003001 self.importProcess = subprocess.Popen(["git","fast-import"],3002 stdin=subprocess.PIPE,3003 stdout=subprocess.PIPE,3004 stderr=subprocess.PIPE);3005 self.gitOutput = self.importProcess.stdout3006 self.gitStream = self.importProcess.stdin3007 self.gitError = self.importProcess.stderr30083009if revision:3010 self.importHeadRevision(revision)3011else:3012 changes = []30133014iflen(self.changesFile) >0:3015 output =open(self.changesFile).readlines()3016 changeSet =set()3017for line in output:3018 changeSet.add(int(line))30193020for change in changeSet:3021 changes.append(change)30223023 changes.sort()3024else:3025# catch "git p4 sync" with no new branches, in a repo that3026# does not have any existing p4 branches3027iflen(args) ==0:3028if not self.p4BranchesInGit:3029die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30303031# The default branch is master, unless --branch is used to3032# specify something else. Make sure it exists, or complain3033# nicely about how to use --branch.3034if not self.detectBranches:3035if notbranch_exists(self.branch):3036if branch_arg_given:3037die("Error: branch%sdoes not exist."% self.branch)3038else:3039die("Error: no branch%s; perhaps specify one with --branch."%3040 self.branch)30413042if self.verbose:3043print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3044 self.changeRange)3045 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30463047iflen(self.maxChanges) >0:3048 changes = changes[:min(int(self.maxChanges),len(changes))]30493050iflen(changes) ==0:3051if not self.silent:3052print"No changes to import!"3053else:3054if not self.silent and not self.detectBranches:3055print"Import destination:%s"% self.branch30563057 self.updatedBranches =set()30583059if not self.detectBranches:3060if args:3061# start a new branch3062 self.initialParent =""3063else:3064# build on a previous revision3065 self.initialParent =parseRevision(self.branch)30663067 self.importChanges(changes)30683069if not self.silent:3070print""3071iflen(self.updatedBranches) >0:3072 sys.stdout.write("Updated branches: ")3073for b in self.updatedBranches:3074 sys.stdout.write("%s"% b)3075 sys.stdout.write("\n")30763077ifgitConfig("git-p4.importLabels","--bool") =="true":3078 self.importLabels =True30793080if self.importLabels:3081 p4Labels =getP4Labels(self.depotPaths)3082 gitTags =getGitTags()30833084 missingP4Labels = p4Labels - gitTags3085 self.importP4Labels(self.gitStream, missingP4Labels)30863087 self.gitStream.close()3088if self.importProcess.wait() !=0:3089die("fast-import failed:%s"% self.gitError.read())3090 self.gitOutput.close()3091 self.gitError.close()30923093# Cleanup temporary branches created during import3094if self.tempBranches != []:3095for branch in self.tempBranches:3096read_pipe("git update-ref -d%s"% branch)3097 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30983099# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3100# a convenient shortcut refname "p4".3101if self.importIntoRemotes:3102 head_ref = self.refPrefix +"HEAD"3103if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3104system(["git","symbolic-ref", head_ref, self.branch])31053106return True31073108classP4Rebase(Command):3109def__init__(self):3110 Command.__init__(self)3111 self.options = [3112 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3113]3114 self.importLabels =False3115 self.description = ("Fetches the latest revision from perforce and "3116+"rebases the current work (branch) against it")31173118defrun(self, args):3119 sync =P4Sync()3120 sync.importLabels = self.importLabels3121 sync.run([])31223123return self.rebase()31243125defrebase(self):3126if os.system("git update-index --refresh") !=0:3127die("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.");3128iflen(read_pipe("git diff-index HEAD --")) >0:3129die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");31303131[upstream, settings] =findUpstreamBranchPoint()3132iflen(upstream) ==0:3133die("Cannot find upstream branchpoint for rebase")31343135# the branchpoint may be p4/foo~3, so strip off the parent3136 upstream = re.sub("~[0-9]+$","", upstream)31373138print"Rebasing the current branch onto%s"% upstream3139 oldHead =read_pipe("git rev-parse HEAD").strip()3140system("git rebase%s"% upstream)3141system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3142return True31433144classP4Clone(P4Sync):3145def__init__(self):3146 P4Sync.__init__(self)3147 self.description ="Creates a new git repository and imports from Perforce into it"3148 self.usage ="usage: %prog [options] //depot/path[@revRange]"3149 self.options += [3150 optparse.make_option("--destination", dest="cloneDestination",3151 action='store', default=None,3152help="where to leave result of the clone"),3153 optparse.make_option("-/", dest="cloneExclude",3154 action="append",type="string",3155help="exclude depot path"),3156 optparse.make_option("--bare", dest="cloneBare",3157 action="store_true", default=False),3158]3159 self.cloneDestination =None3160 self.needsGit =False3161 self.cloneBare =False31623163# This is required for the "append" cloneExclude action3164defensure_value(self, attr, value):3165if nothasattr(self, attr)orgetattr(self, attr)is None:3166setattr(self, attr, value)3167returngetattr(self, attr)31683169defdefaultDestination(self, args):3170## TODO: use common prefix of args?3171 depotPath = args[0]3172 depotDir = re.sub("(@[^@]*)$","", depotPath)3173 depotDir = re.sub("(#[^#]*)$","", depotDir)3174 depotDir = re.sub(r"\.\.\.$","", depotDir)3175 depotDir = re.sub(r"/$","", depotDir)3176return os.path.split(depotDir)[1]31773178defrun(self, args):3179iflen(args) <1:3180return False31813182if self.keepRepoPath and not self.cloneDestination:3183 sys.stderr.write("Must specify destination for --keep-path\n")3184 sys.exit(1)31853186 depotPaths = args31873188if not self.cloneDestination andlen(depotPaths) >1:3189 self.cloneDestination = depotPaths[-1]3190 depotPaths = depotPaths[:-1]31913192 self.cloneExclude = ["/"+p for p in self.cloneExclude]3193for p in depotPaths:3194if not p.startswith("//"):3195 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3196return False31973198if not self.cloneDestination:3199 self.cloneDestination = self.defaultDestination(args)32003201print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32023203if not os.path.exists(self.cloneDestination):3204 os.makedirs(self.cloneDestination)3205chdir(self.cloneDestination)32063207 init_cmd = ["git","init"]3208if self.cloneBare:3209 init_cmd.append("--bare")3210 subprocess.check_call(init_cmd)32113212if not P4Sync.run(self, depotPaths):3213return False32143215# create a master branch and check out a work tree3216ifgitBranchExists(self.branch):3217system(["git","branch","master", self.branch ])3218if not self.cloneBare:3219system(["git","checkout","-f"])3220else:3221print'Not checking out any branch, use ' \3222'"git checkout -q -b master <branch>"'32233224# auto-set this variable if invoked with --use-client-spec3225if self.useClientSpec_from_options:3226system("git config --bool git-p4.useclientspec true")32273228return True32293230classP4Branches(Command):3231def__init__(self):3232 Command.__init__(self)3233 self.options = [ ]3234 self.description = ("Shows the git branches that hold imports and their "3235+"corresponding perforce depot paths")3236 self.verbose =False32373238defrun(self, args):3239iforiginP4BranchesExist():3240createOrUpdateBranchesFromOrigin()32413242 cmdline ="git rev-parse --symbolic "3243 cmdline +=" --remotes"32443245for line inread_pipe_lines(cmdline):3246 line = line.strip()32473248if not line.startswith('p4/')or line =="p4/HEAD":3249continue3250 branch = line32513252 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3253 settings =extractSettingsGitLog(log)32543255print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3256return True32573258classHelpFormatter(optparse.IndentedHelpFormatter):3259def__init__(self):3260 optparse.IndentedHelpFormatter.__init__(self)32613262defformat_description(self, description):3263if description:3264return description +"\n"3265else:3266return""32673268defprintUsage(commands):3269print"usage:%s<command> [options]"% sys.argv[0]3270print""3271print"valid commands:%s"%", ".join(commands)3272print""3273print"Try%s<command> --help for command specific help."% sys.argv[0]3274print""32753276commands = {3277"debug": P4Debug,3278"submit": P4Submit,3279"commit": P4Submit,3280"sync": P4Sync,3281"rebase": P4Rebase,3282"clone": P4Clone,3283"rollback": P4RollBack,3284"branches": P4Branches3285}328632873288defmain():3289iflen(sys.argv[1:]) ==0:3290printUsage(commands.keys())3291 sys.exit(2)32923293 cmdName = sys.argv[1]3294try:3295 klass = commands[cmdName]3296 cmd =klass()3297exceptKeyError:3298print"unknown command%s"% cmdName3299print""3300printUsage(commands.keys())3301 sys.exit(2)33023303 options = cmd.options3304 cmd.gitdir = os.environ.get("GIT_DIR",None)33053306 args = sys.argv[2:]33073308 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3309if cmd.needsGit:3310 options.append(optparse.make_option("--git-dir", dest="gitdir"))33113312 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3313 options,3314 description = cmd.description,3315 formatter =HelpFormatter())33163317(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3318global verbose3319 verbose = cmd.verbose3320if cmd.needsGit:3321if cmd.gitdir ==None:3322 cmd.gitdir = os.path.abspath(".git")3323if notisValidGitDir(cmd.gitdir):3324 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3325if os.path.exists(cmd.gitdir):3326 cdup =read_pipe("git rev-parse --show-cdup").strip()3327iflen(cdup) >0:3328chdir(cdup);33293330if notisValidGitDir(cmd.gitdir):3331ifisValidGitDir(cmd.gitdir +"/.git"):3332 cmd.gitdir +="/.git"3333else:3334die("fatal: cannot locate git repository at%s"% cmd.gitdir)33353336 os.environ["GIT_DIR"] = cmd.gitdir33373338if not cmd.run(args):3339 parser.print_help()3340 sys.exit(2)334133423343if __name__ =='__main__':3344main()