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#1546 submitted =False15471548try:1549if self.edit_template(fileName):1550# read the edited message and submit1551 tmpFile =open(fileName,"rb")1552 message = tmpFile.read()1553 tmpFile.close()1554if self.isWindows:1555 message = message.replace("\r\n","\n")1556 submitTemplate = message[:message.index(separatorLine)]1557p4_write_pipe(['submit','-i'], submitTemplate)15581559if self.preserveUser:1560if p4User:1561# Get last changelist number. Cannot easily get it from1562# the submit command output as the output is1563# unmarshalled.1564 changelist = self.lastP4Changelist()1565 self.modifyChangelistUser(changelist, p4User)15661567# The rename/copy happened by applying a patch that created a1568# new file. This leaves it writable, which confuses p4.1569for f in pureRenameCopy:1570p4_sync(f,"-f")1571 submitted =True15721573finally:1574# skip this patch1575if not submitted:1576print"Submission cancelled, undoing p4 changes."1577for f in editedFiles:1578p4_revert(f)1579for f in filesToAdd:1580p4_revert(f)1581 os.remove(f)1582for f in filesToDelete:1583p4_revert(f)15841585 os.remove(fileName)1586return submitted15871588# Export git tags as p4 labels. Create a p4 label and then tag1589# with that.1590defexportGitTags(self, gitTags):1591 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1592iflen(validLabelRegexp) ==0:1593 validLabelRegexp = defaultLabelRegexp1594 m = re.compile(validLabelRegexp)15951596for name in gitTags:15971598if not m.match(name):1599if verbose:1600print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1601continue16021603# Get the p4 commit this corresponds to1604 logMessage =extractLogMessageFromGitCommit(name)1605 values =extractSettingsGitLog(logMessage)16061607if not values.has_key('change'):1608# a tag pointing to something not sent to p4; ignore1609if verbose:1610print"git tag%sdoes not give a p4 commit"% name1611continue1612else:1613 changelist = values['change']16141615# Get the tag details.1616 inHeader =True1617 isAnnotated =False1618 body = []1619for l inread_pipe_lines(["git","cat-file","-p", name]):1620 l = l.strip()1621if inHeader:1622if re.match(r'tag\s+', l):1623 isAnnotated =True1624elif re.match(r'\s*$', l):1625 inHeader =False1626continue1627else:1628 body.append(l)16291630if not isAnnotated:1631 body = ["lightweight tag imported by git p4\n"]16321633# Create the label - use the same view as the client spec we are using1634 clientSpec =getClientSpec()16351636 labelTemplate ="Label:%s\n"% name1637 labelTemplate +="Description:\n"1638for b in body:1639 labelTemplate +="\t"+ b +"\n"1640 labelTemplate +="View:\n"1641for depot_side in clientSpec.mappings:1642 labelTemplate +="\t%s\n"% depot_side16431644if self.dry_run:1645print"Would create p4 label%sfor tag"% name1646elif self.prepare_p4_only:1647print"Not creating p4 label%sfor tag due to option" \1648" --prepare-p4-only"% name1649else:1650p4_write_pipe(["label","-i"], labelTemplate)16511652# Use the label1653p4_system(["tag","-l", name] +1654["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])16551656if verbose:1657print"created p4 label for tag%s"% name16581659defrun(self, args):1660iflen(args) ==0:1661 self.master =currentGitBranch()1662eliflen(args) ==1:1663 self.master = args[0]1664if notbranchExists(self.master):1665die("Branch%sdoes not exist"% self.master)1666else:1667return False16681669if self.master:1670 allowSubmit =gitConfig("git-p4.allowSubmit")1671iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1672die("%sis not in git-p4.allowSubmit"% self.master)16731674[upstream, settings] =findUpstreamBranchPoint()1675 self.depotPath = settings['depot-paths'][0]1676iflen(self.origin) ==0:1677 self.origin = upstream16781679if self.preserveUser:1680if not self.canChangeChangelists():1681die("Cannot preserve user names without p4 super-user or admin permissions")16821683# if not set from the command line, try the config file1684if self.conflict_behavior is None:1685 val =gitConfig("git-p4.conflict")1686if val:1687if val not in self.conflict_behavior_choices:1688die("Invalid value '%s' for config git-p4.conflict"% val)1689else:1690 val ="ask"1691 self.conflict_behavior = val16921693if self.verbose:1694print"Origin branch is "+ self.origin16951696iflen(self.depotPath) ==0:1697print"Internal error: cannot locate perforce depot path from existing branches"1698 sys.exit(128)16991700 self.useClientSpec =False1701ifgitConfigBool("git-p4.useclientspec"):1702 self.useClientSpec =True1703if self.useClientSpec:1704 self.clientSpecDirs =getClientSpec()17051706# Check for the existance of P4 branches1707 branchesDetected = (len(p4BranchesInGit().keys()) >1)17081709if self.useClientSpec and not branchesDetected:1710# all files are relative to the client spec1711 self.clientPath =getClientRoot()1712else:1713 self.clientPath =p4Where(self.depotPath)17141715if self.clientPath =="":1716die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)17171718print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1719 self.oldWorkingDirectory = os.getcwd()17201721# ensure the clientPath exists1722 new_client_dir =False1723if not os.path.exists(self.clientPath):1724 new_client_dir =True1725 os.makedirs(self.clientPath)17261727chdir(self.clientPath, is_client_path=True)1728if self.dry_run:1729print"Would synchronize p4 checkout in%s"% self.clientPath1730else:1731print"Synchronizing p4 checkout..."1732if new_client_dir:1733# old one was destroyed, and maybe nobody told p41734p4_sync("...","-f")1735else:1736p4_sync("...")1737 self.check()17381739 commits = []1740if self.master:1741 commitish = self.master1742else:1743 commitish ='HEAD'17441745for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):1746 commits.append(line.strip())1747 commits.reverse()17481749if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1750 self.checkAuthorship =False1751else:1752 self.checkAuthorship =True17531754if self.preserveUser:1755 self.checkValidP4Users(commits)17561757#1758# Build up a set of options to be passed to diff when1759# submitting each commit to p4.1760#1761if self.detectRenames:1762# command-line -M arg1763 self.diffOpts ="-M"1764else:1765# If not explicitly set check the config variable1766 detectRenames =gitConfig("git-p4.detectRenames")17671768if detectRenames.lower() =="false"or detectRenames =="":1769 self.diffOpts =""1770elif detectRenames.lower() =="true":1771 self.diffOpts ="-M"1772else:1773 self.diffOpts ="-M%s"% detectRenames17741775# no command-line arg for -C or --find-copies-harder, just1776# config variables1777 detectCopies =gitConfig("git-p4.detectCopies")1778if detectCopies.lower() =="false"or detectCopies =="":1779pass1780elif detectCopies.lower() =="true":1781 self.diffOpts +=" -C"1782else:1783 self.diffOpts +=" -C%s"% detectCopies17841785ifgitConfigBool("git-p4.detectCopiesHarder"):1786 self.diffOpts +=" --find-copies-harder"17871788#1789# Apply the commits, one at a time. On failure, ask if should1790# continue to try the rest of the patches, or quit.1791#1792if self.dry_run:1793print"Would apply"1794 applied = []1795 last =len(commits) -11796for i, commit inenumerate(commits):1797if self.dry_run:1798print" ",read_pipe(["git","show","-s",1799"--format=format:%h%s", commit])1800 ok =True1801else:1802 ok = self.applyCommit(commit)1803if ok:1804 applied.append(commit)1805else:1806if self.prepare_p4_only and i < last:1807print"Processing only the first commit due to option" \1808" --prepare-p4-only"1809break1810if i < last:1811 quit =False1812while True:1813# prompt for what to do, or use the option/variable1814if self.conflict_behavior =="ask":1815print"What do you want to do?"1816 response =raw_input("[s]kip this commit but apply"1817" the rest, or [q]uit? ")1818if not response:1819continue1820elif self.conflict_behavior =="skip":1821 response ="s"1822elif self.conflict_behavior =="quit":1823 response ="q"1824else:1825die("Unknown conflict_behavior '%s'"%1826 self.conflict_behavior)18271828if response[0] =="s":1829print"Skipping this commit, but applying the rest"1830break1831if response[0] =="q":1832print"Quitting"1833 quit =True1834break1835if quit:1836break18371838chdir(self.oldWorkingDirectory)18391840if self.dry_run:1841pass1842elif self.prepare_p4_only:1843pass1844eliflen(commits) ==len(applied):1845print"All commits applied!"18461847 sync =P4Sync()1848if self.branch:1849 sync.branch = self.branch1850 sync.run([])18511852 rebase =P4Rebase()1853 rebase.rebase()18541855else:1856iflen(applied) ==0:1857print"No commits applied."1858else:1859print"Applied only the commits marked with '*':"1860for c in commits:1861if c in applied:1862 star ="*"1863else:1864 star =" "1865print star,read_pipe(["git","show","-s",1866"--format=format:%h%s", c])1867print"You will have to do 'git p4 sync' and rebase."18681869ifgitConfigBool("git-p4.exportLabels"):1870 self.exportLabels =True18711872if self.exportLabels:1873 p4Labels =getP4Labels(self.depotPath)1874 gitTags =getGitTags()18751876 missingGitTags = gitTags - p4Labels1877 self.exportGitTags(missingGitTags)18781879# exit with error unless everything applied perfectly1880iflen(commits) !=len(applied):1881 sys.exit(1)18821883return True18841885classView(object):1886"""Represent a p4 view ("p4 help views"), and map files in a1887 repo according to the view."""18881889def__init__(self, client_name):1890 self.mappings = []1891 self.client_prefix ="//%s/"% client_name1892# cache results of "p4 where" to lookup client file locations1893 self.client_spec_path_cache = {}18941895defappend(self, view_line):1896"""Parse a view line, splitting it into depot and client1897 sides. Append to self.mappings, preserving order. This1898 is only needed for tag creation."""18991900# Split the view line into exactly two words. P4 enforces1901# structure on these lines that simplifies this quite a bit.1902#1903# Either or both words may be double-quoted.1904# Single quotes do not matter.1905# Double-quote marks cannot occur inside the words.1906# A + or - prefix is also inside the quotes.1907# There are no quotes unless they contain a space.1908# The line is already white-space stripped.1909# The two words are separated by a single space.1910#1911if view_line[0] =='"':1912# First word is double quoted. Find its end.1913 close_quote_index = view_line.find('"',1)1914if close_quote_index <=0:1915die("No first-word closing quote found:%s"% view_line)1916 depot_side = view_line[1:close_quote_index]1917# skip closing quote and space1918 rhs_index = close_quote_index +1+11919else:1920 space_index = view_line.find(" ")1921if space_index <=0:1922die("No word-splitting space found:%s"% view_line)1923 depot_side = view_line[0:space_index]1924 rhs_index = space_index +119251926# prefix + means overlay on previous mapping1927if depot_side.startswith("+"):1928 depot_side = depot_side[1:]19291930# prefix - means exclude this path, leave out of mappings1931 exclude =False1932if depot_side.startswith("-"):1933 exclude =True1934 depot_side = depot_side[1:]19351936if not exclude:1937 self.mappings.append(depot_side)19381939defconvert_client_path(self, clientFile):1940# chop off //client/ part to make it relative1941if not clientFile.startswith(self.client_prefix):1942die("No prefix '%s' on clientFile '%s'"%1943(self.client_prefix, clientFile))1944return clientFile[len(self.client_prefix):]19451946defupdate_client_spec_path_cache(self, files):1947""" Caching file paths by "p4 where" batch query """19481949# List depot file paths exclude that already cached1950 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]19511952iflen(fileArgs) ==0:1953return# All files in cache19541955 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)1956for res in where_result:1957if"code"in res and res["code"] =="error":1958# assume error is "... file(s) not in client view"1959continue1960if"clientFile"not in res:1961die("No clientFile in 'p4 where' output")1962if"unmap"in res:1963# it will list all of them, but only one not unmap-ped1964continue1965ifgitConfigBool("core.ignorecase"):1966 res['depotFile'] = res['depotFile'].lower()1967 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])19681969# not found files or unmap files set to ""1970for depotFile in fileArgs:1971ifgitConfigBool("core.ignorecase"):1972 depotFile = depotFile.lower()1973if depotFile not in self.client_spec_path_cache:1974 self.client_spec_path_cache[depotFile] =""19751976defmap_in_client(self, depot_path):1977"""Return the relative location in the client where this1978 depot file should live. Returns "" if the file should1979 not be mapped in the client."""19801981ifgitConfigBool("core.ignorecase"):1982 depot_path = depot_path.lower()19831984if depot_path in self.client_spec_path_cache:1985return self.client_spec_path_cache[depot_path]19861987die("Error:%sis not found in client spec path"% depot_path )1988return""19891990classP4Sync(Command, P4UserMap):1991 delete_actions = ("delete","move/delete","purge")19921993def__init__(self):1994 Command.__init__(self)1995 P4UserMap.__init__(self)1996 self.options = [1997 optparse.make_option("--branch", dest="branch"),1998 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1999 optparse.make_option("--changesfile", dest="changesFile"),2000 optparse.make_option("--silent", dest="silent", action="store_true"),2001 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2002 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2003 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2004help="Import into refs/heads/ , not refs/remotes"),2005 optparse.make_option("--max-changes", dest="maxChanges",2006help="Maximum number of changes to import"),2007 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2008help="Internal block size to use when iteratively calling p4 changes"),2009 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2010help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2011 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2012help="Only sync files that are included in the Perforce Client Spec"),2013 optparse.make_option("-/", dest="cloneExclude",2014 action="append",type="string",2015help="exclude depot path"),2016]2017 self.description ="""Imports from Perforce into a git repository.\n2018 example:2019 //depot/my/project/ -- to import the current head2020 //depot/my/project/@all -- to import everything2021 //depot/my/project/@1,6 -- to import only from revision 1 to 620222023 (a ... is not needed in the path p4 specification, it's added implicitly)"""20242025 self.usage +=" //depot/path[@revRange]"2026 self.silent =False2027 self.createdBranches =set()2028 self.committedChanges =set()2029 self.branch =""2030 self.detectBranches =False2031 self.detectLabels =False2032 self.importLabels =False2033 self.changesFile =""2034 self.syncWithOrigin =True2035 self.importIntoRemotes =True2036 self.maxChanges =""2037 self.changes_block_size =None2038 self.keepRepoPath =False2039 self.depotPaths =None2040 self.p4BranchesInGit = []2041 self.cloneExclude = []2042 self.useClientSpec =False2043 self.useClientSpec_from_options =False2044 self.clientSpecDirs =None2045 self.tempBranches = []2046 self.tempBranchLocation ="git-p4-tmp"20472048ifgitConfig("git-p4.syncFromOrigin") =="false":2049 self.syncWithOrigin =False20502051# This is required for the "append" cloneExclude action2052defensure_value(self, attr, value):2053if nothasattr(self, attr)orgetattr(self, attr)is None:2054setattr(self, attr, value)2055returngetattr(self, attr)20562057# Force a checkpoint in fast-import and wait for it to finish2058defcheckpoint(self):2059 self.gitStream.write("checkpoint\n\n")2060 self.gitStream.write("progress checkpoint\n\n")2061 out = self.gitOutput.readline()2062if self.verbose:2063print"checkpoint finished: "+ out20642065defextractFilesFromCommit(self, commit):2066 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2067for path in self.cloneExclude]2068 files = []2069 fnum =02070while commit.has_key("depotFile%s"% fnum):2071 path = commit["depotFile%s"% fnum]20722073if[p for p in self.cloneExclude2074ifp4PathStartsWith(path, p)]:2075 found =False2076else:2077 found = [p for p in self.depotPaths2078ifp4PathStartsWith(path, p)]2079if not found:2080 fnum = fnum +12081continue20822083file= {}2084file["path"] = path2085file["rev"] = commit["rev%s"% fnum]2086file["action"] = commit["action%s"% fnum]2087file["type"] = commit["type%s"% fnum]2088 files.append(file)2089 fnum = fnum +12090return files20912092defstripRepoPath(self, path, prefixes):2093"""When streaming files, this is called to map a p4 depot path2094 to where it should go in git. The prefixes are either2095 self.depotPaths, or self.branchPrefixes in the case of2096 branch detection."""20972098if self.useClientSpec:2099# branch detection moves files up a level (the branch name)2100# from what client spec interpretation gives2101 path = self.clientSpecDirs.map_in_client(path)2102if self.detectBranches:2103for b in self.knownBranches:2104if path.startswith(b +"/"):2105 path = path[len(b)+1:]21062107elif self.keepRepoPath:2108# Preserve everything in relative path name except leading2109# //depot/; just look at first prefix as they all should2110# be in the same depot.2111 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2112ifp4PathStartsWith(path, depot):2113 path = path[len(depot):]21142115else:2116for p in prefixes:2117ifp4PathStartsWith(path, p):2118 path = path[len(p):]2119break21202121 path =wildcard_decode(path)2122return path21232124defsplitFilesIntoBranches(self, commit):2125"""Look at each depotFile in the commit to figure out to what2126 branch it belongs."""21272128if self.clientSpecDirs:2129 files = self.extractFilesFromCommit(commit)2130 self.clientSpecDirs.update_client_spec_path_cache(files)21312132 branches = {}2133 fnum =02134while commit.has_key("depotFile%s"% fnum):2135 path = commit["depotFile%s"% fnum]2136 found = [p for p in self.depotPaths2137ifp4PathStartsWith(path, p)]2138if not found:2139 fnum = fnum +12140continue21412142file= {}2143file["path"] = path2144file["rev"] = commit["rev%s"% fnum]2145file["action"] = commit["action%s"% fnum]2146file["type"] = commit["type%s"% fnum]2147 fnum = fnum +121482149# start with the full relative path where this file would2150# go in a p4 client2151if self.useClientSpec:2152 relPath = self.clientSpecDirs.map_in_client(path)2153else:2154 relPath = self.stripRepoPath(path, self.depotPaths)21552156for branch in self.knownBranches.keys():2157# add a trailing slash so that a commit into qt/4.2foo2158# doesn't end up in qt/4.2, e.g.2159if relPath.startswith(branch +"/"):2160if branch not in branches:2161 branches[branch] = []2162 branches[branch].append(file)2163break21642165return branches21662167# output one file from the P4 stream2168# - helper for streamP4Files21692170defstreamOneP4File(self,file, contents):2171 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2172if verbose:2173 sys.stderr.write("%s\n"% relPath)21742175(type_base, type_mods) =split_p4_type(file["type"])21762177 git_mode ="100644"2178if"x"in type_mods:2179 git_mode ="100755"2180if type_base =="symlink":2181 git_mode ="120000"2182# p4 print on a symlink sometimes contains "target\n";2183# if it does, remove the newline2184 data =''.join(contents)2185if not data:2186# Some version of p4 allowed creating a symlink that pointed2187# to nothing. This causes p4 errors when checking out such2188# a change, and errors here too. Work around it by ignoring2189# the bad symlink; hopefully a future change fixes it.2190print"\nIgnoring empty symlink in%s"%file['depotFile']2191return2192elif data[-1] =='\n':2193 contents = [data[:-1]]2194else:2195 contents = [data]21962197if type_base =="utf16":2198# p4 delivers different text in the python output to -G2199# than it does when using "print -o", or normal p4 client2200# operations. utf16 is converted to ascii or utf8, perhaps.2201# But ascii text saved as -t utf16 is completely mangled.2202# Invoke print -o to get the real contents.2203#2204# On windows, the newlines will always be mangled by print, so put2205# them back too. This is not needed to the cygwin windows version,2206# just the native "NT" type.2207#2208try:2209 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2210exceptExceptionas e:2211if'Translation of file content failed'instr(e):2212 type_base ='binary'2213else:2214raise e2215else:2216ifp4_version_string().find('/NT') >=0:2217 text = text.replace('\r\n','\n')2218 contents = [ text ]22192220if type_base =="apple":2221# Apple filetype files will be streamed as a concatenation of2222# its appledouble header and the contents. This is useless2223# on both macs and non-macs. If using "print -q -o xx", it2224# will create "xx" with the data, and "%xx" with the header.2225# This is also not very useful.2226#2227# Ideally, someday, this script can learn how to generate2228# appledouble files directly and import those to git, but2229# non-mac machines can never find a use for apple filetype.2230print"\nIgnoring apple filetype file%s"%file['depotFile']2231return22322233# Note that we do not try to de-mangle keywords on utf16 files,2234# even though in theory somebody may want that.2235 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2236if pattern:2237 regexp = re.compile(pattern, re.VERBOSE)2238 text =''.join(contents)2239 text = regexp.sub(r'$\1$', text)2240 contents = [ text ]22412242 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22432244# total length...2245 length =02246for d in contents:2247 length = length +len(d)22482249 self.gitStream.write("data%d\n"% length)2250for d in contents:2251 self.gitStream.write(d)2252 self.gitStream.write("\n")22532254defstreamOneP4Deletion(self,file):2255 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2256if verbose:2257 sys.stderr.write("delete%s\n"% relPath)2258 self.gitStream.write("D%s\n"% relPath)22592260# handle another chunk of streaming data2261defstreamP4FilesCb(self, marshalled):22622263# catch p4 errors and complain2264 err =None2265if"code"in marshalled:2266if marshalled["code"] =="error":2267if"data"in marshalled:2268 err = marshalled["data"].rstrip()2269if err:2270 f =None2271if self.stream_have_file_info:2272if"depotFile"in self.stream_file:2273 f = self.stream_file["depotFile"]2274# force a failure in fast-import, else an empty2275# commit will be made2276 self.gitStream.write("\n")2277 self.gitStream.write("die-now\n")2278 self.gitStream.close()2279# ignore errors, but make sure it exits first2280 self.importProcess.wait()2281if f:2282die("Error from p4 print for%s:%s"% (f, err))2283else:2284die("Error from p4 print:%s"% err)22852286if marshalled.has_key('depotFile')and self.stream_have_file_info:2287# start of a new file - output the old one first2288 self.streamOneP4File(self.stream_file, self.stream_contents)2289 self.stream_file = {}2290 self.stream_contents = []2291 self.stream_have_file_info =False22922293# pick up the new file information... for the2294# 'data' field we need to append to our array2295for k in marshalled.keys():2296if k =='data':2297 self.stream_contents.append(marshalled['data'])2298else:2299 self.stream_file[k] = marshalled[k]23002301 self.stream_have_file_info =True23022303# Stream directly from "p4 files" into "git fast-import"2304defstreamP4Files(self, files):2305 filesForCommit = []2306 filesToRead = []2307 filesToDelete = []23082309for f in files:2310 filesForCommit.append(f)2311if f['action']in self.delete_actions:2312 filesToDelete.append(f)2313else:2314 filesToRead.append(f)23152316# deleted files...2317for f in filesToDelete:2318 self.streamOneP4Deletion(f)23192320iflen(filesToRead) >0:2321 self.stream_file = {}2322 self.stream_contents = []2323 self.stream_have_file_info =False23242325# curry self argument2326defstreamP4FilesCbSelf(entry):2327 self.streamP4FilesCb(entry)23282329 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23302331p4CmdList(["-x","-","print"],2332 stdin=fileArgs,2333 cb=streamP4FilesCbSelf)23342335# do the last chunk2336if self.stream_file.has_key('depotFile'):2337 self.streamOneP4File(self.stream_file, self.stream_contents)23382339defmake_email(self, userid):2340if userid in self.users:2341return self.users[userid]2342else:2343return"%s<a@b>"% userid23442345defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2346""" Stream a p4 tag.2347 commit is either a git commit, or a fast-import mark, ":<p4commit>"2348 """23492350if verbose:2351print"writing tag%sfor commit%s"% (labelName, commit)2352 gitStream.write("tag%s\n"% labelName)2353 gitStream.write("from%s\n"% commit)23542355if labelDetails.has_key('Owner'):2356 owner = labelDetails["Owner"]2357else:2358 owner =None23592360# Try to use the owner of the p4 label, or failing that,2361# the current p4 user id.2362if owner:2363 email = self.make_email(owner)2364else:2365 email = self.make_email(self.p4UserId())2366 tagger ="%s %s %s"% (email, epoch, self.tz)23672368 gitStream.write("tagger%s\n"% tagger)23692370print"labelDetails=",labelDetails2371if labelDetails.has_key('Description'):2372 description = labelDetails['Description']2373else:2374 description ='Label from git p4'23752376 gitStream.write("data%d\n"%len(description))2377 gitStream.write(description)2378 gitStream.write("\n")23792380definClientSpec(self, path):2381if not self.clientSpecDirs:2382return True2383 inClientSpec = self.clientSpecDirs.map_in_client(path)2384if not inClientSpec and self.verbose:2385print('Ignoring file outside of client spec:{0}'.format(path))2386return inClientSpec23872388defhasBranchPrefix(self, path):2389if not self.branchPrefixes:2390return True2391 hasPrefix = [p for p in self.branchPrefixes2392ifp4PathStartsWith(path, p)]2393if hasPrefix and self.verbose:2394print('Ignoring file outside of prefix:{0}'.format(path))2395return hasPrefix23962397defcommit(self, details, files, branch, parent =""):2398 epoch = details["time"]2399 author = details["user"]24002401if self.verbose:2402print('commit into{0}'.format(branch))24032404if self.clientSpecDirs:2405 self.clientSpecDirs.update_client_spec_path_cache(files)24062407 files = [f for f in files2408if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]24092410if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2411print('Ignoring revision{0}as it would produce an empty commit.'2412.format(details['change']))2413return24142415 self.gitStream.write("commit%s\n"% branch)2416 self.gitStream.write("mark :%s\n"% details["change"])2417 self.committedChanges.add(int(details["change"]))2418 committer =""2419if author not in self.users:2420 self.getUserMapFromPerforceServer()2421 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)24222423 self.gitStream.write("committer%s\n"% committer)24242425 self.gitStream.write("data <<EOT\n")2426 self.gitStream.write(details["desc"])2427 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2428(','.join(self.branchPrefixes), details["change"]))2429iflen(details['options']) >0:2430 self.gitStream.write(": options =%s"% details['options'])2431 self.gitStream.write("]\nEOT\n\n")24322433iflen(parent) >0:2434if self.verbose:2435print"parent%s"% parent2436 self.gitStream.write("from%s\n"% parent)24372438 self.streamP4Files(files)2439 self.gitStream.write("\n")24402441 change =int(details["change"])24422443if self.labels.has_key(change):2444 label = self.labels[change]2445 labelDetails = label[0]2446 labelRevisions = label[1]2447if self.verbose:2448print"Change%sis labelled%s"% (change, labelDetails)24492450 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2451for p in self.branchPrefixes])24522453iflen(files) ==len(labelRevisions):24542455 cleanedFiles = {}2456for info in files:2457if info["action"]in self.delete_actions:2458continue2459 cleanedFiles[info["depotFile"]] = info["rev"]24602461if cleanedFiles == labelRevisions:2462 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24632464else:2465if not self.silent:2466print("Tag%sdoes not match with change%s: files do not match."2467% (labelDetails["label"], change))24682469else:2470if not self.silent:2471print("Tag%sdoes not match with change%s: file count is different."2472% (labelDetails["label"], change))24732474# Build a dictionary of changelists and labels, for "detect-labels" option.2475defgetLabels(self):2476 self.labels = {}24772478 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2479iflen(l) >0and not self.silent:2480print"Finding files belonging to labels in%s"% `self.depotPaths`24812482for output in l:2483 label = output["label"]2484 revisions = {}2485 newestChange =02486if self.verbose:2487print"Querying files for label%s"% label2488forfileinp4CmdList(["files"] +2489["%s...@%s"% (p, label)2490for p in self.depotPaths]):2491 revisions[file["depotFile"]] =file["rev"]2492 change =int(file["change"])2493if change > newestChange:2494 newestChange = change24952496 self.labels[newestChange] = [output, revisions]24972498if self.verbose:2499print"Label changes:%s"% self.labels.keys()25002501# Import p4 labels as git tags. A direct mapping does not2502# exist, so assume that if all the files are at the same revision2503# then we can use that, or it's something more complicated we should2504# just ignore.2505defimportP4Labels(self, stream, p4Labels):2506if verbose:2507print"import p4 labels: "+' '.join(p4Labels)25082509 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2510 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2511iflen(validLabelRegexp) ==0:2512 validLabelRegexp = defaultLabelRegexp2513 m = re.compile(validLabelRegexp)25142515for name in p4Labels:2516 commitFound =False25172518if not m.match(name):2519if verbose:2520print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2521continue25222523if name in ignoredP4Labels:2524continue25252526 labelDetails =p4CmdList(['label',"-o", name])[0]25272528# get the most recent changelist for each file in this label2529 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2530for p in self.depotPaths])25312532if change.has_key('change'):2533# find the corresponding git commit; take the oldest commit2534 changelist =int(change['change'])2535if changelist in self.committedChanges:2536 gitCommit =":%d"% changelist # use a fast-import mark2537 commitFound =True2538else:2539 gitCommit =read_pipe(["git","rev-list","--max-count=1",2540"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2541iflen(gitCommit) ==0:2542print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2543else:2544 commitFound =True2545 gitCommit = gitCommit.strip()25462547if commitFound:2548# Convert from p4 time format2549try:2550 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2551exceptValueError:2552print"Could not convert label time%s"% labelDetails['Update']2553 tmwhen =125542555 when =int(time.mktime(tmwhen))2556 self.streamTag(stream, name, labelDetails, gitCommit, when)2557if verbose:2558print"p4 label%smapped to git commit%s"% (name, gitCommit)2559else:2560if verbose:2561print"Label%shas no changelists - possibly deleted?"% name25622563if not commitFound:2564# We can't import this label; don't try again as it will get very2565# expensive repeatedly fetching all the files for labels that will2566# never be imported. If the label is moved in the future, the2567# ignore will need to be removed manually.2568system(["git","config","--add","git-p4.ignoredP4Labels", name])25692570defguessProjectName(self):2571for p in self.depotPaths:2572if p.endswith("/"):2573 p = p[:-1]2574 p = p[p.strip().rfind("/") +1:]2575if not p.endswith("/"):2576 p +="/"2577return p25782579defgetBranchMapping(self):2580 lostAndFoundBranches =set()25812582 user =gitConfig("git-p4.branchUser")2583iflen(user) >0:2584 command ="branches -u%s"% user2585else:2586 command ="branches"25872588for info inp4CmdList(command):2589 details =p4Cmd(["branch","-o", info["branch"]])2590 viewIdx =02591while details.has_key("View%s"% viewIdx):2592 paths = details["View%s"% viewIdx].split(" ")2593 viewIdx = viewIdx +12594# require standard //depot/foo/... //depot/bar/... mapping2595iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2596continue2597 source = paths[0]2598 destination = paths[1]2599## HACK2600ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2601 source = source[len(self.depotPaths[0]):-4]2602 destination = destination[len(self.depotPaths[0]):-4]26032604if destination in self.knownBranches:2605if not self.silent:2606print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2607print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2608continue26092610 self.knownBranches[destination] = source26112612 lostAndFoundBranches.discard(destination)26132614if source not in self.knownBranches:2615 lostAndFoundBranches.add(source)26162617# Perforce does not strictly require branches to be defined, so we also2618# check git config for a branch list.2619#2620# Example of branch definition in git config file:2621# [git-p4]2622# branchList=main:branchA2623# branchList=main:branchB2624# branchList=branchA:branchC2625 configBranches =gitConfigList("git-p4.branchList")2626for branch in configBranches:2627if branch:2628(source, destination) = branch.split(":")2629 self.knownBranches[destination] = source26302631 lostAndFoundBranches.discard(destination)26322633if source not in self.knownBranches:2634 lostAndFoundBranches.add(source)263526362637for branch in lostAndFoundBranches:2638 self.knownBranches[branch] = branch26392640defgetBranchMappingFromGitBranches(self):2641 branches =p4BranchesInGit(self.importIntoRemotes)2642for branch in branches.keys():2643if branch =="master":2644 branch ="main"2645else:2646 branch = branch[len(self.projectName):]2647 self.knownBranches[branch] = branch26482649defupdateOptionDict(self, d):2650 option_keys = {}2651if self.keepRepoPath:2652 option_keys['keepRepoPath'] =126532654 d["options"] =' '.join(sorted(option_keys.keys()))26552656defreadOptions(self, d):2657 self.keepRepoPath = (d.has_key('options')2658and('keepRepoPath'in d['options']))26592660defgitRefForBranch(self, branch):2661if branch =="main":2662return self.refPrefix +"master"26632664iflen(branch) <=0:2665return branch26662667return self.refPrefix + self.projectName + branch26682669defgitCommitByP4Change(self, ref, change):2670if self.verbose:2671print"looking in ref "+ ref +" for change%susing bisect..."% change26722673 earliestCommit =""2674 latestCommit =parseRevision(ref)26752676while True:2677if self.verbose:2678print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2679 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2680iflen(next) ==0:2681if self.verbose:2682print"argh"2683return""2684 log =extractLogMessageFromGitCommit(next)2685 settings =extractSettingsGitLog(log)2686 currentChange =int(settings['change'])2687if self.verbose:2688print"current change%s"% currentChange26892690if currentChange == change:2691if self.verbose:2692print"found%s"% next2693return next26942695if currentChange < change:2696 earliestCommit ="^%s"% next2697else:2698 latestCommit ="%s"% next26992700return""27012702defimportNewBranch(self, branch, maxChange):2703# make fast-import flush all changes to disk and update the refs using the checkpoint2704# command so that we can try to find the branch parent in the git history2705 self.gitStream.write("checkpoint\n\n");2706 self.gitStream.flush();2707 branchPrefix = self.depotPaths[0] + branch +"/"2708range="@1,%s"% maxChange2709#print "prefix" + branchPrefix2710 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2711iflen(changes) <=0:2712return False2713 firstChange = changes[0]2714#print "first change in branch: %s" % firstChange2715 sourceBranch = self.knownBranches[branch]2716 sourceDepotPath = self.depotPaths[0] + sourceBranch2717 sourceRef = self.gitRefForBranch(sourceBranch)2718#print "source " + sourceBranch27192720 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2721#print "branch parent: %s" % branchParentChange2722 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2723iflen(gitParent) >0:2724 self.initialParents[self.gitRefForBranch(branch)] = gitParent2725#print "parent git commit: %s" % gitParent27262727 self.importChanges(changes)2728return True27292730defsearchParent(self, parent, branch, target):2731 parentFound =False2732for blob inread_pipe_lines(["git","rev-list","--reverse",2733"--no-merges", parent]):2734 blob = blob.strip()2735iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2736 parentFound =True2737if self.verbose:2738print"Found parent of%sin commit%s"% (branch, blob)2739break2740if parentFound:2741return blob2742else:2743return None27442745defimportChanges(self, changes):2746 cnt =12747for change in changes:2748 description =p4_describe(change)2749 self.updateOptionDict(description)27502751if not self.silent:2752 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2753 sys.stdout.flush()2754 cnt = cnt +127552756try:2757if self.detectBranches:2758 branches = self.splitFilesIntoBranches(description)2759for branch in branches.keys():2760## HACK --hwn2761 branchPrefix = self.depotPaths[0] + branch +"/"2762 self.branchPrefixes = [ branchPrefix ]27632764 parent =""27652766 filesForCommit = branches[branch]27672768if self.verbose:2769print"branch is%s"% branch27702771 self.updatedBranches.add(branch)27722773if branch not in self.createdBranches:2774 self.createdBranches.add(branch)2775 parent = self.knownBranches[branch]2776if parent == branch:2777 parent =""2778else:2779 fullBranch = self.projectName + branch2780if fullBranch not in self.p4BranchesInGit:2781if not self.silent:2782print("\nImporting new branch%s"% fullBranch);2783if self.importNewBranch(branch, change -1):2784 parent =""2785 self.p4BranchesInGit.append(fullBranch)2786if not self.silent:2787print("\nResuming with change%s"% change);27882789if self.verbose:2790print"parent determined through known branches:%s"% parent27912792 branch = self.gitRefForBranch(branch)2793 parent = self.gitRefForBranch(parent)27942795if self.verbose:2796print"looking for initial parent for%s; current parent is%s"% (branch, parent)27972798iflen(parent) ==0and branch in self.initialParents:2799 parent = self.initialParents[branch]2800del self.initialParents[branch]28012802 blob =None2803iflen(parent) >0:2804 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2805if self.verbose:2806print"Creating temporary branch: "+ tempBranch2807 self.commit(description, filesForCommit, tempBranch)2808 self.tempBranches.append(tempBranch)2809 self.checkpoint()2810 blob = self.searchParent(parent, branch, tempBranch)2811if blob:2812 self.commit(description, filesForCommit, branch, blob)2813else:2814if self.verbose:2815print"Parent of%snot found. Committing into head of%s"% (branch, parent)2816 self.commit(description, filesForCommit, branch, parent)2817else:2818 files = self.extractFilesFromCommit(description)2819 self.commit(description, files, self.branch,2820 self.initialParent)2821# only needed once, to connect to the previous commit2822 self.initialParent =""2823exceptIOError:2824print self.gitError.read()2825 sys.exit(1)28262827defimportHeadRevision(self, revision):2828print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)28292830 details = {}2831 details["user"] ="git perforce import user"2832 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2833% (' '.join(self.depotPaths), revision))2834 details["change"] = revision2835 newestRevision =028362837 fileCnt =02838 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28392840for info inp4CmdList(["files"] + fileArgs):28412842if'code'in info and info['code'] =='error':2843 sys.stderr.write("p4 returned an error:%s\n"2844% info['data'])2845if info['data'].find("must refer to client") >=0:2846 sys.stderr.write("This particular p4 error is misleading.\n")2847 sys.stderr.write("Perhaps the depot path was misspelled.\n");2848 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2849 sys.exit(1)2850if'p4ExitCode'in info:2851 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2852 sys.exit(1)285328542855 change =int(info["change"])2856if change > newestRevision:2857 newestRevision = change28582859if info["action"]in self.delete_actions:2860# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2861#fileCnt = fileCnt + 12862continue28632864for prop in["depotFile","rev","action","type"]:2865 details["%s%s"% (prop, fileCnt)] = info[prop]28662867 fileCnt = fileCnt +128682869 details["change"] = newestRevision28702871# Use time from top-most change so that all git p4 clones of2872# the same p4 repo have the same commit SHA1s.2873 res =p4_describe(newestRevision)2874 details["time"] = res["time"]28752876 self.updateOptionDict(details)2877try:2878 self.commit(details, self.extractFilesFromCommit(details), self.branch)2879exceptIOError:2880print"IO error with git fast-import. Is your git version recent enough?"2881print self.gitError.read()288228832884defrun(self, args):2885 self.depotPaths = []2886 self.changeRange =""2887 self.previousDepotPaths = []2888 self.hasOrigin =False28892890# map from branch depot path to parent branch2891 self.knownBranches = {}2892 self.initialParents = {}28932894if self.importIntoRemotes:2895 self.refPrefix ="refs/remotes/p4/"2896else:2897 self.refPrefix ="refs/heads/p4/"28982899if self.syncWithOrigin:2900 self.hasOrigin =originP4BranchesExist()2901if self.hasOrigin:2902if not self.silent:2903print'Syncing with origin first, using "git fetch origin"'2904system("git fetch origin")29052906 branch_arg_given =bool(self.branch)2907iflen(self.branch) ==0:2908 self.branch = self.refPrefix +"master"2909ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2910system("git update-ref%srefs/heads/p4"% self.branch)2911system("git branch -D p4")29122913# accept either the command-line option, or the configuration variable2914if self.useClientSpec:2915# will use this after clone to set the variable2916 self.useClientSpec_from_options =True2917else:2918ifgitConfigBool("git-p4.useclientspec"):2919 self.useClientSpec =True2920if self.useClientSpec:2921 self.clientSpecDirs =getClientSpec()29222923# TODO: should always look at previous commits,2924# merge with previous imports, if possible.2925if args == []:2926if self.hasOrigin:2927createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)29282929# branches holds mapping from branch name to sha12930 branches =p4BranchesInGit(self.importIntoRemotes)29312932# restrict to just this one, disabling detect-branches2933if branch_arg_given:2934 short = self.branch.split("/")[-1]2935if short in branches:2936 self.p4BranchesInGit = [ short ]2937else:2938 self.p4BranchesInGit = branches.keys()29392940iflen(self.p4BranchesInGit) >1:2941if not self.silent:2942print"Importing from/into multiple branches"2943 self.detectBranches =True2944for branch in branches.keys():2945 self.initialParents[self.refPrefix + branch] = \2946 branches[branch]29472948if self.verbose:2949print"branches:%s"% self.p4BranchesInGit29502951 p4Change =02952for branch in self.p4BranchesInGit:2953 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29542955 settings =extractSettingsGitLog(logMsg)29562957 self.readOptions(settings)2958if(settings.has_key('depot-paths')2959and settings.has_key('change')):2960 change =int(settings['change']) +12961 p4Change =max(p4Change, change)29622963 depotPaths =sorted(settings['depot-paths'])2964if self.previousDepotPaths == []:2965 self.previousDepotPaths = depotPaths2966else:2967 paths = []2968for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2969 prev_list = prev.split("/")2970 cur_list = cur.split("/")2971for i inrange(0,min(len(cur_list),len(prev_list))):2972if cur_list[i] <> prev_list[i]:2973 i = i -12974break29752976 paths.append("/".join(cur_list[:i +1]))29772978 self.previousDepotPaths = paths29792980if p4Change >0:2981 self.depotPaths =sorted(self.previousDepotPaths)2982 self.changeRange ="@%s,#head"% p4Change2983if not self.silent and not self.detectBranches:2984print"Performing incremental import into%sgit branch"% self.branch29852986# accept multiple ref name abbreviations:2987# refs/foo/bar/branch -> use it exactly2988# p4/branch -> prepend refs/remotes/ or refs/heads/2989# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2990if not self.branch.startswith("refs/"):2991if self.importIntoRemotes:2992 prepend ="refs/remotes/"2993else:2994 prepend ="refs/heads/"2995if not self.branch.startswith("p4/"):2996 prepend +="p4/"2997 self.branch = prepend + self.branch29982999iflen(args) ==0and self.depotPaths:3000if not self.silent:3001print"Depot paths:%s"%' '.join(self.depotPaths)3002else:3003if self.depotPaths and self.depotPaths != args:3004print("previous import used depot path%sand now%swas specified. "3005"This doesn't work!"% (' '.join(self.depotPaths),3006' '.join(args)))3007 sys.exit(1)30083009 self.depotPaths =sorted(args)30103011 revision =""3012 self.users = {}30133014# Make sure no revision specifiers are used when --changesfile3015# is specified.3016 bad_changesfile =False3017iflen(self.changesFile) >0:3018for p in self.depotPaths:3019if p.find("@") >=0or p.find("#") >=0:3020 bad_changesfile =True3021break3022if bad_changesfile:3023die("Option --changesfile is incompatible with revision specifiers")30243025 newPaths = []3026for p in self.depotPaths:3027if p.find("@") != -1:3028 atIdx = p.index("@")3029 self.changeRange = p[atIdx:]3030if self.changeRange =="@all":3031 self.changeRange =""3032elif','not in self.changeRange:3033 revision = self.changeRange3034 self.changeRange =""3035 p = p[:atIdx]3036elif p.find("#") != -1:3037 hashIdx = p.index("#")3038 revision = p[hashIdx:]3039 p = p[:hashIdx]3040elif self.previousDepotPaths == []:3041# pay attention to changesfile, if given, else import3042# the entire p4 tree at the head revision3043iflen(self.changesFile) ==0:3044 revision ="#head"30453046 p = re.sub("\.\.\.$","", p)3047if not p.endswith("/"):3048 p +="/"30493050 newPaths.append(p)30513052 self.depotPaths = newPaths30533054# --detect-branches may change this for each branch3055 self.branchPrefixes = self.depotPaths30563057 self.loadUserMapFromCache()3058 self.labels = {}3059if self.detectLabels:3060 self.getLabels();30613062if self.detectBranches:3063## FIXME - what's a P4 projectName ?3064 self.projectName = self.guessProjectName()30653066if self.hasOrigin:3067 self.getBranchMappingFromGitBranches()3068else:3069 self.getBranchMapping()3070if self.verbose:3071print"p4-git branches:%s"% self.p4BranchesInGit3072print"initial parents:%s"% self.initialParents3073for b in self.p4BranchesInGit:3074if b !="master":30753076## FIXME3077 b = b[len(self.projectName):]3078 self.createdBranches.add(b)30793080 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30813082 self.importProcess = subprocess.Popen(["git","fast-import"],3083 stdin=subprocess.PIPE,3084 stdout=subprocess.PIPE,3085 stderr=subprocess.PIPE);3086 self.gitOutput = self.importProcess.stdout3087 self.gitStream = self.importProcess.stdin3088 self.gitError = self.importProcess.stderr30893090if revision:3091 self.importHeadRevision(revision)3092else:3093 changes = []30943095iflen(self.changesFile) >0:3096 output =open(self.changesFile).readlines()3097 changeSet =set()3098for line in output:3099 changeSet.add(int(line))31003101for change in changeSet:3102 changes.append(change)31033104 changes.sort()3105else:3106# catch "git p4 sync" with no new branches, in a repo that3107# does not have any existing p4 branches3108iflen(args) ==0:3109if not self.p4BranchesInGit:3110die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")31113112# The default branch is master, unless --branch is used to3113# specify something else. Make sure it exists, or complain3114# nicely about how to use --branch.3115if not self.detectBranches:3116if notbranch_exists(self.branch):3117if branch_arg_given:3118die("Error: branch%sdoes not exist."% self.branch)3119else:3120die("Error: no branch%s; perhaps specify one with --branch."%3121 self.branch)31223123if self.verbose:3124print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3125 self.changeRange)3126 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)31273128iflen(self.maxChanges) >0:3129 changes = changes[:min(int(self.maxChanges),len(changes))]31303131iflen(changes) ==0:3132if not self.silent:3133print"No changes to import!"3134else:3135if not self.silent and not self.detectBranches:3136print"Import destination:%s"% self.branch31373138 self.updatedBranches =set()31393140if not self.detectBranches:3141if args:3142# start a new branch3143 self.initialParent =""3144else:3145# build on a previous revision3146 self.initialParent =parseRevision(self.branch)31473148 self.importChanges(changes)31493150if not self.silent:3151print""3152iflen(self.updatedBranches) >0:3153 sys.stdout.write("Updated branches: ")3154for b in self.updatedBranches:3155 sys.stdout.write("%s"% b)3156 sys.stdout.write("\n")31573158ifgitConfigBool("git-p4.importLabels"):3159 self.importLabels =True31603161if self.importLabels:3162 p4Labels =getP4Labels(self.depotPaths)3163 gitTags =getGitTags()31643165 missingP4Labels = p4Labels - gitTags3166 self.importP4Labels(self.gitStream, missingP4Labels)31673168 self.gitStream.close()3169if self.importProcess.wait() !=0:3170die("fast-import failed:%s"% self.gitError.read())3171 self.gitOutput.close()3172 self.gitError.close()31733174# Cleanup temporary branches created during import3175if self.tempBranches != []:3176for branch in self.tempBranches:3177read_pipe("git update-ref -d%s"% branch)3178 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31793180# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3181# a convenient shortcut refname "p4".3182if self.importIntoRemotes:3183 head_ref = self.refPrefix +"HEAD"3184if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3185system(["git","symbolic-ref", head_ref, self.branch])31863187return True31883189classP4Rebase(Command):3190def__init__(self):3191 Command.__init__(self)3192 self.options = [3193 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3194]3195 self.importLabels =False3196 self.description = ("Fetches the latest revision from perforce and "3197+"rebases the current work (branch) against it")31983199defrun(self, args):3200 sync =P4Sync()3201 sync.importLabels = self.importLabels3202 sync.run([])32033204return self.rebase()32053206defrebase(self):3207if os.system("git update-index --refresh") !=0:3208die("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.");3209iflen(read_pipe("git diff-index HEAD --")) >0:3210die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");32113212[upstream, settings] =findUpstreamBranchPoint()3213iflen(upstream) ==0:3214die("Cannot find upstream branchpoint for rebase")32153216# the branchpoint may be p4/foo~3, so strip off the parent3217 upstream = re.sub("~[0-9]+$","", upstream)32183219print"Rebasing the current branch onto%s"% upstream3220 oldHead =read_pipe("git rev-parse HEAD").strip()3221system("git rebase%s"% upstream)3222system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3223return True32243225classP4Clone(P4Sync):3226def__init__(self):3227 P4Sync.__init__(self)3228 self.description ="Creates a new git repository and imports from Perforce into it"3229 self.usage ="usage: %prog [options] //depot/path[@revRange]"3230 self.options += [3231 optparse.make_option("--destination", dest="cloneDestination",3232 action='store', default=None,3233help="where to leave result of the clone"),3234 optparse.make_option("--bare", dest="cloneBare",3235 action="store_true", default=False),3236]3237 self.cloneDestination =None3238 self.needsGit =False3239 self.cloneBare =False32403241defdefaultDestination(self, args):3242## TODO: use common prefix of args?3243 depotPath = args[0]3244 depotDir = re.sub("(@[^@]*)$","", depotPath)3245 depotDir = re.sub("(#[^#]*)$","", depotDir)3246 depotDir = re.sub(r"\.\.\.$","", depotDir)3247 depotDir = re.sub(r"/$","", depotDir)3248return os.path.split(depotDir)[1]32493250defrun(self, args):3251iflen(args) <1:3252return False32533254if self.keepRepoPath and not self.cloneDestination:3255 sys.stderr.write("Must specify destination for --keep-path\n")3256 sys.exit(1)32573258 depotPaths = args32593260if not self.cloneDestination andlen(depotPaths) >1:3261 self.cloneDestination = depotPaths[-1]3262 depotPaths = depotPaths[:-1]32633264 self.cloneExclude = ["/"+p for p in self.cloneExclude]3265for p in depotPaths:3266if not p.startswith("//"):3267 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3268return False32693270if not self.cloneDestination:3271 self.cloneDestination = self.defaultDestination(args)32723273print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32743275if not os.path.exists(self.cloneDestination):3276 os.makedirs(self.cloneDestination)3277chdir(self.cloneDestination)32783279 init_cmd = ["git","init"]3280if self.cloneBare:3281 init_cmd.append("--bare")3282 retcode = subprocess.call(init_cmd)3283if retcode:3284raiseCalledProcessError(retcode, init_cmd)32853286if not P4Sync.run(self, depotPaths):3287return False32883289# create a master branch and check out a work tree3290ifgitBranchExists(self.branch):3291system(["git","branch","master", self.branch ])3292if not self.cloneBare:3293system(["git","checkout","-f"])3294else:3295print'Not checking out any branch, use ' \3296'"git checkout -q -b master <branch>"'32973298# auto-set this variable if invoked with --use-client-spec3299if self.useClientSpec_from_options:3300system("git config --bool git-p4.useclientspec true")33013302return True33033304classP4Branches(Command):3305def__init__(self):3306 Command.__init__(self)3307 self.options = [ ]3308 self.description = ("Shows the git branches that hold imports and their "3309+"corresponding perforce depot paths")3310 self.verbose =False33113312defrun(self, args):3313iforiginP4BranchesExist():3314createOrUpdateBranchesFromOrigin()33153316 cmdline ="git rev-parse --symbolic "3317 cmdline +=" --remotes"33183319for line inread_pipe_lines(cmdline):3320 line = line.strip()33213322if not line.startswith('p4/')or line =="p4/HEAD":3323continue3324 branch = line33253326 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3327 settings =extractSettingsGitLog(log)33283329print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3330return True33313332classHelpFormatter(optparse.IndentedHelpFormatter):3333def__init__(self):3334 optparse.IndentedHelpFormatter.__init__(self)33353336defformat_description(self, description):3337if description:3338return description +"\n"3339else:3340return""33413342defprintUsage(commands):3343print"usage:%s<command> [options]"% sys.argv[0]3344print""3345print"valid commands:%s"%", ".join(commands)3346print""3347print"Try%s<command> --help for command specific help."% sys.argv[0]3348print""33493350commands = {3351"debug": P4Debug,3352"submit": P4Submit,3353"commit": P4Submit,3354"sync": P4Sync,3355"rebase": P4Rebase,3356"clone": P4Clone,3357"rollback": P4RollBack,3358"branches": P4Branches3359}336033613362defmain():3363iflen(sys.argv[1:]) ==0:3364printUsage(commands.keys())3365 sys.exit(2)33663367 cmdName = sys.argv[1]3368try:3369 klass = commands[cmdName]3370 cmd =klass()3371exceptKeyError:3372print"unknown command%s"% cmdName3373print""3374printUsage(commands.keys())3375 sys.exit(2)33763377 options = cmd.options3378 cmd.gitdir = os.environ.get("GIT_DIR",None)33793380 args = sys.argv[2:]33813382 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3383if cmd.needsGit:3384 options.append(optparse.make_option("--git-dir", dest="gitdir"))33853386 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3387 options,3388 description = cmd.description,3389 formatter =HelpFormatter())33903391(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3392global verbose3393 verbose = cmd.verbose3394if cmd.needsGit:3395if cmd.gitdir ==None:3396 cmd.gitdir = os.path.abspath(".git")3397if notisValidGitDir(cmd.gitdir):3398 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3399if os.path.exists(cmd.gitdir):3400 cdup =read_pipe("git rev-parse --show-cdup").strip()3401iflen(cdup) >0:3402chdir(cdup);34033404if notisValidGitDir(cmd.gitdir):3405ifisValidGitDir(cmd.gitdir +"/.git"):3406 cmd.gitdir +="/.git"3407else:3408die("fatal: cannot locate git repository at%s"% cmd.gitdir)34093410 os.environ["GIT_DIR"] = cmd.gitdir34113412if not cmd.run(args):3413 parser.print_help()3414 sys.exit(2)341534163417if __name__ =='__main__':3418main()