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",2648"--no-merges", parent]):2649 blob = blob.strip()2650iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2651 parentFound =True2652if self.verbose:2653print"Found parent of%sin commit%s"% (branch, blob)2654break2655if parentFound:2656return blob2657else:2658return None26592660defimportChanges(self, changes):2661 cnt =12662for change in changes:2663 description =p4_describe(change)2664 self.updateOptionDict(description)26652666if not self.silent:2667 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2668 sys.stdout.flush()2669 cnt = cnt +126702671try:2672if self.detectBranches:2673 branches = self.splitFilesIntoBranches(description)2674for branch in branches.keys():2675## HACK --hwn2676 branchPrefix = self.depotPaths[0] + branch +"/"2677 self.branchPrefixes = [ branchPrefix ]26782679 parent =""26802681 filesForCommit = branches[branch]26822683if self.verbose:2684print"branch is%s"% branch26852686 self.updatedBranches.add(branch)26872688if branch not in self.createdBranches:2689 self.createdBranches.add(branch)2690 parent = self.knownBranches[branch]2691if parent == branch:2692 parent =""2693else:2694 fullBranch = self.projectName + branch2695if fullBranch not in self.p4BranchesInGit:2696if not self.silent:2697print("\nImporting new branch%s"% fullBranch);2698if self.importNewBranch(branch, change -1):2699 parent =""2700 self.p4BranchesInGit.append(fullBranch)2701if not self.silent:2702print("\nResuming with change%s"% change);27032704if self.verbose:2705print"parent determined through known branches:%s"% parent27062707 branch = self.gitRefForBranch(branch)2708 parent = self.gitRefForBranch(parent)27092710if self.verbose:2711print"looking for initial parent for%s; current parent is%s"% (branch, parent)27122713iflen(parent) ==0and branch in self.initialParents:2714 parent = self.initialParents[branch]2715del self.initialParents[branch]27162717 blob =None2718iflen(parent) >0:2719 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2720if self.verbose:2721print"Creating temporary branch: "+ tempBranch2722 self.commit(description, filesForCommit, tempBranch)2723 self.tempBranches.append(tempBranch)2724 self.checkpoint()2725 blob = self.searchParent(parent, branch, tempBranch)2726if blob:2727 self.commit(description, filesForCommit, branch, blob)2728else:2729if self.verbose:2730print"Parent of%snot found. Committing into head of%s"% (branch, parent)2731 self.commit(description, filesForCommit, branch, parent)2732else:2733 files = self.extractFilesFromCommit(description)2734 self.commit(description, files, self.branch,2735 self.initialParent)2736# only needed once, to connect to the previous commit2737 self.initialParent =""2738exceptIOError:2739print self.gitError.read()2740 sys.exit(1)27412742defimportHeadRevision(self, revision):2743print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27442745 details = {}2746 details["user"] ="git perforce import user"2747 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2748% (' '.join(self.depotPaths), revision))2749 details["change"] = revision2750 newestRevision =027512752 fileCnt =02753 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27542755for info inp4CmdList(["files"] + fileArgs):27562757if'code'in info and info['code'] =='error':2758 sys.stderr.write("p4 returned an error:%s\n"2759% info['data'])2760if info['data'].find("must refer to client") >=0:2761 sys.stderr.write("This particular p4 error is misleading.\n")2762 sys.stderr.write("Perhaps the depot path was misspelled.\n");2763 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2764 sys.exit(1)2765if'p4ExitCode'in info:2766 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2767 sys.exit(1)276827692770 change =int(info["change"])2771if change > newestRevision:2772 newestRevision = change27732774if info["action"]in self.delete_actions:2775# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2776#fileCnt = fileCnt + 12777continue27782779for prop in["depotFile","rev","action","type"]:2780 details["%s%s"% (prop, fileCnt)] = info[prop]27812782 fileCnt = fileCnt +127832784 details["change"] = newestRevision27852786# Use time from top-most change so that all git p4 clones of2787# the same p4 repo have the same commit SHA1s.2788 res =p4_describe(newestRevision)2789 details["time"] = res["time"]27902791 self.updateOptionDict(details)2792try:2793 self.commit(details, self.extractFilesFromCommit(details), self.branch)2794exceptIOError:2795print"IO error with git fast-import. Is your git version recent enough?"2796print self.gitError.read()279727982799defrun(self, args):2800 self.depotPaths = []2801 self.changeRange =""2802 self.previousDepotPaths = []2803 self.hasOrigin =False28042805# map from branch depot path to parent branch2806 self.knownBranches = {}2807 self.initialParents = {}28082809if self.importIntoRemotes:2810 self.refPrefix ="refs/remotes/p4/"2811else:2812 self.refPrefix ="refs/heads/p4/"28132814if self.syncWithOrigin:2815 self.hasOrigin =originP4BranchesExist()2816if self.hasOrigin:2817if not self.silent:2818print'Syncing with origin first, using "git fetch origin"'2819system("git fetch origin")28202821 branch_arg_given =bool(self.branch)2822iflen(self.branch) ==0:2823 self.branch = self.refPrefix +"master"2824ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2825system("git update-ref%srefs/heads/p4"% self.branch)2826system("git branch -D p4")28272828# accept either the command-line option, or the configuration variable2829if self.useClientSpec:2830# will use this after clone to set the variable2831 self.useClientSpec_from_options =True2832else:2833ifgitConfig("git-p4.useclientspec","--bool") =="true":2834 self.useClientSpec =True2835if self.useClientSpec:2836 self.clientSpecDirs =getClientSpec()28372838# TODO: should always look at previous commits,2839# merge with previous imports, if possible.2840if args == []:2841if self.hasOrigin:2842createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28432844# branches holds mapping from branch name to sha12845 branches =p4BranchesInGit(self.importIntoRemotes)28462847# restrict to just this one, disabling detect-branches2848if branch_arg_given:2849 short = self.branch.split("/")[-1]2850if short in branches:2851 self.p4BranchesInGit = [ short ]2852else:2853 self.p4BranchesInGit = branches.keys()28542855iflen(self.p4BranchesInGit) >1:2856if not self.silent:2857print"Importing from/into multiple branches"2858 self.detectBranches =True2859for branch in branches.keys():2860 self.initialParents[self.refPrefix + branch] = \2861 branches[branch]28622863if self.verbose:2864print"branches:%s"% self.p4BranchesInGit28652866 p4Change =02867for branch in self.p4BranchesInGit:2868 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28692870 settings =extractSettingsGitLog(logMsg)28712872 self.readOptions(settings)2873if(settings.has_key('depot-paths')2874and settings.has_key('change')):2875 change =int(settings['change']) +12876 p4Change =max(p4Change, change)28772878 depotPaths =sorted(settings['depot-paths'])2879if self.previousDepotPaths == []:2880 self.previousDepotPaths = depotPaths2881else:2882 paths = []2883for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2884 prev_list = prev.split("/")2885 cur_list = cur.split("/")2886for i inrange(0,min(len(cur_list),len(prev_list))):2887if cur_list[i] <> prev_list[i]:2888 i = i -12889break28902891 paths.append("/".join(cur_list[:i +1]))28922893 self.previousDepotPaths = paths28942895if p4Change >0:2896 self.depotPaths =sorted(self.previousDepotPaths)2897 self.changeRange ="@%s,#head"% p4Change2898if not self.silent and not self.detectBranches:2899print"Performing incremental import into%sgit branch"% self.branch29002901# accept multiple ref name abbreviations:2902# refs/foo/bar/branch -> use it exactly2903# p4/branch -> prepend refs/remotes/ or refs/heads/2904# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2905if not self.branch.startswith("refs/"):2906if self.importIntoRemotes:2907 prepend ="refs/remotes/"2908else:2909 prepend ="refs/heads/"2910if not self.branch.startswith("p4/"):2911 prepend +="p4/"2912 self.branch = prepend + self.branch29132914iflen(args) ==0and self.depotPaths:2915if not self.silent:2916print"Depot paths:%s"%' '.join(self.depotPaths)2917else:2918if self.depotPaths and self.depotPaths != args:2919print("previous import used depot path%sand now%swas specified. "2920"This doesn't work!"% (' '.join(self.depotPaths),2921' '.join(args)))2922 sys.exit(1)29232924 self.depotPaths =sorted(args)29252926 revision =""2927 self.users = {}29282929# Make sure no revision specifiers are used when --changesfile2930# is specified.2931 bad_changesfile =False2932iflen(self.changesFile) >0:2933for p in self.depotPaths:2934if p.find("@") >=0or p.find("#") >=0:2935 bad_changesfile =True2936break2937if bad_changesfile:2938die("Option --changesfile is incompatible with revision specifiers")29392940 newPaths = []2941for p in self.depotPaths:2942if p.find("@") != -1:2943 atIdx = p.index("@")2944 self.changeRange = p[atIdx:]2945if self.changeRange =="@all":2946 self.changeRange =""2947elif','not in self.changeRange:2948 revision = self.changeRange2949 self.changeRange =""2950 p = p[:atIdx]2951elif p.find("#") != -1:2952 hashIdx = p.index("#")2953 revision = p[hashIdx:]2954 p = p[:hashIdx]2955elif self.previousDepotPaths == []:2956# pay attention to changesfile, if given, else import2957# the entire p4 tree at the head revision2958iflen(self.changesFile) ==0:2959 revision ="#head"29602961 p = re.sub("\.\.\.$","", p)2962if not p.endswith("/"):2963 p +="/"29642965 newPaths.append(p)29662967 self.depotPaths = newPaths29682969# --detect-branches may change this for each branch2970 self.branchPrefixes = self.depotPaths29712972 self.loadUserMapFromCache()2973 self.labels = {}2974if self.detectLabels:2975 self.getLabels();29762977if self.detectBranches:2978## FIXME - what's a P4 projectName ?2979 self.projectName = self.guessProjectName()29802981if self.hasOrigin:2982 self.getBranchMappingFromGitBranches()2983else:2984 self.getBranchMapping()2985if self.verbose:2986print"p4-git branches:%s"% self.p4BranchesInGit2987print"initial parents:%s"% self.initialParents2988for b in self.p4BranchesInGit:2989if b !="master":29902991## FIXME2992 b = b[len(self.projectName):]2993 self.createdBranches.add(b)29942995 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29962997 self.importProcess = subprocess.Popen(["git","fast-import"],2998 stdin=subprocess.PIPE,2999 stdout=subprocess.PIPE,3000 stderr=subprocess.PIPE);3001 self.gitOutput = self.importProcess.stdout3002 self.gitStream = self.importProcess.stdin3003 self.gitError = self.importProcess.stderr30043005if revision:3006 self.importHeadRevision(revision)3007else:3008 changes = []30093010iflen(self.changesFile) >0:3011 output =open(self.changesFile).readlines()3012 changeSet =set()3013for line in output:3014 changeSet.add(int(line))30153016for change in changeSet:3017 changes.append(change)30183019 changes.sort()3020else:3021# catch "git p4 sync" with no new branches, in a repo that3022# does not have any existing p4 branches3023iflen(args) ==0:3024if not self.p4BranchesInGit:3025die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30263027# The default branch is master, unless --branch is used to3028# specify something else. Make sure it exists, or complain3029# nicely about how to use --branch.3030if not self.detectBranches:3031if notbranch_exists(self.branch):3032if branch_arg_given:3033die("Error: branch%sdoes not exist."% self.branch)3034else:3035die("Error: no branch%s; perhaps specify one with --branch."%3036 self.branch)30373038if self.verbose:3039print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3040 self.changeRange)3041 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30423043iflen(self.maxChanges) >0:3044 changes = changes[:min(int(self.maxChanges),len(changes))]30453046iflen(changes) ==0:3047if not self.silent:3048print"No changes to import!"3049else:3050if not self.silent and not self.detectBranches:3051print"Import destination:%s"% self.branch30523053 self.updatedBranches =set()30543055if not self.detectBranches:3056if args:3057# start a new branch3058 self.initialParent =""3059else:3060# build on a previous revision3061 self.initialParent =parseRevision(self.branch)30623063 self.importChanges(changes)30643065if not self.silent:3066print""3067iflen(self.updatedBranches) >0:3068 sys.stdout.write("Updated branches: ")3069for b in self.updatedBranches:3070 sys.stdout.write("%s"% b)3071 sys.stdout.write("\n")30723073ifgitConfig("git-p4.importLabels","--bool") =="true":3074 self.importLabels =True30753076if self.importLabels:3077 p4Labels =getP4Labels(self.depotPaths)3078 gitTags =getGitTags()30793080 missingP4Labels = p4Labels - gitTags3081 self.importP4Labels(self.gitStream, missingP4Labels)30823083 self.gitStream.close()3084if self.importProcess.wait() !=0:3085die("fast-import failed:%s"% self.gitError.read())3086 self.gitOutput.close()3087 self.gitError.close()30883089# Cleanup temporary branches created during import3090if self.tempBranches != []:3091for branch in self.tempBranches:3092read_pipe("git update-ref -d%s"% branch)3093 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30943095# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3096# a convenient shortcut refname "p4".3097if self.importIntoRemotes:3098 head_ref = self.refPrefix +"HEAD"3099if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3100system(["git","symbolic-ref", head_ref, self.branch])31013102return True31033104classP4Rebase(Command):3105def__init__(self):3106 Command.__init__(self)3107 self.options = [3108 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3109]3110 self.importLabels =False3111 self.description = ("Fetches the latest revision from perforce and "3112+"rebases the current work (branch) against it")31133114defrun(self, args):3115 sync =P4Sync()3116 sync.importLabels = self.importLabels3117 sync.run([])31183119return self.rebase()31203121defrebase(self):3122if os.system("git update-index --refresh") !=0:3123die("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.");3124iflen(read_pipe("git diff-index HEAD --")) >0:3125die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");31263127[upstream, settings] =findUpstreamBranchPoint()3128iflen(upstream) ==0:3129die("Cannot find upstream branchpoint for rebase")31303131# the branchpoint may be p4/foo~3, so strip off the parent3132 upstream = re.sub("~[0-9]+$","", upstream)31333134print"Rebasing the current branch onto%s"% upstream3135 oldHead =read_pipe("git rev-parse HEAD").strip()3136system("git rebase%s"% upstream)3137system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3138return True31393140classP4Clone(P4Sync):3141def__init__(self):3142 P4Sync.__init__(self)3143 self.description ="Creates a new git repository and imports from Perforce into it"3144 self.usage ="usage: %prog [options] //depot/path[@revRange]"3145 self.options += [3146 optparse.make_option("--destination", dest="cloneDestination",3147 action='store', default=None,3148help="where to leave result of the clone"),3149 optparse.make_option("-/", dest="cloneExclude",3150 action="append",type="string",3151help="exclude depot path"),3152 optparse.make_option("--bare", dest="cloneBare",3153 action="store_true", default=False),3154]3155 self.cloneDestination =None3156 self.needsGit =False3157 self.cloneBare =False31583159# This is required for the "append" cloneExclude action3160defensure_value(self, attr, value):3161if nothasattr(self, attr)orgetattr(self, attr)is None:3162setattr(self, attr, value)3163returngetattr(self, attr)31643165defdefaultDestination(self, args):3166## TODO: use common prefix of args?3167 depotPath = args[0]3168 depotDir = re.sub("(@[^@]*)$","", depotPath)3169 depotDir = re.sub("(#[^#]*)$","", depotDir)3170 depotDir = re.sub(r"\.\.\.$","", depotDir)3171 depotDir = re.sub(r"/$","", depotDir)3172return os.path.split(depotDir)[1]31733174defrun(self, args):3175iflen(args) <1:3176return False31773178if self.keepRepoPath and not self.cloneDestination:3179 sys.stderr.write("Must specify destination for --keep-path\n")3180 sys.exit(1)31813182 depotPaths = args31833184if not self.cloneDestination andlen(depotPaths) >1:3185 self.cloneDestination = depotPaths[-1]3186 depotPaths = depotPaths[:-1]31873188 self.cloneExclude = ["/"+p for p in self.cloneExclude]3189for p in depotPaths:3190if not p.startswith("//"):3191 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3192return False31933194if not self.cloneDestination:3195 self.cloneDestination = self.defaultDestination(args)31963197print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31983199if not os.path.exists(self.cloneDestination):3200 os.makedirs(self.cloneDestination)3201chdir(self.cloneDestination)32023203 init_cmd = ["git","init"]3204if self.cloneBare:3205 init_cmd.append("--bare")3206 subprocess.check_call(init_cmd)32073208if not P4Sync.run(self, depotPaths):3209return False32103211# create a master branch and check out a work tree3212ifgitBranchExists(self.branch):3213system(["git","branch","master", self.branch ])3214if not self.cloneBare:3215system(["git","checkout","-f"])3216else:3217print'Not checking out any branch, use ' \3218'"git checkout -q -b master <branch>"'32193220# auto-set this variable if invoked with --use-client-spec3221if self.useClientSpec_from_options:3222system("git config --bool git-p4.useclientspec true")32233224return True32253226classP4Branches(Command):3227def__init__(self):3228 Command.__init__(self)3229 self.options = [ ]3230 self.description = ("Shows the git branches that hold imports and their "3231+"corresponding perforce depot paths")3232 self.verbose =False32333234defrun(self, args):3235iforiginP4BranchesExist():3236createOrUpdateBranchesFromOrigin()32373238 cmdline ="git rev-parse --symbolic "3239 cmdline +=" --remotes"32403241for line inread_pipe_lines(cmdline):3242 line = line.strip()32433244if not line.startswith('p4/')or line =="p4/HEAD":3245continue3246 branch = line32473248 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3249 settings =extractSettingsGitLog(log)32503251print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3252return True32533254classHelpFormatter(optparse.IndentedHelpFormatter):3255def__init__(self):3256 optparse.IndentedHelpFormatter.__init__(self)32573258defformat_description(self, description):3259if description:3260return description +"\n"3261else:3262return""32633264defprintUsage(commands):3265print"usage:%s<command> [options]"% sys.argv[0]3266print""3267print"valid commands:%s"%", ".join(commands)3268print""3269print"Try%s<command> --help for command specific help."% sys.argv[0]3270print""32713272commands = {3273"debug": P4Debug,3274"submit": P4Submit,3275"commit": P4Submit,3276"sync": P4Sync,3277"rebase": P4Rebase,3278"clone": P4Clone,3279"rollback": P4RollBack,3280"branches": P4Branches3281}328232833284defmain():3285iflen(sys.argv[1:]) ==0:3286printUsage(commands.keys())3287 sys.exit(2)32883289 cmdName = sys.argv[1]3290try:3291 klass = commands[cmdName]3292 cmd =klass()3293exceptKeyError:3294print"unknown command%s"% cmdName3295print""3296printUsage(commands.keys())3297 sys.exit(2)32983299 options = cmd.options3300 cmd.gitdir = os.environ.get("GIT_DIR",None)33013302 args = sys.argv[2:]33033304 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3305if cmd.needsGit:3306 options.append(optparse.make_option("--git-dir", dest="gitdir"))33073308 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3309 options,3310 description = cmd.description,3311 formatter =HelpFormatter())33123313(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3314global verbose3315 verbose = cmd.verbose3316if cmd.needsGit:3317if cmd.gitdir ==None:3318 cmd.gitdir = os.path.abspath(".git")3319if notisValidGitDir(cmd.gitdir):3320 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3321if os.path.exists(cmd.gitdir):3322 cdup =read_pipe("git rev-parse --show-cdup").strip()3323iflen(cdup) >0:3324chdir(cdup);33253326if notisValidGitDir(cmd.gitdir):3327ifisValidGitDir(cmd.gitdir +"/.git"):3328 cmd.gitdir +="/.git"3329else:3330die("fatal: cannot locate git repository at%s"% cmd.gitdir)33313332 os.environ["GIT_DIR"] = cmd.gitdir33333334if not cmd.run(args):3335 parser.print_help()3336 sys.exit(2)333733383339if __name__ =='__main__':3340main()