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 = {} 563 564defgitConfig(key): 565if not _gitConfig.has_key(key): 566 cmd = ["git","config", key ] 567 s =read_pipe(cmd, ignore_error=True) 568 _gitConfig[key] = s.strip() 569return _gitConfig[key] 570 571defgitConfigBool(key): 572"""Return a bool, using git config --bool. It is True only if the 573 variable is set to true, and False if set to false or not present 574 in the config.""" 575 576if not _gitConfig.has_key(key): 577 cmd = ["git","config","--bool", key ] 578 s =read_pipe(cmd, ignore_error=True) 579 v = s.strip() 580 _gitConfig[key] = v =="true" 581return _gitConfig[key] 582 583defgitConfigList(key): 584if not _gitConfig.has_key(key): 585 s =read_pipe(["git","config","--get-all", key], ignore_error=True) 586 _gitConfig[key] = s.strip().split(os.linesep) 587return _gitConfig[key] 588 589defp4BranchesInGit(branchesAreInRemotes=True): 590"""Find all the branches whose names start with "p4/", looking 591 in remotes or heads as specified by the argument. Return 592 a dictionary of{ branch: revision }for each one found. 593 The branch names are the short names, without any 594 "p4/" prefix.""" 595 596 branches = {} 597 598 cmdline ="git rev-parse --symbolic " 599if branchesAreInRemotes: 600 cmdline +="--remotes" 601else: 602 cmdline +="--branches" 603 604for line inread_pipe_lines(cmdline): 605 line = line.strip() 606 607# only import to p4/ 608if not line.startswith('p4/'): 609continue 610# special symbolic ref to p4/master 611if line =="p4/HEAD": 612continue 613 614# strip off p4/ prefix 615 branch = line[len("p4/"):] 616 617 branches[branch] =parseRevision(line) 618 619return branches 620 621defbranch_exists(branch): 622"""Make sure that the given ref name really exists.""" 623 624 cmd = ["git","rev-parse","--symbolic","--verify", branch ] 625 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 626 out, _ = p.communicate() 627if p.returncode: 628return False 629# expect exactly one line of output: the branch name 630return out.rstrip() == branch 631 632deffindUpstreamBranchPoint(head ="HEAD"): 633 branches =p4BranchesInGit() 634# map from depot-path to branch name 635 branchByDepotPath = {} 636for branch in branches.keys(): 637 tip = branches[branch] 638 log =extractLogMessageFromGitCommit(tip) 639 settings =extractSettingsGitLog(log) 640if settings.has_key("depot-paths"): 641 paths =",".join(settings["depot-paths"]) 642 branchByDepotPath[paths] ="remotes/p4/"+ branch 643 644 settings =None 645 parent =0 646while parent <65535: 647 commit = head +"~%s"% parent 648 log =extractLogMessageFromGitCommit(commit) 649 settings =extractSettingsGitLog(log) 650if settings.has_key("depot-paths"): 651 paths =",".join(settings["depot-paths"]) 652if branchByDepotPath.has_key(paths): 653return[branchByDepotPath[paths], settings] 654 655 parent = parent +1 656 657return["", settings] 658 659defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 660if not silent: 661print("Creating/updating branch(es) in%sbased on origin branch(es)" 662% localRefPrefix) 663 664 originPrefix ="origin/p4/" 665 666for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 667 line = line.strip() 668if(not line.startswith(originPrefix))or line.endswith("HEAD"): 669continue 670 671 headName = line[len(originPrefix):] 672 remoteHead = localRefPrefix + headName 673 originHead = line 674 675 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 676if(not original.has_key('depot-paths') 677or not original.has_key('change')): 678continue 679 680 update =False 681if notgitBranchExists(remoteHead): 682if verbose: 683print"creating%s"% remoteHead 684 update =True 685else: 686 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 687if settings.has_key('change') >0: 688if settings['depot-paths'] == original['depot-paths']: 689 originP4Change =int(original['change']) 690 p4Change =int(settings['change']) 691if originP4Change > p4Change: 692print("%s(%s) is newer than%s(%s). " 693"Updating p4 branch from origin." 694% (originHead, originP4Change, 695 remoteHead, p4Change)) 696 update =True 697else: 698print("Ignoring:%swas imported from%swhile " 699"%swas imported from%s" 700% (originHead,','.join(original['depot-paths']), 701 remoteHead,','.join(settings['depot-paths']))) 702 703if update: 704system("git update-ref%s %s"% (remoteHead, originHead)) 705 706deforiginP4BranchesExist(): 707returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 708 709defp4ChangesForPaths(depotPaths, changeRange): 710assert depotPaths 711 cmd = ['changes'] 712for p in depotPaths: 713 cmd += ["%s...%s"% (p, changeRange)] 714 output =p4_read_pipe_lines(cmd) 715 716 changes = {} 717for line in output: 718 changeNum =int(line.split(" ")[1]) 719 changes[changeNum] =True 720 721 changelist = changes.keys() 722 changelist.sort() 723return changelist 724 725defp4PathStartsWith(path, prefix): 726# This method tries to remedy a potential mixed-case issue: 727# 728# If UserA adds //depot/DirA/file1 729# and UserB adds //depot/dira/file2 730# 731# we may or may not have a problem. If you have core.ignorecase=true, 732# we treat DirA and dira as the same directory 733ifgitConfigBool("core.ignorecase"): 734return path.lower().startswith(prefix.lower()) 735return path.startswith(prefix) 736 737defgetClientSpec(): 738"""Look at the p4 client spec, create a View() object that contains 739 all the mappings, and return it.""" 740 741 specList =p4CmdList("client -o") 742iflen(specList) !=1: 743die('Output from "client -o" is%dlines, expecting 1'% 744len(specList)) 745 746# dictionary of all client parameters 747 entry = specList[0] 748 749# just the keys that start with "View" 750 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 751 752# hold this new View 753 view =View() 754 755# append the lines, in order, to the view 756for view_num inrange(len(view_keys)): 757 k ="View%d"% view_num 758if k not in view_keys: 759die("Expected view key%smissing"% k) 760 view.append(entry[k]) 761 762return view 763 764defgetClientRoot(): 765"""Grab the client directory.""" 766 767 output =p4CmdList("client -o") 768iflen(output) !=1: 769die('Output from "client -o" is%dlines, expecting 1'%len(output)) 770 771 entry = output[0] 772if"Root"not in entry: 773die('Client has no "Root"') 774 775return entry["Root"] 776 777# 778# P4 wildcards are not allowed in filenames. P4 complains 779# if you simply add them, but you can force it with "-f", in 780# which case it translates them into %xx encoding internally. 781# 782defwildcard_decode(path): 783# Search for and fix just these four characters. Do % last so 784# that fixing it does not inadvertently create new %-escapes. 785# Cannot have * in a filename in windows; untested as to 786# what p4 would do in such a case. 787if not platform.system() =="Windows": 788 path = path.replace("%2A","*") 789 path = path.replace("%23","#") \ 790.replace("%40","@") \ 791.replace("%25","%") 792return path 793 794defwildcard_encode(path): 795# do % first to avoid double-encoding the %s introduced here 796 path = path.replace("%","%25") \ 797.replace("*","%2A") \ 798.replace("#","%23") \ 799.replace("@","%40") 800return path 801 802defwildcard_present(path): 803return path.translate(None,"*#@%") != path 804 805class Command: 806def__init__(self): 807 self.usage ="usage: %prog [options]" 808 self.needsGit =True 809 self.verbose =False 810 811class P4UserMap: 812def__init__(self): 813 self.userMapFromPerforceServer =False 814 self.myP4UserId =None 815 816defp4UserId(self): 817if self.myP4UserId: 818return self.myP4UserId 819 820 results =p4CmdList("user -o") 821for r in results: 822if r.has_key('User'): 823 self.myP4UserId = r['User'] 824return r['User'] 825die("Could not find your p4 user id") 826 827defp4UserIsMe(self, p4User): 828# return True if the given p4 user is actually me 829 me = self.p4UserId() 830if not p4User or p4User != me: 831return False 832else: 833return True 834 835defgetUserCacheFilename(self): 836 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 837return home +"/.gitp4-usercache.txt" 838 839defgetUserMapFromPerforceServer(self): 840if self.userMapFromPerforceServer: 841return 842 self.users = {} 843 self.emails = {} 844 845for output inp4CmdList("users"): 846if not output.has_key("User"): 847continue 848 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 849 self.emails[output["Email"]] = output["User"] 850 851 852 s ='' 853for(key, val)in self.users.items(): 854 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 855 856open(self.getUserCacheFilename(),"wb").write(s) 857 self.userMapFromPerforceServer =True 858 859defloadUserMapFromCache(self): 860 self.users = {} 861 self.userMapFromPerforceServer =False 862try: 863 cache =open(self.getUserCacheFilename(),"rb") 864 lines = cache.readlines() 865 cache.close() 866for line in lines: 867 entry = line.strip().split("\t") 868 self.users[entry[0]] = entry[1] 869exceptIOError: 870 self.getUserMapFromPerforceServer() 871 872classP4Debug(Command): 873def__init__(self): 874 Command.__init__(self) 875 self.options = [] 876 self.description ="A tool to debug the output of p4 -G." 877 self.needsGit =False 878 879defrun(self, args): 880 j =0 881for output inp4CmdList(args): 882print'Element:%d'% j 883 j +=1 884print output 885return True 886 887classP4RollBack(Command): 888def__init__(self): 889 Command.__init__(self) 890 self.options = [ 891 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 892] 893 self.description ="A tool to debug the multi-branch import. Don't use :)" 894 self.rollbackLocalBranches =False 895 896defrun(self, args): 897iflen(args) !=1: 898return False 899 maxChange =int(args[0]) 900 901if"p4ExitCode"inp4Cmd("changes -m 1"): 902die("Problems executing p4"); 903 904if self.rollbackLocalBranches: 905 refPrefix ="refs/heads/" 906 lines =read_pipe_lines("git rev-parse --symbolic --branches") 907else: 908 refPrefix ="refs/remotes/" 909 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 910 911for line in lines: 912if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 913 line = line.strip() 914 ref = refPrefix + line 915 log =extractLogMessageFromGitCommit(ref) 916 settings =extractSettingsGitLog(log) 917 918 depotPaths = settings['depot-paths'] 919 change = settings['change'] 920 921 changed =False 922 923iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 924for p in depotPaths]))) ==0: 925print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 926system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 927continue 928 929while change andint(change) > maxChange: 930 changed =True 931if self.verbose: 932print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 933system("git update-ref%s\"%s^\""% (ref, ref)) 934 log =extractLogMessageFromGitCommit(ref) 935 settings =extractSettingsGitLog(log) 936 937 938 depotPaths = settings['depot-paths'] 939 change = settings['change'] 940 941if changed: 942print"%srewound to%s"% (ref, change) 943 944return True 945 946classP4Submit(Command, P4UserMap): 947 948 conflict_behavior_choices = ("ask","skip","quit") 949 950def__init__(self): 951 Command.__init__(self) 952 P4UserMap.__init__(self) 953 self.options = [ 954 optparse.make_option("--origin", dest="origin"), 955 optparse.make_option("-M", dest="detectRenames", action="store_true"), 956# preserve the user, requires relevant p4 permissions 957 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 958 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 959 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 960 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 961 optparse.make_option("--conflict", dest="conflict_behavior", 962 choices=self.conflict_behavior_choices), 963 optparse.make_option("--branch", dest="branch"), 964] 965 self.description ="Submit changes from git to the perforce depot." 966 self.usage +=" [name of git branch to submit into perforce depot]" 967 self.origin ="" 968 self.detectRenames =False 969 self.preserveUser =gitConfigBool("git-p4.preserveUser") 970 self.dry_run =False 971 self.prepare_p4_only =False 972 self.conflict_behavior =None 973 self.isWindows = (platform.system() =="Windows") 974 self.exportLabels =False 975 self.p4HasMoveCommand =p4_has_move_command() 976 self.branch =None 977 978defcheck(self): 979iflen(p4CmdList("opened ...")) >0: 980die("You have files opened with perforce! Close them before starting the sync.") 981 982defseparate_jobs_from_description(self, message): 983"""Extract and return a possible Jobs field in the commit 984 message. It goes into a separate section in the p4 change 985 specification. 986 987 A jobs line starts with "Jobs:" and looks like a new field 988 in a form. Values are white-space separated on the same 989 line or on following lines that start with a tab. 990 991 This does not parse and extract the full git commit message 992 like a p4 form. It just sees the Jobs: line as a marker 993 to pass everything from then on directly into the p4 form, 994 but outside the description section. 995 996 Return a tuple (stripped log message, jobs string).""" 997 998 m = re.search(r'^Jobs:', message, re.MULTILINE) 999if m is None:1000return(message,None)10011002 jobtext = message[m.start():]1003 stripped_message = message[:m.start()].rstrip()1004return(stripped_message, jobtext)10051006defprepareLogMessage(self, template, message, jobs):1007"""Edits the template returned from "p4 change -o" to insert1008 the message in the Description field, and the jobs text in1009 the Jobs field."""1010 result =""10111012 inDescriptionSection =False10131014for line in template.split("\n"):1015if line.startswith("#"):1016 result += line +"\n"1017continue10181019if inDescriptionSection:1020if line.startswith("Files:")or line.startswith("Jobs:"):1021 inDescriptionSection =False1022# insert Jobs section1023if jobs:1024 result += jobs +"\n"1025else:1026continue1027else:1028if line.startswith("Description:"):1029 inDescriptionSection =True1030 line +="\n"1031for messageLine in message.split("\n"):1032 line +="\t"+ messageLine +"\n"10331034 result += line +"\n"10351036return result10371038defpatchRCSKeywords(self,file, pattern):1039# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern1040(handle, outFileName) = tempfile.mkstemp(dir='.')1041try:1042 outFile = os.fdopen(handle,"w+")1043 inFile =open(file,"r")1044 regexp = re.compile(pattern, re.VERBOSE)1045for line in inFile.readlines():1046 line = regexp.sub(r'$\1$', line)1047 outFile.write(line)1048 inFile.close()1049 outFile.close()1050# Forcibly overwrite the original file1051 os.unlink(file)1052 shutil.move(outFileName,file)1053except:1054# cleanup our temporary file1055 os.unlink(outFileName)1056print"Failed to strip RCS keywords in%s"%file1057raise10581059print"Patched up RCS keywords in%s"%file10601061defp4UserForCommit(self,id):1062# Return the tuple (perforce user,git email) for a given git commit id1063 self.getUserMapFromPerforceServer()1064 gitEmail =read_pipe(["git","log","--max-count=1",1065"--format=%ae",id])1066 gitEmail = gitEmail.strip()1067if not self.emails.has_key(gitEmail):1068return(None,gitEmail)1069else:1070return(self.emails[gitEmail],gitEmail)10711072defcheckValidP4Users(self,commits):1073# check if any git authors cannot be mapped to p4 users1074foridin commits:1075(user,email) = self.p4UserForCommit(id)1076if not user:1077 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1078ifgitConfigBool("git-p4.allowMissingP4Users"):1079print"%s"% msg1080else:1081die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10821083deflastP4Changelist(self):1084# Get back the last changelist number submitted in this client spec. This1085# then gets used to patch up the username in the change. If the same1086# client spec is being used by multiple processes then this might go1087# wrong.1088 results =p4CmdList("client -o")# find the current client1089 client =None1090for r in results:1091if r.has_key('Client'):1092 client = r['Client']1093break1094if not client:1095die("could not get client spec")1096 results =p4CmdList(["changes","-c", client,"-m","1"])1097for r in results:1098if r.has_key('change'):1099return r['change']1100die("Could not get changelist number for last submit - cannot patch up user details")11011102defmodifyChangelistUser(self, changelist, newUser):1103# fixup the user field of a changelist after it has been submitted.1104 changes =p4CmdList("change -o%s"% changelist)1105iflen(changes) !=1:1106die("Bad output from p4 change modifying%sto user%s"%1107(changelist, newUser))11081109 c = changes[0]1110if c['User'] == newUser:return# nothing to do1111 c['User'] = newUser1112input= marshal.dumps(c)11131114 result =p4CmdList("change -f -i", stdin=input)1115for r in result:1116if r.has_key('code'):1117if r['code'] =='error':1118die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1119if r.has_key('data'):1120print("Updated user field for changelist%sto%s"% (changelist, newUser))1121return1122die("Could not modify user field of changelist%sto%s"% (changelist, newUser))11231124defcanChangeChangelists(self):1125# check to see if we have p4 admin or super-user permissions, either of1126# which are required to modify changelists.1127 results =p4CmdList(["protects", self.depotPath])1128for r in results:1129if r.has_key('perm'):1130if r['perm'] =='admin':1131return11132if r['perm'] =='super':1133return11134return011351136defprepareSubmitTemplate(self):1137"""Run "p4 change -o" to grab a change specification template.1138 This does not use "p4 -G", as it is nice to keep the submission1139 template in original order, since a human might edit it.11401141 Remove lines in the Files section that show changes to files1142 outside the depot path we're committing into."""11431144 template =""1145 inFilesSection =False1146for line inp4_read_pipe_lines(['change','-o']):1147if line.endswith("\r\n"):1148 line = line[:-2] +"\n"1149if inFilesSection:1150if line.startswith("\t"):1151# path starts and ends with a tab1152 path = line[1:]1153 lastTab = path.rfind("\t")1154if lastTab != -1:1155 path = path[:lastTab]1156if notp4PathStartsWith(path, self.depotPath):1157continue1158else:1159 inFilesSection =False1160else:1161if line.startswith("Files:"):1162 inFilesSection =True11631164 template += line11651166return template11671168defedit_template(self, template_file):1169"""Invoke the editor to let the user change the submission1170 message. Return true if okay to continue with the submit."""11711172# if configured to skip the editing part, just submit1173ifgitConfigBool("git-p4.skipSubmitEdit"):1174return True11751176# look at the modification time, to check later if the user saved1177# the file1178 mtime = os.stat(template_file).st_mtime11791180# invoke the editor1181if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1182 editor = os.environ.get("P4EDITOR")1183else:1184 editor =read_pipe("git var GIT_EDITOR").strip()1185system(editor +" "+ template_file)11861187# If the file was not saved, prompt to see if this patch should1188# be skipped. But skip this verification step if configured so.1189ifgitConfigBool("git-p4.skipSubmitEditCheck"):1190return True11911192# modification time updated means user saved the file1193if os.stat(template_file).st_mtime > mtime:1194return True11951196while True:1197 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1198if response =='y':1199return True1200if response =='n':1201return False12021203defapplyCommit(self,id):1204"""Apply one commit, return True if it succeeded."""12051206print"Applying",read_pipe(["git","show","-s",1207"--format=format:%h%s",id])12081209(p4User, gitEmail) = self.p4UserForCommit(id)12101211 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1212 filesToAdd =set()1213 filesToDelete =set()1214 editedFiles =set()1215 pureRenameCopy =set()1216 filesToChangeExecBit = {}12171218for line in diff:1219 diff =parseDiffTreeEntry(line)1220 modifier = diff['status']1221 path = diff['src']1222if modifier =="M":1223p4_edit(path)1224ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1225 filesToChangeExecBit[path] = diff['dst_mode']1226 editedFiles.add(path)1227elif modifier =="A":1228 filesToAdd.add(path)1229 filesToChangeExecBit[path] = diff['dst_mode']1230if path in filesToDelete:1231 filesToDelete.remove(path)1232elif modifier =="D":1233 filesToDelete.add(path)1234if path in filesToAdd:1235 filesToAdd.remove(path)1236elif modifier =="C":1237 src, dest = diff['src'], diff['dst']1238p4_integrate(src, dest)1239 pureRenameCopy.add(dest)1240if diff['src_sha1'] != diff['dst_sha1']:1241p4_edit(dest)1242 pureRenameCopy.discard(dest)1243ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1244p4_edit(dest)1245 pureRenameCopy.discard(dest)1246 filesToChangeExecBit[dest] = diff['dst_mode']1247if self.isWindows:1248# turn off read-only attribute1249 os.chmod(dest, stat.S_IWRITE)1250 os.unlink(dest)1251 editedFiles.add(dest)1252elif modifier =="R":1253 src, dest = diff['src'], diff['dst']1254if self.p4HasMoveCommand:1255p4_edit(src)# src must be open before move1256p4_move(src, dest)# opens for (move/delete, move/add)1257else:1258p4_integrate(src, dest)1259if diff['src_sha1'] != diff['dst_sha1']:1260p4_edit(dest)1261else:1262 pureRenameCopy.add(dest)1263ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1264if not self.p4HasMoveCommand:1265p4_edit(dest)# with move: already open, writable1266 filesToChangeExecBit[dest] = diff['dst_mode']1267if not self.p4HasMoveCommand:1268if self.isWindows:1269 os.chmod(dest, stat.S_IWRITE)1270 os.unlink(dest)1271 filesToDelete.add(src)1272 editedFiles.add(dest)1273else:1274die("unknown modifier%sfor%s"% (modifier, path))12751276 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1277 patchcmd = diffcmd +" | git apply "1278 tryPatchCmd = patchcmd +"--check -"1279 applyPatchCmd = patchcmd +"--check --apply -"1280 patch_succeeded =True12811282if os.system(tryPatchCmd) !=0:1283 fixed_rcs_keywords =False1284 patch_succeeded =False1285print"Unfortunately applying the change failed!"12861287# Patch failed, maybe it's just RCS keyword woes. Look through1288# the patch to see if that's possible.1289ifgitConfigBool("git-p4.attemptRCSCleanup"):1290file=None1291 pattern =None1292 kwfiles = {}1293forfilein editedFiles | filesToDelete:1294# did this file's delta contain RCS keywords?1295 pattern =p4_keywords_regexp_for_file(file)12961297if pattern:1298# this file is a possibility...look for RCS keywords.1299 regexp = re.compile(pattern, re.VERBOSE)1300for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1301if regexp.search(line):1302if verbose:1303print"got keyword match on%sin%sin%s"% (pattern, line,file)1304 kwfiles[file] = pattern1305break13061307forfilein kwfiles:1308if verbose:1309print"zapping%swith%s"% (line,pattern)1310# File is being deleted, so not open in p4. Must1311# disable the read-only bit on windows.1312if self.isWindows andfilenot in editedFiles:1313 os.chmod(file, stat.S_IWRITE)1314 self.patchRCSKeywords(file, kwfiles[file])1315 fixed_rcs_keywords =True13161317if fixed_rcs_keywords:1318print"Retrying the patch with RCS keywords cleaned up"1319if os.system(tryPatchCmd) ==0:1320 patch_succeeded =True13211322if not patch_succeeded:1323for f in editedFiles:1324p4_revert(f)1325return False13261327#1328# Apply the patch for real, and do add/delete/+x handling.1329#1330system(applyPatchCmd)13311332for f in filesToAdd:1333p4_add(f)1334for f in filesToDelete:1335p4_revert(f)1336p4_delete(f)13371338# Set/clear executable bits1339for f in filesToChangeExecBit.keys():1340 mode = filesToChangeExecBit[f]1341setP4ExecBit(f, mode)13421343#1344# Build p4 change description, starting with the contents1345# of the git commit message.1346#1347 logMessage =extractLogMessageFromGitCommit(id)1348 logMessage = logMessage.strip()1349(logMessage, jobs) = self.separate_jobs_from_description(logMessage)13501351 template = self.prepareSubmitTemplate()1352 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)13531354if self.preserveUser:1355 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User13561357if self.checkAuthorship and not self.p4UserIsMe(p4User):1358 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1359 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1360 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"13611362 separatorLine ="######## everything below this line is just the diff #######\n"13631364# diff1365if os.environ.has_key("P4DIFF"):1366del(os.environ["P4DIFF"])1367 diff =""1368for editedFile in editedFiles:1369 diff +=p4_read_pipe(['diff','-du',1370wildcard_encode(editedFile)])13711372# new file diff1373 newdiff =""1374for newFile in filesToAdd:1375 newdiff +="==== new file ====\n"1376 newdiff +="--- /dev/null\n"1377 newdiff +="+++%s\n"% newFile1378 f =open(newFile,"r")1379for line in f.readlines():1380 newdiff +="+"+ line1381 f.close()13821383# change description file: submitTemplate, separatorLine, diff, newdiff1384(handle, fileName) = tempfile.mkstemp()1385 tmpFile = os.fdopen(handle,"w+")1386if self.isWindows:1387 submitTemplate = submitTemplate.replace("\n","\r\n")1388 separatorLine = separatorLine.replace("\n","\r\n")1389 newdiff = newdiff.replace("\n","\r\n")1390 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1391 tmpFile.close()13921393if self.prepare_p4_only:1394#1395# Leave the p4 tree prepared, and the submit template around1396# and let the user decide what to do next1397#1398print1399print"P4 workspace prepared for submission."1400print"To submit or revert, go to client workspace"1401print" "+ self.clientPath1402print1403print"To submit, use\"p4 submit\"to write a new description,"1404print"or\"p4 submit -i%s\"to use the one prepared by" \1405"\"git p4\"."% fileName1406print"You can delete the file\"%s\"when finished."% fileName14071408if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1409print"To preserve change ownership by user%s, you must\n" \1410"do\"p4 change -f <change>\"after submitting and\n" \1411"edit the User field."1412if pureRenameCopy:1413print"After submitting, renamed files must be re-synced."1414print"Invoke\"p4 sync -f\"on each of these files:"1415for f in pureRenameCopy:1416print" "+ f14171418print1419print"To revert the changes, use\"p4 revert ...\", and delete"1420print"the submit template file\"%s\""% fileName1421if filesToAdd:1422print"Since the commit adds new files, they must be deleted:"1423for f in filesToAdd:1424print" "+ f1425print1426return True14271428#1429# Let the user edit the change description, then submit it.1430#1431if self.edit_template(fileName):1432# read the edited message and submit1433 ret =True1434 tmpFile =open(fileName,"rb")1435 message = tmpFile.read()1436 tmpFile.close()1437 submitTemplate = message[:message.index(separatorLine)]1438if self.isWindows:1439 submitTemplate = submitTemplate.replace("\r\n","\n")1440p4_write_pipe(['submit','-i'], submitTemplate)14411442if self.preserveUser:1443if p4User:1444# Get last changelist number. Cannot easily get it from1445# the submit command output as the output is1446# unmarshalled.1447 changelist = self.lastP4Changelist()1448 self.modifyChangelistUser(changelist, p4User)14491450# The rename/copy happened by applying a patch that created a1451# new file. This leaves it writable, which confuses p4.1452for f in pureRenameCopy:1453p4_sync(f,"-f")14541455else:1456# skip this patch1457 ret =False1458print"Submission cancelled, undoing p4 changes."1459for f in editedFiles:1460p4_revert(f)1461for f in filesToAdd:1462p4_revert(f)1463 os.remove(f)1464for f in filesToDelete:1465p4_revert(f)14661467 os.remove(fileName)1468return ret14691470# Export git tags as p4 labels. Create a p4 label and then tag1471# with that.1472defexportGitTags(self, gitTags):1473 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1474iflen(validLabelRegexp) ==0:1475 validLabelRegexp = defaultLabelRegexp1476 m = re.compile(validLabelRegexp)14771478for name in gitTags:14791480if not m.match(name):1481if verbose:1482print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1483continue14841485# Get the p4 commit this corresponds to1486 logMessage =extractLogMessageFromGitCommit(name)1487 values =extractSettingsGitLog(logMessage)14881489if not values.has_key('change'):1490# a tag pointing to something not sent to p4; ignore1491if verbose:1492print"git tag%sdoes not give a p4 commit"% name1493continue1494else:1495 changelist = values['change']14961497# Get the tag details.1498 inHeader =True1499 isAnnotated =False1500 body = []1501for l inread_pipe_lines(["git","cat-file","-p", name]):1502 l = l.strip()1503if inHeader:1504if re.match(r'tag\s+', l):1505 isAnnotated =True1506elif re.match(r'\s*$', l):1507 inHeader =False1508continue1509else:1510 body.append(l)15111512if not isAnnotated:1513 body = ["lightweight tag imported by git p4\n"]15141515# Create the label - use the same view as the client spec we are using1516 clientSpec =getClientSpec()15171518 labelTemplate ="Label:%s\n"% name1519 labelTemplate +="Description:\n"1520for b in body:1521 labelTemplate +="\t"+ b +"\n"1522 labelTemplate +="View:\n"1523for mapping in clientSpec.mappings:1524 labelTemplate +="\t%s\n"% mapping.depot_side.path15251526if self.dry_run:1527print"Would create p4 label%sfor tag"% name1528elif self.prepare_p4_only:1529print"Not creating p4 label%sfor tag due to option" \1530" --prepare-p4-only"% name1531else:1532p4_write_pipe(["label","-i"], labelTemplate)15331534# Use the label1535p4_system(["tag","-l", name] +1536["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])15371538if verbose:1539print"created p4 label for tag%s"% name15401541defrun(self, args):1542iflen(args) ==0:1543 self.master =currentGitBranch()1544iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1545die("Detecting current git branch failed!")1546eliflen(args) ==1:1547 self.master = args[0]1548if notbranchExists(self.master):1549die("Branch%sdoes not exist"% self.master)1550else:1551return False15521553 allowSubmit =gitConfig("git-p4.allowSubmit")1554iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1555die("%sis not in git-p4.allowSubmit"% self.master)15561557[upstream, settings] =findUpstreamBranchPoint()1558 self.depotPath = settings['depot-paths'][0]1559iflen(self.origin) ==0:1560 self.origin = upstream15611562if self.preserveUser:1563if not self.canChangeChangelists():1564die("Cannot preserve user names without p4 super-user or admin permissions")15651566# if not set from the command line, try the config file1567if self.conflict_behavior is None:1568 val =gitConfig("git-p4.conflict")1569if val:1570if val not in self.conflict_behavior_choices:1571die("Invalid value '%s' for config git-p4.conflict"% val)1572else:1573 val ="ask"1574 self.conflict_behavior = val15751576if self.verbose:1577print"Origin branch is "+ self.origin15781579iflen(self.depotPath) ==0:1580print"Internal error: cannot locate perforce depot path from existing branches"1581 sys.exit(128)15821583 self.useClientSpec =False1584ifgitConfigBool("git-p4.useclientspec"):1585 self.useClientSpec =True1586if self.useClientSpec:1587 self.clientSpecDirs =getClientSpec()15881589if self.useClientSpec:1590# all files are relative to the client spec1591 self.clientPath =getClientRoot()1592else:1593 self.clientPath =p4Where(self.depotPath)15941595if self.clientPath =="":1596die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15971598print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1599 self.oldWorkingDirectory = os.getcwd()16001601# ensure the clientPath exists1602 new_client_dir =False1603if not os.path.exists(self.clientPath):1604 new_client_dir =True1605 os.makedirs(self.clientPath)16061607chdir(self.clientPath)1608if self.dry_run:1609print"Would synchronize p4 checkout in%s"% self.clientPath1610else:1611print"Synchronizing p4 checkout..."1612if new_client_dir:1613# old one was destroyed, and maybe nobody told p41614p4_sync("...","-f")1615else:1616p4_sync("...")1617 self.check()16181619 commits = []1620for line inread_pipe_lines(["git","rev-list","--no-merges","%s..%s"% (self.origin, self.master)]):1621 commits.append(line.strip())1622 commits.reverse()16231624if self.preserveUser orgitConfigBool("git-p4.skipUserNameCheck"):1625 self.checkAuthorship =False1626else:1627 self.checkAuthorship =True16281629if self.preserveUser:1630 self.checkValidP4Users(commits)16311632#1633# Build up a set of options to be passed to diff when1634# submitting each commit to p4.1635#1636if self.detectRenames:1637# command-line -M arg1638 self.diffOpts ="-M"1639else:1640# If not explicitly set check the config variable1641 detectRenames =gitConfig("git-p4.detectRenames")16421643if detectRenames.lower() =="false"or detectRenames =="":1644 self.diffOpts =""1645elif detectRenames.lower() =="true":1646 self.diffOpts ="-M"1647else:1648 self.diffOpts ="-M%s"% detectRenames16491650# no command-line arg for -C or --find-copies-harder, just1651# config variables1652 detectCopies =gitConfig("git-p4.detectCopies")1653if detectCopies.lower() =="false"or detectCopies =="":1654pass1655elif detectCopies.lower() =="true":1656 self.diffOpts +=" -C"1657else:1658 self.diffOpts +=" -C%s"% detectCopies16591660ifgitConfigBool("git-p4.detectCopiesHarder"):1661 self.diffOpts +=" --find-copies-harder"16621663#1664# Apply the commits, one at a time. On failure, ask if should1665# continue to try the rest of the patches, or quit.1666#1667if self.dry_run:1668print"Would apply"1669 applied = []1670 last =len(commits) -11671for i, commit inenumerate(commits):1672if self.dry_run:1673print" ",read_pipe(["git","show","-s",1674"--format=format:%h%s", commit])1675 ok =True1676else:1677 ok = self.applyCommit(commit)1678if ok:1679 applied.append(commit)1680else:1681if self.prepare_p4_only and i < last:1682print"Processing only the first commit due to option" \1683" --prepare-p4-only"1684break1685if i < last:1686 quit =False1687while True:1688# prompt for what to do, or use the option/variable1689if self.conflict_behavior =="ask":1690print"What do you want to do?"1691 response =raw_input("[s]kip this commit but apply"1692" the rest, or [q]uit? ")1693if not response:1694continue1695elif self.conflict_behavior =="skip":1696 response ="s"1697elif self.conflict_behavior =="quit":1698 response ="q"1699else:1700die("Unknown conflict_behavior '%s'"%1701 self.conflict_behavior)17021703if response[0] =="s":1704print"Skipping this commit, but applying the rest"1705break1706if response[0] =="q":1707print"Quitting"1708 quit =True1709break1710if quit:1711break17121713chdir(self.oldWorkingDirectory)17141715if self.dry_run:1716pass1717elif self.prepare_p4_only:1718pass1719eliflen(commits) ==len(applied):1720print"All commits applied!"17211722 sync =P4Sync()1723if self.branch:1724 sync.branch = self.branch1725 sync.run([])17261727 rebase =P4Rebase()1728 rebase.rebase()17291730else:1731iflen(applied) ==0:1732print"No commits applied."1733else:1734print"Applied only the commits marked with '*':"1735for c in commits:1736if c in applied:1737 star ="*"1738else:1739 star =" "1740print star,read_pipe(["git","show","-s",1741"--format=format:%h%s", c])1742print"You will have to do 'git p4 sync' and rebase."17431744ifgitConfigBool("git-p4.exportLabels"):1745 self.exportLabels =True17461747if self.exportLabels:1748 p4Labels =getP4Labels(self.depotPath)1749 gitTags =getGitTags()17501751 missingGitTags = gitTags - p4Labels1752 self.exportGitTags(missingGitTags)17531754# exit with error unless everything applied perfecly1755iflen(commits) !=len(applied):1756 sys.exit(1)17571758return True17591760classView(object):1761"""Represent a p4 view ("p4 help views"), and map files in a1762 repo according to the view."""17631764classPath(object):1765"""A depot or client path, possibly containing wildcards.1766 The only one supported is ... at the end, currently.1767 Initialize with the full path, with //depot or //client."""17681769def__init__(self, path, is_depot):1770 self.path = path1771 self.is_depot = is_depot1772 self.find_wildcards()1773# remember the prefix bit, useful for relative mappings1774 m = re.match("(//[^/]+/)", self.path)1775if not m:1776die("Path%sdoes not start with //prefix/"% self.path)1777 prefix = m.group(1)1778if not self.is_depot:1779# strip //client/ on client paths1780 self.path = self.path[len(prefix):]17811782deffind_wildcards(self):1783"""Make sure wildcards are valid, and set up internal1784 variables."""17851786 self.ends_triple_dot =False1787# There are three wildcards allowed in p4 views1788# (see "p4 help views"). This code knows how to1789# handle "..." (only at the end), but cannot deal with1790# "%%n" or "*". Only check the depot_side, as p4 should1791# validate that the client_side matches too.1792if re.search(r'%%[1-9]', self.path):1793die("Can't handle%%n wildcards in view:%s"% self.path)1794if self.path.find("*") >=0:1795die("Can't handle * wildcards in view:%s"% self.path)1796 triple_dot_index = self.path.find("...")1797if triple_dot_index >=0:1798if triple_dot_index !=len(self.path) -3:1799die("Can handle only single ... wildcard, at end:%s"%1800 self.path)1801 self.ends_triple_dot =True18021803defensure_compatible(self, other_path):1804"""Make sure the wildcards agree."""1805if self.ends_triple_dot != other_path.ends_triple_dot:1806die("Both paths must end with ... if either does;\n"+1807"paths:%s %s"% (self.path, other_path.path))18081809defmatch_wildcards(self, test_path):1810"""See if this test_path matches us, and fill in the value1811 of the wildcards if so. Returns a tuple of1812 (True|False, wildcards[]). For now, only the ... at end1813 is supported, so at most one wildcard."""1814if self.ends_triple_dot:1815 dotless = self.path[:-3]1816if test_path.startswith(dotless):1817 wildcard = test_path[len(dotless):]1818return(True, [ wildcard ])1819else:1820if test_path == self.path:1821return(True, [])1822return(False, [])18231824defmatch(self, test_path):1825"""Just return if it matches; don't bother with the wildcards."""1826 b, _ = self.match_wildcards(test_path)1827return b18281829deffill_in_wildcards(self, wildcards):1830"""Return the relative path, with the wildcards filled in1831 if there are any."""1832if self.ends_triple_dot:1833return self.path[:-3] + wildcards[0]1834else:1835return self.path18361837classMapping(object):1838def__init__(self, depot_side, client_side, overlay, exclude):1839# depot_side is without the trailing /... if it had one1840 self.depot_side = View.Path(depot_side, is_depot=True)1841 self.client_side = View.Path(client_side, is_depot=False)1842 self.overlay = overlay # started with "+"1843 self.exclude = exclude # started with "-"1844assert not(self.overlay and self.exclude)1845 self.depot_side.ensure_compatible(self.client_side)18461847def__str__(self):1848 c =" "1849if self.overlay:1850 c ="+"1851if self.exclude:1852 c ="-"1853return"View.Mapping:%s%s->%s"% \1854(c, self.depot_side.path, self.client_side.path)18551856defmap_depot_to_client(self, depot_path):1857"""Calculate the client path if using this mapping on the1858 given depot path; does not consider the effect of other1859 mappings in a view. Even excluded mappings are returned."""1860 matches, wildcards = self.depot_side.match_wildcards(depot_path)1861if not matches:1862return""1863 client_path = self.client_side.fill_in_wildcards(wildcards)1864return client_path18651866#1867# View methods1868#1869def__init__(self):1870 self.mappings = []18711872defappend(self, view_line):1873"""Parse a view line, splitting it into depot and client1874 sides. Append to self.mappings, preserving order."""18751876# Split the view line into exactly two words. P4 enforces1877# structure on these lines that simplifies this quite a bit.1878#1879# Either or both words may be double-quoted.1880# Single quotes do not matter.1881# Double-quote marks cannot occur inside the words.1882# A + or - prefix is also inside the quotes.1883# There are no quotes unless they contain a space.1884# The line is already white-space stripped.1885# The two words are separated by a single space.1886#1887if view_line[0] =='"':1888# First word is double quoted. Find its end.1889 close_quote_index = view_line.find('"',1)1890if close_quote_index <=0:1891die("No first-word closing quote found:%s"% view_line)1892 depot_side = view_line[1:close_quote_index]1893# skip closing quote and space1894 rhs_index = close_quote_index +1+11895else:1896 space_index = view_line.find(" ")1897if space_index <=0:1898die("No word-splitting space found:%s"% view_line)1899 depot_side = view_line[0:space_index]1900 rhs_index = space_index +119011902if view_line[rhs_index] =='"':1903# Second word is double quoted. Make sure there is a1904# double quote at the end too.1905if not view_line.endswith('"'):1906die("View line with rhs quote should end with one:%s"%1907 view_line)1908# skip the quotes1909 client_side = view_line[rhs_index+1:-1]1910else:1911 client_side = view_line[rhs_index:]19121913# prefix + means overlay on previous mapping1914 overlay =False1915if depot_side.startswith("+"):1916 overlay =True1917 depot_side = depot_side[1:]19181919# prefix - means exclude this path1920 exclude =False1921if depot_side.startswith("-"):1922 exclude =True1923 depot_side = depot_side[1:]19241925 m = View.Mapping(depot_side, client_side, overlay, exclude)1926 self.mappings.append(m)19271928defmap_in_client(self, depot_path):1929"""Return the relative location in the client where this1930 depot file should live. Returns "" if the file should1931 not be mapped in the client."""19321933 paths_filled = []1934 client_path =""19351936# look at later entries first1937for m in self.mappings[::-1]:19381939# see where will this path end up in the client1940 p = m.map_depot_to_client(depot_path)19411942if p =="":1943# Depot path does not belong in client. Must remember1944# this, as previous items should not cause files to1945# exist in this path either. Remember that the list is1946# being walked from the end, which has higher precedence.1947# Overlap mappings do not exclude previous mappings.1948if not m.overlay:1949 paths_filled.append(m.client_side)19501951else:1952# This mapping matched; no need to search any further.1953# But, the mapping could be rejected if the client path1954# has already been claimed by an earlier mapping (i.e.1955# one later in the list, which we are walking backwards).1956 already_mapped_in_client =False1957for f in paths_filled:1958# this is View.Path.match1959if f.match(p):1960 already_mapped_in_client =True1961break1962if not already_mapped_in_client:1963# Include this file, unless it is from a line that1964# explicitly said to exclude it.1965if not m.exclude:1966 client_path = p19671968# a match, even if rejected, always stops the search1969break19701971return client_path19721973classP4Sync(Command, P4UserMap):1974 delete_actions = ("delete","move/delete","purge")19751976def__init__(self):1977 Command.__init__(self)1978 P4UserMap.__init__(self)1979 self.options = [1980 optparse.make_option("--branch", dest="branch"),1981 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1982 optparse.make_option("--changesfile", dest="changesFile"),1983 optparse.make_option("--silent", dest="silent", action="store_true"),1984 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1985 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1986 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1987help="Import into refs/heads/ , not refs/remotes"),1988 optparse.make_option("--max-changes", dest="maxChanges"),1989 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1990help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1991 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1992help="Only sync files that are included in the Perforce Client Spec")1993]1994 self.description ="""Imports from Perforce into a git repository.\n1995 example:1996 //depot/my/project/ -- to import the current head1997 //depot/my/project/@all -- to import everything1998 //depot/my/project/@1,6 -- to import only from revision 1 to 619992000 (a ... is not needed in the path p4 specification, it's added implicitly)"""20012002 self.usage +=" //depot/path[@revRange]"2003 self.silent =False2004 self.createdBranches =set()2005 self.committedChanges =set()2006 self.branch =""2007 self.detectBranches =False2008 self.detectLabels =False2009 self.importLabels =False2010 self.changesFile =""2011 self.syncWithOrigin =True2012 self.importIntoRemotes =True2013 self.maxChanges =""2014 self.keepRepoPath =False2015 self.depotPaths =None2016 self.p4BranchesInGit = []2017 self.cloneExclude = []2018 self.useClientSpec =False2019 self.useClientSpec_from_options =False2020 self.clientSpecDirs =None2021 self.tempBranches = []2022 self.tempBranchLocation ="git-p4-tmp"20232024ifgitConfig("git-p4.syncFromOrigin") =="false":2025 self.syncWithOrigin =False20262027# Force a checkpoint in fast-import and wait for it to finish2028defcheckpoint(self):2029 self.gitStream.write("checkpoint\n\n")2030 self.gitStream.write("progress checkpoint\n\n")2031 out = self.gitOutput.readline()2032if self.verbose:2033print"checkpoint finished: "+ out20342035defextractFilesFromCommit(self, commit):2036 self.cloneExclude = [re.sub(r"\.\.\.$","", path)2037for path in self.cloneExclude]2038 files = []2039 fnum =02040while commit.has_key("depotFile%s"% fnum):2041 path = commit["depotFile%s"% fnum]20422043if[p for p in self.cloneExclude2044ifp4PathStartsWith(path, p)]:2045 found =False2046else:2047 found = [p for p in self.depotPaths2048ifp4PathStartsWith(path, p)]2049if not found:2050 fnum = fnum +12051continue20522053file= {}2054file["path"] = path2055file["rev"] = commit["rev%s"% fnum]2056file["action"] = commit["action%s"% fnum]2057file["type"] = commit["type%s"% fnum]2058 files.append(file)2059 fnum = fnum +12060return files20612062defstripRepoPath(self, path, prefixes):2063"""When streaming files, this is called to map a p4 depot path2064 to where it should go in git. The prefixes are either2065 self.depotPaths, or self.branchPrefixes in the case of2066 branch detection."""20672068if self.useClientSpec:2069# branch detection moves files up a level (the branch name)2070# from what client spec interpretation gives2071 path = self.clientSpecDirs.map_in_client(path)2072if self.detectBranches:2073for b in self.knownBranches:2074if path.startswith(b +"/"):2075 path = path[len(b)+1:]20762077elif self.keepRepoPath:2078# Preserve everything in relative path name except leading2079# //depot/; just look at first prefix as they all should2080# be in the same depot.2081 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2082ifp4PathStartsWith(path, depot):2083 path = path[len(depot):]20842085else:2086for p in prefixes:2087ifp4PathStartsWith(path, p):2088 path = path[len(p):]2089break20902091 path =wildcard_decode(path)2092return path20932094defsplitFilesIntoBranches(self, commit):2095"""Look at each depotFile in the commit to figure out to what2096 branch it belongs."""20972098 branches = {}2099 fnum =02100while commit.has_key("depotFile%s"% fnum):2101 path = commit["depotFile%s"% fnum]2102 found = [p for p in self.depotPaths2103ifp4PathStartsWith(path, p)]2104if not found:2105 fnum = fnum +12106continue21072108file= {}2109file["path"] = path2110file["rev"] = commit["rev%s"% fnum]2111file["action"] = commit["action%s"% fnum]2112file["type"] = commit["type%s"% fnum]2113 fnum = fnum +121142115# start with the full relative path where this file would2116# go in a p4 client2117if self.useClientSpec:2118 relPath = self.clientSpecDirs.map_in_client(path)2119else:2120 relPath = self.stripRepoPath(path, self.depotPaths)21212122for branch in self.knownBranches.keys():2123# add a trailing slash so that a commit into qt/4.2foo2124# doesn't end up in qt/4.2, e.g.2125if relPath.startswith(branch +"/"):2126if branch not in branches:2127 branches[branch] = []2128 branches[branch].append(file)2129break21302131return branches21322133# output one file from the P4 stream2134# - helper for streamP4Files21352136defstreamOneP4File(self,file, contents):2137 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2138if verbose:2139 sys.stderr.write("%s\n"% relPath)21402141(type_base, type_mods) =split_p4_type(file["type"])21422143 git_mode ="100644"2144if"x"in type_mods:2145 git_mode ="100755"2146if type_base =="symlink":2147 git_mode ="120000"2148# p4 print on a symlink contains "target\n"; remove the newline2149 data =''.join(contents)2150 contents = [data[:-1]]21512152if type_base =="utf16":2153# p4 delivers different text in the python output to -G2154# than it does when using "print -o", or normal p4 client2155# operations. utf16 is converted to ascii or utf8, perhaps.2156# But ascii text saved as -t utf16 is completely mangled.2157# Invoke print -o to get the real contents.2158#2159# On windows, the newlines will always be mangled by print, so put2160# them back too. This is not needed to the cygwin windows version,2161# just the native "NT" type.2162#2163 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2164ifp4_version_string().find("/NT") >=0:2165 text = text.replace("\r\n","\n")2166 contents = [ text ]21672168if type_base =="apple":2169# Apple filetype files will be streamed as a concatenation of2170# its appledouble header and the contents. This is useless2171# on both macs and non-macs. If using "print -q -o xx", it2172# will create "xx" with the data, and "%xx" with the header.2173# This is also not very useful.2174#2175# Ideally, someday, this script can learn how to generate2176# appledouble files directly and import those to git, but2177# non-mac machines can never find a use for apple filetype.2178print"\nIgnoring apple filetype file%s"%file['depotFile']2179return21802181# Note that we do not try to de-mangle keywords on utf16 files,2182# even though in theory somebody may want that.2183 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2184if pattern:2185 regexp = re.compile(pattern, re.VERBOSE)2186 text =''.join(contents)2187 text = regexp.sub(r'$\1$', text)2188 contents = [ text ]21892190 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21912192# total length...2193 length =02194for d in contents:2195 length = length +len(d)21962197 self.gitStream.write("data%d\n"% length)2198for d in contents:2199 self.gitStream.write(d)2200 self.gitStream.write("\n")22012202defstreamOneP4Deletion(self,file):2203 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2204if verbose:2205 sys.stderr.write("delete%s\n"% relPath)2206 self.gitStream.write("D%s\n"% relPath)22072208# handle another chunk of streaming data2209defstreamP4FilesCb(self, marshalled):22102211# catch p4 errors and complain2212 err =None2213if"code"in marshalled:2214if marshalled["code"] =="error":2215if"data"in marshalled:2216 err = marshalled["data"].rstrip()2217if err:2218 f =None2219if self.stream_have_file_info:2220if"depotFile"in self.stream_file:2221 f = self.stream_file["depotFile"]2222# force a failure in fast-import, else an empty2223# commit will be made2224 self.gitStream.write("\n")2225 self.gitStream.write("die-now\n")2226 self.gitStream.close()2227# ignore errors, but make sure it exits first2228 self.importProcess.wait()2229if f:2230die("Error from p4 print for%s:%s"% (f, err))2231else:2232die("Error from p4 print:%s"% err)22332234if marshalled.has_key('depotFile')and self.stream_have_file_info:2235# start of a new file - output the old one first2236 self.streamOneP4File(self.stream_file, self.stream_contents)2237 self.stream_file = {}2238 self.stream_contents = []2239 self.stream_have_file_info =False22402241# pick up the new file information... for the2242# 'data' field we need to append to our array2243for k in marshalled.keys():2244if k =='data':2245 self.stream_contents.append(marshalled['data'])2246else:2247 self.stream_file[k] = marshalled[k]22482249 self.stream_have_file_info =True22502251# Stream directly from "p4 files" into "git fast-import"2252defstreamP4Files(self, files):2253 filesForCommit = []2254 filesToRead = []2255 filesToDelete = []22562257for f in files:2258# if using a client spec, only add the files that have2259# a path in the client2260if self.clientSpecDirs:2261if self.clientSpecDirs.map_in_client(f['path']) =="":2262continue22632264 filesForCommit.append(f)2265if f['action']in self.delete_actions:2266 filesToDelete.append(f)2267else:2268 filesToRead.append(f)22692270# deleted files...2271for f in filesToDelete:2272 self.streamOneP4Deletion(f)22732274iflen(filesToRead) >0:2275 self.stream_file = {}2276 self.stream_contents = []2277 self.stream_have_file_info =False22782279# curry self argument2280defstreamP4FilesCbSelf(entry):2281 self.streamP4FilesCb(entry)22822283 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22842285p4CmdList(["-x","-","print"],2286 stdin=fileArgs,2287 cb=streamP4FilesCbSelf)22882289# do the last chunk2290if self.stream_file.has_key('depotFile'):2291 self.streamOneP4File(self.stream_file, self.stream_contents)22922293defmake_email(self, userid):2294if userid in self.users:2295return self.users[userid]2296else:2297return"%s<a@b>"% userid22982299# Stream a p4 tag2300defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2301if verbose:2302print"writing tag%sfor commit%s"% (labelName, commit)2303 gitStream.write("tag%s\n"% labelName)2304 gitStream.write("from%s\n"% commit)23052306if labelDetails.has_key('Owner'):2307 owner = labelDetails["Owner"]2308else:2309 owner =None23102311# Try to use the owner of the p4 label, or failing that,2312# the current p4 user id.2313if owner:2314 email = self.make_email(owner)2315else:2316 email = self.make_email(self.p4UserId())2317 tagger ="%s %s %s"% (email, epoch, self.tz)23182319 gitStream.write("tagger%s\n"% tagger)23202321print"labelDetails=",labelDetails2322if labelDetails.has_key('Description'):2323 description = labelDetails['Description']2324else:2325 description ='Label from git p4'23262327 gitStream.write("data%d\n"%len(description))2328 gitStream.write(description)2329 gitStream.write("\n")23302331defcommit(self, details, files, branch, parent =""):2332 epoch = details["time"]2333 author = details["user"]23342335if self.verbose:2336print"commit into%s"% branch23372338# start with reading files; if that fails, we should not2339# create a commit.2340 new_files = []2341for f in files:2342if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2343 new_files.append(f)2344else:2345 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])23462347 self.gitStream.write("commit%s\n"% branch)2348# gitStream.write("mark :%s\n" % details["change"])2349 self.committedChanges.add(int(details["change"]))2350 committer =""2351if author not in self.users:2352 self.getUserMapFromPerforceServer()2353 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)23542355 self.gitStream.write("committer%s\n"% committer)23562357 self.gitStream.write("data <<EOT\n")2358 self.gitStream.write(details["desc"])2359 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2360(','.join(self.branchPrefixes), details["change"]))2361iflen(details['options']) >0:2362 self.gitStream.write(": options =%s"% details['options'])2363 self.gitStream.write("]\nEOT\n\n")23642365iflen(parent) >0:2366if self.verbose:2367print"parent%s"% parent2368 self.gitStream.write("from%s\n"% parent)23692370 self.streamP4Files(new_files)2371 self.gitStream.write("\n")23722373 change =int(details["change"])23742375if self.labels.has_key(change):2376 label = self.labels[change]2377 labelDetails = label[0]2378 labelRevisions = label[1]2379if self.verbose:2380print"Change%sis labelled%s"% (change, labelDetails)23812382 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2383for p in self.branchPrefixes])23842385iflen(files) ==len(labelRevisions):23862387 cleanedFiles = {}2388for info in files:2389if info["action"]in self.delete_actions:2390continue2391 cleanedFiles[info["depotFile"]] = info["rev"]23922393if cleanedFiles == labelRevisions:2394 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23952396else:2397if not self.silent:2398print("Tag%sdoes not match with change%s: files do not match."2399% (labelDetails["label"], change))24002401else:2402if not self.silent:2403print("Tag%sdoes not match with change%s: file count is different."2404% (labelDetails["label"], change))24052406# Build a dictionary of changelists and labels, for "detect-labels" option.2407defgetLabels(self):2408 self.labels = {}24092410 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2411iflen(l) >0and not self.silent:2412print"Finding files belonging to labels in%s"% `self.depotPaths`24132414for output in l:2415 label = output["label"]2416 revisions = {}2417 newestChange =02418if self.verbose:2419print"Querying files for label%s"% label2420forfileinp4CmdList(["files"] +2421["%s...@%s"% (p, label)2422for p in self.depotPaths]):2423 revisions[file["depotFile"]] =file["rev"]2424 change =int(file["change"])2425if change > newestChange:2426 newestChange = change24272428 self.labels[newestChange] = [output, revisions]24292430if self.verbose:2431print"Label changes:%s"% self.labels.keys()24322433# Import p4 labels as git tags. A direct mapping does not2434# exist, so assume that if all the files are at the same revision2435# then we can use that, or it's something more complicated we should2436# just ignore.2437defimportP4Labels(self, stream, p4Labels):2438if verbose:2439print"import p4 labels: "+' '.join(p4Labels)24402441 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2442 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2443iflen(validLabelRegexp) ==0:2444 validLabelRegexp = defaultLabelRegexp2445 m = re.compile(validLabelRegexp)24462447for name in p4Labels:2448 commitFound =False24492450if not m.match(name):2451if verbose:2452print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2453continue24542455if name in ignoredP4Labels:2456continue24572458 labelDetails =p4CmdList(['label',"-o", name])[0]24592460# get the most recent changelist for each file in this label2461 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2462for p in self.depotPaths])24632464if change.has_key('change'):2465# find the corresponding git commit; take the oldest commit2466 changelist =int(change['change'])2467 gitCommit =read_pipe(["git","rev-list","--max-count=1",2468"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2469iflen(gitCommit) ==0:2470print"could not find git commit for changelist%d"% changelist2471else:2472 gitCommit = gitCommit.strip()2473 commitFound =True2474# Convert from p4 time format2475try:2476 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2477exceptValueError:2478print"Could not convert label time%s"% labelDetails['Update']2479 tmwhen =124802481 when =int(time.mktime(tmwhen))2482 self.streamTag(stream, name, labelDetails, gitCommit, when)2483if verbose:2484print"p4 label%smapped to git commit%s"% (name, gitCommit)2485else:2486if verbose:2487print"Label%shas no changelists - possibly deleted?"% name24882489if not commitFound:2490# We can't import this label; don't try again as it will get very2491# expensive repeatedly fetching all the files for labels that will2492# never be imported. If the label is moved in the future, the2493# ignore will need to be removed manually.2494system(["git","config","--add","git-p4.ignoredP4Labels", name])24952496defguessProjectName(self):2497for p in self.depotPaths:2498if p.endswith("/"):2499 p = p[:-1]2500 p = p[p.strip().rfind("/") +1:]2501if not p.endswith("/"):2502 p +="/"2503return p25042505defgetBranchMapping(self):2506 lostAndFoundBranches =set()25072508 user =gitConfig("git-p4.branchUser")2509iflen(user) >0:2510 command ="branches -u%s"% user2511else:2512 command ="branches"25132514for info inp4CmdList(command):2515 details =p4Cmd(["branch","-o", info["branch"]])2516 viewIdx =02517while details.has_key("View%s"% viewIdx):2518 paths = details["View%s"% viewIdx].split(" ")2519 viewIdx = viewIdx +12520# require standard //depot/foo/... //depot/bar/... mapping2521iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2522continue2523 source = paths[0]2524 destination = paths[1]2525## HACK2526ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2527 source = source[len(self.depotPaths[0]):-4]2528 destination = destination[len(self.depotPaths[0]):-4]25292530if destination in self.knownBranches:2531if not self.silent:2532print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2533print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2534continue25352536 self.knownBranches[destination] = source25372538 lostAndFoundBranches.discard(destination)25392540if source not in self.knownBranches:2541 lostAndFoundBranches.add(source)25422543# Perforce does not strictly require branches to be defined, so we also2544# check git config for a branch list.2545#2546# Example of branch definition in git config file:2547# [git-p4]2548# branchList=main:branchA2549# branchList=main:branchB2550# branchList=branchA:branchC2551 configBranches =gitConfigList("git-p4.branchList")2552for branch in configBranches:2553if branch:2554(source, destination) = branch.split(":")2555 self.knownBranches[destination] = source25562557 lostAndFoundBranches.discard(destination)25582559if source not in self.knownBranches:2560 lostAndFoundBranches.add(source)256125622563for branch in lostAndFoundBranches:2564 self.knownBranches[branch] = branch25652566defgetBranchMappingFromGitBranches(self):2567 branches =p4BranchesInGit(self.importIntoRemotes)2568for branch in branches.keys():2569if branch =="master":2570 branch ="main"2571else:2572 branch = branch[len(self.projectName):]2573 self.knownBranches[branch] = branch25742575defupdateOptionDict(self, d):2576 option_keys = {}2577if self.keepRepoPath:2578 option_keys['keepRepoPath'] =125792580 d["options"] =' '.join(sorted(option_keys.keys()))25812582defreadOptions(self, d):2583 self.keepRepoPath = (d.has_key('options')2584and('keepRepoPath'in d['options']))25852586defgitRefForBranch(self, branch):2587if branch =="main":2588return self.refPrefix +"master"25892590iflen(branch) <=0:2591return branch25922593return self.refPrefix + self.projectName + branch25942595defgitCommitByP4Change(self, ref, change):2596if self.verbose:2597print"looking in ref "+ ref +" for change%susing bisect..."% change25982599 earliestCommit =""2600 latestCommit =parseRevision(ref)26012602while True:2603if self.verbose:2604print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2605 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2606iflen(next) ==0:2607if self.verbose:2608print"argh"2609return""2610 log =extractLogMessageFromGitCommit(next)2611 settings =extractSettingsGitLog(log)2612 currentChange =int(settings['change'])2613if self.verbose:2614print"current change%s"% currentChange26152616if currentChange == change:2617if self.verbose:2618print"found%s"% next2619return next26202621if currentChange < change:2622 earliestCommit ="^%s"% next2623else:2624 latestCommit ="%s"% next26252626return""26272628defimportNewBranch(self, branch, maxChange):2629# make fast-import flush all changes to disk and update the refs using the checkpoint2630# command so that we can try to find the branch parent in the git history2631 self.gitStream.write("checkpoint\n\n");2632 self.gitStream.flush();2633 branchPrefix = self.depotPaths[0] + branch +"/"2634range="@1,%s"% maxChange2635#print "prefix" + branchPrefix2636 changes =p4ChangesForPaths([branchPrefix],range)2637iflen(changes) <=0:2638return False2639 firstChange = changes[0]2640#print "first change in branch: %s" % firstChange2641 sourceBranch = self.knownBranches[branch]2642 sourceDepotPath = self.depotPaths[0] + sourceBranch2643 sourceRef = self.gitRefForBranch(sourceBranch)2644#print "source " + sourceBranch26452646 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2647#print "branch parent: %s" % branchParentChange2648 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2649iflen(gitParent) >0:2650 self.initialParents[self.gitRefForBranch(branch)] = gitParent2651#print "parent git commit: %s" % gitParent26522653 self.importChanges(changes)2654return True26552656defsearchParent(self, parent, branch, target):2657 parentFound =False2658for blob inread_pipe_lines(["git","rev-list","--reverse",2659"--no-merges", parent]):2660 blob = blob.strip()2661iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2662 parentFound =True2663if self.verbose:2664print"Found parent of%sin commit%s"% (branch, blob)2665break2666if parentFound:2667return blob2668else:2669return None26702671defimportChanges(self, changes):2672 cnt =12673for change in changes:2674 description =p4_describe(change)2675 self.updateOptionDict(description)26762677if not self.silent:2678 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2679 sys.stdout.flush()2680 cnt = cnt +126812682try:2683if self.detectBranches:2684 branches = self.splitFilesIntoBranches(description)2685for branch in branches.keys():2686## HACK --hwn2687 branchPrefix = self.depotPaths[0] + branch +"/"2688 self.branchPrefixes = [ branchPrefix ]26892690 parent =""26912692 filesForCommit = branches[branch]26932694if self.verbose:2695print"branch is%s"% branch26962697 self.updatedBranches.add(branch)26982699if branch not in self.createdBranches:2700 self.createdBranches.add(branch)2701 parent = self.knownBranches[branch]2702if parent == branch:2703 parent =""2704else:2705 fullBranch = self.projectName + branch2706if fullBranch not in self.p4BranchesInGit:2707if not self.silent:2708print("\nImporting new branch%s"% fullBranch);2709if self.importNewBranch(branch, change -1):2710 parent =""2711 self.p4BranchesInGit.append(fullBranch)2712if not self.silent:2713print("\nResuming with change%s"% change);27142715if self.verbose:2716print"parent determined through known branches:%s"% parent27172718 branch = self.gitRefForBranch(branch)2719 parent = self.gitRefForBranch(parent)27202721if self.verbose:2722print"looking for initial parent for%s; current parent is%s"% (branch, parent)27232724iflen(parent) ==0and branch in self.initialParents:2725 parent = self.initialParents[branch]2726del self.initialParents[branch]27272728 blob =None2729iflen(parent) >0:2730 tempBranch ="%s/%d"% (self.tempBranchLocation, change)2731if self.verbose:2732print"Creating temporary branch: "+ tempBranch2733 self.commit(description, filesForCommit, tempBranch)2734 self.tempBranches.append(tempBranch)2735 self.checkpoint()2736 blob = self.searchParent(parent, branch, tempBranch)2737if blob:2738 self.commit(description, filesForCommit, branch, blob)2739else:2740if self.verbose:2741print"Parent of%snot found. Committing into head of%s"% (branch, parent)2742 self.commit(description, filesForCommit, branch, parent)2743else:2744 files = self.extractFilesFromCommit(description)2745 self.commit(description, files, self.branch,2746 self.initialParent)2747# only needed once, to connect to the previous commit2748 self.initialParent =""2749exceptIOError:2750print self.gitError.read()2751 sys.exit(1)27522753defimportHeadRevision(self, revision):2754print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)27552756 details = {}2757 details["user"] ="git perforce import user"2758 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2759% (' '.join(self.depotPaths), revision))2760 details["change"] = revision2761 newestRevision =027622763 fileCnt =02764 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27652766for info inp4CmdList(["files"] + fileArgs):27672768if'code'in info and info['code'] =='error':2769 sys.stderr.write("p4 returned an error:%s\n"2770% info['data'])2771if info['data'].find("must refer to client") >=0:2772 sys.stderr.write("This particular p4 error is misleading.\n")2773 sys.stderr.write("Perhaps the depot path was misspelled.\n");2774 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2775 sys.exit(1)2776if'p4ExitCode'in info:2777 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2778 sys.exit(1)277927802781 change =int(info["change"])2782if change > newestRevision:2783 newestRevision = change27842785if info["action"]in self.delete_actions:2786# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2787#fileCnt = fileCnt + 12788continue27892790for prop in["depotFile","rev","action","type"]:2791 details["%s%s"% (prop, fileCnt)] = info[prop]27922793 fileCnt = fileCnt +127942795 details["change"] = newestRevision27962797# Use time from top-most change so that all git p4 clones of2798# the same p4 repo have the same commit SHA1s.2799 res =p4_describe(newestRevision)2800 details["time"] = res["time"]28012802 self.updateOptionDict(details)2803try:2804 self.commit(details, self.extractFilesFromCommit(details), self.branch)2805exceptIOError:2806print"IO error with git fast-import. Is your git version recent enough?"2807print self.gitError.read()280828092810defrun(self, args):2811 self.depotPaths = []2812 self.changeRange =""2813 self.previousDepotPaths = []2814 self.hasOrigin =False28152816# map from branch depot path to parent branch2817 self.knownBranches = {}2818 self.initialParents = {}28192820if self.importIntoRemotes:2821 self.refPrefix ="refs/remotes/p4/"2822else:2823 self.refPrefix ="refs/heads/p4/"28242825if self.syncWithOrigin:2826 self.hasOrigin =originP4BranchesExist()2827if self.hasOrigin:2828if not self.silent:2829print'Syncing with origin first, using "git fetch origin"'2830system("git fetch origin")28312832 branch_arg_given =bool(self.branch)2833iflen(self.branch) ==0:2834 self.branch = self.refPrefix +"master"2835ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2836system("git update-ref%srefs/heads/p4"% self.branch)2837system("git branch -D p4")28382839# accept either the command-line option, or the configuration variable2840if self.useClientSpec:2841# will use this after clone to set the variable2842 self.useClientSpec_from_options =True2843else:2844ifgitConfigBool("git-p4.useclientspec"):2845 self.useClientSpec =True2846if self.useClientSpec:2847 self.clientSpecDirs =getClientSpec()28482849# TODO: should always look at previous commits,2850# merge with previous imports, if possible.2851if args == []:2852if self.hasOrigin:2853createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)28542855# branches holds mapping from branch name to sha12856 branches =p4BranchesInGit(self.importIntoRemotes)28572858# restrict to just this one, disabling detect-branches2859if branch_arg_given:2860 short = self.branch.split("/")[-1]2861if short in branches:2862 self.p4BranchesInGit = [ short ]2863else:2864 self.p4BranchesInGit = branches.keys()28652866iflen(self.p4BranchesInGit) >1:2867if not self.silent:2868print"Importing from/into multiple branches"2869 self.detectBranches =True2870for branch in branches.keys():2871 self.initialParents[self.refPrefix + branch] = \2872 branches[branch]28732874if self.verbose:2875print"branches:%s"% self.p4BranchesInGit28762877 p4Change =02878for branch in self.p4BranchesInGit:2879 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28802881 settings =extractSettingsGitLog(logMsg)28822883 self.readOptions(settings)2884if(settings.has_key('depot-paths')2885and settings.has_key('change')):2886 change =int(settings['change']) +12887 p4Change =max(p4Change, change)28882889 depotPaths =sorted(settings['depot-paths'])2890if self.previousDepotPaths == []:2891 self.previousDepotPaths = depotPaths2892else:2893 paths = []2894for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2895 prev_list = prev.split("/")2896 cur_list = cur.split("/")2897for i inrange(0,min(len(cur_list),len(prev_list))):2898if cur_list[i] <> prev_list[i]:2899 i = i -12900break29012902 paths.append("/".join(cur_list[:i +1]))29032904 self.previousDepotPaths = paths29052906if p4Change >0:2907 self.depotPaths =sorted(self.previousDepotPaths)2908 self.changeRange ="@%s,#head"% p4Change2909if not self.silent and not self.detectBranches:2910print"Performing incremental import into%sgit branch"% self.branch29112912# accept multiple ref name abbreviations:2913# refs/foo/bar/branch -> use it exactly2914# p4/branch -> prepend refs/remotes/ or refs/heads/2915# branch -> prepend refs/remotes/p4/ or refs/heads/p4/2916if not self.branch.startswith("refs/"):2917if self.importIntoRemotes:2918 prepend ="refs/remotes/"2919else:2920 prepend ="refs/heads/"2921if not self.branch.startswith("p4/"):2922 prepend +="p4/"2923 self.branch = prepend + self.branch29242925iflen(args) ==0and self.depotPaths:2926if not self.silent:2927print"Depot paths:%s"%' '.join(self.depotPaths)2928else:2929if self.depotPaths and self.depotPaths != args:2930print("previous import used depot path%sand now%swas specified. "2931"This doesn't work!"% (' '.join(self.depotPaths),2932' '.join(args)))2933 sys.exit(1)29342935 self.depotPaths =sorted(args)29362937 revision =""2938 self.users = {}29392940# Make sure no revision specifiers are used when --changesfile2941# is specified.2942 bad_changesfile =False2943iflen(self.changesFile) >0:2944for p in self.depotPaths:2945if p.find("@") >=0or p.find("#") >=0:2946 bad_changesfile =True2947break2948if bad_changesfile:2949die("Option --changesfile is incompatible with revision specifiers")29502951 newPaths = []2952for p in self.depotPaths:2953if p.find("@") != -1:2954 atIdx = p.index("@")2955 self.changeRange = p[atIdx:]2956if self.changeRange =="@all":2957 self.changeRange =""2958elif','not in self.changeRange:2959 revision = self.changeRange2960 self.changeRange =""2961 p = p[:atIdx]2962elif p.find("#") != -1:2963 hashIdx = p.index("#")2964 revision = p[hashIdx:]2965 p = p[:hashIdx]2966elif self.previousDepotPaths == []:2967# pay attention to changesfile, if given, else import2968# the entire p4 tree at the head revision2969iflen(self.changesFile) ==0:2970 revision ="#head"29712972 p = re.sub("\.\.\.$","", p)2973if not p.endswith("/"):2974 p +="/"29752976 newPaths.append(p)29772978 self.depotPaths = newPaths29792980# --detect-branches may change this for each branch2981 self.branchPrefixes = self.depotPaths29822983 self.loadUserMapFromCache()2984 self.labels = {}2985if self.detectLabels:2986 self.getLabels();29872988if self.detectBranches:2989## FIXME - what's a P4 projectName ?2990 self.projectName = self.guessProjectName()29912992if self.hasOrigin:2993 self.getBranchMappingFromGitBranches()2994else:2995 self.getBranchMapping()2996if self.verbose:2997print"p4-git branches:%s"% self.p4BranchesInGit2998print"initial parents:%s"% self.initialParents2999for b in self.p4BranchesInGit:3000if b !="master":30013002## FIXME3003 b = b[len(self.projectName):]3004 self.createdBranches.add(b)30053006 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))30073008 self.importProcess = subprocess.Popen(["git","fast-import"],3009 stdin=subprocess.PIPE,3010 stdout=subprocess.PIPE,3011 stderr=subprocess.PIPE);3012 self.gitOutput = self.importProcess.stdout3013 self.gitStream = self.importProcess.stdin3014 self.gitError = self.importProcess.stderr30153016if revision:3017 self.importHeadRevision(revision)3018else:3019 changes = []30203021iflen(self.changesFile) >0:3022 output =open(self.changesFile).readlines()3023 changeSet =set()3024for line in output:3025 changeSet.add(int(line))30263027for change in changeSet:3028 changes.append(change)30293030 changes.sort()3031else:3032# catch "git p4 sync" with no new branches, in a repo that3033# does not have any existing p4 branches3034iflen(args) ==0:3035if not self.p4BranchesInGit:3036die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.")30373038# The default branch is master, unless --branch is used to3039# specify something else. Make sure it exists, or complain3040# nicely about how to use --branch.3041if not self.detectBranches:3042if notbranch_exists(self.branch):3043if branch_arg_given:3044die("Error: branch%sdoes not exist."% self.branch)3045else:3046die("Error: no branch%s; perhaps specify one with --branch."%3047 self.branch)30483049if self.verbose:3050print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),3051 self.changeRange)3052 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)30533054iflen(self.maxChanges) >0:3055 changes = changes[:min(int(self.maxChanges),len(changes))]30563057iflen(changes) ==0:3058if not self.silent:3059print"No changes to import!"3060else:3061if not self.silent and not self.detectBranches:3062print"Import destination:%s"% self.branch30633064 self.updatedBranches =set()30653066if not self.detectBranches:3067if args:3068# start a new branch3069 self.initialParent =""3070else:3071# build on a previous revision3072 self.initialParent =parseRevision(self.branch)30733074 self.importChanges(changes)30753076if not self.silent:3077print""3078iflen(self.updatedBranches) >0:3079 sys.stdout.write("Updated branches: ")3080for b in self.updatedBranches:3081 sys.stdout.write("%s"% b)3082 sys.stdout.write("\n")30833084ifgitConfigBool("git-p4.importLabels"):3085 self.importLabels =True30863087if self.importLabels:3088 p4Labels =getP4Labels(self.depotPaths)3089 gitTags =getGitTags()30903091 missingP4Labels = p4Labels - gitTags3092 self.importP4Labels(self.gitStream, missingP4Labels)30933094 self.gitStream.close()3095if self.importProcess.wait() !=0:3096die("fast-import failed:%s"% self.gitError.read())3097 self.gitOutput.close()3098 self.gitError.close()30993100# Cleanup temporary branches created during import3101if self.tempBranches != []:3102for branch in self.tempBranches:3103read_pipe("git update-ref -d%s"% branch)3104 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))31053106# Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow3107# a convenient shortcut refname "p4".3108if self.importIntoRemotes:3109 head_ref = self.refPrefix +"HEAD"3110if notgitBranchExists(head_ref)andgitBranchExists(self.branch):3111system(["git","symbolic-ref", head_ref, self.branch])31123113return True31143115classP4Rebase(Command):3116def__init__(self):3117 Command.__init__(self)3118 self.options = [3119 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3120]3121 self.importLabels =False3122 self.description = ("Fetches the latest revision from perforce and "3123+"rebases the current work (branch) against it")31243125defrun(self, args):3126 sync =P4Sync()3127 sync.importLabels = self.importLabels3128 sync.run([])31293130return self.rebase()31313132defrebase(self):3133if os.system("git update-index --refresh") !=0:3134die("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.");3135iflen(read_pipe("git diff-index HEAD --")) >0:3136die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");31373138[upstream, settings] =findUpstreamBranchPoint()3139iflen(upstream) ==0:3140die("Cannot find upstream branchpoint for rebase")31413142# the branchpoint may be p4/foo~3, so strip off the parent3143 upstream = re.sub("~[0-9]+$","", upstream)31443145print"Rebasing the current branch onto%s"% upstream3146 oldHead =read_pipe("git rev-parse HEAD").strip()3147system("git rebase%s"% upstream)3148system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3149return True31503151classP4Clone(P4Sync):3152def__init__(self):3153 P4Sync.__init__(self)3154 self.description ="Creates a new git repository and imports from Perforce into it"3155 self.usage ="usage: %prog [options] //depot/path[@revRange]"3156 self.options += [3157 optparse.make_option("--destination", dest="cloneDestination",3158 action='store', default=None,3159help="where to leave result of the clone"),3160 optparse.make_option("-/", dest="cloneExclude",3161 action="append",type="string",3162help="exclude depot path"),3163 optparse.make_option("--bare", dest="cloneBare",3164 action="store_true", default=False),3165]3166 self.cloneDestination =None3167 self.needsGit =False3168 self.cloneBare =False31693170# This is required for the "append" cloneExclude action3171defensure_value(self, attr, value):3172if nothasattr(self, attr)orgetattr(self, attr)is None:3173setattr(self, attr, value)3174returngetattr(self, attr)31753176defdefaultDestination(self, args):3177## TODO: use common prefix of args?3178 depotPath = args[0]3179 depotDir = re.sub("(@[^@]*)$","", depotPath)3180 depotDir = re.sub("(#[^#]*)$","", depotDir)3181 depotDir = re.sub(r"\.\.\.$","", depotDir)3182 depotDir = re.sub(r"/$","", depotDir)3183return os.path.split(depotDir)[1]31843185defrun(self, args):3186iflen(args) <1:3187return False31883189if self.keepRepoPath and not self.cloneDestination:3190 sys.stderr.write("Must specify destination for --keep-path\n")3191 sys.exit(1)31923193 depotPaths = args31943195if not self.cloneDestination andlen(depotPaths) >1:3196 self.cloneDestination = depotPaths[-1]3197 depotPaths = depotPaths[:-1]31983199 self.cloneExclude = ["/"+p for p in self.cloneExclude]3200for p in depotPaths:3201if not p.startswith("//"):3202 sys.stderr.write('Depot paths must start with "//":%s\n'% p)3203return False32043205if not self.cloneDestination:3206 self.cloneDestination = self.defaultDestination(args)32073208print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)32093210if not os.path.exists(self.cloneDestination):3211 os.makedirs(self.cloneDestination)3212chdir(self.cloneDestination)32133214 init_cmd = ["git","init"]3215if self.cloneBare:3216 init_cmd.append("--bare")3217 subprocess.check_call(init_cmd)32183219if not P4Sync.run(self, depotPaths):3220return False32213222# create a master branch and check out a work tree3223ifgitBranchExists(self.branch):3224system(["git","branch","master", self.branch ])3225if not self.cloneBare:3226system(["git","checkout","-f"])3227else:3228print'Not checking out any branch, use ' \3229'"git checkout -q -b master <branch>"'32303231# auto-set this variable if invoked with --use-client-spec3232if self.useClientSpec_from_options:3233system("git config --bool git-p4.useclientspec true")32343235return True32363237classP4Branches(Command):3238def__init__(self):3239 Command.__init__(self)3240 self.options = [ ]3241 self.description = ("Shows the git branches that hold imports and their "3242+"corresponding perforce depot paths")3243 self.verbose =False32443245defrun(self, args):3246iforiginP4BranchesExist():3247createOrUpdateBranchesFromOrigin()32483249 cmdline ="git rev-parse --symbolic "3250 cmdline +=" --remotes"32513252for line inread_pipe_lines(cmdline):3253 line = line.strip()32543255if not line.startswith('p4/')or line =="p4/HEAD":3256continue3257 branch = line32583259 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3260 settings =extractSettingsGitLog(log)32613262print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3263return True32643265classHelpFormatter(optparse.IndentedHelpFormatter):3266def__init__(self):3267 optparse.IndentedHelpFormatter.__init__(self)32683269defformat_description(self, description):3270if description:3271return description +"\n"3272else:3273return""32743275defprintUsage(commands):3276print"usage:%s<command> [options]"% sys.argv[0]3277print""3278print"valid commands:%s"%", ".join(commands)3279print""3280print"Try%s<command> --help for command specific help."% sys.argv[0]3281print""32823283commands = {3284"debug": P4Debug,3285"submit": P4Submit,3286"commit": P4Submit,3287"sync": P4Sync,3288"rebase": P4Rebase,3289"clone": P4Clone,3290"rollback": P4RollBack,3291"branches": P4Branches3292}329332943295defmain():3296iflen(sys.argv[1:]) ==0:3297printUsage(commands.keys())3298 sys.exit(2)32993300 cmdName = sys.argv[1]3301try:3302 klass = commands[cmdName]3303 cmd =klass()3304exceptKeyError:3305print"unknown command%s"% cmdName3306print""3307printUsage(commands.keys())3308 sys.exit(2)33093310 options = cmd.options3311 cmd.gitdir = os.environ.get("GIT_DIR",None)33123313 args = sys.argv[2:]33143315 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3316if cmd.needsGit:3317 options.append(optparse.make_option("--git-dir", dest="gitdir"))33183319 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3320 options,3321 description = cmd.description,3322 formatter =HelpFormatter())33233324(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3325global verbose3326 verbose = cmd.verbose3327if cmd.needsGit:3328if cmd.gitdir ==None:3329 cmd.gitdir = os.path.abspath(".git")3330if notisValidGitDir(cmd.gitdir):3331 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3332if os.path.exists(cmd.gitdir):3333 cdup =read_pipe("git rev-parse --show-cdup").strip()3334iflen(cdup) >0:3335chdir(cdup);33363337if notisValidGitDir(cmd.gitdir):3338ifisValidGitDir(cmd.gitdir +"/.git"):3339 cmd.gitdir +="/.git"3340else:3341die("fatal: cannot locate git repository at%s"% cmd.gitdir)33423343 os.environ["GIT_DIR"] = cmd.gitdir33443345if not cmd.run(args):3346 parser.print_help()3347 sys.exit(2)334833493350if __name__ =='__main__':3351main()