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): 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 ds =p4CmdList(["describe","-s",str(change)], skip_info=True) 325iflen(ds) !=1: 326die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 327 328 d = ds[0] 329 330if"p4ExitCode"in d: 331die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 332str(d))) 333if"code"in d: 334if d["code"] =="error": 335die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 336 337if"time"not in d: 338die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 339 340return d 341 342# 343# Canonicalize the p4 type and return a tuple of the 344# base type, plus any modifiers. See "p4 help filetypes" 345# for a list and explanation. 346# 347defsplit_p4_type(p4type): 348 349 p4_filetypes_historical = { 350"ctempobj":"binary+Sw", 351"ctext":"text+C", 352"cxtext":"text+Cx", 353"ktext":"text+k", 354"kxtext":"text+kx", 355"ltext":"text+F", 356"tempobj":"binary+FSw", 357"ubinary":"binary+F", 358"uresource":"resource+F", 359"uxbinary":"binary+Fx", 360"xbinary":"binary+x", 361"xltext":"text+Fx", 362"xtempobj":"binary+Swx", 363"xtext":"text+x", 364"xunicode":"unicode+x", 365"xutf16":"utf16+x", 366} 367if p4type in p4_filetypes_historical: 368 p4type = p4_filetypes_historical[p4type] 369 mods ="" 370 s = p4type.split("+") 371 base = s[0] 372 mods ="" 373iflen(s) >1: 374 mods = s[1] 375return(base, mods) 376 377# 378# return the raw p4 type of a file (text, text+ko, etc) 379# 380defp4_type(f): 381 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 382return results[0]['headType'] 383 384# 385# Given a type base and modifier, return a regexp matching 386# the keywords that can be expanded in the file 387# 388defp4_keywords_regexp_for_type(base, type_mods): 389if base in("text","unicode","binary"): 390 kwords =None 391if"ko"in type_mods: 392 kwords ='Id|Header' 393elif"k"in type_mods: 394 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 395else: 396return None 397 pattern = r""" 398 \$ # Starts with a dollar, followed by... 399 (%s) # one of the keywords, followed by... 400 (:[^$\n]+)? # possibly an old expansion, followed by... 401 \$ # another dollar 402 """% kwords 403return pattern 404else: 405return None 406 407# 408# Given a file, return a regexp matching the possible 409# RCS keywords that will be expanded, or None for files 410# with kw expansion turned off. 411# 412defp4_keywords_regexp_for_file(file): 413if not os.path.exists(file): 414return None 415else: 416(type_base, type_mods) =split_p4_type(p4_type(file)) 417returnp4_keywords_regexp_for_type(type_base, type_mods) 418 419defsetP4ExecBit(file, mode): 420# Reopens an already open file and changes the execute bit to match 421# the execute bit setting in the passed in mode. 422 423 p4Type ="+x" 424 425if notisModeExec(mode): 426 p4Type =getP4OpenedType(file) 427 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 428 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 429if p4Type[-1] =="+": 430 p4Type = p4Type[0:-1] 431 432p4_reopen(p4Type,file) 433 434defgetP4OpenedType(file): 435# Returns the perforce file type for the given file. 436 437 result =p4_read_pipe(["opened",wildcard_encode(file)]) 438 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 439if match: 440return match.group(1) 441else: 442die("Could not determine file type for%s(result: '%s')"% (file, result)) 443 444# Return the set of all p4 labels 445defgetP4Labels(depotPaths): 446 labels =set() 447ifisinstance(depotPaths,basestring): 448 depotPaths = [depotPaths] 449 450for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 451 label = l['label'] 452 labels.add(label) 453 454return labels 455 456# Return the set of all git tags 457defgetGitTags(): 458 gitTags =set() 459for line inread_pipe_lines(["git","tag"]): 460 tag = line.strip() 461 gitTags.add(tag) 462return gitTags 463 464defdiffTreePattern(): 465# This is a simple generator for the diff tree regex pattern. This could be 466# a class variable if this and parseDiffTreeEntry were a part of a class. 467 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 468while True: 469yield pattern 470 471defparseDiffTreeEntry(entry): 472"""Parses a single diff tree entry into its component elements. 473 474 See git-diff-tree(1) manpage for details about the format of the diff 475 output. This method returns a dictionary with the following elements: 476 477 src_mode - The mode of the source file 478 dst_mode - The mode of the destination file 479 src_sha1 - The sha1 for the source file 480 dst_sha1 - The sha1 fr the destination file 481 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 482 status_score - The score for the status (applicable for 'C' and 'R' 483 statuses). This is None if there is no score. 484 src - The path for the source file. 485 dst - The path for the destination file. This is only present for 486 copy or renames. If it is not present, this is None. 487 488 If the pattern is not matched, None is returned.""" 489 490 match =diffTreePattern().next().match(entry) 491if match: 492return{ 493'src_mode': match.group(1), 494'dst_mode': match.group(2), 495'src_sha1': match.group(3), 496'dst_sha1': match.group(4), 497'status': match.group(5), 498'status_score': match.group(6), 499'src': match.group(7), 500'dst': match.group(10) 501} 502return None 503 504defisModeExec(mode): 505# Returns True if the given git mode represents an executable file, 506# otherwise False. 507return mode[-3:] =="755" 508 509defisModeExecChanged(src_mode, dst_mode): 510returnisModeExec(src_mode) !=isModeExec(dst_mode) 511 512defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False): 513 514ifisinstance(cmd,basestring): 515 cmd ="-G "+ cmd 516 expand =True 517else: 518 cmd = ["-G"] + cmd 519 expand =False 520 521 cmd =p4_build_cmd(cmd) 522if verbose: 523 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 524 525# Use a temporary file to avoid deadlocks without 526# subprocess.communicate(), which would put another copy 527# of stdout into memory. 528 stdin_file =None 529if stdin is not None: 530 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 531ifisinstance(stdin,basestring): 532 stdin_file.write(stdin) 533else: 534for i in stdin: 535 stdin_file.write(i +'\n') 536 stdin_file.flush() 537 stdin_file.seek(0) 538 539 p4 = subprocess.Popen(cmd, 540 shell=expand, 541 stdin=stdin_file, 542 stdout=subprocess.PIPE) 543 544 result = [] 545try: 546while True: 547 entry = marshal.load(p4.stdout) 548if skip_info: 549if'code'in entry and entry['code'] =='info': 550continue 551if cb is not None: 552cb(entry) 553else: 554 result.append(entry) 555exceptEOFError: 556pass 557 exitCode = p4.wait() 558if exitCode !=0: 559 entry = {} 560 entry["p4ExitCode"] = exitCode 561 result.append(entry) 562 563return result 564 565defp4Cmd(cmd): 566list=p4CmdList(cmd) 567 result = {} 568for entry inlist: 569 result.update(entry) 570return result; 571 572defp4Where(depotPath): 573if not depotPath.endswith("/"): 574 depotPath +="/" 575 depotPathLong = depotPath +"..." 576 outputList =p4CmdList(["where", depotPathLong]) 577 output =None 578for entry in outputList: 579if"depotFile"in entry: 580# Search for the base client side depot path, as long as it starts with the branch's P4 path. 581# The base path always ends with "/...". 582if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 583 output = entry 584break 585elif"data"in entry: 586 data = entry.get("data") 587 space = data.find(" ") 588if data[:space] == depotPath: 589 output = entry 590break 591if output ==None: 592return"" 593if output["code"] =="error": 594return"" 595 clientPath ="" 596if"path"in output: 597 clientPath = output.get("path") 598elif"data"in output: 599 data = output.get("data") 600 lastSpace = data.rfind(" ") 601 clientPath = data[lastSpace +1:] 602 603if clientPath.endswith("..."): 604 clientPath = clientPath[:-3] 605return clientPath 606 607defcurrentGitBranch(): 608returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 609 610defisValidGitDir(path): 611returngit_dir(path) !=None 612 613defparseRevision(ref): 614returnread_pipe("git rev-parse%s"% ref).strip() 615 616defbranchExists(ref): 617 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 618 ignore_error=True) 619returnlen(rev) >0 620 621defextractLogMessageFromGitCommit(commit): 622 logMessage ="" 623 624## fixme: title is first line of commit, not 1st paragraph. 625 foundTitle =False 626for log inread_pipe_lines("git cat-file commit%s"% commit): 627if not foundTitle: 628iflen(log) ==1: 629 foundTitle =True 630continue 631 632 logMessage += log 633return logMessage 634 635defextractSettingsGitLog(log): 636 values = {} 637for line in log.split("\n"): 638 line = line.strip() 639 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 640if not m: 641continue 642 643 assignments = m.group(1).split(':') 644for a in assignments: 645 vals = a.split('=') 646 key = vals[0].strip() 647 val = ('='.join(vals[1:])).strip() 648if val.endswith('\"')and val.startswith('"'): 649 val = val[1:-1] 650 651 values[key] = val 652 653 paths = values.get("depot-paths") 654if not paths: 655 paths = values.get("depot-path") 656if paths: 657 values['depot-paths'] = paths.split(',') 658return values 659 660defgitBranchExists(branch): 661 proc = subprocess.Popen(["git","rev-parse", branch], 662 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 663return proc.wait() ==0; 664 665_gitConfig = {} 666 667defgitConfig(key, typeSpecifier=None): 668if not _gitConfig.has_key(key): 669 cmd = ["git","config"] 670if typeSpecifier: 671 cmd += [ typeSpecifier ] 672 cmd += [ key ] 673 s =read_pipe(cmd, ignore_error=True) 674 _gitConfig[key] = s.strip() 675return _gitConfig[key] 676 677defgitConfigBool(key): 678"""Return a bool, using git config --bool. It is True only if the 679 variable is set to true, and False if set to false or not present 680 in the config.""" 681 682if not _gitConfig.has_key(key): 683 _gitConfig[key] =gitConfig(key,'--bool') =="true" 684return _gitConfig[key] 685 686defgitConfigInt(key): 687if not _gitConfig.has_key(key): 688 cmd = ["git","config","--int", key ] 689 s =read_pipe(cmd, ignore_error=True) 690 v = s.strip() 691try: 692 _gitConfig[key] =int(gitConfig(key,'--int')) 693exceptValueError: 694 _gitConfig[key] =None 695return _gitConfig[key] 696 697defgitConfigList(key): 698if not _gitConfig.has_key(key): 699 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 700 _gitConfig[key] = s.strip().splitlines() 701if _gitConfig[key] == ['']: 702 _gitConfig[key] = [] 703return _gitConfig[key] 704 705defp4BranchesInGit(branchesAreInRemotes=True): 706"""Find all the branches whose names start with "p4/", looking 707 in remotes or heads as specified by the argument. Return 708 a dictionary of{ branch: revision }for each one found. 709 The branch names are the short names, without any 710 "p4/" prefix.""" 711 712 branches = {} 713 714 cmdline ="git rev-parse --symbolic " 715if branchesAreInRemotes: 716 cmdline +="--remotes" 717else: 718 cmdline +="--branches" 719 720for line inread_pipe_lines(cmdline): 721 line = line.strip() 722 723# only import to p4/ 724if not line.startswith('p4/'): 725continue 726# special symbolic ref to p4/master 727if line =="p4/HEAD": 728continue 729 730# strip off p4/ prefix 731 branch = line[len("p4/"):] 732 733 branches[branch] =parseRevision(line) 734 735return branches 736 737defbranch_exists(branch): 738"""Make sure that the given ref name really exists.""" 739 740 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 741 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 742 out, _ = p.communicate() 743if p.returncode: 744return False 745# expect exactly one line of output: the branch name 746return out.rstrip() == branch 747 748deffindUpstreamBranchPoint(head ="HEAD"): 749 branches =p4BranchesInGit() 750# map from depot-path to branch name 751 branchByDepotPath = {} 752for branch in branches.keys(): 753 tip = branches[branch] 754 log =extractLogMessageFromGitCommit(tip) 755 settings =extractSettingsGitLog(log) 756if settings.has_key("depot-paths"): 757 paths =",".join(settings["depot-paths"]) 758 branchByDepotPath[paths] ="remotes/p4/"+ branch 759 760 settings =None 761 parent =0 762while parent <65535: 763 commit = head +"~%s"% parent 764 log =extractLogMessageFromGitCommit(commit) 765 settings =extractSettingsGitLog(log) 766if settings.has_key("depot-paths"): 767 paths =",".join(settings["depot-paths"]) 768if branchByDepotPath.has_key(paths): 769return[branchByDepotPath[paths], settings] 770 771 parent = parent +1 772 773return["", settings] 774 775defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 776if not silent: 777print("Creating/updating branch(es) in%sbased on origin branch(es)" 778% localRefPrefix) 779 780 originPrefix ="origin/p4/" 781 782for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 783 line = line.strip() 784if(not line.startswith(originPrefix))or line.endswith("HEAD"): 785continue 786 787 headName = line[len(originPrefix):] 788 remoteHead = localRefPrefix + headName 789 originHead = line 790 791 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 792if(not original.has_key('depot-paths') 793or not original.has_key('change')): 794continue 795 796 update =False 797if notgitBranchExists(remoteHead): 798if verbose: 799print"creating%s"% remoteHead 800 update =True 801else: 802 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 803if settings.has_key('change') >0: 804if settings['depot-paths'] == original['depot-paths']: 805 originP4Change =int(original['change']) 806 p4Change =int(settings['change']) 807if originP4Change > p4Change: 808print("%s(%s) is newer than%s(%s). " 809"Updating p4 branch from origin." 810% (originHead, originP4Change, 811 remoteHead, p4Change)) 812 update =True 813else: 814print("Ignoring:%swas imported from%swhile " 815"%swas imported from%s" 816% (originHead,','.join(original['depot-paths']), 817 remoteHead,','.join(settings['depot-paths']))) 818 819if update: 820system("git update-ref%s %s"% (remoteHead, originHead)) 821 822deforiginP4BranchesExist(): 823returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 824 825 826defp4ParseNumericChangeRange(parts): 827 changeStart =int(parts[0][1:]) 828if parts[1] =='#head': 829 changeEnd =p4_last_change() 830else: 831 changeEnd =int(parts[1]) 832 833return(changeStart, changeEnd) 834 835defchooseBlockSize(blockSize): 836if blockSize: 837return blockSize 838else: 839return defaultBlockSize 840 841defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 842assert depotPaths 843 844# Parse the change range into start and end. Try to find integer 845# revision ranges as these can be broken up into blocks to avoid 846# hitting server-side limits (maxrows, maxscanresults). But if 847# that doesn't work, fall back to using the raw revision specifier 848# strings, without using block mode. 849 850if changeRange is None or changeRange =='': 851 changeStart =1 852 changeEnd =p4_last_change() 853 block_size =chooseBlockSize(requestedBlockSize) 854else: 855 parts = changeRange.split(',') 856assertlen(parts) ==2 857try: 858(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 859 block_size =chooseBlockSize(requestedBlockSize) 860except: 861 changeStart = parts[0][1:] 862 changeEnd = parts[1] 863if requestedBlockSize: 864die("cannot use --changes-block-size with non-numeric revisions") 865 block_size =None 866 867 changes =set() 868 869# Retrieve changes a block at a time, to prevent running 870# into a MaxResults/MaxScanRows error from the server. 871 872while True: 873 cmd = ['changes'] 874 875if block_size: 876 end =min(changeEnd, changeStart + block_size) 877 revisionRange ="%d,%d"% (changeStart, end) 878else: 879 revisionRange ="%s,%s"% (changeStart, changeEnd) 880 881for p in depotPaths: 882 cmd += ["%s...@%s"% (p, revisionRange)] 883 884# Insert changes in chronological order 885for entry inreversed(p4CmdList(cmd)): 886if entry.has_key('p4ExitCode'): 887die('Error retrieving changes descriptions ({})'.format(entry['p4ExitCode'])) 888if not entry.has_key('change'): 889continue 890 changes.add(int(entry['change'])) 891 892if not block_size: 893break 894 895if end >= changeEnd: 896break 897 898 changeStart = end +1 899 900 changes =sorted(changes) 901return changes 902 903defp4PathStartsWith(path, prefix): 904# This method tries to remedy a potential mixed-case issue: 905# 906# If UserA adds //depot/DirA/file1 907# and UserB adds //depot/dira/file2 908# 909# we may or may not have a problem. If you have core.ignorecase=true, 910# we treat DirA and dira as the same directory 911ifgitConfigBool("core.ignorecase"): 912return path.lower().startswith(prefix.lower()) 913return path.startswith(prefix) 914 915defgetClientSpec(): 916"""Look at the p4 client spec, create a View() object that contains 917 all the mappings, and return it.""" 918 919 specList =p4CmdList("client -o") 920iflen(specList) !=1: 921die('Output from "client -o" is%dlines, expecting 1'% 922len(specList)) 923 924# dictionary of all client parameters 925 entry = specList[0] 926 927# the //client/ name 928 client_name = entry["Client"] 929 930# just the keys that start with "View" 931 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 932 933# hold this new View 934 view =View(client_name) 935 936# append the lines, in order, to the view 937for view_num inrange(len(view_keys)): 938 k ="View%d"% view_num 939if k not in view_keys: 940die("Expected view key%smissing"% k) 941 view.append(entry[k]) 942 943return view 944 945defgetClientRoot(): 946"""Grab the client directory.""" 947 948 output =p4CmdList("client -o") 949iflen(output) !=1: 950die('Output from "client -o" is%dlines, expecting 1'%len(output)) 951 952 entry = output[0] 953if"Root"not in entry: 954die('Client has no "Root"') 955 956return entry["Root"] 957 958# 959# P4 wildcards are not allowed in filenames. P4 complains 960# if you simply add them, but you can force it with "-f", in 961# which case it translates them into %xx encoding internally. 962# 963defwildcard_decode(path): 964# Search for and fix just these four characters. Do % last so 965# that fixing it does not inadvertently create new %-escapes. 966# Cannot have * in a filename in windows; untested as to 967# what p4 would do in such a case. 968if not platform.system() =="Windows": 969 path = path.replace("%2A","*") 970 path = path.replace("%23","#") \ 971.replace("%40","@") \ 972.replace("%25","%") 973return path 974 975defwildcard_encode(path): 976# do % first to avoid double-encoding the %s introduced here 977 path = path.replace("%","%25") \ 978.replace("*","%2A") \ 979.replace("#","%23") \ 980.replace("@","%40") 981return path 982 983defwildcard_present(path): 984 m = re.search("[*#@%]", path) 985return m is not None 986 987classLargeFileSystem(object): 988"""Base class for large file system support.""" 989 990def__init__(self, writeToGitStream): 991 self.largeFiles =set() 992 self.writeToGitStream = writeToGitStream 993 994defgeneratePointer(self, cloneDestination, contentFile): 995"""Return the content of a pointer file that is stored in Git instead of 996 the actual content.""" 997assert False,"Method 'generatePointer' required in "+ self.__class__.__name__ 998 999defpushFile(self, localLargeFile):1000"""Push the actual content which is not stored in the Git repository to1001 a server."""1002assert False,"Method 'pushFile' required in "+ self.__class__.__name__10031004defhasLargeFileExtension(self, relPath):1005returnreduce(1006lambda a, b: a or b,1007[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1008False1009)10101011defgenerateTempFile(self, contents):1012 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1013for d in contents:1014 contentFile.write(d)1015 contentFile.close()1016return contentFile.name10171018defexceedsLargeFileThreshold(self, relPath, contents):1019ifgitConfigInt('git-p4.largeFileThreshold'):1020 contentsSize =sum(len(d)for d in contents)1021if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1022return True1023ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1024 contentsSize =sum(len(d)for d in contents)1025if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1026return False1027 contentTempFile = self.generateTempFile(contents)1028 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1029 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1030 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1031 zf.close()1032 compressedContentsSize = zf.infolist()[0].compress_size1033 os.remove(contentTempFile)1034 os.remove(compressedContentFile.name)1035if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1036return True1037return False10381039defaddLargeFile(self, relPath):1040 self.largeFiles.add(relPath)10411042defremoveLargeFile(self, relPath):1043 self.largeFiles.remove(relPath)10441045defisLargeFile(self, relPath):1046return relPath in self.largeFiles10471048defprocessContent(self, git_mode, relPath, contents):1049"""Processes the content of git fast import. This method decides if a1050 file is stored in the large file system and handles all necessary1051 steps."""1052if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1053 contentTempFile = self.generateTempFile(contents)1054(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1055if pointer_git_mode:1056 git_mode = pointer_git_mode1057if localLargeFile:1058# Move temp file to final location in large file system1059 largeFileDir = os.path.dirname(localLargeFile)1060if not os.path.isdir(largeFileDir):1061 os.makedirs(largeFileDir)1062 shutil.move(contentTempFile, localLargeFile)1063 self.addLargeFile(relPath)1064ifgitConfigBool('git-p4.largeFilePush'):1065 self.pushFile(localLargeFile)1066if verbose:1067 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1068return(git_mode, contents)10691070classMockLFS(LargeFileSystem):1071"""Mock large file system for testing."""10721073defgeneratePointer(self, contentFile):1074"""The pointer content is the original content prefixed with "pointer-".1075 The local filename of the large file storage is derived from the file content.1076 """1077withopen(contentFile,'r')as f:1078 content =next(f)1079 gitMode ='100644'1080 pointerContents ='pointer-'+ content1081 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1082return(gitMode, pointerContents, localLargeFile)10831084defpushFile(self, localLargeFile):1085"""The remote filename of the large file storage is the same as the local1086 one but in a different directory.1087 """1088 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1089if not os.path.exists(remotePath):1090 os.makedirs(remotePath)1091 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))10921093classGitLFS(LargeFileSystem):1094"""Git LFS as backend for the git-p4 large file system.1095 See https://git-lfs.github.com/ for details."""10961097def__init__(self, *args):1098 LargeFileSystem.__init__(self, *args)1099 self.baseGitAttributes = []11001101defgeneratePointer(self, contentFile):1102"""Generate a Git LFS pointer for the content. Return LFS Pointer file1103 mode and content which is stored in the Git repository instead of1104 the actual content. Return also the new location of the actual1105 content.1106 """1107if os.path.getsize(contentFile) ==0:1108return(None,'',None)11091110 pointerProcess = subprocess.Popen(1111['git','lfs','pointer','--file='+ contentFile],1112 stdout=subprocess.PIPE1113)1114 pointerFile = pointerProcess.stdout.read()1115if pointerProcess.wait():1116 os.remove(contentFile)1117die('git-lfs pointer command failed. Did you install the extension?')11181119# Git LFS removed the preamble in the output of the 'pointer' command1120# starting from version 1.2.0. Check for the preamble here to support1121# earlier versions.1122# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431123if pointerFile.startswith('Git LFS pointer for'):1124 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)11251126 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1127 localLargeFile = os.path.join(1128 os.getcwd(),1129'.git','lfs','objects', oid[:2], oid[2:4],1130 oid,1131)1132# LFS Spec states that pointer files should not have the executable bit set.1133 gitMode ='100644'1134return(gitMode, pointerFile, localLargeFile)11351136defpushFile(self, localLargeFile):1137 uploadProcess = subprocess.Popen(1138['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1139)1140if uploadProcess.wait():1141die('git-lfs push command failed. Did you define a remote?')11421143defgenerateGitAttributes(self):1144return(1145 self.baseGitAttributes +1146[1147'\n',1148'#\n',1149'# Git LFS (see https://git-lfs.github.com/)\n',1150'#\n',1151] +1152['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1153for f insorted(gitConfigList('git-p4.largeFileExtensions'))1154] +1155['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1156for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1157]1158)11591160defaddLargeFile(self, relPath):1161 LargeFileSystem.addLargeFile(self, relPath)1162 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11631164defremoveLargeFile(self, relPath):1165 LargeFileSystem.removeLargeFile(self, relPath)1166 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())11671168defprocessContent(self, git_mode, relPath, contents):1169if relPath =='.gitattributes':1170 self.baseGitAttributes = contents1171return(git_mode, self.generateGitAttributes())1172else:1173return LargeFileSystem.processContent(self, git_mode, relPath, contents)11741175class Command:1176def__init__(self):1177 self.usage ="usage: %prog [options]"1178 self.needsGit =True1179 self.verbose =False11801181# This is required for the "append" cloneExclude action1182defensure_value(self, attr, value):1183if nothasattr(self, attr)orgetattr(self, attr)is None:1184setattr(self, attr, value)1185returngetattr(self, attr)11861187class P4UserMap:1188def__init__(self):1189 self.userMapFromPerforceServer =False1190 self.myP4UserId =None11911192defp4UserId(self):1193if self.myP4UserId:1194return self.myP4UserId11951196 results =p4CmdList("user -o")1197for r in results:1198if r.has_key('User'):1199 self.myP4UserId = r['User']1200return r['User']1201die("Could not find your p4 user id")12021203defp4UserIsMe(self, p4User):1204# return True if the given p4 user is actually me1205 me = self.p4UserId()1206if not p4User or p4User != me:1207return False1208else:1209return True12101211defgetUserCacheFilename(self):1212 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1213return home +"/.gitp4-usercache.txt"12141215defgetUserMapFromPerforceServer(self):1216if self.userMapFromPerforceServer:1217return1218 self.users = {}1219 self.emails = {}12201221for output inp4CmdList("users"):1222if not output.has_key("User"):1223continue1224 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1225 self.emails[output["Email"]] = output["User"]12261227 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1228for mapUserConfig ingitConfigList("git-p4.mapUser"):1229 mapUser = mapUserConfigRegex.findall(mapUserConfig)1230if mapUser andlen(mapUser[0]) ==3:1231 user = mapUser[0][0]1232 fullname = mapUser[0][1]1233 email = mapUser[0][2]1234 self.users[user] = fullname +" <"+ email +">"1235 self.emails[email] = user12361237 s =''1238for(key, val)in self.users.items():1239 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12401241open(self.getUserCacheFilename(),"wb").write(s)1242 self.userMapFromPerforceServer =True12431244defloadUserMapFromCache(self):1245 self.users = {}1246 self.userMapFromPerforceServer =False1247try:1248 cache =open(self.getUserCacheFilename(),"rb")1249 lines = cache.readlines()1250 cache.close()1251for line in lines:1252 entry = line.strip().split("\t")1253 self.users[entry[0]] = entry[1]1254exceptIOError:1255 self.getUserMapFromPerforceServer()12561257classP4Debug(Command):1258def__init__(self):1259 Command.__init__(self)1260 self.options = []1261 self.description ="A tool to debug the output of p4 -G."1262 self.needsGit =False12631264defrun(self, args):1265 j =01266for output inp4CmdList(args):1267print'Element:%d'% j1268 j +=11269print output1270return True12711272classP4RollBack(Command):1273def__init__(self):1274 Command.__init__(self)1275 self.options = [1276 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1277]1278 self.description ="A tool to debug the multi-branch import. Don't use :)"1279 self.rollbackLocalBranches =False12801281defrun(self, args):1282iflen(args) !=1:1283return False1284 maxChange =int(args[0])12851286if"p4ExitCode"inp4Cmd("changes -m 1"):1287die("Problems executing p4");12881289if self.rollbackLocalBranches:1290 refPrefix ="refs/heads/"1291 lines =read_pipe_lines("git rev-parse --symbolic --branches")1292else:1293 refPrefix ="refs/remotes/"1294 lines =read_pipe_lines("git rev-parse --symbolic --remotes")12951296for line in lines:1297if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1298 line = line.strip()1299 ref = refPrefix + line1300 log =extractLogMessageFromGitCommit(ref)1301 settings =extractSettingsGitLog(log)13021303 depotPaths = settings['depot-paths']1304 change = settings['change']13051306 changed =False13071308iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1309for p in depotPaths]))) ==0:1310print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1311system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1312continue13131314while change andint(change) > maxChange:1315 changed =True1316if self.verbose:1317print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1318system("git update-ref%s\"%s^\""% (ref, ref))1319 log =extractLogMessageFromGitCommit(ref)1320 settings =extractSettingsGitLog(log)132113221323 depotPaths = settings['depot-paths']1324 change = settings['change']13251326if changed:1327print"%srewound to%s"% (ref, change)13281329return True13301331classP4Submit(Command, P4UserMap):13321333 conflict_behavior_choices = ("ask","skip","quit")13341335def__init__(self):1336 Command.__init__(self)1337 P4UserMap.__init__(self)1338 self.options = [1339 optparse.make_option("--origin", dest="origin"),1340 optparse.make_option("-M", dest="detectRenames", action="store_true"),1341# preserve the user, requires relevant p4 permissions1342 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1343 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1344 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1345 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1346 optparse.make_option("--conflict", dest="conflict_behavior",1347 choices=self.conflict_behavior_choices),1348 optparse.make_option("--branch", dest="branch"),1349 optparse.make_option("--shelve", dest="shelve", action="store_true",1350help="Shelve instead of submit. Shelved files are reverted, "1351"restoring the workspace to the state before the shelve"),1352 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1353 metavar="CHANGELIST",1354help="update an existing shelved changelist, implies --shelve, "1355"repeat in-order for multiple shelved changelists"),1356 optparse.make_option("--commit", dest="commit", metavar="COMMIT",1357help="submit only the specified commit(s), one commit or xxx..xxx"),1358 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",1359help="Disable rebase after submit is completed. Can be useful if you "1360"work from a local git branch that is not master"),1361 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",1362help="Skip Perforce sync of p4/master after submit or shelve"),1363]1364 self.description ="Submit changes from git to the perforce depot."1365 self.usage +=" [name of git branch to submit into perforce depot]"1366 self.origin =""1367 self.detectRenames =False1368 self.preserveUser =gitConfigBool("git-p4.preserveUser")1369 self.dry_run =False1370 self.shelve =False1371 self.update_shelve =list()1372 self.commit =""1373 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1374 self.disable_p4sync =gitConfigBool("git-p4.disableP4Sync")1375 self.prepare_p4_only =False1376 self.conflict_behavior =None1377 self.isWindows = (platform.system() =="Windows")1378 self.exportLabels =False1379 self.p4HasMoveCommand =p4_has_move_command()1380 self.branch =None13811382ifgitConfig('git-p4.largeFileSystem'):1383die("Large file system not supported for git-p4 submit command. Please remove it from config.")13841385defcheck(self):1386iflen(p4CmdList("opened ...")) >0:1387die("You have files opened with perforce! Close them before starting the sync.")13881389defseparate_jobs_from_description(self, message):1390"""Extract and return a possible Jobs field in the commit1391 message. It goes into a separate section in the p4 change1392 specification.13931394 A jobs line starts with "Jobs:" and looks like a new field1395 in a form. Values are white-space separated on the same1396 line or on following lines that start with a tab.13971398 This does not parse and extract the full git commit message1399 like a p4 form. It just sees the Jobs: line as a marker1400 to pass everything from then on directly into the p4 form,1401 but outside the description section.14021403 Return a tuple (stripped log message, jobs string)."""14041405 m = re.search(r'^Jobs:', message, re.MULTILINE)1406if m is None:1407return(message,None)14081409 jobtext = message[m.start():]1410 stripped_message = message[:m.start()].rstrip()1411return(stripped_message, jobtext)14121413defprepareLogMessage(self, template, message, jobs):1414"""Edits the template returned from "p4 change -o" to insert1415 the message in the Description field, and the jobs text in1416 the Jobs field."""1417 result =""14181419 inDescriptionSection =False14201421for line in template.split("\n"):1422if line.startswith("#"):1423 result += line +"\n"1424continue14251426if inDescriptionSection:1427if line.startswith("Files:")or line.startswith("Jobs:"):1428 inDescriptionSection =False1429# insert Jobs section1430if jobs:1431 result += jobs +"\n"1432else:1433continue1434else:1435if line.startswith("Description:"):1436 inDescriptionSection =True1437 line +="\n"1438for messageLine in message.split("\n"):1439 line +="\t"+ messageLine +"\n"14401441 result += line +"\n"14421443return result14441445defpatchRCSKeywords(self,file, pattern):1446# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1447(handle, outFileName) = tempfile.mkstemp(dir='.')1448try:1449 outFile = os.fdopen(handle,"w+")1450 inFile =open(file,"r")1451 regexp = re.compile(pattern, re.VERBOSE)1452for line in inFile.readlines():1453 line = regexp.sub(r'$\1$', line)1454 outFile.write(line)1455 inFile.close()1456 outFile.close()1457# Forcibly overwrite the original file1458 os.unlink(file)1459 shutil.move(outFileName,file)1460except:1461# cleanup our temporary file1462 os.unlink(outFileName)1463print"Failed to strip RCS keywords in%s"%file1464raise14651466print"Patched up RCS keywords in%s"%file14671468defp4UserForCommit(self,id):1469# Return the tuple (perforce user,git email) for a given git commit id1470 self.getUserMapFromPerforceServer()1471 gitEmail =read_pipe(["git","log","--max-count=1",1472"--format=%ae",id])1473 gitEmail = gitEmail.strip()1474if not self.emails.has_key(gitEmail):1475return(None,gitEmail)1476else:1477return(self.emails[gitEmail],gitEmail)14781479defcheckValidP4Users(self,commits):1480# check if any git authors cannot be mapped to p4 users1481foridin commits:1482(user,email) = self.p4UserForCommit(id)1483if not user:1484 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1485ifgitConfigBool("git-p4.allowMissingP4Users"):1486print"%s"% msg1487else:1488die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14891490deflastP4Changelist(self):1491# Get back the last changelist number submitted in this client spec. This1492# then gets used to patch up the username in the change. If the same1493# client spec is being used by multiple processes then this might go1494# wrong.1495 results =p4CmdList("client -o")# find the current client1496 client =None1497for r in results:1498if r.has_key('Client'):1499 client = r['Client']1500break1501if not client:1502die("could not get client spec")1503 results =p4CmdList(["changes","-c", client,"-m","1"])1504for r in results:1505if r.has_key('change'):1506return r['change']1507die("Could not get changelist number for last submit - cannot patch up user details")15081509defmodifyChangelistUser(self, changelist, newUser):1510# fixup the user field of a changelist after it has been submitted.1511 changes =p4CmdList("change -o%s"% changelist)1512iflen(changes) !=1:1513die("Bad output from p4 change modifying%sto user%s"%1514(changelist, newUser))15151516 c = changes[0]1517if c['User'] == newUser:return# nothing to do1518 c['User'] = newUser1519input= marshal.dumps(c)15201521 result =p4CmdList("change -f -i", stdin=input)1522for r in result:1523if r.has_key('code'):1524if r['code'] =='error':1525die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1526if r.has_key('data'):1527print("Updated user field for changelist%sto%s"% (changelist, newUser))1528return1529die("Could not modify user field of changelist%sto%s"% (changelist, newUser))15301531defcanChangeChangelists(self):1532# check to see if we have p4 admin or super-user permissions, either of1533# which are required to modify changelists.1534 results =p4CmdList(["protects", self.depotPath])1535for r in results:1536if r.has_key('perm'):1537if r['perm'] =='admin':1538return11539if r['perm'] =='super':1540return11541return015421543defprepareSubmitTemplate(self, changelist=None):1544"""Run "p4 change -o" to grab a change specification template.1545 This does not use "p4 -G", as it is nice to keep the submission1546 template in original order, since a human might edit it.15471548 Remove lines in the Files section that show changes to files1549 outside the depot path we're committing into."""15501551[upstream, settings] =findUpstreamBranchPoint()15521553 template ="""\1554# A Perforce Change Specification.1555#1556# Change: The change number. 'new' on a new changelist.1557# Date: The date this specification was last modified.1558# Client: The client on which the changelist was created. Read-only.1559# User: The user who created the changelist.1560# Status: Either 'pending' or 'submitted'. Read-only.1561# Type: Either 'public' or 'restricted'. Default is 'public'.1562# Description: Comments about the changelist. Required.1563# Jobs: What opened jobs are to be closed by this changelist.1564# You may delete jobs from this list. (New changelists only.)1565# Files: What opened files from the default changelist are to be added1566# to this changelist. You may delete files from this list.1567# (New changelists only.)1568"""1569 files_list = []1570 inFilesSection =False1571 change_entry =None1572 args = ['change','-o']1573if changelist:1574 args.append(str(changelist))1575for entry inp4CmdList(args):1576if not entry.has_key('code'):1577continue1578if entry['code'] =='stat':1579 change_entry = entry1580break1581if not change_entry:1582die('Failed to decode output of p4 change -o')1583for key, value in change_entry.iteritems():1584if key.startswith('File'):1585if settings.has_key('depot-paths'):1586if not[p for p in settings['depot-paths']1587ifp4PathStartsWith(value, p)]:1588continue1589else:1590if notp4PathStartsWith(value, self.depotPath):1591continue1592 files_list.append(value)1593continue1594# Output in the order expected by prepareLogMessage1595for key in['Change','Client','User','Status','Description','Jobs']:1596if not change_entry.has_key(key):1597continue1598 template +='\n'1599 template += key +':'1600if key =='Description':1601 template +='\n'1602for field_line in change_entry[key].splitlines():1603 template +='\t'+field_line+'\n'1604iflen(files_list) >0:1605 template +='\n'1606 template +='Files:\n'1607for path in files_list:1608 template +='\t'+path+'\n'1609return template16101611defedit_template(self, template_file):1612"""Invoke the editor to let the user change the submission1613 message. Return true if okay to continue with the submit."""16141615# if configured to skip the editing part, just submit1616ifgitConfigBool("git-p4.skipSubmitEdit"):1617return True16181619# look at the modification time, to check later if the user saved1620# the file1621 mtime = os.stat(template_file).st_mtime16221623# invoke the editor1624if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1625 editor = os.environ.get("P4EDITOR")1626else:1627 editor =read_pipe("git var GIT_EDITOR").strip()1628system(["sh","-c", ('%s"$@"'% editor), editor, template_file])16291630# If the file was not saved, prompt to see if this patch should1631# be skipped. But skip this verification step if configured so.1632ifgitConfigBool("git-p4.skipSubmitEditCheck"):1633return True16341635# modification time updated means user saved the file1636if os.stat(template_file).st_mtime > mtime:1637return True16381639while True:1640 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1641if response =='y':1642return True1643if response =='n':1644return False16451646defget_diff_description(self, editedFiles, filesToAdd, symlinks):1647# diff1648if os.environ.has_key("P4DIFF"):1649del(os.environ["P4DIFF"])1650 diff =""1651for editedFile in editedFiles:1652 diff +=p4_read_pipe(['diff','-du',1653wildcard_encode(editedFile)])16541655# new file diff1656 newdiff =""1657for newFile in filesToAdd:1658 newdiff +="==== new file ====\n"1659 newdiff +="--- /dev/null\n"1660 newdiff +="+++%s\n"% newFile16611662 is_link = os.path.islink(newFile)1663 expect_link = newFile in symlinks16641665if is_link and expect_link:1666 newdiff +="+%s\n"% os.readlink(newFile)1667else:1668 f =open(newFile,"r")1669for line in f.readlines():1670 newdiff +="+"+ line1671 f.close()16721673return(diff + newdiff).replace('\r\n','\n')16741675defapplyCommit(self,id):1676"""Apply one commit, return True if it succeeded."""16771678print"Applying",read_pipe(["git","show","-s",1679"--format=format:%h%s",id])16801681(p4User, gitEmail) = self.p4UserForCommit(id)16821683 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1684 filesToAdd =set()1685 filesToChangeType =set()1686 filesToDelete =set()1687 editedFiles =set()1688 pureRenameCopy =set()1689 symlinks =set()1690 filesToChangeExecBit = {}1691 all_files =list()16921693for line in diff:1694 diff =parseDiffTreeEntry(line)1695 modifier = diff['status']1696 path = diff['src']1697 all_files.append(path)16981699if modifier =="M":1700p4_edit(path)1701ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1702 filesToChangeExecBit[path] = diff['dst_mode']1703 editedFiles.add(path)1704elif modifier =="A":1705 filesToAdd.add(path)1706 filesToChangeExecBit[path] = diff['dst_mode']1707if path in filesToDelete:1708 filesToDelete.remove(path)17091710 dst_mode =int(diff['dst_mode'],8)1711if dst_mode ==0120000:1712 symlinks.add(path)17131714elif modifier =="D":1715 filesToDelete.add(path)1716if path in filesToAdd:1717 filesToAdd.remove(path)1718elif modifier =="C":1719 src, dest = diff['src'], diff['dst']1720p4_integrate(src, dest)1721 pureRenameCopy.add(dest)1722if diff['src_sha1'] != diff['dst_sha1']:1723p4_edit(dest)1724 pureRenameCopy.discard(dest)1725ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1726p4_edit(dest)1727 pureRenameCopy.discard(dest)1728 filesToChangeExecBit[dest] = diff['dst_mode']1729if self.isWindows:1730# turn off read-only attribute1731 os.chmod(dest, stat.S_IWRITE)1732 os.unlink(dest)1733 editedFiles.add(dest)1734elif modifier =="R":1735 src, dest = diff['src'], diff['dst']1736if self.p4HasMoveCommand:1737p4_edit(src)# src must be open before move1738p4_move(src, dest)# opens for (move/delete, move/add)1739else:1740p4_integrate(src, dest)1741if diff['src_sha1'] != diff['dst_sha1']:1742p4_edit(dest)1743else:1744 pureRenameCopy.add(dest)1745ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1746if not self.p4HasMoveCommand:1747p4_edit(dest)# with move: already open, writable1748 filesToChangeExecBit[dest] = diff['dst_mode']1749if not self.p4HasMoveCommand:1750if self.isWindows:1751 os.chmod(dest, stat.S_IWRITE)1752 os.unlink(dest)1753 filesToDelete.add(src)1754 editedFiles.add(dest)1755elif modifier =="T":1756 filesToChangeType.add(path)1757else:1758die("unknown modifier%sfor%s"% (modifier, path))17591760 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1761 patchcmd = diffcmd +" | git apply "1762 tryPatchCmd = patchcmd +"--check -"1763 applyPatchCmd = patchcmd +"--check --apply -"1764 patch_succeeded =True17651766if os.system(tryPatchCmd) !=0:1767 fixed_rcs_keywords =False1768 patch_succeeded =False1769print"Unfortunately applying the change failed!"17701771# Patch failed, maybe it's just RCS keyword woes. Look through1772# the patch to see if that's possible.1773ifgitConfigBool("git-p4.attemptRCSCleanup"):1774file=None1775 pattern =None1776 kwfiles = {}1777forfilein editedFiles | filesToDelete:1778# did this file's delta contain RCS keywords?1779 pattern =p4_keywords_regexp_for_file(file)17801781if pattern:1782# this file is a possibility...look for RCS keywords.1783 regexp = re.compile(pattern, re.VERBOSE)1784for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1785if regexp.search(line):1786if verbose:1787print"got keyword match on%sin%sin%s"% (pattern, line,file)1788 kwfiles[file] = pattern1789break17901791forfilein kwfiles:1792if verbose:1793print"zapping%swith%s"% (line,pattern)1794# File is being deleted, so not open in p4. Must1795# disable the read-only bit on windows.1796if self.isWindows andfilenot in editedFiles:1797 os.chmod(file, stat.S_IWRITE)1798 self.patchRCSKeywords(file, kwfiles[file])1799 fixed_rcs_keywords =True18001801if fixed_rcs_keywords:1802print"Retrying the patch with RCS keywords cleaned up"1803if os.system(tryPatchCmd) ==0:1804 patch_succeeded =True18051806if not patch_succeeded:1807for f in editedFiles:1808p4_revert(f)1809return False18101811#1812# Apply the patch for real, and do add/delete/+x handling.1813#1814system(applyPatchCmd)18151816for f in filesToChangeType:1817p4_edit(f,"-t","auto")1818for f in filesToAdd:1819p4_add(f)1820for f in filesToDelete:1821p4_revert(f)1822p4_delete(f)18231824# Set/clear executable bits1825for f in filesToChangeExecBit.keys():1826 mode = filesToChangeExecBit[f]1827setP4ExecBit(f, mode)18281829 update_shelve =01830iflen(self.update_shelve) >0:1831 update_shelve = self.update_shelve.pop(0)1832p4_reopen_in_change(update_shelve, all_files)18331834#1835# Build p4 change description, starting with the contents1836# of the git commit message.1837#1838 logMessage =extractLogMessageFromGitCommit(id)1839 logMessage = logMessage.strip()1840(logMessage, jobs) = self.separate_jobs_from_description(logMessage)18411842 template = self.prepareSubmitTemplate(update_shelve)1843 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)18441845if self.preserveUser:1846 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User18471848if self.checkAuthorship and not self.p4UserIsMe(p4User):1849 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1850 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1851 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"18521853 separatorLine ="######## everything below this line is just the diff #######\n"1854if not self.prepare_p4_only:1855 submitTemplate += separatorLine1856 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)18571858(handle, fileName) = tempfile.mkstemp()1859 tmpFile = os.fdopen(handle,"w+b")1860if self.isWindows:1861 submitTemplate = submitTemplate.replace("\n","\r\n")1862 tmpFile.write(submitTemplate)1863 tmpFile.close()18641865if self.prepare_p4_only:1866#1867# Leave the p4 tree prepared, and the submit template around1868# and let the user decide what to do next1869#1870print1871print"P4 workspace prepared for submission."1872print"To submit or revert, go to client workspace"1873print" "+ self.clientPath1874print1875print"To submit, use\"p4 submit\"to write a new description,"1876print"or\"p4 submit -i <%s\"to use the one prepared by" \1877"\"git p4\"."% fileName1878print"You can delete the file\"%s\"when finished."% fileName18791880if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1881print"To preserve change ownership by user%s, you must\n" \1882"do\"p4 change -f <change>\"after submitting and\n" \1883"edit the User field."1884if pureRenameCopy:1885print"After submitting, renamed files must be re-synced."1886print"Invoke\"p4 sync -f\"on each of these files:"1887for f in pureRenameCopy:1888print" "+ f18891890print1891print"To revert the changes, use\"p4 revert ...\", and delete"1892print"the submit template file\"%s\""% fileName1893if filesToAdd:1894print"Since the commit adds new files, they must be deleted:"1895for f in filesToAdd:1896print" "+ f1897print1898return True18991900#1901# Let the user edit the change description, then submit it.1902#1903 submitted =False19041905try:1906if self.edit_template(fileName):1907# read the edited message and submit1908 tmpFile =open(fileName,"rb")1909 message = tmpFile.read()1910 tmpFile.close()1911if self.isWindows:1912 message = message.replace("\r\n","\n")1913 submitTemplate = message[:message.index(separatorLine)]19141915if update_shelve:1916p4_write_pipe(['shelve','-r','-i'], submitTemplate)1917elif self.shelve:1918p4_write_pipe(['shelve','-i'], submitTemplate)1919else:1920p4_write_pipe(['submit','-i'], submitTemplate)1921# The rename/copy happened by applying a patch that created a1922# new file. This leaves it writable, which confuses p4.1923for f in pureRenameCopy:1924p4_sync(f,"-f")19251926if self.preserveUser:1927if p4User:1928# Get last changelist number. Cannot easily get it from1929# the submit command output as the output is1930# unmarshalled.1931 changelist = self.lastP4Changelist()1932 self.modifyChangelistUser(changelist, p4User)19331934 submitted =True19351936finally:1937# skip this patch1938if not submitted or self.shelve:1939if self.shelve:1940print("Reverting shelved files.")1941else:1942print("Submission cancelled, undoing p4 changes.")1943for f in editedFiles | filesToDelete:1944p4_revert(f)1945for f in filesToAdd:1946p4_revert(f)1947 os.remove(f)19481949 os.remove(fileName)1950return submitted19511952# Export git tags as p4 labels. Create a p4 label and then tag1953# with that.1954defexportGitTags(self, gitTags):1955 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1956iflen(validLabelRegexp) ==0:1957 validLabelRegexp = defaultLabelRegexp1958 m = re.compile(validLabelRegexp)19591960for name in gitTags:19611962if not m.match(name):1963if verbose:1964print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1965continue19661967# Get the p4 commit this corresponds to1968 logMessage =extractLogMessageFromGitCommit(name)1969 values =extractSettingsGitLog(logMessage)19701971if not values.has_key('change'):1972# a tag pointing to something not sent to p4; ignore1973if verbose:1974print"git tag%sdoes not give a p4 commit"% name1975continue1976else:1977 changelist = values['change']19781979# Get the tag details.1980 inHeader =True1981 isAnnotated =False1982 body = []1983for l inread_pipe_lines(["git","cat-file","-p", name]):1984 l = l.strip()1985if inHeader:1986if re.match(r'tag\s+', l):1987 isAnnotated =True1988elif re.match(r'\s*$', l):1989 inHeader =False1990continue1991else:1992 body.append(l)19931994if not isAnnotated:1995 body = ["lightweight tag imported by git p4\n"]19961997# Create the label - use the same view as the client spec we are using1998 clientSpec =getClientSpec()19992000 labelTemplate ="Label:%s\n"% name2001 labelTemplate +="Description:\n"2002for b in body:2003 labelTemplate +="\t"+ b +"\n"2004 labelTemplate +="View:\n"2005for depot_side in clientSpec.mappings:2006 labelTemplate +="\t%s\n"% depot_side20072008if self.dry_run:2009print"Would create p4 label%sfor tag"% name2010elif self.prepare_p4_only:2011print"Not creating p4 label%sfor tag due to option" \2012" --prepare-p4-only"% name2013else:2014p4_write_pipe(["label","-i"], labelTemplate)20152016# Use the label2017p4_system(["tag","-l", name] +2018["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])20192020if verbose:2021print"created p4 label for tag%s"% name20222023defrun(self, args):2024iflen(args) ==0:2025 self.master =currentGitBranch()2026eliflen(args) ==1:2027 self.master = args[0]2028if notbranchExists(self.master):2029die("Branch%sdoes not exist"% self.master)2030else:2031return False20322033for i in self.update_shelve:2034if i <=0:2035 sys.exit("invalid changelist%d"% i)20362037if self.master:2038 allowSubmit =gitConfig("git-p4.allowSubmit")2039iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2040die("%sis not in git-p4.allowSubmit"% self.master)20412042[upstream, settings] =findUpstreamBranchPoint()2043 self.depotPath = settings['depot-paths'][0]2044iflen(self.origin) ==0:2045 self.origin = upstream20462047iflen(self.update_shelve) >0:2048 self.shelve =True20492050if self.preserveUser:2051if not self.canChangeChangelists():2052die("Cannot preserve user names without p4 super-user or admin permissions")20532054# if not set from the command line, try the config file2055if self.conflict_behavior is None:2056 val =gitConfig("git-p4.conflict")2057if val:2058if val not in self.conflict_behavior_choices:2059die("Invalid value '%s' for config git-p4.conflict"% val)2060else:2061 val ="ask"2062 self.conflict_behavior = val20632064if self.verbose:2065print"Origin branch is "+ self.origin20662067iflen(self.depotPath) ==0:2068print"Internal error: cannot locate perforce depot path from existing branches"2069 sys.exit(128)20702071 self.useClientSpec =False2072ifgitConfigBool("git-p4.useclientspec"):2073 self.useClientSpec =True2074if self.useClientSpec:2075 self.clientSpecDirs =getClientSpec()20762077# Check for the existence of P4 branches2078 branchesDetected = (len(p4BranchesInGit().keys()) >1)20792080if self.useClientSpec and not branchesDetected:2081# all files are relative to the client spec2082 self.clientPath =getClientRoot()2083else:2084 self.clientPath =p4Where(self.depotPath)20852086if self.clientPath =="":2087die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)20882089print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2090 self.oldWorkingDirectory = os.getcwd()20912092# ensure the clientPath exists2093 new_client_dir =False2094if not os.path.exists(self.clientPath):2095 new_client_dir =True2096 os.makedirs(self.clientPath)20972098chdir(self.clientPath, is_client_path=True)2099if self.dry_run:2100print"Would synchronize p4 checkout in%s"% self.clientPath2101else:2102print"Synchronizing p4 checkout..."2103if new_client_dir:2104# old one was destroyed, and maybe nobody told p42105p4_sync("...","-f")2106else:2107p4_sync("...")2108 self.check()21092110 commits = []2111if self.master:2112 commitish = self.master2113else:2114 commitish ='HEAD'21152116if self.commit !="":2117if self.commit.find("..") != -1:2118 limits_ish = self.commit.split("..")2119for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2120 commits.append(line.strip())2121 commits.reverse()2122else:2123 commits.append(self.commit)2124else:2125for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2126 commits.append(line.strip())2127 commits.reverse()21282129if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2130 self.checkAuthorship =False2131else:2132 self.checkAuthorship =True21332134if self.preserveUser:2135 self.checkValidP4Users(commits)21362137#2138# Build up a set of options to be passed to diff when2139# submitting each commit to p4.2140#2141if self.detectRenames:2142# command-line -M arg2143 self.diffOpts ="-M"2144else:2145# If not explicitly set check the config variable2146 detectRenames =gitConfig("git-p4.detectRenames")21472148if detectRenames.lower() =="false"or detectRenames =="":2149 self.diffOpts =""2150elif detectRenames.lower() =="true":2151 self.diffOpts ="-M"2152else:2153 self.diffOpts ="-M%s"% detectRenames21542155# no command-line arg for -C or --find-copies-harder, just2156# config variables2157 detectCopies =gitConfig("git-p4.detectCopies")2158if detectCopies.lower() =="false"or detectCopies =="":2159pass2160elif detectCopies.lower() =="true":2161 self.diffOpts +=" -C"2162else:2163 self.diffOpts +=" -C%s"% detectCopies21642165ifgitConfigBool("git-p4.detectCopiesHarder"):2166 self.diffOpts +=" --find-copies-harder"21672168 num_shelves =len(self.update_shelve)2169if num_shelves >0and num_shelves !=len(commits):2170 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2171(len(commits), num_shelves))21722173#2174# Apply the commits, one at a time. On failure, ask if should2175# continue to try the rest of the patches, or quit.2176#2177if self.dry_run:2178print"Would apply"2179 applied = []2180 last =len(commits) -12181for i, commit inenumerate(commits):2182if self.dry_run:2183print" ",read_pipe(["git","show","-s",2184"--format=format:%h%s", commit])2185 ok =True2186else:2187 ok = self.applyCommit(commit)2188if ok:2189 applied.append(commit)2190else:2191if self.prepare_p4_only and i < last:2192print"Processing only the first commit due to option" \2193" --prepare-p4-only"2194break2195if i < last:2196 quit =False2197while True:2198# prompt for what to do, or use the option/variable2199if self.conflict_behavior =="ask":2200print"What do you want to do?"2201 response =raw_input("[s]kip this commit but apply"2202" the rest, or [q]uit? ")2203if not response:2204continue2205elif self.conflict_behavior =="skip":2206 response ="s"2207elif self.conflict_behavior =="quit":2208 response ="q"2209else:2210die("Unknown conflict_behavior '%s'"%2211 self.conflict_behavior)22122213if response[0] =="s":2214print"Skipping this commit, but applying the rest"2215break2216if response[0] =="q":2217print"Quitting"2218 quit =True2219break2220if quit:2221break22222223chdir(self.oldWorkingDirectory)2224 shelved_applied ="shelved"if self.shelve else"applied"2225if self.dry_run:2226pass2227elif self.prepare_p4_only:2228pass2229eliflen(commits) ==len(applied):2230print("All commits{0}!".format(shelved_applied))22312232 sync =P4Sync()2233if self.branch:2234 sync.branch = self.branch2235if self.disable_p4sync:2236 sync.sync_origin_only()2237else:2238 sync.run([])22392240if not self.disable_rebase:2241 rebase =P4Rebase()2242 rebase.rebase()22432244else:2245iflen(applied) ==0:2246print("No commits{0}.".format(shelved_applied))2247else:2248print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2249for c in commits:2250if c in applied:2251 star ="*"2252else:2253 star =" "2254print star,read_pipe(["git","show","-s",2255"--format=format:%h%s", c])2256print"You will have to do 'git p4 sync' and rebase."22572258ifgitConfigBool("git-p4.exportLabels"):2259 self.exportLabels =True22602261if self.exportLabels:2262 p4Labels =getP4Labels(self.depotPath)2263 gitTags =getGitTags()22642265 missingGitTags = gitTags - p4Labels2266 self.exportGitTags(missingGitTags)22672268# exit with error unless everything applied perfectly2269iflen(commits) !=len(applied):2270 sys.exit(1)22712272return True22732274classView(object):2275"""Represent a p4 view ("p4 help views"), and map files in a2276 repo according to the view."""22772278def__init__(self, client_name):2279 self.mappings = []2280 self.client_prefix ="//%s/"% client_name2281# cache results of "p4 where" to lookup client file locations2282 self.client_spec_path_cache = {}22832284defappend(self, view_line):2285"""Parse a view line, splitting it into depot and client2286 sides. Append to self.mappings, preserving order. This2287 is only needed for tag creation."""22882289# Split the view line into exactly two words. P4 enforces2290# structure on these lines that simplifies this quite a bit.2291#2292# Either or both words may be double-quoted.2293# Single quotes do not matter.2294# Double-quote marks cannot occur inside the words.2295# A + or - prefix is also inside the quotes.2296# There are no quotes unless they contain a space.2297# The line is already white-space stripped.2298# The two words are separated by a single space.2299#2300if view_line[0] =='"':2301# First word is double quoted. Find its end.2302 close_quote_index = view_line.find('"',1)2303if close_quote_index <=0:2304die("No first-word closing quote found:%s"% view_line)2305 depot_side = view_line[1:close_quote_index]2306# skip closing quote and space2307 rhs_index = close_quote_index +1+12308else:2309 space_index = view_line.find(" ")2310if space_index <=0:2311die("No word-splitting space found:%s"% view_line)2312 depot_side = view_line[0:space_index]2313 rhs_index = space_index +123142315# prefix + means overlay on previous mapping2316if depot_side.startswith("+"):2317 depot_side = depot_side[1:]23182319# prefix - means exclude this path, leave out of mappings2320 exclude =False2321if depot_side.startswith("-"):2322 exclude =True2323 depot_side = depot_side[1:]23242325if not exclude:2326 self.mappings.append(depot_side)23272328defconvert_client_path(self, clientFile):2329# chop off //client/ part to make it relative2330if not clientFile.startswith(self.client_prefix):2331die("No prefix '%s' on clientFile '%s'"%2332(self.client_prefix, clientFile))2333return clientFile[len(self.client_prefix):]23342335defupdate_client_spec_path_cache(self, files):2336""" Caching file paths by "p4 where" batch query """23372338# List depot file paths exclude that already cached2339 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]23402341iflen(fileArgs) ==0:2342return# All files in cache23432344 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2345for res in where_result:2346if"code"in res and res["code"] =="error":2347# assume error is "... file(s) not in client view"2348continue2349if"clientFile"not in res:2350die("No clientFile in 'p4 where' output")2351if"unmap"in res:2352# it will list all of them, but only one not unmap-ped2353continue2354ifgitConfigBool("core.ignorecase"):2355 res['depotFile'] = res['depotFile'].lower()2356 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])23572358# not found files or unmap files set to ""2359for depotFile in fileArgs:2360ifgitConfigBool("core.ignorecase"):2361 depotFile = depotFile.lower()2362if depotFile not in self.client_spec_path_cache:2363 self.client_spec_path_cache[depotFile] =""23642365defmap_in_client(self, depot_path):2366"""Return the relative location in the client where this2367 depot file should live. Returns "" if the file should2368 not be mapped in the client."""23692370ifgitConfigBool("core.ignorecase"):2371 depot_path = depot_path.lower()23722373if depot_path in self.client_spec_path_cache:2374return self.client_spec_path_cache[depot_path]23752376die("Error:%sis not found in client spec path"% depot_path )2377return""23782379classP4Sync(Command, P4UserMap):2380 delete_actions = ("delete","move/delete","purge")23812382def__init__(self):2383 Command.__init__(self)2384 P4UserMap.__init__(self)2385 self.options = [2386 optparse.make_option("--branch", dest="branch"),2387 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2388 optparse.make_option("--changesfile", dest="changesFile"),2389 optparse.make_option("--silent", dest="silent", action="store_true"),2390 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2391 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2392 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2393help="Import into refs/heads/ , not refs/remotes"),2394 optparse.make_option("--max-changes", dest="maxChanges",2395help="Maximum number of changes to import"),2396 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2397help="Internal block size to use when iteratively calling p4 changes"),2398 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2399help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2400 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2401help="Only sync files that are included in the Perforce Client Spec"),2402 optparse.make_option("-/", dest="cloneExclude",2403 action="append",type="string",2404help="exclude depot path"),2405]2406 self.description ="""Imports from Perforce into a git repository.\n2407 example:2408 //depot/my/project/ -- to import the current head2409 //depot/my/project/@all -- to import everything2410 //depot/my/project/@1,6 -- to import only from revision 1 to 624112412 (a ... is not needed in the path p4 specification, it's added implicitly)"""24132414 self.usage +=" //depot/path[@revRange]"2415 self.silent =False2416 self.createdBranches =set()2417 self.committedChanges =set()2418 self.branch =""2419 self.detectBranches =False2420 self.detectLabels =False2421 self.importLabels =False2422 self.changesFile =""2423 self.syncWithOrigin =True2424 self.importIntoRemotes =True2425 self.maxChanges =""2426 self.changes_block_size =None2427 self.keepRepoPath =False2428 self.depotPaths =None2429 self.p4BranchesInGit = []2430 self.cloneExclude = []2431 self.useClientSpec =False2432 self.useClientSpec_from_options =False2433 self.clientSpecDirs =None2434 self.tempBranches = []2435 self.tempBranchLocation ="refs/git-p4-tmp"2436 self.largeFileSystem =None24372438ifgitConfig('git-p4.largeFileSystem'):2439 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2440 self.largeFileSystem =largeFileSystemConstructor(2441lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2442)24432444ifgitConfig("git-p4.syncFromOrigin") =="false":2445 self.syncWithOrigin =False24462447# Force a checkpoint in fast-import and wait for it to finish2448defcheckpoint(self):2449 self.gitStream.write("checkpoint\n\n")2450 self.gitStream.write("progress checkpoint\n\n")2451 out = self.gitOutput.readline()2452if self.verbose:2453print"checkpoint finished: "+ out24542455defextractFilesFromCommit(self, commit):2456 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2457for path in self.cloneExclude]2458 files = []2459 fnum =02460while commit.has_key("depotFile%s"% fnum):2461 path = commit["depotFile%s"% fnum]24622463if[p for p in self.cloneExclude2464ifp4PathStartsWith(path, p)]:2465 found =False2466else:2467 found = [p for p in self.depotPaths2468ifp4PathStartsWith(path, p)]2469if not found:2470 fnum = fnum +12471continue24722473file= {}2474file["path"] = path2475file["rev"] = commit["rev%s"% fnum]2476file["action"] = commit["action%s"% fnum]2477file["type"] = commit["type%s"% fnum]2478 files.append(file)2479 fnum = fnum +12480return files24812482defextractJobsFromCommit(self, commit):2483 jobs = []2484 jnum =02485while commit.has_key("job%s"% jnum):2486 job = commit["job%s"% jnum]2487 jobs.append(job)2488 jnum = jnum +12489return jobs24902491defstripRepoPath(self, path, prefixes):2492"""When streaming files, this is called to map a p4 depot path2493 to where it should go in git. The prefixes are either2494 self.depotPaths, or self.branchPrefixes in the case of2495 branch detection."""24962497if self.useClientSpec:2498# branch detection moves files up a level (the branch name)2499# from what client spec interpretation gives2500 path = self.clientSpecDirs.map_in_client(path)2501if self.detectBranches:2502for b in self.knownBranches:2503if path.startswith(b +"/"):2504 path = path[len(b)+1:]25052506elif self.keepRepoPath:2507# Preserve everything in relative path name except leading2508# //depot/; just look at first prefix as they all should2509# be in the same depot.2510 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2511ifp4PathStartsWith(path, depot):2512 path = path[len(depot):]25132514else:2515for p in prefixes:2516ifp4PathStartsWith(path, p):2517 path = path[len(p):]2518break25192520 path =wildcard_decode(path)2521return path25222523defsplitFilesIntoBranches(self, commit):2524"""Look at each depotFile in the commit to figure out to what2525 branch it belongs."""25262527if self.clientSpecDirs:2528 files = self.extractFilesFromCommit(commit)2529 self.clientSpecDirs.update_client_spec_path_cache(files)25302531 branches = {}2532 fnum =02533while commit.has_key("depotFile%s"% fnum):2534 path = commit["depotFile%s"% fnum]2535 found = [p for p in self.depotPaths2536ifp4PathStartsWith(path, p)]2537if not found:2538 fnum = fnum +12539continue25402541file= {}2542file["path"] = path2543file["rev"] = commit["rev%s"% fnum]2544file["action"] = commit["action%s"% fnum]2545file["type"] = commit["type%s"% fnum]2546 fnum = fnum +125472548# start with the full relative path where this file would2549# go in a p4 client2550if self.useClientSpec:2551 relPath = self.clientSpecDirs.map_in_client(path)2552else:2553 relPath = self.stripRepoPath(path, self.depotPaths)25542555for branch in self.knownBranches.keys():2556# add a trailing slash so that a commit into qt/4.2foo2557# doesn't end up in qt/4.2, e.g.2558if relPath.startswith(branch +"/"):2559if branch not in branches:2560 branches[branch] = []2561 branches[branch].append(file)2562break25632564return branches25652566defwriteToGitStream(self, gitMode, relPath, contents):2567 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2568 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2569for d in contents:2570 self.gitStream.write(d)2571 self.gitStream.write('\n')25722573defencodeWithUTF8(self, path):2574try:2575 path.decode('ascii')2576except:2577 encoding ='utf8'2578ifgitConfig('git-p4.pathEncoding'):2579 encoding =gitConfig('git-p4.pathEncoding')2580 path = path.decode(encoding,'replace').encode('utf8','replace')2581if self.verbose:2582print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2583return path25842585# output one file from the P4 stream2586# - helper for streamP4Files25872588defstreamOneP4File(self,file, contents):2589 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2590 relPath = self.encodeWithUTF8(relPath)2591if verbose:2592 size =int(self.stream_file['fileSize'])2593 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2594 sys.stdout.flush()25952596(type_base, type_mods) =split_p4_type(file["type"])25972598 git_mode ="100644"2599if"x"in type_mods:2600 git_mode ="100755"2601if type_base =="symlink":2602 git_mode ="120000"2603# p4 print on a symlink sometimes contains "target\n";2604# if it does, remove the newline2605 data =''.join(contents)2606if not data:2607# Some version of p4 allowed creating a symlink that pointed2608# to nothing. This causes p4 errors when checking out such2609# a change, and errors here too. Work around it by ignoring2610# the bad symlink; hopefully a future change fixes it.2611print"\nIgnoring empty symlink in%s"%file['depotFile']2612return2613elif data[-1] =='\n':2614 contents = [data[:-1]]2615else:2616 contents = [data]26172618if type_base =="utf16":2619# p4 delivers different text in the python output to -G2620# than it does when using "print -o", or normal p4 client2621# operations. utf16 is converted to ascii or utf8, perhaps.2622# But ascii text saved as -t utf16 is completely mangled.2623# Invoke print -o to get the real contents.2624#2625# On windows, the newlines will always be mangled by print, so put2626# them back too. This is not needed to the cygwin windows version,2627# just the native "NT" type.2628#2629try:2630 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2631exceptExceptionas e:2632if'Translation of file content failed'instr(e):2633 type_base ='binary'2634else:2635raise e2636else:2637ifp4_version_string().find('/NT') >=0:2638 text = text.replace('\r\n','\n')2639 contents = [ text ]26402641if type_base =="apple":2642# Apple filetype files will be streamed as a concatenation of2643# its appledouble header and the contents. This is useless2644# on both macs and non-macs. If using "print -q -o xx", it2645# will create "xx" with the data, and "%xx" with the header.2646# This is also not very useful.2647#2648# Ideally, someday, this script can learn how to generate2649# appledouble files directly and import those to git, but2650# non-mac machines can never find a use for apple filetype.2651print"\nIgnoring apple filetype file%s"%file['depotFile']2652return26532654# Note that we do not try to de-mangle keywords on utf16 files,2655# even though in theory somebody may want that.2656 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2657if pattern:2658 regexp = re.compile(pattern, re.VERBOSE)2659 text =''.join(contents)2660 text = regexp.sub(r'$\1$', text)2661 contents = [ text ]26622663if self.largeFileSystem:2664(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)26652666 self.writeToGitStream(git_mode, relPath, contents)26672668defstreamOneP4Deletion(self,file):2669 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2670 relPath = self.encodeWithUTF8(relPath)2671if verbose:2672 sys.stdout.write("delete%s\n"% relPath)2673 sys.stdout.flush()2674 self.gitStream.write("D%s\n"% relPath)26752676if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2677 self.largeFileSystem.removeLargeFile(relPath)26782679# handle another chunk of streaming data2680defstreamP4FilesCb(self, marshalled):26812682# catch p4 errors and complain2683 err =None2684if"code"in marshalled:2685if marshalled["code"] =="error":2686if"data"in marshalled:2687 err = marshalled["data"].rstrip()26882689if not err and'fileSize'in self.stream_file:2690 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2691if required_bytes >0:2692 err ='Not enough space left on%s! Free at least%iMB.'% (2693 os.getcwd(), required_bytes/1024/10242694)26952696if err:2697 f =None2698if self.stream_have_file_info:2699if"depotFile"in self.stream_file:2700 f = self.stream_file["depotFile"]2701# force a failure in fast-import, else an empty2702# commit will be made2703 self.gitStream.write("\n")2704 self.gitStream.write("die-now\n")2705 self.gitStream.close()2706# ignore errors, but make sure it exits first2707 self.importProcess.wait()2708if f:2709die("Error from p4 print for%s:%s"% (f, err))2710else:2711die("Error from p4 print:%s"% err)27122713if marshalled.has_key('depotFile')and self.stream_have_file_info:2714# start of a new file - output the old one first2715 self.streamOneP4File(self.stream_file, self.stream_contents)2716 self.stream_file = {}2717 self.stream_contents = []2718 self.stream_have_file_info =False27192720# pick up the new file information... for the2721# 'data' field we need to append to our array2722for k in marshalled.keys():2723if k =='data':2724if'streamContentSize'not in self.stream_file:2725 self.stream_file['streamContentSize'] =02726 self.stream_file['streamContentSize'] +=len(marshalled['data'])2727 self.stream_contents.append(marshalled['data'])2728else:2729 self.stream_file[k] = marshalled[k]27302731if(verbose and2732'streamContentSize'in self.stream_file and2733'fileSize'in self.stream_file and2734'depotFile'in self.stream_file):2735 size =int(self.stream_file["fileSize"])2736if size >0:2737 progress =100*self.stream_file['streamContentSize']/size2738 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2739 sys.stdout.flush()27402741 self.stream_have_file_info =True27422743# Stream directly from "p4 files" into "git fast-import"2744defstreamP4Files(self, files):2745 filesForCommit = []2746 filesToRead = []2747 filesToDelete = []27482749for f in files:2750 filesForCommit.append(f)2751if f['action']in self.delete_actions:2752 filesToDelete.append(f)2753else:2754 filesToRead.append(f)27552756# deleted files...2757for f in filesToDelete:2758 self.streamOneP4Deletion(f)27592760iflen(filesToRead) >0:2761 self.stream_file = {}2762 self.stream_contents = []2763 self.stream_have_file_info =False27642765# curry self argument2766defstreamP4FilesCbSelf(entry):2767 self.streamP4FilesCb(entry)27682769 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]27702771p4CmdList(["-x","-","print"],2772 stdin=fileArgs,2773 cb=streamP4FilesCbSelf)27742775# do the last chunk2776if self.stream_file.has_key('depotFile'):2777 self.streamOneP4File(self.stream_file, self.stream_contents)27782779defmake_email(self, userid):2780if userid in self.users:2781return self.users[userid]2782else:2783return"%s<a@b>"% userid27842785defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2786""" Stream a p4 tag.2787 commit is either a git commit, or a fast-import mark, ":<p4commit>"2788 """27892790if verbose:2791print"writing tag%sfor commit%s"% (labelName, commit)2792 gitStream.write("tag%s\n"% labelName)2793 gitStream.write("from%s\n"% commit)27942795if labelDetails.has_key('Owner'):2796 owner = labelDetails["Owner"]2797else:2798 owner =None27992800# Try to use the owner of the p4 label, or failing that,2801# the current p4 user id.2802if owner:2803 email = self.make_email(owner)2804else:2805 email = self.make_email(self.p4UserId())2806 tagger ="%s %s %s"% (email, epoch, self.tz)28072808 gitStream.write("tagger%s\n"% tagger)28092810print"labelDetails=",labelDetails2811if labelDetails.has_key('Description'):2812 description = labelDetails['Description']2813else:2814 description ='Label from git p4'28152816 gitStream.write("data%d\n"%len(description))2817 gitStream.write(description)2818 gitStream.write("\n")28192820definClientSpec(self, path):2821if not self.clientSpecDirs:2822return True2823 inClientSpec = self.clientSpecDirs.map_in_client(path)2824if not inClientSpec and self.verbose:2825print('Ignoring file outside of client spec:{0}'.format(path))2826return inClientSpec28272828defhasBranchPrefix(self, path):2829if not self.branchPrefixes:2830return True2831 hasPrefix = [p for p in self.branchPrefixes2832ifp4PathStartsWith(path, p)]2833if not hasPrefix and self.verbose:2834print('Ignoring file outside of prefix:{0}'.format(path))2835return hasPrefix28362837defcommit(self, details, files, branch, parent =""):2838 epoch = details["time"]2839 author = details["user"]2840 jobs = self.extractJobsFromCommit(details)28412842if self.verbose:2843print('commit into{0}'.format(branch))28442845if self.clientSpecDirs:2846 self.clientSpecDirs.update_client_spec_path_cache(files)28472848 files = [f for f in files2849if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]28502851if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2852print('Ignoring revision{0}as it would produce an empty commit.'2853.format(details['change']))2854return28552856 self.gitStream.write("commit%s\n"% branch)2857 self.gitStream.write("mark :%s\n"% details["change"])2858 self.committedChanges.add(int(details["change"]))2859 committer =""2860if author not in self.users:2861 self.getUserMapFromPerforceServer()2862 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)28632864 self.gitStream.write("committer%s\n"% committer)28652866 self.gitStream.write("data <<EOT\n")2867 self.gitStream.write(details["desc"])2868iflen(jobs) >0:2869 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2870 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2871(','.join(self.branchPrefixes), details["change"]))2872iflen(details['options']) >0:2873 self.gitStream.write(": options =%s"% details['options'])2874 self.gitStream.write("]\nEOT\n\n")28752876iflen(parent) >0:2877if self.verbose:2878print"parent%s"% parent2879 self.gitStream.write("from%s\n"% parent)28802881 self.streamP4Files(files)2882 self.gitStream.write("\n")28832884 change =int(details["change"])28852886if self.labels.has_key(change):2887 label = self.labels[change]2888 labelDetails = label[0]2889 labelRevisions = label[1]2890if self.verbose:2891print"Change%sis labelled%s"% (change, labelDetails)28922893 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2894for p in self.branchPrefixes])28952896iflen(files) ==len(labelRevisions):28972898 cleanedFiles = {}2899for info in files:2900if info["action"]in self.delete_actions:2901continue2902 cleanedFiles[info["depotFile"]] = info["rev"]29032904if cleanedFiles == labelRevisions:2905 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)29062907else:2908if not self.silent:2909print("Tag%sdoes not match with change%s: files do not match."2910% (labelDetails["label"], change))29112912else:2913if not self.silent:2914print("Tag%sdoes not match with change%s: file count is different."2915% (labelDetails["label"], change))29162917# Build a dictionary of changelists and labels, for "detect-labels" option.2918defgetLabels(self):2919 self.labels = {}29202921 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2922iflen(l) >0and not self.silent:2923print"Finding files belonging to labels in%s"% `self.depotPaths`29242925for output in l:2926 label = output["label"]2927 revisions = {}2928 newestChange =02929if self.verbose:2930print"Querying files for label%s"% label2931forfileinp4CmdList(["files"] +2932["%s...@%s"% (p, label)2933for p in self.depotPaths]):2934 revisions[file["depotFile"]] =file["rev"]2935 change =int(file["change"])2936if change > newestChange:2937 newestChange = change29382939 self.labels[newestChange] = [output, revisions]29402941if self.verbose:2942print"Label changes:%s"% self.labels.keys()29432944# Import p4 labels as git tags. A direct mapping does not2945# exist, so assume that if all the files are at the same revision2946# then we can use that, or it's something more complicated we should2947# just ignore.2948defimportP4Labels(self, stream, p4Labels):2949if verbose:2950print"import p4 labels: "+' '.join(p4Labels)29512952 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2953 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2954iflen(validLabelRegexp) ==0:2955 validLabelRegexp = defaultLabelRegexp2956 m = re.compile(validLabelRegexp)29572958for name in p4Labels:2959 commitFound =False29602961if not m.match(name):2962if verbose:2963print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2964continue29652966if name in ignoredP4Labels:2967continue29682969 labelDetails =p4CmdList(['label',"-o", name])[0]29702971# get the most recent changelist for each file in this label2972 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2973for p in self.depotPaths])29742975if change.has_key('change'):2976# find the corresponding git commit; take the oldest commit2977 changelist =int(change['change'])2978if changelist in self.committedChanges:2979 gitCommit =":%d"% changelist # use a fast-import mark2980 commitFound =True2981else:2982 gitCommit =read_pipe(["git","rev-list","--max-count=1",2983"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2984iflen(gitCommit) ==0:2985print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2986else:2987 commitFound =True2988 gitCommit = gitCommit.strip()29892990if commitFound:2991# Convert from p4 time format2992try:2993 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2994exceptValueError:2995print"Could not convert label time%s"% labelDetails['Update']2996 tmwhen =129972998 when =int(time.mktime(tmwhen))2999 self.streamTag(stream, name, labelDetails, gitCommit, when)3000if verbose:3001print"p4 label%smapped to git commit%s"% (name, gitCommit)3002else:3003if verbose:3004print"Label%shas no changelists - possibly deleted?"% name30053006if not commitFound:3007# We can't import this label; don't try again as it will get very3008# expensive repeatedly fetching all the files for labels that will3009# never be imported. If the label is moved in the future, the3010# ignore will need to be removed manually.3011system(["git","config","--add","git-p4.ignoredP4Labels", name])30123013defguessProjectName(self):3014for p in self.depotPaths:3015if p.endswith("/"):3016 p = p[:-1]3017 p = p[p.strip().rfind("/") +1:]3018if not p.endswith("/"):3019 p +="/"3020return p30213022defgetBranchMapping(self):3023 lostAndFoundBranches =set()30243025 user =gitConfig("git-p4.branchUser")3026iflen(user) >0:3027 command ="branches -u%s"% user3028else:3029 command ="branches"30303031for info inp4CmdList(command):3032 details =p4Cmd(["branch","-o", info["branch"]])3033 viewIdx =03034while details.has_key("View%s"% viewIdx):3035 paths = details["View%s"% viewIdx].split(" ")3036 viewIdx = viewIdx +13037# require standard //depot/foo/... //depot/bar/... mapping3038iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3039continue3040 source = paths[0]3041 destination = paths[1]3042## HACK3043ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3044 source = source[len(self.depotPaths[0]):-4]3045 destination = destination[len(self.depotPaths[0]):-4]30463047if destination in self.knownBranches:3048if not self.silent:3049print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)3050print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)3051continue30523053 self.knownBranches[destination] = source30543055 lostAndFoundBranches.discard(destination)30563057if source not in self.knownBranches:3058 lostAndFoundBranches.add(source)30593060# Perforce does not strictly require branches to be defined, so we also3061# check git config for a branch list.3062#3063# Example of branch definition in git config file:3064# [git-p4]3065# branchList=main:branchA3066# branchList=main:branchB3067# branchList=branchA:branchC3068 configBranches =gitConfigList("git-p4.branchList")3069for branch in configBranches:3070if branch:3071(source, destination) = branch.split(":")3072 self.knownBranches[destination] = source30733074 lostAndFoundBranches.discard(destination)30753076if source not in self.knownBranches:3077 lostAndFoundBranches.add(source)307830793080for branch in lostAndFoundBranches:3081 self.knownBranches[branch] = branch30823083defgetBranchMappingFromGitBranches(self):3084 branches =p4BranchesInGit(self.importIntoRemotes)3085for branch in branches.keys():3086if branch =="master":3087 branch ="main"3088else:3089 branch = branch[len(self.projectName):]3090 self.knownBranches[branch] = branch30913092defupdateOptionDict(self, d):3093 option_keys = {}3094if self.keepRepoPath:3095 option_keys['keepRepoPath'] =130963097 d["options"] =' '.join(sorted(option_keys.keys()))30983099defreadOptions(self, d):3100 self.keepRepoPath = (d.has_key('options')3101and('keepRepoPath'in d['options']))31023103defgitRefForBranch(self, branch):3104if branch =="main":3105return self.refPrefix +"master"31063107iflen(branch) <=0:3108return branch31093110return self.refPrefix + self.projectName + branch31113112defgitCommitByP4Change(self, ref, change):3113if self.verbose:3114print"looking in ref "+ ref +" for change%susing bisect..."% change31153116 earliestCommit =""3117 latestCommit =parseRevision(ref)31183119while True:3120if self.verbose:3121print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3122 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3123iflen(next) ==0:3124if self.verbose:3125print"argh"3126return""3127 log =extractLogMessageFromGitCommit(next)3128 settings =extractSettingsGitLog(log)3129 currentChange =int(settings['change'])3130if self.verbose:3131print"current change%s"% currentChange31323133if currentChange == change:3134if self.verbose:3135print"found%s"% next3136return next31373138if currentChange < change:3139 earliestCommit ="^%s"% next3140else:3141 latestCommit ="%s"% next31423143return""31443145defimportNewBranch(self, branch, maxChange):3146# make fast-import flush all changes to disk and update the refs using the checkpoint3147# command so that we can try to find the branch parent in the git history3148 self.gitStream.write("checkpoint\n\n");3149 self.gitStream.flush();3150 branchPrefix = self.depotPaths[0] + branch +"/"3151range="@1,%s"% maxChange3152#print "prefix" + branchPrefix3153 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3154iflen(changes) <=0:3155return False3156 firstChange = changes[0]3157#print "first change in branch: %s" % firstChange3158 sourceBranch = self.knownBranches[branch]3159 sourceDepotPath = self.depotPaths[0] + sourceBranch3160 sourceRef = self.gitRefForBranch(sourceBranch)3161#print "source " + sourceBranch31623163 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3164#print "branch parent: %s" % branchParentChange3165 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3166iflen(gitParent) >0:3167 self.initialParents[self.gitRefForBranch(branch)] = gitParent3168#print "parent git commit: %s" % gitParent31693170 self.importChanges(changes)3171return True31723173defsearchParent(self, parent, branch, target):3174 parentFound =False3175for blob inread_pipe_lines(["git","rev-list","--reverse",3176"--no-merges", parent]):3177 blob = blob.strip()3178iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3179 parentFound =True3180if self.verbose:3181print"Found parent of%sin commit%s"% (branch, blob)3182break3183if parentFound:3184return blob3185else:3186return None31873188defimportChanges(self, changes):3189 cnt =13190for change in changes:3191 description =p4_describe(change)3192 self.updateOptionDict(description)31933194if not self.silent:3195 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3196 sys.stdout.flush()3197 cnt = cnt +131983199try:3200if self.detectBranches:3201 branches = self.splitFilesIntoBranches(description)3202for branch in branches.keys():3203## HACK --hwn3204 branchPrefix = self.depotPaths[0] + branch +"/"3205 self.branchPrefixes = [ branchPrefix ]32063207 parent =""32083209 filesForCommit = branches[branch]32103211if self.verbose:3212print"branch is%s"% branch32133214 self.updatedBranches.add(branch)32153216if branch not in self.createdBranches:3217 self.createdBranches.add(branch)3218 parent = self.knownBranches[branch]3219if parent == branch:3220 parent =""3221else:3222 fullBranch = self.projectName + branch3223if fullBranch not in self.p4BranchesInGit:3224if not self.silent:3225print("\nImporting new branch%s"% fullBranch);3226if self.importNewBranch(branch, change -1):3227 parent =""3228 self.p4BranchesInGit.append(fullBranch)3229if not self.silent:3230print("\nResuming with change%s"% change);32313232if self.verbose:3233print"parent determined through known branches:%s"% parent32343235 branch = self.gitRefForBranch(branch)3236 parent = self.gitRefForBranch(parent)32373238if self.verbose:3239print"looking for initial parent for%s; current parent is%s"% (branch, parent)32403241iflen(parent) ==0and branch in self.initialParents:3242 parent = self.initialParents[branch]3243del self.initialParents[branch]32443245 blob =None3246iflen(parent) >0:3247 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3248if self.verbose:3249print"Creating temporary branch: "+ tempBranch3250 self.commit(description, filesForCommit, tempBranch)3251 self.tempBranches.append(tempBranch)3252 self.checkpoint()3253 blob = self.searchParent(parent, branch, tempBranch)3254if blob:3255 self.commit(description, filesForCommit, branch, blob)3256else:3257if self.verbose:3258print"Parent of%snot found. Committing into head of%s"% (branch, parent)3259 self.commit(description, filesForCommit, branch, parent)3260else:3261 files = self.extractFilesFromCommit(description)3262 self.commit(description, files, self.branch,3263 self.initialParent)3264# only needed once, to connect to the previous commit3265 self.initialParent =""3266exceptIOError:3267print self.gitError.read()3268 sys.exit(1)32693270defsync_origin_only(self):3271if self.syncWithOrigin:3272 self.hasOrigin =originP4BranchesExist()3273if self.hasOrigin:3274if not self.silent:3275print'Syncing with origin first, using "git fetch origin"'3276system("git fetch origin")32773278defimportHeadRevision(self, revision):3279print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)32803281 details = {}3282 details["user"] ="git perforce import user"3283 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3284% (' '.join(self.depotPaths), revision))3285 details["change"] = revision3286 newestRevision =032873288 fileCnt =03289 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]32903291for info inp4CmdList(["files"] + fileArgs):32923293if'code'in info and info['code'] =='error':3294 sys.stderr.write("p4 returned an error:%s\n"3295% info['data'])3296if info['data'].find("must refer to client") >=0:3297 sys.stderr.write("This particular p4 error is misleading.\n")3298 sys.stderr.write("Perhaps the depot path was misspelled.\n");3299 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3300 sys.exit(1)3301if'p4ExitCode'in info:3302 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3303 sys.exit(1)330433053306 change =int(info["change"])3307if change > newestRevision:3308 newestRevision = change33093310if info["action"]in self.delete_actions:3311# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3312#fileCnt = fileCnt + 13313continue33143315for prop in["depotFile","rev","action","type"]:3316 details["%s%s"% (prop, fileCnt)] = info[prop]33173318 fileCnt = fileCnt +133193320 details["change"] = newestRevision33213322# Use time from top-most change so that all git p4 clones of3323# the same p4 repo have the same commit SHA1s.3324 res =p4_describe(newestRevision)3325 details["time"] = res["time"]33263327 self.updateOptionDict(details)3328try:3329 self.commit(details, self.extractFilesFromCommit(details), self.branch)3330exceptIOError:3331print"IO error with git fast-import. Is your git version recent enough?"3332print self.gitError.read()333333343335defrun(self, args):3336 self.depotPaths = []3337 self.changeRange =""3338 self.previousDepotPaths = []3339 self.hasOrigin =False33403341# map from branch depot path to parent branch3342 self.knownBranches = {}3343 self.initialParents = {}33443345if self.importIntoRemotes:3346 self.refPrefix ="refs/remotes/p4/"3347else:3348 self.refPrefix ="refs/heads/p4/"33493350 self.sync_origin_only()33513352 branch_arg_given =bool(self.branch)3353iflen(self.branch) ==0:3354 self.branch = self.refPrefix +"master"3355ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3356system("git update-ref%srefs/heads/p4"% self.branch)3357system("git branch -D p4")33583359# accept either the command-line option, or the configuration variable3360if self.useClientSpec:3361# will use this after clone to set the variable3362 self.useClientSpec_from_options =True3363else:3364ifgitConfigBool("git-p4.useclientspec"):3365 self.useClientSpec =True3366if self.useClientSpec:3367 self.clientSpecDirs =getClientSpec()33683369# TODO: should always look at previous commits,3370# merge with previous imports, if possible.3371if args == []:3372if self.hasOrigin:3373createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)33743375# branches holds mapping from branch name to sha13376 branches =p4BranchesInGit(self.importIntoRemotes)33773378# restrict to just this one, disabling detect-branches3379if branch_arg_given:3380 short = self.branch.split("/")[-1]3381if short in branches:3382 self.p4BranchesInGit = [ short ]3383else:3384 self.p4BranchesInGit = branches.keys()33853386iflen(self.p4BranchesInGit) >1:3387if not self.silent:3388print"Importing from/into multiple branches"3389 self.detectBranches =True3390for branch in branches.keys():3391 self.initialParents[self.refPrefix + branch] = \3392 branches[branch]33933394if self.verbose:3395print"branches:%s"% self.p4BranchesInGit33963397 p4Change =03398for branch in self.p4BranchesInGit:3399 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)34003401 settings =extractSettingsGitLog(logMsg)34023403 self.readOptions(settings)3404if(settings.has_key('depot-paths')3405and settings.has_key('change')):3406 change =int(settings['change']) +13407 p4Change =max(p4Change, change)34083409 depotPaths =sorted(settings['depot-paths'])3410if self.previousDepotPaths == []:3411 self.previousDepotPaths = depotPaths3412else:3413 paths = []3414for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3415 prev_list = prev.split("/")3416 cur_list = cur.split("/")3417for i inrange(0,min(len(cur_list),len(prev_list))):3418if cur_list[i] <> prev_list[i]:3419 i = i -13420break34213422 paths.append("/".join(cur_list[:i +1]))34233424 self.previousDepotPaths = paths34253426if p4Change >0:3427 self.depotPaths =sorted(self.previousDepotPaths)3428 self.changeRange ="@%s,#head"% p4Change3429if not self.silent and not self.detectBranches:3430print"Performing incremental import into%sgit branch"% self.branch34313432# accept multiple ref name abbreviations:3433# refs/foo/bar/branch -> use it exactly3434# p4/branch -> prepend refs/remotes/ or refs/heads/3435# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3436if not self.branch.startswith("refs/"):3437if self.importIntoRemotes:3438 prepend ="refs/remotes/"3439else:3440 prepend ="refs/heads/"3441if not self.branch.startswith("p4/"):3442 prepend +="p4/"3443 self.branch = prepend + self.branch34443445iflen(args) ==0and self.depotPaths:3446if not self.silent:3447print"Depot paths:%s"%' '.join(self.depotPaths)3448else:3449if self.depotPaths and self.depotPaths != args:3450print("previous import used depot path%sand now%swas specified. "3451"This doesn't work!"% (' '.join(self.depotPaths),3452' '.join(args)))3453 sys.exit(1)34543455 self.depotPaths =sorted(args)34563457 revision =""3458 self.users = {}34593460# Make sure no revision specifiers are used when --changesfile3461# is specified.3462 bad_changesfile =False3463iflen(self.changesFile) >0:3464for p in self.depotPaths:3465if p.find("@") >=0or p.find("#") >=0:3466 bad_changesfile =True3467break3468if bad_changesfile:3469die("Option --changesfile is incompatible with revision specifiers")34703471 newPaths = []3472for p in self.depotPaths:3473if p.find("@") != -1:3474 atIdx = p.index("@")3475 self.changeRange = p[atIdx:]3476if self.changeRange =="@all":3477 self.changeRange =""3478elif','not in self.changeRange:3479 revision = self.changeRange3480 self.changeRange =""3481 p = p[:atIdx]3482elif p.find("#") != -1:3483 hashIdx = p.index("#")3484 revision = p[hashIdx:]3485 p = p[:hashIdx]3486elif self.previousDepotPaths == []:3487# pay attention to changesfile, if given, else import3488# the entire p4 tree at the head revision3489iflen(self.changesFile) ==0:3490 revision ="#head"34913492 p = re.sub("\.\.\.$","", p)3493if not p.endswith("/"):3494 p +="/"34953496 newPaths.append(p)34973498 self.depotPaths = newPaths34993500# --detect-branches may change this for each branch3501 self.branchPrefixes = self.depotPaths35023503 self.loadUserMapFromCache()3504 self.labels = {}3505if self.detectLabels:3506 self.getLabels();35073508if self.detectBranches:3509## FIXME - what's a P4 projectName ?3510 self.projectName = self.guessProjectName()35113512if self.hasOrigin:3513 self.getBranchMappingFromGitBranches()3514else:3515 self.getBranchMapping()3516if self.verbose:3517print"p4-git branches:%s"% self.p4BranchesInGit3518print"initial parents:%s"% self.initialParents3519for b in self.p4BranchesInGit:3520if b !="master":35213522## FIXME3523 b = b[len(self.projectName):]3524 self.createdBranches.add(b)35253526 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))35273528 self.importProcess = subprocess.Popen(["git","fast-import"],3529 stdin=subprocess.PIPE,3530 stdout=subprocess.PIPE,3531 stderr=subprocess.PIPE);3532 self.gitOutput = self.importProcess.stdout3533 self.gitStream = self.importProcess.stdin3534 self.gitError = self.importProcess.stderr35353536if revision:3537 self.importHeadRevision(revision)3538else:3539 changes = []35403541iflen(self.changesFile) >0:3542 output =open(self.changesFile).readlines()3543 changeSet =set()3544for line in output:3545 changeSet.add(int(line))35463547for change in changeSet:3548 changes.append(change)35493550 changes.sort()3551else:3552# catch "git p4 sync" with no new branches, in a repo that3553# does not have any existing p4 branches3554iflen(args) ==0:3555if not self.p4BranchesInGit:3556die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")35573558# The default branch is master, unless --branch is used to3559# specify something else. Make sure it exists, or complain3560# nicely about how to use --branch.3561if not self.detectBranches:3562if notbranch_exists(self.branch):3563if branch_arg_given:3564die("Error: branch%sdoes not exist."% self.branch)3565else:3566die("Error: no branch%s; perhaps specify one with --branch."%3567 self.branch)35683569if self.verbose:3570print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3571 self.changeRange)3572 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)35733574iflen(self.maxChanges) >0:3575 changes = changes[:min(int(self.maxChanges),len(changes))]35763577iflen(changes) ==0:3578if not self.silent:3579print"No changes to import!"3580else:3581if not self.silent and not self.detectBranches:3582print"Import destination:%s"% self.branch35833584 self.updatedBranches =set()35853586if not self.detectBranches:3587if args:3588# start a new branch3589 self.initialParent =""3590else:3591# build on a previous revision3592 self.initialParent =parseRevision(self.branch)35933594 self.importChanges(changes)35953596if not self.silent:3597print""3598iflen(self.updatedBranches) >0:3599 sys.stdout.write("Updated branches: ")3600for b in self.updatedBranches:3601 sys.stdout.write("%s"% b)3602 sys.stdout.write("\n")36033604ifgitConfigBool("git-p4.importLabels"):3605 self.importLabels =True36063607if self.importLabels:3608 p4Labels =getP4Labels(self.depotPaths)3609 gitTags =getGitTags()36103611 missingP4Labels = p4Labels - gitTags3612 self.importP4Labels(self.gitStream, missingP4Labels)36133614 self.gitStream.close()3615if self.importProcess.wait() !=0:3616die("fast-import failed:%s"% self.gitError.read())3617 self.gitOutput.close()3618 self.gitError.close()36193620# Cleanup temporary branches created during import3621if self.tempBranches != []:3622for branch in self.tempBranches:3623read_pipe("git update-ref -d%s"% branch)3624 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))36253626# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3627# a convenient shortcut refname "p4".3628if self.importIntoRemotes:3629 head_ref = self.refPrefix +"HEAD"3630if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3631system(["git","symbolic-ref", head_ref, self.branch])36323633return True36343635classP4Rebase(Command):3636def__init__(self):3637 Command.__init__(self)3638 self.options = [3639 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3640]3641 self.importLabels =False3642 self.description = ("Fetches the latest revision from perforce and "3643+"rebases the current work (branch) against it")36443645defrun(self, args):3646 sync =P4Sync()3647 sync.importLabels = self.importLabels3648 sync.run([])36493650return self.rebase()36513652defrebase(self):3653if os.system("git update-index --refresh") !=0:3654die("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.");3655iflen(read_pipe("git diff-index HEAD --")) >0:3656die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");36573658[upstream, settings] =findUpstreamBranchPoint()3659iflen(upstream) ==0:3660die("Cannot find upstream branchpoint for rebase")36613662# the branchpoint may be p4/foo~3, so strip off the parent3663 upstream = re.sub("~[0-9]+$","", upstream)36643665print"Rebasing the current branch onto%s"% upstream3666 oldHead =read_pipe("git rev-parse HEAD").strip()3667system("git rebase%s"% upstream)3668system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3669return True36703671classP4Clone(P4Sync):3672def__init__(self):3673 P4Sync.__init__(self)3674 self.description ="Creates a new git repository and imports from Perforce into it"3675 self.usage ="usage: %prog [options] //depot/path[@revRange]"3676 self.options += [3677 optparse.make_option("--destination", dest="cloneDestination",3678 action='store', default=None,3679help="where to leave result of the clone"),3680 optparse.make_option("--bare", dest="cloneBare",3681 action="store_true", default=False),3682]3683 self.cloneDestination =None3684 self.needsGit =False3685 self.cloneBare =False36863687defdefaultDestination(self, args):3688## TODO: use common prefix of args?3689 depotPath = args[0]3690 depotDir = re.sub("(@[^@]*)$","", depotPath)3691 depotDir = re.sub("(#[^#]*)$","", depotDir)3692 depotDir = re.sub(r"\.\.\.$","", depotDir)3693 depotDir = re.sub(r"/$","", depotDir)3694return os.path.split(depotDir)[1]36953696defrun(self, args):3697iflen(args) <1:3698return False36993700if self.keepRepoPath and not self.cloneDestination:3701 sys.stderr.write("Must specify destination for --keep-path\n")3702 sys.exit(1)37033704 depotPaths = args37053706if not self.cloneDestination andlen(depotPaths) >1:3707 self.cloneDestination = depotPaths[-1]3708 depotPaths = depotPaths[:-1]37093710 self.cloneExclude = ["/"+p for p in self.cloneExclude]3711for p in depotPaths:3712if not p.startswith("//"):3713 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3714return False37153716if not self.cloneDestination:3717 self.cloneDestination = self.defaultDestination(args)37183719print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)37203721if not os.path.exists(self.cloneDestination):3722 os.makedirs(self.cloneDestination)3723chdir(self.cloneDestination)37243725 init_cmd = ["git","init"]3726if self.cloneBare:3727 init_cmd.append("--bare")3728 retcode = subprocess.call(init_cmd)3729if retcode:3730raiseCalledProcessError(retcode, init_cmd)37313732if not P4Sync.run(self, depotPaths):3733return False37343735# create a master branch and check out a work tree3736ifgitBranchExists(self.branch):3737system(["git","branch","master", self.branch ])3738if not self.cloneBare:3739system(["git","checkout","-f"])3740else:3741print'Not checking out any branch, use ' \3742'"git checkout -q -b master <branch>"'37433744# auto-set this variable if invoked with --use-client-spec3745if self.useClientSpec_from_options:3746system("git config --bool git-p4.useclientspec true")37473748return True37493750classP4Branches(Command):3751def__init__(self):3752 Command.__init__(self)3753 self.options = [ ]3754 self.description = ("Shows the git branches that hold imports and their "3755+"corresponding perforce depot paths")3756 self.verbose =False37573758defrun(self, args):3759iforiginP4BranchesExist():3760createOrUpdateBranchesFromOrigin()37613762 cmdline ="git rev-parse --symbolic "3763 cmdline +=" --remotes"37643765for line inread_pipe_lines(cmdline):3766 line = line.strip()37673768if not line.startswith('p4/')or line =="p4/HEAD":3769continue3770 branch = line37713772 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3773 settings =extractSettingsGitLog(log)37743775print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3776return True37773778classHelpFormatter(optparse.IndentedHelpFormatter):3779def__init__(self):3780 optparse.IndentedHelpFormatter.__init__(self)37813782defformat_description(self, description):3783if description:3784return description +"\n"3785else:3786return""37873788defprintUsage(commands):3789print"usage:%s<command> [options]"% sys.argv[0]3790print""3791print"valid commands:%s"%", ".join(commands)3792print""3793print"Try%s<command> --help for command specific help."% sys.argv[0]3794print""37953796commands = {3797"debug": P4Debug,3798"submit": P4Submit,3799"commit": P4Submit,3800"sync": P4Sync,3801"rebase": P4Rebase,3802"clone": P4Clone,3803"rollback": P4RollBack,3804"branches": P4Branches3805}380638073808defmain():3809iflen(sys.argv[1:]) ==0:3810printUsage(commands.keys())3811 sys.exit(2)38123813 cmdName = sys.argv[1]3814try:3815 klass = commands[cmdName]3816 cmd =klass()3817exceptKeyError:3818print"unknown command%s"% cmdName3819print""3820printUsage(commands.keys())3821 sys.exit(2)38223823 options = cmd.options3824 cmd.gitdir = os.environ.get("GIT_DIR",None)38253826 args = sys.argv[2:]38273828 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3829if cmd.needsGit:3830 options.append(optparse.make_option("--git-dir", dest="gitdir"))38313832 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3833 options,3834 description = cmd.description,3835 formatter =HelpFormatter())38363837(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3838global verbose3839 verbose = cmd.verbose3840if cmd.needsGit:3841if cmd.gitdir ==None:3842 cmd.gitdir = os.path.abspath(".git")3843if notisValidGitDir(cmd.gitdir):3844# "rev-parse --git-dir" without arguments will try $PWD/.git3845 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3846if os.path.exists(cmd.gitdir):3847 cdup =read_pipe("git rev-parse --show-cdup").strip()3848iflen(cdup) >0:3849chdir(cdup);38503851if notisValidGitDir(cmd.gitdir):3852ifisValidGitDir(cmd.gitdir +"/.git"):3853 cmd.gitdir +="/.git"3854else:3855die("fatal: cannot locate git repository at%s"% cmd.gitdir)38563857# so git commands invoked from the P4 workspace will succeed3858 os.environ["GIT_DIR"] = cmd.gitdir38593860if not cmd.run(args):3861 parser.print_help()3862 sys.exit(2)386338643865if __name__ =='__main__':3866main()