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, ignore_error=False): 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 and not ignore_error: 201raiseCalledProcessError(retcode, cmd) 202 203return retcode 204 205defp4_system(cmd): 206"""Specifically invoke p4 as the system command. """ 207 real_cmd =p4_build_cmd(cmd) 208 expand =isinstance(real_cmd, basestring) 209 retcode = subprocess.call(real_cmd, shell=expand) 210if retcode: 211raiseCalledProcessError(retcode, real_cmd) 212 213_p4_version_string =None 214defp4_version_string(): 215"""Read the version string, showing just the last line, which 216 hopefully is the interesting version bit. 217 218 $ p4 -V 219 Perforce - The Fast Software Configuration Management System. 220 Copyright 1995-2011 Perforce Software. All rights reserved. 221 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 222 """ 223global _p4_version_string 224if not _p4_version_string: 225 a =p4_read_pipe_lines(["-V"]) 226 _p4_version_string = a[-1].rstrip() 227return _p4_version_string 228 229defp4_integrate(src, dest): 230p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 231 232defp4_sync(f, *options): 233p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 234 235defp4_add(f): 236# forcibly add file names with wildcards 237ifwildcard_present(f): 238p4_system(["add","-f", f]) 239else: 240p4_system(["add", f]) 241 242defp4_delete(f): 243p4_system(["delete",wildcard_encode(f)]) 244 245defp4_edit(f): 246p4_system(["edit",wildcard_encode(f)]) 247 248defp4_revert(f): 249p4_system(["revert",wildcard_encode(f)]) 250 251defp4_reopen(type, f): 252p4_system(["reopen","-t",type,wildcard_encode(f)]) 253 254defp4_move(src, dest): 255p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 256 257defp4_last_change(): 258 results =p4CmdList(["changes","-m","1"]) 259returnint(results[0]['change']) 260 261defp4_describe(change): 262"""Make sure it returns a valid result by checking for 263 the presence of field "time". Return a dict of the 264 results.""" 265 266 ds =p4CmdList(["describe","-s",str(change)]) 267iflen(ds) !=1: 268die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 269 270 d = ds[0] 271 272if"p4ExitCode"in d: 273die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 274str(d))) 275if"code"in d: 276if d["code"] =="error": 277die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 278 279if"time"not in d: 280die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 281 282return d 283 284# 285# Canonicalize the p4 type and return a tuple of the 286# base type, plus any modifiers. See "p4 help filetypes" 287# for a list and explanation. 288# 289defsplit_p4_type(p4type): 290 291 p4_filetypes_historical = { 292"ctempobj":"binary+Sw", 293"ctext":"text+C", 294"cxtext":"text+Cx", 295"ktext":"text+k", 296"kxtext":"text+kx", 297"ltext":"text+F", 298"tempobj":"binary+FSw", 299"ubinary":"binary+F", 300"uresource":"resource+F", 301"uxbinary":"binary+Fx", 302"xbinary":"binary+x", 303"xltext":"text+Fx", 304"xtempobj":"binary+Swx", 305"xtext":"text+x", 306"xunicode":"unicode+x", 307"xutf16":"utf16+x", 308} 309if p4type in p4_filetypes_historical: 310 p4type = p4_filetypes_historical[p4type] 311 mods ="" 312 s = p4type.split("+") 313 base = s[0] 314 mods ="" 315iflen(s) >1: 316 mods = s[1] 317return(base, mods) 318 319# 320# return the raw p4 type of a file (text, text+ko, etc) 321# 322defp4_type(f): 323 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 324return results[0]['headType'] 325 326# 327# Given a type base and modifier, return a regexp matching 328# the keywords that can be expanded in the file 329# 330defp4_keywords_regexp_for_type(base, type_mods): 331if base in("text","unicode","binary"): 332 kwords =None 333if"ko"in type_mods: 334 kwords ='Id|Header' 335elif"k"in type_mods: 336 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 337else: 338return None 339 pattern = r""" 340 \$ # Starts with a dollar, followed by... 341 (%s) # one of the keywords, followed by... 342 (:[^$\n]+)? # possibly an old expansion, followed by... 343 \$ # another dollar 344 """% kwords 345return pattern 346else: 347return None 348 349# 350# Given a file, return a regexp matching the possible 351# RCS keywords that will be expanded, or None for files 352# with kw expansion turned off. 353# 354defp4_keywords_regexp_for_file(file): 355if not os.path.exists(file): 356return None 357else: 358(type_base, type_mods) =split_p4_type(p4_type(file)) 359returnp4_keywords_regexp_for_type(type_base, type_mods) 360 361defsetP4ExecBit(file, mode): 362# Reopens an already open file and changes the execute bit to match 363# the execute bit setting in the passed in mode. 364 365 p4Type ="+x" 366 367if notisModeExec(mode): 368 p4Type =getP4OpenedType(file) 369 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 370 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 371if p4Type[-1] =="+": 372 p4Type = p4Type[0:-1] 373 374p4_reopen(p4Type,file) 375 376defgetP4OpenedType(file): 377# Returns the perforce file type for the given file. 378 379 result =p4_read_pipe(["opened",wildcard_encode(file)]) 380 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 381if match: 382return match.group(1) 383else: 384die("Could not determine file type for%s(result: '%s')"% (file, result)) 385 386# Return the set of all p4 labels 387defgetP4Labels(depotPaths): 388 labels =set() 389ifisinstance(depotPaths,basestring): 390 depotPaths = [depotPaths] 391 392for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 393 label = l['label'] 394 labels.add(label) 395 396return labels 397 398# Return the set of all git tags 399defgetGitTags(): 400 gitTags =set() 401for line inread_pipe_lines(["git","tag"]): 402 tag = line.strip() 403 gitTags.add(tag) 404return gitTags 405 406defdiffTreePattern(): 407# This is a simple generator for the diff tree regex pattern. This could be 408# a class variable if this and parseDiffTreeEntry were a part of a class. 409 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 410while True: 411yield pattern 412 413defparseDiffTreeEntry(entry): 414"""Parses a single diff tree entry into its component elements. 415 416 See git-diff-tree(1) manpage for details about the format of the diff 417 output. This method returns a dictionary with the following elements: 418 419 src_mode - The mode of the source file 420 dst_mode - The mode of the destination file 421 src_sha1 - The sha1 for the source file 422 dst_sha1 - The sha1 fr the destination file 423 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 424 status_score - The score for the status (applicable for 'C' and 'R' 425 statuses). This is None if there is no score. 426 src - The path for the source file. 427 dst - The path for the destination file. This is only present for 428 copy or renames. If it is not present, this is None. 429 430 If the pattern is not matched, None is returned.""" 431 432 match =diffTreePattern().next().match(entry) 433if match: 434return{ 435'src_mode': match.group(1), 436'dst_mode': match.group(2), 437'src_sha1': match.group(3), 438'dst_sha1': match.group(4), 439'status': match.group(5), 440'status_score': match.group(6), 441'src': match.group(7), 442'dst': match.group(10) 443} 444return None 445 446defisModeExec(mode): 447# Returns True if the given git mode represents an executable file, 448# otherwise False. 449return mode[-3:] =="755" 450 451defisModeExecChanged(src_mode, dst_mode): 452returnisModeExec(src_mode) !=isModeExec(dst_mode) 453 454defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 455 456ifisinstance(cmd,basestring): 457 cmd ="-G "+ cmd 458 expand =True 459else: 460 cmd = ["-G"] + cmd 461 expand =False 462 463 cmd =p4_build_cmd(cmd) 464if verbose: 465 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 466 467# Use a temporary file to avoid deadlocks without 468# subprocess.communicate(), which would put another copy 469# of stdout into memory. 470 stdin_file =None 471if stdin is not None: 472 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 473ifisinstance(stdin,basestring): 474 stdin_file.write(stdin) 475else: 476for i in stdin: 477 stdin_file.write(i +'\n') 478 stdin_file.flush() 479 stdin_file.seek(0) 480 481 p4 = subprocess.Popen(cmd, 482 shell=expand, 483 stdin=stdin_file, 484 stdout=subprocess.PIPE) 485 486 result = [] 487try: 488while True: 489 entry = marshal.load(p4.stdout) 490if cb is not None: 491cb(entry) 492else: 493 result.append(entry) 494exceptEOFError: 495pass 496 exitCode = p4.wait() 497if exitCode !=0: 498 entry = {} 499 entry["p4ExitCode"] = exitCode 500 result.append(entry) 501 502return result 503 504defp4Cmd(cmd): 505list=p4CmdList(cmd) 506 result = {} 507for entry inlist: 508 result.update(entry) 509return result; 510 511defp4Where(depotPath): 512if not depotPath.endswith("/"): 513 depotPath +="/" 514 depotPathLong = depotPath +"..." 515 outputList =p4CmdList(["where", depotPathLong]) 516 output =None 517for entry in outputList: 518if"depotFile"in entry: 519# Search for the base client side depot path, as long as it starts with the branch's P4 path. 520# The base path always ends with "/...". 521if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 522 output = entry 523break 524elif"data"in entry: 525 data = entry.get("data") 526 space = data.find(" ") 527if data[:space] == depotPath: 528 output = entry 529break 530if output ==None: 531return"" 532if output["code"] =="error": 533return"" 534 clientPath ="" 535if"path"in output: 536 clientPath = output.get("path") 537elif"data"in output: 538 data = output.get("data") 539 lastSpace = data.rfind(" ") 540 clientPath = data[lastSpace +1:] 541 542if clientPath.endswith("..."): 543 clientPath = clientPath[:-3] 544return clientPath 545 546defcurrentGitBranch(): 547 retcode =system(["git","symbolic-ref","-q","HEAD"], ignore_error=True) 548if retcode !=0: 549# on a detached head 550return None 551else: 552returnread_pipe(["git","name-rev","HEAD"]).split(" ")[1].strip() 553 554defisValidGitDir(path): 555if(os.path.exists(path +"/HEAD") 556and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 557return True; 558return False 559 560defparseRevision(ref): 561returnread_pipe("git rev-parse%s"% ref).strip() 562 563defbranchExists(ref): 564 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 565 ignore_error=True) 566returnlen(rev) >0 567 568defextractLogMessageFromGitCommit(commit): 569 logMessage ="" 570 571## fixme: title is first line of commit, not 1st paragraph. 572 foundTitle =False 573for log inread_pipe_lines("git cat-file commit%s"% commit): 574if not foundTitle: 575iflen(log) ==1: 576 foundTitle =True 577continue 578 579 logMessage += log 580return logMessage 581 582defextractSettingsGitLog(log): 583 values = {} 584for line in log.split("\n"): 585 line = line.strip() 586 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 587if not m: 588continue 589 590 assignments = m.group(1).split(':') 591for a in assignments: 592 vals = a.split('=') 593 key = vals[0].strip() 594 val = ('='.join(vals[1:])).strip() 595if val.endswith('\"')and val.startswith('"'): 596 val = val[1:-1] 597 598 values[key] = val 599 600 paths = values.get("depot-paths") 601if not paths: 602 paths = values.get("depot-path") 603if paths: 604 values['depot-paths'] = paths.split(',') 605return values 606 607defgitBranchExists(branch): 608 proc = subprocess.Popen(["git","rev-parse", branch], 609 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 610return proc.wait() ==0; 611 612_gitConfig = {} 613 614defgitConfig(key): 615if not _gitConfig.has_key(key): 616 cmd = ["git","config", key ] 617 s =read_pipe(cmd, ignore_error=True) 618 _gitConfig[key] = s.strip() 619return _gitConfig[key] 620 621defgitConfigBool(key): 622"""Return a bool, using git config --bool. It is True only if the 623 variable is set to true, and False if set to false or not present 624 in the config.""" 625 626if not _gitConfig.has_key(key): 627 cmd = ["git","config","--bool", key ] 628 s =read_pipe(cmd, ignore_error=True) 629 v = s.strip() 630 _gitConfig[key] = v =="true" 631return _gitConfig[key] 632 633defgitConfigList(key): 634if not _gitConfig.has_key(key): 635 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 636 _gitConfig[key] = s.strip().split(os.linesep) 637return _gitConfig[key] 638 639defp4BranchesInGit(branchesAreInRemotes=True): 640"""Find all the branches whose names start with "p4/", looking 641 in remotes or heads as specified by the argument. Return 642 a dictionary of{ branch: revision }for each one found. 643 The branch names are the short names, without any 644 "p4/" prefix.""" 645 646 branches = {} 647 648 cmdline ="git rev-parse --symbolic " 649if branchesAreInRemotes: 650 cmdline +="--remotes" 651else: 652 cmdline +="--branches" 653 654for line inread_pipe_lines(cmdline): 655 line = line.strip() 656 657# only import to p4/ 658if not line.startswith('p4/'): 659continue 660# special symbolic ref to p4/master 661if line =="p4/HEAD": 662continue 663 664# strip off p4/ prefix 665 branch = line[len("p4/"):] 666 667 branches[branch] =parseRevision(line) 668 669return branches 670 671defbranch_exists(branch): 672"""Make sure that the given ref name really exists.""" 673 674 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 675 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 676 out, _ = p.communicate() 677if p.returncode: 678return False 679# expect exactly one line of output: the branch name 680return out.rstrip() == branch 681 682deffindUpstreamBranchPoint(head ="HEAD"): 683 branches =p4BranchesInGit() 684# map from depot-path to branch name 685 branchByDepotPath = {} 686for branch in branches.keys(): 687 tip = branches[branch] 688 log =extractLogMessageFromGitCommit(tip) 689 settings =extractSettingsGitLog(log) 690if settings.has_key("depot-paths"): 691 paths =",".join(settings["depot-paths"]) 692 branchByDepotPath[paths] ="remotes/p4/"+ branch 693 694 settings =None 695 parent =0 696while parent <65535: 697 commit = head +"~%s"% parent 698 log =extractLogMessageFromGitCommit(commit) 699 settings =extractSettingsGitLog(log) 700if settings.has_key("depot-paths"): 701 paths =",".join(settings["depot-paths"]) 702if branchByDepotPath.has_key(paths): 703return[branchByDepotPath[paths], settings] 704 705 parent = parent +1 706 707return["", settings] 708 709defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 710if not silent: 711print("Creating/updating branch(es) in%sbased on origin branch(es)" 712% localRefPrefix) 713 714 originPrefix ="origin/p4/" 715 716for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 717 line = line.strip() 718if(not line.startswith(originPrefix))or line.endswith("HEAD"): 719continue 720 721 headName = line[len(originPrefix):] 722 remoteHead = localRefPrefix + headName 723 originHead = line 724 725 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 726if(not original.has_key('depot-paths') 727or not original.has_key('change')): 728continue 729 730 update =False 731if notgitBranchExists(remoteHead): 732if verbose: 733print"creating%s"% remoteHead 734 update =True 735else: 736 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 737if settings.has_key('change') >0: 738if settings['depot-paths'] == original['depot-paths']: 739 originP4Change =int(original['change']) 740 p4Change =int(settings['change']) 741if originP4Change > p4Change: 742print("%s(%s) is newer than%s(%s). " 743"Updating p4 branch from origin." 744% (originHead, originP4Change, 745 remoteHead, p4Change)) 746 update =True 747else: 748print("Ignoring:%swas imported from%swhile " 749"%swas imported from%s" 750% (originHead,','.join(original['depot-paths']), 751 remoteHead,','.join(settings['depot-paths']))) 752 753if update: 754system("git update-ref%s %s"% (remoteHead, originHead)) 755 756deforiginP4BranchesExist(): 757returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 758 759 760defp4ParseNumericChangeRange(parts): 761 changeStart =int(parts[0][1:]) 762if parts[1] =='#head': 763 changeEnd =p4_last_change() 764else: 765 changeEnd =int(parts[1]) 766 767return(changeStart, changeEnd) 768 769defchooseBlockSize(blockSize): 770if blockSize: 771return blockSize 772else: 773return defaultBlockSize 774 775defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 776assert depotPaths 777 778# Parse the change range into start and end. Try to find integer 779# revision ranges as these can be broken up into blocks to avoid 780# hitting server-side limits (maxrows, maxscanresults). But if 781# that doesn't work, fall back to using the raw revision specifier 782# strings, without using block mode. 783 784if changeRange is None or changeRange =='': 785 changeStart =1 786 changeEnd =p4_last_change() 787 block_size =chooseBlockSize(requestedBlockSize) 788else: 789 parts = changeRange.split(',') 790assertlen(parts) ==2 791try: 792(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 793 block_size =chooseBlockSize(requestedBlockSize) 794except: 795 changeStart = parts[0][1:] 796 changeEnd = parts[1] 797if requestedBlockSize: 798die("cannot use --changes-block-size with non-numeric revisions") 799 block_size =None 800 801# Accumulate change numbers in a dictionary to avoid duplicates 802 changes = {} 803 804for p in depotPaths: 805# Retrieve changes a block at a time, to prevent running 806# into a MaxResults/MaxScanRows error from the server. 807 808while True: 809 cmd = ['changes'] 810 811if block_size: 812 end =min(changeEnd, changeStart + block_size) 813 revisionRange ="%d,%d"% (changeStart, end) 814else: 815 revisionRange ="%s,%s"% (changeStart, changeEnd) 816 817 cmd += ["%s...@%s"% (p, revisionRange)] 818 819for line inp4_read_pipe_lines(cmd): 820 changeNum =int(line.split(" ")[1]) 821 changes[changeNum] =True 822 823if not block_size: 824break 825 826if end >= changeEnd: 827break 828 829 changeStart = end +1 830 831 changelist = changes.keys() 832 changelist.sort() 833return changelist 834 835defp4PathStartsWith(path, prefix): 836# This method tries to remedy a potential mixed-case issue: 837# 838# If UserA adds //depot/DirA/file1 839# and UserB adds //depot/dira/file2 840# 841# we may or may not have a problem. If you have core.ignorecase=true, 842# we treat DirA and dira as the same directory 843ifgitConfigBool("core.ignorecase"): 844return path.lower().startswith(prefix.lower()) 845return path.startswith(prefix) 846 847defgetClientSpec(): 848"""Look at the p4 client spec, create a View() object that contains 849 all the mappings, and return it.""" 850 851 specList =p4CmdList("client -o") 852iflen(specList) !=1: 853die('Output from "client -o" is%dlines, expecting 1'% 854len(specList)) 855 856# dictionary of all client parameters 857 entry = specList[0] 858 859# the //client/ name 860 client_name = entry["Client"] 861 862# just the keys that start with "View" 863 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 864 865# hold this new View 866 view =View(client_name) 867 868# append the lines, in order, to the view 869for view_num inrange(len(view_keys)): 870 k ="View%d"% view_num 871if k not in view_keys: 872die("Expected view key%smissing"% k) 873 view.append(entry[k]) 874 875return view 876 877defgetClientRoot(): 878"""Grab the client directory.""" 879 880 output =p4CmdList("client -o") 881iflen(output) !=1: 882die('Output from "client -o" is%dlines, expecting 1'%len(output)) 883 884 entry = output[0] 885if"Root"not in entry: 886die('Client has no "Root"') 887 888return entry["Root"] 889 890# 891# P4 wildcards are not allowed in filenames. P4 complains 892# if you simply add them, but you can force it with "-f", in 893# which case it translates them into %xx encoding internally. 894# 895defwildcard_decode(path): 896# Search for and fix just these four characters. Do % last so 897# that fixing it does not inadvertently create new %-escapes. 898# Cannot have * in a filename in windows; untested as to 899# what p4 would do in such a case. 900if not platform.system() =="Windows": 901 path = path.replace("%2A","*") 902 path = path.replace("%23","#") \ 903.replace("%40","@") \ 904.replace("%25","%") 905return path 906 907defwildcard_encode(path): 908# do % first to avoid double-encoding the %s introduced here 909 path = path.replace("%","%25") \ 910.replace("*","%2A") \ 911.replace("#","%23") \ 912.replace("@","%40") 913return path 914 915defwildcard_present(path): 916 m = re.search("[*#@%]", path) 917return m is not None 918 919class Command: 920def__init__(self): 921 self.usage ="usage: %prog [options]" 922 self.needsGit =True 923 self.verbose =False 924 925class P4UserMap: 926def__init__(self): 927 self.userMapFromPerforceServer =False 928 self.myP4UserId =None 929 930defp4UserId(self): 931if self.myP4UserId: 932return self.myP4UserId 933 934 results =p4CmdList("user -o") 935for r in results: 936if r.has_key('User'): 937 self.myP4UserId = r['User'] 938return r['User'] 939die("Could not find your p4 user id") 940 941defp4UserIsMe(self, p4User): 942# return True if the given p4 user is actually me 943 me = self.p4UserId() 944if not p4User or p4User != me: 945return False 946else: 947return True 948 949defgetUserCacheFilename(self): 950 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 951return home +"/.gitp4-usercache.txt" 952 953defgetUserMapFromPerforceServer(self): 954if self.userMapFromPerforceServer: 955return 956 self.users = {} 957 self.emails = {} 958 959for output inp4CmdList("users"): 960if not output.has_key("User"): 961continue 962 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 963 self.emails[output["Email"]] = output["User"] 964 965 966 s ='' 967for(key, val)in self.users.items(): 968 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 969 970open(self.getUserCacheFilename(),"wb").write(s) 971 self.userMapFromPerforceServer =True 972 973defloadUserMapFromCache(self): 974 self.users = {} 975 self.userMapFromPerforceServer =False 976try: 977 cache =open(self.getUserCacheFilename(),"rb") 978 lines = cache.readlines() 979 cache.close() 980for line in lines: 981 entry = line.strip().split("\t") 982 self.users[entry[0]] = entry[1] 983exceptIOError: 984 self.getUserMapFromPerforceServer() 985 986classP4Debug(Command): 987def__init__(self): 988 Command.__init__(self) 989 self.options = [] 990 self.description ="A tool to debug the output of p4 -G." 991 self.needsGit =False 992 993defrun(self, args): 994 j =0 995for output inp4CmdList(args): 996print'Element:%d'% j 997 j +=1 998print output 999return True10001001classP4RollBack(Command):1002def__init__(self):1003 Command.__init__(self)1004 self.options = [1005 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1006]1007 self.description ="A tool to debug the multi-branch import. Don't use :)"1008 self.rollbackLocalBranches =False10091010defrun(self, args):1011iflen(args) !=1:1012return False1013 maxChange =int(args[0])10141015if"p4ExitCode"inp4Cmd("changes -m 1"):1016die("Problems executing p4");10171018if self.rollbackLocalBranches:1019 refPrefix ="refs/heads/"1020 lines =read_pipe_lines("git rev-parse --symbolic --branches")1021else:1022 refPrefix ="refs/remotes/"1023 lines =read_pipe_lines("git rev-parse --symbolic --remotes")10241025for line in lines:1026if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1027 line = line.strip()1028 ref = refPrefix + line1029 log =extractLogMessageFromGitCommit(ref)1030 settings =extractSettingsGitLog(log)10311032 depotPaths = settings['depot-paths']1033 change = settings['change']10341035 changed =False10361037iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1038for p in depotPaths]))) ==0:1039print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1040system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1041continue10421043while change andint(change) > maxChange:1044 changed =True1045if self.verbose:1046print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1047system("git update-ref%s\"%s^\""% (ref, ref))1048 log =extractLogMessageFromGitCommit(ref)1049 settings =extractSettingsGitLog(log)105010511052 depotPaths = settings['depot-paths']1053 change = settings['change']10541055if changed:1056print"%srewound to%s"% (ref, change)10571058return True10591060classP4Submit(Command, P4UserMap):10611062 conflict_behavior_choices = ("ask","skip","quit")10631064def__init__(self):1065 Command.__init__(self)1066 P4UserMap.__init__(self)1067 self.options = [1068 optparse.make_option("--origin", dest="origin"),1069 optparse.make_option("-M", dest="detectRenames", action="store_true"),1070# preserve the user, requires relevant p4 permissions1071 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1072 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1073 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1074 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1075 optparse.make_option("--conflict", dest="conflict_behavior",1076 choices=self.conflict_behavior_choices),1077 optparse.make_option("--branch", dest="branch"),1078]1079 self.description ="Submit changes from git to the perforce depot."1080 self.usage +=" [name of git branch to submit into perforce depot]"1081 self.origin =""1082 self.detectRenames =False1083 self.preserveUser =gitConfigBool("git-p4.preserveUser")1084 self.dry_run =False1085 self.prepare_p4_only =False1086 self.conflict_behavior =None1087 self.isWindows = (platform.system() =="Windows")1088 self.exportLabels =False1089 self.p4HasMoveCommand =p4_has_move_command()1090 self.branch =None10911092defcheck(self):1093iflen(p4CmdList("opened ...")) >0:1094die("You have files opened with perforce! Close them before starting the sync.")10951096defseparate_jobs_from_description(self, message):1097"""Extract and return a possible Jobs field in the commit1098 message. It goes into a separate section in the p4 change1099 specification.11001101 A jobs line starts with "Jobs:" and looks like a new field1102 in a form. Values are white-space separated on the same1103 line or on following lines that start with a tab.11041105 This does not parse and extract the full git commit message1106 like a p4 form. It just sees the Jobs: line as a marker1107 to pass everything from then on directly into the p4 form,1108 but outside the description section.11091110 Return a tuple (stripped log message, jobs string)."""11111112 m = re.search(r'^Jobs:', message, re.MULTILINE)1113if m is None:1114return(message,None)11151116 jobtext = message[m.start():]1117 stripped_message = message[:m.start()].rstrip()1118return(stripped_message, jobtext)11191120defprepareLogMessage(self, template, message, jobs):1121"""Edits the template returned from "p4 change -o" to insert1122 the message in the Description field, and the jobs text in1123 the Jobs field."""1124 result =""11251126 inDescriptionSection =False11271128for line in template.split("\n"):1129if line.startswith("#"):1130 result += line +"\n"1131continue11321133if inDescriptionSection:1134if line.startswith("Files:")or line.startswith("Jobs:"):1135 inDescriptionSection =False1136# insert Jobs section1137if jobs:1138 result += jobs +"\n"1139else:1140continue1141else:1142if line.startswith("Description:"):1143 inDescriptionSection =True1144 line +="\n"1145for messageLine in message.split("\n"):1146 line +="\t"+ messageLine +"\n"11471148 result += line +"\n"11491150return result11511152defpatchRCSKeywords(self,file, pattern):1153# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1154(handle, outFileName) = tempfile.mkstemp(dir='.')1155try:1156 outFile = os.fdopen(handle,"w+")1157 inFile =open(file,"r")1158 regexp = re.compile(pattern, re.VERBOSE)1159for line in inFile.readlines():1160 line = regexp.sub(r'$\1$', line)1161 outFile.write(line)1162 inFile.close()1163 outFile.close()1164# Forcibly overwrite the original file1165 os.unlink(file)1166 shutil.move(outFileName,file)1167except:1168# cleanup our temporary file1169 os.unlink(outFileName)1170print"Failed to strip RCS keywords in%s"%file1171raise11721173print"Patched up RCS keywords in%s"%file11741175defp4UserForCommit(self,id):1176# Return the tuple (perforce user,git email) for a given git commit id1177 self.getUserMapFromPerforceServer()1178 gitEmail =read_pipe(["git","log","--max-count=1",1179"--format=%ae",id])1180 gitEmail = gitEmail.strip()1181if not self.emails.has_key(gitEmail):1182return(None,gitEmail)1183else:1184return(self.emails[gitEmail],gitEmail)11851186defcheckValidP4Users(self,commits):1187# check if any git authors cannot be mapped to p4 users1188foridin commits:1189(user,email) = self.p4UserForCommit(id)1190if not user:1191 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1192ifgitConfigBool("git-p4.allowMissingP4Users"):1193print"%s"% msg1194else:1195die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11961197deflastP4Changelist(self):1198# Get back the last changelist number submitted in this client spec. This1199# then gets used to patch up the username in the change. If the same1200# client spec is being used by multiple processes then this might go1201# wrong.1202 results =p4CmdList("client -o")# find the current client1203 client =None1204for r in results:1205if r.has_key('Client'):1206 client = r['Client']1207break1208if not client:1209die("could not get client spec")1210 results =p4CmdList(["changes","-c", client,"-m","1"])1211for r in results:1212if r.has_key('change'):1213return r['change']1214die("Could not get changelist number for last submit - cannot patch up user details")12151216defmodifyChangelistUser(self, changelist, newUser):1217# fixup the user field of a changelist after it has been submitted.1218 changes =p4CmdList("change -o%s"% changelist)1219iflen(changes) !=1:1220die("Bad output from p4 change modifying%sto user%s"%1221(changelist, newUser))12221223 c = changes[0]1224if c['User'] == newUser:return# nothing to do1225 c['User'] = newUser1226input= marshal.dumps(c)12271228 result =p4CmdList("change -f -i", stdin=input)1229for r in result:1230if r.has_key('code'):1231if r['code'] =='error':1232die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1233if r.has_key('data'):1234print("Updated user field for changelist%sto%s"% (changelist, newUser))1235return1236die("Could not modify user field of changelist%sto%s"% (changelist, newUser))12371238defcanChangeChangelists(self):1239# check to see if we have p4 admin or super-user permissions, either of1240# which are required to modify changelists.1241 results =p4CmdList(["protects", self.depotPath])1242for r in results:1243if r.has_key('perm'):1244if r['perm'] =='admin':1245return11246if r['perm'] =='super':1247return11248return012491250defprepareSubmitTemplate(self):1251"""Run "p4 change -o" to grab a change specification template.1252 This does not use "p4 -G", as it is nice to keep the submission1253 template in original order, since a human might edit it.12541255 Remove lines in the Files section that show changes to files1256 outside the depot path we're committing into."""12571258 template =""1259 inFilesSection =False1260for line inp4_read_pipe_lines(['change','-o']):1261if line.endswith("\r\n"):1262 line = line[:-2] +"\n"1263if inFilesSection:1264if line.startswith("\t"):1265# path starts and ends with a tab1266 path = line[1:]1267 lastTab = path.rfind("\t")1268if lastTab != -1:1269 path = path[:lastTab]1270if notp4PathStartsWith(path, self.depotPath):1271continue1272else:1273 inFilesSection =False1274else:1275if line.startswith("Files:"):1276 inFilesSection =True12771278 template += line12791280return template12811282defedit_template(self, template_file):1283"""Invoke the editor to let the user change the submission1284 message. Return true if okay to continue with the submit."""12851286# if configured to skip the editing part, just submit1287ifgitConfigBool("git-p4.skipSubmitEdit"):1288return True12891290# look at the modification time, to check later if the user saved1291# the file1292 mtime = os.stat(template_file).st_mtime12931294# invoke the editor1295if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1296 editor = os.environ.get("P4EDITOR")1297else:1298 editor =read_pipe("git var GIT_EDITOR").strip()1299system(["sh","-c", ('%s"$@"'% editor), editor, template_file])13001301# If the file was not saved, prompt to see if this patch should1302# be skipped. But skip this verification step if configured so.1303ifgitConfigBool("git-p4.skipSubmitEditCheck"):1304return True13051306# modification time updated means user saved the file1307if os.stat(template_file).st_mtime > mtime:1308return True13091310while True:1311 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1312if response =='y':1313return True1314if response =='n':1315return False13161317defget_diff_description(self, editedFiles, filesToAdd):1318# diff1319if os.environ.has_key("P4DIFF"):1320del(os.environ["P4DIFF"])1321 diff =""1322for editedFile in editedFiles:1323 diff +=p4_read_pipe(['diff','-du',1324wildcard_encode(editedFile)])13251326# new file diff1327 newdiff =""1328for newFile in filesToAdd:1329 newdiff +="==== new file ====\n"1330 newdiff +="--- /dev/null\n"1331 newdiff +="+++%s\n"% newFile1332 f =open(newFile,"r")1333for line in f.readlines():1334 newdiff +="+"+ line1335 f.close()13361337return(diff + newdiff).replace('\r\n','\n')13381339defapplyCommit(self,id):1340"""Apply one commit, return True if it succeeded."""13411342print"Applying",read_pipe(["git","show","-s",1343"--format=format:%h%s",id])13441345(p4User, gitEmail) = self.p4UserForCommit(id)13461347 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1348 filesToAdd =set()1349 filesToDelete =set()1350 editedFiles =set()1351 pureRenameCopy =set()1352 filesToChangeExecBit = {}13531354for line in diff:1355 diff =parseDiffTreeEntry(line)1356 modifier = diff['status']1357 path = diff['src']1358if modifier =="M":1359p4_edit(path)1360ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1361 filesToChangeExecBit[path] = diff['dst_mode']1362 editedFiles.add(path)1363elif modifier =="A":1364 filesToAdd.add(path)1365 filesToChangeExecBit[path] = diff['dst_mode']1366if path in filesToDelete:1367 filesToDelete.remove(path)1368elif modifier =="D":1369 filesToDelete.add(path)1370if path in filesToAdd:1371 filesToAdd.remove(path)1372elif modifier =="C":1373 src, dest = diff['src'], diff['dst']1374p4_integrate(src, dest)1375 pureRenameCopy.add(dest)1376if diff['src_sha1'] != diff['dst_sha1']:1377p4_edit(dest)1378 pureRenameCopy.discard(dest)1379ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1380p4_edit(dest)1381 pureRenameCopy.discard(dest)1382 filesToChangeExecBit[dest] = diff['dst_mode']1383if self.isWindows:1384# turn off read-only attribute1385 os.chmod(dest, stat.S_IWRITE)1386 os.unlink(dest)1387 editedFiles.add(dest)1388elif modifier =="R":1389 src, dest = diff['src'], diff['dst']1390if self.p4HasMoveCommand:1391p4_edit(src)# src must be open before move1392p4_move(src, dest)# opens for (move/delete, move/add)1393else:1394p4_integrate(src, dest)1395if diff['src_sha1'] != diff['dst_sha1']:1396p4_edit(dest)1397else:1398 pureRenameCopy.add(dest)1399ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1400if not self.p4HasMoveCommand:1401p4_edit(dest)# with move: already open, writable1402 filesToChangeExecBit[dest] = diff['dst_mode']1403if not self.p4HasMoveCommand:1404if self.isWindows:1405 os.chmod(dest, stat.S_IWRITE)1406 os.unlink(dest)1407 filesToDelete.add(src)1408 editedFiles.add(dest)1409else:1410die("unknown modifier%sfor%s"% (modifier, path))14111412 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1413 patchcmd = diffcmd +" | git apply "1414 tryPatchCmd = patchcmd +"--check -"1415 applyPatchCmd = patchcmd +"--check --apply -"1416 patch_succeeded =True14171418if os.system(tryPatchCmd) !=0:1419 fixed_rcs_keywords =False1420 patch_succeeded =False1421print"Unfortunately applying the change failed!"14221423# Patch failed, maybe it's just RCS keyword woes. Look through1424# the patch to see if that's possible.1425ifgitConfigBool("git-p4.attemptRCSCleanup"):1426file=None1427 pattern =None1428 kwfiles = {}1429forfilein editedFiles | filesToDelete:1430# did this file's delta contain RCS keywords?1431 pattern =p4_keywords_regexp_for_file(file)14321433if pattern:1434# this file is a possibility...look for RCS keywords.1435 regexp = re.compile(pattern, re.VERBOSE)1436for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1437if regexp.search(line):1438if verbose:1439print"got keyword match on%sin%sin%s"% (pattern, line,file)1440 kwfiles[file] = pattern1441break14421443forfilein kwfiles:1444if verbose:1445print"zapping%swith%s"% (line,pattern)1446# File is being deleted, so not open in p4. Must1447# disable the read-only bit on windows.1448if self.isWindows andfilenot in editedFiles:1449 os.chmod(file, stat.S_IWRITE)1450 self.patchRCSKeywords(file, kwfiles[file])1451 fixed_rcs_keywords =True14521453if fixed_rcs_keywords:1454print"Retrying the patch with RCS keywords cleaned up"1455if os.system(tryPatchCmd) ==0:1456 patch_succeeded =True14571458if not patch_succeeded:1459for f in editedFiles:1460p4_revert(f)1461return False14621463#1464# Apply the patch for real, and do add/delete/+x handling.1465#1466system(applyPatchCmd)14671468for f in filesToAdd:1469p4_add(f)1470for f in filesToDelete:1471p4_revert(f)1472p4_delete(f)14731474# Set/clear executable bits1475for f in filesToChangeExecBit.keys():1476 mode = filesToChangeExecBit[f]1477setP4ExecBit(f, mode)14781479#1480# Build p4 change description, starting with the contents1481# of the git commit message.1482#1483 logMessage =extractLogMessageFromGitCommit(id)1484 logMessage = logMessage.strip()1485(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14861487 template = self.prepareSubmitTemplate()1488 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14891490if self.preserveUser:1491 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14921493if self.checkAuthorship and not self.p4UserIsMe(p4User):1494 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1495 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1496 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"14971498 separatorLine ="######## everything below this line is just the diff #######\n"1499if not self.prepare_p4_only:1500 submitTemplate += separatorLine1501 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)15021503(handle, fileName) = tempfile.mkstemp()1504 tmpFile = os.fdopen(handle,"w+b")1505if self.isWindows:1506 submitTemplate = submitTemplate.replace("\n","\r\n")1507 tmpFile.write(submitTemplate)1508 tmpFile.close()15091510if self.prepare_p4_only:1511#1512# Leave the p4 tree prepared, and the submit template around1513# and let the user decide what to do next1514#1515print1516print"P4 workspace prepared for submission."1517print"To submit or revert, go to client workspace"1518print" "+ self.clientPath1519print1520print"To submit, use\"p4 submit\"to write a new description,"1521print"or\"p4 submit -i <%s\"to use the one prepared by" \1522"\"git p4\"."% fileName1523print"You can delete the file\"%s\"when finished."% fileName15241525if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1526print"To preserve change ownership by user%s, you must\n" \1527"do\"p4 change -f <change>\"after submitting and\n" \1528"edit the User field."1529if pureRenameCopy:1530print"After submitting, renamed files must be re-synced."1531print"Invoke\"p4 sync -f\"on each of these files:"1532for f in pureRenameCopy:1533print" "+ f15341535print1536print"To revert the changes, use\"p4 revert ...\", and delete"1537print"the submit template file\"%s\""% fileName1538if filesToAdd:1539print"Since the commit adds new files, they must be deleted:"1540for f in filesToAdd:1541print" "+ f1542print1543return True15441545#1546# Let the user edit the change description, then submit it.1547#1548if self.edit_template(fileName):1549# read the edited message and submit1550 ret =True1551 tmpFile =open(fileName,"rb")1552 message = tmpFile.read()1553 tmpFile.close()1554if self.isWindows:1555 message = message.replace("\r\n","\n")1556 submitTemplate = message[:message.index(separatorLine)]1557p4_write_pipe(['submit','-i'], submitTemplate)15581559if self.preserveUser:1560if p4User:1561# Get last changelist number. Cannot easily get it from1562# the submit command output as the output is1563# unmarshalled.1564 changelist = self.lastP4Changelist()1565 self.modifyChangelistUser(changelist, p4User)15661567# The rename/copy happened by applying a patch that created a1568# new file. This leaves it writable, which confuses p4.1569for f in pureRenameCopy:1570p4_sync(f,"-f")15711572else:1573# skip this patch1574 ret =False1575print"Submission cancelled, undoing p4 changes."1576for f in editedFiles:1577p4_revert(f)1578for f in filesToAdd:1579p4_revert(f)1580 os.remove(f)1581for f in filesToDelete:1582p4_revert(f)15831584 os.remove(fileName)1585return ret15861587# Export git tags as p4 labels. Create a p4 label and then tag1588# with that.1589defexportGitTags(self, gitTags):1590 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1591iflen(validLabelRegexp) ==0:1592 validLabelRegexp = defaultLabelRegexp1593 m = re.compile(validLabelRegexp)15941595for name in gitTags:15961597if not m.match(name):1598if verbose:1599print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1600continue16011602# Get the p4 commit this corresponds to1603 logMessage =extractLogMessageFromGitCommit(name)1604 values =extractSettingsGitLog(logMessage)16051606if not values.has_key('change'):1607# a tag pointing to something not sent to p4; ignore1608if verbose:1609print"git tag%sdoes not give a p4 commit"% name1610continue1611else:1612 changelist = values['change']16131614# Get the tag details.1615 inHeader =True1616 isAnnotated =False1617 body = []1618for l inread_pipe_lines(["git","cat-file","-p", name]):1619 l = l.strip()1620if inHeader:1621if re.match(r'tag\s+', l):1622 isAnnotated =True1623elif re.match(r'\s*$', l):1624 inHeader =False1625continue1626else:1627 body.append(l)16281629if not isAnnotated:1630 body = ["lightweight tag imported by git p4\n"]16311632# Create the label - use the same view as the client spec we are using1633 clientSpec =getClientSpec()16341635 labelTemplate ="Label:%s\n"% name1636 labelTemplate +="Description:\n"1637for b in body:1638 labelTemplate +="\t"+ b +"\n"1639 labelTemplate +="View:\n"1640for depot_side in clientSpec.mappings:1641 labelTemplate +="\t%s\n"% depot_side16421643if self.dry_run:1644print"Would create p4 label%sfor tag"% name1645elif self.prepare_p4_only:1646print"Not creating p4 label%sfor tag due to option" \1647" --prepare-p4-only"% name1648else:1649p4_write_pipe(["label","-i"], labelTemplate)16501651# Use the label1652p4_system(["tag","-l", name] +1653["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16541655if verbose:1656print"created p4 label for tag%s"% name16571658defrun(self, args):1659iflen(args) ==0:1660 self.master =currentGitBranch()1661eliflen(args) ==1:1662 self.master = args[0]1663if notbranchExists(self.master):1664die("Branch%sdoes not exist"% self.master)1665else:1666return False16671668if self.master:1669 allowSubmit =gitConfig("git-p4.allowSubmit")1670iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1671die("%sis not in git-p4.allowSubmit"% self.master)16721673[upstream, settings] =findUpstreamBranchPoint()1674 self.depotPath = settings['depot-paths'][0]1675iflen(self.origin) ==0:1676 self.origin = upstream16771678if self.preserveUser:1679if not self.canChangeChangelists():1680die("Cannot preserve user names without p4 super-user or admin permissions")16811682# if not set from the command line, try the config file1683if self.conflict_behavior is None:1684 val =gitConfig("git-p4.conflict")1685if val:1686if val not in self.conflict_behavior_choices:1687die("Invalid value '%s' for config git-p4.conflict"% val)1688else:1689 val ="ask"1690 self.conflict_behavior = val16911692if self.verbose:1693print"Origin branch is "+ self.origin16941695iflen(self.depotPath) ==0:1696print"Internal error: cannot locate perforce depot path from existing branches"1697 sys.exit(128)16981699 self.useClientSpec =False1700ifgitConfigBool("git-p4.useclientspec"):1701 self.useClientSpec =True1702if self.useClientSpec:1703 self.clientSpecDirs =getClientSpec()17041705# Check for the existance of P4 branches1706 branchesDetected = (len(p4BranchesInGit().keys()) >1)17071708if self.useClientSpec and not branchesDetected:1709# all files are relative to the client spec1710 self.clientPath =getClientRoot()1711else:1712 self.clientPath =p4Where(self.depotPath)17131714if self.clientPath =="":1715die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)17161717print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1718 self.oldWorkingDirectory = os.getcwd()17191720# ensure the clientPath exists1721 new_client_dir =False1722if not os.path.exists(self.clientPath):1723 new_client_dir =True1724 os.makedirs(self.clientPath)17251726chdir(self.clientPath, is_client_path=True)1727if self.dry_run:1728print"Would synchronize p4 checkout in%s"% self.clientPath1729else:1730print"Synchronizing p4 checkout..."1731if new_client_dir:1732# old one was destroyed, and maybe nobody told p41733p4_sync("...","-f")1734else:1735p4_sync("...")1736 self.check()17371738 commits = []1739if self.master:1740 commitish = self.master1741else:1742 commitish ='HEAD'17431744for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):1745 commits.append(line.strip())1746 commits.reverse()17471748if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1749 self.checkAuthorship =False1750else:1751 self.checkAuthorship =True17521753if self.preserveUser:1754 self.checkValidP4Users(commits)17551756#1757# Build up a set of options to be passed to diff when1758# submitting each commit to p4.1759#1760if self.detectRenames:1761# command-line -M arg1762 self.diffOpts ="-M"1763else:1764# If not explicitly set check the config variable1765 detectRenames =gitConfig("git-p4.detectRenames")17661767if detectRenames.lower() =="false"or detectRenames =="":1768 self.diffOpts =""1769elif detectRenames.lower() =="true":1770 self.diffOpts ="-M"1771else:1772 self.diffOpts ="-M%s"% detectRenames17731774# no command-line arg for -C or --find-copies-harder, just1775# config variables1776 detectCopies =gitConfig("git-p4.detectCopies")1777if detectCopies.lower() =="false"or detectCopies =="":1778pass1779elif detectCopies.lower() =="true":1780 self.diffOpts +=" -C"1781else:1782 self.diffOpts +=" -C%s"% detectCopies17831784ifgitConfigBool("git-p4.detectCopiesHarder"):1785 self.diffOpts +=" --find-copies-harder"17861787#1788# Apply the commits, one at a time. On failure, ask if should1789# continue to try the rest of the patches, or quit.1790#1791if self.dry_run:1792print"Would apply"1793 applied = []1794 last =len(commits) -11795for i, commit inenumerate(commits):1796if self.dry_run:1797print" ",read_pipe(["git","show","-s",1798"--format=format:%h%s", commit])1799 ok =True1800else:1801 ok = self.applyCommit(commit)1802if ok:1803 applied.append(commit)1804else:1805if self.prepare_p4_only and i < last:1806print"Processing only the first commit due to option" \1807" --prepare-p4-only"1808break1809if i < last:1810 quit =False1811while True:1812# prompt for what to do, or use the option/variable1813if self.conflict_behavior =="ask":1814print"What do you want to do?"1815 response =raw_input("[s]kip this commit but apply"1816" the rest, or [q]uit? ")1817if not response:1818continue1819elif self.conflict_behavior =="skip":1820 response ="s"1821elif self.conflict_behavior =="quit":1822 response ="q"1823else:1824die("Unknown conflict_behavior '%s'"%1825 self.conflict_behavior)18261827if response[0] =="s":1828print"Skipping this commit, but applying the rest"1829break1830if response[0] =="q":1831print"Quitting"1832 quit =True1833break1834if quit:1835break18361837chdir(self.oldWorkingDirectory)18381839if self.dry_run:1840pass1841elif self.prepare_p4_only:1842pass1843eliflen(commits) ==len(applied):1844print"All commits applied!"18451846 sync =P4Sync()1847if self.branch:1848 sync.branch = self.branch1849 sync.run([])18501851 rebase =P4Rebase()1852 rebase.rebase()18531854else:1855iflen(applied) ==0:1856print"No commits applied."1857else:1858print"Applied only the commits marked with '*':"1859for c in commits:1860if c in applied:1861 star ="*"1862else:1863 star =" "1864print star,read_pipe(["git","show","-s",1865"--format=format:%h%s", c])1866print"You will have to do 'git p4 sync' and rebase."18671868ifgitConfigBool("git-p4.exportLabels"):1869 self.exportLabels =True18701871if self.exportLabels:1872 p4Labels =getP4Labels(self.depotPath)1873 gitTags =getGitTags()18741875 missingGitTags = gitTags - p4Labels1876 self.exportGitTags(missingGitTags)18771878# exit with error unless everything applied perfectly1879iflen(commits) !=len(applied):1880 sys.exit(1)18811882return True18831884classView(object):1885"""Represent a p4 view ("p4 help views"), and map files in a1886 repo according to the view."""18871888def__init__(self, client_name):1889 self.mappings = []1890 self.client_prefix ="//%s/"% client_name1891# cache results of "p4 where" to lookup client file locations1892 self.client_spec_path_cache = {}18931894defappend(self, view_line):1895"""Parse a view line, splitting it into depot and client1896 sides. Append to self.mappings, preserving order. This1897 is only needed for tag creation."""18981899# Split the view line into exactly two words. P4 enforces1900# structure on these lines that simplifies this quite a bit.1901#1902# Either or both words may be double-quoted.1903# Single quotes do not matter.1904# Double-quote marks cannot occur inside the words.1905# A + or - prefix is also inside the quotes.1906# There are no quotes unless they contain a space.1907# The line is already white-space stripped.1908# The two words are separated by a single space.1909#1910if view_line[0] =='"':1911# First word is double quoted. Find its end.1912 close_quote_index = view_line.find('"',1)1913if close_quote_index <=0:1914die("No first-word closing quote found:%s"% view_line)1915 depot_side = view_line[1:close_quote_index]1916# skip closing quote and space1917 rhs_index = close_quote_index +1+11918else:1919 space_index = view_line.find(" ")1920if space_index <=0:1921die("No word-splitting space found:%s"% view_line)1922 depot_side = view_line[0:space_index]1923 rhs_index = space_index +119241925# prefix + means overlay on previous mapping1926if depot_side.startswith("+"):1927 depot_side = depot_side[1:]19281929# prefix - means exclude this path, leave out of mappings1930 exclude =False1931if depot_side.startswith("-"):1932 exclude =True1933 depot_side = depot_side[1:]19341935if not exclude:1936 self.mappings.append(depot_side)19371938defconvert_client_path(self, clientFile):1939# chop off //client/ part to make it relative1940if not clientFile.startswith(self.client_prefix):1941die("No prefix '%s' on clientFile '%s'"%1942(self.client_prefix, clientFile))1943return clientFile[len(self.client_prefix):]19441945defupdate_client_spec_path_cache(self, files):1946""" Caching file paths by "p4 where" batch query """19471948# List depot file paths exclude that already cached1949 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]19501951iflen(fileArgs) ==0:1952return# All files in cache19531954 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1955for res in where_result:1956if"code"in res and res["code"] =="error":1957# assume error is "... file(s) not in client view"1958continue1959if"clientFile"not in res:1960die("No clientFile in 'p4 where' output")1961if"unmap"in res:1962# it will list all of them, but only one not unmap-ped1963continue1964ifgitConfigBool("core.ignorecase"):1965 res['depotFile'] = res['depotFile'].lower()1966 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19671968# not found files or unmap files set to ""1969for depotFile in fileArgs:1970ifgitConfigBool("core.ignorecase"):1971 depotFile = depotFile.lower()1972if depotFile not in self.client_spec_path_cache:1973 self.client_spec_path_cache[depotFile] =""19741975defmap_in_client(self, depot_path):1976"""Return the relative location in the client where this1977 depot file should live. Returns "" if the file should1978 not be mapped in the client."""19791980ifgitConfigBool("core.ignorecase"):1981 depot_path = depot_path.lower()19821983if depot_path in self.client_spec_path_cache:1984return self.client_spec_path_cache[depot_path]19851986die("Error:%sis not found in client spec path"% depot_path )1987return""19881989classP4Sync(Command, P4UserMap):1990 delete_actions = ("delete","move/delete","purge")19911992def__init__(self):1993 Command.__init__(self)1994 P4UserMap.__init__(self)1995 self.options = [1996 optparse.make_option("--branch", dest="branch"),1997 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1998 optparse.make_option("--changesfile", dest="changesFile"),1999 optparse.make_option("--silent", dest="silent", action="store_true"),2000 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2001 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2002 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2003help="Import into refs/heads/ , not refs/remotes"),2004 optparse.make_option("--max-changes", dest="maxChanges",2005help="Maximum number of changes to import"),2006 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2007help="Internal block size to use when iteratively calling p4 changes"),2008 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2009help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2010 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2011help="Only sync files that are included in the Perforce Client Spec"),2012 optparse.make_option("-/", dest="cloneExclude",2013 action="append",type="string",2014help="exclude depot path"),2015]2016 self.description ="""Imports from Perforce into a git repository.\n2017 example:2018 //depot/my/project/ -- to import the current head2019 //depot/my/project/@all -- to import everything2020 //depot/my/project/@1,6 -- to import only from revision 1 to 620212022 (a ... is not needed in the path p4 specification, it's added implicitly)"""20232024 self.usage +=" //depot/path[@revRange]"2025 self.silent =False2026 self.createdBranches =set()2027 self.committedChanges =set()2028 self.branch =""2029 self.detectBranches =False2030 self.detectLabels =False2031 self.importLabels =False2032 self.changesFile =""2033 self.syncWithOrigin =True2034 self.importIntoRemotes =True2035 self.maxChanges =""2036 self.changes_block_size =None2037 self.keepRepoPath =False2038 self.depotPaths =None2039 self.p4BranchesInGit = []2040 self.cloneExclude = []2041 self.useClientSpec =False2042 self.useClientSpec_from_options =False2043 self.clientSpecDirs =None2044 self.tempBranches = []2045 self.tempBranchLocation ="git-p4-tmp"20462047ifgitConfig("git-p4.syncFromOrigin") =="false":2048 self.syncWithOrigin =False20492050# This is required for the "append" cloneExclude action2051defensure_value(self, attr, value):2052if nothasattr(self, attr)orgetattr(self, attr)is None:2053setattr(self, attr, value)2054returngetattr(self, attr)20552056# Force a checkpoint in fast-import and wait for it to finish2057defcheckpoint(self):2058 self.gitStream.write("checkpoint\n\n")2059 self.gitStream.write("progress checkpoint\n\n")2060 out = self.gitOutput.readline()2061if self.verbose:2062print"checkpoint finished: "+ out20632064defextractFilesFromCommit(self, commit):2065 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2066for path in self.cloneExclude]2067 files = []2068 fnum =02069while commit.has_key("depotFile%s"% fnum):2070 path = commit["depotFile%s"% fnum]20712072if[p for p in self.cloneExclude2073ifp4PathStartsWith(path, p)]:2074 found =False2075else:2076 found = [p for p in self.depotPaths2077ifp4PathStartsWith(path, p)]2078if not found:2079 fnum = fnum +12080continue20812082file= {}2083file["path"] = path2084file["rev"] = commit["rev%s"% fnum]2085file["action"] = commit["action%s"% fnum]2086file["type"] = commit["type%s"% fnum]2087 files.append(file)2088 fnum = fnum +12089return files20902091defstripRepoPath(self, path, prefixes):2092"""When streaming files, this is called to map a p4 depot path2093 to where it should go in git. The prefixes are either2094 self.depotPaths, or self.branchPrefixes in the case of2095 branch detection."""20962097if self.useClientSpec:2098# branch detection moves files up a level (the branch name)2099# from what client spec interpretation gives2100 path = self.clientSpecDirs.map_in_client(path)2101if self.detectBranches:2102for b in self.knownBranches:2103if path.startswith(b +"/"):2104 path = path[len(b)+1:]21052106elif self.keepRepoPath:2107# Preserve everything in relative path name except leading2108# //depot/; just look at first prefix as they all should2109# be in the same depot.2110 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2111ifp4PathStartsWith(path, depot):2112 path = path[len(depot):]21132114else:2115for p in prefixes:2116ifp4PathStartsWith(path, p):2117 path = path[len(p):]2118break21192120 path =wildcard_decode(path)2121return path21222123defsplitFilesIntoBranches(self, commit):2124"""Look at each depotFile in the commit to figure out to what2125 branch it belongs."""21262127if self.clientSpecDirs:2128 files = self.extractFilesFromCommit(commit)2129 self.clientSpecDirs.update_client_spec_path_cache(files)21302131 branches = {}2132 fnum =02133while commit.has_key("depotFile%s"% fnum):2134 path = commit["depotFile%s"% fnum]2135 found = [p for p in self.depotPaths2136ifp4PathStartsWith(path, p)]2137if not found:2138 fnum = fnum +12139continue21402141file= {}2142file["path"] = path2143file["rev"] = commit["rev%s"% fnum]2144file["action"] = commit["action%s"% fnum]2145file["type"] = commit["type%s"% fnum]2146 fnum = fnum +121472148# start with the full relative path where this file would2149# go in a p4 client2150if self.useClientSpec:2151 relPath = self.clientSpecDirs.map_in_client(path)2152else:2153 relPath = self.stripRepoPath(path, self.depotPaths)21542155for branch in self.knownBranches.keys():2156# add a trailing slash so that a commit into qt/4.2foo2157# doesn't end up in qt/4.2, e.g.2158if relPath.startswith(branch +"/"):2159if branch not in branches:2160 branches[branch] = []2161 branches[branch].append(file)2162break21632164return branches21652166# output one file from the P4 stream2167# - helper for streamP4Files21682169defstreamOneP4File(self,file, contents):2170 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2171if verbose:2172 sys.stderr.write("%s\n"% relPath)21732174(type_base, type_mods) =split_p4_type(file["type"])21752176 git_mode ="100644"2177if"x"in type_mods:2178 git_mode ="100755"2179if type_base =="symlink":2180 git_mode ="120000"2181# p4 print on a symlink sometimes contains "target\n";2182# if it does, remove the newline2183 data =''.join(contents)2184if not data:2185# Some version of p4 allowed creating a symlink that pointed2186# to nothing. This causes p4 errors when checking out such2187# a change, and errors here too. Work around it by ignoring2188# the bad symlink; hopefully a future change fixes it.2189print"\nIgnoring empty symlink in%s"%file['depotFile']2190return2191elif data[-1] =='\n':2192 contents = [data[:-1]]2193else:2194 contents = [data]21952196if type_base =="utf16":2197# p4 delivers different text in the python output to -G2198# than it does when using "print -o", or normal p4 client2199# operations. utf16 is converted to ascii or utf8, perhaps.2200# But ascii text saved as -t utf16 is completely mangled.2201# Invoke print -o to get the real contents.2202#2203# On windows, the newlines will always be mangled by print, so put2204# them back too. This is not needed to the cygwin windows version,2205# just the native "NT" type.2206#2207 text =p4_read_pipe(['print','-q','-o','-',"%s@%s"% (file['depotFile'],file['change']) ])2208ifp4_version_string().find("/NT") >=0:2209 text = text.replace("\r\n","\n")2210 contents = [ text ]22112212if type_base =="apple":2213# Apple filetype files will be streamed as a concatenation of2214# its appledouble header and the contents. This is useless2215# on both macs and non-macs. If using "print -q -o xx", it2216# will create "xx" with the data, and "%xx" with the header.2217# This is also not very useful.2218#2219# Ideally, someday, this script can learn how to generate2220# appledouble files directly and import those to git, but2221# non-mac machines can never find a use for apple filetype.2222print"\nIgnoring apple filetype file%s"%file['depotFile']2223return22242225# Note that we do not try to de-mangle keywords on utf16 files,2226# even though in theory somebody may want that.2227 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2228if pattern:2229 regexp = re.compile(pattern, re.VERBOSE)2230 text =''.join(contents)2231 text = regexp.sub(r'$\1$', text)2232 contents = [ text ]22332234 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22352236# total length...2237 length =02238for d in contents:2239 length = length +len(d)22402241 self.gitStream.write("data%d\n"% length)2242for d in contents:2243 self.gitStream.write(d)2244 self.gitStream.write("\n")22452246defstreamOneP4Deletion(self,file):2247 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2248if verbose:2249 sys.stderr.write("delete%s\n"% relPath)2250 self.gitStream.write("D%s\n"% relPath)22512252# handle another chunk of streaming data2253defstreamP4FilesCb(self, marshalled):22542255# catch p4 errors and complain2256 err =None2257if"code"in marshalled:2258if marshalled["code"] =="error":2259if"data"in marshalled:2260 err = marshalled["data"].rstrip()2261if err:2262 f =None2263if self.stream_have_file_info:2264if"depotFile"in self.stream_file:2265 f = self.stream_file["depotFile"]2266# force a failure in fast-import, else an empty2267# commit will be made2268 self.gitStream.write("\n")2269 self.gitStream.write("die-now\n")2270 self.gitStream.close()2271# ignore errors, but make sure it exits first2272 self.importProcess.wait()2273if f:2274die("Error from p4 print for%s:%s"% (f, err))2275else:2276die("Error from p4 print:%s"% err)22772278if marshalled.has_key('depotFile')and self.stream_have_file_info:2279# start of a new file - output the old one first2280 self.streamOneP4File(self.stream_file, self.stream_contents)2281 self.stream_file = {}2282 self.stream_contents = []2283 self.stream_have_file_info =False22842285# pick up the new file information... for the2286# 'data' field we need to append to our array2287for k in marshalled.keys():2288if k =='data':2289 self.stream_contents.append(marshalled['data'])2290else:2291 self.stream_file[k] = marshalled[k]22922293 self.stream_have_file_info =True22942295# Stream directly from "p4 files" into "git fast-import"2296defstreamP4Files(self, files):2297 filesForCommit = []2298 filesToRead = []2299 filesToDelete = []23002301for f in files:2302# if using a client spec, only add the files that have2303# a path in the client2304if self.clientSpecDirs:2305if self.clientSpecDirs.map_in_client(f['path']) =="":2306continue23072308 filesForCommit.append(f)2309if f['action']in self.delete_actions:2310 filesToDelete.append(f)2311else:2312 filesToRead.append(f)23132314# deleted files...2315for f in filesToDelete:2316 self.streamOneP4Deletion(f)23172318iflen(filesToRead) >0:2319 self.stream_file = {}2320 self.stream_contents = []2321 self.stream_have_file_info =False23222323# curry self argument2324defstreamP4FilesCbSelf(entry):2325 self.streamP4FilesCb(entry)23262327 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23282329p4CmdList(["-x","-","print"],2330 stdin=fileArgs,2331 cb=streamP4FilesCbSelf)23322333# do the last chunk2334if self.stream_file.has_key('depotFile'):2335 self.streamOneP4File(self.stream_file, self.stream_contents)23362337defmake_email(self, userid):2338if userid in self.users:2339return self.users[userid]2340else:2341return"%s<a@b>"% userid23422343# Stream a p4 tag2344defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2345if verbose:2346print"writing tag%sfor commit%s"% (labelName, commit)2347 gitStream.write("tag%s\n"% labelName)2348 gitStream.write("from%s\n"% commit)23492350if labelDetails.has_key('Owner'):2351 owner = labelDetails["Owner"]2352else:2353 owner =None23542355# Try to use the owner of the p4 label, or failing that,2356# the current p4 user id.2357if owner:2358 email = self.make_email(owner)2359else:2360 email = self.make_email(self.p4UserId())2361 tagger ="%s %s %s"% (email, epoch, self.tz)23622363 gitStream.write("tagger%s\n"% tagger)23642365print"labelDetails=",labelDetails2366if labelDetails.has_key('Description'):2367 description = labelDetails['Description']2368else:2369 description ='Label from git p4'23702371 gitStream.write("data%d\n"%len(description))2372 gitStream.write(description)2373 gitStream.write("\n")23742375defcommit(self, details, files, branch, parent =""):2376 epoch = details["time"]2377 author = details["user"]23782379if self.verbose:2380print"commit into%s"% branch23812382# start with reading files; if that fails, we should not2383# create a commit.2384 new_files = []2385for f in files:2386if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2387 new_files.append(f)2388else:2389 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23902391if self.clientSpecDirs:2392 self.clientSpecDirs.update_client_spec_path_cache(files)23932394 self.gitStream.write("commit%s\n"% branch)2395# gitStream.write("mark :%s\n" % details["change"])2396 self.committedChanges.add(int(details["change"]))2397 committer =""2398if author not in self.users:2399 self.getUserMapFromPerforceServer()2400 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)24012402 self.gitStream.write("committer%s\n"% committer)24032404 self.gitStream.write("data <<EOT\n")2405 self.gitStream.write(details["desc"])2406 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2407(','.join(self.branchPrefixes), details["change"]))2408iflen(details['options']) >0:2409 self.gitStream.write(": options =%s"% details['options'])2410 self.gitStream.write("]\nEOT\n\n")24112412iflen(parent) >0:2413if self.verbose:2414print"parent%s"% parent2415 self.gitStream.write("from%s\n"% parent)24162417 self.streamP4Files(new_files)2418 self.gitStream.write("\n")24192420 change =int(details["change"])24212422if self.labels.has_key(change):2423 label = self.labels[change]2424 labelDetails = label[0]2425 labelRevisions = label[1]2426if self.verbose:2427print"Change%sis labelled%s"% (change, labelDetails)24282429 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2430for p in self.branchPrefixes])24312432iflen(files) ==len(labelRevisions):24332434 cleanedFiles = {}2435for info in files:2436if info["action"]in self.delete_actions:2437continue2438 cleanedFiles[info["depotFile"]] = info["rev"]24392440if cleanedFiles == labelRevisions:2441 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24422443else:2444if not self.silent:2445print("Tag%sdoes not match with change%s: files do not match."2446% (labelDetails["label"], change))24472448else:2449if not self.silent:2450print("Tag%sdoes not match with change%s: file count is different."2451% (labelDetails["label"], change))24522453# Build a dictionary of changelists and labels, for "detect-labels" option.2454defgetLabels(self):2455 self.labels = {}24562457 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2458iflen(l) >0and not self.silent:2459print"Finding files belonging to labels in%s"% `self.depotPaths`24602461for output in l:2462 label = output["label"]2463 revisions = {}2464 newestChange =02465if self.verbose:2466print"Querying files for label%s"% label2467forfileinp4CmdList(["files"] +2468["%s...@%s"% (p, label)2469for p in self.depotPaths]):2470 revisions[file["depotFile"]] =file["rev"]2471 change =int(file["change"])2472if change > newestChange:2473 newestChange = change24742475 self.labels[newestChange] = [output, revisions]24762477if self.verbose:2478print"Label changes:%s"% self.labels.keys()24792480# Import p4 labels as git tags. A direct mapping does not2481# exist, so assume that if all the files are at the same revision2482# then we can use that, or it's something more complicated we should2483# just ignore.2484defimportP4Labels(self, stream, p4Labels):2485if verbose:2486print"import p4 labels: "+' '.join(p4Labels)24872488 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2489 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2490iflen(validLabelRegexp) ==0:2491 validLabelRegexp = defaultLabelRegexp2492 m = re.compile(validLabelRegexp)24932494for name in p4Labels:2495 commitFound =False24962497if not m.match(name):2498if verbose:2499print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2500continue25012502if name in ignoredP4Labels:2503continue25042505 labelDetails =p4CmdList(['label',"-o", name])[0]25062507# get the most recent changelist for each file in this label2508 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2509for p in self.depotPaths])25102511if change.has_key('change'):2512# find the corresponding git commit; take the oldest commit2513 changelist =int(change['change'])2514 gitCommit =read_pipe(["git","rev-list","--max-count=1",2515"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2516iflen(gitCommit) ==0:2517print"could not find git commit for changelist%d"% changelist2518else:2519 gitCommit = gitCommit.strip()2520 commitFound =True2521# Convert from p4 time format2522try:2523 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2524exceptValueError:2525print"Could not convert label time%s"% labelDetails['Update']2526 tmwhen =125272528 when =int(time.mktime(tmwhen))2529 self.streamTag(stream, name, labelDetails, gitCommit, when)2530if verbose:2531print"p4 label%smapped to git commit%s"% (name, gitCommit)2532else:2533if verbose:2534print"Label%shas no changelists - possibly deleted?"% name25352536if not commitFound:2537# We can't import this label; don't try again as it will get very2538# expensive repeatedly fetching all the files for labels that will2539# never be imported. If the label is moved in the future, the2540# ignore will need to be removed manually.2541system(["git","config","--add","git-p4.ignoredP4Labels", name])25422543defguessProjectName(self):2544for p in self.depotPaths:2545if p.endswith("/"):2546 p = p[:-1]2547 p = p[p.strip().rfind("/") +1:]2548if not p.endswith("/"):2549 p +="/"2550return p25512552defgetBranchMapping(self):2553 lostAndFoundBranches =set()25542555 user =gitConfig("git-p4.branchUser")2556iflen(user) >0:2557 command ="branches -u%s"% user2558else:2559 command ="branches"25602561for info inp4CmdList(command):2562 details =p4Cmd(["branch","-o", info["branch"]])2563 viewIdx =02564while details.has_key("View%s"% viewIdx):2565 paths = details["View%s"% viewIdx].split(" ")2566 viewIdx = viewIdx +12567# require standard //depot/foo/... //depot/bar/... mapping2568iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2569continue2570 source = paths[0]2571 destination = paths[1]2572## HACK2573ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2574 source = source[len(self.depotPaths[0]):-4]2575 destination = destination[len(self.depotPaths[0]):-4]25762577if destination in self.knownBranches:2578if not self.silent:2579print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2580print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2581continue25822583 self.knownBranches[destination] = source25842585 lostAndFoundBranches.discard(destination)25862587if source not in self.knownBranches:2588 lostAndFoundBranches.add(source)25892590# Perforce does not strictly require branches to be defined, so we also2591# check git config for a branch list.2592#2593# Example of branch definition in git config file:2594# [git-p4]2595# branchList=main:branchA2596# branchList=main:branchB2597# branchList=branchA:branchC2598 configBranches =gitConfigList("git-p4.branchList")2599for branch in configBranches:2600if branch:2601(source, destination) = branch.split(":")2602 self.knownBranches[destination] = source26032604 lostAndFoundBranches.discard(destination)26052606if source not in self.knownBranches:2607 lostAndFoundBranches.add(source)260826092610for branch in lostAndFoundBranches:2611 self.knownBranches[branch] = branch26122613defgetBranchMappingFromGitBranches(self):2614 branches =p4BranchesInGit(self.importIntoRemotes)2615for branch in branches.keys():2616if branch =="master":2617 branch ="main"2618else:2619 branch = branch[len(self.projectName):]2620 self.knownBranches[branch] = branch26212622defupdateOptionDict(self, d):2623 option_keys = {}2624if self.keepRepoPath:2625 option_keys['keepRepoPath'] =126262627 d["options"] =' '.join(sorted(option_keys.keys()))26282629defreadOptions(self, d):2630 self.keepRepoPath = (d.has_key('options')2631and('keepRepoPath'in d['options']))26322633defgitRefForBranch(self, branch):2634if branch =="main":2635return self.refPrefix +"master"26362637iflen(branch) <=0:2638return branch26392640return self.refPrefix + self.projectName + branch26412642defgitCommitByP4Change(self, ref, change):2643if self.verbose:2644print"looking in ref "+ ref +" for change%susing bisect..."% change26452646 earliestCommit =""2647 latestCommit =parseRevision(ref)26482649while True:2650if self.verbose:2651print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2652 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2653iflen(next) ==0:2654if self.verbose:2655print"argh"2656return""2657 log =extractLogMessageFromGitCommit(next)2658 settings =extractSettingsGitLog(log)2659 currentChange =int(settings['change'])2660if self.verbose:2661print"current change%s"% currentChange26622663if currentChange == change:2664if self.verbose:2665print"found%s"% next2666return next26672668if currentChange < change:2669 earliestCommit ="^%s"% next2670else:2671 latestCommit ="%s"% next26722673return""26742675defimportNewBranch(self, branch, maxChange):2676# make fast-import flush all changes to disk and update the refs using the checkpoint2677# command so that we can try to find the branch parent in the git history2678 self.gitStream.write("checkpoint\n\n");2679 self.gitStream.flush();2680 branchPrefix = self.depotPaths[0] + branch +"/"2681range="@1,%s"% maxChange2682#print "prefix" + branchPrefix2683 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2684iflen(changes) <=0:2685return False2686 firstChange = changes[0]2687#print "first change in branch: %s" % firstChange2688 sourceBranch = self.knownBranches[branch]2689 sourceDepotPath = self.depotPaths[0] + sourceBranch2690 sourceRef = self.gitRefForBranch(sourceBranch)2691#print "source " + sourceBranch26922693 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2694#print "branch parent: %s" % branchParentChange2695 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2696iflen(gitParent) >0:2697 self.initialParents[self.gitRefForBranch(branch)] = gitParent2698#print "parent git commit: %s" % gitParent26992700 self.importChanges(changes)2701return True27022703defsearchParent(self, parent, branch, target):2704 parentFound =False2705for blob inread_pipe_lines(["git","rev-list","--reverse",2706"--no-merges", parent]):2707 blob = blob.strip()2708iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2709 parentFound =True2710if self.verbose:2711print"Found parent of%sin commit%s"% (branch, blob)2712break2713if parentFound:2714return blob2715else:2716return None27172718defimportChanges(self, changes):2719 cnt =12720for change in changes:2721 description =p4_describe(change)2722 self.updateOptionDict(description)27232724if not self.silent:2725 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2726 sys.stdout.flush()2727 cnt = cnt +127282729try:2730if self.detectBranches:2731 branches = self.splitFilesIntoBranches(description)2732for branch in branches.keys():2733## HACK --hwn2734 branchPrefix = self.depotPaths[0] + branch +"/"2735 self.branchPrefixes = [ branchPrefix ]27362737 parent =""27382739 filesForCommit = branches[branch]27402741if self.verbose:2742print"branch is%s"% branch27432744 self.updatedBranches.add(branch)27452746if branch not in self.createdBranches:2747 self.createdBranches.add(branch)2748 parent = self.knownBranches[branch]2749if parent == branch:2750 parent =""2751else:2752 fullBranch = self.projectName + branch2753if fullBranch not in self.p4BranchesInGit:2754if not self.silent:2755print("\nImporting new branch%s"% fullBranch);2756if self.importNewBranch(branch, change -1):2757 parent =""2758 self.p4BranchesInGit.append(fullBranch)2759if not self.silent:2760print("\nResuming with change%s"% change);27612762if self.verbose:2763print"parent determined through known branches:%s"% parent27642765 branch = self.gitRefForBranch(branch)2766 parent = self.gitRefForBranch(parent)27672768if self.verbose:2769print"looking for initial parent for%s; current parent is%s"% (branch, parent)27702771iflen(parent) ==0and branch in self.initialParents:2772 parent = self.initialParents[branch]2773del self.initialParents[branch]27742775 blob =None2776iflen(parent) >0:2777 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2778if self.verbose:2779print"Creating temporary branch: "+ tempBranch2780 self.commit(description, filesForCommit, tempBranch)2781 self.tempBranches.append(tempBranch)2782 self.checkpoint()2783 blob = self.searchParent(parent, branch, tempBranch)2784if blob:2785 self.commit(description, filesForCommit, branch, blob)2786else:2787if self.verbose:2788print"Parent of%snot found. Committing into head of%s"% (branch, parent)2789 self.commit(description, filesForCommit, branch, parent)2790else:2791 files = self.extractFilesFromCommit(description)2792 self.commit(description, files, self.branch,2793 self.initialParent)2794# only needed once, to connect to the previous commit2795 self.initialParent =""2796exceptIOError:2797print self.gitError.read()2798 sys.exit(1)27992800defimportHeadRevision(self, revision):2801print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)28022803 details = {}2804 details["user"] ="git perforce import user"2805 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2806% (' '.join(self.depotPaths), revision))2807 details["change"] = revision2808 newestRevision =028092810 fileCnt =02811 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28122813for info inp4CmdList(["files"] + fileArgs):28142815if'code'in info and info['code'] =='error':2816 sys.stderr.write("p4 returned an error:%s\n"2817% info['data'])2818if info['data'].find("must refer to client") >=0:2819 sys.stderr.write("This particular p4 error is misleading.\n")2820 sys.stderr.write("Perhaps the depot path was misspelled.\n");2821 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2822 sys.exit(1)2823if'p4ExitCode'in info:2824 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2825 sys.exit(1)282628272828 change =int(info["change"])2829if change > newestRevision:2830 newestRevision = change28312832if info["action"]in self.delete_actions:2833# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2834#fileCnt = fileCnt + 12835continue28362837for prop in["depotFile","rev","action","type"]:2838 details["%s%s"% (prop, fileCnt)] = info[prop]28392840 fileCnt = fileCnt +128412842 details["change"] = newestRevision28432844# Use time from top-most change so that all git p4 clones of2845# the same p4 repo have the same commit SHA1s.2846 res =p4_describe(newestRevision)2847 details["time"] = res["time"]28482849 self.updateOptionDict(details)2850try:2851 self.commit(details, self.extractFilesFromCommit(details), self.branch)2852exceptIOError:2853print"IO error with git fast-import. Is your git version recent enough?"2854print self.gitError.read()285528562857defrun(self, args):2858 self.depotPaths = []2859 self.changeRange =""2860 self.previousDepotPaths = []2861 self.hasOrigin =False28622863# map from branch depot path to parent branch2864 self.knownBranches = {}2865 self.initialParents = {}28662867if self.importIntoRemotes:2868 self.refPrefix ="refs/remotes/p4/"2869else:2870 self.refPrefix ="refs/heads/p4/"28712872if self.syncWithOrigin:2873 self.hasOrigin =originP4BranchesExist()2874if self.hasOrigin:2875if not self.silent:2876print'Syncing with origin first, using "git fetch origin"'2877system("git fetch origin")28782879 branch_arg_given =bool(self.branch)2880iflen(self.branch) ==0:2881 self.branch = self.refPrefix +"master"2882ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2883system("git update-ref%srefs/heads/p4"% self.branch)2884system("git branch -D p4")28852886# accept either the command-line option, or the configuration variable2887if self.useClientSpec:2888# will use this after clone to set the variable2889 self.useClientSpec_from_options =True2890else:2891ifgitConfigBool("git-p4.useclientspec"):2892 self.useClientSpec =True2893if self.useClientSpec:2894 self.clientSpecDirs =getClientSpec()28952896# TODO: should always look at previous commits,2897# merge with previous imports, if possible.2898if args == []:2899if self.hasOrigin:2900createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)29012902# branches holds mapping from branch name to sha12903 branches =p4BranchesInGit(self.importIntoRemotes)29042905# restrict to just this one, disabling detect-branches2906if branch_arg_given:2907 short = self.branch.split("/")[-1]2908if short in branches:2909 self.p4BranchesInGit = [ short ]2910else:2911 self.p4BranchesInGit = branches.keys()29122913iflen(self.p4BranchesInGit) >1:2914if not self.silent:2915print"Importing from/into multiple branches"2916 self.detectBranches =True2917for branch in branches.keys():2918 self.initialParents[self.refPrefix + branch] = \2919 branches[branch]29202921if self.verbose:2922print"branches:%s"% self.p4BranchesInGit29232924 p4Change =02925for branch in self.p4BranchesInGit:2926 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29272928 settings =extractSettingsGitLog(logMsg)29292930 self.readOptions(settings)2931if(settings.has_key('depot-paths')2932and settings.has_key('change')):2933 change =int(settings['change']) +12934 p4Change =max(p4Change, change)29352936 depotPaths =sorted(settings['depot-paths'])2937if self.previousDepotPaths == []:2938 self.previousDepotPaths = depotPaths2939else:2940 paths = []2941for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2942 prev_list = prev.split("/")2943 cur_list = cur.split("/")2944for i inrange(0,min(len(cur_list),len(prev_list))):2945if cur_list[i] <> prev_list[i]:2946 i = i -12947break29482949 paths.append("/".join(cur_list[:i +1]))29502951 self.previousDepotPaths = paths29522953if p4Change >0:2954 self.depotPaths =sorted(self.previousDepotPaths)2955 self.changeRange ="@%s,#head"% p4Change2956if not self.silent and not self.detectBranches:2957print"Performing incremental import into%sgit branch"% self.branch29582959# accept multiple ref name abbreviations:2960# refs/foo/bar/branch -> use it exactly2961# p4/branch -> prepend refs/remotes/ or refs/heads/2962# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2963if not self.branch.startswith("refs/"):2964if self.importIntoRemotes:2965 prepend ="refs/remotes/"2966else:2967 prepend ="refs/heads/"2968if not self.branch.startswith("p4/"):2969 prepend +="p4/"2970 self.branch = prepend + self.branch29712972iflen(args) ==0and self.depotPaths:2973if not self.silent:2974print"Depot paths:%s"%' '.join(self.depotPaths)2975else:2976if self.depotPaths and self.depotPaths != args:2977print("previous import used depot path%sand now%swas specified. "2978"This doesn't work!"% (' '.join(self.depotPaths),2979' '.join(args)))2980 sys.exit(1)29812982 self.depotPaths =sorted(args)29832984 revision =""2985 self.users = {}29862987# Make sure no revision specifiers are used when --changesfile2988# is specified.2989 bad_changesfile =False2990iflen(self.changesFile) >0:2991for p in self.depotPaths:2992if p.find("@") >=0or p.find("#") >=0:2993 bad_changesfile =True2994break2995if bad_changesfile:2996die("Option --changesfile is incompatible with revision specifiers")29972998 newPaths = []2999for p in self.depotPaths:3000if p.find("@") != -1:3001 atIdx = p.index("@")3002 self.changeRange = p[atIdx:]3003if self.changeRange =="@all":3004 self.changeRange =""3005elif','not in self.changeRange:3006 revision = self.changeRange3007 self.changeRange =""3008 p = p[:atIdx]3009elif p.find("#") != -1:3010 hashIdx = p.index("#")3011 revision = p[hashIdx:]3012 p = p[:hashIdx]3013elif self.previousDepotPaths == []:3014# pay attention to changesfile, if given, else import3015# the entire p4 tree at the head revision3016iflen(self.changesFile) ==0:3017 revision ="#head"30183019 p = re.sub("\.\.\.$","", p)3020if not p.endswith("/"):3021 p +="/"30223023 newPaths.append(p)30243025 self.depotPaths = newPaths30263027# --detect-branches may change this for each branch3028 self.branchPrefixes = self.depotPaths30293030 self.loadUserMapFromCache()3031 self.labels = {}3032if self.detectLabels:3033 self.getLabels();30343035if self.detectBranches:3036## FIXME - what's a P4 projectName ?3037 self.projectName = self.guessProjectName()30383039if self.hasOrigin:3040 self.getBranchMappingFromGitBranches()3041else:3042 self.getBranchMapping()3043if self.verbose:3044print"p4-git branches:%s"% self.p4BranchesInGit3045print"initial parents:%s"% self.initialParents3046for b in self.p4BranchesInGit:3047if b !="master":30483049## FIXME3050 b = b[len(self.projectName):]3051 self.createdBranches.add(b)30523053 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30543055 self.importProcess = subprocess.Popen(["git","fast-import"],3056 stdin=subprocess.PIPE,3057 stdout=subprocess.PIPE,3058 stderr=subprocess.PIPE);3059 self.gitOutput = self.importProcess.stdout3060 self.gitStream = self.importProcess.stdin3061 self.gitError = self.importProcess.stderr30623063if revision:3064 self.importHeadRevision(revision)3065else:3066 changes = []30673068iflen(self.changesFile) >0:3069 output =open(self.changesFile).readlines()3070 changeSet =set()3071for line in output:3072 changeSet.add(int(line))30733074for change in changeSet:3075 changes.append(change)30763077 changes.sort()3078else:3079# catch "git p4 sync" with no new branches, in a repo that3080# does not have any existing p4 branches3081iflen(args) ==0:3082if not self.p4BranchesInGit:3083die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30843085# The default branch is master, unless --branch is used to3086# specify something else. Make sure it exists, or complain3087# nicely about how to use --branch.3088if not self.detectBranches:3089if notbranch_exists(self.branch):3090if branch_arg_given:3091die("Error: branch%sdoes not exist."% self.branch)3092else:3093die("Error: no branch%s; perhaps specify one with --branch."%3094 self.branch)30953096if self.verbose:3097print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3098 self.changeRange)3099 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)31003101iflen(self.maxChanges) >0:3102 changes = changes[:min(int(self.maxChanges),len(changes))]31033104iflen(changes) ==0:3105if not self.silent:3106print"No changes to import!"3107else:3108if not self.silent and not self.detectBranches:3109print"Import destination:%s"% self.branch31103111 self.updatedBranches =set()31123113if not self.detectBranches:3114if args:3115# start a new branch3116 self.initialParent =""3117else:3118# build on a previous revision3119 self.initialParent =parseRevision(self.branch)31203121 self.importChanges(changes)31223123if not self.silent:3124print""3125iflen(self.updatedBranches) >0:3126 sys.stdout.write("Updated branches: ")3127for b in self.updatedBranches:3128 sys.stdout.write("%s"% b)3129 sys.stdout.write("\n")31303131ifgitConfigBool("git-p4.importLabels"):3132 self.importLabels =True31333134if self.importLabels:3135 p4Labels =getP4Labels(self.depotPaths)3136 gitTags =getGitTags()31373138 missingP4Labels = p4Labels - gitTags3139 self.importP4Labels(self.gitStream, missingP4Labels)31403141 self.gitStream.close()3142if self.importProcess.wait() !=0:3143die("fast-import failed:%s"% self.gitError.read())3144 self.gitOutput.close()3145 self.gitError.close()31463147# Cleanup temporary branches created during import3148if self.tempBranches != []:3149for branch in self.tempBranches:3150read_pipe("git update-ref -d%s"% branch)3151 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31523153# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3154# a convenient shortcut refname "p4".3155if self.importIntoRemotes:3156 head_ref = self.refPrefix +"HEAD"3157if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3158system(["git","symbolic-ref", head_ref, self.branch])31593160return True31613162classP4Rebase(Command):3163def__init__(self):3164 Command.__init__(self)3165 self.options = [3166 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3167]3168 self.importLabels =False3169 self.description = ("Fetches the latest revision from perforce and "3170+"rebases the current work (branch) against it")31713172defrun(self, args):3173 sync =P4Sync()3174 sync.importLabels = self.importLabels3175 sync.run([])31763177return self.rebase()31783179defrebase(self):3180if os.system("git update-index --refresh") !=0:3181die("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.");3182iflen(read_pipe("git diff-index HEAD --")) >0:3183die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");31843185[upstream, settings] =findUpstreamBranchPoint()3186iflen(upstream) ==0:3187die("Cannot find upstream branchpoint for rebase")31883189# the branchpoint may be p4/foo~3, so strip off the parent3190 upstream = re.sub("~[0-9]+$","", upstream)31913192print"Rebasing the current branch onto%s"% upstream3193 oldHead =read_pipe("git rev-parse HEAD").strip()3194system("git rebase%s"% upstream)3195system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3196return True31973198classP4Clone(P4Sync):3199def__init__(self):3200 P4Sync.__init__(self)3201 self.description ="Creates a new git repository and imports from Perforce into it"3202 self.usage ="usage: %prog [options] //depot/path[@revRange]"3203 self.options += [3204 optparse.make_option("--destination", dest="cloneDestination",3205 action='store', default=None,3206help="where to leave result of the clone"),3207 optparse.make_option("--bare", dest="cloneBare",3208 action="store_true", default=False),3209]3210 self.cloneDestination =None3211 self.needsGit =False3212 self.cloneBare =False32133214defdefaultDestination(self, args):3215## TODO: use common prefix of args?3216 depotPath = args[0]3217 depotDir = re.sub("(@[^@]*)$","", depotPath)3218 depotDir = re.sub("(#[^#]*)$","", depotDir)3219 depotDir = re.sub(r"\.\.\.$","", depotDir)3220 depotDir = re.sub(r"/$","", depotDir)3221return os.path.split(depotDir)[1]32223223defrun(self, args):3224iflen(args) <1:3225return False32263227if self.keepRepoPath and not self.cloneDestination:3228 sys.stderr.write("Must specify destination for --keep-path\n")3229 sys.exit(1)32303231 depotPaths = args32323233if not self.cloneDestination andlen(depotPaths) >1:3234 self.cloneDestination = depotPaths[-1]3235 depotPaths = depotPaths[:-1]32363237 self.cloneExclude = ["/"+p for p in self.cloneExclude]3238for p in depotPaths:3239if not p.startswith("//"):3240 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3241return False32423243if not self.cloneDestination:3244 self.cloneDestination = self.defaultDestination(args)32453246print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32473248if not os.path.exists(self.cloneDestination):3249 os.makedirs(self.cloneDestination)3250chdir(self.cloneDestination)32513252 init_cmd = ["git","init"]3253if self.cloneBare:3254 init_cmd.append("--bare")3255 retcode = subprocess.call(init_cmd)3256if retcode:3257raiseCalledProcessError(retcode, init_cmd)32583259if not P4Sync.run(self, depotPaths):3260return False32613262# create a master branch and check out a work tree3263ifgitBranchExists(self.branch):3264system(["git","branch","master", self.branch ])3265if not self.cloneBare:3266system(["git","checkout","-f"])3267else:3268print'Not checking out any branch, use ' \3269'"git checkout -q -b master <branch>"'32703271# auto-set this variable if invoked with --use-client-spec3272if self.useClientSpec_from_options:3273system("git config --bool git-p4.useclientspec true")32743275return True32763277classP4Branches(Command):3278def__init__(self):3279 Command.__init__(self)3280 self.options = [ ]3281 self.description = ("Shows the git branches that hold imports and their "3282+"corresponding perforce depot paths")3283 self.verbose =False32843285defrun(self, args):3286iforiginP4BranchesExist():3287createOrUpdateBranchesFromOrigin()32883289 cmdline ="git rev-parse --symbolic "3290 cmdline +=" --remotes"32913292for line inread_pipe_lines(cmdline):3293 line = line.strip()32943295if not line.startswith('p4/')or line =="p4/HEAD":3296continue3297 branch = line32983299 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3300 settings =extractSettingsGitLog(log)33013302print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3303return True33043305classHelpFormatter(optparse.IndentedHelpFormatter):3306def__init__(self):3307 optparse.IndentedHelpFormatter.__init__(self)33083309defformat_description(self, description):3310if description:3311return description +"\n"3312else:3313return""33143315defprintUsage(commands):3316print"usage:%s<command> [options]"% sys.argv[0]3317print""3318print"valid commands:%s"%", ".join(commands)3319print""3320print"Try%s<command> --help for command specific help."% sys.argv[0]3321print""33223323commands = {3324"debug": P4Debug,3325"submit": P4Submit,3326"commit": P4Submit,3327"sync": P4Sync,3328"rebase": P4Rebase,3329"clone": P4Clone,3330"rollback": P4RollBack,3331"branches": P4Branches3332}333333343335defmain():3336iflen(sys.argv[1:]) ==0:3337printUsage(commands.keys())3338 sys.exit(2)33393340 cmdName = sys.argv[1]3341try:3342 klass = commands[cmdName]3343 cmd =klass()3344exceptKeyError:3345print"unknown command%s"% cmdName3346print""3347printUsage(commands.keys())3348 sys.exit(2)33493350 options = cmd.options3351 cmd.gitdir = os.environ.get("GIT_DIR",None)33523353 args = sys.argv[2:]33543355 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3356if cmd.needsGit:3357 options.append(optparse.make_option("--git-dir", dest="gitdir"))33583359 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3360 options,3361 description = cmd.description,3362 formatter =HelpFormatter())33633364(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3365global verbose3366 verbose = cmd.verbose3367if cmd.needsGit:3368if cmd.gitdir ==None:3369 cmd.gitdir = os.path.abspath(".git")3370if notisValidGitDir(cmd.gitdir):3371 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3372if os.path.exists(cmd.gitdir):3373 cdup =read_pipe("git rev-parse --show-cdup").strip()3374iflen(cdup) >0:3375chdir(cdup);33763377if notisValidGitDir(cmd.gitdir):3378ifisValidGitDir(cmd.gitdir +"/.git"):3379 cmd.gitdir +="/.git"3380else:3381die("fatal: cannot locate git repository at%s"% cmd.gitdir)33823383 os.environ["GIT_DIR"] = cmd.gitdir33843385if not cmd.run(args):3386 parser.print_help()3387 sys.exit(2)338833893390if __name__ =='__main__':3391main()