1#!/usr/bin/env python 2# 3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. 4# 5# Author: Simon Hausmann <simon@lst.de> 6# Copyright: 2007 Simon Hausmann <simon@lst.de> 7# 2007 Trolltech ASA 8# License: MIT <http://www.opensource.org/licenses/mit-license.php> 9# 10import sys 11if sys.hexversion <0x02040000: 12# The limiter is the subprocess module 13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 14 sys.exit(1) 15import os 16import optparse 17import marshal 18import subprocess 19import tempfile 20import time 21import platform 22import re 23import shutil 24import stat 25 26try: 27from subprocess import CalledProcessError 28exceptImportError: 29# from python2.7:subprocess.py 30# Exception classes used by this module. 31classCalledProcessError(Exception): 32"""This exception is raised when a process run by check_call() returns 33 a non-zero exit status. The exit status will be stored in the 34 returncode attribute.""" 35def__init__(self, returncode, cmd): 36 self.returncode = returncode 37 self.cmd = cmd 38def__str__(self): 39return"Command '%s' returned non-zero exit status%d"% (self.cmd, self.returncode) 40 41verbose =False 42 43# Only labels/tags matching this will be imported/exported 44defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 45 46defp4_build_cmd(cmd): 47"""Build a suitable p4 command line. 48 49 This consolidates building and returning a p4 command line into one 50 location. It means that hooking into the environment, or other configuration 51 can be done more easily. 52 """ 53 real_cmd = ["p4"] 54 55 user =gitConfig("git-p4.user") 56iflen(user) >0: 57 real_cmd += ["-u",user] 58 59 password =gitConfig("git-p4.password") 60iflen(password) >0: 61 real_cmd += ["-P", password] 62 63 port =gitConfig("git-p4.port") 64iflen(port) >0: 65 real_cmd += ["-p", port] 66 67 host =gitConfig("git-p4.host") 68iflen(host) >0: 69 real_cmd += ["-H", host] 70 71 client =gitConfig("git-p4.client") 72iflen(client) >0: 73 real_cmd += ["-c", client] 74 75 76ifisinstance(cmd,basestring): 77 real_cmd =' '.join(real_cmd) +' '+ cmd 78else: 79 real_cmd += cmd 80return real_cmd 81 82defchdir(dir): 83# P4 uses the PWD environment variable rather than getcwd(). Since we're 84# not using the shell, we have to set it ourselves. This path could 85# be relative, so go there first, then figure out where we ended up. 86 os.chdir(dir) 87 os.environ['PWD'] = os.getcwd() 88 89defdie(msg): 90if verbose: 91raiseException(msg) 92else: 93 sys.stderr.write(msg +"\n") 94 sys.exit(1) 95 96defwrite_pipe(c, stdin): 97if verbose: 98 sys.stderr.write('Writing pipe:%s\n'%str(c)) 99 100 expand =isinstance(c,basestring) 101 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 102 pipe = p.stdin 103 val = pipe.write(stdin) 104 pipe.close() 105if p.wait(): 106die('Command failed:%s'%str(c)) 107 108return val 109 110defp4_write_pipe(c, stdin): 111 real_cmd =p4_build_cmd(c) 112returnwrite_pipe(real_cmd, stdin) 113 114defread_pipe(c, ignore_error=False): 115if verbose: 116 sys.stderr.write('Reading pipe:%s\n'%str(c)) 117 118 expand =isinstance(c,basestring) 119 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 120 pipe = p.stdout 121 val = pipe.read() 122if p.wait()and not ignore_error: 123die('Command failed:%s'%str(c)) 124 125return val 126 127defp4_read_pipe(c, ignore_error=False): 128 real_cmd =p4_build_cmd(c) 129returnread_pipe(real_cmd, ignore_error) 130 131defread_pipe_lines(c): 132if verbose: 133 sys.stderr.write('Reading pipe:%s\n'%str(c)) 134 135 expand =isinstance(c, basestring) 136 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 137 pipe = p.stdout 138 val = pipe.readlines() 139if pipe.close()or p.wait(): 140die('Command failed:%s'%str(c)) 141 142return val 143 144defp4_read_pipe_lines(c): 145"""Specifically invoke p4 on the command supplied. """ 146 real_cmd =p4_build_cmd(c) 147returnread_pipe_lines(real_cmd) 148 149defp4_has_command(cmd): 150"""Ask p4 for help on this command. If it returns an error, the 151 command does not exist in this version of p4.""" 152 real_cmd =p4_build_cmd(["help", cmd]) 153 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 154 stderr=subprocess.PIPE) 155 p.communicate() 156return p.returncode ==0 157 158defp4_has_move_command(): 159"""See if the move command exists, that it supports -k, and that 160 it has not been administratively disabled. The arguments 161 must be correct, but the filenames do not have to exist. Use 162 ones with wildcards so even if they exist, it will fail.""" 163 164if notp4_has_command("move"): 165return False 166 cmd =p4_build_cmd(["move","-k","@from","@to"]) 167 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 168(out, err) = p.communicate() 169# return code will be 1 in either case 170if err.find("Invalid option") >=0: 171return False 172if err.find("disabled") >=0: 173return False 174# assume it failed because @... was invalid changelist 175return True 176 177defsystem(cmd): 178 expand =isinstance(cmd,basestring) 179if verbose: 180 sys.stderr.write("executing%s\n"%str(cmd)) 181 retcode = subprocess.call(cmd, shell=expand) 182if retcode: 183raiseCalledProcessError(retcode, cmd) 184 185defp4_system(cmd): 186"""Specifically invoke p4 as the system command. """ 187 real_cmd =p4_build_cmd(cmd) 188 expand =isinstance(real_cmd, basestring) 189 retcode = subprocess.call(real_cmd, shell=expand) 190if retcode: 191raiseCalledProcessError(retcode, real_cmd) 192 193_p4_version_string =None 194defp4_version_string(): 195"""Read the version string, showing just the last line, which 196 hopefully is the interesting version bit. 197 198 $ p4 -V 199 Perforce - The Fast Software Configuration Management System. 200 Copyright 1995-2011 Perforce Software. All rights reserved. 201 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 202 """ 203global _p4_version_string 204if not _p4_version_string: 205 a =p4_read_pipe_lines(["-V"]) 206 _p4_version_string = a[-1].rstrip() 207return _p4_version_string 208 209defp4_integrate(src, dest): 210p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 211 212defp4_sync(f, *options): 213p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 214 215defp4_add(f): 216# forcibly add file names with wildcards 217ifwildcard_present(f): 218p4_system(["add","-f", f]) 219else: 220p4_system(["add", f]) 221 222defp4_delete(f): 223p4_system(["delete",wildcard_encode(f)]) 224 225defp4_edit(f): 226p4_system(["edit",wildcard_encode(f)]) 227 228defp4_revert(f): 229p4_system(["revert",wildcard_encode(f)]) 230 231defp4_reopen(type, f): 232p4_system(["reopen","-t",type,wildcard_encode(f)]) 233 234defp4_move(src, dest): 235p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 236 237defp4_describe(change): 238"""Make sure it returns a valid result by checking for 239 the presence of field "time". Return a dict of the 240 results.""" 241 242 ds =p4CmdList(["describe","-s",str(change)]) 243iflen(ds) !=1: 244die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 245 246 d = ds[0] 247 248if"p4ExitCode"in d: 249die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 250str(d))) 251if"code"in d: 252if d["code"] =="error": 253die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 254 255if"time"not in d: 256die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 257 258return d 259 260# 261# Canonicalize the p4 type and return a tuple of the 262# base type, plus any modifiers. See "p4 help filetypes" 263# for a list and explanation. 264# 265defsplit_p4_type(p4type): 266 267 p4_filetypes_historical = { 268"ctempobj":"binary+Sw", 269"ctext":"text+C", 270"cxtext":"text+Cx", 271"ktext":"text+k", 272"kxtext":"text+kx", 273"ltext":"text+F", 274"tempobj":"binary+FSw", 275"ubinary":"binary+F", 276"uresource":"resource+F", 277"uxbinary":"binary+Fx", 278"xbinary":"binary+x", 279"xltext":"text+Fx", 280"xtempobj":"binary+Swx", 281"xtext":"text+x", 282"xunicode":"unicode+x", 283"xutf16":"utf16+x", 284} 285if p4type in p4_filetypes_historical: 286 p4type = p4_filetypes_historical[p4type] 287 mods ="" 288 s = p4type.split("+") 289 base = s[0] 290 mods ="" 291iflen(s) >1: 292 mods = s[1] 293return(base, mods) 294 295# 296# return the raw p4 type of a file (text, text+ko, etc) 297# 298defp4_type(file): 299 results =p4CmdList(["fstat","-T","headType",file]) 300return results[0]['headType'] 301 302# 303# Given a type base and modifier, return a regexp matching 304# the keywords that can be expanded in the file 305# 306defp4_keywords_regexp_for_type(base, type_mods): 307if base in("text","unicode","binary"): 308 kwords =None 309if"ko"in type_mods: 310 kwords ='Id|Header' 311elif"k"in type_mods: 312 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 313else: 314return None 315 pattern = r""" 316 \$ # Starts with a dollar, followed by... 317 (%s) # one of the keywords, followed by... 318 (:[^$\n]+)? # possibly an old expansion, followed by... 319 \$ # another dollar 320 """% kwords 321return pattern 322else: 323return None 324 325# 326# Given a file, return a regexp matching the possible 327# RCS keywords that will be expanded, or None for files 328# with kw expansion turned off. 329# 330defp4_keywords_regexp_for_file(file): 331if not os.path.exists(file): 332return None 333else: 334(type_base, type_mods) =split_p4_type(p4_type(file)) 335returnp4_keywords_regexp_for_type(type_base, type_mods) 336 337defsetP4ExecBit(file, mode): 338# Reopens an already open file and changes the execute bit to match 339# the execute bit setting in the passed in mode. 340 341 p4Type ="+x" 342 343if notisModeExec(mode): 344 p4Type =getP4OpenedType(file) 345 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 346 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 347if p4Type[-1] =="+": 348 p4Type = p4Type[0:-1] 349 350p4_reopen(p4Type,file) 351 352defgetP4OpenedType(file): 353# Returns the perforce file type for the given file. 354 355 result =p4_read_pipe(["opened",wildcard_encode(file)]) 356 match = re.match(".*\((.+)\)\r?$", result) 357if match: 358return match.group(1) 359else: 360die("Could not determine file type for%s(result: '%s')"% (file, result)) 361 362# Return the set of all p4 labels 363defgetP4Labels(depotPaths): 364 labels =set() 365ifisinstance(depotPaths,basestring): 366 depotPaths = [depotPaths] 367 368for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 369 label = l['label'] 370 labels.add(label) 371 372return labels 373 374# Return the set of all git tags 375defgetGitTags(): 376 gitTags =set() 377for line inread_pipe_lines(["git","tag"]): 378 tag = line.strip() 379 gitTags.add(tag) 380return gitTags 381 382defdiffTreePattern(): 383# This is a simple generator for the diff tree regex pattern. This could be 384# a class variable if this and parseDiffTreeEntry were a part of a class. 385 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 386while True: 387yield pattern 388 389defparseDiffTreeEntry(entry): 390"""Parses a single diff tree entry into its component elements. 391 392 See git-diff-tree(1) manpage for details about the format of the diff 393 output. This method returns a dictionary with the following elements: 394 395 src_mode - The mode of the source file 396 dst_mode - The mode of the destination file 397 src_sha1 - The sha1 for the source file 398 dst_sha1 - The sha1 fr the destination file 399 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 400 status_score - The score for the status (applicable for 'C' and 'R' 401 statuses). This is None if there is no score. 402 src - The path for the source file. 403 dst - The path for the destination file. This is only present for 404 copy or renames. If it is not present, this is None. 405 406 If the pattern is not matched, None is returned.""" 407 408 match =diffTreePattern().next().match(entry) 409if match: 410return{ 411'src_mode': match.group(1), 412'dst_mode': match.group(2), 413'src_sha1': match.group(3), 414'dst_sha1': match.group(4), 415'status': match.group(5), 416'status_score': match.group(6), 417'src': match.group(7), 418'dst': match.group(10) 419} 420return None 421 422defisModeExec(mode): 423# Returns True if the given git mode represents an executable file, 424# otherwise False. 425return mode[-3:] =="755" 426 427defisModeExecChanged(src_mode, dst_mode): 428returnisModeExec(src_mode) !=isModeExec(dst_mode) 429 430defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 431 432ifisinstance(cmd,basestring): 433 cmd ="-G "+ cmd 434 expand =True 435else: 436 cmd = ["-G"] + cmd 437 expand =False 438 439 cmd =p4_build_cmd(cmd) 440if verbose: 441 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 442 443# Use a temporary file to avoid deadlocks without 444# subprocess.communicate(), which would put another copy 445# of stdout into memory. 446 stdin_file =None 447if stdin is not None: 448 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 449ifisinstance(stdin,basestring): 450 stdin_file.write(stdin) 451else: 452for i in stdin: 453 stdin_file.write(i +'\n') 454 stdin_file.flush() 455 stdin_file.seek(0) 456 457 p4 = subprocess.Popen(cmd, 458 shell=expand, 459 stdin=stdin_file, 460 stdout=subprocess.PIPE) 461 462 result = [] 463try: 464while True: 465 entry = marshal.load(p4.stdout) 466if cb is not None: 467cb(entry) 468else: 469 result.append(entry) 470exceptEOFError: 471pass 472 exitCode = p4.wait() 473if exitCode !=0: 474 entry = {} 475 entry["p4ExitCode"] = exitCode 476 result.append(entry) 477 478return result 479 480defp4Cmd(cmd): 481list=p4CmdList(cmd) 482 result = {} 483for entry inlist: 484 result.update(entry) 485return result; 486 487defp4Where(depotPath): 488if not depotPath.endswith("/"): 489 depotPath +="/" 490 depotPath = depotPath +"..." 491 outputList =p4CmdList(["where", depotPath]) 492 output =None 493for entry in outputList: 494if"depotFile"in entry: 495if entry["depotFile"] == depotPath: 496 output = entry 497break 498elif"data"in entry: 499 data = entry.get("data") 500 space = data.find(" ") 501if data[:space] == depotPath: 502 output = entry 503break 504if output ==None: 505return"" 506if output["code"] =="error": 507return"" 508 clientPath ="" 509if"path"in output: 510 clientPath = output.get("path") 511elif"data"in output: 512 data = output.get("data") 513 lastSpace = data.rfind(" ") 514 clientPath = data[lastSpace +1:] 515 516if clientPath.endswith("..."): 517 clientPath = clientPath[:-3] 518return clientPath 519 520defcurrentGitBranch(): 521returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 522 523defisValidGitDir(path): 524if(os.path.exists(path +"/HEAD") 525and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 526return True; 527return False 528 529defparseRevision(ref): 530returnread_pipe("git rev-parse%s"% ref).strip() 531 532defbranchExists(ref): 533 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 534 ignore_error=True) 535returnlen(rev) >0 536 537defextractLogMessageFromGitCommit(commit): 538 logMessage ="" 539 540## fixme: title is first line of commit, not 1st paragraph. 541 foundTitle =False 542for log inread_pipe_lines("git cat-file commit%s"% commit): 543if not foundTitle: 544iflen(log) ==1: 545 foundTitle =True 546continue 547 548 logMessage += log 549return logMessage 550 551defextractSettingsGitLog(log): 552 values = {} 553for line in log.split("\n"): 554 line = line.strip() 555 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 556if not m: 557continue 558 559 assignments = m.group(1).split(':') 560for a in assignments: 561 vals = a.split('=') 562 key = vals[0].strip() 563 val = ('='.join(vals[1:])).strip() 564if val.endswith('\"')and val.startswith('"'): 565 val = val[1:-1] 566 567 values[key] = val 568 569 paths = values.get("depot-paths") 570if not paths: 571 paths = values.get("depot-path") 572if paths: 573 values['depot-paths'] = paths.split(',') 574return values 575 576defgitBranchExists(branch): 577 proc = subprocess.Popen(["git","rev-parse", branch], 578 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 579return proc.wait() ==0; 580 581_gitConfig = {} 582 583defgitConfig(key): 584if not _gitConfig.has_key(key): 585 cmd = ["git","config", key ] 586 s =read_pipe(cmd, ignore_error=True) 587 _gitConfig[key] = s.strip() 588return _gitConfig[key] 589 590defgitConfigBool(key): 591"""Return a bool, using git config --bool. It is True only if the 592 variable is set to true, and False if set to false or not present 593 in the config.""" 594 595if not _gitConfig.has_key(key): 596 cmd = ["git","config","--bool", key ] 597 s =read_pipe(cmd, ignore_error=True) 598 v = s.strip() 599 _gitConfig[key] = v =="true" 600return _gitConfig[key] 601 602defgitConfigList(key): 603if not _gitConfig.has_key(key): 604 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 605 _gitConfig[key] = s.strip().split(os.linesep) 606return _gitConfig[key] 607 608defp4BranchesInGit(branchesAreInRemotes=True): 609"""Find all the branches whose names start with "p4/", looking 610 in remotes or heads as specified by the argument. Return 611 a dictionary of{ branch: revision }for each one found. 612 The branch names are the short names, without any 613 "p4/" prefix.""" 614 615 branches = {} 616 617 cmdline ="git rev-parse --symbolic " 618if branchesAreInRemotes: 619 cmdline +="--remotes" 620else: 621 cmdline +="--branches" 622 623for line inread_pipe_lines(cmdline): 624 line = line.strip() 625 626# only import to p4/ 627if not line.startswith('p4/'): 628continue 629# special symbolic ref to p4/master 630if line =="p4/HEAD": 631continue 632 633# strip off p4/ prefix 634 branch = line[len("p4/"):] 635 636 branches[branch] =parseRevision(line) 637 638return branches 639 640defbranch_exists(branch): 641"""Make sure that the given ref name really exists.""" 642 643 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 644 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 645 out, _ = p.communicate() 646if p.returncode: 647return False 648# expect exactly one line of output: the branch name 649return out.rstrip() == branch 650 651deffindUpstreamBranchPoint(head ="HEAD"): 652 branches =p4BranchesInGit() 653# map from depot-path to branch name 654 branchByDepotPath = {} 655for branch in branches.keys(): 656 tip = branches[branch] 657 log =extractLogMessageFromGitCommit(tip) 658 settings =extractSettingsGitLog(log) 659if settings.has_key("depot-paths"): 660 paths =",".join(settings["depot-paths"]) 661 branchByDepotPath[paths] ="remotes/p4/"+ branch 662 663 settings =None 664 parent =0 665while parent <65535: 666 commit = head +"~%s"% parent 667 log =extractLogMessageFromGitCommit(commit) 668 settings =extractSettingsGitLog(log) 669if settings.has_key("depot-paths"): 670 paths =",".join(settings["depot-paths"]) 671if branchByDepotPath.has_key(paths): 672return[branchByDepotPath[paths], settings] 673 674 parent = parent +1 675 676return["", settings] 677 678defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 679if not silent: 680print("Creating/updating branch(es) in%sbased on origin branch(es)" 681% localRefPrefix) 682 683 originPrefix ="origin/p4/" 684 685for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 686 line = line.strip() 687if(not line.startswith(originPrefix))or line.endswith("HEAD"): 688continue 689 690 headName = line[len(originPrefix):] 691 remoteHead = localRefPrefix + headName 692 originHead = line 693 694 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 695if(not original.has_key('depot-paths') 696or not original.has_key('change')): 697continue 698 699 update =False 700if notgitBranchExists(remoteHead): 701if verbose: 702print"creating%s"% remoteHead 703 update =True 704else: 705 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 706if settings.has_key('change') >0: 707if settings['depot-paths'] == original['depot-paths']: 708 originP4Change =int(original['change']) 709 p4Change =int(settings['change']) 710if originP4Change > p4Change: 711print("%s(%s) is newer than%s(%s). " 712"Updating p4 branch from origin." 713% (originHead, originP4Change, 714 remoteHead, p4Change)) 715 update =True 716else: 717print("Ignoring:%swas imported from%swhile " 718"%swas imported from%s" 719% (originHead,','.join(original['depot-paths']), 720 remoteHead,','.join(settings['depot-paths']))) 721 722if update: 723system("git update-ref%s %s"% (remoteHead, originHead)) 724 725deforiginP4BranchesExist(): 726returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 727 728defp4ChangesForPaths(depotPaths, changeRange): 729assert depotPaths 730 cmd = ['changes'] 731for p in depotPaths: 732 cmd += ["%s...%s"% (p, changeRange)] 733 output =p4_read_pipe_lines(cmd) 734 735 changes = {} 736for line in output: 737 changeNum =int(line.split(" ")[1]) 738 changes[changeNum] =True 739 740 changelist = changes.keys() 741 changelist.sort() 742return changelist 743 744defp4PathStartsWith(path, prefix): 745# This method tries to remedy a potential mixed-case issue: 746# 747# If UserA adds //depot/DirA/file1 748# and UserB adds //depot/dira/file2 749# 750# we may or may not have a problem. If you have core.ignorecase=true, 751# we treat DirA and dira as the same directory 752ifgitConfigBool("core.ignorecase"): 753return path.lower().startswith(prefix.lower()) 754return path.startswith(prefix) 755 756defgetClientSpec(): 757"""Look at the p4 client spec, create a View() object that contains 758 all the mappings, and return it.""" 759 760 specList =p4CmdList("client -o") 761iflen(specList) !=1: 762die('Output from "client -o" is%dlines, expecting 1'% 763len(specList)) 764 765# dictionary of all client parameters 766 entry = specList[0] 767 768# just the keys that start with "View" 769 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 770 771# hold this new View 772 view =View() 773 774# append the lines, in order, to the view 775for view_num inrange(len(view_keys)): 776 k ="View%d"% view_num 777if k not in view_keys: 778die("Expected view key%smissing"% k) 779 view.append(entry[k]) 780 781return view 782 783defgetClientRoot(): 784"""Grab the client directory.""" 785 786 output =p4CmdList("client -o") 787iflen(output) !=1: 788die('Output from "client -o" is%dlines, expecting 1'%len(output)) 789 790 entry = output[0] 791if"Root"not in entry: 792die('Client has no "Root"') 793 794return entry["Root"] 795 796# 797# P4 wildcards are not allowed in filenames. P4 complains 798# if you simply add them, but you can force it with "-f", in 799# which case it translates them into %xx encoding internally. 800# 801defwildcard_decode(path): 802# Search for and fix just these four characters. Do % last so 803# that fixing it does not inadvertently create new %-escapes. 804# Cannot have * in a filename in windows; untested as to 805# what p4 would do in such a case. 806if not platform.system() =="Windows": 807 path = path.replace("%2A","*") 808 path = path.replace("%23","#") \ 809.replace("%40","@") \ 810.replace("%25","%") 811return path 812 813defwildcard_encode(path): 814# do % first to avoid double-encoding the %s introduced here 815 path = path.replace("%","%25") \ 816.replace("*","%2A") \ 817.replace("#","%23") \ 818.replace("@","%40") 819return path 820 821defwildcard_present(path): 822 m = re.search("[*#@%]", path) 823return m is not None 824 825class Command: 826def__init__(self): 827 self.usage ="usage: %prog [options]" 828 self.needsGit =True 829 self.verbose =False 830 831class P4UserMap: 832def__init__(self): 833 self.userMapFromPerforceServer =False 834 self.myP4UserId =None 835 836defp4UserId(self): 837if self.myP4UserId: 838return self.myP4UserId 839 840 results =p4CmdList("user -o") 841for r in results: 842if r.has_key('User'): 843 self.myP4UserId = r['User'] 844return r['User'] 845die("Could not find your p4 user id") 846 847defp4UserIsMe(self, p4User): 848# return True if the given p4 user is actually me 849 me = self.p4UserId() 850if not p4User or p4User != me: 851return False 852else: 853return True 854 855defgetUserCacheFilename(self): 856 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 857return home +"/.gitp4-usercache.txt" 858 859defgetUserMapFromPerforceServer(self): 860if self.userMapFromPerforceServer: 861return 862 self.users = {} 863 self.emails = {} 864 865for output inp4CmdList("users"): 866if not output.has_key("User"): 867continue 868 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 869 self.emails[output["Email"]] = output["User"] 870 871 872 s ='' 873for(key, val)in self.users.items(): 874 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 875 876open(self.getUserCacheFilename(),"wb").write(s) 877 self.userMapFromPerforceServer =True 878 879defloadUserMapFromCache(self): 880 self.users = {} 881 self.userMapFromPerforceServer =False 882try: 883 cache =open(self.getUserCacheFilename(),"rb") 884 lines = cache.readlines() 885 cache.close() 886for line in lines: 887 entry = line.strip().split("\t") 888 self.users[entry[0]] = entry[1] 889exceptIOError: 890 self.getUserMapFromPerforceServer() 891 892classP4Debug(Command): 893def__init__(self): 894 Command.__init__(self) 895 self.options = [] 896 self.description ="A tool to debug the output of p4 -G." 897 self.needsGit =False 898 899defrun(self, args): 900 j =0 901for output inp4CmdList(args): 902print'Element:%d'% j 903 j +=1 904print output 905return True 906 907classP4RollBack(Command): 908def__init__(self): 909 Command.__init__(self) 910 self.options = [ 911 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 912] 913 self.description ="A tool to debug the multi-branch import. Don't use :)" 914 self.rollbackLocalBranches =False 915 916defrun(self, args): 917iflen(args) !=1: 918return False 919 maxChange =int(args[0]) 920 921if"p4ExitCode"inp4Cmd("changes -m 1"): 922die("Problems executing p4"); 923 924if self.rollbackLocalBranches: 925 refPrefix ="refs/heads/" 926 lines =read_pipe_lines("git rev-parse --symbolic --branches") 927else: 928 refPrefix ="refs/remotes/" 929 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 930 931for line in lines: 932if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 933 line = line.strip() 934 ref = refPrefix + line 935 log =extractLogMessageFromGitCommit(ref) 936 settings =extractSettingsGitLog(log) 937 938 depotPaths = settings['depot-paths'] 939 change = settings['change'] 940 941 changed =False 942 943iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 944for p in depotPaths]))) ==0: 945print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 946system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 947continue 948 949while change andint(change) > maxChange: 950 changed =True 951if self.verbose: 952print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 953system("git update-ref%s\"%s^\""% (ref, ref)) 954 log =extractLogMessageFromGitCommit(ref) 955 settings =extractSettingsGitLog(log) 956 957 958 depotPaths = settings['depot-paths'] 959 change = settings['change'] 960 961if changed: 962print"%srewound to%s"% (ref, change) 963 964return True 965 966classP4Submit(Command, P4UserMap): 967 968 conflict_behavior_choices = ("ask","skip","quit") 969 970def__init__(self): 971 Command.__init__(self) 972 P4UserMap.__init__(self) 973 self.options = [ 974 optparse.make_option("--origin", dest="origin"), 975 optparse.make_option("-M", dest="detectRenames", action="store_true"), 976# preserve the user, requires relevant p4 permissions 977 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 978 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 979 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 980 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 981 optparse.make_option("--conflict", dest="conflict_behavior", 982 choices=self.conflict_behavior_choices), 983 optparse.make_option("--branch", dest="branch"), 984] 985 self.description ="Submit changes from git to the perforce depot." 986 self.usage +=" [name of git branch to submit into perforce depot]" 987 self.origin ="" 988 self.detectRenames =False 989 self.preserveUser =gitConfigBool("git-p4.preserveUser") 990 self.dry_run =False 991 self.prepare_p4_only =False 992 self.conflict_behavior =None 993 self.isWindows = (platform.system() =="Windows") 994 self.exportLabels =False 995 self.p4HasMoveCommand =p4_has_move_command() 996 self.branch =None 997 998defcheck(self): 999iflen(p4CmdList("opened ...")) >0:1000die("You have files opened with perforce! Close them before starting the sync.")10011002defseparate_jobs_from_description(self, message):1003"""Extract and return a possible Jobs field in the commit1004 message. It goes into a separate section in the p4 change1005 specification.10061007 A jobs line starts with "Jobs:" and looks like a new field1008 in a form. Values are white-space separated on the same1009 line or on following lines that start with a tab.10101011 This does not parse and extract the full git commit message1012 like a p4 form. It just sees the Jobs: line as a marker1013 to pass everything from then on directly into the p4 form,1014 but outside the description section.10151016 Return a tuple (stripped log message, jobs string)."""10171018 m = re.search(r'^Jobs:', message, re.MULTILINE)1019if m is None:1020return(message,None)10211022 jobtext = message[m.start():]1023 stripped_message = message[:m.start()].rstrip()1024return(stripped_message, jobtext)10251026defprepareLogMessage(self, template, message, jobs):1027"""Edits the template returned from "p4 change -o" to insert1028 the message in the Description field, and the jobs text in1029 the Jobs field."""1030 result =""10311032 inDescriptionSection =False10331034for line in template.split("\n"):1035if line.startswith("#"):1036 result += line +"\n"1037continue10381039if inDescriptionSection:1040if line.startswith("Files:")or line.startswith("Jobs:"):1041 inDescriptionSection =False1042# insert Jobs section1043if jobs:1044 result += jobs +"\n"1045else:1046continue1047else:1048if line.startswith("Description:"):1049 inDescriptionSection =True1050 line +="\n"1051for messageLine in message.split("\n"):1052 line +="\t"+ messageLine +"\n"10531054 result += line +"\n"10551056return result10571058defpatchRCSKeywords(self,file, pattern):1059# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1060(handle, outFileName) = tempfile.mkstemp(dir='.')1061try:1062 outFile = os.fdopen(handle,"w+")1063 inFile =open(file,"r")1064 regexp = re.compile(pattern, re.VERBOSE)1065for line in inFile.readlines():1066 line = regexp.sub(r'$\1$', line)1067 outFile.write(line)1068 inFile.close()1069 outFile.close()1070# Forcibly overwrite the original file1071 os.unlink(file)1072 shutil.move(outFileName,file)1073except:1074# cleanup our temporary file1075 os.unlink(outFileName)1076print"Failed to strip RCS keywords in%s"%file1077raise10781079print"Patched up RCS keywords in%s"%file10801081defp4UserForCommit(self,id):1082# Return the tuple (perforce user,git email) for a given git commit id1083 self.getUserMapFromPerforceServer()1084 gitEmail =read_pipe(["git","log","--max-count=1",1085"--format=%ae",id])1086 gitEmail = gitEmail.strip()1087if not self.emails.has_key(gitEmail):1088return(None,gitEmail)1089else:1090return(self.emails[gitEmail],gitEmail)10911092defcheckValidP4Users(self,commits):1093# check if any git authors cannot be mapped to p4 users1094foridin commits:1095(user,email) = self.p4UserForCommit(id)1096if not user:1097 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1098ifgitConfigBool("git-p4.allowMissingP4Users"):1099print"%s"% msg1100else:1101die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)11021103deflastP4Changelist(self):1104# Get back the last changelist number submitted in this client spec. This1105# then gets used to patch up the username in the change. If the same1106# client spec is being used by multiple processes then this might go1107# wrong.1108 results =p4CmdList("client -o")# find the current client1109 client =None1110for r in results:1111if r.has_key('Client'):1112 client = r['Client']1113break1114if not client:1115die("could not get client spec")1116 results =p4CmdList(["changes","-c", client,"-m","1"])1117for r in results:1118if r.has_key('change'):1119return r['change']1120die("Could not get changelist number for last submit - cannot patch up user details")11211122defmodifyChangelistUser(self, changelist, newUser):1123# fixup the user field of a changelist after it has been submitted.1124 changes =p4CmdList("change -o%s"% changelist)1125iflen(changes) !=1:1126die("Bad output from p4 change modifying%sto user%s"%1127(changelist, newUser))11281129 c = changes[0]1130if c['User'] == newUser:return# nothing to do1131 c['User'] = newUser1132input= marshal.dumps(c)11331134 result =p4CmdList("change -f -i", stdin=input)1135for r in result:1136if r.has_key('code'):1137if r['code'] =='error':1138die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1139if r.has_key('data'):1140print("Updated user field for changelist%sto%s"% (changelist, newUser))1141return1142die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11431144defcanChangeChangelists(self):1145# check to see if we have p4 admin or super-user permissions, either of1146# which are required to modify changelists.1147 results =p4CmdList(["protects", self.depotPath])1148for r in results:1149if r.has_key('perm'):1150if r['perm'] =='admin':1151return11152if r['perm'] =='super':1153return11154return011551156defprepareSubmitTemplate(self):1157"""Run "p4 change -o" to grab a change specification template.1158 This does not use "p4 -G", as it is nice to keep the submission1159 template in original order, since a human might edit it.11601161 Remove lines in the Files section that show changes to files1162 outside the depot path we're committing into."""11631164 template =""1165 inFilesSection =False1166for line inp4_read_pipe_lines(['change','-o']):1167if line.endswith("\r\n"):1168 line = line[:-2] +"\n"1169if inFilesSection:1170if line.startswith("\t"):1171# path starts and ends with a tab1172 path = line[1:]1173 lastTab = path.rfind("\t")1174if lastTab != -1:1175 path = path[:lastTab]1176if notp4PathStartsWith(path, self.depotPath):1177continue1178else:1179 inFilesSection =False1180else:1181if line.startswith("Files:"):1182 inFilesSection =True11831184 template += line11851186return template11871188defedit_template(self, template_file):1189"""Invoke the editor to let the user change the submission1190 message. Return true if okay to continue with the submit."""11911192# if configured to skip the editing part, just submit1193ifgitConfigBool("git-p4.skipSubmitEdit"):1194return True11951196# look at the modification time, to check later if the user saved1197# the file1198 mtime = os.stat(template_file).st_mtime11991200# invoke the editor1201if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1202 editor = os.environ.get("P4EDITOR")1203else:1204 editor =read_pipe("git var GIT_EDITOR").strip()1205system(editor +" "+ template_file)12061207# If the file was not saved, prompt to see if this patch should1208# be skipped. But skip this verification step if configured so.1209ifgitConfigBool("git-p4.skipSubmitEditCheck"):1210return True12111212# modification time updated means user saved the file1213if os.stat(template_file).st_mtime > mtime:1214return True12151216while True:1217 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1218if response =='y':1219return True1220if response =='n':1221return False12221223defapplyCommit(self,id):1224"""Apply one commit, return True if it succeeded."""12251226print"Applying",read_pipe(["git","show","-s",1227"--format=format:%h%s",id])12281229(p4User, gitEmail) = self.p4UserForCommit(id)12301231 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1232 filesToAdd =set()1233 filesToDelete =set()1234 editedFiles =set()1235 pureRenameCopy =set()1236 filesToChangeExecBit = {}12371238for line in diff:1239 diff =parseDiffTreeEntry(line)1240 modifier = diff['status']1241 path = diff['src']1242if modifier =="M":1243p4_edit(path)1244ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1245 filesToChangeExecBit[path] = diff['dst_mode']1246 editedFiles.add(path)1247elif modifier =="A":1248 filesToAdd.add(path)1249 filesToChangeExecBit[path] = diff['dst_mode']1250if path in filesToDelete:1251 filesToDelete.remove(path)1252elif modifier =="D":1253 filesToDelete.add(path)1254if path in filesToAdd:1255 filesToAdd.remove(path)1256elif modifier =="C":1257 src, dest = diff['src'], diff['dst']1258p4_integrate(src, dest)1259 pureRenameCopy.add(dest)1260if diff['src_sha1'] != diff['dst_sha1']:1261p4_edit(dest)1262 pureRenameCopy.discard(dest)1263ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1264p4_edit(dest)1265 pureRenameCopy.discard(dest)1266 filesToChangeExecBit[dest] = diff['dst_mode']1267if self.isWindows:1268# turn off read-only attribute1269 os.chmod(dest, stat.S_IWRITE)1270 os.unlink(dest)1271 editedFiles.add(dest)1272elif modifier =="R":1273 src, dest = diff['src'], diff['dst']1274if self.p4HasMoveCommand:1275p4_edit(src)# src must be open before move1276p4_move(src, dest)# opens for (move/delete, move/add)1277else:1278p4_integrate(src, dest)1279if diff['src_sha1'] != diff['dst_sha1']:1280p4_edit(dest)1281else:1282 pureRenameCopy.add(dest)1283ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1284if not self.p4HasMoveCommand:1285p4_edit(dest)# with move: already open, writable1286 filesToChangeExecBit[dest] = diff['dst_mode']1287if not self.p4HasMoveCommand:1288if self.isWindows:1289 os.chmod(dest, stat.S_IWRITE)1290 os.unlink(dest)1291 filesToDelete.add(src)1292 editedFiles.add(dest)1293else:1294die("unknown modifier%sfor%s"% (modifier, path))12951296 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1297 patchcmd = diffcmd +" | git apply "1298 tryPatchCmd = patchcmd +"--check -"1299 applyPatchCmd = patchcmd +"--check --apply -"1300 patch_succeeded =True13011302if os.system(tryPatchCmd) !=0:1303 fixed_rcs_keywords =False1304 patch_succeeded =False1305print"Unfortunately applying the change failed!"13061307# Patch failed, maybe it's just RCS keyword woes. Look through1308# the patch to see if that's possible.1309ifgitConfigBool("git-p4.attemptRCSCleanup"):1310file=None1311 pattern =None1312 kwfiles = {}1313forfilein editedFiles | filesToDelete:1314# did this file's delta contain RCS keywords?1315 pattern =p4_keywords_regexp_for_file(file)13161317if pattern:1318# this file is a possibility...look for RCS keywords.1319 regexp = re.compile(pattern, re.VERBOSE)1320for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1321if regexp.search(line):1322if verbose:1323print"got keyword match on%sin%sin%s"% (pattern, line,file)1324 kwfiles[file] = pattern1325break13261327forfilein kwfiles:1328if verbose:1329print"zapping%swith%s"% (line,pattern)1330# File is being deleted, so not open in p4. Must1331# disable the read-only bit on windows.1332if self.isWindows andfilenot in editedFiles:1333 os.chmod(file, stat.S_IWRITE)1334 self.patchRCSKeywords(file, kwfiles[file])1335 fixed_rcs_keywords =True13361337if fixed_rcs_keywords:1338print"Retrying the patch with RCS keywords cleaned up"1339if os.system(tryPatchCmd) ==0:1340 patch_succeeded =True13411342if not patch_succeeded:1343for f in editedFiles:1344p4_revert(f)1345return False13461347#1348# Apply the patch for real, and do add/delete/+x handling.1349#1350system(applyPatchCmd)13511352for f in filesToAdd:1353p4_add(f)1354for f in filesToDelete:1355p4_revert(f)1356p4_delete(f)13571358# Set/clear executable bits1359for f in filesToChangeExecBit.keys():1360 mode = filesToChangeExecBit[f]1361setP4ExecBit(f, mode)13621363#1364# Build p4 change description, starting with the contents1365# of the git commit message.1366#1367 logMessage =extractLogMessageFromGitCommit(id)1368 logMessage = logMessage.strip()1369(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13701371 template = self.prepareSubmitTemplate()1372 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13731374if self.preserveUser:1375 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13761377if self.checkAuthorship and not self.p4UserIsMe(p4User):1378 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1379 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1380 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13811382 separatorLine ="######## everything below this line is just the diff #######\n"13831384# diff1385if os.environ.has_key("P4DIFF"):1386del(os.environ["P4DIFF"])1387 diff =""1388for editedFile in editedFiles:1389 diff +=p4_read_pipe(['diff','-du',1390wildcard_encode(editedFile)])13911392# new file diff1393 newdiff =""1394for newFile in filesToAdd:1395 newdiff +="==== new file ====\n"1396 newdiff +="--- /dev/null\n"1397 newdiff +="+++%s\n"% newFile1398 f =open(newFile,"r")1399for line in f.readlines():1400 newdiff +="+"+ line1401 f.close()14021403# change description file: submitTemplate, separatorLine, diff, newdiff1404(handle, fileName) = tempfile.mkstemp()1405 tmpFile = os.fdopen(handle,"w+")1406if self.isWindows:1407 submitTemplate = submitTemplate.replace("\n","\r\n")1408 separatorLine = separatorLine.replace("\n","\r\n")1409 newdiff = newdiff.replace("\n","\r\n")1410 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1411 tmpFile.close()14121413if self.prepare_p4_only:1414#1415# Leave the p4 tree prepared, and the submit template around1416# and let the user decide what to do next1417#1418print1419print"P4 workspace prepared for submission."1420print"To submit or revert, go to client workspace"1421print" "+ self.clientPath1422print1423print"To submit, use\"p4 submit\"to write a new description,"1424print"or\"p4 submit -i%s\"to use the one prepared by" \1425"\"git p4\"."% fileName1426print"You can delete the file\"%s\"when finished."% fileName14271428if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1429print"To preserve change ownership by user%s, you must\n" \1430"do\"p4 change -f <change>\"after submitting and\n" \1431"edit the User field."1432if pureRenameCopy:1433print"After submitting, renamed files must be re-synced."1434print"Invoke\"p4 sync -f\"on each of these files:"1435for f in pureRenameCopy:1436print" "+ f14371438print1439print"To revert the changes, use\"p4 revert ...\", and delete"1440print"the submit template file\"%s\""% fileName1441if filesToAdd:1442print"Since the commit adds new files, they must be deleted:"1443for f in filesToAdd:1444print" "+ f1445print1446return True14471448#1449# Let the user edit the change description, then submit it.1450#1451if self.edit_template(fileName):1452# read the edited message and submit1453 ret =True1454 tmpFile =open(fileName,"rb")1455 message = tmpFile.read()1456 tmpFile.close()1457 submitTemplate = message[:message.index(separatorLine)]1458if self.isWindows:1459 submitTemplate = submitTemplate.replace("\r\n","\n")1460p4_write_pipe(['submit','-i'], submitTemplate)14611462if self.preserveUser:1463if p4User:1464# Get last changelist number. Cannot easily get it from1465# the submit command output as the output is1466# unmarshalled.1467 changelist = self.lastP4Changelist()1468 self.modifyChangelistUser(changelist, p4User)14691470# The rename/copy happened by applying a patch that created a1471# new file. This leaves it writable, which confuses p4.1472for f in pureRenameCopy:1473p4_sync(f,"-f")14741475else:1476# skip this patch1477 ret =False1478print"Submission cancelled, undoing p4 changes."1479for f in editedFiles:1480p4_revert(f)1481for f in filesToAdd:1482p4_revert(f)1483 os.remove(f)1484for f in filesToDelete:1485p4_revert(f)14861487 os.remove(fileName)1488return ret14891490# Export git tags as p4 labels. Create a p4 label and then tag1491# with that.1492defexportGitTags(self, gitTags):1493 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1494iflen(validLabelRegexp) ==0:1495 validLabelRegexp = defaultLabelRegexp1496 m = re.compile(validLabelRegexp)14971498for name in gitTags:14991500if not m.match(name):1501if verbose:1502print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1503continue15041505# Get the p4 commit this corresponds to1506 logMessage =extractLogMessageFromGitCommit(name)1507 values =extractSettingsGitLog(logMessage)15081509if not values.has_key('change'):1510# a tag pointing to something not sent to p4; ignore1511if verbose:1512print"git tag%sdoes not give a p4 commit"% name1513continue1514else:1515 changelist = values['change']15161517# Get the tag details.1518 inHeader =True1519 isAnnotated =False1520 body = []1521for l inread_pipe_lines(["git","cat-file","-p", name]):1522 l = l.strip()1523if inHeader:1524if re.match(r'tag\s+', l):1525 isAnnotated =True1526elif re.match(r'\s*$', l):1527 inHeader =False1528continue1529else:1530 body.append(l)15311532if not isAnnotated:1533 body = ["lightweight tag imported by git p4\n"]15341535# Create the label - use the same view as the client spec we are using1536 clientSpec =getClientSpec()15371538 labelTemplate ="Label:%s\n"% name1539 labelTemplate +="Description:\n"1540for b in body:1541 labelTemplate +="\t"+ b +"\n"1542 labelTemplate +="View:\n"1543for mapping in clientSpec.mappings:1544 labelTemplate +="\t%s\n"% mapping.depot_side.path15451546if self.dry_run:1547print"Would create p4 label%sfor tag"% name1548elif self.prepare_p4_only:1549print"Not creating p4 label%sfor tag due to option" \1550" --prepare-p4-only"% name1551else:1552p4_write_pipe(["label","-i"], labelTemplate)15531554# Use the label1555p4_system(["tag","-l", name] +1556["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])15571558if verbose:1559print"created p4 label for tag%s"% name15601561defrun(self, args):1562iflen(args) ==0:1563 self.master =currentGitBranch()1564iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1565die("Detecting current git branch failed!")1566eliflen(args) ==1:1567 self.master = args[0]1568if notbranchExists(self.master):1569die("Branch%sdoes not exist"% self.master)1570else:1571return False15721573 allowSubmit =gitConfig("git-p4.allowSubmit")1574iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1575die("%sis not in git-p4.allowSubmit"% self.master)15761577[upstream, settings] =findUpstreamBranchPoint()1578 self.depotPath = settings['depot-paths'][0]1579iflen(self.origin) ==0:1580 self.origin = upstream15811582if self.preserveUser:1583if not self.canChangeChangelists():1584die("Cannot preserve user names without p4 super-user or admin permissions")15851586# if not set from the command line, try the config file1587if self.conflict_behavior is None:1588 val =gitConfig("git-p4.conflict")1589if val:1590if val not in self.conflict_behavior_choices:1591die("Invalid value '%s' for config git-p4.conflict"% val)1592else:1593 val ="ask"1594 self.conflict_behavior = val15951596if self.verbose:1597print"Origin branch is "+ self.origin15981599iflen(self.depotPath) ==0:1600print"Internal error: cannot locate perforce depot path from existing branches"1601 sys.exit(128)16021603 self.useClientSpec =False1604ifgitConfigBool("git-p4.useclientspec"):1605 self.useClientSpec =True1606if self.useClientSpec:1607 self.clientSpecDirs =getClientSpec()16081609if self.useClientSpec:1610# all files are relative to the client spec1611 self.clientPath =getClientRoot()1612else:1613 self.clientPath =p4Where(self.depotPath)16141615if self.clientPath =="":1616die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)16171618print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1619 self.oldWorkingDirectory = os.getcwd()16201621# ensure the clientPath exists1622 new_client_dir =False1623if not os.path.exists(self.clientPath):1624 new_client_dir =True1625 os.makedirs(self.clientPath)16261627chdir(self.clientPath)1628if self.dry_run:1629print"Would synchronize p4 checkout in%s"% self.clientPath1630else:1631print"Synchronizing p4 checkout..."1632if new_client_dir:1633# old one was destroyed, and maybe nobody told p41634p4_sync("...","-f")1635else:1636p4_sync("...")1637 self.check()16381639 commits = []1640for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1641 commits.append(line.strip())1642 commits.reverse()16431644if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1645 self.checkAuthorship =False1646else:1647 self.checkAuthorship =True16481649if self.preserveUser:1650 self.checkValidP4Users(commits)16511652#1653# Build up a set of options to be passed to diff when1654# submitting each commit to p4.1655#1656if self.detectRenames:1657# command-line -M arg1658 self.diffOpts ="-M"1659else:1660# If not explicitly set check the config variable1661 detectRenames =gitConfig("git-p4.detectRenames")16621663if detectRenames.lower() =="false"or detectRenames =="":1664 self.diffOpts =""1665elif detectRenames.lower() =="true":1666 self.diffOpts ="-M"1667else:1668 self.diffOpts ="-M%s"% detectRenames16691670# no command-line arg for -C or --find-copies-harder, just1671# config variables1672 detectCopies =gitConfig("git-p4.detectCopies")1673if detectCopies.lower() =="false"or detectCopies =="":1674pass1675elif detectCopies.lower() =="true":1676 self.diffOpts +=" -C"1677else:1678 self.diffOpts +=" -C%s"% detectCopies16791680ifgitConfigBool("git-p4.detectCopiesHarder"):1681 self.diffOpts +=" --find-copies-harder"16821683#1684# Apply the commits, one at a time. On failure, ask if should1685# continue to try the rest of the patches, or quit.1686#1687if self.dry_run:1688print"Would apply"1689 applied = []1690 last =len(commits) -11691for i, commit inenumerate(commits):1692if self.dry_run:1693print" ",read_pipe(["git","show","-s",1694"--format=format:%h%s", commit])1695 ok =True1696else:1697 ok = self.applyCommit(commit)1698if ok:1699 applied.append(commit)1700else:1701if self.prepare_p4_only and i < last:1702print"Processing only the first commit due to option" \1703" --prepare-p4-only"1704break1705if i < last:1706 quit =False1707while True:1708# prompt for what to do, or use the option/variable1709if self.conflict_behavior =="ask":1710print"What do you want to do?"1711 response =raw_input("[s]kip this commit but apply"1712" the rest, or [q]uit? ")1713if not response:1714continue1715elif self.conflict_behavior =="skip":1716 response ="s"1717elif self.conflict_behavior =="quit":1718 response ="q"1719else:1720die("Unknown conflict_behavior '%s'"%1721 self.conflict_behavior)17221723if response[0] =="s":1724print"Skipping this commit, but applying the rest"1725break1726if response[0] =="q":1727print"Quitting"1728 quit =True1729break1730if quit:1731break17321733chdir(self.oldWorkingDirectory)17341735if self.dry_run:1736pass1737elif self.prepare_p4_only:1738pass1739eliflen(commits) ==len(applied):1740print"All commits applied!"17411742 sync =P4Sync()1743if self.branch:1744 sync.branch = self.branch1745 sync.run([])17461747 rebase =P4Rebase()1748 rebase.rebase()17491750else:1751iflen(applied) ==0:1752print"No commits applied."1753else:1754print"Applied only the commits marked with '*':"1755for c in commits:1756if c in applied:1757 star ="*"1758else:1759 star =" "1760print star,read_pipe(["git","show","-s",1761"--format=format:%h%s", c])1762print"You will have to do 'git p4 sync' and rebase."17631764ifgitConfigBool("git-p4.exportLabels"):1765 self.exportLabels =True17661767if self.exportLabels:1768 p4Labels =getP4Labels(self.depotPath)1769 gitTags =getGitTags()17701771 missingGitTags = gitTags - p4Labels1772 self.exportGitTags(missingGitTags)17731774# exit with error unless everything applied perfecly1775iflen(commits) !=len(applied):1776 sys.exit(1)17771778return True17791780classView(object):1781"""Represent a p4 view ("p4 help views"), and map files in a1782 repo according to the view."""17831784classPath(object):1785"""A depot or client path, possibly containing wildcards.1786 The only one supported is ... at the end, currently.1787 Initialize with the full path, with //depot or //client."""17881789def__init__(self, path, is_depot):1790 self.path = path1791 self.is_depot = is_depot1792 self.find_wildcards()1793# remember the prefix bit, useful for relative mappings1794 m = re.match("(//[^/]+/)", self.path)1795if not m:1796die("Path%sdoes not start with //prefix/"% self.path)1797 prefix = m.group(1)1798if not self.is_depot:1799# strip //client/ on client paths1800 self.path = self.path[len(prefix):]18011802deffind_wildcards(self):1803"""Make sure wildcards are valid, and set up internal1804 variables."""18051806 self.ends_triple_dot =False1807# There are three wildcards allowed in p4 views1808# (see "p4 help views"). This code knows how to1809# handle "..." (only at the end), but cannot deal with1810# "%%n" or "*". Only check the depot_side, as p4 should1811# validate that the client_side matches too.1812if re.search(r'%%[1-9]', self.path):1813die("Can't handle%%n wildcards in view:%s"% self.path)1814if self.path.find("*") >=0:1815die("Can't handle * wildcards in view:%s"% self.path)1816 triple_dot_index = self.path.find("...")1817if triple_dot_index >=0:1818if triple_dot_index !=len(self.path) -3:1819die("Can handle only single ... wildcard, at end:%s"%1820 self.path)1821 self.ends_triple_dot =True18221823defensure_compatible(self, other_path):1824"""Make sure the wildcards agree."""1825if self.ends_triple_dot != other_path.ends_triple_dot:1826die("Both paths must end with ... if either does;\n"+1827"paths:%s %s"% (self.path, other_path.path))18281829defmatch_wildcards(self, test_path):1830"""See if this test_path matches us, and fill in the value1831 of the wildcards if so. Returns a tuple of1832 (True|False, wildcards[]). For now, only the ... at end1833 is supported, so at most one wildcard."""1834if self.ends_triple_dot:1835 dotless = self.path[:-3]1836if test_path.startswith(dotless):1837 wildcard = test_path[len(dotless):]1838return(True, [ wildcard ])1839else:1840if test_path == self.path:1841return(True, [])1842return(False, [])18431844defmatch(self, test_path):1845"""Just return if it matches; don't bother with the wildcards."""1846 b, _ = self.match_wildcards(test_path)1847return b18481849deffill_in_wildcards(self, wildcards):1850"""Return the relative path, with the wildcards filled in1851 if there are any."""1852if self.ends_triple_dot:1853return self.path[:-3] + wildcards[0]1854else:1855return self.path18561857classMapping(object):1858def__init__(self, depot_side, client_side, overlay, exclude):1859# depot_side is without the trailing /... if it had one1860 self.depot_side = View.Path(depot_side, is_depot=True)1861 self.client_side = View.Path(client_side, is_depot=False)1862 self.overlay = overlay # started with "+"1863 self.exclude = exclude # started with "-"1864assert not(self.overlay and self.exclude)1865 self.depot_side.ensure_compatible(self.client_side)18661867def__str__(self):1868 c =" "1869if self.overlay:1870 c ="+"1871if self.exclude:1872 c ="-"1873return"View.Mapping:%s%s->%s"% \1874(c, self.depot_side.path, self.client_side.path)18751876defmap_depot_to_client(self, depot_path):1877"""Calculate the client path if using this mapping on the1878 given depot path; does not consider the effect of other1879 mappings in a view. Even excluded mappings are returned."""1880 matches, wildcards = self.depot_side.match_wildcards(depot_path)1881if not matches:1882return""1883 client_path = self.client_side.fill_in_wildcards(wildcards)1884return client_path18851886#1887# View methods1888#1889def__init__(self):1890 self.mappings = []18911892defappend(self, view_line):1893"""Parse a view line, splitting it into depot and client1894 sides. Append to self.mappings, preserving order."""18951896# Split the view line into exactly two words. P4 enforces1897# structure on these lines that simplifies this quite a bit.1898#1899# Either or both words may be double-quoted.1900# Single quotes do not matter.1901# Double-quote marks cannot occur inside the words.1902# A + or - prefix is also inside the quotes.1903# There are no quotes unless they contain a space.1904# The line is already white-space stripped.1905# The two words are separated by a single space.1906#1907if view_line[0] =='"':1908# First word is double quoted. Find its end.1909 close_quote_index = view_line.find('"',1)1910if close_quote_index <=0:1911die("No first-word closing quote found:%s"% view_line)1912 depot_side = view_line[1:close_quote_index]1913# skip closing quote and space1914 rhs_index = close_quote_index +1+11915else:1916 space_index = view_line.find(" ")1917if space_index <=0:1918die("No word-splitting space found:%s"% view_line)1919 depot_side = view_line[0:space_index]1920 rhs_index = space_index +119211922if view_line[rhs_index] =='"':1923# Second word is double quoted. Make sure there is a1924# double quote at the end too.1925if not view_line.endswith('"'):1926die("View line with rhs quote should end with one:%s"%1927 view_line)1928# skip the quotes1929 client_side = view_line[rhs_index+1:-1]1930else:1931 client_side = view_line[rhs_index:]19321933# prefix + means overlay on previous mapping1934 overlay =False1935if depot_side.startswith("+"):1936 overlay =True1937 depot_side = depot_side[1:]19381939# prefix - means exclude this path1940 exclude =False1941if depot_side.startswith("-"):1942 exclude =True1943 depot_side = depot_side[1:]19441945 m = View.Mapping(depot_side, client_side, overlay, exclude)1946 self.mappings.append(m)19471948defmap_in_client(self, depot_path):1949"""Return the relative location in the client where this1950 depot file should live. Returns "" if the file should1951 not be mapped in the client."""19521953 paths_filled = []1954 client_path =""19551956# look at later entries first1957for m in self.mappings[::-1]:19581959# see where will this path end up in the client1960 p = m.map_depot_to_client(depot_path)19611962if p =="":1963# Depot path does not belong in client. Must remember1964# this, as previous items should not cause files to1965# exist in this path either. Remember that the list is1966# being walked from the end, which has higher precedence.1967# Overlap mappings do not exclude previous mappings.1968if not m.overlay:1969 paths_filled.append(m.client_side)19701971else:1972# This mapping matched; no need to search any further.1973# But, the mapping could be rejected if the client path1974# has already been claimed by an earlier mapping (i.e.1975# one later in the list, which we are walking backwards).1976 already_mapped_in_client =False1977for f in paths_filled:1978# this is View.Path.match1979if f.match(p):1980 already_mapped_in_client =True1981break1982if not already_mapped_in_client:1983# Include this file, unless it is from a line that1984# explicitly said to exclude it.1985if not m.exclude:1986 client_path = p19871988# a match, even if rejected, always stops the search1989break19901991return client_path19921993classP4Sync(Command, P4UserMap):1994 delete_actions = ("delete","move/delete","purge")19951996def__init__(self):1997 Command.__init__(self)1998 P4UserMap.__init__(self)1999 self.options = [2000 optparse.make_option("--branch", dest="branch"),2001 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),2002 optparse.make_option("--changesfile", dest="changesFile"),2003 optparse.make_option("--silent", dest="silent", action="store_true"),2004 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),2005 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),2006 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",2007help="Import into refs/heads/ , not refs/remotes"),2008 optparse.make_option("--max-changes", dest="maxChanges"),2009 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',2010help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),2011 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',2012help="Only sync files that are included in the Perforce Client Spec")2013]2014 self.description ="""Imports from Perforce into a git repository.\n2015 example:2016 //depot/my/project/ -- to import the current head2017 //depot/my/project/@all -- to import everything2018 //depot/my/project/@1,6 -- to import only from revision 1 to 620192020 (a ... is not needed in the path p4 specification, it's added implicitly)"""20212022 self.usage +=" //depot/path[@revRange]"2023 self.silent =False2024 self.createdBranches =set()2025 self.committedChanges =set()2026 self.branch =""2027 self.detectBranches =False2028 self.detectLabels =False2029 self.importLabels =False2030 self.changesFile =""2031 self.syncWithOrigin =True2032 self.importIntoRemotes =True2033 self.maxChanges =""2034 self.keepRepoPath =False2035 self.depotPaths =None2036 self.p4BranchesInGit = []2037 self.cloneExclude = []2038 self.useClientSpec =False2039 self.useClientSpec_from_options =False2040 self.clientSpecDirs =None2041 self.tempBranches = []2042 self.tempBranchLocation ="git-p4-tmp"20432044ifgitConfig("git-p4.syncFromOrigin") =="false":2045 self.syncWithOrigin =False20462047# Force a checkpoint in fast-import and wait for it to finish2048defcheckpoint(self):2049 self.gitStream.write("checkpoint\n\n")2050 self.gitStream.write("progress checkpoint\n\n")2051 out = self.gitOutput.readline()2052if self.verbose:2053print"checkpoint finished: "+ out20542055defextractFilesFromCommit(self, commit):2056 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2057for path in self.cloneExclude]2058 files = []2059 fnum =02060while commit.has_key("depotFile%s"% fnum):2061 path = commit["depotFile%s"% fnum]20622063if[p for p in self.cloneExclude2064ifp4PathStartsWith(path, p)]:2065 found =False2066else:2067 found = [p for p in self.depotPaths2068ifp4PathStartsWith(path, p)]2069if not found:2070 fnum = fnum +12071continue20722073file= {}2074file["path"] = path2075file["rev"] = commit["rev%s"% fnum]2076file["action"] = commit["action%s"% fnum]2077file["type"] = commit["type%s"% fnum]2078 files.append(file)2079 fnum = fnum +12080return files20812082defstripRepoPath(self, path, prefixes):2083"""When streaming files, this is called to map a p4 depot path2084 to where it should go in git. The prefixes are either2085 self.depotPaths, or self.branchPrefixes in the case of2086 branch detection."""20872088if self.useClientSpec:2089# branch detection moves files up a level (the branch name)2090# from what client spec interpretation gives2091 path = self.clientSpecDirs.map_in_client(path)2092if self.detectBranches:2093for b in self.knownBranches:2094if path.startswith(b +"/"):2095 path = path[len(b)+1:]20962097elif self.keepRepoPath:2098# Preserve everything in relative path name except leading2099# //depot/; just look at first prefix as they all should2100# be in the same depot.2101 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2102ifp4PathStartsWith(path, depot):2103 path = path[len(depot):]21042105else:2106for p in prefixes:2107ifp4PathStartsWith(path, p):2108 path = path[len(p):]2109break21102111 path =wildcard_decode(path)2112return path21132114defsplitFilesIntoBranches(self, commit):2115"""Look at each depotFile in the commit to figure out to what2116 branch it belongs."""21172118 branches = {}2119 fnum =02120while commit.has_key("depotFile%s"% fnum):2121 path = commit["depotFile%s"% fnum]2122 found = [p for p in self.depotPaths2123ifp4PathStartsWith(path, p)]2124if not found:2125 fnum = fnum +12126continue21272128file= {}2129file["path"] = path2130file["rev"] = commit["rev%s"% fnum]2131file["action"] = commit["action%s"% fnum]2132file["type"] = commit["type%s"% fnum]2133 fnum = fnum +121342135# start with the full relative path where this file would2136# go in a p4 client2137if self.useClientSpec:2138 relPath = self.clientSpecDirs.map_in_client(path)2139else:2140 relPath = self.stripRepoPath(path, self.depotPaths)21412142for branch in self.knownBranches.keys():2143# add a trailing slash so that a commit into qt/4.2foo2144# doesn't end up in qt/4.2, e.g.2145if relPath.startswith(branch +"/"):2146if branch not in branches:2147 branches[branch] = []2148 branches[branch].append(file)2149break21502151return branches21522153# output one file from the P4 stream2154# - helper for streamP4Files21552156defstreamOneP4File(self,file, contents):2157 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2158if verbose:2159 sys.stderr.write("%s\n"% relPath)21602161(type_base, type_mods) =split_p4_type(file["type"])21622163 git_mode ="100644"2164if"x"in type_mods:2165 git_mode ="100755"2166if type_base =="symlink":2167 git_mode ="120000"2168# p4 print on a symlink contains "target\n"; remove the newline2169 data =''.join(contents)2170 contents = [data[:-1]]21712172if type_base =="utf16":2173# p4 delivers different text in the python output to -G2174# than it does when using "print -o", or normal p4 client2175# operations. utf16 is converted to ascii or utf8, perhaps.2176# But ascii text saved as -t utf16 is completely mangled.2177# Invoke print -o to get the real contents.2178#2179# On windows, the newlines will always be mangled by print, so put2180# them back too. This is not needed to the cygwin windows version,2181# just the native "NT" type.2182#2183 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2184ifp4_version_string().find("/NT") >=0:2185 text = text.replace("\r\n","\n")2186 contents = [ text ]21872188if type_base =="apple":2189# Apple filetype files will be streamed as a concatenation of2190# its appledouble header and the contents. This is useless2191# on both macs and non-macs. If using "print -q -o xx", it2192# will create "xx" with the data, and "%xx" with the header.2193# This is also not very useful.2194#2195# Ideally, someday, this script can learn how to generate2196# appledouble files directly and import those to git, but2197# non-mac machines can never find a use for apple filetype.2198print"\nIgnoring apple filetype file%s"%file['depotFile']2199return22002201# Note that we do not try to de-mangle keywords on utf16 files,2202# even though in theory somebody may want that.2203 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2204if pattern:2205 regexp = re.compile(pattern, re.VERBOSE)2206 text =''.join(contents)2207 text = regexp.sub(r'$\1$', text)2208 contents = [ text ]22092210 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))22112212# total length...2213 length =02214for d in contents:2215 length = length +len(d)22162217 self.gitStream.write("data%d\n"% length)2218for d in contents:2219 self.gitStream.write(d)2220 self.gitStream.write("\n")22212222defstreamOneP4Deletion(self,file):2223 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2224if verbose:2225 sys.stderr.write("delete%s\n"% relPath)2226 self.gitStream.write("D%s\n"% relPath)22272228# handle another chunk of streaming data2229defstreamP4FilesCb(self, marshalled):22302231# catch p4 errors and complain2232 err =None2233if"code"in marshalled:2234if marshalled["code"] =="error":2235if"data"in marshalled:2236 err = marshalled["data"].rstrip()2237if err:2238 f =None2239if self.stream_have_file_info:2240if"depotFile"in self.stream_file:2241 f = self.stream_file["depotFile"]2242# force a failure in fast-import, else an empty2243# commit will be made2244 self.gitStream.write("\n")2245 self.gitStream.write("die-now\n")2246 self.gitStream.close()2247# ignore errors, but make sure it exits first2248 self.importProcess.wait()2249if f:2250die("Error from p4 print for%s:%s"% (f, err))2251else:2252die("Error from p4 print:%s"% err)22532254if marshalled.has_key('depotFile')and self.stream_have_file_info:2255# start of a new file - output the old one first2256 self.streamOneP4File(self.stream_file, self.stream_contents)2257 self.stream_file = {}2258 self.stream_contents = []2259 self.stream_have_file_info =False22602261# pick up the new file information... for the2262# 'data' field we need to append to our array2263for k in marshalled.keys():2264if k =='data':2265 self.stream_contents.append(marshalled['data'])2266else:2267 self.stream_file[k] = marshalled[k]22682269 self.stream_have_file_info =True22702271# Stream directly from "p4 files" into "git fast-import"2272defstreamP4Files(self, files):2273 filesForCommit = []2274 filesToRead = []2275 filesToDelete = []22762277for f in files:2278# if using a client spec, only add the files that have2279# a path in the client2280if self.clientSpecDirs:2281if self.clientSpecDirs.map_in_client(f['path']) =="":2282continue22832284 filesForCommit.append(f)2285if f['action']in self.delete_actions:2286 filesToDelete.append(f)2287else:2288 filesToRead.append(f)22892290# deleted files...2291for f in filesToDelete:2292 self.streamOneP4Deletion(f)22932294iflen(filesToRead) >0:2295 self.stream_file = {}2296 self.stream_contents = []2297 self.stream_have_file_info =False22982299# curry self argument2300defstreamP4FilesCbSelf(entry):2301 self.streamP4FilesCb(entry)23022303 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]23042305p4CmdList(["-x","-","print"],2306 stdin=fileArgs,2307 cb=streamP4FilesCbSelf)23082309# do the last chunk2310if self.stream_file.has_key('depotFile'):2311 self.streamOneP4File(self.stream_file, self.stream_contents)23122313defmake_email(self, userid):2314if userid in self.users:2315return self.users[userid]2316else:2317return"%s<a@b>"% userid23182319# Stream a p4 tag2320defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2321if verbose:2322print"writing tag%sfor commit%s"% (labelName, commit)2323 gitStream.write("tag%s\n"% labelName)2324 gitStream.write("from%s\n"% commit)23252326if labelDetails.has_key('Owner'):2327 owner = labelDetails["Owner"]2328else:2329 owner =None23302331# Try to use the owner of the p4 label, or failing that,2332# the current p4 user id.2333if owner:2334 email = self.make_email(owner)2335else:2336 email = self.make_email(self.p4UserId())2337 tagger ="%s %s %s"% (email, epoch, self.tz)23382339 gitStream.write("tagger%s\n"% tagger)23402341print"labelDetails=",labelDetails2342if labelDetails.has_key('Description'):2343 description = labelDetails['Description']2344else:2345 description ='Label from git p4'23462347 gitStream.write("data%d\n"%len(description))2348 gitStream.write(description)2349 gitStream.write("\n")23502351defcommit(self, details, files, branch, parent =""):2352 epoch = details["time"]2353 author = details["user"]23542355if self.verbose:2356print"commit into%s"% branch23572358# start with reading files; if that fails, we should not2359# create a commit.2360 new_files = []2361for f in files:2362if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2363 new_files.append(f)2364else:2365 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23662367 self.gitStream.write("commit%s\n"% branch)2368# gitStream.write("mark :%s\n" % details["change"])2369 self.committedChanges.add(int(details["change"]))2370 committer =""2371if author not in self.users:2372 self.getUserMapFromPerforceServer()2373 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23742375 self.gitStream.write("committer%s\n"% committer)23762377 self.gitStream.write("data <<EOT\n")2378 self.gitStream.write(details["desc"])2379 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2380(','.join(self.branchPrefixes), details["change"]))2381iflen(details['options']) >0:2382 self.gitStream.write(": options =%s"% details['options'])2383 self.gitStream.write("]\nEOT\n\n")23842385iflen(parent) >0:2386if self.verbose:2387print"parent%s"% parent2388 self.gitStream.write("from%s\n"% parent)23892390 self.streamP4Files(new_files)2391 self.gitStream.write("\n")23922393 change =int(details["change"])23942395if self.labels.has_key(change):2396 label = self.labels[change]2397 labelDetails = label[0]2398 labelRevisions = label[1]2399if self.verbose:2400print"Change%sis labelled%s"% (change, labelDetails)24012402 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2403for p in self.branchPrefixes])24042405iflen(files) ==len(labelRevisions):24062407 cleanedFiles = {}2408for info in files:2409if info["action"]in self.delete_actions:2410continue2411 cleanedFiles[info["depotFile"]] = info["rev"]24122413if cleanedFiles == labelRevisions:2414 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)24152416else:2417if not self.silent:2418print("Tag%sdoes not match with change%s: files do not match."2419% (labelDetails["label"], change))24202421else:2422if not self.silent:2423print("Tag%sdoes not match with change%s: file count is different."2424% (labelDetails["label"], change))24252426# Build a dictionary of changelists and labels, for "detect-labels" option.2427defgetLabels(self):2428 self.labels = {}24292430 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2431iflen(l) >0and not self.silent:2432print"Finding files belonging to labels in%s"% `self.depotPaths`24332434for output in l:2435 label = output["label"]2436 revisions = {}2437 newestChange =02438if self.verbose:2439print"Querying files for label%s"% label2440forfileinp4CmdList(["files"] +2441["%s...@%s"% (p, label)2442for p in self.depotPaths]):2443 revisions[file["depotFile"]] =file["rev"]2444 change =int(file["change"])2445if change > newestChange:2446 newestChange = change24472448 self.labels[newestChange] = [output, revisions]24492450if self.verbose:2451print"Label changes:%s"% self.labels.keys()24522453# Import p4 labels as git tags. A direct mapping does not2454# exist, so assume that if all the files are at the same revision2455# then we can use that, or it's something more complicated we should2456# just ignore.2457defimportP4Labels(self, stream, p4Labels):2458if verbose:2459print"import p4 labels: "+' '.join(p4Labels)24602461 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2462 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2463iflen(validLabelRegexp) ==0:2464 validLabelRegexp = defaultLabelRegexp2465 m = re.compile(validLabelRegexp)24662467for name in p4Labels:2468 commitFound =False24692470if not m.match(name):2471if verbose:2472print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2473continue24742475if name in ignoredP4Labels:2476continue24772478 labelDetails =p4CmdList(['label',"-o", name])[0]24792480# get the most recent changelist for each file in this label2481 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2482for p in self.depotPaths])24832484if change.has_key('change'):2485# find the corresponding git commit; take the oldest commit2486 changelist =int(change['change'])2487 gitCommit =read_pipe(["git","rev-list","--max-count=1",2488"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2489iflen(gitCommit) ==0:2490print"could not find git commit for changelist%d"% changelist2491else:2492 gitCommit = gitCommit.strip()2493 commitFound =True2494# Convert from p4 time format2495try:2496 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2497exceptValueError:2498print"Could not convert label time%s"% labelDetails['Update']2499 tmwhen =125002501 when =int(time.mktime(tmwhen))2502 self.streamTag(stream, name, labelDetails, gitCommit, when)2503if verbose:2504print"p4 label%smapped to git commit%s"% (name, gitCommit)2505else:2506if verbose:2507print"Label%shas no changelists - possibly deleted?"% name25082509if not commitFound:2510# We can't import this label; don't try again as it will get very2511# expensive repeatedly fetching all the files for labels that will2512# never be imported. If the label is moved in the future, the2513# ignore will need to be removed manually.2514system(["git","config","--add","git-p4.ignoredP4Labels", name])25152516defguessProjectName(self):2517for p in self.depotPaths:2518if p.endswith("/"):2519 p = p[:-1]2520 p = p[p.strip().rfind("/") +1:]2521if not p.endswith("/"):2522 p +="/"2523return p25242525defgetBranchMapping(self):2526 lostAndFoundBranches =set()25272528 user =gitConfig("git-p4.branchUser")2529iflen(user) >0:2530 command ="branches -u%s"% user2531else:2532 command ="branches"25332534for info inp4CmdList(command):2535 details =p4Cmd(["branch","-o", info["branch"]])2536 viewIdx =02537while details.has_key("View%s"% viewIdx):2538 paths = details["View%s"% viewIdx].split(" ")2539 viewIdx = viewIdx +12540# require standard //depot/foo/... //depot/bar/... mapping2541iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2542continue2543 source = paths[0]2544 destination = paths[1]2545## HACK2546ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2547 source = source[len(self.depotPaths[0]):-4]2548 destination = destination[len(self.depotPaths[0]):-4]25492550if destination in self.knownBranches:2551if not self.silent:2552print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2553print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2554continue25552556 self.knownBranches[destination] = source25572558 lostAndFoundBranches.discard(destination)25592560if source not in self.knownBranches:2561 lostAndFoundBranches.add(source)25622563# Perforce does not strictly require branches to be defined, so we also2564# check git config for a branch list.2565#2566# Example of branch definition in git config file:2567# [git-p4]2568# branchList=main:branchA2569# branchList=main:branchB2570# branchList=branchA:branchC2571 configBranches =gitConfigList("git-p4.branchList")2572for branch in configBranches:2573if branch:2574(source, destination) = branch.split(":")2575 self.knownBranches[destination] = source25762577 lostAndFoundBranches.discard(destination)25782579if source not in self.knownBranches:2580 lostAndFoundBranches.add(source)258125822583for branch in lostAndFoundBranches:2584 self.knownBranches[branch] = branch25852586defgetBranchMappingFromGitBranches(self):2587 branches =p4BranchesInGit(self.importIntoRemotes)2588for branch in branches.keys():2589if branch =="master":2590 branch ="main"2591else:2592 branch = branch[len(self.projectName):]2593 self.knownBranches[branch] = branch25942595defupdateOptionDict(self, d):2596 option_keys = {}2597if self.keepRepoPath:2598 option_keys['keepRepoPath'] =125992600 d["options"] =' '.join(sorted(option_keys.keys()))26012602defreadOptions(self, d):2603 self.keepRepoPath = (d.has_key('options')2604and('keepRepoPath'in d['options']))26052606defgitRefForBranch(self, branch):2607if branch =="main":2608return self.refPrefix +"master"26092610iflen(branch) <=0:2611return branch26122613return self.refPrefix + self.projectName + branch26142615defgitCommitByP4Change(self, ref, change):2616if self.verbose:2617print"looking in ref "+ ref +" for change%susing bisect..."% change26182619 earliestCommit =""2620 latestCommit =parseRevision(ref)26212622while True:2623if self.verbose:2624print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2625 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2626iflen(next) ==0:2627if self.verbose:2628print"argh"2629return""2630 log =extractLogMessageFromGitCommit(next)2631 settings =extractSettingsGitLog(log)2632 currentChange =int(settings['change'])2633if self.verbose:2634print"current change%s"% currentChange26352636if currentChange == change:2637if self.verbose:2638print"found%s"% next2639return next26402641if currentChange < change:2642 earliestCommit ="^%s"% next2643else:2644 latestCommit ="%s"% next26452646return""26472648defimportNewBranch(self, branch, maxChange):2649# make fast-import flush all changes to disk and update the refs using the checkpoint2650# command so that we can try to find the branch parent in the git history2651 self.gitStream.write("checkpoint\n\n");2652 self.gitStream.flush();2653 branchPrefix = self.depotPaths[0] + branch +"/"2654range="@1,%s"% maxChange2655#print "prefix" + branchPrefix2656 changes =p4ChangesForPaths([branchPrefix],range)2657iflen(changes) <=0:2658return False2659 firstChange = changes[0]2660#print "first change in branch: %s" % firstChange2661 sourceBranch = self.knownBranches[branch]2662 sourceDepotPath = self.depotPaths[0] + sourceBranch2663 sourceRef = self.gitRefForBranch(sourceBranch)2664#print "source " + sourceBranch26652666 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2667#print "branch parent: %s" % branchParentChange2668 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2669iflen(gitParent) >0:2670 self.initialParents[self.gitRefForBranch(branch)] = gitParent2671#print "parent git commit: %s" % gitParent26722673 self.importChanges(changes)2674return True26752676defsearchParent(self, parent, branch, target):2677 parentFound =False2678for blob inread_pipe_lines(["git","rev-list","--reverse",2679"--no-merges", parent]):2680 blob = blob.strip()2681iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2682 parentFound =True2683if self.verbose:2684print"Found parent of%sin commit%s"% (branch, blob)2685break2686if parentFound:2687return blob2688else:2689return None26902691defimportChanges(self, changes):2692 cnt =12693for change in changes:2694 description =p4_describe(change)2695 self.updateOptionDict(description)26962697if not self.silent:2698 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2699 sys.stdout.flush()2700 cnt = cnt +127012702try:2703if self.detectBranches:2704 branches = self.splitFilesIntoBranches(description)2705for branch in branches.keys():2706## HACK --hwn2707 branchPrefix = self.depotPaths[0] + branch +"/"2708 self.branchPrefixes = [ branchPrefix ]27092710 parent =""27112712 filesForCommit = branches[branch]27132714if self.verbose:2715print"branch is%s"% branch27162717 self.updatedBranches.add(branch)27182719if branch not in self.createdBranches:2720 self.createdBranches.add(branch)2721 parent = self.knownBranches[branch]2722if parent == branch:2723 parent =""2724else:2725 fullBranch = self.projectName + branch2726if fullBranch not in self.p4BranchesInGit:2727if not self.silent:2728print("\nImporting new branch%s"% fullBranch);2729if self.importNewBranch(branch, change -1):2730 parent =""2731 self.p4BranchesInGit.append(fullBranch)2732if not self.silent:2733print("\nResuming with change%s"% change);27342735if self.verbose:2736print"parent determined through known branches:%s"% parent27372738 branch = self.gitRefForBranch(branch)2739 parent = self.gitRefForBranch(parent)27402741if self.verbose:2742print"looking for initial parent for%s; current parent is%s"% (branch, parent)27432744iflen(parent) ==0and branch in self.initialParents:2745 parent = self.initialParents[branch]2746del self.initialParents[branch]27472748 blob =None2749iflen(parent) >0:2750 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2751if self.verbose:2752print"Creating temporary branch: "+ tempBranch2753 self.commit(description, filesForCommit, tempBranch)2754 self.tempBranches.append(tempBranch)2755 self.checkpoint()2756 blob = self.searchParent(parent, branch, tempBranch)2757if blob:2758 self.commit(description, filesForCommit, branch, blob)2759else:2760if self.verbose:2761print"Parent of%snot found. Committing into head of%s"% (branch, parent)2762 self.commit(description, filesForCommit, branch, parent)2763else:2764 files = self.extractFilesFromCommit(description)2765 self.commit(description, files, self.branch,2766 self.initialParent)2767# only needed once, to connect to the previous commit2768 self.initialParent =""2769exceptIOError:2770print self.gitError.read()2771 sys.exit(1)27722773defimportHeadRevision(self, revision):2774print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27752776 details = {}2777 details["user"] ="git perforce import user"2778 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2779% (' '.join(self.depotPaths), revision))2780 details["change"] = revision2781 newestRevision =027822783 fileCnt =02784 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27852786for info inp4CmdList(["files"] + fileArgs):27872788if'code'in info and info['code'] =='error':2789 sys.stderr.write("p4 returned an error:%s\n"2790% info['data'])2791if info['data'].find("must refer to client") >=0:2792 sys.stderr.write("This particular p4 error is misleading.\n")2793 sys.stderr.write("Perhaps the depot path was misspelled.\n");2794 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2795 sys.exit(1)2796if'p4ExitCode'in info:2797 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2798 sys.exit(1)279928002801 change =int(info["change"])2802if change > newestRevision:2803 newestRevision = change28042805if info["action"]in self.delete_actions:2806# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2807#fileCnt = fileCnt + 12808continue28092810for prop in["depotFile","rev","action","type"]:2811 details["%s%s"% (prop, fileCnt)] = info[prop]28122813 fileCnt = fileCnt +128142815 details["change"] = newestRevision28162817# Use time from top-most change so that all git p4 clones of2818# the same p4 repo have the same commit SHA1s.2819 res =p4_describe(newestRevision)2820 details["time"] = res["time"]28212822 self.updateOptionDict(details)2823try:2824 self.commit(details, self.extractFilesFromCommit(details), self.branch)2825exceptIOError:2826print"IO error with git fast-import. Is your git version recent enough?"2827print self.gitError.read()282828292830defrun(self, args):2831 self.depotPaths = []2832 self.changeRange =""2833 self.previousDepotPaths = []2834 self.hasOrigin =False28352836# map from branch depot path to parent branch2837 self.knownBranches = {}2838 self.initialParents = {}28392840if self.importIntoRemotes:2841 self.refPrefix ="refs/remotes/p4/"2842else:2843 self.refPrefix ="refs/heads/p4/"28442845if self.syncWithOrigin:2846 self.hasOrigin =originP4BranchesExist()2847if self.hasOrigin:2848if not self.silent:2849print'Syncing with origin first, using "git fetch origin"'2850system("git fetch origin")28512852 branch_arg_given =bool(self.branch)2853iflen(self.branch) ==0:2854 self.branch = self.refPrefix +"master"2855ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2856system("git update-ref%srefs/heads/p4"% self.branch)2857system("git branch -D p4")28582859# accept either the command-line option, or the configuration variable2860if self.useClientSpec:2861# will use this after clone to set the variable2862 self.useClientSpec_from_options =True2863else:2864ifgitConfigBool("git-p4.useclientspec"):2865 self.useClientSpec =True2866if self.useClientSpec:2867 self.clientSpecDirs =getClientSpec()28682869# TODO: should always look at previous commits,2870# merge with previous imports, if possible.2871if args == []:2872if self.hasOrigin:2873createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28742875# branches holds mapping from branch name to sha12876 branches =p4BranchesInGit(self.importIntoRemotes)28772878# restrict to just this one, disabling detect-branches2879if branch_arg_given:2880 short = self.branch.split("/")[-1]2881if short in branches:2882 self.p4BranchesInGit = [ short ]2883else:2884 self.p4BranchesInGit = branches.keys()28852886iflen(self.p4BranchesInGit) >1:2887if not self.silent:2888print"Importing from/into multiple branches"2889 self.detectBranches =True2890for branch in branches.keys():2891 self.initialParents[self.refPrefix + branch] = \2892 branches[branch]28932894if self.verbose:2895print"branches:%s"% self.p4BranchesInGit28962897 p4Change =02898for branch in self.p4BranchesInGit:2899 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)29002901 settings =extractSettingsGitLog(logMsg)29022903 self.readOptions(settings)2904if(settings.has_key('depot-paths')2905and settings.has_key('change')):2906 change =int(settings['change']) +12907 p4Change =max(p4Change, change)29082909 depotPaths =sorted(settings['depot-paths'])2910if self.previousDepotPaths == []:2911 self.previousDepotPaths = depotPaths2912else:2913 paths = []2914for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2915 prev_list = prev.split("/")2916 cur_list = cur.split("/")2917for i inrange(0,min(len(cur_list),len(prev_list))):2918if cur_list[i] <> prev_list[i]:2919 i = i -12920break29212922 paths.append("/".join(cur_list[:i +1]))29232924 self.previousDepotPaths = paths29252926if p4Change >0:2927 self.depotPaths =sorted(self.previousDepotPaths)2928 self.changeRange ="@%s,#head"% p4Change2929if not self.silent and not self.detectBranches:2930print"Performing incremental import into%sgit branch"% self.branch29312932# accept multiple ref name abbreviations:2933# refs/foo/bar/branch -> use it exactly2934# p4/branch -> prepend refs/remotes/ or refs/heads/2935# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2936if not self.branch.startswith("refs/"):2937if self.importIntoRemotes:2938 prepend ="refs/remotes/"2939else:2940 prepend ="refs/heads/"2941if not self.branch.startswith("p4/"):2942 prepend +="p4/"2943 self.branch = prepend + self.branch29442945iflen(args) ==0and self.depotPaths:2946if not self.silent:2947print"Depot paths:%s"%' '.join(self.depotPaths)2948else:2949if self.depotPaths and self.depotPaths != args:2950print("previous import used depot path%sand now%swas specified. "2951"This doesn't work!"% (' '.join(self.depotPaths),2952' '.join(args)))2953 sys.exit(1)29542955 self.depotPaths =sorted(args)29562957 revision =""2958 self.users = {}29592960# Make sure no revision specifiers are used when --changesfile2961# is specified.2962 bad_changesfile =False2963iflen(self.changesFile) >0:2964for p in self.depotPaths:2965if p.find("@") >=0or p.find("#") >=0:2966 bad_changesfile =True2967break2968if bad_changesfile:2969die("Option --changesfile is incompatible with revision specifiers")29702971 newPaths = []2972for p in self.depotPaths:2973if p.find("@") != -1:2974 atIdx = p.index("@")2975 self.changeRange = p[atIdx:]2976if self.changeRange =="@all":2977 self.changeRange =""2978elif','not in self.changeRange:2979 revision = self.changeRange2980 self.changeRange =""2981 p = p[:atIdx]2982elif p.find("#") != -1:2983 hashIdx = p.index("#")2984 revision = p[hashIdx:]2985 p = p[:hashIdx]2986elif self.previousDepotPaths == []:2987# pay attention to changesfile, if given, else import2988# the entire p4 tree at the head revision2989iflen(self.changesFile) ==0:2990 revision ="#head"29912992 p = re.sub("\.\.\.$","", p)2993if not p.endswith("/"):2994 p +="/"29952996 newPaths.append(p)29972998 self.depotPaths = newPaths29993000# --detect-branches may change this for each branch3001 self.branchPrefixes = self.depotPaths30023003 self.loadUserMapFromCache()3004 self.labels = {}3005if self.detectLabels:3006 self.getLabels();30073008if self.detectBranches:3009## FIXME - what's a P4 projectName ?3010 self.projectName = self.guessProjectName()30113012if self.hasOrigin:3013 self.getBranchMappingFromGitBranches()3014else:3015 self.getBranchMapping()3016if self.verbose:3017print"p4-git branches:%s"% self.p4BranchesInGit3018print"initial parents:%s"% self.initialParents3019for b in self.p4BranchesInGit:3020if b !="master":30213022## FIXME3023 b = b[len(self.projectName):]3024 self.createdBranches.add(b)30253026 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30273028 self.importProcess = subprocess.Popen(["git","fast-import"],3029 stdin=subprocess.PIPE,3030 stdout=subprocess.PIPE,3031 stderr=subprocess.PIPE);3032 self.gitOutput = self.importProcess.stdout3033 self.gitStream = self.importProcess.stdin3034 self.gitError = self.importProcess.stderr30353036if revision:3037 self.importHeadRevision(revision)3038else:3039 changes = []30403041iflen(self.changesFile) >0:3042 output =open(self.changesFile).readlines()3043 changeSet =set()3044for line in output:3045 changeSet.add(int(line))30463047for change in changeSet:3048 changes.append(change)30493050 changes.sort()3051else:3052# catch "git p4 sync" with no new branches, in a repo that3053# does not have any existing p4 branches3054iflen(args) ==0:3055if not self.p4BranchesInGit:3056die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30573058# The default branch is master, unless --branch is used to3059# specify something else. Make sure it exists, or complain3060# nicely about how to use --branch.3061if not self.detectBranches:3062if notbranch_exists(self.branch):3063if branch_arg_given:3064die("Error: branch%sdoes not exist."% self.branch)3065else:3066die("Error: no branch%s; perhaps specify one with --branch."%3067 self.branch)30683069if self.verbose:3070print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3071 self.changeRange)3072 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30733074iflen(self.maxChanges) >0:3075 changes = changes[:min(int(self.maxChanges),len(changes))]30763077iflen(changes) ==0:3078if not self.silent:3079print"No changes to import!"3080else:3081if not self.silent and not self.detectBranches:3082print"Import destination:%s"% self.branch30833084 self.updatedBranches =set()30853086if not self.detectBranches:3087if args:3088# start a new branch3089 self.initialParent =""3090else:3091# build on a previous revision3092 self.initialParent =parseRevision(self.branch)30933094 self.importChanges(changes)30953096if not self.silent:3097print""3098iflen(self.updatedBranches) >0:3099 sys.stdout.write("Updated branches: ")3100for b in self.updatedBranches:3101 sys.stdout.write("%s"% b)3102 sys.stdout.write("\n")31033104ifgitConfigBool("git-p4.importLabels"):3105 self.importLabels =True31063107if self.importLabels:3108 p4Labels =getP4Labels(self.depotPaths)3109 gitTags =getGitTags()31103111 missingP4Labels = p4Labels - gitTags3112 self.importP4Labels(self.gitStream, missingP4Labels)31133114 self.gitStream.close()3115if self.importProcess.wait() !=0:3116die("fast-import failed:%s"% self.gitError.read())3117 self.gitOutput.close()3118 self.gitError.close()31193120# Cleanup temporary branches created during import3121if self.tempBranches != []:3122for branch in self.tempBranches:3123read_pipe("git update-ref -d%s"% branch)3124 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31253126# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3127# a convenient shortcut refname "p4".3128if self.importIntoRemotes:3129 head_ref = self.refPrefix +"HEAD"3130if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3131system(["git","symbolic-ref", head_ref, self.branch])31323133return True31343135classP4Rebase(Command):3136def__init__(self):3137 Command.__init__(self)3138 self.options = [3139 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3140]3141 self.importLabels =False3142 self.description = ("Fetches the latest revision from perforce and "3143+"rebases the current work (branch) against it")31443145defrun(self, args):3146 sync =P4Sync()3147 sync.importLabels = self.importLabels3148 sync.run([])31493150return self.rebase()31513152defrebase(self):3153if os.system("git update-index --refresh") !=0:3154die("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.");3155iflen(read_pipe("git diff-index HEAD --")) >0:3156die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");31573158[upstream, settings] =findUpstreamBranchPoint()3159iflen(upstream) ==0:3160die("Cannot find upstream branchpoint for rebase")31613162# the branchpoint may be p4/foo~3, so strip off the parent3163 upstream = re.sub("~[0-9]+$","", upstream)31643165print"Rebasing the current branch onto%s"% upstream3166 oldHead =read_pipe("git rev-parse HEAD").strip()3167system("git rebase%s"% upstream)3168system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3169return True31703171classP4Clone(P4Sync):3172def__init__(self):3173 P4Sync.__init__(self)3174 self.description ="Creates a new git repository and imports from Perforce into it"3175 self.usage ="usage: %prog [options] //depot/path[@revRange]"3176 self.options += [3177 optparse.make_option("--destination", dest="cloneDestination",3178 action='store', default=None,3179help="where to leave result of the clone"),3180 optparse.make_option("-/", dest="cloneExclude",3181 action="append",type="string",3182help="exclude depot path"),3183 optparse.make_option("--bare", dest="cloneBare",3184 action="store_true", default=False),3185]3186 self.cloneDestination =None3187 self.needsGit =False3188 self.cloneBare =False31893190# This is required for the "append" cloneExclude action3191defensure_value(self, attr, value):3192if nothasattr(self, attr)orgetattr(self, attr)is None:3193setattr(self, attr, value)3194returngetattr(self, attr)31953196defdefaultDestination(self, args):3197## TODO: use common prefix of args?3198 depotPath = args[0]3199 depotDir = re.sub("(@[^@]*)$","", depotPath)3200 depotDir = re.sub("(#[^#]*)$","", depotDir)3201 depotDir = re.sub(r"\.\.\.$","", depotDir)3202 depotDir = re.sub(r"/$","", depotDir)3203return os.path.split(depotDir)[1]32043205defrun(self, args):3206iflen(args) <1:3207return False32083209if self.keepRepoPath and not self.cloneDestination:3210 sys.stderr.write("Must specify destination for --keep-path\n")3211 sys.exit(1)32123213 depotPaths = args32143215if not self.cloneDestination andlen(depotPaths) >1:3216 self.cloneDestination = depotPaths[-1]3217 depotPaths = depotPaths[:-1]32183219 self.cloneExclude = ["/"+p for p in self.cloneExclude]3220for p in depotPaths:3221if not p.startswith("//"):3222 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3223return False32243225if not self.cloneDestination:3226 self.cloneDestination = self.defaultDestination(args)32273228print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32293230if not os.path.exists(self.cloneDestination):3231 os.makedirs(self.cloneDestination)3232chdir(self.cloneDestination)32333234 init_cmd = ["git","init"]3235if self.cloneBare:3236 init_cmd.append("--bare")3237 retcode = subprocess.call(init_cmd)3238if retcode:3239raiseCalledProcessError(retcode, init_cmd)32403241if not P4Sync.run(self, depotPaths):3242return False32433244# create a master branch and check out a work tree3245ifgitBranchExists(self.branch):3246system(["git","branch","master", self.branch ])3247if not self.cloneBare:3248system(["git","checkout","-f"])3249else:3250print'Not checking out any branch, use ' \3251'"git checkout -q -b master <branch>"'32523253# auto-set this variable if invoked with --use-client-spec3254if self.useClientSpec_from_options:3255system("git config --bool git-p4.useclientspec true")32563257return True32583259classP4Branches(Command):3260def__init__(self):3261 Command.__init__(self)3262 self.options = [ ]3263 self.description = ("Shows the git branches that hold imports and their "3264+"corresponding perforce depot paths")3265 self.verbose =False32663267defrun(self, args):3268iforiginP4BranchesExist():3269createOrUpdateBranchesFromOrigin()32703271 cmdline ="git rev-parse --symbolic "3272 cmdline +=" --remotes"32733274for line inread_pipe_lines(cmdline):3275 line = line.strip()32763277if not line.startswith('p4/')or line =="p4/HEAD":3278continue3279 branch = line32803281 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3282 settings =extractSettingsGitLog(log)32833284print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3285return True32863287classHelpFormatter(optparse.IndentedHelpFormatter):3288def__init__(self):3289 optparse.IndentedHelpFormatter.__init__(self)32903291defformat_description(self, description):3292if description:3293return description +"\n"3294else:3295return""32963297defprintUsage(commands):3298print"usage:%s<command> [options]"% sys.argv[0]3299print""3300print"valid commands:%s"%", ".join(commands)3301print""3302print"Try%s<command> --help for command specific help."% sys.argv[0]3303print""33043305commands = {3306"debug": P4Debug,3307"submit": P4Submit,3308"commit": P4Submit,3309"sync": P4Sync,3310"rebase": P4Rebase,3311"clone": P4Clone,3312"rollback": P4RollBack,3313"branches": P4Branches3314}331533163317defmain():3318iflen(sys.argv[1:]) ==0:3319printUsage(commands.keys())3320 sys.exit(2)33213322 cmdName = sys.argv[1]3323try:3324 klass = commands[cmdName]3325 cmd =klass()3326exceptKeyError:3327print"unknown command%s"% cmdName3328print""3329printUsage(commands.keys())3330 sys.exit(2)33313332 options = cmd.options3333 cmd.gitdir = os.environ.get("GIT_DIR",None)33343335 args = sys.argv[2:]33363337 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3338if cmd.needsGit:3339 options.append(optparse.make_option("--git-dir", dest="gitdir"))33403341 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3342 options,3343 description = cmd.description,3344 formatter =HelpFormatter())33453346(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3347global verbose3348 verbose = cmd.verbose3349if cmd.needsGit:3350if cmd.gitdir ==None:3351 cmd.gitdir = os.path.abspath(".git")3352if notisValidGitDir(cmd.gitdir):3353 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3354if os.path.exists(cmd.gitdir):3355 cdup =read_pipe("git rev-parse --show-cdup").strip()3356iflen(cdup) >0:3357chdir(cdup);33583359if notisValidGitDir(cmd.gitdir):3360ifisValidGitDir(cmd.gitdir +"/.git"):3361 cmd.gitdir +="/.git"3362else:3363die("fatal: cannot locate git repository at%s"% cmd.gitdir)33643365 os.environ["GIT_DIR"] = cmd.gitdir33663367if not cmd.run(args):3368 parser.print_help()3369 sys.exit(2)337033713372if __name__ =='__main__':3373main()