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, stderr=subprocess.PIPE, shell=expand) 138(out, err) = p.communicate() 139if p.returncode !=0and not ignore_error: 140die('Command failed:%s\nError:%s'% (str(c), err)) 141return out 142 143defp4_read_pipe(c, ignore_error=False): 144 real_cmd =p4_build_cmd(c) 145returnread_pipe(real_cmd, ignore_error) 146 147defread_pipe_lines(c): 148if verbose: 149 sys.stderr.write('Reading pipe:%s\n'%str(c)) 150 151 expand =isinstance(c, basestring) 152 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 153 pipe = p.stdout 154 val = pipe.readlines() 155if pipe.close()or p.wait(): 156die('Command failed:%s'%str(c)) 157 158return val 159 160defp4_read_pipe_lines(c): 161"""Specifically invoke p4 on the command supplied. """ 162 real_cmd =p4_build_cmd(c) 163returnread_pipe_lines(real_cmd) 164 165defp4_has_command(cmd): 166"""Ask p4 for help on this command. If it returns an error, the 167 command does not exist in this version of p4.""" 168 real_cmd =p4_build_cmd(["help", cmd]) 169 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 170 stderr=subprocess.PIPE) 171 p.communicate() 172return p.returncode ==0 173 174defp4_has_move_command(): 175"""See if the move command exists, that it supports -k, and that 176 it has not been administratively disabled. The arguments 177 must be correct, but the filenames do not have to exist. Use 178 ones with wildcards so even if they exist, it will fail.""" 179 180if notp4_has_command("move"): 181return False 182 cmd =p4_build_cmd(["move","-k","@from","@to"]) 183 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 184(out, err) = p.communicate() 185# return code will be 1 in either case 186if err.find("Invalid option") >=0: 187return False 188if err.find("disabled") >=0: 189return False 190# assume it failed because @... was invalid changelist 191return True 192 193defsystem(cmd, ignore_error=False): 194 expand =isinstance(cmd,basestring) 195if verbose: 196 sys.stderr.write("executing%s\n"%str(cmd)) 197 retcode = subprocess.call(cmd, shell=expand) 198if retcode and not ignore_error: 199raiseCalledProcessError(retcode, cmd) 200 201return retcode 202 203defp4_system(cmd): 204"""Specifically invoke p4 as the system command. """ 205 real_cmd =p4_build_cmd(cmd) 206 expand =isinstance(real_cmd, basestring) 207 retcode = subprocess.call(real_cmd, shell=expand) 208if retcode: 209raiseCalledProcessError(retcode, real_cmd) 210 211_p4_version_string =None 212defp4_version_string(): 213"""Read the version string, showing just the last line, which 214 hopefully is the interesting version bit. 215 216 $ p4 -V 217 Perforce - The Fast Software Configuration Management System. 218 Copyright 1995-2011 Perforce Software. All rights reserved. 219 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 220 """ 221global _p4_version_string 222if not _p4_version_string: 223 a =p4_read_pipe_lines(["-V"]) 224 _p4_version_string = a[-1].rstrip() 225return _p4_version_string 226 227defp4_integrate(src, dest): 228p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 229 230defp4_sync(f, *options): 231p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 232 233defp4_add(f): 234# forcibly add file names with wildcards 235ifwildcard_present(f): 236p4_system(["add","-f", f]) 237else: 238p4_system(["add", f]) 239 240defp4_delete(f): 241p4_system(["delete",wildcard_encode(f)]) 242 243defp4_edit(f): 244p4_system(["edit",wildcard_encode(f)]) 245 246defp4_revert(f): 247p4_system(["revert",wildcard_encode(f)]) 248 249defp4_reopen(type, f): 250p4_system(["reopen","-t",type,wildcard_encode(f)]) 251 252defp4_move(src, dest): 253p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 254 255defp4_last_change(): 256 results =p4CmdList(["changes","-m","1"]) 257returnint(results[0]['change']) 258 259defp4_describe(change): 260"""Make sure it returns a valid result by checking for 261 the presence of field "time". Return a dict of the 262 results.""" 263 264 ds =p4CmdList(["describe","-s",str(change)]) 265iflen(ds) !=1: 266die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 267 268 d = ds[0] 269 270if"p4ExitCode"in d: 271die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 272str(d))) 273if"code"in d: 274if d["code"] =="error": 275die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 276 277if"time"not in d: 278die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 279 280return d 281 282# 283# Canonicalize the p4 type and return a tuple of the 284# base type, plus any modifiers. See "p4 help filetypes" 285# for a list and explanation. 286# 287defsplit_p4_type(p4type): 288 289 p4_filetypes_historical = { 290"ctempobj":"binary+Sw", 291"ctext":"text+C", 292"cxtext":"text+Cx", 293"ktext":"text+k", 294"kxtext":"text+kx", 295"ltext":"text+F", 296"tempobj":"binary+FSw", 297"ubinary":"binary+F", 298"uresource":"resource+F", 299"uxbinary":"binary+Fx", 300"xbinary":"binary+x", 301"xltext":"text+Fx", 302"xtempobj":"binary+Swx", 303"xtext":"text+x", 304"xunicode":"unicode+x", 305"xutf16":"utf16+x", 306} 307if p4type in p4_filetypes_historical: 308 p4type = p4_filetypes_historical[p4type] 309 mods ="" 310 s = p4type.split("+") 311 base = s[0] 312 mods ="" 313iflen(s) >1: 314 mods = s[1] 315return(base, mods) 316 317# 318# return the raw p4 type of a file (text, text+ko, etc) 319# 320defp4_type(f): 321 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 322return results[0]['headType'] 323 324# 325# Given a type base and modifier, return a regexp matching 326# the keywords that can be expanded in the file 327# 328defp4_keywords_regexp_for_type(base, type_mods): 329if base in("text","unicode","binary"): 330 kwords =None 331if"ko"in type_mods: 332 kwords ='Id|Header' 333elif"k"in type_mods: 334 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 335else: 336return None 337 pattern = r""" 338 \$ # Starts with a dollar, followed by... 339 (%s) # one of the keywords, followed by... 340 (:[^$\n]+)? # possibly an old expansion, followed by... 341 \$ # another dollar 342 """% kwords 343return pattern 344else: 345return None 346 347# 348# Given a file, return a regexp matching the possible 349# RCS keywords that will be expanded, or None for files 350# with kw expansion turned off. 351# 352defp4_keywords_regexp_for_file(file): 353if not os.path.exists(file): 354return None 355else: 356(type_base, type_mods) =split_p4_type(p4_type(file)) 357returnp4_keywords_regexp_for_type(type_base, type_mods) 358 359defsetP4ExecBit(file, mode): 360# Reopens an already open file and changes the execute bit to match 361# the execute bit setting in the passed in mode. 362 363 p4Type ="+x" 364 365if notisModeExec(mode): 366 p4Type =getP4OpenedType(file) 367 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 368 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 369if p4Type[-1] =="+": 370 p4Type = p4Type[0:-1] 371 372p4_reopen(p4Type,file) 373 374defgetP4OpenedType(file): 375# Returns the perforce file type for the given file. 376 377 result =p4_read_pipe(["opened",wildcard_encode(file)]) 378 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 379if match: 380return match.group(1) 381else: 382die("Could not determine file type for%s(result: '%s')"% (file, result)) 383 384# Return the set of all p4 labels 385defgetP4Labels(depotPaths): 386 labels =set() 387ifisinstance(depotPaths,basestring): 388 depotPaths = [depotPaths] 389 390for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 391 label = l['label'] 392 labels.add(label) 393 394return labels 395 396# Return the set of all git tags 397defgetGitTags(): 398 gitTags =set() 399for line inread_pipe_lines(["git","tag"]): 400 tag = line.strip() 401 gitTags.add(tag) 402return gitTags 403 404defdiffTreePattern(): 405# This is a simple generator for the diff tree regex pattern. This could be 406# a class variable if this and parseDiffTreeEntry were a part of a class. 407 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 408while True: 409yield pattern 410 411defparseDiffTreeEntry(entry): 412"""Parses a single diff tree entry into its component elements. 413 414 See git-diff-tree(1) manpage for details about the format of the diff 415 output. This method returns a dictionary with the following elements: 416 417 src_mode - The mode of the source file 418 dst_mode - The mode of the destination file 419 src_sha1 - The sha1 for the source file 420 dst_sha1 - The sha1 fr the destination file 421 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 422 status_score - The score for the status (applicable for 'C' and 'R' 423 statuses). This is None if there is no score. 424 src - The path for the source file. 425 dst - The path for the destination file. This is only present for 426 copy or renames. If it is not present, this is None. 427 428 If the pattern is not matched, None is returned.""" 429 430 match =diffTreePattern().next().match(entry) 431if match: 432return{ 433'src_mode': match.group(1), 434'dst_mode': match.group(2), 435'src_sha1': match.group(3), 436'dst_sha1': match.group(4), 437'status': match.group(5), 438'status_score': match.group(6), 439'src': match.group(7), 440'dst': match.group(10) 441} 442return None 443 444defisModeExec(mode): 445# Returns True if the given git mode represents an executable file, 446# otherwise False. 447return mode[-3:] =="755" 448 449defisModeExecChanged(src_mode, dst_mode): 450returnisModeExec(src_mode) !=isModeExec(dst_mode) 451 452defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 453 454ifisinstance(cmd,basestring): 455 cmd ="-G "+ cmd 456 expand =True 457else: 458 cmd = ["-G"] + cmd 459 expand =False 460 461 cmd =p4_build_cmd(cmd) 462if verbose: 463 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 464 465# Use a temporary file to avoid deadlocks without 466# subprocess.communicate(), which would put another copy 467# of stdout into memory. 468 stdin_file =None 469if stdin is not None: 470 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 471ifisinstance(stdin,basestring): 472 stdin_file.write(stdin) 473else: 474for i in stdin: 475 stdin_file.write(i +'\n') 476 stdin_file.flush() 477 stdin_file.seek(0) 478 479 p4 = subprocess.Popen(cmd, 480 shell=expand, 481 stdin=stdin_file, 482 stdout=subprocess.PIPE) 483 484 result = [] 485try: 486while True: 487 entry = marshal.load(p4.stdout) 488if cb is not None: 489cb(entry) 490else: 491 result.append(entry) 492exceptEOFError: 493pass 494 exitCode = p4.wait() 495if exitCode !=0: 496 entry = {} 497 entry["p4ExitCode"] = exitCode 498 result.append(entry) 499 500return result 501 502defp4Cmd(cmd): 503list=p4CmdList(cmd) 504 result = {} 505for entry inlist: 506 result.update(entry) 507return result; 508 509defp4Where(depotPath): 510if not depotPath.endswith("/"): 511 depotPath +="/" 512 depotPathLong = depotPath +"..." 513 outputList =p4CmdList(["where", depotPathLong]) 514 output =None 515for entry in outputList: 516if"depotFile"in entry: 517# Search for the base client side depot path, as long as it starts with the branch's P4 path. 518# The base path always ends with "/...". 519if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 520 output = entry 521break 522elif"data"in entry: 523 data = entry.get("data") 524 space = data.find(" ") 525if data[:space] == depotPath: 526 output = entry 527break 528if output ==None: 529return"" 530if output["code"] =="error": 531return"" 532 clientPath ="" 533if"path"in output: 534 clientPath = output.get("path") 535elif"data"in output: 536 data = output.get("data") 537 lastSpace = data.rfind(" ") 538 clientPath = data[lastSpace +1:] 539 540if clientPath.endswith("..."): 541 clientPath = clientPath[:-3] 542return clientPath 543 544defcurrentGitBranch(): 545 retcode =system(["git","symbolic-ref","-q","HEAD"], ignore_error=True) 546if retcode !=0: 547# on a detached head 548return None 549else: 550returnread_pipe(["git","name-rev","HEAD"]).split(" ")[1].strip() 551 552defisValidGitDir(path): 553if(os.path.exists(path +"/HEAD") 554and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 555return True; 556return False 557 558defparseRevision(ref): 559returnread_pipe("git rev-parse%s"% ref).strip() 560 561defbranchExists(ref): 562 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 563 ignore_error=True) 564returnlen(rev) >0 565 566defextractLogMessageFromGitCommit(commit): 567 logMessage ="" 568 569## fixme: title is first line of commit, not 1st paragraph. 570 foundTitle =False 571for log inread_pipe_lines("git cat-file commit%s"% commit): 572if not foundTitle: 573iflen(log) ==1: 574 foundTitle =True 575continue 576 577 logMessage += log 578return logMessage 579 580defextractSettingsGitLog(log): 581 values = {} 582for line in log.split("\n"): 583 line = line.strip() 584 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 585if not m: 586continue 587 588 assignments = m.group(1).split(':') 589for a in assignments: 590 vals = a.split('=') 591 key = vals[0].strip() 592 val = ('='.join(vals[1:])).strip() 593if val.endswith('\"')and val.startswith('"'): 594 val = val[1:-1] 595 596 values[key] = val 597 598 paths = values.get("depot-paths") 599if not paths: 600 paths = values.get("depot-path") 601if paths: 602 values['depot-paths'] = paths.split(',') 603return values 604 605defgitBranchExists(branch): 606 proc = subprocess.Popen(["git","rev-parse", branch], 607 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 608return proc.wait() ==0; 609 610_gitConfig = {} 611 612defgitConfig(key): 613if not _gitConfig.has_key(key): 614 cmd = ["git","config", key ] 615 s =read_pipe(cmd, ignore_error=True) 616 _gitConfig[key] = s.strip() 617return _gitConfig[key] 618 619defgitConfigBool(key): 620"""Return a bool, using git config --bool. It is True only if the 621 variable is set to true, and False if set to false or not present 622 in the config.""" 623 624if not _gitConfig.has_key(key): 625 cmd = ["git","config","--bool", key ] 626 s =read_pipe(cmd, ignore_error=True) 627 v = s.strip() 628 _gitConfig[key] = v =="true" 629return _gitConfig[key] 630 631defgitConfigList(key): 632if not _gitConfig.has_key(key): 633 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 634 _gitConfig[key] = s.strip().split(os.linesep) 635return _gitConfig[key] 636 637defp4BranchesInGit(branchesAreInRemotes=True): 638"""Find all the branches whose names start with "p4/", looking 639 in remotes or heads as specified by the argument. Return 640 a dictionary of{ branch: revision }for each one found. 641 The branch names are the short names, without any 642 "p4/" prefix.""" 643 644 branches = {} 645 646 cmdline ="git rev-parse --symbolic " 647if branchesAreInRemotes: 648 cmdline +="--remotes" 649else: 650 cmdline +="--branches" 651 652for line inread_pipe_lines(cmdline): 653 line = line.strip() 654 655# only import to p4/ 656if not line.startswith('p4/'): 657continue 658# special symbolic ref to p4/master 659if line =="p4/HEAD": 660continue 661 662# strip off p4/ prefix 663 branch = line[len("p4/"):] 664 665 branches[branch] =parseRevision(line) 666 667return branches 668 669defbranch_exists(branch): 670"""Make sure that the given ref name really exists.""" 671 672 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 673 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 674 out, _ = p.communicate() 675if p.returncode: 676return False 677# expect exactly one line of output: the branch name 678return out.rstrip() == branch 679 680deffindUpstreamBranchPoint(head ="HEAD"): 681 branches =p4BranchesInGit() 682# map from depot-path to branch name 683 branchByDepotPath = {} 684for branch in branches.keys(): 685 tip = branches[branch] 686 log =extractLogMessageFromGitCommit(tip) 687 settings =extractSettingsGitLog(log) 688if settings.has_key("depot-paths"): 689 paths =",".join(settings["depot-paths"]) 690 branchByDepotPath[paths] ="remotes/p4/"+ branch 691 692 settings =None 693 parent =0 694while parent <65535: 695 commit = head +"~%s"% parent 696 log =extractLogMessageFromGitCommit(commit) 697 settings =extractSettingsGitLog(log) 698if settings.has_key("depot-paths"): 699 paths =",".join(settings["depot-paths"]) 700if branchByDepotPath.has_key(paths): 701return[branchByDepotPath[paths], settings] 702 703 parent = parent +1 704 705return["", settings] 706 707defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 708if not silent: 709print("Creating/updating branch(es) in%sbased on origin branch(es)" 710% localRefPrefix) 711 712 originPrefix ="origin/p4/" 713 714for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 715 line = line.strip() 716if(not line.startswith(originPrefix))or line.endswith("HEAD"): 717continue 718 719 headName = line[len(originPrefix):] 720 remoteHead = localRefPrefix + headName 721 originHead = line 722 723 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 724if(not original.has_key('depot-paths') 725or not original.has_key('change')): 726continue 727 728 update =False 729if notgitBranchExists(remoteHead): 730if verbose: 731print"creating%s"% remoteHead 732 update =True 733else: 734 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 735if settings.has_key('change') >0: 736if settings['depot-paths'] == original['depot-paths']: 737 originP4Change =int(original['change']) 738 p4Change =int(settings['change']) 739if originP4Change > p4Change: 740print("%s(%s) is newer than%s(%s). " 741"Updating p4 branch from origin." 742% (originHead, originP4Change, 743 remoteHead, p4Change)) 744 update =True 745else: 746print("Ignoring:%swas imported from%swhile " 747"%swas imported from%s" 748% (originHead,','.join(original['depot-paths']), 749 remoteHead,','.join(settings['depot-paths']))) 750 751if update: 752system("git update-ref%s %s"% (remoteHead, originHead)) 753 754deforiginP4BranchesExist(): 755returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 756 757 758defp4ParseNumericChangeRange(parts): 759 changeStart =int(parts[0][1:]) 760if parts[1] =='#head': 761 changeEnd =p4_last_change() 762else: 763 changeEnd =int(parts[1]) 764 765return(changeStart, changeEnd) 766 767defchooseBlockSize(blockSize): 768if blockSize: 769return blockSize 770else: 771return defaultBlockSize 772 773defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 774assert depotPaths 775 776# Parse the change range into start and end. Try to find integer 777# revision ranges as these can be broken up into blocks to avoid 778# hitting server-side limits (maxrows, maxscanresults). But if 779# that doesn't work, fall back to using the raw revision specifier 780# strings, without using block mode. 781 782if changeRange is None or changeRange =='': 783 changeStart =1 784 changeEnd =p4_last_change() 785 block_size =chooseBlockSize(requestedBlockSize) 786else: 787 parts = changeRange.split(',') 788assertlen(parts) ==2 789try: 790(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 791 block_size =chooseBlockSize(requestedBlockSize) 792except: 793 changeStart = parts[0][1:] 794 changeEnd = parts[1] 795if requestedBlockSize: 796die("cannot use --changes-block-size with non-numeric revisions") 797 block_size =None 798 799# Accumulate change numbers in a dictionary to avoid duplicates 800 changes = {} 801 802for p in depotPaths: 803# Retrieve changes a block at a time, to prevent running 804# into a MaxResults/MaxScanRows error from the server. 805 806while True: 807 cmd = ['changes'] 808 809if block_size: 810 end =min(changeEnd, changeStart + block_size) 811 revisionRange ="%d,%d"% (changeStart, end) 812else: 813 revisionRange ="%s,%s"% (changeStart, changeEnd) 814 815 cmd += ["%s...@%s"% (p, revisionRange)] 816 817for line inp4_read_pipe_lines(cmd): 818 changeNum =int(line.split(" ")[1]) 819 changes[changeNum] =True 820 821if not block_size: 822break 823 824if end >= changeEnd: 825break 826 827 changeStart = end +1 828 829 changelist = changes.keys() 830 changelist.sort() 831return changelist 832 833defp4PathStartsWith(path, prefix): 834# This method tries to remedy a potential mixed-case issue: 835# 836# If UserA adds //depot/DirA/file1 837# and UserB adds //depot/dira/file2 838# 839# we may or may not have a problem. If you have core.ignorecase=true, 840# we treat DirA and dira as the same directory 841ifgitConfigBool("core.ignorecase"): 842return path.lower().startswith(prefix.lower()) 843return path.startswith(prefix) 844 845defgetClientSpec(): 846"""Look at the p4 client spec, create a View() object that contains 847 all the mappings, and return it.""" 848 849 specList =p4CmdList("client -o") 850iflen(specList) !=1: 851die('Output from "client -o" is%dlines, expecting 1'% 852len(specList)) 853 854# dictionary of all client parameters 855 entry = specList[0] 856 857# the //client/ name 858 client_name = entry["Client"] 859 860# just the keys that start with "View" 861 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 862 863# hold this new View 864 view =View(client_name) 865 866# append the lines, in order, to the view 867for view_num inrange(len(view_keys)): 868 k ="View%d"% view_num 869if k not in view_keys: 870die("Expected view key%smissing"% k) 871 view.append(entry[k]) 872 873return view 874 875defgetClientRoot(): 876"""Grab the client directory.""" 877 878 output =p4CmdList("client -o") 879iflen(output) !=1: 880die('Output from "client -o" is%dlines, expecting 1'%len(output)) 881 882 entry = output[0] 883if"Root"not in entry: 884die('Client has no "Root"') 885 886return entry["Root"] 887 888# 889# P4 wildcards are not allowed in filenames. P4 complains 890# if you simply add them, but you can force it with "-f", in 891# which case it translates them into %xx encoding internally. 892# 893defwildcard_decode(path): 894# Search for and fix just these four characters. Do % last so 895# that fixing it does not inadvertently create new %-escapes. 896# Cannot have * in a filename in windows; untested as to 897# what p4 would do in such a case. 898if not platform.system() =="Windows": 899 path = path.replace("%2A","*") 900 path = path.replace("%23","#") \ 901.replace("%40","@") \ 902.replace("%25","%") 903return path 904 905defwildcard_encode(path): 906# do % first to avoid double-encoding the %s introduced here 907 path = path.replace("%","%25") \ 908.replace("*","%2A") \ 909.replace("#","%23") \ 910.replace("@","%40") 911return path 912 913defwildcard_present(path): 914 m = re.search("[*#@%]", path) 915return m is not None 916 917class Command: 918def__init__(self): 919 self.usage ="usage: %prog [options]" 920 self.needsGit =True 921 self.verbose =False 922 923class P4UserMap: 924def__init__(self): 925 self.userMapFromPerforceServer =False 926 self.myP4UserId =None 927 928defp4UserId(self): 929if self.myP4UserId: 930return self.myP4UserId 931 932 results =p4CmdList("user -o") 933for r in results: 934if r.has_key('User'): 935 self.myP4UserId = r['User'] 936return r['User'] 937die("Could not find your p4 user id") 938 939defp4UserIsMe(self, p4User): 940# return True if the given p4 user is actually me 941 me = self.p4UserId() 942if not p4User or p4User != me: 943return False 944else: 945return True 946 947defgetUserCacheFilename(self): 948 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 949return home +"/.gitp4-usercache.txt" 950 951defgetUserMapFromPerforceServer(self): 952if self.userMapFromPerforceServer: 953return 954 self.users = {} 955 self.emails = {} 956 957for output inp4CmdList("users"): 958if not output.has_key("User"): 959continue 960 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 961 self.emails[output["Email"]] = output["User"] 962 963 964 s ='' 965for(key, val)in self.users.items(): 966 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 967 968open(self.getUserCacheFilename(),"wb").write(s) 969 self.userMapFromPerforceServer =True 970 971defloadUserMapFromCache(self): 972 self.users = {} 973 self.userMapFromPerforceServer =False 974try: 975 cache =open(self.getUserCacheFilename(),"rb") 976 lines = cache.readlines() 977 cache.close() 978for line in lines: 979 entry = line.strip().split("\t") 980 self.users[entry[0]] = entry[1] 981exceptIOError: 982 self.getUserMapFromPerforceServer() 983 984classP4Debug(Command): 985def__init__(self): 986 Command.__init__(self) 987 self.options = [] 988 self.description ="A tool to debug the output of p4 -G." 989 self.needsGit =False 990 991defrun(self, args): 992 j =0 993for output inp4CmdList(args): 994print'Element:%d'% j 995 j +=1 996print output 997return True 998 999classP4RollBack(Command):1000def__init__(self):1001 Command.__init__(self)1002 self.options = [1003 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1004]1005 self.description ="A tool to debug the multi-branch import. Don't use :)"1006 self.rollbackLocalBranches =False10071008defrun(self, args):1009iflen(args) !=1:1010return False1011 maxChange =int(args[0])10121013if"p4ExitCode"inp4Cmd("changes -m 1"):1014die("Problems executing p4");10151016if self.rollbackLocalBranches:1017 refPrefix ="refs/heads/"1018 lines =read_pipe_lines("git rev-parse --symbolic --branches")1019else:1020 refPrefix ="refs/remotes/"1021 lines =read_pipe_lines("git rev-parse --symbolic --remotes")10221023for line in lines:1024if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1025 line = line.strip()1026 ref = refPrefix + line1027 log =extractLogMessageFromGitCommit(ref)1028 settings =extractSettingsGitLog(log)10291030 depotPaths = settings['depot-paths']1031 change = settings['change']10321033 changed =False10341035iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1036for p in depotPaths]))) ==0:1037print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1038system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1039continue10401041while change andint(change) > maxChange:1042 changed =True1043if self.verbose:1044print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1045system("git update-ref%s\"%s^\""% (ref, ref))1046 log =extractLogMessageFromGitCommit(ref)1047 settings =extractSettingsGitLog(log)104810491050 depotPaths = settings['depot-paths']1051 change = settings['change']10521053if changed:1054print"%srewound to%s"% (ref, change)10551056return True10571058classP4Submit(Command, P4UserMap):10591060 conflict_behavior_choices = ("ask","skip","quit")10611062def__init__(self):1063 Command.__init__(self)1064 P4UserMap.__init__(self)1065 self.options = [1066 optparse.make_option("--origin", dest="origin"),1067 optparse.make_option("-M", dest="detectRenames", action="store_true"),1068# preserve the user, requires relevant p4 permissions1069 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1070 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1071 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1072 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1073 optparse.make_option("--conflict", dest="conflict_behavior",1074 choices=self.conflict_behavior_choices),1075 optparse.make_option("--branch", dest="branch"),1076]1077 self.description ="Submit changes from git to the perforce depot."1078 self.usage +=" [name of git branch to submit into perforce depot]"1079 self.origin =""1080 self.detectRenames =False1081 self.preserveUser =gitConfigBool("git-p4.preserveUser")1082 self.dry_run =False1083 self.prepare_p4_only =False1084 self.conflict_behavior =None1085 self.isWindows = (platform.system() =="Windows")1086 self.exportLabels =False1087 self.p4HasMoveCommand =p4_has_move_command()1088 self.branch =None10891090defcheck(self):1091iflen(p4CmdList("opened ...")) >0:1092die("You have files opened with perforce! Close them before starting the sync.")10931094defseparate_jobs_from_description(self, message):1095"""Extract and return a possible Jobs field in the commit1096 message. It goes into a separate section in the p4 change1097 specification.10981099 A jobs line starts with "Jobs:" and looks like a new field1100 in a form. Values are white-space separated on the same1101 line or on following lines that start with a tab.11021103 This does not parse and extract the full git commit message1104 like a p4 form. It just sees the Jobs: line as a marker1105 to pass everything from then on directly into the p4 form,1106 but outside the description section.11071108 Return a tuple (stripped log message, jobs string)."""11091110 m = re.search(r'^Jobs:', message, re.MULTILINE)1111if m is None:1112return(message,None)11131114 jobtext = message[m.start():]1115 stripped_message = message[:m.start()].rstrip()1116return(stripped_message, jobtext)11171118defprepareLogMessage(self, template, message, jobs):1119"""Edits the template returned from "p4 change -o" to insert1120 the message in the Description field, and the jobs text in1121 the Jobs field."""1122 result =""11231124 inDescriptionSection =False11251126for line in template.split("\n"):1127if line.startswith("#"):1128 result += line +"\n"1129continue11301131if inDescriptionSection:1132if line.startswith("Files:")or line.startswith("Jobs:"):1133 inDescriptionSection =False1134# insert Jobs section1135if jobs:1136 result += jobs +"\n"1137else:1138continue1139else:1140if line.startswith("Description:"):1141 inDescriptionSection =True1142 line +="\n"1143for messageLine in message.split("\n"):1144 line +="\t"+ messageLine +"\n"11451146 result += line +"\n"11471148return result11491150defpatchRCSKeywords(self,file, pattern):1151# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1152(handle, outFileName) = tempfile.mkstemp(dir='.')1153try:1154 outFile = os.fdopen(handle,"w+")1155 inFile =open(file,"r")1156 regexp = re.compile(pattern, re.VERBOSE)1157for line in inFile.readlines():1158 line = regexp.sub(r'$\1$', line)1159 outFile.write(line)1160 inFile.close()1161 outFile.close()1162# Forcibly overwrite the original file1163 os.unlink(file)1164 shutil.move(outFileName,file)1165except:1166# cleanup our temporary file1167 os.unlink(outFileName)1168print"Failed to strip RCS keywords in%s"%file1169raise11701171print"Patched up RCS keywords in%s"%file11721173defp4UserForCommit(self,id):1174# Return the tuple (perforce user,git email) for a given git commit id1175 self.getUserMapFromPerforceServer()1176 gitEmail =read_pipe(["git","log","--max-count=1",1177"--format=%ae",id])1178 gitEmail = gitEmail.strip()1179if not self.emails.has_key(gitEmail):1180return(None,gitEmail)1181else:1182return(self.emails[gitEmail],gitEmail)11831184defcheckValidP4Users(self,commits):1185# check if any git authors cannot be mapped to p4 users1186foridin commits:1187(user,email) = self.p4UserForCommit(id)1188if not user:1189 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1190ifgitConfigBool("git-p4.allowMissingP4Users"):1191print"%s"% msg1192else:1193die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11941195deflastP4Changelist(self):1196# Get back the last changelist number submitted in this client spec. This1197# then gets used to patch up the username in the change. If the same1198# client spec is being used by multiple processes then this might go1199# wrong.1200 results =p4CmdList("client -o")# find the current client1201 client =None1202for r in results:1203if r.has_key('Client'):1204 client = r['Client']1205break1206if not client:1207die("could not get client spec")1208 results =p4CmdList(["changes","-c", client,"-m","1"])1209for r in results:1210if r.has_key('change'):1211return r['change']1212die("Could not get changelist number for last submit - cannot patch up user details")12131214defmodifyChangelistUser(self, changelist, newUser):1215# fixup the user field of a changelist after it has been submitted.1216 changes =p4CmdList("change -o%s"% changelist)1217iflen(changes) !=1:1218die("Bad output from p4 change modifying%sto user%s"%1219(changelist, newUser))12201221 c = changes[0]1222if c['User'] == newUser:return# nothing to do1223 c['User'] = newUser1224input= marshal.dumps(c)12251226 result =p4CmdList("change -f -i", stdin=input)1227for r in result:1228if r.has_key('code'):1229if r['code'] =='error':1230die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1231if r.has_key('data'):1232print("Updated user field for changelist%sto%s"% (changelist, newUser))1233return1234die("Could not modify user field of changelist%sto%s"% (changelist, newUser))12351236defcanChangeChangelists(self):1237# check to see if we have p4 admin or super-user permissions, either of1238# which are required to modify changelists.1239 results =p4CmdList(["protects", self.depotPath])1240for r in results:1241if r.has_key('perm'):1242if r['perm'] =='admin':1243return11244if r['perm'] =='super':1245return11246return012471248defprepareSubmitTemplate(self):1249"""Run "p4 change -o" to grab a change specification template.1250 This does not use "p4 -G", as it is nice to keep the submission1251 template in original order, since a human might edit it.12521253 Remove lines in the Files section that show changes to files1254 outside the depot path we're committing into."""12551256 template =""1257 inFilesSection =False1258for line inp4_read_pipe_lines(['change','-o']):1259if line.endswith("\r\n"):1260 line = line[:-2] +"\n"1261if inFilesSection:1262if line.startswith("\t"):1263# path starts and ends with a tab1264 path = line[1:]1265 lastTab = path.rfind("\t")1266if lastTab != -1:1267 path = path[:lastTab]1268if notp4PathStartsWith(path, self.depotPath):1269continue1270else:1271 inFilesSection =False1272else:1273if line.startswith("Files:"):1274 inFilesSection =True12751276 template += line12771278return template12791280defedit_template(self, template_file):1281"""Invoke the editor to let the user change the submission1282 message. Return true if okay to continue with the submit."""12831284# if configured to skip the editing part, just submit1285ifgitConfigBool("git-p4.skipSubmitEdit"):1286return True12871288# look at the modification time, to check later if the user saved1289# the file1290 mtime = os.stat(template_file).st_mtime12911292# invoke the editor1293if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1294 editor = os.environ.get("P4EDITOR")1295else:1296 editor =read_pipe("git var GIT_EDITOR").strip()1297system(["sh","-c", ('%s"$@"'% editor), editor, template_file])12981299# If the file was not saved, prompt to see if this patch should1300# be skipped. But skip this verification step if configured so.1301ifgitConfigBool("git-p4.skipSubmitEditCheck"):1302return True13031304# modification time updated means user saved the file1305if os.stat(template_file).st_mtime > mtime:1306return True13071308while True:1309 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1310if response =='y':1311return True1312if response =='n':1313return False13141315defget_diff_description(self, editedFiles, filesToAdd):1316# diff1317if os.environ.has_key("P4DIFF"):1318del(os.environ["P4DIFF"])1319 diff =""1320for editedFile in editedFiles:1321 diff +=p4_read_pipe(['diff','-du',1322wildcard_encode(editedFile)])13231324# new file diff1325 newdiff =""1326for newFile in filesToAdd:1327 newdiff +="==== new file ====\n"1328 newdiff +="--- /dev/null\n"1329 newdiff +="+++%s\n"% newFile1330 f =open(newFile,"r")1331for line in f.readlines():1332 newdiff +="+"+ line1333 f.close()13341335return(diff + newdiff).replace('\r\n','\n')13361337defapplyCommit(self,id):1338"""Apply one commit, return True if it succeeded."""13391340print"Applying",read_pipe(["git","show","-s",1341"--format=format:%h%s",id])13421343(p4User, gitEmail) = self.p4UserForCommit(id)13441345 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1346 filesToAdd =set()1347 filesToDelete =set()1348 editedFiles =set()1349 pureRenameCopy =set()1350 filesToChangeExecBit = {}13511352for line in diff:1353 diff =parseDiffTreeEntry(line)1354 modifier = diff['status']1355 path = diff['src']1356if modifier =="M":1357p4_edit(path)1358ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1359 filesToChangeExecBit[path] = diff['dst_mode']1360 editedFiles.add(path)1361elif modifier =="A":1362 filesToAdd.add(path)1363 filesToChangeExecBit[path] = diff['dst_mode']1364if path in filesToDelete:1365 filesToDelete.remove(path)1366elif modifier =="D":1367 filesToDelete.add(path)1368if path in filesToAdd:1369 filesToAdd.remove(path)1370elif modifier =="C":1371 src, dest = diff['src'], diff['dst']1372p4_integrate(src, dest)1373 pureRenameCopy.add(dest)1374if diff['src_sha1'] != diff['dst_sha1']:1375p4_edit(dest)1376 pureRenameCopy.discard(dest)1377ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1378p4_edit(dest)1379 pureRenameCopy.discard(dest)1380 filesToChangeExecBit[dest] = diff['dst_mode']1381if self.isWindows:1382# turn off read-only attribute1383 os.chmod(dest, stat.S_IWRITE)1384 os.unlink(dest)1385 editedFiles.add(dest)1386elif modifier =="R":1387 src, dest = diff['src'], diff['dst']1388if self.p4HasMoveCommand:1389p4_edit(src)# src must be open before move1390p4_move(src, dest)# opens for (move/delete, move/add)1391else:1392p4_integrate(src, dest)1393if diff['src_sha1'] != diff['dst_sha1']:1394p4_edit(dest)1395else:1396 pureRenameCopy.add(dest)1397ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1398if not self.p4HasMoveCommand:1399p4_edit(dest)# with move: already open, writable1400 filesToChangeExecBit[dest] = diff['dst_mode']1401if not self.p4HasMoveCommand:1402if self.isWindows:1403 os.chmod(dest, stat.S_IWRITE)1404 os.unlink(dest)1405 filesToDelete.add(src)1406 editedFiles.add(dest)1407else:1408die("unknown modifier%sfor%s"% (modifier, path))14091410 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1411 patchcmd = diffcmd +" | git apply "1412 tryPatchCmd = patchcmd +"--check -"1413 applyPatchCmd = patchcmd +"--check --apply -"1414 patch_succeeded =True14151416if os.system(tryPatchCmd) !=0:1417 fixed_rcs_keywords =False1418 patch_succeeded =False1419print"Unfortunately applying the change failed!"14201421# Patch failed, maybe it's just RCS keyword woes. Look through1422# the patch to see if that's possible.1423ifgitConfigBool("git-p4.attemptRCSCleanup"):1424file=None1425 pattern =None1426 kwfiles = {}1427forfilein editedFiles | filesToDelete:1428# did this file's delta contain RCS keywords?1429 pattern =p4_keywords_regexp_for_file(file)14301431if pattern:1432# this file is a possibility...look for RCS keywords.1433 regexp = re.compile(pattern, re.VERBOSE)1434for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1435if regexp.search(line):1436if verbose:1437print"got keyword match on%sin%sin%s"% (pattern, line,file)1438 kwfiles[file] = pattern1439break14401441forfilein kwfiles:1442if verbose:1443print"zapping%swith%s"% (line,pattern)1444# File is being deleted, so not open in p4. Must1445# disable the read-only bit on windows.1446if self.isWindows andfilenot in editedFiles:1447 os.chmod(file, stat.S_IWRITE)1448 self.patchRCSKeywords(file, kwfiles[file])1449 fixed_rcs_keywords =True14501451if fixed_rcs_keywords:1452print"Retrying the patch with RCS keywords cleaned up"1453if os.system(tryPatchCmd) ==0:1454 patch_succeeded =True14551456if not patch_succeeded:1457for f in editedFiles:1458p4_revert(f)1459return False14601461#1462# Apply the patch for real, and do add/delete/+x handling.1463#1464system(applyPatchCmd)14651466for f in filesToAdd:1467p4_add(f)1468for f in filesToDelete:1469p4_revert(f)1470p4_delete(f)14711472# Set/clear executable bits1473for f in filesToChangeExecBit.keys():1474 mode = filesToChangeExecBit[f]1475setP4ExecBit(f, mode)14761477#1478# Build p4 change description, starting with the contents1479# of the git commit message.1480#1481 logMessage =extractLogMessageFromGitCommit(id)1482 logMessage = logMessage.strip()1483(logMessage, jobs) = self.separate_jobs_from_description(logMessage)14841485 template = self.prepareSubmitTemplate()1486 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)14871488if self.preserveUser:1489 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User14901491if self.checkAuthorship and not self.p4UserIsMe(p4User):1492 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1493 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1494 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"14951496 separatorLine ="######## everything below this line is just the diff #######\n"1497if not self.prepare_p4_only:1498 submitTemplate += separatorLine1499 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)15001501(handle, fileName) = tempfile.mkstemp()1502 tmpFile = os.fdopen(handle,"w+b")1503if self.isWindows:1504 submitTemplate = submitTemplate.replace("\n","\r\n")1505 tmpFile.write(submitTemplate)1506 tmpFile.close()15071508if self.prepare_p4_only:1509#1510# Leave the p4 tree prepared, and the submit template around1511# and let the user decide what to do next1512#1513print1514print"P4 workspace prepared for submission."1515print"To submit or revert, go to client workspace"1516print" "+ self.clientPath1517print1518print"To submit, use\"p4 submit\"to write a new description,"1519print"or\"p4 submit -i <%s\"to use the one prepared by" \1520"\"git p4\"."% fileName1521print"You can delete the file\"%s\"when finished."% fileName15221523if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1524print"To preserve change ownership by user%s, you must\n" \1525"do\"p4 change -f <change>\"after submitting and\n" \1526"edit the User field."1527if pureRenameCopy:1528print"After submitting, renamed files must be re-synced."1529print"Invoke\"p4 sync -f\"on each of these files:"1530for f in pureRenameCopy:1531print" "+ f15321533print1534print"To revert the changes, use\"p4 revert ...\", and delete"1535print"the submit template file\"%s\""% fileName1536if filesToAdd:1537print"Since the commit adds new files, they must be deleted:"1538for f in filesToAdd:1539print" "+ f1540print1541return True15421543#1544# Let the user edit the change description, then submit it.1545#1546if self.edit_template(fileName):1547# read the edited message and submit1548 ret =True1549 tmpFile =open(fileName,"rb")1550 message = tmpFile.read()1551 tmpFile.close()1552if self.isWindows:1553 message = message.replace("\r\n","\n")1554 submitTemplate = message[:message.index(separatorLine)]1555p4_write_pipe(['submit','-i'], submitTemplate)15561557if self.preserveUser:1558if p4User:1559# Get last changelist number. Cannot easily get it from1560# the submit command output as the output is1561# unmarshalled.1562 changelist = self.lastP4Changelist()1563 self.modifyChangelistUser(changelist, p4User)15641565# The rename/copy happened by applying a patch that created a1566# new file. This leaves it writable, which confuses p4.1567for f in pureRenameCopy:1568p4_sync(f,"-f")15691570else:1571# skip this patch1572 ret =False1573print"Submission cancelled, undoing p4 changes."1574for f in editedFiles:1575p4_revert(f)1576for f in filesToAdd:1577p4_revert(f)1578 os.remove(f)1579for f in filesToDelete:1580p4_revert(f)15811582 os.remove(fileName)1583return ret15841585# Export git tags as p4 labels. Create a p4 label and then tag1586# with that.1587defexportGitTags(self, gitTags):1588 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1589iflen(validLabelRegexp) ==0:1590 validLabelRegexp = defaultLabelRegexp1591 m = re.compile(validLabelRegexp)15921593for name in gitTags:15941595if not m.match(name):1596if verbose:1597print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1598continue15991600# Get the p4 commit this corresponds to1601 logMessage =extractLogMessageFromGitCommit(name)1602 values =extractSettingsGitLog(logMessage)16031604if not values.has_key('change'):1605# a tag pointing to something not sent to p4; ignore1606if verbose:1607print"git tag%sdoes not give a p4 commit"% name1608continue1609else:1610 changelist = values['change']16111612# Get the tag details.1613 inHeader =True1614 isAnnotated =False1615 body = []1616for l inread_pipe_lines(["git","cat-file","-p", name]):1617 l = l.strip()1618if inHeader:1619if re.match(r'tag\s+', l):1620 isAnnotated =True1621elif re.match(r'\s*$', l):1622 inHeader =False1623continue1624else:1625 body.append(l)16261627if not isAnnotated:1628 body = ["lightweight tag imported by git p4\n"]16291630# Create the label - use the same view as the client spec we are using1631 clientSpec =getClientSpec()16321633 labelTemplate ="Label:%s\n"% name1634 labelTemplate +="Description:\n"1635for b in body:1636 labelTemplate +="\t"+ b +"\n"1637 labelTemplate +="View:\n"1638for depot_side in clientSpec.mappings:1639 labelTemplate +="\t%s\n"% depot_side16401641if self.dry_run:1642print"Would create p4 label%sfor tag"% name1643elif self.prepare_p4_only:1644print"Not creating p4 label%sfor tag due to option" \1645" --prepare-p4-only"% name1646else:1647p4_write_pipe(["label","-i"], labelTemplate)16481649# Use the label1650p4_system(["tag","-l", name] +1651["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16521653if verbose:1654print"created p4 label for tag%s"% name16551656defrun(self, args):1657iflen(args) ==0:1658 self.master =currentGitBranch()1659eliflen(args) ==1:1660 self.master = args[0]1661if notbranchExists(self.master):1662die("Branch%sdoes not exist"% self.master)1663else:1664return False16651666if self.master:1667 allowSubmit =gitConfig("git-p4.allowSubmit")1668iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1669die("%sis not in git-p4.allowSubmit"% self.master)16701671[upstream, settings] =findUpstreamBranchPoint()1672 self.depotPath = settings['depot-paths'][0]1673iflen(self.origin) ==0:1674 self.origin = upstream16751676if self.preserveUser:1677if not self.canChangeChangelists():1678die("Cannot preserve user names without p4 super-user or admin permissions")16791680# if not set from the command line, try the config file1681if self.conflict_behavior is None:1682 val =gitConfig("git-p4.conflict")1683if val:1684if val not in self.conflict_behavior_choices:1685die("Invalid value '%s' for config git-p4.conflict"% val)1686else:1687 val ="ask"1688 self.conflict_behavior = val16891690if self.verbose:1691print"Origin branch is "+ self.origin16921693iflen(self.depotPath) ==0:1694print"Internal error: cannot locate perforce depot path from existing branches"1695 sys.exit(128)16961697 self.useClientSpec =False1698ifgitConfigBool("git-p4.useclientspec"):1699 self.useClientSpec =True1700if self.useClientSpec:1701 self.clientSpecDirs =getClientSpec()17021703# Check for the existance of P4 branches1704 branchesDetected = (len(p4BranchesInGit().keys()) >1)17051706if self.useClientSpec and not branchesDetected:1707# all files are relative to the client spec1708 self.clientPath =getClientRoot()1709else:1710 self.clientPath =p4Where(self.depotPath)17111712if self.clientPath =="":1713die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)17141715print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1716 self.oldWorkingDirectory = os.getcwd()17171718# ensure the clientPath exists1719 new_client_dir =False1720if not os.path.exists(self.clientPath):1721 new_client_dir =True1722 os.makedirs(self.clientPath)17231724chdir(self.clientPath, is_client_path=True)1725if self.dry_run:1726print"Would synchronize p4 checkout in%s"% self.clientPath1727else:1728print"Synchronizing p4 checkout..."1729if new_client_dir:1730# old one was destroyed, and maybe nobody told p41731p4_sync("...","-f")1732else:1733p4_sync("...")1734 self.check()17351736 commits = []1737if self.master:1738 commitish = self.master1739else:1740 commitish ='HEAD'17411742for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):1743 commits.append(line.strip())1744 commits.reverse()17451746if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1747 self.checkAuthorship =False1748else:1749 self.checkAuthorship =True17501751if self.preserveUser:1752 self.checkValidP4Users(commits)17531754#1755# Build up a set of options to be passed to diff when1756# submitting each commit to p4.1757#1758if self.detectRenames:1759# command-line -M arg1760 self.diffOpts ="-M"1761else:1762# If not explicitly set check the config variable1763 detectRenames =gitConfig("git-p4.detectRenames")17641765if detectRenames.lower() =="false"or detectRenames =="":1766 self.diffOpts =""1767elif detectRenames.lower() =="true":1768 self.diffOpts ="-M"1769else:1770 self.diffOpts ="-M%s"% detectRenames17711772# no command-line arg for -C or --find-copies-harder, just1773# config variables1774 detectCopies =gitConfig("git-p4.detectCopies")1775if detectCopies.lower() =="false"or detectCopies =="":1776pass1777elif detectCopies.lower() =="true":1778 self.diffOpts +=" -C"1779else:1780 self.diffOpts +=" -C%s"% detectCopies17811782ifgitConfigBool("git-p4.detectCopiesHarder"):1783 self.diffOpts +=" --find-copies-harder"17841785#1786# Apply the commits, one at a time. On failure, ask if should1787# continue to try the rest of the patches, or quit.1788#1789if self.dry_run:1790print"Would apply"1791 applied = []1792 last =len(commits) -11793for i, commit inenumerate(commits):1794if self.dry_run:1795print" ",read_pipe(["git","show","-s",1796"--format=format:%h%s", commit])1797 ok =True1798else:1799 ok = self.applyCommit(commit)1800if ok:1801 applied.append(commit)1802else:1803if self.prepare_p4_only and i < last:1804print"Processing only the first commit due to option" \1805" --prepare-p4-only"1806break1807if i < last:1808 quit =False1809while True:1810# prompt for what to do, or use the option/variable1811if self.conflict_behavior =="ask":1812print"What do you want to do?"1813 response =raw_input("[s]kip this commit but apply"1814" the rest, or [q]uit? ")1815if not response:1816continue1817elif self.conflict_behavior =="skip":1818 response ="s"1819elif self.conflict_behavior =="quit":1820 response ="q"1821else:1822die("Unknown conflict_behavior '%s'"%1823 self.conflict_behavior)18241825if response[0] =="s":1826print"Skipping this commit, but applying the rest"1827break1828if response[0] =="q":1829print"Quitting"1830 quit =True1831break1832if quit:1833break18341835chdir(self.oldWorkingDirectory)18361837if self.dry_run:1838pass1839elif self.prepare_p4_only:1840pass1841eliflen(commits) ==len(applied):1842print"All commits applied!"18431844 sync =P4Sync()1845if self.branch:1846 sync.branch = self.branch1847 sync.run([])18481849 rebase =P4Rebase()1850 rebase.rebase()18511852else:1853iflen(applied) ==0:1854print"No commits applied."1855else:1856print"Applied only the commits marked with '*':"1857for c in commits:1858if c in applied:1859 star ="*"1860else:1861 star =" "1862print star,read_pipe(["git","show","-s",1863"--format=format:%h%s", c])1864print"You will have to do 'git p4 sync' and rebase."18651866ifgitConfigBool("git-p4.exportLabels"):1867 self.exportLabels =True18681869if self.exportLabels:1870 p4Labels =getP4Labels(self.depotPath)1871 gitTags =getGitTags()18721873 missingGitTags = gitTags - p4Labels1874 self.exportGitTags(missingGitTags)18751876# exit with error unless everything applied perfectly1877iflen(commits) !=len(applied):1878 sys.exit(1)18791880return True18811882classView(object):1883"""Represent a p4 view ("p4 help views"), and map files in a1884 repo according to the view."""18851886def__init__(self, client_name):1887 self.mappings = []1888 self.client_prefix ="//%s/"% client_name1889# cache results of "p4 where" to lookup client file locations1890 self.client_spec_path_cache = {}18911892defappend(self, view_line):1893"""Parse a view line, splitting it into depot and client1894 sides. Append to self.mappings, preserving order. This1895 is only needed for tag creation."""18961897# Split the view line into exactly two words. P4 enforces1898# structure on these lines that simplifies this quite a bit.1899#1900# Either or both words may be double-quoted.1901# Single quotes do not matter.1902# Double-quote marks cannot occur inside the words.1903# A + or - prefix is also inside the quotes.1904# There are no quotes unless they contain a space.1905# The line is already white-space stripped.1906# The two words are separated by a single space.1907#1908if view_line[0] =='"':1909# First word is double quoted. Find its end.1910 close_quote_index = view_line.find('"',1)1911if close_quote_index <=0:1912die("No first-word closing quote found:%s"% view_line)1913 depot_side = view_line[1:close_quote_index]1914# skip closing quote and space1915 rhs_index = close_quote_index +1+11916else:1917 space_index = view_line.find(" ")1918if space_index <=0:1919die("No word-splitting space found:%s"% view_line)1920 depot_side = view_line[0:space_index]1921 rhs_index = space_index +119221923# prefix + means overlay on previous mapping1924if depot_side.startswith("+"):1925 depot_side = depot_side[1:]19261927# prefix - means exclude this path, leave out of mappings1928 exclude =False1929if depot_side.startswith("-"):1930 exclude =True1931 depot_side = depot_side[1:]19321933if not exclude:1934 self.mappings.append(depot_side)19351936defconvert_client_path(self, clientFile):1937# chop off //client/ part to make it relative1938if not clientFile.startswith(self.client_prefix):1939die("No prefix '%s' on clientFile '%s'"%1940(self.client_prefix, clientFile))1941return clientFile[len(self.client_prefix):]19421943defupdate_client_spec_path_cache(self, files):1944""" Caching file paths by "p4 where" batch query """19451946# List depot file paths exclude that already cached1947 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]19481949iflen(fileArgs) ==0:1950return# All files in cache19511952 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1953for res in where_result:1954if"code"in res and res["code"] =="error":1955# assume error is "... file(s) not in client view"1956continue1957if"clientFile"not in res:1958die("No clientFile in 'p4 where' output")1959if"unmap"in res:1960# it will list all of them, but only one not unmap-ped1961continue1962ifgitConfigBool("core.ignorecase"):1963 res['depotFile'] = res['depotFile'].lower()1964 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19651966# not found files or unmap files set to ""1967for depotFile in fileArgs:1968ifgitConfigBool("core.ignorecase"):1969 depotFile = depotFile.lower()1970if depotFile not in self.client_spec_path_cache:1971 self.client_spec_path_cache[depotFile] =""19721973defmap_in_client(self, depot_path):1974"""Return the relative location in the client where this1975 depot file should live. Returns "" if the file should1976 not be mapped in the client."""19771978ifgitConfigBool("core.ignorecase"):1979 depot_path = depot_path.lower()19801981if depot_path in self.client_spec_path_cache:1982return self.client_spec_path_cache[depot_path]19831984die("Error:%sis not found in client spec path"% depot_path )1985return""19861987classP4Sync(Command, P4UserMap):1988 delete_actions = ("delete","move/delete","purge")19891990def__init__(self):1991 Command.__init__(self)1992 P4UserMap.__init__(self)1993 self.options = [1994 optparse.make_option("--branch", dest="branch"),1995 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1996 optparse.make_option("--changesfile", dest="changesFile"),1997 optparse.make_option("--silent", dest="silent", action="store_true"),1998 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1999 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2000 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2001help="Import into refs/heads/ , not refs/remotes"),2002 optparse.make_option("--max-changes", dest="maxChanges",2003help="Maximum number of changes to import"),2004 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2005help="Internal block size to use when iteratively calling p4 changes"),2006 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2007help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2008 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2009help="Only sync files that are included in the Perforce Client Spec"),2010 optparse.make_option("-/", dest="cloneExclude",2011 action="append",type="string",2012help="exclude depot path"),2013]2014 self.description ="""Imports from Perforce into a git repository.\n2015 example:2016 //depot/my/project/ -- to import the current head2017 //depot/my/project/@all -- to import everything2018 //depot/my/project/@1,6 -- to import only from revision 1 to 620192020 (a ... is not needed in the path p4 specification, it's added implicitly)"""20212022 self.usage +=" //depot/path[@revRange]"2023 self.silent =False2024 self.createdBranches =set()2025 self.committedChanges =set()2026 self.branch =""2027 self.detectBranches =False2028 self.detectLabels =False2029 self.importLabels =False2030 self.changesFile =""2031 self.syncWithOrigin =True2032 self.importIntoRemotes =True2033 self.maxChanges =""2034 self.changes_block_size =None2035 self.keepRepoPath =False2036 self.depotPaths =None2037 self.p4BranchesInGit = []2038 self.cloneExclude = []2039 self.useClientSpec =False2040 self.useClientSpec_from_options =False2041 self.clientSpecDirs =None2042 self.tempBranches = []2043 self.tempBranchLocation ="git-p4-tmp"20442045ifgitConfig("git-p4.syncFromOrigin") =="false":2046 self.syncWithOrigin =False20472048# This is required for the "append" cloneExclude action2049defensure_value(self, attr, value):2050if nothasattr(self, attr)orgetattr(self, attr)is None:2051setattr(self, attr, value)2052returngetattr(self, attr)20532054# Force a checkpoint in fast-import and wait for it to finish2055defcheckpoint(self):2056 self.gitStream.write("checkpoint\n\n")2057 self.gitStream.write("progress checkpoint\n\n")2058 out = self.gitOutput.readline()2059if self.verbose:2060print"checkpoint finished: "+ out20612062defextractFilesFromCommit(self, commit):2063 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2064for path in self.cloneExclude]2065 files = []2066 fnum =02067while commit.has_key("depotFile%s"% fnum):2068 path = commit["depotFile%s"% fnum]20692070if[p for p in self.cloneExclude2071ifp4PathStartsWith(path, p)]:2072 found =False2073else:2074 found = [p for p in self.depotPaths2075ifp4PathStartsWith(path, p)]2076if not found:2077 fnum = fnum +12078continue20792080file= {}2081file["path"] = path2082file["rev"] = commit["rev%s"% fnum]2083file["action"] = commit["action%s"% fnum]2084file["type"] = commit["type%s"% fnum]2085 files.append(file)2086 fnum = fnum +12087return files20882089defstripRepoPath(self, path, prefixes):2090"""When streaming files, this is called to map a p4 depot path2091 to where it should go in git. The prefixes are either2092 self.depotPaths, or self.branchPrefixes in the case of2093 branch detection."""20942095if self.useClientSpec:2096# branch detection moves files up a level (the branch name)2097# from what client spec interpretation gives2098 path = self.clientSpecDirs.map_in_client(path)2099if self.detectBranches:2100for b in self.knownBranches:2101if path.startswith(b +"/"):2102 path = path[len(b)+1:]21032104elif self.keepRepoPath:2105# Preserve everything in relative path name except leading2106# //depot/; just look at first prefix as they all should2107# be in the same depot.2108 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2109ifp4PathStartsWith(path, depot):2110 path = path[len(depot):]21112112else:2113for p in prefixes:2114ifp4PathStartsWith(path, p):2115 path = path[len(p):]2116break21172118 path =wildcard_decode(path)2119return path21202121defsplitFilesIntoBranches(self, commit):2122"""Look at each depotFile in the commit to figure out to what2123 branch it belongs."""21242125if self.clientSpecDirs:2126 files = self.extractFilesFromCommit(commit)2127 self.clientSpecDirs.update_client_spec_path_cache(files)21282129 branches = {}2130 fnum =02131while commit.has_key("depotFile%s"% fnum):2132 path = commit["depotFile%s"% fnum]2133 found = [p for p in self.depotPaths2134ifp4PathStartsWith(path, p)]2135if not found:2136 fnum = fnum +12137continue21382139file= {}2140file["path"] = path2141file["rev"] = commit["rev%s"% fnum]2142file["action"] = commit["action%s"% fnum]2143file["type"] = commit["type%s"% fnum]2144 fnum = fnum +121452146# start with the full relative path where this file would2147# go in a p4 client2148if self.useClientSpec:2149 relPath = self.clientSpecDirs.map_in_client(path)2150else:2151 relPath = self.stripRepoPath(path, self.depotPaths)21522153for branch in self.knownBranches.keys():2154# add a trailing slash so that a commit into qt/4.2foo2155# doesn't end up in qt/4.2, e.g.2156if relPath.startswith(branch +"/"):2157if branch not in branches:2158 branches[branch] = []2159 branches[branch].append(file)2160break21612162return branches21632164# output one file from the P4 stream2165# - helper for streamP4Files21662167defstreamOneP4File(self,file, contents):2168 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2169if verbose:2170 sys.stderr.write("%s\n"% relPath)21712172(type_base, type_mods) =split_p4_type(file["type"])21732174 git_mode ="100644"2175if"x"in type_mods:2176 git_mode ="100755"2177if type_base =="symlink":2178 git_mode ="120000"2179# p4 print on a symlink sometimes contains "target\n";2180# if it does, remove the newline2181 data =''.join(contents)2182if not data:2183# Some version of p4 allowed creating a symlink that pointed2184# to nothing. This causes p4 errors when checking out such2185# a change, and errors here too. Work around it by ignoring2186# the bad symlink; hopefully a future change fixes it.2187print"\nIgnoring empty symlink in%s"%file['depotFile']2188return2189elif data[-1] =='\n':2190 contents = [data[:-1]]2191else:2192 contents = [data]21932194if type_base =="utf16":2195# p4 delivers different text in the python output to -G2196# than it does when using "print -o", or normal p4 client2197# operations. utf16 is converted to ascii or utf8, perhaps.2198# But ascii text saved as -t utf16 is completely mangled.2199# Invoke print -o to get the real contents.2200#2201# On windows, the newlines will always be mangled by print, so put2202# them back too. This is not needed to the cygwin windows version,2203# just the native "NT" type.2204#2205try:2206 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2207exceptExceptionas e:2208if'Translation of file content failed'instr(e):2209 type_base ='binary'2210else:2211raise e2212else:2213ifp4_version_string().find('/NT') >=0:2214 text = text.replace('\r\n','\n')2215 contents = [ text ]22162217if type_base =="apple":2218# Apple filetype files will be streamed as a concatenation of2219# its appledouble header and the contents. This is useless2220# on both macs and non-macs. If using "print -q -o xx", it2221# will create "xx" with the data, and "%xx" with the header.2222# This is also not very useful.2223#2224# Ideally, someday, this script can learn how to generate2225# appledouble files directly and import those to git, but2226# non-mac machines can never find a use for apple filetype.2227print"\nIgnoring apple filetype file%s"%file['depotFile']2228return22292230# Note that we do not try to de-mangle keywords on utf16 files,2231# even though in theory somebody may want that.2232 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2233if pattern:2234 regexp = re.compile(pattern, re.VERBOSE)2235 text =''.join(contents)2236 text = regexp.sub(r'$\1$', text)2237 contents = [ text ]22382239 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22402241# total length...2242 length =02243for d in contents:2244 length = length +len(d)22452246 self.gitStream.write("data%d\n"% length)2247for d in contents:2248 self.gitStream.write(d)2249 self.gitStream.write("\n")22502251defstreamOneP4Deletion(self,file):2252 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2253if verbose:2254 sys.stderr.write("delete%s\n"% relPath)2255 self.gitStream.write("D%s\n"% relPath)22562257# handle another chunk of streaming data2258defstreamP4FilesCb(self, marshalled):22592260# catch p4 errors and complain2261 err =None2262if"code"in marshalled:2263if marshalled["code"] =="error":2264if"data"in marshalled:2265 err = marshalled["data"].rstrip()2266if err:2267 f =None2268if self.stream_have_file_info:2269if"depotFile"in self.stream_file:2270 f = self.stream_file["depotFile"]2271# force a failure in fast-import, else an empty2272# commit will be made2273 self.gitStream.write("\n")2274 self.gitStream.write("die-now\n")2275 self.gitStream.close()2276# ignore errors, but make sure it exits first2277 self.importProcess.wait()2278if f:2279die("Error from p4 print for%s:%s"% (f, err))2280else:2281die("Error from p4 print:%s"% err)22822283if marshalled.has_key('depotFile')and self.stream_have_file_info:2284# start of a new file - output the old one first2285 self.streamOneP4File(self.stream_file, self.stream_contents)2286 self.stream_file = {}2287 self.stream_contents = []2288 self.stream_have_file_info =False22892290# pick up the new file information... for the2291# 'data' field we need to append to our array2292for k in marshalled.keys():2293if k =='data':2294 self.stream_contents.append(marshalled['data'])2295else:2296 self.stream_file[k] = marshalled[k]22972298 self.stream_have_file_info =True22992300# Stream directly from "p4 files" into "git fast-import"2301defstreamP4Files(self, files):2302 filesForCommit = []2303 filesToRead = []2304 filesToDelete = []23052306for f in files:2307# if using a client spec, only add the files that have2308# a path in the client2309if self.clientSpecDirs:2310if self.clientSpecDirs.map_in_client(f['path']) =="":2311continue23122313 filesForCommit.append(f)2314if f['action']in self.delete_actions:2315 filesToDelete.append(f)2316else:2317 filesToRead.append(f)23182319# deleted files...2320for f in filesToDelete:2321 self.streamOneP4Deletion(f)23222323iflen(filesToRead) >0:2324 self.stream_file = {}2325 self.stream_contents = []2326 self.stream_have_file_info =False23272328# curry self argument2329defstreamP4FilesCbSelf(entry):2330 self.streamP4FilesCb(entry)23312332 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23332334p4CmdList(["-x","-","print"],2335 stdin=fileArgs,2336 cb=streamP4FilesCbSelf)23372338# do the last chunk2339if self.stream_file.has_key('depotFile'):2340 self.streamOneP4File(self.stream_file, self.stream_contents)23412342defmake_email(self, userid):2343if userid in self.users:2344return self.users[userid]2345else:2346return"%s<a@b>"% userid23472348defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2349""" Stream a p4 tag.2350 commit is either a git commit, or a fast-import mark, ":<p4commit>"2351 """23522353if verbose:2354print"writing tag%sfor commit%s"% (labelName, commit)2355 gitStream.write("tag%s\n"% labelName)2356 gitStream.write("from%s\n"% commit)23572358if labelDetails.has_key('Owner'):2359 owner = labelDetails["Owner"]2360else:2361 owner =None23622363# Try to use the owner of the p4 label, or failing that,2364# the current p4 user id.2365if owner:2366 email = self.make_email(owner)2367else:2368 email = self.make_email(self.p4UserId())2369 tagger ="%s %s %s"% (email, epoch, self.tz)23702371 gitStream.write("tagger%s\n"% tagger)23722373print"labelDetails=",labelDetails2374if labelDetails.has_key('Description'):2375 description = labelDetails['Description']2376else:2377 description ='Label from git p4'23782379 gitStream.write("data%d\n"%len(description))2380 gitStream.write(description)2381 gitStream.write("\n")23822383defcommit(self, details, files, branch, parent =""):2384 epoch = details["time"]2385 author = details["user"]23862387if self.verbose:2388print"commit into%s"% branch23892390# start with reading files; if that fails, we should not2391# create a commit.2392 new_files = []2393for f in files:2394if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2395 new_files.append(f)2396else:2397 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23982399if self.clientSpecDirs:2400 self.clientSpecDirs.update_client_spec_path_cache(files)24012402 self.gitStream.write("commit%s\n"% branch)2403 self.gitStream.write("mark :%s\n"% details["change"])2404 self.committedChanges.add(int(details["change"]))2405 committer =""2406if author not in self.users:2407 self.getUserMapFromPerforceServer()2408 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)24092410 self.gitStream.write("committer%s\n"% committer)24112412 self.gitStream.write("data <<EOT\n")2413 self.gitStream.write(details["desc"])2414 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2415(','.join(self.branchPrefixes), details["change"]))2416iflen(details['options']) >0:2417 self.gitStream.write(": options =%s"% details['options'])2418 self.gitStream.write("]\nEOT\n\n")24192420iflen(parent) >0:2421if self.verbose:2422print"parent%s"% parent2423 self.gitStream.write("from%s\n"% parent)24242425 self.streamP4Files(new_files)2426 self.gitStream.write("\n")24272428 change =int(details["change"])24292430if self.labels.has_key(change):2431 label = self.labels[change]2432 labelDetails = label[0]2433 labelRevisions = label[1]2434if self.verbose:2435print"Change%sis labelled%s"% (change, labelDetails)24362437 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2438for p in self.branchPrefixes])24392440iflen(files) ==len(labelRevisions):24412442 cleanedFiles = {}2443for info in files:2444if info["action"]in self.delete_actions:2445continue2446 cleanedFiles[info["depotFile"]] = info["rev"]24472448if cleanedFiles == labelRevisions:2449 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24502451else:2452if not self.silent:2453print("Tag%sdoes not match with change%s: files do not match."2454% (labelDetails["label"], change))24552456else:2457if not self.silent:2458print("Tag%sdoes not match with change%s: file count is different."2459% (labelDetails["label"], change))24602461# Build a dictionary of changelists and labels, for "detect-labels" option.2462defgetLabels(self):2463 self.labels = {}24642465 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2466iflen(l) >0and not self.silent:2467print"Finding files belonging to labels in%s"% `self.depotPaths`24682469for output in l:2470 label = output["label"]2471 revisions = {}2472 newestChange =02473if self.verbose:2474print"Querying files for label%s"% label2475forfileinp4CmdList(["files"] +2476["%s...@%s"% (p, label)2477for p in self.depotPaths]):2478 revisions[file["depotFile"]] =file["rev"]2479 change =int(file["change"])2480if change > newestChange:2481 newestChange = change24822483 self.labels[newestChange] = [output, revisions]24842485if self.verbose:2486print"Label changes:%s"% self.labels.keys()24872488# Import p4 labels as git tags. A direct mapping does not2489# exist, so assume that if all the files are at the same revision2490# then we can use that, or it's something more complicated we should2491# just ignore.2492defimportP4Labels(self, stream, p4Labels):2493if verbose:2494print"import p4 labels: "+' '.join(p4Labels)24952496 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2497 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2498iflen(validLabelRegexp) ==0:2499 validLabelRegexp = defaultLabelRegexp2500 m = re.compile(validLabelRegexp)25012502for name in p4Labels:2503 commitFound =False25042505if not m.match(name):2506if verbose:2507print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2508continue25092510if name in ignoredP4Labels:2511continue25122513 labelDetails =p4CmdList(['label',"-o", name])[0]25142515# get the most recent changelist for each file in this label2516 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2517for p in self.depotPaths])25182519if change.has_key('change'):2520# find the corresponding git commit; take the oldest commit2521 changelist =int(change['change'])2522if changelist in self.committedChanges:2523 gitCommit =":%d"% changelist # use a fast-import mark2524 commitFound =True2525else:2526 gitCommit =read_pipe(["git","rev-list","--max-count=1",2527"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2528iflen(gitCommit) ==0:2529print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2530else:2531 commitFound =True2532 gitCommit = gitCommit.strip()25332534if commitFound:2535# Convert from p4 time format2536try:2537 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2538exceptValueError:2539print"Could not convert label time%s"% labelDetails['Update']2540 tmwhen =125412542 when =int(time.mktime(tmwhen))2543 self.streamTag(stream, name, labelDetails, gitCommit, when)2544if verbose:2545print"p4 label%smapped to git commit%s"% (name, gitCommit)2546else:2547if verbose:2548print"Label%shas no changelists - possibly deleted?"% name25492550if not commitFound:2551# We can't import this label; don't try again as it will get very2552# expensive repeatedly fetching all the files for labels that will2553# never be imported. If the label is moved in the future, the2554# ignore will need to be removed manually.2555system(["git","config","--add","git-p4.ignoredP4Labels", name])25562557defguessProjectName(self):2558for p in self.depotPaths:2559if p.endswith("/"):2560 p = p[:-1]2561 p = p[p.strip().rfind("/") +1:]2562if not p.endswith("/"):2563 p +="/"2564return p25652566defgetBranchMapping(self):2567 lostAndFoundBranches =set()25682569 user =gitConfig("git-p4.branchUser")2570iflen(user) >0:2571 command ="branches -u%s"% user2572else:2573 command ="branches"25742575for info inp4CmdList(command):2576 details =p4Cmd(["branch","-o", info["branch"]])2577 viewIdx =02578while details.has_key("View%s"% viewIdx):2579 paths = details["View%s"% viewIdx].split(" ")2580 viewIdx = viewIdx +12581# require standard //depot/foo/... //depot/bar/... mapping2582iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2583continue2584 source = paths[0]2585 destination = paths[1]2586## HACK2587ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2588 source = source[len(self.depotPaths[0]):-4]2589 destination = destination[len(self.depotPaths[0]):-4]25902591if destination in self.knownBranches:2592if not self.silent:2593print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2594print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2595continue25962597 self.knownBranches[destination] = source25982599 lostAndFoundBranches.discard(destination)26002601if source not in self.knownBranches:2602 lostAndFoundBranches.add(source)26032604# Perforce does not strictly require branches to be defined, so we also2605# check git config for a branch list.2606#2607# Example of branch definition in git config file:2608# [git-p4]2609# branchList=main:branchA2610# branchList=main:branchB2611# branchList=branchA:branchC2612 configBranches =gitConfigList("git-p4.branchList")2613for branch in configBranches:2614if branch:2615(source, destination) = branch.split(":")2616 self.knownBranches[destination] = source26172618 lostAndFoundBranches.discard(destination)26192620if source not in self.knownBranches:2621 lostAndFoundBranches.add(source)262226232624for branch in lostAndFoundBranches:2625 self.knownBranches[branch] = branch26262627defgetBranchMappingFromGitBranches(self):2628 branches =p4BranchesInGit(self.importIntoRemotes)2629for branch in branches.keys():2630if branch =="master":2631 branch ="main"2632else:2633 branch = branch[len(self.projectName):]2634 self.knownBranches[branch] = branch26352636defupdateOptionDict(self, d):2637 option_keys = {}2638if self.keepRepoPath:2639 option_keys['keepRepoPath'] =126402641 d["options"] =' '.join(sorted(option_keys.keys()))26422643defreadOptions(self, d):2644 self.keepRepoPath = (d.has_key('options')2645and('keepRepoPath'in d['options']))26462647defgitRefForBranch(self, branch):2648if branch =="main":2649return self.refPrefix +"master"26502651iflen(branch) <=0:2652return branch26532654return self.refPrefix + self.projectName + branch26552656defgitCommitByP4Change(self, ref, change):2657if self.verbose:2658print"looking in ref "+ ref +" for change%susing bisect..."% change26592660 earliestCommit =""2661 latestCommit =parseRevision(ref)26622663while True:2664if self.verbose:2665print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2666 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2667iflen(next) ==0:2668if self.verbose:2669print"argh"2670return""2671 log =extractLogMessageFromGitCommit(next)2672 settings =extractSettingsGitLog(log)2673 currentChange =int(settings['change'])2674if self.verbose:2675print"current change%s"% currentChange26762677if currentChange == change:2678if self.verbose:2679print"found%s"% next2680return next26812682if currentChange < change:2683 earliestCommit ="^%s"% next2684else:2685 latestCommit ="%s"% next26862687return""26882689defimportNewBranch(self, branch, maxChange):2690# make fast-import flush all changes to disk and update the refs using the checkpoint2691# command so that we can try to find the branch parent in the git history2692 self.gitStream.write("checkpoint\n\n");2693 self.gitStream.flush();2694 branchPrefix = self.depotPaths[0] + branch +"/"2695range="@1,%s"% maxChange2696#print "prefix" + branchPrefix2697 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2698iflen(changes) <=0:2699return False2700 firstChange = changes[0]2701#print "first change in branch: %s" % firstChange2702 sourceBranch = self.knownBranches[branch]2703 sourceDepotPath = self.depotPaths[0] + sourceBranch2704 sourceRef = self.gitRefForBranch(sourceBranch)2705#print "source " + sourceBranch27062707 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2708#print "branch parent: %s" % branchParentChange2709 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2710iflen(gitParent) >0:2711 self.initialParents[self.gitRefForBranch(branch)] = gitParent2712#print "parent git commit: %s" % gitParent27132714 self.importChanges(changes)2715return True27162717defsearchParent(self, parent, branch, target):2718 parentFound =False2719for blob inread_pipe_lines(["git","rev-list","--reverse",2720"--no-merges", parent]):2721 blob = blob.strip()2722iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2723 parentFound =True2724if self.verbose:2725print"Found parent of%sin commit%s"% (branch, blob)2726break2727if parentFound:2728return blob2729else:2730return None27312732defimportChanges(self, changes):2733 cnt =12734for change in changes:2735 description =p4_describe(change)2736 self.updateOptionDict(description)27372738if not self.silent:2739 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2740 sys.stdout.flush()2741 cnt = cnt +127422743try:2744if self.detectBranches:2745 branches = self.splitFilesIntoBranches(description)2746for branch in branches.keys():2747## HACK --hwn2748 branchPrefix = self.depotPaths[0] + branch +"/"2749 self.branchPrefixes = [ branchPrefix ]27502751 parent =""27522753 filesForCommit = branches[branch]27542755if self.verbose:2756print"branch is%s"% branch27572758 self.updatedBranches.add(branch)27592760if branch not in self.createdBranches:2761 self.createdBranches.add(branch)2762 parent = self.knownBranches[branch]2763if parent == branch:2764 parent =""2765else:2766 fullBranch = self.projectName + branch2767if fullBranch not in self.p4BranchesInGit:2768if not self.silent:2769print("\nImporting new branch%s"% fullBranch);2770if self.importNewBranch(branch, change -1):2771 parent =""2772 self.p4BranchesInGit.append(fullBranch)2773if not self.silent:2774print("\nResuming with change%s"% change);27752776if self.verbose:2777print"parent determined through known branches:%s"% parent27782779 branch = self.gitRefForBranch(branch)2780 parent = self.gitRefForBranch(parent)27812782if self.verbose:2783print"looking for initial parent for%s; current parent is%s"% (branch, parent)27842785iflen(parent) ==0and branch in self.initialParents:2786 parent = self.initialParents[branch]2787del self.initialParents[branch]27882789 blob =None2790iflen(parent) >0:2791 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2792if self.verbose:2793print"Creating temporary branch: "+ tempBranch2794 self.commit(description, filesForCommit, tempBranch)2795 self.tempBranches.append(tempBranch)2796 self.checkpoint()2797 blob = self.searchParent(parent, branch, tempBranch)2798if blob:2799 self.commit(description, filesForCommit, branch, blob)2800else:2801if self.verbose:2802print"Parent of%snot found. Committing into head of%s"% (branch, parent)2803 self.commit(description, filesForCommit, branch, parent)2804else:2805 files = self.extractFilesFromCommit(description)2806 self.commit(description, files, self.branch,2807 self.initialParent)2808# only needed once, to connect to the previous commit2809 self.initialParent =""2810exceptIOError:2811print self.gitError.read()2812 sys.exit(1)28132814defimportHeadRevision(self, revision):2815print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)28162817 details = {}2818 details["user"] ="git perforce import user"2819 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2820% (' '.join(self.depotPaths), revision))2821 details["change"] = revision2822 newestRevision =028232824 fileCnt =02825 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28262827for info inp4CmdList(["files"] + fileArgs):28282829if'code'in info and info['code'] =='error':2830 sys.stderr.write("p4 returned an error:%s\n"2831% info['data'])2832if info['data'].find("must refer to client") >=0:2833 sys.stderr.write("This particular p4 error is misleading.\n")2834 sys.stderr.write("Perhaps the depot path was misspelled.\n");2835 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2836 sys.exit(1)2837if'p4ExitCode'in info:2838 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2839 sys.exit(1)284028412842 change =int(info["change"])2843if change > newestRevision:2844 newestRevision = change28452846if info["action"]in self.delete_actions:2847# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2848#fileCnt = fileCnt + 12849continue28502851for prop in["depotFile","rev","action","type"]:2852 details["%s%s"% (prop, fileCnt)] = info[prop]28532854 fileCnt = fileCnt +128552856 details["change"] = newestRevision28572858# Use time from top-most change so that all git p4 clones of2859# the same p4 repo have the same commit SHA1s.2860 res =p4_describe(newestRevision)2861 details["time"] = res["time"]28622863 self.updateOptionDict(details)2864try:2865 self.commit(details, self.extractFilesFromCommit(details), self.branch)2866exceptIOError:2867print"IO error with git fast-import. Is your git version recent enough?"2868print self.gitError.read()286928702871defrun(self, args):2872 self.depotPaths = []2873 self.changeRange =""2874 self.previousDepotPaths = []2875 self.hasOrigin =False28762877# map from branch depot path to parent branch2878 self.knownBranches = {}2879 self.initialParents = {}28802881if self.importIntoRemotes:2882 self.refPrefix ="refs/remotes/p4/"2883else:2884 self.refPrefix ="refs/heads/p4/"28852886if self.syncWithOrigin:2887 self.hasOrigin =originP4BranchesExist()2888if self.hasOrigin:2889if not self.silent:2890print'Syncing with origin first, using "git fetch origin"'2891system("git fetch origin")28922893 branch_arg_given =bool(self.branch)2894iflen(self.branch) ==0:2895 self.branch = self.refPrefix +"master"2896ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2897system("git update-ref%srefs/heads/p4"% self.branch)2898system("git branch -D p4")28992900# accept either the command-line option, or the configuration variable2901if self.useClientSpec:2902# will use this after clone to set the variable2903 self.useClientSpec_from_options =True2904else:2905ifgitConfigBool("git-p4.useclientspec"):2906 self.useClientSpec =True2907if self.useClientSpec:2908 self.clientSpecDirs =getClientSpec()29092910# TODO: should always look at previous commits,2911# merge with previous imports, if possible.2912if args == []:2913if self.hasOrigin:2914createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)29152916# branches holds mapping from branch name to sha12917 branches =p4BranchesInGit(self.importIntoRemotes)29182919# restrict to just this one, disabling detect-branches2920if branch_arg_given:2921 short = self.branch.split("/")[-1]2922if short in branches:2923 self.p4BranchesInGit = [ short ]2924else:2925 self.p4BranchesInGit = branches.keys()29262927iflen(self.p4BranchesInGit) >1:2928if not self.silent:2929print"Importing from/into multiple branches"2930 self.detectBranches =True2931for branch in branches.keys():2932 self.initialParents[self.refPrefix + branch] = \2933 branches[branch]29342935if self.verbose:2936print"branches:%s"% self.p4BranchesInGit29372938 p4Change =02939for branch in self.p4BranchesInGit:2940 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29412942 settings =extractSettingsGitLog(logMsg)29432944 self.readOptions(settings)2945if(settings.has_key('depot-paths')2946and settings.has_key('change')):2947 change =int(settings['change']) +12948 p4Change =max(p4Change, change)29492950 depotPaths =sorted(settings['depot-paths'])2951if self.previousDepotPaths == []:2952 self.previousDepotPaths = depotPaths2953else:2954 paths = []2955for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2956 prev_list = prev.split("/")2957 cur_list = cur.split("/")2958for i inrange(0,min(len(cur_list),len(prev_list))):2959if cur_list[i] <> prev_list[i]:2960 i = i -12961break29622963 paths.append("/".join(cur_list[:i +1]))29642965 self.previousDepotPaths = paths29662967if p4Change >0:2968 self.depotPaths =sorted(self.previousDepotPaths)2969 self.changeRange ="@%s,#head"% p4Change2970if not self.silent and not self.detectBranches:2971print"Performing incremental import into%sgit branch"% self.branch29722973# accept multiple ref name abbreviations:2974# refs/foo/bar/branch -> use it exactly2975# p4/branch -> prepend refs/remotes/ or refs/heads/2976# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2977if not self.branch.startswith("refs/"):2978if self.importIntoRemotes:2979 prepend ="refs/remotes/"2980else:2981 prepend ="refs/heads/"2982if not self.branch.startswith("p4/"):2983 prepend +="p4/"2984 self.branch = prepend + self.branch29852986iflen(args) ==0and self.depotPaths:2987if not self.silent:2988print"Depot paths:%s"%' '.join(self.depotPaths)2989else:2990if self.depotPaths and self.depotPaths != args:2991print("previous import used depot path%sand now%swas specified. "2992"This doesn't work!"% (' '.join(self.depotPaths),2993' '.join(args)))2994 sys.exit(1)29952996 self.depotPaths =sorted(args)29972998 revision =""2999 self.users = {}30003001# Make sure no revision specifiers are used when --changesfile3002# is specified.3003 bad_changesfile =False3004iflen(self.changesFile) >0:3005for p in self.depotPaths:3006if p.find("@") >=0or p.find("#") >=0:3007 bad_changesfile =True3008break3009if bad_changesfile:3010die("Option --changesfile is incompatible with revision specifiers")30113012 newPaths = []3013for p in self.depotPaths:3014if p.find("@") != -1:3015 atIdx = p.index("@")3016 self.changeRange = p[atIdx:]3017if self.changeRange =="@all":3018 self.changeRange =""3019elif','not in self.changeRange:3020 revision = self.changeRange3021 self.changeRange =""3022 p = p[:atIdx]3023elif p.find("#") != -1:3024 hashIdx = p.index("#")3025 revision = p[hashIdx:]3026 p = p[:hashIdx]3027elif self.previousDepotPaths == []:3028# pay attention to changesfile, if given, else import3029# the entire p4 tree at the head revision3030iflen(self.changesFile) ==0:3031 revision ="#head"30323033 p = re.sub("\.\.\.$","", p)3034if not p.endswith("/"):3035 p +="/"30363037 newPaths.append(p)30383039 self.depotPaths = newPaths30403041# --detect-branches may change this for each branch3042 self.branchPrefixes = self.depotPaths30433044 self.loadUserMapFromCache()3045 self.labels = {}3046if self.detectLabels:3047 self.getLabels();30483049if self.detectBranches:3050## FIXME - what's a P4 projectName ?3051 self.projectName = self.guessProjectName()30523053if self.hasOrigin:3054 self.getBranchMappingFromGitBranches()3055else:3056 self.getBranchMapping()3057if self.verbose:3058print"p4-git branches:%s"% self.p4BranchesInGit3059print"initial parents:%s"% self.initialParents3060for b in self.p4BranchesInGit:3061if b !="master":30623063## FIXME3064 b = b[len(self.projectName):]3065 self.createdBranches.add(b)30663067 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30683069 self.importProcess = subprocess.Popen(["git","fast-import"],3070 stdin=subprocess.PIPE,3071 stdout=subprocess.PIPE,3072 stderr=subprocess.PIPE);3073 self.gitOutput = self.importProcess.stdout3074 self.gitStream = self.importProcess.stdin3075 self.gitError = self.importProcess.stderr30763077if revision:3078 self.importHeadRevision(revision)3079else:3080 changes = []30813082iflen(self.changesFile) >0:3083 output =open(self.changesFile).readlines()3084 changeSet =set()3085for line in output:3086 changeSet.add(int(line))30873088for change in changeSet:3089 changes.append(change)30903091 changes.sort()3092else:3093# catch "git p4 sync" with no new branches, in a repo that3094# does not have any existing p4 branches3095iflen(args) ==0:3096if not self.p4BranchesInGit:3097die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30983099# The default branch is master, unless --branch is used to3100# specify something else. Make sure it exists, or complain3101# nicely about how to use --branch.3102if not self.detectBranches:3103if notbranch_exists(self.branch):3104if branch_arg_given:3105die("Error: branch%sdoes not exist."% self.branch)3106else:3107die("Error: no branch%s; perhaps specify one with --branch."%3108 self.branch)31093110if self.verbose:3111print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3112 self.changeRange)3113 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)31143115iflen(self.maxChanges) >0:3116 changes = changes[:min(int(self.maxChanges),len(changes))]31173118iflen(changes) ==0:3119if not self.silent:3120print"No changes to import!"3121else:3122if not self.silent and not self.detectBranches:3123print"Import destination:%s"% self.branch31243125 self.updatedBranches =set()31263127if not self.detectBranches:3128if args:3129# start a new branch3130 self.initialParent =""3131else:3132# build on a previous revision3133 self.initialParent =parseRevision(self.branch)31343135 self.importChanges(changes)31363137if not self.silent:3138print""3139iflen(self.updatedBranches) >0:3140 sys.stdout.write("Updated branches: ")3141for b in self.updatedBranches:3142 sys.stdout.write("%s"% b)3143 sys.stdout.write("\n")31443145ifgitConfigBool("git-p4.importLabels"):3146 self.importLabels =True31473148if self.importLabels:3149 p4Labels =getP4Labels(self.depotPaths)3150 gitTags =getGitTags()31513152 missingP4Labels = p4Labels - gitTags3153 self.importP4Labels(self.gitStream, missingP4Labels)31543155 self.gitStream.close()3156if self.importProcess.wait() !=0:3157die("fast-import failed:%s"% self.gitError.read())3158 self.gitOutput.close()3159 self.gitError.close()31603161# Cleanup temporary branches created during import3162if self.tempBranches != []:3163for branch in self.tempBranches:3164read_pipe("git update-ref -d%s"% branch)3165 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31663167# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3168# a convenient shortcut refname "p4".3169if self.importIntoRemotes:3170 head_ref = self.refPrefix +"HEAD"3171if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3172system(["git","symbolic-ref", head_ref, self.branch])31733174return True31753176classP4Rebase(Command):3177def__init__(self):3178 Command.__init__(self)3179 self.options = [3180 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3181]3182 self.importLabels =False3183 self.description = ("Fetches the latest revision from perforce and "3184+"rebases the current work (branch) against it")31853186defrun(self, args):3187 sync =P4Sync()3188 sync.importLabels = self.importLabels3189 sync.run([])31903191return self.rebase()31923193defrebase(self):3194if os.system("git update-index --refresh") !=0:3195die("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.");3196iflen(read_pipe("git diff-index HEAD --")) >0:3197die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");31983199[upstream, settings] =findUpstreamBranchPoint()3200iflen(upstream) ==0:3201die("Cannot find upstream branchpoint for rebase")32023203# the branchpoint may be p4/foo~3, so strip off the parent3204 upstream = re.sub("~[0-9]+$","", upstream)32053206print"Rebasing the current branch onto%s"% upstream3207 oldHead =read_pipe("git rev-parse HEAD").strip()3208system("git rebase%s"% upstream)3209system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3210return True32113212classP4Clone(P4Sync):3213def__init__(self):3214 P4Sync.__init__(self)3215 self.description ="Creates a new git repository and imports from Perforce into it"3216 self.usage ="usage: %prog [options] //depot/path[@revRange]"3217 self.options += [3218 optparse.make_option("--destination", dest="cloneDestination",3219 action='store', default=None,3220help="where to leave result of the clone"),3221 optparse.make_option("--bare", dest="cloneBare",3222 action="store_true", default=False),3223]3224 self.cloneDestination =None3225 self.needsGit =False3226 self.cloneBare =False32273228defdefaultDestination(self, args):3229## TODO: use common prefix of args?3230 depotPath = args[0]3231 depotDir = re.sub("(@[^@]*)$","", depotPath)3232 depotDir = re.sub("(#[^#]*)$","", depotDir)3233 depotDir = re.sub(r"\.\.\.$","", depotDir)3234 depotDir = re.sub(r"/$","", depotDir)3235return os.path.split(depotDir)[1]32363237defrun(self, args):3238iflen(args) <1:3239return False32403241if self.keepRepoPath and not self.cloneDestination:3242 sys.stderr.write("Must specify destination for --keep-path\n")3243 sys.exit(1)32443245 depotPaths = args32463247if not self.cloneDestination andlen(depotPaths) >1:3248 self.cloneDestination = depotPaths[-1]3249 depotPaths = depotPaths[:-1]32503251 self.cloneExclude = ["/"+p for p in self.cloneExclude]3252for p in depotPaths:3253if not p.startswith("//"):3254 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3255return False32563257if not self.cloneDestination:3258 self.cloneDestination = self.defaultDestination(args)32593260print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32613262if not os.path.exists(self.cloneDestination):3263 os.makedirs(self.cloneDestination)3264chdir(self.cloneDestination)32653266 init_cmd = ["git","init"]3267if self.cloneBare:3268 init_cmd.append("--bare")3269 retcode = subprocess.call(init_cmd)3270if retcode:3271raiseCalledProcessError(retcode, init_cmd)32723273if not P4Sync.run(self, depotPaths):3274return False32753276# create a master branch and check out a work tree3277ifgitBranchExists(self.branch):3278system(["git","branch","master", self.branch ])3279if not self.cloneBare:3280system(["git","checkout","-f"])3281else:3282print'Not checking out any branch, use ' \3283'"git checkout -q -b master <branch>"'32843285# auto-set this variable if invoked with --use-client-spec3286if self.useClientSpec_from_options:3287system("git config --bool git-p4.useclientspec true")32883289return True32903291classP4Branches(Command):3292def__init__(self):3293 Command.__init__(self)3294 self.options = [ ]3295 self.description = ("Shows the git branches that hold imports and their "3296+"corresponding perforce depot paths")3297 self.verbose =False32983299defrun(self, args):3300iforiginP4BranchesExist():3301createOrUpdateBranchesFromOrigin()33023303 cmdline ="git rev-parse --symbolic "3304 cmdline +=" --remotes"33053306for line inread_pipe_lines(cmdline):3307 line = line.strip()33083309if not line.startswith('p4/')or line =="p4/HEAD":3310continue3311 branch = line33123313 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3314 settings =extractSettingsGitLog(log)33153316print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3317return True33183319classHelpFormatter(optparse.IndentedHelpFormatter):3320def__init__(self):3321 optparse.IndentedHelpFormatter.__init__(self)33223323defformat_description(self, description):3324if description:3325return description +"\n"3326else:3327return""33283329defprintUsage(commands):3330print"usage:%s<command> [options]"% sys.argv[0]3331print""3332print"valid commands:%s"%", ".join(commands)3333print""3334print"Try%s<command> --help for command specific help."% sys.argv[0]3335print""33363337commands = {3338"debug": P4Debug,3339"submit": P4Submit,3340"commit": P4Submit,3341"sync": P4Sync,3342"rebase": P4Rebase,3343"clone": P4Clone,3344"rollback": P4RollBack,3345"branches": P4Branches3346}334733483349defmain():3350iflen(sys.argv[1:]) ==0:3351printUsage(commands.keys())3352 sys.exit(2)33533354 cmdName = sys.argv[1]3355try:3356 klass = commands[cmdName]3357 cmd =klass()3358exceptKeyError:3359print"unknown command%s"% cmdName3360print""3361printUsage(commands.keys())3362 sys.exit(2)33633364 options = cmd.options3365 cmd.gitdir = os.environ.get("GIT_DIR",None)33663367 args = sys.argv[2:]33683369 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3370if cmd.needsGit:3371 options.append(optparse.make_option("--git-dir", dest="gitdir"))33723373 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3374 options,3375 description = cmd.description,3376 formatter =HelpFormatter())33773378(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3379global verbose3380 verbose = cmd.verbose3381if cmd.needsGit:3382if cmd.gitdir ==None:3383 cmd.gitdir = os.path.abspath(".git")3384if notisValidGitDir(cmd.gitdir):3385 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3386if os.path.exists(cmd.gitdir):3387 cdup =read_pipe("git rev-parse --show-cdup").strip()3388iflen(cdup) >0:3389chdir(cdup);33903391if notisValidGitDir(cmd.gitdir):3392ifisValidGitDir(cmd.gitdir +"/.git"):3393 cmd.gitdir +="/.git"3394else:3395die("fatal: cannot locate git repository at%s"% cmd.gitdir)33963397 os.environ["GIT_DIR"] = cmd.gitdir33983399if not cmd.run(args):3400 parser.print_help()3401 sys.exit(2)340234033404if __name__ =='__main__':3405main()