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 107defdie(msg): 108if verbose: 109raiseException(msg) 110else: 111 sys.stderr.write(msg +"\n") 112 sys.exit(1) 113 114defwrite_pipe(c, stdin): 115if verbose: 116 sys.stderr.write('Writing pipe:%s\n'%str(c)) 117 118 expand =isinstance(c,basestring) 119 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 120 pipe = p.stdin 121 val = pipe.write(stdin) 122 pipe.close() 123if p.wait(): 124die('Command failed:%s'%str(c)) 125 126return val 127 128defp4_write_pipe(c, stdin): 129 real_cmd =p4_build_cmd(c) 130returnwrite_pipe(real_cmd, stdin) 131 132defread_pipe(c, ignore_error=False): 133if verbose: 134 sys.stderr.write('Reading pipe:%s\n'%str(c)) 135 136 expand =isinstance(c,basestring) 137 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 138 pipe = p.stdout 139 val = pipe.read() 140if p.wait()and not ignore_error: 141die('Command failed:%s'%str(c)) 142 143return val 144 145defp4_read_pipe(c, ignore_error=False): 146 real_cmd =p4_build_cmd(c) 147returnread_pipe(real_cmd, ignore_error) 148 149defread_pipe_lines(c): 150if verbose: 151 sys.stderr.write('Reading pipe:%s\n'%str(c)) 152 153 expand =isinstance(c, basestring) 154 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 155 pipe = p.stdout 156 val = pipe.readlines() 157if pipe.close()or p.wait(): 158die('Command failed:%s'%str(c)) 159 160return val 161 162defp4_read_pipe_lines(c): 163"""Specifically invoke p4 on the command supplied. """ 164 real_cmd =p4_build_cmd(c) 165returnread_pipe_lines(real_cmd) 166 167defp4_has_command(cmd): 168"""Ask p4 for help on this command. If it returns an error, the 169 command does not exist in this version of p4.""" 170 real_cmd =p4_build_cmd(["help", cmd]) 171 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 172 stderr=subprocess.PIPE) 173 p.communicate() 174return p.returncode ==0 175 176defp4_has_move_command(): 177"""See if the move command exists, that it supports -k, and that 178 it has not been administratively disabled. The arguments 179 must be correct, but the filenames do not have to exist. Use 180 ones with wildcards so even if they exist, it will fail.""" 181 182if notp4_has_command("move"): 183return False 184 cmd =p4_build_cmd(["move","-k","@from","@to"]) 185 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 186(out, err) = p.communicate() 187# return code will be 1 in either case 188if err.find("Invalid option") >=0: 189return False 190if err.find("disabled") >=0: 191return False 192# assume it failed because @... was invalid changelist 193return True 194 195defsystem(cmd): 196 expand =isinstance(cmd,basestring) 197if verbose: 198 sys.stderr.write("executing%s\n"%str(cmd)) 199 retcode = subprocess.call(cmd, shell=expand) 200if retcode: 201raiseCalledProcessError(retcode, cmd) 202 203defp4_system(cmd): 204"""Specifically invoke p4 as the system command. """ 205 real_cmd =p4_build_cmd(cmd) 206 expand =isinstance(real_cmd, basestring) 207 retcode = subprocess.call(real_cmd, shell=expand) 208if retcode: 209raiseCalledProcessError(retcode, real_cmd) 210 211_p4_version_string =None 212defp4_version_string(): 213"""Read the version string, showing just the last line, which 214 hopefully is the interesting version bit. 215 216 $ p4 -V 217 Perforce - The Fast Software Configuration Management System. 218 Copyright 1995-2011 Perforce Software. All rights reserved. 219 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 220 """ 221global _p4_version_string 222if not _p4_version_string: 223 a =p4_read_pipe_lines(["-V"]) 224 _p4_version_string = a[-1].rstrip() 225return _p4_version_string 226 227defp4_integrate(src, dest): 228p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 229 230defp4_sync(f, *options): 231p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 232 233defp4_add(f): 234# forcibly add file names with wildcards 235ifwildcard_present(f): 236p4_system(["add","-f", f]) 237else: 238p4_system(["add", f]) 239 240defp4_delete(f): 241p4_system(["delete",wildcard_encode(f)]) 242 243defp4_edit(f): 244p4_system(["edit",wildcard_encode(f)]) 245 246defp4_revert(f): 247p4_system(["revert",wildcard_encode(f)]) 248 249defp4_reopen(type, f): 250p4_system(["reopen","-t",type,wildcard_encode(f)]) 251 252defp4_move(src, dest): 253p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 254 255defp4_last_change(): 256 results =p4CmdList(["changes","-m","1"]) 257returnint(results[0]['change']) 258 259defp4_describe(change): 260"""Make sure it returns a valid result by checking for 261 the presence of field "time". Return a dict of the 262 results.""" 263 264 ds =p4CmdList(["describe","-s",str(change)]) 265iflen(ds) !=1: 266die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 267 268 d = ds[0] 269 270if"p4ExitCode"in d: 271die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 272str(d))) 273if"code"in d: 274if d["code"] =="error": 275die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 276 277if"time"not in d: 278die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 279 280return d 281 282# 283# Canonicalize the p4 type and return a tuple of the 284# base type, plus any modifiers. See "p4 help filetypes" 285# for a list and explanation. 286# 287defsplit_p4_type(p4type): 288 289 p4_filetypes_historical = { 290"ctempobj":"binary+Sw", 291"ctext":"text+C", 292"cxtext":"text+Cx", 293"ktext":"text+k", 294"kxtext":"text+kx", 295"ltext":"text+F", 296"tempobj":"binary+FSw", 297"ubinary":"binary+F", 298"uresource":"resource+F", 299"uxbinary":"binary+Fx", 300"xbinary":"binary+x", 301"xltext":"text+Fx", 302"xtempobj":"binary+Swx", 303"xtext":"text+x", 304"xunicode":"unicode+x", 305"xutf16":"utf16+x", 306} 307if p4type in p4_filetypes_historical: 308 p4type = p4_filetypes_historical[p4type] 309 mods ="" 310 s = p4type.split("+") 311 base = s[0] 312 mods ="" 313iflen(s) >1: 314 mods = s[1] 315return(base, mods) 316 317# 318# return the raw p4 type of a file (text, text+ko, etc) 319# 320defp4_type(f): 321 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 322return results[0]['headType'] 323 324# 325# Given a type base and modifier, return a regexp matching 326# the keywords that can be expanded in the file 327# 328defp4_keywords_regexp_for_type(base, type_mods): 329if base in("text","unicode","binary"): 330 kwords =None 331if"ko"in type_mods: 332 kwords ='Id|Header' 333elif"k"in type_mods: 334 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 335else: 336return None 337 pattern = r""" 338 \$ # Starts with a dollar, followed by... 339 (%s) # one of the keywords, followed by... 340 (:[^$\n]+)? # possibly an old expansion, followed by... 341 \$ # another dollar 342 """% kwords 343return pattern 344else: 345return None 346 347# 348# Given a file, return a regexp matching the possible 349# RCS keywords that will be expanded, or None for files 350# with kw expansion turned off. 351# 352defp4_keywords_regexp_for_file(file): 353if not os.path.exists(file): 354return None 355else: 356(type_base, type_mods) =split_p4_type(p4_type(file)) 357returnp4_keywords_regexp_for_type(type_base, type_mods) 358 359defsetP4ExecBit(file, mode): 360# Reopens an already open file and changes the execute bit to match 361# the execute bit setting in the passed in mode. 362 363 p4Type ="+x" 364 365if notisModeExec(mode): 366 p4Type =getP4OpenedType(file) 367 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 368 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 369if p4Type[-1] =="+": 370 p4Type = p4Type[0:-1] 371 372p4_reopen(p4Type,file) 373 374defgetP4OpenedType(file): 375# Returns the perforce file type for the given file. 376 377 result =p4_read_pipe(["opened",wildcard_encode(file)]) 378 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 379if match: 380return match.group(1) 381else: 382die("Could not determine file type for%s(result: '%s')"% (file, result)) 383 384# Return the set of all p4 labels 385defgetP4Labels(depotPaths): 386 labels =set() 387ifisinstance(depotPaths,basestring): 388 depotPaths = [depotPaths] 389 390for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 391 label = l['label'] 392 labels.add(label) 393 394return labels 395 396# Return the set of all git tags 397defgetGitTags(): 398 gitTags =set() 399for line inread_pipe_lines(["git","tag"]): 400 tag = line.strip() 401 gitTags.add(tag) 402return gitTags 403 404defdiffTreePattern(): 405# This is a simple generator for the diff tree regex pattern. This could be 406# a class variable if this and parseDiffTreeEntry were a part of a class. 407 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 408while True: 409yield pattern 410 411defparseDiffTreeEntry(entry): 412"""Parses a single diff tree entry into its component elements. 413 414 See git-diff-tree(1) manpage for details about the format of the diff 415 output. This method returns a dictionary with the following elements: 416 417 src_mode - The mode of the source file 418 dst_mode - The mode of the destination file 419 src_sha1 - The sha1 for the source file 420 dst_sha1 - The sha1 fr the destination file 421 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 422 status_score - The score for the status (applicable for 'C' and 'R' 423 statuses). This is None if there is no score. 424 src - The path for the source file. 425 dst - The path for the destination file. This is only present for 426 copy or renames. If it is not present, this is None. 427 428 If the pattern is not matched, None is returned.""" 429 430 match =diffTreePattern().next().match(entry) 431if match: 432return{ 433'src_mode': match.group(1), 434'dst_mode': match.group(2), 435'src_sha1': match.group(3), 436'dst_sha1': match.group(4), 437'status': match.group(5), 438'status_score': match.group(6), 439'src': match.group(7), 440'dst': match.group(10) 441} 442return None 443 444defisModeExec(mode): 445# Returns True if the given git mode represents an executable file, 446# otherwise False. 447return mode[-3:] =="755" 448 449defisModeExecChanged(src_mode, dst_mode): 450returnisModeExec(src_mode) !=isModeExec(dst_mode) 451 452defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 453 454ifisinstance(cmd,basestring): 455 cmd ="-G "+ cmd 456 expand =True 457else: 458 cmd = ["-G"] + cmd 459 expand =False 460 461 cmd =p4_build_cmd(cmd) 462if verbose: 463 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 464 465# Use a temporary file to avoid deadlocks without 466# subprocess.communicate(), which would put another copy 467# of stdout into memory. 468 stdin_file =None 469if stdin is not None: 470 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 471ifisinstance(stdin,basestring): 472 stdin_file.write(stdin) 473else: 474for i in stdin: 475 stdin_file.write(i +'\n') 476 stdin_file.flush() 477 stdin_file.seek(0) 478 479 p4 = subprocess.Popen(cmd, 480 shell=expand, 481 stdin=stdin_file, 482 stdout=subprocess.PIPE) 483 484 result = [] 485try: 486while True: 487 entry = marshal.load(p4.stdout) 488if cb is not None: 489cb(entry) 490else: 491 result.append(entry) 492exceptEOFError: 493pass 494 exitCode = p4.wait() 495if exitCode !=0: 496 entry = {} 497 entry["p4ExitCode"] = exitCode 498 result.append(entry) 499 500return result 501 502defp4Cmd(cmd): 503list=p4CmdList(cmd) 504 result = {} 505for entry inlist: 506 result.update(entry) 507return result; 508 509defp4Where(depotPath): 510if not depotPath.endswith("/"): 511 depotPath +="/" 512 depotPathLong = depotPath +"..." 513 outputList =p4CmdList(["where", depotPathLong]) 514 output =None 515for entry in outputList: 516if"depotFile"in entry: 517# Search for the base client side depot path, as long as it starts with the branch's P4 path. 518# The base path always ends with "/...". 519if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 520 output = entry 521break 522elif"data"in entry: 523 data = entry.get("data") 524 space = data.find(" ") 525if data[:space] == depotPath: 526 output = entry 527break 528if output ==None: 529return"" 530if output["code"] =="error": 531return"" 532 clientPath ="" 533if"path"in output: 534 clientPath = output.get("path") 535elif"data"in output: 536 data = output.get("data") 537 lastSpace = data.rfind(" ") 538 clientPath = data[lastSpace +1:] 539 540if clientPath.endswith("..."): 541 clientPath = clientPath[:-3] 542return clientPath 543 544defcurrentGitBranch(): 545returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 546 547defisValidGitDir(path): 548if(os.path.exists(path +"/HEAD") 549and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 550return True; 551return False 552 553defparseRevision(ref): 554returnread_pipe("git rev-parse%s"% ref).strip() 555 556defbranchExists(ref): 557 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 558 ignore_error=True) 559returnlen(rev) >0 560 561defextractLogMessageFromGitCommit(commit): 562 logMessage ="" 563 564## fixme: title is first line of commit, not 1st paragraph. 565 foundTitle =False 566for log inread_pipe_lines("git cat-file commit%s"% commit): 567if not foundTitle: 568iflen(log) ==1: 569 foundTitle =True 570continue 571 572 logMessage += log 573return logMessage 574 575defextractSettingsGitLog(log): 576 values = {} 577for line in log.split("\n"): 578 line = line.strip() 579 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 580if not m: 581continue 582 583 assignments = m.group(1).split(':') 584for a in assignments: 585 vals = a.split('=') 586 key = vals[0].strip() 587 val = ('='.join(vals[1:])).strip() 588if val.endswith('\"')and val.startswith('"'): 589 val = val[1:-1] 590 591 values[key] = val 592 593 paths = values.get("depot-paths") 594if not paths: 595 paths = values.get("depot-path") 596if paths: 597 values['depot-paths'] = paths.split(',') 598return values 599 600defgitBranchExists(branch): 601 proc = subprocess.Popen(["git","rev-parse", branch], 602 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 603return proc.wait() ==0; 604 605_gitConfig = {} 606 607defgitConfig(key, typeSpecifier=None): 608if not _gitConfig.has_key(key): 609 cmd = ["git","config"] 610if typeSpecifier: 611 cmd += [ typeSpecifier ] 612 cmd += [ key ] 613 s =read_pipe(cmd, ignore_error=True) 614 _gitConfig[key] = s.strip() 615return _gitConfig[key] 616 617defgitConfigBool(key): 618"""Return a bool, using git config --bool. It is True only if the 619 variable is set to true, and False if set to false or not present 620 in the config.""" 621 622if not _gitConfig.has_key(key): 623 _gitConfig[key] =gitConfig(key,'--bool') =="true" 624return _gitConfig[key] 625 626defgitConfigInt(key): 627if not _gitConfig.has_key(key): 628 cmd = ["git","config","--int", key ] 629 s =read_pipe(cmd, ignore_error=True) 630 v = s.strip() 631try: 632 _gitConfig[key] =int(gitConfig(key,'--int')) 633exceptValueError: 634 _gitConfig[key] =None 635return _gitConfig[key] 636 637defgitConfigList(key): 638if not _gitConfig.has_key(key): 639 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 640 _gitConfig[key] = s.strip().split(os.linesep) 641if _gitConfig[key] == ['']: 642 _gitConfig[key] = [] 643return _gitConfig[key] 644 645defp4BranchesInGit(branchesAreInRemotes=True): 646"""Find all the branches whose names start with "p4/", looking 647 in remotes or heads as specified by the argument. Return 648 a dictionary of{ branch: revision }for each one found. 649 The branch names are the short names, without any 650 "p4/" prefix.""" 651 652 branches = {} 653 654 cmdline ="git rev-parse --symbolic " 655if branchesAreInRemotes: 656 cmdline +="--remotes" 657else: 658 cmdline +="--branches" 659 660for line inread_pipe_lines(cmdline): 661 line = line.strip() 662 663# only import to p4/ 664if not line.startswith('p4/'): 665continue 666# special symbolic ref to p4/master 667if line =="p4/HEAD": 668continue 669 670# strip off p4/ prefix 671 branch = line[len("p4/"):] 672 673 branches[branch] =parseRevision(line) 674 675return branches 676 677defbranch_exists(branch): 678"""Make sure that the given ref name really exists.""" 679 680 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 681 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 682 out, _ = p.communicate() 683if p.returncode: 684return False 685# expect exactly one line of output: the branch name 686return out.rstrip() == branch 687 688deffindUpstreamBranchPoint(head ="HEAD"): 689 branches =p4BranchesInGit() 690# map from depot-path to branch name 691 branchByDepotPath = {} 692for branch in branches.keys(): 693 tip = branches[branch] 694 log =extractLogMessageFromGitCommit(tip) 695 settings =extractSettingsGitLog(log) 696if settings.has_key("depot-paths"): 697 paths =",".join(settings["depot-paths"]) 698 branchByDepotPath[paths] ="remotes/p4/"+ branch 699 700 settings =None 701 parent =0 702while parent <65535: 703 commit = head +"~%s"% parent 704 log =extractLogMessageFromGitCommit(commit) 705 settings =extractSettingsGitLog(log) 706if settings.has_key("depot-paths"): 707 paths =",".join(settings["depot-paths"]) 708if branchByDepotPath.has_key(paths): 709return[branchByDepotPath[paths], settings] 710 711 parent = parent +1 712 713return["", settings] 714 715defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 716if not silent: 717print("Creating/updating branch(es) in%sbased on origin branch(es)" 718% localRefPrefix) 719 720 originPrefix ="origin/p4/" 721 722for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 723 line = line.strip() 724if(not line.startswith(originPrefix))or line.endswith("HEAD"): 725continue 726 727 headName = line[len(originPrefix):] 728 remoteHead = localRefPrefix + headName 729 originHead = line 730 731 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 732if(not original.has_key('depot-paths') 733or not original.has_key('change')): 734continue 735 736 update =False 737if notgitBranchExists(remoteHead): 738if verbose: 739print"creating%s"% remoteHead 740 update =True 741else: 742 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 743if settings.has_key('change') >0: 744if settings['depot-paths'] == original['depot-paths']: 745 originP4Change =int(original['change']) 746 p4Change =int(settings['change']) 747if originP4Change > p4Change: 748print("%s(%s) is newer than%s(%s). " 749"Updating p4 branch from origin." 750% (originHead, originP4Change, 751 remoteHead, p4Change)) 752 update =True 753else: 754print("Ignoring:%swas imported from%swhile " 755"%swas imported from%s" 756% (originHead,','.join(original['depot-paths']), 757 remoteHead,','.join(settings['depot-paths']))) 758 759if update: 760system("git update-ref%s %s"% (remoteHead, originHead)) 761 762deforiginP4BranchesExist(): 763returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 764 765 766defp4ParseNumericChangeRange(parts): 767 changeStart =int(parts[0][1:]) 768if parts[1] =='#head': 769 changeEnd =p4_last_change() 770else: 771 changeEnd =int(parts[1]) 772 773return(changeStart, changeEnd) 774 775defchooseBlockSize(blockSize): 776if blockSize: 777return blockSize 778else: 779return defaultBlockSize 780 781defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 782assert depotPaths 783 784# Parse the change range into start and end. Try to find integer 785# revision ranges as these can be broken up into blocks to avoid 786# hitting server-side limits (maxrows, maxscanresults). But if 787# that doesn't work, fall back to using the raw revision specifier 788# strings, without using block mode. 789 790if changeRange is None or changeRange =='': 791 changeStart =1 792 changeEnd =p4_last_change() 793 block_size =chooseBlockSize(requestedBlockSize) 794else: 795 parts = changeRange.split(',') 796assertlen(parts) ==2 797try: 798(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 799 block_size =chooseBlockSize(requestedBlockSize) 800except: 801 changeStart = parts[0][1:] 802 changeEnd = parts[1] 803if requestedBlockSize: 804die("cannot use --changes-block-size with non-numeric revisions") 805 block_size =None 806 807# Accumulate change numbers in a dictionary to avoid duplicates 808 changes = {} 809 810for p in depotPaths: 811# Retrieve changes a block at a time, to prevent running 812# into a MaxResults/MaxScanRows error from the server. 813 814while True: 815 cmd = ['changes'] 816 817if block_size: 818 end =min(changeEnd, changeStart + block_size) 819 revisionRange ="%d,%d"% (changeStart, end) 820else: 821 revisionRange ="%s,%s"% (changeStart, changeEnd) 822 823 cmd += ["%s...@%s"% (p, revisionRange)] 824 825for line inp4_read_pipe_lines(cmd): 826 changeNum =int(line.split(" ")[1]) 827 changes[changeNum] =True 828 829if not block_size: 830break 831 832if end >= changeEnd: 833break 834 835 changeStart = end +1 836 837 changelist = changes.keys() 838 changelist.sort() 839return changelist 840 841defp4PathStartsWith(path, prefix): 842# This method tries to remedy a potential mixed-case issue: 843# 844# If UserA adds //depot/DirA/file1 845# and UserB adds //depot/dira/file2 846# 847# we may or may not have a problem. If you have core.ignorecase=true, 848# we treat DirA and dira as the same directory 849ifgitConfigBool("core.ignorecase"): 850return path.lower().startswith(prefix.lower()) 851return path.startswith(prefix) 852 853defgetClientSpec(): 854"""Look at the p4 client spec, create a View() object that contains 855 all the mappings, and return it.""" 856 857 specList =p4CmdList("client -o") 858iflen(specList) !=1: 859die('Output from "client -o" is%dlines, expecting 1'% 860len(specList)) 861 862# dictionary of all client parameters 863 entry = specList[0] 864 865# the //client/ name 866 client_name = entry["Client"] 867 868# just the keys that start with "View" 869 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 870 871# hold this new View 872 view =View(client_name) 873 874# append the lines, in order, to the view 875for view_num inrange(len(view_keys)): 876 k ="View%d"% view_num 877if k not in view_keys: 878die("Expected view key%smissing"% k) 879 view.append(entry[k]) 880 881return view 882 883defgetClientRoot(): 884"""Grab the client directory.""" 885 886 output =p4CmdList("client -o") 887iflen(output) !=1: 888die('Output from "client -o" is%dlines, expecting 1'%len(output)) 889 890 entry = output[0] 891if"Root"not in entry: 892die('Client has no "Root"') 893 894return entry["Root"] 895 896# 897# P4 wildcards are not allowed in filenames. P4 complains 898# if you simply add them, but you can force it with "-f", in 899# which case it translates them into %xx encoding internally. 900# 901defwildcard_decode(path): 902# Search for and fix just these four characters. Do % last so 903# that fixing it does not inadvertently create new %-escapes. 904# Cannot have * in a filename in windows; untested as to 905# what p4 would do in such a case. 906if not platform.system() =="Windows": 907 path = path.replace("%2A","*") 908 path = path.replace("%23","#") \ 909.replace("%40","@") \ 910.replace("%25","%") 911return path 912 913defwildcard_encode(path): 914# do % first to avoid double-encoding the %s introduced here 915 path = path.replace("%","%25") \ 916.replace("*","%2A") \ 917.replace("#","%23") \ 918.replace("@","%40") 919return path 920 921defwildcard_present(path): 922 m = re.search("[*#@%]", path) 923return m is not None 924 925class Command: 926def__init__(self): 927 self.usage ="usage: %prog [options]" 928 self.needsGit =True 929 self.verbose =False 930 931class P4UserMap: 932def__init__(self): 933 self.userMapFromPerforceServer =False 934 self.myP4UserId =None 935 936defp4UserId(self): 937if self.myP4UserId: 938return self.myP4UserId 939 940 results =p4CmdList("user -o") 941for r in results: 942if r.has_key('User'): 943 self.myP4UserId = r['User'] 944return r['User'] 945die("Could not find your p4 user id") 946 947defp4UserIsMe(self, p4User): 948# return True if the given p4 user is actually me 949 me = self.p4UserId() 950if not p4User or p4User != me: 951return False 952else: 953return True 954 955defgetUserCacheFilename(self): 956 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 957return home +"/.gitp4-usercache.txt" 958 959defgetUserMapFromPerforceServer(self): 960if self.userMapFromPerforceServer: 961return 962 self.users = {} 963 self.emails = {} 964 965for output inp4CmdList("users"): 966if not output.has_key("User"): 967continue 968 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 969 self.emails[output["Email"]] = output["User"] 970 971 972 s ='' 973for(key, val)in self.users.items(): 974 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 975 976open(self.getUserCacheFilename(),"wb").write(s) 977 self.userMapFromPerforceServer =True 978 979defloadUserMapFromCache(self): 980 self.users = {} 981 self.userMapFromPerforceServer =False 982try: 983 cache =open(self.getUserCacheFilename(),"rb") 984 lines = cache.readlines() 985 cache.close() 986for line in lines: 987 entry = line.strip().split("\t") 988 self.users[entry[0]] = entry[1] 989exceptIOError: 990 self.getUserMapFromPerforceServer() 991 992classP4Debug(Command): 993def__init__(self): 994 Command.__init__(self) 995 self.options = [] 996 self.description ="A tool to debug the output of p4 -G." 997 self.needsGit =False 998 999defrun(self, args):1000 j =01001for output inp4CmdList(args):1002print'Element:%d'% j1003 j +=11004print output1005return True10061007classP4RollBack(Command):1008def__init__(self):1009 Command.__init__(self)1010 self.options = [1011 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1012]1013 self.description ="A tool to debug the multi-branch import. Don't use :)"1014 self.rollbackLocalBranches =False10151016defrun(self, args):1017iflen(args) !=1:1018return False1019 maxChange =int(args[0])10201021if"p4ExitCode"inp4Cmd("changes -m 1"):1022die("Problems executing p4");10231024if self.rollbackLocalBranches:1025 refPrefix ="refs/heads/"1026 lines =read_pipe_lines("git rev-parse --symbolic --branches")1027else:1028 refPrefix ="refs/remotes/"1029 lines =read_pipe_lines("git rev-parse --symbolic --remotes")10301031for line in lines:1032if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1033 line = line.strip()1034 ref = refPrefix + line1035 log =extractLogMessageFromGitCommit(ref)1036 settings =extractSettingsGitLog(log)10371038 depotPaths = settings['depot-paths']1039 change = settings['change']10401041 changed =False10421043iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1044for p in depotPaths]))) ==0:1045print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1046system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1047continue10481049while change andint(change) > maxChange:1050 changed =True1051if self.verbose:1052print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1053system("git update-ref%s\"%s^\""% (ref, ref))1054 log =extractLogMessageFromGitCommit(ref)1055 settings =extractSettingsGitLog(log)105610571058 depotPaths = settings['depot-paths']1059 change = settings['change']10601061if changed:1062print"%srewound to%s"% (ref, change)10631064return True10651066classP4Submit(Command, P4UserMap):10671068 conflict_behavior_choices = ("ask","skip","quit")10691070def__init__(self):1071 Command.__init__(self)1072 P4UserMap.__init__(self)1073 self.options = [1074 optparse.make_option("--origin", dest="origin"),1075 optparse.make_option("-M", dest="detectRenames", action="store_true"),1076# preserve the user, requires relevant p4 permissions1077 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1078 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1079 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1080 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1081 optparse.make_option("--conflict", dest="conflict_behavior",1082 choices=self.conflict_behavior_choices),1083 optparse.make_option("--branch", dest="branch"),1084]1085 self.description ="Submit changes from git to the perforce depot."1086 self.usage +=" [name of git branch to submit into perforce depot]"1087 self.origin =""1088 self.detectRenames =False1089 self.preserveUser =gitConfigBool("git-p4.preserveUser")1090 self.dry_run =False1091 self.prepare_p4_only =False1092 self.conflict_behavior =None1093 self.isWindows = (platform.system() =="Windows")1094 self.exportLabels =False1095 self.p4HasMoveCommand =p4_has_move_command()1096 self.branch =None10971098defcheck(self):1099iflen(p4CmdList("opened ...")) >0:1100die("You have files opened with perforce! Close them before starting the sync.")11011102defseparate_jobs_from_description(self, message):1103"""Extract and return a possible Jobs field in the commit1104 message. It goes into a separate section in the p4 change1105 specification.11061107 A jobs line starts with "Jobs:" and looks like a new field1108 in a form. Values are white-space separated on the same1109 line or on following lines that start with a tab.11101111 This does not parse and extract the full git commit message1112 like a p4 form. It just sees the Jobs: line as a marker1113 to pass everything from then on directly into the p4 form,1114 but outside the description section.11151116 Return a tuple (stripped log message, jobs string)."""11171118 m = re.search(r'^Jobs:', message, re.MULTILINE)1119if m is None:1120return(message,None)11211122 jobtext = message[m.start():]1123 stripped_message = message[:m.start()].rstrip()1124return(stripped_message, jobtext)11251126defprepareLogMessage(self, template, message, jobs):1127"""Edits the template returned from "p4 change -o" to insert1128 the message in the Description field, and the jobs text in1129 the Jobs field."""1130 result =""11311132 inDescriptionSection =False11331134for line in template.split("\n"):1135if line.startswith("#"):1136 result += line +"\n"1137continue11381139if inDescriptionSection:1140if line.startswith("Files:")or line.startswith("Jobs:"):1141 inDescriptionSection =False1142# insert Jobs section1143if jobs:1144 result += jobs +"\n"1145else:1146continue1147else:1148if line.startswith("Description:"):1149 inDescriptionSection =True1150 line +="\n"1151for messageLine in message.split("\n"):1152 line +="\t"+ messageLine +"\n"11531154 result += line +"\n"11551156return result11571158defpatchRCSKeywords(self,file, pattern):1159# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1160(handle, outFileName) = tempfile.mkstemp(dir='.')1161try:1162 outFile = os.fdopen(handle,"w+")1163 inFile =open(file,"r")1164 regexp = re.compile(pattern, re.VERBOSE)1165for line in inFile.readlines():1166 line = regexp.sub(r'$\1$', line)1167 outFile.write(line)1168 inFile.close()1169 outFile.close()1170# Forcibly overwrite the original file1171 os.unlink(file)1172 shutil.move(outFileName,file)1173except:1174# cleanup our temporary file1175 os.unlink(outFileName)1176print"Failed to strip RCS keywords in%s"%file1177raise11781179print"Patched up RCS keywords in%s"%file11801181defp4UserForCommit(self,id):1182# Return the tuple (perforce user,git email) for a given git commit id1183 self.getUserMapFromPerforceServer()1184 gitEmail =read_pipe(["git","log","--max-count=1",1185"--format=%ae",id])1186 gitEmail = gitEmail.strip()1187if not self.emails.has_key(gitEmail):1188return(None,gitEmail)1189else:1190return(self.emails[gitEmail],gitEmail)11911192defcheckValidP4Users(self,commits):1193# check if any git authors cannot be mapped to p4 users1194foridin commits:1195(user,email) = self.p4UserForCommit(id)1196if not user:1197 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1198ifgitConfigBool("git-p4.allowMissingP4Users"):1199print"%s"% msg1200else:1201die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)12021203deflastP4Changelist(self):1204# Get back the last changelist number submitted in this client spec. This1205# then gets used to patch up the username in the change. If the same1206# client spec is being used by multiple processes then this might go1207# wrong.1208 results =p4CmdList("client -o")# find the current client1209 client =None1210for r in results:1211if r.has_key('Client'):1212 client = r['Client']1213break1214if not client:1215die("could not get client spec")1216 results =p4CmdList(["changes","-c", client,"-m","1"])1217for r in results:1218if r.has_key('change'):1219return r['change']1220die("Could not get changelist number for last submit - cannot patch up user details")12211222defmodifyChangelistUser(self, changelist, newUser):1223# fixup the user field of a changelist after it has been submitted.1224 changes =p4CmdList("change -o%s"% changelist)1225iflen(changes) !=1:1226die("Bad output from p4 change modifying%sto user%s"%1227(changelist, newUser))12281229 c = changes[0]1230if c['User'] == newUser:return# nothing to do1231 c['User'] = newUser1232input= marshal.dumps(c)12331234 result =p4CmdList("change -f -i", stdin=input)1235for r in result:1236if r.has_key('code'):1237if r['code'] =='error':1238die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1239if r.has_key('data'):1240print("Updated user field for changelist%sto%s"% (changelist, newUser))1241return1242die("Could not modify user field of changelist%sto%s"% (changelist, newUser))12431244defcanChangeChangelists(self):1245# check to see if we have p4 admin or super-user permissions, either of1246# which are required to modify changelists.1247 results =p4CmdList(["protects", self.depotPath])1248for r in results:1249if r.has_key('perm'):1250if r['perm'] =='admin':1251return11252if r['perm'] =='super':1253return11254return012551256defprepareSubmitTemplate(self):1257"""Run "p4 change -o" to grab a change specification template.1258 This does not use "p4 -G", as it is nice to keep the submission1259 template in original order, since a human might edit it.12601261 Remove lines in the Files section that show changes to files1262 outside the depot path we're committing into."""12631264 template =""1265 inFilesSection =False1266for line inp4_read_pipe_lines(['change','-o']):1267if line.endswith("\r\n"):1268 line = line[:-2] +"\n"1269if inFilesSection:1270if line.startswith("\t"):1271# path starts and ends with a tab1272 path = line[1:]1273 lastTab = path.rfind("\t")1274if lastTab != -1:1275 path = path[:lastTab]1276if notp4PathStartsWith(path, self.depotPath):1277continue1278else:1279 inFilesSection =False1280else:1281if line.startswith("Files:"):1282 inFilesSection =True12831284 template += line12851286return template12871288defedit_template(self, template_file):1289"""Invoke the editor to let the user change the submission1290 message. Return true if okay to continue with the submit."""12911292# if configured to skip the editing part, just submit1293ifgitConfigBool("git-p4.skipSubmitEdit"):1294return True12951296# look at the modification time, to check later if the user saved1297# the file1298 mtime = os.stat(template_file).st_mtime12991300# invoke the editor1301if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1302 editor = os.environ.get("P4EDITOR")1303else:1304 editor =read_pipe("git var GIT_EDITOR").strip()1305system(["sh","-c", ('%s"$@"'% editor), editor, template_file])13061307# If the file was not saved, prompt to see if this patch should1308# be skipped. But skip this verification step if configured so.1309ifgitConfigBool("git-p4.skipSubmitEditCheck"):1310return True13111312# modification time updated means user saved the file1313if os.stat(template_file).st_mtime > mtime:1314return True13151316while True:1317 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1318if response =='y':1319return True1320if response =='n':1321return False13221323defget_diff_description(self, editedFiles, filesToAdd):1324# diff1325if os.environ.has_key("P4DIFF"):1326del(os.environ["P4DIFF"])1327 diff =""1328for editedFile in editedFiles:1329 diff +=p4_read_pipe(['diff','-du',1330wildcard_encode(editedFile)])13311332# new file diff1333 newdiff =""1334for newFile in filesToAdd:1335 newdiff +="==== new file ====\n"1336 newdiff +="--- /dev/null\n"1337 newdiff +="+++%s\n"% newFile1338 f =open(newFile,"r")1339for line in f.readlines():1340 newdiff +="+"+ line1341 f.close()13421343return(diff + newdiff).replace('\r\n','\n')13441345defapplyCommit(self,id):1346"""Apply one commit, return True if it succeeded."""13471348print"Applying",read_pipe(["git","show","-s",1349"--format=format:%h%s",id])13501351(p4User, gitEmail) = self.p4UserForCommit(id)13521353 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1354 filesToAdd =set()1355 filesToDelete =set()1356 editedFiles =set()1357 pureRenameCopy =set()1358 filesToChangeExecBit = {}13591360for line in diff:1361 diff =parseDiffTreeEntry(line)1362 modifier = diff['status']1363 path = diff['src']1364if modifier =="M":1365p4_edit(path)1366ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1367 filesToChangeExecBit[path] = diff['dst_mode']1368 editedFiles.add(path)1369elif modifier =="A":1370 filesToAdd.add(path)1371 filesToChangeExecBit[path] = diff['dst_mode']1372if path in filesToDelete:1373 filesToDelete.remove(path)1374elif modifier =="D":1375 filesToDelete.add(path)1376if path in filesToAdd:1377 filesToAdd.remove(path)1378elif modifier =="C":1379 src, dest = diff['src'], diff['dst']1380p4_integrate(src, dest)1381 pureRenameCopy.add(dest)1382if diff['src_sha1'] != diff['dst_sha1']:1383p4_edit(dest)1384 pureRenameCopy.discard(dest)1385ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1386p4_edit(dest)1387 pureRenameCopy.discard(dest)1388 filesToChangeExecBit[dest] = diff['dst_mode']1389if self.isWindows:1390# turn off read-only attribute1391 os.chmod(dest, stat.S_IWRITE)1392 os.unlink(dest)1393 editedFiles.add(dest)1394elif modifier =="R":1395 src, dest = diff['src'], diff['dst']1396if self.p4HasMoveCommand:1397p4_edit(src)# src must be open before move1398p4_move(src, dest)# opens for (move/delete, move/add)1399else:1400p4_integrate(src, dest)1401if diff['src_sha1'] != diff['dst_sha1']:1402p4_edit(dest)1403else:1404 pureRenameCopy.add(dest)1405ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1406if not self.p4HasMoveCommand:1407p4_edit(dest)# with move: already open, writable1408 filesToChangeExecBit[dest] = diff['dst_mode']1409if not self.p4HasMoveCommand:1410if self.isWindows:1411 os.chmod(dest, stat.S_IWRITE)1412 os.unlink(dest)1413 filesToDelete.add(src)1414 editedFiles.add(dest)1415else:1416die("unknown modifier%sfor%s"% (modifier, path))14171418 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1419 patchcmd = diffcmd +" | git apply "1420 tryPatchCmd = patchcmd +"--check -"1421 applyPatchCmd = patchcmd +"--check --apply -"1422 patch_succeeded =True14231424if os.system(tryPatchCmd) !=0:1425 fixed_rcs_keywords =False1426 patch_succeeded =False1427print"Unfortunately applying the change failed!"14281429# Patch failed, maybe it's just RCS keyword woes. Look through1430# the patch to see if that's possible.1431ifgitConfigBool("git-p4.attemptRCSCleanup"):1432file=None1433 pattern =None1434 kwfiles = {}1435forfilein editedFiles | filesToDelete:1436# did this file's delta contain RCS keywords?1437 pattern =p4_keywords_regexp_for_file(file)14381439if pattern:1440# this file is a possibility...look for RCS keywords.1441 regexp = re.compile(pattern, re.VERBOSE)1442for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1443if regexp.search(line):1444if verbose:1445print"got keyword match on%sin%sin%s"% (pattern, line,file)1446 kwfiles[file] = pattern1447break14481449forfilein kwfiles:1450if verbose:1451print"zapping%swith%s"% (line,pattern)1452# File is being deleted, so not open in p4. Must1453# disable the read-only bit on windows.1454if self.isWindows andfilenot in editedFiles:1455 os.chmod(file, stat.S_IWRITE)1456 self.patchRCSKeywords(file, kwfiles[file])1457 fixed_rcs_keywords =True14581459if fixed_rcs_keywords:1460print"Retrying the patch with RCS keywords cleaned up"1461if os.system(tryPatchCmd) ==0:1462 patch_succeeded =True14631464if not patch_succeeded:1465for f in editedFiles:1466p4_revert(f)1467return False14681469#1470# Apply the patch for real, and do add/delete/+x handling.1471#1472system(applyPatchCmd)14731474for f in filesToAdd:1475p4_add(f)1476for f in filesToDelete:1477p4_revert(f)1478p4_delete(f)14791480# Set/clear executable bits1481for f in filesToChangeExecBit.keys():1482 mode = filesToChangeExecBit[f]1483setP4ExecBit(f, mode)14841485#1486# Build p4 change description, starting with the contents1487# of the git commit message.1488#1489 logMessage =extractLogMessageFromGitCommit(id)1490 logMessage = logMessage.strip()1491(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14921493 template = self.prepareSubmitTemplate()1494 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14951496if self.preserveUser:1497 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14981499if self.checkAuthorship and not self.p4UserIsMe(p4User):1500 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1501 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1502 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"15031504 separatorLine ="######## everything below this line is just the diff #######\n"1505if not self.prepare_p4_only:1506 submitTemplate += separatorLine1507 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)15081509(handle, fileName) = tempfile.mkstemp()1510 tmpFile = os.fdopen(handle,"w+b")1511if self.isWindows:1512 submitTemplate = submitTemplate.replace("\n","\r\n")1513 tmpFile.write(submitTemplate)1514 tmpFile.close()15151516if self.prepare_p4_only:1517#1518# Leave the p4 tree prepared, and the submit template around1519# and let the user decide what to do next1520#1521print1522print"P4 workspace prepared for submission."1523print"To submit or revert, go to client workspace"1524print" "+ self.clientPath1525print1526print"To submit, use\"p4 submit\"to write a new description,"1527print"or\"p4 submit -i <%s\"to use the one prepared by" \1528"\"git p4\"."% fileName1529print"You can delete the file\"%s\"when finished."% fileName15301531if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1532print"To preserve change ownership by user%s, you must\n" \1533"do\"p4 change -f <change>\"after submitting and\n" \1534"edit the User field."1535if pureRenameCopy:1536print"After submitting, renamed files must be re-synced."1537print"Invoke\"p4 sync -f\"on each of these files:"1538for f in pureRenameCopy:1539print" "+ f15401541print1542print"To revert the changes, use\"p4 revert ...\", and delete"1543print"the submit template file\"%s\""% fileName1544if filesToAdd:1545print"Since the commit adds new files, they must be deleted:"1546for f in filesToAdd:1547print" "+ f1548print1549return True15501551#1552# Let the user edit the change description, then submit it.1553#1554if self.edit_template(fileName):1555# read the edited message and submit1556 ret =True1557 tmpFile =open(fileName,"rb")1558 message = tmpFile.read()1559 tmpFile.close()1560if self.isWindows:1561 message = message.replace("\r\n","\n")1562 submitTemplate = message[:message.index(separatorLine)]1563p4_write_pipe(['submit','-i'], submitTemplate)15641565if self.preserveUser:1566if p4User:1567# Get last changelist number. Cannot easily get it from1568# the submit command output as the output is1569# unmarshalled.1570 changelist = self.lastP4Changelist()1571 self.modifyChangelistUser(changelist, p4User)15721573# The rename/copy happened by applying a patch that created a1574# new file. This leaves it writable, which confuses p4.1575for f in pureRenameCopy:1576p4_sync(f,"-f")15771578else:1579# skip this patch1580 ret =False1581print"Submission cancelled, undoing p4 changes."1582for f in editedFiles:1583p4_revert(f)1584for f in filesToAdd:1585p4_revert(f)1586 os.remove(f)1587for f in filesToDelete:1588p4_revert(f)15891590 os.remove(fileName)1591return ret15921593# Export git tags as p4 labels. Create a p4 label and then tag1594# with that.1595defexportGitTags(self, gitTags):1596 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1597iflen(validLabelRegexp) ==0:1598 validLabelRegexp = defaultLabelRegexp1599 m = re.compile(validLabelRegexp)16001601for name in gitTags:16021603if not m.match(name):1604if verbose:1605print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1606continue16071608# Get the p4 commit this corresponds to1609 logMessage =extractLogMessageFromGitCommit(name)1610 values =extractSettingsGitLog(logMessage)16111612if not values.has_key('change'):1613# a tag pointing to something not sent to p4; ignore1614if verbose:1615print"git tag%sdoes not give a p4 commit"% name1616continue1617else:1618 changelist = values['change']16191620# Get the tag details.1621 inHeader =True1622 isAnnotated =False1623 body = []1624for l inread_pipe_lines(["git","cat-file","-p", name]):1625 l = l.strip()1626if inHeader:1627if re.match(r'tag\s+', l):1628 isAnnotated =True1629elif re.match(r'\s*$', l):1630 inHeader =False1631continue1632else:1633 body.append(l)16341635if not isAnnotated:1636 body = ["lightweight tag imported by git p4\n"]16371638# Create the label - use the same view as the client spec we are using1639 clientSpec =getClientSpec()16401641 labelTemplate ="Label:%s\n"% name1642 labelTemplate +="Description:\n"1643for b in body:1644 labelTemplate +="\t"+ b +"\n"1645 labelTemplate +="View:\n"1646for depot_side in clientSpec.mappings:1647 labelTemplate +="\t%s\n"% depot_side16481649if self.dry_run:1650print"Would create p4 label%sfor tag"% name1651elif self.prepare_p4_only:1652print"Not creating p4 label%sfor tag due to option" \1653" --prepare-p4-only"% name1654else:1655p4_write_pipe(["label","-i"], labelTemplate)16561657# Use the label1658p4_system(["tag","-l", name] +1659["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16601661if verbose:1662print"created p4 label for tag%s"% name16631664defrun(self, args):1665iflen(args) ==0:1666 self.master =currentGitBranch()1667iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1668die("Detecting current git branch failed!")1669eliflen(args) ==1:1670 self.master = args[0]1671if notbranchExists(self.master):1672die("Branch%sdoes not exist"% self.master)1673else:1674return False16751676 allowSubmit =gitConfig("git-p4.allowSubmit")1677iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1678die("%sis not in git-p4.allowSubmit"% self.master)16791680[upstream, settings] =findUpstreamBranchPoint()1681 self.depotPath = settings['depot-paths'][0]1682iflen(self.origin) ==0:1683 self.origin = upstream16841685if self.preserveUser:1686if not self.canChangeChangelists():1687die("Cannot preserve user names without p4 super-user or admin permissions")16881689# if not set from the command line, try the config file1690if self.conflict_behavior is None:1691 val =gitConfig("git-p4.conflict")1692if val:1693if val not in self.conflict_behavior_choices:1694die("Invalid value '%s' for config git-p4.conflict"% val)1695else:1696 val ="ask"1697 self.conflict_behavior = val16981699if self.verbose:1700print"Origin branch is "+ self.origin17011702iflen(self.depotPath) ==0:1703print"Internal error: cannot locate perforce depot path from existing branches"1704 sys.exit(128)17051706 self.useClientSpec =False1707ifgitConfigBool("git-p4.useclientspec"):1708 self.useClientSpec =True1709if self.useClientSpec:1710 self.clientSpecDirs =getClientSpec()17111712# Check for the existance of P4 branches1713 branchesDetected = (len(p4BranchesInGit().keys()) >1)17141715if self.useClientSpec and not branchesDetected:1716# all files are relative to the client spec1717 self.clientPath =getClientRoot()1718else:1719 self.clientPath =p4Where(self.depotPath)17201721if self.clientPath =="":1722die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)17231724print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1725 self.oldWorkingDirectory = os.getcwd()17261727# ensure the clientPath exists1728 new_client_dir =False1729if not os.path.exists(self.clientPath):1730 new_client_dir =True1731 os.makedirs(self.clientPath)17321733chdir(self.clientPath, is_client_path=True)1734if self.dry_run:1735print"Would synchronize p4 checkout in%s"% self.clientPath1736else:1737print"Synchronizing p4 checkout..."1738if new_client_dir:1739# old one was destroyed, and maybe nobody told p41740p4_sync("...","-f")1741else:1742p4_sync("...")1743 self.check()17441745 commits = []1746for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1747 commits.append(line.strip())1748 commits.reverse()17491750if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1751 self.checkAuthorship =False1752else:1753 self.checkAuthorship =True17541755if self.preserveUser:1756 self.checkValidP4Users(commits)17571758#1759# Build up a set of options to be passed to diff when1760# submitting each commit to p4.1761#1762if self.detectRenames:1763# command-line -M arg1764 self.diffOpts ="-M"1765else:1766# If not explicitly set check the config variable1767 detectRenames =gitConfig("git-p4.detectRenames")17681769if detectRenames.lower() =="false"or detectRenames =="":1770 self.diffOpts =""1771elif detectRenames.lower() =="true":1772 self.diffOpts ="-M"1773else:1774 self.diffOpts ="-M%s"% detectRenames17751776# no command-line arg for -C or --find-copies-harder, just1777# config variables1778 detectCopies =gitConfig("git-p4.detectCopies")1779if detectCopies.lower() =="false"or detectCopies =="":1780pass1781elif detectCopies.lower() =="true":1782 self.diffOpts +=" -C"1783else:1784 self.diffOpts +=" -C%s"% detectCopies17851786ifgitConfigBool("git-p4.detectCopiesHarder"):1787 self.diffOpts +=" --find-copies-harder"17881789#1790# Apply the commits, one at a time. On failure, ask if should1791# continue to try the rest of the patches, or quit.1792#1793if self.dry_run:1794print"Would apply"1795 applied = []1796 last =len(commits) -11797for i, commit inenumerate(commits):1798if self.dry_run:1799print" ",read_pipe(["git","show","-s",1800"--format=format:%h%s", commit])1801 ok =True1802else:1803 ok = self.applyCommit(commit)1804if ok:1805 applied.append(commit)1806else:1807if self.prepare_p4_only and i < last:1808print"Processing only the first commit due to option" \1809" --prepare-p4-only"1810break1811if i < last:1812 quit =False1813while True:1814# prompt for what to do, or use the option/variable1815if self.conflict_behavior =="ask":1816print"What do you want to do?"1817 response =raw_input("[s]kip this commit but apply"1818" the rest, or [q]uit? ")1819if not response:1820continue1821elif self.conflict_behavior =="skip":1822 response ="s"1823elif self.conflict_behavior =="quit":1824 response ="q"1825else:1826die("Unknown conflict_behavior '%s'"%1827 self.conflict_behavior)18281829if response[0] =="s":1830print"Skipping this commit, but applying the rest"1831break1832if response[0] =="q":1833print"Quitting"1834 quit =True1835break1836if quit:1837break18381839chdir(self.oldWorkingDirectory)18401841if self.dry_run:1842pass1843elif self.prepare_p4_only:1844pass1845eliflen(commits) ==len(applied):1846print"All commits applied!"18471848 sync =P4Sync()1849if self.branch:1850 sync.branch = self.branch1851 sync.run([])18521853 rebase =P4Rebase()1854 rebase.rebase()18551856else:1857iflen(applied) ==0:1858print"No commits applied."1859else:1860print"Applied only the commits marked with '*':"1861for c in commits:1862if c in applied:1863 star ="*"1864else:1865 star =" "1866print star,read_pipe(["git","show","-s",1867"--format=format:%h%s", c])1868print"You will have to do 'git p4 sync' and rebase."18691870ifgitConfigBool("git-p4.exportLabels"):1871 self.exportLabels =True18721873if self.exportLabels:1874 p4Labels =getP4Labels(self.depotPath)1875 gitTags =getGitTags()18761877 missingGitTags = gitTags - p4Labels1878 self.exportGitTags(missingGitTags)18791880# exit with error unless everything applied perfectly1881iflen(commits) !=len(applied):1882 sys.exit(1)18831884return True18851886classView(object):1887"""Represent a p4 view ("p4 help views"), and map files in a1888 repo according to the view."""18891890def__init__(self, client_name):1891 self.mappings = []1892 self.client_prefix ="//%s/"% client_name1893# cache results of "p4 where" to lookup client file locations1894 self.client_spec_path_cache = {}18951896defappend(self, view_line):1897"""Parse a view line, splitting it into depot and client1898 sides. Append to self.mappings, preserving order. This1899 is only needed for tag creation."""19001901# Split the view line into exactly two words. P4 enforces1902# structure on these lines that simplifies this quite a bit.1903#1904# Either or both words may be double-quoted.1905# Single quotes do not matter.1906# Double-quote marks cannot occur inside the words.1907# A + or - prefix is also inside the quotes.1908# There are no quotes unless they contain a space.1909# The line is already white-space stripped.1910# The two words are separated by a single space.1911#1912if view_line[0] =='"':1913# First word is double quoted. Find its end.1914 close_quote_index = view_line.find('"',1)1915if close_quote_index <=0:1916die("No first-word closing quote found:%s"% view_line)1917 depot_side = view_line[1:close_quote_index]1918# skip closing quote and space1919 rhs_index = close_quote_index +1+11920else:1921 space_index = view_line.find(" ")1922if space_index <=0:1923die("No word-splitting space found:%s"% view_line)1924 depot_side = view_line[0:space_index]1925 rhs_index = space_index +119261927# prefix + means overlay on previous mapping1928if depot_side.startswith("+"):1929 depot_side = depot_side[1:]19301931# prefix - means exclude this path, leave out of mappings1932 exclude =False1933if depot_side.startswith("-"):1934 exclude =True1935 depot_side = depot_side[1:]19361937if not exclude:1938 self.mappings.append(depot_side)19391940defconvert_client_path(self, clientFile):1941# chop off //client/ part to make it relative1942if not clientFile.startswith(self.client_prefix):1943die("No prefix '%s' on clientFile '%s'"%1944(self.client_prefix, clientFile))1945return clientFile[len(self.client_prefix):]19461947defupdate_client_spec_path_cache(self, files):1948""" Caching file paths by "p4 where" batch query """19491950# List depot file paths exclude that already cached1951 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]19521953iflen(fileArgs) ==0:1954return# All files in cache19551956 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1957for res in where_result:1958if"code"in res and res["code"] =="error":1959# assume error is "... file(s) not in client view"1960continue1961if"clientFile"not in res:1962die("No clientFile in 'p4 where' output")1963if"unmap"in res:1964# it will list all of them, but only one not unmap-ped1965continue1966ifgitConfigBool("core.ignorecase"):1967 res['depotFile'] = res['depotFile'].lower()1968 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19691970# not found files or unmap files set to ""1971for depotFile in fileArgs:1972ifgitConfigBool("core.ignorecase"):1973 depotFile = depotFile.lower()1974if depotFile not in self.client_spec_path_cache:1975 self.client_spec_path_cache[depotFile] =""19761977defmap_in_client(self, depot_path):1978"""Return the relative location in the client where this1979 depot file should live. Returns "" if the file should1980 not be mapped in the client."""19811982ifgitConfigBool("core.ignorecase"):1983 depot_path = depot_path.lower()19841985if depot_path in self.client_spec_path_cache:1986return self.client_spec_path_cache[depot_path]19871988die("Error:%sis not found in client spec path"% depot_path )1989return""19901991classP4Sync(Command, P4UserMap):1992 delete_actions = ("delete","move/delete","purge")19931994def__init__(self):1995 Command.__init__(self)1996 P4UserMap.__init__(self)1997 self.options = [1998 optparse.make_option("--branch", dest="branch"),1999 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2000 optparse.make_option("--changesfile", dest="changesFile"),2001 optparse.make_option("--silent", dest="silent", action="store_true"),2002 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2003 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2004 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2005help="Import into refs/heads/ , not refs/remotes"),2006 optparse.make_option("--max-changes", dest="maxChanges",2007help="Maximum number of changes to import"),2008 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2009help="Internal block size to use when iteratively calling p4 changes"),2010 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2011help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2012 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2013help="Only sync files that are included in the Perforce Client Spec"),2014 optparse.make_option("-/", dest="cloneExclude",2015 action="append",type="string",2016help="exclude depot path"),2017]2018 self.description ="""Imports from Perforce into a git repository.\n2019 example:2020 //depot/my/project/ -- to import the current head2021 //depot/my/project/@all -- to import everything2022 //depot/my/project/@1,6 -- to import only from revision 1 to 620232024 (a ... is not needed in the path p4 specification, it's added implicitly)"""20252026 self.usage +=" //depot/path[@revRange]"2027 self.silent =False2028 self.createdBranches =set()2029 self.committedChanges =set()2030 self.branch =""2031 self.detectBranches =False2032 self.detectLabels =False2033 self.importLabels =False2034 self.changesFile =""2035 self.syncWithOrigin =True2036 self.importIntoRemotes =True2037 self.maxChanges =""2038 self.changes_block_size =None2039 self.keepRepoPath =False2040 self.depotPaths =None2041 self.p4BranchesInGit = []2042 self.cloneExclude = []2043 self.useClientSpec =False2044 self.useClientSpec_from_options =False2045 self.clientSpecDirs =None2046 self.tempBranches = []2047 self.tempBranchLocation ="git-p4-tmp"20482049ifgitConfig("git-p4.syncFromOrigin") =="false":2050 self.syncWithOrigin =False20512052# This is required for the "append" cloneExclude action2053defensure_value(self, attr, value):2054if nothasattr(self, attr)orgetattr(self, attr)is None:2055setattr(self, attr, value)2056returngetattr(self, attr)20572058# Force a checkpoint in fast-import and wait for it to finish2059defcheckpoint(self):2060 self.gitStream.write("checkpoint\n\n")2061 self.gitStream.write("progress checkpoint\n\n")2062 out = self.gitOutput.readline()2063if self.verbose:2064print"checkpoint finished: "+ out20652066defextractFilesFromCommit(self, commit):2067 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2068for path in self.cloneExclude]2069 files = []2070 fnum =02071while commit.has_key("depotFile%s"% fnum):2072 path = commit["depotFile%s"% fnum]20732074if[p for p in self.cloneExclude2075ifp4PathStartsWith(path, p)]:2076 found =False2077else:2078 found = [p for p in self.depotPaths2079ifp4PathStartsWith(path, p)]2080if not found:2081 fnum = fnum +12082continue20832084file= {}2085file["path"] = path2086file["rev"] = commit["rev%s"% fnum]2087file["action"] = commit["action%s"% fnum]2088file["type"] = commit["type%s"% fnum]2089 files.append(file)2090 fnum = fnum +12091return files20922093defstripRepoPath(self, path, prefixes):2094"""When streaming files, this is called to map a p4 depot path2095 to where it should go in git. The prefixes are either2096 self.depotPaths, or self.branchPrefixes in the case of2097 branch detection."""20982099if self.useClientSpec:2100# branch detection moves files up a level (the branch name)2101# from what client spec interpretation gives2102 path = self.clientSpecDirs.map_in_client(path)2103if self.detectBranches:2104for b in self.knownBranches:2105if path.startswith(b +"/"):2106 path = path[len(b)+1:]21072108elif self.keepRepoPath:2109# Preserve everything in relative path name except leading2110# //depot/; just look at first prefix as they all should2111# be in the same depot.2112 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2113ifp4PathStartsWith(path, depot):2114 path = path[len(depot):]21152116else:2117for p in prefixes:2118ifp4PathStartsWith(path, p):2119 path = path[len(p):]2120break21212122 path =wildcard_decode(path)2123return path21242125defsplitFilesIntoBranches(self, commit):2126"""Look at each depotFile in the commit to figure out to what2127 branch it belongs."""21282129if self.clientSpecDirs:2130 files = self.extractFilesFromCommit(commit)2131 self.clientSpecDirs.update_client_spec_path_cache(files)21322133 branches = {}2134 fnum =02135while commit.has_key("depotFile%s"% fnum):2136 path = commit["depotFile%s"% fnum]2137 found = [p for p in self.depotPaths2138ifp4PathStartsWith(path, p)]2139if not found:2140 fnum = fnum +12141continue21422143file= {}2144file["path"] = path2145file["rev"] = commit["rev%s"% fnum]2146file["action"] = commit["action%s"% fnum]2147file["type"] = commit["type%s"% fnum]2148 fnum = fnum +121492150# start with the full relative path where this file would2151# go in a p4 client2152if self.useClientSpec:2153 relPath = self.clientSpecDirs.map_in_client(path)2154else:2155 relPath = self.stripRepoPath(path, self.depotPaths)21562157for branch in self.knownBranches.keys():2158# add a trailing slash so that a commit into qt/4.2foo2159# doesn't end up in qt/4.2, e.g.2160if relPath.startswith(branch +"/"):2161if branch not in branches:2162 branches[branch] = []2163 branches[branch].append(file)2164break21652166return branches21672168# output one file from the P4 stream2169# - helper for streamP4Files21702171defstreamOneP4File(self,file, contents):2172 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2173if verbose:2174 size =int(self.stream_file['fileSize'])2175 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2176 sys.stdout.flush()21772178(type_base, type_mods) =split_p4_type(file["type"])21792180 git_mode ="100644"2181if"x"in type_mods:2182 git_mode ="100755"2183if type_base =="symlink":2184 git_mode ="120000"2185# p4 print on a symlink sometimes contains "target\n";2186# if it does, remove the newline2187 data =''.join(contents)2188if not data:2189# Some version of p4 allowed creating a symlink that pointed2190# to nothing. This causes p4 errors when checking out such2191# a change, and errors here too. Work around it by ignoring2192# the bad symlink; hopefully a future change fixes it.2193print"\nIgnoring empty symlink in%s"%file['depotFile']2194return2195elif data[-1] =='\n':2196 contents = [data[:-1]]2197else:2198 contents = [data]21992200if type_base =="utf16":2201# p4 delivers different text in the python output to -G2202# than it does when using "print -o", or normal p4 client2203# operations. utf16 is converted to ascii or utf8, perhaps.2204# But ascii text saved as -t utf16 is completely mangled.2205# Invoke print -o to get the real contents.2206#2207# On windows, the newlines will always be mangled by print, so put2208# them back too. This is not needed to the cygwin windows version,2209# just the native "NT" type.2210#2211 text =p4_read_pipe(['print','-q','-o','-',"%s@%s"% (file['depotFile'],file['change']) ])2212ifp4_version_string().find("/NT") >=0:2213 text = text.replace("\r\n","\n")2214 contents = [ text ]22152216if type_base =="apple":2217# Apple filetype files will be streamed as a concatenation of2218# its appledouble header and the contents. This is useless2219# on both macs and non-macs. If using "print -q -o xx", it2220# will create "xx" with the data, and "%xx" with the header.2221# This is also not very useful.2222#2223# Ideally, someday, this script can learn how to generate2224# appledouble files directly and import those to git, but2225# non-mac machines can never find a use for apple filetype.2226print"\nIgnoring apple filetype file%s"%file['depotFile']2227return22282229# Note that we do not try to de-mangle keywords on utf16 files,2230# even though in theory somebody may want that.2231 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2232if pattern:2233 regexp = re.compile(pattern, re.VERBOSE)2234 text =''.join(contents)2235 text = regexp.sub(r'$\1$', text)2236 contents = [ text ]22372238 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22392240# total length...2241 length =02242for d in contents:2243 length = length +len(d)22442245 self.gitStream.write("data%d\n"% length)2246for d in contents:2247 self.gitStream.write(d)2248 self.gitStream.write("\n")22492250defstreamOneP4Deletion(self,file):2251 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2252if verbose:2253 sys.stdout.write("delete%s\n"% relPath)2254 sys.stdout.flush()2255 self.gitStream.write("D%s\n"% relPath)22562257# handle another chunk of streaming data2258defstreamP4FilesCb(self, marshalled):22592260# catch p4 errors and complain2261 err =None2262if"code"in marshalled:2263if marshalled["code"] =="error":2264if"data"in marshalled:2265 err = marshalled["data"].rstrip()2266if err:2267 f =None2268if self.stream_have_file_info:2269if"depotFile"in self.stream_file:2270 f = self.stream_file["depotFile"]2271# force a failure in fast-import, else an empty2272# commit will be made2273 self.gitStream.write("\n")2274 self.gitStream.write("die-now\n")2275 self.gitStream.close()2276# ignore errors, but make sure it exits first2277 self.importProcess.wait()2278if f:2279die("Error from p4 print for%s:%s"% (f, err))2280else:2281die("Error from p4 print:%s"% err)22822283if marshalled.has_key('depotFile')and self.stream_have_file_info:2284# start of a new file - output the old one first2285 self.streamOneP4File(self.stream_file, self.stream_contents)2286 self.stream_file = {}2287 self.stream_contents = []2288 self.stream_have_file_info =False22892290# pick up the new file information... for the2291# 'data' field we need to append to our array2292for k in marshalled.keys():2293if k =='data':2294if'streamContentSize'not in self.stream_file:2295 self.stream_file['streamContentSize'] =02296 self.stream_file['streamContentSize'] +=len(marshalled['data'])2297 self.stream_contents.append(marshalled['data'])2298else:2299 self.stream_file[k] = marshalled[k]23002301if(verbose and2302'streamContentSize'in self.stream_file and2303'fileSize'in self.stream_file and2304'depotFile'in self.stream_file):2305 size =int(self.stream_file["fileSize"])2306if size >0:2307 progress =100*self.stream_file['streamContentSize']/size2308 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2309 sys.stdout.flush()23102311 self.stream_have_file_info =True23122313# Stream directly from "p4 files" into "git fast-import"2314defstreamP4Files(self, files):2315 filesForCommit = []2316 filesToRead = []2317 filesToDelete = []23182319for f in files:2320# if using a client spec, only add the files that have2321# a path in the client2322if self.clientSpecDirs:2323if self.clientSpecDirs.map_in_client(f['path']) =="":2324continue23252326 filesForCommit.append(f)2327if f['action']in self.delete_actions:2328 filesToDelete.append(f)2329else:2330 filesToRead.append(f)23312332# deleted files...2333for f in filesToDelete:2334 self.streamOneP4Deletion(f)23352336iflen(filesToRead) >0:2337 self.stream_file = {}2338 self.stream_contents = []2339 self.stream_have_file_info =False23402341# curry self argument2342defstreamP4FilesCbSelf(entry):2343 self.streamP4FilesCb(entry)23442345 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23462347p4CmdList(["-x","-","print"],2348 stdin=fileArgs,2349 cb=streamP4FilesCbSelf)23502351# do the last chunk2352if self.stream_file.has_key('depotFile'):2353 self.streamOneP4File(self.stream_file, self.stream_contents)23542355defmake_email(self, userid):2356if userid in self.users:2357return self.users[userid]2358else:2359return"%s<a@b>"% userid23602361# Stream a p4 tag2362defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2363if verbose:2364print"writing tag%sfor commit%s"% (labelName, commit)2365 gitStream.write("tag%s\n"% labelName)2366 gitStream.write("from%s\n"% commit)23672368if labelDetails.has_key('Owner'):2369 owner = labelDetails["Owner"]2370else:2371 owner =None23722373# Try to use the owner of the p4 label, or failing that,2374# the current p4 user id.2375if owner:2376 email = self.make_email(owner)2377else:2378 email = self.make_email(self.p4UserId())2379 tagger ="%s %s %s"% (email, epoch, self.tz)23802381 gitStream.write("tagger%s\n"% tagger)23822383print"labelDetails=",labelDetails2384if labelDetails.has_key('Description'):2385 description = labelDetails['Description']2386else:2387 description ='Label from git p4'23882389 gitStream.write("data%d\n"%len(description))2390 gitStream.write(description)2391 gitStream.write("\n")23922393defcommit(self, details, files, branch, parent =""):2394 epoch = details["time"]2395 author = details["user"]23962397if self.verbose:2398print"commit into%s"% branch23992400# start with reading files; if that fails, we should not2401# create a commit.2402 new_files = []2403for f in files:2404if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2405 new_files.append(f)2406else:2407 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])24082409if self.clientSpecDirs:2410 self.clientSpecDirs.update_client_spec_path_cache(files)24112412 self.gitStream.write("commit%s\n"% branch)2413# gitStream.write("mark :%s\n" % details["change"])2414 self.committedChanges.add(int(details["change"]))2415 committer =""2416if author not in self.users:2417 self.getUserMapFromPerforceServer()2418 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)24192420 self.gitStream.write("committer%s\n"% committer)24212422 self.gitStream.write("data <<EOT\n")2423 self.gitStream.write(details["desc"])2424 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2425(','.join(self.branchPrefixes), details["change"]))2426iflen(details['options']) >0:2427 self.gitStream.write(": options =%s"% details['options'])2428 self.gitStream.write("]\nEOT\n\n")24292430iflen(parent) >0:2431if self.verbose:2432print"parent%s"% parent2433 self.gitStream.write("from%s\n"% parent)24342435 self.streamP4Files(new_files)2436 self.gitStream.write("\n")24372438 change =int(details["change"])24392440if self.labels.has_key(change):2441 label = self.labels[change]2442 labelDetails = label[0]2443 labelRevisions = label[1]2444if self.verbose:2445print"Change%sis labelled%s"% (change, labelDetails)24462447 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2448for p in self.branchPrefixes])24492450iflen(files) ==len(labelRevisions):24512452 cleanedFiles = {}2453for info in files:2454if info["action"]in self.delete_actions:2455continue2456 cleanedFiles[info["depotFile"]] = info["rev"]24572458if cleanedFiles == labelRevisions:2459 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24602461else:2462if not self.silent:2463print("Tag%sdoes not match with change%s: files do not match."2464% (labelDetails["label"], change))24652466else:2467if not self.silent:2468print("Tag%sdoes not match with change%s: file count is different."2469% (labelDetails["label"], change))24702471# Build a dictionary of changelists and labels, for "detect-labels" option.2472defgetLabels(self):2473 self.labels = {}24742475 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2476iflen(l) >0and not self.silent:2477print"Finding files belonging to labels in%s"% `self.depotPaths`24782479for output in l:2480 label = output["label"]2481 revisions = {}2482 newestChange =02483if self.verbose:2484print"Querying files for label%s"% label2485forfileinp4CmdList(["files"] +2486["%s...@%s"% (p, label)2487for p in self.depotPaths]):2488 revisions[file["depotFile"]] =file["rev"]2489 change =int(file["change"])2490if change > newestChange:2491 newestChange = change24922493 self.labels[newestChange] = [output, revisions]24942495if self.verbose:2496print"Label changes:%s"% self.labels.keys()24972498# Import p4 labels as git tags. A direct mapping does not2499# exist, so assume that if all the files are at the same revision2500# then we can use that, or it's something more complicated we should2501# just ignore.2502defimportP4Labels(self, stream, p4Labels):2503if verbose:2504print"import p4 labels: "+' '.join(p4Labels)25052506 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2507 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2508iflen(validLabelRegexp) ==0:2509 validLabelRegexp = defaultLabelRegexp2510 m = re.compile(validLabelRegexp)25112512for name in p4Labels:2513 commitFound =False25142515if not m.match(name):2516if verbose:2517print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2518continue25192520if name in ignoredP4Labels:2521continue25222523 labelDetails =p4CmdList(['label',"-o", name])[0]25242525# get the most recent changelist for each file in this label2526 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2527for p in self.depotPaths])25282529if change.has_key('change'):2530# find the corresponding git commit; take the oldest commit2531 changelist =int(change['change'])2532 gitCommit =read_pipe(["git","rev-list","--max-count=1",2533"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2534iflen(gitCommit) ==0:2535print"could not find git commit for changelist%d"% changelist2536else:2537 gitCommit = gitCommit.strip()2538 commitFound =True2539# Convert from p4 time format2540try:2541 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2542exceptValueError:2543print"Could not convert label time%s"% labelDetails['Update']2544 tmwhen =125452546 when =int(time.mktime(tmwhen))2547 self.streamTag(stream, name, labelDetails, gitCommit, when)2548if verbose:2549print"p4 label%smapped to git commit%s"% (name, gitCommit)2550else:2551if verbose:2552print"Label%shas no changelists - possibly deleted?"% name25532554if not commitFound:2555# We can't import this label; don't try again as it will get very2556# expensive repeatedly fetching all the files for labels that will2557# never be imported. If the label is moved in the future, the2558# ignore will need to be removed manually.2559system(["git","config","--add","git-p4.ignoredP4Labels", name])25602561defguessProjectName(self):2562for p in self.depotPaths:2563if p.endswith("/"):2564 p = p[:-1]2565 p = p[p.strip().rfind("/") +1:]2566if not p.endswith("/"):2567 p +="/"2568return p25692570defgetBranchMapping(self):2571 lostAndFoundBranches =set()25722573 user =gitConfig("git-p4.branchUser")2574iflen(user) >0:2575 command ="branches -u%s"% user2576else:2577 command ="branches"25782579for info inp4CmdList(command):2580 details =p4Cmd(["branch","-o", info["branch"]])2581 viewIdx =02582while details.has_key("View%s"% viewIdx):2583 paths = details["View%s"% viewIdx].split(" ")2584 viewIdx = viewIdx +12585# require standard //depot/foo/... //depot/bar/... mapping2586iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2587continue2588 source = paths[0]2589 destination = paths[1]2590## HACK2591ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2592 source = source[len(self.depotPaths[0]):-4]2593 destination = destination[len(self.depotPaths[0]):-4]25942595if destination in self.knownBranches:2596if not self.silent:2597print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2598print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2599continue26002601 self.knownBranches[destination] = source26022603 lostAndFoundBranches.discard(destination)26042605if source not in self.knownBranches:2606 lostAndFoundBranches.add(source)26072608# Perforce does not strictly require branches to be defined, so we also2609# check git config for a branch list.2610#2611# Example of branch definition in git config file:2612# [git-p4]2613# branchList=main:branchA2614# branchList=main:branchB2615# branchList=branchA:branchC2616 configBranches =gitConfigList("git-p4.branchList")2617for branch in configBranches:2618if branch:2619(source, destination) = branch.split(":")2620 self.knownBranches[destination] = source26212622 lostAndFoundBranches.discard(destination)26232624if source not in self.knownBranches:2625 lostAndFoundBranches.add(source)262626272628for branch in lostAndFoundBranches:2629 self.knownBranches[branch] = branch26302631defgetBranchMappingFromGitBranches(self):2632 branches =p4BranchesInGit(self.importIntoRemotes)2633for branch in branches.keys():2634if branch =="master":2635 branch ="main"2636else:2637 branch = branch[len(self.projectName):]2638 self.knownBranches[branch] = branch26392640defupdateOptionDict(self, d):2641 option_keys = {}2642if self.keepRepoPath:2643 option_keys['keepRepoPath'] =126442645 d["options"] =' '.join(sorted(option_keys.keys()))26462647defreadOptions(self, d):2648 self.keepRepoPath = (d.has_key('options')2649and('keepRepoPath'in d['options']))26502651defgitRefForBranch(self, branch):2652if branch =="main":2653return self.refPrefix +"master"26542655iflen(branch) <=0:2656return branch26572658return self.refPrefix + self.projectName + branch26592660defgitCommitByP4Change(self, ref, change):2661if self.verbose:2662print"looking in ref "+ ref +" for change%susing bisect..."% change26632664 earliestCommit =""2665 latestCommit =parseRevision(ref)26662667while True:2668if self.verbose:2669print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2670 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2671iflen(next) ==0:2672if self.verbose:2673print"argh"2674return""2675 log =extractLogMessageFromGitCommit(next)2676 settings =extractSettingsGitLog(log)2677 currentChange =int(settings['change'])2678if self.verbose:2679print"current change%s"% currentChange26802681if currentChange == change:2682if self.verbose:2683print"found%s"% next2684return next26852686if currentChange < change:2687 earliestCommit ="^%s"% next2688else:2689 latestCommit ="%s"% next26902691return""26922693defimportNewBranch(self, branch, maxChange):2694# make fast-import flush all changes to disk and update the refs using the checkpoint2695# command so that we can try to find the branch parent in the git history2696 self.gitStream.write("checkpoint\n\n");2697 self.gitStream.flush();2698 branchPrefix = self.depotPaths[0] + branch +"/"2699range="@1,%s"% maxChange2700#print "prefix" + branchPrefix2701 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2702iflen(changes) <=0:2703return False2704 firstChange = changes[0]2705#print "first change in branch: %s" % firstChange2706 sourceBranch = self.knownBranches[branch]2707 sourceDepotPath = self.depotPaths[0] + sourceBranch2708 sourceRef = self.gitRefForBranch(sourceBranch)2709#print "source " + sourceBranch27102711 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2712#print "branch parent: %s" % branchParentChange2713 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2714iflen(gitParent) >0:2715 self.initialParents[self.gitRefForBranch(branch)] = gitParent2716#print "parent git commit: %s" % gitParent27172718 self.importChanges(changes)2719return True27202721defsearchParent(self, parent, branch, target):2722 parentFound =False2723for blob inread_pipe_lines(["git","rev-list","--reverse",2724"--no-merges", parent]):2725 blob = blob.strip()2726iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2727 parentFound =True2728if self.verbose:2729print"Found parent of%sin commit%s"% (branch, blob)2730break2731if parentFound:2732return blob2733else:2734return None27352736defimportChanges(self, changes):2737 cnt =12738for change in changes:2739 description =p4_describe(change)2740 self.updateOptionDict(description)27412742if not self.silent:2743 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2744 sys.stdout.flush()2745 cnt = cnt +127462747try:2748if self.detectBranches:2749 branches = self.splitFilesIntoBranches(description)2750for branch in branches.keys():2751## HACK --hwn2752 branchPrefix = self.depotPaths[0] + branch +"/"2753 self.branchPrefixes = [ branchPrefix ]27542755 parent =""27562757 filesForCommit = branches[branch]27582759if self.verbose:2760print"branch is%s"% branch27612762 self.updatedBranches.add(branch)27632764if branch not in self.createdBranches:2765 self.createdBranches.add(branch)2766 parent = self.knownBranches[branch]2767if parent == branch:2768 parent =""2769else:2770 fullBranch = self.projectName + branch2771if fullBranch not in self.p4BranchesInGit:2772if not self.silent:2773print("\nImporting new branch%s"% fullBranch);2774if self.importNewBranch(branch, change -1):2775 parent =""2776 self.p4BranchesInGit.append(fullBranch)2777if not self.silent:2778print("\nResuming with change%s"% change);27792780if self.verbose:2781print"parent determined through known branches:%s"% parent27822783 branch = self.gitRefForBranch(branch)2784 parent = self.gitRefForBranch(parent)27852786if self.verbose:2787print"looking for initial parent for%s; current parent is%s"% (branch, parent)27882789iflen(parent) ==0and branch in self.initialParents:2790 parent = self.initialParents[branch]2791del self.initialParents[branch]27922793 blob =None2794iflen(parent) >0:2795 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2796if self.verbose:2797print"Creating temporary branch: "+ tempBranch2798 self.commit(description, filesForCommit, tempBranch)2799 self.tempBranches.append(tempBranch)2800 self.checkpoint()2801 blob = self.searchParent(parent, branch, tempBranch)2802if blob:2803 self.commit(description, filesForCommit, branch, blob)2804else:2805if self.verbose:2806print"Parent of%snot found. Committing into head of%s"% (branch, parent)2807 self.commit(description, filesForCommit, branch, parent)2808else:2809 files = self.extractFilesFromCommit(description)2810 self.commit(description, files, self.branch,2811 self.initialParent)2812# only needed once, to connect to the previous commit2813 self.initialParent =""2814exceptIOError:2815print self.gitError.read()2816 sys.exit(1)28172818defimportHeadRevision(self, revision):2819print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)28202821 details = {}2822 details["user"] ="git perforce import user"2823 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2824% (' '.join(self.depotPaths), revision))2825 details["change"] = revision2826 newestRevision =028272828 fileCnt =02829 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28302831for info inp4CmdList(["files"] + fileArgs):28322833if'code'in info and info['code'] =='error':2834 sys.stderr.write("p4 returned an error:%s\n"2835% info['data'])2836if info['data'].find("must refer to client") >=0:2837 sys.stderr.write("This particular p4 error is misleading.\n")2838 sys.stderr.write("Perhaps the depot path was misspelled.\n");2839 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2840 sys.exit(1)2841if'p4ExitCode'in info:2842 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2843 sys.exit(1)284428452846 change =int(info["change"])2847if change > newestRevision:2848 newestRevision = change28492850if info["action"]in self.delete_actions:2851# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2852#fileCnt = fileCnt + 12853continue28542855for prop in["depotFile","rev","action","type"]:2856 details["%s%s"% (prop, fileCnt)] = info[prop]28572858 fileCnt = fileCnt +128592860 details["change"] = newestRevision28612862# Use time from top-most change so that all git p4 clones of2863# the same p4 repo have the same commit SHA1s.2864 res =p4_describe(newestRevision)2865 details["time"] = res["time"]28662867 self.updateOptionDict(details)2868try:2869 self.commit(details, self.extractFilesFromCommit(details), self.branch)2870exceptIOError:2871print"IO error with git fast-import. Is your git version recent enough?"2872print self.gitError.read()287328742875defrun(self, args):2876 self.depotPaths = []2877 self.changeRange =""2878 self.previousDepotPaths = []2879 self.hasOrigin =False28802881# map from branch depot path to parent branch2882 self.knownBranches = {}2883 self.initialParents = {}28842885if self.importIntoRemotes:2886 self.refPrefix ="refs/remotes/p4/"2887else:2888 self.refPrefix ="refs/heads/p4/"28892890if self.syncWithOrigin:2891 self.hasOrigin =originP4BranchesExist()2892if self.hasOrigin:2893if not self.silent:2894print'Syncing with origin first, using "git fetch origin"'2895system("git fetch origin")28962897 branch_arg_given =bool(self.branch)2898iflen(self.branch) ==0:2899 self.branch = self.refPrefix +"master"2900ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2901system("git update-ref%srefs/heads/p4"% self.branch)2902system("git branch -D p4")29032904# accept either the command-line option, or the configuration variable2905if self.useClientSpec:2906# will use this after clone to set the variable2907 self.useClientSpec_from_options =True2908else:2909ifgitConfigBool("git-p4.useclientspec"):2910 self.useClientSpec =True2911if self.useClientSpec:2912 self.clientSpecDirs =getClientSpec()29132914# TODO: should always look at previous commits,2915# merge with previous imports, if possible.2916if args == []:2917if self.hasOrigin:2918createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)29192920# branches holds mapping from branch name to sha12921 branches =p4BranchesInGit(self.importIntoRemotes)29222923# restrict to just this one, disabling detect-branches2924if branch_arg_given:2925 short = self.branch.split("/")[-1]2926if short in branches:2927 self.p4BranchesInGit = [ short ]2928else:2929 self.p4BranchesInGit = branches.keys()29302931iflen(self.p4BranchesInGit) >1:2932if not self.silent:2933print"Importing from/into multiple branches"2934 self.detectBranches =True2935for branch in branches.keys():2936 self.initialParents[self.refPrefix + branch] = \2937 branches[branch]29382939if self.verbose:2940print"branches:%s"% self.p4BranchesInGit29412942 p4Change =02943for branch in self.p4BranchesInGit:2944 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29452946 settings =extractSettingsGitLog(logMsg)29472948 self.readOptions(settings)2949if(settings.has_key('depot-paths')2950and settings.has_key('change')):2951 change =int(settings['change']) +12952 p4Change =max(p4Change, change)29532954 depotPaths =sorted(settings['depot-paths'])2955if self.previousDepotPaths == []:2956 self.previousDepotPaths = depotPaths2957else:2958 paths = []2959for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2960 prev_list = prev.split("/")2961 cur_list = cur.split("/")2962for i inrange(0,min(len(cur_list),len(prev_list))):2963if cur_list[i] <> prev_list[i]:2964 i = i -12965break29662967 paths.append("/".join(cur_list[:i +1]))29682969 self.previousDepotPaths = paths29702971if p4Change >0:2972 self.depotPaths =sorted(self.previousDepotPaths)2973 self.changeRange ="@%s,#head"% p4Change2974if not self.silent and not self.detectBranches:2975print"Performing incremental import into%sgit branch"% self.branch29762977# accept multiple ref name abbreviations:2978# refs/foo/bar/branch -> use it exactly2979# p4/branch -> prepend refs/remotes/ or refs/heads/2980# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2981if not self.branch.startswith("refs/"):2982if self.importIntoRemotes:2983 prepend ="refs/remotes/"2984else:2985 prepend ="refs/heads/"2986if not self.branch.startswith("p4/"):2987 prepend +="p4/"2988 self.branch = prepend + self.branch29892990iflen(args) ==0and self.depotPaths:2991if not self.silent:2992print"Depot paths:%s"%' '.join(self.depotPaths)2993else:2994if self.depotPaths and self.depotPaths != args:2995print("previous import used depot path%sand now%swas specified. "2996"This doesn't work!"% (' '.join(self.depotPaths),2997' '.join(args)))2998 sys.exit(1)29993000 self.depotPaths =sorted(args)30013002 revision =""3003 self.users = {}30043005# Make sure no revision specifiers are used when --changesfile3006# is specified.3007 bad_changesfile =False3008iflen(self.changesFile) >0:3009for p in self.depotPaths:3010if p.find("@") >=0or p.find("#") >=0:3011 bad_changesfile =True3012break3013if bad_changesfile:3014die("Option --changesfile is incompatible with revision specifiers")30153016 newPaths = []3017for p in self.depotPaths:3018if p.find("@") != -1:3019 atIdx = p.index("@")3020 self.changeRange = p[atIdx:]3021if self.changeRange =="@all":3022 self.changeRange =""3023elif','not in self.changeRange:3024 revision = self.changeRange3025 self.changeRange =""3026 p = p[:atIdx]3027elif p.find("#") != -1:3028 hashIdx = p.index("#")3029 revision = p[hashIdx:]3030 p = p[:hashIdx]3031elif self.previousDepotPaths == []:3032# pay attention to changesfile, if given, else import3033# the entire p4 tree at the head revision3034iflen(self.changesFile) ==0:3035 revision ="#head"30363037 p = re.sub("\.\.\.$","", p)3038if not p.endswith("/"):3039 p +="/"30403041 newPaths.append(p)30423043 self.depotPaths = newPaths30443045# --detect-branches may change this for each branch3046 self.branchPrefixes = self.depotPaths30473048 self.loadUserMapFromCache()3049 self.labels = {}3050if self.detectLabels:3051 self.getLabels();30523053if self.detectBranches:3054## FIXME - what's a P4 projectName ?3055 self.projectName = self.guessProjectName()30563057if self.hasOrigin:3058 self.getBranchMappingFromGitBranches()3059else:3060 self.getBranchMapping()3061if self.verbose:3062print"p4-git branches:%s"% self.p4BranchesInGit3063print"initial parents:%s"% self.initialParents3064for b in self.p4BranchesInGit:3065if b !="master":30663067## FIXME3068 b = b[len(self.projectName):]3069 self.createdBranches.add(b)30703071 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30723073 self.importProcess = subprocess.Popen(["git","fast-import"],3074 stdin=subprocess.PIPE,3075 stdout=subprocess.PIPE,3076 stderr=subprocess.PIPE);3077 self.gitOutput = self.importProcess.stdout3078 self.gitStream = self.importProcess.stdin3079 self.gitError = self.importProcess.stderr30803081if revision:3082 self.importHeadRevision(revision)3083else:3084 changes = []30853086iflen(self.changesFile) >0:3087 output =open(self.changesFile).readlines()3088 changeSet =set()3089for line in output:3090 changeSet.add(int(line))30913092for change in changeSet:3093 changes.append(change)30943095 changes.sort()3096else:3097# catch "git p4 sync" with no new branches, in a repo that3098# does not have any existing p4 branches3099iflen(args) ==0:3100if not self.p4BranchesInGit:3101die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")31023103# The default branch is master, unless --branch is used to3104# specify something else. Make sure it exists, or complain3105# nicely about how to use --branch.3106if not self.detectBranches:3107if notbranch_exists(self.branch):3108if branch_arg_given:3109die("Error: branch%sdoes not exist."% self.branch)3110else:3111die("Error: no branch%s; perhaps specify one with --branch."%3112 self.branch)31133114if self.verbose:3115print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3116 self.changeRange)3117 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)31183119iflen(self.maxChanges) >0:3120 changes = changes[:min(int(self.maxChanges),len(changes))]31213122iflen(changes) ==0:3123if not self.silent:3124print"No changes to import!"3125else:3126if not self.silent and not self.detectBranches:3127print"Import destination:%s"% self.branch31283129 self.updatedBranches =set()31303131if not self.detectBranches:3132if args:3133# start a new branch3134 self.initialParent =""3135else:3136# build on a previous revision3137 self.initialParent =parseRevision(self.branch)31383139 self.importChanges(changes)31403141if not self.silent:3142print""3143iflen(self.updatedBranches) >0:3144 sys.stdout.write("Updated branches: ")3145for b in self.updatedBranches:3146 sys.stdout.write("%s"% b)3147 sys.stdout.write("\n")31483149ifgitConfigBool("git-p4.importLabels"):3150 self.importLabels =True31513152if self.importLabels:3153 p4Labels =getP4Labels(self.depotPaths)3154 gitTags =getGitTags()31553156 missingP4Labels = p4Labels - gitTags3157 self.importP4Labels(self.gitStream, missingP4Labels)31583159 self.gitStream.close()3160if self.importProcess.wait() !=0:3161die("fast-import failed:%s"% self.gitError.read())3162 self.gitOutput.close()3163 self.gitError.close()31643165# Cleanup temporary branches created during import3166if self.tempBranches != []:3167for branch in self.tempBranches:3168read_pipe("git update-ref -d%s"% branch)3169 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31703171# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3172# a convenient shortcut refname "p4".3173if self.importIntoRemotes:3174 head_ref = self.refPrefix +"HEAD"3175if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3176system(["git","symbolic-ref", head_ref, self.branch])31773178return True31793180classP4Rebase(Command):3181def__init__(self):3182 Command.__init__(self)3183 self.options = [3184 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3185]3186 self.importLabels =False3187 self.description = ("Fetches the latest revision from perforce and "3188+"rebases the current work (branch) against it")31893190defrun(self, args):3191 sync =P4Sync()3192 sync.importLabels = self.importLabels3193 sync.run([])31943195return self.rebase()31963197defrebase(self):3198if os.system("git update-index --refresh") !=0:3199die("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.");3200iflen(read_pipe("git diff-index HEAD --")) >0:3201die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");32023203[upstream, settings] =findUpstreamBranchPoint()3204iflen(upstream) ==0:3205die("Cannot find upstream branchpoint for rebase")32063207# the branchpoint may be p4/foo~3, so strip off the parent3208 upstream = re.sub("~[0-9]+$","", upstream)32093210print"Rebasing the current branch onto%s"% upstream3211 oldHead =read_pipe("git rev-parse HEAD").strip()3212system("git rebase%s"% upstream)3213system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3214return True32153216classP4Clone(P4Sync):3217def__init__(self):3218 P4Sync.__init__(self)3219 self.description ="Creates a new git repository and imports from Perforce into it"3220 self.usage ="usage: %prog [options] //depot/path[@revRange]"3221 self.options += [3222 optparse.make_option("--destination", dest="cloneDestination",3223 action='store', default=None,3224help="where to leave result of the clone"),3225 optparse.make_option("--bare", dest="cloneBare",3226 action="store_true", default=False),3227]3228 self.cloneDestination =None3229 self.needsGit =False3230 self.cloneBare =False32313232defdefaultDestination(self, args):3233## TODO: use common prefix of args?3234 depotPath = args[0]3235 depotDir = re.sub("(@[^@]*)$","", depotPath)3236 depotDir = re.sub("(#[^#]*)$","", depotDir)3237 depotDir = re.sub(r"\.\.\.$","", depotDir)3238 depotDir = re.sub(r"/$","", depotDir)3239return os.path.split(depotDir)[1]32403241defrun(self, args):3242iflen(args) <1:3243return False32443245if self.keepRepoPath and not self.cloneDestination:3246 sys.stderr.write("Must specify destination for --keep-path\n")3247 sys.exit(1)32483249 depotPaths = args32503251if not self.cloneDestination andlen(depotPaths) >1:3252 self.cloneDestination = depotPaths[-1]3253 depotPaths = depotPaths[:-1]32543255 self.cloneExclude = ["/"+p for p in self.cloneExclude]3256for p in depotPaths:3257if not p.startswith("//"):3258 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3259return False32603261if not self.cloneDestination:3262 self.cloneDestination = self.defaultDestination(args)32633264print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32653266if not os.path.exists(self.cloneDestination):3267 os.makedirs(self.cloneDestination)3268chdir(self.cloneDestination)32693270 init_cmd = ["git","init"]3271if self.cloneBare:3272 init_cmd.append("--bare")3273 retcode = subprocess.call(init_cmd)3274if retcode:3275raiseCalledProcessError(retcode, init_cmd)32763277if not P4Sync.run(self, depotPaths):3278return False32793280# create a master branch and check out a work tree3281ifgitBranchExists(self.branch):3282system(["git","branch","master", self.branch ])3283if not self.cloneBare:3284system(["git","checkout","-f"])3285else:3286print'Not checking out any branch, use ' \3287'"git checkout -q -b master <branch>"'32883289# auto-set this variable if invoked with --use-client-spec3290if self.useClientSpec_from_options:3291system("git config --bool git-p4.useclientspec true")32923293return True32943295classP4Branches(Command):3296def__init__(self):3297 Command.__init__(self)3298 self.options = [ ]3299 self.description = ("Shows the git branches that hold imports and their "3300+"corresponding perforce depot paths")3301 self.verbose =False33023303defrun(self, args):3304iforiginP4BranchesExist():3305createOrUpdateBranchesFromOrigin()33063307 cmdline ="git rev-parse --symbolic "3308 cmdline +=" --remotes"33093310for line inread_pipe_lines(cmdline):3311 line = line.strip()33123313if not line.startswith('p4/')or line =="p4/HEAD":3314continue3315 branch = line33163317 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3318 settings =extractSettingsGitLog(log)33193320print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3321return True33223323classHelpFormatter(optparse.IndentedHelpFormatter):3324def__init__(self):3325 optparse.IndentedHelpFormatter.__init__(self)33263327defformat_description(self, description):3328if description:3329return description +"\n"3330else:3331return""33323333defprintUsage(commands):3334print"usage:%s<command> [options]"% sys.argv[0]3335print""3336print"valid commands:%s"%", ".join(commands)3337print""3338print"Try%s<command> --help for command specific help."% sys.argv[0]3339print""33403341commands = {3342"debug": P4Debug,3343"submit": P4Submit,3344"commit": P4Submit,3345"sync": P4Sync,3346"rebase": P4Rebase,3347"clone": P4Clone,3348"rollback": P4RollBack,3349"branches": P4Branches3350}335133523353defmain():3354iflen(sys.argv[1:]) ==0:3355printUsage(commands.keys())3356 sys.exit(2)33573358 cmdName = sys.argv[1]3359try:3360 klass = commands[cmdName]3361 cmd =klass()3362exceptKeyError:3363print"unknown command%s"% cmdName3364print""3365printUsage(commands.keys())3366 sys.exit(2)33673368 options = cmd.options3369 cmd.gitdir = os.environ.get("GIT_DIR",None)33703371 args = sys.argv[2:]33723373 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3374if cmd.needsGit:3375 options.append(optparse.make_option("--git-dir", dest="gitdir"))33763377 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3378 options,3379 description = cmd.description,3380 formatter =HelpFormatter())33813382(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3383global verbose3384 verbose = cmd.verbose3385if cmd.needsGit:3386if cmd.gitdir ==None:3387 cmd.gitdir = os.path.abspath(".git")3388if notisValidGitDir(cmd.gitdir):3389 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3390if os.path.exists(cmd.gitdir):3391 cdup =read_pipe("git rev-parse --show-cdup").strip()3392iflen(cdup) >0:3393chdir(cdup);33943395if notisValidGitDir(cmd.gitdir):3396ifisValidGitDir(cmd.gitdir +"/.git"):3397 cmd.gitdir +="/.git"3398else:3399die("fatal: cannot locate git repository at%s"% cmd.gitdir)34003401 os.environ["GIT_DIR"] = cmd.gitdir34023403if not cmd.run(args):3404 parser.print_help()3405 sys.exit(2)340634073408if __name__ =='__main__':3409main()