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 optparse, sys, os, marshal, subprocess, shelve 12import tempfile, getopt, os.path, time, platform 13import re, shutil 14 15verbose =False 16 17# Only labels/tags matching this will be imported/exported 18defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' 19 20defp4_build_cmd(cmd): 21"""Build a suitable p4 command line. 22 23 This consolidates building and returning a p4 command line into one 24 location. It means that hooking into the environment, or other configuration 25 can be done more easily. 26 """ 27 real_cmd = ["p4"] 28 29 user =gitConfig("git-p4.user") 30iflen(user) >0: 31 real_cmd += ["-u",user] 32 33 password =gitConfig("git-p4.password") 34iflen(password) >0: 35 real_cmd += ["-P", password] 36 37 port =gitConfig("git-p4.port") 38iflen(port) >0: 39 real_cmd += ["-p", port] 40 41 host =gitConfig("git-p4.host") 42iflen(host) >0: 43 real_cmd += ["-H", host] 44 45 client =gitConfig("git-p4.client") 46iflen(client) >0: 47 real_cmd += ["-c", client] 48 49 50ifisinstance(cmd,basestring): 51 real_cmd =' '.join(real_cmd) +' '+ cmd 52else: 53 real_cmd += cmd 54return real_cmd 55 56defchdir(dir): 57# P4 uses the PWD environment variable rather than getcwd(). Since we're 58# not using the shell, we have to set it ourselves. This path could 59# be relative, so go there first, then figure out where we ended up. 60 os.chdir(dir) 61 os.environ['PWD'] = os.getcwd() 62 63defdie(msg): 64if verbose: 65raiseException(msg) 66else: 67 sys.stderr.write(msg +"\n") 68 sys.exit(1) 69 70defwrite_pipe(c, stdin): 71if verbose: 72 sys.stderr.write('Writing pipe:%s\n'%str(c)) 73 74 expand =isinstance(c,basestring) 75 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) 76 pipe = p.stdin 77 val = pipe.write(stdin) 78 pipe.close() 79if p.wait(): 80die('Command failed:%s'%str(c)) 81 82return val 83 84defp4_write_pipe(c, stdin): 85 real_cmd =p4_build_cmd(c) 86returnwrite_pipe(real_cmd, stdin) 87 88defread_pipe(c, ignore_error=False): 89if verbose: 90 sys.stderr.write('Reading pipe:%s\n'%str(c)) 91 92 expand =isinstance(c,basestring) 93 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 94 pipe = p.stdout 95 val = pipe.read() 96if p.wait()and not ignore_error: 97die('Command failed:%s'%str(c)) 98 99return val 100 101defp4_read_pipe(c, ignore_error=False): 102 real_cmd =p4_build_cmd(c) 103returnread_pipe(real_cmd, ignore_error) 104 105defread_pipe_lines(c): 106if verbose: 107 sys.stderr.write('Reading pipe:%s\n'%str(c)) 108 109 expand =isinstance(c, basestring) 110 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) 111 pipe = p.stdout 112 val = pipe.readlines() 113if pipe.close()or p.wait(): 114die('Command failed:%s'%str(c)) 115 116return val 117 118defp4_read_pipe_lines(c): 119"""Specifically invoke p4 on the command supplied. """ 120 real_cmd =p4_build_cmd(c) 121returnread_pipe_lines(real_cmd) 122 123defp4_has_command(cmd): 124"""Ask p4 for help on this command. If it returns an error, the 125 command does not exist in this version of p4.""" 126 real_cmd =p4_build_cmd(["help", cmd]) 127 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, 128 stderr=subprocess.PIPE) 129 p.communicate() 130return p.returncode ==0 131 132defp4_has_move_command(): 133"""See if the move command exists, that it supports -k, and that 134 it has not been administratively disabled. The arguments 135 must be correct, but the filenames do not have to exist. Use 136 ones with wildcards so even if they exist, it will fail.""" 137 138if notp4_has_command("move"): 139return False 140 cmd =p4_build_cmd(["move","-k","@from","@to"]) 141 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 142(out, err) = p.communicate() 143# return code will be 1 in either case 144if err.find("Invalid option") >=0: 145return False 146if err.find("disabled") >=0: 147return False 148# assume it failed because @... was invalid changelist 149return True 150 151defsystem(cmd): 152 expand =isinstance(cmd,basestring) 153if verbose: 154 sys.stderr.write("executing%s\n"%str(cmd)) 155 subprocess.check_call(cmd, shell=expand) 156 157defp4_system(cmd): 158"""Specifically invoke p4 as the system command. """ 159 real_cmd =p4_build_cmd(cmd) 160 expand =isinstance(real_cmd, basestring) 161 subprocess.check_call(real_cmd, shell=expand) 162 163defp4_integrate(src, dest): 164p4_system(["integrate","-Dt",wildcard_encode(src),wildcard_encode(dest)]) 165 166defp4_sync(f, *options): 167p4_system(["sync"] +list(options) + [wildcard_encode(f)]) 168 169defp4_add(f): 170# forcibly add file names with wildcards 171ifwildcard_present(f): 172p4_system(["add","-f", f]) 173else: 174p4_system(["add", f]) 175 176defp4_delete(f): 177p4_system(["delete",wildcard_encode(f)]) 178 179defp4_edit(f): 180p4_system(["edit",wildcard_encode(f)]) 181 182defp4_revert(f): 183p4_system(["revert",wildcard_encode(f)]) 184 185defp4_reopen(type, f): 186p4_system(["reopen","-t",type,wildcard_encode(f)]) 187 188defp4_move(src, dest): 189p4_system(["move","-k",wildcard_encode(src),wildcard_encode(dest)]) 190 191defp4_describe(change): 192"""Make sure it returns a valid result by checking for 193 the presence of field "time". Return a dict of the 194 results.""" 195 196 ds =p4CmdList(["describe","-s",str(change)]) 197iflen(ds) !=1: 198die("p4 describe -s%ddid not return 1 result:%s"% (change,str(ds))) 199 200 d = ds[0] 201 202if"p4ExitCode"in d: 203die("p4 describe -s%dexited with%d:%s"% (change, d["p4ExitCode"], 204str(d))) 205if"code"in d: 206if d["code"] =="error": 207die("p4 describe -s%dreturned error code:%s"% (change,str(d))) 208 209if"time"not in d: 210die("p4 describe -s%dreturned no\"time\":%s"% (change,str(d))) 211 212return d 213 214# 215# Canonicalize the p4 type and return a tuple of the 216# base type, plus any modifiers. See "p4 help filetypes" 217# for a list and explanation. 218# 219defsplit_p4_type(p4type): 220 221 p4_filetypes_historical = { 222"ctempobj":"binary+Sw", 223"ctext":"text+C", 224"cxtext":"text+Cx", 225"ktext":"text+k", 226"kxtext":"text+kx", 227"ltext":"text+F", 228"tempobj":"binary+FSw", 229"ubinary":"binary+F", 230"uresource":"resource+F", 231"uxbinary":"binary+Fx", 232"xbinary":"binary+x", 233"xltext":"text+Fx", 234"xtempobj":"binary+Swx", 235"xtext":"text+x", 236"xunicode":"unicode+x", 237"xutf16":"utf16+x", 238} 239if p4type in p4_filetypes_historical: 240 p4type = p4_filetypes_historical[p4type] 241 mods ="" 242 s = p4type.split("+") 243 base = s[0] 244 mods ="" 245iflen(s) >1: 246 mods = s[1] 247return(base, mods) 248 249# 250# return the raw p4 type of a file (text, text+ko, etc) 251# 252defp4_type(file): 253 results =p4CmdList(["fstat","-T","headType",file]) 254return results[0]['headType'] 255 256# 257# Given a type base and modifier, return a regexp matching 258# the keywords that can be expanded in the file 259# 260defp4_keywords_regexp_for_type(base, type_mods): 261if base in("text","unicode","binary"): 262 kwords =None 263if"ko"in type_mods: 264 kwords ='Id|Header' 265elif"k"in type_mods: 266 kwords ='Id|Header|Author|Date|DateTime|Change|File|Revision' 267else: 268return None 269 pattern = r""" 270 \$ # Starts with a dollar, followed by... 271 (%s) # one of the keywords, followed by... 272 (:[^$\n]+)? # possibly an old expansion, followed by... 273 \$ # another dollar 274 """% kwords 275return pattern 276else: 277return None 278 279# 280# Given a file, return a regexp matching the possible 281# RCS keywords that will be expanded, or None for files 282# with kw expansion turned off. 283# 284defp4_keywords_regexp_for_file(file): 285if not os.path.exists(file): 286return None 287else: 288(type_base, type_mods) =split_p4_type(p4_type(file)) 289returnp4_keywords_regexp_for_type(type_base, type_mods) 290 291defsetP4ExecBit(file, mode): 292# Reopens an already open file and changes the execute bit to match 293# the execute bit setting in the passed in mode. 294 295 p4Type ="+x" 296 297if notisModeExec(mode): 298 p4Type =getP4OpenedType(file) 299 p4Type = re.sub('^([cku]?)x(.*)','\\1\\2', p4Type) 300 p4Type = re.sub('(.*?\+.*?)x(.*?)','\\1\\2', p4Type) 301if p4Type[-1] =="+": 302 p4Type = p4Type[0:-1] 303 304p4_reopen(p4Type,file) 305 306defgetP4OpenedType(file): 307# Returns the perforce file type for the given file. 308 309 result =p4_read_pipe(["opened",wildcard_encode(file)]) 310 match = re.match(".*\((.+)\)\r?$", result) 311if match: 312return match.group(1) 313else: 314die("Could not determine file type for%s(result: '%s')"% (file, result)) 315 316# Return the set of all p4 labels 317defgetP4Labels(depotPaths): 318 labels =set() 319ifisinstance(depotPaths,basestring): 320 depotPaths = [depotPaths] 321 322for l inp4CmdList(["labels"] + ["%s..."% p for p in depotPaths]): 323 label = l['label'] 324 labels.add(label) 325 326return labels 327 328# Return the set of all git tags 329defgetGitTags(): 330 gitTags =set() 331for line inread_pipe_lines(["git","tag"]): 332 tag = line.strip() 333 gitTags.add(tag) 334return gitTags 335 336defdiffTreePattern(): 337# This is a simple generator for the diff tree regex pattern. This could be 338# a class variable if this and parseDiffTreeEntry were a part of a class. 339 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') 340while True: 341yield pattern 342 343defparseDiffTreeEntry(entry): 344"""Parses a single diff tree entry into its component elements. 345 346 See git-diff-tree(1) manpage for details about the format of the diff 347 output. This method returns a dictionary with the following elements: 348 349 src_mode - The mode of the source file 350 dst_mode - The mode of the destination file 351 src_sha1 - The sha1 for the source file 352 dst_sha1 - The sha1 fr the destination file 353 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) 354 status_score - The score for the status (applicable for 'C' and 'R' 355 statuses). This is None if there is no score. 356 src - The path for the source file. 357 dst - The path for the destination file. This is only present for 358 copy or renames. If it is not present, this is None. 359 360 If the pattern is not matched, None is returned.""" 361 362 match =diffTreePattern().next().match(entry) 363if match: 364return{ 365'src_mode': match.group(1), 366'dst_mode': match.group(2), 367'src_sha1': match.group(3), 368'dst_sha1': match.group(4), 369'status': match.group(5), 370'status_score': match.group(6), 371'src': match.group(7), 372'dst': match.group(10) 373} 374return None 375 376defisModeExec(mode): 377# Returns True if the given git mode represents an executable file, 378# otherwise False. 379return mode[-3:] =="755" 380 381defisModeExecChanged(src_mode, dst_mode): 382returnisModeExec(src_mode) !=isModeExec(dst_mode) 383 384defp4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None): 385 386ifisinstance(cmd,basestring): 387 cmd ="-G "+ cmd 388 expand =True 389else: 390 cmd = ["-G"] + cmd 391 expand =False 392 393 cmd =p4_build_cmd(cmd) 394if verbose: 395 sys.stderr.write("Opening pipe:%s\n"%str(cmd)) 396 397# Use a temporary file to avoid deadlocks without 398# subprocess.communicate(), which would put another copy 399# of stdout into memory. 400 stdin_file =None 401if stdin is not None: 402 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) 403ifisinstance(stdin,basestring): 404 stdin_file.write(stdin) 405else: 406for i in stdin: 407 stdin_file.write(i +'\n') 408 stdin_file.flush() 409 stdin_file.seek(0) 410 411 p4 = subprocess.Popen(cmd, 412 shell=expand, 413 stdin=stdin_file, 414 stdout=subprocess.PIPE) 415 416 result = [] 417try: 418while True: 419 entry = marshal.load(p4.stdout) 420if cb is not None: 421cb(entry) 422else: 423 result.append(entry) 424exceptEOFError: 425pass 426 exitCode = p4.wait() 427if exitCode !=0: 428 entry = {} 429 entry["p4ExitCode"] = exitCode 430 result.append(entry) 431 432return result 433 434defp4Cmd(cmd): 435list=p4CmdList(cmd) 436 result = {} 437for entry inlist: 438 result.update(entry) 439return result; 440 441defp4Where(depotPath): 442if not depotPath.endswith("/"): 443 depotPath +="/" 444 depotPath = depotPath +"..." 445 outputList =p4CmdList(["where", depotPath]) 446 output =None 447for entry in outputList: 448if"depotFile"in entry: 449if entry["depotFile"] == depotPath: 450 output = entry 451break 452elif"data"in entry: 453 data = entry.get("data") 454 space = data.find(" ") 455if data[:space] == depotPath: 456 output = entry 457break 458if output ==None: 459return"" 460if output["code"] =="error": 461return"" 462 clientPath ="" 463if"path"in output: 464 clientPath = output.get("path") 465elif"data"in output: 466 data = output.get("data") 467 lastSpace = data.rfind(" ") 468 clientPath = data[lastSpace +1:] 469 470if clientPath.endswith("..."): 471 clientPath = clientPath[:-3] 472return clientPath 473 474defcurrentGitBranch(): 475returnread_pipe("git name-rev HEAD").split(" ")[1].strip() 476 477defisValidGitDir(path): 478if(os.path.exists(path +"/HEAD") 479and os.path.exists(path +"/refs")and os.path.exists(path +"/objects")): 480return True; 481return False 482 483defparseRevision(ref): 484returnread_pipe("git rev-parse%s"% ref).strip() 485 486defbranchExists(ref): 487 rev =read_pipe(["git","rev-parse","-q","--verify", ref], 488 ignore_error=True) 489returnlen(rev) >0 490 491defextractLogMessageFromGitCommit(commit): 492 logMessage ="" 493 494## fixme: title is first line of commit, not 1st paragraph. 495 foundTitle =False 496for log inread_pipe_lines("git cat-file commit%s"% commit): 497if not foundTitle: 498iflen(log) ==1: 499 foundTitle =True 500continue 501 502 logMessage += log 503return logMessage 504 505defextractSettingsGitLog(log): 506 values = {} 507for line in log.split("\n"): 508 line = line.strip() 509 m = re.search(r"^ *\[git-p4: (.*)\]$", line) 510if not m: 511continue 512 513 assignments = m.group(1).split(':') 514for a in assignments: 515 vals = a.split('=') 516 key = vals[0].strip() 517 val = ('='.join(vals[1:])).strip() 518if val.endswith('\"')and val.startswith('"'): 519 val = val[1:-1] 520 521 values[key] = val 522 523 paths = values.get("depot-paths") 524if not paths: 525 paths = values.get("depot-path") 526if paths: 527 values['depot-paths'] = paths.split(',') 528return values 529 530defgitBranchExists(branch): 531 proc = subprocess.Popen(["git","rev-parse", branch], 532 stderr=subprocess.PIPE, stdout=subprocess.PIPE); 533return proc.wait() ==0; 534 535_gitConfig = {} 536defgitConfig(key, args =None):# set args to "--bool", for instance 537if not _gitConfig.has_key(key): 538 argsFilter ="" 539if args !=None: 540 argsFilter ="%s"% args 541 cmd ="git config%s%s"% (argsFilter, key) 542 _gitConfig[key] =read_pipe(cmd, ignore_error=True).strip() 543return _gitConfig[key] 544 545defgitConfigList(key): 546if not _gitConfig.has_key(key): 547 _gitConfig[key] =read_pipe("git config --get-all%s"% key, ignore_error=True).strip().split(os.linesep) 548return _gitConfig[key] 549 550defp4BranchesInGit(branchesAreInRemotes =True): 551 branches = {} 552 553 cmdline ="git rev-parse --symbolic " 554if branchesAreInRemotes: 555 cmdline +=" --remotes" 556else: 557 cmdline +=" --branches" 558 559for line inread_pipe_lines(cmdline): 560 line = line.strip() 561 562## only import to p4/ 563if not line.startswith('p4/')or line =="p4/HEAD": 564continue 565 branch = line 566 567# strip off p4 568 branch = re.sub("^p4/","", line) 569 570 branches[branch] =parseRevision(line) 571return branches 572 573deffindUpstreamBranchPoint(head ="HEAD"): 574 branches =p4BranchesInGit() 575# map from depot-path to branch name 576 branchByDepotPath = {} 577for branch in branches.keys(): 578 tip = branches[branch] 579 log =extractLogMessageFromGitCommit(tip) 580 settings =extractSettingsGitLog(log) 581if settings.has_key("depot-paths"): 582 paths =",".join(settings["depot-paths"]) 583 branchByDepotPath[paths] ="remotes/p4/"+ branch 584 585 settings =None 586 parent =0 587while parent <65535: 588 commit = head +"~%s"% parent 589 log =extractLogMessageFromGitCommit(commit) 590 settings =extractSettingsGitLog(log) 591if settings.has_key("depot-paths"): 592 paths =",".join(settings["depot-paths"]) 593if branchByDepotPath.has_key(paths): 594return[branchByDepotPath[paths], settings] 595 596 parent = parent +1 597 598return["", settings] 599 600defcreateOrUpdateBranchesFromOrigin(localRefPrefix ="refs/remotes/p4/", silent=True): 601if not silent: 602print("Creating/updating branch(es) in%sbased on origin branch(es)" 603% localRefPrefix) 604 605 originPrefix ="origin/p4/" 606 607for line inread_pipe_lines("git rev-parse --symbolic --remotes"): 608 line = line.strip() 609if(not line.startswith(originPrefix))or line.endswith("HEAD"): 610continue 611 612 headName = line[len(originPrefix):] 613 remoteHead = localRefPrefix + headName 614 originHead = line 615 616 original =extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) 617if(not original.has_key('depot-paths') 618or not original.has_key('change')): 619continue 620 621 update =False 622if notgitBranchExists(remoteHead): 623if verbose: 624print"creating%s"% remoteHead 625 update =True 626else: 627 settings =extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) 628if settings.has_key('change') >0: 629if settings['depot-paths'] == original['depot-paths']: 630 originP4Change =int(original['change']) 631 p4Change =int(settings['change']) 632if originP4Change > p4Change: 633print("%s(%s) is newer than%s(%s). " 634"Updating p4 branch from origin." 635% (originHead, originP4Change, 636 remoteHead, p4Change)) 637 update =True 638else: 639print("Ignoring:%swas imported from%swhile " 640"%swas imported from%s" 641% (originHead,','.join(original['depot-paths']), 642 remoteHead,','.join(settings['depot-paths']))) 643 644if update: 645system("git update-ref%s %s"% (remoteHead, originHead)) 646 647deforiginP4BranchesExist(): 648returngitBranchExists("origin")orgitBranchExists("origin/p4")orgitBranchExists("origin/p4/master") 649 650defp4ChangesForPaths(depotPaths, changeRange): 651assert depotPaths 652 cmd = ['changes'] 653for p in depotPaths: 654 cmd += ["%s...%s"% (p, changeRange)] 655 output =p4_read_pipe_lines(cmd) 656 657 changes = {} 658for line in output: 659 changeNum =int(line.split(" ")[1]) 660 changes[changeNum] =True 661 662 changelist = changes.keys() 663 changelist.sort() 664return changelist 665 666defp4PathStartsWith(path, prefix): 667# This method tries to remedy a potential mixed-case issue: 668# 669# If UserA adds //depot/DirA/file1 670# and UserB adds //depot/dira/file2 671# 672# we may or may not have a problem. If you have core.ignorecase=true, 673# we treat DirA and dira as the same directory 674 ignorecase =gitConfig("core.ignorecase","--bool") =="true" 675if ignorecase: 676return path.lower().startswith(prefix.lower()) 677return path.startswith(prefix) 678 679defgetClientSpec(): 680"""Look at the p4 client spec, create a View() object that contains 681 all the mappings, and return it.""" 682 683 specList =p4CmdList("client -o") 684iflen(specList) !=1: 685die('Output from "client -o" is%dlines, expecting 1'% 686len(specList)) 687 688# dictionary of all client parameters 689 entry = specList[0] 690 691# just the keys that start with "View" 692 view_keys = [ k for k in entry.keys()if k.startswith("View") ] 693 694# hold this new View 695 view =View() 696 697# append the lines, in order, to the view 698for view_num inrange(len(view_keys)): 699 k ="View%d"% view_num 700if k not in view_keys: 701die("Expected view key%smissing"% k) 702 view.append(entry[k]) 703 704return view 705 706defgetClientRoot(): 707"""Grab the client directory.""" 708 709 output =p4CmdList("client -o") 710iflen(output) !=1: 711die('Output from "client -o" is%dlines, expecting 1'%len(output)) 712 713 entry = output[0] 714if"Root"not in entry: 715die('Client has no "Root"') 716 717return entry["Root"] 718 719# 720# P4 wildcards are not allowed in filenames. P4 complains 721# if you simply add them, but you can force it with "-f", in 722# which case it translates them into %xx encoding internally. 723# 724defwildcard_decode(path): 725# Search for and fix just these four characters. Do % last so 726# that fixing it does not inadvertently create new %-escapes. 727# Cannot have * in a filename in windows; untested as to 728# what p4 would do in such a case. 729if not platform.system() =="Windows": 730 path = path.replace("%2A","*") 731 path = path.replace("%23","#") \ 732.replace("%40","@") \ 733.replace("%25","%") 734return path 735 736defwildcard_encode(path): 737# do % first to avoid double-encoding the %s introduced here 738 path = path.replace("%","%25") \ 739.replace("*","%2A") \ 740.replace("#","%23") \ 741.replace("@","%40") 742return path 743 744defwildcard_present(path): 745 m = re.search("[*#@%]", path) 746return m is not None 747 748class Command: 749def__init__(self): 750 self.usage ="usage: %prog [options]" 751 self.needsGit =True 752 self.verbose =False 753 754class P4UserMap: 755def__init__(self): 756 self.userMapFromPerforceServer =False 757 self.myP4UserId =None 758 759defp4UserId(self): 760if self.myP4UserId: 761return self.myP4UserId 762 763 results =p4CmdList("user -o") 764for r in results: 765if r.has_key('User'): 766 self.myP4UserId = r['User'] 767return r['User'] 768die("Could not find your p4 user id") 769 770defp4UserIsMe(self, p4User): 771# return True if the given p4 user is actually me 772 me = self.p4UserId() 773if not p4User or p4User != me: 774return False 775else: 776return True 777 778defgetUserCacheFilename(self): 779 home = os.environ.get("HOME", os.environ.get("USERPROFILE")) 780return home +"/.gitp4-usercache.txt" 781 782defgetUserMapFromPerforceServer(self): 783if self.userMapFromPerforceServer: 784return 785 self.users = {} 786 self.emails = {} 787 788for output inp4CmdList("users"): 789if not output.has_key("User"): 790continue 791 self.users[output["User"]] = output["FullName"] +" <"+ output["Email"] +">" 792 self.emails[output["Email"]] = output["User"] 793 794 795 s ='' 796for(key, val)in self.users.items(): 797 s +="%s\t%s\n"% (key.expandtabs(1), val.expandtabs(1)) 798 799open(self.getUserCacheFilename(),"wb").write(s) 800 self.userMapFromPerforceServer =True 801 802defloadUserMapFromCache(self): 803 self.users = {} 804 self.userMapFromPerforceServer =False 805try: 806 cache =open(self.getUserCacheFilename(),"rb") 807 lines = cache.readlines() 808 cache.close() 809for line in lines: 810 entry = line.strip().split("\t") 811 self.users[entry[0]] = entry[1] 812exceptIOError: 813 self.getUserMapFromPerforceServer() 814 815classP4Debug(Command): 816def__init__(self): 817 Command.__init__(self) 818 self.options = [] 819 self.description ="A tool to debug the output of p4 -G." 820 self.needsGit =False 821 822defrun(self, args): 823 j =0 824for output inp4CmdList(args): 825print'Element:%d'% j 826 j +=1 827print output 828return True 829 830classP4RollBack(Command): 831def__init__(self): 832 Command.__init__(self) 833 self.options = [ 834 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") 835] 836 self.description ="A tool to debug the multi-branch import. Don't use :)" 837 self.rollbackLocalBranches =False 838 839defrun(self, args): 840iflen(args) !=1: 841return False 842 maxChange =int(args[0]) 843 844if"p4ExitCode"inp4Cmd("changes -m 1"): 845die("Problems executing p4"); 846 847if self.rollbackLocalBranches: 848 refPrefix ="refs/heads/" 849 lines =read_pipe_lines("git rev-parse --symbolic --branches") 850else: 851 refPrefix ="refs/remotes/" 852 lines =read_pipe_lines("git rev-parse --symbolic --remotes") 853 854for line in lines: 855if self.rollbackLocalBranches or(line.startswith("p4/")and line !="p4/HEAD\n"): 856 line = line.strip() 857 ref = refPrefix + line 858 log =extractLogMessageFromGitCommit(ref) 859 settings =extractSettingsGitLog(log) 860 861 depotPaths = settings['depot-paths'] 862 change = settings['change'] 863 864 changed =False 865 866iflen(p4Cmd("changes -m 1 "+' '.join(['%s...@%s'% (p, maxChange) 867for p in depotPaths]))) ==0: 868print"Branch%sdid not exist at change%s, deleting."% (ref, maxChange) 869system("git update-ref -d%s`git rev-parse%s`"% (ref, ref)) 870continue 871 872while change andint(change) > maxChange: 873 changed =True 874if self.verbose: 875print"%sis at%s; rewinding towards%s"% (ref, change, maxChange) 876system("git update-ref%s\"%s^\""% (ref, ref)) 877 log =extractLogMessageFromGitCommit(ref) 878 settings =extractSettingsGitLog(log) 879 880 881 depotPaths = settings['depot-paths'] 882 change = settings['change'] 883 884if changed: 885print"%srewound to%s"% (ref, change) 886 887return True 888 889classP4Submit(Command, P4UserMap): 890 891 conflict_behavior_choices = ("ask","skip","quit") 892 893def__init__(self): 894 Command.__init__(self) 895 P4UserMap.__init__(self) 896 self.options = [ 897 optparse.make_option("--origin", dest="origin"), 898 optparse.make_option("-M", dest="detectRenames", action="store_true"), 899# preserve the user, requires relevant p4 permissions 900 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), 901 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), 902 optparse.make_option("--dry-run","-n", dest="dry_run", action="store_true"), 903 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), 904 optparse.make_option("--conflict", dest="conflict_behavior", 905 choices=self.conflict_behavior_choices) 906] 907 self.description ="Submit changes from git to the perforce depot." 908 self.usage +=" [name of git branch to submit into perforce depot]" 909 self.origin ="" 910 self.detectRenames =False 911 self.preserveUser =gitConfig("git-p4.preserveUser").lower() =="true" 912 self.dry_run =False 913 self.prepare_p4_only =False 914 self.conflict_behavior =None 915 self.isWindows = (platform.system() =="Windows") 916 self.exportLabels =False 917 self.p4HasMoveCommand =p4_has_move_command() 918 919defcheck(self): 920iflen(p4CmdList("opened ...")) >0: 921die("You have files opened with perforce! Close them before starting the sync.") 922 923defseparate_jobs_from_description(self, message): 924"""Extract and return a possible Jobs field in the commit 925 message. It goes into a separate section in the p4 change 926 specification. 927 928 A jobs line starts with "Jobs:" and looks like a new field 929 in a form. Values are white-space separated on the same 930 line or on following lines that start with a tab. 931 932 This does not parse and extract the full git commit message 933 like a p4 form. It just sees the Jobs: line as a marker 934 to pass everything from then on directly into the p4 form, 935 but outside the description section. 936 937 Return a tuple (stripped log message, jobs string).""" 938 939 m = re.search(r'^Jobs:', message, re.MULTILINE) 940if m is None: 941return(message,None) 942 943 jobtext = message[m.start():] 944 stripped_message = message[:m.start()].rstrip() 945return(stripped_message, jobtext) 946 947defprepareLogMessage(self, template, message, jobs): 948"""Edits the template returned from "p4 change -o" to insert 949 the message in the Description field, and the jobs text in 950 the Jobs field.""" 951 result ="" 952 953 inDescriptionSection =False 954 955for line in template.split("\n"): 956if line.startswith("#"): 957 result += line +"\n" 958continue 959 960if inDescriptionSection: 961if line.startswith("Files:")or line.startswith("Jobs:"): 962 inDescriptionSection =False 963# insert Jobs section 964if jobs: 965 result += jobs +"\n" 966else: 967continue 968else: 969if line.startswith("Description:"): 970 inDescriptionSection =True 971 line +="\n" 972for messageLine in message.split("\n"): 973 line +="\t"+ messageLine +"\n" 974 975 result += line +"\n" 976 977return result 978 979defpatchRCSKeywords(self,file, pattern): 980# Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern 981(handle, outFileName) = tempfile.mkstemp(dir='.') 982try: 983 outFile = os.fdopen(handle,"w+") 984 inFile =open(file,"r") 985 regexp = re.compile(pattern, re.VERBOSE) 986for line in inFile.readlines(): 987 line = regexp.sub(r'$\1$', line) 988 outFile.write(line) 989 inFile.close() 990 outFile.close() 991# Forcibly overwrite the original file 992 os.unlink(file) 993 shutil.move(outFileName,file) 994except: 995# cleanup our temporary file 996 os.unlink(outFileName) 997print"Failed to strip RCS keywords in%s"%file 998raise 9991000print"Patched up RCS keywords in%s"%file10011002defp4UserForCommit(self,id):1003# Return the tuple (perforce user,git email) for a given git commit id1004 self.getUserMapFromPerforceServer()1005 gitEmail =read_pipe("git log --max-count=1 --format='%%ae'%s"%id)1006 gitEmail = gitEmail.strip()1007if not self.emails.has_key(gitEmail):1008return(None,gitEmail)1009else:1010return(self.emails[gitEmail],gitEmail)10111012defcheckValidP4Users(self,commits):1013# check if any git authors cannot be mapped to p4 users1014foridin commits:1015(user,email) = self.p4UserForCommit(id)1016if not user:1017 msg ="Cannot find p4 user for email%sin commit%s."% (email,id)1018ifgitConfig('git-p4.allowMissingP4Users').lower() =="true":1019print"%s"% msg1020else:1021die("Error:%s\nSet git-p4.allowMissingP4Users to true to allow this."% msg)10221023deflastP4Changelist(self):1024# Get back the last changelist number submitted in this client spec. This1025# then gets used to patch up the username in the change. If the same1026# client spec is being used by multiple processes then this might go1027# wrong.1028 results =p4CmdList("client -o")# find the current client1029 client =None1030for r in results:1031if r.has_key('Client'):1032 client = r['Client']1033break1034if not client:1035die("could not get client spec")1036 results =p4CmdList(["changes","-c", client,"-m","1"])1037for r in results:1038if r.has_key('change'):1039return r['change']1040die("Could not get changelist number for last submit - cannot patch up user details")10411042defmodifyChangelistUser(self, changelist, newUser):1043# fixup the user field of a changelist after it has been submitted.1044 changes =p4CmdList("change -o%s"% changelist)1045iflen(changes) !=1:1046die("Bad output from p4 change modifying%sto user%s"%1047(changelist, newUser))10481049 c = changes[0]1050if c['User'] == newUser:return# nothing to do1051 c['User'] = newUser1052input= marshal.dumps(c)10531054 result =p4CmdList("change -f -i", stdin=input)1055for r in result:1056if r.has_key('code'):1057if r['code'] =='error':1058die("Could not modify user field of changelist%sto%s:%s"% (changelist, newUser, r['data']))1059if r.has_key('data'):1060print("Updated user field for changelist%sto%s"% (changelist, newUser))1061return1062die("Could not modify user field of changelist%sto%s"% (changelist, newUser))10631064defcanChangeChangelists(self):1065# check to see if we have p4 admin or super-user permissions, either of1066# which are required to modify changelists.1067 results =p4CmdList(["protects", self.depotPath])1068for r in results:1069if r.has_key('perm'):1070if r['perm'] =='admin':1071return11072if r['perm'] =='super':1073return11074return010751076defprepareSubmitTemplate(self):1077"""Run "p4 change -o" to grab a change specification template.1078 This does not use "p4 -G", as it is nice to keep the submission1079 template in original order, since a human might edit it.10801081 Remove lines in the Files section that show changes to files1082 outside the depot path we're committing into."""10831084 template =""1085 inFilesSection =False1086for line inp4_read_pipe_lines(['change','-o']):1087if line.endswith("\r\n"):1088 line = line[:-2] +"\n"1089if inFilesSection:1090if line.startswith("\t"):1091# path starts and ends with a tab1092 path = line[1:]1093 lastTab = path.rfind("\t")1094if lastTab != -1:1095 path = path[:lastTab]1096if notp4PathStartsWith(path, self.depotPath):1097continue1098else:1099 inFilesSection =False1100else:1101if line.startswith("Files:"):1102 inFilesSection =True11031104 template += line11051106return template11071108defedit_template(self, template_file):1109"""Invoke the editor to let the user change the submission1110 message. Return true if okay to continue with the submit."""11111112# if configured to skip the editing part, just submit1113ifgitConfig("git-p4.skipSubmitEdit") =="true":1114return True11151116# look at the modification time, to check later if the user saved1117# the file1118 mtime = os.stat(template_file).st_mtime11191120# invoke the editor1121if os.environ.has_key("P4EDITOR")and(os.environ.get("P4EDITOR") !=""):1122 editor = os.environ.get("P4EDITOR")1123else:1124 editor =read_pipe("git var GIT_EDITOR").strip()1125system(editor +" "+ template_file)11261127# If the file was not saved, prompt to see if this patch should1128# be skipped. But skip this verification step if configured so.1129ifgitConfig("git-p4.skipSubmitEditCheck") =="true":1130return True11311132# modification time updated means user saved the file1133if os.stat(template_file).st_mtime > mtime:1134return True11351136while True:1137 response =raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")1138if response =='y':1139return True1140if response =='n':1141return False11421143defapplyCommit(self,id):1144"""Apply one commit, return True if it succeeded."""11451146print"Applying",read_pipe(["git","show","-s",1147"--format=format:%h%s",id])11481149(p4User, gitEmail) = self.p4UserForCommit(id)11501151 diff =read_pipe_lines("git diff-tree -r%s\"%s^\" \"%s\""% (self.diffOpts,id,id))1152 filesToAdd =set()1153 filesToDelete =set()1154 editedFiles =set()1155 pureRenameCopy =set()1156 filesToChangeExecBit = {}11571158for line in diff:1159 diff =parseDiffTreeEntry(line)1160 modifier = diff['status']1161 path = diff['src']1162if modifier =="M":1163p4_edit(path)1164ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1165 filesToChangeExecBit[path] = diff['dst_mode']1166 editedFiles.add(path)1167elif modifier =="A":1168 filesToAdd.add(path)1169 filesToChangeExecBit[path] = diff['dst_mode']1170if path in filesToDelete:1171 filesToDelete.remove(path)1172elif modifier =="D":1173 filesToDelete.add(path)1174if path in filesToAdd:1175 filesToAdd.remove(path)1176elif modifier =="C":1177 src, dest = diff['src'], diff['dst']1178p4_integrate(src, dest)1179 pureRenameCopy.add(dest)1180if diff['src_sha1'] != diff['dst_sha1']:1181p4_edit(dest)1182 pureRenameCopy.discard(dest)1183ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1184p4_edit(dest)1185 pureRenameCopy.discard(dest)1186 filesToChangeExecBit[dest] = diff['dst_mode']1187 os.unlink(dest)1188 editedFiles.add(dest)1189elif modifier =="R":1190 src, dest = diff['src'], diff['dst']1191if self.p4HasMoveCommand:1192p4_edit(src)# src must be open before move1193p4_move(src, dest)# opens for (move/delete, move/add)1194else:1195p4_integrate(src, dest)1196if diff['src_sha1'] != diff['dst_sha1']:1197p4_edit(dest)1198else:1199 pureRenameCopy.add(dest)1200ifisModeExecChanged(diff['src_mode'], diff['dst_mode']):1201if not self.p4HasMoveCommand:1202p4_edit(dest)# with move: already open, writable1203 filesToChangeExecBit[dest] = diff['dst_mode']1204if not self.p4HasMoveCommand:1205 os.unlink(dest)1206 filesToDelete.add(src)1207 editedFiles.add(dest)1208else:1209die("unknown modifier%sfor%s"% (modifier, path))12101211 diffcmd ="git format-patch -k --stdout\"%s^\"..\"%s\""% (id,id)1212 patchcmd = diffcmd +" | git apply "1213 tryPatchCmd = patchcmd +"--check -"1214 applyPatchCmd = patchcmd +"--check --apply -"1215 patch_succeeded =True12161217if os.system(tryPatchCmd) !=0:1218 fixed_rcs_keywords =False1219 patch_succeeded =False1220print"Unfortunately applying the change failed!"12211222# Patch failed, maybe it's just RCS keyword woes. Look through1223# the patch to see if that's possible.1224ifgitConfig("git-p4.attemptRCSCleanup","--bool") =="true":1225file=None1226 pattern =None1227 kwfiles = {}1228forfilein editedFiles | filesToDelete:1229# did this file's delta contain RCS keywords?1230 pattern =p4_keywords_regexp_for_file(file)12311232if pattern:1233# this file is a possibility...look for RCS keywords.1234 regexp = re.compile(pattern, re.VERBOSE)1235for line inread_pipe_lines(["git","diff","%s^..%s"% (id,id),file]):1236if regexp.search(line):1237if verbose:1238print"got keyword match on%sin%sin%s"% (pattern, line,file)1239 kwfiles[file] = pattern1240break12411242forfilein kwfiles:1243if verbose:1244print"zapping%swith%s"% (line,pattern)1245 self.patchRCSKeywords(file, kwfiles[file])1246 fixed_rcs_keywords =True12471248if fixed_rcs_keywords:1249print"Retrying the patch with RCS keywords cleaned up"1250if os.system(tryPatchCmd) ==0:1251 patch_succeeded =True12521253if not patch_succeeded:1254for f in editedFiles:1255p4_revert(f)1256return False12571258#1259# Apply the patch for real, and do add/delete/+x handling.1260#1261system(applyPatchCmd)12621263for f in filesToAdd:1264p4_add(f)1265for f in filesToDelete:1266p4_revert(f)1267p4_delete(f)12681269# Set/clear executable bits1270for f in filesToChangeExecBit.keys():1271 mode = filesToChangeExecBit[f]1272setP4ExecBit(f, mode)12731274#1275# Build p4 change description, starting with the contents1276# of the git commit message.1277#1278 logMessage =extractLogMessageFromGitCommit(id)1279 logMessage = logMessage.strip()1280(logMessage, jobs) = self.separate_jobs_from_description(logMessage)12811282 template = self.prepareSubmitTemplate()1283 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)12841285if self.preserveUser:1286 submitTemplate +="\n######## Actual user%s, modified after commit\n"% p4User12871288if self.checkAuthorship and not self.p4UserIsMe(p4User):1289 submitTemplate +="######## git author%sdoes not match your p4 account.\n"% gitEmail1290 submitTemplate +="######## Use option --preserve-user to modify authorship.\n"1291 submitTemplate +="######## Variable git-p4.skipUserNameCheck hides this message.\n"12921293 separatorLine ="######## everything below this line is just the diff #######\n"12941295# diff1296if os.environ.has_key("P4DIFF"):1297del(os.environ["P4DIFF"])1298 diff =""1299for editedFile in editedFiles:1300 diff +=p4_read_pipe(['diff','-du',1301wildcard_encode(editedFile)])13021303# new file diff1304 newdiff =""1305for newFile in filesToAdd:1306 newdiff +="==== new file ====\n"1307 newdiff +="--- /dev/null\n"1308 newdiff +="+++%s\n"% newFile1309 f =open(newFile,"r")1310for line in f.readlines():1311 newdiff +="+"+ line1312 f.close()13131314# change description file: submitTemplate, separatorLine, diff, newdiff1315(handle, fileName) = tempfile.mkstemp()1316 tmpFile = os.fdopen(handle,"w+")1317if self.isWindows:1318 submitTemplate = submitTemplate.replace("\n","\r\n")1319 separatorLine = separatorLine.replace("\n","\r\n")1320 newdiff = newdiff.replace("\n","\r\n")1321 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)1322 tmpFile.close()13231324if self.prepare_p4_only:1325#1326# Leave the p4 tree prepared, and the submit template around1327# and let the user decide what to do next1328#1329print1330print"P4 workspace prepared for submission."1331print"To submit or revert, go to client workspace"1332print" "+ self.clientPath1333print1334print"To submit, use\"p4 submit\"to write a new description,"1335print"or\"p4 submit -i%s\"to use the one prepared by" \1336"\"git p4\"."% fileName1337print"You can delete the file\"%s\"when finished."% fileName13381339if self.preserveUser and p4User and not self.p4UserIsMe(p4User):1340print"To preserve change ownership by user%s, you must\n" \1341"do\"p4 change -f <change>\"after submitting and\n" \1342"edit the User field."1343if pureRenameCopy:1344print"After submitting, renamed files must be re-synced."1345print"Invoke\"p4 sync -f\"on each of these files:"1346for f in pureRenameCopy:1347print" "+ f13481349print1350print"To revert the changes, use\"p4 revert ...\", and delete"1351print"the submit template file\"%s\""% fileName1352if filesToAdd:1353print"Since the commit adds new files, they must be deleted:"1354for f in filesToAdd:1355print" "+ f1356print1357return True13581359#1360# Let the user edit the change description, then submit it.1361#1362if self.edit_template(fileName):1363# read the edited message and submit1364 ret =True1365 tmpFile =open(fileName,"rb")1366 message = tmpFile.read()1367 tmpFile.close()1368 submitTemplate = message[:message.index(separatorLine)]1369if self.isWindows:1370 submitTemplate = submitTemplate.replace("\r\n","\n")1371p4_write_pipe(['submit','-i'], submitTemplate)13721373if self.preserveUser:1374if p4User:1375# Get last changelist number. Cannot easily get it from1376# the submit command output as the output is1377# unmarshalled.1378 changelist = self.lastP4Changelist()1379 self.modifyChangelistUser(changelist, p4User)13801381# The rename/copy happened by applying a patch that created a1382# new file. This leaves it writable, which confuses p4.1383for f in pureRenameCopy:1384p4_sync(f,"-f")13851386else:1387# skip this patch1388 ret =False1389print"Submission cancelled, undoing p4 changes."1390for f in editedFiles:1391p4_revert(f)1392for f in filesToAdd:1393p4_revert(f)1394 os.remove(f)1395for f in filesToDelete:1396p4_revert(f)13971398 os.remove(fileName)1399return ret14001401# Export git tags as p4 labels. Create a p4 label and then tag1402# with that.1403defexportGitTags(self, gitTags):1404 validLabelRegexp =gitConfig("git-p4.labelExportRegexp")1405iflen(validLabelRegexp) ==0:1406 validLabelRegexp = defaultLabelRegexp1407 m = re.compile(validLabelRegexp)14081409for name in gitTags:14101411if not m.match(name):1412if verbose:1413print"tag%sdoes not match regexp%s"% (name, validLabelRegexp)1414continue14151416# Get the p4 commit this corresponds to1417 logMessage =extractLogMessageFromGitCommit(name)1418 values =extractSettingsGitLog(logMessage)14191420if not values.has_key('change'):1421# a tag pointing to something not sent to p4; ignore1422if verbose:1423print"git tag%sdoes not give a p4 commit"% name1424continue1425else:1426 changelist = values['change']14271428# Get the tag details.1429 inHeader =True1430 isAnnotated =False1431 body = []1432for l inread_pipe_lines(["git","cat-file","-p", name]):1433 l = l.strip()1434if inHeader:1435if re.match(r'tag\s+', l):1436 isAnnotated =True1437elif re.match(r'\s*$', l):1438 inHeader =False1439continue1440else:1441 body.append(l)14421443if not isAnnotated:1444 body = ["lightweight tag imported by git p4\n"]14451446# Create the label - use the same view as the client spec we are using1447 clientSpec =getClientSpec()14481449 labelTemplate ="Label:%s\n"% name1450 labelTemplate +="Description:\n"1451for b in body:1452 labelTemplate +="\t"+ b +"\n"1453 labelTemplate +="View:\n"1454for mapping in clientSpec.mappings:1455 labelTemplate +="\t%s\n"% mapping.depot_side.path14561457if self.dry_run:1458print"Would create p4 label%sfor tag"% name1459elif self.prepare_p4_only:1460print"Not creating p4 label%sfor tag due to option" \1461" --prepare-p4-only"% name1462else:1463p4_write_pipe(["label","-i"], labelTemplate)14641465# Use the label1466p4_system(["tag","-l", name] +1467["%s@%s"% (mapping.depot_side.path, changelist)for mapping in clientSpec.mappings])14681469if verbose:1470print"created p4 label for tag%s"% name14711472defrun(self, args):1473iflen(args) ==0:1474 self.master =currentGitBranch()1475iflen(self.master) ==0or notgitBranchExists("refs/heads/%s"% self.master):1476die("Detecting current git branch failed!")1477eliflen(args) ==1:1478 self.master = args[0]1479if notbranchExists(self.master):1480die("Branch%sdoes not exist"% self.master)1481else:1482return False14831484 allowSubmit =gitConfig("git-p4.allowSubmit")1485iflen(allowSubmit) >0and not self.master in allowSubmit.split(","):1486die("%sis not in git-p4.allowSubmit"% self.master)14871488[upstream, settings] =findUpstreamBranchPoint()1489 self.depotPath = settings['depot-paths'][0]1490iflen(self.origin) ==0:1491 self.origin = upstream14921493if self.preserveUser:1494if not self.canChangeChangelists():1495die("Cannot preserve user names without p4 super-user or admin permissions")14961497# if not set from the command line, try the config file1498if self.conflict_behavior is None:1499 val =gitConfig("git-p4.conflict")1500if val:1501if val not in self.conflict_behavior_choices:1502die("Invalid value '%s' for config git-p4.conflict"% val)1503else:1504 val ="ask"1505 self.conflict_behavior = val15061507if self.verbose:1508print"Origin branch is "+ self.origin15091510iflen(self.depotPath) ==0:1511print"Internal error: cannot locate perforce depot path from existing branches"1512 sys.exit(128)15131514 self.useClientSpec =False1515ifgitConfig("git-p4.useclientspec","--bool") =="true":1516 self.useClientSpec =True1517if self.useClientSpec:1518 self.clientSpecDirs =getClientSpec()15191520if self.useClientSpec:1521# all files are relative to the client spec1522 self.clientPath =getClientRoot()1523else:1524 self.clientPath =p4Where(self.depotPath)15251526if self.clientPath =="":1527die("Error: Cannot locate perforce checkout of%sin client view"% self.depotPath)15281529print"Perforce checkout for depot path%slocated at%s"% (self.depotPath, self.clientPath)1530 self.oldWorkingDirectory = os.getcwd()15311532# ensure the clientPath exists1533 new_client_dir =False1534if not os.path.exists(self.clientPath):1535 new_client_dir =True1536 os.makedirs(self.clientPath)15371538chdir(self.clientPath)1539if self.dry_run:1540print"Would synchronize p4 checkout in%s"% self.clientPath1541else:1542print"Synchronizing p4 checkout..."1543if new_client_dir:1544# old one was destroyed, and maybe nobody told p41545p4_sync("...","-f")1546else:1547p4_sync("...")1548 self.check()15491550 commits = []1551for line inread_pipe_lines("git rev-list --no-merges%s..%s"% (self.origin, self.master)):1552 commits.append(line.strip())1553 commits.reverse()15541555if self.preserveUser or(gitConfig("git-p4.skipUserNameCheck") =="true"):1556 self.checkAuthorship =False1557else:1558 self.checkAuthorship =True15591560if self.preserveUser:1561 self.checkValidP4Users(commits)15621563#1564# Build up a set of options to be passed to diff when1565# submitting each commit to p4.1566#1567if self.detectRenames:1568# command-line -M arg1569 self.diffOpts ="-M"1570else:1571# If not explicitly set check the config variable1572 detectRenames =gitConfig("git-p4.detectRenames")15731574if detectRenames.lower() =="false"or detectRenames =="":1575 self.diffOpts =""1576elif detectRenames.lower() =="true":1577 self.diffOpts ="-M"1578else:1579 self.diffOpts ="-M%s"% detectRenames15801581# no command-line arg for -C or --find-copies-harder, just1582# config variables1583 detectCopies =gitConfig("git-p4.detectCopies")1584if detectCopies.lower() =="false"or detectCopies =="":1585pass1586elif detectCopies.lower() =="true":1587 self.diffOpts +=" -C"1588else:1589 self.diffOpts +=" -C%s"% detectCopies15901591ifgitConfig("git-p4.detectCopiesHarder","--bool") =="true":1592 self.diffOpts +=" --find-copies-harder"15931594#1595# Apply the commits, one at a time. On failure, ask if should1596# continue to try the rest of the patches, or quit.1597#1598if self.dry_run:1599print"Would apply"1600 applied = []1601 last =len(commits) -11602for i, commit inenumerate(commits):1603if self.dry_run:1604print" ",read_pipe(["git","show","-s",1605"--format=format:%h%s", commit])1606 ok =True1607else:1608 ok = self.applyCommit(commit)1609if ok:1610 applied.append(commit)1611else:1612if self.prepare_p4_only and i < last:1613print"Processing only the first commit due to option" \1614" --prepare-p4-only"1615break1616if i < last:1617 quit =False1618while True:1619# prompt for what to do, or use the option/variable1620if self.conflict_behavior =="ask":1621print"What do you want to do?"1622 response =raw_input("[s]kip this commit but apply"1623" the rest, or [q]uit? ")1624if not response:1625continue1626elif self.conflict_behavior =="skip":1627 response ="s"1628elif self.conflict_behavior =="quit":1629 response ="q"1630else:1631die("Unknown conflict_behavior '%s'"%1632 self.conflict_behavior)16331634if response[0] =="s":1635print"Skipping this commit, but applying the rest"1636break1637if response[0] =="q":1638print"Quitting"1639 quit =True1640break1641if quit:1642break16431644chdir(self.oldWorkingDirectory)16451646if self.dry_run:1647pass1648elif self.prepare_p4_only:1649pass1650eliflen(commits) ==len(applied):1651print"All commits applied!"16521653 sync =P4Sync()1654 sync.run([])16551656 rebase =P4Rebase()1657 rebase.rebase()16581659else:1660iflen(applied) ==0:1661print"No commits applied."1662else:1663print"Applied only the commits marked with '*':"1664for c in commits:1665if c in applied:1666 star ="*"1667else:1668 star =" "1669print star,read_pipe(["git","show","-s",1670"--format=format:%h%s", c])1671print"You will have to do 'git p4 sync' and rebase."16721673ifgitConfig("git-p4.exportLabels","--bool") =="true":1674 self.exportLabels =True16751676if self.exportLabels:1677 p4Labels =getP4Labels(self.depotPath)1678 gitTags =getGitTags()16791680 missingGitTags = gitTags - p4Labels1681 self.exportGitTags(missingGitTags)16821683# exit with error unless everything applied perfecly1684iflen(commits) !=len(applied):1685 sys.exit(1)16861687return True16881689classView(object):1690"""Represent a p4 view ("p4 help views"), and map files in a1691 repo according to the view."""16921693classPath(object):1694"""A depot or client path, possibly containing wildcards.1695 The only one supported is ... at the end, currently.1696 Initialize with the full path, with //depot or //client."""16971698def__init__(self, path, is_depot):1699 self.path = path1700 self.is_depot = is_depot1701 self.find_wildcards()1702# remember the prefix bit, useful for relative mappings1703 m = re.match("(//[^/]+/)", self.path)1704if not m:1705die("Path%sdoes not start with //prefix/"% self.path)1706 prefix = m.group(1)1707if not self.is_depot:1708# strip //client/ on client paths1709 self.path = self.path[len(prefix):]17101711deffind_wildcards(self):1712"""Make sure wildcards are valid, and set up internal1713 variables."""17141715 self.ends_triple_dot =False1716# There are three wildcards allowed in p4 views1717# (see "p4 help views"). This code knows how to1718# handle "..." (only at the end), but cannot deal with1719# "%%n" or "*". Only check the depot_side, as p4 should1720# validate that the client_side matches too.1721if re.search(r'%%[1-9]', self.path):1722die("Can't handle%%n wildcards in view:%s"% self.path)1723if self.path.find("*") >=0:1724die("Can't handle * wildcards in view:%s"% self.path)1725 triple_dot_index = self.path.find("...")1726if triple_dot_index >=0:1727if triple_dot_index !=len(self.path) -3:1728die("Can handle only single ... wildcard, at end:%s"%1729 self.path)1730 self.ends_triple_dot =True17311732defensure_compatible(self, other_path):1733"""Make sure the wildcards agree."""1734if self.ends_triple_dot != other_path.ends_triple_dot:1735die("Both paths must end with ... if either does;\n"+1736"paths:%s %s"% (self.path, other_path.path))17371738defmatch_wildcards(self, test_path):1739"""See if this test_path matches us, and fill in the value1740 of the wildcards if so. Returns a tuple of1741 (True|False, wildcards[]). For now, only the ... at end1742 is supported, so at most one wildcard."""1743if self.ends_triple_dot:1744 dotless = self.path[:-3]1745if test_path.startswith(dotless):1746 wildcard = test_path[len(dotless):]1747return(True, [ wildcard ])1748else:1749if test_path == self.path:1750return(True, [])1751return(False, [])17521753defmatch(self, test_path):1754"""Just return if it matches; don't bother with the wildcards."""1755 b, _ = self.match_wildcards(test_path)1756return b17571758deffill_in_wildcards(self, wildcards):1759"""Return the relative path, with the wildcards filled in1760 if there are any."""1761if self.ends_triple_dot:1762return self.path[:-3] + wildcards[0]1763else:1764return self.path17651766classMapping(object):1767def__init__(self, depot_side, client_side, overlay, exclude):1768# depot_side is without the trailing /... if it had one1769 self.depot_side = View.Path(depot_side, is_depot=True)1770 self.client_side = View.Path(client_side, is_depot=False)1771 self.overlay = overlay # started with "+"1772 self.exclude = exclude # started with "-"1773assert not(self.overlay and self.exclude)1774 self.depot_side.ensure_compatible(self.client_side)17751776def__str__(self):1777 c =" "1778if self.overlay:1779 c ="+"1780if self.exclude:1781 c ="-"1782return"View.Mapping:%s%s->%s"% \1783(c, self.depot_side.path, self.client_side.path)17841785defmap_depot_to_client(self, depot_path):1786"""Calculate the client path if using this mapping on the1787 given depot path; does not consider the effect of other1788 mappings in a view. Even excluded mappings are returned."""1789 matches, wildcards = self.depot_side.match_wildcards(depot_path)1790if not matches:1791return""1792 client_path = self.client_side.fill_in_wildcards(wildcards)1793return client_path17941795#1796# View methods1797#1798def__init__(self):1799 self.mappings = []18001801defappend(self, view_line):1802"""Parse a view line, splitting it into depot and client1803 sides. Append to self.mappings, preserving order."""18041805# Split the view line into exactly two words. P4 enforces1806# structure on these lines that simplifies this quite a bit.1807#1808# Either or both words may be double-quoted.1809# Single quotes do not matter.1810# Double-quote marks cannot occur inside the words.1811# A + or - prefix is also inside the quotes.1812# There are no quotes unless they contain a space.1813# The line is already white-space stripped.1814# The two words are separated by a single space.1815#1816if view_line[0] =='"':1817# First word is double quoted. Find its end.1818 close_quote_index = view_line.find('"',1)1819if close_quote_index <=0:1820die("No first-word closing quote found:%s"% view_line)1821 depot_side = view_line[1:close_quote_index]1822# skip closing quote and space1823 rhs_index = close_quote_index +1+11824else:1825 space_index = view_line.find(" ")1826if space_index <=0:1827die("No word-splitting space found:%s"% view_line)1828 depot_side = view_line[0:space_index]1829 rhs_index = space_index +118301831if view_line[rhs_index] =='"':1832# Second word is double quoted. Make sure there is a1833# double quote at the end too.1834if not view_line.endswith('"'):1835die("View line with rhs quote should end with one:%s"%1836 view_line)1837# skip the quotes1838 client_side = view_line[rhs_index+1:-1]1839else:1840 client_side = view_line[rhs_index:]18411842# prefix + means overlay on previous mapping1843 overlay =False1844if depot_side.startswith("+"):1845 overlay =True1846 depot_side = depot_side[1:]18471848# prefix - means exclude this path1849 exclude =False1850if depot_side.startswith("-"):1851 exclude =True1852 depot_side = depot_side[1:]18531854 m = View.Mapping(depot_side, client_side, overlay, exclude)1855 self.mappings.append(m)18561857defmap_in_client(self, depot_path):1858"""Return the relative location in the client where this1859 depot file should live. Returns "" if the file should1860 not be mapped in the client."""18611862 paths_filled = []1863 client_path =""18641865# look at later entries first1866for m in self.mappings[::-1]:18671868# see where will this path end up in the client1869 p = m.map_depot_to_client(depot_path)18701871if p =="":1872# Depot path does not belong in client. Must remember1873# this, as previous items should not cause files to1874# exist in this path either. Remember that the list is1875# being walked from the end, which has higher precedence.1876# Overlap mappings do not exclude previous mappings.1877if not m.overlay:1878 paths_filled.append(m.client_side)18791880else:1881# This mapping matched; no need to search any further.1882# But, the mapping could be rejected if the client path1883# has already been claimed by an earlier mapping (i.e.1884# one later in the list, which we are walking backwards).1885 already_mapped_in_client =False1886for f in paths_filled:1887# this is View.Path.match1888if f.match(p):1889 already_mapped_in_client =True1890break1891if not already_mapped_in_client:1892# Include this file, unless it is from a line that1893# explicitly said to exclude it.1894if not m.exclude:1895 client_path = p18961897# a match, even if rejected, always stops the search1898break18991900return client_path19011902classP4Sync(Command, P4UserMap):1903 delete_actions = ("delete","move/delete","purge")19041905def__init__(self):1906 Command.__init__(self)1907 P4UserMap.__init__(self)1908 self.options = [1909 optparse.make_option("--branch", dest="branch"),1910 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),1911 optparse.make_option("--changesfile", dest="changesFile"),1912 optparse.make_option("--silent", dest="silent", action="store_true"),1913 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),1914 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),1915 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",1916help="Import into refs/heads/ , not refs/remotes"),1917 optparse.make_option("--max-changes", dest="maxChanges"),1918 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',1919help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),1920 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',1921help="Only sync files that are included in the Perforce Client Spec")1922]1923 self.description ="""Imports from Perforce into a git repository.\n1924 example:1925 //depot/my/project/ -- to import the current head1926 //depot/my/project/@all -- to import everything1927 //depot/my/project/@1,6 -- to import only from revision 1 to 619281929 (a ... is not needed in the path p4 specification, it's added implicitly)"""19301931 self.usage +=" //depot/path[@revRange]"1932 self.silent =False1933 self.createdBranches =set()1934 self.committedChanges =set()1935 self.branch =""1936 self.detectBranches =False1937 self.detectLabels =False1938 self.importLabels =False1939 self.changesFile =""1940 self.syncWithOrigin =True1941 self.importIntoRemotes =True1942 self.maxChanges =""1943 self.isWindows = (platform.system() =="Windows")1944 self.keepRepoPath =False1945 self.depotPaths =None1946 self.p4BranchesInGit = []1947 self.cloneExclude = []1948 self.useClientSpec =False1949 self.useClientSpec_from_options =False1950 self.clientSpecDirs =None1951 self.tempBranches = []1952 self.tempBranchLocation ="git-p4-tmp"19531954ifgitConfig("git-p4.syncFromOrigin") =="false":1955 self.syncWithOrigin =False19561957# Force a checkpoint in fast-import and wait for it to finish1958defcheckpoint(self):1959 self.gitStream.write("checkpoint\n\n")1960 self.gitStream.write("progress checkpoint\n\n")1961 out = self.gitOutput.readline()1962if self.verbose:1963print"checkpoint finished: "+ out19641965defextractFilesFromCommit(self, commit):1966 self.cloneExclude = [re.sub(r"\.\.\.$","", path)1967for path in self.cloneExclude]1968 files = []1969 fnum =01970while commit.has_key("depotFile%s"% fnum):1971 path = commit["depotFile%s"% fnum]19721973if[p for p in self.cloneExclude1974ifp4PathStartsWith(path, p)]:1975 found =False1976else:1977 found = [p for p in self.depotPaths1978ifp4PathStartsWith(path, p)]1979if not found:1980 fnum = fnum +11981continue19821983file= {}1984file["path"] = path1985file["rev"] = commit["rev%s"% fnum]1986file["action"] = commit["action%s"% fnum]1987file["type"] = commit["type%s"% fnum]1988 files.append(file)1989 fnum = fnum +11990return files19911992defstripRepoPath(self, path, prefixes):1993"""When streaming files, this is called to map a p4 depot path1994 to where it should go in git. The prefixes are either1995 self.depotPaths, or self.branchPrefixes in the case of1996 branch detection."""19971998if self.useClientSpec:1999# branch detection moves files up a level (the branch name)2000# from what client spec interpretation gives2001 path = self.clientSpecDirs.map_in_client(path)2002if self.detectBranches:2003for b in self.knownBranches:2004if path.startswith(b +"/"):2005 path = path[len(b)+1:]20062007elif self.keepRepoPath:2008# Preserve everything in relative path name except leading2009# //depot/; just look at first prefix as they all should2010# be in the same depot.2011 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])2012ifp4PathStartsWith(path, depot):2013 path = path[len(depot):]20142015else:2016for p in prefixes:2017ifp4PathStartsWith(path, p):2018 path = path[len(p):]2019break20202021 path =wildcard_decode(path)2022return path20232024defsplitFilesIntoBranches(self, commit):2025"""Look at each depotFile in the commit to figure out to what2026 branch it belongs."""20272028 branches = {}2029 fnum =02030while commit.has_key("depotFile%s"% fnum):2031 path = commit["depotFile%s"% fnum]2032 found = [p for p in self.depotPaths2033ifp4PathStartsWith(path, p)]2034if not found:2035 fnum = fnum +12036continue20372038file= {}2039file["path"] = path2040file["rev"] = commit["rev%s"% fnum]2041file["action"] = commit["action%s"% fnum]2042file["type"] = commit["type%s"% fnum]2043 fnum = fnum +120442045# start with the full relative path where this file would2046# go in a p4 client2047if self.useClientSpec:2048 relPath = self.clientSpecDirs.map_in_client(path)2049else:2050 relPath = self.stripRepoPath(path, self.depotPaths)20512052for branch in self.knownBranches.keys():2053# add a trailing slash so that a commit into qt/4.2foo2054# doesn't end up in qt/4.2, e.g.2055if relPath.startswith(branch +"/"):2056if branch not in branches:2057 branches[branch] = []2058 branches[branch].append(file)2059break20602061return branches20622063# output one file from the P4 stream2064# - helper for streamP4Files20652066defstreamOneP4File(self,file, contents):2067 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)2068if verbose:2069 sys.stderr.write("%s\n"% relPath)20702071(type_base, type_mods) =split_p4_type(file["type"])20722073 git_mode ="100644"2074if"x"in type_mods:2075 git_mode ="100755"2076if type_base =="symlink":2077 git_mode ="120000"2078# p4 print on a symlink contains "target\n"; remove the newline2079 data =''.join(contents)2080 contents = [data[:-1]]20812082if type_base =="utf16":2083# p4 delivers different text in the python output to -G2084# than it does when using "print -o", or normal p4 client2085# operations. utf16 is converted to ascii or utf8, perhaps.2086# But ascii text saved as -t utf16 is completely mangled.2087# Invoke print -o to get the real contents.2088 text =p4_read_pipe(['print','-q','-o','-',file['depotFile']])2089 contents = [ text ]20902091if type_base =="apple":2092# Apple filetype files will be streamed as a concatenation of2093# its appledouble header and the contents. This is useless2094# on both macs and non-macs. If using "print -q -o xx", it2095# will create "xx" with the data, and "%xx" with the header.2096# This is also not very useful.2097#2098# Ideally, someday, this script can learn how to generate2099# appledouble files directly and import those to git, but2100# non-mac machines can never find a use for apple filetype.2101print"\nIgnoring apple filetype file%s"%file['depotFile']2102return21032104# Perhaps windows wants unicode, utf16 newlines translated too;2105# but this is not doing it.2106if self.isWindows and type_base =="text":2107 mangled = []2108for data in contents:2109 data = data.replace("\r\n","\n")2110 mangled.append(data)2111 contents = mangled21122113# Note that we do not try to de-mangle keywords on utf16 files,2114# even though in theory somebody may want that.2115 pattern =p4_keywords_regexp_for_type(type_base, type_mods)2116if pattern:2117 regexp = re.compile(pattern, re.VERBOSE)2118 text =''.join(contents)2119 text = regexp.sub(r'$\1$', text)2120 contents = [ text ]21212122 self.gitStream.write("M%sinline%s\n"% (git_mode, relPath))21232124# total length...2125 length =02126for d in contents:2127 length = length +len(d)21282129 self.gitStream.write("data%d\n"% length)2130for d in contents:2131 self.gitStream.write(d)2132 self.gitStream.write("\n")21332134defstreamOneP4Deletion(self,file):2135 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)2136if verbose:2137 sys.stderr.write("delete%s\n"% relPath)2138 self.gitStream.write("D%s\n"% relPath)21392140# handle another chunk of streaming data2141defstreamP4FilesCb(self, marshalled):21422143# catch p4 errors and complain2144 err =None2145if"code"in marshalled:2146if marshalled["code"] =="error":2147if"data"in marshalled:2148 err = marshalled["data"].rstrip()2149if err:2150 f =None2151if self.stream_have_file_info:2152if"depotFile"in self.stream_file:2153 f = self.stream_file["depotFile"]2154# force a failure in fast-import, else an empty2155# commit will be made2156 self.gitStream.write("\n")2157 self.gitStream.write("die-now\n")2158 self.gitStream.close()2159# ignore errors, but make sure it exits first2160 self.importProcess.wait()2161if f:2162die("Error from p4 print for%s:%s"% (f, err))2163else:2164die("Error from p4 print:%s"% err)21652166if marshalled.has_key('depotFile')and self.stream_have_file_info:2167# start of a new file - output the old one first2168 self.streamOneP4File(self.stream_file, self.stream_contents)2169 self.stream_file = {}2170 self.stream_contents = []2171 self.stream_have_file_info =False21722173# pick up the new file information... for the2174# 'data' field we need to append to our array2175for k in marshalled.keys():2176if k =='data':2177 self.stream_contents.append(marshalled['data'])2178else:2179 self.stream_file[k] = marshalled[k]21802181 self.stream_have_file_info =True21822183# Stream directly from "p4 files" into "git fast-import"2184defstreamP4Files(self, files):2185 filesForCommit = []2186 filesToRead = []2187 filesToDelete = []21882189for f in files:2190# if using a client spec, only add the files that have2191# a path in the client2192if self.clientSpecDirs:2193if self.clientSpecDirs.map_in_client(f['path']) =="":2194continue21952196 filesForCommit.append(f)2197if f['action']in self.delete_actions:2198 filesToDelete.append(f)2199else:2200 filesToRead.append(f)22012202# deleted files...2203for f in filesToDelete:2204 self.streamOneP4Deletion(f)22052206iflen(filesToRead) >0:2207 self.stream_file = {}2208 self.stream_contents = []2209 self.stream_have_file_info =False22102211# curry self argument2212defstreamP4FilesCbSelf(entry):2213 self.streamP4FilesCb(entry)22142215 fileArgs = ['%s#%s'% (f['path'], f['rev'])for f in filesToRead]22162217p4CmdList(["-x","-","print"],2218 stdin=fileArgs,2219 cb=streamP4FilesCbSelf)22202221# do the last chunk2222if self.stream_file.has_key('depotFile'):2223 self.streamOneP4File(self.stream_file, self.stream_contents)22242225defmake_email(self, userid):2226if userid in self.users:2227return self.users[userid]2228else:2229return"%s<a@b>"% userid22302231# Stream a p4 tag2232defstreamTag(self, gitStream, labelName, labelDetails, commit, epoch):2233if verbose:2234print"writing tag%sfor commit%s"% (labelName, commit)2235 gitStream.write("tag%s\n"% labelName)2236 gitStream.write("from%s\n"% commit)22372238if labelDetails.has_key('Owner'):2239 owner = labelDetails["Owner"]2240else:2241 owner =None22422243# Try to use the owner of the p4 label, or failing that,2244# the current p4 user id.2245if owner:2246 email = self.make_email(owner)2247else:2248 email = self.make_email(self.p4UserId())2249 tagger ="%s %s %s"% (email, epoch, self.tz)22502251 gitStream.write("tagger%s\n"% tagger)22522253print"labelDetails=",labelDetails2254if labelDetails.has_key('Description'):2255 description = labelDetails['Description']2256else:2257 description ='Label from git p4'22582259 gitStream.write("data%d\n"%len(description))2260 gitStream.write(description)2261 gitStream.write("\n")22622263defcommit(self, details, files, branch, parent =""):2264 epoch = details["time"]2265 author = details["user"]22662267if self.verbose:2268print"commit into%s"% branch22692270# start with reading files; if that fails, we should not2271# create a commit.2272 new_files = []2273for f in files:2274if[p for p in self.branchPrefixes ifp4PathStartsWith(f['path'], p)]:2275 new_files.append(f)2276else:2277 sys.stderr.write("Ignoring file outside of prefix:%s\n"% f['path'])22782279 self.gitStream.write("commit%s\n"% branch)2280# gitStream.write("mark :%s\n" % details["change"])2281 self.committedChanges.add(int(details["change"]))2282 committer =""2283if author not in self.users:2284 self.getUserMapFromPerforceServer()2285 committer ="%s %s %s"% (self.make_email(author), epoch, self.tz)22862287 self.gitStream.write("committer%s\n"% committer)22882289 self.gitStream.write("data <<EOT\n")2290 self.gitStream.write(details["desc"])2291 self.gitStream.write("\n[git-p4: depot-paths =\"%s\": change =%s"%2292(','.join(self.branchPrefixes), details["change"]))2293iflen(details['options']) >0:2294 self.gitStream.write(": options =%s"% details['options'])2295 self.gitStream.write("]\nEOT\n\n")22962297iflen(parent) >0:2298if self.verbose:2299print"parent%s"% parent2300 self.gitStream.write("from%s\n"% parent)23012302 self.streamP4Files(new_files)2303 self.gitStream.write("\n")23042305 change =int(details["change"])23062307if self.labels.has_key(change):2308 label = self.labels[change]2309 labelDetails = label[0]2310 labelRevisions = label[1]2311if self.verbose:2312print"Change%sis labelled%s"% (change, labelDetails)23132314 files =p4CmdList(["files"] + ["%s...@%s"% (p, change)2315for p in self.branchPrefixes])23162317iflen(files) ==len(labelRevisions):23182319 cleanedFiles = {}2320for info in files:2321if info["action"]in self.delete_actions:2322continue2323 cleanedFiles[info["depotFile"]] = info["rev"]23242325if cleanedFiles == labelRevisions:2326 self.streamTag(self.gitStream,'tag_%s'% labelDetails['label'], labelDetails, branch, epoch)23272328else:2329if not self.silent:2330print("Tag%sdoes not match with change%s: files do not match."2331% (labelDetails["label"], change))23322333else:2334if not self.silent:2335print("Tag%sdoes not match with change%s: file count is different."2336% (labelDetails["label"], change))23372338# Build a dictionary of changelists and labels, for "detect-labels" option.2339defgetLabels(self):2340 self.labels = {}23412342 l =p4CmdList(["labels"] + ["%s..."% p for p in self.depotPaths])2343iflen(l) >0and not self.silent:2344print"Finding files belonging to labels in%s"% `self.depotPaths`23452346for output in l:2347 label = output["label"]2348 revisions = {}2349 newestChange =02350if self.verbose:2351print"Querying files for label%s"% label2352forfileinp4CmdList(["files"] +2353["%s...@%s"% (p, label)2354for p in self.depotPaths]):2355 revisions[file["depotFile"]] =file["rev"]2356 change =int(file["change"])2357if change > newestChange:2358 newestChange = change23592360 self.labels[newestChange] = [output, revisions]23612362if self.verbose:2363print"Label changes:%s"% self.labels.keys()23642365# Import p4 labels as git tags. A direct mapping does not2366# exist, so assume that if all the files are at the same revision2367# then we can use that, or it's something more complicated we should2368# just ignore.2369defimportP4Labels(self, stream, p4Labels):2370if verbose:2371print"import p4 labels: "+' '.join(p4Labels)23722373 ignoredP4Labels =gitConfigList("git-p4.ignoredP4Labels")2374 validLabelRegexp =gitConfig("git-p4.labelImportRegexp")2375iflen(validLabelRegexp) ==0:2376 validLabelRegexp = defaultLabelRegexp2377 m = re.compile(validLabelRegexp)23782379for name in p4Labels:2380 commitFound =False23812382if not m.match(name):2383if verbose:2384print"label%sdoes not match regexp%s"% (name,validLabelRegexp)2385continue23862387if name in ignoredP4Labels:2388continue23892390 labelDetails =p4CmdList(['label',"-o", name])[0]23912392# get the most recent changelist for each file in this label2393 change =p4Cmd(["changes","-m","1"] + ["%s...@%s"% (p, name)2394for p in self.depotPaths])23952396if change.has_key('change'):2397# find the corresponding git commit; take the oldest commit2398 changelist =int(change['change'])2399 gitCommit =read_pipe(["git","rev-list","--max-count=1",2400"--reverse",":/\[git-p4:.*change =%d\]"% changelist])2401iflen(gitCommit) ==0:2402print"could not find git commit for changelist%d"% changelist2403else:2404 gitCommit = gitCommit.strip()2405 commitFound =True2406# Convert from p4 time format2407try:2408 tmwhen = time.strptime(labelDetails['Update'],"%Y/%m/%d%H:%M:%S")2409exceptValueError:2410print"Could not convert label time%s"% labelDetails['Update']2411 tmwhen =124122413 when =int(time.mktime(tmwhen))2414 self.streamTag(stream, name, labelDetails, gitCommit, when)2415if verbose:2416print"p4 label%smapped to git commit%s"% (name, gitCommit)2417else:2418if verbose:2419print"Label%shas no changelists - possibly deleted?"% name24202421if not commitFound:2422# We can't import this label; don't try again as it will get very2423# expensive repeatedly fetching all the files for labels that will2424# never be imported. If the label is moved in the future, the2425# ignore will need to be removed manually.2426system(["git","config","--add","git-p4.ignoredP4Labels", name])24272428defguessProjectName(self):2429for p in self.depotPaths:2430if p.endswith("/"):2431 p = p[:-1]2432 p = p[p.strip().rfind("/") +1:]2433if not p.endswith("/"):2434 p +="/"2435return p24362437defgetBranchMapping(self):2438 lostAndFoundBranches =set()24392440 user =gitConfig("git-p4.branchUser")2441iflen(user) >0:2442 command ="branches -u%s"% user2443else:2444 command ="branches"24452446for info inp4CmdList(command):2447 details =p4Cmd(["branch","-o", info["branch"]])2448 viewIdx =02449while details.has_key("View%s"% viewIdx):2450 paths = details["View%s"% viewIdx].split(" ")2451 viewIdx = viewIdx +12452# require standard //depot/foo/... //depot/bar/... mapping2453iflen(paths) !=2or not paths[0].endswith("/...")or not paths[1].endswith("/..."):2454continue2455 source = paths[0]2456 destination = paths[1]2457## HACK2458ifp4PathStartsWith(source, self.depotPaths[0])andp4PathStartsWith(destination, self.depotPaths[0]):2459 source = source[len(self.depotPaths[0]):-4]2460 destination = destination[len(self.depotPaths[0]):-4]24612462if destination in self.knownBranches:2463if not self.silent:2464print"p4 branch%sdefines a mapping from%sto%s"% (info["branch"], source, destination)2465print"but there exists another mapping from%sto%salready!"% (self.knownBranches[destination], destination)2466continue24672468 self.knownBranches[destination] = source24692470 lostAndFoundBranches.discard(destination)24712472if source not in self.knownBranches:2473 lostAndFoundBranches.add(source)24742475# Perforce does not strictly require branches to be defined, so we also2476# check git config for a branch list.2477#2478# Example of branch definition in git config file:2479# [git-p4]2480# branchList=main:branchA2481# branchList=main:branchB2482# branchList=branchA:branchC2483 configBranches =gitConfigList("git-p4.branchList")2484for branch in configBranches:2485if branch:2486(source, destination) = branch.split(":")2487 self.knownBranches[destination] = source24882489 lostAndFoundBranches.discard(destination)24902491if source not in self.knownBranches:2492 lostAndFoundBranches.add(source)249324942495for branch in lostAndFoundBranches:2496 self.knownBranches[branch] = branch24972498defgetBranchMappingFromGitBranches(self):2499 branches =p4BranchesInGit(self.importIntoRemotes)2500for branch in branches.keys():2501if branch =="master":2502 branch ="main"2503else:2504 branch = branch[len(self.projectName):]2505 self.knownBranches[branch] = branch25062507deflistExistingP4GitBranches(self):2508# branches holds mapping from name to commit2509 branches =p4BranchesInGit(self.importIntoRemotes)2510 self.p4BranchesInGit = branches.keys()2511for branch in branches.keys():2512 self.initialParents[self.refPrefix + branch] = branches[branch]25132514defupdateOptionDict(self, d):2515 option_keys = {}2516if self.keepRepoPath:2517 option_keys['keepRepoPath'] =125182519 d["options"] =' '.join(sorted(option_keys.keys()))25202521defreadOptions(self, d):2522 self.keepRepoPath = (d.has_key('options')2523and('keepRepoPath'in d['options']))25242525defgitRefForBranch(self, branch):2526if branch =="main":2527return self.refPrefix +"master"25282529iflen(branch) <=0:2530return branch25312532return self.refPrefix + self.projectName + branch25332534defgitCommitByP4Change(self, ref, change):2535if self.verbose:2536print"looking in ref "+ ref +" for change%susing bisect..."% change25372538 earliestCommit =""2539 latestCommit =parseRevision(ref)25402541while True:2542if self.verbose:2543print"trying: earliest%slatest%s"% (earliestCommit, latestCommit)2544 next =read_pipe("git rev-list --bisect%s %s"% (latestCommit, earliestCommit)).strip()2545iflen(next) ==0:2546if self.verbose:2547print"argh"2548return""2549 log =extractLogMessageFromGitCommit(next)2550 settings =extractSettingsGitLog(log)2551 currentChange =int(settings['change'])2552if self.verbose:2553print"current change%s"% currentChange25542555if currentChange == change:2556if self.verbose:2557print"found%s"% next2558return next25592560if currentChange < change:2561 earliestCommit ="^%s"% next2562else:2563 latestCommit ="%s"% next25642565return""25662567defimportNewBranch(self, branch, maxChange):2568# make fast-import flush all changes to disk and update the refs using the checkpoint2569# command so that we can try to find the branch parent in the git history2570 self.gitStream.write("checkpoint\n\n");2571 self.gitStream.flush();2572 branchPrefix = self.depotPaths[0] + branch +"/"2573range="@1,%s"% maxChange2574#print "prefix" + branchPrefix2575 changes =p4ChangesForPaths([branchPrefix],range)2576iflen(changes) <=0:2577return False2578 firstChange = changes[0]2579#print "first change in branch: %s" % firstChange2580 sourceBranch = self.knownBranches[branch]2581 sourceDepotPath = self.depotPaths[0] + sourceBranch2582 sourceRef = self.gitRefForBranch(sourceBranch)2583#print "source " + sourceBranch25842585 branchParentChange =int(p4Cmd(["changes","-m","1","%s...@1,%s"% (sourceDepotPath, firstChange)])["change"])2586#print "branch parent: %s" % branchParentChange2587 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)2588iflen(gitParent) >0:2589 self.initialParents[self.gitRefForBranch(branch)] = gitParent2590#print "parent git commit: %s" % gitParent25912592 self.importChanges(changes)2593return True25942595defsearchParent(self, parent, branch, target):2596 parentFound =False2597for blob inread_pipe_lines(["git","rev-list","--reverse","--no-merges", parent]):2598 blob = blob.strip()2599iflen(read_pipe(["git","diff-tree", blob, target])) ==0:2600 parentFound =True2601if self.verbose:2602print"Found parent of%sin commit%s"% (branch, blob)2603break2604if parentFound:2605return blob2606else:2607return None26082609defimportChanges(self, changes):2610 cnt =12611for change in changes:2612 description =p4_describe(change)2613 self.updateOptionDict(description)26142615if not self.silent:2616 sys.stdout.write("\rImporting revision%s(%s%%)"% (change, cnt *100/len(changes)))2617 sys.stdout.flush()2618 cnt = cnt +126192620try:2621if self.detectBranches:2622 branches = self.splitFilesIntoBranches(description)2623for branch in branches.keys():2624## HACK --hwn2625 branchPrefix = self.depotPaths[0] + branch +"/"2626 self.branchPrefixes = [ branchPrefix ]26272628 parent =""26292630 filesForCommit = branches[branch]26312632if self.verbose:2633print"branch is%s"% branch26342635 self.updatedBranches.add(branch)26362637if branch not in self.createdBranches:2638 self.createdBranches.add(branch)2639 parent = self.knownBranches[branch]2640if parent == branch:2641 parent =""2642else:2643 fullBranch = self.projectName + branch2644if fullBranch not in self.p4BranchesInGit:2645if not self.silent:2646print("\nImporting new branch%s"% fullBranch);2647if self.importNewBranch(branch, change -1):2648 parent =""2649 self.p4BranchesInGit.append(fullBranch)2650if not self.silent:2651print("\nResuming with change%s"% change);26522653if self.verbose:2654print"parent determined through known branches:%s"% parent26552656 branch = self.gitRefForBranch(branch)2657 parent = self.gitRefForBranch(parent)26582659if self.verbose:2660print"looking for initial parent for%s; current parent is%s"% (branch, parent)26612662iflen(parent) ==0and branch in self.initialParents:2663 parent = self.initialParents[branch]2664del self.initialParents[branch]26652666 blob =None2667iflen(parent) >0:2668 tempBranch = os.path.join(self.tempBranchLocation,"%d"% (change))2669if self.verbose:2670print"Creating temporary branch: "+ tempBranch2671 self.commit(description, filesForCommit, tempBranch)2672 self.tempBranches.append(tempBranch)2673 self.checkpoint()2674 blob = self.searchParent(parent, branch, tempBranch)2675if blob:2676 self.commit(description, filesForCommit, branch, blob)2677else:2678if self.verbose:2679print"Parent of%snot found. Committing into head of%s"% (branch, parent)2680 self.commit(description, filesForCommit, branch, parent)2681else:2682 files = self.extractFilesFromCommit(description)2683 self.commit(description, files, self.branch,2684 self.initialParent)2685 self.initialParent =""2686exceptIOError:2687print self.gitError.read()2688 sys.exit(1)26892690defimportHeadRevision(self, revision):2691print"Doing initial import of%sfrom revision%sinto%s"% (' '.join(self.depotPaths), revision, self.branch)26922693 details = {}2694 details["user"] ="git perforce import user"2695 details["desc"] = ("Initial import of%sfrom the state at revision%s\n"2696% (' '.join(self.depotPaths), revision))2697 details["change"] = revision2698 newestRevision =026992700 fileCnt =02701 fileArgs = ["%s...%s"% (p,revision)for p in self.depotPaths]27022703for info inp4CmdList(["files"] + fileArgs):27042705if'code'in info and info['code'] =='error':2706 sys.stderr.write("p4 returned an error:%s\n"2707% info['data'])2708if info['data'].find("must refer to client") >=0:2709 sys.stderr.write("This particular p4 error is misleading.\n")2710 sys.stderr.write("Perhaps the depot path was misspelled.\n");2711 sys.stderr.write("Depot path:%s\n"%" ".join(self.depotPaths))2712 sys.exit(1)2713if'p4ExitCode'in info:2714 sys.stderr.write("p4 exitcode:%s\n"% info['p4ExitCode'])2715 sys.exit(1)271627172718 change =int(info["change"])2719if change > newestRevision:2720 newestRevision = change27212722if info["action"]in self.delete_actions:2723# don't increase the file cnt, otherwise details["depotFile123"] will have gaps!2724#fileCnt = fileCnt + 12725continue27262727for prop in["depotFile","rev","action","type"]:2728 details["%s%s"% (prop, fileCnt)] = info[prop]27292730 fileCnt = fileCnt +127312732 details["change"] = newestRevision27332734# Use time from top-most change so that all git p4 clones of2735# the same p4 repo have the same commit SHA1s.2736 res =p4_describe(newestRevision)2737 details["time"] = res["time"]27382739 self.updateOptionDict(details)2740try:2741 self.commit(details, self.extractFilesFromCommit(details), self.branch)2742exceptIOError:2743print"IO error with git fast-import. Is your git version recent enough?"2744print self.gitError.read()274527462747defrun(self, args):2748 self.depotPaths = []2749 self.changeRange =""2750 self.initialParent =""2751 self.previousDepotPaths = []27522753# map from branch depot path to parent branch2754 self.knownBranches = {}2755 self.initialParents = {}2756 self.hasOrigin =originP4BranchesExist()2757if not self.syncWithOrigin:2758 self.hasOrigin =False27592760if self.importIntoRemotes:2761 self.refPrefix ="refs/remotes/p4/"2762else:2763 self.refPrefix ="refs/heads/p4/"27642765if self.syncWithOrigin and self.hasOrigin:2766if not self.silent:2767print"Syncing with origin first by calling git fetch origin"2768system("git fetch origin")27692770iflen(self.branch) ==0:2771 self.branch = self.refPrefix +"master"2772ifgitBranchExists("refs/heads/p4")and self.importIntoRemotes:2773system("git update-ref%srefs/heads/p4"% self.branch)2774system("git branch -D p4");2775# create it /after/ importing, when master exists2776if notgitBranchExists(self.refPrefix +"HEAD")and self.importIntoRemotes andgitBranchExists(self.branch):2777system("git symbolic-ref%sHEAD%s"% (self.refPrefix, self.branch))27782779# accept either the command-line option, or the configuration variable2780if self.useClientSpec:2781# will use this after clone to set the variable2782 self.useClientSpec_from_options =True2783else:2784ifgitConfig("git-p4.useclientspec","--bool") =="true":2785 self.useClientSpec =True2786if self.useClientSpec:2787 self.clientSpecDirs =getClientSpec()27882789# TODO: should always look at previous commits,2790# merge with previous imports, if possible.2791if args == []:2792if self.hasOrigin:2793createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)2794 self.listExistingP4GitBranches()27952796iflen(self.p4BranchesInGit) >1:2797if not self.silent:2798print"Importing from/into multiple branches"2799 self.detectBranches =True28002801if self.verbose:2802print"branches:%s"% self.p4BranchesInGit28032804 p4Change =02805for branch in self.p4BranchesInGit:2806 logMsg =extractLogMessageFromGitCommit(self.refPrefix + branch)28072808 settings =extractSettingsGitLog(logMsg)28092810 self.readOptions(settings)2811if(settings.has_key('depot-paths')2812and settings.has_key('change')):2813 change =int(settings['change']) +12814 p4Change =max(p4Change, change)28152816 depotPaths =sorted(settings['depot-paths'])2817if self.previousDepotPaths == []:2818 self.previousDepotPaths = depotPaths2819else:2820 paths = []2821for(prev, cur)inzip(self.previousDepotPaths, depotPaths):2822 prev_list = prev.split("/")2823 cur_list = cur.split("/")2824for i inrange(0,min(len(cur_list),len(prev_list))):2825if cur_list[i] <> prev_list[i]:2826 i = i -12827break28282829 paths.append("/".join(cur_list[:i +1]))28302831 self.previousDepotPaths = paths28322833if p4Change >0:2834 self.depotPaths =sorted(self.previousDepotPaths)2835 self.changeRange ="@%s,#head"% p4Change2836if not self.detectBranches:2837 self.initialParent =parseRevision(self.branch)2838if not self.silent and not self.detectBranches:2839print"Performing incremental import into%sgit branch"% self.branch28402841if not self.branch.startswith("refs/"):2842 self.branch ="refs/heads/"+ self.branch28432844iflen(args) ==0and self.depotPaths:2845if not self.silent:2846print"Depot paths:%s"%' '.join(self.depotPaths)2847else:2848if self.depotPaths and self.depotPaths != args:2849print("previous import used depot path%sand now%swas specified. "2850"This doesn't work!"% (' '.join(self.depotPaths),2851' '.join(args)))2852 sys.exit(1)28532854 self.depotPaths =sorted(args)28552856 revision =""2857 self.users = {}28582859# Make sure no revision specifiers are used when --changesfile2860# is specified.2861 bad_changesfile =False2862iflen(self.changesFile) >0:2863for p in self.depotPaths:2864if p.find("@") >=0or p.find("#") >=0:2865 bad_changesfile =True2866break2867if bad_changesfile:2868die("Option --changesfile is incompatible with revision specifiers")28692870 newPaths = []2871for p in self.depotPaths:2872if p.find("@") != -1:2873 atIdx = p.index("@")2874 self.changeRange = p[atIdx:]2875if self.changeRange =="@all":2876 self.changeRange =""2877elif','not in self.changeRange:2878 revision = self.changeRange2879 self.changeRange =""2880 p = p[:atIdx]2881elif p.find("#") != -1:2882 hashIdx = p.index("#")2883 revision = p[hashIdx:]2884 p = p[:hashIdx]2885elif self.previousDepotPaths == []:2886# pay attention to changesfile, if given, else import2887# the entire p4 tree at the head revision2888iflen(self.changesFile) ==0:2889 revision ="#head"28902891 p = re.sub("\.\.\.$","", p)2892if not p.endswith("/"):2893 p +="/"28942895 newPaths.append(p)28962897 self.depotPaths = newPaths28982899# --detect-branches may change this for each branch2900 self.branchPrefixes = self.depotPaths29012902 self.loadUserMapFromCache()2903 self.labels = {}2904if self.detectLabels:2905 self.getLabels();29062907if self.detectBranches:2908## FIXME - what's a P4 projectName ?2909 self.projectName = self.guessProjectName()29102911if self.hasOrigin:2912 self.getBranchMappingFromGitBranches()2913else:2914 self.getBranchMapping()2915if self.verbose:2916print"p4-git branches:%s"% self.p4BranchesInGit2917print"initial parents:%s"% self.initialParents2918for b in self.p4BranchesInGit:2919if b !="master":29202921## FIXME2922 b = b[len(self.projectName):]2923 self.createdBranches.add(b)29242925 self.tz ="%+03d%02d"% (- time.timezone /3600, ((- time.timezone %3600) /60))29262927 self.importProcess = subprocess.Popen(["git","fast-import"],2928 stdin=subprocess.PIPE,2929 stdout=subprocess.PIPE,2930 stderr=subprocess.PIPE);2931 self.gitOutput = self.importProcess.stdout2932 self.gitStream = self.importProcess.stdin2933 self.gitError = self.importProcess.stderr29342935if revision:2936 self.importHeadRevision(revision)2937else:2938 changes = []29392940iflen(self.changesFile) >0:2941 output =open(self.changesFile).readlines()2942 changeSet =set()2943for line in output:2944 changeSet.add(int(line))29452946for change in changeSet:2947 changes.append(change)29482949 changes.sort()2950else:2951# catch "git p4 sync" with no new branches, in a repo that2952# does not have any existing p4 branches2953iflen(args) ==0and not self.p4BranchesInGit:2954die("No remote p4 branches. Perhaps you never did\"git p4 clone\"in here.");2955if self.verbose:2956print"Getting p4 changes for%s...%s"% (', '.join(self.depotPaths),2957 self.changeRange)2958 changes =p4ChangesForPaths(self.depotPaths, self.changeRange)29592960iflen(self.maxChanges) >0:2961 changes = changes[:min(int(self.maxChanges),len(changes))]29622963iflen(changes) ==0:2964if not self.silent:2965print"No changes to import!"2966else:2967if not self.silent and not self.detectBranches:2968print"Import destination:%s"% self.branch29692970 self.updatedBranches =set()29712972 self.importChanges(changes)29732974if not self.silent:2975print""2976iflen(self.updatedBranches) >0:2977 sys.stdout.write("Updated branches: ")2978for b in self.updatedBranches:2979 sys.stdout.write("%s"% b)2980 sys.stdout.write("\n")29812982ifgitConfig("git-p4.importLabels","--bool") =="true":2983 self.importLabels =True29842985if self.importLabels:2986 p4Labels =getP4Labels(self.depotPaths)2987 gitTags =getGitTags()29882989 missingP4Labels = p4Labels - gitTags2990 self.importP4Labels(self.gitStream, missingP4Labels)29912992 self.gitStream.close()2993if self.importProcess.wait() !=0:2994die("fast-import failed:%s"% self.gitError.read())2995 self.gitOutput.close()2996 self.gitError.close()29972998# Cleanup temporary branches created during import2999if self.tempBranches != []:3000for branch in self.tempBranches:3001read_pipe("git update-ref -d%s"% branch)3002 os.rmdir(os.path.join(os.environ.get("GIT_DIR",".git"), self.tempBranchLocation))30033004return True30053006classP4Rebase(Command):3007def__init__(self):3008 Command.__init__(self)3009 self.options = [3010 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),3011]3012 self.importLabels =False3013 self.description = ("Fetches the latest revision from perforce and "3014+"rebases the current work (branch) against it")30153016defrun(self, args):3017 sync =P4Sync()3018 sync.importLabels = self.importLabels3019 sync.run([])30203021return self.rebase()30223023defrebase(self):3024if os.system("git update-index --refresh") !=0:3025die("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.");3026iflen(read_pipe("git diff-index HEAD --")) >0:3027die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");30283029[upstream, settings] =findUpstreamBranchPoint()3030iflen(upstream) ==0:3031die("Cannot find upstream branchpoint for rebase")30323033# the branchpoint may be p4/foo~3, so strip off the parent3034 upstream = re.sub("~[0-9]+$","", upstream)30353036print"Rebasing the current branch onto%s"% upstream3037 oldHead =read_pipe("git rev-parse HEAD").strip()3038system("git rebase%s"% upstream)3039system("git diff-tree --stat --summary -M%sHEAD"% oldHead)3040return True30413042classP4Clone(P4Sync):3043def__init__(self):3044 P4Sync.__init__(self)3045 self.description ="Creates a new git repository and imports from Perforce into it"3046 self.usage ="usage: %prog [options] //depot/path[@revRange]"3047 self.options += [3048 optparse.make_option("--destination", dest="cloneDestination",3049 action='store', default=None,3050help="where to leave result of the clone"),3051 optparse.make_option("-/", dest="cloneExclude",3052 action="append",type="string",3053help="exclude depot path"),3054 optparse.make_option("--bare", dest="cloneBare",3055 action="store_true", default=False),3056]3057 self.cloneDestination =None3058 self.needsGit =False3059 self.cloneBare =False30603061# This is required for the "append" cloneExclude action3062defensure_value(self, attr, value):3063if nothasattr(self, attr)orgetattr(self, attr)is None:3064setattr(self, attr, value)3065returngetattr(self, attr)30663067defdefaultDestination(self, args):3068## TODO: use common prefix of args?3069 depotPath = args[0]3070 depotDir = re.sub("(@[^@]*)$","", depotPath)3071 depotDir = re.sub("(#[^#]*)$","", depotDir)3072 depotDir = re.sub(r"\.\.\.$","", depotDir)3073 depotDir = re.sub(r"/$","", depotDir)3074return os.path.split(depotDir)[1]30753076defrun(self, args):3077iflen(args) <1:3078return False30793080if self.keepRepoPath and not self.cloneDestination:3081 sys.stderr.write("Must specify destination for --keep-path\n")3082 sys.exit(1)30833084 depotPaths = args30853086if not self.cloneDestination andlen(depotPaths) >1:3087 self.cloneDestination = depotPaths[-1]3088 depotPaths = depotPaths[:-1]30893090 self.cloneExclude = ["/"+p for p in self.cloneExclude]3091for p in depotPaths:3092if not p.startswith("//"):3093return False30943095if not self.cloneDestination:3096 self.cloneDestination = self.defaultDestination(args)30973098print"Importing from%sinto%s"% (', '.join(depotPaths), self.cloneDestination)30993100if not os.path.exists(self.cloneDestination):3101 os.makedirs(self.cloneDestination)3102chdir(self.cloneDestination)31033104 init_cmd = ["git","init"]3105if self.cloneBare:3106 init_cmd.append("--bare")3107 subprocess.check_call(init_cmd)31083109if not P4Sync.run(self, depotPaths):3110return False3111if self.branch !="master":3112if self.importIntoRemotes:3113 masterbranch ="refs/remotes/p4/master"3114else:3115 masterbranch ="refs/heads/p4/master"3116ifgitBranchExists(masterbranch):3117system("git branch master%s"% masterbranch)3118if not self.cloneBare:3119system("git checkout -f")3120else:3121print"Could not detect main branch. No checkout/master branch created."31223123# auto-set this variable if invoked with --use-client-spec3124if self.useClientSpec_from_options:3125system("git config --bool git-p4.useclientspec true")31263127return True31283129classP4Branches(Command):3130def__init__(self):3131 Command.__init__(self)3132 self.options = [ ]3133 self.description = ("Shows the git branches that hold imports and their "3134+"corresponding perforce depot paths")3135 self.verbose =False31363137defrun(self, args):3138iforiginP4BranchesExist():3139createOrUpdateBranchesFromOrigin()31403141 cmdline ="git rev-parse --symbolic "3142 cmdline +=" --remotes"31433144for line inread_pipe_lines(cmdline):3145 line = line.strip()31463147if not line.startswith('p4/')or line =="p4/HEAD":3148continue3149 branch = line31503151 log =extractLogMessageFromGitCommit("refs/remotes/%s"% branch)3152 settings =extractSettingsGitLog(log)31533154print"%s<=%s(%s)"% (branch,",".join(settings["depot-paths"]), settings["change"])3155return True31563157classHelpFormatter(optparse.IndentedHelpFormatter):3158def__init__(self):3159 optparse.IndentedHelpFormatter.__init__(self)31603161defformat_description(self, description):3162if description:3163return description +"\n"3164else:3165return""31663167defprintUsage(commands):3168print"usage:%s<command> [options]"% sys.argv[0]3169print""3170print"valid commands:%s"%", ".join(commands)3171print""3172print"Try%s<command> --help for command specific help."% sys.argv[0]3173print""31743175commands = {3176"debug": P4Debug,3177"submit": P4Submit,3178"commit": P4Submit,3179"sync": P4Sync,3180"rebase": P4Rebase,3181"clone": P4Clone,3182"rollback": P4RollBack,3183"branches": P4Branches3184}318531863187defmain():3188iflen(sys.argv[1:]) ==0:3189printUsage(commands.keys())3190 sys.exit(2)31913192 cmdName = sys.argv[1]3193try:3194 klass = commands[cmdName]3195 cmd =klass()3196exceptKeyError:3197print"unknown command%s"% cmdName3198print""3199printUsage(commands.keys())3200 sys.exit(2)32013202 options = cmd.options3203 cmd.gitdir = os.environ.get("GIT_DIR",None)32043205 args = sys.argv[2:]32063207 options.append(optparse.make_option("--verbose","-v", dest="verbose", action="store_true"))3208if cmd.needsGit:3209 options.append(optparse.make_option("--git-dir", dest="gitdir"))32103211 parser = optparse.OptionParser(cmd.usage.replace("%prog","%prog "+ cmdName),3212 options,3213 description = cmd.description,3214 formatter =HelpFormatter())32153216(cmd, args) = parser.parse_args(sys.argv[2:], cmd);3217global verbose3218 verbose = cmd.verbose3219if cmd.needsGit:3220if cmd.gitdir ==None:3221 cmd.gitdir = os.path.abspath(".git")3222if notisValidGitDir(cmd.gitdir):3223 cmd.gitdir =read_pipe("git rev-parse --git-dir").strip()3224if os.path.exists(cmd.gitdir):3225 cdup =read_pipe("git rev-parse --show-cdup").strip()3226iflen(cdup) >0:3227chdir(cdup);32283229if notisValidGitDir(cmd.gitdir):3230ifisValidGitDir(cmd.gitdir +"/.git"):3231 cmd.gitdir +="/.git"3232else:3233die("fatal: cannot locate git repository at%s"% cmd.gitdir)32343235 os.environ["GIT_DIR"] = cmd.gitdir32363237if not cmd.run(args):3238 parser.print_help()3239 sys.exit(2)324032413242if __name__ =='__main__':3243main()