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 26try: 27from subprocess import CalledProcessError 28exceptImportError: 29# from python2.7:subprocess.py 30# Exception classes used by this module. 31classCalledProcessError(Exception): 32"""This exception is raised when a process run by check_call() returns 33 a non-zero exit status. The exit status will be stored in the 34 returncode attribute.""" 35def__init__(self, returncode, cmd): 36 self.returncode = returncode 37 self.cmd = cmd 38def__str__(self): 39return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 40 41verbose =False 42 43# Only labels/tags matching this will be imported/exported 44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 45 46# Grab changes in blocks of this many revisions, unless otherwise requested 47defaultBlockSize =512 48 49defp4_build_cmd(cmd): 50"""Build a suitable p4 command line. 51 52 This consolidates building and returning a p4 command line into one 53 location. It means that hooking into the environment, or other configuration 54 can be done more easily. 55 """ 56 real_cmd = ["p4"] 57 58 user =gitConfig("git-p4.user") 59iflen(user) >0: 60 real_cmd += ["-u",user] 61 62 password =gitConfig("git-p4.password") 63iflen(password) >0: 64 real_cmd += ["-P", password] 65 66 port =gitConfig("git-p4.port") 67iflen(port) >0: 68 real_cmd += ["-p", port] 69 70 host =gitConfig("git-p4.host") 71iflen(host) >0: 72 real_cmd += ["-H", host] 73 74 client =gitConfig("git-p4.client") 75iflen(client) >0: 76 real_cmd += ["-c", client] 77 78 79ifisinstance(cmd,basestring): 80 real_cmd =' '.join(real_cmd) +' '+ cmd 81else: 82 real_cmd += cmd 83return real_cmd 84 85defchdir(path, is_client_path=False): 86"""Do chdir to the given path, and set the PWD environment 87 variable for use by P4. It does not look at getcwd() output. 88 Since we're not using the shell, it is necessary to set the 89 PWD environment variable explicitly. 90 91 Normally, expand the path to force it to be absolute. This 92 addresses the use of relative path names inside P4 settings, 93 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 94 as given; it looks for .p4config using PWD. 95 96 If is_client_path, the path was handed to us directly by p4, 97 and may be a symbolic link. Do not call os.getcwd() in this 98 case, because it will cause p4 to think that PWD is not inside 99 the client path. 100 """ 101 102 os.chdir(path) 103if not is_client_path: 104 path = os.getcwd() 105 os.environ['PWD'] = path 106 107defcalcDiskFree(): 108"""Return free space in bytes on the disk of the given dirname.""" 109if platform.system() =='Windows': 110 free_bytes = ctypes.c_ulonglong(0) 111 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 112return free_bytes.value 113else: 114 st = os.statvfs(os.getcwd()) 115return st.f_bavail * st.f_frsize 116 117defdie(msg): 118if verbose: 119raiseException(msg) 120else: 121 sys.stderr.write(msg +"\n") 122 sys.exit(1) 123 124defwrite_pipe(c, stdin): 125if verbose: 126 sys.stderr.write('Writing pipe:%s\n'%str(c)) 127 128 expand =isinstance(c,basestring) 129 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 130 pipe = p.stdin 131 val = pipe.write(stdin) 132 pipe.close() 133if p.wait(): 134die('Command failed:%s'%str(c)) 135 136return val 137 138defp4_write_pipe(c, stdin): 139 real_cmd =p4_build_cmd(c) 140returnwrite_pipe(real_cmd, stdin) 141 142defread_pipe(c, ignore_error=False): 143if verbose: 144 sys.stderr.write('Reading pipe:%s\n'%str(c)) 145 146 expand =isinstance(c,basestring) 147 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 148 pipe = p.stdout 149 val = pipe.read() 150if p.wait()and not ignore_error: 151die('Command failed:%s'%str(c)) 152 153return val 154 155defp4_read_pipe(c, ignore_error=False): 156 real_cmd =p4_build_cmd(c) 157returnread_pipe(real_cmd, ignore_error) 158 159defread_pipe_lines(c): 160if verbose: 161 sys.stderr.write('Reading pipe:%s\n'%str(c)) 162 163 expand =isinstance(c, basestring) 164 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 165 pipe = p.stdout 166 val = pipe.readlines() 167if pipe.close()or p.wait(): 168die('Command failed:%s'%str(c)) 169 170return val 171 172defp4_read_pipe_lines(c): 173"""Specifically invoke p4 on the command supplied. """ 174 real_cmd =p4_build_cmd(c) 175returnread_pipe_lines(real_cmd) 176 177defp4_has_command(cmd): 178"""Ask p4 for help on this command. If it returns an error, the 179 command does not exist in this version of p4.""" 180 real_cmd =p4_build_cmd(["help", cmd]) 181 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 182 stderr=subprocess.PIPE) 183 p.communicate() 184return p.returncode ==0 185 186defp4_has_move_command(): 187"""See if the move command exists, that it supports -k, and that 188 it has not been administratively disabled. The arguments 189 must be correct, but the filenames do not have to exist. Use 190 ones with wildcards so even if they exist, it will fail.""" 191 192if notp4_has_command("move"): 193return False 194 cmd =p4_build_cmd(["move","-k","@from","@to"]) 195 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 196(out, err) = p.communicate() 197# return code will be 1 in either case 198if err.find("Invalid option") >=0: 199return False 200if err.find("disabled") >=0: 201return False 202# assume it failed because @... was invalid changelist 203return True 204 205defsystem(cmd): 206 expand =isinstance(cmd,basestring) 207if verbose: 208 sys.stderr.write("executing%s\n"%str(cmd)) 209 retcode = subprocess.call(cmd, shell=expand) 210if retcode: 211raiseCalledProcessError(retcode, cmd) 212 213defp4_system(cmd): 214"""Specifically invoke p4 as the system command. """ 215 real_cmd =p4_build_cmd(cmd) 216 expand =isinstance(real_cmd, basestring) 217 retcode = subprocess.call(real_cmd, shell=expand) 218if retcode: 219raiseCalledProcessError(retcode, real_cmd) 220 221_p4_version_string =None 222defp4_version_string(): 223"""Read the version string, showing just the last line, which 224 hopefully is the interesting version bit. 225 226 $ p4 -V 227 Perforce - The Fast Software Configuration Management System. 228 Copyright 1995-2011 Perforce Software. All rights reserved. 229 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 230 """ 231global _p4_version_string 232if not _p4_version_string: 233 a =p4_read_pipe_lines(["-V"]) 234 _p4_version_string = a[-1].rstrip() 235return _p4_version_string 236 237defp4_integrate(src, dest): 238p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 239 240defp4_sync(f, *options): 241p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 242 243defp4_add(f): 244# forcibly add file names with wildcards 245ifwildcard_present(f): 246p4_system(["add","-f", f]) 247else: 248p4_system(["add", f]) 249 250defp4_delete(f): 251p4_system(["delete",wildcard_encode(f)]) 252 253defp4_edit(f): 254p4_system(["edit",wildcard_encode(f)]) 255 256defp4_revert(f): 257p4_system(["revert",wildcard_encode(f)]) 258 259defp4_reopen(type, f): 260p4_system(["reopen","-t",type,wildcard_encode(f)]) 261 262defp4_move(src, dest): 263p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 264 265defp4_last_change(): 266 results =p4CmdList(["changes","-m","1"]) 267returnint(results[0]['change']) 268 269defp4_describe(change): 270"""Make sure it returns a valid result by checking for 271 the presence of field "time". Return a dict of the 272 results.""" 273 274 ds =p4CmdList(["describe","-s",str(change)]) 275iflen(ds) !=1: 276die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 277 278 d = ds[0] 279 280if"p4ExitCode"in d: 281die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 282str(d))) 283if"code"in d: 284if d["code"] =="error": 285die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 286 287if"time"not in d: 288die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 289 290return d 291 292# 293# Canonicalize the p4 type and return a tuple of the 294# base type, plus any modifiers. See "p4 help filetypes" 295# for a list and explanation. 296# 297defsplit_p4_type(p4type): 298 299 p4_filetypes_historical = { 300"ctempobj":"binary+Sw", 301"ctext":"text+C", 302"cxtext":"text+Cx", 303"ktext":"text+k", 304"kxtext":"text+kx", 305"ltext":"text+F", 306"tempobj":"binary+FSw", 307"ubinary":"binary+F", 308"uresource":"resource+F", 309"uxbinary":"binary+Fx", 310"xbinary":"binary+x", 311"xltext":"text+Fx", 312"xtempobj":"binary+Swx", 313"xtext":"text+x", 314"xunicode":"unicode+x", 315"xutf16":"utf16+x", 316} 317if p4type in p4_filetypes_historical: 318 p4type = p4_filetypes_historical[p4type] 319 mods ="" 320 s = p4type.split("+") 321 base = s[0] 322 mods ="" 323iflen(s) >1: 324 mods = s[1] 325return(base, mods) 326 327# 328# return the raw p4 type of a file (text, text+ko, etc) 329# 330defp4_type(f): 331 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 332return results[0]['headType'] 333 334# 335# Given a type base and modifier, return a regexp matching 336# the keywords that can be expanded in the file 337# 338defp4_keywords_regexp_for_type(base, type_mods): 339if base in("text","unicode","binary"): 340 kwords =None 341if"ko"in type_mods: 342 kwords ='Id|Header' 343elif"k"in type_mods: 344 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 345else: 346return None 347 pattern = r""" 348 \$ # Starts with a dollar, followed by... 349 (%s) # one of the keywords, followed by... 350 (:[^$\n]+)? # possibly an old expansion, followed by... 351 \$ # another dollar 352 """% kwords 353return pattern 354else: 355return None 356 357# 358# Given a file, return a regexp matching the possible 359# RCS keywords that will be expanded, or None for files 360# with kw expansion turned off. 361# 362defp4_keywords_regexp_for_file(file): 363if not os.path.exists(file): 364return None 365else: 366(type_base, type_mods) =split_p4_type(p4_type(file)) 367returnp4_keywords_regexp_for_type(type_base, type_mods) 368 369defsetP4ExecBit(file, mode): 370# Reopens an already open file and changes the execute bit to match 371# the execute bit setting in the passed in mode. 372 373 p4Type ="+x" 374 375if notisModeExec(mode): 376 p4Type =getP4OpenedType(file) 377 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 378 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 379if p4Type[-1] =="+": 380 p4Type = p4Type[0:-1] 381 382p4_reopen(p4Type,file) 383 384defgetP4OpenedType(file): 385# Returns the perforce file type for the given file. 386 387 result =p4_read_pipe(["opened",wildcard_encode(file)]) 388 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 389if match: 390return match.group(1) 391else: 392die("Could not determine file type for%s(result: '%s')"% (file, result)) 393 394# Return the set of all p4 labels 395defgetP4Labels(depotPaths): 396 labels =set() 397ifisinstance(depotPaths,basestring): 398 depotPaths = [depotPaths] 399 400for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 401 label = l['label'] 402 labels.add(label) 403 404return labels 405 406# Return the set of all git tags 407defgetGitTags(): 408 gitTags =set() 409for line inread_pipe_lines(["git","tag"]): 410 tag = line.strip() 411 gitTags.add(tag) 412return gitTags 413 414defdiffTreePattern(): 415# This is a simple generator for the diff tree regex pattern. This could be 416# a class variable if this and parseDiffTreeEntry were a part of a class. 417 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 418while True: 419yield pattern 420 421defparseDiffTreeEntry(entry): 422"""Parses a single diff tree entry into its component elements. 423 424 See git-diff-tree(1) manpage for details about the format of the diff 425 output. This method returns a dictionary with the following elements: 426 427 src_mode - The mode of the source file 428 dst_mode - The mode of the destination file 429 src_sha1 - The sha1 for the source file 430 dst_sha1 - The sha1 fr the destination file 431 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 432 status_score - The score for the status (applicable for 'C' and 'R' 433 statuses). This is None if there is no score. 434 src - The path for the source file. 435 dst - The path for the destination file. This is only present for 436 copy or renames. If it is not present, this is None. 437 438 If the pattern is not matched, None is returned.""" 439 440 match =diffTreePattern().next().match(entry) 441if match: 442return{ 443'src_mode': match.group(1), 444'dst_mode': match.group(2), 445'src_sha1': match.group(3), 446'dst_sha1': match.group(4), 447'status': match.group(5), 448'status_score': match.group(6), 449'src': match.group(7), 450'dst': match.group(10) 451} 452return None 453 454defisModeExec(mode): 455# Returns True if the given git mode represents an executable file, 456# otherwise False. 457return mode[-3:] =="755" 458 459defisModeExecChanged(src_mode, dst_mode): 460returnisModeExec(src_mode) !=isModeExec(dst_mode) 461 462defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 463 464ifisinstance(cmd,basestring): 465 cmd ="-G "+ cmd 466 expand =True 467else: 468 cmd = ["-G"] + cmd 469 expand =False 470 471 cmd =p4_build_cmd(cmd) 472if verbose: 473 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 474 475# Use a temporary file to avoid deadlocks without 476# subprocess.communicate(), which would put another copy 477# of stdout into memory. 478 stdin_file =None 479if stdin is not None: 480 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 481ifisinstance(stdin,basestring): 482 stdin_file.write(stdin) 483else: 484for i in stdin: 485 stdin_file.write(i +'\n') 486 stdin_file.flush() 487 stdin_file.seek(0) 488 489 p4 = subprocess.Popen(cmd, 490 shell=expand, 491 stdin=stdin_file, 492 stdout=subprocess.PIPE) 493 494 result = [] 495try: 496while True: 497 entry = marshal.load(p4.stdout) 498if cb is not None: 499cb(entry) 500else: 501 result.append(entry) 502exceptEOFError: 503pass 504 exitCode = p4.wait() 505if exitCode !=0: 506 entry = {} 507 entry["p4ExitCode"] = exitCode 508 result.append(entry) 509 510return result 511 512defp4Cmd(cmd): 513list=p4CmdList(cmd) 514 result = {} 515for entry inlist: 516 result.update(entry) 517return result; 518 519defp4Where(depotPath): 520if not depotPath.endswith("/"): 521 depotPath +="/" 522 depotPathLong = depotPath +"..." 523 outputList =p4CmdList(["where", depotPathLong]) 524 output =None 525for entry in outputList: 526if"depotFile"in entry: 527# Search for the base client side depot path, as long as it starts with the branch's P4 path. 528# The base path always ends with "/...". 529if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 530 output = entry 531break 532elif"data"in entry: 533 data = entry.get("data") 534 space = data.find(" ") 535if data[:space] == depotPath: 536 output = entry 537break 538if output ==None: 539return"" 540if output["code"] =="error": 541return"" 542 clientPath ="" 543if"path"in output: 544 clientPath = output.get("path") 545elif"data"in output: 546 data = output.get("data") 547 lastSpace = data.rfind(" ") 548 clientPath = data[lastSpace +1:] 549 550if clientPath.endswith("..."): 551 clientPath = clientPath[:-3] 552return clientPath 553 554defcurrentGitBranch(): 555returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 556 557defisValidGitDir(path): 558if(os.path.exists(path +"/HEAD") 559and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 560return True; 561return False 562 563defparseRevision(ref): 564returnread_pipe("git rev-parse%s"% ref).strip() 565 566defbranchExists(ref): 567 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 568 ignore_error=True) 569returnlen(rev) >0 570 571defextractLogMessageFromGitCommit(commit): 572 logMessage ="" 573 574## fixme: title is first line of commit, not 1st paragraph. 575 foundTitle =False 576for log inread_pipe_lines("git cat-file commit%s"% commit): 577if not foundTitle: 578iflen(log) ==1: 579 foundTitle =True 580continue 581 582 logMessage += log 583return logMessage 584 585defextractSettingsGitLog(log): 586 values = {} 587for line in log.split("\n"): 588 line = line.strip() 589 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 590if not m: 591continue 592 593 assignments = m.group(1).split(':') 594for a in assignments: 595 vals = a.split('=') 596 key = vals[0].strip() 597 val = ('='.join(vals[1:])).strip() 598if val.endswith('\"')and val.startswith('"'): 599 val = val[1:-1] 600 601 values[key] = val 602 603 paths = values.get("depot-paths") 604if not paths: 605 paths = values.get("depot-path") 606if paths: 607 values['depot-paths'] = paths.split(',') 608return values 609 610defgitBranchExists(branch): 611 proc = subprocess.Popen(["git","rev-parse", branch], 612 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 613return proc.wait() ==0; 614 615_gitConfig = {} 616 617defgitConfig(key, typeSpecifier=None): 618if not _gitConfig.has_key(key): 619 cmd = ["git","config"] 620if typeSpecifier: 621 cmd += [ typeSpecifier ] 622 cmd += [ key ] 623 s =read_pipe(cmd, ignore_error=True) 624 _gitConfig[key] = s.strip() 625return _gitConfig[key] 626 627defgitConfigBool(key): 628"""Return a bool, using git config --bool. It is True only if the 629 variable is set to true, and False if set to false or not present 630 in the config.""" 631 632if not _gitConfig.has_key(key): 633 _gitConfig[key] =gitConfig(key,'--bool') =="true" 634return _gitConfig[key] 635 636defgitConfigInt(key): 637if not _gitConfig.has_key(key): 638 cmd = ["git","config","--int", key ] 639 s =read_pipe(cmd, ignore_error=True) 640 v = s.strip() 641try: 642 _gitConfig[key] =int(gitConfig(key,'--int')) 643exceptValueError: 644 _gitConfig[key] =None 645return _gitConfig[key] 646 647defgitConfigList(key): 648if not _gitConfig.has_key(key): 649 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 650 _gitConfig[key] = s.strip().split(os.linesep) 651if _gitConfig[key] == ['']: 652 _gitConfig[key] = [] 653return _gitConfig[key] 654 655defp4BranchesInGit(branchesAreInRemotes=True): 656"""Find all the branches whose names start with "p4/", looking 657 in remotes or heads as specified by the argument. Return 658 a dictionary of{ branch: revision }for each one found. 659 The branch names are the short names, without any 660 "p4/" prefix.""" 661 662 branches = {} 663 664 cmdline ="git rev-parse --symbolic " 665if branchesAreInRemotes: 666 cmdline +="--remotes" 667else: 668 cmdline +="--branches" 669 670for line inread_pipe_lines(cmdline): 671 line = line.strip() 672 673# only import to p4/ 674if not line.startswith('p4/'): 675continue 676# special symbolic ref to p4/master 677if line =="p4/HEAD": 678continue 679 680# strip off p4/ prefix 681 branch = line[len("p4/"):] 682 683 branches[branch] =parseRevision(line) 684 685return branches 686 687defbranch_exists(branch): 688"""Make sure that the given ref name really exists.""" 689 690 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 691 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 692 out, _ = p.communicate() 693if p.returncode: 694return False 695# expect exactly one line of output: the branch name 696return out.rstrip() == branch 697 698deffindUpstreamBranchPoint(head ="HEAD"): 699 branches =p4BranchesInGit() 700# map from depot-path to branch name 701 branchByDepotPath = {} 702for branch in branches.keys(): 703 tip = branches[branch] 704 log =extractLogMessageFromGitCommit(tip) 705 settings =extractSettingsGitLog(log) 706if settings.has_key("depot-paths"): 707 paths =",".join(settings["depot-paths"]) 708 branchByDepotPath[paths] ="remotes/p4/"+ branch 709 710 settings =None 711 parent =0 712while parent <65535: 713 commit = head +"~%s"% parent 714 log =extractLogMessageFromGitCommit(commit) 715 settings =extractSettingsGitLog(log) 716if settings.has_key("depot-paths"): 717 paths =",".join(settings["depot-paths"]) 718if branchByDepotPath.has_key(paths): 719return[branchByDepotPath[paths], settings] 720 721 parent = parent +1 722 723return["", settings] 724 725defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 726if not silent: 727print("Creating/updating branch(es) in%sbased on origin branch(es)" 728% localRefPrefix) 729 730 originPrefix ="origin/p4/" 731 732for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 733 line = line.strip() 734if(not line.startswith(originPrefix))or line.endswith("HEAD"): 735continue 736 737 headName = line[len(originPrefix):] 738 remoteHead = localRefPrefix + headName 739 originHead = line 740 741 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 742if(not original.has_key('depot-paths') 743or not original.has_key('change')): 744continue 745 746 update =False 747if notgitBranchExists(remoteHead): 748if verbose: 749print"creating%s"% remoteHead 750 update =True 751else: 752 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 753if settings.has_key('change') >0: 754if settings['depot-paths'] == original['depot-paths']: 755 originP4Change =int(original['change']) 756 p4Change =int(settings['change']) 757if originP4Change > p4Change: 758print("%s(%s) is newer than%s(%s). " 759"Updating p4 branch from origin." 760% (originHead, originP4Change, 761 remoteHead, p4Change)) 762 update =True 763else: 764print("Ignoring:%swas imported from%swhile " 765"%swas imported from%s" 766% (originHead,','.join(original['depot-paths']), 767 remoteHead,','.join(settings['depot-paths']))) 768 769if update: 770system("git update-ref%s %s"% (remoteHead, originHead)) 771 772deforiginP4BranchesExist(): 773returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 774 775 776defp4ParseNumericChangeRange(parts): 777 changeStart =int(parts[0][1:]) 778if parts[1] =='#head': 779 changeEnd =p4_last_change() 780else: 781 changeEnd =int(parts[1]) 782 783return(changeStart, changeEnd) 784 785defchooseBlockSize(blockSize): 786if blockSize: 787return blockSize 788else: 789return defaultBlockSize 790 791defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 792assert depotPaths 793 794# Parse the change range into start and end. Try to find integer 795# revision ranges as these can be broken up into blocks to avoid 796# hitting server-side limits (maxrows, maxscanresults). But if 797# that doesn't work, fall back to using the raw revision specifier 798# strings, without using block mode. 799 800if changeRange is None or changeRange =='': 801 changeStart =1 802 changeEnd =p4_last_change() 803 block_size =chooseBlockSize(requestedBlockSize) 804else: 805 parts = changeRange.split(',') 806assertlen(parts) ==2 807try: 808(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 809 block_size =chooseBlockSize(requestedBlockSize) 810except: 811 changeStart = parts[0][1:] 812 changeEnd = parts[1] 813if requestedBlockSize: 814die("cannot use --changes-block-size with non-numeric revisions") 815 block_size =None 816 817# Accumulate change numbers in a dictionary to avoid duplicates 818 changes = {} 819 820for p in depotPaths: 821# Retrieve changes a block at a time, to prevent running 822# into a MaxResults/MaxScanRows error from the server. 823 824while True: 825 cmd = ['changes'] 826 827if block_size: 828 end =min(changeEnd, changeStart + block_size) 829 revisionRange ="%d,%d"% (changeStart, end) 830else: 831 revisionRange ="%s,%s"% (changeStart, changeEnd) 832 833 cmd += ["%s...@%s"% (p, revisionRange)] 834 835for line inp4_read_pipe_lines(cmd): 836 changeNum =int(line.split(" ")[1]) 837 changes[changeNum] =True 838 839if not block_size: 840break 841 842if end >= changeEnd: 843break 844 845 changeStart = end +1 846 847 changelist = changes.keys() 848 changelist.sort() 849return changelist 850 851defp4PathStartsWith(path, prefix): 852# This method tries to remedy a potential mixed-case issue: 853# 854# If UserA adds //depot/DirA/file1 855# and UserB adds //depot/dira/file2 856# 857# we may or may not have a problem. If you have core.ignorecase=true, 858# we treat DirA and dira as the same directory 859ifgitConfigBool("core.ignorecase"): 860return path.lower().startswith(prefix.lower()) 861return path.startswith(prefix) 862 863defgetClientSpec(): 864"""Look at the p4 client spec, create a View() object that contains 865 all the mappings, and return it.""" 866 867 specList =p4CmdList("client -o") 868iflen(specList) !=1: 869die('Output from "client -o" is%dlines, expecting 1'% 870len(specList)) 871 872# dictionary of all client parameters 873 entry = specList[0] 874 875# the //client/ name 876 client_name = entry["Client"] 877 878# just the keys that start with "View" 879 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 880 881# hold this new View 882 view =View(client_name) 883 884# append the lines, in order, to the view 885for view_num inrange(len(view_keys)): 886 k ="View%d"% view_num 887if k not in view_keys: 888die("Expected view key%smissing"% k) 889 view.append(entry[k]) 890 891return view 892 893defgetClientRoot(): 894"""Grab the client directory.""" 895 896 output =p4CmdList("client -o") 897iflen(output) !=1: 898die('Output from "client -o" is%dlines, expecting 1'%len(output)) 899 900 entry = output[0] 901if"Root"not in entry: 902die('Client has no "Root"') 903 904return entry["Root"] 905 906# 907# P4 wildcards are not allowed in filenames. P4 complains 908# if you simply add them, but you can force it with "-f", in 909# which case it translates them into %xx encoding internally. 910# 911defwildcard_decode(path): 912# Search for and fix just these four characters. Do % last so 913# that fixing it does not inadvertently create new %-escapes. 914# Cannot have * in a filename in windows; untested as to 915# what p4 would do in such a case. 916if not platform.system() =="Windows": 917 path = path.replace("%2A","*") 918 path = path.replace("%23","#") \ 919.replace("%40","@") \ 920.replace("%25","%") 921return path 922 923defwildcard_encode(path): 924# do % first to avoid double-encoding the %s introduced here 925 path = path.replace("%","%25") \ 926.replace("*","%2A") \ 927.replace("#","%23") \ 928.replace("@","%40") 929return path 930 931defwildcard_present(path): 932 m = re.search("[*#@%]", path) 933return m is not None 934 935class Command: 936def__init__(self): 937 self.usage ="usage: %prog [options]" 938 self.needsGit =True 939 self.verbose =False 940 941class P4UserMap: 942def__init__(self): 943 self.userMapFromPerforceServer =False 944 self.myP4UserId =None 945 946defp4UserId(self): 947if self.myP4UserId: 948return self.myP4UserId 949 950 results =p4CmdList("user -o") 951for r in results: 952if r.has_key('User'): 953 self.myP4UserId = r['User'] 954return r['User'] 955die("Could not find your p4 user id") 956 957defp4UserIsMe(self, p4User): 958# return True if the given p4 user is actually me 959 me = self.p4UserId() 960if not p4User or p4User != me: 961return False 962else: 963return True 964 965defgetUserCacheFilename(self): 966 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 967return home +"/.gitp4-usercache.txt" 968 969defgetUserMapFromPerforceServer(self): 970if self.userMapFromPerforceServer: 971return 972 self.users = {} 973 self.emails = {} 974 975for output inp4CmdList("users"): 976if not output.has_key("User"): 977continue 978 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 979 self.emails[output["Email"]] = output["User"] 980 981 982 s ='' 983for(key, val)in self.users.items(): 984 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 985 986open(self.getUserCacheFilename(),"wb").write(s) 987 self.userMapFromPerforceServer =True 988 989defloadUserMapFromCache(self): 990 self.users = {} 991 self.userMapFromPerforceServer =False 992try: 993 cache =open(self.getUserCacheFilename(),"rb") 994 lines = cache.readlines() 995 cache.close() 996for line in lines: 997 entry = line.strip().split("\t") 998 self.users[entry[0]] = entry[1] 999exceptIOError:1000 self.getUserMapFromPerforceServer()10011002classP4Debug(Command):1003def__init__(self):1004 Command.__init__(self)1005 self.options = []1006 self.description ="A tool to debug the output of p4 -G."1007 self.needsGit =False10081009defrun(self, args):1010 j =01011for output inp4CmdList(args):1012print'Element:%d'% j1013 j +=11014print output1015return True10161017classP4RollBack(Command):1018def__init__(self):1019 Command.__init__(self)1020 self.options = [1021 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1022]1023 self.description ="A tool to debug the multi-branch import. Don't use :)"1024 self.rollbackLocalBranches =False10251026defrun(self, args):1027iflen(args) !=1:1028return False1029 maxChange =int(args[0])10301031if"p4ExitCode"inp4Cmd("changes -m 1"):1032die("Problems executing p4");10331034if self.rollbackLocalBranches:1035 refPrefix ="refs/heads/"1036 lines =read_pipe_lines("git rev-parse --symbolic --branches")1037else:1038 refPrefix ="refs/remotes/"1039 lines =read_pipe_lines("git rev-parse --symbolic --remotes")10401041for line in lines:1042if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1043 line = line.strip()1044 ref = refPrefix + line1045 log =extractLogMessageFromGitCommit(ref)1046 settings =extractSettingsGitLog(log)10471048 depotPaths = settings['depot-paths']1049 change = settings['change']10501051 changed =False10521053iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1054for p in depotPaths]))) ==0:1055print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1056system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1057continue10581059while change andint(change) > maxChange:1060 changed =True1061if self.verbose:1062print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1063system("git update-ref%s\"%s^\""% (ref, ref))1064 log =extractLogMessageFromGitCommit(ref)1065 settings =extractSettingsGitLog(log)106610671068 depotPaths = settings['depot-paths']1069 change = settings['change']10701071if changed:1072print"%srewound to%s"% (ref, change)10731074return True10751076classP4Submit(Command, P4UserMap):10771078 conflict_behavior_choices = ("ask","skip","quit")10791080def__init__(self):1081 Command.__init__(self)1082 P4UserMap.__init__(self)1083 self.options = [1084 optparse.make_option("--origin", dest="origin"),1085 optparse.make_option("-M", dest="detectRenames", action="store_true"),1086# preserve the user, requires relevant p4 permissions1087 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1088 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1089 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1090 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1091 optparse.make_option("--conflict", dest="conflict_behavior",1092 choices=self.conflict_behavior_choices),1093 optparse.make_option("--branch", dest="branch"),1094]1095 self.description ="Submit changes from git to the perforce depot."1096 self.usage +=" [name of git branch to submit into perforce depot]"1097 self.origin =""1098 self.detectRenames =False1099 self.preserveUser =gitConfigBool("git-p4.preserveUser")1100 self.dry_run =False1101 self.prepare_p4_only =False1102 self.conflict_behavior =None1103 self.isWindows = (platform.system() =="Windows")1104 self.exportLabels =False1105 self.p4HasMoveCommand =p4_has_move_command()1106 self.branch =None11071108defcheck(self):1109iflen(p4CmdList("opened ...")) >0:1110die("You have files opened with perforce! Close them before starting the sync.")11111112defseparate_jobs_from_description(self, message):1113"""Extract and return a possible Jobs field in the commit1114 message. It goes into a separate section in the p4 change1115 specification.11161117 A jobs line starts with "Jobs:" and looks like a new field1118 in a form. Values are white-space separated on the same1119 line or on following lines that start with a tab.11201121 This does not parse and extract the full git commit message1122 like a p4 form. It just sees the Jobs: line as a marker1123 to pass everything from then on directly into the p4 form,1124 but outside the description section.11251126 Return a tuple (stripped log message, jobs string)."""11271128 m = re.search(r'^Jobs:', message, re.MULTILINE)1129if m is None:1130return(message,None)11311132 jobtext = message[m.start():]1133 stripped_message = message[:m.start()].rstrip()1134return(stripped_message, jobtext)11351136defprepareLogMessage(self, template, message, jobs):1137"""Edits the template returned from "p4 change -o" to insert1138 the message in the Description field, and the jobs text in1139 the Jobs field."""1140 result =""11411142 inDescriptionSection =False11431144for line in template.split("\n"):1145if line.startswith("#"):1146 result += line +"\n"1147continue11481149if inDescriptionSection:1150if line.startswith("Files:")or line.startswith("Jobs:"):1151 inDescriptionSection =False1152# insert Jobs section1153if jobs:1154 result += jobs +"\n"1155else:1156continue1157else:1158if line.startswith("Description:"):1159 inDescriptionSection =True1160 line +="\n"1161for messageLine in message.split("\n"):1162 line +="\t"+ messageLine +"\n"11631164 result += line +"\n"11651166return result11671168defpatchRCSKeywords(self,file, pattern):1169# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1170(handle, outFileName) = tempfile.mkstemp(dir='.')1171try:1172 outFile = os.fdopen(handle,"w+")1173 inFile =open(file,"r")1174 regexp = re.compile(pattern, re.VERBOSE)1175for line in inFile.readlines():1176 line = regexp.sub(r'$\1$', line)1177 outFile.write(line)1178 inFile.close()1179 outFile.close()1180# Forcibly overwrite the original file1181 os.unlink(file)1182 shutil.move(outFileName,file)1183except:1184# cleanup our temporary file1185 os.unlink(outFileName)1186print"Failed to strip RCS keywords in%s"%file1187raise11881189print"Patched up RCS keywords in%s"%file11901191defp4UserForCommit(self,id):1192# Return the tuple (perforce user,git email) for a given git commit id1193 self.getUserMapFromPerforceServer()1194 gitEmail =read_pipe(["git","log","--max-count=1",1195"--format=%ae",id])1196 gitEmail = gitEmail.strip()1197if not self.emails.has_key(gitEmail):1198return(None,gitEmail)1199else:1200return(self.emails[gitEmail],gitEmail)12011202defcheckValidP4Users(self,commits):1203# check if any git authors cannot be mapped to p4 users1204foridin commits:1205(user,email) = self.p4UserForCommit(id)1206if not user:1207 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1208ifgitConfigBool("git-p4.allowMissingP4Users"):1209print"%s"% msg1210else:1211die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)12121213deflastP4Changelist(self):1214# Get back the last changelist number submitted in this client spec. This1215# then gets used to patch up the username in the change. If the same1216# client spec is being used by multiple processes then this might go1217# wrong.1218 results =p4CmdList("client -o")# find the current client1219 client =None1220for r in results:1221if r.has_key('Client'):1222 client = r['Client']1223break1224if not client:1225die("could not get client spec")1226 results =p4CmdList(["changes","-c", client,"-m","1"])1227for r in results:1228if r.has_key('change'):1229return r['change']1230die("Could not get changelist number for last submit - cannot patch up user details")12311232defmodifyChangelistUser(self, changelist, newUser):1233# fixup the user field of a changelist after it has been submitted.1234 changes =p4CmdList("change -o%s"% changelist)1235iflen(changes) !=1:1236die("Bad output from p4 change modifying%sto user%s"%1237(changelist, newUser))12381239 c = changes[0]1240if c['User'] == newUser:return# nothing to do1241 c['User'] = newUser1242input= marshal.dumps(c)12431244 result =p4CmdList("change -f -i", stdin=input)1245for r in result:1246if r.has_key('code'):1247if r['code'] =='error':1248die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1249if r.has_key('data'):1250print("Updated user field for changelist%sto%s"% (changelist, newUser))1251return1252die("Could not modify user field of changelist%sto%s"% (changelist, newUser))12531254defcanChangeChangelists(self):1255# check to see if we have p4 admin or super-user permissions, either of1256# which are required to modify changelists.1257 results =p4CmdList(["protects", self.depotPath])1258for r in results:1259if r.has_key('perm'):1260if r['perm'] =='admin':1261return11262if r['perm'] =='super':1263return11264return012651266defprepareSubmitTemplate(self):1267"""Run "p4 change -o" to grab a change specification template.1268 This does not use "p4 -G", as it is nice to keep the submission1269 template in original order, since a human might edit it.12701271 Remove lines in the Files section that show changes to files1272 outside the depot path we're committing into."""12731274 template =""1275 inFilesSection =False1276for line inp4_read_pipe_lines(['change','-o']):1277if line.endswith("\r\n"):1278 line = line[:-2] +"\n"1279if inFilesSection:1280if line.startswith("\t"):1281# path starts and ends with a tab1282 path = line[1:]1283 lastTab = path.rfind("\t")1284if lastTab != -1:1285 path = path[:lastTab]1286if notp4PathStartsWith(path, self.depotPath):1287continue1288else:1289 inFilesSection =False1290else:1291if line.startswith("Files:"):1292 inFilesSection =True12931294 template += line12951296return template12971298defedit_template(self, template_file):1299"""Invoke the editor to let the user change the submission1300 message. Return true if okay to continue with the submit."""13011302# if configured to skip the editing part, just submit1303ifgitConfigBool("git-p4.skipSubmitEdit"):1304return True13051306# look at the modification time, to check later if the user saved1307# the file1308 mtime = os.stat(template_file).st_mtime13091310# invoke the editor1311if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1312 editor = os.environ.get("P4EDITOR")1313else:1314 editor =read_pipe("git var GIT_EDITOR").strip()1315system(["sh","-c", ('%s"$@"'% editor), editor, template_file])13161317# If the file was not saved, prompt to see if this patch should1318# be skipped. But skip this verification step if configured so.1319ifgitConfigBool("git-p4.skipSubmitEditCheck"):1320return True13211322# modification time updated means user saved the file1323if os.stat(template_file).st_mtime > mtime:1324return True13251326while True:1327 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1328if response =='y':1329return True1330if response =='n':1331return False13321333defget_diff_description(self, editedFiles, filesToAdd):1334# diff1335if os.environ.has_key("P4DIFF"):1336del(os.environ["P4DIFF"])1337 diff =""1338for editedFile in editedFiles:1339 diff +=p4_read_pipe(['diff','-du',1340wildcard_encode(editedFile)])13411342# new file diff1343 newdiff =""1344for newFile in filesToAdd:1345 newdiff +="==== new file ====\n"1346 newdiff +="--- /dev/null\n"1347 newdiff +="+++%s\n"% newFile1348 f =open(newFile,"r")1349for line in f.readlines():1350 newdiff +="+"+ line1351 f.close()13521353return(diff + newdiff).replace('\r\n','\n')13541355defapplyCommit(self,id):1356"""Apply one commit, return True if it succeeded."""13571358print"Applying",read_pipe(["git","show","-s",1359"--format=format:%h%s",id])13601361(p4User, gitEmail) = self.p4UserForCommit(id)13621363 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1364 filesToAdd =set()1365 filesToDelete =set()1366 editedFiles =set()1367 pureRenameCopy =set()1368 filesToChangeExecBit = {}13691370for line in diff:1371 diff =parseDiffTreeEntry(line)1372 modifier = diff['status']1373 path = diff['src']1374if modifier =="M":1375p4_edit(path)1376ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1377 filesToChangeExecBit[path] = diff['dst_mode']1378 editedFiles.add(path)1379elif modifier =="A":1380 filesToAdd.add(path)1381 filesToChangeExecBit[path] = diff['dst_mode']1382if path in filesToDelete:1383 filesToDelete.remove(path)1384elif modifier =="D":1385 filesToDelete.add(path)1386if path in filesToAdd:1387 filesToAdd.remove(path)1388elif modifier =="C":1389 src, dest = diff['src'], diff['dst']1390p4_integrate(src, dest)1391 pureRenameCopy.add(dest)1392if diff['src_sha1'] != diff['dst_sha1']:1393p4_edit(dest)1394 pureRenameCopy.discard(dest)1395ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1396p4_edit(dest)1397 pureRenameCopy.discard(dest)1398 filesToChangeExecBit[dest] = diff['dst_mode']1399if self.isWindows:1400# turn off read-only attribute1401 os.chmod(dest, stat.S_IWRITE)1402 os.unlink(dest)1403 editedFiles.add(dest)1404elif modifier =="R":1405 src, dest = diff['src'], diff['dst']1406if self.p4HasMoveCommand:1407p4_edit(src)# src must be open before move1408p4_move(src, dest)# opens for (move/delete, move/add)1409else:1410p4_integrate(src, dest)1411if diff['src_sha1'] != diff['dst_sha1']:1412p4_edit(dest)1413else:1414 pureRenameCopy.add(dest)1415ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1416if not self.p4HasMoveCommand:1417p4_edit(dest)# with move: already open, writable1418 filesToChangeExecBit[dest] = diff['dst_mode']1419if not self.p4HasMoveCommand:1420if self.isWindows:1421 os.chmod(dest, stat.S_IWRITE)1422 os.unlink(dest)1423 filesToDelete.add(src)1424 editedFiles.add(dest)1425else:1426die("unknown modifier%sfor%s"% (modifier, path))14271428 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1429 patchcmd = diffcmd +" | git apply "1430 tryPatchCmd = patchcmd +"--check -"1431 applyPatchCmd = patchcmd +"--check --apply -"1432 patch_succeeded =True14331434if os.system(tryPatchCmd) !=0:1435 fixed_rcs_keywords =False1436 patch_succeeded =False1437print"Unfortunately applying the change failed!"14381439# Patch failed, maybe it's just RCS keyword woes. Look through1440# the patch to see if that's possible.1441ifgitConfigBool("git-p4.attemptRCSCleanup"):1442file=None1443 pattern =None1444 kwfiles = {}1445forfilein editedFiles | filesToDelete:1446# did this file's delta contain RCS keywords?1447 pattern =p4_keywords_regexp_for_file(file)14481449if pattern:1450# this file is a possibility...look for RCS keywords.1451 regexp = re.compile(pattern, re.VERBOSE)1452for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1453if regexp.search(line):1454if verbose:1455print"got keyword match on%sin%sin%s"% (pattern, line,file)1456 kwfiles[file] = pattern1457break14581459forfilein kwfiles:1460if verbose:1461print"zapping%swith%s"% (line,pattern)1462# File is being deleted, so not open in p4. Must1463# disable the read-only bit on windows.1464if self.isWindows andfilenot in editedFiles:1465 os.chmod(file, stat.S_IWRITE)1466 self.patchRCSKeywords(file, kwfiles[file])1467 fixed_rcs_keywords =True14681469if fixed_rcs_keywords:1470print"Retrying the patch with RCS keywords cleaned up"1471if os.system(tryPatchCmd) ==0:1472 patch_succeeded =True14731474if not patch_succeeded:1475for f in editedFiles:1476p4_revert(f)1477return False14781479#1480# Apply the patch for real, and do add/delete/+x handling.1481#1482system(applyPatchCmd)14831484for f in filesToAdd:1485p4_add(f)1486for f in filesToDelete:1487p4_revert(f)1488p4_delete(f)14891490# Set/clear executable bits1491for f in filesToChangeExecBit.keys():1492 mode = filesToChangeExecBit[f]1493setP4ExecBit(f, mode)14941495#1496# Build p4 change description, starting with the contents1497# of the git commit message.1498#1499 logMessage =extractLogMessageFromGitCommit(id)1500 logMessage = logMessage.strip()1501(logMessage, jobs) = self.separate_jobs_from_description(logMessage)15021503 template = self.prepareSubmitTemplate()1504 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)15051506if self.preserveUser:1507 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User15081509if self.checkAuthorship and not self.p4UserIsMe(p4User):1510 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1511 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1512 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"15131514 separatorLine ="######## everything below this line is just the diff #######\n"1515if not self.prepare_p4_only:1516 submitTemplate += separatorLine1517 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)15181519(handle, fileName) = tempfile.mkstemp()1520 tmpFile = os.fdopen(handle,"w+b")1521if self.isWindows:1522 submitTemplate = submitTemplate.replace("\n","\r\n")1523 tmpFile.write(submitTemplate)1524 tmpFile.close()15251526if self.prepare_p4_only:1527#1528# Leave the p4 tree prepared, and the submit template around1529# and let the user decide what to do next1530#1531print1532print"P4 workspace prepared for submission."1533print"To submit or revert, go to client workspace"1534print" "+ self.clientPath1535print1536print"To submit, use\"p4 submit\"to write a new description,"1537print"or\"p4 submit -i <%s\"to use the one prepared by" \1538"\"git p4\"."% fileName1539print"You can delete the file\"%s\"when finished."% fileName15401541if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1542print"To preserve change ownership by user%s, you must\n" \1543"do\"p4 change -f <change>\"after submitting and\n" \1544"edit the User field."1545if pureRenameCopy:1546print"After submitting, renamed files must be re-synced."1547print"Invoke\"p4 sync -f\"on each of these files:"1548for f in pureRenameCopy:1549print" "+ f15501551print1552print"To revert the changes, use\"p4 revert ...\", and delete"1553print"the submit template file\"%s\""% fileName1554if filesToAdd:1555print"Since the commit adds new files, they must be deleted:"1556for f in filesToAdd:1557print" "+ f1558print1559return True15601561#1562# Let the user edit the change description, then submit it.1563#1564if self.edit_template(fileName):1565# read the edited message and submit1566 ret =True1567 tmpFile =open(fileName,"rb")1568 message = tmpFile.read()1569 tmpFile.close()1570if self.isWindows:1571 message = message.replace("\r\n","\n")1572 submitTemplate = message[:message.index(separatorLine)]1573p4_write_pipe(['submit','-i'], submitTemplate)15741575if self.preserveUser:1576if p4User:1577# Get last changelist number. Cannot easily get it from1578# the submit command output as the output is1579# unmarshalled.1580 changelist = self.lastP4Changelist()1581 self.modifyChangelistUser(changelist, p4User)15821583# The rename/copy happened by applying a patch that created a1584# new file. This leaves it writable, which confuses p4.1585for f in pureRenameCopy:1586p4_sync(f,"-f")15871588else:1589# skip this patch1590 ret =False1591print"Submission cancelled, undoing p4 changes."1592for f in editedFiles:1593p4_revert(f)1594for f in filesToAdd:1595p4_revert(f)1596 os.remove(f)1597for f in filesToDelete:1598p4_revert(f)15991600 os.remove(fileName)1601return ret16021603# Export git tags as p4 labels. Create a p4 label and then tag1604# with that.1605defexportGitTags(self, gitTags):1606 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1607iflen(validLabelRegexp) ==0:1608 validLabelRegexp = defaultLabelRegexp1609 m = re.compile(validLabelRegexp)16101611for name in gitTags:16121613if not m.match(name):1614if verbose:1615print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1616continue16171618# Get the p4 commit this corresponds to1619 logMessage =extractLogMessageFromGitCommit(name)1620 values =extractSettingsGitLog(logMessage)16211622if not values.has_key('change'):1623# a tag pointing to something not sent to p4; ignore1624if verbose:1625print"git tag%sdoes not give a p4 commit"% name1626continue1627else:1628 changelist = values['change']16291630# Get the tag details.1631 inHeader =True1632 isAnnotated =False1633 body = []1634for l inread_pipe_lines(["git","cat-file","-p", name]):1635 l = l.strip()1636if inHeader:1637if re.match(r'tag\s+', l):1638 isAnnotated =True1639elif re.match(r'\s*$', l):1640 inHeader =False1641continue1642else:1643 body.append(l)16441645if not isAnnotated:1646 body = ["lightweight tag imported by git p4\n"]16471648# Create the label - use the same view as the client spec we are using1649 clientSpec =getClientSpec()16501651 labelTemplate ="Label:%s\n"% name1652 labelTemplate +="Description:\n"1653for b in body:1654 labelTemplate +="\t"+ b +"\n"1655 labelTemplate +="View:\n"1656for depot_side in clientSpec.mappings:1657 labelTemplate +="\t%s\n"% depot_side16581659if self.dry_run:1660print"Would create p4 label%sfor tag"% name1661elif self.prepare_p4_only:1662print"Not creating p4 label%sfor tag due to option" \1663" --prepare-p4-only"% name1664else:1665p4_write_pipe(["label","-i"], labelTemplate)16661667# Use the label1668p4_system(["tag","-l", name] +1669["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16701671if verbose:1672print"created p4 label for tag%s"% name16731674defrun(self, args):1675iflen(args) ==0:1676 self.master =currentGitBranch()1677iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1678die("Detecting current git branch failed!")1679eliflen(args) ==1:1680 self.master = args[0]1681if notbranchExists(self.master):1682die("Branch%sdoes not exist"% self.master)1683else:1684return False16851686 allowSubmit =gitConfig("git-p4.allowSubmit")1687iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1688die("%sis not in git-p4.allowSubmit"% self.master)16891690[upstream, settings] =findUpstreamBranchPoint()1691 self.depotPath = settings['depot-paths'][0]1692iflen(self.origin) ==0:1693 self.origin = upstream16941695if self.preserveUser:1696if not self.canChangeChangelists():1697die("Cannot preserve user names without p4 super-user or admin permissions")16981699# if not set from the command line, try the config file1700if self.conflict_behavior is None:1701 val =gitConfig("git-p4.conflict")1702if val:1703if val not in self.conflict_behavior_choices:1704die("Invalid value '%s' for config git-p4.conflict"% val)1705else:1706 val ="ask"1707 self.conflict_behavior = val17081709if self.verbose:1710print"Origin branch is "+ self.origin17111712iflen(self.depotPath) ==0:1713print"Internal error: cannot locate perforce depot path from existing branches"1714 sys.exit(128)17151716 self.useClientSpec =False1717ifgitConfigBool("git-p4.useclientspec"):1718 self.useClientSpec =True1719if self.useClientSpec:1720 self.clientSpecDirs =getClientSpec()17211722# Check for the existance of P4 branches1723 branchesDetected = (len(p4BranchesInGit().keys()) >1)17241725if self.useClientSpec and not branchesDetected:1726# all files are relative to the client spec1727 self.clientPath =getClientRoot()1728else:1729 self.clientPath =p4Where(self.depotPath)17301731if self.clientPath =="":1732die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)17331734print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1735 self.oldWorkingDirectory = os.getcwd()17361737# ensure the clientPath exists1738 new_client_dir =False1739if not os.path.exists(self.clientPath):1740 new_client_dir =True1741 os.makedirs(self.clientPath)17421743chdir(self.clientPath, is_client_path=True)1744if self.dry_run:1745print"Would synchronize p4 checkout in%s"% self.clientPath1746else:1747print"Synchronizing p4 checkout..."1748if new_client_dir:1749# old one was destroyed, and maybe nobody told p41750p4_sync("...","-f")1751else:1752p4_sync("...")1753 self.check()17541755 commits = []1756for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1757 commits.append(line.strip())1758 commits.reverse()17591760if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1761 self.checkAuthorship =False1762else:1763 self.checkAuthorship =True17641765if self.preserveUser:1766 self.checkValidP4Users(commits)17671768#1769# Build up a set of options to be passed to diff when1770# submitting each commit to p4.1771#1772if self.detectRenames:1773# command-line -M arg1774 self.diffOpts ="-M"1775else:1776# If not explicitly set check the config variable1777 detectRenames =gitConfig("git-p4.detectRenames")17781779if detectRenames.lower() =="false"or detectRenames =="":1780 self.diffOpts =""1781elif detectRenames.lower() =="true":1782 self.diffOpts ="-M"1783else:1784 self.diffOpts ="-M%s"% detectRenames17851786# no command-line arg for -C or --find-copies-harder, just1787# config variables1788 detectCopies =gitConfig("git-p4.detectCopies")1789if detectCopies.lower() =="false"or detectCopies =="":1790pass1791elif detectCopies.lower() =="true":1792 self.diffOpts +=" -C"1793else:1794 self.diffOpts +=" -C%s"% detectCopies17951796ifgitConfigBool("git-p4.detectCopiesHarder"):1797 self.diffOpts +=" --find-copies-harder"17981799#1800# Apply the commits, one at a time. On failure, ask if should1801# continue to try the rest of the patches, or quit.1802#1803if self.dry_run:1804print"Would apply"1805 applied = []1806 last =len(commits) -11807for i, commit inenumerate(commits):1808if self.dry_run:1809print" ",read_pipe(["git","show","-s",1810"--format=format:%h%s", commit])1811 ok =True1812else:1813 ok = self.applyCommit(commit)1814if ok:1815 applied.append(commit)1816else:1817if self.prepare_p4_only and i < last:1818print"Processing only the first commit due to option" \1819" --prepare-p4-only"1820break1821if i < last:1822 quit =False1823while True:1824# prompt for what to do, or use the option/variable1825if self.conflict_behavior =="ask":1826print"What do you want to do?"1827 response =raw_input("[s]kip this commit but apply"1828" the rest, or [q]uit? ")1829if not response:1830continue1831elif self.conflict_behavior =="skip":1832 response ="s"1833elif self.conflict_behavior =="quit":1834 response ="q"1835else:1836die("Unknown conflict_behavior '%s'"%1837 self.conflict_behavior)18381839if response[0] =="s":1840print"Skipping this commit, but applying the rest"1841break1842if response[0] =="q":1843print"Quitting"1844 quit =True1845break1846if quit:1847break18481849chdir(self.oldWorkingDirectory)18501851if self.dry_run:1852pass1853elif self.prepare_p4_only:1854pass1855eliflen(commits) ==len(applied):1856print"All commits applied!"18571858 sync =P4Sync()1859if self.branch:1860 sync.branch = self.branch1861 sync.run([])18621863 rebase =P4Rebase()1864 rebase.rebase()18651866else:1867iflen(applied) ==0:1868print"No commits applied."1869else:1870print"Applied only the commits marked with '*':"1871for c in commits:1872if c in applied:1873 star ="*"1874else:1875 star =" "1876print star,read_pipe(["git","show","-s",1877"--format=format:%h%s", c])1878print"You will have to do 'git p4 sync' and rebase."18791880ifgitConfigBool("git-p4.exportLabels"):1881 self.exportLabels =True18821883if self.exportLabels:1884 p4Labels =getP4Labels(self.depotPath)1885 gitTags =getGitTags()18861887 missingGitTags = gitTags - p4Labels1888 self.exportGitTags(missingGitTags)18891890# exit with error unless everything applied perfectly1891iflen(commits) !=len(applied):1892 sys.exit(1)18931894return True18951896classView(object):1897"""Represent a p4 view ("p4 help views"), and map files in a1898 repo according to the view."""18991900def__init__(self, client_name):1901 self.mappings = []1902 self.client_prefix ="//%s/"% client_name1903# cache results of "p4 where" to lookup client file locations1904 self.client_spec_path_cache = {}19051906defappend(self, view_line):1907"""Parse a view line, splitting it into depot and client1908 sides. Append to self.mappings, preserving order. This1909 is only needed for tag creation."""19101911# Split the view line into exactly two words. P4 enforces1912# structure on these lines that simplifies this quite a bit.1913#1914# Either or both words may be double-quoted.1915# Single quotes do not matter.1916# Double-quote marks cannot occur inside the words.1917# A + or - prefix is also inside the quotes.1918# There are no quotes unless they contain a space.1919# The line is already white-space stripped.1920# The two words are separated by a single space.1921#1922if view_line[0] =='"':1923# First word is double quoted. Find its end.1924 close_quote_index = view_line.find('"',1)1925if close_quote_index <=0:1926die("No first-word closing quote found:%s"% view_line)1927 depot_side = view_line[1:close_quote_index]1928# skip closing quote and space1929 rhs_index = close_quote_index +1+11930else:1931 space_index = view_line.find(" ")1932if space_index <=0:1933die("No word-splitting space found:%s"% view_line)1934 depot_side = view_line[0:space_index]1935 rhs_index = space_index +119361937# prefix + means overlay on previous mapping1938if depot_side.startswith("+"):1939 depot_side = depot_side[1:]19401941# prefix - means exclude this path, leave out of mappings1942 exclude =False1943if depot_side.startswith("-"):1944 exclude =True1945 depot_side = depot_side[1:]19461947if not exclude:1948 self.mappings.append(depot_side)19491950defconvert_client_path(self, clientFile):1951# chop off //client/ part to make it relative1952if not clientFile.startswith(self.client_prefix):1953die("No prefix '%s' on clientFile '%s'"%1954(self.client_prefix, clientFile))1955return clientFile[len(self.client_prefix):]19561957defupdate_client_spec_path_cache(self, files):1958""" Caching file paths by "p4 where" batch query """19591960# List depot file paths exclude that already cached1961 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]19621963iflen(fileArgs) ==0:1964return# All files in cache19651966 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1967for res in where_result:1968if"code"in res and res["code"] =="error":1969# assume error is "... file(s) not in client view"1970continue1971if"clientFile"not in res:1972die("No clientFile in 'p4 where' output")1973if"unmap"in res:1974# it will list all of them, but only one not unmap-ped1975continue1976ifgitConfigBool("core.ignorecase"):1977 res['depotFile'] = res['depotFile'].lower()1978 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19791980# not found files or unmap files set to ""1981for depotFile in fileArgs:1982ifgitConfigBool("core.ignorecase"):1983 depotFile = depotFile.lower()1984if depotFile not in self.client_spec_path_cache:1985 self.client_spec_path_cache[depotFile] =""19861987defmap_in_client(self, depot_path):1988"""Return the relative location in the client where this1989 depot file should live. Returns "" if the file should1990 not be mapped in the client."""19911992ifgitConfigBool("core.ignorecase"):1993 depot_path = depot_path.lower()19941995if depot_path in self.client_spec_path_cache:1996return self.client_spec_path_cache[depot_path]19971998die("Error:%sis not found in client spec path"% depot_path )1999return""20002001classP4Sync(Command, P4UserMap):2002 delete_actions = ("delete","move/delete","purge")20032004def__init__(self):2005 Command.__init__(self)2006 P4UserMap.__init__(self)2007 self.options = [2008 optparse.make_option("--branch", dest="branch"),2009 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2010 optparse.make_option("--changesfile", dest="changesFile"),2011 optparse.make_option("--silent", dest="silent", action="store_true"),2012 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2013 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2014 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2015help="Import into refs/heads/ , not refs/remotes"),2016 optparse.make_option("--max-changes", dest="maxChanges",2017help="Maximum number of changes to import"),2018 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2019help="Internal block size to use when iteratively calling p4 changes"),2020 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2021help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2022 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2023help="Only sync files that are included in the Perforce Client Spec"),2024 optparse.make_option("-/", dest="cloneExclude",2025 action="append",type="string",2026help="exclude depot path"),2027]2028 self.description ="""Imports from Perforce into a git repository.\n2029 example:2030 //depot/my/project/ -- to import the current head2031 //depot/my/project/@all -- to import everything2032 //depot/my/project/@1,6 -- to import only from revision 1 to 620332034 (a ... is not needed in the path p4 specification, it's added implicitly)"""20352036 self.usage +=" //depot/path[@revRange]"2037 self.silent =False2038 self.createdBranches =set()2039 self.committedChanges =set()2040 self.branch =""2041 self.detectBranches =False2042 self.detectLabels =False2043 self.importLabels =False2044 self.changesFile =""2045 self.syncWithOrigin =True2046 self.importIntoRemotes =True2047 self.maxChanges =""2048 self.changes_block_size =None2049 self.keepRepoPath =False2050 self.depotPaths =None2051 self.p4BranchesInGit = []2052 self.cloneExclude = []2053 self.useClientSpec =False2054 self.useClientSpec_from_options =False2055 self.clientSpecDirs =None2056 self.tempBranches = []2057 self.tempBranchLocation ="git-p4-tmp"20582059ifgitConfig("git-p4.syncFromOrigin") =="false":2060 self.syncWithOrigin =False20612062# This is required for the "append" cloneExclude action2063defensure_value(self, attr, value):2064if nothasattr(self, attr)orgetattr(self, attr)is None:2065setattr(self, attr, value)2066returngetattr(self, attr)20672068# Force a checkpoint in fast-import and wait for it to finish2069defcheckpoint(self):2070 self.gitStream.write("checkpoint\n\n")2071 self.gitStream.write("progress checkpoint\n\n")2072 out = self.gitOutput.readline()2073if self.verbose:2074print"checkpoint finished: "+ out20752076defextractFilesFromCommit(self, commit):2077 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2078for path in self.cloneExclude]2079 files = []2080 fnum =02081while commit.has_key("depotFile%s"% fnum):2082 path = commit["depotFile%s"% fnum]20832084if[p for p in self.cloneExclude2085ifp4PathStartsWith(path, p)]:2086 found =False2087else:2088 found = [p for p in self.depotPaths2089ifp4PathStartsWith(path, p)]2090if not found:2091 fnum = fnum +12092continue20932094file= {}2095file["path"] = path2096file["rev"] = commit["rev%s"% fnum]2097file["action"] = commit["action%s"% fnum]2098file["type"] = commit["type%s"% fnum]2099 files.append(file)2100 fnum = fnum +12101return files21022103defstripRepoPath(self, path, prefixes):2104"""When streaming files, this is called to map a p4 depot path2105 to where it should go in git. The prefixes are either2106 self.depotPaths, or self.branchPrefixes in the case of2107 branch detection."""21082109if self.useClientSpec:2110# branch detection moves files up a level (the branch name)2111# from what client spec interpretation gives2112 path = self.clientSpecDirs.map_in_client(path)2113if self.detectBranches:2114for b in self.knownBranches:2115if path.startswith(b +"/"):2116 path = path[len(b)+1:]21172118elif self.keepRepoPath:2119# Preserve everything in relative path name except leading2120# //depot/; just look at first prefix as they all should2121# be in the same depot.2122 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2123ifp4PathStartsWith(path, depot):2124 path = path[len(depot):]21252126else:2127for p in prefixes:2128ifp4PathStartsWith(path, p):2129 path = path[len(p):]2130break21312132 path =wildcard_decode(path)2133return path21342135defsplitFilesIntoBranches(self, commit):2136"""Look at each depotFile in the commit to figure out to what2137 branch it belongs."""21382139if self.clientSpecDirs:2140 files = self.extractFilesFromCommit(commit)2141 self.clientSpecDirs.update_client_spec_path_cache(files)21422143 branches = {}2144 fnum =02145while commit.has_key("depotFile%s"% fnum):2146 path = commit["depotFile%s"% fnum]2147 found = [p for p in self.depotPaths2148ifp4PathStartsWith(path, p)]2149if not found:2150 fnum = fnum +12151continue21522153file= {}2154file["path"] = path2155file["rev"] = commit["rev%s"% fnum]2156file["action"] = commit["action%s"% fnum]2157file["type"] = commit["type%s"% fnum]2158 fnum = fnum +121592160# start with the full relative path where this file would2161# go in a p4 client2162if self.useClientSpec:2163 relPath = self.clientSpecDirs.map_in_client(path)2164else:2165 relPath = self.stripRepoPath(path, self.depotPaths)21662167for branch in self.knownBranches.keys():2168# add a trailing slash so that a commit into qt/4.2foo2169# doesn't end up in qt/4.2, e.g.2170if relPath.startswith(branch +"/"):2171if branch not in branches:2172 branches[branch] = []2173 branches[branch].append(file)2174break21752176return branches21772178# output one file from the P4 stream2179# - helper for streamP4Files21802181defstreamOneP4File(self,file, contents):2182 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2183if verbose:2184 size =int(self.stream_file['fileSize'])2185 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2186 sys.stdout.flush()21872188(type_base, type_mods) =split_p4_type(file["type"])21892190 git_mode ="100644"2191if"x"in type_mods:2192 git_mode ="100755"2193if type_base =="symlink":2194 git_mode ="120000"2195# p4 print on a symlink sometimes contains "target\n";2196# if it does, remove the newline2197 data =''.join(contents)2198if not data:2199# Some version of p4 allowed creating a symlink that pointed2200# to nothing. This causes p4 errors when checking out such2201# a change, and errors here too. Work around it by ignoring2202# the bad symlink; hopefully a future change fixes it.2203print"\nIgnoring empty symlink in%s"%file['depotFile']2204return2205elif data[-1] =='\n':2206 contents = [data[:-1]]2207else:2208 contents = [data]22092210if type_base =="utf16":2211# p4 delivers different text in the python output to -G2212# than it does when using "print -o", or normal p4 client2213# operations. utf16 is converted to ascii or utf8, perhaps.2214# But ascii text saved as -t utf16 is completely mangled.2215# Invoke print -o to get the real contents.2216#2217# On windows, the newlines will always be mangled by print, so put2218# them back too. This is not needed to the cygwin windows version,2219# just the native "NT" type.2220#2221 text =p4_read_pipe(['print','-q','-o','-',"%s@%s"% (file['depotFile'],file['change']) ])2222ifp4_version_string().find("/NT") >=0:2223 text = text.replace("\r\n","\n")2224 contents = [ text ]22252226if type_base =="apple":2227# Apple filetype files will be streamed as a concatenation of2228# its appledouble header and the contents. This is useless2229# on both macs and non-macs. If using "print -q -o xx", it2230# will create "xx" with the data, and "%xx" with the header.2231# This is also not very useful.2232#2233# Ideally, someday, this script can learn how to generate2234# appledouble files directly and import those to git, but2235# non-mac machines can never find a use for apple filetype.2236print"\nIgnoring apple filetype file%s"%file['depotFile']2237return22382239# Note that we do not try to de-mangle keywords on utf16 files,2240# even though in theory somebody may want that.2241 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2242if pattern:2243 regexp = re.compile(pattern, re.VERBOSE)2244 text =''.join(contents)2245 text = regexp.sub(r'$\1$', text)2246 contents = [ text ]22472248 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22492250# total length...2251 length =02252for d in contents:2253 length = length +len(d)22542255 self.gitStream.write("data%d\n"% length)2256for d in contents:2257 self.gitStream.write(d)2258 self.gitStream.write("\n")22592260defstreamOneP4Deletion(self,file):2261 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2262if verbose:2263 sys.stdout.write("delete%s\n"% relPath)2264 sys.stdout.flush()2265 self.gitStream.write("D%s\n"% relPath)22662267# handle another chunk of streaming data2268defstreamP4FilesCb(self, marshalled):22692270# catch p4 errors and complain2271 err =None2272if"code"in marshalled:2273if marshalled["code"] =="error":2274if"data"in marshalled:2275 err = marshalled["data"].rstrip()22762277if not err and'fileSize'in self.stream_file:2278 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2279if required_bytes >0:2280 err ='Not enough space left on%s! Free at least%iMB.'% (2281 os.getcwd(), required_bytes/1024/10242282)22832284if err:2285 f =None2286if self.stream_have_file_info:2287if"depotFile"in self.stream_file:2288 f = self.stream_file["depotFile"]2289# force a failure in fast-import, else an empty2290# commit will be made2291 self.gitStream.write("\n")2292 self.gitStream.write("die-now\n")2293 self.gitStream.close()2294# ignore errors, but make sure it exits first2295 self.importProcess.wait()2296if f:2297die("Error from p4 print for%s:%s"% (f, err))2298else:2299die("Error from p4 print:%s"% err)23002301if marshalled.has_key('depotFile')and self.stream_have_file_info:2302# start of a new file - output the old one first2303 self.streamOneP4File(self.stream_file, self.stream_contents)2304 self.stream_file = {}2305 self.stream_contents = []2306 self.stream_have_file_info =False23072308# pick up the new file information... for the2309# 'data' field we need to append to our array2310for k in marshalled.keys():2311if k =='data':2312if'streamContentSize'not in self.stream_file:2313 self.stream_file['streamContentSize'] =02314 self.stream_file['streamContentSize'] +=len(marshalled['data'])2315 self.stream_contents.append(marshalled['data'])2316else:2317 self.stream_file[k] = marshalled[k]23182319if(verbose and2320'streamContentSize'in self.stream_file and2321'fileSize'in self.stream_file and2322'depotFile'in self.stream_file):2323 size =int(self.stream_file["fileSize"])2324if size >0:2325 progress =100*self.stream_file['streamContentSize']/size2326 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2327 sys.stdout.flush()23282329 self.stream_have_file_info =True23302331# Stream directly from "p4 files" into "git fast-import"2332defstreamP4Files(self, files):2333 filesForCommit = []2334 filesToRead = []2335 filesToDelete = []23362337for f in files:2338# if using a client spec, only add the files that have2339# a path in the client2340if self.clientSpecDirs:2341if self.clientSpecDirs.map_in_client(f['path']) =="":2342continue23432344 filesForCommit.append(f)2345if f['action']in self.delete_actions:2346 filesToDelete.append(f)2347else:2348 filesToRead.append(f)23492350# deleted files...2351for f in filesToDelete:2352 self.streamOneP4Deletion(f)23532354iflen(filesToRead) >0:2355 self.stream_file = {}2356 self.stream_contents = []2357 self.stream_have_file_info =False23582359# curry self argument2360defstreamP4FilesCbSelf(entry):2361 self.streamP4FilesCb(entry)23622363 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23642365p4CmdList(["-x","-","print"],2366 stdin=fileArgs,2367 cb=streamP4FilesCbSelf)23682369# do the last chunk2370if self.stream_file.has_key('depotFile'):2371 self.streamOneP4File(self.stream_file, self.stream_contents)23722373defmake_email(self, userid):2374if userid in self.users:2375return self.users[userid]2376else:2377return"%s<a@b>"% userid23782379# Stream a p4 tag2380defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2381if verbose:2382print"writing tag%sfor commit%s"% (labelName, commit)2383 gitStream.write("tag%s\n"% labelName)2384 gitStream.write("from%s\n"% commit)23852386if labelDetails.has_key('Owner'):2387 owner = labelDetails["Owner"]2388else:2389 owner =None23902391# Try to use the owner of the p4 label, or failing that,2392# the current p4 user id.2393if owner:2394 email = self.make_email(owner)2395else:2396 email = self.make_email(self.p4UserId())2397 tagger ="%s %s %s"% (email, epoch, self.tz)23982399 gitStream.write("tagger%s\n"% tagger)24002401print"labelDetails=",labelDetails2402if labelDetails.has_key('Description'):2403 description = labelDetails['Description']2404else:2405 description ='Label from git p4'24062407 gitStream.write("data%d\n"%len(description))2408 gitStream.write(description)2409 gitStream.write("\n")24102411defcommit(self, details, files, branch, parent =""):2412 epoch = details["time"]2413 author = details["user"]24142415if self.verbose:2416print"commit into%s"% branch24172418# start with reading files; if that fails, we should not2419# create a commit.2420 new_files = []2421for f in files:2422if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2423 new_files.append(f)2424else:2425 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])24262427if self.clientSpecDirs:2428 self.clientSpecDirs.update_client_spec_path_cache(files)24292430 self.gitStream.write("commit%s\n"% branch)2431# gitStream.write("mark :%s\n" % details["change"])2432 self.committedChanges.add(int(details["change"]))2433 committer =""2434if author not in self.users:2435 self.getUserMapFromPerforceServer()2436 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)24372438 self.gitStream.write("committer%s\n"% committer)24392440 self.gitStream.write("data <<EOT\n")2441 self.gitStream.write(details["desc"])2442 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2443(','.join(self.branchPrefixes), details["change"]))2444iflen(details['options']) >0:2445 self.gitStream.write(": options =%s"% details['options'])2446 self.gitStream.write("]\nEOT\n\n")24472448iflen(parent) >0:2449if self.verbose:2450print"parent%s"% parent2451 self.gitStream.write("from%s\n"% parent)24522453 self.streamP4Files(new_files)2454 self.gitStream.write("\n")24552456 change =int(details["change"])24572458if self.labels.has_key(change):2459 label = self.labels[change]2460 labelDetails = label[0]2461 labelRevisions = label[1]2462if self.verbose:2463print"Change%sis labelled%s"% (change, labelDetails)24642465 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2466for p in self.branchPrefixes])24672468iflen(files) ==len(labelRevisions):24692470 cleanedFiles = {}2471for info in files:2472if info["action"]in self.delete_actions:2473continue2474 cleanedFiles[info["depotFile"]] = info["rev"]24752476if cleanedFiles == labelRevisions:2477 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24782479else:2480if not self.silent:2481print("Tag%sdoes not match with change%s: files do not match."2482% (labelDetails["label"], change))24832484else:2485if not self.silent:2486print("Tag%sdoes not match with change%s: file count is different."2487% (labelDetails["label"], change))24882489# Build a dictionary of changelists and labels, for "detect-labels" option.2490defgetLabels(self):2491 self.labels = {}24922493 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2494iflen(l) >0and not self.silent:2495print"Finding files belonging to labels in%s"% `self.depotPaths`24962497for output in l:2498 label = output["label"]2499 revisions = {}2500 newestChange =02501if self.verbose:2502print"Querying files for label%s"% label2503forfileinp4CmdList(["files"] +2504["%s...@%s"% (p, label)2505for p in self.depotPaths]):2506 revisions[file["depotFile"]] =file["rev"]2507 change =int(file["change"])2508if change > newestChange:2509 newestChange = change25102511 self.labels[newestChange] = [output, revisions]25122513if self.verbose:2514print"Label changes:%s"% self.labels.keys()25152516# Import p4 labels as git tags. A direct mapping does not2517# exist, so assume that if all the files are at the same revision2518# then we can use that, or it's something more complicated we should2519# just ignore.2520defimportP4Labels(self, stream, p4Labels):2521if verbose:2522print"import p4 labels: "+' '.join(p4Labels)25232524 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2525 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2526iflen(validLabelRegexp) ==0:2527 validLabelRegexp = defaultLabelRegexp2528 m = re.compile(validLabelRegexp)25292530for name in p4Labels:2531 commitFound =False25322533if not m.match(name):2534if verbose:2535print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2536continue25372538if name in ignoredP4Labels:2539continue25402541 labelDetails =p4CmdList(['label',"-o", name])[0]25422543# get the most recent changelist for each file in this label2544 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2545for p in self.depotPaths])25462547if change.has_key('change'):2548# find the corresponding git commit; take the oldest commit2549 changelist =int(change['change'])2550 gitCommit =read_pipe(["git","rev-list","--max-count=1",2551"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2552iflen(gitCommit) ==0:2553print"could not find git commit for changelist%d"% changelist2554else:2555 gitCommit = gitCommit.strip()2556 commitFound =True2557# Convert from p4 time format2558try:2559 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2560exceptValueError:2561print"Could not convert label time%s"% labelDetails['Update']2562 tmwhen =125632564 when =int(time.mktime(tmwhen))2565 self.streamTag(stream, name, labelDetails, gitCommit, when)2566if verbose:2567print"p4 label%smapped to git commit%s"% (name, gitCommit)2568else:2569if verbose:2570print"Label%shas no changelists - possibly deleted?"% name25712572if not commitFound:2573# We can't import this label; don't try again as it will get very2574# expensive repeatedly fetching all the files for labels that will2575# never be imported. If the label is moved in the future, the2576# ignore will need to be removed manually.2577system(["git","config","--add","git-p4.ignoredP4Labels", name])25782579defguessProjectName(self):2580for p in self.depotPaths:2581if p.endswith("/"):2582 p = p[:-1]2583 p = p[p.strip().rfind("/") +1:]2584if not p.endswith("/"):2585 p +="/"2586return p25872588defgetBranchMapping(self):2589 lostAndFoundBranches =set()25902591 user =gitConfig("git-p4.branchUser")2592iflen(user) >0:2593 command ="branches -u%s"% user2594else:2595 command ="branches"25962597for info inp4CmdList(command):2598 details =p4Cmd(["branch","-o", info["branch"]])2599 viewIdx =02600while details.has_key("View%s"% viewIdx):2601 paths = details["View%s"% viewIdx].split(" ")2602 viewIdx = viewIdx +12603# require standard //depot/foo/... //depot/bar/... mapping2604iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2605continue2606 source = paths[0]2607 destination = paths[1]2608## HACK2609ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2610 source = source[len(self.depotPaths[0]):-4]2611 destination = destination[len(self.depotPaths[0]):-4]26122613if destination in self.knownBranches:2614if not self.silent:2615print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2616print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2617continue26182619 self.knownBranches[destination] = source26202621 lostAndFoundBranches.discard(destination)26222623if source not in self.knownBranches:2624 lostAndFoundBranches.add(source)26252626# Perforce does not strictly require branches to be defined, so we also2627# check git config for a branch list.2628#2629# Example of branch definition in git config file:2630# [git-p4]2631# branchList=main:branchA2632# branchList=main:branchB2633# branchList=branchA:branchC2634 configBranches =gitConfigList("git-p4.branchList")2635for branch in configBranches:2636if branch:2637(source, destination) = branch.split(":")2638 self.knownBranches[destination] = source26392640 lostAndFoundBranches.discard(destination)26412642if source not in self.knownBranches:2643 lostAndFoundBranches.add(source)264426452646for branch in lostAndFoundBranches:2647 self.knownBranches[branch] = branch26482649defgetBranchMappingFromGitBranches(self):2650 branches =p4BranchesInGit(self.importIntoRemotes)2651for branch in branches.keys():2652if branch =="master":2653 branch ="main"2654else:2655 branch = branch[len(self.projectName):]2656 self.knownBranches[branch] = branch26572658defupdateOptionDict(self, d):2659 option_keys = {}2660if self.keepRepoPath:2661 option_keys['keepRepoPath'] =126622663 d["options"] =' '.join(sorted(option_keys.keys()))26642665defreadOptions(self, d):2666 self.keepRepoPath = (d.has_key('options')2667and('keepRepoPath'in d['options']))26682669defgitRefForBranch(self, branch):2670if branch =="main":2671return self.refPrefix +"master"26722673iflen(branch) <=0:2674return branch26752676return self.refPrefix + self.projectName + branch26772678defgitCommitByP4Change(self, ref, change):2679if self.verbose:2680print"looking in ref "+ ref +" for change%susing bisect..."% change26812682 earliestCommit =""2683 latestCommit =parseRevision(ref)26842685while True:2686if self.verbose:2687print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2688 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2689iflen(next) ==0:2690if self.verbose:2691print"argh"2692return""2693 log =extractLogMessageFromGitCommit(next)2694 settings =extractSettingsGitLog(log)2695 currentChange =int(settings['change'])2696if self.verbose:2697print"current change%s"% currentChange26982699if currentChange == change:2700if self.verbose:2701print"found%s"% next2702return next27032704if currentChange < change:2705 earliestCommit ="^%s"% next2706else:2707 latestCommit ="%s"% next27082709return""27102711defimportNewBranch(self, branch, maxChange):2712# make fast-import flush all changes to disk and update the refs using the checkpoint2713# command so that we can try to find the branch parent in the git history2714 self.gitStream.write("checkpoint\n\n");2715 self.gitStream.flush();2716 branchPrefix = self.depotPaths[0] + branch +"/"2717range="@1,%s"% maxChange2718#print "prefix" + branchPrefix2719 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2720iflen(changes) <=0:2721return False2722 firstChange = changes[0]2723#print "first change in branch: %s" % firstChange2724 sourceBranch = self.knownBranches[branch]2725 sourceDepotPath = self.depotPaths[0] + sourceBranch2726 sourceRef = self.gitRefForBranch(sourceBranch)2727#print "source " + sourceBranch27282729 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2730#print "branch parent: %s" % branchParentChange2731 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2732iflen(gitParent) >0:2733 self.initialParents[self.gitRefForBranch(branch)] = gitParent2734#print "parent git commit: %s" % gitParent27352736 self.importChanges(changes)2737return True27382739defsearchParent(self, parent, branch, target):2740 parentFound =False2741for blob inread_pipe_lines(["git","rev-list","--reverse",2742"--no-merges", parent]):2743 blob = blob.strip()2744iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2745 parentFound =True2746if self.verbose:2747print"Found parent of%sin commit%s"% (branch, blob)2748break2749if parentFound:2750return blob2751else:2752return None27532754defimportChanges(self, changes):2755 cnt =12756for change in changes:2757 description =p4_describe(change)2758 self.updateOptionDict(description)27592760if not self.silent:2761 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2762 sys.stdout.flush()2763 cnt = cnt +127642765try:2766if self.detectBranches:2767 branches = self.splitFilesIntoBranches(description)2768for branch in branches.keys():2769## HACK --hwn2770 branchPrefix = self.depotPaths[0] + branch +"/"2771 self.branchPrefixes = [ branchPrefix ]27722773 parent =""27742775 filesForCommit = branches[branch]27762777if self.verbose:2778print"branch is%s"% branch27792780 self.updatedBranches.add(branch)27812782if branch not in self.createdBranches:2783 self.createdBranches.add(branch)2784 parent = self.knownBranches[branch]2785if parent == branch:2786 parent =""2787else:2788 fullBranch = self.projectName + branch2789if fullBranch not in self.p4BranchesInGit:2790if not self.silent:2791print("\nImporting new branch%s"% fullBranch);2792if self.importNewBranch(branch, change -1):2793 parent =""2794 self.p4BranchesInGit.append(fullBranch)2795if not self.silent:2796print("\nResuming with change%s"% change);27972798if self.verbose:2799print"parent determined through known branches:%s"% parent28002801 branch = self.gitRefForBranch(branch)2802 parent = self.gitRefForBranch(parent)28032804if self.verbose:2805print"looking for initial parent for%s; current parent is%s"% (branch, parent)28062807iflen(parent) ==0and branch in self.initialParents:2808 parent = self.initialParents[branch]2809del self.initialParents[branch]28102811 blob =None2812iflen(parent) >0:2813 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2814if self.verbose:2815print"Creating temporary branch: "+ tempBranch2816 self.commit(description, filesForCommit, tempBranch)2817 self.tempBranches.append(tempBranch)2818 self.checkpoint()2819 blob = self.searchParent(parent, branch, tempBranch)2820if blob:2821 self.commit(description, filesForCommit, branch, blob)2822else:2823if self.verbose:2824print"Parent of%snot found. Committing into head of%s"% (branch, parent)2825 self.commit(description, filesForCommit, branch, parent)2826else:2827 files = self.extractFilesFromCommit(description)2828 self.commit(description, files, self.branch,2829 self.initialParent)2830# only needed once, to connect to the previous commit2831 self.initialParent =""2832exceptIOError:2833print self.gitError.read()2834 sys.exit(1)28352836defimportHeadRevision(self, revision):2837print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)28382839 details = {}2840 details["user"] ="git perforce import user"2841 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2842% (' '.join(self.depotPaths), revision))2843 details["change"] = revision2844 newestRevision =028452846 fileCnt =02847 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28482849for info inp4CmdList(["files"] + fileArgs):28502851if'code'in info and info['code'] =='error':2852 sys.stderr.write("p4 returned an error:%s\n"2853% info['data'])2854if info['data'].find("must refer to client") >=0:2855 sys.stderr.write("This particular p4 error is misleading.\n")2856 sys.stderr.write("Perhaps the depot path was misspelled.\n");2857 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2858 sys.exit(1)2859if'p4ExitCode'in info:2860 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2861 sys.exit(1)286228632864 change =int(info["change"])2865if change > newestRevision:2866 newestRevision = change28672868if info["action"]in self.delete_actions:2869# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2870#fileCnt = fileCnt + 12871continue28722873for prop in["depotFile","rev","action","type"]:2874 details["%s%s"% (prop, fileCnt)] = info[prop]28752876 fileCnt = fileCnt +128772878 details["change"] = newestRevision28792880# Use time from top-most change so that all git p4 clones of2881# the same p4 repo have the same commit SHA1s.2882 res =p4_describe(newestRevision)2883 details["time"] = res["time"]28842885 self.updateOptionDict(details)2886try:2887 self.commit(details, self.extractFilesFromCommit(details), self.branch)2888exceptIOError:2889print"IO error with git fast-import. Is your git version recent enough?"2890print self.gitError.read()289128922893defrun(self, args):2894 self.depotPaths = []2895 self.changeRange =""2896 self.previousDepotPaths = []2897 self.hasOrigin =False28982899# map from branch depot path to parent branch2900 self.knownBranches = {}2901 self.initialParents = {}29022903if self.importIntoRemotes:2904 self.refPrefix ="refs/remotes/p4/"2905else:2906 self.refPrefix ="refs/heads/p4/"29072908if self.syncWithOrigin:2909 self.hasOrigin =originP4BranchesExist()2910if self.hasOrigin:2911if not self.silent:2912print'Syncing with origin first, using "git fetch origin"'2913system("git fetch origin")29142915 branch_arg_given =bool(self.branch)2916iflen(self.branch) ==0:2917 self.branch = self.refPrefix +"master"2918ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2919system("git update-ref%srefs/heads/p4"% self.branch)2920system("git branch -D p4")29212922# accept either the command-line option, or the configuration variable2923if self.useClientSpec:2924# will use this after clone to set the variable2925 self.useClientSpec_from_options =True2926else:2927ifgitConfigBool("git-p4.useclientspec"):2928 self.useClientSpec =True2929if self.useClientSpec:2930 self.clientSpecDirs =getClientSpec()29312932# TODO: should always look at previous commits,2933# merge with previous imports, if possible.2934if args == []:2935if self.hasOrigin:2936createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)29372938# branches holds mapping from branch name to sha12939 branches =p4BranchesInGit(self.importIntoRemotes)29402941# restrict to just this one, disabling detect-branches2942if branch_arg_given:2943 short = self.branch.split("/")[-1]2944if short in branches:2945 self.p4BranchesInGit = [ short ]2946else:2947 self.p4BranchesInGit = branches.keys()29482949iflen(self.p4BranchesInGit) >1:2950if not self.silent:2951print"Importing from/into multiple branches"2952 self.detectBranches =True2953for branch in branches.keys():2954 self.initialParents[self.refPrefix + branch] = \2955 branches[branch]29562957if self.verbose:2958print"branches:%s"% self.p4BranchesInGit29592960 p4Change =02961for branch in self.p4BranchesInGit:2962 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29632964 settings =extractSettingsGitLog(logMsg)29652966 self.readOptions(settings)2967if(settings.has_key('depot-paths')2968and settings.has_key('change')):2969 change =int(settings['change']) +12970 p4Change =max(p4Change, change)29712972 depotPaths =sorted(settings['depot-paths'])2973if self.previousDepotPaths == []:2974 self.previousDepotPaths = depotPaths2975else:2976 paths = []2977for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2978 prev_list = prev.split("/")2979 cur_list = cur.split("/")2980for i inrange(0,min(len(cur_list),len(prev_list))):2981if cur_list[i] <> prev_list[i]:2982 i = i -12983break29842985 paths.append("/".join(cur_list[:i +1]))29862987 self.previousDepotPaths = paths29882989if p4Change >0:2990 self.depotPaths =sorted(self.previousDepotPaths)2991 self.changeRange ="@%s,#head"% p4Change2992if not self.silent and not self.detectBranches:2993print"Performing incremental import into%sgit branch"% self.branch29942995# accept multiple ref name abbreviations:2996# refs/foo/bar/branch -> use it exactly2997# p4/branch -> prepend refs/remotes/ or refs/heads/2998# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2999if not self.branch.startswith("refs/"):3000if self.importIntoRemotes:3001 prepend ="refs/remotes/"3002else:3003 prepend ="refs/heads/"3004if not self.branch.startswith("p4/"):3005 prepend +="p4/"3006 self.branch = prepend + self.branch30073008iflen(args) ==0and self.depotPaths:3009if not self.silent:3010print"Depot paths:%s"%' '.join(self.depotPaths)3011else:3012if self.depotPaths and self.depotPaths != args:3013print("previous import used depot path%sand now%swas specified. "3014"This doesn't work!"% (' '.join(self.depotPaths),3015' '.join(args)))3016 sys.exit(1)30173018 self.depotPaths =sorted(args)30193020 revision =""3021 self.users = {}30223023# Make sure no revision specifiers are used when --changesfile3024# is specified.3025 bad_changesfile =False3026iflen(self.changesFile) >0:3027for p in self.depotPaths:3028if p.find("@") >=0or p.find("#") >=0:3029 bad_changesfile =True3030break3031if bad_changesfile:3032die("Option --changesfile is incompatible with revision specifiers")30333034 newPaths = []3035for p in self.depotPaths:3036if p.find("@") != -1:3037 atIdx = p.index("@")3038 self.changeRange = p[atIdx:]3039if self.changeRange =="@all":3040 self.changeRange =""3041elif','not in self.changeRange:3042 revision = self.changeRange3043 self.changeRange =""3044 p = p[:atIdx]3045elif p.find("#") != -1:3046 hashIdx = p.index("#")3047 revision = p[hashIdx:]3048 p = p[:hashIdx]3049elif self.previousDepotPaths == []:3050# pay attention to changesfile, if given, else import3051# the entire p4 tree at the head revision3052iflen(self.changesFile) ==0:3053 revision ="#head"30543055 p = re.sub("\.\.\.$","", p)3056if not p.endswith("/"):3057 p +="/"30583059 newPaths.append(p)30603061 self.depotPaths = newPaths30623063# --detect-branches may change this for each branch3064 self.branchPrefixes = self.depotPaths30653066 self.loadUserMapFromCache()3067 self.labels = {}3068if self.detectLabels:3069 self.getLabels();30703071if self.detectBranches:3072## FIXME - what's a P4 projectName ?3073 self.projectName = self.guessProjectName()30743075if self.hasOrigin:3076 self.getBranchMappingFromGitBranches()3077else:3078 self.getBranchMapping()3079if self.verbose:3080print"p4-git branches:%s"% self.p4BranchesInGit3081print"initial parents:%s"% self.initialParents3082for b in self.p4BranchesInGit:3083if b !="master":30843085## FIXME3086 b = b[len(self.projectName):]3087 self.createdBranches.add(b)30883089 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30903091 self.importProcess = subprocess.Popen(["git","fast-import"],3092 stdin=subprocess.PIPE,3093 stdout=subprocess.PIPE,3094 stderr=subprocess.PIPE);3095 self.gitOutput = self.importProcess.stdout3096 self.gitStream = self.importProcess.stdin3097 self.gitError = self.importProcess.stderr30983099if revision:3100 self.importHeadRevision(revision)3101else:3102 changes = []31033104iflen(self.changesFile) >0:3105 output =open(self.changesFile).readlines()3106 changeSet =set()3107for line in output:3108 changeSet.add(int(line))31093110for change in changeSet:3111 changes.append(change)31123113 changes.sort()3114else:3115# catch "git p4 sync" with no new branches, in a repo that3116# does not have any existing p4 branches3117iflen(args) ==0:3118if not self.p4BranchesInGit:3119die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")31203121# The default branch is master, unless --branch is used to3122# specify something else. Make sure it exists, or complain3123# nicely about how to use --branch.3124if not self.detectBranches:3125if notbranch_exists(self.branch):3126if branch_arg_given:3127die("Error: branch%sdoes not exist."% self.branch)3128else:3129die("Error: no branch%s; perhaps specify one with --branch."%3130 self.branch)31313132if self.verbose:3133print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3134 self.changeRange)3135 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)31363137iflen(self.maxChanges) >0:3138 changes = changes[:min(int(self.maxChanges),len(changes))]31393140iflen(changes) ==0:3141if not self.silent:3142print"No changes to import!"3143else:3144if not self.silent and not self.detectBranches:3145print"Import destination:%s"% self.branch31463147 self.updatedBranches =set()31483149if not self.detectBranches:3150if args:3151# start a new branch3152 self.initialParent =""3153else:3154# build on a previous revision3155 self.initialParent =parseRevision(self.branch)31563157 self.importChanges(changes)31583159if not self.silent:3160print""3161iflen(self.updatedBranches) >0:3162 sys.stdout.write("Updated branches: ")3163for b in self.updatedBranches:3164 sys.stdout.write("%s"% b)3165 sys.stdout.write("\n")31663167ifgitConfigBool("git-p4.importLabels"):3168 self.importLabels =True31693170if self.importLabels:3171 p4Labels =getP4Labels(self.depotPaths)3172 gitTags =getGitTags()31733174 missingP4Labels = p4Labels - gitTags3175 self.importP4Labels(self.gitStream, missingP4Labels)31763177 self.gitStream.close()3178if self.importProcess.wait() !=0:3179die("fast-import failed:%s"% self.gitError.read())3180 self.gitOutput.close()3181 self.gitError.close()31823183# Cleanup temporary branches created during import3184if self.tempBranches != []:3185for branch in self.tempBranches:3186read_pipe("git update-ref -d%s"% branch)3187 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31883189# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3190# a convenient shortcut refname "p4".3191if self.importIntoRemotes:3192 head_ref = self.refPrefix +"HEAD"3193if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3194system(["git","symbolic-ref", head_ref, self.branch])31953196return True31973198classP4Rebase(Command):3199def__init__(self):3200 Command.__init__(self)3201 self.options = [3202 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3203]3204 self.importLabels =False3205 self.description = ("Fetches the latest revision from perforce and "3206+"rebases the current work (branch) against it")32073208defrun(self, args):3209 sync =P4Sync()3210 sync.importLabels = self.importLabels3211 sync.run([])32123213return self.rebase()32143215defrebase(self):3216if os.system("git update-index --refresh") !=0:3217die("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.");3218iflen(read_pipe("git diff-index HEAD --")) >0:3219die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");32203221[upstream, settings] =findUpstreamBranchPoint()3222iflen(upstream) ==0:3223die("Cannot find upstream branchpoint for rebase")32243225# the branchpoint may be p4/foo~3, so strip off the parent3226 upstream = re.sub("~[0-9]+$","", upstream)32273228print"Rebasing the current branch onto%s"% upstream3229 oldHead =read_pipe("git rev-parse HEAD").strip()3230system("git rebase%s"% upstream)3231system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3232return True32333234classP4Clone(P4Sync):3235def__init__(self):3236 P4Sync.__init__(self)3237 self.description ="Creates a new git repository and imports from Perforce into it"3238 self.usage ="usage: %prog [options] //depot/path[@revRange]"3239 self.options += [3240 optparse.make_option("--destination", dest="cloneDestination",3241 action='store', default=None,3242help="where to leave result of the clone"),3243 optparse.make_option("--bare", dest="cloneBare",3244 action="store_true", default=False),3245]3246 self.cloneDestination =None3247 self.needsGit =False3248 self.cloneBare =False32493250defdefaultDestination(self, args):3251## TODO: use common prefix of args?3252 depotPath = args[0]3253 depotDir = re.sub("(@[^@]*)$","", depotPath)3254 depotDir = re.sub("(#[^#]*)$","", depotDir)3255 depotDir = re.sub(r"\.\.\.$","", depotDir)3256 depotDir = re.sub(r"/$","", depotDir)3257return os.path.split(depotDir)[1]32583259defrun(self, args):3260iflen(args) <1:3261return False32623263if self.keepRepoPath and not self.cloneDestination:3264 sys.stderr.write("Must specify destination for --keep-path\n")3265 sys.exit(1)32663267 depotPaths = args32683269if not self.cloneDestination andlen(depotPaths) >1:3270 self.cloneDestination = depotPaths[-1]3271 depotPaths = depotPaths[:-1]32723273 self.cloneExclude = ["/"+p for p in self.cloneExclude]3274for p in depotPaths:3275if not p.startswith("//"):3276 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3277return False32783279if not self.cloneDestination:3280 self.cloneDestination = self.defaultDestination(args)32813282print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32833284if not os.path.exists(self.cloneDestination):3285 os.makedirs(self.cloneDestination)3286chdir(self.cloneDestination)32873288 init_cmd = ["git","init"]3289if self.cloneBare:3290 init_cmd.append("--bare")3291 retcode = subprocess.call(init_cmd)3292if retcode:3293raiseCalledProcessError(retcode, init_cmd)32943295if not P4Sync.run(self, depotPaths):3296return False32973298# create a master branch and check out a work tree3299ifgitBranchExists(self.branch):3300system(["git","branch","master", self.branch ])3301if not self.cloneBare:3302system(["git","checkout","-f"])3303else:3304print'Not checking out any branch, use ' \3305'"git checkout -q -b master <branch>"'33063307# auto-set this variable if invoked with --use-client-spec3308if self.useClientSpec_from_options:3309system("git config --bool git-p4.useclientspec true")33103311return True33123313classP4Branches(Command):3314def__init__(self):3315 Command.__init__(self)3316 self.options = [ ]3317 self.description = ("Shows the git branches that hold imports and their "3318+"corresponding perforce depot paths")3319 self.verbose =False33203321defrun(self, args):3322iforiginP4BranchesExist():3323createOrUpdateBranchesFromOrigin()33243325 cmdline ="git rev-parse --symbolic "3326 cmdline +=" --remotes"33273328for line inread_pipe_lines(cmdline):3329 line = line.strip()33303331if not line.startswith('p4/')or line =="p4/HEAD":3332continue3333 branch = line33343335 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3336 settings =extractSettingsGitLog(log)33373338print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3339return True33403341classHelpFormatter(optparse.IndentedHelpFormatter):3342def__init__(self):3343 optparse.IndentedHelpFormatter.__init__(self)33443345defformat_description(self, description):3346if description:3347return description +"\n"3348else:3349return""33503351defprintUsage(commands):3352print"usage:%s<command> [options]"% sys.argv[0]3353print""3354print"valid commands:%s"%", ".join(commands)3355print""3356print"Try%s<command> --help for command specific help."% sys.argv[0]3357print""33583359commands = {3360"debug": P4Debug,3361"submit": P4Submit,3362"commit": P4Submit,3363"sync": P4Sync,3364"rebase": P4Rebase,3365"clone": P4Clone,3366"rollback": P4RollBack,3367"branches": P4Branches3368}336933703371defmain():3372iflen(sys.argv[1:]) ==0:3373printUsage(commands.keys())3374 sys.exit(2)33753376 cmdName = sys.argv[1]3377try:3378 klass = commands[cmdName]3379 cmd =klass()3380exceptKeyError:3381print"unknown command%s"% cmdName3382print""3383printUsage(commands.keys())3384 sys.exit(2)33853386 options = cmd.options3387 cmd.gitdir = os.environ.get("GIT_DIR",None)33883389 args = sys.argv[2:]33903391 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3392if cmd.needsGit:3393 options.append(optparse.make_option("--git-dir", dest="gitdir"))33943395 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3396 options,3397 description = cmd.description,3398 formatter =HelpFormatter())33993400(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3401global verbose3402 verbose = cmd.verbose3403if cmd.needsGit:3404if cmd.gitdir ==None:3405 cmd.gitdir = os.path.abspath(".git")3406if notisValidGitDir(cmd.gitdir):3407 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3408if os.path.exists(cmd.gitdir):3409 cdup =read_pipe("git rev-parse --show-cdup").strip()3410iflen(cdup) >0:3411chdir(cdup);34123413if notisValidGitDir(cmd.gitdir):3414ifisValidGitDir(cmd.gitdir +"/.git"):3415 cmd.gitdir +="/.git"3416else:3417die("fatal: cannot locate git repository at%s"% cmd.gitdir)34183419 os.environ["GIT_DIR"] = cmd.gitdir34203421if not cmd.run(args):3422 parser.print_help()3423 sys.exit(2)342434253426if __name__ =='__main__':3427main()