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]1362 self.description ="Submit changes from git to the perforce depot."1363 self.usage +=" [name of git branch to submit into perforce depot]"1364 self.origin =""1365 self.detectRenames =False1366 self.preserveUser =gitConfigBool("git-p4.preserveUser")1367 self.dry_run =False1368 self.shelve =False1369 self.update_shelve =list()1370 self.commit =""1371 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1372 self.prepare_p4_only =False1373 self.conflict_behavior =None1374 self.isWindows = (platform.system() =="Windows")1375 self.exportLabels =False1376 self.p4HasMoveCommand =p4_has_move_command()1377 self.branch =None13781379ifgitConfig('git-p4.largeFileSystem'):1380die("Large file system not supported for git-p4 submit command. Please remove it from config.")13811382defcheck(self):1383iflen(p4CmdList("opened ...")) >0:1384die("You have files opened with perforce! Close them before starting the sync.")13851386defseparate_jobs_from_description(self, message):1387"""Extract and return a possible Jobs field in the commit1388 message. It goes into a separate section in the p4 change1389 specification.13901391 A jobs line starts with "Jobs:" and looks like a new field1392 in a form. Values are white-space separated on the same1393 line or on following lines that start with a tab.13941395 This does not parse and extract the full git commit message1396 like a p4 form. It just sees the Jobs: line as a marker1397 to pass everything from then on directly into the p4 form,1398 but outside the description section.13991400 Return a tuple (stripped log message, jobs string)."""14011402 m = re.search(r'^Jobs:', message, re.MULTILINE)1403if m is None:1404return(message,None)14051406 jobtext = message[m.start():]1407 stripped_message = message[:m.start()].rstrip()1408return(stripped_message, jobtext)14091410defprepareLogMessage(self, template, message, jobs):1411"""Edits the template returned from "p4 change -o" to insert1412 the message in the Description field, and the jobs text in1413 the Jobs field."""1414 result =""14151416 inDescriptionSection =False14171418for line in template.split("\n"):1419if line.startswith("#"):1420 result += line +"\n"1421continue14221423if inDescriptionSection:1424if line.startswith("Files:")or line.startswith("Jobs:"):1425 inDescriptionSection =False1426# insert Jobs section1427if jobs:1428 result += jobs +"\n"1429else:1430continue1431else:1432if line.startswith("Description:"):1433 inDescriptionSection =True1434 line +="\n"1435for messageLine in message.split("\n"):1436 line +="\t"+ messageLine +"\n"14371438 result += line +"\n"14391440return result14411442defpatchRCSKeywords(self,file, pattern):1443# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1444(handle, outFileName) = tempfile.mkstemp(dir='.')1445try:1446 outFile = os.fdopen(handle,"w+")1447 inFile =open(file,"r")1448 regexp = re.compile(pattern, re.VERBOSE)1449for line in inFile.readlines():1450 line = regexp.sub(r'$\1$', line)1451 outFile.write(line)1452 inFile.close()1453 outFile.close()1454# Forcibly overwrite the original file1455 os.unlink(file)1456 shutil.move(outFileName,file)1457except:1458# cleanup our temporary file1459 os.unlink(outFileName)1460print"Failed to strip RCS keywords in%s"%file1461raise14621463print"Patched up RCS keywords in%s"%file14641465defp4UserForCommit(self,id):1466# Return the tuple (perforce user,git email) for a given git commit id1467 self.getUserMapFromPerforceServer()1468 gitEmail =read_pipe(["git","log","--max-count=1",1469"--format=%ae",id])1470 gitEmail = gitEmail.strip()1471if not self.emails.has_key(gitEmail):1472return(None,gitEmail)1473else:1474return(self.emails[gitEmail],gitEmail)14751476defcheckValidP4Users(self,commits):1477# check if any git authors cannot be mapped to p4 users1478foridin commits:1479(user,email) = self.p4UserForCommit(id)1480if not user:1481 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1482ifgitConfigBool("git-p4.allowMissingP4Users"):1483print"%s"% msg1484else:1485die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)14861487deflastP4Changelist(self):1488# Get back the last changelist number submitted in this client spec. This1489# then gets used to patch up the username in the change. If the same1490# client spec is being used by multiple processes then this might go1491# wrong.1492 results =p4CmdList("client -o")# find the current client1493 client =None1494for r in results:1495if r.has_key('Client'):1496 client = r['Client']1497break1498if not client:1499die("could not get client spec")1500 results =p4CmdList(["changes","-c", client,"-m","1"])1501for r in results:1502if r.has_key('change'):1503return r['change']1504die("Could not get changelist number for last submit - cannot patch up user details")15051506defmodifyChangelistUser(self, changelist, newUser):1507# fixup the user field of a changelist after it has been submitted.1508 changes =p4CmdList("change -o%s"% changelist)1509iflen(changes) !=1:1510die("Bad output from p4 change modifying%sto user%s"%1511(changelist, newUser))15121513 c = changes[0]1514if c['User'] == newUser:return# nothing to do1515 c['User'] = newUser1516input= marshal.dumps(c)15171518 result =p4CmdList("change -f -i", stdin=input)1519for r in result:1520if r.has_key('code'):1521if r['code'] =='error':1522die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1523if r.has_key('data'):1524print("Updated user field for changelist%sto%s"% (changelist, newUser))1525return1526die("Could not modify user field of changelist%sto%s"% (changelist, newUser))15271528defcanChangeChangelists(self):1529# check to see if we have p4 admin or super-user permissions, either of1530# which are required to modify changelists.1531 results =p4CmdList(["protects", self.depotPath])1532for r in results:1533if r.has_key('perm'):1534if r['perm'] =='admin':1535return11536if r['perm'] =='super':1537return11538return015391540defprepareSubmitTemplate(self, changelist=None):1541"""Run "p4 change -o" to grab a change specification template.1542 This does not use "p4 -G", as it is nice to keep the submission1543 template in original order, since a human might edit it.15441545 Remove lines in the Files section that show changes to files1546 outside the depot path we're committing into."""15471548[upstream, settings] =findUpstreamBranchPoint()15491550 template ="""\1551# A Perforce Change Specification.1552#1553# Change: The change number. 'new' on a new changelist.1554# Date: The date this specification was last modified.1555# Client: The client on which the changelist was created. Read-only.1556# User: The user who created the changelist.1557# Status: Either 'pending' or 'submitted'. Read-only.1558# Type: Either 'public' or 'restricted'. Default is 'public'.1559# Description: Comments about the changelist. Required.1560# Jobs: What opened jobs are to be closed by this changelist.1561# You may delete jobs from this list. (New changelists only.)1562# Files: What opened files from the default changelist are to be added1563# to this changelist. You may delete files from this list.1564# (New changelists only.)1565"""1566 files_list = []1567 inFilesSection =False1568 change_entry =None1569 args = ['change','-o']1570if changelist:1571 args.append(str(changelist))1572for entry inp4CmdList(args):1573if not entry.has_key('code'):1574continue1575if entry['code'] =='stat':1576 change_entry = entry1577break1578if not change_entry:1579die('Failed to decode output of p4 change -o')1580for key, value in change_entry.iteritems():1581if key.startswith('File'):1582if settings.has_key('depot-paths'):1583if not[p for p in settings['depot-paths']1584ifp4PathStartsWith(value, p)]:1585continue1586else:1587if notp4PathStartsWith(value, self.depotPath):1588continue1589 files_list.append(value)1590continue1591# Output in the order expected by prepareLogMessage1592for key in['Change','Client','User','Status','Description','Jobs']:1593if not change_entry.has_key(key):1594continue1595 template +='\n'1596 template += key +':'1597if key =='Description':1598 template +='\n'1599for field_line in change_entry[key].splitlines():1600 template +='\t'+field_line+'\n'1601iflen(files_list) >0:1602 template +='\n'1603 template +='Files:\n'1604for path in files_list:1605 template +='\t'+path+'\n'1606return template16071608defedit_template(self, template_file):1609"""Invoke the editor to let the user change the submission1610 message. Return true if okay to continue with the submit."""16111612# if configured to skip the editing part, just submit1613ifgitConfigBool("git-p4.skipSubmitEdit"):1614return True16151616# look at the modification time, to check later if the user saved1617# the file1618 mtime = os.stat(template_file).st_mtime16191620# invoke the editor1621if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1622 editor = os.environ.get("P4EDITOR")1623else:1624 editor =read_pipe("git var GIT_EDITOR").strip()1625system(["sh","-c", ('%s"$@"'% editor), editor, template_file])16261627# If the file was not saved, prompt to see if this patch should1628# be skipped. But skip this verification step if configured so.1629ifgitConfigBool("git-p4.skipSubmitEditCheck"):1630return True16311632# modification time updated means user saved the file1633if os.stat(template_file).st_mtime > mtime:1634return True16351636while True:1637 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1638if response =='y':1639return True1640if response =='n':1641return False16421643defget_diff_description(self, editedFiles, filesToAdd, symlinks):1644# diff1645if os.environ.has_key("P4DIFF"):1646del(os.environ["P4DIFF"])1647 diff =""1648for editedFile in editedFiles:1649 diff +=p4_read_pipe(['diff','-du',1650wildcard_encode(editedFile)])16511652# new file diff1653 newdiff =""1654for newFile in filesToAdd:1655 newdiff +="==== new file ====\n"1656 newdiff +="--- /dev/null\n"1657 newdiff +="+++%s\n"% newFile16581659 is_link = os.path.islink(newFile)1660 expect_link = newFile in symlinks16611662if is_link and expect_link:1663 newdiff +="+%s\n"% os.readlink(newFile)1664else:1665 f =open(newFile,"r")1666for line in f.readlines():1667 newdiff +="+"+ line1668 f.close()16691670return(diff + newdiff).replace('\r\n','\n')16711672defapplyCommit(self,id):1673"""Apply one commit, return True if it succeeded."""16741675print"Applying",read_pipe(["git","show","-s",1676"--format=format:%h%s",id])16771678(p4User, gitEmail) = self.p4UserForCommit(id)16791680 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1681 filesToAdd =set()1682 filesToChangeType =set()1683 filesToDelete =set()1684 editedFiles =set()1685 pureRenameCopy =set()1686 symlinks =set()1687 filesToChangeExecBit = {}1688 all_files =list()16891690for line in diff:1691 diff =parseDiffTreeEntry(line)1692 modifier = diff['status']1693 path = diff['src']1694 all_files.append(path)16951696if modifier =="M":1697p4_edit(path)1698ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1699 filesToChangeExecBit[path] = diff['dst_mode']1700 editedFiles.add(path)1701elif modifier =="A":1702 filesToAdd.add(path)1703 filesToChangeExecBit[path] = diff['dst_mode']1704if path in filesToDelete:1705 filesToDelete.remove(path)17061707 dst_mode =int(diff['dst_mode'],8)1708if dst_mode ==0120000:1709 symlinks.add(path)17101711elif modifier =="D":1712 filesToDelete.add(path)1713if path in filesToAdd:1714 filesToAdd.remove(path)1715elif modifier =="C":1716 src, dest = diff['src'], diff['dst']1717p4_integrate(src, dest)1718 pureRenameCopy.add(dest)1719if diff['src_sha1'] != diff['dst_sha1']:1720p4_edit(dest)1721 pureRenameCopy.discard(dest)1722ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1723p4_edit(dest)1724 pureRenameCopy.discard(dest)1725 filesToChangeExecBit[dest] = diff['dst_mode']1726if self.isWindows:1727# turn off read-only attribute1728 os.chmod(dest, stat.S_IWRITE)1729 os.unlink(dest)1730 editedFiles.add(dest)1731elif modifier =="R":1732 src, dest = diff['src'], diff['dst']1733if self.p4HasMoveCommand:1734p4_edit(src)# src must be open before move1735p4_move(src, dest)# opens for (move/delete, move/add)1736else:1737p4_integrate(src, dest)1738if diff['src_sha1'] != diff['dst_sha1']:1739p4_edit(dest)1740else:1741 pureRenameCopy.add(dest)1742ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1743if not self.p4HasMoveCommand:1744p4_edit(dest)# with move: already open, writable1745 filesToChangeExecBit[dest] = diff['dst_mode']1746if not self.p4HasMoveCommand:1747if self.isWindows:1748 os.chmod(dest, stat.S_IWRITE)1749 os.unlink(dest)1750 filesToDelete.add(src)1751 editedFiles.add(dest)1752elif modifier =="T":1753 filesToChangeType.add(path)1754else:1755die("unknown modifier%sfor%s"% (modifier, path))17561757 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1758 patchcmd = diffcmd +" | git apply "1759 tryPatchCmd = patchcmd +"--check -"1760 applyPatchCmd = patchcmd +"--check --apply -"1761 patch_succeeded =True17621763if os.system(tryPatchCmd) !=0:1764 fixed_rcs_keywords =False1765 patch_succeeded =False1766print"Unfortunately applying the change failed!"17671768# Patch failed, maybe it's just RCS keyword woes. Look through1769# the patch to see if that's possible.1770ifgitConfigBool("git-p4.attemptRCSCleanup"):1771file=None1772 pattern =None1773 kwfiles = {}1774forfilein editedFiles | filesToDelete:1775# did this file's delta contain RCS keywords?1776 pattern =p4_keywords_regexp_for_file(file)17771778if pattern:1779# this file is a possibility...look for RCS keywords.1780 regexp = re.compile(pattern, re.VERBOSE)1781for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1782if regexp.search(line):1783if verbose:1784print"got keyword match on%sin%sin%s"% (pattern, line,file)1785 kwfiles[file] = pattern1786break17871788forfilein kwfiles:1789if verbose:1790print"zapping%swith%s"% (line,pattern)1791# File is being deleted, so not open in p4. Must1792# disable the read-only bit on windows.1793if self.isWindows andfilenot in editedFiles:1794 os.chmod(file, stat.S_IWRITE)1795 self.patchRCSKeywords(file, kwfiles[file])1796 fixed_rcs_keywords =True17971798if fixed_rcs_keywords:1799print"Retrying the patch with RCS keywords cleaned up"1800if os.system(tryPatchCmd) ==0:1801 patch_succeeded =True18021803if not patch_succeeded:1804for f in editedFiles:1805p4_revert(f)1806return False18071808#1809# Apply the patch for real, and do add/delete/+x handling.1810#1811system(applyPatchCmd)18121813for f in filesToChangeType:1814p4_edit(f,"-t","auto")1815for f in filesToAdd:1816p4_add(f)1817for f in filesToDelete:1818p4_revert(f)1819p4_delete(f)18201821# Set/clear executable bits1822for f in filesToChangeExecBit.keys():1823 mode = filesToChangeExecBit[f]1824setP4ExecBit(f, mode)18251826 update_shelve =01827iflen(self.update_shelve) >0:1828 update_shelve = self.update_shelve.pop(0)1829p4_reopen_in_change(update_shelve, all_files)18301831#1832# Build p4 change description, starting with the contents1833# of the git commit message.1834#1835 logMessage =extractLogMessageFromGitCommit(id)1836 logMessage = logMessage.strip()1837(logMessage, jobs) = self.separate_jobs_from_description(logMessage)18381839 template = self.prepareSubmitTemplate(update_shelve)1840 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)18411842if self.preserveUser:1843 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User18441845if self.checkAuthorship and not self.p4UserIsMe(p4User):1846 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1847 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1848 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"18491850 separatorLine ="######## everything below this line is just the diff #######\n"1851if not self.prepare_p4_only:1852 submitTemplate += separatorLine1853 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)18541855(handle, fileName) = tempfile.mkstemp()1856 tmpFile = os.fdopen(handle,"w+b")1857if self.isWindows:1858 submitTemplate = submitTemplate.replace("\n","\r\n")1859 tmpFile.write(submitTemplate)1860 tmpFile.close()18611862if self.prepare_p4_only:1863#1864# Leave the p4 tree prepared, and the submit template around1865# and let the user decide what to do next1866#1867print1868print"P4 workspace prepared for submission."1869print"To submit or revert, go to client workspace"1870print" "+ self.clientPath1871print1872print"To submit, use\"p4 submit\"to write a new description,"1873print"or\"p4 submit -i <%s\"to use the one prepared by" \1874"\"git p4\"."% fileName1875print"You can delete the file\"%s\"when finished."% fileName18761877if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1878print"To preserve change ownership by user%s, you must\n" \1879"do\"p4 change -f <change>\"after submitting and\n" \1880"edit the User field."1881if pureRenameCopy:1882print"After submitting, renamed files must be re-synced."1883print"Invoke\"p4 sync -f\"on each of these files:"1884for f in pureRenameCopy:1885print" "+ f18861887print1888print"To revert the changes, use\"p4 revert ...\", and delete"1889print"the submit template file\"%s\""% fileName1890if filesToAdd:1891print"Since the commit adds new files, they must be deleted:"1892for f in filesToAdd:1893print" "+ f1894print1895return True18961897#1898# Let the user edit the change description, then submit it.1899#1900 submitted =False19011902try:1903if self.edit_template(fileName):1904# read the edited message and submit1905 tmpFile =open(fileName,"rb")1906 message = tmpFile.read()1907 tmpFile.close()1908if self.isWindows:1909 message = message.replace("\r\n","\n")1910 submitTemplate = message[:message.index(separatorLine)]19111912if update_shelve:1913p4_write_pipe(['shelve','-r','-i'], submitTemplate)1914elif self.shelve:1915p4_write_pipe(['shelve','-i'], submitTemplate)1916else:1917p4_write_pipe(['submit','-i'], submitTemplate)1918# The rename/copy happened by applying a patch that created a1919# new file. This leaves it writable, which confuses p4.1920for f in pureRenameCopy:1921p4_sync(f,"-f")19221923if self.preserveUser:1924if p4User:1925# Get last changelist number. Cannot easily get it from1926# the submit command output as the output is1927# unmarshalled.1928 changelist = self.lastP4Changelist()1929 self.modifyChangelistUser(changelist, p4User)19301931 submitted =True19321933finally:1934# skip this patch1935if not submitted or self.shelve:1936if self.shelve:1937print("Reverting shelved files.")1938else:1939print("Submission cancelled, undoing p4 changes.")1940for f in editedFiles | filesToDelete:1941p4_revert(f)1942for f in filesToAdd:1943p4_revert(f)1944 os.remove(f)19451946 os.remove(fileName)1947return submitted19481949# Export git tags as p4 labels. Create a p4 label and then tag1950# with that.1951defexportGitTags(self, gitTags):1952 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1953iflen(validLabelRegexp) ==0:1954 validLabelRegexp = defaultLabelRegexp1955 m = re.compile(validLabelRegexp)19561957for name in gitTags:19581959if not m.match(name):1960if verbose:1961print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1962continue19631964# Get the p4 commit this corresponds to1965 logMessage =extractLogMessageFromGitCommit(name)1966 values =extractSettingsGitLog(logMessage)19671968if not values.has_key('change'):1969# a tag pointing to something not sent to p4; ignore1970if verbose:1971print"git tag%sdoes not give a p4 commit"% name1972continue1973else:1974 changelist = values['change']19751976# Get the tag details.1977 inHeader =True1978 isAnnotated =False1979 body = []1980for l inread_pipe_lines(["git","cat-file","-p", name]):1981 l = l.strip()1982if inHeader:1983if re.match(r'tag\s+', l):1984 isAnnotated =True1985elif re.match(r'\s*$', l):1986 inHeader =False1987continue1988else:1989 body.append(l)19901991if not isAnnotated:1992 body = ["lightweight tag imported by git p4\n"]19931994# Create the label - use the same view as the client spec we are using1995 clientSpec =getClientSpec()19961997 labelTemplate ="Label:%s\n"% name1998 labelTemplate +="Description:\n"1999for b in body:2000 labelTemplate +="\t"+ b +"\n"2001 labelTemplate +="View:\n"2002for depot_side in clientSpec.mappings:2003 labelTemplate +="\t%s\n"% depot_side20042005if self.dry_run:2006print"Would create p4 label%sfor tag"% name2007elif self.prepare_p4_only:2008print"Not creating p4 label%sfor tag due to option" \2009" --prepare-p4-only"% name2010else:2011p4_write_pipe(["label","-i"], labelTemplate)20122013# Use the label2014p4_system(["tag","-l", name] +2015["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])20162017if verbose:2018print"created p4 label for tag%s"% name20192020defrun(self, args):2021iflen(args) ==0:2022 self.master =currentGitBranch()2023eliflen(args) ==1:2024 self.master = args[0]2025if notbranchExists(self.master):2026die("Branch%sdoes not exist"% self.master)2027else:2028return False20292030for i in self.update_shelve:2031if i <=0:2032 sys.exit("invalid changelist%d"% i)20332034if self.master:2035 allowSubmit =gitConfig("git-p4.allowSubmit")2036iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2037die("%sis not in git-p4.allowSubmit"% self.master)20382039[upstream, settings] =findUpstreamBranchPoint()2040 self.depotPath = settings['depot-paths'][0]2041iflen(self.origin) ==0:2042 self.origin = upstream20432044iflen(self.update_shelve) >0:2045 self.shelve =True20462047if self.preserveUser:2048if not self.canChangeChangelists():2049die("Cannot preserve user names without p4 super-user or admin permissions")20502051# if not set from the command line, try the config file2052if self.conflict_behavior is None:2053 val =gitConfig("git-p4.conflict")2054if val:2055if val not in self.conflict_behavior_choices:2056die("Invalid value '%s' for config git-p4.conflict"% val)2057else:2058 val ="ask"2059 self.conflict_behavior = val20602061if self.verbose:2062print"Origin branch is "+ self.origin20632064iflen(self.depotPath) ==0:2065print"Internal error: cannot locate perforce depot path from existing branches"2066 sys.exit(128)20672068 self.useClientSpec =False2069ifgitConfigBool("git-p4.useclientspec"):2070 self.useClientSpec =True2071if self.useClientSpec:2072 self.clientSpecDirs =getClientSpec()20732074# Check for the existence of P4 branches2075 branchesDetected = (len(p4BranchesInGit().keys()) >1)20762077if self.useClientSpec and not branchesDetected:2078# all files are relative to the client spec2079 self.clientPath =getClientRoot()2080else:2081 self.clientPath =p4Where(self.depotPath)20822083if self.clientPath =="":2084die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)20852086print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2087 self.oldWorkingDirectory = os.getcwd()20882089# ensure the clientPath exists2090 new_client_dir =False2091if not os.path.exists(self.clientPath):2092 new_client_dir =True2093 os.makedirs(self.clientPath)20942095chdir(self.clientPath, is_client_path=True)2096if self.dry_run:2097print"Would synchronize p4 checkout in%s"% self.clientPath2098else:2099print"Synchronizing p4 checkout..."2100if new_client_dir:2101# old one was destroyed, and maybe nobody told p42102p4_sync("...","-f")2103else:2104p4_sync("...")2105 self.check()21062107 commits = []2108if self.master:2109 commitish = self.master2110else:2111 commitish ='HEAD'21122113if self.commit !="":2114if self.commit.find("..") != -1:2115 limits_ish = self.commit.split("..")2116for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2117 commits.append(line.strip())2118 commits.reverse()2119else:2120 commits.append(self.commit)2121else:2122for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2123 commits.append(line.strip())2124 commits.reverse()21252126if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2127 self.checkAuthorship =False2128else:2129 self.checkAuthorship =True21302131if self.preserveUser:2132 self.checkValidP4Users(commits)21332134#2135# Build up a set of options to be passed to diff when2136# submitting each commit to p4.2137#2138if self.detectRenames:2139# command-line -M arg2140 self.diffOpts ="-M"2141else:2142# If not explicitly set check the config variable2143 detectRenames =gitConfig("git-p4.detectRenames")21442145if detectRenames.lower() =="false"or detectRenames =="":2146 self.diffOpts =""2147elif detectRenames.lower() =="true":2148 self.diffOpts ="-M"2149else:2150 self.diffOpts ="-M%s"% detectRenames21512152# no command-line arg for -C or --find-copies-harder, just2153# config variables2154 detectCopies =gitConfig("git-p4.detectCopies")2155if detectCopies.lower() =="false"or detectCopies =="":2156pass2157elif detectCopies.lower() =="true":2158 self.diffOpts +=" -C"2159else:2160 self.diffOpts +=" -C%s"% detectCopies21612162ifgitConfigBool("git-p4.detectCopiesHarder"):2163 self.diffOpts +=" --find-copies-harder"21642165 num_shelves =len(self.update_shelve)2166if num_shelves >0and num_shelves !=len(commits):2167 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2168(len(commits), num_shelves))21692170#2171# Apply the commits, one at a time. On failure, ask if should2172# continue to try the rest of the patches, or quit.2173#2174if self.dry_run:2175print"Would apply"2176 applied = []2177 last =len(commits) -12178for i, commit inenumerate(commits):2179if self.dry_run:2180print" ",read_pipe(["git","show","-s",2181"--format=format:%h%s", commit])2182 ok =True2183else:2184 ok = self.applyCommit(commit)2185if ok:2186 applied.append(commit)2187else:2188if self.prepare_p4_only and i < last:2189print"Processing only the first commit due to option" \2190" --prepare-p4-only"2191break2192if i < last:2193 quit =False2194while True:2195# prompt for what to do, or use the option/variable2196if self.conflict_behavior =="ask":2197print"What do you want to do?"2198 response =raw_input("[s]kip this commit but apply"2199" the rest, or [q]uit? ")2200if not response:2201continue2202elif self.conflict_behavior =="skip":2203 response ="s"2204elif self.conflict_behavior =="quit":2205 response ="q"2206else:2207die("Unknown conflict_behavior '%s'"%2208 self.conflict_behavior)22092210if response[0] =="s":2211print"Skipping this commit, but applying the rest"2212break2213if response[0] =="q":2214print"Quitting"2215 quit =True2216break2217if quit:2218break22192220chdir(self.oldWorkingDirectory)2221 shelved_applied ="shelved"if self.shelve else"applied"2222if self.dry_run:2223pass2224elif self.prepare_p4_only:2225pass2226eliflen(commits) ==len(applied):2227print("All commits{0}!".format(shelved_applied))22282229 sync =P4Sync()2230if self.branch:2231 sync.branch = self.branch2232 sync.run([])22332234if self.disable_rebase is False:2235 rebase =P4Rebase()2236 rebase.rebase()22372238else:2239iflen(applied) ==0:2240print("No commits{0}.".format(shelved_applied))2241else:2242print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2243for c in commits:2244if c in applied:2245 star ="*"2246else:2247 star =" "2248print star,read_pipe(["git","show","-s",2249"--format=format:%h%s", c])2250print"You will have to do 'git p4 sync' and rebase."22512252ifgitConfigBool("git-p4.exportLabels"):2253 self.exportLabels =True22542255if self.exportLabels:2256 p4Labels =getP4Labels(self.depotPath)2257 gitTags =getGitTags()22582259 missingGitTags = gitTags - p4Labels2260 self.exportGitTags(missingGitTags)22612262# exit with error unless everything applied perfectly2263iflen(commits) !=len(applied):2264 sys.exit(1)22652266return True22672268classView(object):2269"""Represent a p4 view ("p4 help views"), and map files in a2270 repo according to the view."""22712272def__init__(self, client_name):2273 self.mappings = []2274 self.client_prefix ="//%s/"% client_name2275# cache results of "p4 where" to lookup client file locations2276 self.client_spec_path_cache = {}22772278defappend(self, view_line):2279"""Parse a view line, splitting it into depot and client2280 sides. Append to self.mappings, preserving order. This2281 is only needed for tag creation."""22822283# Split the view line into exactly two words. P4 enforces2284# structure on these lines that simplifies this quite a bit.2285#2286# Either or both words may be double-quoted.2287# Single quotes do not matter.2288# Double-quote marks cannot occur inside the words.2289# A + or - prefix is also inside the quotes.2290# There are no quotes unless they contain a space.2291# The line is already white-space stripped.2292# The two words are separated by a single space.2293#2294if view_line[0] =='"':2295# First word is double quoted. Find its end.2296 close_quote_index = view_line.find('"',1)2297if close_quote_index <=0:2298die("No first-word closing quote found:%s"% view_line)2299 depot_side = view_line[1:close_quote_index]2300# skip closing quote and space2301 rhs_index = close_quote_index +1+12302else:2303 space_index = view_line.find(" ")2304if space_index <=0:2305die("No word-splitting space found:%s"% view_line)2306 depot_side = view_line[0:space_index]2307 rhs_index = space_index +123082309# prefix + means overlay on previous mapping2310if depot_side.startswith("+"):2311 depot_side = depot_side[1:]23122313# prefix - means exclude this path, leave out of mappings2314 exclude =False2315if depot_side.startswith("-"):2316 exclude =True2317 depot_side = depot_side[1:]23182319if not exclude:2320 self.mappings.append(depot_side)23212322defconvert_client_path(self, clientFile):2323# chop off //client/ part to make it relative2324if not clientFile.startswith(self.client_prefix):2325die("No prefix '%s' on clientFile '%s'"%2326(self.client_prefix, clientFile))2327return clientFile[len(self.client_prefix):]23282329defupdate_client_spec_path_cache(self, files):2330""" Caching file paths by "p4 where" batch query """23312332# List depot file paths exclude that already cached2333 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]23342335iflen(fileArgs) ==0:2336return# All files in cache23372338 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2339for res in where_result:2340if"code"in res and res["code"] =="error":2341# assume error is "... file(s) not in client view"2342continue2343if"clientFile"not in res:2344die("No clientFile in 'p4 where' output")2345if"unmap"in res:2346# it will list all of them, but only one not unmap-ped2347continue2348ifgitConfigBool("core.ignorecase"):2349 res['depotFile'] = res['depotFile'].lower()2350 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])23512352# not found files or unmap files set to ""2353for depotFile in fileArgs:2354ifgitConfigBool("core.ignorecase"):2355 depotFile = depotFile.lower()2356if depotFile not in self.client_spec_path_cache:2357 self.client_spec_path_cache[depotFile] =""23582359defmap_in_client(self, depot_path):2360"""Return the relative location in the client where this2361 depot file should live. Returns "" if the file should2362 not be mapped in the client."""23632364ifgitConfigBool("core.ignorecase"):2365 depot_path = depot_path.lower()23662367if depot_path in self.client_spec_path_cache:2368return self.client_spec_path_cache[depot_path]23692370die("Error:%sis not found in client spec path"% depot_path )2371return""23722373classP4Sync(Command, P4UserMap):2374 delete_actions = ("delete","move/delete","purge")23752376def__init__(self):2377 Command.__init__(self)2378 P4UserMap.__init__(self)2379 self.options = [2380 optparse.make_option("--branch", dest="branch"),2381 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2382 optparse.make_option("--changesfile", dest="changesFile"),2383 optparse.make_option("--silent", dest="silent", action="store_true"),2384 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2385 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2386 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2387help="Import into refs/heads/ , not refs/remotes"),2388 optparse.make_option("--max-changes", dest="maxChanges",2389help="Maximum number of changes to import"),2390 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2391help="Internal block size to use when iteratively calling p4 changes"),2392 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2393help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2394 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2395help="Only sync files that are included in the Perforce Client Spec"),2396 optparse.make_option("-/", dest="cloneExclude",2397 action="append",type="string",2398help="exclude depot path"),2399]2400 self.description ="""Imports from Perforce into a git repository.\n2401 example:2402 //depot/my/project/ -- to import the current head2403 //depot/my/project/@all -- to import everything2404 //depot/my/project/@1,6 -- to import only from revision 1 to 624052406 (a ... is not needed in the path p4 specification, it's added implicitly)"""24072408 self.usage +=" //depot/path[@revRange]"2409 self.silent =False2410 self.createdBranches =set()2411 self.committedChanges =set()2412 self.branch =""2413 self.detectBranches =False2414 self.detectLabels =False2415 self.importLabels =False2416 self.changesFile =""2417 self.syncWithOrigin =True2418 self.importIntoRemotes =True2419 self.maxChanges =""2420 self.changes_block_size =None2421 self.keepRepoPath =False2422 self.depotPaths =None2423 self.p4BranchesInGit = []2424 self.cloneExclude = []2425 self.useClientSpec =False2426 self.useClientSpec_from_options =False2427 self.clientSpecDirs =None2428 self.tempBranches = []2429 self.tempBranchLocation ="refs/git-p4-tmp"2430 self.largeFileSystem =None24312432ifgitConfig('git-p4.largeFileSystem'):2433 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2434 self.largeFileSystem =largeFileSystemConstructor(2435lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2436)24372438ifgitConfig("git-p4.syncFromOrigin") =="false":2439 self.syncWithOrigin =False24402441# Force a checkpoint in fast-import and wait for it to finish2442defcheckpoint(self):2443 self.gitStream.write("checkpoint\n\n")2444 self.gitStream.write("progress checkpoint\n\n")2445 out = self.gitOutput.readline()2446if self.verbose:2447print"checkpoint finished: "+ out24482449defextractFilesFromCommit(self, commit):2450 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2451for path in self.cloneExclude]2452 files = []2453 fnum =02454while commit.has_key("depotFile%s"% fnum):2455 path = commit["depotFile%s"% fnum]24562457if[p for p in self.cloneExclude2458ifp4PathStartsWith(path, p)]:2459 found =False2460else:2461 found = [p for p in self.depotPaths2462ifp4PathStartsWith(path, p)]2463if not found:2464 fnum = fnum +12465continue24662467file= {}2468file["path"] = path2469file["rev"] = commit["rev%s"% fnum]2470file["action"] = commit["action%s"% fnum]2471file["type"] = commit["type%s"% fnum]2472 files.append(file)2473 fnum = fnum +12474return files24752476defextractJobsFromCommit(self, commit):2477 jobs = []2478 jnum =02479while commit.has_key("job%s"% jnum):2480 job = commit["job%s"% jnum]2481 jobs.append(job)2482 jnum = jnum +12483return jobs24842485defstripRepoPath(self, path, prefixes):2486"""When streaming files, this is called to map a p4 depot path2487 to where it should go in git. The prefixes are either2488 self.depotPaths, or self.branchPrefixes in the case of2489 branch detection."""24902491if self.useClientSpec:2492# branch detection moves files up a level (the branch name)2493# from what client spec interpretation gives2494 path = self.clientSpecDirs.map_in_client(path)2495if self.detectBranches:2496for b in self.knownBranches:2497if path.startswith(b +"/"):2498 path = path[len(b)+1:]24992500elif self.keepRepoPath:2501# Preserve everything in relative path name except leading2502# //depot/; just look at first prefix as they all should2503# be in the same depot.2504 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2505ifp4PathStartsWith(path, depot):2506 path = path[len(depot):]25072508else:2509for p in prefixes:2510ifp4PathStartsWith(path, p):2511 path = path[len(p):]2512break25132514 path =wildcard_decode(path)2515return path25162517defsplitFilesIntoBranches(self, commit):2518"""Look at each depotFile in the commit to figure out to what2519 branch it belongs."""25202521if self.clientSpecDirs:2522 files = self.extractFilesFromCommit(commit)2523 self.clientSpecDirs.update_client_spec_path_cache(files)25242525 branches = {}2526 fnum =02527while commit.has_key("depotFile%s"% fnum):2528 path = commit["depotFile%s"% fnum]2529 found = [p for p in self.depotPaths2530ifp4PathStartsWith(path, p)]2531if not found:2532 fnum = fnum +12533continue25342535file= {}2536file["path"] = path2537file["rev"] = commit["rev%s"% fnum]2538file["action"] = commit["action%s"% fnum]2539file["type"] = commit["type%s"% fnum]2540 fnum = fnum +125412542# start with the full relative path where this file would2543# go in a p4 client2544if self.useClientSpec:2545 relPath = self.clientSpecDirs.map_in_client(path)2546else:2547 relPath = self.stripRepoPath(path, self.depotPaths)25482549for branch in self.knownBranches.keys():2550# add a trailing slash so that a commit into qt/4.2foo2551# doesn't end up in qt/4.2, e.g.2552if relPath.startswith(branch +"/"):2553if branch not in branches:2554 branches[branch] = []2555 branches[branch].append(file)2556break25572558return branches25592560defwriteToGitStream(self, gitMode, relPath, contents):2561 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2562 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2563for d in contents:2564 self.gitStream.write(d)2565 self.gitStream.write('\n')25662567defencodeWithUTF8(self, path):2568try:2569 path.decode('ascii')2570except:2571 encoding ='utf8'2572ifgitConfig('git-p4.pathEncoding'):2573 encoding =gitConfig('git-p4.pathEncoding')2574 path = path.decode(encoding,'replace').encode('utf8','replace')2575if self.verbose:2576print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2577return path25782579# output one file from the P4 stream2580# - helper for streamP4Files25812582defstreamOneP4File(self,file, contents):2583 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2584 relPath = self.encodeWithUTF8(relPath)2585if verbose:2586 size =int(self.stream_file['fileSize'])2587 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2588 sys.stdout.flush()25892590(type_base, type_mods) =split_p4_type(file["type"])25912592 git_mode ="100644"2593if"x"in type_mods:2594 git_mode ="100755"2595if type_base =="symlink":2596 git_mode ="120000"2597# p4 print on a symlink sometimes contains "target\n";2598# if it does, remove the newline2599 data =''.join(contents)2600if not data:2601# Some version of p4 allowed creating a symlink that pointed2602# to nothing. This causes p4 errors when checking out such2603# a change, and errors here too. Work around it by ignoring2604# the bad symlink; hopefully a future change fixes it.2605print"\nIgnoring empty symlink in%s"%file['depotFile']2606return2607elif data[-1] =='\n':2608 contents = [data[:-1]]2609else:2610 contents = [data]26112612if type_base =="utf16":2613# p4 delivers different text in the python output to -G2614# than it does when using "print -o", or normal p4 client2615# operations. utf16 is converted to ascii or utf8, perhaps.2616# But ascii text saved as -t utf16 is completely mangled.2617# Invoke print -o to get the real contents.2618#2619# On windows, the newlines will always be mangled by print, so put2620# them back too. This is not needed to the cygwin windows version,2621# just the native "NT" type.2622#2623try:2624 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2625exceptExceptionas e:2626if'Translation of file content failed'instr(e):2627 type_base ='binary'2628else:2629raise e2630else:2631ifp4_version_string().find('/NT') >=0:2632 text = text.replace('\r\n','\n')2633 contents = [ text ]26342635if type_base =="apple":2636# Apple filetype files will be streamed as a concatenation of2637# its appledouble header and the contents. This is useless2638# on both macs and non-macs. If using "print -q -o xx", it2639# will create "xx" with the data, and "%xx" with the header.2640# This is also not very useful.2641#2642# Ideally, someday, this script can learn how to generate2643# appledouble files directly and import those to git, but2644# non-mac machines can never find a use for apple filetype.2645print"\nIgnoring apple filetype file%s"%file['depotFile']2646return26472648# Note that we do not try to de-mangle keywords on utf16 files,2649# even though in theory somebody may want that.2650 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2651if pattern:2652 regexp = re.compile(pattern, re.VERBOSE)2653 text =''.join(contents)2654 text = regexp.sub(r'$\1$', text)2655 contents = [ text ]26562657if self.largeFileSystem:2658(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)26592660 self.writeToGitStream(git_mode, relPath, contents)26612662defstreamOneP4Deletion(self,file):2663 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2664 relPath = self.encodeWithUTF8(relPath)2665if verbose:2666 sys.stdout.write("delete%s\n"% relPath)2667 sys.stdout.flush()2668 self.gitStream.write("D%s\n"% relPath)26692670if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2671 self.largeFileSystem.removeLargeFile(relPath)26722673# handle another chunk of streaming data2674defstreamP4FilesCb(self, marshalled):26752676# catch p4 errors and complain2677 err =None2678if"code"in marshalled:2679if marshalled["code"] =="error":2680if"data"in marshalled:2681 err = marshalled["data"].rstrip()26822683if not err and'fileSize'in self.stream_file:2684 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2685if required_bytes >0:2686 err ='Not enough space left on%s! Free at least%iMB.'% (2687 os.getcwd(), required_bytes/1024/10242688)26892690if err:2691 f =None2692if self.stream_have_file_info:2693if"depotFile"in self.stream_file:2694 f = self.stream_file["depotFile"]2695# force a failure in fast-import, else an empty2696# commit will be made2697 self.gitStream.write("\n")2698 self.gitStream.write("die-now\n")2699 self.gitStream.close()2700# ignore errors, but make sure it exits first2701 self.importProcess.wait()2702if f:2703die("Error from p4 print for%s:%s"% (f, err))2704else:2705die("Error from p4 print:%s"% err)27062707if marshalled.has_key('depotFile')and self.stream_have_file_info:2708# start of a new file - output the old one first2709 self.streamOneP4File(self.stream_file, self.stream_contents)2710 self.stream_file = {}2711 self.stream_contents = []2712 self.stream_have_file_info =False27132714# pick up the new file information... for the2715# 'data' field we need to append to our array2716for k in marshalled.keys():2717if k =='data':2718if'streamContentSize'not in self.stream_file:2719 self.stream_file['streamContentSize'] =02720 self.stream_file['streamContentSize'] +=len(marshalled['data'])2721 self.stream_contents.append(marshalled['data'])2722else:2723 self.stream_file[k] = marshalled[k]27242725if(verbose and2726'streamContentSize'in self.stream_file and2727'fileSize'in self.stream_file and2728'depotFile'in self.stream_file):2729 size =int(self.stream_file["fileSize"])2730if size >0:2731 progress =100*self.stream_file['streamContentSize']/size2732 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2733 sys.stdout.flush()27342735 self.stream_have_file_info =True27362737# Stream directly from "p4 files" into "git fast-import"2738defstreamP4Files(self, files):2739 filesForCommit = []2740 filesToRead = []2741 filesToDelete = []27422743for f in files:2744 filesForCommit.append(f)2745if f['action']in self.delete_actions:2746 filesToDelete.append(f)2747else:2748 filesToRead.append(f)27492750# deleted files...2751for f in filesToDelete:2752 self.streamOneP4Deletion(f)27532754iflen(filesToRead) >0:2755 self.stream_file = {}2756 self.stream_contents = []2757 self.stream_have_file_info =False27582759# curry self argument2760defstreamP4FilesCbSelf(entry):2761 self.streamP4FilesCb(entry)27622763 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]27642765p4CmdList(["-x","-","print"],2766 stdin=fileArgs,2767 cb=streamP4FilesCbSelf)27682769# do the last chunk2770if self.stream_file.has_key('depotFile'):2771 self.streamOneP4File(self.stream_file, self.stream_contents)27722773defmake_email(self, userid):2774if userid in self.users:2775return self.users[userid]2776else:2777return"%s<a@b>"% userid27782779defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2780""" Stream a p4 tag.2781 commit is either a git commit, or a fast-import mark, ":<p4commit>"2782 """27832784if verbose:2785print"writing tag%sfor commit%s"% (labelName, commit)2786 gitStream.write("tag%s\n"% labelName)2787 gitStream.write("from%s\n"% commit)27882789if labelDetails.has_key('Owner'):2790 owner = labelDetails["Owner"]2791else:2792 owner =None27932794# Try to use the owner of the p4 label, or failing that,2795# the current p4 user id.2796if owner:2797 email = self.make_email(owner)2798else:2799 email = self.make_email(self.p4UserId())2800 tagger ="%s %s %s"% (email, epoch, self.tz)28012802 gitStream.write("tagger%s\n"% tagger)28032804print"labelDetails=",labelDetails2805if labelDetails.has_key('Description'):2806 description = labelDetails['Description']2807else:2808 description ='Label from git p4'28092810 gitStream.write("data%d\n"%len(description))2811 gitStream.write(description)2812 gitStream.write("\n")28132814definClientSpec(self, path):2815if not self.clientSpecDirs:2816return True2817 inClientSpec = self.clientSpecDirs.map_in_client(path)2818if not inClientSpec and self.verbose:2819print('Ignoring file outside of client spec:{0}'.format(path))2820return inClientSpec28212822defhasBranchPrefix(self, path):2823if not self.branchPrefixes:2824return True2825 hasPrefix = [p for p in self.branchPrefixes2826ifp4PathStartsWith(path, p)]2827if not hasPrefix and self.verbose:2828print('Ignoring file outside of prefix:{0}'.format(path))2829return hasPrefix28302831defcommit(self, details, files, branch, parent =""):2832 epoch = details["time"]2833 author = details["user"]2834 jobs = self.extractJobsFromCommit(details)28352836if self.verbose:2837print('commit into{0}'.format(branch))28382839if self.clientSpecDirs:2840 self.clientSpecDirs.update_client_spec_path_cache(files)28412842 files = [f for f in files2843if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]28442845if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2846print('Ignoring revision{0}as it would produce an empty commit.'2847.format(details['change']))2848return28492850 self.gitStream.write("commit%s\n"% branch)2851 self.gitStream.write("mark :%s\n"% details["change"])2852 self.committedChanges.add(int(details["change"]))2853 committer =""2854if author not in self.users:2855 self.getUserMapFromPerforceServer()2856 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)28572858 self.gitStream.write("committer%s\n"% committer)28592860 self.gitStream.write("data <<EOT\n")2861 self.gitStream.write(details["desc"])2862iflen(jobs) >0:2863 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2864 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2865(','.join(self.branchPrefixes), details["change"]))2866iflen(details['options']) >0:2867 self.gitStream.write(": options =%s"% details['options'])2868 self.gitStream.write("]\nEOT\n\n")28692870iflen(parent) >0:2871if self.verbose:2872print"parent%s"% parent2873 self.gitStream.write("from%s\n"% parent)28742875 self.streamP4Files(files)2876 self.gitStream.write("\n")28772878 change =int(details["change"])28792880if self.labels.has_key(change):2881 label = self.labels[change]2882 labelDetails = label[0]2883 labelRevisions = label[1]2884if self.verbose:2885print"Change%sis labelled%s"% (change, labelDetails)28862887 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2888for p in self.branchPrefixes])28892890iflen(files) ==len(labelRevisions):28912892 cleanedFiles = {}2893for info in files:2894if info["action"]in self.delete_actions:2895continue2896 cleanedFiles[info["depotFile"]] = info["rev"]28972898if cleanedFiles == labelRevisions:2899 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)29002901else:2902if not self.silent:2903print("Tag%sdoes not match with change%s: files do not match."2904% (labelDetails["label"], change))29052906else:2907if not self.silent:2908print("Tag%sdoes not match with change%s: file count is different."2909% (labelDetails["label"], change))29102911# Build a dictionary of changelists and labels, for "detect-labels" option.2912defgetLabels(self):2913 self.labels = {}29142915 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2916iflen(l) >0and not self.silent:2917print"Finding files belonging to labels in%s"% `self.depotPaths`29182919for output in l:2920 label = output["label"]2921 revisions = {}2922 newestChange =02923if self.verbose:2924print"Querying files for label%s"% label2925forfileinp4CmdList(["files"] +2926["%s...@%s"% (p, label)2927for p in self.depotPaths]):2928 revisions[file["depotFile"]] =file["rev"]2929 change =int(file["change"])2930if change > newestChange:2931 newestChange = change29322933 self.labels[newestChange] = [output, revisions]29342935if self.verbose:2936print"Label changes:%s"% self.labels.keys()29372938# Import p4 labels as git tags. A direct mapping does not2939# exist, so assume that if all the files are at the same revision2940# then we can use that, or it's something more complicated we should2941# just ignore.2942defimportP4Labels(self, stream, p4Labels):2943if verbose:2944print"import p4 labels: "+' '.join(p4Labels)29452946 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2947 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2948iflen(validLabelRegexp) ==0:2949 validLabelRegexp = defaultLabelRegexp2950 m = re.compile(validLabelRegexp)29512952for name in p4Labels:2953 commitFound =False29542955if not m.match(name):2956if verbose:2957print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2958continue29592960if name in ignoredP4Labels:2961continue29622963 labelDetails =p4CmdList(['label',"-o", name])[0]29642965# get the most recent changelist for each file in this label2966 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2967for p in self.depotPaths])29682969if change.has_key('change'):2970# find the corresponding git commit; take the oldest commit2971 changelist =int(change['change'])2972if changelist in self.committedChanges:2973 gitCommit =":%d"% changelist # use a fast-import mark2974 commitFound =True2975else:2976 gitCommit =read_pipe(["git","rev-list","--max-count=1",2977"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)2978iflen(gitCommit) ==0:2979print"importing label%s: could not find git commit for changelist%d"% (name, changelist)2980else:2981 commitFound =True2982 gitCommit = gitCommit.strip()29832984if commitFound:2985# Convert from p4 time format2986try:2987 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2988exceptValueError:2989print"Could not convert label time%s"% labelDetails['Update']2990 tmwhen =129912992 when =int(time.mktime(tmwhen))2993 self.streamTag(stream, name, labelDetails, gitCommit, when)2994if verbose:2995print"p4 label%smapped to git commit%s"% (name, gitCommit)2996else:2997if verbose:2998print"Label%shas no changelists - possibly deleted?"% name29993000if not commitFound:3001# We can't import this label; don't try again as it will get very3002# expensive repeatedly fetching all the files for labels that will3003# never be imported. If the label is moved in the future, the3004# ignore will need to be removed manually.3005system(["git","config","--add","git-p4.ignoredP4Labels", name])30063007defguessProjectName(self):3008for p in self.depotPaths:3009if p.endswith("/"):3010 p = p[:-1]3011 p = p[p.strip().rfind("/") +1:]3012if not p.endswith("/"):3013 p +="/"3014return p30153016defgetBranchMapping(self):3017 lostAndFoundBranches =set()30183019 user =gitConfig("git-p4.branchUser")3020iflen(user) >0:3021 command ="branches -u%s"% user3022else:3023 command ="branches"30243025for info inp4CmdList(command):3026 details =p4Cmd(["branch","-o", info["branch"]])3027 viewIdx =03028while details.has_key("View%s"% viewIdx):3029 paths = details["View%s"% viewIdx].split(" ")3030 viewIdx = viewIdx +13031# require standard //depot/foo/... //depot/bar/... mapping3032iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3033continue3034 source = paths[0]3035 destination = paths[1]3036## HACK3037ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3038 source = source[len(self.depotPaths[0]):-4]3039 destination = destination[len(self.depotPaths[0]):-4]30403041if destination in self.knownBranches:3042if not self.silent:3043print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)3044print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)3045continue30463047 self.knownBranches[destination] = source30483049 lostAndFoundBranches.discard(destination)30503051if source not in self.knownBranches:3052 lostAndFoundBranches.add(source)30533054# Perforce does not strictly require branches to be defined, so we also3055# check git config for a branch list.3056#3057# Example of branch definition in git config file:3058# [git-p4]3059# branchList=main:branchA3060# branchList=main:branchB3061# branchList=branchA:branchC3062 configBranches =gitConfigList("git-p4.branchList")3063for branch in configBranches:3064if branch:3065(source, destination) = branch.split(":")3066 self.knownBranches[destination] = source30673068 lostAndFoundBranches.discard(destination)30693070if source not in self.knownBranches:3071 lostAndFoundBranches.add(source)307230733074for branch in lostAndFoundBranches:3075 self.knownBranches[branch] = branch30763077defgetBranchMappingFromGitBranches(self):3078 branches =p4BranchesInGit(self.importIntoRemotes)3079for branch in branches.keys():3080if branch =="master":3081 branch ="main"3082else:3083 branch = branch[len(self.projectName):]3084 self.knownBranches[branch] = branch30853086defupdateOptionDict(self, d):3087 option_keys = {}3088if self.keepRepoPath:3089 option_keys['keepRepoPath'] =130903091 d["options"] =' '.join(sorted(option_keys.keys()))30923093defreadOptions(self, d):3094 self.keepRepoPath = (d.has_key('options')3095and('keepRepoPath'in d['options']))30963097defgitRefForBranch(self, branch):3098if branch =="main":3099return self.refPrefix +"master"31003101iflen(branch) <=0:3102return branch31033104return self.refPrefix + self.projectName + branch31053106defgitCommitByP4Change(self, ref, change):3107if self.verbose:3108print"looking in ref "+ ref +" for change%susing bisect..."% change31093110 earliestCommit =""3111 latestCommit =parseRevision(ref)31123113while True:3114if self.verbose:3115print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3116 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3117iflen(next) ==0:3118if self.verbose:3119print"argh"3120return""3121 log =extractLogMessageFromGitCommit(next)3122 settings =extractSettingsGitLog(log)3123 currentChange =int(settings['change'])3124if self.verbose:3125print"current change%s"% currentChange31263127if currentChange == change:3128if self.verbose:3129print"found%s"% next3130return next31313132if currentChange < change:3133 earliestCommit ="^%s"% next3134else:3135 latestCommit ="%s"% next31363137return""31383139defimportNewBranch(self, branch, maxChange):3140# make fast-import flush all changes to disk and update the refs using the checkpoint3141# command so that we can try to find the branch parent in the git history3142 self.gitStream.write("checkpoint\n\n");3143 self.gitStream.flush();3144 branchPrefix = self.depotPaths[0] + branch +"/"3145range="@1,%s"% maxChange3146#print "prefix" + branchPrefix3147 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3148iflen(changes) <=0:3149return False3150 firstChange = changes[0]3151#print "first change in branch: %s" % firstChange3152 sourceBranch = self.knownBranches[branch]3153 sourceDepotPath = self.depotPaths[0] + sourceBranch3154 sourceRef = self.gitRefForBranch(sourceBranch)3155#print "source " + sourceBranch31563157 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3158#print "branch parent: %s" % branchParentChange3159 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3160iflen(gitParent) >0:3161 self.initialParents[self.gitRefForBranch(branch)] = gitParent3162#print "parent git commit: %s" % gitParent31633164 self.importChanges(changes)3165return True31663167defsearchParent(self, parent, branch, target):3168 parentFound =False3169for blob inread_pipe_lines(["git","rev-list","--reverse",3170"--no-merges", parent]):3171 blob = blob.strip()3172iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3173 parentFound =True3174if self.verbose:3175print"Found parent of%sin commit%s"% (branch, blob)3176break3177if parentFound:3178return blob3179else:3180return None31813182defimportChanges(self, changes):3183 cnt =13184for change in changes:3185 description =p4_describe(change)3186 self.updateOptionDict(description)31873188if not self.silent:3189 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3190 sys.stdout.flush()3191 cnt = cnt +131923193try:3194if self.detectBranches:3195 branches = self.splitFilesIntoBranches(description)3196for branch in branches.keys():3197## HACK --hwn3198 branchPrefix = self.depotPaths[0] + branch +"/"3199 self.branchPrefixes = [ branchPrefix ]32003201 parent =""32023203 filesForCommit = branches[branch]32043205if self.verbose:3206print"branch is%s"% branch32073208 self.updatedBranches.add(branch)32093210if branch not in self.createdBranches:3211 self.createdBranches.add(branch)3212 parent = self.knownBranches[branch]3213if parent == branch:3214 parent =""3215else:3216 fullBranch = self.projectName + branch3217if fullBranch not in self.p4BranchesInGit:3218if not self.silent:3219print("\nImporting new branch%s"% fullBranch);3220if self.importNewBranch(branch, change -1):3221 parent =""3222 self.p4BranchesInGit.append(fullBranch)3223if not self.silent:3224print("\nResuming with change%s"% change);32253226if self.verbose:3227print"parent determined through known branches:%s"% parent32283229 branch = self.gitRefForBranch(branch)3230 parent = self.gitRefForBranch(parent)32313232if self.verbose:3233print"looking for initial parent for%s; current parent is%s"% (branch, parent)32343235iflen(parent) ==0and branch in self.initialParents:3236 parent = self.initialParents[branch]3237del self.initialParents[branch]32383239 blob =None3240iflen(parent) >0:3241 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3242if self.verbose:3243print"Creating temporary branch: "+ tempBranch3244 self.commit(description, filesForCommit, tempBranch)3245 self.tempBranches.append(tempBranch)3246 self.checkpoint()3247 blob = self.searchParent(parent, branch, tempBranch)3248if blob:3249 self.commit(description, filesForCommit, branch, blob)3250else:3251if self.verbose:3252print"Parent of%snot found. Committing into head of%s"% (branch, parent)3253 self.commit(description, filesForCommit, branch, parent)3254else:3255 files = self.extractFilesFromCommit(description)3256 self.commit(description, files, self.branch,3257 self.initialParent)3258# only needed once, to connect to the previous commit3259 self.initialParent =""3260exceptIOError:3261print self.gitError.read()3262 sys.exit(1)32633264defimportHeadRevision(self, revision):3265print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)32663267 details = {}3268 details["user"] ="git perforce import user"3269 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3270% (' '.join(self.depotPaths), revision))3271 details["change"] = revision3272 newestRevision =032733274 fileCnt =03275 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]32763277for info inp4CmdList(["files"] + fileArgs):32783279if'code'in info and info['code'] =='error':3280 sys.stderr.write("p4 returned an error:%s\n"3281% info['data'])3282if info['data'].find("must refer to client") >=0:3283 sys.stderr.write("This particular p4 error is misleading.\n")3284 sys.stderr.write("Perhaps the depot path was misspelled.\n");3285 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3286 sys.exit(1)3287if'p4ExitCode'in info:3288 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3289 sys.exit(1)329032913292 change =int(info["change"])3293if change > newestRevision:3294 newestRevision = change32953296if info["action"]in self.delete_actions:3297# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3298#fileCnt = fileCnt + 13299continue33003301for prop in["depotFile","rev","action","type"]:3302 details["%s%s"% (prop, fileCnt)] = info[prop]33033304 fileCnt = fileCnt +133053306 details["change"] = newestRevision33073308# Use time from top-most change so that all git p4 clones of3309# the same p4 repo have the same commit SHA1s.3310 res =p4_describe(newestRevision)3311 details["time"] = res["time"]33123313 self.updateOptionDict(details)3314try:3315 self.commit(details, self.extractFilesFromCommit(details), self.branch)3316exceptIOError:3317print"IO error with git fast-import. Is your git version recent enough?"3318print self.gitError.read()331933203321defrun(self, args):3322 self.depotPaths = []3323 self.changeRange =""3324 self.previousDepotPaths = []3325 self.hasOrigin =False33263327# map from branch depot path to parent branch3328 self.knownBranches = {}3329 self.initialParents = {}33303331if self.importIntoRemotes:3332 self.refPrefix ="refs/remotes/p4/"3333else:3334 self.refPrefix ="refs/heads/p4/"33353336if self.syncWithOrigin:3337 self.hasOrigin =originP4BranchesExist()3338if self.hasOrigin:3339if not self.silent:3340print'Syncing with origin first, using "git fetch origin"'3341system("git fetch origin")33423343 branch_arg_given =bool(self.branch)3344iflen(self.branch) ==0:3345 self.branch = self.refPrefix +"master"3346ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3347system("git update-ref%srefs/heads/p4"% self.branch)3348system("git branch -D p4")33493350# accept either the command-line option, or the configuration variable3351if self.useClientSpec:3352# will use this after clone to set the variable3353 self.useClientSpec_from_options =True3354else:3355ifgitConfigBool("git-p4.useclientspec"):3356 self.useClientSpec =True3357if self.useClientSpec:3358 self.clientSpecDirs =getClientSpec()33593360# TODO: should always look at previous commits,3361# merge with previous imports, if possible.3362if args == []:3363if self.hasOrigin:3364createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)33653366# branches holds mapping from branch name to sha13367 branches =p4BranchesInGit(self.importIntoRemotes)33683369# restrict to just this one, disabling detect-branches3370if branch_arg_given:3371 short = self.branch.split("/")[-1]3372if short in branches:3373 self.p4BranchesInGit = [ short ]3374else:3375 self.p4BranchesInGit = branches.keys()33763377iflen(self.p4BranchesInGit) >1:3378if not self.silent:3379print"Importing from/into multiple branches"3380 self.detectBranches =True3381for branch in branches.keys():3382 self.initialParents[self.refPrefix + branch] = \3383 branches[branch]33843385if self.verbose:3386print"branches:%s"% self.p4BranchesInGit33873388 p4Change =03389for branch in self.p4BranchesInGit:3390 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)33913392 settings =extractSettingsGitLog(logMsg)33933394 self.readOptions(settings)3395if(settings.has_key('depot-paths')3396and settings.has_key('change')):3397 change =int(settings['change']) +13398 p4Change =max(p4Change, change)33993400 depotPaths =sorted(settings['depot-paths'])3401if self.previousDepotPaths == []:3402 self.previousDepotPaths = depotPaths3403else:3404 paths = []3405for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3406 prev_list = prev.split("/")3407 cur_list = cur.split("/")3408for i inrange(0,min(len(cur_list),len(prev_list))):3409if cur_list[i] <> prev_list[i]:3410 i = i -13411break34123413 paths.append("/".join(cur_list[:i +1]))34143415 self.previousDepotPaths = paths34163417if p4Change >0:3418 self.depotPaths =sorted(self.previousDepotPaths)3419 self.changeRange ="@%s,#head"% p4Change3420if not self.silent and not self.detectBranches:3421print"Performing incremental import into%sgit branch"% self.branch34223423# accept multiple ref name abbreviations:3424# refs/foo/bar/branch -> use it exactly3425# p4/branch -> prepend refs/remotes/ or refs/heads/3426# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3427if not self.branch.startswith("refs/"):3428if self.importIntoRemotes:3429 prepend ="refs/remotes/"3430else:3431 prepend ="refs/heads/"3432if not self.branch.startswith("p4/"):3433 prepend +="p4/"3434 self.branch = prepend + self.branch34353436iflen(args) ==0and self.depotPaths:3437if not self.silent:3438print"Depot paths:%s"%' '.join(self.depotPaths)3439else:3440if self.depotPaths and self.depotPaths != args:3441print("previous import used depot path%sand now%swas specified. "3442"This doesn't work!"% (' '.join(self.depotPaths),3443' '.join(args)))3444 sys.exit(1)34453446 self.depotPaths =sorted(args)34473448 revision =""3449 self.users = {}34503451# Make sure no revision specifiers are used when --changesfile3452# is specified.3453 bad_changesfile =False3454iflen(self.changesFile) >0:3455for p in self.depotPaths:3456if p.find("@") >=0or p.find("#") >=0:3457 bad_changesfile =True3458break3459if bad_changesfile:3460die("Option --changesfile is incompatible with revision specifiers")34613462 newPaths = []3463for p in self.depotPaths:3464if p.find("@") != -1:3465 atIdx = p.index("@")3466 self.changeRange = p[atIdx:]3467if self.changeRange =="@all":3468 self.changeRange =""3469elif','not in self.changeRange:3470 revision = self.changeRange3471 self.changeRange =""3472 p = p[:atIdx]3473elif p.find("#") != -1:3474 hashIdx = p.index("#")3475 revision = p[hashIdx:]3476 p = p[:hashIdx]3477elif self.previousDepotPaths == []:3478# pay attention to changesfile, if given, else import3479# the entire p4 tree at the head revision3480iflen(self.changesFile) ==0:3481 revision ="#head"34823483 p = re.sub("\.\.\.$","", p)3484if not p.endswith("/"):3485 p +="/"34863487 newPaths.append(p)34883489 self.depotPaths = newPaths34903491# --detect-branches may change this for each branch3492 self.branchPrefixes = self.depotPaths34933494 self.loadUserMapFromCache()3495 self.labels = {}3496if self.detectLabels:3497 self.getLabels();34983499if self.detectBranches:3500## FIXME - what's a P4 projectName ?3501 self.projectName = self.guessProjectName()35023503if self.hasOrigin:3504 self.getBranchMappingFromGitBranches()3505else:3506 self.getBranchMapping()3507if self.verbose:3508print"p4-git branches:%s"% self.p4BranchesInGit3509print"initial parents:%s"% self.initialParents3510for b in self.p4BranchesInGit:3511if b !="master":35123513## FIXME3514 b = b[len(self.projectName):]3515 self.createdBranches.add(b)35163517 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))35183519 self.importProcess = subprocess.Popen(["git","fast-import"],3520 stdin=subprocess.PIPE,3521 stdout=subprocess.PIPE,3522 stderr=subprocess.PIPE);3523 self.gitOutput = self.importProcess.stdout3524 self.gitStream = self.importProcess.stdin3525 self.gitError = self.importProcess.stderr35263527if revision:3528 self.importHeadRevision(revision)3529else:3530 changes = []35313532iflen(self.changesFile) >0:3533 output =open(self.changesFile).readlines()3534 changeSet =set()3535for line in output:3536 changeSet.add(int(line))35373538for change in changeSet:3539 changes.append(change)35403541 changes.sort()3542else:3543# catch "git p4 sync" with no new branches, in a repo that3544# does not have any existing p4 branches3545iflen(args) ==0:3546if not self.p4BranchesInGit:3547die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")35483549# The default branch is master, unless --branch is used to3550# specify something else. Make sure it exists, or complain3551# nicely about how to use --branch.3552if not self.detectBranches:3553if notbranch_exists(self.branch):3554if branch_arg_given:3555die("Error: branch%sdoes not exist."% self.branch)3556else:3557die("Error: no branch%s; perhaps specify one with --branch."%3558 self.branch)35593560if self.verbose:3561print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3562 self.changeRange)3563 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)35643565iflen(self.maxChanges) >0:3566 changes = changes[:min(int(self.maxChanges),len(changes))]35673568iflen(changes) ==0:3569if not self.silent:3570print"No changes to import!"3571else:3572if not self.silent and not self.detectBranches:3573print"Import destination:%s"% self.branch35743575 self.updatedBranches =set()35763577if not self.detectBranches:3578if args:3579# start a new branch3580 self.initialParent =""3581else:3582# build on a previous revision3583 self.initialParent =parseRevision(self.branch)35843585 self.importChanges(changes)35863587if not self.silent:3588print""3589iflen(self.updatedBranches) >0:3590 sys.stdout.write("Updated branches: ")3591for b in self.updatedBranches:3592 sys.stdout.write("%s"% b)3593 sys.stdout.write("\n")35943595ifgitConfigBool("git-p4.importLabels"):3596 self.importLabels =True35973598if self.importLabels:3599 p4Labels =getP4Labels(self.depotPaths)3600 gitTags =getGitTags()36013602 missingP4Labels = p4Labels - gitTags3603 self.importP4Labels(self.gitStream, missingP4Labels)36043605 self.gitStream.close()3606if self.importProcess.wait() !=0:3607die("fast-import failed:%s"% self.gitError.read())3608 self.gitOutput.close()3609 self.gitError.close()36103611# Cleanup temporary branches created during import3612if self.tempBranches != []:3613for branch in self.tempBranches:3614read_pipe("git update-ref -d%s"% branch)3615 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))36163617# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3618# a convenient shortcut refname "p4".3619if self.importIntoRemotes:3620 head_ref = self.refPrefix +"HEAD"3621if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3622system(["git","symbolic-ref", head_ref, self.branch])36233624return True36253626classP4Rebase(Command):3627def__init__(self):3628 Command.__init__(self)3629 self.options = [3630 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3631]3632 self.importLabels =False3633 self.description = ("Fetches the latest revision from perforce and "3634+"rebases the current work (branch) against it")36353636defrun(self, args):3637 sync =P4Sync()3638 sync.importLabels = self.importLabels3639 sync.run([])36403641return self.rebase()36423643defrebase(self):3644if os.system("git update-index --refresh") !=0:3645die("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.");3646iflen(read_pipe("git diff-index HEAD --")) >0:3647die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");36483649[upstream, settings] =findUpstreamBranchPoint()3650iflen(upstream) ==0:3651die("Cannot find upstream branchpoint for rebase")36523653# the branchpoint may be p4/foo~3, so strip off the parent3654 upstream = re.sub("~[0-9]+$","", upstream)36553656print"Rebasing the current branch onto%s"% upstream3657 oldHead =read_pipe("git rev-parse HEAD").strip()3658system("git rebase%s"% upstream)3659system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3660return True36613662classP4Clone(P4Sync):3663def__init__(self):3664 P4Sync.__init__(self)3665 self.description ="Creates a new git repository and imports from Perforce into it"3666 self.usage ="usage: %prog [options] //depot/path[@revRange]"3667 self.options += [3668 optparse.make_option("--destination", dest="cloneDestination",3669 action='store', default=None,3670help="where to leave result of the clone"),3671 optparse.make_option("--bare", dest="cloneBare",3672 action="store_true", default=False),3673]3674 self.cloneDestination =None3675 self.needsGit =False3676 self.cloneBare =False36773678defdefaultDestination(self, args):3679## TODO: use common prefix of args?3680 depotPath = args[0]3681 depotDir = re.sub("(@[^@]*)$","", depotPath)3682 depotDir = re.sub("(#[^#]*)$","", depotDir)3683 depotDir = re.sub(r"\.\.\.$","", depotDir)3684 depotDir = re.sub(r"/$","", depotDir)3685return os.path.split(depotDir)[1]36863687defrun(self, args):3688iflen(args) <1:3689return False36903691if self.keepRepoPath and not self.cloneDestination:3692 sys.stderr.write("Must specify destination for --keep-path\n")3693 sys.exit(1)36943695 depotPaths = args36963697if not self.cloneDestination andlen(depotPaths) >1:3698 self.cloneDestination = depotPaths[-1]3699 depotPaths = depotPaths[:-1]37003701 self.cloneExclude = ["/"+p for p in self.cloneExclude]3702for p in depotPaths:3703if not p.startswith("//"):3704 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3705return False37063707if not self.cloneDestination:3708 self.cloneDestination = self.defaultDestination(args)37093710print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)37113712if not os.path.exists(self.cloneDestination):3713 os.makedirs(self.cloneDestination)3714chdir(self.cloneDestination)37153716 init_cmd = ["git","init"]3717if self.cloneBare:3718 init_cmd.append("--bare")3719 retcode = subprocess.call(init_cmd)3720if retcode:3721raiseCalledProcessError(retcode, init_cmd)37223723if not P4Sync.run(self, depotPaths):3724return False37253726# create a master branch and check out a work tree3727ifgitBranchExists(self.branch):3728system(["git","branch","master", self.branch ])3729if not self.cloneBare:3730system(["git","checkout","-f"])3731else:3732print'Not checking out any branch, use ' \3733'"git checkout -q -b master <branch>"'37343735# auto-set this variable if invoked with --use-client-spec3736if self.useClientSpec_from_options:3737system("git config --bool git-p4.useclientspec true")37383739return True37403741classP4Branches(Command):3742def__init__(self):3743 Command.__init__(self)3744 self.options = [ ]3745 self.description = ("Shows the git branches that hold imports and their "3746+"corresponding perforce depot paths")3747 self.verbose =False37483749defrun(self, args):3750iforiginP4BranchesExist():3751createOrUpdateBranchesFromOrigin()37523753 cmdline ="git rev-parse --symbolic "3754 cmdline +=" --remotes"37553756for line inread_pipe_lines(cmdline):3757 line = line.strip()37583759if not line.startswith('p4/')or line =="p4/HEAD":3760continue3761 branch = line37623763 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3764 settings =extractSettingsGitLog(log)37653766print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3767return True37683769classHelpFormatter(optparse.IndentedHelpFormatter):3770def__init__(self):3771 optparse.IndentedHelpFormatter.__init__(self)37723773defformat_description(self, description):3774if description:3775return description +"\n"3776else:3777return""37783779defprintUsage(commands):3780print"usage:%s<command> [options]"% sys.argv[0]3781print""3782print"valid commands:%s"%", ".join(commands)3783print""3784print"Try%s<command> --help for command specific help."% sys.argv[0]3785print""37863787commands = {3788"debug": P4Debug,3789"submit": P4Submit,3790"commit": P4Submit,3791"sync": P4Sync,3792"rebase": P4Rebase,3793"clone": P4Clone,3794"rollback": P4RollBack,3795"branches": P4Branches3796}379737983799defmain():3800iflen(sys.argv[1:]) ==0:3801printUsage(commands.keys())3802 sys.exit(2)38033804 cmdName = sys.argv[1]3805try:3806 klass = commands[cmdName]3807 cmd =klass()3808exceptKeyError:3809print"unknown command%s"% cmdName3810print""3811printUsage(commands.keys())3812 sys.exit(2)38133814 options = cmd.options3815 cmd.gitdir = os.environ.get("GIT_DIR",None)38163817 args = sys.argv[2:]38183819 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3820if cmd.needsGit:3821 options.append(optparse.make_option("--git-dir", dest="gitdir"))38223823 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3824 options,3825 description = cmd.description,3826 formatter =HelpFormatter())38273828(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3829global verbose3830 verbose = cmd.verbose3831if cmd.needsGit:3832if cmd.gitdir ==None:3833 cmd.gitdir = os.path.abspath(".git")3834if notisValidGitDir(cmd.gitdir):3835# "rev-parse --git-dir" without arguments will try $PWD/.git3836 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3837if os.path.exists(cmd.gitdir):3838 cdup =read_pipe("git rev-parse --show-cdup").strip()3839iflen(cdup) >0:3840chdir(cdup);38413842if notisValidGitDir(cmd.gitdir):3843ifisValidGitDir(cmd.gitdir +"/.git"):3844 cmd.gitdir +="/.git"3845else:3846die("fatal: cannot locate git repository at%s"% cmd.gitdir)38473848# so git commands invoked from the P4 workspace will succeed3849 os.environ["GIT_DIR"] = cmd.gitdir38503851if not cmd.run(args):3852 parser.print_help()3853 sys.exit(2)385438553856if __name__ =='__main__':3857main()