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 53p4_access_checked =False 54 55defp4_build_cmd(cmd): 56"""Build a suitable p4 command line. 57 58 This consolidates building and returning a p4 command line into one 59 location. It means that hooking into the environment, or other configuration 60 can be done more easily. 61 """ 62 real_cmd = ["p4"] 63 64 user =gitConfig("git-p4.user") 65iflen(user) >0: 66 real_cmd += ["-u",user] 67 68 password =gitConfig("git-p4.password") 69iflen(password) >0: 70 real_cmd += ["-P", password] 71 72 port =gitConfig("git-p4.port") 73iflen(port) >0: 74 real_cmd += ["-p", port] 75 76 host =gitConfig("git-p4.host") 77iflen(host) >0: 78 real_cmd += ["-H", host] 79 80 client =gitConfig("git-p4.client") 81iflen(client) >0: 82 real_cmd += ["-c", client] 83 84 retries =gitConfigInt("git-p4.retries") 85if retries is None: 86# Perform 3 retries by default 87 retries =3 88if retries >0: 89# Provide a way to not pass this option by setting git-p4.retries to 0 90 real_cmd += ["-r",str(retries)] 91 92ifisinstance(cmd,basestring): 93 real_cmd =' '.join(real_cmd) +' '+ cmd 94else: 95 real_cmd += cmd 96 97# now check that we can actually talk to the server 98global p4_access_checked 99if not p4_access_checked: 100 p4_access_checked =True# suppress access checks in p4_check_access itself 101p4_check_access() 102 103return real_cmd 104 105defgit_dir(path): 106""" Return TRUE if the given path is a git directory (/path/to/dir/.git). 107 This won't automatically add ".git" to a directory. 108 """ 109 d =read_pipe(["git","--git-dir", path,"rev-parse","--git-dir"],True).strip() 110if not d orlen(d) ==0: 111return None 112else: 113return d 114 115defchdir(path, is_client_path=False): 116"""Do chdir to the given path, and set the PWD environment 117 variable for use by P4. It does not look at getcwd() output. 118 Since we're not using the shell, it is necessary to set the 119 PWD environment variable explicitly. 120 121 Normally, expand the path to force it to be absolute. This 122 addresses the use of relative path names inside P4 settings, 123 e.g. P4CONFIG=.p4config. P4 does not simply open the filename 124 as given; it looks for .p4config using PWD. 125 126 If is_client_path, the path was handed to us directly by p4, 127 and may be a symbolic link. Do not call os.getcwd() in this 128 case, because it will cause p4 to think that PWD is not inside 129 the client path. 130 """ 131 132 os.chdir(path) 133if not is_client_path: 134 path = os.getcwd() 135 os.environ['PWD'] = path 136 137defcalcDiskFree(): 138"""Return free space in bytes on the disk of the given dirname.""" 139if platform.system() =='Windows': 140 free_bytes = ctypes.c_ulonglong(0) 141 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()),None,None, ctypes.pointer(free_bytes)) 142return free_bytes.value 143else: 144 st = os.statvfs(os.getcwd()) 145return st.f_bavail * st.f_frsize 146 147defdie(msg): 148if verbose: 149raiseException(msg) 150else: 151 sys.stderr.write(msg +"\n") 152 sys.exit(1) 153 154defwrite_pipe(c, stdin): 155if verbose: 156 sys.stderr.write('Writing pipe:%s\n'%str(c)) 157 158 expand =isinstance(c,basestring) 159 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 160 pipe = p.stdin 161 val = pipe.write(stdin) 162 pipe.close() 163if p.wait(): 164die('Command failed:%s'%str(c)) 165 166return val 167 168defp4_write_pipe(c, stdin): 169 real_cmd =p4_build_cmd(c) 170returnwrite_pipe(real_cmd, stdin) 171 172defread_pipe_full(c): 173""" Read output from command. Returns a tuple 174 of the return status, stdout text and stderr 175 text. 176 """ 177if verbose: 178 sys.stderr.write('Reading pipe:%s\n'%str(c)) 179 180 expand =isinstance(c,basestring) 181 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) 182(out, err) = p.communicate() 183return(p.returncode, out, err) 184 185defread_pipe(c, ignore_error=False): 186""" Read output from command. Returns the output text on 187 success. On failure, terminates execution, unless 188 ignore_error is True, when it returns an empty string. 189 """ 190(retcode, out, err) =read_pipe_full(c) 191if retcode !=0: 192if ignore_error: 193 out ="" 194else: 195die('Command failed:%s\nError:%s'% (str(c), err)) 196return out 197 198defread_pipe_text(c): 199""" Read output from a command with trailing whitespace stripped. 200 On error, returns None. 201 """ 202(retcode, out, err) =read_pipe_full(c) 203if retcode !=0: 204return None 205else: 206return out.rstrip() 207 208defp4_read_pipe(c, ignore_error=False): 209 real_cmd =p4_build_cmd(c) 210returnread_pipe(real_cmd, ignore_error) 211 212defread_pipe_lines(c): 213if verbose: 214 sys.stderr.write('Reading pipe:%s\n'%str(c)) 215 216 expand =isinstance(c, basestring) 217 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 218 pipe = p.stdout 219 val = pipe.readlines() 220if pipe.close()or p.wait(): 221die('Command failed:%s'%str(c)) 222 223return val 224 225defp4_read_pipe_lines(c): 226"""Specifically invoke p4 on the command supplied. """ 227 real_cmd =p4_build_cmd(c) 228returnread_pipe_lines(real_cmd) 229 230defp4_has_command(cmd): 231"""Ask p4 for help on this command. If it returns an error, the 232 command does not exist in this version of p4.""" 233 real_cmd =p4_build_cmd(["help", cmd]) 234 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 235 stderr=subprocess.PIPE) 236 p.communicate() 237return p.returncode ==0 238 239defp4_has_move_command(): 240"""See if the move command exists, that it supports -k, and that 241 it has not been administratively disabled. The arguments 242 must be correct, but the filenames do not have to exist. Use 243 ones with wildcards so even if they exist, it will fail.""" 244 245if notp4_has_command("move"): 246return False 247 cmd =p4_build_cmd(["move","-k","@from","@to"]) 248 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 249(out, err) = p.communicate() 250# return code will be 1 in either case 251if err.find("Invalid option") >=0: 252return False 253if err.find("disabled") >=0: 254return False 255# assume it failed because @... was invalid changelist 256return True 257 258defsystem(cmd, ignore_error=False): 259 expand =isinstance(cmd,basestring) 260if verbose: 261 sys.stderr.write("executing%s\n"%str(cmd)) 262 retcode = subprocess.call(cmd, shell=expand) 263if retcode and not ignore_error: 264raiseCalledProcessError(retcode, cmd) 265 266return retcode 267 268defp4_system(cmd): 269"""Specifically invoke p4 as the system command. """ 270 real_cmd =p4_build_cmd(cmd) 271 expand =isinstance(real_cmd, basestring) 272 retcode = subprocess.call(real_cmd, shell=expand) 273if retcode: 274raiseCalledProcessError(retcode, real_cmd) 275 276defdie_bad_access(s): 277die("failure accessing depot:{0}".format(s.rstrip())) 278 279defp4_check_access(min_expiration=1): 280""" Check if we can access Perforce - account still logged in 281 """ 282 results =p4CmdList(["login","-s"]) 283 284iflen(results) ==0: 285# should never get here: always get either some results, or a p4ExitCode 286assert("could not parse response from perforce") 287 288 result = results[0] 289 290if'p4ExitCode'in result: 291# p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path 292die_bad_access("could not run p4") 293 294 code = result.get("code") 295if not code: 296# we get here if we couldn't connect and there was nothing to unmarshal 297die_bad_access("could not connect") 298 299elif code =="stat": 300 expiry = result.get("TicketExpiration") 301if expiry: 302 expiry =int(expiry) 303if expiry > min_expiration: 304# ok to carry on 305return 306else: 307die_bad_access("perforce ticket expires in{0}seconds".format(expiry)) 308 309else: 310# account without a timeout - all ok 311return 312 313elif code =="error": 314 data = result.get("data") 315if data: 316die_bad_access("p4 error:{0}".format(data)) 317else: 318die_bad_access("unknown error") 319else: 320die_bad_access("unknown error code{0}".format(code)) 321 322_p4_version_string =None 323defp4_version_string(): 324"""Read the version string, showing just the last line, which 325 hopefully is the interesting version bit. 326 327 $ p4 -V 328 Perforce - The Fast Software Configuration Management System. 329 Copyright 1995-2011 Perforce Software. All rights reserved. 330 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 331 """ 332global _p4_version_string 333if not _p4_version_string: 334 a =p4_read_pipe_lines(["-V"]) 335 _p4_version_string = a[-1].rstrip() 336return _p4_version_string 337 338defp4_integrate(src, dest): 339p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 340 341defp4_sync(f, *options): 342p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 343 344defp4_add(f): 345# forcibly add file names with wildcards 346ifwildcard_present(f): 347p4_system(["add","-f", f]) 348else: 349p4_system(["add", f]) 350 351defp4_delete(f): 352p4_system(["delete",wildcard_encode(f)]) 353 354defp4_edit(f, *options): 355p4_system(["edit"] +list(options) + [wildcard_encode(f)]) 356 357defp4_revert(f): 358p4_system(["revert",wildcard_encode(f)]) 359 360defp4_reopen(type, f): 361p4_system(["reopen","-t",type,wildcard_encode(f)]) 362 363defp4_reopen_in_change(changelist, files): 364 cmd = ["reopen","-c",str(changelist)] + files 365p4_system(cmd) 366 367defp4_move(src, dest): 368p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 369 370defp4_last_change(): 371 results =p4CmdList(["changes","-m","1"], skip_info=True) 372returnint(results[0]['change']) 373 374defp4_describe(change): 375"""Make sure it returns a valid result by checking for 376 the presence of field "time". Return a dict of the 377 results.""" 378 379 ds =p4CmdList(["describe","-s",str(change)], skip_info=True) 380iflen(ds) !=1: 381die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 382 383 d = ds[0] 384 385if"p4ExitCode"in d: 386die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 387str(d))) 388if"code"in d: 389if d["code"] =="error": 390die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 391 392if"time"not in d: 393die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 394 395return d 396 397# 398# Canonicalize the p4 type and return a tuple of the 399# base type, plus any modifiers. See "p4 help filetypes" 400# for a list and explanation. 401# 402defsplit_p4_type(p4type): 403 404 p4_filetypes_historical = { 405"ctempobj":"binary+Sw", 406"ctext":"text+C", 407"cxtext":"text+Cx", 408"ktext":"text+k", 409"kxtext":"text+kx", 410"ltext":"text+F", 411"tempobj":"binary+FSw", 412"ubinary":"binary+F", 413"uresource":"resource+F", 414"uxbinary":"binary+Fx", 415"xbinary":"binary+x", 416"xltext":"text+Fx", 417"xtempobj":"binary+Swx", 418"xtext":"text+x", 419"xunicode":"unicode+x", 420"xutf16":"utf16+x", 421} 422if p4type in p4_filetypes_historical: 423 p4type = p4_filetypes_historical[p4type] 424 mods ="" 425 s = p4type.split("+") 426 base = s[0] 427 mods ="" 428iflen(s) >1: 429 mods = s[1] 430return(base, mods) 431 432# 433# return the raw p4 type of a file (text, text+ko, etc) 434# 435defp4_type(f): 436 results =p4CmdList(["fstat","-T","headType",wildcard_encode(f)]) 437return results[0]['headType'] 438 439# 440# Given a type base and modifier, return a regexp matching 441# the keywords that can be expanded in the file 442# 443defp4_keywords_regexp_for_type(base, type_mods): 444if base in("text","unicode","binary"): 445 kwords =None 446if"ko"in type_mods: 447 kwords ='Id|Header' 448elif"k"in type_mods: 449 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 450else: 451return None 452 pattern = r""" 453 \$ # Starts with a dollar, followed by... 454 (%s) # one of the keywords, followed by... 455 (:[^$\n]+)? # possibly an old expansion, followed by... 456 \$ # another dollar 457 """% kwords 458return pattern 459else: 460return None 461 462# 463# Given a file, return a regexp matching the possible 464# RCS keywords that will be expanded, or None for files 465# with kw expansion turned off. 466# 467defp4_keywords_regexp_for_file(file): 468if not os.path.exists(file): 469return None 470else: 471(type_base, type_mods) =split_p4_type(p4_type(file)) 472returnp4_keywords_regexp_for_type(type_base, type_mods) 473 474defsetP4ExecBit(file, mode): 475# Reopens an already open file and changes the execute bit to match 476# the execute bit setting in the passed in mode. 477 478 p4Type ="+x" 479 480if notisModeExec(mode): 481 p4Type =getP4OpenedType(file) 482 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 483 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 484if p4Type[-1] =="+": 485 p4Type = p4Type[0:-1] 486 487p4_reopen(p4Type,file) 488 489defgetP4OpenedType(file): 490# Returns the perforce file type for the given file. 491 492 result =p4_read_pipe(["opened",wildcard_encode(file)]) 493 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) 494if match: 495return match.group(1) 496else: 497die("Could not determine file type for%s(result: '%s')"% (file, result)) 498 499# Return the set of all p4 labels 500defgetP4Labels(depotPaths): 501 labels =set() 502ifisinstance(depotPaths,basestring): 503 depotPaths = [depotPaths] 504 505for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 506 label = l['label'] 507 labels.add(label) 508 509return labels 510 511# Return the set of all git tags 512defgetGitTags(): 513 gitTags =set() 514for line inread_pipe_lines(["git","tag"]): 515 tag = line.strip() 516 gitTags.add(tag) 517return gitTags 518 519defdiffTreePattern(): 520# This is a simple generator for the diff tree regex pattern. This could be 521# a class variable if this and parseDiffTreeEntry were a part of a class. 522 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 523while True: 524yield pattern 525 526defparseDiffTreeEntry(entry): 527"""Parses a single diff tree entry into its component elements. 528 529 See git-diff-tree(1) manpage for details about the format of the diff 530 output. This method returns a dictionary with the following elements: 531 532 src_mode - The mode of the source file 533 dst_mode - The mode of the destination file 534 src_sha1 - The sha1 for the source file 535 dst_sha1 - The sha1 fr the destination file 536 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 537 status_score - The score for the status (applicable for 'C' and 'R' 538 statuses). This is None if there is no score. 539 src - The path for the source file. 540 dst - The path for the destination file. This is only present for 541 copy or renames. If it is not present, this is None. 542 543 If the pattern is not matched, None is returned.""" 544 545 match =diffTreePattern().next().match(entry) 546if match: 547return{ 548'src_mode': match.group(1), 549'dst_mode': match.group(2), 550'src_sha1': match.group(3), 551'dst_sha1': match.group(4), 552'status': match.group(5), 553'status_score': match.group(6), 554'src': match.group(7), 555'dst': match.group(10) 556} 557return None 558 559defisModeExec(mode): 560# Returns True if the given git mode represents an executable file, 561# otherwise False. 562return mode[-3:] =="755" 563 564defisModeExecChanged(src_mode, dst_mode): 565returnisModeExec(src_mode) !=isModeExec(dst_mode) 566 567defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False): 568 569ifisinstance(cmd,basestring): 570 cmd ="-G "+ cmd 571 expand =True 572else: 573 cmd = ["-G"] + cmd 574 expand =False 575 576 cmd =p4_build_cmd(cmd) 577if verbose: 578 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 579 580# Use a temporary file to avoid deadlocks without 581# subprocess.communicate(), which would put another copy 582# of stdout into memory. 583 stdin_file =None 584if stdin is not None: 585 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 586ifisinstance(stdin,basestring): 587 stdin_file.write(stdin) 588else: 589for i in stdin: 590 stdin_file.write(i +'\n') 591 stdin_file.flush() 592 stdin_file.seek(0) 593 594 p4 = subprocess.Popen(cmd, 595 shell=expand, 596 stdin=stdin_file, 597 stdout=subprocess.PIPE) 598 599 result = [] 600try: 601while True: 602 entry = marshal.load(p4.stdout) 603if skip_info: 604if'code'in entry and entry['code'] =='info': 605continue 606if cb is not None: 607cb(entry) 608else: 609 result.append(entry) 610exceptEOFError: 611pass 612 exitCode = p4.wait() 613if exitCode !=0: 614 entry = {} 615 entry["p4ExitCode"] = exitCode 616 result.append(entry) 617 618return result 619 620defp4Cmd(cmd): 621list=p4CmdList(cmd) 622 result = {} 623for entry inlist: 624 result.update(entry) 625return result; 626 627defp4Where(depotPath): 628if not depotPath.endswith("/"): 629 depotPath +="/" 630 depotPathLong = depotPath +"..." 631 outputList =p4CmdList(["where", depotPathLong]) 632 output =None 633for entry in outputList: 634if"depotFile"in entry: 635# Search for the base client side depot path, as long as it starts with the branch's P4 path. 636# The base path always ends with "/...". 637if entry["depotFile"].find(depotPath) ==0and entry["depotFile"][-4:] =="/...": 638 output = entry 639break 640elif"data"in entry: 641 data = entry.get("data") 642 space = data.find(" ") 643if data[:space] == depotPath: 644 output = entry 645break 646if output ==None: 647return"" 648if output["code"] =="error": 649return"" 650 clientPath ="" 651if"path"in output: 652 clientPath = output.get("path") 653elif"data"in output: 654 data = output.get("data") 655 lastSpace = data.rfind(" ") 656 clientPath = data[lastSpace +1:] 657 658if clientPath.endswith("..."): 659 clientPath = clientPath[:-3] 660return clientPath 661 662defcurrentGitBranch(): 663returnread_pipe_text(["git","symbolic-ref","--short","-q","HEAD"]) 664 665defisValidGitDir(path): 666returngit_dir(path) !=None 667 668defparseRevision(ref): 669returnread_pipe("git rev-parse%s"% ref).strip() 670 671defbranchExists(ref): 672 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 673 ignore_error=True) 674returnlen(rev) >0 675 676defextractLogMessageFromGitCommit(commit): 677 logMessage ="" 678 679## fixme: title is first line of commit, not 1st paragraph. 680 foundTitle =False 681for log inread_pipe_lines("git cat-file commit%s"% commit): 682if not foundTitle: 683iflen(log) ==1: 684 foundTitle =True 685continue 686 687 logMessage += log 688return logMessage 689 690defextractSettingsGitLog(log): 691 values = {} 692for line in log.split("\n"): 693 line = line.strip() 694 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 695if not m: 696continue 697 698 assignments = m.group(1).split(':') 699for a in assignments: 700 vals = a.split('=') 701 key = vals[0].strip() 702 val = ('='.join(vals[1:])).strip() 703if val.endswith('\"')and val.startswith('"'): 704 val = val[1:-1] 705 706 values[key] = val 707 708 paths = values.get("depot-paths") 709if not paths: 710 paths = values.get("depot-path") 711if paths: 712 values['depot-paths'] = paths.split(',') 713return values 714 715defgitBranchExists(branch): 716 proc = subprocess.Popen(["git","rev-parse", branch], 717 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 718return proc.wait() ==0; 719 720_gitConfig = {} 721 722defgitConfig(key, typeSpecifier=None): 723if not _gitConfig.has_key(key): 724 cmd = ["git","config"] 725if typeSpecifier: 726 cmd += [ typeSpecifier ] 727 cmd += [ key ] 728 s =read_pipe(cmd, ignore_error=True) 729 _gitConfig[key] = s.strip() 730return _gitConfig[key] 731 732defgitConfigBool(key): 733"""Return a bool, using git config --bool. It is True only if the 734 variable is set to true, and False if set to false or not present 735 in the config.""" 736 737if not _gitConfig.has_key(key): 738 _gitConfig[key] =gitConfig(key,'--bool') =="true" 739return _gitConfig[key] 740 741defgitConfigInt(key): 742if not _gitConfig.has_key(key): 743 cmd = ["git","config","--int", key ] 744 s =read_pipe(cmd, ignore_error=True) 745 v = s.strip() 746try: 747 _gitConfig[key] =int(gitConfig(key,'--int')) 748exceptValueError: 749 _gitConfig[key] =None 750return _gitConfig[key] 751 752defgitConfigList(key): 753if not _gitConfig.has_key(key): 754 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 755 _gitConfig[key] = s.strip().splitlines() 756if _gitConfig[key] == ['']: 757 _gitConfig[key] = [] 758return _gitConfig[key] 759 760defp4BranchesInGit(branchesAreInRemotes=True): 761"""Find all the branches whose names start with "p4/", looking 762 in remotes or heads as specified by the argument. Return 763 a dictionary of{ branch: revision }for each one found. 764 The branch names are the short names, without any 765 "p4/" prefix.""" 766 767 branches = {} 768 769 cmdline ="git rev-parse --symbolic " 770if branchesAreInRemotes: 771 cmdline +="--remotes" 772else: 773 cmdline +="--branches" 774 775for line inread_pipe_lines(cmdline): 776 line = line.strip() 777 778# only import to p4/ 779if not line.startswith('p4/'): 780continue 781# special symbolic ref to p4/master 782if line =="p4/HEAD": 783continue 784 785# strip off p4/ prefix 786 branch = line[len("p4/"):] 787 788 branches[branch] =parseRevision(line) 789 790return branches 791 792defbranch_exists(branch): 793"""Make sure that the given ref name really exists.""" 794 795 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 796 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 797 out, _ = p.communicate() 798if p.returncode: 799return False 800# expect exactly one line of output: the branch name 801return out.rstrip() == branch 802 803deffindUpstreamBranchPoint(head ="HEAD"): 804 branches =p4BranchesInGit() 805# map from depot-path to branch name 806 branchByDepotPath = {} 807for branch in branches.keys(): 808 tip = branches[branch] 809 log =extractLogMessageFromGitCommit(tip) 810 settings =extractSettingsGitLog(log) 811if settings.has_key("depot-paths"): 812 paths =",".join(settings["depot-paths"]) 813 branchByDepotPath[paths] ="remotes/p4/"+ branch 814 815 settings =None 816 parent =0 817while parent <65535: 818 commit = head +"~%s"% parent 819 log =extractLogMessageFromGitCommit(commit) 820 settings =extractSettingsGitLog(log) 821if settings.has_key("depot-paths"): 822 paths =",".join(settings["depot-paths"]) 823if branchByDepotPath.has_key(paths): 824return[branchByDepotPath[paths], settings] 825 826 parent = parent +1 827 828return["", settings] 829 830defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 831if not silent: 832print("Creating/updating branch(es) in%sbased on origin branch(es)" 833% localRefPrefix) 834 835 originPrefix ="origin/p4/" 836 837for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 838 line = line.strip() 839if(not line.startswith(originPrefix))or line.endswith("HEAD"): 840continue 841 842 headName = line[len(originPrefix):] 843 remoteHead = localRefPrefix + headName 844 originHead = line 845 846 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 847if(not original.has_key('depot-paths') 848or not original.has_key('change')): 849continue 850 851 update =False 852if notgitBranchExists(remoteHead): 853if verbose: 854print"creating%s"% remoteHead 855 update =True 856else: 857 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 858if settings.has_key('change') >0: 859if settings['depot-paths'] == original['depot-paths']: 860 originP4Change =int(original['change']) 861 p4Change =int(settings['change']) 862if originP4Change > p4Change: 863print("%s(%s) is newer than%s(%s). " 864"Updating p4 branch from origin." 865% (originHead, originP4Change, 866 remoteHead, p4Change)) 867 update =True 868else: 869print("Ignoring:%swas imported from%swhile " 870"%swas imported from%s" 871% (originHead,','.join(original['depot-paths']), 872 remoteHead,','.join(settings['depot-paths']))) 873 874if update: 875system("git update-ref%s %s"% (remoteHead, originHead)) 876 877deforiginP4BranchesExist(): 878returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 879 880 881defp4ParseNumericChangeRange(parts): 882 changeStart =int(parts[0][1:]) 883if parts[1] =='#head': 884 changeEnd =p4_last_change() 885else: 886 changeEnd =int(parts[1]) 887 888return(changeStart, changeEnd) 889 890defchooseBlockSize(blockSize): 891if blockSize: 892return blockSize 893else: 894return defaultBlockSize 895 896defp4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): 897assert depotPaths 898 899# Parse the change range into start and end. Try to find integer 900# revision ranges as these can be broken up into blocks to avoid 901# hitting server-side limits (maxrows, maxscanresults). But if 902# that doesn't work, fall back to using the raw revision specifier 903# strings, without using block mode. 904 905if changeRange is None or changeRange =='': 906 changeStart =1 907 changeEnd =p4_last_change() 908 block_size =chooseBlockSize(requestedBlockSize) 909else: 910 parts = changeRange.split(',') 911assertlen(parts) ==2 912try: 913(changeStart, changeEnd) =p4ParseNumericChangeRange(parts) 914 block_size =chooseBlockSize(requestedBlockSize) 915except: 916 changeStart = parts[0][1:] 917 changeEnd = parts[1] 918if requestedBlockSize: 919die("cannot use --changes-block-size with non-numeric revisions") 920 block_size =None 921 922 changes =set() 923 924# Retrieve changes a block at a time, to prevent running 925# into a MaxResults/MaxScanRows error from the server. 926 927while True: 928 cmd = ['changes'] 929 930if block_size: 931 end =min(changeEnd, changeStart + block_size) 932 revisionRange ="%d,%d"% (changeStart, end) 933else: 934 revisionRange ="%s,%s"% (changeStart, changeEnd) 935 936for p in depotPaths: 937 cmd += ["%s...@%s"% (p, revisionRange)] 938 939# Insert changes in chronological order 940for entry inreversed(p4CmdList(cmd)): 941if entry.has_key('p4ExitCode'): 942die('Error retrieving changes descriptions ({})'.format(entry['p4ExitCode'])) 943if not entry.has_key('change'): 944continue 945 changes.add(int(entry['change'])) 946 947if not block_size: 948break 949 950if end >= changeEnd: 951break 952 953 changeStart = end +1 954 955 changes =sorted(changes) 956return changes 957 958defp4PathStartsWith(path, prefix): 959# This method tries to remedy a potential mixed-case issue: 960# 961# If UserA adds //depot/DirA/file1 962# and UserB adds //depot/dira/file2 963# 964# we may or may not have a problem. If you have core.ignorecase=true, 965# we treat DirA and dira as the same directory 966ifgitConfigBool("core.ignorecase"): 967return path.lower().startswith(prefix.lower()) 968return path.startswith(prefix) 969 970defgetClientSpec(): 971"""Look at the p4 client spec, create a View() object that contains 972 all the mappings, and return it.""" 973 974 specList =p4CmdList("client -o") 975iflen(specList) !=1: 976die('Output from "client -o" is%dlines, expecting 1'% 977len(specList)) 978 979# dictionary of all client parameters 980 entry = specList[0] 981 982# the //client/ name 983 client_name = entry["Client"] 984 985# just the keys that start with "View" 986 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 987 988# hold this new View 989 view =View(client_name) 990 991# append the lines, in order, to the view 992for view_num inrange(len(view_keys)): 993 k ="View%d"% view_num 994if k not in view_keys: 995die("Expected view key%smissing"% k) 996 view.append(entry[k]) 997 998return view 9991000defgetClientRoot():1001"""Grab the client directory."""10021003 output =p4CmdList("client -o")1004iflen(output) !=1:1005die('Output from "client -o" is%dlines, expecting 1'%len(output))10061007 entry = output[0]1008if"Root"not in entry:1009die('Client has no "Root"')10101011return entry["Root"]10121013#1014# P4 wildcards are not allowed in filenames. P4 complains1015# if you simply add them, but you can force it with "-f", in1016# which case it translates them into %xx encoding internally.1017#1018defwildcard_decode(path):1019# Search for and fix just these four characters. Do % last so1020# that fixing it does not inadvertently create new %-escapes.1021# Cannot have * in a filename in windows; untested as to1022# what p4 would do in such a case.1023if not platform.system() =="Windows":1024 path = path.replace("%2A","*")1025 path = path.replace("%23","#") \1026.replace("%40","@") \1027.replace("%25","%")1028return path10291030defwildcard_encode(path):1031# do % first to avoid double-encoding the %s introduced here1032 path = path.replace("%","%25") \1033.replace("*","%2A") \1034.replace("#","%23") \1035.replace("@","%40")1036return path10371038defwildcard_present(path):1039 m = re.search("[*#@%]", path)1040return m is not None10411042classLargeFileSystem(object):1043"""Base class for large file system support."""10441045def__init__(self, writeToGitStream):1046 self.largeFiles =set()1047 self.writeToGitStream = writeToGitStream10481049defgeneratePointer(self, cloneDestination, contentFile):1050"""Return the content of a pointer file that is stored in Git instead of1051 the actual content."""1052assert False,"Method 'generatePointer' required in "+ self.__class__.__name__10531054defpushFile(self, localLargeFile):1055"""Push the actual content which is not stored in the Git repository to1056 a server."""1057assert False,"Method 'pushFile' required in "+ self.__class__.__name__10581059defhasLargeFileExtension(self, relPath):1060returnreduce(1061lambda a, b: a or b,1062[relPath.endswith('.'+ e)for e ingitConfigList('git-p4.largeFileExtensions')],1063False1064)10651066defgenerateTempFile(self, contents):1067 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1068for d in contents:1069 contentFile.write(d)1070 contentFile.close()1071return contentFile.name10721073defexceedsLargeFileThreshold(self, relPath, contents):1074ifgitConfigInt('git-p4.largeFileThreshold'):1075 contentsSize =sum(len(d)for d in contents)1076if contentsSize >gitConfigInt('git-p4.largeFileThreshold'):1077return True1078ifgitConfigInt('git-p4.largeFileCompressedThreshold'):1079 contentsSize =sum(len(d)for d in contents)1080if contentsSize <=gitConfigInt('git-p4.largeFileCompressedThreshold'):1081return False1082 contentTempFile = self.generateTempFile(contents)1083 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)1084 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')1085 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)1086 zf.close()1087 compressedContentsSize = zf.infolist()[0].compress_size1088 os.remove(contentTempFile)1089 os.remove(compressedContentFile.name)1090if compressedContentsSize >gitConfigInt('git-p4.largeFileCompressedThreshold'):1091return True1092return False10931094defaddLargeFile(self, relPath):1095 self.largeFiles.add(relPath)10961097defremoveLargeFile(self, relPath):1098 self.largeFiles.remove(relPath)10991100defisLargeFile(self, relPath):1101return relPath in self.largeFiles11021103defprocessContent(self, git_mode, relPath, contents):1104"""Processes the content of git fast import. This method decides if a1105 file is stored in the large file system and handles all necessary1106 steps."""1107if self.exceedsLargeFileThreshold(relPath, contents)or self.hasLargeFileExtension(relPath):1108 contentTempFile = self.generateTempFile(contents)1109(pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)1110if pointer_git_mode:1111 git_mode = pointer_git_mode1112if localLargeFile:1113# Move temp file to final location in large file system1114 largeFileDir = os.path.dirname(localLargeFile)1115if not os.path.isdir(largeFileDir):1116 os.makedirs(largeFileDir)1117 shutil.move(contentTempFile, localLargeFile)1118 self.addLargeFile(relPath)1119ifgitConfigBool('git-p4.largeFilePush'):1120 self.pushFile(localLargeFile)1121if verbose:1122 sys.stderr.write("%smoved to large file system (%s)\n"% (relPath, localLargeFile))1123return(git_mode, contents)11241125classMockLFS(LargeFileSystem):1126"""Mock large file system for testing."""11271128defgeneratePointer(self, contentFile):1129"""The pointer content is the original content prefixed with "pointer-".1130 The local filename of the large file storage is derived from the file content.1131 """1132withopen(contentFile,'r')as f:1133 content =next(f)1134 gitMode ='100644'1135 pointerContents ='pointer-'+ content1136 localLargeFile = os.path.join(os.getcwd(),'.git','mock-storage','local', content[:-1])1137return(gitMode, pointerContents, localLargeFile)11381139defpushFile(self, localLargeFile):1140"""The remote filename of the large file storage is the same as the local1141 one but in a different directory.1142 """1143 remotePath = os.path.join(os.path.dirname(localLargeFile),'..','remote')1144if not os.path.exists(remotePath):1145 os.makedirs(remotePath)1146 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))11471148classGitLFS(LargeFileSystem):1149"""Git LFS as backend for the git-p4 large file system.1150 See https://git-lfs.github.com/ for details."""11511152def__init__(self, *args):1153 LargeFileSystem.__init__(self, *args)1154 self.baseGitAttributes = []11551156defgeneratePointer(self, contentFile):1157"""Generate a Git LFS pointer for the content. Return LFS Pointer file1158 mode and content which is stored in the Git repository instead of1159 the actual content. Return also the new location of the actual1160 content.1161 """1162if os.path.getsize(contentFile) ==0:1163return(None,'',None)11641165 pointerProcess = subprocess.Popen(1166['git','lfs','pointer','--file='+ contentFile],1167 stdout=subprocess.PIPE1168)1169 pointerFile = pointerProcess.stdout.read()1170if pointerProcess.wait():1171 os.remove(contentFile)1172die('git-lfs pointer command failed. Did you install the extension?')11731174# Git LFS removed the preamble in the output of the 'pointer' command1175# starting from version 1.2.0. Check for the preamble here to support1176# earlier versions.1177# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b431178if pointerFile.startswith('Git LFS pointer for'):1179 pointerFile = re.sub(r'Git LFS pointer for.*\n\n','', pointerFile)11801181 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)1182 localLargeFile = os.path.join(1183 os.getcwd(),1184'.git','lfs','objects', oid[:2], oid[2:4],1185 oid,1186)1187# LFS Spec states that pointer files should not have the executable bit set.1188 gitMode ='100644'1189return(gitMode, pointerFile, localLargeFile)11901191defpushFile(self, localLargeFile):1192 uploadProcess = subprocess.Popen(1193['git','lfs','push','--object-id','origin', os.path.basename(localLargeFile)]1194)1195if uploadProcess.wait():1196die('git-lfs push command failed. Did you define a remote?')11971198defgenerateGitAttributes(self):1199return(1200 self.baseGitAttributes +1201[1202'\n',1203'#\n',1204'# Git LFS (see https://git-lfs.github.com/)\n',1205'#\n',1206] +1207['*.'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1208for f insorted(gitConfigList('git-p4.largeFileExtensions'))1209] +1210['/'+ f.replace(' ','[[:space:]]') +' filter=lfs diff=lfs merge=lfs -text\n'1211for f insorted(self.largeFiles)if not self.hasLargeFileExtension(f)1212]1213)12141215defaddLargeFile(self, relPath):1216 LargeFileSystem.addLargeFile(self, relPath)1217 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12181219defremoveLargeFile(self, relPath):1220 LargeFileSystem.removeLargeFile(self, relPath)1221 self.writeToGitStream('100644','.gitattributes', self.generateGitAttributes())12221223defprocessContent(self, git_mode, relPath, contents):1224if relPath =='.gitattributes':1225 self.baseGitAttributes = contents1226return(git_mode, self.generateGitAttributes())1227else:1228return LargeFileSystem.processContent(self, git_mode, relPath, contents)12291230class Command:1231def__init__(self):1232 self.usage ="usage: %prog [options]"1233 self.needsGit =True1234 self.verbose =False12351236# This is required for the "append" cloneExclude action1237defensure_value(self, attr, value):1238if nothasattr(self, attr)orgetattr(self, attr)is None:1239setattr(self, attr, value)1240returngetattr(self, attr)12411242class P4UserMap:1243def__init__(self):1244 self.userMapFromPerforceServer =False1245 self.myP4UserId =None12461247defp4UserId(self):1248if self.myP4UserId:1249return self.myP4UserId12501251 results =p4CmdList("user -o")1252for r in results:1253if r.has_key('User'):1254 self.myP4UserId = r['User']1255return r['User']1256die("Could not find your p4 user id")12571258defp4UserIsMe(self, p4User):1259# return True if the given p4 user is actually me1260 me = self.p4UserId()1261if not p4User or p4User != me:1262return False1263else:1264return True12651266defgetUserCacheFilename(self):1267 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))1268return home +"/.gitp4-usercache.txt"12691270defgetUserMapFromPerforceServer(self):1271if self.userMapFromPerforceServer:1272return1273 self.users = {}1274 self.emails = {}12751276for output inp4CmdList("users"):1277if not output.has_key("User"):1278continue1279 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">"1280 self.emails[output["Email"]] = output["User"]12811282 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)1283for mapUserConfig ingitConfigList("git-p4.mapUser"):1284 mapUser = mapUserConfigRegex.findall(mapUserConfig)1285if mapUser andlen(mapUser[0]) ==3:1286 user = mapUser[0][0]1287 fullname = mapUser[0][1]1288 email = mapUser[0][2]1289 self.users[user] = fullname +" <"+ email +">"1290 self.emails[email] = user12911292 s =''1293for(key, val)in self.users.items():1294 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1))12951296open(self.getUserCacheFilename(),"wb").write(s)1297 self.userMapFromPerforceServer =True12981299defloadUserMapFromCache(self):1300 self.users = {}1301 self.userMapFromPerforceServer =False1302try:1303 cache =open(self.getUserCacheFilename(),"rb")1304 lines = cache.readlines()1305 cache.close()1306for line in lines:1307 entry = line.strip().split("\t")1308 self.users[entry[0]] = entry[1]1309exceptIOError:1310 self.getUserMapFromPerforceServer()13111312classP4Debug(Command):1313def__init__(self):1314 Command.__init__(self)1315 self.options = []1316 self.description ="A tool to debug the output of p4 -G."1317 self.needsGit =False13181319defrun(self, args):1320 j =01321for output inp4CmdList(args):1322print'Element:%d'% j1323 j +=11324print output1325return True13261327classP4RollBack(Command):1328def__init__(self):1329 Command.__init__(self)1330 self.options = [1331 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")1332]1333 self.description ="A tool to debug the multi-branch import. Don't use :)"1334 self.rollbackLocalBranches =False13351336defrun(self, args):1337iflen(args) !=1:1338return False1339 maxChange =int(args[0])13401341if"p4ExitCode"inp4Cmd("changes -m 1"):1342die("Problems executing p4");13431344if self.rollbackLocalBranches:1345 refPrefix ="refs/heads/"1346 lines =read_pipe_lines("git rev-parse --symbolic --branches")1347else:1348 refPrefix ="refs/remotes/"1349 lines =read_pipe_lines("git rev-parse --symbolic --remotes")13501351for line in lines:1352if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"):1353 line = line.strip()1354 ref = refPrefix + line1355 log =extractLogMessageFromGitCommit(ref)1356 settings =extractSettingsGitLog(log)13571358 depotPaths = settings['depot-paths']1359 change = settings['change']13601361 changed =False13621363iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange)1364for p in depotPaths]))) ==0:1365print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange)1366system("git update-ref -d%s`git rev-parse%s`"% (ref, ref))1367continue13681369while change andint(change) > maxChange:1370 changed =True1371if self.verbose:1372print"%sis at%s; rewinding towards%s"% (ref, change, maxChange)1373system("git update-ref%s\"%s^\""% (ref, ref))1374 log =extractLogMessageFromGitCommit(ref)1375 settings =extractSettingsGitLog(log)137613771378 depotPaths = settings['depot-paths']1379 change = settings['change']13801381if changed:1382print"%srewound to%s"% (ref, change)13831384return True13851386classP4Submit(Command, P4UserMap):13871388 conflict_behavior_choices = ("ask","skip","quit")13891390def__init__(self):1391 Command.__init__(self)1392 P4UserMap.__init__(self)1393 self.options = [1394 optparse.make_option("--origin", dest="origin"),1395 optparse.make_option("-M", dest="detectRenames", action="store_true"),1396# preserve the user, requires relevant p4 permissions1397 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),1398 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),1399 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"),1400 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),1401 optparse.make_option("--conflict", dest="conflict_behavior",1402 choices=self.conflict_behavior_choices),1403 optparse.make_option("--branch", dest="branch"),1404 optparse.make_option("--shelve", dest="shelve", action="store_true",1405help="Shelve instead of submit. Shelved files are reverted, "1406"restoring the workspace to the state before the shelve"),1407 optparse.make_option("--update-shelve", dest="update_shelve", action="append",type="int",1408 metavar="CHANGELIST",1409help="update an existing shelved changelist, implies --shelve, "1410"repeat in-order for multiple shelved changelists"),1411 optparse.make_option("--commit", dest="commit", metavar="COMMIT",1412help="submit only the specified commit(s), one commit or xxx..xxx"),1413 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",1414help="Disable rebase after submit is completed. Can be useful if you "1415"work from a local git branch that is not master"),1416 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",1417help="Skip Perforce sync of p4/master after submit or shelve"),1418]1419 self.description ="Submit changes from git to the perforce depot."1420 self.usage +=" [name of git branch to submit into perforce depot]"1421 self.origin =""1422 self.detectRenames =False1423 self.preserveUser =gitConfigBool("git-p4.preserveUser")1424 self.dry_run =False1425 self.shelve =False1426 self.update_shelve =list()1427 self.commit =""1428 self.disable_rebase =gitConfigBool("git-p4.disableRebase")1429 self.disable_p4sync =gitConfigBool("git-p4.disableP4Sync")1430 self.prepare_p4_only =False1431 self.conflict_behavior =None1432 self.isWindows = (platform.system() =="Windows")1433 self.exportLabels =False1434 self.p4HasMoveCommand =p4_has_move_command()1435 self.branch =None14361437ifgitConfig('git-p4.largeFileSystem'):1438die("Large file system not supported for git-p4 submit command. Please remove it from config.")14391440defcheck(self):1441iflen(p4CmdList("opened ...")) >0:1442die("You have files opened with perforce! Close them before starting the sync.")14431444defseparate_jobs_from_description(self, message):1445"""Extract and return a possible Jobs field in the commit1446 message. It goes into a separate section in the p4 change1447 specification.14481449 A jobs line starts with "Jobs:" and looks like a new field1450 in a form. Values are white-space separated on the same1451 line or on following lines that start with a tab.14521453 This does not parse and extract the full git commit message1454 like a p4 form. It just sees the Jobs: line as a marker1455 to pass everything from then on directly into the p4 form,1456 but outside the description section.14571458 Return a tuple (stripped log message, jobs string)."""14591460 m = re.search(r'^Jobs:', message, re.MULTILINE)1461if m is None:1462return(message,None)14631464 jobtext = message[m.start():]1465 stripped_message = message[:m.start()].rstrip()1466return(stripped_message, jobtext)14671468defprepareLogMessage(self, template, message, jobs):1469"""Edits the template returned from "p4 change -o" to insert1470 the message in the Description field, and the jobs text in1471 the Jobs field."""1472 result =""14731474 inDescriptionSection =False14751476for line in template.split("\n"):1477if line.startswith("#"):1478 result += line +"\n"1479continue14801481if inDescriptionSection:1482if line.startswith("Files:")or line.startswith("Jobs:"):1483 inDescriptionSection =False1484# insert Jobs section1485if jobs:1486 result += jobs +"\n"1487else:1488continue1489else:1490if line.startswith("Description:"):1491 inDescriptionSection =True1492 line +="\n"1493for messageLine in message.split("\n"):1494 line +="\t"+ messageLine +"\n"14951496 result += line +"\n"14971498return result14991500defpatchRCSKeywords(self,file, pattern):1501# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1502(handle, outFileName) = tempfile.mkstemp(dir='.')1503try:1504 outFile = os.fdopen(handle,"w+")1505 inFile =open(file,"r")1506 regexp = re.compile(pattern, re.VERBOSE)1507for line in inFile.readlines():1508 line = regexp.sub(r'$\1$', line)1509 outFile.write(line)1510 inFile.close()1511 outFile.close()1512# Forcibly overwrite the original file1513 os.unlink(file)1514 shutil.move(outFileName,file)1515except:1516# cleanup our temporary file1517 os.unlink(outFileName)1518print"Failed to strip RCS keywords in%s"%file1519raise15201521print"Patched up RCS keywords in%s"%file15221523defp4UserForCommit(self,id):1524# Return the tuple (perforce user,git email) for a given git commit id1525 self.getUserMapFromPerforceServer()1526 gitEmail =read_pipe(["git","log","--max-count=1",1527"--format=%ae",id])1528 gitEmail = gitEmail.strip()1529if not self.emails.has_key(gitEmail):1530return(None,gitEmail)1531else:1532return(self.emails[gitEmail],gitEmail)15331534defcheckValidP4Users(self,commits):1535# check if any git authors cannot be mapped to p4 users1536foridin commits:1537(user,email) = self.p4UserForCommit(id)1538if not user:1539 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1540ifgitConfigBool("git-p4.allowMissingP4Users"):1541print"%s"% msg1542else:1543die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)15441545deflastP4Changelist(self):1546# Get back the last changelist number submitted in this client spec. This1547# then gets used to patch up the username in the change. If the same1548# client spec is being used by multiple processes then this might go1549# wrong.1550 results =p4CmdList("client -o")# find the current client1551 client =None1552for r in results:1553if r.has_key('Client'):1554 client = r['Client']1555break1556if not client:1557die("could not get client spec")1558 results =p4CmdList(["changes","-c", client,"-m","1"])1559for r in results:1560if r.has_key('change'):1561return r['change']1562die("Could not get changelist number for last submit - cannot patch up user details")15631564defmodifyChangelistUser(self, changelist, newUser):1565# fixup the user field of a changelist after it has been submitted.1566 changes =p4CmdList("change -o%s"% changelist)1567iflen(changes) !=1:1568die("Bad output from p4 change modifying%sto user%s"%1569(changelist, newUser))15701571 c = changes[0]1572if c['User'] == newUser:return# nothing to do1573 c['User'] = newUser1574input= marshal.dumps(c)15751576 result =p4CmdList("change -f -i", stdin=input)1577for r in result:1578if r.has_key('code'):1579if r['code'] =='error':1580die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1581if r.has_key('data'):1582print("Updated user field for changelist%sto%s"% (changelist, newUser))1583return1584die("Could not modify user field of changelist%sto%s"% (changelist, newUser))15851586defcanChangeChangelists(self):1587# check to see if we have p4 admin or super-user permissions, either of1588# which are required to modify changelists.1589 results =p4CmdList(["protects", self.depotPath])1590for r in results:1591if r.has_key('perm'):1592if r['perm'] =='admin':1593return11594if r['perm'] =='super':1595return11596return015971598defprepareSubmitTemplate(self, changelist=None):1599"""Run "p4 change -o" to grab a change specification template.1600 This does not use "p4 -G", as it is nice to keep the submission1601 template in original order, since a human might edit it.16021603 Remove lines in the Files section that show changes to files1604 outside the depot path we're committing into."""16051606[upstream, settings] =findUpstreamBranchPoint()16071608 template ="""\1609# A Perforce Change Specification.1610#1611# Change: The change number. 'new' on a new changelist.1612# Date: The date this specification was last modified.1613# Client: The client on which the changelist was created. Read-only.1614# User: The user who created the changelist.1615# Status: Either 'pending' or 'submitted'. Read-only.1616# Type: Either 'public' or 'restricted'. Default is 'public'.1617# Description: Comments about the changelist. Required.1618# Jobs: What opened jobs are to be closed by this changelist.1619# You may delete jobs from this list. (New changelists only.)1620# Files: What opened files from the default changelist are to be added1621# to this changelist. You may delete files from this list.1622# (New changelists only.)1623"""1624 files_list = []1625 inFilesSection =False1626 change_entry =None1627 args = ['change','-o']1628if changelist:1629 args.append(str(changelist))1630for entry inp4CmdList(args):1631if not entry.has_key('code'):1632continue1633if entry['code'] =='stat':1634 change_entry = entry1635break1636if not change_entry:1637die('Failed to decode output of p4 change -o')1638for key, value in change_entry.iteritems():1639if key.startswith('File'):1640if settings.has_key('depot-paths'):1641if not[p for p in settings['depot-paths']1642ifp4PathStartsWith(value, p)]:1643continue1644else:1645if notp4PathStartsWith(value, self.depotPath):1646continue1647 files_list.append(value)1648continue1649# Output in the order expected by prepareLogMessage1650for key in['Change','Client','User','Status','Description','Jobs']:1651if not change_entry.has_key(key):1652continue1653 template +='\n'1654 template += key +':'1655if key =='Description':1656 template +='\n'1657for field_line in change_entry[key].splitlines():1658 template +='\t'+field_line+'\n'1659iflen(files_list) >0:1660 template +='\n'1661 template +='Files:\n'1662for path in files_list:1663 template +='\t'+path+'\n'1664return template16651666defedit_template(self, template_file):1667"""Invoke the editor to let the user change the submission1668 message. Return true if okay to continue with the submit."""16691670# if configured to skip the editing part, just submit1671ifgitConfigBool("git-p4.skipSubmitEdit"):1672return True16731674# look at the modification time, to check later if the user saved1675# the file1676 mtime = os.stat(template_file).st_mtime16771678# invoke the editor1679if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1680 editor = os.environ.get("P4EDITOR")1681else:1682 editor =read_pipe("git var GIT_EDITOR").strip()1683system(["sh","-c", ('%s"$@"'% editor), editor, template_file])16841685# If the file was not saved, prompt to see if this patch should1686# be skipped. But skip this verification step if configured so.1687ifgitConfigBool("git-p4.skipSubmitEditCheck"):1688return True16891690# modification time updated means user saved the file1691if os.stat(template_file).st_mtime > mtime:1692return True16931694while True:1695 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1696if response =='y':1697return True1698if response =='n':1699return False17001701defget_diff_description(self, editedFiles, filesToAdd, symlinks):1702# diff1703if os.environ.has_key("P4DIFF"):1704del(os.environ["P4DIFF"])1705 diff =""1706for editedFile in editedFiles:1707 diff +=p4_read_pipe(['diff','-du',1708wildcard_encode(editedFile)])17091710# new file diff1711 newdiff =""1712for newFile in filesToAdd:1713 newdiff +="==== new file ====\n"1714 newdiff +="--- /dev/null\n"1715 newdiff +="+++%s\n"% newFile17161717 is_link = os.path.islink(newFile)1718 expect_link = newFile in symlinks17191720if is_link and expect_link:1721 newdiff +="+%s\n"% os.readlink(newFile)1722else:1723 f =open(newFile,"r")1724for line in f.readlines():1725 newdiff +="+"+ line1726 f.close()17271728return(diff + newdiff).replace('\r\n','\n')17291730defapplyCommit(self,id):1731"""Apply one commit, return True if it succeeded."""17321733print"Applying",read_pipe(["git","show","-s",1734"--format=format:%h%s",id])17351736(p4User, gitEmail) = self.p4UserForCommit(id)17371738 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1739 filesToAdd =set()1740 filesToChangeType =set()1741 filesToDelete =set()1742 editedFiles =set()1743 pureRenameCopy =set()1744 symlinks =set()1745 filesToChangeExecBit = {}1746 all_files =list()17471748for line in diff:1749 diff =parseDiffTreeEntry(line)1750 modifier = diff['status']1751 path = diff['src']1752 all_files.append(path)17531754if modifier =="M":1755p4_edit(path)1756ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1757 filesToChangeExecBit[path] = diff['dst_mode']1758 editedFiles.add(path)1759elif modifier =="A":1760 filesToAdd.add(path)1761 filesToChangeExecBit[path] = diff['dst_mode']1762if path in filesToDelete:1763 filesToDelete.remove(path)17641765 dst_mode =int(diff['dst_mode'],8)1766if dst_mode ==0120000:1767 symlinks.add(path)17681769elif modifier =="D":1770 filesToDelete.add(path)1771if path in filesToAdd:1772 filesToAdd.remove(path)1773elif modifier =="C":1774 src, dest = diff['src'], diff['dst']1775p4_integrate(src, dest)1776 pureRenameCopy.add(dest)1777if diff['src_sha1'] != diff['dst_sha1']:1778p4_edit(dest)1779 pureRenameCopy.discard(dest)1780ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1781p4_edit(dest)1782 pureRenameCopy.discard(dest)1783 filesToChangeExecBit[dest] = diff['dst_mode']1784if self.isWindows:1785# turn off read-only attribute1786 os.chmod(dest, stat.S_IWRITE)1787 os.unlink(dest)1788 editedFiles.add(dest)1789elif modifier =="R":1790 src, dest = diff['src'], diff['dst']1791if self.p4HasMoveCommand:1792p4_edit(src)# src must be open before move1793p4_move(src, dest)# opens for (move/delete, move/add)1794else:1795p4_integrate(src, dest)1796if diff['src_sha1'] != diff['dst_sha1']:1797p4_edit(dest)1798else:1799 pureRenameCopy.add(dest)1800ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1801if not self.p4HasMoveCommand:1802p4_edit(dest)# with move: already open, writable1803 filesToChangeExecBit[dest] = diff['dst_mode']1804if not self.p4HasMoveCommand:1805if self.isWindows:1806 os.chmod(dest, stat.S_IWRITE)1807 os.unlink(dest)1808 filesToDelete.add(src)1809 editedFiles.add(dest)1810elif modifier =="T":1811 filesToChangeType.add(path)1812else:1813die("unknown modifier%sfor%s"% (modifier, path))18141815 diffcmd ="git diff-tree --full-index -p\"%s\""% (id)1816 patchcmd = diffcmd +" | git apply "1817 tryPatchCmd = patchcmd +"--check -"1818 applyPatchCmd = patchcmd +"--check --apply -"1819 patch_succeeded =True18201821if os.system(tryPatchCmd) !=0:1822 fixed_rcs_keywords =False1823 patch_succeeded =False1824print"Unfortunately applying the change failed!"18251826# Patch failed, maybe it's just RCS keyword woes. Look through1827# the patch to see if that's possible.1828ifgitConfigBool("git-p4.attemptRCSCleanup"):1829file=None1830 pattern =None1831 kwfiles = {}1832forfilein editedFiles | filesToDelete:1833# did this file's delta contain RCS keywords?1834 pattern =p4_keywords_regexp_for_file(file)18351836if pattern:1837# this file is a possibility...look for RCS keywords.1838 regexp = re.compile(pattern, re.VERBOSE)1839for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1840if regexp.search(line):1841if verbose:1842print"got keyword match on%sin%sin%s"% (pattern, line,file)1843 kwfiles[file] = pattern1844break18451846forfilein kwfiles:1847if verbose:1848print"zapping%swith%s"% (line,pattern)1849# File is being deleted, so not open in p4. Must1850# disable the read-only bit on windows.1851if self.isWindows andfilenot in editedFiles:1852 os.chmod(file, stat.S_IWRITE)1853 self.patchRCSKeywords(file, kwfiles[file])1854 fixed_rcs_keywords =True18551856if fixed_rcs_keywords:1857print"Retrying the patch with RCS keywords cleaned up"1858if os.system(tryPatchCmd) ==0:1859 patch_succeeded =True18601861if not patch_succeeded:1862for f in editedFiles:1863p4_revert(f)1864return False18651866#1867# Apply the patch for real, and do add/delete/+x handling.1868#1869system(applyPatchCmd)18701871for f in filesToChangeType:1872p4_edit(f,"-t","auto")1873for f in filesToAdd:1874p4_add(f)1875for f in filesToDelete:1876p4_revert(f)1877p4_delete(f)18781879# Set/clear executable bits1880for f in filesToChangeExecBit.keys():1881 mode = filesToChangeExecBit[f]1882setP4ExecBit(f, mode)18831884 update_shelve =01885iflen(self.update_shelve) >0:1886 update_shelve = self.update_shelve.pop(0)1887p4_reopen_in_change(update_shelve, all_files)18881889#1890# Build p4 change description, starting with the contents1891# of the git commit message.1892#1893 logMessage =extractLogMessageFromGitCommit(id)1894 logMessage = logMessage.strip()1895(logMessage, jobs) = self.separate_jobs_from_description(logMessage)18961897 template = self.prepareSubmitTemplate(update_shelve)1898 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)18991900if self.preserveUser:1901 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User19021903if self.checkAuthorship and not self.p4UserIsMe(p4User):1904 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1905 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1906 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"19071908 separatorLine ="######## everything below this line is just the diff #######\n"1909if not self.prepare_p4_only:1910 submitTemplate += separatorLine1911 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)19121913(handle, fileName) = tempfile.mkstemp()1914 tmpFile = os.fdopen(handle,"w+b")1915if self.isWindows:1916 submitTemplate = submitTemplate.replace("\n","\r\n")1917 tmpFile.write(submitTemplate)1918 tmpFile.close()19191920if self.prepare_p4_only:1921#1922# Leave the p4 tree prepared, and the submit template around1923# and let the user decide what to do next1924#1925print1926print"P4 workspace prepared for submission."1927print"To submit or revert, go to client workspace"1928print" "+ self.clientPath1929print1930print"To submit, use\"p4 submit\"to write a new description,"1931print"or\"p4 submit -i <%s\"to use the one prepared by" \1932"\"git p4\"."% fileName1933print"You can delete the file\"%s\"when finished."% fileName19341935if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1936print"To preserve change ownership by user%s, you must\n" \1937"do\"p4 change -f <change>\"after submitting and\n" \1938"edit the User field."1939if pureRenameCopy:1940print"After submitting, renamed files must be re-synced."1941print"Invoke\"p4 sync -f\"on each of these files:"1942for f in pureRenameCopy:1943print" "+ f19441945print1946print"To revert the changes, use\"p4 revert ...\", and delete"1947print"the submit template file\"%s\""% fileName1948if filesToAdd:1949print"Since the commit adds new files, they must be deleted:"1950for f in filesToAdd:1951print" "+ f1952print1953return True19541955#1956# Let the user edit the change description, then submit it.1957#1958 submitted =False19591960try:1961if self.edit_template(fileName):1962# read the edited message and submit1963 tmpFile =open(fileName,"rb")1964 message = tmpFile.read()1965 tmpFile.close()1966if self.isWindows:1967 message = message.replace("\r\n","\n")1968 submitTemplate = message[:message.index(separatorLine)]19691970if update_shelve:1971p4_write_pipe(['shelve','-r','-i'], submitTemplate)1972elif self.shelve:1973p4_write_pipe(['shelve','-i'], submitTemplate)1974else:1975p4_write_pipe(['submit','-i'], submitTemplate)1976# The rename/copy happened by applying a patch that created a1977# new file. This leaves it writable, which confuses p4.1978for f in pureRenameCopy:1979p4_sync(f,"-f")19801981if self.preserveUser:1982if p4User:1983# Get last changelist number. Cannot easily get it from1984# the submit command output as the output is1985# unmarshalled.1986 changelist = self.lastP4Changelist()1987 self.modifyChangelistUser(changelist, p4User)19881989 submitted =True19901991finally:1992# skip this patch1993if not submitted or self.shelve:1994if self.shelve:1995print("Reverting shelved files.")1996else:1997print("Submission cancelled, undoing p4 changes.")1998for f in editedFiles | filesToDelete:1999p4_revert(f)2000for f in filesToAdd:2001p4_revert(f)2002 os.remove(f)20032004 os.remove(fileName)2005return submitted20062007# Export git tags as p4 labels. Create a p4 label and then tag2008# with that.2009defexportGitTags(self, gitTags):2010 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")2011iflen(validLabelRegexp) ==0:2012 validLabelRegexp = defaultLabelRegexp2013 m = re.compile(validLabelRegexp)20142015for name in gitTags:20162017if not m.match(name):2018if verbose:2019print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)2020continue20212022# Get the p4 commit this corresponds to2023 logMessage =extractLogMessageFromGitCommit(name)2024 values =extractSettingsGitLog(logMessage)20252026if not values.has_key('change'):2027# a tag pointing to something not sent to p4; ignore2028if verbose:2029print"git tag%sdoes not give a p4 commit"% name2030continue2031else:2032 changelist = values['change']20332034# Get the tag details.2035 inHeader =True2036 isAnnotated =False2037 body = []2038for l inread_pipe_lines(["git","cat-file","-p", name]):2039 l = l.strip()2040if inHeader:2041if re.match(r'tag\s+', l):2042 isAnnotated =True2043elif re.match(r'\s*$', l):2044 inHeader =False2045continue2046else:2047 body.append(l)20482049if not isAnnotated:2050 body = ["lightweight tag imported by git p4\n"]20512052# Create the label - use the same view as the client spec we are using2053 clientSpec =getClientSpec()20542055 labelTemplate ="Label:%s\n"% name2056 labelTemplate +="Description:\n"2057for b in body:2058 labelTemplate +="\t"+ b +"\n"2059 labelTemplate +="View:\n"2060for depot_side in clientSpec.mappings:2061 labelTemplate +="\t%s\n"% depot_side20622063if self.dry_run:2064print"Would create p4 label%sfor tag"% name2065elif self.prepare_p4_only:2066print"Not creating p4 label%sfor tag due to option" \2067" --prepare-p4-only"% name2068else:2069p4_write_pipe(["label","-i"], labelTemplate)20702071# Use the label2072p4_system(["tag","-l", name] +2073["%s@%s"% (depot_side, changelist)for depot_side in clientSpec.mappings])20742075if verbose:2076print"created p4 label for tag%s"% name20772078defrun(self, args):2079iflen(args) ==0:2080 self.master =currentGitBranch()2081eliflen(args) ==1:2082 self.master = args[0]2083if notbranchExists(self.master):2084die("Branch%sdoes not exist"% self.master)2085else:2086return False20872088for i in self.update_shelve:2089if i <=0:2090 sys.exit("invalid changelist%d"% i)20912092if self.master:2093 allowSubmit =gitConfig("git-p4.allowSubmit")2094iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):2095die("%sis not in git-p4.allowSubmit"% self.master)20962097[upstream, settings] =findUpstreamBranchPoint()2098 self.depotPath = settings['depot-paths'][0]2099iflen(self.origin) ==0:2100 self.origin = upstream21012102iflen(self.update_shelve) >0:2103 self.shelve =True21042105if self.preserveUser:2106if not self.canChangeChangelists():2107die("Cannot preserve user names without p4 super-user or admin permissions")21082109# if not set from the command line, try the config file2110if self.conflict_behavior is None:2111 val =gitConfig("git-p4.conflict")2112if val:2113if val not in self.conflict_behavior_choices:2114die("Invalid value '%s' for config git-p4.conflict"% val)2115else:2116 val ="ask"2117 self.conflict_behavior = val21182119if self.verbose:2120print"Origin branch is "+ self.origin21212122iflen(self.depotPath) ==0:2123print"Internal error: cannot locate perforce depot path from existing branches"2124 sys.exit(128)21252126 self.useClientSpec =False2127ifgitConfigBool("git-p4.useclientspec"):2128 self.useClientSpec =True2129if self.useClientSpec:2130 self.clientSpecDirs =getClientSpec()21312132# Check for the existence of P4 branches2133 branchesDetected = (len(p4BranchesInGit().keys()) >1)21342135if self.useClientSpec and not branchesDetected:2136# all files are relative to the client spec2137 self.clientPath =getClientRoot()2138else:2139 self.clientPath =p4Where(self.depotPath)21402141if self.clientPath =="":2142die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)21432144print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)2145 self.oldWorkingDirectory = os.getcwd()21462147# ensure the clientPath exists2148 new_client_dir =False2149if not os.path.exists(self.clientPath):2150 new_client_dir =True2151 os.makedirs(self.clientPath)21522153chdir(self.clientPath, is_client_path=True)2154if self.dry_run:2155print"Would synchronize p4 checkout in%s"% self.clientPath2156else:2157print"Synchronizing p4 checkout..."2158if new_client_dir:2159# old one was destroyed, and maybe nobody told p42160p4_sync("...","-f")2161else:2162p4_sync("...")2163 self.check()21642165 commits = []2166if self.master:2167 commitish = self.master2168else:2169 commitish ='HEAD'21702171if self.commit !="":2172if self.commit.find("..") != -1:2173 limits_ish = self.commit.split("..")2174for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (limits_ish[0], limits_ish[1])]):2175 commits.append(line.strip())2176 commits.reverse()2177else:2178 commits.append(self.commit)2179else:2180for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, commitish)]):2181 commits.append(line.strip())2182 commits.reverse()21832184if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):2185 self.checkAuthorship =False2186else:2187 self.checkAuthorship =True21882189if self.preserveUser:2190 self.checkValidP4Users(commits)21912192#2193# Build up a set of options to be passed to diff when2194# submitting each commit to p4.2195#2196if self.detectRenames:2197# command-line -M arg2198 self.diffOpts ="-M"2199else:2200# If not explicitly set check the config variable2201 detectRenames =gitConfig("git-p4.detectRenames")22022203if detectRenames.lower() =="false"or detectRenames =="":2204 self.diffOpts =""2205elif detectRenames.lower() =="true":2206 self.diffOpts ="-M"2207else:2208 self.diffOpts ="-M%s"% detectRenames22092210# no command-line arg for -C or --find-copies-harder, just2211# config variables2212 detectCopies =gitConfig("git-p4.detectCopies")2213if detectCopies.lower() =="false"or detectCopies =="":2214pass2215elif detectCopies.lower() =="true":2216 self.diffOpts +=" -C"2217else:2218 self.diffOpts +=" -C%s"% detectCopies22192220ifgitConfigBool("git-p4.detectCopiesHarder"):2221 self.diffOpts +=" --find-copies-harder"22222223 num_shelves =len(self.update_shelve)2224if num_shelves >0and num_shelves !=len(commits):2225 sys.exit("number of commits (%d) must match number of shelved changelist (%d)"%2226(len(commits), num_shelves))22272228#2229# Apply the commits, one at a time. On failure, ask if should2230# continue to try the rest of the patches, or quit.2231#2232if self.dry_run:2233print"Would apply"2234 applied = []2235 last =len(commits) -12236for i, commit inenumerate(commits):2237if self.dry_run:2238print" ",read_pipe(["git","show","-s",2239"--format=format:%h%s", commit])2240 ok =True2241else:2242 ok = self.applyCommit(commit)2243if ok:2244 applied.append(commit)2245else:2246if self.prepare_p4_only and i < last:2247print"Processing only the first commit due to option" \2248" --prepare-p4-only"2249break2250if i < last:2251 quit =False2252while True:2253# prompt for what to do, or use the option/variable2254if self.conflict_behavior =="ask":2255print"What do you want to do?"2256 response =raw_input("[s]kip this commit but apply"2257" the rest, or [q]uit? ")2258if not response:2259continue2260elif self.conflict_behavior =="skip":2261 response ="s"2262elif self.conflict_behavior =="quit":2263 response ="q"2264else:2265die("Unknown conflict_behavior '%s'"%2266 self.conflict_behavior)22672268if response[0] =="s":2269print"Skipping this commit, but applying the rest"2270break2271if response[0] =="q":2272print"Quitting"2273 quit =True2274break2275if quit:2276break22772278chdir(self.oldWorkingDirectory)2279 shelved_applied ="shelved"if self.shelve else"applied"2280if self.dry_run:2281pass2282elif self.prepare_p4_only:2283pass2284eliflen(commits) ==len(applied):2285print("All commits{0}!".format(shelved_applied))22862287 sync =P4Sync()2288if self.branch:2289 sync.branch = self.branch2290if self.disable_p4sync:2291 sync.sync_origin_only()2292else:2293 sync.run([])22942295if not self.disable_rebase:2296 rebase =P4Rebase()2297 rebase.rebase()22982299else:2300iflen(applied) ==0:2301print("No commits{0}.".format(shelved_applied))2302else:2303print("{0}only the commits marked with '*':".format(shelved_applied.capitalize()))2304for c in commits:2305if c in applied:2306 star ="*"2307else:2308 star =" "2309print star,read_pipe(["git","show","-s",2310"--format=format:%h%s", c])2311print"You will have to do 'git p4 sync' and rebase."23122313ifgitConfigBool("git-p4.exportLabels"):2314 self.exportLabels =True23152316if self.exportLabels:2317 p4Labels =getP4Labels(self.depotPath)2318 gitTags =getGitTags()23192320 missingGitTags = gitTags - p4Labels2321 self.exportGitTags(missingGitTags)23222323# exit with error unless everything applied perfectly2324iflen(commits) !=len(applied):2325 sys.exit(1)23262327return True23282329classView(object):2330"""Represent a p4 view ("p4 help views"), and map files in a2331 repo according to the view."""23322333def__init__(self, client_name):2334 self.mappings = []2335 self.client_prefix ="//%s/"% client_name2336# cache results of "p4 where" to lookup client file locations2337 self.client_spec_path_cache = {}23382339defappend(self, view_line):2340"""Parse a view line, splitting it into depot and client2341 sides. Append to self.mappings, preserving order. This2342 is only needed for tag creation."""23432344# Split the view line into exactly two words. P4 enforces2345# structure on these lines that simplifies this quite a bit.2346#2347# Either or both words may be double-quoted.2348# Single quotes do not matter.2349# Double-quote marks cannot occur inside the words.2350# A + or - prefix is also inside the quotes.2351# There are no quotes unless they contain a space.2352# The line is already white-space stripped.2353# The two words are separated by a single space.2354#2355if view_line[0] =='"':2356# First word is double quoted. Find its end.2357 close_quote_index = view_line.find('"',1)2358if close_quote_index <=0:2359die("No first-word closing quote found:%s"% view_line)2360 depot_side = view_line[1:close_quote_index]2361# skip closing quote and space2362 rhs_index = close_quote_index +1+12363else:2364 space_index = view_line.find(" ")2365if space_index <=0:2366die("No word-splitting space found:%s"% view_line)2367 depot_side = view_line[0:space_index]2368 rhs_index = space_index +123692370# prefix + means overlay on previous mapping2371if depot_side.startswith("+"):2372 depot_side = depot_side[1:]23732374# prefix - means exclude this path, leave out of mappings2375 exclude =False2376if depot_side.startswith("-"):2377 exclude =True2378 depot_side = depot_side[1:]23792380if not exclude:2381 self.mappings.append(depot_side)23822383defconvert_client_path(self, clientFile):2384# chop off //client/ part to make it relative2385if not clientFile.startswith(self.client_prefix):2386die("No prefix '%s' on clientFile '%s'"%2387(self.client_prefix, clientFile))2388return clientFile[len(self.client_prefix):]23892390defupdate_client_spec_path_cache(self, files):2391""" Caching file paths by "p4 where" batch query """23922393# List depot file paths exclude that already cached2394 fileArgs = [f['path']for f in files if f['path']not in self.client_spec_path_cache]23952396iflen(fileArgs) ==0:2397return# All files in cache23982399 where_result =p4CmdList(["-x","-","where"], stdin=fileArgs)2400for res in where_result:2401if"code"in res and res["code"] =="error":2402# assume error is "... file(s) not in client view"2403continue2404if"clientFile"not in res:2405die("No clientFile in 'p4 where' output")2406if"unmap"in res:2407# it will list all of them, but only one not unmap-ped2408continue2409ifgitConfigBool("core.ignorecase"):2410 res['depotFile'] = res['depotFile'].lower()2411 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])24122413# not found files or unmap files set to ""2414for depotFile in fileArgs:2415ifgitConfigBool("core.ignorecase"):2416 depotFile = depotFile.lower()2417if depotFile not in self.client_spec_path_cache:2418 self.client_spec_path_cache[depotFile] =""24192420defmap_in_client(self, depot_path):2421"""Return the relative location in the client where this2422 depot file should live. Returns "" if the file should2423 not be mapped in the client."""24242425ifgitConfigBool("core.ignorecase"):2426 depot_path = depot_path.lower()24272428if depot_path in self.client_spec_path_cache:2429return self.client_spec_path_cache[depot_path]24302431die("Error:%sis not found in client spec path"% depot_path )2432return""24332434classP4Sync(Command, P4UserMap):2435 delete_actions = ("delete","move/delete","purge")24362437def__init__(self):2438 Command.__init__(self)2439 P4UserMap.__init__(self)2440 self.options = [2441 optparse.make_option("--branch", dest="branch"),2442 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2443 optparse.make_option("--changesfile", dest="changesFile"),2444 optparse.make_option("--silent", dest="silent", action="store_true"),2445 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2446 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2447 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2448help="Import into refs/heads/ , not refs/remotes"),2449 optparse.make_option("--max-changes", dest="maxChanges",2450help="Maximum number of changes to import"),2451 optparse.make_option("--changes-block-size", dest="changes_block_size",type="int",2452help="Internal block size to use when iteratively calling p4 changes"),2453 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2454help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2455 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2456help="Only sync files that are included in the Perforce Client Spec"),2457 optparse.make_option("-/", dest="cloneExclude",2458 action="append",type="string",2459help="exclude depot path"),2460]2461 self.description ="""Imports from Perforce into a git repository.\n2462 example:2463 //depot/my/project/ -- to import the current head2464 //depot/my/project/@all -- to import everything2465 //depot/my/project/@1,6 -- to import only from revision 1 to 624662467 (a ... is not needed in the path p4 specification, it's added implicitly)"""24682469 self.usage +=" //depot/path[@revRange]"2470 self.silent =False2471 self.createdBranches =set()2472 self.committedChanges =set()2473 self.branch =""2474 self.detectBranches =False2475 self.detectLabels =False2476 self.importLabels =False2477 self.changesFile =""2478 self.syncWithOrigin =True2479 self.importIntoRemotes =True2480 self.maxChanges =""2481 self.changes_block_size =None2482 self.keepRepoPath =False2483 self.depotPaths =None2484 self.p4BranchesInGit = []2485 self.cloneExclude = []2486 self.useClientSpec =False2487 self.useClientSpec_from_options =False2488 self.clientSpecDirs =None2489 self.tempBranches = []2490 self.tempBranchLocation ="refs/git-p4-tmp"2491 self.largeFileSystem =None24922493ifgitConfig('git-p4.largeFileSystem'):2494 largeFileSystemConstructor =globals()[gitConfig('git-p4.largeFileSystem')]2495 self.largeFileSystem =largeFileSystemConstructor(2496lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)2497)24982499ifgitConfig("git-p4.syncFromOrigin") =="false":2500 self.syncWithOrigin =False25012502# Force a checkpoint in fast-import and wait for it to finish2503defcheckpoint(self):2504 self.gitStream.write("checkpoint\n\n")2505 self.gitStream.write("progress checkpoint\n\n")2506 out = self.gitOutput.readline()2507if self.verbose:2508print"checkpoint finished: "+ out25092510defextractFilesFromCommit(self, commit):2511 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2512for path in self.cloneExclude]2513 files = []2514 fnum =02515while commit.has_key("depotFile%s"% fnum):2516 path = commit["depotFile%s"% fnum]25172518if[p for p in self.cloneExclude2519ifp4PathStartsWith(path, p)]:2520 found =False2521else:2522 found = [p for p in self.depotPaths2523ifp4PathStartsWith(path, p)]2524if not found:2525 fnum = fnum +12526continue25272528file= {}2529file["path"] = path2530file["rev"] = commit["rev%s"% fnum]2531file["action"] = commit["action%s"% fnum]2532file["type"] = commit["type%s"% fnum]2533 files.append(file)2534 fnum = fnum +12535return files25362537defextractJobsFromCommit(self, commit):2538 jobs = []2539 jnum =02540while commit.has_key("job%s"% jnum):2541 job = commit["job%s"% jnum]2542 jobs.append(job)2543 jnum = jnum +12544return jobs25452546defstripRepoPath(self, path, prefixes):2547"""When streaming files, this is called to map a p4 depot path2548 to where it should go in git. The prefixes are either2549 self.depotPaths, or self.branchPrefixes in the case of2550 branch detection."""25512552if self.useClientSpec:2553# branch detection moves files up a level (the branch name)2554# from what client spec interpretation gives2555 path = self.clientSpecDirs.map_in_client(path)2556if self.detectBranches:2557for b in self.knownBranches:2558if path.startswith(b +"/"):2559 path = path[len(b)+1:]25602561elif self.keepRepoPath:2562# Preserve everything in relative path name except leading2563# //depot/; just look at first prefix as they all should2564# be in the same depot.2565 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2566ifp4PathStartsWith(path, depot):2567 path = path[len(depot):]25682569else:2570for p in prefixes:2571ifp4PathStartsWith(path, p):2572 path = path[len(p):]2573break25742575 path =wildcard_decode(path)2576return path25772578defsplitFilesIntoBranches(self, commit):2579"""Look at each depotFile in the commit to figure out to what2580 branch it belongs."""25812582if self.clientSpecDirs:2583 files = self.extractFilesFromCommit(commit)2584 self.clientSpecDirs.update_client_spec_path_cache(files)25852586 branches = {}2587 fnum =02588while commit.has_key("depotFile%s"% fnum):2589 path = commit["depotFile%s"% fnum]2590 found = [p for p in self.depotPaths2591ifp4PathStartsWith(path, p)]2592if not found:2593 fnum = fnum +12594continue25952596file= {}2597file["path"] = path2598file["rev"] = commit["rev%s"% fnum]2599file["action"] = commit["action%s"% fnum]2600file["type"] = commit["type%s"% fnum]2601 fnum = fnum +126022603# start with the full relative path where this file would2604# go in a p4 client2605if self.useClientSpec:2606 relPath = self.clientSpecDirs.map_in_client(path)2607else:2608 relPath = self.stripRepoPath(path, self.depotPaths)26092610for branch in self.knownBranches.keys():2611# add a trailing slash so that a commit into qt/4.2foo2612# doesn't end up in qt/4.2, e.g.2613if relPath.startswith(branch +"/"):2614if branch not in branches:2615 branches[branch] = []2616 branches[branch].append(file)2617break26182619return branches26202621defwriteToGitStream(self, gitMode, relPath, contents):2622 self.gitStream.write('M%sinline%s\n'% (gitMode, relPath))2623 self.gitStream.write('data%d\n'%sum(len(d)for d in contents))2624for d in contents:2625 self.gitStream.write(d)2626 self.gitStream.write('\n')26272628defencodeWithUTF8(self, path):2629try:2630 path.decode('ascii')2631except:2632 encoding ='utf8'2633ifgitConfig('git-p4.pathEncoding'):2634 encoding =gitConfig('git-p4.pathEncoding')2635 path = path.decode(encoding,'replace').encode('utf8','replace')2636if self.verbose:2637print'Path with non-ASCII characters detected. Used%sto encode:%s'% (encoding, path)2638return path26392640# output one file from the P4 stream2641# - helper for streamP4Files26422643defstreamOneP4File(self,file, contents):2644 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2645 relPath = self.encodeWithUTF8(relPath)2646if verbose:2647 size =int(self.stream_file['fileSize'])2648 sys.stdout.write('\r%s-->%s(%iMB)\n'% (file['depotFile'], relPath, size/1024/1024))2649 sys.stdout.flush()26502651(type_base, type_mods) =split_p4_type(file["type"])26522653 git_mode ="100644"2654if"x"in type_mods:2655 git_mode ="100755"2656if type_base =="symlink":2657 git_mode ="120000"2658# p4 print on a symlink sometimes contains "target\n";2659# if it does, remove the newline2660 data =''.join(contents)2661if not data:2662# Some version of p4 allowed creating a symlink that pointed2663# to nothing. This causes p4 errors when checking out such2664# a change, and errors here too. Work around it by ignoring2665# the bad symlink; hopefully a future change fixes it.2666print"\nIgnoring empty symlink in%s"%file['depotFile']2667return2668elif data[-1] =='\n':2669 contents = [data[:-1]]2670else:2671 contents = [data]26722673if type_base =="utf16":2674# p4 delivers different text in the python output to -G2675# than it does when using "print -o", or normal p4 client2676# operations. utf16 is converted to ascii or utf8, perhaps.2677# But ascii text saved as -t utf16 is completely mangled.2678# Invoke print -o to get the real contents.2679#2680# On windows, the newlines will always be mangled by print, so put2681# them back too. This is not needed to the cygwin windows version,2682# just the native "NT" type.2683#2684try:2685 text =p4_read_pipe(['print','-q','-o','-','%s@%s'% (file['depotFile'],file['change'])])2686exceptExceptionas e:2687if'Translation of file content failed'instr(e):2688 type_base ='binary'2689else:2690raise e2691else:2692ifp4_version_string().find('/NT') >=0:2693 text = text.replace('\r\n','\n')2694 contents = [ text ]26952696if type_base =="apple":2697# Apple filetype files will be streamed as a concatenation of2698# its appledouble header and the contents. This is useless2699# on both macs and non-macs. If using "print -q -o xx", it2700# will create "xx" with the data, and "%xx" with the header.2701# This is also not very useful.2702#2703# Ideally, someday, this script can learn how to generate2704# appledouble files directly and import those to git, but2705# non-mac machines can never find a use for apple filetype.2706print"\nIgnoring apple filetype file%s"%file['depotFile']2707return27082709# Note that we do not try to de-mangle keywords on utf16 files,2710# even though in theory somebody may want that.2711 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2712if pattern:2713 regexp = re.compile(pattern, re.VERBOSE)2714 text =''.join(contents)2715 text = regexp.sub(r'$\1$', text)2716 contents = [ text ]27172718if self.largeFileSystem:2719(git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)27202721 self.writeToGitStream(git_mode, relPath, contents)27222723defstreamOneP4Deletion(self,file):2724 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2725 relPath = self.encodeWithUTF8(relPath)2726if verbose:2727 sys.stdout.write("delete%s\n"% relPath)2728 sys.stdout.flush()2729 self.gitStream.write("D%s\n"% relPath)27302731if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):2732 self.largeFileSystem.removeLargeFile(relPath)27332734# handle another chunk of streaming data2735defstreamP4FilesCb(self, marshalled):27362737# catch p4 errors and complain2738 err =None2739if"code"in marshalled:2740if marshalled["code"] =="error":2741if"data"in marshalled:2742 err = marshalled["data"].rstrip()27432744if not err and'fileSize'in self.stream_file:2745 required_bytes =int((4*int(self.stream_file["fileSize"])) -calcDiskFree())2746if required_bytes >0:2747 err ='Not enough space left on%s! Free at least%iMB.'% (2748 os.getcwd(), required_bytes/1024/10242749)27502751if err:2752 f =None2753if self.stream_have_file_info:2754if"depotFile"in self.stream_file:2755 f = self.stream_file["depotFile"]2756# force a failure in fast-import, else an empty2757# commit will be made2758 self.gitStream.write("\n")2759 self.gitStream.write("die-now\n")2760 self.gitStream.close()2761# ignore errors, but make sure it exits first2762 self.importProcess.wait()2763if f:2764die("Error from p4 print for%s:%s"% (f, err))2765else:2766die("Error from p4 print:%s"% err)27672768if marshalled.has_key('depotFile')and self.stream_have_file_info:2769# start of a new file - output the old one first2770 self.streamOneP4File(self.stream_file, self.stream_contents)2771 self.stream_file = {}2772 self.stream_contents = []2773 self.stream_have_file_info =False27742775# pick up the new file information... for the2776# 'data' field we need to append to our array2777for k in marshalled.keys():2778if k =='data':2779if'streamContentSize'not in self.stream_file:2780 self.stream_file['streamContentSize'] =02781 self.stream_file['streamContentSize'] +=len(marshalled['data'])2782 self.stream_contents.append(marshalled['data'])2783else:2784 self.stream_file[k] = marshalled[k]27852786if(verbose and2787'streamContentSize'in self.stream_file and2788'fileSize'in self.stream_file and2789'depotFile'in self.stream_file):2790 size =int(self.stream_file["fileSize"])2791if size >0:2792 progress =100*self.stream_file['streamContentSize']/size2793 sys.stdout.write('\r%s %d%%(%iMB)'% (self.stream_file['depotFile'], progress,int(size/1024/1024)))2794 sys.stdout.flush()27952796 self.stream_have_file_info =True27972798# Stream directly from "p4 files" into "git fast-import"2799defstreamP4Files(self, files):2800 filesForCommit = []2801 filesToRead = []2802 filesToDelete = []28032804for f in files:2805 filesForCommit.append(f)2806if f['action']in self.delete_actions:2807 filesToDelete.append(f)2808else:2809 filesToRead.append(f)28102811# deleted files...2812for f in filesToDelete:2813 self.streamOneP4Deletion(f)28142815iflen(filesToRead) >0:2816 self.stream_file = {}2817 self.stream_contents = []2818 self.stream_have_file_info =False28192820# curry self argument2821defstreamP4FilesCbSelf(entry):2822 self.streamP4FilesCb(entry)28232824 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]28252826p4CmdList(["-x","-","print"],2827 stdin=fileArgs,2828 cb=streamP4FilesCbSelf)28292830# do the last chunk2831if self.stream_file.has_key('depotFile'):2832 self.streamOneP4File(self.stream_file, self.stream_contents)28332834defmake_email(self, userid):2835if userid in self.users:2836return self.users[userid]2837else:2838return"%s<a@b>"% userid28392840defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2841""" Stream a p4 tag.2842 commit is either a git commit, or a fast-import mark, ":<p4commit>"2843 """28442845if verbose:2846print"writing tag%sfor commit%s"% (labelName, commit)2847 gitStream.write("tag%s\n"% labelName)2848 gitStream.write("from%s\n"% commit)28492850if labelDetails.has_key('Owner'):2851 owner = labelDetails["Owner"]2852else:2853 owner =None28542855# Try to use the owner of the p4 label, or failing that,2856# the current p4 user id.2857if owner:2858 email = self.make_email(owner)2859else:2860 email = self.make_email(self.p4UserId())2861 tagger ="%s %s %s"% (email, epoch, self.tz)28622863 gitStream.write("tagger%s\n"% tagger)28642865print"labelDetails=",labelDetails2866if labelDetails.has_key('Description'):2867 description = labelDetails['Description']2868else:2869 description ='Label from git p4'28702871 gitStream.write("data%d\n"%len(description))2872 gitStream.write(description)2873 gitStream.write("\n")28742875definClientSpec(self, path):2876if not self.clientSpecDirs:2877return True2878 inClientSpec = self.clientSpecDirs.map_in_client(path)2879if not inClientSpec and self.verbose:2880print('Ignoring file outside of client spec:{0}'.format(path))2881return inClientSpec28822883defhasBranchPrefix(self, path):2884if not self.branchPrefixes:2885return True2886 hasPrefix = [p for p in self.branchPrefixes2887ifp4PathStartsWith(path, p)]2888if not hasPrefix and self.verbose:2889print('Ignoring file outside of prefix:{0}'.format(path))2890return hasPrefix28912892defcommit(self, details, files, branch, parent =""):2893 epoch = details["time"]2894 author = details["user"]2895 jobs = self.extractJobsFromCommit(details)28962897if self.verbose:2898print('commit into{0}'.format(branch))28992900if self.clientSpecDirs:2901 self.clientSpecDirs.update_client_spec_path_cache(files)29022903 files = [f for f in files2904if self.inClientSpec(f['path'])and self.hasBranchPrefix(f['path'])]29052906if not files and notgitConfigBool('git-p4.keepEmptyCommits'):2907print('Ignoring revision{0}as it would produce an empty commit.'2908.format(details['change']))2909return29102911 self.gitStream.write("commit%s\n"% branch)2912 self.gitStream.write("mark :%s\n"% details["change"])2913 self.committedChanges.add(int(details["change"]))2914 committer =""2915if author not in self.users:2916 self.getUserMapFromPerforceServer()2917 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)29182919 self.gitStream.write("committer%s\n"% committer)29202921 self.gitStream.write("data <<EOT\n")2922 self.gitStream.write(details["desc"])2923iflen(jobs) >0:2924 self.gitStream.write("\nJobs:%s"% (' '.join(jobs)))2925 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2926(','.join(self.branchPrefixes), details["change"]))2927iflen(details['options']) >0:2928 self.gitStream.write(": options =%s"% details['options'])2929 self.gitStream.write("]\nEOT\n\n")29302931iflen(parent) >0:2932if self.verbose:2933print"parent%s"% parent2934 self.gitStream.write("from%s\n"% parent)29352936 self.streamP4Files(files)2937 self.gitStream.write("\n")29382939 change =int(details["change"])29402941if self.labels.has_key(change):2942 label = self.labels[change]2943 labelDetails = label[0]2944 labelRevisions = label[1]2945if self.verbose:2946print"Change%sis labelled%s"% (change, labelDetails)29472948 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2949for p in self.branchPrefixes])29502951iflen(files) ==len(labelRevisions):29522953 cleanedFiles = {}2954for info in files:2955if info["action"]in self.delete_actions:2956continue2957 cleanedFiles[info["depotFile"]] = info["rev"]29582959if cleanedFiles == labelRevisions:2960 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)29612962else:2963if not self.silent:2964print("Tag%sdoes not match with change%s: files do not match."2965% (labelDetails["label"], change))29662967else:2968if not self.silent:2969print("Tag%sdoes not match with change%s: file count is different."2970% (labelDetails["label"], change))29712972# Build a dictionary of changelists and labels, for "detect-labels" option.2973defgetLabels(self):2974 self.labels = {}29752976 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2977iflen(l) >0and not self.silent:2978print"Finding files belonging to labels in%s"% `self.depotPaths`29792980for output in l:2981 label = output["label"]2982 revisions = {}2983 newestChange =02984if self.verbose:2985print"Querying files for label%s"% label2986forfileinp4CmdList(["files"] +2987["%s...@%s"% (p, label)2988for p in self.depotPaths]):2989 revisions[file["depotFile"]] =file["rev"]2990 change =int(file["change"])2991if change > newestChange:2992 newestChange = change29932994 self.labels[newestChange] = [output, revisions]29952996if self.verbose:2997print"Label changes:%s"% self.labels.keys()29982999# Import p4 labels as git tags. A direct mapping does not3000# exist, so assume that if all the files are at the same revision3001# then we can use that, or it's something more complicated we should3002# just ignore.3003defimportP4Labels(self, stream, p4Labels):3004if verbose:3005print"import p4 labels: "+' '.join(p4Labels)30063007 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")3008 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")3009iflen(validLabelRegexp) ==0:3010 validLabelRegexp = defaultLabelRegexp3011 m = re.compile(validLabelRegexp)30123013for name in p4Labels:3014 commitFound =False30153016if not m.match(name):3017if verbose:3018print"label%sdoes not match regexp%s"% (name,validLabelRegexp)3019continue30203021if name in ignoredP4Labels:3022continue30233024 labelDetails =p4CmdList(['label',"-o", name])[0]30253026# get the most recent changelist for each file in this label3027 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)3028for p in self.depotPaths])30293030if change.has_key('change'):3031# find the corresponding git commit; take the oldest commit3032 changelist =int(change['change'])3033if changelist in self.committedChanges:3034 gitCommit =":%d"% changelist # use a fast-import mark3035 commitFound =True3036else:3037 gitCommit =read_pipe(["git","rev-list","--max-count=1",3038"--reverse",":/\[git-p4:.*change =%d\]"% changelist], ignore_error=True)3039iflen(gitCommit) ==0:3040print"importing label%s: could not find git commit for changelist%d"% (name, changelist)3041else:3042 commitFound =True3043 gitCommit = gitCommit.strip()30443045if commitFound:3046# Convert from p4 time format3047try:3048 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")3049exceptValueError:3050print"Could not convert label time%s"% labelDetails['Update']3051 tmwhen =130523053 when =int(time.mktime(tmwhen))3054 self.streamTag(stream, name, labelDetails, gitCommit, when)3055if verbose:3056print"p4 label%smapped to git commit%s"% (name, gitCommit)3057else:3058if verbose:3059print"Label%shas no changelists - possibly deleted?"% name30603061if not commitFound:3062# We can't import this label; don't try again as it will get very3063# expensive repeatedly fetching all the files for labels that will3064# never be imported. If the label is moved in the future, the3065# ignore will need to be removed manually.3066system(["git","config","--add","git-p4.ignoredP4Labels", name])30673068defguessProjectName(self):3069for p in self.depotPaths:3070if p.endswith("/"):3071 p = p[:-1]3072 p = p[p.strip().rfind("/") +1:]3073if not p.endswith("/"):3074 p +="/"3075return p30763077defgetBranchMapping(self):3078 lostAndFoundBranches =set()30793080 user =gitConfig("git-p4.branchUser")3081iflen(user) >0:3082 command ="branches -u%s"% user3083else:3084 command ="branches"30853086for info inp4CmdList(command):3087 details =p4Cmd(["branch","-o", info["branch"]])3088 viewIdx =03089while details.has_key("View%s"% viewIdx):3090 paths = details["View%s"% viewIdx].split(" ")3091 viewIdx = viewIdx +13092# require standard //depot/foo/... //depot/bar/... mapping3093iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):3094continue3095 source = paths[0]3096 destination = paths[1]3097## HACK3098ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):3099 source = source[len(self.depotPaths[0]):-4]3100 destination = destination[len(self.depotPaths[0]):-4]31013102if destination in self.knownBranches:3103if not self.silent:3104print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)3105print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)3106continue31073108 self.knownBranches[destination] = source31093110 lostAndFoundBranches.discard(destination)31113112if source not in self.knownBranches:3113 lostAndFoundBranches.add(source)31143115# Perforce does not strictly require branches to be defined, so we also3116# check git config for a branch list.3117#3118# Example of branch definition in git config file:3119# [git-p4]3120# branchList=main:branchA3121# branchList=main:branchB3122# branchList=branchA:branchC3123 configBranches =gitConfigList("git-p4.branchList")3124for branch in configBranches:3125if branch:3126(source, destination) = branch.split(":")3127 self.knownBranches[destination] = source31283129 lostAndFoundBranches.discard(destination)31303131if source not in self.knownBranches:3132 lostAndFoundBranches.add(source)313331343135for branch in lostAndFoundBranches:3136 self.knownBranches[branch] = branch31373138defgetBranchMappingFromGitBranches(self):3139 branches =p4BranchesInGit(self.importIntoRemotes)3140for branch in branches.keys():3141if branch =="master":3142 branch ="main"3143else:3144 branch = branch[len(self.projectName):]3145 self.knownBranches[branch] = branch31463147defupdateOptionDict(self, d):3148 option_keys = {}3149if self.keepRepoPath:3150 option_keys['keepRepoPath'] =131513152 d["options"] =' '.join(sorted(option_keys.keys()))31533154defreadOptions(self, d):3155 self.keepRepoPath = (d.has_key('options')3156and('keepRepoPath'in d['options']))31573158defgitRefForBranch(self, branch):3159if branch =="main":3160return self.refPrefix +"master"31613162iflen(branch) <=0:3163return branch31643165return self.refPrefix + self.projectName + branch31663167defgitCommitByP4Change(self, ref, change):3168if self.verbose:3169print"looking in ref "+ ref +" for change%susing bisect..."% change31703171 earliestCommit =""3172 latestCommit =parseRevision(ref)31733174while True:3175if self.verbose:3176print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)3177 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()3178iflen(next) ==0:3179if self.verbose:3180print"argh"3181return""3182 log =extractLogMessageFromGitCommit(next)3183 settings =extractSettingsGitLog(log)3184 currentChange =int(settings['change'])3185if self.verbose:3186print"current change%s"% currentChange31873188if currentChange == change:3189if self.verbose:3190print"found%s"% next3191return next31923193if currentChange < change:3194 earliestCommit ="^%s"% next3195else:3196 latestCommit ="%s"% next31973198return""31993200defimportNewBranch(self, branch, maxChange):3201# make fast-import flush all changes to disk and update the refs using the checkpoint3202# command so that we can try to find the branch parent in the git history3203 self.gitStream.write("checkpoint\n\n");3204 self.gitStream.flush();3205 branchPrefix = self.depotPaths[0] + branch +"/"3206range="@1,%s"% maxChange3207#print "prefix" + branchPrefix3208 changes =p4ChangesForPaths([branchPrefix],range, self.changes_block_size)3209iflen(changes) <=0:3210return False3211 firstChange = changes[0]3212#print "first change in branch: %s" % firstChange3213 sourceBranch = self.knownBranches[branch]3214 sourceDepotPath = self.depotPaths[0] + sourceBranch3215 sourceRef = self.gitRefForBranch(sourceBranch)3216#print "source " + sourceBranch32173218 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])3219#print "branch parent: %s" % branchParentChange3220 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)3221iflen(gitParent) >0:3222 self.initialParents[self.gitRefForBranch(branch)] = gitParent3223#print "parent git commit: %s" % gitParent32243225 self.importChanges(changes)3226return True32273228defsearchParent(self, parent, branch, target):3229 parentFound =False3230for blob inread_pipe_lines(["git","rev-list","--reverse",3231"--no-merges", parent]):3232 blob = blob.strip()3233iflen(read_pipe(["git","diff-tree", blob, target])) ==0:3234 parentFound =True3235if self.verbose:3236print"Found parent of%sin commit%s"% (branch, blob)3237break3238if parentFound:3239return blob3240else:3241return None32423243defimportChanges(self, changes):3244 cnt =13245for change in changes:3246 description =p4_describe(change)3247 self.updateOptionDict(description)32483249if not self.silent:3250 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))3251 sys.stdout.flush()3252 cnt = cnt +132533254try:3255if self.detectBranches:3256 branches = self.splitFilesIntoBranches(description)3257for branch in branches.keys():3258## HACK --hwn3259 branchPrefix = self.depotPaths[0] + branch +"/"3260 self.branchPrefixes = [ branchPrefix ]32613262 parent =""32633264 filesForCommit = branches[branch]32653266if self.verbose:3267print"branch is%s"% branch32683269 self.updatedBranches.add(branch)32703271if branch not in self.createdBranches:3272 self.createdBranches.add(branch)3273 parent = self.knownBranches[branch]3274if parent == branch:3275 parent =""3276else:3277 fullBranch = self.projectName + branch3278if fullBranch not in self.p4BranchesInGit:3279if not self.silent:3280print("\nImporting new branch%s"% fullBranch);3281if self.importNewBranch(branch, change -1):3282 parent =""3283 self.p4BranchesInGit.append(fullBranch)3284if not self.silent:3285print("\nResuming with change%s"% change);32863287if self.verbose:3288print"parent determined through known branches:%s"% parent32893290 branch = self.gitRefForBranch(branch)3291 parent = self.gitRefForBranch(parent)32923293if self.verbose:3294print"looking for initial parent for%s; current parent is%s"% (branch, parent)32953296iflen(parent) ==0and branch in self.initialParents:3297 parent = self.initialParents[branch]3298del self.initialParents[branch]32993300 blob =None3301iflen(parent) >0:3302 tempBranch ="%s/%d"% (self.tempBranchLocation, change)3303if self.verbose:3304print"Creating temporary branch: "+ tempBranch3305 self.commit(description, filesForCommit, tempBranch)3306 self.tempBranches.append(tempBranch)3307 self.checkpoint()3308 blob = self.searchParent(parent, branch, tempBranch)3309if blob:3310 self.commit(description, filesForCommit, branch, blob)3311else:3312if self.verbose:3313print"Parent of%snot found. Committing into head of%s"% (branch, parent)3314 self.commit(description, filesForCommit, branch, parent)3315else:3316 files = self.extractFilesFromCommit(description)3317 self.commit(description, files, self.branch,3318 self.initialParent)3319# only needed once, to connect to the previous commit3320 self.initialParent =""3321exceptIOError:3322print self.gitError.read()3323 sys.exit(1)33243325defsync_origin_only(self):3326if self.syncWithOrigin:3327 self.hasOrigin =originP4BranchesExist()3328if self.hasOrigin:3329if not self.silent:3330print'Syncing with origin first, using "git fetch origin"'3331system("git fetch origin")33323333defimportHeadRevision(self, revision):3334print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)33353336 details = {}3337 details["user"] ="git perforce import user"3338 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"3339% (' '.join(self.depotPaths), revision))3340 details["change"] = revision3341 newestRevision =033423343 fileCnt =03344 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]33453346for info inp4CmdList(["files"] + fileArgs):33473348if'code'in info and info['code'] =='error':3349 sys.stderr.write("p4 returned an error:%s\n"3350% info['data'])3351if info['data'].find("must refer to client") >=0:3352 sys.stderr.write("This particular p4 error is misleading.\n")3353 sys.stderr.write("Perhaps the depot path was misspelled.\n");3354 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))3355 sys.exit(1)3356if'p4ExitCode'in info:3357 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])3358 sys.exit(1)335933603361 change =int(info["change"])3362if change > newestRevision:3363 newestRevision = change33643365if info["action"]in self.delete_actions:3366# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!3367#fileCnt = fileCnt + 13368continue33693370for prop in["depotFile","rev","action","type"]:3371 details["%s%s"% (prop, fileCnt)] = info[prop]33723373 fileCnt = fileCnt +133743375 details["change"] = newestRevision33763377# Use time from top-most change so that all git p4 clones of3378# the same p4 repo have the same commit SHA1s.3379 res =p4_describe(newestRevision)3380 details["time"] = res["time"]33813382 self.updateOptionDict(details)3383try:3384 self.commit(details, self.extractFilesFromCommit(details), self.branch)3385exceptIOError:3386print"IO error with git fast-import. Is your git version recent enough?"3387print self.gitError.read()338833893390defrun(self, args):3391 self.depotPaths = []3392 self.changeRange =""3393 self.previousDepotPaths = []3394 self.hasOrigin =False33953396# map from branch depot path to parent branch3397 self.knownBranches = {}3398 self.initialParents = {}33993400if self.importIntoRemotes:3401 self.refPrefix ="refs/remotes/p4/"3402else:3403 self.refPrefix ="refs/heads/p4/"34043405 self.sync_origin_only()34063407 branch_arg_given =bool(self.branch)3408iflen(self.branch) ==0:3409 self.branch = self.refPrefix +"master"3410ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:3411system("git update-ref%srefs/heads/p4"% self.branch)3412system("git branch -D p4")34133414# accept either the command-line option, or the configuration variable3415if self.useClientSpec:3416# will use this after clone to set the variable3417 self.useClientSpec_from_options =True3418else:3419ifgitConfigBool("git-p4.useclientspec"):3420 self.useClientSpec =True3421if self.useClientSpec:3422 self.clientSpecDirs =getClientSpec()34233424# TODO: should always look at previous commits,3425# merge with previous imports, if possible.3426if args == []:3427if self.hasOrigin:3428createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)34293430# branches holds mapping from branch name to sha13431 branches =p4BranchesInGit(self.importIntoRemotes)34323433# restrict to just this one, disabling detect-branches3434if branch_arg_given:3435 short = self.branch.split("/")[-1]3436if short in branches:3437 self.p4BranchesInGit = [ short ]3438else:3439 self.p4BranchesInGit = branches.keys()34403441iflen(self.p4BranchesInGit) >1:3442if not self.silent:3443print"Importing from/into multiple branches"3444 self.detectBranches =True3445for branch in branches.keys():3446 self.initialParents[self.refPrefix + branch] = \3447 branches[branch]34483449if self.verbose:3450print"branches:%s"% self.p4BranchesInGit34513452 p4Change =03453for branch in self.p4BranchesInGit:3454 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)34553456 settings =extractSettingsGitLog(logMsg)34573458 self.readOptions(settings)3459if(settings.has_key('depot-paths')3460and settings.has_key('change')):3461 change =int(settings['change']) +13462 p4Change =max(p4Change, change)34633464 depotPaths =sorted(settings['depot-paths'])3465if self.previousDepotPaths == []:3466 self.previousDepotPaths = depotPaths3467else:3468 paths = []3469for(prev, cur)inzip(self.previousDepotPaths, depotPaths):3470 prev_list = prev.split("/")3471 cur_list = cur.split("/")3472for i inrange(0,min(len(cur_list),len(prev_list))):3473if cur_list[i] <> prev_list[i]:3474 i = i -13475break34763477 paths.append("/".join(cur_list[:i +1]))34783479 self.previousDepotPaths = paths34803481if p4Change >0:3482 self.depotPaths =sorted(self.previousDepotPaths)3483 self.changeRange ="@%s,#head"% p4Change3484if not self.silent and not self.detectBranches:3485print"Performing incremental import into%sgit branch"% self.branch34863487# accept multiple ref name abbreviations:3488# refs/foo/bar/branch -> use it exactly3489# p4/branch -> prepend refs/remotes/ or refs/heads/3490# branch -> prepend refs/remotes/p4/ or refs/heads/p4/3491if not self.branch.startswith("refs/"):3492if self.importIntoRemotes:3493 prepend ="refs/remotes/"3494else:3495 prepend ="refs/heads/"3496if not self.branch.startswith("p4/"):3497 prepend +="p4/"3498 self.branch = prepend + self.branch34993500iflen(args) ==0and self.depotPaths:3501if not self.silent:3502print"Depot paths:%s"%' '.join(self.depotPaths)3503else:3504if self.depotPaths and self.depotPaths != args:3505print("previous import used depot path%sand now%swas specified. "3506"This doesn't work!"% (' '.join(self.depotPaths),3507' '.join(args)))3508 sys.exit(1)35093510 self.depotPaths =sorted(args)35113512 revision =""3513 self.users = {}35143515# Make sure no revision specifiers are used when --changesfile3516# is specified.3517 bad_changesfile =False3518iflen(self.changesFile) >0:3519for p in self.depotPaths:3520if p.find("@") >=0or p.find("#") >=0:3521 bad_changesfile =True3522break3523if bad_changesfile:3524die("Option --changesfile is incompatible with revision specifiers")35253526 newPaths = []3527for p in self.depotPaths:3528if p.find("@") != -1:3529 atIdx = p.index("@")3530 self.changeRange = p[atIdx:]3531if self.changeRange =="@all":3532 self.changeRange =""3533elif','not in self.changeRange:3534 revision = self.changeRange3535 self.changeRange =""3536 p = p[:atIdx]3537elif p.find("#") != -1:3538 hashIdx = p.index("#")3539 revision = p[hashIdx:]3540 p = p[:hashIdx]3541elif self.previousDepotPaths == []:3542# pay attention to changesfile, if given, else import3543# the entire p4 tree at the head revision3544iflen(self.changesFile) ==0:3545 revision ="#head"35463547 p = re.sub("\.\.\.$","", p)3548if not p.endswith("/"):3549 p +="/"35503551 newPaths.append(p)35523553 self.depotPaths = newPaths35543555# --detect-branches may change this for each branch3556 self.branchPrefixes = self.depotPaths35573558 self.loadUserMapFromCache()3559 self.labels = {}3560if self.detectLabels:3561 self.getLabels();35623563if self.detectBranches:3564## FIXME - what's a P4 projectName ?3565 self.projectName = self.guessProjectName()35663567if self.hasOrigin:3568 self.getBranchMappingFromGitBranches()3569else:3570 self.getBranchMapping()3571if self.verbose:3572print"p4-git branches:%s"% self.p4BranchesInGit3573print"initial parents:%s"% self.initialParents3574for b in self.p4BranchesInGit:3575if b !="master":35763577## FIXME3578 b = b[len(self.projectName):]3579 self.createdBranches.add(b)35803581 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))35823583 self.importProcess = subprocess.Popen(["git","fast-import"],3584 stdin=subprocess.PIPE,3585 stdout=subprocess.PIPE,3586 stderr=subprocess.PIPE);3587 self.gitOutput = self.importProcess.stdout3588 self.gitStream = self.importProcess.stdin3589 self.gitError = self.importProcess.stderr35903591if revision:3592 self.importHeadRevision(revision)3593else:3594 changes = []35953596iflen(self.changesFile) >0:3597 output =open(self.changesFile).readlines()3598 changeSet =set()3599for line in output:3600 changeSet.add(int(line))36013602for change in changeSet:3603 changes.append(change)36043605 changes.sort()3606else:3607# catch "git p4 sync" with no new branches, in a repo that3608# does not have any existing p4 branches3609iflen(args) ==0:3610if not self.p4BranchesInGit:3611die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")36123613# The default branch is master, unless --branch is used to3614# specify something else. Make sure it exists, or complain3615# nicely about how to use --branch.3616if not self.detectBranches:3617if notbranch_exists(self.branch):3618if branch_arg_given:3619die("Error: branch%sdoes not exist."% self.branch)3620else:3621die("Error: no branch%s; perhaps specify one with --branch."%3622 self.branch)36233624if self.verbose:3625print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3626 self.changeRange)3627 changes =p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)36283629iflen(self.maxChanges) >0:3630 changes = changes[:min(int(self.maxChanges),len(changes))]36313632iflen(changes) ==0:3633if not self.silent:3634print"No changes to import!"3635else:3636if not self.silent and not self.detectBranches:3637print"Import destination:%s"% self.branch36383639 self.updatedBranches =set()36403641if not self.detectBranches:3642if args:3643# start a new branch3644 self.initialParent =""3645else:3646# build on a previous revision3647 self.initialParent =parseRevision(self.branch)36483649 self.importChanges(changes)36503651if not self.silent:3652print""3653iflen(self.updatedBranches) >0:3654 sys.stdout.write("Updated branches: ")3655for b in self.updatedBranches:3656 sys.stdout.write("%s"% b)3657 sys.stdout.write("\n")36583659ifgitConfigBool("git-p4.importLabels"):3660 self.importLabels =True36613662if self.importLabels:3663 p4Labels =getP4Labels(self.depotPaths)3664 gitTags =getGitTags()36653666 missingP4Labels = p4Labels - gitTags3667 self.importP4Labels(self.gitStream, missingP4Labels)36683669 self.gitStream.close()3670if self.importProcess.wait() !=0:3671die("fast-import failed:%s"% self.gitError.read())3672 self.gitOutput.close()3673 self.gitError.close()36743675# Cleanup temporary branches created during import3676if self.tempBranches != []:3677for branch in self.tempBranches:3678read_pipe("git update-ref -d%s"% branch)3679 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))36803681# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3682# a convenient shortcut refname "p4".3683if self.importIntoRemotes:3684 head_ref = self.refPrefix +"HEAD"3685if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3686system(["git","symbolic-ref", head_ref, self.branch])36873688return True36893690classP4Rebase(Command):3691def__init__(self):3692 Command.__init__(self)3693 self.options = [3694 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3695]3696 self.importLabels =False3697 self.description = ("Fetches the latest revision from perforce and "3698+"rebases the current work (branch) against it")36993700defrun(self, args):3701 sync =P4Sync()3702 sync.importLabels = self.importLabels3703 sync.run([])37043705return self.rebase()37063707defrebase(self):3708if os.system("git update-index --refresh") !=0:3709die("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.");3710iflen(read_pipe("git diff-index HEAD --")) >0:3711die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");37123713[upstream, settings] =findUpstreamBranchPoint()3714iflen(upstream) ==0:3715die("Cannot find upstream branchpoint for rebase")37163717# the branchpoint may be p4/foo~3, so strip off the parent3718 upstream = re.sub("~[0-9]+$","", upstream)37193720print"Rebasing the current branch onto%s"% upstream3721 oldHead =read_pipe("git rev-parse HEAD").strip()3722system("git rebase%s"% upstream)3723system("git diff-tree --stat --summary -M%sHEAD --"% oldHead)3724return True37253726classP4Clone(P4Sync):3727def__init__(self):3728 P4Sync.__init__(self)3729 self.description ="Creates a new git repository and imports from Perforce into it"3730 self.usage ="usage: %prog [options] //depot/path[@revRange]"3731 self.options += [3732 optparse.make_option("--destination", dest="cloneDestination",3733 action='store', default=None,3734help="where to leave result of the clone"),3735 optparse.make_option("--bare", dest="cloneBare",3736 action="store_true", default=False),3737]3738 self.cloneDestination =None3739 self.needsGit =False3740 self.cloneBare =False37413742defdefaultDestination(self, args):3743## TODO: use common prefix of args?3744 depotPath = args[0]3745 depotDir = re.sub("(@[^@]*)$","", depotPath)3746 depotDir = re.sub("(#[^#]*)$","", depotDir)3747 depotDir = re.sub(r"\.\.\.$","", depotDir)3748 depotDir = re.sub(r"/$","", depotDir)3749return os.path.split(depotDir)[1]37503751defrun(self, args):3752iflen(args) <1:3753return False37543755if self.keepRepoPath and not self.cloneDestination:3756 sys.stderr.write("Must specify destination for --keep-path\n")3757 sys.exit(1)37583759 depotPaths = args37603761if not self.cloneDestination andlen(depotPaths) >1:3762 self.cloneDestination = depotPaths[-1]3763 depotPaths = depotPaths[:-1]37643765 self.cloneExclude = ["/"+p for p in self.cloneExclude]3766for p in depotPaths:3767if not p.startswith("//"):3768 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3769return False37703771if not self.cloneDestination:3772 self.cloneDestination = self.defaultDestination(args)37733774print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)37753776if not os.path.exists(self.cloneDestination):3777 os.makedirs(self.cloneDestination)3778chdir(self.cloneDestination)37793780 init_cmd = ["git","init"]3781if self.cloneBare:3782 init_cmd.append("--bare")3783 retcode = subprocess.call(init_cmd)3784if retcode:3785raiseCalledProcessError(retcode, init_cmd)37863787if not P4Sync.run(self, depotPaths):3788return False37893790# create a master branch and check out a work tree3791ifgitBranchExists(self.branch):3792system(["git","branch","master", self.branch ])3793if not self.cloneBare:3794system(["git","checkout","-f"])3795else:3796print'Not checking out any branch, use ' \3797'"git checkout -q -b master <branch>"'37983799# auto-set this variable if invoked with --use-client-spec3800if self.useClientSpec_from_options:3801system("git config --bool git-p4.useclientspec true")38023803return True38043805classP4Branches(Command):3806def__init__(self):3807 Command.__init__(self)3808 self.options = [ ]3809 self.description = ("Shows the git branches that hold imports and their "3810+"corresponding perforce depot paths")3811 self.verbose =False38123813defrun(self, args):3814iforiginP4BranchesExist():3815createOrUpdateBranchesFromOrigin()38163817 cmdline ="git rev-parse --symbolic "3818 cmdline +=" --remotes"38193820for line inread_pipe_lines(cmdline):3821 line = line.strip()38223823if not line.startswith('p4/')or line =="p4/HEAD":3824continue3825 branch = line38263827 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3828 settings =extractSettingsGitLog(log)38293830print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3831return True38323833classHelpFormatter(optparse.IndentedHelpFormatter):3834def__init__(self):3835 optparse.IndentedHelpFormatter.__init__(self)38363837defformat_description(self, description):3838if description:3839return description +"\n"3840else:3841return""38423843defprintUsage(commands):3844print"usage:%s<command> [options]"% sys.argv[0]3845print""3846print"valid commands:%s"%", ".join(commands)3847print""3848print"Try%s<command> --help for command specific help."% sys.argv[0]3849print""38503851commands = {3852"debug": P4Debug,3853"submit": P4Submit,3854"commit": P4Submit,3855"sync": P4Sync,3856"rebase": P4Rebase,3857"clone": P4Clone,3858"rollback": P4RollBack,3859"branches": P4Branches3860}386138623863defmain():3864iflen(sys.argv[1:]) ==0:3865printUsage(commands.keys())3866 sys.exit(2)38673868 cmdName = sys.argv[1]3869try:3870 klass = commands[cmdName]3871 cmd =klass()3872exceptKeyError:3873print"unknown command%s"% cmdName3874print""3875printUsage(commands.keys())3876 sys.exit(2)38773878 options = cmd.options3879 cmd.gitdir = os.environ.get("GIT_DIR",None)38803881 args = sys.argv[2:]38823883 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3884if cmd.needsGit:3885 options.append(optparse.make_option("--git-dir", dest="gitdir"))38863887 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3888 options,3889 description = cmd.description,3890 formatter =HelpFormatter())38913892(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3893global verbose3894 verbose = cmd.verbose3895if cmd.needsGit:3896if cmd.gitdir ==None:3897 cmd.gitdir = os.path.abspath(".git")3898if notisValidGitDir(cmd.gitdir):3899# "rev-parse --git-dir" without arguments will try $PWD/.git3900 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3901if os.path.exists(cmd.gitdir):3902 cdup =read_pipe("git rev-parse --show-cdup").strip()3903iflen(cdup) >0:3904chdir(cdup);39053906if notisValidGitDir(cmd.gitdir):3907ifisValidGitDir(cmd.gitdir +"/.git"):3908 cmd.gitdir +="/.git"3909else:3910die("fatal: cannot locate git repository at%s"% cmd.gitdir)39113912# so git commands invoked from the P4 workspace will succeed3913 os.environ["GIT_DIR"] = cmd.gitdir39143915if not cmd.run(args):3916 parser.print_help()3917 sys.exit(2)391839193920if __name__ =='__main__':3921main()