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# if using a client spec, only add the files that have2311# a path in the client2312if self.clientSpecDirs:2313if self.clientSpecDirs.map_in_client(f['path']) =="":2314continue23152316 filesForCommit.append(f)2317if f['action']in self.delete_actions:2318 filesToDelete.append(f)2319else:2320 filesToRead.append(f)23212322# deleted files...2323for f in filesToDelete:2324 self.streamOneP4Deletion(f)23252326iflen(filesToRead) >0:2327 self.stream_file = {}2328 self.stream_contents = []2329 self.stream_have_file_info =False23302331# curry self argument2332defstreamP4FilesCbSelf(entry):2333 self.streamP4FilesCb(entry)23342335 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23362337p4CmdList(["-x","-","print"],2338 stdin=fileArgs,2339 cb=streamP4FilesCbSelf)23402341# do the last chunk2342if self.stream_file.has_key('depotFile'):2343 self.streamOneP4File(self.stream_file, self.stream_contents)23442345defmake_email(self, userid):2346if userid in self.users:2347return self.users[userid]2348else:2349return"%s<a@b>"% userid23502351defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2352""" Stream a p4 tag.2353 commit is either a git commit, or a fast-import mark, ":<p4commit>"2354 """23552356if verbose:2357print"writing tag%sfor commit%s"% (labelName, commit)2358 gitStream.write("tag%s\n"% labelName)2359 gitStream.write("from%s\n"% commit)23602361if labelDetails.has_key('Owner'):2362 owner = labelDetails["Owner"]2363else:2364 owner =None23652366# Try to use the owner of the p4 label, or failing that,2367# the current p4 user id.2368if owner:2369 email = self.make_email(owner)2370else:2371 email = self.make_email(self.p4UserId())2372 tagger ="%s %s %s"% (email, epoch, self.tz)23732374 gitStream.write("tagger%s\n"% tagger)23752376print"labelDetails=",labelDetails2377if labelDetails.has_key('Description'):2378 description = labelDetails['Description']2379else:2380 description ='Label from git p4'23812382 gitStream.write("data%d\n"%len(description))2383 gitStream.write(description)2384 gitStream.write("\n")23852386defcommit(self, details, files, branch, parent =""):2387 epoch = details["time"]2388 author = details["user"]23892390if self.verbose:2391print"commit into%s"% branch23922393# start with reading files; if that fails, we should not2394# create a commit.2395 new_files = []2396for f in files:2397if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2398 new_files.append(f)2399else:2400 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])24012402if self.clientSpecDirs:2403 self.clientSpecDirs.update_client_spec_path_cache(files)24042405 self.gitStream.write("commit%s\n"% branch)2406 self.gitStream.write("mark :%s\n"% details["change"])2407 self.committedChanges.add(int(details["change"]))2408 committer =""2409if author not in self.users:2410 self.getUserMapFromPerforceServer()2411 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)24122413 self.gitStream.write("committer%s\n"% committer)24142415 self.gitStream.write("data <<EOT\n")2416 self.gitStream.write(details["desc"])2417 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2418(','.join(self.branchPrefixes), details["change"]))2419iflen(details['options']) >0:2420 self.gitStream.write(": options =%s"% details['options'])2421 self.gitStream.write("]\nEOT\n\n")24222423iflen(parent) >0:2424if self.verbose:2425print"parent%s"% parent2426 self.gitStream.write("from%s\n"% parent)24272428 self.streamP4Files(new_files)2429 self.gitStream.write("\n")24302431 change =int(details["change"])24322433if self.labels.has_key(change):2434 label = self.labels[change]2435 labelDetails = label[0]2436 labelRevisions = label[1]2437if self.verbose:2438print"Change%sis labelled%s"% (change, labelDetails)24392440 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2441for p in self.branchPrefixes])24422443iflen(files) ==len(labelRevisions):24442445 cleanedFiles = {}2446for info in files:2447if info["action"]in self.delete_actions:2448continue2449 cleanedFiles[info["depotFile"]] = info["rev"]24502451if cleanedFiles == labelRevisions:2452 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24532454else:2455if not self.silent:2456print("Tag%sdoes not match with change%s: files do not match."2457% (labelDetails["label"], change))24582459else:2460if not self.silent:2461print("Tag%sdoes not match with change%s: file count is different."2462% (labelDetails["label"], change))24632464# Build a dictionary of changelists and labels, for "detect-labels" option.2465defgetLabels(self):2466 self.labels = {}24672468 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2469iflen(l) >0and not self.silent:2470print"Finding files belonging to labels in%s"% `self.depotPaths`24712472for output in l:2473 label = output["label"]2474 revisions = {}2475 newestChange =02476if self.verbose:2477print"Querying files for label%s"% label2478forfileinp4CmdList(["files"] +2479["%s...@%s"% (p, label)2480for p in self.depotPaths]):2481 revisions[file["depotFile"]] =file["rev"]2482 change =int(file["change"])2483if change > newestChange:2484 newestChange = change24852486 self.labels[newestChange] = [output, revisions]24872488if self.verbose:2489print"Label changes:%s"% self.labels.keys()24902491# Import p4 labels as git tags. A direct mapping does not2492# exist, so assume that if all the files are at the same revision2493# then we can use that, or it's something more complicated we should2494# just ignore.2495defimportP4Labels(self, stream, p4Labels):2496if verbose:2497print"import p4 labels: "+' '.join(p4Labels)24982499 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2500 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2501iflen(validLabelRegexp) ==0:2502 validLabelRegexp = defaultLabelRegexp2503 m = re.compile(validLabelRegexp)25042505for name in p4Labels:2506 commitFound =False25072508if not m.match(name):2509if verbose:2510print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2511continue25122513if name in ignoredP4Labels:2514continue25152516 labelDetails =p4CmdList(['label',"-o", name])[0]25172518# get the most recent changelist for each file in this label2519 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2520for p in self.depotPaths])25212522if change.has_key('change'):2523# find the corresponding git commit; take the oldest commit2524 changelist =int(change['change'])2525if changelist in self.committedChanges:2526 gitCommit =":%d"% changelist # use a fast-import mark2527 commitFound =True2528else:2529 gitCommit =read_pipe(["git","rev-list","--max-count=1",2530"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2531iflen(gitCommit) ==0:2532print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2533else:2534 commitFound =True2535 gitCommit = gitCommit.strip()25362537if commitFound:2538# Convert from p4 time format2539try:2540 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2541exceptValueError:2542print"Could not convert label time%s"% labelDetails['Update']2543 tmwhen =125442545 when =int(time.mktime(tmwhen))2546 self.streamTag(stream, name, labelDetails, gitCommit, when)2547if verbose:2548print"p4 label%smapped to git commit%s"% (name, gitCommit)2549else:2550if verbose:2551print"Label%shas no changelists - possibly deleted?"% name25522553if not commitFound:2554# We can't import this label; don't try again as it will get very2555# expensive repeatedly fetching all the files for labels that will2556# never be imported. If the label is moved in the future, the2557# ignore will need to be removed manually.2558system(["git","config","--add","git-p4.ignoredP4Labels", name])25592560defguessProjectName(self):2561for p in self.depotPaths:2562if p.endswith("/"):2563 p = p[:-1]2564 p = p[p.strip().rfind("/") +1:]2565if not p.endswith("/"):2566 p +="/"2567return p25682569defgetBranchMapping(self):2570 lostAndFoundBranches =set()25712572 user =gitConfig("git-p4.branchUser")2573iflen(user) >0:2574 command ="branches -u%s"% user2575else:2576 command ="branches"25772578for info inp4CmdList(command):2579 details =p4Cmd(["branch","-o", info["branch"]])2580 viewIdx =02581while details.has_key("View%s"% viewIdx):2582 paths = details["View%s"% viewIdx].split(" ")2583 viewIdx = viewIdx +12584# require standard //depot/foo/... //depot/bar/... mapping2585iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2586continue2587 source = paths[0]2588 destination = paths[1]2589## HACK2590ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2591 source = source[len(self.depotPaths[0]):-4]2592 destination = destination[len(self.depotPaths[0]):-4]25932594if destination in self.knownBranches:2595if not self.silent:2596print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2597print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2598continue25992600 self.knownBranches[destination] = source26012602 lostAndFoundBranches.discard(destination)26032604if source not in self.knownBranches:2605 lostAndFoundBranches.add(source)26062607# Perforce does not strictly require branches to be defined, so we also2608# check git config for a branch list.2609#2610# Example of branch definition in git config file:2611# [git-p4]2612# branchList=main:branchA2613# branchList=main:branchB2614# branchList=branchA:branchC2615 configBranches =gitConfigList("git-p4.branchList")2616for branch in configBranches:2617if branch:2618(source, destination) = branch.split(":")2619 self.knownBranches[destination] = source26202621 lostAndFoundBranches.discard(destination)26222623if source not in self.knownBranches:2624 lostAndFoundBranches.add(source)262526262627for branch in lostAndFoundBranches:2628 self.knownBranches[branch] = branch26292630defgetBranchMappingFromGitBranches(self):2631 branches =p4BranchesInGit(self.importIntoRemotes)2632for branch in branches.keys():2633if branch =="master":2634 branch ="main"2635else:2636 branch = branch[len(self.projectName):]2637 self.knownBranches[branch] = branch26382639defupdateOptionDict(self, d):2640 option_keys = {}2641if self.keepRepoPath:2642 option_keys['keepRepoPath'] =126432644 d["options"] =' '.join(sorted(option_keys.keys()))26452646defreadOptions(self, d):2647 self.keepRepoPath = (d.has_key('options')2648and('keepRepoPath'in d['options']))26492650defgitRefForBranch(self, branch):2651if branch =="main":2652return self.refPrefix +"master"26532654iflen(branch) <=0:2655return branch26562657return self.refPrefix + self.projectName + branch26582659defgitCommitByP4Change(self, ref, change):2660if self.verbose:2661print"looking in ref "+ ref +" for change%susing bisect..."% change26622663 earliestCommit =""2664 latestCommit =parseRevision(ref)26652666while True:2667if self.verbose:2668print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2669 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2670iflen(next) ==0:2671if self.verbose:2672print"argh"2673return""2674 log =extractLogMessageFromGitCommit(next)2675 settings =extractSettingsGitLog(log)2676 currentChange =int(settings['change'])2677if self.verbose:2678print"current change%s"% currentChange26792680if currentChange == change:2681if self.verbose:2682print"found%s"% next2683return next26842685if currentChange < change:2686 earliestCommit ="^%s"% next2687else:2688 latestCommit ="%s"% next26892690return""26912692defimportNewBranch(self, branch, maxChange):2693# make fast-import flush all changes to disk and update the refs using the checkpoint2694# command so that we can try to find the branch parent in the git history2695 self.gitStream.write("checkpoint\n\n");2696 self.gitStream.flush();2697 branchPrefix = self.depotPaths[0] + branch +"/"2698range="@1,%s"% maxChange2699#print "prefix" + branchPrefix2700 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)2701iflen(changes) <=0:2702return False2703 firstChange = changes[0]2704#print "first change in branch: %s" % firstChange2705 sourceBranch = self.knownBranches[branch]2706 sourceDepotPath = self.depotPaths[0] + sourceBranch2707 sourceRef = self.gitRefForBranch(sourceBranch)2708#print "source " + sourceBranch27092710 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2711#print "branch parent: %s" % branchParentChange2712 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2713iflen(gitParent) >0:2714 self.initialParents[self.gitRefForBranch(branch)] = gitParent2715#print "parent git commit: %s" % gitParent27162717 self.importChanges(changes)2718return True27192720defsearchParent(self, parent, branch, target):2721 parentFound =False2722for blob inread_pipe_lines(["git","rev-list","--reverse",2723"--no-merges", parent]):2724 blob = blob.strip()2725iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2726 parentFound =True2727if self.verbose:2728print"Found parent of%sin commit%s"% (branch, blob)2729break2730if parentFound:2731return blob2732else:2733return None27342735defimportChanges(self, changes):2736 cnt =12737for change in changes:2738 description =p4_describe(change)2739 self.updateOptionDict(description)27402741if not self.silent:2742 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2743 sys.stdout.flush()2744 cnt = cnt +127452746try:2747if self.detectBranches:2748 branches = self.splitFilesIntoBranches(description)2749for branch in branches.keys():2750## HACK --hwn2751 branchPrefix = self.depotPaths[0] + branch +"/"2752 self.branchPrefixes = [ branchPrefix ]27532754 parent =""27552756 filesForCommit = branches[branch]27572758if self.verbose:2759print"branch is%s"% branch27602761 self.updatedBranches.add(branch)27622763if branch not in self.createdBranches:2764 self.createdBranches.add(branch)2765 parent = self.knownBranches[branch]2766if parent == branch:2767 parent =""2768else:2769 fullBranch = self.projectName + branch2770if fullBranch not in self.p4BranchesInGit:2771if not self.silent:2772print("\nImporting new branch%s"% fullBranch);2773if self.importNewBranch(branch, change -1):2774 parent =""2775 self.p4BranchesInGit.append(fullBranch)2776if not self.silent:2777print("\nResuming with change%s"% change);27782779if self.verbose:2780print"parent determined through known branches:%s"% parent27812782 branch = self.gitRefForBranch(branch)2783 parent = self.gitRefForBranch(parent)27842785if self.verbose:2786print"looking for initial parent for%s; current parent is%s"% (branch, parent)27872788iflen(parent) ==0and branch in self.initialParents:2789 parent = self.initialParents[branch]2790del self.initialParents[branch]27912792 blob =None2793iflen(parent) >0:2794 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2795if self.verbose:2796print"Creating temporary branch: "+ tempBranch2797 self.commit(description, filesForCommit, tempBranch)2798 self.tempBranches.append(tempBranch)2799 self.checkpoint()2800 blob = self.searchParent(parent, branch, tempBranch)2801if blob:2802 self.commit(description, filesForCommit, branch, blob)2803else:2804if self.verbose:2805print"Parent of%snot found. Committing into head of%s"% (branch, parent)2806 self.commit(description, filesForCommit, branch, parent)2807else:2808 files = self.extractFilesFromCommit(description)2809 self.commit(description, files, self.branch,2810 self.initialParent)2811# only needed once, to connect to the previous commit2812 self.initialParent =""2813exceptIOError:2814print self.gitError.read()2815 sys.exit(1)28162817defimportHeadRevision(self, revision):2818print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)28192820 details = {}2821 details["user"] ="git perforce import user"2822 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2823% (' '.join(self.depotPaths), revision))2824 details["change"] = revision2825 newestRevision =028262827 fileCnt =02828 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]28292830for info inp4CmdList(["files"] + fileArgs):28312832if'code'in info and info['code'] =='error':2833 sys.stderr.write("p4 returned an error:%s\n"2834% info['data'])2835if info['data'].find("must refer to client") >=0:2836 sys.stderr.write("This particular p4 error is misleading.\n")2837 sys.stderr.write("Perhaps the depot path was misspelled.\n");2838 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2839 sys.exit(1)2840if'p4ExitCode'in info:2841 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2842 sys.exit(1)284328442845 change =int(info["change"])2846if change > newestRevision:2847 newestRevision = change28482849if info["action"]in self.delete_actions:2850# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2851#fileCnt = fileCnt + 12852continue28532854for prop in["depotFile","rev","action","type"]:2855 details["%s%s"% (prop, fileCnt)] = info[prop]28562857 fileCnt = fileCnt +128582859 details["change"] = newestRevision28602861# Use time from top-most change so that all git p4 clones of2862# the same p4 repo have the same commit SHA1s.2863 res =p4_describe(newestRevision)2864 details["time"] = res["time"]28652866 self.updateOptionDict(details)2867try:2868 self.commit(details, self.extractFilesFromCommit(details), self.branch)2869exceptIOError:2870print"IO error with git fast-import. Is your git version recent enough?"2871print self.gitError.read()287228732874defrun(self, args):2875 self.depotPaths = []2876 self.changeRange =""2877 self.previousDepotPaths = []2878 self.hasOrigin =False28792880# map from branch depot path to parent branch2881 self.knownBranches = {}2882 self.initialParents = {}28832884if self.importIntoRemotes:2885 self.refPrefix ="refs/remotes/p4/"2886else:2887 self.refPrefix ="refs/heads/p4/"28882889if self.syncWithOrigin:2890 self.hasOrigin =originP4BranchesExist()2891if self.hasOrigin:2892if not self.silent:2893print'Syncing with origin first, using "git fetch origin"'2894system("git fetch origin")28952896 branch_arg_given =bool(self.branch)2897iflen(self.branch) ==0:2898 self.branch = self.refPrefix +"master"2899ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2900system("git update-ref%srefs/heads/p4"% self.branch)2901system("git branch -D p4")29022903# accept either the command-line option, or the configuration variable2904if self.useClientSpec:2905# will use this after clone to set the variable2906 self.useClientSpec_from_options =True2907else:2908ifgitConfigBool("git-p4.useclientspec"):2909 self.useClientSpec =True2910if self.useClientSpec:2911 self.clientSpecDirs =getClientSpec()29122913# TODO: should always look at previous commits,2914# merge with previous imports, if possible.2915if args == []:2916if self.hasOrigin:2917createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)29182919# branches holds mapping from branch name to sha12920 branches =p4BranchesInGit(self.importIntoRemotes)29212922# restrict to just this one, disabling detect-branches2923if branch_arg_given:2924 short = self.branch.split("/")[-1]2925if short in branches:2926 self.p4BranchesInGit = [ short ]2927else:2928 self.p4BranchesInGit = branches.keys()29292930iflen(self.p4BranchesInGit) >1:2931if not self.silent:2932print"Importing from/into multiple branches"2933 self.detectBranches =True2934for branch in branches.keys():2935 self.initialParents[self.refPrefix + branch] = \2936 branches[branch]29372938if self.verbose:2939print"branches:%s"% self.p4BranchesInGit29402941 p4Change =02942for branch in self.p4BranchesInGit:2943 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29442945 settings =extractSettingsGitLog(logMsg)29462947 self.readOptions(settings)2948if(settings.has_key('depot-paths')2949and settings.has_key('change')):2950 change =int(settings['change']) +12951 p4Change =max(p4Change, change)29522953 depotPaths =sorted(settings['depot-paths'])2954if self.previousDepotPaths == []:2955 self.previousDepotPaths = depotPaths2956else:2957 paths = []2958for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2959 prev_list = prev.split("/")2960 cur_list = cur.split("/")2961for i inrange(0,min(len(cur_list),len(prev_list))):2962if cur_list[i] <> prev_list[i]:2963 i = i -12964break29652966 paths.append("/".join(cur_list[:i +1]))29672968 self.previousDepotPaths = paths29692970if p4Change >0:2971 self.depotPaths =sorted(self.previousDepotPaths)2972 self.changeRange ="@%s,#head"% p4Change2973if not self.silent and not self.detectBranches:2974print"Performing incremental import into%sgit branch"% self.branch29752976# accept multiple ref name abbreviations:2977# refs/foo/bar/branch -> use it exactly2978# p4/branch -> prepend refs/remotes/ or refs/heads/2979# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2980if not self.branch.startswith("refs/"):2981if self.importIntoRemotes:2982 prepend ="refs/remotes/"2983else:2984 prepend ="refs/heads/"2985if not self.branch.startswith("p4/"):2986 prepend +="p4/"2987 self.branch = prepend + self.branch29882989iflen(args) ==0and self.depotPaths:2990if not self.silent:2991print"Depot paths:%s"%' '.join(self.depotPaths)2992else:2993if self.depotPaths and self.depotPaths != args:2994print("previous import used depot path%sand now%swas specified. "2995"This doesn't work!"% (' '.join(self.depotPaths),2996' '.join(args)))2997 sys.exit(1)29982999 self.depotPaths =sorted(args)30003001 revision =""3002 self.users = {}30033004# Make sure no revision specifiers are used when --changesfile3005# is specified.3006 bad_changesfile =False3007iflen(self.changesFile) >0:3008for p in self.depotPaths:3009if p.find("@") >=0or p.find("#") >=0:3010 bad_changesfile =True3011break3012if bad_changesfile:3013die("Option --changesfile is incompatible with revision specifiers")30143015 newPaths = []3016for p in self.depotPaths:3017if p.find("@") != -1:3018 atIdx = p.index("@")3019 self.changeRange = p[atIdx:]3020if self.changeRange =="@all":3021 self.changeRange =""3022elif','not in self.changeRange:3023 revision = self.changeRange3024 self.changeRange =""3025 p = p[:atIdx]3026elif p.find("#") != -1:3027 hashIdx = p.index("#")3028 revision = p[hashIdx:]3029 p = p[:hashIdx]3030elif self.previousDepotPaths == []:3031# pay attention to changesfile, if given, else import3032# the entire p4 tree at the head revision3033iflen(self.changesFile) ==0:3034 revision ="#head"30353036 p = re.sub("\.\.\.$","", p)3037if not p.endswith("/"):3038 p +="/"30393040 newPaths.append(p)30413042 self.depotPaths = newPaths30433044# --detect-branches may change this for each branch3045 self.branchPrefixes = self.depotPaths30463047 self.loadUserMapFromCache()3048 self.labels = {}3049if self.detectLabels:3050 self.getLabels();30513052if self.detectBranches:3053## FIXME - what's a P4 projectName ?3054 self.projectName = self.guessProjectName()30553056if self.hasOrigin:3057 self.getBranchMappingFromGitBranches()3058else:3059 self.getBranchMapping()3060if self.verbose:3061print"p4-git branches:%s"% self.p4BranchesInGit3062print"initial parents:%s"% self.initialParents3063for b in self.p4BranchesInGit:3064if b !="master":30653066## FIXME3067 b = b[len(self.projectName):]3068 self.createdBranches.add(b)30693070 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30713072 self.importProcess = subprocess.Popen(["git","fast-import"],3073 stdin=subprocess.PIPE,3074 stdout=subprocess.PIPE,3075 stderr=subprocess.PIPE);3076 self.gitOutput = self.importProcess.stdout3077 self.gitStream = self.importProcess.stdin3078 self.gitError = self.importProcess.stderr30793080if revision:3081 self.importHeadRevision(revision)3082else:3083 changes = []30843085iflen(self.changesFile) >0:3086 output =open(self.changesFile).readlines()3087 changeSet =set()3088for line in output:3089 changeSet.add(int(line))30903091for change in changeSet:3092 changes.append(change)30933094 changes.sort()3095else:3096# catch "git p4 sync" with no new branches, in a repo that3097# does not have any existing p4 branches3098iflen(args) ==0:3099if not self.p4BranchesInGit:3100die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")31013102# The default branch is master, unless --branch is used to3103# specify something else. Make sure it exists, or complain3104# nicely about how to use --branch.3105if not self.detectBranches:3106if notbranch_exists(self.branch):3107if branch_arg_given:3108die("Error: branch%sdoes not exist."% self.branch)3109else:3110die("Error: no branch%s; perhaps specify one with --branch."%3111 self.branch)31123113if self.verbose:3114print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3115 self.changeRange)3116 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)31173118iflen(self.maxChanges) >0:3119 changes = changes[:min(int(self.maxChanges),len(changes))]31203121iflen(changes) ==0:3122if not self.silent:3123print"No changes to import!"3124else:3125if not self.silent and not self.detectBranches:3126print"Import destination:%s"% self.branch31273128 self.updatedBranches =set()31293130if not self.detectBranches:3131if args:3132# start a new branch3133 self.initialParent =""3134else:3135# build on a previous revision3136 self.initialParent =parseRevision(self.branch)31373138 self.importChanges(changes)31393140if not self.silent:3141print""3142iflen(self.updatedBranches) >0:3143 sys.stdout.write("Updated branches: ")3144for b in self.updatedBranches:3145 sys.stdout.write("%s"% b)3146 sys.stdout.write("\n")31473148ifgitConfigBool("git-p4.importLabels"):3149 self.importLabels =True31503151if self.importLabels:3152 p4Labels =getP4Labels(self.depotPaths)3153 gitTags =getGitTags()31543155 missingP4Labels = p4Labels - gitTags3156 self.importP4Labels(self.gitStream, missingP4Labels)31573158 self.gitStream.close()3159if self.importProcess.wait() !=0:3160die("fast-import failed:%s"% self.gitError.read())3161 self.gitOutput.close()3162 self.gitError.close()31633164# Cleanup temporary branches created during import3165if self.tempBranches != []:3166for branch in self.tempBranches:3167read_pipe("git update-ref -d%s"% branch)3168 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31693170# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3171# a convenient shortcut refname "p4".3172if self.importIntoRemotes:3173 head_ref = self.refPrefix +"HEAD"3174if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3175system(["git","symbolic-ref", head_ref, self.branch])31763177return True31783179classP4Rebase(Command):3180def__init__(self):3181 Command.__init__(self)3182 self.options = [3183 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3184]3185 self.importLabels =False3186 self.description = ("Fetches the latest revision from perforce and "3187+"rebases the current work (branch) against it")31883189defrun(self, args):3190 sync =P4Sync()3191 sync.importLabels = self.importLabels3192 sync.run([])31933194return self.rebase()31953196defrebase(self):3197if os.system("git update-index --refresh") !=0:3198die("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.");3199iflen(read_pipe("git diff-index HEAD --")) >0:3200die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");32013202[upstream, settings] =findUpstreamBranchPoint()3203iflen(upstream) ==0:3204die("Cannot find upstream branchpoint for rebase")32053206# the branchpoint may be p4/foo~3, so strip off the parent3207 upstream = re.sub("~[0-9]+$","", upstream)32083209print"Rebasing the current branch onto%s"% upstream3210 oldHead =read_pipe("git rev-parse HEAD").strip()3211system("git rebase%s"% upstream)3212system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3213return True32143215classP4Clone(P4Sync):3216def__init__(self):3217 P4Sync.__init__(self)3218 self.description ="Creates a new git repository and imports from Perforce into it"3219 self.usage ="usage: %prog [options] //depot/path[@revRange]"3220 self.options += [3221 optparse.make_option("--destination", dest="cloneDestination",3222 action='store', default=None,3223help="where to leave result of the clone"),3224 optparse.make_option("--bare", dest="cloneBare",3225 action="store_true", default=False),3226]3227 self.cloneDestination =None3228 self.needsGit =False3229 self.cloneBare =False32303231defdefaultDestination(self, args):3232## TODO: use common prefix of args?3233 depotPath = args[0]3234 depotDir = re.sub("(@[^@]*)$","", depotPath)3235 depotDir = re.sub("(#[^#]*)$","", depotDir)3236 depotDir = re.sub(r"\.\.\.$","", depotDir)3237 depotDir = re.sub(r"/$","", depotDir)3238return os.path.split(depotDir)[1]32393240defrun(self, args):3241iflen(args) <1:3242return False32433244if self.keepRepoPath and not self.cloneDestination:3245 sys.stderr.write("Must specify destination for --keep-path\n")3246 sys.exit(1)32473248 depotPaths = args32493250if not self.cloneDestination andlen(depotPaths) >1:3251 self.cloneDestination = depotPaths[-1]3252 depotPaths = depotPaths[:-1]32533254 self.cloneExclude = ["/"+p for p in self.cloneExclude]3255for p in depotPaths:3256if not p.startswith("//"):3257 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3258return False32593260if not self.cloneDestination:3261 self.cloneDestination = self.defaultDestination(args)32623263print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32643265if not os.path.exists(self.cloneDestination):3266 os.makedirs(self.cloneDestination)3267chdir(self.cloneDestination)32683269 init_cmd = ["git","init"]3270if self.cloneBare:3271 init_cmd.append("--bare")3272 retcode = subprocess.call(init_cmd)3273if retcode:3274raiseCalledProcessError(retcode, init_cmd)32753276if not P4Sync.run(self, depotPaths):3277return False32783279# create a master branch and check out a work tree3280ifgitBranchExists(self.branch):3281system(["git","branch","master", self.branch ])3282if not self.cloneBare:3283system(["git","checkout","-f"])3284else:3285print'Not checking out any branch, use ' \3286'"git checkout -q -b master <branch>"'32873288# auto-set this variable if invoked with --use-client-spec3289if self.useClientSpec_from_options:3290system("git config --bool git-p4.useclientspec true")32913292return True32933294classP4Branches(Command):3295def__init__(self):3296 Command.__init__(self)3297 self.options = [ ]3298 self.description = ("Shows the git branches that hold imports and their "3299+"corresponding perforce depot paths")3300 self.verbose =False33013302defrun(self, args):3303iforiginP4BranchesExist():3304createOrUpdateBranchesFromOrigin()33053306 cmdline ="git rev-parse --symbolic "3307 cmdline +=" --remotes"33083309for line inread_pipe_lines(cmdline):3310 line = line.strip()33113312if not line.startswith('p4/')or line =="p4/HEAD":3313continue3314 branch = line33153316 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3317 settings =extractSettingsGitLog(log)33183319print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3320return True33213322classHelpFormatter(optparse.IndentedHelpFormatter):3323def__init__(self):3324 optparse.IndentedHelpFormatter.__init__(self)33253326defformat_description(self, description):3327if description:3328return description +"\n"3329else:3330return""33313332defprintUsage(commands):3333print"usage:%s<command> [options]"% sys.argv[0]3334print""3335print"valid commands:%s"%", ".join(commands)3336print""3337print"Try%s<command> --help for command specific help."% sys.argv[0]3338print""33393340commands = {3341"debug": P4Debug,3342"submit": P4Submit,3343"commit": P4Submit,3344"sync": P4Sync,3345"rebase": P4Rebase,3346"clone": P4Clone,3347"rollback": P4RollBack,3348"branches": P4Branches3349}335033513352defmain():3353iflen(sys.argv[1:]) ==0:3354printUsage(commands.keys())3355 sys.exit(2)33563357 cmdName = sys.argv[1]3358try:3359 klass = commands[cmdName]3360 cmd =klass()3361exceptKeyError:3362print"unknown command%s"% cmdName3363print""3364printUsage(commands.keys())3365 sys.exit(2)33663367 options = cmd.options3368 cmd.gitdir = os.environ.get("GIT_DIR",None)33693370 args = sys.argv[2:]33713372 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3373if cmd.needsGit:3374 options.append(optparse.make_option("--git-dir", dest="gitdir"))33753376 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3377 options,3378 description = cmd.description,3379 formatter =HelpFormatter())33803381(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3382global verbose3383 verbose = cmd.verbose3384if cmd.needsGit:3385if cmd.gitdir ==None:3386 cmd.gitdir = os.path.abspath(".git")3387if notisValidGitDir(cmd.gitdir):3388 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3389if os.path.exists(cmd.gitdir):3390 cdup =read_pipe("git rev-parse --show-cdup").strip()3391iflen(cdup) >0:3392chdir(cdup);33933394if notisValidGitDir(cmd.gitdir):3395ifisValidGitDir(cmd.gitdir +"/.git"):3396 cmd.gitdir +="/.git"3397else:3398die("fatal: cannot locate git repository at%s"% cmd.gitdir)33993400 os.environ["GIT_DIR"] = cmd.gitdir34013402if not cmd.run(args):3403 parser.print_help()3404 sys.exit(2)340534063407if __name__ =='__main__':3408main()