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# 10 11import sys 12if sys.hexversion <0x02040000: 13# The limiter is the subprocess module 14 sys.stderr.write("git-p4: requires Python 2.4 or later.\n") 15 sys.exit(1) 16 17import optparse, os, marshal, subprocess, shelve 18import tempfile, getopt, os.path, time, platform 19import re, shutil 20 21verbose =False 22 23# Only labels/tags matching this will be imported/exported 24defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 25 26defp4_build_cmd(cmd): 27"""Build a suitable p4 command line. 28 29 This consolidates building and returning a p4 command line into one 30 location. It means that hooking into the environment, or other configuration 31 can be done more easily. 32 """ 33 real_cmd = ["p4"] 34 35 user =gitConfig("git-p4.user") 36iflen(user) >0: 37 real_cmd += ["-u",user] 38 39 password =gitConfig("git-p4.password") 40iflen(password) >0: 41 real_cmd += ["-P", password] 42 43 port =gitConfig("git-p4.port") 44iflen(port) >0: 45 real_cmd += ["-p", port] 46 47 host =gitConfig("git-p4.host") 48iflen(host) >0: 49 real_cmd += ["-H", host] 50 51 client =gitConfig("git-p4.client") 52iflen(client) >0: 53 real_cmd += ["-c", client] 54 55 56ifisinstance(cmd,basestring): 57 real_cmd =' '.join(real_cmd) +' '+ cmd 58else: 59 real_cmd += cmd 60return real_cmd 61 62defchdir(dir): 63# P4 uses the PWD environment variable rather than getcwd(). Since we're 64# not using the shell, we have to set it ourselves. This path could 65# be relative, so go there first, then figure out where we ended up. 66 os.chdir(dir) 67 os.environ['PWD'] = os.getcwd() 68 69defdie(msg): 70if verbose: 71raiseException(msg) 72else: 73 sys.stderr.write(msg +"\n") 74 sys.exit(1) 75 76defwrite_pipe(c, stdin): 77if verbose: 78 sys.stderr.write('Writing pipe:%s\n'%str(c)) 79 80 expand =isinstance(c,basestring) 81 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 82 pipe = p.stdin 83 val = pipe.write(stdin) 84 pipe.close() 85if p.wait(): 86die('Command failed:%s'%str(c)) 87 88return val 89 90defp4_write_pipe(c, stdin): 91 real_cmd =p4_build_cmd(c) 92returnwrite_pipe(real_cmd, stdin) 93 94defread_pipe(c, ignore_error=False): 95if verbose: 96 sys.stderr.write('Reading pipe:%s\n'%str(c)) 97 98 expand =isinstance(c,basestring) 99 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 100 pipe = p.stdout 101 val = pipe.read() 102if p.wait()and not ignore_error: 103die('Command failed:%s'%str(c)) 104 105return val 106 107defp4_read_pipe(c, ignore_error=False): 108 real_cmd =p4_build_cmd(c) 109returnread_pipe(real_cmd, ignore_error) 110 111defread_pipe_lines(c): 112if verbose: 113 sys.stderr.write('Reading pipe:%s\n'%str(c)) 114 115 expand =isinstance(c, basestring) 116 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 117 pipe = p.stdout 118 val = pipe.readlines() 119if pipe.close()or p.wait(): 120die('Command failed:%s'%str(c)) 121 122return val 123 124defp4_read_pipe_lines(c): 125"""Specifically invoke p4 on the command supplied. """ 126 real_cmd =p4_build_cmd(c) 127returnread_pipe_lines(real_cmd) 128 129defp4_has_command(cmd): 130"""Ask p4 for help on this command. If it returns an error, the 131 command does not exist in this version of p4.""" 132 real_cmd =p4_build_cmd(["help", cmd]) 133 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 134 stderr=subprocess.PIPE) 135 p.communicate() 136return p.returncode ==0 137 138defp4_has_move_command(): 139"""See if the move command exists, that it supports -k, and that 140 it has not been administratively disabled. The arguments 141 must be correct, but the filenames do not have to exist. Use 142 ones with wildcards so even if they exist, it will fail.""" 143 144if notp4_has_command("move"): 145return False 146 cmd =p4_build_cmd(["move","-k","@from","@to"]) 147 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 148(out, err) = p.communicate() 149# return code will be 1 in either case 150if err.find("Invalid option") >=0: 151return False 152if err.find("disabled") >=0: 153return False 154# assume it failed because @... was invalid changelist 155return True 156 157defsystem(cmd): 158 expand =isinstance(cmd,basestring) 159if verbose: 160 sys.stderr.write("executing%s\n"%str(cmd)) 161 subprocess.check_call(cmd, shell=expand) 162 163defp4_system(cmd): 164"""Specifically invoke p4 as the system command. """ 165 real_cmd =p4_build_cmd(cmd) 166 expand =isinstance(real_cmd, basestring) 167 subprocess.check_call(real_cmd, shell=expand) 168 169defp4_integrate(src, dest): 170p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 171 172defp4_sync(f, *options): 173p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 174 175defp4_add(f): 176# forcibly add file names with wildcards 177ifwildcard_present(f): 178p4_system(["add","-f", f]) 179else: 180p4_system(["add", f]) 181 182defp4_delete(f): 183p4_system(["delete",wildcard_encode(f)]) 184 185defp4_edit(f): 186p4_system(["edit",wildcard_encode(f)]) 187 188defp4_revert(f): 189p4_system(["revert",wildcard_encode(f)]) 190 191defp4_reopen(type, f): 192p4_system(["reopen","-t",type,wildcard_encode(f)]) 193 194defp4_move(src, dest): 195p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 196 197defp4_describe(change): 198"""Make sure it returns a valid result by checking for 199 the presence of field "time". Return a dict of the 200 results.""" 201 202 ds =p4CmdList(["describe","-s",str(change)]) 203iflen(ds) !=1: 204die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 205 206 d = ds[0] 207 208if"p4ExitCode"in d: 209die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 210str(d))) 211if"code"in d: 212if d["code"] =="error": 213die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 214 215if"time"not in d: 216die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 217 218return d 219 220# 221# Canonicalize the p4 type and return a tuple of the 222# base type, plus any modifiers. See "p4 help filetypes" 223# for a list and explanation. 224# 225defsplit_p4_type(p4type): 226 227 p4_filetypes_historical = { 228"ctempobj":"binary+Sw", 229"ctext":"text+C", 230"cxtext":"text+Cx", 231"ktext":"text+k", 232"kxtext":"text+kx", 233"ltext":"text+F", 234"tempobj":"binary+FSw", 235"ubinary":"binary+F", 236"uresource":"resource+F", 237"uxbinary":"binary+Fx", 238"xbinary":"binary+x", 239"xltext":"text+Fx", 240"xtempobj":"binary+Swx", 241"xtext":"text+x", 242"xunicode":"unicode+x", 243"xutf16":"utf16+x", 244} 245if p4type in p4_filetypes_historical: 246 p4type = p4_filetypes_historical[p4type] 247 mods ="" 248 s = p4type.split("+") 249 base = s[0] 250 mods ="" 251iflen(s) >1: 252 mods = s[1] 253return(base, mods) 254 255# 256# return the raw p4 type of a file (text, text+ko, etc) 257# 258defp4_type(file): 259 results =p4CmdList(["fstat","-T","headType",file]) 260return results[0]['headType'] 261 262# 263# Given a type base and modifier, return a regexp matching 264# the keywords that can be expanded in the file 265# 266defp4_keywords_regexp_for_type(base, type_mods): 267if base in("text","unicode","binary"): 268 kwords =None 269if"ko"in type_mods: 270 kwords ='Id|Header' 271elif"k"in type_mods: 272 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 273else: 274return None 275 pattern = r""" 276 \$ # Starts with a dollar, followed by... 277 (%s) # one of the keywords, followed by... 278 (:[^$\n]+)? # possibly an old expansion, followed by... 279 \$ # another dollar 280 """% kwords 281return pattern 282else: 283return None 284 285# 286# Given a file, return a regexp matching the possible 287# RCS keywords that will be expanded, or None for files 288# with kw expansion turned off. 289# 290defp4_keywords_regexp_for_file(file): 291if not os.path.exists(file): 292return None 293else: 294(type_base, type_mods) =split_p4_type(p4_type(file)) 295returnp4_keywords_regexp_for_type(type_base, type_mods) 296 297defsetP4ExecBit(file, mode): 298# Reopens an already open file and changes the execute bit to match 299# the execute bit setting in the passed in mode. 300 301 p4Type ="+x" 302 303if notisModeExec(mode): 304 p4Type =getP4OpenedType(file) 305 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 306 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 307if p4Type[-1] =="+": 308 p4Type = p4Type[0:-1] 309 310p4_reopen(p4Type,file) 311 312defgetP4OpenedType(file): 313# Returns the perforce file type for the given file. 314 315 result =p4_read_pipe(["opened",wildcard_encode(file)]) 316 match = re.match(".*\((.+)\)\r?$", result) 317if match: 318return match.group(1) 319else: 320die("Could not determine file type for%s(result: '%s')"% (file, result)) 321 322# Return the set of all p4 labels 323defgetP4Labels(depotPaths): 324 labels =set() 325ifisinstance(depotPaths,basestring): 326 depotPaths = [depotPaths] 327 328for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 329 label = l['label'] 330 labels.add(label) 331 332return labels 333 334# Return the set of all git tags 335defgetGitTags(): 336 gitTags =set() 337for line inread_pipe_lines(["git","tag"]): 338 tag = line.strip() 339 gitTags.add(tag) 340return gitTags 341 342defdiffTreePattern(): 343# This is a simple generator for the diff tree regex pattern. This could be 344# a class variable if this and parseDiffTreeEntry were a part of a class. 345 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 346while True: 347yield pattern 348 349defparseDiffTreeEntry(entry): 350"""Parses a single diff tree entry into its component elements. 351 352 See git-diff-tree(1) manpage for details about the format of the diff 353 output. This method returns a dictionary with the following elements: 354 355 src_mode - The mode of the source file 356 dst_mode - The mode of the destination file 357 src_sha1 - The sha1 for the source file 358 dst_sha1 - The sha1 fr the destination file 359 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 360 status_score - The score for the status (applicable for 'C' and 'R' 361 statuses). This is None if there is no score. 362 src - The path for the source file. 363 dst - The path for the destination file. This is only present for 364 copy or renames. If it is not present, this is None. 365 366 If the pattern is not matched, None is returned.""" 367 368 match =diffTreePattern().next().match(entry) 369if match: 370return{ 371'src_mode': match.group(1), 372'dst_mode': match.group(2), 373'src_sha1': match.group(3), 374'dst_sha1': match.group(4), 375'status': match.group(5), 376'status_score': match.group(6), 377'src': match.group(7), 378'dst': match.group(10) 379} 380return None 381 382defisModeExec(mode): 383# Returns True if the given git mode represents an executable file, 384# otherwise False. 385return mode[-3:] =="755" 386 387defisModeExecChanged(src_mode, dst_mode): 388returnisModeExec(src_mode) !=isModeExec(dst_mode) 389 390defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 391 392ifisinstance(cmd,basestring): 393 cmd ="-G "+ cmd 394 expand =True 395else: 396 cmd = ["-G"] + cmd 397 expand =False 398 399 cmd =p4_build_cmd(cmd) 400if verbose: 401 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 402 403# Use a temporary file to avoid deadlocks without 404# subprocess.communicate(), which would put another copy 405# of stdout into memory. 406 stdin_file =None 407if stdin is not None: 408 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 409ifisinstance(stdin,basestring): 410 stdin_file.write(stdin) 411else: 412for i in stdin: 413 stdin_file.write(i +'\n') 414 stdin_file.flush() 415 stdin_file.seek(0) 416 417 p4 = subprocess.Popen(cmd, 418 shell=expand, 419 stdin=stdin_file, 420 stdout=subprocess.PIPE) 421 422 result = [] 423try: 424while True: 425 entry = marshal.load(p4.stdout) 426if cb is not None: 427cb(entry) 428else: 429 result.append(entry) 430exceptEOFError: 431pass 432 exitCode = p4.wait() 433if exitCode !=0: 434 entry = {} 435 entry["p4ExitCode"] = exitCode 436 result.append(entry) 437 438return result 439 440defp4Cmd(cmd): 441list=p4CmdList(cmd) 442 result = {} 443for entry inlist: 444 result.update(entry) 445return result; 446 447defp4Where(depotPath): 448if not depotPath.endswith("/"): 449 depotPath +="/" 450 depotPath = depotPath +"..." 451 outputList =p4CmdList(["where", depotPath]) 452 output =None 453for entry in outputList: 454if"depotFile"in entry: 455if entry["depotFile"] == depotPath: 456 output = entry 457break 458elif"data"in entry: 459 data = entry.get("data") 460 space = data.find(" ") 461if data[:space] == depotPath: 462 output = entry 463break 464if output ==None: 465return"" 466if output["code"] =="error": 467return"" 468 clientPath ="" 469if"path"in output: 470 clientPath = output.get("path") 471elif"data"in output: 472 data = output.get("data") 473 lastSpace = data.rfind(" ") 474 clientPath = data[lastSpace +1:] 475 476if clientPath.endswith("..."): 477 clientPath = clientPath[:-3] 478return clientPath 479 480defcurrentGitBranch(): 481returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 482 483defisValidGitDir(path): 484if(os.path.exists(path +"/HEAD") 485and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 486return True; 487return False 488 489defparseRevision(ref): 490returnread_pipe("git rev-parse%s"% ref).strip() 491 492defbranchExists(ref): 493 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 494 ignore_error=True) 495returnlen(rev) >0 496 497defextractLogMessageFromGitCommit(commit): 498 logMessage ="" 499 500## fixme: title is first line of commit, not 1st paragraph. 501 foundTitle =False 502for log inread_pipe_lines("git cat-file commit%s"% commit): 503if not foundTitle: 504iflen(log) ==1: 505 foundTitle =True 506continue 507 508 logMessage += log 509return logMessage 510 511defextractSettingsGitLog(log): 512 values = {} 513for line in log.split("\n"): 514 line = line.strip() 515 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 516if not m: 517continue 518 519 assignments = m.group(1).split(':') 520for a in assignments: 521 vals = a.split('=') 522 key = vals[0].strip() 523 val = ('='.join(vals[1:])).strip() 524if val.endswith('\"')and val.startswith('"'): 525 val = val[1:-1] 526 527 values[key] = val 528 529 paths = values.get("depot-paths") 530if not paths: 531 paths = values.get("depot-path") 532if paths: 533 values['depot-paths'] = paths.split(',') 534return values 535 536defgitBranchExists(branch): 537 proc = subprocess.Popen(["git","rev-parse", branch], 538 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 539return proc.wait() ==0; 540 541_gitConfig = {} 542defgitConfig(key, args =None):# set args to "--bool", for instance 543if not _gitConfig.has_key(key): 544 argsFilter ="" 545if args !=None: 546 argsFilter ="%s"% args 547 cmd ="git config%s%s"% (argsFilter, key) 548 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 549return _gitConfig[key] 550 551defgitConfigList(key): 552if not _gitConfig.has_key(key): 553 _gitConfig[key] =read_pipe("git config --get-all%s"% key, ignore_error=True).strip().split(os.linesep) 554return _gitConfig[key] 555 556defp4BranchesInGit(branchesAreInRemotes =True): 557 branches = {} 558 559 cmdline ="git rev-parse --symbolic " 560if branchesAreInRemotes: 561 cmdline +=" --remotes" 562else: 563 cmdline +=" --branches" 564 565for line inread_pipe_lines(cmdline): 566 line = line.strip() 567 568## only import to p4/ 569if not line.startswith('p4/')or line =="p4/HEAD": 570continue 571 branch = line 572 573# strip off p4 574 branch = re.sub("^p4/","", line) 575 576 branches[branch] =parseRevision(line) 577return branches 578 579deffindUpstreamBranchPoint(head ="HEAD"): 580 branches =p4BranchesInGit() 581# map from depot-path to branch name 582 branchByDepotPath = {} 583for branch in branches.keys(): 584 tip = branches[branch] 585 log =extractLogMessageFromGitCommit(tip) 586 settings =extractSettingsGitLog(log) 587if settings.has_key("depot-paths"): 588 paths =",".join(settings["depot-paths"]) 589 branchByDepotPath[paths] ="remotes/p4/"+ branch 590 591 settings =None 592 parent =0 593while parent <65535: 594 commit = head +"~%s"% parent 595 log =extractLogMessageFromGitCommit(commit) 596 settings =extractSettingsGitLog(log) 597if settings.has_key("depot-paths"): 598 paths =",".join(settings["depot-paths"]) 599if branchByDepotPath.has_key(paths): 600return[branchByDepotPath[paths], settings] 601 602 parent = parent +1 603 604return["", settings] 605 606defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 607if not silent: 608print("Creating/updating branch(es) in%sbased on origin branch(es)" 609% localRefPrefix) 610 611 originPrefix ="origin/p4/" 612 613for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 614 line = line.strip() 615if(not line.startswith(originPrefix))or line.endswith("HEAD"): 616continue 617 618 headName = line[len(originPrefix):] 619 remoteHead = localRefPrefix + headName 620 originHead = line 621 622 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 623if(not original.has_key('depot-paths') 624or not original.has_key('change')): 625continue 626 627 update =False 628if notgitBranchExists(remoteHead): 629if verbose: 630print"creating%s"% remoteHead 631 update =True 632else: 633 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 634if settings.has_key('change') >0: 635if settings['depot-paths'] == original['depot-paths']: 636 originP4Change =int(original['change']) 637 p4Change =int(settings['change']) 638if originP4Change > p4Change: 639print("%s(%s) is newer than%s(%s). " 640"Updating p4 branch from origin." 641% (originHead, originP4Change, 642 remoteHead, p4Change)) 643 update =True 644else: 645print("Ignoring:%swas imported from%swhile " 646"%swas imported from%s" 647% (originHead,','.join(original['depot-paths']), 648 remoteHead,','.join(settings['depot-paths']))) 649 650if update: 651system("git update-ref%s %s"% (remoteHead, originHead)) 652 653deforiginP4BranchesExist(): 654returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 655 656defp4ChangesForPaths(depotPaths, changeRange): 657assert depotPaths 658 cmd = ['changes'] 659for p in depotPaths: 660 cmd += ["%s...%s"% (p, changeRange)] 661 output =p4_read_pipe_lines(cmd) 662 663 changes = {} 664for line in output: 665 changeNum =int(line.split(" ")[1]) 666 changes[changeNum] =True 667 668 changelist = changes.keys() 669 changelist.sort() 670return changelist 671 672defp4PathStartsWith(path, prefix): 673# This method tries to remedy a potential mixed-case issue: 674# 675# If UserA adds //depot/DirA/file1 676# and UserB adds //depot/dira/file2 677# 678# we may or may not have a problem. If you have core.ignorecase=true, 679# we treat DirA and dira as the same directory 680 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 681if ignorecase: 682return path.lower().startswith(prefix.lower()) 683return path.startswith(prefix) 684 685defgetClientSpec(): 686"""Look at the p4 client spec, create a View() object that contains 687 all the mappings, and return it.""" 688 689 specList =p4CmdList("client -o") 690iflen(specList) !=1: 691die('Output from "client -o" is%dlines, expecting 1'% 692len(specList)) 693 694# dictionary of all client parameters 695 entry = specList[0] 696 697# just the keys that start with "View" 698 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 699 700# hold this new View 701 view =View() 702 703# append the lines, in order, to the view 704for view_num inrange(len(view_keys)): 705 k ="View%d"% view_num 706if k not in view_keys: 707die("Expected view key%smissing"% k) 708 view.append(entry[k]) 709 710return view 711 712defgetClientRoot(): 713"""Grab the client directory.""" 714 715 output =p4CmdList("client -o") 716iflen(output) !=1: 717die('Output from "client -o" is%dlines, expecting 1'%len(output)) 718 719 entry = output[0] 720if"Root"not in entry: 721die('Client has no "Root"') 722 723return entry["Root"] 724 725# 726# P4 wildcards are not allowed in filenames. P4 complains 727# if you simply add them, but you can force it with "-f", in 728# which case it translates them into %xx encoding internally. 729# 730defwildcard_decode(path): 731# Search for and fix just these four characters. Do % last so 732# that fixing it does not inadvertently create new %-escapes. 733# Cannot have * in a filename in windows; untested as to 734# what p4 would do in such a case. 735if not platform.system() =="Windows": 736 path = path.replace("%2A","*") 737 path = path.replace("%23","#") \ 738.replace("%40","@") \ 739.replace("%25","%") 740return path 741 742defwildcard_encode(path): 743# do % first to avoid double-encoding the %s introduced here 744 path = path.replace("%","%25") \ 745.replace("*","%2A") \ 746.replace("#","%23") \ 747.replace("@","%40") 748return path 749 750defwildcard_present(path): 751return path.translate(None,"*#@%") != path 752 753class Command: 754def__init__(self): 755 self.usage ="usage: %prog [options]" 756 self.needsGit =True 757 self.verbose =False 758 759class P4UserMap: 760def__init__(self): 761 self.userMapFromPerforceServer =False 762 self.myP4UserId =None 763 764defp4UserId(self): 765if self.myP4UserId: 766return self.myP4UserId 767 768 results =p4CmdList("user -o") 769for r in results: 770if r.has_key('User'): 771 self.myP4UserId = r['User'] 772return r['User'] 773die("Could not find your p4 user id") 774 775defp4UserIsMe(self, p4User): 776# return True if the given p4 user is actually me 777 me = self.p4UserId() 778if not p4User or p4User != me: 779return False 780else: 781return True 782 783defgetUserCacheFilename(self): 784 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 785return home +"/.gitp4-usercache.txt" 786 787defgetUserMapFromPerforceServer(self): 788if self.userMapFromPerforceServer: 789return 790 self.users = {} 791 self.emails = {} 792 793for output inp4CmdList("users"): 794if not output.has_key("User"): 795continue 796 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 797 self.emails[output["Email"]] = output["User"] 798 799 800 s ='' 801for(key, val)in self.users.items(): 802 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 803 804open(self.getUserCacheFilename(),"wb").write(s) 805 self.userMapFromPerforceServer =True 806 807defloadUserMapFromCache(self): 808 self.users = {} 809 self.userMapFromPerforceServer =False 810try: 811 cache =open(self.getUserCacheFilename(),"rb") 812 lines = cache.readlines() 813 cache.close() 814for line in lines: 815 entry = line.strip().split("\t") 816 self.users[entry[0]] = entry[1] 817exceptIOError: 818 self.getUserMapFromPerforceServer() 819 820classP4Debug(Command): 821def__init__(self): 822 Command.__init__(self) 823 self.options = [] 824 self.description ="A tool to debug the output of p4 -G." 825 self.needsGit =False 826 827defrun(self, args): 828 j =0 829for output inp4CmdList(args): 830print'Element:%d'% j 831 j +=1 832print output 833return True 834 835classP4RollBack(Command): 836def__init__(self): 837 Command.__init__(self) 838 self.options = [ 839 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 840] 841 self.description ="A tool to debug the multi-branch import. Don't use :)" 842 self.rollbackLocalBranches =False 843 844defrun(self, args): 845iflen(args) !=1: 846return False 847 maxChange =int(args[0]) 848 849if"p4ExitCode"inp4Cmd("changes -m 1"): 850die("Problems executing p4"); 851 852if self.rollbackLocalBranches: 853 refPrefix ="refs/heads/" 854 lines =read_pipe_lines("git rev-parse --symbolic --branches") 855else: 856 refPrefix ="refs/remotes/" 857 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 858 859for line in lines: 860if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 861 line = line.strip() 862 ref = refPrefix + line 863 log =extractLogMessageFromGitCommit(ref) 864 settings =extractSettingsGitLog(log) 865 866 depotPaths = settings['depot-paths'] 867 change = settings['change'] 868 869 changed =False 870 871iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 872for p in depotPaths]))) ==0: 873print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 874system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 875continue 876 877while change andint(change) > maxChange: 878 changed =True 879if self.verbose: 880print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 881system("git update-ref%s\"%s^\""% (ref, ref)) 882 log =extractLogMessageFromGitCommit(ref) 883 settings =extractSettingsGitLog(log) 884 885 886 depotPaths = settings['depot-paths'] 887 change = settings['change'] 888 889if changed: 890print"%srewound to%s"% (ref, change) 891 892return True 893 894classP4Submit(Command, P4UserMap): 895 896 conflict_behavior_choices = ("ask","skip","quit") 897 898def__init__(self): 899 Command.__init__(self) 900 P4UserMap.__init__(self) 901 self.options = [ 902 optparse.make_option("--origin", dest="origin"), 903 optparse.make_option("-M", dest="detectRenames", action="store_true"), 904# preserve the user, requires relevant p4 permissions 905 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 906 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 907 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 908 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 909 optparse.make_option("--conflict", dest="conflict_behavior", 910 choices=self.conflict_behavior_choices) 911] 912 self.description ="Submit changes from git to the perforce depot." 913 self.usage +=" [name of git branch to submit into perforce depot]" 914 self.origin ="" 915 self.detectRenames =False 916 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 917 self.dry_run =False 918 self.prepare_p4_only =False 919 self.conflict_behavior =None 920 self.isWindows = (platform.system() =="Windows") 921 self.exportLabels =False 922 self.p4HasMoveCommand =p4_has_move_command() 923 924defcheck(self): 925iflen(p4CmdList("opened ...")) >0: 926die("You have files opened with perforce! Close them before starting the sync.") 927 928defseparate_jobs_from_description(self, message): 929"""Extract and return a possible Jobs field in the commit 930 message. It goes into a separate section in the p4 change 931 specification. 932 933 A jobs line starts with "Jobs:" and looks like a new field 934 in a form. Values are white-space separated on the same 935 line or on following lines that start with a tab. 936 937 This does not parse and extract the full git commit message 938 like a p4 form. It just sees the Jobs: line as a marker 939 to pass everything from then on directly into the p4 form, 940 but outside the description section. 941 942 Return a tuple (stripped log message, jobs string).""" 943 944 m = re.search(r'^Jobs:', message, re.MULTILINE) 945if m is None: 946return(message,None) 947 948 jobtext = message[m.start():] 949 stripped_message = message[:m.start()].rstrip() 950return(stripped_message, jobtext) 951 952defprepareLogMessage(self, template, message, jobs): 953"""Edits the template returned from "p4 change -o" to insert 954 the message in the Description field, and the jobs text in 955 the Jobs field.""" 956 result ="" 957 958 inDescriptionSection =False 959 960for line in template.split("\n"): 961if line.startswith("#"): 962 result += line +"\n" 963continue 964 965if inDescriptionSection: 966if line.startswith("Files:")or line.startswith("Jobs:"): 967 inDescriptionSection =False 968# insert Jobs section 969if jobs: 970 result += jobs +"\n" 971else: 972continue 973else: 974if line.startswith("Description:"): 975 inDescriptionSection =True 976 line +="\n" 977for messageLine in message.split("\n"): 978 line +="\t"+ messageLine +"\n" 979 980 result += line +"\n" 981 982return result 983 984defpatchRCSKeywords(self,file, pattern): 985# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern 986(handle, outFileName) = tempfile.mkstemp(dir='.') 987try: 988 outFile = os.fdopen(handle,"w+") 989 inFile =open(file,"r") 990 regexp = re.compile(pattern, re.VERBOSE) 991for line in inFile.readlines(): 992 line = regexp.sub(r'$\1$', line) 993 outFile.write(line) 994 inFile.close() 995 outFile.close() 996# Forcibly overwrite the original file 997 os.unlink(file) 998 shutil.move(outFileName,file) 999except:1000# cleanup our temporary file1001 os.unlink(outFileName)1002print"Failed to strip RCS keywords in%s"%file1003raise10041005print"Patched up RCS keywords in%s"%file10061007defp4UserForCommit(self,id):1008# Return the tuple (perforce user,git email) for a given git commit id1009 self.getUserMapFromPerforceServer()1010 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id)1011 gitEmail = gitEmail.strip()1012if not self.emails.has_key(gitEmail):1013return(None,gitEmail)1014else:1015return(self.emails[gitEmail],gitEmail)10161017defcheckValidP4Users(self,commits):1018# check if any git authors cannot be mapped to p4 users1019foridin commits:1020(user,email) = self.p4UserForCommit(id)1021if not user:1022 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1023ifgitConfig('git-p4.allowMissingP4Users').lower() =="true":1024print"%s"% msg1025else:1026die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10271028deflastP4Changelist(self):1029# Get back the last changelist number submitted in this client spec. This1030# then gets used to patch up the username in the change. If the same1031# client spec is being used by multiple processes then this might go1032# wrong.1033 results =p4CmdList("client -o")# find the current client1034 client =None1035for r in results:1036if r.has_key('Client'):1037 client = r['Client']1038break1039if not client:1040die("could not get client spec")1041 results =p4CmdList(["changes","-c", client,"-m","1"])1042for r in results:1043if r.has_key('change'):1044return r['change']1045die("Could not get changelist number for last submit - cannot patch up user details")10461047defmodifyChangelistUser(self, changelist, newUser):1048# fixup the user field of a changelist after it has been submitted.1049 changes =p4CmdList("change -o%s"% changelist)1050iflen(changes) !=1:1051die("Bad output from p4 change modifying%sto user%s"%1052(changelist, newUser))10531054 c = changes[0]1055if c['User'] == newUser:return# nothing to do1056 c['User'] = newUser1057input= marshal.dumps(c)10581059 result =p4CmdList("change -f -i", stdin=input)1060for r in result:1061if r.has_key('code'):1062if r['code'] =='error':1063die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1064if r.has_key('data'):1065print("Updated user field for changelist%sto%s"% (changelist, newUser))1066return1067die("Could not modify user field of changelist%sto%s"% (changelist, newUser))10681069defcanChangeChangelists(self):1070# check to see if we have p4 admin or super-user permissions, either of1071# which are required to modify changelists.1072 results =p4CmdList(["protects", self.depotPath])1073for r in results:1074if r.has_key('perm'):1075if r['perm'] =='admin':1076return11077if r['perm'] =='super':1078return11079return010801081defprepareSubmitTemplate(self):1082"""Run "p4 change -o" to grab a change specification template.1083 This does not use "p4 -G", as it is nice to keep the submission1084 template in original order, since a human might edit it.10851086 Remove lines in the Files section that show changes to files1087 outside the depot path we're committing into."""10881089 template =""1090 inFilesSection =False1091for line inp4_read_pipe_lines(['change','-o']):1092if line.endswith("\r\n"):1093 line = line[:-2] +"\n"1094if inFilesSection:1095if line.startswith("\t"):1096# path starts and ends with a tab1097 path = line[1:]1098 lastTab = path.rfind("\t")1099if lastTab != -1:1100 path = path[:lastTab]1101if notp4PathStartsWith(path, self.depotPath):1102continue1103else:1104 inFilesSection =False1105else:1106if line.startswith("Files:"):1107 inFilesSection =True11081109 template += line11101111return template11121113defedit_template(self, template_file):1114"""Invoke the editor to let the user change the submission1115 message. Return true if okay to continue with the submit."""11161117# if configured to skip the editing part, just submit1118ifgitConfig("git-p4.skipSubmitEdit") =="true":1119return True11201121# look at the modification time, to check later if the user saved1122# the file1123 mtime = os.stat(template_file).st_mtime11241125# invoke the editor1126if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1127 editor = os.environ.get("P4EDITOR")1128else:1129 editor =read_pipe("git var GIT_EDITOR").strip()1130system(editor +" "+ template_file)11311132# If the file was not saved, prompt to see if this patch should1133# be skipped. But skip this verification step if configured so.1134ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1135return True11361137# modification time updated means user saved the file1138if os.stat(template_file).st_mtime > mtime:1139return True11401141while True:1142 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1143if response =='y':1144return True1145if response =='n':1146return False11471148defapplyCommit(self,id):1149"""Apply one commit, return True if it succeeded."""11501151print"Applying",read_pipe(["git","show","-s",1152"--format=format:%h%s",id])11531154(p4User, gitEmail) = self.p4UserForCommit(id)11551156 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1157 filesToAdd =set()1158 filesToDelete =set()1159 editedFiles =set()1160 pureRenameCopy =set()1161 filesToChangeExecBit = {}11621163for line in diff:1164 diff =parseDiffTreeEntry(line)1165 modifier = diff['status']1166 path = diff['src']1167if modifier =="M":1168p4_edit(path)1169ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1170 filesToChangeExecBit[path] = diff['dst_mode']1171 editedFiles.add(path)1172elif modifier =="A":1173 filesToAdd.add(path)1174 filesToChangeExecBit[path] = diff['dst_mode']1175if path in filesToDelete:1176 filesToDelete.remove(path)1177elif modifier =="D":1178 filesToDelete.add(path)1179if path in filesToAdd:1180 filesToAdd.remove(path)1181elif modifier =="C":1182 src, dest = diff['src'], diff['dst']1183p4_integrate(src, dest)1184 pureRenameCopy.add(dest)1185if diff['src_sha1'] != diff['dst_sha1']:1186p4_edit(dest)1187 pureRenameCopy.discard(dest)1188ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1189p4_edit(dest)1190 pureRenameCopy.discard(dest)1191 filesToChangeExecBit[dest] = diff['dst_mode']1192 os.unlink(dest)1193 editedFiles.add(dest)1194elif modifier =="R":1195 src, dest = diff['src'], diff['dst']1196if self.p4HasMoveCommand:1197p4_edit(src)# src must be open before move1198p4_move(src, dest)# opens for (move/delete, move/add)1199else:1200p4_integrate(src, dest)1201if diff['src_sha1'] != diff['dst_sha1']:1202p4_edit(dest)1203else:1204 pureRenameCopy.add(dest)1205ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1206if not self.p4HasMoveCommand:1207p4_edit(dest)# with move: already open, writable1208 filesToChangeExecBit[dest] = diff['dst_mode']1209if not self.p4HasMoveCommand:1210 os.unlink(dest)1211 filesToDelete.add(src)1212 editedFiles.add(dest)1213else:1214die("unknown modifier%sfor%s"% (modifier, path))12151216 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1217 patchcmd = diffcmd +" | git apply "1218 tryPatchCmd = patchcmd +"--check -"1219 applyPatchCmd = patchcmd +"--check --apply -"1220 patch_succeeded =True12211222if os.system(tryPatchCmd) !=0:1223 fixed_rcs_keywords =False1224 patch_succeeded =False1225print"Unfortunately applying the change failed!"12261227# Patch failed, maybe it's just RCS keyword woes. Look through1228# the patch to see if that's possible.1229ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1230file=None1231 pattern =None1232 kwfiles = {}1233forfilein editedFiles | filesToDelete:1234# did this file's delta contain RCS keywords?1235 pattern =p4_keywords_regexp_for_file(file)12361237if pattern:1238# this file is a possibility...look for RCS keywords.1239 regexp = re.compile(pattern, re.VERBOSE)1240for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1241if regexp.search(line):1242if verbose:1243print"got keyword match on%sin%sin%s"% (pattern, line,file)1244 kwfiles[file] = pattern1245break12461247forfilein kwfiles:1248if verbose:1249print"zapping%swith%s"% (line,pattern)1250 self.patchRCSKeywords(file, kwfiles[file])1251 fixed_rcs_keywords =True12521253if fixed_rcs_keywords:1254print"Retrying the patch with RCS keywords cleaned up"1255if os.system(tryPatchCmd) ==0:1256 patch_succeeded =True12571258if not patch_succeeded:1259for f in editedFiles:1260p4_revert(f)1261return False12621263#1264# Apply the patch for real, and do add/delete/+x handling.1265#1266system(applyPatchCmd)12671268for f in filesToAdd:1269p4_add(f)1270for f in filesToDelete:1271p4_revert(f)1272p4_delete(f)12731274# Set/clear executable bits1275for f in filesToChangeExecBit.keys():1276 mode = filesToChangeExecBit[f]1277setP4ExecBit(f, mode)12781279#1280# Build p4 change description, starting with the contents1281# of the git commit message.1282#1283 logMessage =extractLogMessageFromGitCommit(id)1284 logMessage = logMessage.strip()1285(logMessage, jobs) = self.separate_jobs_from_description(logMessage)12861287 template = self.prepareSubmitTemplate()1288 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)12891290if self.preserveUser:1291 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User12921293if self.checkAuthorship and not self.p4UserIsMe(p4User):1294 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1295 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1296 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"12971298 separatorLine ="######## everything below this line is just the diff #######\n"12991300# diff1301if os.environ.has_key("P4DIFF"):1302del(os.environ["P4DIFF"])1303 diff =""1304for editedFile in editedFiles:1305 diff +=p4_read_pipe(['diff','-du',1306wildcard_encode(editedFile)])13071308# new file diff1309 newdiff =""1310for newFile in filesToAdd:1311 newdiff +="==== new file ====\n"1312 newdiff +="--- /dev/null\n"1313 newdiff +="+++%s\n"% newFile1314 f =open(newFile,"r")1315for line in f.readlines():1316 newdiff +="+"+ line1317 f.close()13181319# change description file: submitTemplate, separatorLine, diff, newdiff1320(handle, fileName) = tempfile.mkstemp()1321 tmpFile = os.fdopen(handle,"w+")1322if self.isWindows:1323 submitTemplate = submitTemplate.replace("\n","\r\n")1324 separatorLine = separatorLine.replace("\n","\r\n")1325 newdiff = newdiff.replace("\n","\r\n")1326 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1327 tmpFile.close()13281329if self.prepare_p4_only:1330#1331# Leave the p4 tree prepared, and the submit template around1332# and let the user decide what to do next1333#1334print1335print"P4 workspace prepared for submission."1336print"To submit or revert, go to client workspace"1337print" "+ self.clientPath1338print1339print"To submit, use\"p4 submit\"to write a new description,"1340print"or\"p4 submit -i%s\"to use the one prepared by" \1341"\"git p4\"."% fileName1342print"You can delete the file\"%s\"when finished."% fileName13431344if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1345print"To preserve change ownership by user%s, you must\n" \1346"do\"p4 change -f <change>\"after submitting and\n" \1347"edit the User field."1348if pureRenameCopy:1349print"After submitting, renamed files must be re-synced."1350print"Invoke\"p4 sync -f\"on each of these files:"1351for f in pureRenameCopy:1352print" "+ f13531354print1355print"To revert the changes, use\"p4 revert ...\", and delete"1356print"the submit template file\"%s\""% fileName1357if filesToAdd:1358print"Since the commit adds new files, they must be deleted:"1359for f in filesToAdd:1360print" "+ f1361print1362return True13631364#1365# Let the user edit the change description, then submit it.1366#1367if self.edit_template(fileName):1368# read the edited message and submit1369 ret =True1370 tmpFile =open(fileName,"rb")1371 message = tmpFile.read()1372 tmpFile.close()1373 submitTemplate = message[:message.index(separatorLine)]1374if self.isWindows:1375 submitTemplate = submitTemplate.replace("\r\n","\n")1376p4_write_pipe(['submit','-i'], submitTemplate)13771378if self.preserveUser:1379if p4User:1380# Get last changelist number. Cannot easily get it from1381# the submit command output as the output is1382# unmarshalled.1383 changelist = self.lastP4Changelist()1384 self.modifyChangelistUser(changelist, p4User)13851386# The rename/copy happened by applying a patch that created a1387# new file. This leaves it writable, which confuses p4.1388for f in pureRenameCopy:1389p4_sync(f,"-f")13901391else:1392# skip this patch1393 ret =False1394print"Submission cancelled, undoing p4 changes."1395for f in editedFiles:1396p4_revert(f)1397for f in filesToAdd:1398p4_revert(f)1399 os.remove(f)1400for f in filesToDelete:1401p4_revert(f)14021403 os.remove(fileName)1404return ret14051406# Export git tags as p4 labels. Create a p4 label and then tag1407# with that.1408defexportGitTags(self, gitTags):1409 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1410iflen(validLabelRegexp) ==0:1411 validLabelRegexp = defaultLabelRegexp1412 m = re.compile(validLabelRegexp)14131414for name in gitTags:14151416if not m.match(name):1417if verbose:1418print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1419continue14201421# Get the p4 commit this corresponds to1422 logMessage =extractLogMessageFromGitCommit(name)1423 values =extractSettingsGitLog(logMessage)14241425if not values.has_key('change'):1426# a tag pointing to something not sent to p4; ignore1427if verbose:1428print"git tag%sdoes not give a p4 commit"% name1429continue1430else:1431 changelist = values['change']14321433# Get the tag details.1434 inHeader =True1435 isAnnotated =False1436 body = []1437for l inread_pipe_lines(["git","cat-file","-p", name]):1438 l = l.strip()1439if inHeader:1440if re.match(r'tag\s+', l):1441 isAnnotated =True1442elif re.match(r'\s*$', l):1443 inHeader =False1444continue1445else:1446 body.append(l)14471448if not isAnnotated:1449 body = ["lightweight tag imported by git p4\n"]14501451# Create the label - use the same view as the client spec we are using1452 clientSpec =getClientSpec()14531454 labelTemplate ="Label:%s\n"% name1455 labelTemplate +="Description:\n"1456for b in body:1457 labelTemplate +="\t"+ b +"\n"1458 labelTemplate +="View:\n"1459for mapping in clientSpec.mappings:1460 labelTemplate +="\t%s\n"% mapping.depot_side.path14611462if self.dry_run:1463print"Would create p4 label%sfor tag"% name1464elif self.prepare_p4_only:1465print"Not creating p4 label%sfor tag due to option" \1466" --prepare-p4-only"% name1467else:1468p4_write_pipe(["label","-i"], labelTemplate)14691470# Use the label1471p4_system(["tag","-l", name] +1472["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])14731474if verbose:1475print"created p4 label for tag%s"% name14761477defrun(self, args):1478iflen(args) ==0:1479 self.master =currentGitBranch()1480iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1481die("Detecting current git branch failed!")1482eliflen(args) ==1:1483 self.master = args[0]1484if notbranchExists(self.master):1485die("Branch%sdoes not exist"% self.master)1486else:1487return False14881489 allowSubmit =gitConfig("git-p4.allowSubmit")1490iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1491die("%sis not in git-p4.allowSubmit"% self.master)14921493[upstream, settings] =findUpstreamBranchPoint()1494 self.depotPath = settings['depot-paths'][0]1495iflen(self.origin) ==0:1496 self.origin = upstream14971498if self.preserveUser:1499if not self.canChangeChangelists():1500die("Cannot preserve user names without p4 super-user or admin permissions")15011502# if not set from the command line, try the config file1503if self.conflict_behavior is None:1504 val =gitConfig("git-p4.conflict")1505if val:1506if val not in self.conflict_behavior_choices:1507die("Invalid value '%s' for config git-p4.conflict"% val)1508else:1509 val ="ask"1510 self.conflict_behavior = val15111512if self.verbose:1513print"Origin branch is "+ self.origin15141515iflen(self.depotPath) ==0:1516print"Internal error: cannot locate perforce depot path from existing branches"1517 sys.exit(128)15181519 self.useClientSpec =False1520ifgitConfig("git-p4.useclientspec","--bool") =="true":1521 self.useClientSpec =True1522if self.useClientSpec:1523 self.clientSpecDirs =getClientSpec()15241525if self.useClientSpec:1526# all files are relative to the client spec1527 self.clientPath =getClientRoot()1528else:1529 self.clientPath =p4Where(self.depotPath)15301531if self.clientPath =="":1532die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15331534print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1535 self.oldWorkingDirectory = os.getcwd()15361537# ensure the clientPath exists1538 new_client_dir =False1539if not os.path.exists(self.clientPath):1540 new_client_dir =True1541 os.makedirs(self.clientPath)15421543chdir(self.clientPath)1544if self.dry_run:1545print"Would synchronize p4 checkout in%s"% self.clientPath1546else:1547print"Synchronizing p4 checkout..."1548if new_client_dir:1549# old one was destroyed, and maybe nobody told p41550p4_sync("...","-f")1551else:1552p4_sync("...")1553 self.check()15541555 commits = []1556for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1557 commits.append(line.strip())1558 commits.reverse()15591560if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1561 self.checkAuthorship =False1562else:1563 self.checkAuthorship =True15641565if self.preserveUser:1566 self.checkValidP4Users(commits)15671568#1569# Build up a set of options to be passed to diff when1570# submitting each commit to p4.1571#1572if self.detectRenames:1573# command-line -M arg1574 self.diffOpts ="-M"1575else:1576# If not explicitly set check the config variable1577 detectRenames =gitConfig("git-p4.detectRenames")15781579if detectRenames.lower() =="false"or detectRenames =="":1580 self.diffOpts =""1581elif detectRenames.lower() =="true":1582 self.diffOpts ="-M"1583else:1584 self.diffOpts ="-M%s"% detectRenames15851586# no command-line arg for -C or --find-copies-harder, just1587# config variables1588 detectCopies =gitConfig("git-p4.detectCopies")1589if detectCopies.lower() =="false"or detectCopies =="":1590pass1591elif detectCopies.lower() =="true":1592 self.diffOpts +=" -C"1593else:1594 self.diffOpts +=" -C%s"% detectCopies15951596ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1597 self.diffOpts +=" --find-copies-harder"15981599#1600# Apply the commits, one at a time. On failure, ask if should1601# continue to try the rest of the patches, or quit.1602#1603if self.dry_run:1604print"Would apply"1605 applied = []1606 last =len(commits) -11607for i, commit inenumerate(commits):1608if self.dry_run:1609print" ",read_pipe(["git","show","-s",1610"--format=format:%h%s", commit])1611 ok =True1612else:1613 ok = self.applyCommit(commit)1614if ok:1615 applied.append(commit)1616else:1617if self.prepare_p4_only and i < last:1618print"Processing only the first commit due to option" \1619" --prepare-p4-only"1620break1621if i < last:1622 quit =False1623while True:1624# prompt for what to do, or use the option/variable1625if self.conflict_behavior =="ask":1626print"What do you want to do?"1627 response =raw_input("[s]kip this commit but apply"1628" the rest, or [q]uit? ")1629if not response:1630continue1631elif self.conflict_behavior =="skip":1632 response ="s"1633elif self.conflict_behavior =="quit":1634 response ="q"1635else:1636die("Unknown conflict_behavior '%s'"%1637 self.conflict_behavior)16381639if response[0] =="s":1640print"Skipping this commit, but applying the rest"1641break1642if response[0] =="q":1643print"Quitting"1644 quit =True1645break1646if quit:1647break16481649chdir(self.oldWorkingDirectory)16501651if self.dry_run:1652pass1653elif self.prepare_p4_only:1654pass1655eliflen(commits) ==len(applied):1656print"All commits applied!"16571658 sync =P4Sync()1659 sync.run([])16601661 rebase =P4Rebase()1662 rebase.rebase()16631664else:1665iflen(applied) ==0:1666print"No commits applied."1667else:1668print"Applied only the commits marked with '*':"1669for c in commits:1670if c in applied:1671 star ="*"1672else:1673 star =" "1674print star,read_pipe(["git","show","-s",1675"--format=format:%h%s", c])1676print"You will have to do 'git p4 sync' and rebase."16771678ifgitConfig("git-p4.exportLabels","--bool") =="true":1679 self.exportLabels =True16801681if self.exportLabels:1682 p4Labels =getP4Labels(self.depotPath)1683 gitTags =getGitTags()16841685 missingGitTags = gitTags - p4Labels1686 self.exportGitTags(missingGitTags)16871688# exit with error unless everything applied perfecly1689iflen(commits) !=len(applied):1690 sys.exit(1)16911692return True16931694classView(object):1695"""Represent a p4 view ("p4 help views"), and map files in a1696 repo according to the view."""16971698classPath(object):1699"""A depot or client path, possibly containing wildcards.1700 The only one supported is ... at the end, currently.1701 Initialize with the full path, with //depot or //client."""17021703def__init__(self, path, is_depot):1704 self.path = path1705 self.is_depot = is_depot1706 self.find_wildcards()1707# remember the prefix bit, useful for relative mappings1708 m = re.match("(//[^/]+/)", self.path)1709if not m:1710die("Path%sdoes not start with //prefix/"% self.path)1711 prefix = m.group(1)1712if not self.is_depot:1713# strip //client/ on client paths1714 self.path = self.path[len(prefix):]17151716deffind_wildcards(self):1717"""Make sure wildcards are valid, and set up internal1718 variables."""17191720 self.ends_triple_dot =False1721# There are three wildcards allowed in p4 views1722# (see "p4 help views"). This code knows how to1723# handle "..." (only at the end), but cannot deal with1724# "%%n" or "*". Only check the depot_side, as p4 should1725# validate that the client_side matches too.1726if re.search(r'%%[1-9]', self.path):1727die("Can't handle%%n wildcards in view:%s"% self.path)1728if self.path.find("*") >=0:1729die("Can't handle * wildcards in view:%s"% self.path)1730 triple_dot_index = self.path.find("...")1731if triple_dot_index >=0:1732if triple_dot_index !=len(self.path) -3:1733die("Can handle only single ... wildcard, at end:%s"%1734 self.path)1735 self.ends_triple_dot =True17361737defensure_compatible(self, other_path):1738"""Make sure the wildcards agree."""1739if self.ends_triple_dot != other_path.ends_triple_dot:1740die("Both paths must end with ... if either does;\n"+1741"paths:%s %s"% (self.path, other_path.path))17421743defmatch_wildcards(self, test_path):1744"""See if this test_path matches us, and fill in the value1745 of the wildcards if so. Returns a tuple of1746 (True|False, wildcards[]). For now, only the ... at end1747 is supported, so at most one wildcard."""1748if self.ends_triple_dot:1749 dotless = self.path[:-3]1750if test_path.startswith(dotless):1751 wildcard = test_path[len(dotless):]1752return(True, [ wildcard ])1753else:1754if test_path == self.path:1755return(True, [])1756return(False, [])17571758defmatch(self, test_path):1759"""Just return if it matches; don't bother with the wildcards."""1760 b, _ = self.match_wildcards(test_path)1761return b17621763deffill_in_wildcards(self, wildcards):1764"""Return the relative path, with the wildcards filled in1765 if there are any."""1766if self.ends_triple_dot:1767return self.path[:-3] + wildcards[0]1768else:1769return self.path17701771classMapping(object):1772def__init__(self, depot_side, client_side, overlay, exclude):1773# depot_side is without the trailing /... if it had one1774 self.depot_side = View.Path(depot_side, is_depot=True)1775 self.client_side = View.Path(client_side, is_depot=False)1776 self.overlay = overlay # started with "+"1777 self.exclude = exclude # started with "-"1778assert not(self.overlay and self.exclude)1779 self.depot_side.ensure_compatible(self.client_side)17801781def__str__(self):1782 c =" "1783if self.overlay:1784 c ="+"1785if self.exclude:1786 c ="-"1787return"View.Mapping:%s%s->%s"% \1788(c, self.depot_side.path, self.client_side.path)17891790defmap_depot_to_client(self, depot_path):1791"""Calculate the client path if using this mapping on the1792 given depot path; does not consider the effect of other1793 mappings in a view. Even excluded mappings are returned."""1794 matches, wildcards = self.depot_side.match_wildcards(depot_path)1795if not matches:1796return""1797 client_path = self.client_side.fill_in_wildcards(wildcards)1798return client_path17991800#1801# View methods1802#1803def__init__(self):1804 self.mappings = []18051806defappend(self, view_line):1807"""Parse a view line, splitting it into depot and client1808 sides. Append to self.mappings, preserving order."""18091810# Split the view line into exactly two words. P4 enforces1811# structure on these lines that simplifies this quite a bit.1812#1813# Either or both words may be double-quoted.1814# Single quotes do not matter.1815# Double-quote marks cannot occur inside the words.1816# A + or - prefix is also inside the quotes.1817# There are no quotes unless they contain a space.1818# The line is already white-space stripped.1819# The two words are separated by a single space.1820#1821if view_line[0] =='"':1822# First word is double quoted. Find its end.1823 close_quote_index = view_line.find('"',1)1824if close_quote_index <=0:1825die("No first-word closing quote found:%s"% view_line)1826 depot_side = view_line[1:close_quote_index]1827# skip closing quote and space1828 rhs_index = close_quote_index +1+11829else:1830 space_index = view_line.find(" ")1831if space_index <=0:1832die("No word-splitting space found:%s"% view_line)1833 depot_side = view_line[0:space_index]1834 rhs_index = space_index +118351836if view_line[rhs_index] =='"':1837# Second word is double quoted. Make sure there is a1838# double quote at the end too.1839if not view_line.endswith('"'):1840die("View line with rhs quote should end with one:%s"%1841 view_line)1842# skip the quotes1843 client_side = view_line[rhs_index+1:-1]1844else:1845 client_side = view_line[rhs_index:]18461847# prefix + means overlay on previous mapping1848 overlay =False1849if depot_side.startswith("+"):1850 overlay =True1851 depot_side = depot_side[1:]18521853# prefix - means exclude this path1854 exclude =False1855if depot_side.startswith("-"):1856 exclude =True1857 depot_side = depot_side[1:]18581859 m = View.Mapping(depot_side, client_side, overlay, exclude)1860 self.mappings.append(m)18611862defmap_in_client(self, depot_path):1863"""Return the relative location in the client where this1864 depot file should live. Returns "" if the file should1865 not be mapped in the client."""18661867 paths_filled = []1868 client_path =""18691870# look at later entries first1871for m in self.mappings[::-1]:18721873# see where will this path end up in the client1874 p = m.map_depot_to_client(depot_path)18751876if p =="":1877# Depot path does not belong in client. Must remember1878# this, as previous items should not cause files to1879# exist in this path either. Remember that the list is1880# being walked from the end, which has higher precedence.1881# Overlap mappings do not exclude previous mappings.1882if not m.overlay:1883 paths_filled.append(m.client_side)18841885else:1886# This mapping matched; no need to search any further.1887# But, the mapping could be rejected if the client path1888# has already been claimed by an earlier mapping (i.e.1889# one later in the list, which we are walking backwards).1890 already_mapped_in_client =False1891for f in paths_filled:1892# this is View.Path.match1893if f.match(p):1894 already_mapped_in_client =True1895break1896if not already_mapped_in_client:1897# Include this file, unless it is from a line that1898# explicitly said to exclude it.1899if not m.exclude:1900 client_path = p19011902# a match, even if rejected, always stops the search1903break19041905return client_path19061907classP4Sync(Command, P4UserMap):1908 delete_actions = ("delete","move/delete","purge")19091910def__init__(self):1911 Command.__init__(self)1912 P4UserMap.__init__(self)1913 self.options = [1914 optparse.make_option("--branch", dest="branch"),1915 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1916 optparse.make_option("--changesfile", dest="changesFile"),1917 optparse.make_option("--silent", dest="silent", action="store_true"),1918 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1919 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1920 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1921help="Import into refs/heads/ , not refs/remotes"),1922 optparse.make_option("--max-changes", dest="maxChanges"),1923 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1924help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1925 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1926help="Only sync files that are included in the Perforce Client Spec")1927]1928 self.description ="""Imports from Perforce into a git repository.\n1929 example:1930 //depot/my/project/ -- to import the current head1931 //depot/my/project/@all -- to import everything1932 //depot/my/project/@1,6 -- to import only from revision 1 to 619331934 (a ... is not needed in the path p4 specification, it's added implicitly)"""19351936 self.usage +=" //depot/path[@revRange]"1937 self.silent =False1938 self.createdBranches =set()1939 self.committedChanges =set()1940 self.branch =""1941 self.detectBranches =False1942 self.detectLabels =False1943 self.importLabels =False1944 self.changesFile =""1945 self.syncWithOrigin =True1946 self.importIntoRemotes =True1947 self.maxChanges =""1948 self.isWindows = (platform.system() =="Windows")1949 self.keepRepoPath =False1950 self.depotPaths =None1951 self.p4BranchesInGit = []1952 self.cloneExclude = []1953 self.useClientSpec =False1954 self.useClientSpec_from_options =False1955 self.clientSpecDirs =None1956 self.tempBranches = []1957 self.tempBranchLocation ="git-p4-tmp"19581959ifgitConfig("git-p4.syncFromOrigin") =="false":1960 self.syncWithOrigin =False19611962# Force a checkpoint in fast-import and wait for it to finish1963defcheckpoint(self):1964 self.gitStream.write("checkpoint\n\n")1965 self.gitStream.write("progress checkpoint\n\n")1966 out = self.gitOutput.readline()1967if self.verbose:1968print"checkpoint finished: "+ out19691970defextractFilesFromCommit(self, commit):1971 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1972for path in self.cloneExclude]1973 files = []1974 fnum =01975while commit.has_key("depotFile%s"% fnum):1976 path = commit["depotFile%s"% fnum]19771978if[p for p in self.cloneExclude1979ifp4PathStartsWith(path, p)]:1980 found =False1981else:1982 found = [p for p in self.depotPaths1983ifp4PathStartsWith(path, p)]1984if not found:1985 fnum = fnum +11986continue19871988file= {}1989file["path"] = path1990file["rev"] = commit["rev%s"% fnum]1991file["action"] = commit["action%s"% fnum]1992file["type"] = commit["type%s"% fnum]1993 files.append(file)1994 fnum = fnum +11995return files19961997defstripRepoPath(self, path, prefixes):1998"""When streaming files, this is called to map a p4 depot path1999 to where it should go in git. The prefixes are either2000 self.depotPaths, or self.branchPrefixes in the case of2001 branch detection."""20022003if self.useClientSpec:2004# branch detection moves files up a level (the branch name)2005# from what client spec interpretation gives2006 path = self.clientSpecDirs.map_in_client(path)2007if self.detectBranches:2008for b in self.knownBranches:2009if path.startswith(b +"/"):2010 path = path[len(b)+1:]20112012elif self.keepRepoPath:2013# Preserve everything in relative path name except leading2014# //depot/; just look at first prefix as they all should2015# be in the same depot.2016 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2017ifp4PathStartsWith(path, depot):2018 path = path[len(depot):]20192020else:2021for p in prefixes:2022ifp4PathStartsWith(path, p):2023 path = path[len(p):]2024break20252026 path =wildcard_decode(path)2027return path20282029defsplitFilesIntoBranches(self, commit):2030"""Look at each depotFile in the commit to figure out to what2031 branch it belongs."""20322033 branches = {}2034 fnum =02035while commit.has_key("depotFile%s"% fnum):2036 path = commit["depotFile%s"% fnum]2037 found = [p for p in self.depotPaths2038ifp4PathStartsWith(path, p)]2039if not found:2040 fnum = fnum +12041continue20422043file= {}2044file["path"] = path2045file["rev"] = commit["rev%s"% fnum]2046file["action"] = commit["action%s"% fnum]2047file["type"] = commit["type%s"% fnum]2048 fnum = fnum +120492050# start with the full relative path where this file would2051# go in a p4 client2052if self.useClientSpec:2053 relPath = self.clientSpecDirs.map_in_client(path)2054else:2055 relPath = self.stripRepoPath(path, self.depotPaths)20562057for branch in self.knownBranches.keys():2058# add a trailing slash so that a commit into qt/4.2foo2059# doesn't end up in qt/4.2, e.g.2060if relPath.startswith(branch +"/"):2061if branch not in branches:2062 branches[branch] = []2063 branches[branch].append(file)2064break20652066return branches20672068# output one file from the P4 stream2069# - helper for streamP4Files20702071defstreamOneP4File(self,file, contents):2072 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2073if verbose:2074 sys.stderr.write("%s\n"% relPath)20752076(type_base, type_mods) =split_p4_type(file["type"])20772078 git_mode ="100644"2079if"x"in type_mods:2080 git_mode ="100755"2081if type_base =="symlink":2082 git_mode ="120000"2083# p4 print on a symlink contains "target\n"; remove the newline2084 data =''.join(contents)2085 contents = [data[:-1]]20862087if type_base =="utf16":2088# p4 delivers different text in the python output to -G2089# than it does when using "print -o", or normal p4 client2090# operations. utf16 is converted to ascii or utf8, perhaps.2091# But ascii text saved as -t utf16 is completely mangled.2092# Invoke print -o to get the real contents.2093 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2094 contents = [ text ]20952096if type_base =="apple":2097# Apple filetype files will be streamed as a concatenation of2098# its appledouble header and the contents. This is useless2099# on both macs and non-macs. If using "print -q -o xx", it2100# will create "xx" with the data, and "%xx" with the header.2101# This is also not very useful.2102#2103# Ideally, someday, this script can learn how to generate2104# appledouble files directly and import those to git, but2105# non-mac machines can never find a use for apple filetype.2106print"\nIgnoring apple filetype file%s"%file['depotFile']2107return21082109# Perhaps windows wants unicode, utf16 newlines translated too;2110# but this is not doing it.2111if self.isWindows and type_base =="text":2112 mangled = []2113for data in contents:2114 data = data.replace("\r\n","\n")2115 mangled.append(data)2116 contents = mangled21172118# Note that we do not try to de-mangle keywords on utf16 files,2119# even though in theory somebody may want that.2120 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2121if pattern:2122 regexp = re.compile(pattern, re.VERBOSE)2123 text =''.join(contents)2124 text = regexp.sub(r'$\1$', text)2125 contents = [ text ]21262127 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21282129# total length...2130 length =02131for d in contents:2132 length = length +len(d)21332134 self.gitStream.write("data%d\n"% length)2135for d in contents:2136 self.gitStream.write(d)2137 self.gitStream.write("\n")21382139defstreamOneP4Deletion(self,file):2140 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2141if verbose:2142 sys.stderr.write("delete%s\n"% relPath)2143 self.gitStream.write("D%s\n"% relPath)21442145# handle another chunk of streaming data2146defstreamP4FilesCb(self, marshalled):21472148# catch p4 errors and complain2149 err =None2150if"code"in marshalled:2151if marshalled["code"] =="error":2152if"data"in marshalled:2153 err = marshalled["data"].rstrip()2154if err:2155 f =None2156if self.stream_have_file_info:2157if"depotFile"in self.stream_file:2158 f = self.stream_file["depotFile"]2159# force a failure in fast-import, else an empty2160# commit will be made2161 self.gitStream.write("\n")2162 self.gitStream.write("die-now\n")2163 self.gitStream.close()2164# ignore errors, but make sure it exits first2165 self.importProcess.wait()2166if f:2167die("Error from p4 print for%s:%s"% (f, err))2168else:2169die("Error from p4 print:%s"% err)21702171if marshalled.has_key('depotFile')and self.stream_have_file_info:2172# start of a new file - output the old one first2173 self.streamOneP4File(self.stream_file, self.stream_contents)2174 self.stream_file = {}2175 self.stream_contents = []2176 self.stream_have_file_info =False21772178# pick up the new file information... for the2179# 'data' field we need to append to our array2180for k in marshalled.keys():2181if k =='data':2182 self.stream_contents.append(marshalled['data'])2183else:2184 self.stream_file[k] = marshalled[k]21852186 self.stream_have_file_info =True21872188# Stream directly from "p4 files" into "git fast-import"2189defstreamP4Files(self, files):2190 filesForCommit = []2191 filesToRead = []2192 filesToDelete = []21932194for f in files:2195# if using a client spec, only add the files that have2196# a path in the client2197if self.clientSpecDirs:2198if self.clientSpecDirs.map_in_client(f['path']) =="":2199continue22002201 filesForCommit.append(f)2202if f['action']in self.delete_actions:2203 filesToDelete.append(f)2204else:2205 filesToRead.append(f)22062207# deleted files...2208for f in filesToDelete:2209 self.streamOneP4Deletion(f)22102211iflen(filesToRead) >0:2212 self.stream_file = {}2213 self.stream_contents = []2214 self.stream_have_file_info =False22152216# curry self argument2217defstreamP4FilesCbSelf(entry):2218 self.streamP4FilesCb(entry)22192220 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22212222p4CmdList(["-x","-","print"],2223 stdin=fileArgs,2224 cb=streamP4FilesCbSelf)22252226# do the last chunk2227if self.stream_file.has_key('depotFile'):2228 self.streamOneP4File(self.stream_file, self.stream_contents)22292230defmake_email(self, userid):2231if userid in self.users:2232return self.users[userid]2233else:2234return"%s<a@b>"% userid22352236# Stream a p4 tag2237defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2238if verbose:2239print"writing tag%sfor commit%s"% (labelName, commit)2240 gitStream.write("tag%s\n"% labelName)2241 gitStream.write("from%s\n"% commit)22422243if labelDetails.has_key('Owner'):2244 owner = labelDetails["Owner"]2245else:2246 owner =None22472248# Try to use the owner of the p4 label, or failing that,2249# the current p4 user id.2250if owner:2251 email = self.make_email(owner)2252else:2253 email = self.make_email(self.p4UserId())2254 tagger ="%s %s %s"% (email, epoch, self.tz)22552256 gitStream.write("tagger%s\n"% tagger)22572258print"labelDetails=",labelDetails2259if labelDetails.has_key('Description'):2260 description = labelDetails['Description']2261else:2262 description ='Label from git p4'22632264 gitStream.write("data%d\n"%len(description))2265 gitStream.write(description)2266 gitStream.write("\n")22672268defcommit(self, details, files, branch, parent =""):2269 epoch = details["time"]2270 author = details["user"]22712272if self.verbose:2273print"commit into%s"% branch22742275# start with reading files; if that fails, we should not2276# create a commit.2277 new_files = []2278for f in files:2279if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2280 new_files.append(f)2281else:2282 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])22832284 self.gitStream.write("commit%s\n"% branch)2285# gitStream.write("mark :%s\n" % details["change"])2286 self.committedChanges.add(int(details["change"]))2287 committer =""2288if author not in self.users:2289 self.getUserMapFromPerforceServer()2290 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)22912292 self.gitStream.write("committer%s\n"% committer)22932294 self.gitStream.write("data <<EOT\n")2295 self.gitStream.write(details["desc"])2296 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2297(','.join(self.branchPrefixes), details["change"]))2298iflen(details['options']) >0:2299 self.gitStream.write(": options =%s"% details['options'])2300 self.gitStream.write("]\nEOT\n\n")23012302iflen(parent) >0:2303if self.verbose:2304print"parent%s"% parent2305 self.gitStream.write("from%s\n"% parent)23062307 self.streamP4Files(new_files)2308 self.gitStream.write("\n")23092310 change =int(details["change"])23112312if self.labels.has_key(change):2313 label = self.labels[change]2314 labelDetails = label[0]2315 labelRevisions = label[1]2316if self.verbose:2317print"Change%sis labelled%s"% (change, labelDetails)23182319 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2320for p in self.branchPrefixes])23212322iflen(files) ==len(labelRevisions):23232324 cleanedFiles = {}2325for info in files:2326if info["action"]in self.delete_actions:2327continue2328 cleanedFiles[info["depotFile"]] = info["rev"]23292330if cleanedFiles == labelRevisions:2331 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23322333else:2334if not self.silent:2335print("Tag%sdoes not match with change%s: files do not match."2336% (labelDetails["label"], change))23372338else:2339if not self.silent:2340print("Tag%sdoes not match with change%s: file count is different."2341% (labelDetails["label"], change))23422343# Build a dictionary of changelists and labels, for "detect-labels" option.2344defgetLabels(self):2345 self.labels = {}23462347 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2348iflen(l) >0and not self.silent:2349print"Finding files belonging to labels in%s"% `self.depotPaths`23502351for output in l:2352 label = output["label"]2353 revisions = {}2354 newestChange =02355if self.verbose:2356print"Querying files for label%s"% label2357forfileinp4CmdList(["files"] +2358["%s...@%s"% (p, label)2359for p in self.depotPaths]):2360 revisions[file["depotFile"]] =file["rev"]2361 change =int(file["change"])2362if change > newestChange:2363 newestChange = change23642365 self.labels[newestChange] = [output, revisions]23662367if self.verbose:2368print"Label changes:%s"% self.labels.keys()23692370# Import p4 labels as git tags. A direct mapping does not2371# exist, so assume that if all the files are at the same revision2372# then we can use that, or it's something more complicated we should2373# just ignore.2374defimportP4Labels(self, stream, p4Labels):2375if verbose:2376print"import p4 labels: "+' '.join(p4Labels)23772378 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2379 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2380iflen(validLabelRegexp) ==0:2381 validLabelRegexp = defaultLabelRegexp2382 m = re.compile(validLabelRegexp)23832384for name in p4Labels:2385 commitFound =False23862387if not m.match(name):2388if verbose:2389print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2390continue23912392if name in ignoredP4Labels:2393continue23942395 labelDetails =p4CmdList(['label',"-o", name])[0]23962397# get the most recent changelist for each file in this label2398 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2399for p in self.depotPaths])24002401if change.has_key('change'):2402# find the corresponding git commit; take the oldest commit2403 changelist =int(change['change'])2404 gitCommit =read_pipe(["git","rev-list","--max-count=1",2405"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2406iflen(gitCommit) ==0:2407print"could not find git commit for changelist%d"% changelist2408else:2409 gitCommit = gitCommit.strip()2410 commitFound =True2411# Convert from p4 time format2412try:2413 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2414exceptValueError:2415print"Could not convert label time%s"% labelDetails['Update']2416 tmwhen =124172418 when =int(time.mktime(tmwhen))2419 self.streamTag(stream, name, labelDetails, gitCommit, when)2420if verbose:2421print"p4 label%smapped to git commit%s"% (name, gitCommit)2422else:2423if verbose:2424print"Label%shas no changelists - possibly deleted?"% name24252426if not commitFound:2427# We can't import this label; don't try again as it will get very2428# expensive repeatedly fetching all the files for labels that will2429# never be imported. If the label is moved in the future, the2430# ignore will need to be removed manually.2431system(["git","config","--add","git-p4.ignoredP4Labels", name])24322433defguessProjectName(self):2434for p in self.depotPaths:2435if p.endswith("/"):2436 p = p[:-1]2437 p = p[p.strip().rfind("/") +1:]2438if not p.endswith("/"):2439 p +="/"2440return p24412442defgetBranchMapping(self):2443 lostAndFoundBranches =set()24442445 user =gitConfig("git-p4.branchUser")2446iflen(user) >0:2447 command ="branches -u%s"% user2448else:2449 command ="branches"24502451for info inp4CmdList(command):2452 details =p4Cmd(["branch","-o", info["branch"]])2453 viewIdx =02454while details.has_key("View%s"% viewIdx):2455 paths = details["View%s"% viewIdx].split(" ")2456 viewIdx = viewIdx +12457# require standard //depot/foo/... //depot/bar/... mapping2458iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2459continue2460 source = paths[0]2461 destination = paths[1]2462## HACK2463ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2464 source = source[len(self.depotPaths[0]):-4]2465 destination = destination[len(self.depotPaths[0]):-4]24662467if destination in self.knownBranches:2468if not self.silent:2469print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2470print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2471continue24722473 self.knownBranches[destination] = source24742475 lostAndFoundBranches.discard(destination)24762477if source not in self.knownBranches:2478 lostAndFoundBranches.add(source)24792480# Perforce does not strictly require branches to be defined, so we also2481# check git config for a branch list.2482#2483# Example of branch definition in git config file:2484# [git-p4]2485# branchList=main:branchA2486# branchList=main:branchB2487# branchList=branchA:branchC2488 configBranches =gitConfigList("git-p4.branchList")2489for branch in configBranches:2490if branch:2491(source, destination) = branch.split(":")2492 self.knownBranches[destination] = source24932494 lostAndFoundBranches.discard(destination)24952496if source not in self.knownBranches:2497 lostAndFoundBranches.add(source)249824992500for branch in lostAndFoundBranches:2501 self.knownBranches[branch] = branch25022503defgetBranchMappingFromGitBranches(self):2504 branches =p4BranchesInGit(self.importIntoRemotes)2505for branch in branches.keys():2506if branch =="master":2507 branch ="main"2508else:2509 branch = branch[len(self.projectName):]2510 self.knownBranches[branch] = branch25112512deflistExistingP4GitBranches(self):2513# branches holds mapping from name to commit2514 branches =p4BranchesInGit(self.importIntoRemotes)2515 self.p4BranchesInGit = branches.keys()2516for branch in branches.keys():2517 self.initialParents[self.refPrefix + branch] = branches[branch]25182519defupdateOptionDict(self, d):2520 option_keys = {}2521if self.keepRepoPath:2522 option_keys['keepRepoPath'] =125232524 d["options"] =' '.join(sorted(option_keys.keys()))25252526defreadOptions(self, d):2527 self.keepRepoPath = (d.has_key('options')2528and('keepRepoPath'in d['options']))25292530defgitRefForBranch(self, branch):2531if branch =="main":2532return self.refPrefix +"master"25332534iflen(branch) <=0:2535return branch25362537return self.refPrefix + self.projectName + branch25382539defgitCommitByP4Change(self, ref, change):2540if self.verbose:2541print"looking in ref "+ ref +" for change%susing bisect..."% change25422543 earliestCommit =""2544 latestCommit =parseRevision(ref)25452546while True:2547if self.verbose:2548print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2549 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2550iflen(next) ==0:2551if self.verbose:2552print"argh"2553return""2554 log =extractLogMessageFromGitCommit(next)2555 settings =extractSettingsGitLog(log)2556 currentChange =int(settings['change'])2557if self.verbose:2558print"current change%s"% currentChange25592560if currentChange == change:2561if self.verbose:2562print"found%s"% next2563return next25642565if currentChange < change:2566 earliestCommit ="^%s"% next2567else:2568 latestCommit ="%s"% next25692570return""25712572defimportNewBranch(self, branch, maxChange):2573# make fast-import flush all changes to disk and update the refs using the checkpoint2574# command so that we can try to find the branch parent in the git history2575 self.gitStream.write("checkpoint\n\n");2576 self.gitStream.flush();2577 branchPrefix = self.depotPaths[0] + branch +"/"2578range="@1,%s"% maxChange2579#print "prefix" + branchPrefix2580 changes =p4ChangesForPaths([branchPrefix],range)2581iflen(changes) <=0:2582return False2583 firstChange = changes[0]2584#print "first change in branch: %s" % firstChange2585 sourceBranch = self.knownBranches[branch]2586 sourceDepotPath = self.depotPaths[0] + sourceBranch2587 sourceRef = self.gitRefForBranch(sourceBranch)2588#print "source " + sourceBranch25892590 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2591#print "branch parent: %s" % branchParentChange2592 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2593iflen(gitParent) >0:2594 self.initialParents[self.gitRefForBranch(branch)] = gitParent2595#print "parent git commit: %s" % gitParent25962597 self.importChanges(changes)2598return True25992600defsearchParent(self, parent, branch, target):2601 parentFound =False2602for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2603 blob = blob.strip()2604iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2605 parentFound =True2606if self.verbose:2607print"Found parent of%sin commit%s"% (branch, blob)2608break2609if parentFound:2610return blob2611else:2612return None26132614defimportChanges(self, changes):2615 cnt =12616for change in changes:2617 description =p4_describe(change)2618 self.updateOptionDict(description)26192620if not self.silent:2621 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2622 sys.stdout.flush()2623 cnt = cnt +126242625try:2626if self.detectBranches:2627 branches = self.splitFilesIntoBranches(description)2628for branch in branches.keys():2629## HACK --hwn2630 branchPrefix = self.depotPaths[0] + branch +"/"2631 self.branchPrefixes = [ branchPrefix ]26322633 parent =""26342635 filesForCommit = branches[branch]26362637if self.verbose:2638print"branch is%s"% branch26392640 self.updatedBranches.add(branch)26412642if branch not in self.createdBranches:2643 self.createdBranches.add(branch)2644 parent = self.knownBranches[branch]2645if parent == branch:2646 parent =""2647else:2648 fullBranch = self.projectName + branch2649if fullBranch not in self.p4BranchesInGit:2650if not self.silent:2651print("\nImporting new branch%s"% fullBranch);2652if self.importNewBranch(branch, change -1):2653 parent =""2654 self.p4BranchesInGit.append(fullBranch)2655if not self.silent:2656print("\nResuming with change%s"% change);26572658if self.verbose:2659print"parent determined through known branches:%s"% parent26602661 branch = self.gitRefForBranch(branch)2662 parent = self.gitRefForBranch(parent)26632664if self.verbose:2665print"looking for initial parent for%s; current parent is%s"% (branch, parent)26662667iflen(parent) ==0and branch in self.initialParents:2668 parent = self.initialParents[branch]2669del self.initialParents[branch]26702671 blob =None2672iflen(parent) >0:2673 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2674if self.verbose:2675print"Creating temporary branch: "+ tempBranch2676 self.commit(description, filesForCommit, tempBranch)2677 self.tempBranches.append(tempBranch)2678 self.checkpoint()2679 blob = self.searchParent(parent, branch, tempBranch)2680if blob:2681 self.commit(description, filesForCommit, branch, blob)2682else:2683if self.verbose:2684print"Parent of%snot found. Committing into head of%s"% (branch, parent)2685 self.commit(description, filesForCommit, branch, parent)2686else:2687 files = self.extractFilesFromCommit(description)2688 self.commit(description, files, self.branch,2689 self.initialParent)2690 self.initialParent =""2691exceptIOError:2692print self.gitError.read()2693 sys.exit(1)26942695defimportHeadRevision(self, revision):2696print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)26972698 details = {}2699 details["user"] ="git perforce import user"2700 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2701% (' '.join(self.depotPaths), revision))2702 details["change"] = revision2703 newestRevision =027042705 fileCnt =02706 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27072708for info inp4CmdList(["files"] + fileArgs):27092710if'code'in info and info['code'] =='error':2711 sys.stderr.write("p4 returned an error:%s\n"2712% info['data'])2713if info['data'].find("must refer to client") >=0:2714 sys.stderr.write("This particular p4 error is misleading.\n")2715 sys.stderr.write("Perhaps the depot path was misspelled.\n");2716 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2717 sys.exit(1)2718if'p4ExitCode'in info:2719 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2720 sys.exit(1)272127222723 change =int(info["change"])2724if change > newestRevision:2725 newestRevision = change27262727if info["action"]in self.delete_actions:2728# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2729#fileCnt = fileCnt + 12730continue27312732for prop in["depotFile","rev","action","type"]:2733 details["%s%s"% (prop, fileCnt)] = info[prop]27342735 fileCnt = fileCnt +127362737 details["change"] = newestRevision27382739# Use time from top-most change so that all git p4 clones of2740# the same p4 repo have the same commit SHA1s.2741 res =p4_describe(newestRevision)2742 details["time"] = res["time"]27432744 self.updateOptionDict(details)2745try:2746 self.commit(details, self.extractFilesFromCommit(details), self.branch)2747exceptIOError:2748print"IO error with git fast-import. Is your git version recent enough?"2749print self.gitError.read()275027512752defrun(self, args):2753 self.depotPaths = []2754 self.changeRange =""2755 self.initialParent =""2756 self.previousDepotPaths = []27572758# map from branch depot path to parent branch2759 self.knownBranches = {}2760 self.initialParents = {}2761 self.hasOrigin =originP4BranchesExist()2762if not self.syncWithOrigin:2763 self.hasOrigin =False27642765if self.importIntoRemotes:2766 self.refPrefix ="refs/remotes/p4/"2767else:2768 self.refPrefix ="refs/heads/p4/"27692770if self.syncWithOrigin and self.hasOrigin:2771if not self.silent:2772print"Syncing with origin first by calling git fetch origin"2773system("git fetch origin")27742775iflen(self.branch) ==0:2776 self.branch = self.refPrefix +"master"2777ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2778system("git update-ref%srefs/heads/p4"% self.branch)2779system("git branch -D p4");2780# create it /after/ importing, when master exists2781if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2782system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))27832784# accept either the command-line option, or the configuration variable2785if self.useClientSpec:2786# will use this after clone to set the variable2787 self.useClientSpec_from_options =True2788else:2789ifgitConfig("git-p4.useclientspec","--bool") =="true":2790 self.useClientSpec =True2791if self.useClientSpec:2792 self.clientSpecDirs =getClientSpec()27932794# TODO: should always look at previous commits,2795# merge with previous imports, if possible.2796if args == []:2797if self.hasOrigin:2798createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2799 self.listExistingP4GitBranches()28002801iflen(self.p4BranchesInGit) >1:2802if not self.silent:2803print"Importing from/into multiple branches"2804 self.detectBranches =True28052806if self.verbose:2807print"branches:%s"% self.p4BranchesInGit28082809 p4Change =02810for branch in self.p4BranchesInGit:2811 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28122813 settings =extractSettingsGitLog(logMsg)28142815 self.readOptions(settings)2816if(settings.has_key('depot-paths')2817and settings.has_key('change')):2818 change =int(settings['change']) +12819 p4Change =max(p4Change, change)28202821 depotPaths =sorted(settings['depot-paths'])2822if self.previousDepotPaths == []:2823 self.previousDepotPaths = depotPaths2824else:2825 paths = []2826for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2827 prev_list = prev.split("/")2828 cur_list = cur.split("/")2829for i inrange(0,min(len(cur_list),len(prev_list))):2830if cur_list[i] <> prev_list[i]:2831 i = i -12832break28332834 paths.append("/".join(cur_list[:i +1]))28352836 self.previousDepotPaths = paths28372838if p4Change >0:2839 self.depotPaths =sorted(self.previousDepotPaths)2840 self.changeRange ="@%s,#head"% p4Change2841if not self.detectBranches:2842 self.initialParent =parseRevision(self.branch)2843if not self.silent and not self.detectBranches:2844print"Performing incremental import into%sgit branch"% self.branch28452846if not self.branch.startswith("refs/"):2847 self.branch ="refs/heads/"+ self.branch28482849iflen(args) ==0and self.depotPaths:2850if not self.silent:2851print"Depot paths:%s"%' '.join(self.depotPaths)2852else:2853if self.depotPaths and self.depotPaths != args:2854print("previous import used depot path%sand now%swas specified. "2855"This doesn't work!"% (' '.join(self.depotPaths),2856' '.join(args)))2857 sys.exit(1)28582859 self.depotPaths =sorted(args)28602861 revision =""2862 self.users = {}28632864# Make sure no revision specifiers are used when --changesfile2865# is specified.2866 bad_changesfile =False2867iflen(self.changesFile) >0:2868for p in self.depotPaths:2869if p.find("@") >=0or p.find("#") >=0:2870 bad_changesfile =True2871break2872if bad_changesfile:2873die("Option --changesfile is incompatible with revision specifiers")28742875 newPaths = []2876for p in self.depotPaths:2877if p.find("@") != -1:2878 atIdx = p.index("@")2879 self.changeRange = p[atIdx:]2880if self.changeRange =="@all":2881 self.changeRange =""2882elif','not in self.changeRange:2883 revision = self.changeRange2884 self.changeRange =""2885 p = p[:atIdx]2886elif p.find("#") != -1:2887 hashIdx = p.index("#")2888 revision = p[hashIdx:]2889 p = p[:hashIdx]2890elif self.previousDepotPaths == []:2891# pay attention to changesfile, if given, else import2892# the entire p4 tree at the head revision2893iflen(self.changesFile) ==0:2894 revision ="#head"28952896 p = re.sub("\.\.\.$","", p)2897if not p.endswith("/"):2898 p +="/"28992900 newPaths.append(p)29012902 self.depotPaths = newPaths29032904# --detect-branches may change this for each branch2905 self.branchPrefixes = self.depotPaths29062907 self.loadUserMapFromCache()2908 self.labels = {}2909if self.detectLabels:2910 self.getLabels();29112912if self.detectBranches:2913## FIXME - what's a P4 projectName ?2914 self.projectName = self.guessProjectName()29152916if self.hasOrigin:2917 self.getBranchMappingFromGitBranches()2918else:2919 self.getBranchMapping()2920if self.verbose:2921print"p4-git branches:%s"% self.p4BranchesInGit2922print"initial parents:%s"% self.initialParents2923for b in self.p4BranchesInGit:2924if b !="master":29252926## FIXME2927 b = b[len(self.projectName):]2928 self.createdBranches.add(b)29292930 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29312932 self.importProcess = subprocess.Popen(["git","fast-import"],2933 stdin=subprocess.PIPE,2934 stdout=subprocess.PIPE,2935 stderr=subprocess.PIPE);2936 self.gitOutput = self.importProcess.stdout2937 self.gitStream = self.importProcess.stdin2938 self.gitError = self.importProcess.stderr29392940if revision:2941 self.importHeadRevision(revision)2942else:2943 changes = []29442945iflen(self.changesFile) >0:2946 output =open(self.changesFile).readlines()2947 changeSet =set()2948for line in output:2949 changeSet.add(int(line))29502951for change in changeSet:2952 changes.append(change)29532954 changes.sort()2955else:2956# catch "git p4 sync" with no new branches, in a repo that2957# does not have any existing p4 branches2958iflen(args) ==0and not self.p4BranchesInGit:2959die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2960if self.verbose:2961print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2962 self.changeRange)2963 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)29642965iflen(self.maxChanges) >0:2966 changes = changes[:min(int(self.maxChanges),len(changes))]29672968iflen(changes) ==0:2969if not self.silent:2970print"No changes to import!"2971else:2972if not self.silent and not self.detectBranches:2973print"Import destination:%s"% self.branch29742975 self.updatedBranches =set()29762977 self.importChanges(changes)29782979if not self.silent:2980print""2981iflen(self.updatedBranches) >0:2982 sys.stdout.write("Updated branches: ")2983for b in self.updatedBranches:2984 sys.stdout.write("%s"% b)2985 sys.stdout.write("\n")29862987ifgitConfig("git-p4.importLabels","--bool") =="true":2988 self.importLabels =True29892990if self.importLabels:2991 p4Labels =getP4Labels(self.depotPaths)2992 gitTags =getGitTags()29932994 missingP4Labels = p4Labels - gitTags2995 self.importP4Labels(self.gitStream, missingP4Labels)29962997 self.gitStream.close()2998if self.importProcess.wait() !=0:2999die("fast-import failed:%s"% self.gitError.read())3000 self.gitOutput.close()3001 self.gitError.close()30023003# Cleanup temporary branches created during import3004if self.tempBranches != []:3005for branch in self.tempBranches:3006read_pipe("git update-ref -d%s"% branch)3007 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30083009return True30103011classP4Rebase(Command):3012def__init__(self):3013 Command.__init__(self)3014 self.options = [3015 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3016]3017 self.importLabels =False3018 self.description = ("Fetches the latest revision from perforce and "3019+"rebases the current work (branch) against it")30203021defrun(self, args):3022 sync =P4Sync()3023 sync.importLabels = self.importLabels3024 sync.run([])30253026return self.rebase()30273028defrebase(self):3029if os.system("git update-index --refresh") !=0:3030die("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.");3031iflen(read_pipe("git diff-index HEAD --")) >0:3032die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");30333034[upstream, settings] =findUpstreamBranchPoint()3035iflen(upstream) ==0:3036die("Cannot find upstream branchpoint for rebase")30373038# the branchpoint may be p4/foo~3, so strip off the parent3039 upstream = re.sub("~[0-9]+$","", upstream)30403041print"Rebasing the current branch onto%s"% upstream3042 oldHead =read_pipe("git rev-parse HEAD").strip()3043system("git rebase%s"% upstream)3044system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3045return True30463047classP4Clone(P4Sync):3048def__init__(self):3049 P4Sync.__init__(self)3050 self.description ="Creates a new git repository and imports from Perforce into it"3051 self.usage ="usage: %prog [options] //depot/path[@revRange]"3052 self.options += [3053 optparse.make_option("--destination", dest="cloneDestination",3054 action='store', default=None,3055help="where to leave result of the clone"),3056 optparse.make_option("-/", dest="cloneExclude",3057 action="append",type="string",3058help="exclude depot path"),3059 optparse.make_option("--bare", dest="cloneBare",3060 action="store_true", default=False),3061]3062 self.cloneDestination =None3063 self.needsGit =False3064 self.cloneBare =False30653066# This is required for the "append" cloneExclude action3067defensure_value(self, attr, value):3068if nothasattr(self, attr)orgetattr(self, attr)is None:3069setattr(self, attr, value)3070returngetattr(self, attr)30713072defdefaultDestination(self, args):3073## TODO: use common prefix of args?3074 depotPath = args[0]3075 depotDir = re.sub("(@[^@]*)$","", depotPath)3076 depotDir = re.sub("(#[^#]*)$","", depotDir)3077 depotDir = re.sub(r"\.\.\.$","", depotDir)3078 depotDir = re.sub(r"/$","", depotDir)3079return os.path.split(depotDir)[1]30803081defrun(self, args):3082iflen(args) <1:3083return False30843085if self.keepRepoPath and not self.cloneDestination:3086 sys.stderr.write("Must specify destination for --keep-path\n")3087 sys.exit(1)30883089 depotPaths = args30903091if not self.cloneDestination andlen(depotPaths) >1:3092 self.cloneDestination = depotPaths[-1]3093 depotPaths = depotPaths[:-1]30943095 self.cloneExclude = ["/"+p for p in self.cloneExclude]3096for p in depotPaths:3097if not p.startswith("//"):3098return False30993100if not self.cloneDestination:3101 self.cloneDestination = self.defaultDestination(args)31023103print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)31043105if not os.path.exists(self.cloneDestination):3106 os.makedirs(self.cloneDestination)3107chdir(self.cloneDestination)31083109 init_cmd = ["git","init"]3110if self.cloneBare:3111 init_cmd.append("--bare")3112 subprocess.check_call(init_cmd)31133114if not P4Sync.run(self, depotPaths):3115return False3116if self.branch !="master":3117if self.importIntoRemotes:3118 masterbranch ="refs/remotes/p4/master"3119else:3120 masterbranch ="refs/heads/p4/master"3121ifgitBranchExists(masterbranch):3122system("git branch master%s"% masterbranch)3123if not self.cloneBare:3124system("git checkout -f")3125else:3126print"Could not detect main branch. No checkout/master branch created."31273128# auto-set this variable if invoked with --use-client-spec3129if self.useClientSpec_from_options:3130system("git config --bool git-p4.useclientspec true")31313132return True31333134classP4Branches(Command):3135def__init__(self):3136 Command.__init__(self)3137 self.options = [ ]3138 self.description = ("Shows the git branches that hold imports and their "3139+"corresponding perforce depot paths")3140 self.verbose =False31413142defrun(self, args):3143iforiginP4BranchesExist():3144createOrUpdateBranchesFromOrigin()31453146 cmdline ="git rev-parse --symbolic "3147 cmdline +=" --remotes"31483149for line inread_pipe_lines(cmdline):3150 line = line.strip()31513152if not line.startswith('p4/')or line =="p4/HEAD":3153continue3154 branch = line31553156 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3157 settings =extractSettingsGitLog(log)31583159print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3160return True31613162classHelpFormatter(optparse.IndentedHelpFormatter):3163def__init__(self):3164 optparse.IndentedHelpFormatter.__init__(self)31653166defformat_description(self, description):3167if description:3168return description +"\n"3169else:3170return""31713172defprintUsage(commands):3173print"usage:%s<command> [options]"% sys.argv[0]3174print""3175print"valid commands:%s"%", ".join(commands)3176print""3177print"Try%s<command> --help for command specific help."% sys.argv[0]3178print""31793180commands = {3181"debug": P4Debug,3182"submit": P4Submit,3183"commit": P4Submit,3184"sync": P4Sync,3185"rebase": P4Rebase,3186"clone": P4Clone,3187"rollback": P4RollBack,3188"branches": P4Branches3189}319031913192defmain():3193iflen(sys.argv[1:]) ==0:3194printUsage(commands.keys())3195 sys.exit(2)31963197 cmdName = sys.argv[1]3198try:3199 klass = commands[cmdName]3200 cmd =klass()3201exceptKeyError:3202print"unknown command%s"% cmdName3203print""3204printUsage(commands.keys())3205 sys.exit(2)32063207 options = cmd.options3208 cmd.gitdir = os.environ.get("GIT_DIR",None)32093210 args = sys.argv[2:]32113212 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3213if cmd.needsGit:3214 options.append(optparse.make_option("--git-dir", dest="gitdir"))32153216 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3217 options,3218 description = cmd.description,3219 formatter =HelpFormatter())32203221(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3222global verbose3223 verbose = cmd.verbose3224if cmd.needsGit:3225if cmd.gitdir ==None:3226 cmd.gitdir = os.path.abspath(".git")3227if notisValidGitDir(cmd.gitdir):3228 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3229if os.path.exists(cmd.gitdir):3230 cdup =read_pipe("git rev-parse --show-cdup").strip()3231iflen(cdup) >0:3232chdir(cdup);32333234if notisValidGitDir(cmd.gitdir):3235ifisValidGitDir(cmd.gitdir +"/.git"):3236 cmd.gitdir +="/.git"3237else:3238die("fatal: cannot locate git repository at%s"% cmd.gitdir)32393240 os.environ["GIT_DIR"] = cmd.gitdir32413242if not cmd.run(args):3243 parser.print_help()3244 sys.exit(2)324532463247if __name__ =='__main__':3248main()