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 26verbose =False 27 28# Only labels/tags matching this will be imported/exported 29defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 30 31defp4_build_cmd(cmd): 32"""Build a suitable p4 command line. 33 34 This consolidates building and returning a p4 command line into one 35 location. It means that hooking into the environment, or other configuration 36 can be done more easily. 37 """ 38 real_cmd = ["p4"] 39 40 user =gitConfig("git-p4.user") 41iflen(user) >0: 42 real_cmd += ["-u",user] 43 44 password =gitConfig("git-p4.password") 45iflen(password) >0: 46 real_cmd += ["-P", password] 47 48 port =gitConfig("git-p4.port") 49iflen(port) >0: 50 real_cmd += ["-p", port] 51 52 host =gitConfig("git-p4.host") 53iflen(host) >0: 54 real_cmd += ["-H", host] 55 56 client =gitConfig("git-p4.client") 57iflen(client) >0: 58 real_cmd += ["-c", client] 59 60 61ifisinstance(cmd,basestring): 62 real_cmd =' '.join(real_cmd) +' '+ cmd 63else: 64 real_cmd += cmd 65return real_cmd 66 67defchdir(dir): 68# P4 uses the PWD environment variable rather than getcwd(). Since we're 69# not using the shell, we have to set it ourselves. This path could 70# be relative, so go there first, then figure out where we ended up. 71 os.chdir(dir) 72 os.environ['PWD'] = os.getcwd() 73 74defdie(msg): 75if verbose: 76raiseException(msg) 77else: 78 sys.stderr.write(msg +"\n") 79 sys.exit(1) 80 81defwrite_pipe(c, stdin): 82if verbose: 83 sys.stderr.write('Writing pipe:%s\n'%str(c)) 84 85 expand =isinstance(c,basestring) 86 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 87 pipe = p.stdin 88 val = pipe.write(stdin) 89 pipe.close() 90if p.wait(): 91die('Command failed:%s'%str(c)) 92 93return val 94 95defp4_write_pipe(c, stdin): 96 real_cmd =p4_build_cmd(c) 97returnwrite_pipe(real_cmd, stdin) 98 99defread_pipe(c, ignore_error=False): 100if verbose: 101 sys.stderr.write('Reading pipe:%s\n'%str(c)) 102 103 expand =isinstance(c,basestring) 104 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 105 pipe = p.stdout 106 val = pipe.read() 107if p.wait()and not ignore_error: 108die('Command failed:%s'%str(c)) 109 110return val 111 112defp4_read_pipe(c, ignore_error=False): 113 real_cmd =p4_build_cmd(c) 114returnread_pipe(real_cmd, ignore_error) 115 116defread_pipe_lines(c): 117if verbose: 118 sys.stderr.write('Reading pipe:%s\n'%str(c)) 119 120 expand =isinstance(c, basestring) 121 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 122 pipe = p.stdout 123 val = pipe.readlines() 124if pipe.close()or p.wait(): 125die('Command failed:%s'%str(c)) 126 127return val 128 129defp4_read_pipe_lines(c): 130"""Specifically invoke p4 on the command supplied. """ 131 real_cmd =p4_build_cmd(c) 132returnread_pipe_lines(real_cmd) 133 134defp4_has_command(cmd): 135"""Ask p4 for help on this command. If it returns an error, the 136 command does not exist in this version of p4.""" 137 real_cmd =p4_build_cmd(["help", cmd]) 138 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 139 stderr=subprocess.PIPE) 140 p.communicate() 141return p.returncode ==0 142 143defp4_has_move_command(): 144"""See if the move command exists, that it supports -k, and that 145 it has not been administratively disabled. The arguments 146 must be correct, but the filenames do not have to exist. Use 147 ones with wildcards so even if they exist, it will fail.""" 148 149if notp4_has_command("move"): 150return False 151 cmd =p4_build_cmd(["move","-k","@from","@to"]) 152 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 153(out, err) = p.communicate() 154# return code will be 1 in either case 155if err.find("Invalid option") >=0: 156return False 157if err.find("disabled") >=0: 158return False 159# assume it failed because @... was invalid changelist 160return True 161 162defsystem(cmd): 163 expand =isinstance(cmd,basestring) 164if verbose: 165 sys.stderr.write("executing%s\n"%str(cmd)) 166 subprocess.check_call(cmd, shell=expand) 167 168defp4_system(cmd): 169"""Specifically invoke p4 as the system command. """ 170 real_cmd =p4_build_cmd(cmd) 171 expand =isinstance(real_cmd, basestring) 172 subprocess.check_call(real_cmd, shell=expand) 173 174_p4_version_string =None 175defp4_version_string(): 176"""Read the version string, showing just the last line, which 177 hopefully is the interesting version bit. 178 179 $ p4 -V 180 Perforce - The Fast Software Configuration Management System. 181 Copyright 1995-2011 Perforce Software. All rights reserved. 182 Rev. P4/NTX86/2011.1/393975 (2011/12/16). 183 """ 184global _p4_version_string 185if not _p4_version_string: 186 a =p4_read_pipe_lines(["-V"]) 187 _p4_version_string = a[-1].rstrip() 188return _p4_version_string 189 190defp4_integrate(src, dest): 191p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 192 193defp4_sync(f, *options): 194p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 195 196defp4_add(f): 197# forcibly add file names with wildcards 198ifwildcard_present(f): 199p4_system(["add","-f", f]) 200else: 201p4_system(["add", f]) 202 203defp4_delete(f): 204p4_system(["delete",wildcard_encode(f)]) 205 206defp4_edit(f): 207p4_system(["edit",wildcard_encode(f)]) 208 209defp4_revert(f): 210p4_system(["revert",wildcard_encode(f)]) 211 212defp4_reopen(type, f): 213p4_system(["reopen","-t",type,wildcard_encode(f)]) 214 215defp4_move(src, dest): 216p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 217 218defp4_describe(change): 219"""Make sure it returns a valid result by checking for 220 the presence of field "time". Return a dict of the 221 results.""" 222 223 ds =p4CmdList(["describe","-s",str(change)]) 224iflen(ds) !=1: 225die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 226 227 d = ds[0] 228 229if"p4ExitCode"in d: 230die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 231str(d))) 232if"code"in d: 233if d["code"] =="error": 234die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 235 236if"time"not in d: 237die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 238 239return d 240 241# 242# Canonicalize the p4 type and return a tuple of the 243# base type, plus any modifiers. See "p4 help filetypes" 244# for a list and explanation. 245# 246defsplit_p4_type(p4type): 247 248 p4_filetypes_historical = { 249"ctempobj":"binary+Sw", 250"ctext":"text+C", 251"cxtext":"text+Cx", 252"ktext":"text+k", 253"kxtext":"text+kx", 254"ltext":"text+F", 255"tempobj":"binary+FSw", 256"ubinary":"binary+F", 257"uresource":"resource+F", 258"uxbinary":"binary+Fx", 259"xbinary":"binary+x", 260"xltext":"text+Fx", 261"xtempobj":"binary+Swx", 262"xtext":"text+x", 263"xunicode":"unicode+x", 264"xutf16":"utf16+x", 265} 266if p4type in p4_filetypes_historical: 267 p4type = p4_filetypes_historical[p4type] 268 mods ="" 269 s = p4type.split("+") 270 base = s[0] 271 mods ="" 272iflen(s) >1: 273 mods = s[1] 274return(base, mods) 275 276# 277# return the raw p4 type of a file (text, text+ko, etc) 278# 279defp4_type(file): 280 results =p4CmdList(["fstat","-T","headType",file]) 281return results[0]['headType'] 282 283# 284# Given a type base and modifier, return a regexp matching 285# the keywords that can be expanded in the file 286# 287defp4_keywords_regexp_for_type(base, type_mods): 288if base in("text","unicode","binary"): 289 kwords =None 290if"ko"in type_mods: 291 kwords ='Id|Header' 292elif"k"in type_mods: 293 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 294else: 295return None 296 pattern = r""" 297 \$ # Starts with a dollar, followed by... 298 (%s) # one of the keywords, followed by... 299 (:[^$\n]+)? # possibly an old expansion, followed by... 300 \$ # another dollar 301 """% kwords 302return pattern 303else: 304return None 305 306# 307# Given a file, return a regexp matching the possible 308# RCS keywords that will be expanded, or None for files 309# with kw expansion turned off. 310# 311defp4_keywords_regexp_for_file(file): 312if not os.path.exists(file): 313return None 314else: 315(type_base, type_mods) =split_p4_type(p4_type(file)) 316returnp4_keywords_regexp_for_type(type_base, type_mods) 317 318defsetP4ExecBit(file, mode): 319# Reopens an already open file and changes the execute bit to match 320# the execute bit setting in the passed in mode. 321 322 p4Type ="+x" 323 324if notisModeExec(mode): 325 p4Type =getP4OpenedType(file) 326 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 327 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 328if p4Type[-1] =="+": 329 p4Type = p4Type[0:-1] 330 331p4_reopen(p4Type,file) 332 333defgetP4OpenedType(file): 334# Returns the perforce file type for the given file. 335 336 result =p4_read_pipe(["opened",wildcard_encode(file)]) 337 match = re.match(".*\((.+)\)\r?$", result) 338if match: 339return match.group(1) 340else: 341die("Could not determine file type for%s(result: '%s')"% (file, result)) 342 343# Return the set of all p4 labels 344defgetP4Labels(depotPaths): 345 labels =set() 346ifisinstance(depotPaths,basestring): 347 depotPaths = [depotPaths] 348 349for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 350 label = l['label'] 351 labels.add(label) 352 353return labels 354 355# Return the set of all git tags 356defgetGitTags(): 357 gitTags =set() 358for line inread_pipe_lines(["git","tag"]): 359 tag = line.strip() 360 gitTags.add(tag) 361return gitTags 362 363defdiffTreePattern(): 364# This is a simple generator for the diff tree regex pattern. This could be 365# a class variable if this and parseDiffTreeEntry were a part of a class. 366 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 367while True: 368yield pattern 369 370defparseDiffTreeEntry(entry): 371"""Parses a single diff tree entry into its component elements. 372 373 See git-diff-tree(1) manpage for details about the format of the diff 374 output. This method returns a dictionary with the following elements: 375 376 src_mode - The mode of the source file 377 dst_mode - The mode of the destination file 378 src_sha1 - The sha1 for the source file 379 dst_sha1 - The sha1 fr the destination file 380 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 381 status_score - The score for the status (applicable for 'C' and 'R' 382 statuses). This is None if there is no score. 383 src - The path for the source file. 384 dst - The path for the destination file. This is only present for 385 copy or renames. If it is not present, this is None. 386 387 If the pattern is not matched, None is returned.""" 388 389 match =diffTreePattern().next().match(entry) 390if match: 391return{ 392'src_mode': match.group(1), 393'dst_mode': match.group(2), 394'src_sha1': match.group(3), 395'dst_sha1': match.group(4), 396'status': match.group(5), 397'status_score': match.group(6), 398'src': match.group(7), 399'dst': match.group(10) 400} 401return None 402 403defisModeExec(mode): 404# Returns True if the given git mode represents an executable file, 405# otherwise False. 406return mode[-3:] =="755" 407 408defisModeExecChanged(src_mode, dst_mode): 409returnisModeExec(src_mode) !=isModeExec(dst_mode) 410 411defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 412 413ifisinstance(cmd,basestring): 414 cmd ="-G "+ cmd 415 expand =True 416else: 417 cmd = ["-G"] + cmd 418 expand =False 419 420 cmd =p4_build_cmd(cmd) 421if verbose: 422 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 423 424# Use a temporary file to avoid deadlocks without 425# subprocess.communicate(), which would put another copy 426# of stdout into memory. 427 stdin_file =None 428if stdin is not None: 429 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 430ifisinstance(stdin,basestring): 431 stdin_file.write(stdin) 432else: 433for i in stdin: 434 stdin_file.write(i +'\n') 435 stdin_file.flush() 436 stdin_file.seek(0) 437 438 p4 = subprocess.Popen(cmd, 439 shell=expand, 440 stdin=stdin_file, 441 stdout=subprocess.PIPE) 442 443 result = [] 444try: 445while True: 446 entry = marshal.load(p4.stdout) 447if cb is not None: 448cb(entry) 449else: 450 result.append(entry) 451exceptEOFError: 452pass 453 exitCode = p4.wait() 454if exitCode !=0: 455 entry = {} 456 entry["p4ExitCode"] = exitCode 457 result.append(entry) 458 459return result 460 461defp4Cmd(cmd): 462list=p4CmdList(cmd) 463 result = {} 464for entry inlist: 465 result.update(entry) 466return result; 467 468defp4Where(depotPath): 469if not depotPath.endswith("/"): 470 depotPath +="/" 471 depotPath = depotPath +"..." 472 outputList =p4CmdList(["where", depotPath]) 473 output =None 474for entry in outputList: 475if"depotFile"in entry: 476if entry["depotFile"] == depotPath: 477 output = entry 478break 479elif"data"in entry: 480 data = entry.get("data") 481 space = data.find(" ") 482if data[:space] == depotPath: 483 output = entry 484break 485if output ==None: 486return"" 487if output["code"] =="error": 488return"" 489 clientPath ="" 490if"path"in output: 491 clientPath = output.get("path") 492elif"data"in output: 493 data = output.get("data") 494 lastSpace = data.rfind(" ") 495 clientPath = data[lastSpace +1:] 496 497if clientPath.endswith("..."): 498 clientPath = clientPath[:-3] 499return clientPath 500 501defcurrentGitBranch(): 502returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 503 504defisValidGitDir(path): 505if(os.path.exists(path +"/HEAD") 506and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 507return True; 508return False 509 510defparseRevision(ref): 511returnread_pipe("git rev-parse%s"% ref).strip() 512 513defbranchExists(ref): 514 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 515 ignore_error=True) 516returnlen(rev) >0 517 518defextractLogMessageFromGitCommit(commit): 519 logMessage ="" 520 521## fixme: title is first line of commit, not 1st paragraph. 522 foundTitle =False 523for log inread_pipe_lines("git cat-file commit%s"% commit): 524if not foundTitle: 525iflen(log) ==1: 526 foundTitle =True 527continue 528 529 logMessage += log 530return logMessage 531 532defextractSettingsGitLog(log): 533 values = {} 534for line in log.split("\n"): 535 line = line.strip() 536 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 537if not m: 538continue 539 540 assignments = m.group(1).split(':') 541for a in assignments: 542 vals = a.split('=') 543 key = vals[0].strip() 544 val = ('='.join(vals[1:])).strip() 545if val.endswith('\"')and val.startswith('"'): 546 val = val[1:-1] 547 548 values[key] = val 549 550 paths = values.get("depot-paths") 551if not paths: 552 paths = values.get("depot-path") 553if paths: 554 values['depot-paths'] = paths.split(',') 555return values 556 557defgitBranchExists(branch): 558 proc = subprocess.Popen(["git","rev-parse", branch], 559 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 560return proc.wait() ==0; 561 562_gitConfig = {} 563defgitConfig(key, args =None):# set args to "--bool", for instance 564if not _gitConfig.has_key(key): 565 argsFilter ="" 566if args !=None: 567 argsFilter ="%s"% args 568 cmd ="git config%s%s"% (argsFilter, key) 569 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 570return _gitConfig[key] 571 572defgitConfigList(key): 573if not _gitConfig.has_key(key): 574 _gitConfig[key] =read_pipe("git config --get-all%s"% key, ignore_error=True).strip().split(os.linesep) 575return _gitConfig[key] 576 577defp4BranchesInGit(branchesAreInRemotes=True): 578"""Find all the branches whose names start with "p4/", looking 579 in remotes or heads as specified by the argument. Return 580 a dictionary of{ branch: revision }for each one found. 581 The branch names are the short names, without any 582 "p4/" prefix.""" 583 584 branches = {} 585 586 cmdline ="git rev-parse --symbolic " 587if branchesAreInRemotes: 588 cmdline +="--remotes" 589else: 590 cmdline +="--branches" 591 592for line inread_pipe_lines(cmdline): 593 line = line.strip() 594 595# only import to p4/ 596if not line.startswith('p4/'): 597continue 598# special symbolic ref to p4/master 599if line =="p4/HEAD": 600continue 601 602# strip off p4/ prefix 603 branch = line[len("p4/"):] 604 605 branches[branch] =parseRevision(line) 606 607return branches 608 609defbranch_exists(branch): 610"""Make sure that the given ref name really exists.""" 611 612 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 613 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 614 out, _ = p.communicate() 615if p.returncode: 616return False 617# expect exactly one line of output: the branch name 618return out.rstrip() == branch 619 620deffindUpstreamBranchPoint(head ="HEAD"): 621 branches =p4BranchesInGit() 622# map from depot-path to branch name 623 branchByDepotPath = {} 624for branch in branches.keys(): 625 tip = branches[branch] 626 log =extractLogMessageFromGitCommit(tip) 627 settings =extractSettingsGitLog(log) 628if settings.has_key("depot-paths"): 629 paths =",".join(settings["depot-paths"]) 630 branchByDepotPath[paths] ="remotes/p4/"+ branch 631 632 settings =None 633 parent =0 634while parent <65535: 635 commit = head +"~%s"% parent 636 log =extractLogMessageFromGitCommit(commit) 637 settings =extractSettingsGitLog(log) 638if settings.has_key("depot-paths"): 639 paths =",".join(settings["depot-paths"]) 640if branchByDepotPath.has_key(paths): 641return[branchByDepotPath[paths], settings] 642 643 parent = parent +1 644 645return["", settings] 646 647defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 648if not silent: 649print("Creating/updating branch(es) in%sbased on origin branch(es)" 650% localRefPrefix) 651 652 originPrefix ="origin/p4/" 653 654for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 655 line = line.strip() 656if(not line.startswith(originPrefix))or line.endswith("HEAD"): 657continue 658 659 headName = line[len(originPrefix):] 660 remoteHead = localRefPrefix + headName 661 originHead = line 662 663 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 664if(not original.has_key('depot-paths') 665or not original.has_key('change')): 666continue 667 668 update =False 669if notgitBranchExists(remoteHead): 670if verbose: 671print"creating%s"% remoteHead 672 update =True 673else: 674 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 675if settings.has_key('change') >0: 676if settings['depot-paths'] == original['depot-paths']: 677 originP4Change =int(original['change']) 678 p4Change =int(settings['change']) 679if originP4Change > p4Change: 680print("%s(%s) is newer than%s(%s). " 681"Updating p4 branch from origin." 682% (originHead, originP4Change, 683 remoteHead, p4Change)) 684 update =True 685else: 686print("Ignoring:%swas imported from%swhile " 687"%swas imported from%s" 688% (originHead,','.join(original['depot-paths']), 689 remoteHead,','.join(settings['depot-paths']))) 690 691if update: 692system("git update-ref%s %s"% (remoteHead, originHead)) 693 694deforiginP4BranchesExist(): 695returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 696 697defp4ChangesForPaths(depotPaths, changeRange): 698assert depotPaths 699 cmd = ['changes'] 700for p in depotPaths: 701 cmd += ["%s...%s"% (p, changeRange)] 702 output =p4_read_pipe_lines(cmd) 703 704 changes = {} 705for line in output: 706 changeNum =int(line.split(" ")[1]) 707 changes[changeNum] =True 708 709 changelist = changes.keys() 710 changelist.sort() 711return changelist 712 713defp4PathStartsWith(path, prefix): 714# This method tries to remedy a potential mixed-case issue: 715# 716# If UserA adds //depot/DirA/file1 717# and UserB adds //depot/dira/file2 718# 719# we may or may not have a problem. If you have core.ignorecase=true, 720# we treat DirA and dira as the same directory 721 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 722if ignorecase: 723return path.lower().startswith(prefix.lower()) 724return path.startswith(prefix) 725 726defgetClientSpec(): 727"""Look at the p4 client spec, create a View() object that contains 728 all the mappings, and return it.""" 729 730 specList =p4CmdList("client -o") 731iflen(specList) !=1: 732die('Output from "client -o" is%dlines, expecting 1'% 733len(specList)) 734 735# dictionary of all client parameters 736 entry = specList[0] 737 738# just the keys that start with "View" 739 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 740 741# hold this new View 742 view =View() 743 744# append the lines, in order, to the view 745for view_num inrange(len(view_keys)): 746 k ="View%d"% view_num 747if k not in view_keys: 748die("Expected view key%smissing"% k) 749 view.append(entry[k]) 750 751return view 752 753defgetClientRoot(): 754"""Grab the client directory.""" 755 756 output =p4CmdList("client -o") 757iflen(output) !=1: 758die('Output from "client -o" is%dlines, expecting 1'%len(output)) 759 760 entry = output[0] 761if"Root"not in entry: 762die('Client has no "Root"') 763 764return entry["Root"] 765 766# 767# P4 wildcards are not allowed in filenames. P4 complains 768# if you simply add them, but you can force it with "-f", in 769# which case it translates them into %xx encoding internally. 770# 771defwildcard_decode(path): 772# Search for and fix just these four characters. Do % last so 773# that fixing it does not inadvertently create new %-escapes. 774# Cannot have * in a filename in windows; untested as to 775# what p4 would do in such a case. 776if not platform.system() =="Windows": 777 path = path.replace("%2A","*") 778 path = path.replace("%23","#") \ 779.replace("%40","@") \ 780.replace("%25","%") 781return path 782 783defwildcard_encode(path): 784# do % first to avoid double-encoding the %s introduced here 785 path = path.replace("%","%25") \ 786.replace("*","%2A") \ 787.replace("#","%23") \ 788.replace("@","%40") 789return path 790 791defwildcard_present(path): 792return path.translate(None,"*#@%") != path 793 794class Command: 795def__init__(self): 796 self.usage ="usage: %prog [options]" 797 self.needsGit =True 798 self.verbose =False 799 800class P4UserMap: 801def__init__(self): 802 self.userMapFromPerforceServer =False 803 self.myP4UserId =None 804 805defp4UserId(self): 806if self.myP4UserId: 807return self.myP4UserId 808 809 results =p4CmdList("user -o") 810for r in results: 811if r.has_key('User'): 812 self.myP4UserId = r['User'] 813return r['User'] 814die("Could not find your p4 user id") 815 816defp4UserIsMe(self, p4User): 817# return True if the given p4 user is actually me 818 me = self.p4UserId() 819if not p4User or p4User != me: 820return False 821else: 822return True 823 824defgetUserCacheFilename(self): 825 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 826return home +"/.gitp4-usercache.txt" 827 828defgetUserMapFromPerforceServer(self): 829if self.userMapFromPerforceServer: 830return 831 self.users = {} 832 self.emails = {} 833 834for output inp4CmdList("users"): 835if not output.has_key("User"): 836continue 837 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 838 self.emails[output["Email"]] = output["User"] 839 840 841 s ='' 842for(key, val)in self.users.items(): 843 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 844 845open(self.getUserCacheFilename(),"wb").write(s) 846 self.userMapFromPerforceServer =True 847 848defloadUserMapFromCache(self): 849 self.users = {} 850 self.userMapFromPerforceServer =False 851try: 852 cache =open(self.getUserCacheFilename(),"rb") 853 lines = cache.readlines() 854 cache.close() 855for line in lines: 856 entry = line.strip().split("\t") 857 self.users[entry[0]] = entry[1] 858exceptIOError: 859 self.getUserMapFromPerforceServer() 860 861classP4Debug(Command): 862def__init__(self): 863 Command.__init__(self) 864 self.options = [] 865 self.description ="A tool to debug the output of p4 -G." 866 self.needsGit =False 867 868defrun(self, args): 869 j =0 870for output inp4CmdList(args): 871print'Element:%d'% j 872 j +=1 873print output 874return True 875 876classP4RollBack(Command): 877def__init__(self): 878 Command.__init__(self) 879 self.options = [ 880 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 881] 882 self.description ="A tool to debug the multi-branch import. Don't use :)" 883 self.rollbackLocalBranches =False 884 885defrun(self, args): 886iflen(args) !=1: 887return False 888 maxChange =int(args[0]) 889 890if"p4ExitCode"inp4Cmd("changes -m 1"): 891die("Problems executing p4"); 892 893if self.rollbackLocalBranches: 894 refPrefix ="refs/heads/" 895 lines =read_pipe_lines("git rev-parse --symbolic --branches") 896else: 897 refPrefix ="refs/remotes/" 898 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 899 900for line in lines: 901if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 902 line = line.strip() 903 ref = refPrefix + line 904 log =extractLogMessageFromGitCommit(ref) 905 settings =extractSettingsGitLog(log) 906 907 depotPaths = settings['depot-paths'] 908 change = settings['change'] 909 910 changed =False 911 912iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 913for p in depotPaths]))) ==0: 914print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 915system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 916continue 917 918while change andint(change) > maxChange: 919 changed =True 920if self.verbose: 921print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 922system("git update-ref%s\"%s^\""% (ref, ref)) 923 log =extractLogMessageFromGitCommit(ref) 924 settings =extractSettingsGitLog(log) 925 926 927 depotPaths = settings['depot-paths'] 928 change = settings['change'] 929 930if changed: 931print"%srewound to%s"% (ref, change) 932 933return True 934 935classP4Submit(Command, P4UserMap): 936 937 conflict_behavior_choices = ("ask","skip","quit") 938 939def__init__(self): 940 Command.__init__(self) 941 P4UserMap.__init__(self) 942 self.options = [ 943 optparse.make_option("--origin", dest="origin"), 944 optparse.make_option("-M", dest="detectRenames", action="store_true"), 945# preserve the user, requires relevant p4 permissions 946 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 947 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 948 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 949 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 950 optparse.make_option("--conflict", dest="conflict_behavior", 951 choices=self.conflict_behavior_choices), 952 optparse.make_option("--branch", dest="branch"), 953] 954 self.description ="Submit changes from git to the perforce depot." 955 self.usage +=" [name of git branch to submit into perforce depot]" 956 self.origin ="" 957 self.detectRenames =False 958 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 959 self.dry_run =False 960 self.prepare_p4_only =False 961 self.conflict_behavior =None 962 self.isWindows = (platform.system() =="Windows") 963 self.exportLabels =False 964 self.p4HasMoveCommand =p4_has_move_command() 965 self.branch =None 966 967defcheck(self): 968iflen(p4CmdList("opened ...")) >0: 969die("You have files opened with perforce! Close them before starting the sync.") 970 971defseparate_jobs_from_description(self, message): 972"""Extract and return a possible Jobs field in the commit 973 message. It goes into a separate section in the p4 change 974 specification. 975 976 A jobs line starts with "Jobs:" and looks like a new field 977 in a form. Values are white-space separated on the same 978 line or on following lines that start with a tab. 979 980 This does not parse and extract the full git commit message 981 like a p4 form. It just sees the Jobs: line as a marker 982 to pass everything from then on directly into the p4 form, 983 but outside the description section. 984 985 Return a tuple (stripped log message, jobs string).""" 986 987 m = re.search(r'^Jobs:', message, re.MULTILINE) 988if m is None: 989return(message,None) 990 991 jobtext = message[m.start():] 992 stripped_message = message[:m.start()].rstrip() 993return(stripped_message, jobtext) 994 995defprepareLogMessage(self, template, message, jobs): 996"""Edits the template returned from "p4 change -o" to insert 997 the message in the Description field, and the jobs text in 998 the Jobs field.""" 999 result =""10001001 inDescriptionSection =False10021003for line in template.split("\n"):1004if line.startswith("#"):1005 result += line +"\n"1006continue10071008if inDescriptionSection:1009if line.startswith("Files:")or line.startswith("Jobs:"):1010 inDescriptionSection =False1011# insert Jobs section1012if jobs:1013 result += jobs +"\n"1014else:1015continue1016else:1017if line.startswith("Description:"):1018 inDescriptionSection =True1019 line +="\n"1020for messageLine in message.split("\n"):1021 line +="\t"+ messageLine +"\n"10221023 result += line +"\n"10241025return result10261027defpatchRCSKeywords(self,file, pattern):1028# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1029(handle, outFileName) = tempfile.mkstemp(dir='.')1030try:1031 outFile = os.fdopen(handle,"w+")1032 inFile =open(file,"r")1033 regexp = re.compile(pattern, re.VERBOSE)1034for line in inFile.readlines():1035 line = regexp.sub(r'$\1$', line)1036 outFile.write(line)1037 inFile.close()1038 outFile.close()1039# Forcibly overwrite the original file1040 os.unlink(file)1041 shutil.move(outFileName,file)1042except:1043# cleanup our temporary file1044 os.unlink(outFileName)1045print"Failed to strip RCS keywords in%s"%file1046raise10471048print"Patched up RCS keywords in%s"%file10491050defp4UserForCommit(self,id):1051# Return the tuple (perforce user,git email) for a given git commit id1052 self.getUserMapFromPerforceServer()1053 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id)1054 gitEmail = gitEmail.strip()1055if not self.emails.has_key(gitEmail):1056return(None,gitEmail)1057else:1058return(self.emails[gitEmail],gitEmail)10591060defcheckValidP4Users(self,commits):1061# check if any git authors cannot be mapped to p4 users1062foridin commits:1063(user,email) = self.p4UserForCommit(id)1064if not user:1065 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1066ifgitConfig('git-p4.allowMissingP4Users').lower() =="true":1067print"%s"% msg1068else:1069die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10701071deflastP4Changelist(self):1072# Get back the last changelist number submitted in this client spec. This1073# then gets used to patch up the username in the change. If the same1074# client spec is being used by multiple processes then this might go1075# wrong.1076 results =p4CmdList("client -o")# find the current client1077 client =None1078for r in results:1079if r.has_key('Client'):1080 client = r['Client']1081break1082if not client:1083die("could not get client spec")1084 results =p4CmdList(["changes","-c", client,"-m","1"])1085for r in results:1086if r.has_key('change'):1087return r['change']1088die("Could not get changelist number for last submit - cannot patch up user details")10891090defmodifyChangelistUser(self, changelist, newUser):1091# fixup the user field of a changelist after it has been submitted.1092 changes =p4CmdList("change -o%s"% changelist)1093iflen(changes) !=1:1094die("Bad output from p4 change modifying%sto user%s"%1095(changelist, newUser))10961097 c = changes[0]1098if c['User'] == newUser:return# nothing to do1099 c['User'] = newUser1100input= marshal.dumps(c)11011102 result =p4CmdList("change -f -i", stdin=input)1103for r in result:1104if r.has_key('code'):1105if r['code'] =='error':1106die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1107if r.has_key('data'):1108print("Updated user field for changelist%sto%s"% (changelist, newUser))1109return1110die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11111112defcanChangeChangelists(self):1113# check to see if we have p4 admin or super-user permissions, either of1114# which are required to modify changelists.1115 results =p4CmdList(["protects", self.depotPath])1116for r in results:1117if r.has_key('perm'):1118if r['perm'] =='admin':1119return11120if r['perm'] =='super':1121return11122return011231124defprepareSubmitTemplate(self):1125"""Run "p4 change -o" to grab a change specification template.1126 This does not use "p4 -G", as it is nice to keep the submission1127 template in original order, since a human might edit it.11281129 Remove lines in the Files section that show changes to files1130 outside the depot path we're committing into."""11311132 template =""1133 inFilesSection =False1134for line inp4_read_pipe_lines(['change','-o']):1135if line.endswith("\r\n"):1136 line = line[:-2] +"\n"1137if inFilesSection:1138if line.startswith("\t"):1139# path starts and ends with a tab1140 path = line[1:]1141 lastTab = path.rfind("\t")1142if lastTab != -1:1143 path = path[:lastTab]1144if notp4PathStartsWith(path, self.depotPath):1145continue1146else:1147 inFilesSection =False1148else:1149if line.startswith("Files:"):1150 inFilesSection =True11511152 template += line11531154return template11551156defedit_template(self, template_file):1157"""Invoke the editor to let the user change the submission1158 message. Return true if okay to continue with the submit."""11591160# if configured to skip the editing part, just submit1161ifgitConfig("git-p4.skipSubmitEdit") =="true":1162return True11631164# look at the modification time, to check later if the user saved1165# the file1166 mtime = os.stat(template_file).st_mtime11671168# invoke the editor1169if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1170 editor = os.environ.get("P4EDITOR")1171else:1172 editor =read_pipe("git var GIT_EDITOR").strip()1173system(editor +" "+ template_file)11741175# If the file was not saved, prompt to see if this patch should1176# be skipped. But skip this verification step if configured so.1177ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1178return True11791180# modification time updated means user saved the file1181if os.stat(template_file).st_mtime > mtime:1182return True11831184while True:1185 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1186if response =='y':1187return True1188if response =='n':1189return False11901191defapplyCommit(self,id):1192"""Apply one commit, return True if it succeeded."""11931194print"Applying",read_pipe(["git","show","-s",1195"--format=format:%h%s",id])11961197(p4User, gitEmail) = self.p4UserForCommit(id)11981199 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1200 filesToAdd =set()1201 filesToDelete =set()1202 editedFiles =set()1203 pureRenameCopy =set()1204 filesToChangeExecBit = {}12051206for line in diff:1207 diff =parseDiffTreeEntry(line)1208 modifier = diff['status']1209 path = diff['src']1210if modifier =="M":1211p4_edit(path)1212ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1213 filesToChangeExecBit[path] = diff['dst_mode']1214 editedFiles.add(path)1215elif modifier =="A":1216 filesToAdd.add(path)1217 filesToChangeExecBit[path] = diff['dst_mode']1218if path in filesToDelete:1219 filesToDelete.remove(path)1220elif modifier =="D":1221 filesToDelete.add(path)1222if path in filesToAdd:1223 filesToAdd.remove(path)1224elif modifier =="C":1225 src, dest = diff['src'], diff['dst']1226p4_integrate(src, dest)1227 pureRenameCopy.add(dest)1228if diff['src_sha1'] != diff['dst_sha1']:1229p4_edit(dest)1230 pureRenameCopy.discard(dest)1231ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1232p4_edit(dest)1233 pureRenameCopy.discard(dest)1234 filesToChangeExecBit[dest] = diff['dst_mode']1235if self.isWindows:1236# turn off read-only attribute1237 os.chmod(dest, stat.S_IWRITE)1238 os.unlink(dest)1239 editedFiles.add(dest)1240elif modifier =="R":1241 src, dest = diff['src'], diff['dst']1242if self.p4HasMoveCommand:1243p4_edit(src)# src must be open before move1244p4_move(src, dest)# opens for (move/delete, move/add)1245else:1246p4_integrate(src, dest)1247if diff['src_sha1'] != diff['dst_sha1']:1248p4_edit(dest)1249else:1250 pureRenameCopy.add(dest)1251ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1252if not self.p4HasMoveCommand:1253p4_edit(dest)# with move: already open, writable1254 filesToChangeExecBit[dest] = diff['dst_mode']1255if not self.p4HasMoveCommand:1256if self.isWindows:1257 os.chmod(dest, stat.S_IWRITE)1258 os.unlink(dest)1259 filesToDelete.add(src)1260 editedFiles.add(dest)1261else:1262die("unknown modifier%sfor%s"% (modifier, path))12631264 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1265 patchcmd = diffcmd +" | git apply "1266 tryPatchCmd = patchcmd +"--check -"1267 applyPatchCmd = patchcmd +"--check --apply -"1268 patch_succeeded =True12691270if os.system(tryPatchCmd) !=0:1271 fixed_rcs_keywords =False1272 patch_succeeded =False1273print"Unfortunately applying the change failed!"12741275# Patch failed, maybe it's just RCS keyword woes. Look through1276# the patch to see if that's possible.1277ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1278file=None1279 pattern =None1280 kwfiles = {}1281forfilein editedFiles | filesToDelete:1282# did this file's delta contain RCS keywords?1283 pattern =p4_keywords_regexp_for_file(file)12841285if pattern:1286# this file is a possibility...look for RCS keywords.1287 regexp = re.compile(pattern, re.VERBOSE)1288for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1289if regexp.search(line):1290if verbose:1291print"got keyword match on%sin%sin%s"% (pattern, line,file)1292 kwfiles[file] = pattern1293break12941295forfilein kwfiles:1296if verbose:1297print"zapping%swith%s"% (line,pattern)1298# File is being deleted, so not open in p4. Must1299# disable the read-only bit on windows.1300if self.isWindows andfilenot in editedFiles:1301 os.chmod(file, stat.S_IWRITE)1302 self.patchRCSKeywords(file, kwfiles[file])1303 fixed_rcs_keywords =True13041305if fixed_rcs_keywords:1306print"Retrying the patch with RCS keywords cleaned up"1307if os.system(tryPatchCmd) ==0:1308 patch_succeeded =True13091310if not patch_succeeded:1311for f in editedFiles:1312p4_revert(f)1313return False13141315#1316# Apply the patch for real, and do add/delete/+x handling.1317#1318system(applyPatchCmd)13191320for f in filesToAdd:1321p4_add(f)1322for f in filesToDelete:1323p4_revert(f)1324p4_delete(f)13251326# Set/clear executable bits1327for f in filesToChangeExecBit.keys():1328 mode = filesToChangeExecBit[f]1329setP4ExecBit(f, mode)13301331#1332# Build p4 change description, starting with the contents1333# of the git commit message.1334#1335 logMessage =extractLogMessageFromGitCommit(id)1336 logMessage = logMessage.strip()1337(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13381339 template = self.prepareSubmitTemplate()1340 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13411342if self.preserveUser:1343 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13441345if self.checkAuthorship and not self.p4UserIsMe(p4User):1346 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1347 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1348 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13491350 separatorLine ="######## everything below this line is just the diff #######\n"13511352# diff1353if os.environ.has_key("P4DIFF"):1354del(os.environ["P4DIFF"])1355 diff =""1356for editedFile in editedFiles:1357 diff +=p4_read_pipe(['diff','-du',1358wildcard_encode(editedFile)])13591360# new file diff1361 newdiff =""1362for newFile in filesToAdd:1363 newdiff +="==== new file ====\n"1364 newdiff +="--- /dev/null\n"1365 newdiff +="+++%s\n"% newFile1366 f =open(newFile,"r")1367for line in f.readlines():1368 newdiff +="+"+ line1369 f.close()13701371# change description file: submitTemplate, separatorLine, diff, newdiff1372(handle, fileName) = tempfile.mkstemp()1373 tmpFile = os.fdopen(handle,"w+")1374if self.isWindows:1375 submitTemplate = submitTemplate.replace("\n","\r\n")1376 separatorLine = separatorLine.replace("\n","\r\n")1377 newdiff = newdiff.replace("\n","\r\n")1378 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1379 tmpFile.close()13801381if self.prepare_p4_only:1382#1383# Leave the p4 tree prepared, and the submit template around1384# and let the user decide what to do next1385#1386print1387print"P4 workspace prepared for submission."1388print"To submit or revert, go to client workspace"1389print" "+ self.clientPath1390print1391print"To submit, use\"p4 submit\"to write a new description,"1392print"or\"p4 submit -i%s\"to use the one prepared by" \1393"\"git p4\"."% fileName1394print"You can delete the file\"%s\"when finished."% fileName13951396if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1397print"To preserve change ownership by user%s, you must\n" \1398"do\"p4 change -f <change>\"after submitting and\n" \1399"edit the User field."1400if pureRenameCopy:1401print"After submitting, renamed files must be re-synced."1402print"Invoke\"p4 sync -f\"on each of these files:"1403for f in pureRenameCopy:1404print" "+ f14051406print1407print"To revert the changes, use\"p4 revert ...\", and delete"1408print"the submit template file\"%s\""% fileName1409if filesToAdd:1410print"Since the commit adds new files, they must be deleted:"1411for f in filesToAdd:1412print" "+ f1413print1414return True14151416#1417# Let the user edit the change description, then submit it.1418#1419if self.edit_template(fileName):1420# read the edited message and submit1421 ret =True1422 tmpFile =open(fileName,"rb")1423 message = tmpFile.read()1424 tmpFile.close()1425 submitTemplate = message[:message.index(separatorLine)]1426if self.isWindows:1427 submitTemplate = submitTemplate.replace("\r\n","\n")1428p4_write_pipe(['submit','-i'], submitTemplate)14291430if self.preserveUser:1431if p4User:1432# Get last changelist number. Cannot easily get it from1433# the submit command output as the output is1434# unmarshalled.1435 changelist = self.lastP4Changelist()1436 self.modifyChangelistUser(changelist, p4User)14371438# The rename/copy happened by applying a patch that created a1439# new file. This leaves it writable, which confuses p4.1440for f in pureRenameCopy:1441p4_sync(f,"-f")14421443else:1444# skip this patch1445 ret =False1446print"Submission cancelled, undoing p4 changes."1447for f in editedFiles:1448p4_revert(f)1449for f in filesToAdd:1450p4_revert(f)1451 os.remove(f)1452for f in filesToDelete:1453p4_revert(f)14541455 os.remove(fileName)1456return ret14571458# Export git tags as p4 labels. Create a p4 label and then tag1459# with that.1460defexportGitTags(self, gitTags):1461 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1462iflen(validLabelRegexp) ==0:1463 validLabelRegexp = defaultLabelRegexp1464 m = re.compile(validLabelRegexp)14651466for name in gitTags:14671468if not m.match(name):1469if verbose:1470print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1471continue14721473# Get the p4 commit this corresponds to1474 logMessage =extractLogMessageFromGitCommit(name)1475 values =extractSettingsGitLog(logMessage)14761477if not values.has_key('change'):1478# a tag pointing to something not sent to p4; ignore1479if verbose:1480print"git tag%sdoes not give a p4 commit"% name1481continue1482else:1483 changelist = values['change']14841485# Get the tag details.1486 inHeader =True1487 isAnnotated =False1488 body = []1489for l inread_pipe_lines(["git","cat-file","-p", name]):1490 l = l.strip()1491if inHeader:1492if re.match(r'tag\s+', l):1493 isAnnotated =True1494elif re.match(r'\s*$', l):1495 inHeader =False1496continue1497else:1498 body.append(l)14991500if not isAnnotated:1501 body = ["lightweight tag imported by git p4\n"]15021503# Create the label - use the same view as the client spec we are using1504 clientSpec =getClientSpec()15051506 labelTemplate ="Label:%s\n"% name1507 labelTemplate +="Description:\n"1508for b in body:1509 labelTemplate +="\t"+ b +"\n"1510 labelTemplate +="View:\n"1511for mapping in clientSpec.mappings:1512 labelTemplate +="\t%s\n"% mapping.depot_side.path15131514if self.dry_run:1515print"Would create p4 label%sfor tag"% name1516elif self.prepare_p4_only:1517print"Not creating p4 label%sfor tag due to option" \1518" --prepare-p4-only"% name1519else:1520p4_write_pipe(["label","-i"], labelTemplate)15211522# Use the label1523p4_system(["tag","-l", name] +1524["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])15251526if verbose:1527print"created p4 label for tag%s"% name15281529defrun(self, args):1530iflen(args) ==0:1531 self.master =currentGitBranch()1532iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1533die("Detecting current git branch failed!")1534eliflen(args) ==1:1535 self.master = args[0]1536if notbranchExists(self.master):1537die("Branch%sdoes not exist"% self.master)1538else:1539return False15401541 allowSubmit =gitConfig("git-p4.allowSubmit")1542iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1543die("%sis not in git-p4.allowSubmit"% self.master)15441545[upstream, settings] =findUpstreamBranchPoint()1546 self.depotPath = settings['depot-paths'][0]1547iflen(self.origin) ==0:1548 self.origin = upstream15491550if self.preserveUser:1551if not self.canChangeChangelists():1552die("Cannot preserve user names without p4 super-user or admin permissions")15531554# if not set from the command line, try the config file1555if self.conflict_behavior is None:1556 val =gitConfig("git-p4.conflict")1557if val:1558if val not in self.conflict_behavior_choices:1559die("Invalid value '%s' for config git-p4.conflict"% val)1560else:1561 val ="ask"1562 self.conflict_behavior = val15631564if self.verbose:1565print"Origin branch is "+ self.origin15661567iflen(self.depotPath) ==0:1568print"Internal error: cannot locate perforce depot path from existing branches"1569 sys.exit(128)15701571 self.useClientSpec =False1572ifgitConfig("git-p4.useclientspec","--bool") =="true":1573 self.useClientSpec =True1574if self.useClientSpec:1575 self.clientSpecDirs =getClientSpec()15761577if self.useClientSpec:1578# all files are relative to the client spec1579 self.clientPath =getClientRoot()1580else:1581 self.clientPath =p4Where(self.depotPath)15821583if self.clientPath =="":1584die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15851586print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1587 self.oldWorkingDirectory = os.getcwd()15881589# ensure the clientPath exists1590 new_client_dir =False1591if not os.path.exists(self.clientPath):1592 new_client_dir =True1593 os.makedirs(self.clientPath)15941595chdir(self.clientPath)1596if self.dry_run:1597print"Would synchronize p4 checkout in%s"% self.clientPath1598else:1599print"Synchronizing p4 checkout..."1600if new_client_dir:1601# old one was destroyed, and maybe nobody told p41602p4_sync("...","-f")1603else:1604p4_sync("...")1605 self.check()16061607 commits = []1608for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1609 commits.append(line.strip())1610 commits.reverse()16111612if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1613 self.checkAuthorship =False1614else:1615 self.checkAuthorship =True16161617if self.preserveUser:1618 self.checkValidP4Users(commits)16191620#1621# Build up a set of options to be passed to diff when1622# submitting each commit to p4.1623#1624if self.detectRenames:1625# command-line -M arg1626 self.diffOpts ="-M"1627else:1628# If not explicitly set check the config variable1629 detectRenames =gitConfig("git-p4.detectRenames")16301631if detectRenames.lower() =="false"or detectRenames =="":1632 self.diffOpts =""1633elif detectRenames.lower() =="true":1634 self.diffOpts ="-M"1635else:1636 self.diffOpts ="-M%s"% detectRenames16371638# no command-line arg for -C or --find-copies-harder, just1639# config variables1640 detectCopies =gitConfig("git-p4.detectCopies")1641if detectCopies.lower() =="false"or detectCopies =="":1642pass1643elif detectCopies.lower() =="true":1644 self.diffOpts +=" -C"1645else:1646 self.diffOpts +=" -C%s"% detectCopies16471648ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1649 self.diffOpts +=" --find-copies-harder"16501651#1652# Apply the commits, one at a time. On failure, ask if should1653# continue to try the rest of the patches, or quit.1654#1655if self.dry_run:1656print"Would apply"1657 applied = []1658 last =len(commits) -11659for i, commit inenumerate(commits):1660if self.dry_run:1661print" ",read_pipe(["git","show","-s",1662"--format=format:%h%s", commit])1663 ok =True1664else:1665 ok = self.applyCommit(commit)1666if ok:1667 applied.append(commit)1668else:1669if self.prepare_p4_only and i < last:1670print"Processing only the first commit due to option" \1671" --prepare-p4-only"1672break1673if i < last:1674 quit =False1675while True:1676# prompt for what to do, or use the option/variable1677if self.conflict_behavior =="ask":1678print"What do you want to do?"1679 response =raw_input("[s]kip this commit but apply"1680" the rest, or [q]uit? ")1681if not response:1682continue1683elif self.conflict_behavior =="skip":1684 response ="s"1685elif self.conflict_behavior =="quit":1686 response ="q"1687else:1688die("Unknown conflict_behavior '%s'"%1689 self.conflict_behavior)16901691if response[0] =="s":1692print"Skipping this commit, but applying the rest"1693break1694if response[0] =="q":1695print"Quitting"1696 quit =True1697break1698if quit:1699break17001701chdir(self.oldWorkingDirectory)17021703if self.dry_run:1704pass1705elif self.prepare_p4_only:1706pass1707eliflen(commits) ==len(applied):1708print"All commits applied!"17091710 sync =P4Sync()1711if self.branch:1712 sync.branch = self.branch1713 sync.run([])17141715 rebase =P4Rebase()1716 rebase.rebase()17171718else:1719iflen(applied) ==0:1720print"No commits applied."1721else:1722print"Applied only the commits marked with '*':"1723for c in commits:1724if c in applied:1725 star ="*"1726else:1727 star =" "1728print star,read_pipe(["git","show","-s",1729"--format=format:%h%s", c])1730print"You will have to do 'git p4 sync' and rebase."17311732ifgitConfig("git-p4.exportLabels","--bool") =="true":1733 self.exportLabels =True17341735if self.exportLabels:1736 p4Labels =getP4Labels(self.depotPath)1737 gitTags =getGitTags()17381739 missingGitTags = gitTags - p4Labels1740 self.exportGitTags(missingGitTags)17411742# exit with error unless everything applied perfecly1743iflen(commits) !=len(applied):1744 sys.exit(1)17451746return True17471748classView(object):1749"""Represent a p4 view ("p4 help views"), and map files in a1750 repo according to the view."""17511752classPath(object):1753"""A depot or client path, possibly containing wildcards.1754 The only one supported is ... at the end, currently.1755 Initialize with the full path, with //depot or //client."""17561757def__init__(self, path, is_depot):1758 self.path = path1759 self.is_depot = is_depot1760 self.find_wildcards()1761# remember the prefix bit, useful for relative mappings1762 m = re.match("(//[^/]+/)", self.path)1763if not m:1764die("Path%sdoes not start with //prefix/"% self.path)1765 prefix = m.group(1)1766if not self.is_depot:1767# strip //client/ on client paths1768 self.path = self.path[len(prefix):]17691770deffind_wildcards(self):1771"""Make sure wildcards are valid, and set up internal1772 variables."""17731774 self.ends_triple_dot =False1775# There are three wildcards allowed in p4 views1776# (see "p4 help views"). This code knows how to1777# handle "..." (only at the end), but cannot deal with1778# "%%n" or "*". Only check the depot_side, as p4 should1779# validate that the client_side matches too.1780if re.search(r'%%[1-9]', self.path):1781die("Can't handle%%n wildcards in view:%s"% self.path)1782if self.path.find("*") >=0:1783die("Can't handle * wildcards in view:%s"% self.path)1784 triple_dot_index = self.path.find("...")1785if triple_dot_index >=0:1786if triple_dot_index !=len(self.path) -3:1787die("Can handle only single ... wildcard, at end:%s"%1788 self.path)1789 self.ends_triple_dot =True17901791defensure_compatible(self, other_path):1792"""Make sure the wildcards agree."""1793if self.ends_triple_dot != other_path.ends_triple_dot:1794die("Both paths must end with ... if either does;\n"+1795"paths:%s %s"% (self.path, other_path.path))17961797defmatch_wildcards(self, test_path):1798"""See if this test_path matches us, and fill in the value1799 of the wildcards if so. Returns a tuple of1800 (True|False, wildcards[]). For now, only the ... at end1801 is supported, so at most one wildcard."""1802if self.ends_triple_dot:1803 dotless = self.path[:-3]1804if test_path.startswith(dotless):1805 wildcard = test_path[len(dotless):]1806return(True, [ wildcard ])1807else:1808if test_path == self.path:1809return(True, [])1810return(False, [])18111812defmatch(self, test_path):1813"""Just return if it matches; don't bother with the wildcards."""1814 b, _ = self.match_wildcards(test_path)1815return b18161817deffill_in_wildcards(self, wildcards):1818"""Return the relative path, with the wildcards filled in1819 if there are any."""1820if self.ends_triple_dot:1821return self.path[:-3] + wildcards[0]1822else:1823return self.path18241825classMapping(object):1826def__init__(self, depot_side, client_side, overlay, exclude):1827# depot_side is without the trailing /... if it had one1828 self.depot_side = View.Path(depot_side, is_depot=True)1829 self.client_side = View.Path(client_side, is_depot=False)1830 self.overlay = overlay # started with "+"1831 self.exclude = exclude # started with "-"1832assert not(self.overlay and self.exclude)1833 self.depot_side.ensure_compatible(self.client_side)18341835def__str__(self):1836 c =" "1837if self.overlay:1838 c ="+"1839if self.exclude:1840 c ="-"1841return"View.Mapping:%s%s->%s"% \1842(c, self.depot_side.path, self.client_side.path)18431844defmap_depot_to_client(self, depot_path):1845"""Calculate the client path if using this mapping on the1846 given depot path; does not consider the effect of other1847 mappings in a view. Even excluded mappings are returned."""1848 matches, wildcards = self.depot_side.match_wildcards(depot_path)1849if not matches:1850return""1851 client_path = self.client_side.fill_in_wildcards(wildcards)1852return client_path18531854#1855# View methods1856#1857def__init__(self):1858 self.mappings = []18591860defappend(self, view_line):1861"""Parse a view line, splitting it into depot and client1862 sides. Append to self.mappings, preserving order."""18631864# Split the view line into exactly two words. P4 enforces1865# structure on these lines that simplifies this quite a bit.1866#1867# Either or both words may be double-quoted.1868# Single quotes do not matter.1869# Double-quote marks cannot occur inside the words.1870# A + or - prefix is also inside the quotes.1871# There are no quotes unless they contain a space.1872# The line is already white-space stripped.1873# The two words are separated by a single space.1874#1875if view_line[0] =='"':1876# First word is double quoted. Find its end.1877 close_quote_index = view_line.find('"',1)1878if close_quote_index <=0:1879die("No first-word closing quote found:%s"% view_line)1880 depot_side = view_line[1:close_quote_index]1881# skip closing quote and space1882 rhs_index = close_quote_index +1+11883else:1884 space_index = view_line.find(" ")1885if space_index <=0:1886die("No word-splitting space found:%s"% view_line)1887 depot_side = view_line[0:space_index]1888 rhs_index = space_index +118891890if view_line[rhs_index] =='"':1891# Second word is double quoted. Make sure there is a1892# double quote at the end too.1893if not view_line.endswith('"'):1894die("View line with rhs quote should end with one:%s"%1895 view_line)1896# skip the quotes1897 client_side = view_line[rhs_index+1:-1]1898else:1899 client_side = view_line[rhs_index:]19001901# prefix + means overlay on previous mapping1902 overlay =False1903if depot_side.startswith("+"):1904 overlay =True1905 depot_side = depot_side[1:]19061907# prefix - means exclude this path1908 exclude =False1909if depot_side.startswith("-"):1910 exclude =True1911 depot_side = depot_side[1:]19121913 m = View.Mapping(depot_side, client_side, overlay, exclude)1914 self.mappings.append(m)19151916defmap_in_client(self, depot_path):1917"""Return the relative location in the client where this1918 depot file should live. Returns "" if the file should1919 not be mapped in the client."""19201921 paths_filled = []1922 client_path =""19231924# look at later entries first1925for m in self.mappings[::-1]:19261927# see where will this path end up in the client1928 p = m.map_depot_to_client(depot_path)19291930if p =="":1931# Depot path does not belong in client. Must remember1932# this, as previous items should not cause files to1933# exist in this path either. Remember that the list is1934# being walked from the end, which has higher precedence.1935# Overlap mappings do not exclude previous mappings.1936if not m.overlay:1937 paths_filled.append(m.client_side)19381939else:1940# This mapping matched; no need to search any further.1941# But, the mapping could be rejected if the client path1942# has already been claimed by an earlier mapping (i.e.1943# one later in the list, which we are walking backwards).1944 already_mapped_in_client =False1945for f in paths_filled:1946# this is View.Path.match1947if f.match(p):1948 already_mapped_in_client =True1949break1950if not already_mapped_in_client:1951# Include this file, unless it is from a line that1952# explicitly said to exclude it.1953if not m.exclude:1954 client_path = p19551956# a match, even if rejected, always stops the search1957break19581959return client_path19601961classP4Sync(Command, P4UserMap):1962 delete_actions = ("delete","move/delete","purge")19631964def__init__(self):1965 Command.__init__(self)1966 P4UserMap.__init__(self)1967 self.options = [1968 optparse.make_option("--branch", dest="branch"),1969 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1970 optparse.make_option("--changesfile", dest="changesFile"),1971 optparse.make_option("--silent", dest="silent", action="store_true"),1972 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1973 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1974 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1975help="Import into refs/heads/ , not refs/remotes"),1976 optparse.make_option("--max-changes", dest="maxChanges"),1977 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1978help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1979 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1980help="Only sync files that are included in the Perforce Client Spec")1981]1982 self.description ="""Imports from Perforce into a git repository.\n1983 example:1984 //depot/my/project/ -- to import the current head1985 //depot/my/project/@all -- to import everything1986 //depot/my/project/@1,6 -- to import only from revision 1 to 619871988 (a ... is not needed in the path p4 specification, it's added implicitly)"""19891990 self.usage +=" //depot/path[@revRange]"1991 self.silent =False1992 self.createdBranches =set()1993 self.committedChanges =set()1994 self.branch =""1995 self.detectBranches =False1996 self.detectLabels =False1997 self.importLabels =False1998 self.changesFile =""1999 self.syncWithOrigin =True2000 self.importIntoRemotes =True2001 self.maxChanges =""2002 self.keepRepoPath =False2003 self.depotPaths =None2004 self.p4BranchesInGit = []2005 self.cloneExclude = []2006 self.useClientSpec =False2007 self.useClientSpec_from_options =False2008 self.clientSpecDirs =None2009 self.tempBranches = []2010 self.tempBranchLocation ="git-p4-tmp"20112012ifgitConfig("git-p4.syncFromOrigin") =="false":2013 self.syncWithOrigin =False20142015# Force a checkpoint in fast-import and wait for it to finish2016defcheckpoint(self):2017 self.gitStream.write("checkpoint\n\n")2018 self.gitStream.write("progress checkpoint\n\n")2019 out = self.gitOutput.readline()2020if self.verbose:2021print"checkpoint finished: "+ out20222023defextractFilesFromCommit(self, commit):2024 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2025for path in self.cloneExclude]2026 files = []2027 fnum =02028while commit.has_key("depotFile%s"% fnum):2029 path = commit["depotFile%s"% fnum]20302031if[p for p in self.cloneExclude2032ifp4PathStartsWith(path, p)]:2033 found =False2034else:2035 found = [p for p in self.depotPaths2036ifp4PathStartsWith(path, p)]2037if not found:2038 fnum = fnum +12039continue20402041file= {}2042file["path"] = path2043file["rev"] = commit["rev%s"% fnum]2044file["action"] = commit["action%s"% fnum]2045file["type"] = commit["type%s"% fnum]2046 files.append(file)2047 fnum = fnum +12048return files20492050defstripRepoPath(self, path, prefixes):2051"""When streaming files, this is called to map a p4 depot path2052 to where it should go in git. The prefixes are either2053 self.depotPaths, or self.branchPrefixes in the case of2054 branch detection."""20552056if self.useClientSpec:2057# branch detection moves files up a level (the branch name)2058# from what client spec interpretation gives2059 path = self.clientSpecDirs.map_in_client(path)2060if self.detectBranches:2061for b in self.knownBranches:2062if path.startswith(b +"/"):2063 path = path[len(b)+1:]20642065elif self.keepRepoPath:2066# Preserve everything in relative path name except leading2067# //depot/; just look at first prefix as they all should2068# be in the same depot.2069 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2070ifp4PathStartsWith(path, depot):2071 path = path[len(depot):]20722073else:2074for p in prefixes:2075ifp4PathStartsWith(path, p):2076 path = path[len(p):]2077break20782079 path =wildcard_decode(path)2080return path20812082defsplitFilesIntoBranches(self, commit):2083"""Look at each depotFile in the commit to figure out to what2084 branch it belongs."""20852086 branches = {}2087 fnum =02088while commit.has_key("depotFile%s"% fnum):2089 path = commit["depotFile%s"% fnum]2090 found = [p for p in self.depotPaths2091ifp4PathStartsWith(path, p)]2092if not found:2093 fnum = fnum +12094continue20952096file= {}2097file["path"] = path2098file["rev"] = commit["rev%s"% fnum]2099file["action"] = commit["action%s"% fnum]2100file["type"] = commit["type%s"% fnum]2101 fnum = fnum +121022103# start with the full relative path where this file would2104# go in a p4 client2105if self.useClientSpec:2106 relPath = self.clientSpecDirs.map_in_client(path)2107else:2108 relPath = self.stripRepoPath(path, self.depotPaths)21092110for branch in self.knownBranches.keys():2111# add a trailing slash so that a commit into qt/4.2foo2112# doesn't end up in qt/4.2, e.g.2113if relPath.startswith(branch +"/"):2114if branch not in branches:2115 branches[branch] = []2116 branches[branch].append(file)2117break21182119return branches21202121# output one file from the P4 stream2122# - helper for streamP4Files21232124defstreamOneP4File(self,file, contents):2125 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2126if verbose:2127 sys.stderr.write("%s\n"% relPath)21282129(type_base, type_mods) =split_p4_type(file["type"])21302131 git_mode ="100644"2132if"x"in type_mods:2133 git_mode ="100755"2134if type_base =="symlink":2135 git_mode ="120000"2136# p4 print on a symlink contains "target\n"; remove the newline2137 data =''.join(contents)2138 contents = [data[:-1]]21392140if type_base =="utf16":2141# p4 delivers different text in the python output to -G2142# than it does when using "print -o", or normal p4 client2143# operations. utf16 is converted to ascii or utf8, perhaps.2144# But ascii text saved as -t utf16 is completely mangled.2145# Invoke print -o to get the real contents.2146#2147# On windows, the newlines will always be mangled by print, so put2148# them back too. This is not needed to the cygwin windows version,2149# just the native "NT" type.2150#2151 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2152ifp4_version_string().find("/NT") >=0:2153 text = text.replace("\r\n","\n")2154 contents = [ text ]21552156if type_base =="apple":2157# Apple filetype files will be streamed as a concatenation of2158# its appledouble header and the contents. This is useless2159# on both macs and non-macs. If using "print -q -o xx", it2160# will create "xx" with the data, and "%xx" with the header.2161# This is also not very useful.2162#2163# Ideally, someday, this script can learn how to generate2164# appledouble files directly and import those to git, but2165# non-mac machines can never find a use for apple filetype.2166print"\nIgnoring apple filetype file%s"%file['depotFile']2167return21682169# Note that we do not try to de-mangle keywords on utf16 files,2170# even though in theory somebody may want that.2171 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2172if pattern:2173 regexp = re.compile(pattern, re.VERBOSE)2174 text =''.join(contents)2175 text = regexp.sub(r'$\1$', text)2176 contents = [ text ]21772178 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21792180# total length...2181 length =02182for d in contents:2183 length = length +len(d)21842185 self.gitStream.write("data%d\n"% length)2186for d in contents:2187 self.gitStream.write(d)2188 self.gitStream.write("\n")21892190defstreamOneP4Deletion(self,file):2191 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2192if verbose:2193 sys.stderr.write("delete%s\n"% relPath)2194 self.gitStream.write("D%s\n"% relPath)21952196# handle another chunk of streaming data2197defstreamP4FilesCb(self, marshalled):21982199# catch p4 errors and complain2200 err =None2201if"code"in marshalled:2202if marshalled["code"] =="error":2203if"data"in marshalled:2204 err = marshalled["data"].rstrip()2205if err:2206 f =None2207if self.stream_have_file_info:2208if"depotFile"in self.stream_file:2209 f = self.stream_file["depotFile"]2210# force a failure in fast-import, else an empty2211# commit will be made2212 self.gitStream.write("\n")2213 self.gitStream.write("die-now\n")2214 self.gitStream.close()2215# ignore errors, but make sure it exits first2216 self.importProcess.wait()2217if f:2218die("Error from p4 print for%s:%s"% (f, err))2219else:2220die("Error from p4 print:%s"% err)22212222if marshalled.has_key('depotFile')and self.stream_have_file_info:2223# start of a new file - output the old one first2224 self.streamOneP4File(self.stream_file, self.stream_contents)2225 self.stream_file = {}2226 self.stream_contents = []2227 self.stream_have_file_info =False22282229# pick up the new file information... for the2230# 'data' field we need to append to our array2231for k in marshalled.keys():2232if k =='data':2233 self.stream_contents.append(marshalled['data'])2234else:2235 self.stream_file[k] = marshalled[k]22362237 self.stream_have_file_info =True22382239# Stream directly from "p4 files" into "git fast-import"2240defstreamP4Files(self, files):2241 filesForCommit = []2242 filesToRead = []2243 filesToDelete = []22442245for f in files:2246# if using a client spec, only add the files that have2247# a path in the client2248if self.clientSpecDirs:2249if self.clientSpecDirs.map_in_client(f['path']) =="":2250continue22512252 filesForCommit.append(f)2253if f['action']in self.delete_actions:2254 filesToDelete.append(f)2255else:2256 filesToRead.append(f)22572258# deleted files...2259for f in filesToDelete:2260 self.streamOneP4Deletion(f)22612262iflen(filesToRead) >0:2263 self.stream_file = {}2264 self.stream_contents = []2265 self.stream_have_file_info =False22662267# curry self argument2268defstreamP4FilesCbSelf(entry):2269 self.streamP4FilesCb(entry)22702271 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22722273p4CmdList(["-x","-","print"],2274 stdin=fileArgs,2275 cb=streamP4FilesCbSelf)22762277# do the last chunk2278if self.stream_file.has_key('depotFile'):2279 self.streamOneP4File(self.stream_file, self.stream_contents)22802281defmake_email(self, userid):2282if userid in self.users:2283return self.users[userid]2284else:2285return"%s<a@b>"% userid22862287# Stream a p4 tag2288defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2289if verbose:2290print"writing tag%sfor commit%s"% (labelName, commit)2291 gitStream.write("tag%s\n"% labelName)2292 gitStream.write("from%s\n"% commit)22932294if labelDetails.has_key('Owner'):2295 owner = labelDetails["Owner"]2296else:2297 owner =None22982299# Try to use the owner of the p4 label, or failing that,2300# the current p4 user id.2301if owner:2302 email = self.make_email(owner)2303else:2304 email = self.make_email(self.p4UserId())2305 tagger ="%s %s %s"% (email, epoch, self.tz)23062307 gitStream.write("tagger%s\n"% tagger)23082309print"labelDetails=",labelDetails2310if labelDetails.has_key('Description'):2311 description = labelDetails['Description']2312else:2313 description ='Label from git p4'23142315 gitStream.write("data%d\n"%len(description))2316 gitStream.write(description)2317 gitStream.write("\n")23182319defcommit(self, details, files, branch, parent =""):2320 epoch = details["time"]2321 author = details["user"]23222323if self.verbose:2324print"commit into%s"% branch23252326# start with reading files; if that fails, we should not2327# create a commit.2328 new_files = []2329for f in files:2330if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2331 new_files.append(f)2332else:2333 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23342335 self.gitStream.write("commit%s\n"% branch)2336# gitStream.write("mark :%s\n" % details["change"])2337 self.committedChanges.add(int(details["change"]))2338 committer =""2339if author not in self.users:2340 self.getUserMapFromPerforceServer()2341 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23422343 self.gitStream.write("committer%s\n"% committer)23442345 self.gitStream.write("data <<EOT\n")2346 self.gitStream.write(details["desc"])2347 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2348(','.join(self.branchPrefixes), details["change"]))2349iflen(details['options']) >0:2350 self.gitStream.write(": options =%s"% details['options'])2351 self.gitStream.write("]\nEOT\n\n")23522353iflen(parent) >0:2354if self.verbose:2355print"parent%s"% parent2356 self.gitStream.write("from%s\n"% parent)23572358 self.streamP4Files(new_files)2359 self.gitStream.write("\n")23602361 change =int(details["change"])23622363if self.labels.has_key(change):2364 label = self.labels[change]2365 labelDetails = label[0]2366 labelRevisions = label[1]2367if self.verbose:2368print"Change%sis labelled%s"% (change, labelDetails)23692370 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2371for p in self.branchPrefixes])23722373iflen(files) ==len(labelRevisions):23742375 cleanedFiles = {}2376for info in files:2377if info["action"]in self.delete_actions:2378continue2379 cleanedFiles[info["depotFile"]] = info["rev"]23802381if cleanedFiles == labelRevisions:2382 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23832384else:2385if not self.silent:2386print("Tag%sdoes not match with change%s: files do not match."2387% (labelDetails["label"], change))23882389else:2390if not self.silent:2391print("Tag%sdoes not match with change%s: file count is different."2392% (labelDetails["label"], change))23932394# Build a dictionary of changelists and labels, for "detect-labels" option.2395defgetLabels(self):2396 self.labels = {}23972398 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2399iflen(l) >0and not self.silent:2400print"Finding files belonging to labels in%s"% `self.depotPaths`24012402for output in l:2403 label = output["label"]2404 revisions = {}2405 newestChange =02406if self.verbose:2407print"Querying files for label%s"% label2408forfileinp4CmdList(["files"] +2409["%s...@%s"% (p, label)2410for p in self.depotPaths]):2411 revisions[file["depotFile"]] =file["rev"]2412 change =int(file["change"])2413if change > newestChange:2414 newestChange = change24152416 self.labels[newestChange] = [output, revisions]24172418if self.verbose:2419print"Label changes:%s"% self.labels.keys()24202421# Import p4 labels as git tags. A direct mapping does not2422# exist, so assume that if all the files are at the same revision2423# then we can use that, or it's something more complicated we should2424# just ignore.2425defimportP4Labels(self, stream, p4Labels):2426if verbose:2427print"import p4 labels: "+' '.join(p4Labels)24282429 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2430 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2431iflen(validLabelRegexp) ==0:2432 validLabelRegexp = defaultLabelRegexp2433 m = re.compile(validLabelRegexp)24342435for name in p4Labels:2436 commitFound =False24372438if not m.match(name):2439if verbose:2440print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2441continue24422443if name in ignoredP4Labels:2444continue24452446 labelDetails =p4CmdList(['label',"-o", name])[0]24472448# get the most recent changelist for each file in this label2449 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2450for p in self.depotPaths])24512452if change.has_key('change'):2453# find the corresponding git commit; take the oldest commit2454 changelist =int(change['change'])2455 gitCommit =read_pipe(["git","rev-list","--max-count=1",2456"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2457iflen(gitCommit) ==0:2458print"could not find git commit for changelist%d"% changelist2459else:2460 gitCommit = gitCommit.strip()2461 commitFound =True2462# Convert from p4 time format2463try:2464 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2465exceptValueError:2466print"Could not convert label time%s"% labelDetails['Update']2467 tmwhen =124682469 when =int(time.mktime(tmwhen))2470 self.streamTag(stream, name, labelDetails, gitCommit, when)2471if verbose:2472print"p4 label%smapped to git commit%s"% (name, gitCommit)2473else:2474if verbose:2475print"Label%shas no changelists - possibly deleted?"% name24762477if not commitFound:2478# We can't import this label; don't try again as it will get very2479# expensive repeatedly fetching all the files for labels that will2480# never be imported. If the label is moved in the future, the2481# ignore will need to be removed manually.2482system(["git","config","--add","git-p4.ignoredP4Labels", name])24832484defguessProjectName(self):2485for p in self.depotPaths:2486if p.endswith("/"):2487 p = p[:-1]2488 p = p[p.strip().rfind("/") +1:]2489if not p.endswith("/"):2490 p +="/"2491return p24922493defgetBranchMapping(self):2494 lostAndFoundBranches =set()24952496 user =gitConfig("git-p4.branchUser")2497iflen(user) >0:2498 command ="branches -u%s"% user2499else:2500 command ="branches"25012502for info inp4CmdList(command):2503 details =p4Cmd(["branch","-o", info["branch"]])2504 viewIdx =02505while details.has_key("View%s"% viewIdx):2506 paths = details["View%s"% viewIdx].split(" ")2507 viewIdx = viewIdx +12508# require standard //depot/foo/... //depot/bar/... mapping2509iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2510continue2511 source = paths[0]2512 destination = paths[1]2513## HACK2514ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2515 source = source[len(self.depotPaths[0]):-4]2516 destination = destination[len(self.depotPaths[0]):-4]25172518if destination in self.knownBranches:2519if not self.silent:2520print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2521print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2522continue25232524 self.knownBranches[destination] = source25252526 lostAndFoundBranches.discard(destination)25272528if source not in self.knownBranches:2529 lostAndFoundBranches.add(source)25302531# Perforce does not strictly require branches to be defined, so we also2532# check git config for a branch list.2533#2534# Example of branch definition in git config file:2535# [git-p4]2536# branchList=main:branchA2537# branchList=main:branchB2538# branchList=branchA:branchC2539 configBranches =gitConfigList("git-p4.branchList")2540for branch in configBranches:2541if branch:2542(source, destination) = branch.split(":")2543 self.knownBranches[destination] = source25442545 lostAndFoundBranches.discard(destination)25462547if source not in self.knownBranches:2548 lostAndFoundBranches.add(source)254925502551for branch in lostAndFoundBranches:2552 self.knownBranches[branch] = branch25532554defgetBranchMappingFromGitBranches(self):2555 branches =p4BranchesInGit(self.importIntoRemotes)2556for branch in branches.keys():2557if branch =="master":2558 branch ="main"2559else:2560 branch = branch[len(self.projectName):]2561 self.knownBranches[branch] = branch25622563defupdateOptionDict(self, d):2564 option_keys = {}2565if self.keepRepoPath:2566 option_keys['keepRepoPath'] =125672568 d["options"] =' '.join(sorted(option_keys.keys()))25692570defreadOptions(self, d):2571 self.keepRepoPath = (d.has_key('options')2572and('keepRepoPath'in d['options']))25732574defgitRefForBranch(self, branch):2575if branch =="main":2576return self.refPrefix +"master"25772578iflen(branch) <=0:2579return branch25802581return self.refPrefix + self.projectName + branch25822583defgitCommitByP4Change(self, ref, change):2584if self.verbose:2585print"looking in ref "+ ref +" for change%susing bisect..."% change25862587 earliestCommit =""2588 latestCommit =parseRevision(ref)25892590while True:2591if self.verbose:2592print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2593 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2594iflen(next) ==0:2595if self.verbose:2596print"argh"2597return""2598 log =extractLogMessageFromGitCommit(next)2599 settings =extractSettingsGitLog(log)2600 currentChange =int(settings['change'])2601if self.verbose:2602print"current change%s"% currentChange26032604if currentChange == change:2605if self.verbose:2606print"found%s"% next2607return next26082609if currentChange < change:2610 earliestCommit ="^%s"% next2611else:2612 latestCommit ="%s"% next26132614return""26152616defimportNewBranch(self, branch, maxChange):2617# make fast-import flush all changes to disk and update the refs using the checkpoint2618# command so that we can try to find the branch parent in the git history2619 self.gitStream.write("checkpoint\n\n");2620 self.gitStream.flush();2621 branchPrefix = self.depotPaths[0] + branch +"/"2622range="@1,%s"% maxChange2623#print "prefix" + branchPrefix2624 changes =p4ChangesForPaths([branchPrefix],range)2625iflen(changes) <=0:2626return False2627 firstChange = changes[0]2628#print "first change in branch: %s" % firstChange2629 sourceBranch = self.knownBranches[branch]2630 sourceDepotPath = self.depotPaths[0] + sourceBranch2631 sourceRef = self.gitRefForBranch(sourceBranch)2632#print "source " + sourceBranch26332634 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2635#print "branch parent: %s" % branchParentChange2636 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2637iflen(gitParent) >0:2638 self.initialParents[self.gitRefForBranch(branch)] = gitParent2639#print "parent git commit: %s" % gitParent26402641 self.importChanges(changes)2642return True26432644defsearchParent(self, parent, branch, target):2645 parentFound =False2646for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2647 blob = blob.strip()2648iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2649 parentFound =True2650if self.verbose:2651print"Found parent of%sin commit%s"% (branch, blob)2652break2653if parentFound:2654return blob2655else:2656return None26572658defimportChanges(self, changes):2659 cnt =12660for change in changes:2661 description =p4_describe(change)2662 self.updateOptionDict(description)26632664if not self.silent:2665 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2666 sys.stdout.flush()2667 cnt = cnt +126682669try:2670if self.detectBranches:2671 branches = self.splitFilesIntoBranches(description)2672for branch in branches.keys():2673## HACK --hwn2674 branchPrefix = self.depotPaths[0] + branch +"/"2675 self.branchPrefixes = [ branchPrefix ]26762677 parent =""26782679 filesForCommit = branches[branch]26802681if self.verbose:2682print"branch is%s"% branch26832684 self.updatedBranches.add(branch)26852686if branch not in self.createdBranches:2687 self.createdBranches.add(branch)2688 parent = self.knownBranches[branch]2689if parent == branch:2690 parent =""2691else:2692 fullBranch = self.projectName + branch2693if fullBranch not in self.p4BranchesInGit:2694if not self.silent:2695print("\nImporting new branch%s"% fullBranch);2696if self.importNewBranch(branch, change -1):2697 parent =""2698 self.p4BranchesInGit.append(fullBranch)2699if not self.silent:2700print("\nResuming with change%s"% change);27012702if self.verbose:2703print"parent determined through known branches:%s"% parent27042705 branch = self.gitRefForBranch(branch)2706 parent = self.gitRefForBranch(parent)27072708if self.verbose:2709print"looking for initial parent for%s; current parent is%s"% (branch, parent)27102711iflen(parent) ==0and branch in self.initialParents:2712 parent = self.initialParents[branch]2713del self.initialParents[branch]27142715 blob =None2716iflen(parent) >0:2717 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2718if self.verbose:2719print"Creating temporary branch: "+ tempBranch2720 self.commit(description, filesForCommit, tempBranch)2721 self.tempBranches.append(tempBranch)2722 self.checkpoint()2723 blob = self.searchParent(parent, branch, tempBranch)2724if blob:2725 self.commit(description, filesForCommit, branch, blob)2726else:2727if self.verbose:2728print"Parent of%snot found. Committing into head of%s"% (branch, parent)2729 self.commit(description, filesForCommit, branch, parent)2730else:2731 files = self.extractFilesFromCommit(description)2732 self.commit(description, files, self.branch,2733 self.initialParent)2734# only needed once, to connect to the previous commit2735 self.initialParent =""2736exceptIOError:2737print self.gitError.read()2738 sys.exit(1)27392740defimportHeadRevision(self, revision):2741print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27422743 details = {}2744 details["user"] ="git perforce import user"2745 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2746% (' '.join(self.depotPaths), revision))2747 details["change"] = revision2748 newestRevision =027492750 fileCnt =02751 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27522753for info inp4CmdList(["files"] + fileArgs):27542755if'code'in info and info['code'] =='error':2756 sys.stderr.write("p4 returned an error:%s\n"2757% info['data'])2758if info['data'].find("must refer to client") >=0:2759 sys.stderr.write("This particular p4 error is misleading.\n")2760 sys.stderr.write("Perhaps the depot path was misspelled.\n");2761 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2762 sys.exit(1)2763if'p4ExitCode'in info:2764 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2765 sys.exit(1)276627672768 change =int(info["change"])2769if change > newestRevision:2770 newestRevision = change27712772if info["action"]in self.delete_actions:2773# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2774#fileCnt = fileCnt + 12775continue27762777for prop in["depotFile","rev","action","type"]:2778 details["%s%s"% (prop, fileCnt)] = info[prop]27792780 fileCnt = fileCnt +127812782 details["change"] = newestRevision27832784# Use time from top-most change so that all git p4 clones of2785# the same p4 repo have the same commit SHA1s.2786 res =p4_describe(newestRevision)2787 details["time"] = res["time"]27882789 self.updateOptionDict(details)2790try:2791 self.commit(details, self.extractFilesFromCommit(details), self.branch)2792exceptIOError:2793print"IO error with git fast-import. Is your git version recent enough?"2794print self.gitError.read()279527962797defrun(self, args):2798 self.depotPaths = []2799 self.changeRange =""2800 self.previousDepotPaths = []2801 self.hasOrigin =False28022803# map from branch depot path to parent branch2804 self.knownBranches = {}2805 self.initialParents = {}28062807if self.importIntoRemotes:2808 self.refPrefix ="refs/remotes/p4/"2809else:2810 self.refPrefix ="refs/heads/p4/"28112812if self.syncWithOrigin:2813 self.hasOrigin =originP4BranchesExist()2814if self.hasOrigin:2815if not self.silent:2816print'Syncing with origin first, using "git fetch origin"'2817system("git fetch origin")28182819 branch_arg_given =bool(self.branch)2820iflen(self.branch) ==0:2821 self.branch = self.refPrefix +"master"2822ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2823system("git update-ref%srefs/heads/p4"% self.branch)2824system("git branch -D p4")28252826# accept either the command-line option, or the configuration variable2827if self.useClientSpec:2828# will use this after clone to set the variable2829 self.useClientSpec_from_options =True2830else:2831ifgitConfig("git-p4.useclientspec","--bool") =="true":2832 self.useClientSpec =True2833if self.useClientSpec:2834 self.clientSpecDirs =getClientSpec()28352836# TODO: should always look at previous commits,2837# merge with previous imports, if possible.2838if args == []:2839if self.hasOrigin:2840createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28412842# branches holds mapping from branch name to sha12843 branches =p4BranchesInGit(self.importIntoRemotes)28442845# restrict to just this one, disabling detect-branches2846if branch_arg_given:2847 short = self.branch.split("/")[-1]2848if short in branches:2849 self.p4BranchesInGit = [ short ]2850else:2851 self.p4BranchesInGit = branches.keys()28522853iflen(self.p4BranchesInGit) >1:2854if not self.silent:2855print"Importing from/into multiple branches"2856 self.detectBranches =True2857for branch in branches.keys():2858 self.initialParents[self.refPrefix + branch] = \2859 branches[branch]28602861if self.verbose:2862print"branches:%s"% self.p4BranchesInGit28632864 p4Change =02865for branch in self.p4BranchesInGit:2866 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28672868 settings =extractSettingsGitLog(logMsg)28692870 self.readOptions(settings)2871if(settings.has_key('depot-paths')2872and settings.has_key('change')):2873 change =int(settings['change']) +12874 p4Change =max(p4Change, change)28752876 depotPaths =sorted(settings['depot-paths'])2877if self.previousDepotPaths == []:2878 self.previousDepotPaths = depotPaths2879else:2880 paths = []2881for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2882 prev_list = prev.split("/")2883 cur_list = cur.split("/")2884for i inrange(0,min(len(cur_list),len(prev_list))):2885if cur_list[i] <> prev_list[i]:2886 i = i -12887break28882889 paths.append("/".join(cur_list[:i +1]))28902891 self.previousDepotPaths = paths28922893if p4Change >0:2894 self.depotPaths =sorted(self.previousDepotPaths)2895 self.changeRange ="@%s,#head"% p4Change2896if not self.silent and not self.detectBranches:2897print"Performing incremental import into%sgit branch"% self.branch28982899# accept multiple ref name abbreviations:2900# refs/foo/bar/branch -> use it exactly2901# p4/branch -> prepend refs/remotes/ or refs/heads/2902# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2903if not self.branch.startswith("refs/"):2904if self.importIntoRemotes:2905 prepend ="refs/remotes/"2906else:2907 prepend ="refs/heads/"2908if not self.branch.startswith("p4/"):2909 prepend +="p4/"2910 self.branch = prepend + self.branch29112912iflen(args) ==0and self.depotPaths:2913if not self.silent:2914print"Depot paths:%s"%' '.join(self.depotPaths)2915else:2916if self.depotPaths and self.depotPaths != args:2917print("previous import used depot path%sand now%swas specified. "2918"This doesn't work!"% (' '.join(self.depotPaths),2919' '.join(args)))2920 sys.exit(1)29212922 self.depotPaths =sorted(args)29232924 revision =""2925 self.users = {}29262927# Make sure no revision specifiers are used when --changesfile2928# is specified.2929 bad_changesfile =False2930iflen(self.changesFile) >0:2931for p in self.depotPaths:2932if p.find("@") >=0or p.find("#") >=0:2933 bad_changesfile =True2934break2935if bad_changesfile:2936die("Option --changesfile is incompatible with revision specifiers")29372938 newPaths = []2939for p in self.depotPaths:2940if p.find("@") != -1:2941 atIdx = p.index("@")2942 self.changeRange = p[atIdx:]2943if self.changeRange =="@all":2944 self.changeRange =""2945elif','not in self.changeRange:2946 revision = self.changeRange2947 self.changeRange =""2948 p = p[:atIdx]2949elif p.find("#") != -1:2950 hashIdx = p.index("#")2951 revision = p[hashIdx:]2952 p = p[:hashIdx]2953elif self.previousDepotPaths == []:2954# pay attention to changesfile, if given, else import2955# the entire p4 tree at the head revision2956iflen(self.changesFile) ==0:2957 revision ="#head"29582959 p = re.sub("\.\.\.$","", p)2960if not p.endswith("/"):2961 p +="/"29622963 newPaths.append(p)29642965 self.depotPaths = newPaths29662967# --detect-branches may change this for each branch2968 self.branchPrefixes = self.depotPaths29692970 self.loadUserMapFromCache()2971 self.labels = {}2972if self.detectLabels:2973 self.getLabels();29742975if self.detectBranches:2976## FIXME - what's a P4 projectName ?2977 self.projectName = self.guessProjectName()29782979if self.hasOrigin:2980 self.getBranchMappingFromGitBranches()2981else:2982 self.getBranchMapping()2983if self.verbose:2984print"p4-git branches:%s"% self.p4BranchesInGit2985print"initial parents:%s"% self.initialParents2986for b in self.p4BranchesInGit:2987if b !="master":29882989## FIXME2990 b = b[len(self.projectName):]2991 self.createdBranches.add(b)29922993 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29942995 self.importProcess = subprocess.Popen(["git","fast-import"],2996 stdin=subprocess.PIPE,2997 stdout=subprocess.PIPE,2998 stderr=subprocess.PIPE);2999 self.gitOutput = self.importProcess.stdout3000 self.gitStream = self.importProcess.stdin3001 self.gitError = self.importProcess.stderr30023003if revision:3004 self.importHeadRevision(revision)3005else:3006 changes = []30073008iflen(self.changesFile) >0:3009 output =open(self.changesFile).readlines()3010 changeSet =set()3011for line in output:3012 changeSet.add(int(line))30133014for change in changeSet:3015 changes.append(change)30163017 changes.sort()3018else:3019# catch "git p4 sync" with no new branches, in a repo that3020# does not have any existing p4 branches3021iflen(args) ==0:3022if not self.p4BranchesInGit:3023die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30243025# The default branch is master, unless --branch is used to3026# specify something else. Make sure it exists, or complain3027# nicely about how to use --branch.3028if not self.detectBranches:3029if notbranch_exists(self.branch):3030if branch_arg_given:3031die("Error: branch%sdoes not exist."% self.branch)3032else:3033die("Error: no branch%s; perhaps specify one with --branch."%3034 self.branch)30353036if self.verbose:3037print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3038 self.changeRange)3039 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30403041iflen(self.maxChanges) >0:3042 changes = changes[:min(int(self.maxChanges),len(changes))]30433044iflen(changes) ==0:3045if not self.silent:3046print"No changes to import!"3047else:3048if not self.silent and not self.detectBranches:3049print"Import destination:%s"% self.branch30503051 self.updatedBranches =set()30523053if not self.detectBranches:3054if args:3055# start a new branch3056 self.initialParent =""3057else:3058# build on a previous revision3059 self.initialParent =parseRevision(self.branch)30603061 self.importChanges(changes)30623063if not self.silent:3064print""3065iflen(self.updatedBranches) >0:3066 sys.stdout.write("Updated branches: ")3067for b in self.updatedBranches:3068 sys.stdout.write("%s"% b)3069 sys.stdout.write("\n")30703071ifgitConfig("git-p4.importLabels","--bool") =="true":3072 self.importLabels =True30733074if self.importLabels:3075 p4Labels =getP4Labels(self.depotPaths)3076 gitTags =getGitTags()30773078 missingP4Labels = p4Labels - gitTags3079 self.importP4Labels(self.gitStream, missingP4Labels)30803081 self.gitStream.close()3082if self.importProcess.wait() !=0:3083die("fast-import failed:%s"% self.gitError.read())3084 self.gitOutput.close()3085 self.gitError.close()30863087# Cleanup temporary branches created during import3088if self.tempBranches != []:3089for branch in self.tempBranches:3090read_pipe("git update-ref -d%s"% branch)3091 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30923093# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3094# a convenient shortcut refname "p4".3095if self.importIntoRemotes:3096 head_ref = self.refPrefix +"HEAD"3097if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3098system(["git","symbolic-ref", head_ref, self.branch])30993100return True31013102classP4Rebase(Command):3103def__init__(self):3104 Command.__init__(self)3105 self.options = [3106 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3107]3108 self.importLabels =False3109 self.description = ("Fetches the latest revision from perforce and "3110+"rebases the current work (branch) against it")31113112defrun(self, args):3113 sync =P4Sync()3114 sync.importLabels = self.importLabels3115 sync.run([])31163117return self.rebase()31183119defrebase(self):3120if os.system("git update-index --refresh") !=0:3121die("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.");3122iflen(read_pipe("git diff-index HEAD --")) >0:3123die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");31243125[upstream, settings] =findUpstreamBranchPoint()3126iflen(upstream) ==0:3127die("Cannot find upstream branchpoint for rebase")31283129# the branchpoint may be p4/foo~3, so strip off the parent3130 upstream = re.sub("~[0-9]+$","", upstream)31313132print"Rebasing the current branch onto%s"% upstream3133 oldHead =read_pipe("git rev-parse HEAD").strip()3134system("git rebase%s"% upstream)3135system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3136return True31373138classP4Clone(P4Sync):3139def__init__(self):3140 P4Sync.__init__(self)3141 self.description ="Creates a new git repository and imports from Perforce into it"3142 self.usage ="usage: %prog [options] //depot/path[@revRange]"3143 self.options += [3144 optparse.make_option("--destination", dest="cloneDestination",3145 action='store', default=None,3146help="where to leave result of the clone"),3147 optparse.make_option("-/", dest="cloneExclude",3148 action="append",type="string",3149help="exclude depot path"),3150 optparse.make_option("--bare", dest="cloneBare",3151 action="store_true", default=False),3152]3153 self.cloneDestination =None3154 self.needsGit =False3155 self.cloneBare =False31563157# This is required for the "append" cloneExclude action3158defensure_value(self, attr, value):3159if nothasattr(self, attr)orgetattr(self, attr)is None:3160setattr(self, attr, value)3161returngetattr(self, attr)31623163defdefaultDestination(self, args):3164## TODO: use common prefix of args?3165 depotPath = args[0]3166 depotDir = re.sub("(@[^@]*)$","", depotPath)3167 depotDir = re.sub("(#[^#]*)$","", depotDir)3168 depotDir = re.sub(r"\.\.\.$","", depotDir)3169 depotDir = re.sub(r"/$","", depotDir)3170return os.path.split(depotDir)[1]31713172defrun(self, args):3173iflen(args) <1:3174return False31753176if self.keepRepoPath and not self.cloneDestination:3177 sys.stderr.write("Must specify destination for --keep-path\n")3178 sys.exit(1)31793180 depotPaths = args31813182if not self.cloneDestination andlen(depotPaths) >1:3183 self.cloneDestination = depotPaths[-1]3184 depotPaths = depotPaths[:-1]31853186 self.cloneExclude = ["/"+p for p in self.cloneExclude]3187for p in depotPaths:3188if not p.startswith("//"):3189 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3190return False31913192if not self.cloneDestination:3193 self.cloneDestination = self.defaultDestination(args)31943195print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31963197if not os.path.exists(self.cloneDestination):3198 os.makedirs(self.cloneDestination)3199chdir(self.cloneDestination)32003201 init_cmd = ["git","init"]3202if self.cloneBare:3203 init_cmd.append("--bare")3204 subprocess.check_call(init_cmd)32053206if not P4Sync.run(self, depotPaths):3207return False32083209# create a master branch and check out a work tree3210ifgitBranchExists(self.branch):3211system(["git","branch","master", self.branch ])3212if not self.cloneBare:3213system(["git","checkout","-f"])3214else:3215print'Not checking out any branch, use ' \3216'"git checkout -q -b master <branch>"'32173218# auto-set this variable if invoked with --use-client-spec3219if self.useClientSpec_from_options:3220system("git config --bool git-p4.useclientspec true")32213222return True32233224classP4Branches(Command):3225def__init__(self):3226 Command.__init__(self)3227 self.options = [ ]3228 self.description = ("Shows the git branches that hold imports and their "3229+"corresponding perforce depot paths")3230 self.verbose =False32313232defrun(self, args):3233iforiginP4BranchesExist():3234createOrUpdateBranchesFromOrigin()32353236 cmdline ="git rev-parse --symbolic "3237 cmdline +=" --remotes"32383239for line inread_pipe_lines(cmdline):3240 line = line.strip()32413242if not line.startswith('p4/')or line =="p4/HEAD":3243continue3244 branch = line32453246 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3247 settings =extractSettingsGitLog(log)32483249print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3250return True32513252classHelpFormatter(optparse.IndentedHelpFormatter):3253def__init__(self):3254 optparse.IndentedHelpFormatter.__init__(self)32553256defformat_description(self, description):3257if description:3258return description +"\n"3259else:3260return""32613262defprintUsage(commands):3263print"usage:%s<command> [options]"% sys.argv[0]3264print""3265print"valid commands:%s"%", ".join(commands)3266print""3267print"Try%s<command> --help for command specific help."% sys.argv[0]3268print""32693270commands = {3271"debug": P4Debug,3272"submit": P4Submit,3273"commit": P4Submit,3274"sync": P4Sync,3275"rebase": P4Rebase,3276"clone": P4Clone,3277"rollback": P4RollBack,3278"branches": P4Branches3279}328032813282defmain():3283iflen(sys.argv[1:]) ==0:3284printUsage(commands.keys())3285 sys.exit(2)32863287 cmdName = sys.argv[1]3288try:3289 klass = commands[cmdName]3290 cmd =klass()3291exceptKeyError:3292print"unknown command%s"% cmdName3293print""3294printUsage(commands.keys())3295 sys.exit(2)32963297 options = cmd.options3298 cmd.gitdir = os.environ.get("GIT_DIR",None)32993300 args = sys.argv[2:]33013302 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3303if cmd.needsGit:3304 options.append(optparse.make_option("--git-dir", dest="gitdir"))33053306 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3307 options,3308 description = cmd.description,3309 formatter =HelpFormatter())33103311(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3312global verbose3313 verbose = cmd.verbose3314if cmd.needsGit:3315if cmd.gitdir ==None:3316 cmd.gitdir = os.path.abspath(".git")3317if notisValidGitDir(cmd.gitdir):3318 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3319if os.path.exists(cmd.gitdir):3320 cdup =read_pipe("git rev-parse --show-cdup").strip()3321iflen(cdup) >0:3322chdir(cdup);33233324if notisValidGitDir(cmd.gitdir):3325ifisValidGitDir(cmd.gitdir +"/.git"):3326 cmd.gitdir +="/.git"3327else:3328die("fatal: cannot locate git repository at%s"% cmd.gitdir)33293330 os.environ["GIT_DIR"] = cmd.gitdir33313332if not cmd.run(args):3333 parser.print_help()3334 sys.exit(2)333533363337if __name__ =='__main__':3338main()