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 25import zipfile 26import zlib 27import ctypes 28import errno 29 30try: 31from subprocess import CalledProcessError 32exceptImportError: 33# from python2.7:subprocess.py 34# Exception classes used by this module. 35classCalledProcessError(Exception): 36"""This exception is raised when a process run by check_call() returns 37 a non-zero exit status. The exit status will be stored in the 38 returncode attribute.""" 39def__init__(self, returncode, cmd): 40 self.returncode = returncode 41 self.cmd = cmd 42def__str__(self): 43return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 44 45verbose =False 46 47# Only labels/tags matching this will be imported/exported 48defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 49 50# Grab changes in blocks of this many revisions, unless otherwise requested 51defaultBlockSize =512 52 53defp4_build_cmd(cmd): 54"""Build a suitable p4 command line. 55 56 This consolidates building and returning a p4 command line into one 57 location. It means that hooking into the environment, or other configuration 58 can be done more easily. 59 """ 60 real_cmd = ["p4"] 61 62 user =gitConfig("git-p4.user") 63iflen(user) >0: 64 real_cmd += ["-u",user] 65 66 password =gitConfig("git-p4.password") 67iflen(password) >0: 68 real_cmd += ["-P", password] 69 70 port =gitConfig("git-p4.port") 71iflen(port) >0: 72 real_cmd += ["-p", port] 73 74 host =gitConfig("git-p4.host") 75iflen(host) >0: 76 real_cmd += ["-H", host] 77 78 client =gitConfig("git-p4.client") 79iflen(client) >0: 80 real_cmd += ["-c", client] 81 82 retries =gitConfigInt("git-p4.retries") 83if retries is None: 84# Perform 3 retries by default 85 retries =3 86if retries >0: 87# Provide a way to not pass this option by setting git-p4.retries to 0 88 real_cmd += ["-r",str(retries)] 89 90ifisinstance(cmd,basestring): 91 real_cmd =' '.join(real_cmd) +' '+ cmd 92else: 93 real_cmd += cmd 94return real_cmd 95 96defgit_dir(path): 97""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 98 This won't automatically add ".git" to a directory. 99 """ 100 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 101if not d orlen(d) ==0: 102return None 103else: 104return d 105 106defchdir(path, is_client_path=False): 107"""Do chdir to the given path, and set the PWD environment 108 variable for use by P4. It does not look at getcwd() output. 109 Since we're not using the shell, it is necessary to set the 110 PWD environment variable explicitly. 111 112 Normally, expand the path to force it to be absolute. This 113 addresses the use of relative path names inside P4 settings, 114 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 115 as given; it looks for .p4config using PWD. 116 117 If is_client_path, the path was handed to us directly by p4, 118 and may be a symbolic link. Do not call os.getcwd() in this 119 case, because it will cause p4 to think that PWD is not inside 120 the client path. 121 """ 122 123 os.chdir(path) 124if not is_client_path: 125 path = os.getcwd() 126 os.environ['PWD'] = path 127 128defcalcDiskFree(): 129"""Return free space in bytes on the disk of the given dirname.""" 130if platform.system() =='Windows': 131 free_bytes = ctypes.c_ulonglong(0) 132 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 133return free_bytes.value 134else: 135 st = os.statvfs(os.getcwd()) 136return st.f_bavail * st.f_frsize 137 138defdie(msg): 139if verbose: 140raiseException(msg) 141else: 142 sys.stderr.write(msg +"\n") 143 sys.exit(1) 144 145defwrite_pipe(c, stdin): 146if verbose: 147 sys.stderr.write('Writing pipe:%s\n'%str(c)) 148 149 expand =isinstance(c,basestring) 150 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 151 pipe = p.stdin 152 val = pipe.write(stdin) 153 pipe.close() 154if p.wait(): 155die('Command failed:%s'%str(c)) 156 157return val 158 159defp4_write_pipe(c, stdin): 160 real_cmd =p4_build_cmd(c) 161returnwrite_pipe(real_cmd, stdin) 162 163defread_pipe_full(c): 164""" Read output from command. Returns a tuple 165 of the return status, stdout text and stderr 166 text. 167 """ 168if verbose: 169 sys.stderr.write('Reading pipe:%s\n'%str(c)) 170 171 expand =isinstance(c,basestring) 172 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 173(out, err) = p.communicate() 174return(p.returncode, out, err) 175 176defread_pipe(c, ignore_error=False): 177""" Read output from command. Returns the output text on 178 success. On failure, terminates execution, unless 179 ignore_error is True, when it returns an empty string. 180 """ 181(retcode, out, err) =read_pipe_full(c) 182if retcode !=0: 183if ignore_error: 184 out ="" 185else: 186die('Command failed:%s\nError:%s'% (str(c), err)) 187return out 188 189defread_pipe_text(c): 190""" Read output from a command with trailing whitespace stripped. 191 On error, returns None. 192 """ 193(retcode, out, err) =read_pipe_full(c) 194if retcode !=0: 195return None 196else: 197return out.rstrip() 198 199defp4_read_pipe(c, ignore_error=False): 200 real_cmd =p4_build_cmd(c) 201returnread_pipe(real_cmd, ignore_error) 202 203defread_pipe_lines(c): 204if verbose: 205 sys.stderr.write('Reading pipe:%s\n'%str(c)) 206 207 expand =isinstance(c, basestring) 208 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 209 pipe = p.stdout 210 val = pipe.readlines() 211if pipe.close()or p.wait(): 212die('Command failed:%s'%str(c)) 213 214return val 215 216defp4_read_pipe_lines(c): 217"""Specifically invoke p4 on the command supplied. """ 218 real_cmd =p4_build_cmd(c) 219returnread_pipe_lines(real_cmd) 220 221defp4_has_command(cmd): 222"""Ask p4 for help on this command. If it returns an error, the 223 command does not exist in this version of p4.""" 224 real_cmd =p4_build_cmd(["help", cmd]) 225 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 226 stderr=subprocess.PIPE) 227 p.communicate() 228return p.returncode ==0 229 230defp4_has_move_command(): 231"""See if the move command exists, that it supports -k, and that 232 it has not been administratively disabled. The arguments 233 must be correct, but the filenames do not have to exist. Use 234 ones with wildcards so even if they exist, it will fail.""" 235 236if notp4_has_command("move"): 237return False 238 cmd =p4_build_cmd(["move","-k","@from","@to"]) 239 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 240(out, err) = p.communicate() 241# return code will be 1 in either case 242if err.find("Invalid option") >=0: 243return False 244if err.find("disabled") >=0: 245return False 246# assume it failed because @... was invalid changelist 247return True 248 249defsystem(cmd, ignore_error=False): 250 expand =isinstance(cmd,basestring) 251if verbose: 252 sys.stderr.write("executing%s\n"%str(cmd)) 253 retcode = subprocess.call(cmd, shell=expand) 254if retcode and not ignore_error: 255raiseCalledProcessError(retcode, cmd) 256 257return retcode 258 259defp4_system(cmd): 260"""Specifically invoke p4 as the system command. """ 261 real_cmd =p4_build_cmd(cmd) 262 expand =isinstance(real_cmd, basestring) 263 retcode = subprocess.call(real_cmd, shell=expand) 264if retcode: 265raiseCalledProcessError(retcode, real_cmd) 266 267_p4_version_string =None 268defp4_version_string(): 269"""Read the version string, showing just the last line, which 270 hopefully is the interesting version bit. 271 272 $ p4 -V 273 Perforce - The Fast Software Configuration Management System. 274 Copyright 1995-2011 Perforce Software. All rights reserved. 275 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 276 """ 277global _p4_version_string 278if not _p4_version_string: 279 a =p4_read_pipe_lines(["-V"]) 280 _p4_version_string = a[-1].rstrip() 281return _p4_version_string 282 283defp4_integrate(src, dest): 284p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 285 286defp4_sync(f, *options): 287p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 288 289defp4_add(f): 290# forcibly add file names with wildcards 291ifwildcard_present(f): 292p4_system(["add","-f", f]) 293else: 294p4_system(["add", f]) 295 296defp4_delete(f): 297p4_system(["delete",wildcard_encode(f)]) 298 299defp4_edit(f, *options): 300p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 301 302defp4_revert(f): 303p4_system(["revert",wildcard_encode(f)]) 304 305defp4_reopen(type, f): 306p4_system(["reopen","-t",type,wildcard_encode(f)]) 307 308defp4_reopen_in_change(changelist, files): 309 cmd = ["reopen","-c",str(changelist)] + files 310p4_system(cmd) 311 312defp4_move(src, dest): 313p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 314 315defp4_last_change(): 316 results =p4CmdList(["changes","-m","1"], skip_info=True) 317returnint(results[0]['change']) 318 319defp4_describe(change, shelved=False): 320"""Make sure it returns a valid result by checking for 321 the presence of field "time". Return a dict of the 322 results.""" 323 324 cmd = ["describe","-s"] 325if shelved: 326 cmd += ["-S"] 327 cmd += [str(change)] 328 329 ds =p4CmdList(cmd, skip_info=True) 330iflen(ds) !=1: 331die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 332 333 d = ds[0] 334 335if"p4ExitCode"in d: 336die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 337str(d))) 338if"code"in d: 339if d["code"] =="error": 340die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 341 342if"time"not in d: 343die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 344 345return d 346 347# 348# Canonicalize the p4 type and return a tuple of the 349# base type, plus any modifiers. See "p4 help filetypes" 350# for a list and explanation. 351# 352defsplit_p4_type(p4type): 353 354 p4_filetypes_historical = { 355"ctempobj":"binary+Sw", 356"ctext":"text+C", 357"cxtext":"text+Cx", 358"ktext":"text+k", 359"kxtext":"text+kx", 360"ltext":"text+F", 361"tempobj":"binary+FSw", 362"ubinary":"binary+F", 363"uresource":"resource+F", 364"uxbinary":"binary+Fx", 365"xbinary":"binary+x", 366"xltext":"text+Fx", 367"xtempobj":"binary+Swx", 368"xtext":"text+x", 369"xunicode":"unicode+x", 370"xutf16":"utf16+x", 371} 372if p4type in p4_filetypes_historical: 373 p4type = p4_filetypes_historical[p4type] 374 mods ="" 375 s = p4type.split("+") 376 base = s[0] 377 mods ="" 378iflen(s) >1: 379 mods = s[1] 380return(base, mods) 381 382# 383# return the raw p4 type of a file (text, text+ko, etc) 384# 385defp4_type(f): 386 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 387return results[0]['headType'] 388 389# 390# Given a type base and modifier, return a regexp matching 391# the keywords that can be expanded in the file 392# 393defp4_keywords_regexp_for_type(base, type_mods): 394if base in("text","unicode","binary"): 395 kwords =None 396if"ko"in type_mods: 397 kwords ='Id|Header' 398elif"k"in type_mods: 399 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 400else: 401return None 402 pattern = r""" 403 \$ # Starts with a dollar, followed by... 404 (%s) # one of the keywords, followed by... 405 (:[^$\n]+)? # possibly an old expansion, followed by... 406 \$ # another dollar 407 """% kwords 408return pattern 409else: 410return None 411 412# 413# Given a file, return a regexp matching the possible 414# RCS keywords that will be expanded, or None for files 415# with kw expansion turned off. 416# 417defp4_keywords_regexp_for_file(file): 418if not os.path.exists(file): 419return None 420else: 421(type_base, type_mods) =split_p4_type(p4_type(file)) 422returnp4_keywords_regexp_for_type(type_base, type_mods) 423 424defsetP4ExecBit(file, mode): 425# Reopens an already open file and changes the execute bit to match 426# the execute bit setting in the passed in mode. 427 428 p4Type ="+x" 429 430if notisModeExec(mode): 431 p4Type =getP4OpenedType(file) 432 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 433 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 434if p4Type[-1] =="+": 435 p4Type = p4Type[0:-1] 436 437p4_reopen(p4Type,file) 438 439defgetP4OpenedType(file): 440# Returns the perforce file type for the given file. 441 442 result =p4_read_pipe(["opened",wildcard_encode(file)]) 443 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 444if match: 445return match.group(1) 446else: 447die("Could not determine file type for%s(result: '%s')"% (file, result)) 448 449# Return the set of all p4 labels 450defgetP4Labels(depotPaths): 451 labels =set() 452ifisinstance(depotPaths,basestring): 453 depotPaths = [depotPaths] 454 455for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 456 label = l['label'] 457 labels.add(label) 458 459return labels 460 461# Return the set of all git tags 462defgetGitTags(): 463 gitTags =set() 464for line inread_pipe_lines(["git","tag"]): 465 tag = line.strip() 466 gitTags.add(tag) 467return gitTags 468 469defdiffTreePattern(): 470# This is a simple generator for the diff tree regex pattern. This could be 471# a class variable if this and parseDiffTreeEntry were a part of a class. 472 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 473while True: 474yield pattern 475 476defparseDiffTreeEntry(entry): 477"""Parses a single diff tree entry into its component elements. 478 479 See git-diff-tree(1) manpage for details about the format of the diff 480 output. This method returns a dictionary with the following elements: 481 482 src_mode - The mode of the source file 483 dst_mode - The mode of the destination file 484 src_sha1 - The sha1 for the source file 485 dst_sha1 - The sha1 fr the destination file 486 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 487 status_score - The score for the status (applicable for 'C' and 'R' 488 statuses). This is None if there is no score. 489 src - The path for the source file. 490 dst - The path for the destination file. This is only present for 491 copy or renames. If it is not present, this is None. 492 493 If the pattern is not matched, None is returned.""" 494 495 match =diffTreePattern().next().match(entry) 496if match: 497return{ 498'src_mode': match.group(1), 499'dst_mode': match.group(2), 500'src_sha1': match.group(3), 501'dst_sha1': match.group(4), 502'status': match.group(5), 503'status_score': match.group(6), 504'src': match.group(7), 505'dst': match.group(10) 506} 507return None 508 509defisModeExec(mode): 510# Returns True if the given git mode represents an executable file, 511# otherwise False. 512return mode[-3:] =="755" 513 514defisModeExecChanged(src_mode, dst_mode): 515returnisModeExec(src_mode) !=isModeExec(dst_mode) 516 517defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False): 518 519ifisinstance(cmd,basestring): 520 cmd ="-G "+ cmd 521 expand =True 522else: 523 cmd = ["-G"] + cmd 524 expand =False 525 526 cmd =p4_build_cmd(cmd) 527if verbose: 528 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 529 530# Use a temporary file to avoid deadlocks without 531# subprocess.communicate(), which would put another copy 532# of stdout into memory. 533 stdin_file =None 534if stdin is not None: 535 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 536ifisinstance(stdin,basestring): 537 stdin_file.write(stdin) 538else: 539for i in stdin: 540 stdin_file.write(i +'\n') 541 stdin_file.flush() 542 stdin_file.seek(0) 543 544 p4 = subprocess.Popen(cmd, 545 shell=expand, 546 stdin=stdin_file, 547 stdout=subprocess.PIPE) 548 549 result = [] 550try: 551while True: 552 entry = marshal.load(p4.stdout) 553if skip_info: 554if'code'in entry and entry['code'] =='info': 555continue 556if cb is not None: 557cb(entry) 558else: 559 result.append(entry) 560exceptEOFError: 561pass 562 exitCode = p4.wait() 563if exitCode !=0: 564 entry = {} 565 entry["p4ExitCode"] = exitCode 566 result.append(entry) 567 568return result 569 570defp4Cmd(cmd): 571list=p4CmdList(cmd) 572 result = {} 573for entry inlist: 574 result.update(entry) 575return result; 576 577defp4Where(depotPath): 578if not depotPath.endswith("/"): 579 depotPath +="/" 580 depotPathLong = depotPath +"..." 581 outputList =p4CmdList(["where", depotPathLong]) 582 output =None 583for entry in outputList: 584if"depotFile"in entry: 585# Search for the base client side depot path, as long as it starts with the branch's P4 path. 586# The base path always ends with "/...". 587if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 588 output = entry 589break 590elif"data"in entry: 591 data = entry.get("data") 592 space = data.find(" ") 593if data[:space] == depotPath: 594 output = entry 595break 596if output ==None: 597return"" 598if output["code"] =="error": 599return"" 600 clientPath ="" 601if"path"in output: 602 clientPath = output.get("path") 603elif"data"in output: 604 data = output.get("data") 605 lastSpace = data.rfind(" ") 606 clientPath = data[lastSpace +1:] 607 608if clientPath.endswith("..."): 609 clientPath = clientPath[:-3] 610return clientPath 611 612defcurrentGitBranch(): 613returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 614 615defisValidGitDir(path): 616returngit_dir(path) !=None 617 618defparseRevision(ref): 619returnread_pipe("git rev-parse%s"% ref).strip() 620 621defbranchExists(ref): 622 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 623 ignore_error=True) 624returnlen(rev) >0 625 626defextractLogMessageFromGitCommit(commit): 627 logMessage ="" 628 629## fixme: title is first line of commit, not 1st paragraph. 630 foundTitle =False 631for log inread_pipe_lines("git cat-file commit%s"% commit): 632if not foundTitle: 633iflen(log) ==1: 634 foundTitle =True 635continue 636 637 logMessage += log 638return logMessage 639 640defextractSettingsGitLog(log): 641 values = {} 642for line in log.split("\n"): 643 line = line.strip() 644 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 645if not m: 646continue 647 648 assignments = m.group(1).split(':') 649for a in assignments: 650 vals = a.split('=') 651 key = vals[0].strip() 652 val = ('='.join(vals[1:])).strip() 653if val.endswith('\"')and val.startswith('"'): 654 val = val[1:-1] 655 656 values[key] = val 657 658 paths = values.get("depot-paths") 659if not paths: 660 paths = values.get("depot-path") 661if paths: 662 values['depot-paths'] = paths.split(',') 663return values 664 665defgitBranchExists(branch): 666 proc = subprocess.Popen(["git","rev-parse", branch], 667 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 668return proc.wait() ==0; 669 670defgitUpdateRef(ref, newvalue): 671 subprocess.check_call(["git","update-ref", ref, newvalue]) 672 673defgitDeleteRef(ref): 674 subprocess.check_call(["git","update-ref","-d", ref]) 675 676_gitConfig = {} 677 678defgitConfig(key, typeSpecifier=None): 679if not _gitConfig.has_key(key): 680 cmd = ["git","config"] 681if typeSpecifier: 682 cmd += [ typeSpecifier ] 683 cmd += [ key ] 684 s =read_pipe(cmd, ignore_error=True) 685 _gitConfig[key] = s.strip() 686return _gitConfig[key] 687 688defgitConfigBool(key): 689"""Return a bool, using git config --bool. It is True only if the 690 variable is set to true, and False if set to false or not present 691 in the config.""" 692 693if not _gitConfig.has_key(key): 694 _gitConfig[key] =gitConfig(key,'--bool') =="true" 695return _gitConfig[key] 696 697defgitConfigInt(key): 698if not _gitConfig.has_key(key): 699 cmd = ["git","config","--int", key ] 700 s =read_pipe(cmd, ignore_error=True) 701 v = s.strip() 702try: 703 _gitConfig[key] =int(gitConfig(key,'--int')) 704exceptValueError: 705 _gitConfig[key] =None 706return _gitConfig[key] 707 708defgitConfigList(key): 709if not _gitConfig.has_key(key): 710 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 711 _gitConfig[key] = s.strip().splitlines() 712if _gitConfig[key] == ['']: 713 _gitConfig[key] = [] 714return _gitConfig[key] 715 716defp4BranchesInGit(branchesAreInRemotes=True): 717"""Find all the branches whose names start with "p4/", looking 718 in remotes or heads as specified by the argument. Return 719 a dictionary of{ branch: revision }for each one found. 720 The branch names are the short names, without any 721 "p4/" prefix.""" 722 723 branches = {} 724 725 cmdline ="git rev-parse --symbolic " 726if branchesAreInRemotes: 727 cmdline +="--remotes" 728else: 729 cmdline +="--branches" 730 731for line inread_pipe_lines(cmdline): 732 line = line.strip() 733 734# only import to p4/ 735if not line.startswith('p4/'): 736continue 737# special symbolic ref to p4/master 738if line =="p4/HEAD": 739continue 740 741# strip off p4/ prefix 742 branch = line[len("p4/"):] 743 744 branches[branch] =parseRevision(line) 745 746return branches 747 748defbranch_exists(branch): 749"""Make sure that the given ref name really exists.""" 750 751 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 752 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 753 out, _ = p.communicate() 754if p.returncode: 755return False 756# expect exactly one line of output: the branch name 757return out.rstrip() == branch 758 759deffindUpstreamBranchPoint(head ="HEAD"): 760 branches =p4BranchesInGit() 761# map from depot-path to branch name 762 branchByDepotPath = {} 763for branch in branches.keys(): 764 tip = branches[branch] 765 log =extractLogMessageFromGitCommit(tip) 766 settings =extractSettingsGitLog(log) 767if settings.has_key("depot-paths"): 768 paths =",".join(settings["depot-paths"]) 769 branchByDepotPath[paths] ="remotes/p4/"+ branch 770 771 settings =None 772 parent =0 773while parent <65535: 774 commit = head +"~%s"% parent 775 log =extractLogMessageFromGitCommit(commit) 776 settings =extractSettingsGitLog(log) 777if settings.has_key("depot-paths"): 778 paths =",".join(settings["depot-paths"]) 779if branchByDepotPath.has_key(paths): 780return[branchByDepotPath[paths], settings] 781 782 parent = parent +1 783 784return["", settings] 785 786defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 787if not silent: 788print("Creating/updating branch(es) in%sbased on origin branch(es)" 789% localRefPrefix) 790 791 originPrefix ="origin/p4/" 792 793for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 794 line = line.strip() 795if(not line.startswith(originPrefix))or line.endswith("HEAD"): 796continue 797 798 headName = line[len(originPrefix):] 799 remoteHead = localRefPrefix + headName 800 originHead = line 801 802 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 803if(not original.has_key('depot-paths') 804or not original.has_key('change')): 805continue 806 807 update =False 808if notgitBranchExists(remoteHead): 809if verbose: 810print"creating%s"% remoteHead 811 update =True 812else: 813 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 814if settings.has_key('change') >0: 815if settings['depot-paths'] == original['depot-paths']: 816 originP4Change =int(original['change']) 817 p4Change =int(settings['change']) 818if originP4Change > p4Change: 819print("%s(%s) is newer than%s(%s). " 820"Updating p4 branch from origin." 821% (originHead, originP4Change, 822 remoteHead, p4Change)) 823 update =True 824else: 825print("Ignoring:%swas imported from%swhile " 826"%swas imported from%s" 827% (originHead,','.join(original['depot-paths']), 828 remoteHead,','.join(settings['depot-paths']))) 829 830if update: 831system("git update-ref%s %s"% (remoteHead, originHead)) 832 833deforiginP4BranchesExist(): 834returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 835 836 837defp4ParseNumericChangeRange(parts): 838 changeStart =int(parts[0][1:]) 839if parts[1] =='#head': 840 changeEnd =p4_last_change() 841else: 842 changeEnd =int(parts[1]) 843 844return(changeStart, changeEnd) 845 846defchooseBlockSize(blockSize): 847if blockSize: 848return blockSize 849else: 850return defaultBlockSize 851 852defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 853assert depotPaths 854 855# Parse the change range into start and end. Try to find integer 856# revision ranges as these can be broken up into blocks to avoid 857# hitting server-side limits (maxrows, maxscanresults). But if 858# that doesn't work, fall back to using the raw revision specifier 859# strings, without using block mode. 860 861if changeRange is None or changeRange =='': 862 changeStart =1 863 changeEnd =p4_last_change() 864 block_size =chooseBlockSize(requestedBlockSize) 865else: 866 parts = changeRange.split(',') 867assertlen(parts) ==2 868try: 869(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 870 block_size =chooseBlockSize(requestedBlockSize) 871except: 872 changeStart = parts[0][1:] 873 changeEnd = parts[1] 874if requestedBlockSize: 875die("cannot use --changes-block-size with non-numeric revisions") 876 block_size =None 877 878 changes =set() 879 880# Retrieve changes a block at a time, to prevent running 881# into a MaxResults/MaxScanRows error from the server. 882 883while True: 884 cmd = ['changes'] 885 886if block_size: 887 end =min(changeEnd, changeStart + block_size) 888 revisionRange ="%d,%d"% (changeStart, end) 889else: 890 revisionRange ="%s,%s"% (changeStart, changeEnd) 891 892for p in depotPaths: 893 cmd += ["%s...@%s"% (p, revisionRange)] 894 895# Insert changes in chronological order 896for entry inreversed(p4CmdList(cmd)): 897if entry.has_key('p4ExitCode'): 898die('Error retrieving changes descriptions ({})'.format(entry['p4ExitCode'])) 899if not entry.has_key('change'): 900continue 901 changes.add(int(entry['change'])) 902 903if not block_size: 904break 905 906if end >= changeEnd: 907break 908 909 changeStart = end +1 910 911 changes =sorted(changes) 912return changes 913 914defp4PathStartsWith(path, prefix): 915# This method tries to remedy a potential mixed-case issue: 916# 917# If UserA adds //depot/DirA/file1 918# and UserB adds //depot/dira/file2 919# 920# we may or may not have a problem. If you have core.ignorecase=true, 921# we treat DirA and dira as the same directory 922ifgitConfigBool("core.ignorecase"): 923return path.lower().startswith(prefix.lower()) 924return path.startswith(prefix) 925 926defgetClientSpec(): 927"""Look at the p4 client spec, create a View() object that contains 928 all the mappings, and return it.""" 929 930 specList =p4CmdList("client -o") 931iflen(specList) !=1: 932die('Output from "client -o" is%dlines, expecting 1'% 933len(specList)) 934 935# dictionary of all client parameters 936 entry = specList[0] 937 938# the //client/ name 939 client_name = entry["Client"] 940 941# just the keys that start with "View" 942 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 943 944# hold this new View 945 view =View(client_name) 946 947# append the lines, in order, to the view 948for view_num inrange(len(view_keys)): 949 k ="View%d"% view_num 950if k not in view_keys: 951die("Expected view key%smissing"% k) 952 view.append(entry[k]) 953 954return view 955 956defgetClientRoot(): 957"""Grab the client directory.""" 958 959 output =p4CmdList("client -o") 960iflen(output) !=1: 961die('Output from "client -o" is%dlines, expecting 1'%len(output)) 962 963 entry = output[0] 964if"Root"not in entry: 965die('Client has no "Root"') 966 967return entry["Root"] 968 969# 970# P4 wildcards are not allowed in filenames. P4 complains 971# if you simply add them, but you can force it with "-f", in 972# which case it translates them into %xx encoding internally. 973# 974defwildcard_decode(path): 975# Search for and fix just these four characters. Do % last so 976# that fixing it does not inadvertently create new %-escapes. 977# Cannot have * in a filename in windows; untested as to 978# what p4 would do in such a case. 979if not platform.system() =="Windows": 980 path = path.replace("%2A","*") 981 path = path.replace("%23","#") \ 982.replace("%40","@") \ 983.replace("%25","%") 984return path 985 986defwildcard_encode(path): 987# do % first to avoid double-encoding the %s introduced here 988 path = path.replace("%","%25") \ 989.replace("*","%2A") \ 990.replace("#","%23") \ 991.replace("@","%40") 992return path 993 994defwildcard_present(path): 995 m = re.search("[*#@%]", path) 996return m is not None 997 998classLargeFileSystem(object): 999"""Base class for large file system support."""10001001def__init__(self, writeToGitStream):1002 self.largeFiles =set()1003 self.writeToGitStream = writeToGitStream10041005defgeneratePointer(self, cloneDestination, contentFile):1006"""Return the content of a pointer file that is stored in Git instead of1007 the actual content."""1008assert False,"Method 'generatePointer' required in "+ self.__class__.__name__10091010defpushFile(self, localLargeFile):1011"""Push the actual content which is not stored in the Git repository to1012 a server."""1013assert False,"Method 'pushFile' required in "+ self.__class__.__name__10141015defhasLargeFileExtension(self, relPath):1016returnreduce(1017lambda a, b: a or b,1018[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1019False1020)10211022defgenerateTempFile(self, contents):1023 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1024for d in contents:1025 contentFile.write(d)1026 contentFile.close()1027return contentFile.name10281029defexceedsLargeFileThreshold(self, relPath, contents):1030ifgitConfigInt('git-p4.largeFileThreshold'):1031 contentsSize =sum(len(d)for d in contents)1032if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1033return True1034ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1035 contentsSize =sum(len(d)for d in contents)1036if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1037return False1038 contentTempFile = self.generateTempFile(contents)1039 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1040 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1041 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1042 zf.close()1043 compressedContentsSize = zf.infolist()[0].compress_size1044 os.remove(contentTempFile)1045 os.remove(compressedContentFile.name)1046if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1047return True1048return False10491050defaddLargeFile(self, relPath):1051 self.largeFiles.add(relPath)10521053defremoveLargeFile(self, relPath):1054 self.largeFiles.remove(relPath)10551056defisLargeFile(self, relPath):1057return relPath in self.largeFiles10581059defprocessContent(self, git_mode, relPath, contents):1060"""Processes the content of git fast import. This method decides if a1061 file is stored in the large file system and handles all necessary1062 steps."""1063if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1064 contentTempFile = self.generateTempFile(contents)1065(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1066if pointer_git_mode:1067 git_mode = pointer_git_mode1068if localLargeFile:1069# Move temp file to final location in large file system1070 largeFileDir = os.path.dirname(localLargeFile)1071if not os.path.isdir(largeFileDir):1072 os.makedirs(largeFileDir)1073 shutil.move(contentTempFile, localLargeFile)1074 self.addLargeFile(relPath)1075ifgitConfigBool('git-p4.largeFilePush'):1076 self.pushFile(localLargeFile)1077if verbose:1078 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1079return(git_mode, contents)10801081classMockLFS(LargeFileSystem):1082"""Mock large file system for testing."""10831084defgeneratePointer(self, contentFile):1085"""The pointer content is the original content prefixed with "pointer-".1086 The local filename of the large file storage is derived from the file content.1087 """1088withopen(contentFile,'r')as f:1089 content =next(f)1090 gitMode ='100644'1091 pointerContents ='pointer-'+ content1092 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1093return(gitMode, pointerContents, localLargeFile)10941095defpushFile(self, localLargeFile):1096"""The remote filename of the large file storage is the same as the local1097 one but in a different directory.1098 """1099 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1100if not os.path.exists(remotePath):1101 os.makedirs(remotePath)1102 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))11031104classGitLFS(LargeFileSystem):1105"""Git LFS as backend for the git-p4 large file system.1106 See https://git-lfs.github.com/ for details."""11071108def__init__(self, *args):1109 LargeFileSystem.__init__(self, *args)1110 self.baseGitAttributes = []11111112defgeneratePointer(self, contentFile):1113"""Generate a Git LFS pointer for the content. Return LFS Pointer file1114 mode and content which is stored in the Git repository instead of1115 the actual content. Return also the new location of the actual1116 content.1117 """1118if os.path.getsize(contentFile) ==0:1119return(None,'',None)11201121 pointerProcess = subprocess.Popen(1122['git','lfs','pointer','--file='+ contentFile],1123 stdout=subprocess.PIPE1124)1125 pointerFile = pointerProcess.stdout.read()1126if pointerProcess.wait():1127 os.remove(contentFile)1128die('git-lfs pointer command failed. Did you install the extension?')11291130# Git LFS removed the preamble in the output of the 'pointer' command1131# starting from version 1.2.0. Check for the preamble here to support1132# earlier versions.1133# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431134if pointerFile.startswith('Git LFS pointer for'):1135 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)11361137 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1138 localLargeFile = os.path.join(1139 os.getcwd(),1140'.git','lfs','objects', oid[:2], oid[2:4],1141 oid,1142)1143# LFS Spec states that pointer files should not have the executable bit set.1144 gitMode ='100644'1145return(gitMode, pointerFile, localLargeFile)11461147defpushFile(self, localLargeFile):1148 uploadProcess = subprocess.Popen(1149['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1150)1151if uploadProcess.wait():1152die('git-lfs push command failed. Did you define a remote?')11531154defgenerateGitAttributes(self):1155return(1156 self.baseGitAttributes +1157[1158'\n',1159'#\n',1160'# Git LFS (see https://git-lfs.github.com/)\n',1161'#\n',1162] +1163['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1164for f insorted(gitConfigList('git-p4.largeFileExtensions'))1165] +1166['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1167for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1168]1169)11701171defaddLargeFile(self, relPath):1172 LargeFileSystem.addLargeFile(self, relPath)1173 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11741175defremoveLargeFile(self, relPath):1176 LargeFileSystem.removeLargeFile(self, relPath)1177 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11781179defprocessContent(self, git_mode, relPath, contents):1180if relPath =='.gitattributes':1181 self.baseGitAttributes = contents1182return(git_mode, self.generateGitAttributes())1183else:1184return LargeFileSystem.processContent(self, git_mode, relPath, contents)11851186class Command:1187def__init__(self):1188 self.usage ="usage: %prog [options]"1189 self.needsGit =True1190 self.verbose =False11911192# This is required for the "append" cloneExclude action1193defensure_value(self, attr, value):1194if nothasattr(self, attr)orgetattr(self, attr)is None:1195setattr(self, attr, value)1196returngetattr(self, attr)11971198class P4UserMap:1199def__init__(self):1200 self.userMapFromPerforceServer =False1201 self.myP4UserId =None12021203defp4UserId(self):1204if self.myP4UserId:1205return self.myP4UserId12061207 results =p4CmdList("user -o")1208for r in results:1209if r.has_key('User'):1210 self.myP4UserId = r['User']1211return r['User']1212die("Could not find your p4 user id")12131214defp4UserIsMe(self, p4User):1215# return True if the given p4 user is actually me1216 me = self.p4UserId()1217if not p4User or p4User != me:1218return False1219else:1220return True12211222defgetUserCacheFilename(self):1223 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1224return home +"/.gitp4-usercache.txt"12251226defgetUserMapFromPerforceServer(self):1227if self.userMapFromPerforceServer:1228return1229 self.users = {}1230 self.emails = {}12311232for output inp4CmdList("users"):1233if not output.has_key("User"):1234continue1235 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1236 self.emails[output["Email"]] = output["User"]12371238 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1239for mapUserConfig ingitConfigList("git-p4.mapUser"):1240 mapUser = mapUserConfigRegex.findall(mapUserConfig)1241if mapUser andlen(mapUser[0]) ==3:1242 user = mapUser[0][0]1243 fullname = mapUser[0][1]1244 email = mapUser[0][2]1245 self.users[user] = fullname +" <"+ email +">"1246 self.emails[email] = user12471248 s =''1249for(key, val)in self.users.items():1250 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12511252open(self.getUserCacheFilename(),"wb").write(s)1253 self.userMapFromPerforceServer =True12541255defloadUserMapFromCache(self):1256 self.users = {}1257 self.userMapFromPerforceServer =False1258try:1259 cache =open(self.getUserCacheFilename(),"rb")1260 lines = cache.readlines()1261 cache.close()1262for line in lines:1263 entry = line.strip().split("\t")1264 self.users[entry[0]] = entry[1]1265exceptIOError:1266 self.getUserMapFromPerforceServer()12671268classP4Debug(Command):1269def__init__(self):1270 Command.__init__(self)1271 self.options = []1272 self.description ="A tool to debug the output of p4 -G."1273 self.needsGit =False12741275defrun(self, args):1276 j =01277for output inp4CmdList(args):1278print'Element:%d'% j1279 j +=11280print output1281return True12821283classP4RollBack(Command):1284def__init__(self):1285 Command.__init__(self)1286 self.options = [1287 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1288]1289 self.description ="A tool to debug the multi-branch import. Don't use :)"1290 self.rollbackLocalBranches =False12911292defrun(self, args):1293iflen(args) !=1:1294return False1295 maxChange =int(args[0])12961297if"p4ExitCode"inp4Cmd("changes -m 1"):1298die("Problems executing p4");12991300if self.rollbackLocalBranches:1301 refPrefix ="refs/heads/"1302 lines =read_pipe_lines("git rev-parse --symbolic --branches")1303else:1304 refPrefix ="refs/remotes/"1305 lines =read_pipe_lines("git rev-parse --symbolic --remotes")13061307for line in lines:1308if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1309 line = line.strip()1310 ref = refPrefix + line1311 log =extractLogMessageFromGitCommit(ref)1312 settings =extractSettingsGitLog(log)13131314 depotPaths = settings['depot-paths']1315 change = settings['change']13161317 changed =False13181319iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1320for p in depotPaths]))) ==0:1321print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1322system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1323continue13241325while change andint(change) > maxChange:1326 changed =True1327if self.verbose:1328print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1329system("git update-ref%s\"%s^\""% (ref, ref))1330 log =extractLogMessageFromGitCommit(ref)1331 settings =extractSettingsGitLog(log)133213331334 depotPaths = settings['depot-paths']1335 change = settings['change']13361337if changed:1338print"%srewound to%s"% (ref, change)13391340return True13411342classP4Submit(Command, P4UserMap):13431344 conflict_behavior_choices = ("ask","skip","quit")13451346def__init__(self):1347 Command.__init__(self)1348 P4UserMap.__init__(self)1349 self.options = [1350 optparse.make_option("--origin", dest="origin"),1351 optparse.make_option("-M", dest="detectRenames", action="store_true"),1352# preserve the user, requires relevant p4 permissions1353 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1354 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1355 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1356 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1357 optparse.make_option("--conflict", dest="conflict_behavior",1358 choices=self.conflict_behavior_choices),1359 optparse.make_option("--branch", dest="branch"),1360 optparse.make_option("--shelve", dest="shelve", action="store_true",1361help="Shelve instead of submit. Shelved files are reverted, "1362"restoring the workspace to the state before the shelve"),1363 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1364 metavar="CHANGELIST",1365help="update an existing shelved changelist, implies --shelve, "1366"repeat in-order for multiple shelved changelists")1367]1368 self.description ="Submit changes from git to the perforce depot."1369 self.usage +=" [name of git branch to submit into perforce depot]"1370 self.origin =""1371 self.detectRenames =False1372 self.preserveUser =gitConfigBool("git-p4.preserveUser")1373 self.dry_run =False1374 self.shelve =False1375 self.update_shelve =list()1376 self.prepare_p4_only =False1377 self.conflict_behavior =None1378 self.isWindows = (platform.system() =="Windows")1379 self.exportLabels =False1380 self.p4HasMoveCommand =p4_has_move_command()1381 self.branch =None13821383ifgitConfig('git-p4.largeFileSystem'):1384die("Large file system not supported for git-p4 submit command. Please remove it from config.")13851386defcheck(self):1387iflen(p4CmdList("opened ...")) >0:1388die("You have files opened with perforce! Close them before starting the sync.")13891390defseparate_jobs_from_description(self, message):1391"""Extract and return a possible Jobs field in the commit1392 message. It goes into a separate section in the p4 change1393 specification.13941395 A jobs line starts with "Jobs:" and looks like a new field1396 in a form. Values are white-space separated on the same1397 line or on following lines that start with a tab.13981399 This does not parse and extract the full git commit message1400 like a p4 form. It just sees the Jobs: line as a marker1401 to pass everything from then on directly into the p4 form,1402 but outside the description section.14031404 Return a tuple (stripped log message, jobs string)."""14051406 m = re.search(r'^Jobs:', message, re.MULTILINE)1407if m is None:1408return(message,None)14091410 jobtext = message[m.start():]1411 stripped_message = message[:m.start()].rstrip()1412return(stripped_message, jobtext)14131414defprepareLogMessage(self, template, message, jobs):1415"""Edits the template returned from "p4 change -o" to insert1416 the message in the Description field, and the jobs text in1417 the Jobs field."""1418 result =""14191420 inDescriptionSection =False14211422for line in template.split("\n"):1423if line.startswith("#"):1424 result += line +"\n"1425continue14261427if inDescriptionSection:1428if line.startswith("Files:")or line.startswith("Jobs:"):1429 inDescriptionSection =False1430# insert Jobs section1431if jobs:1432 result += jobs +"\n"1433else:1434continue1435else:1436if line.startswith("Description:"):1437 inDescriptionSection =True1438 line +="\n"1439for messageLine in message.split("\n"):1440 line +="\t"+ messageLine +"\n"14411442 result += line +"\n"14431444return result14451446defpatchRCSKeywords(self,file, pattern):1447# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1448(handle, outFileName) = tempfile.mkstemp(dir='.')1449try:1450 outFile = os.fdopen(handle,"w+")1451 inFile =open(file,"r")1452 regexp = re.compile(pattern, re.VERBOSE)1453for line in inFile.readlines():1454 line = regexp.sub(r'$\1$', line)1455 outFile.write(line)1456 inFile.close()1457 outFile.close()1458# Forcibly overwrite the original file1459 os.unlink(file)1460 shutil.move(outFileName,file)1461except:1462# cleanup our temporary file1463 os.unlink(outFileName)1464print"Failed to strip RCS keywords in%s"%file1465raise14661467print"Patched up RCS keywords in%s"%file14681469defp4UserForCommit(self,id):1470# Return the tuple (perforce user,git email) for a given git commit id1471 self.getUserMapFromPerforceServer()1472 gitEmail =read_pipe(["git","log","--max-count=1",1473"--format=%ae",id])1474 gitEmail = gitEmail.strip()1475if not self.emails.has_key(gitEmail):1476return(None,gitEmail)1477else:1478return(self.emails[gitEmail],gitEmail)14791480defcheckValidP4Users(self,commits):1481# check if any git authors cannot be mapped to p4 users1482foridin commits:1483(user,email) = self.p4UserForCommit(id)1484if not user:1485 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1486ifgitConfigBool("git-p4.allowMissingP4Users"):1487print"%s"% msg1488else:1489die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14901491deflastP4Changelist(self):1492# Get back the last changelist number submitted in this client spec. This1493# then gets used to patch up the username in the change. If the same1494# client spec is being used by multiple processes then this might go1495# wrong.1496 results =p4CmdList("client -o")# find the current client1497 client =None1498for r in results:1499if r.has_key('Client'):1500 client = r['Client']1501break1502if not client:1503die("could not get client spec")1504 results =p4CmdList(["changes","-c", client,"-m","1"])1505for r in results:1506if r.has_key('change'):1507return r['change']1508die("Could not get changelist number for last submit - cannot patch up user details")15091510defmodifyChangelistUser(self, changelist, newUser):1511# fixup the user field of a changelist after it has been submitted.1512 changes =p4CmdList("change -o%s"% changelist)1513iflen(changes) !=1:1514die("Bad output from p4 change modifying%sto user%s"%1515(changelist, newUser))15161517 c = changes[0]1518if c['User'] == newUser:return# nothing to do1519 c['User'] = newUser1520input= marshal.dumps(c)15211522 result =p4CmdList("change -f -i", stdin=input)1523for r in result:1524if r.has_key('code'):1525if r['code'] =='error':1526die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1527if r.has_key('data'):1528print("Updated user field for changelist%sto%s"% (changelist, newUser))1529return1530die("Could not modify user field of changelist%sto%s"% (changelist, newUser))15311532defcanChangeChangelists(self):1533# check to see if we have p4 admin or super-user permissions, either of1534# which are required to modify changelists.1535 results =p4CmdList(["protects", self.depotPath])1536for r in results:1537if r.has_key('perm'):1538if r['perm'] =='admin':1539return11540if r['perm'] =='super':1541return11542return015431544defprepareSubmitTemplate(self, changelist=None):1545"""Run "p4 change -o" to grab a change specification template.1546 This does not use "p4 -G", as it is nice to keep the submission1547 template in original order, since a human might edit it.15481549 Remove lines in the Files section that show changes to files1550 outside the depot path we're committing into."""15511552[upstream, settings] =findUpstreamBranchPoint()15531554 template ="""\1555# A Perforce Change Specification.1556#1557# Change: The change number. 'new' on a new changelist.1558# Date: The date this specification was last modified.1559# Client: The client on which the changelist was created. Read-only.1560# User: The user who created the changelist.1561# Status: Either 'pending' or 'submitted'. Read-only.1562# Type: Either 'public' or 'restricted'. Default is 'public'.1563# Description: Comments about the changelist. Required.1564# Jobs: What opened jobs are to be closed by this changelist.1565# You may delete jobs from this list. (New changelists only.)1566# Files: What opened files from the default changelist are to be added1567# to this changelist. You may delete files from this list.1568# (New changelists only.)1569"""1570 files_list = []1571 inFilesSection =False1572 change_entry =None1573 args = ['change','-o']1574if changelist:1575 args.append(str(changelist))1576for entry inp4CmdList(args):1577if not entry.has_key('code'):1578continue1579if entry['code'] =='stat':1580 change_entry = entry1581break1582if not change_entry:1583die('Failed to decode output of p4 change -o')1584for key, value in change_entry.iteritems():1585if key.startswith('File'):1586if settings.has_key('depot-paths'):1587if not[p for p in settings['depot-paths']1588ifp4PathStartsWith(value, p)]:1589continue1590else:1591if notp4PathStartsWith(value, self.depotPath):1592continue1593 files_list.append(value)1594continue1595# Output in the order expected by prepareLogMessage1596for key in['Change','Client','User','Status','Description','Jobs']:1597if not change_entry.has_key(key):1598continue1599 template +='\n'1600 template += key +':'1601if key =='Description':1602 template +='\n'1603for field_line in change_entry[key].splitlines():1604 template +='\t'+field_line+'\n'1605iflen(files_list) >0:1606 template +='\n'1607 template +='Files:\n'1608for path in files_list:1609 template +='\t'+path+'\n'1610return template16111612defedit_template(self, template_file):1613"""Invoke the editor to let the user change the submission1614 message. Return true if okay to continue with the submit."""16151616# if configured to skip the editing part, just submit1617ifgitConfigBool("git-p4.skipSubmitEdit"):1618return True16191620# look at the modification time, to check later if the user saved1621# the file1622 mtime = os.stat(template_file).st_mtime16231624# invoke the editor1625if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1626 editor = os.environ.get("P4EDITOR")1627else:1628 editor =read_pipe("git var GIT_EDITOR").strip()1629system(["sh","-c", ('%s"$@"'% editor), editor, template_file])16301631# If the file was not saved, prompt to see if this patch should1632# be skipped. But skip this verification step if configured so.1633ifgitConfigBool("git-p4.skipSubmitEditCheck"):1634return True16351636# modification time updated means user saved the file1637if os.stat(template_file).st_mtime > mtime:1638return True16391640while True:1641 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1642if response =='y':1643return True1644if response =='n':1645return False16461647defget_diff_description(self, editedFiles, filesToAdd, symlinks):1648# diff1649if os.environ.has_key("P4DIFF"):1650del(os.environ["P4DIFF"])1651 diff =""1652for editedFile in editedFiles:1653 diff +=p4_read_pipe(['diff','-du',1654wildcard_encode(editedFile)])16551656# new file diff1657 newdiff =""1658for newFile in filesToAdd:1659 newdiff +="==== new file ====\n"1660 newdiff +="--- /dev/null\n"1661 newdiff +="+++%s\n"% newFile16621663 is_link = os.path.islink(newFile)1664 expect_link = newFile in symlinks16651666if is_link and expect_link:1667 newdiff +="+%s\n"% os.readlink(newFile)1668else:1669 f =open(newFile,"r")1670for line in f.readlines():1671 newdiff +="+"+ line1672 f.close()16731674return(diff + newdiff).replace('\r\n','\n')16751676defapplyCommit(self,id):1677"""Apply one commit, return True if it succeeded."""16781679print"Applying",read_pipe(["git","show","-s",1680"--format=format:%h%s",id])16811682(p4User, gitEmail) = self.p4UserForCommit(id)16831684 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1685 filesToAdd =set()1686 filesToChangeType =set()1687 filesToDelete =set()1688 editedFiles =set()1689 pureRenameCopy =set()1690 symlinks =set()1691 filesToChangeExecBit = {}1692 all_files =list()16931694for line in diff:1695 diff =parseDiffTreeEntry(line)1696 modifier = diff['status']1697 path = diff['src']1698 all_files.append(path)16991700if modifier =="M":1701p4_edit(path)1702ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1703 filesToChangeExecBit[path] = diff['dst_mode']1704 editedFiles.add(path)1705elif modifier =="A":1706 filesToAdd.add(path)1707 filesToChangeExecBit[path] = diff['dst_mode']1708if path in filesToDelete:1709 filesToDelete.remove(path)17101711 dst_mode =int(diff['dst_mode'],8)1712if dst_mode ==0120000:1713 symlinks.add(path)17141715elif modifier =="D":1716 filesToDelete.add(path)1717if path in filesToAdd:1718 filesToAdd.remove(path)1719elif modifier =="C":1720 src, dest = diff['src'], diff['dst']1721p4_integrate(src, dest)1722 pureRenameCopy.add(dest)1723if diff['src_sha1'] != diff['dst_sha1']:1724p4_edit(dest)1725 pureRenameCopy.discard(dest)1726ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1727p4_edit(dest)1728 pureRenameCopy.discard(dest)1729 filesToChangeExecBit[dest] = diff['dst_mode']1730if self.isWindows:1731# turn off read-only attribute1732 os.chmod(dest, stat.S_IWRITE)1733 os.unlink(dest)1734 editedFiles.add(dest)1735elif modifier =="R":1736 src, dest = diff['src'], diff['dst']1737if self.p4HasMoveCommand:1738p4_edit(src)# src must be open before move1739p4_move(src, dest)# opens for (move/delete, move/add)1740else:1741p4_integrate(src, dest)1742if diff['src_sha1'] != diff['dst_sha1']:1743p4_edit(dest)1744else:1745 pureRenameCopy.add(dest)1746ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1747if not self.p4HasMoveCommand:1748p4_edit(dest)# with move: already open, writable1749 filesToChangeExecBit[dest] = diff['dst_mode']1750if not self.p4HasMoveCommand:1751if self.isWindows:1752 os.chmod(dest, stat.S_IWRITE)1753 os.unlink(dest)1754 filesToDelete.add(src)1755 editedFiles.add(dest)1756elif modifier =="T":1757 filesToChangeType.add(path)1758else:1759die("unknown modifier%sfor%s"% (modifier, path))17601761 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1762 patchcmd = diffcmd +" | git apply "1763 tryPatchCmd = patchcmd +"--check -"1764 applyPatchCmd = patchcmd +"--check --apply -"1765 patch_succeeded =True17661767if os.system(tryPatchCmd) !=0:1768 fixed_rcs_keywords =False1769 patch_succeeded =False1770print"Unfortunately applying the change failed!"17711772# Patch failed, maybe it's just RCS keyword woes. Look through1773# the patch to see if that's possible.1774ifgitConfigBool("git-p4.attemptRCSCleanup"):1775file=None1776 pattern =None1777 kwfiles = {}1778forfilein editedFiles | filesToDelete:1779# did this file's delta contain RCS keywords?1780 pattern =p4_keywords_regexp_for_file(file)17811782if pattern:1783# this file is a possibility...look for RCS keywords.1784 regexp = re.compile(pattern, re.VERBOSE)1785for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1786if regexp.search(line):1787if verbose:1788print"got keyword match on%sin%sin%s"% (pattern, line,file)1789 kwfiles[file] = pattern1790break17911792forfilein kwfiles:1793if verbose:1794print"zapping%swith%s"% (line,pattern)1795# File is being deleted, so not open in p4. Must1796# disable the read-only bit on windows.1797if self.isWindows andfilenot in editedFiles:1798 os.chmod(file, stat.S_IWRITE)1799 self.patchRCSKeywords(file, kwfiles[file])1800 fixed_rcs_keywords =True18011802if fixed_rcs_keywords:1803print"Retrying the patch with RCS keywords cleaned up"1804if os.system(tryPatchCmd) ==0:1805 patch_succeeded =True18061807if not patch_succeeded:1808for f in editedFiles:1809p4_revert(f)1810return False18111812#1813# Apply the patch for real, and do add/delete/+x handling.1814#1815system(applyPatchCmd)18161817for f in filesToChangeType:1818p4_edit(f,"-t","auto")1819for f in filesToAdd:1820p4_add(f)1821for f in filesToDelete:1822p4_revert(f)1823p4_delete(f)18241825# Set/clear executable bits1826for f in filesToChangeExecBit.keys():1827 mode = filesToChangeExecBit[f]1828setP4ExecBit(f, mode)18291830 update_shelve =01831iflen(self.update_shelve) >0:1832 update_shelve = self.update_shelve.pop(0)1833p4_reopen_in_change(update_shelve, all_files)18341835#1836# Build p4 change description, starting with the contents1837# of the git commit message.1838#1839 logMessage =extractLogMessageFromGitCommit(id)1840 logMessage = logMessage.strip()1841(logMessage, jobs) = self.separate_jobs_from_description(logMessage)18421843 template = self.prepareSubmitTemplate(update_shelve)1844 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)18451846if self.preserveUser:1847 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User18481849if self.checkAuthorship and not self.p4UserIsMe(p4User):1850 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1851 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1852 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"18531854 separatorLine ="######## everything below this line is just the diff #######\n"1855if not self.prepare_p4_only:1856 submitTemplate += separatorLine1857 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)18581859(handle, fileName) = tempfile.mkstemp()1860 tmpFile = os.fdopen(handle,"w+b")1861if self.isWindows:1862 submitTemplate = submitTemplate.replace("\n","\r\n")1863 tmpFile.write(submitTemplate)1864 tmpFile.close()18651866if self.prepare_p4_only:1867#1868# Leave the p4 tree prepared, and the submit template around1869# and let the user decide what to do next1870#1871print1872print"P4 workspace prepared for submission."1873print"To submit or revert, go to client workspace"1874print" "+ self.clientPath1875print1876print"To submit, use\"p4 submit\"to write a new description,"1877print"or\"p4 submit -i <%s\"to use the one prepared by" \1878"\"git p4\"."% fileName1879print"You can delete the file\"%s\"when finished."% fileName18801881if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1882print"To preserve change ownership by user%s, you must\n" \1883"do\"p4 change -f <change>\"after submitting and\n" \1884"edit the User field."1885if pureRenameCopy:1886print"After submitting, renamed files must be re-synced."1887print"Invoke\"p4 sync -f\"on each of these files:"1888for f in pureRenameCopy:1889print" "+ f18901891print1892print"To revert the changes, use\"p4 revert ...\", and delete"1893print"the submit template file\"%s\""% fileName1894if filesToAdd:1895print"Since the commit adds new files, they must be deleted:"1896for f in filesToAdd:1897print" "+ f1898print1899return True19001901#1902# Let the user edit the change description, then submit it.1903#1904 submitted =False19051906try:1907if self.edit_template(fileName):1908# read the edited message and submit1909 tmpFile =open(fileName,"rb")1910 message = tmpFile.read()1911 tmpFile.close()1912if self.isWindows:1913 message = message.replace("\r\n","\n")1914 submitTemplate = message[:message.index(separatorLine)]19151916if update_shelve:1917p4_write_pipe(['shelve','-r','-i'], submitTemplate)1918elif self.shelve:1919p4_write_pipe(['shelve','-i'], submitTemplate)1920else:1921p4_write_pipe(['submit','-i'], submitTemplate)1922# The rename/copy happened by applying a patch that created a1923# new file. This leaves it writable, which confuses p4.1924for f in pureRenameCopy:1925p4_sync(f,"-f")19261927if self.preserveUser:1928if p4User:1929# Get last changelist number. Cannot easily get it from1930# the submit command output as the output is1931# unmarshalled.1932 changelist = self.lastP4Changelist()1933 self.modifyChangelistUser(changelist, p4User)19341935 submitted =True19361937finally:1938# skip this patch1939if not submitted or self.shelve:1940if self.shelve:1941print("Reverting shelved files.")1942else:1943print("Submission cancelled, undoing p4 changes.")1944for f in editedFiles | filesToDelete:1945p4_revert(f)1946for f in filesToAdd:1947p4_revert(f)1948 os.remove(f)19491950 os.remove(fileName)1951return submitted19521953# Export git tags as p4 labels. Create a p4 label and then tag1954# with that.1955defexportGitTags(self, gitTags):1956 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1957iflen(validLabelRegexp) ==0:1958 validLabelRegexp = defaultLabelRegexp1959 m = re.compile(validLabelRegexp)19601961for name in gitTags:19621963if not m.match(name):1964if verbose:1965print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1966continue19671968# Get the p4 commit this corresponds to1969 logMessage =extractLogMessageFromGitCommit(name)1970 values =extractSettingsGitLog(logMessage)19711972if not values.has_key('change'):1973# a tag pointing to something not sent to p4; ignore1974if verbose:1975print"git tag%sdoes not give a p4 commit"% name1976continue1977else:1978 changelist = values['change']19791980# Get the tag details.1981 inHeader =True1982 isAnnotated =False1983 body = []1984for l inread_pipe_lines(["git","cat-file","-p", name]):1985 l = l.strip()1986if inHeader:1987if re.match(r'tag\s+', l):1988 isAnnotated =True1989elif re.match(r'\s*$', l):1990 inHeader =False1991continue1992else:1993 body.append(l)19941995if not isAnnotated:1996 body = ["lightweight tag imported by git p4\n"]19971998# Create the label - use the same view as the client spec we are using1999 clientSpec =getClientSpec()20002001 labelTemplate ="Label:%s\n"% name2002 labelTemplate +="Description:\n"2003for b in body:2004 labelTemplate +="\t"+ b +"\n"2005 labelTemplate +="View:\n"2006for depot_side in clientSpec.mappings:2007 labelTemplate +="\t%s\n"% depot_side20082009if self.dry_run:2010print"Would create p4 label%sfor tag"% name2011elif self.prepare_p4_only:2012print"Not creating p4 label%sfor tag due to option" \2013" --prepare-p4-only"% name2014else:2015p4_write_pipe(["label","-i"], labelTemplate)20162017# Use the label2018p4_system(["tag","-l", name] +2019["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])20202021if verbose:2022print"created p4 label for tag%s"% name20232024defrun(self, args):2025iflen(args) ==0:2026 self.master =currentGitBranch()2027eliflen(args) ==1:2028 self.master = args[0]2029if notbranchExists(self.master):2030die("Branch%sdoes not exist"% self.master)2031else:2032return False20332034for i in self.update_shelve:2035if i <=0:2036 sys.exit("invalid changelist%d"% i)20372038if self.master:2039 allowSubmit =gitConfig("git-p4.allowSubmit")2040iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2041die("%sis not in git-p4.allowSubmit"% self.master)20422043[upstream, settings] =findUpstreamBranchPoint()2044 self.depotPath = settings['depot-paths'][0]2045iflen(self.origin) ==0:2046 self.origin = upstream20472048iflen(self.update_shelve) >0:2049 self.shelve =True20502051if self.preserveUser:2052if not self.canChangeChangelists():2053die("Cannot preserve user names without p4 super-user or admin permissions")20542055# if not set from the command line, try the config file2056if self.conflict_behavior is None:2057 val =gitConfig("git-p4.conflict")2058if val:2059if val not in self.conflict_behavior_choices:2060die("Invalid value '%s' for config git-p4.conflict"% val)2061else:2062 val ="ask"2063 self.conflict_behavior = val20642065if self.verbose:2066print"Origin branch is "+ self.origin20672068iflen(self.depotPath) ==0:2069print"Internal error: cannot locate perforce depot path from existing branches"2070 sys.exit(128)20712072 self.useClientSpec =False2073ifgitConfigBool("git-p4.useclientspec"):2074 self.useClientSpec =True2075if self.useClientSpec:2076 self.clientSpecDirs =getClientSpec()20772078# Check for the existence of P4 branches2079 branchesDetected = (len(p4BranchesInGit().keys()) >1)20802081if self.useClientSpec and not branchesDetected:2082# all files are relative to the client spec2083 self.clientPath =getClientRoot()2084else:2085 self.clientPath =p4Where(self.depotPath)20862087if self.clientPath =="":2088die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)20892090print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2091 self.oldWorkingDirectory = os.getcwd()20922093# ensure the clientPath exists2094 new_client_dir =False2095if not os.path.exists(self.clientPath):2096 new_client_dir =True2097 os.makedirs(self.clientPath)20982099chdir(self.clientPath, is_client_path=True)2100if self.dry_run:2101print"Would synchronize p4 checkout in%s"% self.clientPath2102else:2103print"Synchronizing p4 checkout..."2104if new_client_dir:2105# old one was destroyed, and maybe nobody told p42106p4_sync("...","-f")2107else:2108p4_sync("...")2109 self.check()21102111 commits = []2112if self.master:2113 committish = self.master2114else:2115 committish ='HEAD'21162117for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, committish)]):2118 commits.append(line.strip())2119 commits.reverse()21202121if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2122 self.checkAuthorship =False2123else:2124 self.checkAuthorship =True21252126if self.preserveUser:2127 self.checkValidP4Users(commits)21282129#2130# Build up a set of options to be passed to diff when2131# submitting each commit to p4.2132#2133if self.detectRenames:2134# command-line -M arg2135 self.diffOpts ="-M"2136else:2137# If not explicitly set check the config variable2138 detectRenames =gitConfig("git-p4.detectRenames")21392140if detectRenames.lower() =="false"or detectRenames =="":2141 self.diffOpts =""2142elif detectRenames.lower() =="true":2143 self.diffOpts ="-M"2144else:2145 self.diffOpts ="-M%s"% detectRenames21462147# no command-line arg for -C or --find-copies-harder, just2148# config variables2149 detectCopies =gitConfig("git-p4.detectCopies")2150if detectCopies.lower() =="false"or detectCopies =="":2151pass2152elif detectCopies.lower() =="true":2153 self.diffOpts +=" -C"2154else:2155 self.diffOpts +=" -C%s"% detectCopies21562157ifgitConfigBool("git-p4.detectCopiesHarder"):2158 self.diffOpts +=" --find-copies-harder"21592160 num_shelves =len(self.update_shelve)2161if num_shelves >0and num_shelves !=len(commits):2162 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2163(len(commits), num_shelves))21642165#2166# Apply the commits, one at a time. On failure, ask if should2167# continue to try the rest of the patches, or quit.2168#2169if self.dry_run:2170print"Would apply"2171 applied = []2172 last =len(commits) -12173for i, commit inenumerate(commits):2174if self.dry_run:2175print" ",read_pipe(["git","show","-s",2176"--format=format:%h%s", commit])2177 ok =True2178else:2179 ok = self.applyCommit(commit)2180if ok:2181 applied.append(commit)2182else:2183if self.prepare_p4_only and i < last:2184print"Processing only the first commit due to option" \2185" --prepare-p4-only"2186break2187if i < last:2188 quit =False2189while True:2190# prompt for what to do, or use the option/variable2191if self.conflict_behavior =="ask":2192print"What do you want to do?"2193 response =raw_input("[s]kip this commit but apply"2194" the rest, or [q]uit? ")2195if not response:2196continue2197elif self.conflict_behavior =="skip":2198 response ="s"2199elif self.conflict_behavior =="quit":2200 response ="q"2201else:2202die("Unknown conflict_behavior '%s'"%2203 self.conflict_behavior)22042205if response[0] =="s":2206print"Skipping this commit, but applying the rest"2207break2208if response[0] =="q":2209print"Quitting"2210 quit =True2211break2212if quit:2213break22142215chdir(self.oldWorkingDirectory)2216 shelved_applied ="shelved"if self.shelve else"applied"2217if self.dry_run:2218pass2219elif self.prepare_p4_only:2220pass2221eliflen(commits) ==len(applied):2222print("All commits{0}!".format(shelved_applied))22232224 sync =P4Sync()2225if self.branch:2226 sync.branch = self.branch2227 sync.run([])22282229 rebase =P4Rebase()2230 rebase.rebase()22312232else:2233iflen(applied) ==0:2234print("No commits{0}.".format(shelved_applied))2235else:2236print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2237for c in commits:2238if c in applied:2239 star ="*"2240else:2241 star =" "2242print star,read_pipe(["git","show","-s",2243"--format=format:%h%s", c])2244print"You will have to do 'git p4 sync' and rebase."22452246ifgitConfigBool("git-p4.exportLabels"):2247 self.exportLabels =True22482249if self.exportLabels:2250 p4Labels =getP4Labels(self.depotPath)2251 gitTags =getGitTags()22522253 missingGitTags = gitTags - p4Labels2254 self.exportGitTags(missingGitTags)22552256# exit with error unless everything applied perfectly2257iflen(commits) !=len(applied):2258 sys.exit(1)22592260return True22612262classView(object):2263"""Represent a p4 view ("p4 help views"), and map files in a2264 repo according to the view."""22652266def__init__(self, client_name):2267 self.mappings = []2268 self.client_prefix ="//%s/"% client_name2269# cache results of "p4 where" to lookup client file locations2270 self.client_spec_path_cache = {}22712272defappend(self, view_line):2273"""Parse a view line, splitting it into depot and client2274 sides. Append to self.mappings, preserving order. This2275 is only needed for tag creation."""22762277# Split the view line into exactly two words. P4 enforces2278# structure on these lines that simplifies this quite a bit.2279#2280# Either or both words may be double-quoted.2281# Single quotes do not matter.2282# Double-quote marks cannot occur inside the words.2283# A + or - prefix is also inside the quotes.2284# There are no quotes unless they contain a space.2285# The line is already white-space stripped.2286# The two words are separated by a single space.2287#2288if view_line[0] =='"':2289# First word is double quoted. Find its end.2290 close_quote_index = view_line.find('"',1)2291if close_quote_index <=0:2292die("No first-word closing quote found:%s"% view_line)2293 depot_side = view_line[1:close_quote_index]2294# skip closing quote and space2295 rhs_index = close_quote_index +1+12296else:2297 space_index = view_line.find(" ")2298if space_index <=0:2299die("No word-splitting space found:%s"% view_line)2300 depot_side = view_line[0:space_index]2301 rhs_index = space_index +123022303# prefix + means overlay on previous mapping2304if depot_side.startswith("+"):2305 depot_side = depot_side[1:]23062307# prefix - means exclude this path, leave out of mappings2308 exclude =False2309if depot_side.startswith("-"):2310 exclude =True2311 depot_side = depot_side[1:]23122313if not exclude:2314 self.mappings.append(depot_side)23152316defconvert_client_path(self, clientFile):2317# chop off //client/ part to make it relative2318if not clientFile.startswith(self.client_prefix):2319die("No prefix '%s' on clientFile '%s'"%2320(self.client_prefix, clientFile))2321return clientFile[len(self.client_prefix):]23222323defupdate_client_spec_path_cache(self, files):2324""" Caching file paths by "p4 where" batch query """23252326# List depot file paths exclude that already cached2327 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]23282329iflen(fileArgs) ==0:2330return# All files in cache23312332 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2333for res in where_result:2334if"code"in res and res["code"] =="error":2335# assume error is "... file(s) not in client view"2336continue2337if"clientFile"not in res:2338die("No clientFile in 'p4 where' output")2339if"unmap"in res:2340# it will list all of them, but only one not unmap-ped2341continue2342ifgitConfigBool("core.ignorecase"):2343 res['depotFile'] = res['depotFile'].lower()2344 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])23452346# not found files or unmap files set to ""2347for depotFile in fileArgs:2348ifgitConfigBool("core.ignorecase"):2349 depotFile = depotFile.lower()2350if depotFile not in self.client_spec_path_cache:2351 self.client_spec_path_cache[depotFile] =""23522353defmap_in_client(self, depot_path):2354"""Return the relative location in the client where this2355 depot file should live. Returns "" if the file should2356 not be mapped in the client."""23572358ifgitConfigBool("core.ignorecase"):2359 depot_path = depot_path.lower()23602361if depot_path in self.client_spec_path_cache:2362return self.client_spec_path_cache[depot_path]23632364die("Error:%sis not found in client spec path"% depot_path )2365return""23662367classP4Sync(Command, P4UserMap):2368 delete_actions = ("delete","move/delete","purge")23692370def__init__(self):2371 Command.__init__(self)2372 P4UserMap.__init__(self)2373 self.options = [2374 optparse.make_option("--branch", dest="branch"),2375 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2376 optparse.make_option("--changesfile", dest="changesFile"),2377 optparse.make_option("--silent", dest="silent", action="store_true"),2378 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2379 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2380 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2381help="Import into refs/heads/ , not refs/remotes"),2382 optparse.make_option("--max-changes", dest="maxChanges",2383help="Maximum number of changes to import"),2384 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2385help="Internal block size to use when iteratively calling p4 changes"),2386 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2387help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2388 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2389help="Only sync files that are included in the Perforce Client Spec"),2390 optparse.make_option("-/", dest="cloneExclude",2391 action="append",type="string",2392help="exclude depot path"),2393]2394 self.description ="""Imports from Perforce into a git repository.\n2395 example:2396 //depot/my/project/ -- to import the current head2397 //depot/my/project/@all -- to import everything2398 //depot/my/project/@1,6 -- to import only from revision 1 to 623992400 (a ... is not needed in the path p4 specification, it's added implicitly)"""24012402 self.usage +=" //depot/path[@revRange]"2403 self.silent =False2404 self.createdBranches =set()2405 self.committedChanges =set()2406 self.branch =""2407 self.detectBranches =False2408 self.detectLabels =False2409 self.importLabels =False2410 self.changesFile =""2411 self.syncWithOrigin =True2412 self.importIntoRemotes =True2413 self.maxChanges =""2414 self.changes_block_size =None2415 self.keepRepoPath =False2416 self.depotPaths =None2417 self.p4BranchesInGit = []2418 self.cloneExclude = []2419 self.useClientSpec =False2420 self.useClientSpec_from_options =False2421 self.clientSpecDirs =None2422 self.tempBranches = []2423 self.tempBranchLocation ="refs/git-p4-tmp"2424 self.largeFileSystem =None2425 self.suppress_meta_comment =False24262427ifgitConfig('git-p4.largeFileSystem'):2428 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2429 self.largeFileSystem =largeFileSystemConstructor(2430lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2431)24322433ifgitConfig("git-p4.syncFromOrigin") =="false":2434 self.syncWithOrigin =False24352436 self.depotPaths = []2437 self.changeRange =""2438 self.previousDepotPaths = []2439 self.hasOrigin =False24402441# map from branch depot path to parent branch2442 self.knownBranches = {}2443 self.initialParents = {}24442445 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))2446 self.labels = {}24472448# Force a checkpoint in fast-import and wait for it to finish2449defcheckpoint(self):2450 self.gitStream.write("checkpoint\n\n")2451 self.gitStream.write("progress checkpoint\n\n")2452 out = self.gitOutput.readline()2453if self.verbose:2454print"checkpoint finished: "+ out24552456defcmp_shelved(self, path, filerev, revision):2457""" Determine if a path at revision #filerev is the same as the file2458 at revision @revision for a shelved changelist. If they don't match,2459 unshelving won't be safe (we will get other changes mixed in).24602461 This is comparing the revision that the shelved changelist is *based* on, not2462 the shelved changelist itself.2463 """2464 ret =p4Cmd(["diff2","{0}#{1}".format(path, filerev),"{0}@{1}".format(path, revision)])2465if verbose:2466print("p4 diff2 path%sfilerev%srevision%s=>%s"% (path, filerev, revision, ret))2467return ret["status"] =="identical"24682469defextractFilesFromCommit(self, commit, shelved=False, shelved_cl =0, origin_revision =0):2470 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2471for path in self.cloneExclude]2472 files = []2473 fnum =02474while commit.has_key("depotFile%s"% fnum):2475 path = commit["depotFile%s"% fnum]24762477if[p for p in self.cloneExclude2478ifp4PathStartsWith(path, p)]:2479 found =False2480else:2481 found = [p for p in self.depotPaths2482ifp4PathStartsWith(path, p)]2483if not found:2484 fnum = fnum +12485continue24862487file= {}2488file["path"] = path2489file["rev"] = commit["rev%s"% fnum]2490file["action"] = commit["action%s"% fnum]2491file["type"] = commit["type%s"% fnum]2492if shelved:2493file["shelved_cl"] =int(shelved_cl)24942495# For shelved changelists, check that the revision of each file that the2496# shelve was based on matches the revision that we are using for the2497# starting point for git-fast-import (self.initialParent). Otherwise2498# the resulting diff will contain deltas from multiple commits.24992500iffile["action"] !="add"and \2501not self.cmp_shelved(path,file["rev"], origin_revision):2502 sys.exit("change{0}not based on{1}for{2}, cannot unshelve".format(2503 commit["change"], self.initialParent, path))25042505 files.append(file)2506 fnum = fnum +12507return files25082509defextractJobsFromCommit(self, commit):2510 jobs = []2511 jnum =02512while commit.has_key("job%s"% jnum):2513 job = commit["job%s"% jnum]2514 jobs.append(job)2515 jnum = jnum +12516return jobs25172518defstripRepoPath(self, path, prefixes):2519"""When streaming files, this is called to map a p4 depot path2520 to where it should go in git. The prefixes are either2521 self.depotPaths, or self.branchPrefixes in the case of2522 branch detection."""25232524if self.useClientSpec:2525# branch detection moves files up a level (the branch name)2526# from what client spec interpretation gives2527 path = self.clientSpecDirs.map_in_client(path)2528if self.detectBranches:2529for b in self.knownBranches:2530if path.startswith(b +"/"):2531 path = path[len(b)+1:]25322533elif self.keepRepoPath:2534# Preserve everything in relative path name except leading2535# //depot/; just look at first prefix as they all should2536# be in the same depot.2537 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2538ifp4PathStartsWith(path, depot):2539 path = path[len(depot):]25402541else:2542for p in prefixes:2543ifp4PathStartsWith(path, p):2544 path = path[len(p):]2545break25462547 path =wildcard_decode(path)2548return path25492550defsplitFilesIntoBranches(self, commit):2551"""Look at each depotFile in the commit to figure out to what2552 branch it belongs."""25532554if self.clientSpecDirs:2555 files = self.extractFilesFromCommit(commit)2556 self.clientSpecDirs.update_client_spec_path_cache(files)25572558 branches = {}2559 fnum =02560while commit.has_key("depotFile%s"% fnum):2561 path = commit["depotFile%s"% fnum]2562 found = [p for p in self.depotPaths2563ifp4PathStartsWith(path, p)]2564if not found:2565 fnum = fnum +12566continue25672568file= {}2569file["path"] = path2570file["rev"] = commit["rev%s"% fnum]2571file["action"] = commit["action%s"% fnum]2572file["type"] = commit["type%s"% fnum]2573 fnum = fnum +125742575# start with the full relative path where this file would2576# go in a p4 client2577if self.useClientSpec:2578 relPath = self.clientSpecDirs.map_in_client(path)2579else:2580 relPath = self.stripRepoPath(path, self.depotPaths)25812582for branch in self.knownBranches.keys():2583# add a trailing slash so that a commit into qt/4.2foo2584# doesn't end up in qt/4.2, e.g.2585if relPath.startswith(branch +"/"):2586if branch not in branches:2587 branches[branch] = []2588 branches[branch].append(file)2589break25902591return branches25922593defwriteToGitStream(self, gitMode, relPath, contents):2594 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2595 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2596for d in contents:2597 self.gitStream.write(d)2598 self.gitStream.write('\n')25992600defencodeWithUTF8(self, path):2601try:2602 path.decode('ascii')2603except:2604 encoding ='utf8'2605ifgitConfig('git-p4.pathEncoding'):2606 encoding =gitConfig('git-p4.pathEncoding')2607 path = path.decode(encoding,'replace').encode('utf8','replace')2608if self.verbose:2609print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2610return path26112612# output one file from the P4 stream2613# - helper for streamP4Files26142615defstreamOneP4File(self,file, contents):2616 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2617 relPath = self.encodeWithUTF8(relPath)2618if verbose:2619 size =int(self.stream_file['fileSize'])2620 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2621 sys.stdout.flush()26222623(type_base, type_mods) =split_p4_type(file["type"])26242625 git_mode ="100644"2626if"x"in type_mods:2627 git_mode ="100755"2628if type_base =="symlink":2629 git_mode ="120000"2630# p4 print on a symlink sometimes contains "target\n";2631# if it does, remove the newline2632 data =''.join(contents)2633if not data:2634# Some version of p4 allowed creating a symlink that pointed2635# to nothing. This causes p4 errors when checking out such2636# a change, and errors here too. Work around it by ignoring2637# the bad symlink; hopefully a future change fixes it.2638print"\nIgnoring empty symlink in%s"%file['depotFile']2639return2640elif data[-1] =='\n':2641 contents = [data[:-1]]2642else:2643 contents = [data]26442645if type_base =="utf16":2646# p4 delivers different text in the python output to -G2647# than it does when using "print -o", or normal p4 client2648# operations. utf16 is converted to ascii or utf8, perhaps.2649# But ascii text saved as -t utf16 is completely mangled.2650# Invoke print -o to get the real contents.2651#2652# On windows, the newlines will always be mangled by print, so put2653# them back too. This is not needed to the cygwin windows version,2654# just the native "NT" type.2655#2656try:2657 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2658exceptExceptionas e:2659if'Translation of file content failed'instr(e):2660 type_base ='binary'2661else:2662raise e2663else:2664ifp4_version_string().find('/NT') >=0:2665 text = text.replace('\r\n','\n')2666 contents = [ text ]26672668if type_base =="apple":2669# Apple filetype files will be streamed as a concatenation of2670# its appledouble header and the contents. This is useless2671# on both macs and non-macs. If using "print -q -o xx", it2672# will create "xx" with the data, and "%xx" with the header.2673# This is also not very useful.2674#2675# Ideally, someday, this script can learn how to generate2676# appledouble files directly and import those to git, but2677# non-mac machines can never find a use for apple filetype.2678print"\nIgnoring apple filetype file%s"%file['depotFile']2679return26802681# Note that we do not try to de-mangle keywords on utf16 files,2682# even though in theory somebody may want that.2683 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2684if pattern:2685 regexp = re.compile(pattern, re.VERBOSE)2686 text =''.join(contents)2687 text = regexp.sub(r'$\1$', text)2688 contents = [ text ]26892690if self.largeFileSystem:2691(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)26922693 self.writeToGitStream(git_mode, relPath, contents)26942695defstreamOneP4Deletion(self,file):2696 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2697 relPath = self.encodeWithUTF8(relPath)2698if verbose:2699 sys.stdout.write("delete%s\n"% relPath)2700 sys.stdout.flush()2701 self.gitStream.write("D%s\n"% relPath)27022703if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2704 self.largeFileSystem.removeLargeFile(relPath)27052706# handle another chunk of streaming data2707defstreamP4FilesCb(self, marshalled):27082709# catch p4 errors and complain2710 err =None2711if"code"in marshalled:2712if marshalled["code"] =="error":2713if"data"in marshalled:2714 err = marshalled["data"].rstrip()27152716if not err and'fileSize'in self.stream_file:2717 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2718if required_bytes >0:2719 err ='Not enough space left on%s! Free at least%iMB.'% (2720 os.getcwd(), required_bytes/1024/10242721)27222723if err:2724 f =None2725if self.stream_have_file_info:2726if"depotFile"in self.stream_file:2727 f = self.stream_file["depotFile"]2728# force a failure in fast-import, else an empty2729# commit will be made2730 self.gitStream.write("\n")2731 self.gitStream.write("die-now\n")2732 self.gitStream.close()2733# ignore errors, but make sure it exits first2734 self.importProcess.wait()2735if f:2736die("Error from p4 print for%s:%s"% (f, err))2737else:2738die("Error from p4 print:%s"% err)27392740if marshalled.has_key('depotFile')and self.stream_have_file_info:2741# start of a new file - output the old one first2742 self.streamOneP4File(self.stream_file, self.stream_contents)2743 self.stream_file = {}2744 self.stream_contents = []2745 self.stream_have_file_info =False27462747# pick up the new file information... for the2748# 'data' field we need to append to our array2749for k in marshalled.keys():2750if k =='data':2751if'streamContentSize'not in self.stream_file:2752 self.stream_file['streamContentSize'] =02753 self.stream_file['streamContentSize'] +=len(marshalled['data'])2754 self.stream_contents.append(marshalled['data'])2755else:2756 self.stream_file[k] = marshalled[k]27572758if(verbose and2759'streamContentSize'in self.stream_file and2760'fileSize'in self.stream_file and2761'depotFile'in self.stream_file):2762 size =int(self.stream_file["fileSize"])2763if size >0:2764 progress =100*self.stream_file['streamContentSize']/size2765 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2766 sys.stdout.flush()27672768 self.stream_have_file_info =True27692770# Stream directly from "p4 files" into "git fast-import"2771defstreamP4Files(self, files):2772 filesForCommit = []2773 filesToRead = []2774 filesToDelete = []27752776for f in files:2777 filesForCommit.append(f)2778if f['action']in self.delete_actions:2779 filesToDelete.append(f)2780else:2781 filesToRead.append(f)27822783# deleted files...2784for f in filesToDelete:2785 self.streamOneP4Deletion(f)27862787iflen(filesToRead) >0:2788 self.stream_file = {}2789 self.stream_contents = []2790 self.stream_have_file_info =False27912792# curry self argument2793defstreamP4FilesCbSelf(entry):2794 self.streamP4FilesCb(entry)27952796 fileArgs = []2797for f in filesToRead:2798if'shelved_cl'in f:2799# Handle shelved CLs using the "p4 print file@=N" syntax to print2800# the contents2801 fileArg ='%s@=%d'% (f['path'], f['shelved_cl'])2802else:2803 fileArg ='%s#%s'% (f['path'], f['rev'])28042805 fileArgs.append(fileArg)28062807p4CmdList(["-x","-","print"],2808 stdin=fileArgs,2809 cb=streamP4FilesCbSelf)28102811# do the last chunk2812if self.stream_file.has_key('depotFile'):2813 self.streamOneP4File(self.stream_file, self.stream_contents)28142815defmake_email(self, userid):2816if userid in self.users:2817return self.users[userid]2818else:2819return"%s<a@b>"% userid28202821defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2822""" Stream a p4 tag.2823 commit is either a git commit, or a fast-import mark, ":<p4commit>"2824 """28252826if verbose:2827print"writing tag%sfor commit%s"% (labelName, commit)2828 gitStream.write("tag%s\n"% labelName)2829 gitStream.write("from%s\n"% commit)28302831if labelDetails.has_key('Owner'):2832 owner = labelDetails["Owner"]2833else:2834 owner =None28352836# Try to use the owner of the p4 label, or failing that,2837# the current p4 user id.2838if owner:2839 email = self.make_email(owner)2840else:2841 email = self.make_email(self.p4UserId())2842 tagger ="%s %s %s"% (email, epoch, self.tz)28432844 gitStream.write("tagger%s\n"% tagger)28452846print"labelDetails=",labelDetails2847if labelDetails.has_key('Description'):2848 description = labelDetails['Description']2849else:2850 description ='Label from git p4'28512852 gitStream.write("data%d\n"%len(description))2853 gitStream.write(description)2854 gitStream.write("\n")28552856definClientSpec(self, path):2857if not self.clientSpecDirs:2858return True2859 inClientSpec = self.clientSpecDirs.map_in_client(path)2860if not inClientSpec and self.verbose:2861print('Ignoring file outside of client spec:{0}'.format(path))2862return inClientSpec28632864defhasBranchPrefix(self, path):2865if not self.branchPrefixes:2866return True2867 hasPrefix = [p for p in self.branchPrefixes2868ifp4PathStartsWith(path, p)]2869if not hasPrefix and self.verbose:2870print('Ignoring file outside of prefix:{0}'.format(path))2871return hasPrefix28722873defcommit(self, details, files, branch, parent =""):2874 epoch = details["time"]2875 author = details["user"]2876 jobs = self.extractJobsFromCommit(details)28772878if self.verbose:2879print('commit into{0}'.format(branch))28802881if self.clientSpecDirs:2882 self.clientSpecDirs.update_client_spec_path_cache(files)28832884 files = [f for f in files2885if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]28862887if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2888print('Ignoring revision{0}as it would produce an empty commit.'2889.format(details['change']))2890return28912892 self.gitStream.write("commit%s\n"% branch)2893 self.gitStream.write("mark :%s\n"% details["change"])2894 self.committedChanges.add(int(details["change"]))2895 committer =""2896if author not in self.users:2897 self.getUserMapFromPerforceServer()2898 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)28992900 self.gitStream.write("committer%s\n"% committer)29012902 self.gitStream.write("data <<EOT\n")2903 self.gitStream.write(details["desc"])2904iflen(jobs) >0:2905 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))29062907if not self.suppress_meta_comment:2908 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2909(','.join(self.branchPrefixes), details["change"]))2910iflen(details['options']) >0:2911 self.gitStream.write(": options =%s"% details['options'])2912 self.gitStream.write("]\n")29132914 self.gitStream.write("EOT\n\n")29152916iflen(parent) >0:2917if self.verbose:2918print"parent%s"% parent2919 self.gitStream.write("from%s\n"% parent)29202921 self.streamP4Files(files)2922 self.gitStream.write("\n")29232924 change =int(details["change"])29252926if self.labels.has_key(change):2927 label = self.labels[change]2928 labelDetails = label[0]2929 labelRevisions = label[1]2930if self.verbose:2931print"Change%sis labelled%s"% (change, labelDetails)29322933 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2934for p in self.branchPrefixes])29352936iflen(files) ==len(labelRevisions):29372938 cleanedFiles = {}2939for info in files:2940if info["action"]in self.delete_actions:2941continue2942 cleanedFiles[info["depotFile"]] = info["rev"]29432944if cleanedFiles == labelRevisions:2945 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)29462947else:2948if not self.silent:2949print("Tag%sdoes not match with change%s: files do not match."2950% (labelDetails["label"], change))29512952else:2953if not self.silent:2954print("Tag%sdoes not match with change%s: file count is different."2955% (labelDetails["label"], change))29562957# Build a dictionary of changelists and labels, for "detect-labels" option.2958defgetLabels(self):2959 self.labels = {}29602961 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2962iflen(l) >0and not self.silent:2963print"Finding files belonging to labels in%s"% `self.depotPaths`29642965for output in l:2966 label = output["label"]2967 revisions = {}2968 newestChange =02969if self.verbose:2970print"Querying files for label%s"% label2971forfileinp4CmdList(["files"] +2972["%s...@%s"% (p, label)2973for p in self.depotPaths]):2974 revisions[file["depotFile"]] =file["rev"]2975 change =int(file["change"])2976if change > newestChange:2977 newestChange = change29782979 self.labels[newestChange] = [output, revisions]29802981if self.verbose:2982print"Label changes:%s"% self.labels.keys()29832984# Import p4 labels as git tags. A direct mapping does not2985# exist, so assume that if all the files are at the same revision2986# then we can use that, or it's something more complicated we should2987# just ignore.2988defimportP4Labels(self, stream, p4Labels):2989if verbose:2990print"import p4 labels: "+' '.join(p4Labels)29912992 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2993 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2994iflen(validLabelRegexp) ==0:2995 validLabelRegexp = defaultLabelRegexp2996 m = re.compile(validLabelRegexp)29972998for name in p4Labels:2999 commitFound =False30003001if not m.match(name):3002if verbose:3003print"label%sdoes not match regexp%s"% (name,validLabelRegexp)3004continue30053006if name in ignoredP4Labels:3007continue30083009 labelDetails =p4CmdList(['label',"-o", name])[0]30103011# get the most recent changelist for each file in this label3012 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)3013for p in self.depotPaths])30143015if change.has_key('change'):3016# find the corresponding git commit; take the oldest commit3017 changelist =int(change['change'])3018if changelist in self.committedChanges:3019 gitCommit =":%d"% changelist # use a fast-import mark3020 commitFound =True3021else:3022 gitCommit =read_pipe(["git","rev-list","--max-count=1",3023"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)3024iflen(gitCommit) ==0:3025print"importing label%s: could not find git commit for changelist%d"% (name, changelist)3026else:3027 commitFound =True3028 gitCommit = gitCommit.strip()30293030if commitFound:3031# Convert from p4 time format3032try:3033 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")3034exceptValueError:3035print"Could not convert label time%s"% labelDetails['Update']3036 tmwhen =130373038 when =int(time.mktime(tmwhen))3039 self.streamTag(stream, name, labelDetails, gitCommit, when)3040if verbose:3041print"p4 label%smapped to git commit%s"% (name, gitCommit)3042else:3043if verbose:3044print"Label%shas no changelists - possibly deleted?"% name30453046if not commitFound:3047# We can't import this label; don't try again as it will get very3048# expensive repeatedly fetching all the files for labels that will3049# never be imported. If the label is moved in the future, the3050# ignore will need to be removed manually.3051system(["git","config","--add","git-p4.ignoredP4Labels", name])30523053defguessProjectName(self):3054for p in self.depotPaths:3055if p.endswith("/"):3056 p = p[:-1]3057 p = p[p.strip().rfind("/") +1:]3058if not p.endswith("/"):3059 p +="/"3060return p30613062defgetBranchMapping(self):3063 lostAndFoundBranches =set()30643065 user =gitConfig("git-p4.branchUser")3066iflen(user) >0:3067 command ="branches -u%s"% user3068else:3069 command ="branches"30703071for info inp4CmdList(command):3072 details =p4Cmd(["branch","-o", info["branch"]])3073 viewIdx =03074while details.has_key("View%s"% viewIdx):3075 paths = details["View%s"% viewIdx].split(" ")3076 viewIdx = viewIdx +13077# require standard //depot/foo/... //depot/bar/... mapping3078iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3079continue3080 source = paths[0]3081 destination = paths[1]3082## HACK3083ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3084 source = source[len(self.depotPaths[0]):-4]3085 destination = destination[len(self.depotPaths[0]):-4]30863087if destination in self.knownBranches:3088if not self.silent:3089print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)3090print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)3091continue30923093 self.knownBranches[destination] = source30943095 lostAndFoundBranches.discard(destination)30963097if source not in self.knownBranches:3098 lostAndFoundBranches.add(source)30993100# Perforce does not strictly require branches to be defined, so we also3101# check git config for a branch list.3102#3103# Example of branch definition in git config file:3104# [git-p4]3105# branchList=main:branchA3106# branchList=main:branchB3107# branchList=branchA:branchC3108 configBranches =gitConfigList("git-p4.branchList")3109for branch in configBranches:3110if branch:3111(source, destination) = branch.split(":")3112 self.knownBranches[destination] = source31133114 lostAndFoundBranches.discard(destination)31153116if source not in self.knownBranches:3117 lostAndFoundBranches.add(source)311831193120for branch in lostAndFoundBranches:3121 self.knownBranches[branch] = branch31223123defgetBranchMappingFromGitBranches(self):3124 branches =p4BranchesInGit(self.importIntoRemotes)3125for branch in branches.keys():3126if branch =="master":3127 branch ="main"3128else:3129 branch = branch[len(self.projectName):]3130 self.knownBranches[branch] = branch31313132defupdateOptionDict(self, d):3133 option_keys = {}3134if self.keepRepoPath:3135 option_keys['keepRepoPath'] =131363137 d["options"] =' '.join(sorted(option_keys.keys()))31383139defreadOptions(self, d):3140 self.keepRepoPath = (d.has_key('options')3141and('keepRepoPath'in d['options']))31423143defgitRefForBranch(self, branch):3144if branch =="main":3145return self.refPrefix +"master"31463147iflen(branch) <=0:3148return branch31493150return self.refPrefix + self.projectName + branch31513152defgitCommitByP4Change(self, ref, change):3153if self.verbose:3154print"looking in ref "+ ref +" for change%susing bisect..."% change31553156 earliestCommit =""3157 latestCommit =parseRevision(ref)31583159while True:3160if self.verbose:3161print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3162 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3163iflen(next) ==0:3164if self.verbose:3165print"argh"3166return""3167 log =extractLogMessageFromGitCommit(next)3168 settings =extractSettingsGitLog(log)3169 currentChange =int(settings['change'])3170if self.verbose:3171print"current change%s"% currentChange31723173if currentChange == change:3174if self.verbose:3175print"found%s"% next3176return next31773178if currentChange < change:3179 earliestCommit ="^%s"% next3180else:3181 latestCommit ="%s"% next31823183return""31843185defimportNewBranch(self, branch, maxChange):3186# make fast-import flush all changes to disk and update the refs using the checkpoint3187# command so that we can try to find the branch parent in the git history3188 self.gitStream.write("checkpoint\n\n");3189 self.gitStream.flush();3190 branchPrefix = self.depotPaths[0] + branch +"/"3191range="@1,%s"% maxChange3192#print "prefix" + branchPrefix3193 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3194iflen(changes) <=0:3195return False3196 firstChange = changes[0]3197#print "first change in branch: %s" % firstChange3198 sourceBranch = self.knownBranches[branch]3199 sourceDepotPath = self.depotPaths[0] + sourceBranch3200 sourceRef = self.gitRefForBranch(sourceBranch)3201#print "source " + sourceBranch32023203 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3204#print "branch parent: %s" % branchParentChange3205 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3206iflen(gitParent) >0:3207 self.initialParents[self.gitRefForBranch(branch)] = gitParent3208#print "parent git commit: %s" % gitParent32093210 self.importChanges(changes)3211return True32123213defsearchParent(self, parent, branch, target):3214 parentFound =False3215for blob inread_pipe_lines(["git","rev-list","--reverse",3216"--no-merges", parent]):3217 blob = blob.strip()3218iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3219 parentFound =True3220if self.verbose:3221print"Found parent of%sin commit%s"% (branch, blob)3222break3223if parentFound:3224return blob3225else:3226return None32273228defimportChanges(self, changes, shelved=False, origin_revision=0):3229 cnt =13230for change in changes:3231 description =p4_describe(change, shelved)3232 self.updateOptionDict(description)32333234if not self.silent:3235 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3236 sys.stdout.flush()3237 cnt = cnt +132383239try:3240if self.detectBranches:3241 branches = self.splitFilesIntoBranches(description)3242for branch in branches.keys():3243## HACK --hwn3244 branchPrefix = self.depotPaths[0] + branch +"/"3245 self.branchPrefixes = [ branchPrefix ]32463247 parent =""32483249 filesForCommit = branches[branch]32503251if self.verbose:3252print"branch is%s"% branch32533254 self.updatedBranches.add(branch)32553256if branch not in self.createdBranches:3257 self.createdBranches.add(branch)3258 parent = self.knownBranches[branch]3259if parent == branch:3260 parent =""3261else:3262 fullBranch = self.projectName + branch3263if fullBranch not in self.p4BranchesInGit:3264if not self.silent:3265print("\nImporting new branch%s"% fullBranch);3266if self.importNewBranch(branch, change -1):3267 parent =""3268 self.p4BranchesInGit.append(fullBranch)3269if not self.silent:3270print("\nResuming with change%s"% change);32713272if self.verbose:3273print"parent determined through known branches:%s"% parent32743275 branch = self.gitRefForBranch(branch)3276 parent = self.gitRefForBranch(parent)32773278if self.verbose:3279print"looking for initial parent for%s; current parent is%s"% (branch, parent)32803281iflen(parent) ==0and branch in self.initialParents:3282 parent = self.initialParents[branch]3283del self.initialParents[branch]32843285 blob =None3286iflen(parent) >0:3287 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3288if self.verbose:3289print"Creating temporary branch: "+ tempBranch3290 self.commit(description, filesForCommit, tempBranch)3291 self.tempBranches.append(tempBranch)3292 self.checkpoint()3293 blob = self.searchParent(parent, branch, tempBranch)3294if blob:3295 self.commit(description, filesForCommit, branch, blob)3296else:3297if self.verbose:3298print"Parent of%snot found. Committing into head of%s"% (branch, parent)3299 self.commit(description, filesForCommit, branch, parent)3300else:3301 files = self.extractFilesFromCommit(description, shelved, change, origin_revision)3302 self.commit(description, files, self.branch,3303 self.initialParent)3304# only needed once, to connect to the previous commit3305 self.initialParent =""3306exceptIOError:3307print self.gitError.read()3308 sys.exit(1)33093310defimportHeadRevision(self, revision):3311print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)33123313 details = {}3314 details["user"] ="git perforce import user"3315 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3316% (' '.join(self.depotPaths), revision))3317 details["change"] = revision3318 newestRevision =033193320 fileCnt =03321 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]33223323for info inp4CmdList(["files"] + fileArgs):33243325if'code'in info and info['code'] =='error':3326 sys.stderr.write("p4 returned an error:%s\n"3327% info['data'])3328if info['data'].find("must refer to client") >=0:3329 sys.stderr.write("This particular p4 error is misleading.\n")3330 sys.stderr.write("Perhaps the depot path was misspelled.\n");3331 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3332 sys.exit(1)3333if'p4ExitCode'in info:3334 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3335 sys.exit(1)333633373338 change =int(info["change"])3339if change > newestRevision:3340 newestRevision = change33413342if info["action"]in self.delete_actions:3343# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3344#fileCnt = fileCnt + 13345continue33463347for prop in["depotFile","rev","action","type"]:3348 details["%s%s"% (prop, fileCnt)] = info[prop]33493350 fileCnt = fileCnt +133513352 details["change"] = newestRevision33533354# Use time from top-most change so that all git p4 clones of3355# the same p4 repo have the same commit SHA1s.3356 res =p4_describe(newestRevision)3357 details["time"] = res["time"]33583359 self.updateOptionDict(details)3360try:3361 self.commit(details, self.extractFilesFromCommit(details), self.branch)3362exceptIOError:3363print"IO error with git fast-import. Is your git version recent enough?"3364print self.gitError.read()33653366defopenStreams(self):3367 self.importProcess = subprocess.Popen(["git","fast-import"],3368 stdin=subprocess.PIPE,3369 stdout=subprocess.PIPE,3370 stderr=subprocess.PIPE);3371 self.gitOutput = self.importProcess.stdout3372 self.gitStream = self.importProcess.stdin3373 self.gitError = self.importProcess.stderr33743375defcloseStreams(self):3376 self.gitStream.close()3377if self.importProcess.wait() !=0:3378die("fast-import failed:%s"% self.gitError.read())3379 self.gitOutput.close()3380 self.gitError.close()33813382defrun(self, args):3383if self.importIntoRemotes:3384 self.refPrefix ="refs/remotes/p4/"3385else:3386 self.refPrefix ="refs/heads/p4/"33873388if self.syncWithOrigin:3389 self.hasOrigin =originP4BranchesExist()3390if self.hasOrigin:3391if not self.silent:3392print'Syncing with origin first, using "git fetch origin"'3393system("git fetch origin")33943395 branch_arg_given =bool(self.branch)3396iflen(self.branch) ==0:3397 self.branch = self.refPrefix +"master"3398ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3399system("git update-ref%srefs/heads/p4"% self.branch)3400system("git branch -D p4")34013402# accept either the command-line option, or the configuration variable3403if self.useClientSpec:3404# will use this after clone to set the variable3405 self.useClientSpec_from_options =True3406else:3407ifgitConfigBool("git-p4.useclientspec"):3408 self.useClientSpec =True3409if self.useClientSpec:3410 self.clientSpecDirs =getClientSpec()34113412# TODO: should always look at previous commits,3413# merge with previous imports, if possible.3414if args == []:3415if self.hasOrigin:3416createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)34173418# branches holds mapping from branch name to sha13419 branches =p4BranchesInGit(self.importIntoRemotes)34203421# restrict to just this one, disabling detect-branches3422if branch_arg_given:3423 short = self.branch.split("/")[-1]3424if short in branches:3425 self.p4BranchesInGit = [ short ]3426else:3427 self.p4BranchesInGit = branches.keys()34283429iflen(self.p4BranchesInGit) >1:3430if not self.silent:3431print"Importing from/into multiple branches"3432 self.detectBranches =True3433for branch in branches.keys():3434 self.initialParents[self.refPrefix + branch] = \3435 branches[branch]34363437if self.verbose:3438print"branches:%s"% self.p4BranchesInGit34393440 p4Change =03441for branch in self.p4BranchesInGit:3442 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)34433444 settings =extractSettingsGitLog(logMsg)34453446 self.readOptions(settings)3447if(settings.has_key('depot-paths')3448and settings.has_key('change')):3449 change =int(settings['change']) +13450 p4Change =max(p4Change, change)34513452 depotPaths =sorted(settings['depot-paths'])3453if self.previousDepotPaths == []:3454 self.previousDepotPaths = depotPaths3455else:3456 paths = []3457for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3458 prev_list = prev.split("/")3459 cur_list = cur.split("/")3460for i inrange(0,min(len(cur_list),len(prev_list))):3461if cur_list[i] <> prev_list[i]:3462 i = i -13463break34643465 paths.append("/".join(cur_list[:i +1]))34663467 self.previousDepotPaths = paths34683469if p4Change >0:3470 self.depotPaths =sorted(self.previousDepotPaths)3471 self.changeRange ="@%s,#head"% p4Change3472if not self.silent and not self.detectBranches:3473print"Performing incremental import into%sgit branch"% self.branch34743475# accept multiple ref name abbreviations:3476# refs/foo/bar/branch -> use it exactly3477# p4/branch -> prepend refs/remotes/ or refs/heads/3478# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3479if not self.branch.startswith("refs/"):3480if self.importIntoRemotes:3481 prepend ="refs/remotes/"3482else:3483 prepend ="refs/heads/"3484if not self.branch.startswith("p4/"):3485 prepend +="p4/"3486 self.branch = prepend + self.branch34873488iflen(args) ==0and self.depotPaths:3489if not self.silent:3490print"Depot paths:%s"%' '.join(self.depotPaths)3491else:3492if self.depotPaths and self.depotPaths != args:3493print("previous import used depot path%sand now%swas specified. "3494"This doesn't work!"% (' '.join(self.depotPaths),3495' '.join(args)))3496 sys.exit(1)34973498 self.depotPaths =sorted(args)34993500 revision =""3501 self.users = {}35023503# Make sure no revision specifiers are used when --changesfile3504# is specified.3505 bad_changesfile =False3506iflen(self.changesFile) >0:3507for p in self.depotPaths:3508if p.find("@") >=0or p.find("#") >=0:3509 bad_changesfile =True3510break3511if bad_changesfile:3512die("Option --changesfile is incompatible with revision specifiers")35133514 newPaths = []3515for p in self.depotPaths:3516if p.find("@") != -1:3517 atIdx = p.index("@")3518 self.changeRange = p[atIdx:]3519if self.changeRange =="@all":3520 self.changeRange =""3521elif','not in self.changeRange:3522 revision = self.changeRange3523 self.changeRange =""3524 p = p[:atIdx]3525elif p.find("#") != -1:3526 hashIdx = p.index("#")3527 revision = p[hashIdx:]3528 p = p[:hashIdx]3529elif self.previousDepotPaths == []:3530# pay attention to changesfile, if given, else import3531# the entire p4 tree at the head revision3532iflen(self.changesFile) ==0:3533 revision ="#head"35343535 p = re.sub("\.\.\.$","", p)3536if not p.endswith("/"):3537 p +="/"35383539 newPaths.append(p)35403541 self.depotPaths = newPaths35423543# --detect-branches may change this for each branch3544 self.branchPrefixes = self.depotPaths35453546 self.loadUserMapFromCache()3547 self.labels = {}3548if self.detectLabels:3549 self.getLabels();35503551if self.detectBranches:3552## FIXME - what's a P4 projectName ?3553 self.projectName = self.guessProjectName()35543555if self.hasOrigin:3556 self.getBranchMappingFromGitBranches()3557else:3558 self.getBranchMapping()3559if self.verbose:3560print"p4-git branches:%s"% self.p4BranchesInGit3561print"initial parents:%s"% self.initialParents3562for b in self.p4BranchesInGit:3563if b !="master":35643565## FIXME3566 b = b[len(self.projectName):]3567 self.createdBranches.add(b)35683569 self.openStreams()35703571if revision:3572 self.importHeadRevision(revision)3573else:3574 changes = []35753576iflen(self.changesFile) >0:3577 output =open(self.changesFile).readlines()3578 changeSet =set()3579for line in output:3580 changeSet.add(int(line))35813582for change in changeSet:3583 changes.append(change)35843585 changes.sort()3586else:3587# catch "git p4 sync" with no new branches, in a repo that3588# does not have any existing p4 branches3589iflen(args) ==0:3590if not self.p4BranchesInGit:3591die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")35923593# The default branch is master, unless --branch is used to3594# specify something else. Make sure it exists, or complain3595# nicely about how to use --branch.3596if not self.detectBranches:3597if notbranch_exists(self.branch):3598if branch_arg_given:3599die("Error: branch%sdoes not exist."% self.branch)3600else:3601die("Error: no branch%s; perhaps specify one with --branch."%3602 self.branch)36033604if self.verbose:3605print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3606 self.changeRange)3607 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)36083609iflen(self.maxChanges) >0:3610 changes = changes[:min(int(self.maxChanges),len(changes))]36113612iflen(changes) ==0:3613if not self.silent:3614print"No changes to import!"3615else:3616if not self.silent and not self.detectBranches:3617print"Import destination:%s"% self.branch36183619 self.updatedBranches =set()36203621if not self.detectBranches:3622if args:3623# start a new branch3624 self.initialParent =""3625else:3626# build on a previous revision3627 self.initialParent =parseRevision(self.branch)36283629 self.importChanges(changes)36303631if not self.silent:3632print""3633iflen(self.updatedBranches) >0:3634 sys.stdout.write("Updated branches: ")3635for b in self.updatedBranches:3636 sys.stdout.write("%s"% b)3637 sys.stdout.write("\n")36383639ifgitConfigBool("git-p4.importLabels"):3640 self.importLabels =True36413642if self.importLabels:3643 p4Labels =getP4Labels(self.depotPaths)3644 gitTags =getGitTags()36453646 missingP4Labels = p4Labels - gitTags3647 self.importP4Labels(self.gitStream, missingP4Labels)36483649 self.closeStreams()36503651# Cleanup temporary branches created during import3652if self.tempBranches != []:3653for branch in self.tempBranches:3654read_pipe("git update-ref -d%s"% branch)3655 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))36563657# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3658# a convenient shortcut refname "p4".3659if self.importIntoRemotes:3660 head_ref = self.refPrefix +"HEAD"3661if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3662system(["git","symbolic-ref", head_ref, self.branch])36633664return True36653666classP4Rebase(Command):3667def__init__(self):3668 Command.__init__(self)3669 self.options = [3670 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3671]3672 self.importLabels =False3673 self.description = ("Fetches the latest revision from perforce and "3674+"rebases the current work (branch) against it")36753676defrun(self, args):3677 sync =P4Sync()3678 sync.importLabels = self.importLabels3679 sync.run([])36803681return self.rebase()36823683defrebase(self):3684if os.system("git update-index --refresh") !=0:3685die("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.");3686iflen(read_pipe("git diff-index HEAD --")) >0:3687die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");36883689[upstream, settings] =findUpstreamBranchPoint()3690iflen(upstream) ==0:3691die("Cannot find upstream branchpoint for rebase")36923693# the branchpoint may be p4/foo~3, so strip off the parent3694 upstream = re.sub("~[0-9]+$","", upstream)36953696print"Rebasing the current branch onto%s"% upstream3697 oldHead =read_pipe("git rev-parse HEAD").strip()3698system("git rebase%s"% upstream)3699system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3700return True37013702classP4Clone(P4Sync):3703def__init__(self):3704 P4Sync.__init__(self)3705 self.description ="Creates a new git repository and imports from Perforce into it"3706 self.usage ="usage: %prog [options] //depot/path[@revRange]"3707 self.options += [3708 optparse.make_option("--destination", dest="cloneDestination",3709 action='store', default=None,3710help="where to leave result of the clone"),3711 optparse.make_option("--bare", dest="cloneBare",3712 action="store_true", default=False),3713]3714 self.cloneDestination =None3715 self.needsGit =False3716 self.cloneBare =False37173718defdefaultDestination(self, args):3719## TODO: use common prefix of args?3720 depotPath = args[0]3721 depotDir = re.sub("(@[^@]*)$","", depotPath)3722 depotDir = re.sub("(#[^#]*)$","", depotDir)3723 depotDir = re.sub(r"\.\.\.$","", depotDir)3724 depotDir = re.sub(r"/$","", depotDir)3725return os.path.split(depotDir)[1]37263727defrun(self, args):3728iflen(args) <1:3729return False37303731if self.keepRepoPath and not self.cloneDestination:3732 sys.stderr.write("Must specify destination for --keep-path\n")3733 sys.exit(1)37343735 depotPaths = args37363737if not self.cloneDestination andlen(depotPaths) >1:3738 self.cloneDestination = depotPaths[-1]3739 depotPaths = depotPaths[:-1]37403741 self.cloneExclude = ["/"+p for p in self.cloneExclude]3742for p in depotPaths:3743if not p.startswith("//"):3744 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3745return False37463747if not self.cloneDestination:3748 self.cloneDestination = self.defaultDestination(args)37493750print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)37513752if not os.path.exists(self.cloneDestination):3753 os.makedirs(self.cloneDestination)3754chdir(self.cloneDestination)37553756 init_cmd = ["git","init"]3757if self.cloneBare:3758 init_cmd.append("--bare")3759 retcode = subprocess.call(init_cmd)3760if retcode:3761raiseCalledProcessError(retcode, init_cmd)37623763if not P4Sync.run(self, depotPaths):3764return False37653766# create a master branch and check out a work tree3767ifgitBranchExists(self.branch):3768system(["git","branch","master", self.branch ])3769if not self.cloneBare:3770system(["git","checkout","-f"])3771else:3772print'Not checking out any branch, use ' \3773'"git checkout -q -b master <branch>"'37743775# auto-set this variable if invoked with --use-client-spec3776if self.useClientSpec_from_options:3777system("git config --bool git-p4.useclientspec true")37783779return True37803781classP4Unshelve(Command):3782def__init__(self):3783 Command.__init__(self)3784 self.options = []3785 self.origin ="HEAD"3786 self.description ="Unshelve a P4 changelist into a git commit"3787 self.usage ="usage: %prog [options] changelist"3788 self.options += [3789 optparse.make_option("--origin", dest="origin",3790help="Use this base revision instead of the default (%s)"% self.origin),3791]3792 self.verbose =False3793 self.noCommit =False3794 self.destbranch ="refs/remotes/p4/unshelved"37953796defrenameBranch(self, branch_name):3797""" Rename the existing branch to branch_name.N3798 """37993800 found =True3801for i inrange(0,1000):3802 backup_branch_name ="{0}.{1}".format(branch_name, i)3803if notgitBranchExists(backup_branch_name):3804gitUpdateRef(backup_branch_name, branch_name)# copy ref to backup3805gitDeleteRef(branch_name)3806 found =True3807print("renamed old unshelve branch to{0}".format(backup_branch_name))3808break38093810if not found:3811 sys.exit("gave up trying to rename existing branch{0}".format(sync.branch))38123813deffindLastP4Revision(self, starting_point):3814""" Look back from starting_point for the first commit created by git-p43815 to find the P4 commit we are based on, and the depot-paths.3816 """38173818for parent in(range(65535)):3819 log =extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))3820 settings =extractSettingsGitLog(log)3821if settings.has_key('change'):3822return settings38233824 sys.exit("could not find git-p4 commits in{0}".format(self.origin))38253826defrun(self, args):3827iflen(args) !=1:3828return False38293830if notgitBranchExists(self.origin):3831 sys.exit("origin branch{0}does not exist".format(self.origin))38323833 sync =P4Sync()3834 changes = args3835 sync.initialParent = self.origin38363837# use the first change in the list to construct the branch to unshelve into3838 change = changes[0]38393840# if the target branch already exists, rename it3841 branch_name ="{0}/{1}".format(self.destbranch, change)3842ifgitBranchExists(branch_name):3843 self.renameBranch(branch_name)3844 sync.branch = branch_name38453846 sync.verbose = self.verbose3847 sync.suppress_meta_comment =True38483849 settings = self.findLastP4Revision(self.origin)3850 origin_revision = settings['change']3851 sync.depotPaths = settings['depot-paths']3852 sync.branchPrefixes = sync.depotPaths38533854 sync.openStreams()3855 sync.loadUserMapFromCache()3856 sync.silent =True3857 sync.importChanges(changes, shelved=True, origin_revision=origin_revision)3858 sync.closeStreams()38593860print("unshelved changelist{0}into{1}".format(change, branch_name))38613862return True38633864classP4Branches(Command):3865def__init__(self):3866 Command.__init__(self)3867 self.options = [ ]3868 self.description = ("Shows the git branches that hold imports and their "3869+"corresponding perforce depot paths")3870 self.verbose =False38713872defrun(self, args):3873iforiginP4BranchesExist():3874createOrUpdateBranchesFromOrigin()38753876 cmdline ="git rev-parse --symbolic "3877 cmdline +=" --remotes"38783879for line inread_pipe_lines(cmdline):3880 line = line.strip()38813882if not line.startswith('p4/')or line =="p4/HEAD":3883continue3884 branch = line38853886 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3887 settings =extractSettingsGitLog(log)38883889print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3890return True38913892classHelpFormatter(optparse.IndentedHelpFormatter):3893def__init__(self):3894 optparse.IndentedHelpFormatter.__init__(self)38953896defformat_description(self, description):3897if description:3898return description +"\n"3899else:3900return""39013902defprintUsage(commands):3903print"usage:%s<command> [options]"% sys.argv[0]3904print""3905print"valid commands:%s"%", ".join(commands)3906print""3907print"Try%s<command> --help for command specific help."% sys.argv[0]3908print""39093910commands = {3911"debug": P4Debug,3912"submit": P4Submit,3913"commit": P4Submit,3914"sync": P4Sync,3915"rebase": P4Rebase,3916"clone": P4Clone,3917"rollback": P4RollBack,3918"branches": P4Branches,3919"unshelve": P4Unshelve,3920}392139223923defmain():3924iflen(sys.argv[1:]) ==0:3925printUsage(commands.keys())3926 sys.exit(2)39273928 cmdName = sys.argv[1]3929try:3930 klass = commands[cmdName]3931 cmd =klass()3932exceptKeyError:3933print"unknown command%s"% cmdName3934print""3935printUsage(commands.keys())3936 sys.exit(2)39373938 options = cmd.options3939 cmd.gitdir = os.environ.get("GIT_DIR",None)39403941 args = sys.argv[2:]39423943 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3944if cmd.needsGit:3945 options.append(optparse.make_option("--git-dir", dest="gitdir"))39463947 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3948 options,3949 description = cmd.description,3950 formatter =HelpFormatter())39513952(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3953global verbose3954 verbose = cmd.verbose3955if cmd.needsGit:3956if cmd.gitdir ==None:3957 cmd.gitdir = os.path.abspath(".git")3958if notisValidGitDir(cmd.gitdir):3959# "rev-parse --git-dir" without arguments will try $PWD/.git3960 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3961if os.path.exists(cmd.gitdir):3962 cdup =read_pipe("git rev-parse --show-cdup").strip()3963iflen(cdup) >0:3964chdir(cdup);39653966if notisValidGitDir(cmd.gitdir):3967ifisValidGitDir(cmd.gitdir +"/.git"):3968 cmd.gitdir +="/.git"3969else:3970die("fatal: cannot locate git repository at%s"% cmd.gitdir)39713972# so git commands invoked from the P4 workspace will succeed3973 os.environ["GIT_DIR"] = cmd.gitdir39743975if not cmd.run(args):3976 parser.print_help()3977 sys.exit(2)397839793980if __name__ =='__main__':3981main()