3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
11 if sys.hexversion < 0x02040000:
12 # The limiter is the subprocess module
13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
27 from subprocess import CalledProcessError
29 # from python2.7:subprocess.py
30 # Exception classes used by this module.
31 class CalledProcessError(Exception):
32 """This exception is raised when a process run by check_call() returns
33 a non-zero exit status. The exit status will be stored in the
34 returncode attribute."""
35 def __init__(self, returncode, cmd):
36 self.returncode = returncode
39 return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
43 # Only labels/tags matching this will be imported/exported
44 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
46 # Grab changes in blocks of this many revisions, unless otherwise requested
47 defaultBlockSize = 512
49 def p4_build_cmd(cmd):
50 """Build a suitable p4 command line.
52 This consolidates building and returning a p4 command line into one
53 location. It means that hooking into the environment, or other configuration
54 can be done more easily.
58 user = gitConfig("git-p4.user")
60 real_cmd += ["-u",user]
62 password = gitConfig("git-p4.password")
64 real_cmd += ["-P", password]
66 port = gitConfig("git-p4.port")
68 real_cmd += ["-p", port]
70 host = gitConfig("git-p4.host")
72 real_cmd += ["-H", host]
74 client = gitConfig("git-p4.client")
76 real_cmd += ["-c", client]
79 if isinstance(cmd,basestring):
80 real_cmd = ' '.join(real_cmd) + ' ' + cmd
85 def chdir(path, is_client_path=False):
86 """Do chdir to the given path, and set the PWD environment
87 variable for use by P4. It does not look at getcwd() output.
88 Since we're not using the shell, it is necessary to set the
89 PWD environment variable explicitly.
91 Normally, expand the path to force it to be absolute. This
92 addresses the use of relative path names inside P4 settings,
93 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
94 as given; it looks for .p4config using PWD.
96 If is_client_path, the path was handed to us directly by p4,
97 and may be a symbolic link. Do not call os.getcwd() in this
98 case, because it will cause p4 to think that PWD is not inside
103 if not is_client_path:
105 os.environ['PWD'] = path
111 sys.stderr.write(msg + "\n")
114 def write_pipe(c, stdin):
116 sys.stderr.write('Writing pipe: %s\n' % str(c))
118 expand = isinstance(c,basestring)
119 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
121 val = pipe.write(stdin)
124 die('Command failed: %s' % str(c))
128 def p4_write_pipe(c, stdin):
129 real_cmd = p4_build_cmd(c)
130 return write_pipe(real_cmd, stdin)
132 def read_pipe(c, ignore_error=False):
134 sys.stderr.write('Reading pipe: %s\n' % str(c))
136 expand = isinstance(c,basestring)
137 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
140 if p.wait() and not ignore_error:
141 die('Command failed: %s' % str(c))
145 def p4_read_pipe(c, ignore_error=False):
146 real_cmd = p4_build_cmd(c)
147 return read_pipe(real_cmd, ignore_error)
149 def read_pipe_lines(c):
151 sys.stderr.write('Reading pipe: %s\n' % str(c))
153 expand = isinstance(c, basestring)
154 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
156 val = pipe.readlines()
157 if pipe.close() or p.wait():
158 die('Command failed: %s' % str(c))
162 def p4_read_pipe_lines(c):
163 """Specifically invoke p4 on the command supplied. """
164 real_cmd = p4_build_cmd(c)
165 return read_pipe_lines(real_cmd)
167 def p4_has_command(cmd):
168 """Ask p4 for help on this command. If it returns an error, the
169 command does not exist in this version of p4."""
170 real_cmd = p4_build_cmd(["help", cmd])
171 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
172 stderr=subprocess.PIPE)
174 return p.returncode == 0
176 def p4_has_move_command():
177 """See if the move command exists, that it supports -k, and that
178 it has not been administratively disabled. The arguments
179 must be correct, but the filenames do not have to exist. Use
180 ones with wildcards so even if they exist, it will fail."""
182 if not p4_has_command("move"):
184 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
185 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
186 (out, err) = p.communicate()
187 # return code will be 1 in either case
188 if err.find("Invalid option") >= 0:
190 if err.find("disabled") >= 0:
192 # assume it failed because @... was invalid changelist
195 def system(cmd, ignore_error=False):
196 expand = isinstance(cmd,basestring)
198 sys.stderr.write("executing %s\n" % str(cmd))
199 retcode = subprocess.call(cmd, shell=expand)
200 if retcode and not ignore_error:
201 raise CalledProcessError(retcode, cmd)
206 """Specifically invoke p4 as the system command. """
207 real_cmd = p4_build_cmd(cmd)
208 expand = isinstance(real_cmd, basestring)
209 retcode = subprocess.call(real_cmd, shell=expand)
211 raise CalledProcessError(retcode, real_cmd)
213 _p4_version_string = None
214 def p4_version_string():
215 """Read the version string, showing just the last line, which
216 hopefully is the interesting version bit.
219 Perforce - The Fast Software Configuration Management System.
220 Copyright 1995-2011 Perforce Software. All rights reserved.
221 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
223 global _p4_version_string
224 if not _p4_version_string:
225 a = p4_read_pipe_lines(["-V"])
226 _p4_version_string = a[-1].rstrip()
227 return _p4_version_string
229 def p4_integrate(src, dest):
230 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
232 def p4_sync(f, *options):
233 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
236 # forcibly add file names with wildcards
237 if wildcard_present(f):
238 p4_system(["add", "-f", f])
240 p4_system(["add", f])
243 p4_system(["delete", wildcard_encode(f)])
246 p4_system(["edit", wildcard_encode(f)])
249 p4_system(["revert", wildcard_encode(f)])
251 def p4_reopen(type, f):
252 p4_system(["reopen", "-t", type, wildcard_encode(f)])
254 def p4_move(src, dest):
255 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
257 def p4_last_change():
258 results = p4CmdList(["changes", "-m", "1"])
259 return int(results[0]['change'])
261 def p4_describe(change):
262 """Make sure it returns a valid result by checking for
263 the presence of field "time". Return a dict of the
266 ds = p4CmdList(["describe", "-s", str(change)])
268 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
272 if "p4ExitCode" in d:
273 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
276 if d["code"] == "error":
277 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
280 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
285 # Canonicalize the p4 type and return a tuple of the
286 # base type, plus any modifiers. See "p4 help filetypes"
287 # for a list and explanation.
289 def split_p4_type(p4type):
291 p4_filetypes_historical = {
292 "ctempobj": "binary+Sw",
298 "tempobj": "binary+FSw",
299 "ubinary": "binary+F",
300 "uresource": "resource+F",
301 "uxbinary": "binary+Fx",
302 "xbinary": "binary+x",
304 "xtempobj": "binary+Swx",
306 "xunicode": "unicode+x",
309 if p4type in p4_filetypes_historical:
310 p4type = p4_filetypes_historical[p4type]
312 s = p4type.split("+")
320 # return the raw p4 type of a file (text, text+ko, etc)
323 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
324 return results[0]['headType']
327 # Given a type base and modifier, return a regexp matching
328 # the keywords that can be expanded in the file
330 def p4_keywords_regexp_for_type(base, type_mods):
331 if base in ("text", "unicode", "binary"):
333 if "ko" in type_mods:
335 elif "k" in type_mods:
336 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
340 \$ # Starts with a dollar, followed by...
341 (%s) # one of the keywords, followed by...
342 (:[^$\n]+)? # possibly an old expansion, followed by...
350 # Given a file, return a regexp matching the possible
351 # RCS keywords that will be expanded, or None for files
352 # with kw expansion turned off.
354 def p4_keywords_regexp_for_file(file):
355 if not os.path.exists(file):
358 (type_base, type_mods) = split_p4_type(p4_type(file))
359 return p4_keywords_regexp_for_type(type_base, type_mods)
361 def setP4ExecBit(file, mode):
362 # Reopens an already open file and changes the execute bit to match
363 # the execute bit setting in the passed in mode.
367 if not isModeExec(mode):
368 p4Type = getP4OpenedType(file)
369 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
370 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
371 if p4Type[-1] == "+":
372 p4Type = p4Type[0:-1]
374 p4_reopen(p4Type, file)
376 def getP4OpenedType(file):
377 # Returns the perforce file type for the given file.
379 result = p4_read_pipe(["opened", wildcard_encode(file)])
380 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
382 return match.group(1)
384 die("Could not determine file type for %s (result: '%s')" % (file, result))
386 # Return the set of all p4 labels
387 def getP4Labels(depotPaths):
389 if isinstance(depotPaths,basestring):
390 depotPaths = [depotPaths]
392 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
398 # Return the set of all git tags
401 for line in read_pipe_lines(["git", "tag"]):
406 def diffTreePattern():
407 # This is a simple generator for the diff tree regex pattern. This could be
408 # a class variable if this and parseDiffTreeEntry were a part of a class.
409 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
413 def parseDiffTreeEntry(entry):
414 """Parses a single diff tree entry into its component elements.
416 See git-diff-tree(1) manpage for details about the format of the diff
417 output. This method returns a dictionary with the following elements:
419 src_mode - The mode of the source file
420 dst_mode - The mode of the destination file
421 src_sha1 - The sha1 for the source file
422 dst_sha1 - The sha1 fr the destination file
423 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
424 status_score - The score for the status (applicable for 'C' and 'R'
425 statuses). This is None if there is no score.
426 src - The path for the source file.
427 dst - The path for the destination file. This is only present for
428 copy or renames. If it is not present, this is None.
430 If the pattern is not matched, None is returned."""
432 match = diffTreePattern().next().match(entry)
435 'src_mode': match.group(1),
436 'dst_mode': match.group(2),
437 'src_sha1': match.group(3),
438 'dst_sha1': match.group(4),
439 'status': match.group(5),
440 'status_score': match.group(6),
441 'src': match.group(7),
442 'dst': match.group(10)
446 def isModeExec(mode):
447 # Returns True if the given git mode represents an executable file,
449 return mode[-3:] == "755"
451 def isModeExecChanged(src_mode, dst_mode):
452 return isModeExec(src_mode) != isModeExec(dst_mode)
454 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
456 if isinstance(cmd,basestring):
463 cmd = p4_build_cmd(cmd)
465 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
467 # Use a temporary file to avoid deadlocks without
468 # subprocess.communicate(), which would put another copy
469 # of stdout into memory.
471 if stdin is not None:
472 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
473 if isinstance(stdin,basestring):
474 stdin_file.write(stdin)
477 stdin_file.write(i + '\n')
481 p4 = subprocess.Popen(cmd,
484 stdout=subprocess.PIPE)
489 entry = marshal.load(p4.stdout)
499 entry["p4ExitCode"] = exitCode
505 list = p4CmdList(cmd)
511 def p4Where(depotPath):
512 if not depotPath.endswith("/"):
514 depotPathLong = depotPath + "..."
515 outputList = p4CmdList(["where", depotPathLong])
517 for entry in outputList:
518 if "depotFile" in entry:
519 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
520 # The base path always ends with "/...".
521 if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
524 elif "data" in entry:
525 data = entry.get("data")
526 space = data.find(" ")
527 if data[:space] == depotPath:
532 if output["code"] == "error":
536 clientPath = output.get("path")
537 elif "data" in output:
538 data = output.get("data")
539 lastSpace = data.rfind(" ")
540 clientPath = data[lastSpace + 1:]
542 if clientPath.endswith("..."):
543 clientPath = clientPath[:-3]
546 def currentGitBranch():
547 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
549 def isValidGitDir(path):
550 if (os.path.exists(path + "/HEAD")
551 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
555 def parseRevision(ref):
556 return read_pipe("git rev-parse %s" % ref).strip()
558 def branchExists(ref):
559 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
563 def extractLogMessageFromGitCommit(commit):
566 ## fixme: title is first line of commit, not 1st paragraph.
568 for log in read_pipe_lines("git cat-file commit %s" % commit):
577 def extractSettingsGitLog(log):
579 for line in log.split("\n"):
581 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
585 assignments = m.group(1).split (':')
586 for a in assignments:
588 key = vals[0].strip()
589 val = ('='.join (vals[1:])).strip()
590 if val.endswith ('\"') and val.startswith('"'):
595 paths = values.get("depot-paths")
597 paths = values.get("depot-path")
599 values['depot-paths'] = paths.split(',')
602 def gitBranchExists(branch):
603 proc = subprocess.Popen(["git", "rev-parse", branch],
604 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
605 return proc.wait() == 0;
610 if not _gitConfig.has_key(key):
611 cmd = [ "git", "config", key ]
612 s = read_pipe(cmd, ignore_error=True)
613 _gitConfig[key] = s.strip()
614 return _gitConfig[key]
616 def gitConfigBool(key):
617 """Return a bool, using git config --bool. It is True only if the
618 variable is set to true, and False if set to false or not present
621 if not _gitConfig.has_key(key):
622 cmd = [ "git", "config", "--bool", key ]
623 s = read_pipe(cmd, ignore_error=True)
625 _gitConfig[key] = v == "true"
626 return _gitConfig[key]
628 def gitConfigList(key):
629 if not _gitConfig.has_key(key):
630 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
631 _gitConfig[key] = s.strip().split(os.linesep)
632 return _gitConfig[key]
634 def p4BranchesInGit(branchesAreInRemotes=True):
635 """Find all the branches whose names start with "p4/", looking
636 in remotes or heads as specified by the argument. Return
637 a dictionary of { branch: revision } for each one found.
638 The branch names are the short names, without any
643 cmdline = "git rev-parse --symbolic "
644 if branchesAreInRemotes:
645 cmdline += "--remotes"
647 cmdline += "--branches"
649 for line in read_pipe_lines(cmdline):
653 if not line.startswith('p4/'):
655 # special symbolic ref to p4/master
656 if line == "p4/HEAD":
659 # strip off p4/ prefix
660 branch = line[len("p4/"):]
662 branches[branch] = parseRevision(line)
666 def branch_exists(branch):
667 """Make sure that the given ref name really exists."""
669 cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
670 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
671 out, _ = p.communicate()
674 # expect exactly one line of output: the branch name
675 return out.rstrip() == branch
677 def findUpstreamBranchPoint(head = "HEAD"):
678 branches = p4BranchesInGit()
679 # map from depot-path to branch name
680 branchByDepotPath = {}
681 for branch in branches.keys():
682 tip = branches[branch]
683 log = extractLogMessageFromGitCommit(tip)
684 settings = extractSettingsGitLog(log)
685 if settings.has_key("depot-paths"):
686 paths = ",".join(settings["depot-paths"])
687 branchByDepotPath[paths] = "remotes/p4/" + branch
691 while parent < 65535:
692 commit = head + "~%s" % parent
693 log = extractLogMessageFromGitCommit(commit)
694 settings = extractSettingsGitLog(log)
695 if settings.has_key("depot-paths"):
696 paths = ",".join(settings["depot-paths"])
697 if branchByDepotPath.has_key(paths):
698 return [branchByDepotPath[paths], settings]
702 return ["", settings]
704 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
706 print ("Creating/updating branch(es) in %s based on origin branch(es)"
709 originPrefix = "origin/p4/"
711 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
713 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
716 headName = line[len(originPrefix):]
717 remoteHead = localRefPrefix + headName
720 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
721 if (not original.has_key('depot-paths')
722 or not original.has_key('change')):
726 if not gitBranchExists(remoteHead):
728 print "creating %s" % remoteHead
731 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
732 if settings.has_key('change') > 0:
733 if settings['depot-paths'] == original['depot-paths']:
734 originP4Change = int(original['change'])
735 p4Change = int(settings['change'])
736 if originP4Change > p4Change:
737 print ("%s (%s) is newer than %s (%s). "
738 "Updating p4 branch from origin."
739 % (originHead, originP4Change,
740 remoteHead, p4Change))
743 print ("Ignoring: %s was imported from %s while "
744 "%s was imported from %s"
745 % (originHead, ','.join(original['depot-paths']),
746 remoteHead, ','.join(settings['depot-paths'])))
749 system("git update-ref %s %s" % (remoteHead, originHead))
751 def originP4BranchesExist():
752 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
755 def p4ParseNumericChangeRange(parts):
756 changeStart = int(parts[0][1:])
757 if parts[1] == '#head':
758 changeEnd = p4_last_change()
760 changeEnd = int(parts[1])
762 return (changeStart, changeEnd)
764 def chooseBlockSize(blockSize):
768 return defaultBlockSize
770 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
773 # Parse the change range into start and end. Try to find integer
774 # revision ranges as these can be broken up into blocks to avoid
775 # hitting server-side limits (maxrows, maxscanresults). But if
776 # that doesn't work, fall back to using the raw revision specifier
777 # strings, without using block mode.
779 if changeRange is None or changeRange == '':
781 changeEnd = p4_last_change()
782 block_size = chooseBlockSize(requestedBlockSize)
784 parts = changeRange.split(',')
785 assert len(parts) == 2
787 (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
788 block_size = chooseBlockSize(requestedBlockSize)
790 changeStart = parts[0][1:]
792 if requestedBlockSize:
793 die("cannot use --changes-block-size with non-numeric revisions")
796 # Accumulate change numbers in a dictionary to avoid duplicates
800 # Retrieve changes a block at a time, to prevent running
801 # into a MaxResults/MaxScanRows error from the server.
807 end = min(changeEnd, changeStart + block_size)
808 revisionRange = "%d,%d" % (changeStart, end)
810 revisionRange = "%s,%s" % (changeStart, changeEnd)
812 cmd += ["%s...@%s" % (p, revisionRange)]
814 for line in p4_read_pipe_lines(cmd):
815 changeNum = int(line.split(" ")[1])
816 changes[changeNum] = True
824 changeStart = end + 1
826 changelist = changes.keys()
830 def p4PathStartsWith(path, prefix):
831 # This method tries to remedy a potential mixed-case issue:
833 # If UserA adds //depot/DirA/file1
834 # and UserB adds //depot/dira/file2
836 # we may or may not have a problem. If you have core.ignorecase=true,
837 # we treat DirA and dira as the same directory
838 if gitConfigBool("core.ignorecase"):
839 return path.lower().startswith(prefix.lower())
840 return path.startswith(prefix)
843 """Look at the p4 client spec, create a View() object that contains
844 all the mappings, and return it."""
846 specList = p4CmdList("client -o")
847 if len(specList) != 1:
848 die('Output from "client -o" is %d lines, expecting 1' %
851 # dictionary of all client parameters
855 client_name = entry["Client"]
857 # just the keys that start with "View"
858 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
861 view = View(client_name)
863 # append the lines, in order, to the view
864 for view_num in range(len(view_keys)):
865 k = "View%d" % view_num
866 if k not in view_keys:
867 die("Expected view key %s missing" % k)
868 view.append(entry[k])
873 """Grab the client directory."""
875 output = p4CmdList("client -o")
877 die('Output from "client -o" is %d lines, expecting 1' % len(output))
880 if "Root" not in entry:
881 die('Client has no "Root"')
886 # P4 wildcards are not allowed in filenames. P4 complains
887 # if you simply add them, but you can force it with "-f", in
888 # which case it translates them into %xx encoding internally.
890 def wildcard_decode(path):
891 # Search for and fix just these four characters. Do % last so
892 # that fixing it does not inadvertently create new %-escapes.
893 # Cannot have * in a filename in windows; untested as to
894 # what p4 would do in such a case.
895 if not platform.system() == "Windows":
896 path = path.replace("%2A", "*")
897 path = path.replace("%23", "#") \
898 .replace("%40", "@") \
902 def wildcard_encode(path):
903 # do % first to avoid double-encoding the %s introduced here
904 path = path.replace("%", "%25") \
905 .replace("*", "%2A") \
906 .replace("#", "%23") \
910 def wildcard_present(path):
911 m = re.search("[*#@%]", path)
916 self.usage = "usage: %prog [options]"
922 self.userMapFromPerforceServer = False
923 self.myP4UserId = None
927 return self.myP4UserId
929 results = p4CmdList("user -o")
931 if r.has_key('User'):
932 self.myP4UserId = r['User']
934 die("Could not find your p4 user id")
936 def p4UserIsMe(self, p4User):
937 # return True if the given p4 user is actually me
939 if not p4User or p4User != me:
944 def getUserCacheFilename(self):
945 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
946 return home + "/.gitp4-usercache.txt"
948 def getUserMapFromPerforceServer(self):
949 if self.userMapFromPerforceServer:
954 for output in p4CmdList("users"):
955 if not output.has_key("User"):
957 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
958 self.emails[output["Email"]] = output["User"]
962 for (key, val) in self.users.items():
963 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
965 open(self.getUserCacheFilename(), "wb").write(s)
966 self.userMapFromPerforceServer = True
968 def loadUserMapFromCache(self):
970 self.userMapFromPerforceServer = False
972 cache = open(self.getUserCacheFilename(), "rb")
973 lines = cache.readlines()
976 entry = line.strip().split("\t")
977 self.users[entry[0]] = entry[1]
979 self.getUserMapFromPerforceServer()
981 class P4Debug(Command):
983 Command.__init__(self)
985 self.description = "A tool to debug the output of p4 -G."
986 self.needsGit = False
990 for output in p4CmdList(args):
991 print 'Element: %d' % j
996 class P4RollBack(Command):
998 Command.__init__(self)
1000 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1002 self.description = "A tool to debug the multi-branch import. Don't use :)"
1003 self.rollbackLocalBranches = False
1005 def run(self, args):
1008 maxChange = int(args[0])
1010 if "p4ExitCode" in p4Cmd("changes -m 1"):
1011 die("Problems executing p4");
1013 if self.rollbackLocalBranches:
1014 refPrefix = "refs/heads/"
1015 lines = read_pipe_lines("git rev-parse --symbolic --branches")
1017 refPrefix = "refs/remotes/"
1018 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1021 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1023 ref = refPrefix + line
1024 log = extractLogMessageFromGitCommit(ref)
1025 settings = extractSettingsGitLog(log)
1027 depotPaths = settings['depot-paths']
1028 change = settings['change']
1032 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
1033 for p in depotPaths]))) == 0:
1034 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1035 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1038 while change and int(change) > maxChange:
1041 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
1042 system("git update-ref %s \"%s^\"" % (ref, ref))
1043 log = extractLogMessageFromGitCommit(ref)
1044 settings = extractSettingsGitLog(log)
1047 depotPaths = settings['depot-paths']
1048 change = settings['change']
1051 print "%s rewound to %s" % (ref, change)
1055 class P4Submit(Command, P4UserMap):
1057 conflict_behavior_choices = ("ask", "skip", "quit")
1060 Command.__init__(self)
1061 P4UserMap.__init__(self)
1063 optparse.make_option("--origin", dest="origin"),
1064 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1065 # preserve the user, requires relevant p4 permissions
1066 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1067 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1068 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1069 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1070 optparse.make_option("--conflict", dest="conflict_behavior",
1071 choices=self.conflict_behavior_choices),
1072 optparse.make_option("--branch", dest="branch"),
1074 self.description = "Submit changes from git to the perforce depot."
1075 self.usage += " [name of git branch to submit into perforce depot]"
1077 self.detectRenames = False
1078 self.preserveUser = gitConfigBool("git-p4.preserveUser")
1079 self.dry_run = False
1080 self.prepare_p4_only = False
1081 self.conflict_behavior = None
1082 self.isWindows = (platform.system() == "Windows")
1083 self.exportLabels = False
1084 self.p4HasMoveCommand = p4_has_move_command()
1088 if len(p4CmdList("opened ...")) > 0:
1089 die("You have files opened with perforce! Close them before starting the sync.")
1091 def separate_jobs_from_description(self, message):
1092 """Extract and return a possible Jobs field in the commit
1093 message. It goes into a separate section in the p4 change
1096 A jobs line starts with "Jobs:" and looks like a new field
1097 in a form. Values are white-space separated on the same
1098 line or on following lines that start with a tab.
1100 This does not parse and extract the full git commit message
1101 like a p4 form. It just sees the Jobs: line as a marker
1102 to pass everything from then on directly into the p4 form,
1103 but outside the description section.
1105 Return a tuple (stripped log message, jobs string)."""
1107 m = re.search(r'^Jobs:', message, re.MULTILINE)
1109 return (message, None)
1111 jobtext = message[m.start():]
1112 stripped_message = message[:m.start()].rstrip()
1113 return (stripped_message, jobtext)
1115 def prepareLogMessage(self, template, message, jobs):
1116 """Edits the template returned from "p4 change -o" to insert
1117 the message in the Description field, and the jobs text in
1121 inDescriptionSection = False
1123 for line in template.split("\n"):
1124 if line.startswith("#"):
1125 result += line + "\n"
1128 if inDescriptionSection:
1129 if line.startswith("Files:") or line.startswith("Jobs:"):
1130 inDescriptionSection = False
1131 # insert Jobs section
1133 result += jobs + "\n"
1137 if line.startswith("Description:"):
1138 inDescriptionSection = True
1140 for messageLine in message.split("\n"):
1141 line += "\t" + messageLine + "\n"
1143 result += line + "\n"
1147 def patchRCSKeywords(self, file, pattern):
1148 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1149 (handle, outFileName) = tempfile.mkstemp(dir='.')
1151 outFile = os.fdopen(handle, "w+")
1152 inFile = open(file, "r")
1153 regexp = re.compile(pattern, re.VERBOSE)
1154 for line in inFile.readlines():
1155 line = regexp.sub(r'$\1$', line)
1159 # Forcibly overwrite the original file
1161 shutil.move(outFileName, file)
1163 # cleanup our temporary file
1164 os.unlink(outFileName)
1165 print "Failed to strip RCS keywords in %s" % file
1168 print "Patched up RCS keywords in %s" % file
1170 def p4UserForCommit(self,id):
1171 # Return the tuple (perforce user,git email) for a given git commit id
1172 self.getUserMapFromPerforceServer()
1173 gitEmail = read_pipe(["git", "log", "--max-count=1",
1174 "--format=%ae", id])
1175 gitEmail = gitEmail.strip()
1176 if not self.emails.has_key(gitEmail):
1177 return (None,gitEmail)
1179 return (self.emails[gitEmail],gitEmail)
1181 def checkValidP4Users(self,commits):
1182 # check if any git authors cannot be mapped to p4 users
1184 (user,email) = self.p4UserForCommit(id)
1186 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1187 if gitConfigBool("git-p4.allowMissingP4Users"):
1190 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1192 def lastP4Changelist(self):
1193 # Get back the last changelist number submitted in this client spec. This
1194 # then gets used to patch up the username in the change. If the same
1195 # client spec is being used by multiple processes then this might go
1197 results = p4CmdList("client -o") # find the current client
1200 if r.has_key('Client'):
1201 client = r['Client']
1204 die("could not get client spec")
1205 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1207 if r.has_key('change'):
1209 die("Could not get changelist number for last submit - cannot patch up user details")
1211 def modifyChangelistUser(self, changelist, newUser):
1212 # fixup the user field of a changelist after it has been submitted.
1213 changes = p4CmdList("change -o %s" % changelist)
1214 if len(changes) != 1:
1215 die("Bad output from p4 change modifying %s to user %s" %
1216 (changelist, newUser))
1219 if c['User'] == newUser: return # nothing to do
1221 input = marshal.dumps(c)
1223 result = p4CmdList("change -f -i", stdin=input)
1225 if r.has_key('code'):
1226 if r['code'] == 'error':
1227 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1228 if r.has_key('data'):
1229 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1231 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1233 def canChangeChangelists(self):
1234 # check to see if we have p4 admin or super-user permissions, either of
1235 # which are required to modify changelists.
1236 results = p4CmdList(["protects", self.depotPath])
1238 if r.has_key('perm'):
1239 if r['perm'] == 'admin':
1241 if r['perm'] == 'super':
1245 def prepareSubmitTemplate(self):
1246 """Run "p4 change -o" to grab a change specification template.
1247 This does not use "p4 -G", as it is nice to keep the submission
1248 template in original order, since a human might edit it.
1250 Remove lines in the Files section that show changes to files
1251 outside the depot path we're committing into."""
1254 inFilesSection = False
1255 for line in p4_read_pipe_lines(['change', '-o']):
1256 if line.endswith("\r\n"):
1257 line = line[:-2] + "\n"
1259 if line.startswith("\t"):
1260 # path starts and ends with a tab
1262 lastTab = path.rfind("\t")
1264 path = path[:lastTab]
1265 if not p4PathStartsWith(path, self.depotPath):
1268 inFilesSection = False
1270 if line.startswith("Files:"):
1271 inFilesSection = True
1277 def edit_template(self, template_file):
1278 """Invoke the editor to let the user change the submission
1279 message. Return true if okay to continue with the submit."""
1281 # if configured to skip the editing part, just submit
1282 if gitConfigBool("git-p4.skipSubmitEdit"):
1285 # look at the modification time, to check later if the user saved
1287 mtime = os.stat(template_file).st_mtime
1290 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1291 editor = os.environ.get("P4EDITOR")
1293 editor = read_pipe("git var GIT_EDITOR").strip()
1294 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1296 # If the file was not saved, prompt to see if this patch should
1297 # be skipped. But skip this verification step if configured so.
1298 if gitConfigBool("git-p4.skipSubmitEditCheck"):
1301 # modification time updated means user saved the file
1302 if os.stat(template_file).st_mtime > mtime:
1306 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1312 def get_diff_description(self, editedFiles, filesToAdd):
1314 if os.environ.has_key("P4DIFF"):
1315 del(os.environ["P4DIFF"])
1317 for editedFile in editedFiles:
1318 diff += p4_read_pipe(['diff', '-du',
1319 wildcard_encode(editedFile)])
1323 for newFile in filesToAdd:
1324 newdiff += "==== new file ====\n"
1325 newdiff += "--- /dev/null\n"
1326 newdiff += "+++ %s\n" % newFile
1327 f = open(newFile, "r")
1328 for line in f.readlines():
1329 newdiff += "+" + line
1332 return (diff + newdiff).replace('\r\n', '\n')
1334 def applyCommit(self, id):
1335 """Apply one commit, return True if it succeeded."""
1337 print "Applying", read_pipe(["git", "show", "-s",
1338 "--format=format:%h %s", id])
1340 (p4User, gitEmail) = self.p4UserForCommit(id)
1342 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1344 filesToDelete = set()
1346 pureRenameCopy = set()
1347 filesToChangeExecBit = {}
1350 diff = parseDiffTreeEntry(line)
1351 modifier = diff['status']
1355 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1356 filesToChangeExecBit[path] = diff['dst_mode']
1357 editedFiles.add(path)
1358 elif modifier == "A":
1359 filesToAdd.add(path)
1360 filesToChangeExecBit[path] = diff['dst_mode']
1361 if path in filesToDelete:
1362 filesToDelete.remove(path)
1363 elif modifier == "D":
1364 filesToDelete.add(path)
1365 if path in filesToAdd:
1366 filesToAdd.remove(path)
1367 elif modifier == "C":
1368 src, dest = diff['src'], diff['dst']
1369 p4_integrate(src, dest)
1370 pureRenameCopy.add(dest)
1371 if diff['src_sha1'] != diff['dst_sha1']:
1373 pureRenameCopy.discard(dest)
1374 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1376 pureRenameCopy.discard(dest)
1377 filesToChangeExecBit[dest] = diff['dst_mode']
1379 # turn off read-only attribute
1380 os.chmod(dest, stat.S_IWRITE)
1382 editedFiles.add(dest)
1383 elif modifier == "R":
1384 src, dest = diff['src'], diff['dst']
1385 if self.p4HasMoveCommand:
1386 p4_edit(src) # src must be open before move
1387 p4_move(src, dest) # opens for (move/delete, move/add)
1389 p4_integrate(src, dest)
1390 if diff['src_sha1'] != diff['dst_sha1']:
1393 pureRenameCopy.add(dest)
1394 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1395 if not self.p4HasMoveCommand:
1396 p4_edit(dest) # with move: already open, writable
1397 filesToChangeExecBit[dest] = diff['dst_mode']
1398 if not self.p4HasMoveCommand:
1400 os.chmod(dest, stat.S_IWRITE)
1402 filesToDelete.add(src)
1403 editedFiles.add(dest)
1405 die("unknown modifier %s for %s" % (modifier, path))
1407 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1408 patchcmd = diffcmd + " | git apply "
1409 tryPatchCmd = patchcmd + "--check -"
1410 applyPatchCmd = patchcmd + "--check --apply -"
1411 patch_succeeded = True
1413 if os.system(tryPatchCmd) != 0:
1414 fixed_rcs_keywords = False
1415 patch_succeeded = False
1416 print "Unfortunately applying the change failed!"
1418 # Patch failed, maybe it's just RCS keyword woes. Look through
1419 # the patch to see if that's possible.
1420 if gitConfigBool("git-p4.attemptRCSCleanup"):
1424 for file in editedFiles | filesToDelete:
1425 # did this file's delta contain RCS keywords?
1426 pattern = p4_keywords_regexp_for_file(file)
1429 # this file is a possibility...look for RCS keywords.
1430 regexp = re.compile(pattern, re.VERBOSE)
1431 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1432 if regexp.search(line):
1434 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1435 kwfiles[file] = pattern
1438 for file in kwfiles:
1440 print "zapping %s with %s" % (line,pattern)
1441 # File is being deleted, so not open in p4. Must
1442 # disable the read-only bit on windows.
1443 if self.isWindows and file not in editedFiles:
1444 os.chmod(file, stat.S_IWRITE)
1445 self.patchRCSKeywords(file, kwfiles[file])
1446 fixed_rcs_keywords = True
1448 if fixed_rcs_keywords:
1449 print "Retrying the patch with RCS keywords cleaned up"
1450 if os.system(tryPatchCmd) == 0:
1451 patch_succeeded = True
1453 if not patch_succeeded:
1454 for f in editedFiles:
1459 # Apply the patch for real, and do add/delete/+x handling.
1461 system(applyPatchCmd)
1463 for f in filesToAdd:
1465 for f in filesToDelete:
1469 # Set/clear executable bits
1470 for f in filesToChangeExecBit.keys():
1471 mode = filesToChangeExecBit[f]
1472 setP4ExecBit(f, mode)
1475 # Build p4 change description, starting with the contents
1476 # of the git commit message.
1478 logMessage = extractLogMessageFromGitCommit(id)
1479 logMessage = logMessage.strip()
1480 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1482 template = self.prepareSubmitTemplate()
1483 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1485 if self.preserveUser:
1486 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1488 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1489 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1490 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1491 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1493 separatorLine = "######## everything below this line is just the diff #######\n"
1494 if not self.prepare_p4_only:
1495 submitTemplate += separatorLine
1496 submitTemplate += self.get_diff_description(editedFiles, filesToAdd)
1498 (handle, fileName) = tempfile.mkstemp()
1499 tmpFile = os.fdopen(handle, "w+b")
1501 submitTemplate = submitTemplate.replace("\n", "\r\n")
1502 tmpFile.write(submitTemplate)
1505 if self.prepare_p4_only:
1507 # Leave the p4 tree prepared, and the submit template around
1508 # and let the user decide what to do next
1511 print "P4 workspace prepared for submission."
1512 print "To submit or revert, go to client workspace"
1513 print " " + self.clientPath
1515 print "To submit, use \"p4 submit\" to write a new description,"
1516 print "or \"p4 submit -i <%s\" to use the one prepared by" \
1517 " \"git p4\"." % fileName
1518 print "You can delete the file \"%s\" when finished." % fileName
1520 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1521 print "To preserve change ownership by user %s, you must\n" \
1522 "do \"p4 change -f <change>\" after submitting and\n" \
1523 "edit the User field."
1525 print "After submitting, renamed files must be re-synced."
1526 print "Invoke \"p4 sync -f\" on each of these files:"
1527 for f in pureRenameCopy:
1531 print "To revert the changes, use \"p4 revert ...\", and delete"
1532 print "the submit template file \"%s\"" % fileName
1534 print "Since the commit adds new files, they must be deleted:"
1535 for f in filesToAdd:
1541 # Let the user edit the change description, then submit it.
1543 if self.edit_template(fileName):
1544 # read the edited message and submit
1546 tmpFile = open(fileName, "rb")
1547 message = tmpFile.read()
1550 message = message.replace("\r\n", "\n")
1551 submitTemplate = message[:message.index(separatorLine)]
1552 p4_write_pipe(['submit', '-i'], submitTemplate)
1554 if self.preserveUser:
1556 # Get last changelist number. Cannot easily get it from
1557 # the submit command output as the output is
1559 changelist = self.lastP4Changelist()
1560 self.modifyChangelistUser(changelist, p4User)
1562 # The rename/copy happened by applying a patch that created a
1563 # new file. This leaves it writable, which confuses p4.
1564 for f in pureRenameCopy:
1570 print "Submission cancelled, undoing p4 changes."
1571 for f in editedFiles:
1573 for f in filesToAdd:
1576 for f in filesToDelete:
1582 # Export git tags as p4 labels. Create a p4 label and then tag
1584 def exportGitTags(self, gitTags):
1585 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1586 if len(validLabelRegexp) == 0:
1587 validLabelRegexp = defaultLabelRegexp
1588 m = re.compile(validLabelRegexp)
1590 for name in gitTags:
1592 if not m.match(name):
1594 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1597 # Get the p4 commit this corresponds to
1598 logMessage = extractLogMessageFromGitCommit(name)
1599 values = extractSettingsGitLog(logMessage)
1601 if not values.has_key('change'):
1602 # a tag pointing to something not sent to p4; ignore
1604 print "git tag %s does not give a p4 commit" % name
1607 changelist = values['change']
1609 # Get the tag details.
1613 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1616 if re.match(r'tag\s+', l):
1618 elif re.match(r'\s*$', l):
1625 body = ["lightweight tag imported by git p4\n"]
1627 # Create the label - use the same view as the client spec we are using
1628 clientSpec = getClientSpec()
1630 labelTemplate = "Label: %s\n" % name
1631 labelTemplate += "Description:\n"
1633 labelTemplate += "\t" + b + "\n"
1634 labelTemplate += "View:\n"
1635 for depot_side in clientSpec.mappings:
1636 labelTemplate += "\t%s\n" % depot_side
1639 print "Would create p4 label %s for tag" % name
1640 elif self.prepare_p4_only:
1641 print "Not creating p4 label %s for tag due to option" \
1642 " --prepare-p4-only" % name
1644 p4_write_pipe(["label", "-i"], labelTemplate)
1647 p4_system(["tag", "-l", name] +
1648 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1651 print "created p4 label for tag %s" % name
1653 def run(self, args):
1655 self.master = currentGitBranch()
1656 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
1657 die("Detecting current git branch failed!")
1658 elif len(args) == 1:
1659 self.master = args[0]
1660 if not branchExists(self.master):
1661 die("Branch %s does not exist" % self.master)
1665 allowSubmit = gitConfig("git-p4.allowSubmit")
1666 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1667 die("%s is not in git-p4.allowSubmit" % self.master)
1669 [upstream, settings] = findUpstreamBranchPoint()
1670 self.depotPath = settings['depot-paths'][0]
1671 if len(self.origin) == 0:
1672 self.origin = upstream
1674 if self.preserveUser:
1675 if not self.canChangeChangelists():
1676 die("Cannot preserve user names without p4 super-user or admin permissions")
1678 # if not set from the command line, try the config file
1679 if self.conflict_behavior is None:
1680 val = gitConfig("git-p4.conflict")
1682 if val not in self.conflict_behavior_choices:
1683 die("Invalid value '%s' for config git-p4.conflict" % val)
1686 self.conflict_behavior = val
1689 print "Origin branch is " + self.origin
1691 if len(self.depotPath) == 0:
1692 print "Internal error: cannot locate perforce depot path from existing branches"
1695 self.useClientSpec = False
1696 if gitConfigBool("git-p4.useclientspec"):
1697 self.useClientSpec = True
1698 if self.useClientSpec:
1699 self.clientSpecDirs = getClientSpec()
1701 # Check for the existance of P4 branches
1702 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
1704 if self.useClientSpec and not branchesDetected:
1705 # all files are relative to the client spec
1706 self.clientPath = getClientRoot()
1708 self.clientPath = p4Where(self.depotPath)
1710 if self.clientPath == "":
1711 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1713 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1714 self.oldWorkingDirectory = os.getcwd()
1716 # ensure the clientPath exists
1717 new_client_dir = False
1718 if not os.path.exists(self.clientPath):
1719 new_client_dir = True
1720 os.makedirs(self.clientPath)
1722 chdir(self.clientPath, is_client_path=True)
1724 print "Would synchronize p4 checkout in %s" % self.clientPath
1726 print "Synchronizing p4 checkout..."
1728 # old one was destroyed, and maybe nobody told p4
1729 p4_sync("...", "-f")
1735 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, self.master)]):
1736 commits.append(line.strip())
1739 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1740 self.checkAuthorship = False
1742 self.checkAuthorship = True
1744 if self.preserveUser:
1745 self.checkValidP4Users(commits)
1748 # Build up a set of options to be passed to diff when
1749 # submitting each commit to p4.
1751 if self.detectRenames:
1752 # command-line -M arg
1753 self.diffOpts = "-M"
1755 # If not explicitly set check the config variable
1756 detectRenames = gitConfig("git-p4.detectRenames")
1758 if detectRenames.lower() == "false" or detectRenames == "":
1760 elif detectRenames.lower() == "true":
1761 self.diffOpts = "-M"
1763 self.diffOpts = "-M%s" % detectRenames
1765 # no command-line arg for -C or --find-copies-harder, just
1767 detectCopies = gitConfig("git-p4.detectCopies")
1768 if detectCopies.lower() == "false" or detectCopies == "":
1770 elif detectCopies.lower() == "true":
1771 self.diffOpts += " -C"
1773 self.diffOpts += " -C%s" % detectCopies
1775 if gitConfigBool("git-p4.detectCopiesHarder"):
1776 self.diffOpts += " --find-copies-harder"
1779 # Apply the commits, one at a time. On failure, ask if should
1780 # continue to try the rest of the patches, or quit.
1785 last = len(commits) - 1
1786 for i, commit in enumerate(commits):
1788 print " ", read_pipe(["git", "show", "-s",
1789 "--format=format:%h %s", commit])
1792 ok = self.applyCommit(commit)
1794 applied.append(commit)
1796 if self.prepare_p4_only and i < last:
1797 print "Processing only the first commit due to option" \
1798 " --prepare-p4-only"
1803 # prompt for what to do, or use the option/variable
1804 if self.conflict_behavior == "ask":
1805 print "What do you want to do?"
1806 response = raw_input("[s]kip this commit but apply"
1807 " the rest, or [q]uit? ")
1810 elif self.conflict_behavior == "skip":
1812 elif self.conflict_behavior == "quit":
1815 die("Unknown conflict_behavior '%s'" %
1816 self.conflict_behavior)
1818 if response[0] == "s":
1819 print "Skipping this commit, but applying the rest"
1821 if response[0] == "q":
1828 chdir(self.oldWorkingDirectory)
1832 elif self.prepare_p4_only:
1834 elif len(commits) == len(applied):
1835 print "All commits applied!"
1839 sync.branch = self.branch
1846 if len(applied) == 0:
1847 print "No commits applied."
1849 print "Applied only the commits marked with '*':"
1855 print star, read_pipe(["git", "show", "-s",
1856 "--format=format:%h %s", c])
1857 print "You will have to do 'git p4 sync' and rebase."
1859 if gitConfigBool("git-p4.exportLabels"):
1860 self.exportLabels = True
1862 if self.exportLabels:
1863 p4Labels = getP4Labels(self.depotPath)
1864 gitTags = getGitTags()
1866 missingGitTags = gitTags - p4Labels
1867 self.exportGitTags(missingGitTags)
1869 # exit with error unless everything applied perfectly
1870 if len(commits) != len(applied):
1876 """Represent a p4 view ("p4 help views"), and map files in a
1877 repo according to the view."""
1879 def __init__(self, client_name):
1881 self.client_prefix = "//%s/" % client_name
1882 # cache results of "p4 where" to lookup client file locations
1883 self.client_spec_path_cache = {}
1885 def append(self, view_line):
1886 """Parse a view line, splitting it into depot and client
1887 sides. Append to self.mappings, preserving order. This
1888 is only needed for tag creation."""
1890 # Split the view line into exactly two words. P4 enforces
1891 # structure on these lines that simplifies this quite a bit.
1893 # Either or both words may be double-quoted.
1894 # Single quotes do not matter.
1895 # Double-quote marks cannot occur inside the words.
1896 # A + or - prefix is also inside the quotes.
1897 # There are no quotes unless they contain a space.
1898 # The line is already white-space stripped.
1899 # The two words are separated by a single space.
1901 if view_line[0] == '"':
1902 # First word is double quoted. Find its end.
1903 close_quote_index = view_line.find('"', 1)
1904 if close_quote_index <= 0:
1905 die("No first-word closing quote found: %s" % view_line)
1906 depot_side = view_line[1:close_quote_index]
1907 # skip closing quote and space
1908 rhs_index = close_quote_index + 1 + 1
1910 space_index = view_line.find(" ")
1911 if space_index <= 0:
1912 die("No word-splitting space found: %s" % view_line)
1913 depot_side = view_line[0:space_index]
1914 rhs_index = space_index + 1
1916 # prefix + means overlay on previous mapping
1917 if depot_side.startswith("+"):
1918 depot_side = depot_side[1:]
1920 # prefix - means exclude this path, leave out of mappings
1922 if depot_side.startswith("-"):
1924 depot_side = depot_side[1:]
1927 self.mappings.append(depot_side)
1929 def convert_client_path(self, clientFile):
1930 # chop off //client/ part to make it relative
1931 if not clientFile.startswith(self.client_prefix):
1932 die("No prefix '%s' on clientFile '%s'" %
1933 (self.client_prefix, clientFile))
1934 return clientFile[len(self.client_prefix):]
1936 def update_client_spec_path_cache(self, files):
1937 """ Caching file paths by "p4 where" batch query """
1939 # List depot file paths exclude that already cached
1940 fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
1942 if len(fileArgs) == 0:
1943 return # All files in cache
1945 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
1946 for res in where_result:
1947 if "code" in res and res["code"] == "error":
1948 # assume error is "... file(s) not in client view"
1950 if "clientFile" not in res:
1951 die("No clientFile in 'p4 where' output")
1953 # it will list all of them, but only one not unmap-ped
1955 if gitConfigBool("core.ignorecase"):
1956 res['depotFile'] = res['depotFile'].lower()
1957 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
1959 # not found files or unmap files set to ""
1960 for depotFile in fileArgs:
1961 if gitConfigBool("core.ignorecase"):
1962 depotFile = depotFile.lower()
1963 if depotFile not in self.client_spec_path_cache:
1964 self.client_spec_path_cache[depotFile] = ""
1966 def map_in_client(self, depot_path):
1967 """Return the relative location in the client where this
1968 depot file should live. Returns "" if the file should
1969 not be mapped in the client."""
1971 if gitConfigBool("core.ignorecase"):
1972 depot_path = depot_path.lower()
1974 if depot_path in self.client_spec_path_cache:
1975 return self.client_spec_path_cache[depot_path]
1977 die( "Error: %s is not found in client spec path" % depot_path )
1980 class P4Sync(Command, P4UserMap):
1981 delete_actions = ( "delete", "move/delete", "purge" )
1984 Command.__init__(self)
1985 P4UserMap.__init__(self)
1987 optparse.make_option("--branch", dest="branch"),
1988 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1989 optparse.make_option("--changesfile", dest="changesFile"),
1990 optparse.make_option("--silent", dest="silent", action="store_true"),
1991 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1992 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
1993 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1994 help="Import into refs/heads/ , not refs/remotes"),
1995 optparse.make_option("--max-changes", dest="maxChanges",
1996 help="Maximum number of changes to import"),
1997 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
1998 help="Internal block size to use when iteratively calling p4 changes"),
1999 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2000 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2001 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2002 help="Only sync files that are included in the Perforce Client Spec"),
2003 optparse.make_option("-/", dest="cloneExclude",
2004 action="append", type="string",
2005 help="exclude depot path"),
2007 self.description = """Imports from Perforce into a git repository.\n
2009 //depot/my/project/ -- to import the current head
2010 //depot/my/project/@all -- to import everything
2011 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2013 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2015 self.usage += " //depot/path[@revRange]"
2017 self.createdBranches = set()
2018 self.committedChanges = set()
2020 self.detectBranches = False
2021 self.detectLabels = False
2022 self.importLabels = False
2023 self.changesFile = ""
2024 self.syncWithOrigin = True
2025 self.importIntoRemotes = True
2026 self.maxChanges = ""
2027 self.changes_block_size = None
2028 self.keepRepoPath = False
2029 self.depotPaths = None
2030 self.p4BranchesInGit = []
2031 self.cloneExclude = []
2032 self.useClientSpec = False
2033 self.useClientSpec_from_options = False
2034 self.clientSpecDirs = None
2035 self.tempBranches = []
2036 self.tempBranchLocation = "git-p4-tmp"
2038 if gitConfig("git-p4.syncFromOrigin") == "false":
2039 self.syncWithOrigin = False
2041 # This is required for the "append" cloneExclude action
2042 def ensure_value(self, attr, value):
2043 if not hasattr(self, attr) or getattr(self, attr) is None:
2044 setattr(self, attr, value)
2045 return getattr(self, attr)
2047 # Force a checkpoint in fast-import and wait for it to finish
2048 def checkpoint(self):
2049 self.gitStream.write("checkpoint\n\n")
2050 self.gitStream.write("progress checkpoint\n\n")
2051 out = self.gitOutput.readline()
2053 print "checkpoint finished: " + out
2055 def extractFilesFromCommit(self, commit):
2056 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2057 for path in self.cloneExclude]
2060 while commit.has_key("depotFile%s" % fnum):
2061 path = commit["depotFile%s" % fnum]
2063 if [p for p in self.cloneExclude
2064 if p4PathStartsWith(path, p)]:
2067 found = [p for p in self.depotPaths
2068 if p4PathStartsWith(path, p)]
2075 file["rev"] = commit["rev%s" % fnum]
2076 file["action"] = commit["action%s" % fnum]
2077 file["type"] = commit["type%s" % fnum]
2082 def stripRepoPath(self, path, prefixes):
2083 """When streaming files, this is called to map a p4 depot path
2084 to where it should go in git. The prefixes are either
2085 self.depotPaths, or self.branchPrefixes in the case of
2086 branch detection."""
2088 if self.useClientSpec:
2089 # branch detection moves files up a level (the branch name)
2090 # from what client spec interpretation gives
2091 path = self.clientSpecDirs.map_in_client(path)
2092 if self.detectBranches:
2093 for b in self.knownBranches:
2094 if path.startswith(b + "/"):
2095 path = path[len(b)+1:]
2097 elif self.keepRepoPath:
2098 # Preserve everything in relative path name except leading
2099 # //depot/; just look at first prefix as they all should
2100 # be in the same depot.
2101 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2102 if p4PathStartsWith(path, depot):
2103 path = path[len(depot):]
2107 if p4PathStartsWith(path, p):
2108 path = path[len(p):]
2111 path = wildcard_decode(path)
2114 def splitFilesIntoBranches(self, commit):
2115 """Look at each depotFile in the commit to figure out to what
2116 branch it belongs."""
2118 if self.clientSpecDirs:
2119 files = self.extractFilesFromCommit(commit)
2120 self.clientSpecDirs.update_client_spec_path_cache(files)
2124 while commit.has_key("depotFile%s" % fnum):
2125 path = commit["depotFile%s" % fnum]
2126 found = [p for p in self.depotPaths
2127 if p4PathStartsWith(path, p)]
2134 file["rev"] = commit["rev%s" % fnum]
2135 file["action"] = commit["action%s" % fnum]
2136 file["type"] = commit["type%s" % fnum]
2139 # start with the full relative path where this file would
2141 if self.useClientSpec:
2142 relPath = self.clientSpecDirs.map_in_client(path)
2144 relPath = self.stripRepoPath(path, self.depotPaths)
2146 for branch in self.knownBranches.keys():
2147 # add a trailing slash so that a commit into qt/4.2foo
2148 # doesn't end up in qt/4.2, e.g.
2149 if relPath.startswith(branch + "/"):
2150 if branch not in branches:
2151 branches[branch] = []
2152 branches[branch].append(file)
2157 # output one file from the P4 stream
2158 # - helper for streamP4Files
2160 def streamOneP4File(self, file, contents):
2161 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2163 sys.stderr.write("%s\n" % relPath)
2165 (type_base, type_mods) = split_p4_type(file["type"])
2168 if "x" in type_mods:
2170 if type_base == "symlink":
2172 # p4 print on a symlink sometimes contains "target\n";
2173 # if it does, remove the newline
2174 data = ''.join(contents)
2176 # Some version of p4 allowed creating a symlink that pointed
2177 # to nothing. This causes p4 errors when checking out such
2178 # a change, and errors here too. Work around it by ignoring
2179 # the bad symlink; hopefully a future change fixes it.
2180 print "\nIgnoring empty symlink in %s" % file['depotFile']
2182 elif data[-1] == '\n':
2183 contents = [data[:-1]]
2187 if type_base == "utf16":
2188 # p4 delivers different text in the python output to -G
2189 # than it does when using "print -o", or normal p4 client
2190 # operations. utf16 is converted to ascii or utf8, perhaps.
2191 # But ascii text saved as -t utf16 is completely mangled.
2192 # Invoke print -o to get the real contents.
2194 # On windows, the newlines will always be mangled by print, so put
2195 # them back too. This is not needed to the cygwin windows version,
2196 # just the native "NT" type.
2198 text = p4_read_pipe(['print', '-q', '-o', '-', "%s@%s" % (file['depotFile'], file['change']) ])
2199 if p4_version_string().find("/NT") >= 0:
2200 text = text.replace("\r\n", "\n")
2203 if type_base == "apple":
2204 # Apple filetype files will be streamed as a concatenation of
2205 # its appledouble header and the contents. This is useless
2206 # on both macs and non-macs. If using "print -q -o xx", it
2207 # will create "xx" with the data, and "%xx" with the header.
2208 # This is also not very useful.
2210 # Ideally, someday, this script can learn how to generate
2211 # appledouble files directly and import those to git, but
2212 # non-mac machines can never find a use for apple filetype.
2213 print "\nIgnoring apple filetype file %s" % file['depotFile']
2216 # Note that we do not try to de-mangle keywords on utf16 files,
2217 # even though in theory somebody may want that.
2218 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2220 regexp = re.compile(pattern, re.VERBOSE)
2221 text = ''.join(contents)
2222 text = regexp.sub(r'$\1$', text)
2225 self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
2230 length = length + len(d)
2232 self.gitStream.write("data %d\n" % length)
2234 self.gitStream.write(d)
2235 self.gitStream.write("\n")
2237 def streamOneP4Deletion(self, file):
2238 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2240 sys.stderr.write("delete %s\n" % relPath)
2241 self.gitStream.write("D %s\n" % relPath)
2243 # handle another chunk of streaming data
2244 def streamP4FilesCb(self, marshalled):
2246 # catch p4 errors and complain
2248 if "code" in marshalled:
2249 if marshalled["code"] == "error":
2250 if "data" in marshalled:
2251 err = marshalled["data"].rstrip()
2254 if self.stream_have_file_info:
2255 if "depotFile" in self.stream_file:
2256 f = self.stream_file["depotFile"]
2257 # force a failure in fast-import, else an empty
2258 # commit will be made
2259 self.gitStream.write("\n")
2260 self.gitStream.write("die-now\n")
2261 self.gitStream.close()
2262 # ignore errors, but make sure it exits first
2263 self.importProcess.wait()
2265 die("Error from p4 print for %s: %s" % (f, err))
2267 die("Error from p4 print: %s" % err)
2269 if marshalled.has_key('depotFile') and self.stream_have_file_info:
2270 # start of a new file - output the old one first
2271 self.streamOneP4File(self.stream_file, self.stream_contents)
2272 self.stream_file = {}
2273 self.stream_contents = []
2274 self.stream_have_file_info = False
2276 # pick up the new file information... for the
2277 # 'data' field we need to append to our array
2278 for k in marshalled.keys():
2280 self.stream_contents.append(marshalled['data'])
2282 self.stream_file[k] = marshalled[k]
2284 self.stream_have_file_info = True
2286 # Stream directly from "p4 files" into "git fast-import"
2287 def streamP4Files(self, files):
2293 # if using a client spec, only add the files that have
2294 # a path in the client
2295 if self.clientSpecDirs:
2296 if self.clientSpecDirs.map_in_client(f['path']) == "":
2299 filesForCommit.append(f)
2300 if f['action'] in self.delete_actions:
2301 filesToDelete.append(f)
2303 filesToRead.append(f)
2306 for f in filesToDelete:
2307 self.streamOneP4Deletion(f)
2309 if len(filesToRead) > 0:
2310 self.stream_file = {}
2311 self.stream_contents = []
2312 self.stream_have_file_info = False
2314 # curry self argument
2315 def streamP4FilesCbSelf(entry):
2316 self.streamP4FilesCb(entry)
2318 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2320 p4CmdList(["-x", "-", "print"],
2322 cb=streamP4FilesCbSelf)
2325 if self.stream_file.has_key('depotFile'):
2326 self.streamOneP4File(self.stream_file, self.stream_contents)
2328 def make_email(self, userid):
2329 if userid in self.users:
2330 return self.users[userid]
2332 return "%s <a@b>" % userid
2335 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2337 print "writing tag %s for commit %s" % (labelName, commit)
2338 gitStream.write("tag %s\n" % labelName)
2339 gitStream.write("from %s\n" % commit)
2341 if labelDetails.has_key('Owner'):
2342 owner = labelDetails["Owner"]
2346 # Try to use the owner of the p4 label, or failing that,
2347 # the current p4 user id.
2349 email = self.make_email(owner)
2351 email = self.make_email(self.p4UserId())
2352 tagger = "%s %s %s" % (email, epoch, self.tz)
2354 gitStream.write("tagger %s\n" % tagger)
2356 print "labelDetails=",labelDetails
2357 if labelDetails.has_key('Description'):
2358 description = labelDetails['Description']
2360 description = 'Label from git p4'
2362 gitStream.write("data %d\n" % len(description))
2363 gitStream.write(description)
2364 gitStream.write("\n")
2366 def commit(self, details, files, branch, parent = ""):
2367 epoch = details["time"]
2368 author = details["user"]
2371 print "commit into %s" % branch
2373 # start with reading files; if that fails, we should not
2377 if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
2378 new_files.append (f)
2380 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2382 if self.clientSpecDirs:
2383 self.clientSpecDirs.update_client_spec_path_cache(files)
2385 self.gitStream.write("commit %s\n" % branch)
2386 # gitStream.write("mark :%s\n" % details["change"])
2387 self.committedChanges.add(int(details["change"]))
2389 if author not in self.users:
2390 self.getUserMapFromPerforceServer()
2391 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2393 self.gitStream.write("committer %s\n" % committer)
2395 self.gitStream.write("data <<EOT\n")
2396 self.gitStream.write(details["desc"])
2397 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2398 (','.join(self.branchPrefixes), details["change"]))
2399 if len(details['options']) > 0:
2400 self.gitStream.write(": options = %s" % details['options'])
2401 self.gitStream.write("]\nEOT\n\n")
2405 print "parent %s" % parent
2406 self.gitStream.write("from %s\n" % parent)
2408 self.streamP4Files(new_files)
2409 self.gitStream.write("\n")
2411 change = int(details["change"])
2413 if self.labels.has_key(change):
2414 label = self.labels[change]
2415 labelDetails = label[0]
2416 labelRevisions = label[1]
2418 print "Change %s is labelled %s" % (change, labelDetails)
2420 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2421 for p in self.branchPrefixes])
2423 if len(files) == len(labelRevisions):
2427 if info["action"] in self.delete_actions:
2429 cleanedFiles[info["depotFile"]] = info["rev"]
2431 if cleanedFiles == labelRevisions:
2432 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2436 print ("Tag %s does not match with change %s: files do not match."
2437 % (labelDetails["label"], change))
2441 print ("Tag %s does not match with change %s: file count is different."
2442 % (labelDetails["label"], change))
2444 # Build a dictionary of changelists and labels, for "detect-labels" option.
2445 def getLabels(self):
2448 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2449 if len(l) > 0 and not self.silent:
2450 print "Finding files belonging to labels in %s" % `self.depotPaths`
2453 label = output["label"]
2457 print "Querying files for label %s" % label
2458 for file in p4CmdList(["files"] +
2459 ["%s...@%s" % (p, label)
2460 for p in self.depotPaths]):
2461 revisions[file["depotFile"]] = file["rev"]
2462 change = int(file["change"])
2463 if change > newestChange:
2464 newestChange = change
2466 self.labels[newestChange] = [output, revisions]
2469 print "Label changes: %s" % self.labels.keys()
2471 # Import p4 labels as git tags. A direct mapping does not
2472 # exist, so assume that if all the files are at the same revision
2473 # then we can use that, or it's something more complicated we should
2475 def importP4Labels(self, stream, p4Labels):
2477 print "import p4 labels: " + ' '.join(p4Labels)
2479 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2480 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2481 if len(validLabelRegexp) == 0:
2482 validLabelRegexp = defaultLabelRegexp
2483 m = re.compile(validLabelRegexp)
2485 for name in p4Labels:
2488 if not m.match(name):
2490 print "label %s does not match regexp %s" % (name,validLabelRegexp)
2493 if name in ignoredP4Labels:
2496 labelDetails = p4CmdList(['label', "-o", name])[0]
2498 # get the most recent changelist for each file in this label
2499 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2500 for p in self.depotPaths])
2502 if change.has_key('change'):
2503 # find the corresponding git commit; take the oldest commit
2504 changelist = int(change['change'])
2505 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2506 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2507 if len(gitCommit) == 0:
2508 print "could not find git commit for changelist %d" % changelist
2510 gitCommit = gitCommit.strip()
2512 # Convert from p4 time format
2514 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2516 print "Could not convert label time %s" % labelDetails['Update']
2519 when = int(time.mktime(tmwhen))
2520 self.streamTag(stream, name, labelDetails, gitCommit, when)
2522 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2525 print "Label %s has no changelists - possibly deleted?" % name
2528 # We can't import this label; don't try again as it will get very
2529 # expensive repeatedly fetching all the files for labels that will
2530 # never be imported. If the label is moved in the future, the
2531 # ignore will need to be removed manually.
2532 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2534 def guessProjectName(self):
2535 for p in self.depotPaths:
2538 p = p[p.strip().rfind("/") + 1:]
2539 if not p.endswith("/"):
2543 def getBranchMapping(self):
2544 lostAndFoundBranches = set()
2546 user = gitConfig("git-p4.branchUser")
2548 command = "branches -u %s" % user
2550 command = "branches"
2552 for info in p4CmdList(command):
2553 details = p4Cmd(["branch", "-o", info["branch"]])
2555 while details.has_key("View%s" % viewIdx):
2556 paths = details["View%s" % viewIdx].split(" ")
2557 viewIdx = viewIdx + 1
2558 # require standard //depot/foo/... //depot/bar/... mapping
2559 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2562 destination = paths[1]
2564 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2565 source = source[len(self.depotPaths[0]):-4]
2566 destination = destination[len(self.depotPaths[0]):-4]
2568 if destination in self.knownBranches:
2570 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2571 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2574 self.knownBranches[destination] = source
2576 lostAndFoundBranches.discard(destination)
2578 if source not in self.knownBranches:
2579 lostAndFoundBranches.add(source)
2581 # Perforce does not strictly require branches to be defined, so we also
2582 # check git config for a branch list.
2584 # Example of branch definition in git config file:
2586 # branchList=main:branchA
2587 # branchList=main:branchB
2588 # branchList=branchA:branchC
2589 configBranches = gitConfigList("git-p4.branchList")
2590 for branch in configBranches:
2592 (source, destination) = branch.split(":")
2593 self.knownBranches[destination] = source
2595 lostAndFoundBranches.discard(destination)
2597 if source not in self.knownBranches:
2598 lostAndFoundBranches.add(source)
2601 for branch in lostAndFoundBranches:
2602 self.knownBranches[branch] = branch
2604 def getBranchMappingFromGitBranches(self):
2605 branches = p4BranchesInGit(self.importIntoRemotes)
2606 for branch in branches.keys():
2607 if branch == "master":
2610 branch = branch[len(self.projectName):]
2611 self.knownBranches[branch] = branch
2613 def updateOptionDict(self, d):
2615 if self.keepRepoPath:
2616 option_keys['keepRepoPath'] = 1
2618 d["options"] = ' '.join(sorted(option_keys.keys()))
2620 def readOptions(self, d):
2621 self.keepRepoPath = (d.has_key('options')
2622 and ('keepRepoPath' in d['options']))
2624 def gitRefForBranch(self, branch):
2625 if branch == "main":
2626 return self.refPrefix + "master"
2628 if len(branch) <= 0:
2631 return self.refPrefix + self.projectName + branch
2633 def gitCommitByP4Change(self, ref, change):
2635 print "looking in ref " + ref + " for change %s using bisect..." % change
2638 latestCommit = parseRevision(ref)
2642 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2643 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2648 log = extractLogMessageFromGitCommit(next)
2649 settings = extractSettingsGitLog(log)
2650 currentChange = int(settings['change'])
2652 print "current change %s" % currentChange
2654 if currentChange == change:
2656 print "found %s" % next
2659 if currentChange < change:
2660 earliestCommit = "^%s" % next
2662 latestCommit = "%s" % next
2666 def importNewBranch(self, branch, maxChange):
2667 # make fast-import flush all changes to disk and update the refs using the checkpoint
2668 # command so that we can try to find the branch parent in the git history
2669 self.gitStream.write("checkpoint\n\n");
2670 self.gitStream.flush();
2671 branchPrefix = self.depotPaths[0] + branch + "/"
2672 range = "@1,%s" % maxChange
2673 #print "prefix" + branchPrefix
2674 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
2675 if len(changes) <= 0:
2677 firstChange = changes[0]
2678 #print "first change in branch: %s" % firstChange
2679 sourceBranch = self.knownBranches[branch]
2680 sourceDepotPath = self.depotPaths[0] + sourceBranch
2681 sourceRef = self.gitRefForBranch(sourceBranch)
2682 #print "source " + sourceBranch
2684 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
2685 #print "branch parent: %s" % branchParentChange
2686 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2687 if len(gitParent) > 0:
2688 self.initialParents[self.gitRefForBranch(branch)] = gitParent
2689 #print "parent git commit: %s" % gitParent
2691 self.importChanges(changes)
2694 def searchParent(self, parent, branch, target):
2696 for blob in read_pipe_lines(["git", "rev-list", "--reverse",
2697 "--no-merges", parent]):
2699 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2702 print "Found parent of %s in commit %s" % (branch, blob)
2709 def importChanges(self, changes):
2711 for change in changes:
2712 description = p4_describe(change)
2713 self.updateOptionDict(description)
2716 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2721 if self.detectBranches:
2722 branches = self.splitFilesIntoBranches(description)
2723 for branch in branches.keys():
2725 branchPrefix = self.depotPaths[0] + branch + "/"
2726 self.branchPrefixes = [ branchPrefix ]
2730 filesForCommit = branches[branch]
2733 print "branch is %s" % branch
2735 self.updatedBranches.add(branch)
2737 if branch not in self.createdBranches:
2738 self.createdBranches.add(branch)
2739 parent = self.knownBranches[branch]
2740 if parent == branch:
2743 fullBranch = self.projectName + branch
2744 if fullBranch not in self.p4BranchesInGit:
2746 print("\n Importing new branch %s" % fullBranch);
2747 if self.importNewBranch(branch, change - 1):
2749 self.p4BranchesInGit.append(fullBranch)
2751 print("\n Resuming with change %s" % change);
2754 print "parent determined through known branches: %s" % parent
2756 branch = self.gitRefForBranch(branch)
2757 parent = self.gitRefForBranch(parent)
2760 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2762 if len(parent) == 0 and branch in self.initialParents:
2763 parent = self.initialParents[branch]
2764 del self.initialParents[branch]
2768 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
2770 print "Creating temporary branch: " + tempBranch
2771 self.commit(description, filesForCommit, tempBranch)
2772 self.tempBranches.append(tempBranch)
2774 blob = self.searchParent(parent, branch, tempBranch)
2776 self.commit(description, filesForCommit, branch, blob)
2779 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
2780 self.commit(description, filesForCommit, branch, parent)
2782 files = self.extractFilesFromCommit(description)
2783 self.commit(description, files, self.branch,
2785 # only needed once, to connect to the previous commit
2786 self.initialParent = ""
2788 print self.gitError.read()
2791 def importHeadRevision(self, revision):
2792 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2795 details["user"] = "git perforce import user"
2796 details["desc"] = ("Initial import of %s from the state at revision %s\n"
2797 % (' '.join(self.depotPaths), revision))
2798 details["change"] = revision
2802 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2804 for info in p4CmdList(["files"] + fileArgs):
2806 if 'code' in info and info['code'] == 'error':
2807 sys.stderr.write("p4 returned an error: %s\n"
2809 if info['data'].find("must refer to client") >= 0:
2810 sys.stderr.write("This particular p4 error is misleading.\n")
2811 sys.stderr.write("Perhaps the depot path was misspelled.\n");
2812 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
2814 if 'p4ExitCode' in info:
2815 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
2819 change = int(info["change"])
2820 if change > newestRevision:
2821 newestRevision = change
2823 if info["action"] in self.delete_actions:
2824 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2825 #fileCnt = fileCnt + 1
2828 for prop in ["depotFile", "rev", "action", "type" ]:
2829 details["%s%s" % (prop, fileCnt)] = info[prop]
2831 fileCnt = fileCnt + 1
2833 details["change"] = newestRevision
2835 # Use time from top-most change so that all git p4 clones of
2836 # the same p4 repo have the same commit SHA1s.
2837 res = p4_describe(newestRevision)
2838 details["time"] = res["time"]
2840 self.updateOptionDict(details)
2842 self.commit(details, self.extractFilesFromCommit(details), self.branch)
2844 print "IO error with git fast-import. Is your git version recent enough?"
2845 print self.gitError.read()
2848 def run(self, args):
2849 self.depotPaths = []
2850 self.changeRange = ""
2851 self.previousDepotPaths = []
2852 self.hasOrigin = False
2854 # map from branch depot path to parent branch
2855 self.knownBranches = {}
2856 self.initialParents = {}
2858 if self.importIntoRemotes:
2859 self.refPrefix = "refs/remotes/p4/"
2861 self.refPrefix = "refs/heads/p4/"
2863 if self.syncWithOrigin:
2864 self.hasOrigin = originP4BranchesExist()
2867 print 'Syncing with origin first, using "git fetch origin"'
2868 system("git fetch origin")
2870 branch_arg_given = bool(self.branch)
2871 if len(self.branch) == 0:
2872 self.branch = self.refPrefix + "master"
2873 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
2874 system("git update-ref %s refs/heads/p4" % self.branch)
2875 system("git branch -D p4")
2877 # accept either the command-line option, or the configuration variable
2878 if self.useClientSpec:
2879 # will use this after clone to set the variable
2880 self.useClientSpec_from_options = True
2882 if gitConfigBool("git-p4.useclientspec"):
2883 self.useClientSpec = True
2884 if self.useClientSpec:
2885 self.clientSpecDirs = getClientSpec()
2887 # TODO: should always look at previous commits,
2888 # merge with previous imports, if possible.
2891 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
2893 # branches holds mapping from branch name to sha1
2894 branches = p4BranchesInGit(self.importIntoRemotes)
2896 # restrict to just this one, disabling detect-branches
2897 if branch_arg_given:
2898 short = self.branch.split("/")[-1]
2899 if short in branches:
2900 self.p4BranchesInGit = [ short ]
2902 self.p4BranchesInGit = branches.keys()
2904 if len(self.p4BranchesInGit) > 1:
2906 print "Importing from/into multiple branches"
2907 self.detectBranches = True
2908 for branch in branches.keys():
2909 self.initialParents[self.refPrefix + branch] = \
2913 print "branches: %s" % self.p4BranchesInGit
2916 for branch in self.p4BranchesInGit:
2917 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
2919 settings = extractSettingsGitLog(logMsg)
2921 self.readOptions(settings)
2922 if (settings.has_key('depot-paths')
2923 and settings.has_key ('change')):
2924 change = int(settings['change']) + 1
2925 p4Change = max(p4Change, change)
2927 depotPaths = sorted(settings['depot-paths'])
2928 if self.previousDepotPaths == []:
2929 self.previousDepotPaths = depotPaths
2932 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
2933 prev_list = prev.split("/")
2934 cur_list = cur.split("/")
2935 for i in range(0, min(len(cur_list), len(prev_list))):
2936 if cur_list[i] <> prev_list[i]:
2940 paths.append ("/".join(cur_list[:i + 1]))
2942 self.previousDepotPaths = paths
2945 self.depotPaths = sorted(self.previousDepotPaths)
2946 self.changeRange = "@%s,#head" % p4Change
2947 if not self.silent and not self.detectBranches:
2948 print "Performing incremental import into %s git branch" % self.branch
2950 # accept multiple ref name abbreviations:
2951 # refs/foo/bar/branch -> use it exactly
2952 # p4/branch -> prepend refs/remotes/ or refs/heads/
2953 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
2954 if not self.branch.startswith("refs/"):
2955 if self.importIntoRemotes:
2956 prepend = "refs/remotes/"
2958 prepend = "refs/heads/"
2959 if not self.branch.startswith("p4/"):
2961 self.branch = prepend + self.branch
2963 if len(args) == 0 and self.depotPaths:
2965 print "Depot paths: %s" % ' '.join(self.depotPaths)
2967 if self.depotPaths and self.depotPaths != args:
2968 print ("previous import used depot path %s and now %s was specified. "
2969 "This doesn't work!" % (' '.join (self.depotPaths),
2973 self.depotPaths = sorted(args)
2978 # Make sure no revision specifiers are used when --changesfile
2980 bad_changesfile = False
2981 if len(self.changesFile) > 0:
2982 for p in self.depotPaths:
2983 if p.find("@") >= 0 or p.find("#") >= 0:
2984 bad_changesfile = True
2987 die("Option --changesfile is incompatible with revision specifiers")
2990 for p in self.depotPaths:
2991 if p.find("@") != -1:
2992 atIdx = p.index("@")
2993 self.changeRange = p[atIdx:]
2994 if self.changeRange == "@all":
2995 self.changeRange = ""
2996 elif ',' not in self.changeRange:
2997 revision = self.changeRange
2998 self.changeRange = ""
3000 elif p.find("#") != -1:
3001 hashIdx = p.index("#")
3002 revision = p[hashIdx:]
3004 elif self.previousDepotPaths == []:
3005 # pay attention to changesfile, if given, else import
3006 # the entire p4 tree at the head revision
3007 if len(self.changesFile) == 0:
3010 p = re.sub ("\.\.\.$", "", p)
3011 if not p.endswith("/"):
3016 self.depotPaths = newPaths
3018 # --detect-branches may change this for each branch
3019 self.branchPrefixes = self.depotPaths
3021 self.loadUserMapFromCache()
3023 if self.detectLabels:
3026 if self.detectBranches:
3027 ## FIXME - what's a P4 projectName ?
3028 self.projectName = self.guessProjectName()
3031 self.getBranchMappingFromGitBranches()
3033 self.getBranchMapping()
3035 print "p4-git branches: %s" % self.p4BranchesInGit
3036 print "initial parents: %s" % self.initialParents
3037 for b in self.p4BranchesInGit:
3041 b = b[len(self.projectName):]
3042 self.createdBranches.add(b)
3044 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3046 self.importProcess = subprocess.Popen(["git", "fast-import"],
3047 stdin=subprocess.PIPE,
3048 stdout=subprocess.PIPE,
3049 stderr=subprocess.PIPE);
3050 self.gitOutput = self.importProcess.stdout
3051 self.gitStream = self.importProcess.stdin
3052 self.gitError = self.importProcess.stderr
3055 self.importHeadRevision(revision)
3059 if len(self.changesFile) > 0:
3060 output = open(self.changesFile).readlines()
3063 changeSet.add(int(line))
3065 for change in changeSet:
3066 changes.append(change)
3070 # catch "git p4 sync" with no new branches, in a repo that
3071 # does not have any existing p4 branches
3073 if not self.p4BranchesInGit:
3074 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3076 # The default branch is master, unless --branch is used to
3077 # specify something else. Make sure it exists, or complain
3078 # nicely about how to use --branch.
3079 if not self.detectBranches:
3080 if not branch_exists(self.branch):
3081 if branch_arg_given:
3082 die("Error: branch %s does not exist." % self.branch)
3084 die("Error: no branch %s; perhaps specify one with --branch." %
3088 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3090 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3092 if len(self.maxChanges) > 0:
3093 changes = changes[:min(int(self.maxChanges), len(changes))]
3095 if len(changes) == 0:
3097 print "No changes to import!"
3099 if not self.silent and not self.detectBranches:
3100 print "Import destination: %s" % self.branch
3102 self.updatedBranches = set()
3104 if not self.detectBranches:
3106 # start a new branch
3107 self.initialParent = ""
3109 # build on a previous revision
3110 self.initialParent = parseRevision(self.branch)
3112 self.importChanges(changes)
3116 if len(self.updatedBranches) > 0:
3117 sys.stdout.write("Updated branches: ")
3118 for b in self.updatedBranches:
3119 sys.stdout.write("%s " % b)
3120 sys.stdout.write("\n")
3122 if gitConfigBool("git-p4.importLabels"):
3123 self.importLabels = True
3125 if self.importLabels:
3126 p4Labels = getP4Labels(self.depotPaths)
3127 gitTags = getGitTags()
3129 missingP4Labels = p4Labels - gitTags
3130 self.importP4Labels(self.gitStream, missingP4Labels)
3132 self.gitStream.close()
3133 if self.importProcess.wait() != 0:
3134 die("fast-import failed: %s" % self.gitError.read())
3135 self.gitOutput.close()
3136 self.gitError.close()
3138 # Cleanup temporary branches created during import
3139 if self.tempBranches != []:
3140 for branch in self.tempBranches:
3141 read_pipe("git update-ref -d %s" % branch)
3142 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3144 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3145 # a convenient shortcut refname "p4".
3146 if self.importIntoRemotes:
3147 head_ref = self.refPrefix + "HEAD"
3148 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3149 system(["git", "symbolic-ref", head_ref, self.branch])
3153 class P4Rebase(Command):
3155 Command.__init__(self)
3157 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3159 self.importLabels = False
3160 self.description = ("Fetches the latest revision from perforce and "
3161 + "rebases the current work (branch) against it")
3163 def run(self, args):
3165 sync.importLabels = self.importLabels
3168 return self.rebase()
3171 if os.system("git update-index --refresh") != 0:
3172 die("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.");
3173 if len(read_pipe("git diff-index HEAD --")) > 0:
3174 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3176 [upstream, settings] = findUpstreamBranchPoint()
3177 if len(upstream) == 0:
3178 die("Cannot find upstream branchpoint for rebase")
3180 # the branchpoint may be p4/foo~3, so strip off the parent
3181 upstream = re.sub("~[0-9]+$", "", upstream)
3183 print "Rebasing the current branch onto %s" % upstream
3184 oldHead = read_pipe("git rev-parse HEAD").strip()
3185 system("git rebase %s" % upstream)
3186 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3189 class P4Clone(P4Sync):
3191 P4Sync.__init__(self)
3192 self.description = "Creates a new git repository and imports from Perforce into it"
3193 self.usage = "usage: %prog [options] //depot/path[@revRange]"
3195 optparse.make_option("--destination", dest="cloneDestination",
3196 action='store', default=None,
3197 help="where to leave result of the clone"),
3198 optparse.make_option("--bare", dest="cloneBare",
3199 action="store_true", default=False),
3201 self.cloneDestination = None
3202 self.needsGit = False
3203 self.cloneBare = False
3205 def defaultDestination(self, args):
3206 ## TODO: use common prefix of args?
3208 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3209 depotDir = re.sub("(#[^#]*)$", "", depotDir)
3210 depotDir = re.sub(r"\.\.\.$", "", depotDir)
3211 depotDir = re.sub(r"/$", "", depotDir)
3212 return os.path.split(depotDir)[1]
3214 def run(self, args):
3218 if self.keepRepoPath and not self.cloneDestination:
3219 sys.stderr.write("Must specify destination for --keep-path\n")
3224 if not self.cloneDestination and len(depotPaths) > 1:
3225 self.cloneDestination = depotPaths[-1]
3226 depotPaths = depotPaths[:-1]
3228 self.cloneExclude = ["/"+p for p in self.cloneExclude]
3229 for p in depotPaths:
3230 if not p.startswith("//"):
3231 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3234 if not self.cloneDestination:
3235 self.cloneDestination = self.defaultDestination(args)
3237 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3239 if not os.path.exists(self.cloneDestination):
3240 os.makedirs(self.cloneDestination)
3241 chdir(self.cloneDestination)
3243 init_cmd = [ "git", "init" ]
3245 init_cmd.append("--bare")
3246 retcode = subprocess.call(init_cmd)
3248 raise CalledProcessError(retcode, init_cmd)
3250 if not P4Sync.run(self, depotPaths):
3253 # create a master branch and check out a work tree
3254 if gitBranchExists(self.branch):
3255 system([ "git", "branch", "master", self.branch ])
3256 if not self.cloneBare:
3257 system([ "git", "checkout", "-f" ])
3259 print 'Not checking out any branch, use ' \
3260 '"git checkout -q -b master <branch>"'
3262 # auto-set this variable if invoked with --use-client-spec
3263 if self.useClientSpec_from_options:
3264 system("git config --bool git-p4.useclientspec true")
3268 class P4Branches(Command):
3270 Command.__init__(self)
3272 self.description = ("Shows the git branches that hold imports and their "
3273 + "corresponding perforce depot paths")
3274 self.verbose = False
3276 def run(self, args):
3277 if originP4BranchesExist():
3278 createOrUpdateBranchesFromOrigin()
3280 cmdline = "git rev-parse --symbolic "
3281 cmdline += " --remotes"
3283 for line in read_pipe_lines(cmdline):
3286 if not line.startswith('p4/') or line == "p4/HEAD":
3290 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3291 settings = extractSettingsGitLog(log)
3293 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3296 class HelpFormatter(optparse.IndentedHelpFormatter):
3298 optparse.IndentedHelpFormatter.__init__(self)
3300 def format_description(self, description):
3302 return description + "\n"
3306 def printUsage(commands):
3307 print "usage: %s <command> [options]" % sys.argv[0]
3309 print "valid commands: %s" % ", ".join(commands)
3311 print "Try %s <command> --help for command specific help." % sys.argv[0]
3316 "submit" : P4Submit,
3317 "commit" : P4Submit,
3319 "rebase" : P4Rebase,
3321 "rollback" : P4RollBack,
3322 "branches" : P4Branches
3327 if len(sys.argv[1:]) == 0:
3328 printUsage(commands.keys())
3331 cmdName = sys.argv[1]
3333 klass = commands[cmdName]
3336 print "unknown command %s" % cmdName
3338 printUsage(commands.keys())
3341 options = cmd.options
3342 cmd.gitdir = os.environ.get("GIT_DIR", None)
3346 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3348 options.append(optparse.make_option("--git-dir", dest="gitdir"))
3350 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3352 description = cmd.description,
3353 formatter = HelpFormatter())
3355 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3357 verbose = cmd.verbose
3359 if cmd.gitdir == None:
3360 cmd.gitdir = os.path.abspath(".git")
3361 if not isValidGitDir(cmd.gitdir):
3362 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3363 if os.path.exists(cmd.gitdir):
3364 cdup = read_pipe("git rev-parse --show-cdup").strip()
3368 if not isValidGitDir(cmd.gitdir):
3369 if isValidGitDir(cmd.gitdir + "/.git"):
3370 cmd.gitdir += "/.git"
3372 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3374 os.environ["GIT_DIR"] = cmd.gitdir
3376 if not cmd.run(args):
3381 if __name__ == '__main__':