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",1054"--format=%ae",id])1055 gitEmail = gitEmail.strip()1056if not self.emails.has_key(gitEmail):1057return(None,gitEmail)1058else:1059return(self.emails[gitEmail],gitEmail)10601061defcheckValidP4Users(self,commits):1062# check if any git authors cannot be mapped to p4 users1063foridin commits:1064(user,email) = self.p4UserForCommit(id)1065if not user:1066 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1067ifgitConfig('git-p4.allowMissingP4Users').lower() =="true":1068print"%s"% msg1069else:1070die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10711072deflastP4Changelist(self):1073# Get back the last changelist number submitted in this client spec. This1074# then gets used to patch up the username in the change. If the same1075# client spec is being used by multiple processes then this might go1076# wrong.1077 results =p4CmdList("client -o")# find the current client1078 client =None1079for r in results:1080if r.has_key('Client'):1081 client = r['Client']1082break1083if not client:1084die("could not get client spec")1085 results =p4CmdList(["changes","-c", client,"-m","1"])1086for r in results:1087if r.has_key('change'):1088return r['change']1089die("Could not get changelist number for last submit - cannot patch up user details")10901091defmodifyChangelistUser(self, changelist, newUser):1092# fixup the user field of a changelist after it has been submitted.1093 changes =p4CmdList("change -o%s"% changelist)1094iflen(changes) !=1:1095die("Bad output from p4 change modifying%sto user%s"%1096(changelist, newUser))10971098 c = changes[0]1099if c['User'] == newUser:return# nothing to do1100 c['User'] = newUser1101input= marshal.dumps(c)11021103 result =p4CmdList("change -f -i", stdin=input)1104for r in result:1105if r.has_key('code'):1106if r['code'] =='error':1107die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1108if r.has_key('data'):1109print("Updated user field for changelist%sto%s"% (changelist, newUser))1110return1111die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11121113defcanChangeChangelists(self):1114# check to see if we have p4 admin or super-user permissions, either of1115# which are required to modify changelists.1116 results =p4CmdList(["protects", self.depotPath])1117for r in results:1118if r.has_key('perm'):1119if r['perm'] =='admin':1120return11121if r['perm'] =='super':1122return11123return011241125defprepareSubmitTemplate(self):1126"""Run "p4 change -o" to grab a change specification template.1127 This does not use "p4 -G", as it is nice to keep the submission1128 template in original order, since a human might edit it.11291130 Remove lines in the Files section that show changes to files1131 outside the depot path we're committing into."""11321133 template =""1134 inFilesSection =False1135for line inp4_read_pipe_lines(['change','-o']):1136if line.endswith("\r\n"):1137 line = line[:-2] +"\n"1138if inFilesSection:1139if line.startswith("\t"):1140# path starts and ends with a tab1141 path = line[1:]1142 lastTab = path.rfind("\t")1143if lastTab != -1:1144 path = path[:lastTab]1145if notp4PathStartsWith(path, self.depotPath):1146continue1147else:1148 inFilesSection =False1149else:1150if line.startswith("Files:"):1151 inFilesSection =True11521153 template += line11541155return template11561157defedit_template(self, template_file):1158"""Invoke the editor to let the user change the submission1159 message. Return true if okay to continue with the submit."""11601161# if configured to skip the editing part, just submit1162ifgitConfig("git-p4.skipSubmitEdit") =="true":1163return True11641165# look at the modification time, to check later if the user saved1166# the file1167 mtime = os.stat(template_file).st_mtime11681169# invoke the editor1170if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1171 editor = os.environ.get("P4EDITOR")1172else:1173 editor =read_pipe("git var GIT_EDITOR").strip()1174system(editor +" "+ template_file)11751176# If the file was not saved, prompt to see if this patch should1177# be skipped. But skip this verification step if configured so.1178ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1179return True11801181# modification time updated means user saved the file1182if os.stat(template_file).st_mtime > mtime:1183return True11841185while True:1186 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1187if response =='y':1188return True1189if response =='n':1190return False11911192defapplyCommit(self,id):1193"""Apply one commit, return True if it succeeded."""11941195print"Applying",read_pipe(["git","show","-s",1196"--format=format:%h%s",id])11971198(p4User, gitEmail) = self.p4UserForCommit(id)11991200 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1201 filesToAdd =set()1202 filesToDelete =set()1203 editedFiles =set()1204 pureRenameCopy =set()1205 filesToChangeExecBit = {}12061207for line in diff:1208 diff =parseDiffTreeEntry(line)1209 modifier = diff['status']1210 path = diff['src']1211if modifier =="M":1212p4_edit(path)1213ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1214 filesToChangeExecBit[path] = diff['dst_mode']1215 editedFiles.add(path)1216elif modifier =="A":1217 filesToAdd.add(path)1218 filesToChangeExecBit[path] = diff['dst_mode']1219if path in filesToDelete:1220 filesToDelete.remove(path)1221elif modifier =="D":1222 filesToDelete.add(path)1223if path in filesToAdd:1224 filesToAdd.remove(path)1225elif modifier =="C":1226 src, dest = diff['src'], diff['dst']1227p4_integrate(src, dest)1228 pureRenameCopy.add(dest)1229if diff['src_sha1'] != diff['dst_sha1']:1230p4_edit(dest)1231 pureRenameCopy.discard(dest)1232ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1233p4_edit(dest)1234 pureRenameCopy.discard(dest)1235 filesToChangeExecBit[dest] = diff['dst_mode']1236if self.isWindows:1237# turn off read-only attribute1238 os.chmod(dest, stat.S_IWRITE)1239 os.unlink(dest)1240 editedFiles.add(dest)1241elif modifier =="R":1242 src, dest = diff['src'], diff['dst']1243if self.p4HasMoveCommand:1244p4_edit(src)# src must be open before move1245p4_move(src, dest)# opens for (move/delete, move/add)1246else:1247p4_integrate(src, dest)1248if diff['src_sha1'] != diff['dst_sha1']:1249p4_edit(dest)1250else:1251 pureRenameCopy.add(dest)1252ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1253if not self.p4HasMoveCommand:1254p4_edit(dest)# with move: already open, writable1255 filesToChangeExecBit[dest] = diff['dst_mode']1256if not self.p4HasMoveCommand:1257if self.isWindows:1258 os.chmod(dest, stat.S_IWRITE)1259 os.unlink(dest)1260 filesToDelete.add(src)1261 editedFiles.add(dest)1262else:1263die("unknown modifier%sfor%s"% (modifier, path))12641265 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1266 patchcmd = diffcmd +" | git apply "1267 tryPatchCmd = patchcmd +"--check -"1268 applyPatchCmd = patchcmd +"--check --apply -"1269 patch_succeeded =True12701271if os.system(tryPatchCmd) !=0:1272 fixed_rcs_keywords =False1273 patch_succeeded =False1274print"Unfortunately applying the change failed!"12751276# Patch failed, maybe it's just RCS keyword woes. Look through1277# the patch to see if that's possible.1278ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1279file=None1280 pattern =None1281 kwfiles = {}1282forfilein editedFiles | filesToDelete:1283# did this file's delta contain RCS keywords?1284 pattern =p4_keywords_regexp_for_file(file)12851286if pattern:1287# this file is a possibility...look for RCS keywords.1288 regexp = re.compile(pattern, re.VERBOSE)1289for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1290if regexp.search(line):1291if verbose:1292print"got keyword match on%sin%sin%s"% (pattern, line,file)1293 kwfiles[file] = pattern1294break12951296forfilein kwfiles:1297if verbose:1298print"zapping%swith%s"% (line,pattern)1299# File is being deleted, so not open in p4. Must1300# disable the read-only bit on windows.1301if self.isWindows andfilenot in editedFiles:1302 os.chmod(file, stat.S_IWRITE)1303 self.patchRCSKeywords(file, kwfiles[file])1304 fixed_rcs_keywords =True13051306if fixed_rcs_keywords:1307print"Retrying the patch with RCS keywords cleaned up"1308if os.system(tryPatchCmd) ==0:1309 patch_succeeded =True13101311if not patch_succeeded:1312for f in editedFiles:1313p4_revert(f)1314return False13151316#1317# Apply the patch for real, and do add/delete/+x handling.1318#1319system(applyPatchCmd)13201321for f in filesToAdd:1322p4_add(f)1323for f in filesToDelete:1324p4_revert(f)1325p4_delete(f)13261327# Set/clear executable bits1328for f in filesToChangeExecBit.keys():1329 mode = filesToChangeExecBit[f]1330setP4ExecBit(f, mode)13311332#1333# Build p4 change description, starting with the contents1334# of the git commit message.1335#1336 logMessage =extractLogMessageFromGitCommit(id)1337 logMessage = logMessage.strip()1338(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13391340 template = self.prepareSubmitTemplate()1341 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13421343if self.preserveUser:1344 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13451346if self.checkAuthorship and not self.p4UserIsMe(p4User):1347 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1348 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1349 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13501351 separatorLine ="######## everything below this line is just the diff #######\n"13521353# diff1354if os.environ.has_key("P4DIFF"):1355del(os.environ["P4DIFF"])1356 diff =""1357for editedFile in editedFiles:1358 diff +=p4_read_pipe(['diff','-du',1359wildcard_encode(editedFile)])13601361# new file diff1362 newdiff =""1363for newFile in filesToAdd:1364 newdiff +="==== new file ====\n"1365 newdiff +="--- /dev/null\n"1366 newdiff +="+++%s\n"% newFile1367 f =open(newFile,"r")1368for line in f.readlines():1369 newdiff +="+"+ line1370 f.close()13711372# change description file: submitTemplate, separatorLine, diff, newdiff1373(handle, fileName) = tempfile.mkstemp()1374 tmpFile = os.fdopen(handle,"w+")1375if self.isWindows:1376 submitTemplate = submitTemplate.replace("\n","\r\n")1377 separatorLine = separatorLine.replace("\n","\r\n")1378 newdiff = newdiff.replace("\n","\r\n")1379 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1380 tmpFile.close()13811382if self.prepare_p4_only:1383#1384# Leave the p4 tree prepared, and the submit template around1385# and let the user decide what to do next1386#1387print1388print"P4 workspace prepared for submission."1389print"To submit or revert, go to client workspace"1390print" "+ self.clientPath1391print1392print"To submit, use\"p4 submit\"to write a new description,"1393print"or\"p4 submit -i%s\"to use the one prepared by" \1394"\"git p4\"."% fileName1395print"You can delete the file\"%s\"when finished."% fileName13961397if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1398print"To preserve change ownership by user%s, you must\n" \1399"do\"p4 change -f <change>\"after submitting and\n" \1400"edit the User field."1401if pureRenameCopy:1402print"After submitting, renamed files must be re-synced."1403print"Invoke\"p4 sync -f\"on each of these files:"1404for f in pureRenameCopy:1405print" "+ f14061407print1408print"To revert the changes, use\"p4 revert ...\", and delete"1409print"the submit template file\"%s\""% fileName1410if filesToAdd:1411print"Since the commit adds new files, they must be deleted:"1412for f in filesToAdd:1413print" "+ f1414print1415return True14161417#1418# Let the user edit the change description, then submit it.1419#1420if self.edit_template(fileName):1421# read the edited message and submit1422 ret =True1423 tmpFile =open(fileName,"rb")1424 message = tmpFile.read()1425 tmpFile.close()1426 submitTemplate = message[:message.index(separatorLine)]1427if self.isWindows:1428 submitTemplate = submitTemplate.replace("\r\n","\n")1429p4_write_pipe(['submit','-i'], submitTemplate)14301431if self.preserveUser:1432if p4User:1433# Get last changelist number. Cannot easily get it from1434# the submit command output as the output is1435# unmarshalled.1436 changelist = self.lastP4Changelist()1437 self.modifyChangelistUser(changelist, p4User)14381439# The rename/copy happened by applying a patch that created a1440# new file. This leaves it writable, which confuses p4.1441for f in pureRenameCopy:1442p4_sync(f,"-f")14431444else:1445# skip this patch1446 ret =False1447print"Submission cancelled, undoing p4 changes."1448for f in editedFiles:1449p4_revert(f)1450for f in filesToAdd:1451p4_revert(f)1452 os.remove(f)1453for f in filesToDelete:1454p4_revert(f)14551456 os.remove(fileName)1457return ret14581459# Export git tags as p4 labels. Create a p4 label and then tag1460# with that.1461defexportGitTags(self, gitTags):1462 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1463iflen(validLabelRegexp) ==0:1464 validLabelRegexp = defaultLabelRegexp1465 m = re.compile(validLabelRegexp)14661467for name in gitTags:14681469if not m.match(name):1470if verbose:1471print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1472continue14731474# Get the p4 commit this corresponds to1475 logMessage =extractLogMessageFromGitCommit(name)1476 values =extractSettingsGitLog(logMessage)14771478if not values.has_key('change'):1479# a tag pointing to something not sent to p4; ignore1480if verbose:1481print"git tag%sdoes not give a p4 commit"% name1482continue1483else:1484 changelist = values['change']14851486# Get the tag details.1487 inHeader =True1488 isAnnotated =False1489 body = []1490for l inread_pipe_lines(["git","cat-file","-p", name]):1491 l = l.strip()1492if inHeader:1493if re.match(r'tag\s+', l):1494 isAnnotated =True1495elif re.match(r'\s*$', l):1496 inHeader =False1497continue1498else:1499 body.append(l)15001501if not isAnnotated:1502 body = ["lightweight tag imported by git p4\n"]15031504# Create the label - use the same view as the client spec we are using1505 clientSpec =getClientSpec()15061507 labelTemplate ="Label:%s\n"% name1508 labelTemplate +="Description:\n"1509for b in body:1510 labelTemplate +="\t"+ b +"\n"1511 labelTemplate +="View:\n"1512for mapping in clientSpec.mappings:1513 labelTemplate +="\t%s\n"% mapping.depot_side.path15141515if self.dry_run:1516print"Would create p4 label%sfor tag"% name1517elif self.prepare_p4_only:1518print"Not creating p4 label%sfor tag due to option" \1519" --prepare-p4-only"% name1520else:1521p4_write_pipe(["label","-i"], labelTemplate)15221523# Use the label1524p4_system(["tag","-l", name] +1525["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])15261527if verbose:1528print"created p4 label for tag%s"% name15291530defrun(self, args):1531iflen(args) ==0:1532 self.master =currentGitBranch()1533iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1534die("Detecting current git branch failed!")1535eliflen(args) ==1:1536 self.master = args[0]1537if notbranchExists(self.master):1538die("Branch%sdoes not exist"% self.master)1539else:1540return False15411542 allowSubmit =gitConfig("git-p4.allowSubmit")1543iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1544die("%sis not in git-p4.allowSubmit"% self.master)15451546[upstream, settings] =findUpstreamBranchPoint()1547 self.depotPath = settings['depot-paths'][0]1548iflen(self.origin) ==0:1549 self.origin = upstream15501551if self.preserveUser:1552if not self.canChangeChangelists():1553die("Cannot preserve user names without p4 super-user or admin permissions")15541555# if not set from the command line, try the config file1556if self.conflict_behavior is None:1557 val =gitConfig("git-p4.conflict")1558if val:1559if val not in self.conflict_behavior_choices:1560die("Invalid value '%s' for config git-p4.conflict"% val)1561else:1562 val ="ask"1563 self.conflict_behavior = val15641565if self.verbose:1566print"Origin branch is "+ self.origin15671568iflen(self.depotPath) ==0:1569print"Internal error: cannot locate perforce depot path from existing branches"1570 sys.exit(128)15711572 self.useClientSpec =False1573ifgitConfig("git-p4.useclientspec","--bool") =="true":1574 self.useClientSpec =True1575if self.useClientSpec:1576 self.clientSpecDirs =getClientSpec()15771578if self.useClientSpec:1579# all files are relative to the client spec1580 self.clientPath =getClientRoot()1581else:1582 self.clientPath =p4Where(self.depotPath)15831584if self.clientPath =="":1585die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15861587print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1588 self.oldWorkingDirectory = os.getcwd()15891590# ensure the clientPath exists1591 new_client_dir =False1592if not os.path.exists(self.clientPath):1593 new_client_dir =True1594 os.makedirs(self.clientPath)15951596chdir(self.clientPath)1597if self.dry_run:1598print"Would synchronize p4 checkout in%s"% self.clientPath1599else:1600print"Synchronizing p4 checkout..."1601if new_client_dir:1602# old one was destroyed, and maybe nobody told p41603p4_sync("...","-f")1604else:1605p4_sync("...")1606 self.check()16071608 commits = []1609for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1610 commits.append(line.strip())1611 commits.reverse()16121613if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1614 self.checkAuthorship =False1615else:1616 self.checkAuthorship =True16171618if self.preserveUser:1619 self.checkValidP4Users(commits)16201621#1622# Build up a set of options to be passed to diff when1623# submitting each commit to p4.1624#1625if self.detectRenames:1626# command-line -M arg1627 self.diffOpts ="-M"1628else:1629# If not explicitly set check the config variable1630 detectRenames =gitConfig("git-p4.detectRenames")16311632if detectRenames.lower() =="false"or detectRenames =="":1633 self.diffOpts =""1634elif detectRenames.lower() =="true":1635 self.diffOpts ="-M"1636else:1637 self.diffOpts ="-M%s"% detectRenames16381639# no command-line arg for -C or --find-copies-harder, just1640# config variables1641 detectCopies =gitConfig("git-p4.detectCopies")1642if detectCopies.lower() =="false"or detectCopies =="":1643pass1644elif detectCopies.lower() =="true":1645 self.diffOpts +=" -C"1646else:1647 self.diffOpts +=" -C%s"% detectCopies16481649ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1650 self.diffOpts +=" --find-copies-harder"16511652#1653# Apply the commits, one at a time. On failure, ask if should1654# continue to try the rest of the patches, or quit.1655#1656if self.dry_run:1657print"Would apply"1658 applied = []1659 last =len(commits) -11660for i, commit inenumerate(commits):1661if self.dry_run:1662print" ",read_pipe(["git","show","-s",1663"--format=format:%h%s", commit])1664 ok =True1665else:1666 ok = self.applyCommit(commit)1667if ok:1668 applied.append(commit)1669else:1670if self.prepare_p4_only and i < last:1671print"Processing only the first commit due to option" \1672" --prepare-p4-only"1673break1674if i < last:1675 quit =False1676while True:1677# prompt for what to do, or use the option/variable1678if self.conflict_behavior =="ask":1679print"What do you want to do?"1680 response =raw_input("[s]kip this commit but apply"1681" the rest, or [q]uit? ")1682if not response:1683continue1684elif self.conflict_behavior =="skip":1685 response ="s"1686elif self.conflict_behavior =="quit":1687 response ="q"1688else:1689die("Unknown conflict_behavior '%s'"%1690 self.conflict_behavior)16911692if response[0] =="s":1693print"Skipping this commit, but applying the rest"1694break1695if response[0] =="q":1696print"Quitting"1697 quit =True1698break1699if quit:1700break17011702chdir(self.oldWorkingDirectory)17031704if self.dry_run:1705pass1706elif self.prepare_p4_only:1707pass1708eliflen(commits) ==len(applied):1709print"All commits applied!"17101711 sync =P4Sync()1712if self.branch:1713 sync.branch = self.branch1714 sync.run([])17151716 rebase =P4Rebase()1717 rebase.rebase()17181719else:1720iflen(applied) ==0:1721print"No commits applied."1722else:1723print"Applied only the commits marked with '*':"1724for c in commits:1725if c in applied:1726 star ="*"1727else:1728 star =" "1729print star,read_pipe(["git","show","-s",1730"--format=format:%h%s", c])1731print"You will have to do 'git p4 sync' and rebase."17321733ifgitConfig("git-p4.exportLabels","--bool") =="true":1734 self.exportLabels =True17351736if self.exportLabels:1737 p4Labels =getP4Labels(self.depotPath)1738 gitTags =getGitTags()17391740 missingGitTags = gitTags - p4Labels1741 self.exportGitTags(missingGitTags)17421743# exit with error unless everything applied perfecly1744iflen(commits) !=len(applied):1745 sys.exit(1)17461747return True17481749classView(object):1750"""Represent a p4 view ("p4 help views"), and map files in a1751 repo according to the view."""17521753classPath(object):1754"""A depot or client path, possibly containing wildcards.1755 The only one supported is ... at the end, currently.1756 Initialize with the full path, with //depot or //client."""17571758def__init__(self, path, is_depot):1759 self.path = path1760 self.is_depot = is_depot1761 self.find_wildcards()1762# remember the prefix bit, useful for relative mappings1763 m = re.match("(//[^/]+/)", self.path)1764if not m:1765die("Path%sdoes not start with //prefix/"% self.path)1766 prefix = m.group(1)1767if not self.is_depot:1768# strip //client/ on client paths1769 self.path = self.path[len(prefix):]17701771deffind_wildcards(self):1772"""Make sure wildcards are valid, and set up internal1773 variables."""17741775 self.ends_triple_dot =False1776# There are three wildcards allowed in p4 views1777# (see "p4 help views"). This code knows how to1778# handle "..." (only at the end), but cannot deal with1779# "%%n" or "*". Only check the depot_side, as p4 should1780# validate that the client_side matches too.1781if re.search(r'%%[1-9]', self.path):1782die("Can't handle%%n wildcards in view:%s"% self.path)1783if self.path.find("*") >=0:1784die("Can't handle * wildcards in view:%s"% self.path)1785 triple_dot_index = self.path.find("...")1786if triple_dot_index >=0:1787if triple_dot_index !=len(self.path) -3:1788die("Can handle only single ... wildcard, at end:%s"%1789 self.path)1790 self.ends_triple_dot =True17911792defensure_compatible(self, other_path):1793"""Make sure the wildcards agree."""1794if self.ends_triple_dot != other_path.ends_triple_dot:1795die("Both paths must end with ... if either does;\n"+1796"paths:%s %s"% (self.path, other_path.path))17971798defmatch_wildcards(self, test_path):1799"""See if this test_path matches us, and fill in the value1800 of the wildcards if so. Returns a tuple of1801 (True|False, wildcards[]). For now, only the ... at end1802 is supported, so at most one wildcard."""1803if self.ends_triple_dot:1804 dotless = self.path[:-3]1805if test_path.startswith(dotless):1806 wildcard = test_path[len(dotless):]1807return(True, [ wildcard ])1808else:1809if test_path == self.path:1810return(True, [])1811return(False, [])18121813defmatch(self, test_path):1814"""Just return if it matches; don't bother with the wildcards."""1815 b, _ = self.match_wildcards(test_path)1816return b18171818deffill_in_wildcards(self, wildcards):1819"""Return the relative path, with the wildcards filled in1820 if there are any."""1821if self.ends_triple_dot:1822return self.path[:-3] + wildcards[0]1823else:1824return self.path18251826classMapping(object):1827def__init__(self, depot_side, client_side, overlay, exclude):1828# depot_side is without the trailing /... if it had one1829 self.depot_side = View.Path(depot_side, is_depot=True)1830 self.client_side = View.Path(client_side, is_depot=False)1831 self.overlay = overlay # started with "+"1832 self.exclude = exclude # started with "-"1833assert not(self.overlay and self.exclude)1834 self.depot_side.ensure_compatible(self.client_side)18351836def__str__(self):1837 c =" "1838if self.overlay:1839 c ="+"1840if self.exclude:1841 c ="-"1842return"View.Mapping:%s%s->%s"% \1843(c, self.depot_side.path, self.client_side.path)18441845defmap_depot_to_client(self, depot_path):1846"""Calculate the client path if using this mapping on the1847 given depot path; does not consider the effect of other1848 mappings in a view. Even excluded mappings are returned."""1849 matches, wildcards = self.depot_side.match_wildcards(depot_path)1850if not matches:1851return""1852 client_path = self.client_side.fill_in_wildcards(wildcards)1853return client_path18541855#1856# View methods1857#1858def__init__(self):1859 self.mappings = []18601861defappend(self, view_line):1862"""Parse a view line, splitting it into depot and client1863 sides. Append to self.mappings, preserving order."""18641865# Split the view line into exactly two words. P4 enforces1866# structure on these lines that simplifies this quite a bit.1867#1868# Either or both words may be double-quoted.1869# Single quotes do not matter.1870# Double-quote marks cannot occur inside the words.1871# A + or - prefix is also inside the quotes.1872# There are no quotes unless they contain a space.1873# The line is already white-space stripped.1874# The two words are separated by a single space.1875#1876if view_line[0] =='"':1877# First word is double quoted. Find its end.1878 close_quote_index = view_line.find('"',1)1879if close_quote_index <=0:1880die("No first-word closing quote found:%s"% view_line)1881 depot_side = view_line[1:close_quote_index]1882# skip closing quote and space1883 rhs_index = close_quote_index +1+11884else:1885 space_index = view_line.find(" ")1886if space_index <=0:1887die("No word-splitting space found:%s"% view_line)1888 depot_side = view_line[0:space_index]1889 rhs_index = space_index +118901891if view_line[rhs_index] =='"':1892# Second word is double quoted. Make sure there is a1893# double quote at the end too.1894if not view_line.endswith('"'):1895die("View line with rhs quote should end with one:%s"%1896 view_line)1897# skip the quotes1898 client_side = view_line[rhs_index+1:-1]1899else:1900 client_side = view_line[rhs_index:]19011902# prefix + means overlay on previous mapping1903 overlay =False1904if depot_side.startswith("+"):1905 overlay =True1906 depot_side = depot_side[1:]19071908# prefix - means exclude this path1909 exclude =False1910if depot_side.startswith("-"):1911 exclude =True1912 depot_side = depot_side[1:]19131914 m = View.Mapping(depot_side, client_side, overlay, exclude)1915 self.mappings.append(m)19161917defmap_in_client(self, depot_path):1918"""Return the relative location in the client where this1919 depot file should live. Returns "" if the file should1920 not be mapped in the client."""19211922 paths_filled = []1923 client_path =""19241925# look at later entries first1926for m in self.mappings[::-1]:19271928# see where will this path end up in the client1929 p = m.map_depot_to_client(depot_path)19301931if p =="":1932# Depot path does not belong in client. Must remember1933# this, as previous items should not cause files to1934# exist in this path either. Remember that the list is1935# being walked from the end, which has higher precedence.1936# Overlap mappings do not exclude previous mappings.1937if not m.overlay:1938 paths_filled.append(m.client_side)19391940else:1941# This mapping matched; no need to search any further.1942# But, the mapping could be rejected if the client path1943# has already been claimed by an earlier mapping (i.e.1944# one later in the list, which we are walking backwards).1945 already_mapped_in_client =False1946for f in paths_filled:1947# this is View.Path.match1948if f.match(p):1949 already_mapped_in_client =True1950break1951if not already_mapped_in_client:1952# Include this file, unless it is from a line that1953# explicitly said to exclude it.1954if not m.exclude:1955 client_path = p19561957# a match, even if rejected, always stops the search1958break19591960return client_path19611962classP4Sync(Command, P4UserMap):1963 delete_actions = ("delete","move/delete","purge")19641965def__init__(self):1966 Command.__init__(self)1967 P4UserMap.__init__(self)1968 self.options = [1969 optparse.make_option("--branch", dest="branch"),1970 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1971 optparse.make_option("--changesfile", dest="changesFile"),1972 optparse.make_option("--silent", dest="silent", action="store_true"),1973 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1974 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1975 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1976help="Import into refs/heads/ , not refs/remotes"),1977 optparse.make_option("--max-changes", dest="maxChanges"),1978 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1979help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1980 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1981help="Only sync files that are included in the Perforce Client Spec")1982]1983 self.description ="""Imports from Perforce into a git repository.\n1984 example:1985 //depot/my/project/ -- to import the current head1986 //depot/my/project/@all -- to import everything1987 //depot/my/project/@1,6 -- to import only from revision 1 to 619881989 (a ... is not needed in the path p4 specification, it's added implicitly)"""19901991 self.usage +=" //depot/path[@revRange]"1992 self.silent =False1993 self.createdBranches =set()1994 self.committedChanges =set()1995 self.branch =""1996 self.detectBranches =False1997 self.detectLabels =False1998 self.importLabels =False1999 self.changesFile =""2000 self.syncWithOrigin =True2001 self.importIntoRemotes =True2002 self.maxChanges =""2003 self.keepRepoPath =False2004 self.depotPaths =None2005 self.p4BranchesInGit = []2006 self.cloneExclude = []2007 self.useClientSpec =False2008 self.useClientSpec_from_options =False2009 self.clientSpecDirs =None2010 self.tempBranches = []2011 self.tempBranchLocation ="git-p4-tmp"20122013ifgitConfig("git-p4.syncFromOrigin") =="false":2014 self.syncWithOrigin =False20152016# Force a checkpoint in fast-import and wait for it to finish2017defcheckpoint(self):2018 self.gitStream.write("checkpoint\n\n")2019 self.gitStream.write("progress checkpoint\n\n")2020 out = self.gitOutput.readline()2021if self.verbose:2022print"checkpoint finished: "+ out20232024defextractFilesFromCommit(self, commit):2025 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2026for path in self.cloneExclude]2027 files = []2028 fnum =02029while commit.has_key("depotFile%s"% fnum):2030 path = commit["depotFile%s"% fnum]20312032if[p for p in self.cloneExclude2033ifp4PathStartsWith(path, p)]:2034 found =False2035else:2036 found = [p for p in self.depotPaths2037ifp4PathStartsWith(path, p)]2038if not found:2039 fnum = fnum +12040continue20412042file= {}2043file["path"] = path2044file["rev"] = commit["rev%s"% fnum]2045file["action"] = commit["action%s"% fnum]2046file["type"] = commit["type%s"% fnum]2047 files.append(file)2048 fnum = fnum +12049return files20502051defstripRepoPath(self, path, prefixes):2052"""When streaming files, this is called to map a p4 depot path2053 to where it should go in git. The prefixes are either2054 self.depotPaths, or self.branchPrefixes in the case of2055 branch detection."""20562057if self.useClientSpec:2058# branch detection moves files up a level (the branch name)2059# from what client spec interpretation gives2060 path = self.clientSpecDirs.map_in_client(path)2061if self.detectBranches:2062for b in self.knownBranches:2063if path.startswith(b +"/"):2064 path = path[len(b)+1:]20652066elif self.keepRepoPath:2067# Preserve everything in relative path name except leading2068# //depot/; just look at first prefix as they all should2069# be in the same depot.2070 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2071ifp4PathStartsWith(path, depot):2072 path = path[len(depot):]20732074else:2075for p in prefixes:2076ifp4PathStartsWith(path, p):2077 path = path[len(p):]2078break20792080 path =wildcard_decode(path)2081return path20822083defsplitFilesIntoBranches(self, commit):2084"""Look at each depotFile in the commit to figure out to what2085 branch it belongs."""20862087 branches = {}2088 fnum =02089while commit.has_key("depotFile%s"% fnum):2090 path = commit["depotFile%s"% fnum]2091 found = [p for p in self.depotPaths2092ifp4PathStartsWith(path, p)]2093if not found:2094 fnum = fnum +12095continue20962097file= {}2098file["path"] = path2099file["rev"] = commit["rev%s"% fnum]2100file["action"] = commit["action%s"% fnum]2101file["type"] = commit["type%s"% fnum]2102 fnum = fnum +121032104# start with the full relative path where this file would2105# go in a p4 client2106if self.useClientSpec:2107 relPath = self.clientSpecDirs.map_in_client(path)2108else:2109 relPath = self.stripRepoPath(path, self.depotPaths)21102111for branch in self.knownBranches.keys():2112# add a trailing slash so that a commit into qt/4.2foo2113# doesn't end up in qt/4.2, e.g.2114if relPath.startswith(branch +"/"):2115if branch not in branches:2116 branches[branch] = []2117 branches[branch].append(file)2118break21192120return branches21212122# output one file from the P4 stream2123# - helper for streamP4Files21242125defstreamOneP4File(self,file, contents):2126 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2127if verbose:2128 sys.stderr.write("%s\n"% relPath)21292130(type_base, type_mods) =split_p4_type(file["type"])21312132 git_mode ="100644"2133if"x"in type_mods:2134 git_mode ="100755"2135if type_base =="symlink":2136 git_mode ="120000"2137# p4 print on a symlink contains "target\n"; remove the newline2138 data =''.join(contents)2139 contents = [data[:-1]]21402141if type_base =="utf16":2142# p4 delivers different text in the python output to -G2143# than it does when using "print -o", or normal p4 client2144# operations. utf16 is converted to ascii or utf8, perhaps.2145# But ascii text saved as -t utf16 is completely mangled.2146# Invoke print -o to get the real contents.2147#2148# On windows, the newlines will always be mangled by print, so put2149# them back too. This is not needed to the cygwin windows version,2150# just the native "NT" type.2151#2152 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2153ifp4_version_string().find("/NT") >=0:2154 text = text.replace("\r\n","\n")2155 contents = [ text ]21562157if type_base =="apple":2158# Apple filetype files will be streamed as a concatenation of2159# its appledouble header and the contents. This is useless2160# on both macs and non-macs. If using "print -q -o xx", it2161# will create "xx" with the data, and "%xx" with the header.2162# This is also not very useful.2163#2164# Ideally, someday, this script can learn how to generate2165# appledouble files directly and import those to git, but2166# non-mac machines can never find a use for apple filetype.2167print"\nIgnoring apple filetype file%s"%file['depotFile']2168return21692170# Note that we do not try to de-mangle keywords on utf16 files,2171# even though in theory somebody may want that.2172 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2173if pattern:2174 regexp = re.compile(pattern, re.VERBOSE)2175 text =''.join(contents)2176 text = regexp.sub(r'$\1$', text)2177 contents = [ text ]21782179 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21802181# total length...2182 length =02183for d in contents:2184 length = length +len(d)21852186 self.gitStream.write("data%d\n"% length)2187for d in contents:2188 self.gitStream.write(d)2189 self.gitStream.write("\n")21902191defstreamOneP4Deletion(self,file):2192 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2193if verbose:2194 sys.stderr.write("delete%s\n"% relPath)2195 self.gitStream.write("D%s\n"% relPath)21962197# handle another chunk of streaming data2198defstreamP4FilesCb(self, marshalled):21992200# catch p4 errors and complain2201 err =None2202if"code"in marshalled:2203if marshalled["code"] =="error":2204if"data"in marshalled:2205 err = marshalled["data"].rstrip()2206if err:2207 f =None2208if self.stream_have_file_info:2209if"depotFile"in self.stream_file:2210 f = self.stream_file["depotFile"]2211# force a failure in fast-import, else an empty2212# commit will be made2213 self.gitStream.write("\n")2214 self.gitStream.write("die-now\n")2215 self.gitStream.close()2216# ignore errors, but make sure it exits first2217 self.importProcess.wait()2218if f:2219die("Error from p4 print for%s:%s"% (f, err))2220else:2221die("Error from p4 print:%s"% err)22222223if marshalled.has_key('depotFile')and self.stream_have_file_info:2224# start of a new file - output the old one first2225 self.streamOneP4File(self.stream_file, self.stream_contents)2226 self.stream_file = {}2227 self.stream_contents = []2228 self.stream_have_file_info =False22292230# pick up the new file information... for the2231# 'data' field we need to append to our array2232for k in marshalled.keys():2233if k =='data':2234 self.stream_contents.append(marshalled['data'])2235else:2236 self.stream_file[k] = marshalled[k]22372238 self.stream_have_file_info =True22392240# Stream directly from "p4 files" into "git fast-import"2241defstreamP4Files(self, files):2242 filesForCommit = []2243 filesToRead = []2244 filesToDelete = []22452246for f in files:2247# if using a client spec, only add the files that have2248# a path in the client2249if self.clientSpecDirs:2250if self.clientSpecDirs.map_in_client(f['path']) =="":2251continue22522253 filesForCommit.append(f)2254if f['action']in self.delete_actions:2255 filesToDelete.append(f)2256else:2257 filesToRead.append(f)22582259# deleted files...2260for f in filesToDelete:2261 self.streamOneP4Deletion(f)22622263iflen(filesToRead) >0:2264 self.stream_file = {}2265 self.stream_contents = []2266 self.stream_have_file_info =False22672268# curry self argument2269defstreamP4FilesCbSelf(entry):2270 self.streamP4FilesCb(entry)22712272 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22732274p4CmdList(["-x","-","print"],2275 stdin=fileArgs,2276 cb=streamP4FilesCbSelf)22772278# do the last chunk2279if self.stream_file.has_key('depotFile'):2280 self.streamOneP4File(self.stream_file, self.stream_contents)22812282defmake_email(self, userid):2283if userid in self.users:2284return self.users[userid]2285else:2286return"%s<a@b>"% userid22872288# Stream a p4 tag2289defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2290if verbose:2291print"writing tag%sfor commit%s"% (labelName, commit)2292 gitStream.write("tag%s\n"% labelName)2293 gitStream.write("from%s\n"% commit)22942295if labelDetails.has_key('Owner'):2296 owner = labelDetails["Owner"]2297else:2298 owner =None22992300# Try to use the owner of the p4 label, or failing that,2301# the current p4 user id.2302if owner:2303 email = self.make_email(owner)2304else:2305 email = self.make_email(self.p4UserId())2306 tagger ="%s %s %s"% (email, epoch, self.tz)23072308 gitStream.write("tagger%s\n"% tagger)23092310print"labelDetails=",labelDetails2311if labelDetails.has_key('Description'):2312 description = labelDetails['Description']2313else:2314 description ='Label from git p4'23152316 gitStream.write("data%d\n"%len(description))2317 gitStream.write(description)2318 gitStream.write("\n")23192320defcommit(self, details, files, branch, parent =""):2321 epoch = details["time"]2322 author = details["user"]23232324if self.verbose:2325print"commit into%s"% branch23262327# start with reading files; if that fails, we should not2328# create a commit.2329 new_files = []2330for f in files:2331if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2332 new_files.append(f)2333else:2334 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23352336 self.gitStream.write("commit%s\n"% branch)2337# gitStream.write("mark :%s\n" % details["change"])2338 self.committedChanges.add(int(details["change"]))2339 committer =""2340if author not in self.users:2341 self.getUserMapFromPerforceServer()2342 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23432344 self.gitStream.write("committer%s\n"% committer)23452346 self.gitStream.write("data <<EOT\n")2347 self.gitStream.write(details["desc"])2348 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2349(','.join(self.branchPrefixes), details["change"]))2350iflen(details['options']) >0:2351 self.gitStream.write(": options =%s"% details['options'])2352 self.gitStream.write("]\nEOT\n\n")23532354iflen(parent) >0:2355if self.verbose:2356print"parent%s"% parent2357 self.gitStream.write("from%s\n"% parent)23582359 self.streamP4Files(new_files)2360 self.gitStream.write("\n")23612362 change =int(details["change"])23632364if self.labels.has_key(change):2365 label = self.labels[change]2366 labelDetails = label[0]2367 labelRevisions = label[1]2368if self.verbose:2369print"Change%sis labelled%s"% (change, labelDetails)23702371 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2372for p in self.branchPrefixes])23732374iflen(files) ==len(labelRevisions):23752376 cleanedFiles = {}2377for info in files:2378if info["action"]in self.delete_actions:2379continue2380 cleanedFiles[info["depotFile"]] = info["rev"]23812382if cleanedFiles == labelRevisions:2383 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23842385else:2386if not self.silent:2387print("Tag%sdoes not match with change%s: files do not match."2388% (labelDetails["label"], change))23892390else:2391if not self.silent:2392print("Tag%sdoes not match with change%s: file count is different."2393% (labelDetails["label"], change))23942395# Build a dictionary of changelists and labels, for "detect-labels" option.2396defgetLabels(self):2397 self.labels = {}23982399 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2400iflen(l) >0and not self.silent:2401print"Finding files belonging to labels in%s"% `self.depotPaths`24022403for output in l:2404 label = output["label"]2405 revisions = {}2406 newestChange =02407if self.verbose:2408print"Querying files for label%s"% label2409forfileinp4CmdList(["files"] +2410["%s...@%s"% (p, label)2411for p in self.depotPaths]):2412 revisions[file["depotFile"]] =file["rev"]2413 change =int(file["change"])2414if change > newestChange:2415 newestChange = change24162417 self.labels[newestChange] = [output, revisions]24182419if self.verbose:2420print"Label changes:%s"% self.labels.keys()24212422# Import p4 labels as git tags. A direct mapping does not2423# exist, so assume that if all the files are at the same revision2424# then we can use that, or it's something more complicated we should2425# just ignore.2426defimportP4Labels(self, stream, p4Labels):2427if verbose:2428print"import p4 labels: "+' '.join(p4Labels)24292430 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2431 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2432iflen(validLabelRegexp) ==0:2433 validLabelRegexp = defaultLabelRegexp2434 m = re.compile(validLabelRegexp)24352436for name in p4Labels:2437 commitFound =False24382439if not m.match(name):2440if verbose:2441print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2442continue24432444if name in ignoredP4Labels:2445continue24462447 labelDetails =p4CmdList(['label',"-o", name])[0]24482449# get the most recent changelist for each file in this label2450 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2451for p in self.depotPaths])24522453if change.has_key('change'):2454# find the corresponding git commit; take the oldest commit2455 changelist =int(change['change'])2456 gitCommit =read_pipe(["git","rev-list","--max-count=1",2457"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2458iflen(gitCommit) ==0:2459print"could not find git commit for changelist%d"% changelist2460else:2461 gitCommit = gitCommit.strip()2462 commitFound =True2463# Convert from p4 time format2464try:2465 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2466exceptValueError:2467print"Could not convert label time%s"% labelDetails['Update']2468 tmwhen =124692470 when =int(time.mktime(tmwhen))2471 self.streamTag(stream, name, labelDetails, gitCommit, when)2472if verbose:2473print"p4 label%smapped to git commit%s"% (name, gitCommit)2474else:2475if verbose:2476print"Label%shas no changelists - possibly deleted?"% name24772478if not commitFound:2479# We can't import this label; don't try again as it will get very2480# expensive repeatedly fetching all the files for labels that will2481# never be imported. If the label is moved in the future, the2482# ignore will need to be removed manually.2483system(["git","config","--add","git-p4.ignoredP4Labels", name])24842485defguessProjectName(self):2486for p in self.depotPaths:2487if p.endswith("/"):2488 p = p[:-1]2489 p = p[p.strip().rfind("/") +1:]2490if not p.endswith("/"):2491 p +="/"2492return p24932494defgetBranchMapping(self):2495 lostAndFoundBranches =set()24962497 user =gitConfig("git-p4.branchUser")2498iflen(user) >0:2499 command ="branches -u%s"% user2500else:2501 command ="branches"25022503for info inp4CmdList(command):2504 details =p4Cmd(["branch","-o", info["branch"]])2505 viewIdx =02506while details.has_key("View%s"% viewIdx):2507 paths = details["View%s"% viewIdx].split(" ")2508 viewIdx = viewIdx +12509# require standard //depot/foo/... //depot/bar/... mapping2510iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2511continue2512 source = paths[0]2513 destination = paths[1]2514## HACK2515ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2516 source = source[len(self.depotPaths[0]):-4]2517 destination = destination[len(self.depotPaths[0]):-4]25182519if destination in self.knownBranches:2520if not self.silent:2521print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2522print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2523continue25242525 self.knownBranches[destination] = source25262527 lostAndFoundBranches.discard(destination)25282529if source not in self.knownBranches:2530 lostAndFoundBranches.add(source)25312532# Perforce does not strictly require branches to be defined, so we also2533# check git config for a branch list.2534#2535# Example of branch definition in git config file:2536# [git-p4]2537# branchList=main:branchA2538# branchList=main:branchB2539# branchList=branchA:branchC2540 configBranches =gitConfigList("git-p4.branchList")2541for branch in configBranches:2542if branch:2543(source, destination) = branch.split(":")2544 self.knownBranches[destination] = source25452546 lostAndFoundBranches.discard(destination)25472548if source not in self.knownBranches:2549 lostAndFoundBranches.add(source)255025512552for branch in lostAndFoundBranches:2553 self.knownBranches[branch] = branch25542555defgetBranchMappingFromGitBranches(self):2556 branches =p4BranchesInGit(self.importIntoRemotes)2557for branch in branches.keys():2558if branch =="master":2559 branch ="main"2560else:2561 branch = branch[len(self.projectName):]2562 self.knownBranches[branch] = branch25632564defupdateOptionDict(self, d):2565 option_keys = {}2566if self.keepRepoPath:2567 option_keys['keepRepoPath'] =125682569 d["options"] =' '.join(sorted(option_keys.keys()))25702571defreadOptions(self, d):2572 self.keepRepoPath = (d.has_key('options')2573and('keepRepoPath'in d['options']))25742575defgitRefForBranch(self, branch):2576if branch =="main":2577return self.refPrefix +"master"25782579iflen(branch) <=0:2580return branch25812582return self.refPrefix + self.projectName + branch25832584defgitCommitByP4Change(self, ref, change):2585if self.verbose:2586print"looking in ref "+ ref +" for change%susing bisect..."% change25872588 earliestCommit =""2589 latestCommit =parseRevision(ref)25902591while True:2592if self.verbose:2593print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2594 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2595iflen(next) ==0:2596if self.verbose:2597print"argh"2598return""2599 log =extractLogMessageFromGitCommit(next)2600 settings =extractSettingsGitLog(log)2601 currentChange =int(settings['change'])2602if self.verbose:2603print"current change%s"% currentChange26042605if currentChange == change:2606if self.verbose:2607print"found%s"% next2608return next26092610if currentChange < change:2611 earliestCommit ="^%s"% next2612else:2613 latestCommit ="%s"% next26142615return""26162617defimportNewBranch(self, branch, maxChange):2618# make fast-import flush all changes to disk and update the refs using the checkpoint2619# command so that we can try to find the branch parent in the git history2620 self.gitStream.write("checkpoint\n\n");2621 self.gitStream.flush();2622 branchPrefix = self.depotPaths[0] + branch +"/"2623range="@1,%s"% maxChange2624#print "prefix" + branchPrefix2625 changes =p4ChangesForPaths([branchPrefix],range)2626iflen(changes) <=0:2627return False2628 firstChange = changes[0]2629#print "first change in branch: %s" % firstChange2630 sourceBranch = self.knownBranches[branch]2631 sourceDepotPath = self.depotPaths[0] + sourceBranch2632 sourceRef = self.gitRefForBranch(sourceBranch)2633#print "source " + sourceBranch26342635 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2636#print "branch parent: %s" % branchParentChange2637 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2638iflen(gitParent) >0:2639 self.initialParents[self.gitRefForBranch(branch)] = gitParent2640#print "parent git commit: %s" % gitParent26412642 self.importChanges(changes)2643return True26442645defsearchParent(self, parent, branch, target):2646 parentFound =False2647for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2648 blob = blob.strip()2649iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2650 parentFound =True2651if self.verbose:2652print"Found parent of%sin commit%s"% (branch, blob)2653break2654if parentFound:2655return blob2656else:2657return None26582659defimportChanges(self, changes):2660 cnt =12661for change in changes:2662 description =p4_describe(change)2663 self.updateOptionDict(description)26642665if not self.silent:2666 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2667 sys.stdout.flush()2668 cnt = cnt +126692670try:2671if self.detectBranches:2672 branches = self.splitFilesIntoBranches(description)2673for branch in branches.keys():2674## HACK --hwn2675 branchPrefix = self.depotPaths[0] + branch +"/"2676 self.branchPrefixes = [ branchPrefix ]26772678 parent =""26792680 filesForCommit = branches[branch]26812682if self.verbose:2683print"branch is%s"% branch26842685 self.updatedBranches.add(branch)26862687if branch not in self.createdBranches:2688 self.createdBranches.add(branch)2689 parent = self.knownBranches[branch]2690if parent == branch:2691 parent =""2692else:2693 fullBranch = self.projectName + branch2694if fullBranch not in self.p4BranchesInGit:2695if not self.silent:2696print("\nImporting new branch%s"% fullBranch);2697if self.importNewBranch(branch, change -1):2698 parent =""2699 self.p4BranchesInGit.append(fullBranch)2700if not self.silent:2701print("\nResuming with change%s"% change);27022703if self.verbose:2704print"parent determined through known branches:%s"% parent27052706 branch = self.gitRefForBranch(branch)2707 parent = self.gitRefForBranch(parent)27082709if self.verbose:2710print"looking for initial parent for%s; current parent is%s"% (branch, parent)27112712iflen(parent) ==0and branch in self.initialParents:2713 parent = self.initialParents[branch]2714del self.initialParents[branch]27152716 blob =None2717iflen(parent) >0:2718 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2719if self.verbose:2720print"Creating temporary branch: "+ tempBranch2721 self.commit(description, filesForCommit, tempBranch)2722 self.tempBranches.append(tempBranch)2723 self.checkpoint()2724 blob = self.searchParent(parent, branch, tempBranch)2725if blob:2726 self.commit(description, filesForCommit, branch, blob)2727else:2728if self.verbose:2729print"Parent of%snot found. Committing into head of%s"% (branch, parent)2730 self.commit(description, filesForCommit, branch, parent)2731else:2732 files = self.extractFilesFromCommit(description)2733 self.commit(description, files, self.branch,2734 self.initialParent)2735# only needed once, to connect to the previous commit2736 self.initialParent =""2737exceptIOError:2738print self.gitError.read()2739 sys.exit(1)27402741defimportHeadRevision(self, revision):2742print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27432744 details = {}2745 details["user"] ="git perforce import user"2746 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2747% (' '.join(self.depotPaths), revision))2748 details["change"] = revision2749 newestRevision =027502751 fileCnt =02752 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27532754for info inp4CmdList(["files"] + fileArgs):27552756if'code'in info and info['code'] =='error':2757 sys.stderr.write("p4 returned an error:%s\n"2758% info['data'])2759if info['data'].find("must refer to client") >=0:2760 sys.stderr.write("This particular p4 error is misleading.\n")2761 sys.stderr.write("Perhaps the depot path was misspelled.\n");2762 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2763 sys.exit(1)2764if'p4ExitCode'in info:2765 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2766 sys.exit(1)276727682769 change =int(info["change"])2770if change > newestRevision:2771 newestRevision = change27722773if info["action"]in self.delete_actions:2774# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2775#fileCnt = fileCnt + 12776continue27772778for prop in["depotFile","rev","action","type"]:2779 details["%s%s"% (prop, fileCnt)] = info[prop]27802781 fileCnt = fileCnt +127822783 details["change"] = newestRevision27842785# Use time from top-most change so that all git p4 clones of2786# the same p4 repo have the same commit SHA1s.2787 res =p4_describe(newestRevision)2788 details["time"] = res["time"]27892790 self.updateOptionDict(details)2791try:2792 self.commit(details, self.extractFilesFromCommit(details), self.branch)2793exceptIOError:2794print"IO error with git fast-import. Is your git version recent enough?"2795print self.gitError.read()279627972798defrun(self, args):2799 self.depotPaths = []2800 self.changeRange =""2801 self.previousDepotPaths = []2802 self.hasOrigin =False28032804# map from branch depot path to parent branch2805 self.knownBranches = {}2806 self.initialParents = {}28072808if self.importIntoRemotes:2809 self.refPrefix ="refs/remotes/p4/"2810else:2811 self.refPrefix ="refs/heads/p4/"28122813if self.syncWithOrigin:2814 self.hasOrigin =originP4BranchesExist()2815if self.hasOrigin:2816if not self.silent:2817print'Syncing with origin first, using "git fetch origin"'2818system("git fetch origin")28192820 branch_arg_given =bool(self.branch)2821iflen(self.branch) ==0:2822 self.branch = self.refPrefix +"master"2823ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2824system("git update-ref%srefs/heads/p4"% self.branch)2825system("git branch -D p4")28262827# accept either the command-line option, or the configuration variable2828if self.useClientSpec:2829# will use this after clone to set the variable2830 self.useClientSpec_from_options =True2831else:2832ifgitConfig("git-p4.useclientspec","--bool") =="true":2833 self.useClientSpec =True2834if self.useClientSpec:2835 self.clientSpecDirs =getClientSpec()28362837# TODO: should always look at previous commits,2838# merge with previous imports, if possible.2839if args == []:2840if self.hasOrigin:2841createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28422843# branches holds mapping from branch name to sha12844 branches =p4BranchesInGit(self.importIntoRemotes)28452846# restrict to just this one, disabling detect-branches2847if branch_arg_given:2848 short = self.branch.split("/")[-1]2849if short in branches:2850 self.p4BranchesInGit = [ short ]2851else:2852 self.p4BranchesInGit = branches.keys()28532854iflen(self.p4BranchesInGit) >1:2855if not self.silent:2856print"Importing from/into multiple branches"2857 self.detectBranches =True2858for branch in branches.keys():2859 self.initialParents[self.refPrefix + branch] = \2860 branches[branch]28612862if self.verbose:2863print"branches:%s"% self.p4BranchesInGit28642865 p4Change =02866for branch in self.p4BranchesInGit:2867 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28682869 settings =extractSettingsGitLog(logMsg)28702871 self.readOptions(settings)2872if(settings.has_key('depot-paths')2873and settings.has_key('change')):2874 change =int(settings['change']) +12875 p4Change =max(p4Change, change)28762877 depotPaths =sorted(settings['depot-paths'])2878if self.previousDepotPaths == []:2879 self.previousDepotPaths = depotPaths2880else:2881 paths = []2882for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2883 prev_list = prev.split("/")2884 cur_list = cur.split("/")2885for i inrange(0,min(len(cur_list),len(prev_list))):2886if cur_list[i] <> prev_list[i]:2887 i = i -12888break28892890 paths.append("/".join(cur_list[:i +1]))28912892 self.previousDepotPaths = paths28932894if p4Change >0:2895 self.depotPaths =sorted(self.previousDepotPaths)2896 self.changeRange ="@%s,#head"% p4Change2897if not self.silent and not self.detectBranches:2898print"Performing incremental import into%sgit branch"% self.branch28992900# accept multiple ref name abbreviations:2901# refs/foo/bar/branch -> use it exactly2902# p4/branch -> prepend refs/remotes/ or refs/heads/2903# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2904if not self.branch.startswith("refs/"):2905if self.importIntoRemotes:2906 prepend ="refs/remotes/"2907else:2908 prepend ="refs/heads/"2909if not self.branch.startswith("p4/"):2910 prepend +="p4/"2911 self.branch = prepend + self.branch29122913iflen(args) ==0and self.depotPaths:2914if not self.silent:2915print"Depot paths:%s"%' '.join(self.depotPaths)2916else:2917if self.depotPaths and self.depotPaths != args:2918print("previous import used depot path%sand now%swas specified. "2919"This doesn't work!"% (' '.join(self.depotPaths),2920' '.join(args)))2921 sys.exit(1)29222923 self.depotPaths =sorted(args)29242925 revision =""2926 self.users = {}29272928# Make sure no revision specifiers are used when --changesfile2929# is specified.2930 bad_changesfile =False2931iflen(self.changesFile) >0:2932for p in self.depotPaths:2933if p.find("@") >=0or p.find("#") >=0:2934 bad_changesfile =True2935break2936if bad_changesfile:2937die("Option --changesfile is incompatible with revision specifiers")29382939 newPaths = []2940for p in self.depotPaths:2941if p.find("@") != -1:2942 atIdx = p.index("@")2943 self.changeRange = p[atIdx:]2944if self.changeRange =="@all":2945 self.changeRange =""2946elif','not in self.changeRange:2947 revision = self.changeRange2948 self.changeRange =""2949 p = p[:atIdx]2950elif p.find("#") != -1:2951 hashIdx = p.index("#")2952 revision = p[hashIdx:]2953 p = p[:hashIdx]2954elif self.previousDepotPaths == []:2955# pay attention to changesfile, if given, else import2956# the entire p4 tree at the head revision2957iflen(self.changesFile) ==0:2958 revision ="#head"29592960 p = re.sub("\.\.\.$","", p)2961if not p.endswith("/"):2962 p +="/"29632964 newPaths.append(p)29652966 self.depotPaths = newPaths29672968# --detect-branches may change this for each branch2969 self.branchPrefixes = self.depotPaths29702971 self.loadUserMapFromCache()2972 self.labels = {}2973if self.detectLabels:2974 self.getLabels();29752976if self.detectBranches:2977## FIXME - what's a P4 projectName ?2978 self.projectName = self.guessProjectName()29792980if self.hasOrigin:2981 self.getBranchMappingFromGitBranches()2982else:2983 self.getBranchMapping()2984if self.verbose:2985print"p4-git branches:%s"% self.p4BranchesInGit2986print"initial parents:%s"% self.initialParents2987for b in self.p4BranchesInGit:2988if b !="master":29892990## FIXME2991 b = b[len(self.projectName):]2992 self.createdBranches.add(b)29932994 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29952996 self.importProcess = subprocess.Popen(["git","fast-import"],2997 stdin=subprocess.PIPE,2998 stdout=subprocess.PIPE,2999 stderr=subprocess.PIPE);3000 self.gitOutput = self.importProcess.stdout3001 self.gitStream = self.importProcess.stdin3002 self.gitError = self.importProcess.stderr30033004if revision:3005 self.importHeadRevision(revision)3006else:3007 changes = []30083009iflen(self.changesFile) >0:3010 output =open(self.changesFile).readlines()3011 changeSet =set()3012for line in output:3013 changeSet.add(int(line))30143015for change in changeSet:3016 changes.append(change)30173018 changes.sort()3019else:3020# catch "git p4 sync" with no new branches, in a repo that3021# does not have any existing p4 branches3022iflen(args) ==0:3023if not self.p4BranchesInGit:3024die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30253026# The default branch is master, unless --branch is used to3027# specify something else. Make sure it exists, or complain3028# nicely about how to use --branch.3029if not self.detectBranches:3030if notbranch_exists(self.branch):3031if branch_arg_given:3032die("Error: branch%sdoes not exist."% self.branch)3033else:3034die("Error: no branch%s; perhaps specify one with --branch."%3035 self.branch)30363037if self.verbose:3038print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3039 self.changeRange)3040 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30413042iflen(self.maxChanges) >0:3043 changes = changes[:min(int(self.maxChanges),len(changes))]30443045iflen(changes) ==0:3046if not self.silent:3047print"No changes to import!"3048else:3049if not self.silent and not self.detectBranches:3050print"Import destination:%s"% self.branch30513052 self.updatedBranches =set()30533054if not self.detectBranches:3055if args:3056# start a new branch3057 self.initialParent =""3058else:3059# build on a previous revision3060 self.initialParent =parseRevision(self.branch)30613062 self.importChanges(changes)30633064if not self.silent:3065print""3066iflen(self.updatedBranches) >0:3067 sys.stdout.write("Updated branches: ")3068for b in self.updatedBranches:3069 sys.stdout.write("%s"% b)3070 sys.stdout.write("\n")30713072ifgitConfig("git-p4.importLabels","--bool") =="true":3073 self.importLabels =True30743075if self.importLabels:3076 p4Labels =getP4Labels(self.depotPaths)3077 gitTags =getGitTags()30783079 missingP4Labels = p4Labels - gitTags3080 self.importP4Labels(self.gitStream, missingP4Labels)30813082 self.gitStream.close()3083if self.importProcess.wait() !=0:3084die("fast-import failed:%s"% self.gitError.read())3085 self.gitOutput.close()3086 self.gitError.close()30873088# Cleanup temporary branches created during import3089if self.tempBranches != []:3090for branch in self.tempBranches:3091read_pipe("git update-ref -d%s"% branch)3092 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30933094# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3095# a convenient shortcut refname "p4".3096if self.importIntoRemotes:3097 head_ref = self.refPrefix +"HEAD"3098if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3099system(["git","symbolic-ref", head_ref, self.branch])31003101return True31023103classP4Rebase(Command):3104def__init__(self):3105 Command.__init__(self)3106 self.options = [3107 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3108]3109 self.importLabels =False3110 self.description = ("Fetches the latest revision from perforce and "3111+"rebases the current work (branch) against it")31123113defrun(self, args):3114 sync =P4Sync()3115 sync.importLabels = self.importLabels3116 sync.run([])31173118return self.rebase()31193120defrebase(self):3121if os.system("git update-index --refresh") !=0:3122die("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.");3123iflen(read_pipe("git diff-index HEAD --")) >0:3124die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");31253126[upstream, settings] =findUpstreamBranchPoint()3127iflen(upstream) ==0:3128die("Cannot find upstream branchpoint for rebase")31293130# the branchpoint may be p4/foo~3, so strip off the parent3131 upstream = re.sub("~[0-9]+$","", upstream)31323133print"Rebasing the current branch onto%s"% upstream3134 oldHead =read_pipe("git rev-parse HEAD").strip()3135system("git rebase%s"% upstream)3136system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3137return True31383139classP4Clone(P4Sync):3140def__init__(self):3141 P4Sync.__init__(self)3142 self.description ="Creates a new git repository and imports from Perforce into it"3143 self.usage ="usage: %prog [options] //depot/path[@revRange]"3144 self.options += [3145 optparse.make_option("--destination", dest="cloneDestination",3146 action='store', default=None,3147help="where to leave result of the clone"),3148 optparse.make_option("-/", dest="cloneExclude",3149 action="append",type="string",3150help="exclude depot path"),3151 optparse.make_option("--bare", dest="cloneBare",3152 action="store_true", default=False),3153]3154 self.cloneDestination =None3155 self.needsGit =False3156 self.cloneBare =False31573158# This is required for the "append" cloneExclude action3159defensure_value(self, attr, value):3160if nothasattr(self, attr)orgetattr(self, attr)is None:3161setattr(self, attr, value)3162returngetattr(self, attr)31633164defdefaultDestination(self, args):3165## TODO: use common prefix of args?3166 depotPath = args[0]3167 depotDir = re.sub("(@[^@]*)$","", depotPath)3168 depotDir = re.sub("(#[^#]*)$","", depotDir)3169 depotDir = re.sub(r"\.\.\.$","", depotDir)3170 depotDir = re.sub(r"/$","", depotDir)3171return os.path.split(depotDir)[1]31723173defrun(self, args):3174iflen(args) <1:3175return False31763177if self.keepRepoPath and not self.cloneDestination:3178 sys.stderr.write("Must specify destination for --keep-path\n")3179 sys.exit(1)31803181 depotPaths = args31823183if not self.cloneDestination andlen(depotPaths) >1:3184 self.cloneDestination = depotPaths[-1]3185 depotPaths = depotPaths[:-1]31863187 self.cloneExclude = ["/"+p for p in self.cloneExclude]3188for p in depotPaths:3189if not p.startswith("//"):3190 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3191return False31923193if not self.cloneDestination:3194 self.cloneDestination = self.defaultDestination(args)31953196print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31973198if not os.path.exists(self.cloneDestination):3199 os.makedirs(self.cloneDestination)3200chdir(self.cloneDestination)32013202 init_cmd = ["git","init"]3203if self.cloneBare:3204 init_cmd.append("--bare")3205 subprocess.check_call(init_cmd)32063207if not P4Sync.run(self, depotPaths):3208return False32093210# create a master branch and check out a work tree3211ifgitBranchExists(self.branch):3212system(["git","branch","master", self.branch ])3213if not self.cloneBare:3214system(["git","checkout","-f"])3215else:3216print'Not checking out any branch, use ' \3217'"git checkout -q -b master <branch>"'32183219# auto-set this variable if invoked with --use-client-spec3220if self.useClientSpec_from_options:3221system("git config --bool git-p4.useclientspec true")32223223return True32243225classP4Branches(Command):3226def__init__(self):3227 Command.__init__(self)3228 self.options = [ ]3229 self.description = ("Shows the git branches that hold imports and their "3230+"corresponding perforce depot paths")3231 self.verbose =False32323233defrun(self, args):3234iforiginP4BranchesExist():3235createOrUpdateBranchesFromOrigin()32363237 cmdline ="git rev-parse --symbolic "3238 cmdline +=" --remotes"32393240for line inread_pipe_lines(cmdline):3241 line = line.strip()32423243if not line.startswith('p4/')or line =="p4/HEAD":3244continue3245 branch = line32463247 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3248 settings =extractSettingsGitLog(log)32493250print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3251return True32523253classHelpFormatter(optparse.IndentedHelpFormatter):3254def__init__(self):3255 optparse.IndentedHelpFormatter.__init__(self)32563257defformat_description(self, description):3258if description:3259return description +"\n"3260else:3261return""32623263defprintUsage(commands):3264print"usage:%s<command> [options]"% sys.argv[0]3265print""3266print"valid commands:%s"%", ".join(commands)3267print""3268print"Try%s<command> --help for command specific help."% sys.argv[0]3269print""32703271commands = {3272"debug": P4Debug,3273"submit": P4Submit,3274"commit": P4Submit,3275"sync": P4Sync,3276"rebase": P4Rebase,3277"clone": P4Clone,3278"rollback": P4RollBack,3279"branches": P4Branches3280}328132823283defmain():3284iflen(sys.argv[1:]) ==0:3285printUsage(commands.keys())3286 sys.exit(2)32873288 cmdName = sys.argv[1]3289try:3290 klass = commands[cmdName]3291 cmd =klass()3292exceptKeyError:3293print"unknown command%s"% cmdName3294print""3295printUsage(commands.keys())3296 sys.exit(2)32973298 options = cmd.options3299 cmd.gitdir = os.environ.get("GIT_DIR",None)33003301 args = sys.argv[2:]33023303 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3304if cmd.needsGit:3305 options.append(optparse.make_option("--git-dir", dest="gitdir"))33063307 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3308 options,3309 description = cmd.description,3310 formatter =HelpFormatter())33113312(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3313global verbose3314 verbose = cmd.verbose3315if cmd.needsGit:3316if cmd.gitdir ==None:3317 cmd.gitdir = os.path.abspath(".git")3318if notisValidGitDir(cmd.gitdir):3319 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3320if os.path.exists(cmd.gitdir):3321 cdup =read_pipe("git rev-parse --show-cdup").strip()3322iflen(cdup) >0:3323chdir(cdup);33243325if notisValidGitDir(cmd.gitdir):3326ifisValidGitDir(cmd.gitdir +"/.git"):3327 cmd.gitdir +="/.git"3328else:3329die("fatal: cannot locate git repository at%s"% cmd.gitdir)33303331 os.environ["GIT_DIR"] = cmd.gitdir33323333if not cmd.run(args):3334 parser.print_help()3335 sys.exit(2)333633373338if __name__ =='__main__':3339main()