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")
31 from subprocess import CalledProcessError
33 # from python2.7:subprocess.py
34 # Exception classes used by this module.
35 class CalledProcessError(Exception):
36 """This exception is raised when a process run by check_call() returns
37 a non-zero exit status. The exit status will be stored in the
38 returncode attribute."""
39 def __init__(self, returncode, cmd):
40 self.returncode = returncode
43 return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
47 # Only labels/tags matching this will be imported/exported
48 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
50 # Grab changes in blocks of this many revisions, unless otherwise requested
51 defaultBlockSize = 512
53 def p4_build_cmd(cmd):
54 """Build a suitable p4 command line.
56 This consolidates building and returning a p4 command line into one
57 location. It means that hooking into the environment, or other configuration
58 can be done more easily.
62 user = gitConfig("git-p4.user")
64 real_cmd += ["-u",user]
66 password = gitConfig("git-p4.password")
68 real_cmd += ["-P", password]
70 port = gitConfig("git-p4.port")
72 real_cmd += ["-p", port]
74 host = gitConfig("git-p4.host")
76 real_cmd += ["-H", host]
78 client = gitConfig("git-p4.client")
80 real_cmd += ["-c", client]
83 if isinstance(cmd,basestring):
84 real_cmd = ' '.join(real_cmd) + ' ' + cmd
89 def chdir(path, is_client_path=False):
90 """Do chdir to the given path, and set the PWD environment
91 variable for use by P4. It does not look at getcwd() output.
92 Since we're not using the shell, it is necessary to set the
93 PWD environment variable explicitly.
95 Normally, expand the path to force it to be absolute. This
96 addresses the use of relative path names inside P4 settings,
97 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
98 as given; it looks for .p4config using PWD.
100 If is_client_path, the path was handed to us directly by p4,
101 and may be a symbolic link. Do not call os.getcwd() in this
102 case, because it will cause p4 to think that PWD is not inside
107 if not is_client_path:
109 os.environ['PWD'] = path
112 """Return free space in bytes on the disk of the given dirname."""
113 if platform.system() == 'Windows':
114 free_bytes = ctypes.c_ulonglong(0)
115 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
116 return free_bytes.value
118 st = os.statvfs(os.getcwd())
119 return st.f_bavail * st.f_frsize
125 sys.stderr.write(msg + "\n")
128 def write_pipe(c, stdin):
130 sys.stderr.write('Writing pipe: %s\n' % str(c))
132 expand = isinstance(c,basestring)
133 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
135 val = pipe.write(stdin)
138 die('Command failed: %s' % str(c))
142 def p4_write_pipe(c, stdin):
143 real_cmd = p4_build_cmd(c)
144 return write_pipe(real_cmd, stdin)
146 def read_pipe(c, ignore_error=False):
148 sys.stderr.write('Reading pipe: %s\n' % str(c))
150 expand = isinstance(c,basestring)
151 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
152 (out, err) = p.communicate()
153 if p.returncode != 0 and not ignore_error:
154 die('Command failed: %s\nError: %s' % (str(c), err))
157 def p4_read_pipe(c, ignore_error=False):
158 real_cmd = p4_build_cmd(c)
159 return read_pipe(real_cmd, ignore_error)
161 def read_pipe_lines(c):
163 sys.stderr.write('Reading pipe: %s\n' % str(c))
165 expand = isinstance(c, basestring)
166 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
168 val = pipe.readlines()
169 if pipe.close() or p.wait():
170 die('Command failed: %s' % str(c))
174 def p4_read_pipe_lines(c):
175 """Specifically invoke p4 on the command supplied. """
176 real_cmd = p4_build_cmd(c)
177 return read_pipe_lines(real_cmd)
179 def p4_has_command(cmd):
180 """Ask p4 for help on this command. If it returns an error, the
181 command does not exist in this version of p4."""
182 real_cmd = p4_build_cmd(["help", cmd])
183 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
184 stderr=subprocess.PIPE)
186 return p.returncode == 0
188 def p4_has_move_command():
189 """See if the move command exists, that it supports -k, and that
190 it has not been administratively disabled. The arguments
191 must be correct, but the filenames do not have to exist. Use
192 ones with wildcards so even if they exist, it will fail."""
194 if not p4_has_command("move"):
196 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
197 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
198 (out, err) = p.communicate()
199 # return code will be 1 in either case
200 if err.find("Invalid option") >= 0:
202 if err.find("disabled") >= 0:
204 # assume it failed because @... was invalid changelist
207 def system(cmd, ignore_error=False):
208 expand = isinstance(cmd,basestring)
210 sys.stderr.write("executing %s\n" % str(cmd))
211 retcode = subprocess.call(cmd, shell=expand)
212 if retcode and not ignore_error:
213 raise CalledProcessError(retcode, cmd)
218 """Specifically invoke p4 as the system command. """
219 real_cmd = p4_build_cmd(cmd)
220 expand = isinstance(real_cmd, basestring)
221 retcode = subprocess.call(real_cmd, shell=expand)
223 raise CalledProcessError(retcode, real_cmd)
225 _p4_version_string = None
226 def p4_version_string():
227 """Read the version string, showing just the last line, which
228 hopefully is the interesting version bit.
231 Perforce - The Fast Software Configuration Management System.
232 Copyright 1995-2011 Perforce Software. All rights reserved.
233 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
235 global _p4_version_string
236 if not _p4_version_string:
237 a = p4_read_pipe_lines(["-V"])
238 _p4_version_string = a[-1].rstrip()
239 return _p4_version_string
241 def p4_integrate(src, dest):
242 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
244 def p4_sync(f, *options):
245 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
248 # forcibly add file names with wildcards
249 if wildcard_present(f):
250 p4_system(["add", "-f", f])
252 p4_system(["add", f])
255 p4_system(["delete", wildcard_encode(f)])
257 def p4_edit(f, *options):
258 p4_system(["edit"] + list(options) + [wildcard_encode(f)])
261 p4_system(["revert", wildcard_encode(f)])
263 def p4_reopen(type, f):
264 p4_system(["reopen", "-t", type, wildcard_encode(f)])
266 def p4_move(src, dest):
267 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
269 def p4_last_change():
270 results = p4CmdList(["changes", "-m", "1"])
271 return int(results[0]['change'])
273 def p4_describe(change):
274 """Make sure it returns a valid result by checking for
275 the presence of field "time". Return a dict of the
278 ds = p4CmdList(["describe", "-s", str(change)])
280 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
284 if "p4ExitCode" in d:
285 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
288 if d["code"] == "error":
289 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
292 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
297 # Canonicalize the p4 type and return a tuple of the
298 # base type, plus any modifiers. See "p4 help filetypes"
299 # for a list and explanation.
301 def split_p4_type(p4type):
303 p4_filetypes_historical = {
304 "ctempobj": "binary+Sw",
310 "tempobj": "binary+FSw",
311 "ubinary": "binary+F",
312 "uresource": "resource+F",
313 "uxbinary": "binary+Fx",
314 "xbinary": "binary+x",
316 "xtempobj": "binary+Swx",
318 "xunicode": "unicode+x",
321 if p4type in p4_filetypes_historical:
322 p4type = p4_filetypes_historical[p4type]
324 s = p4type.split("+")
332 # return the raw p4 type of a file (text, text+ko, etc)
335 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
336 return results[0]['headType']
339 # Given a type base and modifier, return a regexp matching
340 # the keywords that can be expanded in the file
342 def p4_keywords_regexp_for_type(base, type_mods):
343 if base in ("text", "unicode", "binary"):
345 if "ko" in type_mods:
347 elif "k" in type_mods:
348 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
352 \$ # Starts with a dollar, followed by...
353 (%s) # one of the keywords, followed by...
354 (:[^$\n]+)? # possibly an old expansion, followed by...
362 # Given a file, return a regexp matching the possible
363 # RCS keywords that will be expanded, or None for files
364 # with kw expansion turned off.
366 def p4_keywords_regexp_for_file(file):
367 if not os.path.exists(file):
370 (type_base, type_mods) = split_p4_type(p4_type(file))
371 return p4_keywords_regexp_for_type(type_base, type_mods)
373 def setP4ExecBit(file, mode):
374 # Reopens an already open file and changes the execute bit to match
375 # the execute bit setting in the passed in mode.
379 if not isModeExec(mode):
380 p4Type = getP4OpenedType(file)
381 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
382 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
383 if p4Type[-1] == "+":
384 p4Type = p4Type[0:-1]
386 p4_reopen(p4Type, file)
388 def getP4OpenedType(file):
389 # Returns the perforce file type for the given file.
391 result = p4_read_pipe(["opened", wildcard_encode(file)])
392 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
394 return match.group(1)
396 die("Could not determine file type for %s (result: '%s')" % (file, result))
398 # Return the set of all p4 labels
399 def getP4Labels(depotPaths):
401 if isinstance(depotPaths,basestring):
402 depotPaths = [depotPaths]
404 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
410 # Return the set of all git tags
413 for line in read_pipe_lines(["git", "tag"]):
418 def diffTreePattern():
419 # This is a simple generator for the diff tree regex pattern. This could be
420 # a class variable if this and parseDiffTreeEntry were a part of a class.
421 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
425 def parseDiffTreeEntry(entry):
426 """Parses a single diff tree entry into its component elements.
428 See git-diff-tree(1) manpage for details about the format of the diff
429 output. This method returns a dictionary with the following elements:
431 src_mode - The mode of the source file
432 dst_mode - The mode of the destination file
433 src_sha1 - The sha1 for the source file
434 dst_sha1 - The sha1 fr the destination file
435 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
436 status_score - The score for the status (applicable for 'C' and 'R'
437 statuses). This is None if there is no score.
438 src - The path for the source file.
439 dst - The path for the destination file. This is only present for
440 copy or renames. If it is not present, this is None.
442 If the pattern is not matched, None is returned."""
444 match = diffTreePattern().next().match(entry)
447 'src_mode': match.group(1),
448 'dst_mode': match.group(2),
449 'src_sha1': match.group(3),
450 'dst_sha1': match.group(4),
451 'status': match.group(5),
452 'status_score': match.group(6),
453 'src': match.group(7),
454 'dst': match.group(10)
458 def isModeExec(mode):
459 # Returns True if the given git mode represents an executable file,
461 return mode[-3:] == "755"
463 def isModeExecChanged(src_mode, dst_mode):
464 return isModeExec(src_mode) != isModeExec(dst_mode)
466 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
468 if isinstance(cmd,basestring):
475 cmd = p4_build_cmd(cmd)
477 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
479 # Use a temporary file to avoid deadlocks without
480 # subprocess.communicate(), which would put another copy
481 # of stdout into memory.
483 if stdin is not None:
484 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
485 if isinstance(stdin,basestring):
486 stdin_file.write(stdin)
489 stdin_file.write(i + '\n')
493 p4 = subprocess.Popen(cmd,
496 stdout=subprocess.PIPE)
501 entry = marshal.load(p4.stdout)
511 entry["p4ExitCode"] = exitCode
517 list = p4CmdList(cmd)
523 def p4Where(depotPath):
524 if not depotPath.endswith("/"):
526 depotPathLong = depotPath + "..."
527 outputList = p4CmdList(["where", depotPathLong])
529 for entry in outputList:
530 if "depotFile" in entry:
531 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
532 # The base path always ends with "/...".
533 if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
536 elif "data" in entry:
537 data = entry.get("data")
538 space = data.find(" ")
539 if data[:space] == depotPath:
544 if output["code"] == "error":
548 clientPath = output.get("path")
549 elif "data" in output:
550 data = output.get("data")
551 lastSpace = data.rfind(" ")
552 clientPath = data[lastSpace + 1:]
554 if clientPath.endswith("..."):
555 clientPath = clientPath[:-3]
558 def currentGitBranch():
559 retcode = system(["git", "symbolic-ref", "-q", "HEAD"], ignore_error=True)
564 return read_pipe(["git", "name-rev", "HEAD"]).split(" ")[1].strip()
566 def isValidGitDir(path):
567 if (os.path.exists(path + "/HEAD")
568 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
572 def parseRevision(ref):
573 return read_pipe("git rev-parse %s" % ref).strip()
575 def branchExists(ref):
576 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
580 def extractLogMessageFromGitCommit(commit):
583 ## fixme: title is first line of commit, not 1st paragraph.
585 for log in read_pipe_lines("git cat-file commit %s" % commit):
594 def extractSettingsGitLog(log):
596 for line in log.split("\n"):
598 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
602 assignments = m.group(1).split (':')
603 for a in assignments:
605 key = vals[0].strip()
606 val = ('='.join (vals[1:])).strip()
607 if val.endswith ('\"') and val.startswith('"'):
612 paths = values.get("depot-paths")
614 paths = values.get("depot-path")
616 values['depot-paths'] = paths.split(',')
619 def gitBranchExists(branch):
620 proc = subprocess.Popen(["git", "rev-parse", branch],
621 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
622 return proc.wait() == 0;
626 def gitConfig(key, typeSpecifier=None):
627 if not _gitConfig.has_key(key):
628 cmd = [ "git", "config" ]
630 cmd += [ typeSpecifier ]
632 s = read_pipe(cmd, ignore_error=True)
633 _gitConfig[key] = s.strip()
634 return _gitConfig[key]
636 def gitConfigBool(key):
637 """Return a bool, using git config --bool. It is True only if the
638 variable is set to true, and False if set to false or not present
641 if not _gitConfig.has_key(key):
642 _gitConfig[key] = gitConfig(key, '--bool') == "true"
643 return _gitConfig[key]
645 def gitConfigInt(key):
646 if not _gitConfig.has_key(key):
647 cmd = [ "git", "config", "--int", key ]
648 s = read_pipe(cmd, ignore_error=True)
651 _gitConfig[key] = int(gitConfig(key, '--int'))
653 _gitConfig[key] = None
654 return _gitConfig[key]
656 def gitConfigList(key):
657 if not _gitConfig.has_key(key):
658 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
659 _gitConfig[key] = s.strip().split(os.linesep)
660 if _gitConfig[key] == ['']:
662 return _gitConfig[key]
664 def p4BranchesInGit(branchesAreInRemotes=True):
665 """Find all the branches whose names start with "p4/", looking
666 in remotes or heads as specified by the argument. Return
667 a dictionary of { branch: revision } for each one found.
668 The branch names are the short names, without any
673 cmdline = "git rev-parse --symbolic "
674 if branchesAreInRemotes:
675 cmdline += "--remotes"
677 cmdline += "--branches"
679 for line in read_pipe_lines(cmdline):
683 if not line.startswith('p4/'):
685 # special symbolic ref to p4/master
686 if line == "p4/HEAD":
689 # strip off p4/ prefix
690 branch = line[len("p4/"):]
692 branches[branch] = parseRevision(line)
696 def branch_exists(branch):
697 """Make sure that the given ref name really exists."""
699 cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
700 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
701 out, _ = p.communicate()
704 # expect exactly one line of output: the branch name
705 return out.rstrip() == branch
707 def findUpstreamBranchPoint(head = "HEAD"):
708 branches = p4BranchesInGit()
709 # map from depot-path to branch name
710 branchByDepotPath = {}
711 for branch in branches.keys():
712 tip = branches[branch]
713 log = extractLogMessageFromGitCommit(tip)
714 settings = extractSettingsGitLog(log)
715 if settings.has_key("depot-paths"):
716 paths = ",".join(settings["depot-paths"])
717 branchByDepotPath[paths] = "remotes/p4/" + branch
721 while parent < 65535:
722 commit = head + "~%s" % parent
723 log = extractLogMessageFromGitCommit(commit)
724 settings = extractSettingsGitLog(log)
725 if settings.has_key("depot-paths"):
726 paths = ",".join(settings["depot-paths"])
727 if branchByDepotPath.has_key(paths):
728 return [branchByDepotPath[paths], settings]
732 return ["", settings]
734 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
736 print ("Creating/updating branch(es) in %s based on origin branch(es)"
739 originPrefix = "origin/p4/"
741 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
743 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
746 headName = line[len(originPrefix):]
747 remoteHead = localRefPrefix + headName
750 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
751 if (not original.has_key('depot-paths')
752 or not original.has_key('change')):
756 if not gitBranchExists(remoteHead):
758 print "creating %s" % remoteHead
761 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
762 if settings.has_key('change') > 0:
763 if settings['depot-paths'] == original['depot-paths']:
764 originP4Change = int(original['change'])
765 p4Change = int(settings['change'])
766 if originP4Change > p4Change:
767 print ("%s (%s) is newer than %s (%s). "
768 "Updating p4 branch from origin."
769 % (originHead, originP4Change,
770 remoteHead, p4Change))
773 print ("Ignoring: %s was imported from %s while "
774 "%s was imported from %s"
775 % (originHead, ','.join(original['depot-paths']),
776 remoteHead, ','.join(settings['depot-paths'])))
779 system("git update-ref %s %s" % (remoteHead, originHead))
781 def originP4BranchesExist():
782 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
785 def p4ParseNumericChangeRange(parts):
786 changeStart = int(parts[0][1:])
787 if parts[1] == '#head':
788 changeEnd = p4_last_change()
790 changeEnd = int(parts[1])
792 return (changeStart, changeEnd)
794 def chooseBlockSize(blockSize):
798 return defaultBlockSize
800 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
803 # Parse the change range into start and end. Try to find integer
804 # revision ranges as these can be broken up into blocks to avoid
805 # hitting server-side limits (maxrows, maxscanresults). But if
806 # that doesn't work, fall back to using the raw revision specifier
807 # strings, without using block mode.
809 if changeRange is None or changeRange == '':
811 changeEnd = p4_last_change()
812 block_size = chooseBlockSize(requestedBlockSize)
814 parts = changeRange.split(',')
815 assert len(parts) == 2
817 (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
818 block_size = chooseBlockSize(requestedBlockSize)
820 changeStart = parts[0][1:]
822 if requestedBlockSize:
823 die("cannot use --changes-block-size with non-numeric revisions")
828 # Retrieve changes a block at a time, to prevent running
829 # into a MaxResults/MaxScanRows error from the server.
835 end = min(changeEnd, changeStart + block_size)
836 revisionRange = "%d,%d" % (changeStart, end)
838 revisionRange = "%s,%s" % (changeStart, changeEnd)
841 cmd += ["%s...@%s" % (p, revisionRange)]
843 # Insert changes in chronological order
844 for line in reversed(p4_read_pipe_lines(cmd)):
845 changes.append(int(line.split(" ")[1]))
853 changeStart = end + 1
855 changes = sorted(changes)
858 def p4PathStartsWith(path, prefix):
859 # This method tries to remedy a potential mixed-case issue:
861 # If UserA adds //depot/DirA/file1
862 # and UserB adds //depot/dira/file2
864 # we may or may not have a problem. If you have core.ignorecase=true,
865 # we treat DirA and dira as the same directory
866 if gitConfigBool("core.ignorecase"):
867 return path.lower().startswith(prefix.lower())
868 return path.startswith(prefix)
871 """Look at the p4 client spec, create a View() object that contains
872 all the mappings, and return it."""
874 specList = p4CmdList("client -o")
875 if len(specList) != 1:
876 die('Output from "client -o" is %d lines, expecting 1' %
879 # dictionary of all client parameters
883 client_name = entry["Client"]
885 # just the keys that start with "View"
886 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
889 view = View(client_name)
891 # append the lines, in order, to the view
892 for view_num in range(len(view_keys)):
893 k = "View%d" % view_num
894 if k not in view_keys:
895 die("Expected view key %s missing" % k)
896 view.append(entry[k])
901 """Grab the client directory."""
903 output = p4CmdList("client -o")
905 die('Output from "client -o" is %d lines, expecting 1' % len(output))
908 if "Root" not in entry:
909 die('Client has no "Root"')
914 # P4 wildcards are not allowed in filenames. P4 complains
915 # if you simply add them, but you can force it with "-f", in
916 # which case it translates them into %xx encoding internally.
918 def wildcard_decode(path):
919 # Search for and fix just these four characters. Do % last so
920 # that fixing it does not inadvertently create new %-escapes.
921 # Cannot have * in a filename in windows; untested as to
922 # what p4 would do in such a case.
923 if not platform.system() == "Windows":
924 path = path.replace("%2A", "*")
925 path = path.replace("%23", "#") \
926 .replace("%40", "@") \
930 def wildcard_encode(path):
931 # do % first to avoid double-encoding the %s introduced here
932 path = path.replace("%", "%25") \
933 .replace("*", "%2A") \
934 .replace("#", "%23") \
938 def wildcard_present(path):
939 m = re.search("[*#@%]", path)
942 class LargeFileSystem(object):
943 """Base class for large file system support."""
945 def __init__(self, writeToGitStream):
946 self.largeFiles = set()
947 self.writeToGitStream = writeToGitStream
949 def generatePointer(self, cloneDestination, contentFile):
950 """Return the content of a pointer file that is stored in Git instead of
951 the actual content."""
952 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
954 def pushFile(self, localLargeFile):
955 """Push the actual content which is not stored in the Git repository to
957 assert False, "Method 'pushFile' required in " + self.__class__.__name__
959 def hasLargeFileExtension(self, relPath):
962 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
966 def generateTempFile(self, contents):
967 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
971 return contentFile.name
973 def exceedsLargeFileThreshold(self, relPath, contents):
974 if gitConfigInt('git-p4.largeFileThreshold'):
975 contentsSize = sum(len(d) for d in contents)
976 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
978 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
979 contentsSize = sum(len(d) for d in contents)
980 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
982 contentTempFile = self.generateTempFile(contents)
983 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
984 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')
985 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
987 compressedContentsSize = zf.infolist()[0].compress_size
988 os.remove(contentTempFile)
989 os.remove(compressedContentFile.name)
990 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
994 def addLargeFile(self, relPath):
995 self.largeFiles.add(relPath)
997 def removeLargeFile(self, relPath):
998 self.largeFiles.remove(relPath)
1000 def isLargeFile(self, relPath):
1001 return relPath in self.largeFiles
1003 def processContent(self, git_mode, relPath, contents):
1004 """Processes the content of git fast import. This method decides if a
1005 file is stored in the large file system and handles all necessary
1007 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1008 contentTempFile = self.generateTempFile(contents)
1009 (git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
1011 # Move temp file to final location in large file system
1012 largeFileDir = os.path.dirname(localLargeFile)
1013 if not os.path.isdir(largeFileDir):
1014 os.makedirs(largeFileDir)
1015 shutil.move(contentTempFile, localLargeFile)
1016 self.addLargeFile(relPath)
1017 if gitConfigBool('git-p4.largeFilePush'):
1018 self.pushFile(localLargeFile)
1020 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1021 return (git_mode, contents)
1023 class MockLFS(LargeFileSystem):
1024 """Mock large file system for testing."""
1026 def generatePointer(self, contentFile):
1027 """The pointer content is the original content prefixed with "pointer-".
1028 The local filename of the large file storage is derived from the file content.
1030 with open(contentFile, 'r') as f:
1033 pointerContents = 'pointer-' + content
1034 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1035 return (gitMode, pointerContents, localLargeFile)
1037 def pushFile(self, localLargeFile):
1038 """The remote filename of the large file storage is the same as the local
1039 one but in a different directory.
1041 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1042 if not os.path.exists(remotePath):
1043 os.makedirs(remotePath)
1044 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1046 class GitLFS(LargeFileSystem):
1047 """Git LFS as backend for the git-p4 large file system.
1048 See https://git-lfs.github.com/ for details."""
1050 def __init__(self, *args):
1051 LargeFileSystem.__init__(self, *args)
1052 self.baseGitAttributes = []
1054 def generatePointer(self, contentFile):
1055 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1056 mode and content which is stored in the Git repository instead of
1057 the actual content. Return also the new location of the actual
1060 pointerProcess = subprocess.Popen(
1061 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1062 stdout=subprocess.PIPE
1064 pointerFile = pointerProcess.stdout.read()
1065 if pointerProcess.wait():
1066 os.remove(contentFile)
1067 die('git-lfs pointer command failed. Did you install the extension?')
1069 # Git LFS removed the preamble in the output of the 'pointer' command
1070 # starting from version 1.2.0. Check for the preamble here to support
1072 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1073 if pointerFile.startswith('Git LFS pointer for'):
1074 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1076 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
1077 localLargeFile = os.path.join(
1079 '.git', 'lfs', 'objects', oid[:2], oid[2:4],
1082 # LFS Spec states that pointer files should not have the executable bit set.
1084 return (gitMode, pointerFile, localLargeFile)
1086 def pushFile(self, localLargeFile):
1087 uploadProcess = subprocess.Popen(
1088 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1090 if uploadProcess.wait():
1091 die('git-lfs push command failed. Did you define a remote?')
1093 def generateGitAttributes(self):
1095 self.baseGitAttributes +
1099 '# Git LFS (see https://git-lfs.github.com/)\n',
1102 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
1103 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1105 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
1106 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1110 def addLargeFile(self, relPath):
1111 LargeFileSystem.addLargeFile(self, relPath)
1112 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1114 def removeLargeFile(self, relPath):
1115 LargeFileSystem.removeLargeFile(self, relPath)
1116 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1118 def processContent(self, git_mode, relPath, contents):
1119 if relPath == '.gitattributes':
1120 self.baseGitAttributes = contents
1121 return (git_mode, self.generateGitAttributes())
1123 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1127 self.usage = "usage: %prog [options]"
1128 self.needsGit = True
1129 self.verbose = False
1133 self.userMapFromPerforceServer = False
1134 self.myP4UserId = None
1138 return self.myP4UserId
1140 results = p4CmdList("user -o")
1142 if r.has_key('User'):
1143 self.myP4UserId = r['User']
1145 die("Could not find your p4 user id")
1147 def p4UserIsMe(self, p4User):
1148 # return True if the given p4 user is actually me
1149 me = self.p4UserId()
1150 if not p4User or p4User != me:
1155 def getUserCacheFilename(self):
1156 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1157 return home + "/.gitp4-usercache.txt"
1159 def getUserMapFromPerforceServer(self):
1160 if self.userMapFromPerforceServer:
1165 for output in p4CmdList("users"):
1166 if not output.has_key("User"):
1168 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1169 self.emails[output["Email"]] = output["User"]
1171 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1172 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1173 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1174 if mapUser and len(mapUser[0]) == 3:
1175 user = mapUser[0][0]
1176 fullname = mapUser[0][1]
1177 email = mapUser[0][2]
1178 self.users[user] = fullname + " <" + email + ">"
1179 self.emails[email] = user
1182 for (key, val) in self.users.items():
1183 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1185 open(self.getUserCacheFilename(), "wb").write(s)
1186 self.userMapFromPerforceServer = True
1188 def loadUserMapFromCache(self):
1190 self.userMapFromPerforceServer = False
1192 cache = open(self.getUserCacheFilename(), "rb")
1193 lines = cache.readlines()
1196 entry = line.strip().split("\t")
1197 self.users[entry[0]] = entry[1]
1199 self.getUserMapFromPerforceServer()
1201 class P4Debug(Command):
1203 Command.__init__(self)
1205 self.description = "A tool to debug the output of p4 -G."
1206 self.needsGit = False
1208 def run(self, args):
1210 for output in p4CmdList(args):
1211 print 'Element: %d' % j
1216 class P4RollBack(Command):
1218 Command.__init__(self)
1220 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1222 self.description = "A tool to debug the multi-branch import. Don't use :)"
1223 self.rollbackLocalBranches = False
1225 def run(self, args):
1228 maxChange = int(args[0])
1230 if "p4ExitCode" in p4Cmd("changes -m 1"):
1231 die("Problems executing p4");
1233 if self.rollbackLocalBranches:
1234 refPrefix = "refs/heads/"
1235 lines = read_pipe_lines("git rev-parse --symbolic --branches")
1237 refPrefix = "refs/remotes/"
1238 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1241 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1243 ref = refPrefix + line
1244 log = extractLogMessageFromGitCommit(ref)
1245 settings = extractSettingsGitLog(log)
1247 depotPaths = settings['depot-paths']
1248 change = settings['change']
1252 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
1253 for p in depotPaths]))) == 0:
1254 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1255 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1258 while change and int(change) > maxChange:
1261 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
1262 system("git update-ref %s \"%s^\"" % (ref, ref))
1263 log = extractLogMessageFromGitCommit(ref)
1264 settings = extractSettingsGitLog(log)
1267 depotPaths = settings['depot-paths']
1268 change = settings['change']
1271 print "%s rewound to %s" % (ref, change)
1275 class P4Submit(Command, P4UserMap):
1277 conflict_behavior_choices = ("ask", "skip", "quit")
1280 Command.__init__(self)
1281 P4UserMap.__init__(self)
1283 optparse.make_option("--origin", dest="origin"),
1284 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1285 # preserve the user, requires relevant p4 permissions
1286 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1287 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1288 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1289 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1290 optparse.make_option("--conflict", dest="conflict_behavior",
1291 choices=self.conflict_behavior_choices),
1292 optparse.make_option("--branch", dest="branch"),
1294 self.description = "Submit changes from git to the perforce depot."
1295 self.usage += " [name of git branch to submit into perforce depot]"
1297 self.detectRenames = False
1298 self.preserveUser = gitConfigBool("git-p4.preserveUser")
1299 self.dry_run = False
1300 self.prepare_p4_only = False
1301 self.conflict_behavior = None
1302 self.isWindows = (platform.system() == "Windows")
1303 self.exportLabels = False
1304 self.p4HasMoveCommand = p4_has_move_command()
1307 if gitConfig('git-p4.largeFileSystem'):
1308 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1311 if len(p4CmdList("opened ...")) > 0:
1312 die("You have files opened with perforce! Close them before starting the sync.")
1314 def separate_jobs_from_description(self, message):
1315 """Extract and return a possible Jobs field in the commit
1316 message. It goes into a separate section in the p4 change
1319 A jobs line starts with "Jobs:" and looks like a new field
1320 in a form. Values are white-space separated on the same
1321 line or on following lines that start with a tab.
1323 This does not parse and extract the full git commit message
1324 like a p4 form. It just sees the Jobs: line as a marker
1325 to pass everything from then on directly into the p4 form,
1326 but outside the description section.
1328 Return a tuple (stripped log message, jobs string)."""
1330 m = re.search(r'^Jobs:', message, re.MULTILINE)
1332 return (message, None)
1334 jobtext = message[m.start():]
1335 stripped_message = message[:m.start()].rstrip()
1336 return (stripped_message, jobtext)
1338 def prepareLogMessage(self, template, message, jobs):
1339 """Edits the template returned from "p4 change -o" to insert
1340 the message in the Description field, and the jobs text in
1344 inDescriptionSection = False
1346 for line in template.split("\n"):
1347 if line.startswith("#"):
1348 result += line + "\n"
1351 if inDescriptionSection:
1352 if line.startswith("Files:") or line.startswith("Jobs:"):
1353 inDescriptionSection = False
1354 # insert Jobs section
1356 result += jobs + "\n"
1360 if line.startswith("Description:"):
1361 inDescriptionSection = True
1363 for messageLine in message.split("\n"):
1364 line += "\t" + messageLine + "\n"
1366 result += line + "\n"
1370 def patchRCSKeywords(self, file, pattern):
1371 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1372 (handle, outFileName) = tempfile.mkstemp(dir='.')
1374 outFile = os.fdopen(handle, "w+")
1375 inFile = open(file, "r")
1376 regexp = re.compile(pattern, re.VERBOSE)
1377 for line in inFile.readlines():
1378 line = regexp.sub(r'$\1$', line)
1382 # Forcibly overwrite the original file
1384 shutil.move(outFileName, file)
1386 # cleanup our temporary file
1387 os.unlink(outFileName)
1388 print "Failed to strip RCS keywords in %s" % file
1391 print "Patched up RCS keywords in %s" % file
1393 def p4UserForCommit(self,id):
1394 # Return the tuple (perforce user,git email) for a given git commit id
1395 self.getUserMapFromPerforceServer()
1396 gitEmail = read_pipe(["git", "log", "--max-count=1",
1397 "--format=%ae", id])
1398 gitEmail = gitEmail.strip()
1399 if not self.emails.has_key(gitEmail):
1400 return (None,gitEmail)
1402 return (self.emails[gitEmail],gitEmail)
1404 def checkValidP4Users(self,commits):
1405 # check if any git authors cannot be mapped to p4 users
1407 (user,email) = self.p4UserForCommit(id)
1409 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1410 if gitConfigBool("git-p4.allowMissingP4Users"):
1413 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1415 def lastP4Changelist(self):
1416 # Get back the last changelist number submitted in this client spec. This
1417 # then gets used to patch up the username in the change. If the same
1418 # client spec is being used by multiple processes then this might go
1420 results = p4CmdList("client -o") # find the current client
1423 if r.has_key('Client'):
1424 client = r['Client']
1427 die("could not get client spec")
1428 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1430 if r.has_key('change'):
1432 die("Could not get changelist number for last submit - cannot patch up user details")
1434 def modifyChangelistUser(self, changelist, newUser):
1435 # fixup the user field of a changelist after it has been submitted.
1436 changes = p4CmdList("change -o %s" % changelist)
1437 if len(changes) != 1:
1438 die("Bad output from p4 change modifying %s to user %s" %
1439 (changelist, newUser))
1442 if c['User'] == newUser: return # nothing to do
1444 input = marshal.dumps(c)
1446 result = p4CmdList("change -f -i", stdin=input)
1448 if r.has_key('code'):
1449 if r['code'] == 'error':
1450 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1451 if r.has_key('data'):
1452 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1454 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1456 def canChangeChangelists(self):
1457 # check to see if we have p4 admin or super-user permissions, either of
1458 # which are required to modify changelists.
1459 results = p4CmdList(["protects", self.depotPath])
1461 if r.has_key('perm'):
1462 if r['perm'] == 'admin':
1464 if r['perm'] == 'super':
1468 def prepareSubmitTemplate(self):
1469 """Run "p4 change -o" to grab a change specification template.
1470 This does not use "p4 -G", as it is nice to keep the submission
1471 template in original order, since a human might edit it.
1473 Remove lines in the Files section that show changes to files
1474 outside the depot path we're committing into."""
1476 [upstream, settings] = findUpstreamBranchPoint()
1479 inFilesSection = False
1480 for line in p4_read_pipe_lines(['change', '-o']):
1481 if line.endswith("\r\n"):
1482 line = line[:-2] + "\n"
1484 if line.startswith("\t"):
1485 # path starts and ends with a tab
1487 lastTab = path.rfind("\t")
1489 path = path[:lastTab]
1490 if settings.has_key('depot-paths'):
1491 if not [p for p in settings['depot-paths']
1492 if p4PathStartsWith(path, p)]:
1495 if not p4PathStartsWith(path, self.depotPath):
1498 inFilesSection = False
1500 if line.startswith("Files:"):
1501 inFilesSection = True
1507 def edit_template(self, template_file):
1508 """Invoke the editor to let the user change the submission
1509 message. Return true if okay to continue with the submit."""
1511 # if configured to skip the editing part, just submit
1512 if gitConfigBool("git-p4.skipSubmitEdit"):
1515 # look at the modification time, to check later if the user saved
1517 mtime = os.stat(template_file).st_mtime
1520 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1521 editor = os.environ.get("P4EDITOR")
1523 editor = read_pipe("git var GIT_EDITOR").strip()
1524 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1526 # If the file was not saved, prompt to see if this patch should
1527 # be skipped. But skip this verification step if configured so.
1528 if gitConfigBool("git-p4.skipSubmitEditCheck"):
1531 # modification time updated means user saved the file
1532 if os.stat(template_file).st_mtime > mtime:
1536 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1542 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
1544 if os.environ.has_key("P4DIFF"):
1545 del(os.environ["P4DIFF"])
1547 for editedFile in editedFiles:
1548 diff += p4_read_pipe(['diff', '-du',
1549 wildcard_encode(editedFile)])
1553 for newFile in filesToAdd:
1554 newdiff += "==== new file ====\n"
1555 newdiff += "--- /dev/null\n"
1556 newdiff += "+++ %s\n" % newFile
1558 is_link = os.path.islink(newFile)
1559 expect_link = newFile in symlinks
1561 if is_link and expect_link:
1562 newdiff += "+%s\n" % os.readlink(newFile)
1564 f = open(newFile, "r")
1565 for line in f.readlines():
1566 newdiff += "+" + line
1569 return (diff + newdiff).replace('\r\n', '\n')
1571 def applyCommit(self, id):
1572 """Apply one commit, return True if it succeeded."""
1574 print "Applying", read_pipe(["git", "show", "-s",
1575 "--format=format:%h %s", id])
1577 (p4User, gitEmail) = self.p4UserForCommit(id)
1579 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1581 filesToChangeType = set()
1582 filesToDelete = set()
1584 pureRenameCopy = set()
1586 filesToChangeExecBit = {}
1589 diff = parseDiffTreeEntry(line)
1590 modifier = diff['status']
1594 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1595 filesToChangeExecBit[path] = diff['dst_mode']
1596 editedFiles.add(path)
1597 elif modifier == "A":
1598 filesToAdd.add(path)
1599 filesToChangeExecBit[path] = diff['dst_mode']
1600 if path in filesToDelete:
1601 filesToDelete.remove(path)
1603 dst_mode = int(diff['dst_mode'], 8)
1604 if dst_mode == 0120000:
1607 elif modifier == "D":
1608 filesToDelete.add(path)
1609 if path in filesToAdd:
1610 filesToAdd.remove(path)
1611 elif modifier == "C":
1612 src, dest = diff['src'], diff['dst']
1613 p4_integrate(src, dest)
1614 pureRenameCopy.add(dest)
1615 if diff['src_sha1'] != diff['dst_sha1']:
1617 pureRenameCopy.discard(dest)
1618 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1620 pureRenameCopy.discard(dest)
1621 filesToChangeExecBit[dest] = diff['dst_mode']
1623 # turn off read-only attribute
1624 os.chmod(dest, stat.S_IWRITE)
1626 editedFiles.add(dest)
1627 elif modifier == "R":
1628 src, dest = diff['src'], diff['dst']
1629 if self.p4HasMoveCommand:
1630 p4_edit(src) # src must be open before move
1631 p4_move(src, dest) # opens for (move/delete, move/add)
1633 p4_integrate(src, dest)
1634 if diff['src_sha1'] != diff['dst_sha1']:
1637 pureRenameCopy.add(dest)
1638 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1639 if not self.p4HasMoveCommand:
1640 p4_edit(dest) # with move: already open, writable
1641 filesToChangeExecBit[dest] = diff['dst_mode']
1642 if not self.p4HasMoveCommand:
1644 os.chmod(dest, stat.S_IWRITE)
1646 filesToDelete.add(src)
1647 editedFiles.add(dest)
1648 elif modifier == "T":
1649 filesToChangeType.add(path)
1651 die("unknown modifier %s for %s" % (modifier, path))
1653 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1654 patchcmd = diffcmd + " | git apply "
1655 tryPatchCmd = patchcmd + "--check -"
1656 applyPatchCmd = patchcmd + "--check --apply -"
1657 patch_succeeded = True
1659 if os.system(tryPatchCmd) != 0:
1660 fixed_rcs_keywords = False
1661 patch_succeeded = False
1662 print "Unfortunately applying the change failed!"
1664 # Patch failed, maybe it's just RCS keyword woes. Look through
1665 # the patch to see if that's possible.
1666 if gitConfigBool("git-p4.attemptRCSCleanup"):
1670 for file in editedFiles | filesToDelete:
1671 # did this file's delta contain RCS keywords?
1672 pattern = p4_keywords_regexp_for_file(file)
1675 # this file is a possibility...look for RCS keywords.
1676 regexp = re.compile(pattern, re.VERBOSE)
1677 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1678 if regexp.search(line):
1680 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1681 kwfiles[file] = pattern
1684 for file in kwfiles:
1686 print "zapping %s with %s" % (line,pattern)
1687 # File is being deleted, so not open in p4. Must
1688 # disable the read-only bit on windows.
1689 if self.isWindows and file not in editedFiles:
1690 os.chmod(file, stat.S_IWRITE)
1691 self.patchRCSKeywords(file, kwfiles[file])
1692 fixed_rcs_keywords = True
1694 if fixed_rcs_keywords:
1695 print "Retrying the patch with RCS keywords cleaned up"
1696 if os.system(tryPatchCmd) == 0:
1697 patch_succeeded = True
1699 if not patch_succeeded:
1700 for f in editedFiles:
1705 # Apply the patch for real, and do add/delete/+x handling.
1707 system(applyPatchCmd)
1709 for f in filesToChangeType:
1710 p4_edit(f, "-t", "auto")
1711 for f in filesToAdd:
1713 for f in filesToDelete:
1717 # Set/clear executable bits
1718 for f in filesToChangeExecBit.keys():
1719 mode = filesToChangeExecBit[f]
1720 setP4ExecBit(f, mode)
1723 # Build p4 change description, starting with the contents
1724 # of the git commit message.
1726 logMessage = extractLogMessageFromGitCommit(id)
1727 logMessage = logMessage.strip()
1728 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1730 template = self.prepareSubmitTemplate()
1731 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1733 if self.preserveUser:
1734 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1736 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1737 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1738 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1739 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1741 separatorLine = "######## everything below this line is just the diff #######\n"
1742 if not self.prepare_p4_only:
1743 submitTemplate += separatorLine
1744 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
1746 (handle, fileName) = tempfile.mkstemp()
1747 tmpFile = os.fdopen(handle, "w+b")
1749 submitTemplate = submitTemplate.replace("\n", "\r\n")
1750 tmpFile.write(submitTemplate)
1753 if self.prepare_p4_only:
1755 # Leave the p4 tree prepared, and the submit template around
1756 # and let the user decide what to do next
1759 print "P4 workspace prepared for submission."
1760 print "To submit or revert, go to client workspace"
1761 print " " + self.clientPath
1763 print "To submit, use \"p4 submit\" to write a new description,"
1764 print "or \"p4 submit -i <%s\" to use the one prepared by" \
1765 " \"git p4\"." % fileName
1766 print "You can delete the file \"%s\" when finished." % fileName
1768 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1769 print "To preserve change ownership by user %s, you must\n" \
1770 "do \"p4 change -f <change>\" after submitting and\n" \
1771 "edit the User field."
1773 print "After submitting, renamed files must be re-synced."
1774 print "Invoke \"p4 sync -f\" on each of these files:"
1775 for f in pureRenameCopy:
1779 print "To revert the changes, use \"p4 revert ...\", and delete"
1780 print "the submit template file \"%s\"" % fileName
1782 print "Since the commit adds new files, they must be deleted:"
1783 for f in filesToAdd:
1789 # Let the user edit the change description, then submit it.
1794 if self.edit_template(fileName):
1795 # read the edited message and submit
1796 tmpFile = open(fileName, "rb")
1797 message = tmpFile.read()
1800 message = message.replace("\r\n", "\n")
1801 submitTemplate = message[:message.index(separatorLine)]
1802 p4_write_pipe(['submit', '-i'], submitTemplate)
1804 if self.preserveUser:
1806 # Get last changelist number. Cannot easily get it from
1807 # the submit command output as the output is
1809 changelist = self.lastP4Changelist()
1810 self.modifyChangelistUser(changelist, p4User)
1812 # The rename/copy happened by applying a patch that created a
1813 # new file. This leaves it writable, which confuses p4.
1814 for f in pureRenameCopy:
1821 print "Submission cancelled, undoing p4 changes."
1822 for f in editedFiles:
1824 for f in filesToAdd:
1827 for f in filesToDelete:
1833 # Export git tags as p4 labels. Create a p4 label and then tag
1835 def exportGitTags(self, gitTags):
1836 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1837 if len(validLabelRegexp) == 0:
1838 validLabelRegexp = defaultLabelRegexp
1839 m = re.compile(validLabelRegexp)
1841 for name in gitTags:
1843 if not m.match(name):
1845 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
1848 # Get the p4 commit this corresponds to
1849 logMessage = extractLogMessageFromGitCommit(name)
1850 values = extractSettingsGitLog(logMessage)
1852 if not values.has_key('change'):
1853 # a tag pointing to something not sent to p4; ignore
1855 print "git tag %s does not give a p4 commit" % name
1858 changelist = values['change']
1860 # Get the tag details.
1864 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1867 if re.match(r'tag\s+', l):
1869 elif re.match(r'\s*$', l):
1876 body = ["lightweight tag imported by git p4\n"]
1878 # Create the label - use the same view as the client spec we are using
1879 clientSpec = getClientSpec()
1881 labelTemplate = "Label: %s\n" % name
1882 labelTemplate += "Description:\n"
1884 labelTemplate += "\t" + b + "\n"
1885 labelTemplate += "View:\n"
1886 for depot_side in clientSpec.mappings:
1887 labelTemplate += "\t%s\n" % depot_side
1890 print "Would create p4 label %s for tag" % name
1891 elif self.prepare_p4_only:
1892 print "Not creating p4 label %s for tag due to option" \
1893 " --prepare-p4-only" % name
1895 p4_write_pipe(["label", "-i"], labelTemplate)
1898 p4_system(["tag", "-l", name] +
1899 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1902 print "created p4 label for tag %s" % name
1904 def run(self, args):
1906 self.master = currentGitBranch()
1907 elif len(args) == 1:
1908 self.master = args[0]
1909 if not branchExists(self.master):
1910 die("Branch %s does not exist" % self.master)
1915 allowSubmit = gitConfig("git-p4.allowSubmit")
1916 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1917 die("%s is not in git-p4.allowSubmit" % self.master)
1919 [upstream, settings] = findUpstreamBranchPoint()
1920 self.depotPath = settings['depot-paths'][0]
1921 if len(self.origin) == 0:
1922 self.origin = upstream
1924 if self.preserveUser:
1925 if not self.canChangeChangelists():
1926 die("Cannot preserve user names without p4 super-user or admin permissions")
1928 # if not set from the command line, try the config file
1929 if self.conflict_behavior is None:
1930 val = gitConfig("git-p4.conflict")
1932 if val not in self.conflict_behavior_choices:
1933 die("Invalid value '%s' for config git-p4.conflict" % val)
1936 self.conflict_behavior = val
1939 print "Origin branch is " + self.origin
1941 if len(self.depotPath) == 0:
1942 print "Internal error: cannot locate perforce depot path from existing branches"
1945 self.useClientSpec = False
1946 if gitConfigBool("git-p4.useclientspec"):
1947 self.useClientSpec = True
1948 if self.useClientSpec:
1949 self.clientSpecDirs = getClientSpec()
1951 # Check for the existence of P4 branches
1952 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
1954 if self.useClientSpec and not branchesDetected:
1955 # all files are relative to the client spec
1956 self.clientPath = getClientRoot()
1958 self.clientPath = p4Where(self.depotPath)
1960 if self.clientPath == "":
1961 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
1963 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1964 self.oldWorkingDirectory = os.getcwd()
1966 # ensure the clientPath exists
1967 new_client_dir = False
1968 if not os.path.exists(self.clientPath):
1969 new_client_dir = True
1970 os.makedirs(self.clientPath)
1972 chdir(self.clientPath, is_client_path=True)
1974 print "Would synchronize p4 checkout in %s" % self.clientPath
1976 print "Synchronizing p4 checkout..."
1978 # old one was destroyed, and maybe nobody told p4
1979 p4_sync("...", "-f")
1986 commitish = self.master
1990 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, commitish)]):
1991 commits.append(line.strip())
1994 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
1995 self.checkAuthorship = False
1997 self.checkAuthorship = True
1999 if self.preserveUser:
2000 self.checkValidP4Users(commits)
2003 # Build up a set of options to be passed to diff when
2004 # submitting each commit to p4.
2006 if self.detectRenames:
2007 # command-line -M arg
2008 self.diffOpts = "-M"
2010 # If not explicitly set check the config variable
2011 detectRenames = gitConfig("git-p4.detectRenames")
2013 if detectRenames.lower() == "false" or detectRenames == "":
2015 elif detectRenames.lower() == "true":
2016 self.diffOpts = "-M"
2018 self.diffOpts = "-M%s" % detectRenames
2020 # no command-line arg for -C or --find-copies-harder, just
2022 detectCopies = gitConfig("git-p4.detectCopies")
2023 if detectCopies.lower() == "false" or detectCopies == "":
2025 elif detectCopies.lower() == "true":
2026 self.diffOpts += " -C"
2028 self.diffOpts += " -C%s" % detectCopies
2030 if gitConfigBool("git-p4.detectCopiesHarder"):
2031 self.diffOpts += " --find-copies-harder"
2034 # Apply the commits, one at a time. On failure, ask if should
2035 # continue to try the rest of the patches, or quit.
2040 last = len(commits) - 1
2041 for i, commit in enumerate(commits):
2043 print " ", read_pipe(["git", "show", "-s",
2044 "--format=format:%h %s", commit])
2047 ok = self.applyCommit(commit)
2049 applied.append(commit)
2051 if self.prepare_p4_only and i < last:
2052 print "Processing only the first commit due to option" \
2053 " --prepare-p4-only"
2058 # prompt for what to do, or use the option/variable
2059 if self.conflict_behavior == "ask":
2060 print "What do you want to do?"
2061 response = raw_input("[s]kip this commit but apply"
2062 " the rest, or [q]uit? ")
2065 elif self.conflict_behavior == "skip":
2067 elif self.conflict_behavior == "quit":
2070 die("Unknown conflict_behavior '%s'" %
2071 self.conflict_behavior)
2073 if response[0] == "s":
2074 print "Skipping this commit, but applying the rest"
2076 if response[0] == "q":
2083 chdir(self.oldWorkingDirectory)
2087 elif self.prepare_p4_only:
2089 elif len(commits) == len(applied):
2090 print "All commits applied!"
2094 sync.branch = self.branch
2101 if len(applied) == 0:
2102 print "No commits applied."
2104 print "Applied only the commits marked with '*':"
2110 print star, read_pipe(["git", "show", "-s",
2111 "--format=format:%h %s", c])
2112 print "You will have to do 'git p4 sync' and rebase."
2114 if gitConfigBool("git-p4.exportLabels"):
2115 self.exportLabels = True
2117 if self.exportLabels:
2118 p4Labels = getP4Labels(self.depotPath)
2119 gitTags = getGitTags()
2121 missingGitTags = gitTags - p4Labels
2122 self.exportGitTags(missingGitTags)
2124 # exit with error unless everything applied perfectly
2125 if len(commits) != len(applied):
2131 """Represent a p4 view ("p4 help views"), and map files in a
2132 repo according to the view."""
2134 def __init__(self, client_name):
2136 self.client_prefix = "//%s/" % client_name
2137 # cache results of "p4 where" to lookup client file locations
2138 self.client_spec_path_cache = {}
2140 def append(self, view_line):
2141 """Parse a view line, splitting it into depot and client
2142 sides. Append to self.mappings, preserving order. This
2143 is only needed for tag creation."""
2145 # Split the view line into exactly two words. P4 enforces
2146 # structure on these lines that simplifies this quite a bit.
2148 # Either or both words may be double-quoted.
2149 # Single quotes do not matter.
2150 # Double-quote marks cannot occur inside the words.
2151 # A + or - prefix is also inside the quotes.
2152 # There are no quotes unless they contain a space.
2153 # The line is already white-space stripped.
2154 # The two words are separated by a single space.
2156 if view_line[0] == '"':
2157 # First word is double quoted. Find its end.
2158 close_quote_index = view_line.find('"', 1)
2159 if close_quote_index <= 0:
2160 die("No first-word closing quote found: %s" % view_line)
2161 depot_side = view_line[1:close_quote_index]
2162 # skip closing quote and space
2163 rhs_index = close_quote_index + 1 + 1
2165 space_index = view_line.find(" ")
2166 if space_index <= 0:
2167 die("No word-splitting space found: %s" % view_line)
2168 depot_side = view_line[0:space_index]
2169 rhs_index = space_index + 1
2171 # prefix + means overlay on previous mapping
2172 if depot_side.startswith("+"):
2173 depot_side = depot_side[1:]
2175 # prefix - means exclude this path, leave out of mappings
2177 if depot_side.startswith("-"):
2179 depot_side = depot_side[1:]
2182 self.mappings.append(depot_side)
2184 def convert_client_path(self, clientFile):
2185 # chop off //client/ part to make it relative
2186 if not clientFile.startswith(self.client_prefix):
2187 die("No prefix '%s' on clientFile '%s'" %
2188 (self.client_prefix, clientFile))
2189 return clientFile[len(self.client_prefix):]
2191 def update_client_spec_path_cache(self, files):
2192 """ Caching file paths by "p4 where" batch query """
2194 # List depot file paths exclude that already cached
2195 fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
2197 if len(fileArgs) == 0:
2198 return # All files in cache
2200 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2201 for res in where_result:
2202 if "code" in res and res["code"] == "error":
2203 # assume error is "... file(s) not in client view"
2205 if "clientFile" not in res:
2206 die("No clientFile in 'p4 where' output")
2208 # it will list all of them, but only one not unmap-ped
2210 if gitConfigBool("core.ignorecase"):
2211 res['depotFile'] = res['depotFile'].lower()
2212 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
2214 # not found files or unmap files set to ""
2215 for depotFile in fileArgs:
2216 if gitConfigBool("core.ignorecase"):
2217 depotFile = depotFile.lower()
2218 if depotFile not in self.client_spec_path_cache:
2219 self.client_spec_path_cache[depotFile] = ""
2221 def map_in_client(self, depot_path):
2222 """Return the relative location in the client where this
2223 depot file should live. Returns "" if the file should
2224 not be mapped in the client."""
2226 if gitConfigBool("core.ignorecase"):
2227 depot_path = depot_path.lower()
2229 if depot_path in self.client_spec_path_cache:
2230 return self.client_spec_path_cache[depot_path]
2232 die( "Error: %s is not found in client spec path" % depot_path )
2235 class P4Sync(Command, P4UserMap):
2236 delete_actions = ( "delete", "move/delete", "purge" )
2239 Command.__init__(self)
2240 P4UserMap.__init__(self)
2242 optparse.make_option("--branch", dest="branch"),
2243 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2244 optparse.make_option("--changesfile", dest="changesFile"),
2245 optparse.make_option("--silent", dest="silent", action="store_true"),
2246 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2247 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2248 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2249 help="Import into refs/heads/ , not refs/remotes"),
2250 optparse.make_option("--max-changes", dest="maxChanges",
2251 help="Maximum number of changes to import"),
2252 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2253 help="Internal block size to use when iteratively calling p4 changes"),
2254 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2255 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2256 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2257 help="Only sync files that are included in the Perforce Client Spec"),
2258 optparse.make_option("-/", dest="cloneExclude",
2259 action="append", type="string",
2260 help="exclude depot path"),
2262 self.description = """Imports from Perforce into a git repository.\n
2264 //depot/my/project/ -- to import the current head
2265 //depot/my/project/@all -- to import everything
2266 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2268 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2270 self.usage += " //depot/path[@revRange]"
2272 self.createdBranches = set()
2273 self.committedChanges = set()
2275 self.detectBranches = False
2276 self.detectLabels = False
2277 self.importLabels = False
2278 self.changesFile = ""
2279 self.syncWithOrigin = True
2280 self.importIntoRemotes = True
2281 self.maxChanges = ""
2282 self.changes_block_size = None
2283 self.keepRepoPath = False
2284 self.depotPaths = None
2285 self.p4BranchesInGit = []
2286 self.cloneExclude = []
2287 self.useClientSpec = False
2288 self.useClientSpec_from_options = False
2289 self.clientSpecDirs = None
2290 self.tempBranches = []
2291 self.tempBranchLocation = "refs/git-p4-tmp"
2292 self.largeFileSystem = None
2294 if gitConfig('git-p4.largeFileSystem'):
2295 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2296 self.largeFileSystem = largeFileSystemConstructor(
2297 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2300 if gitConfig("git-p4.syncFromOrigin") == "false":
2301 self.syncWithOrigin = False
2303 # This is required for the "append" cloneExclude action
2304 def ensure_value(self, attr, value):
2305 if not hasattr(self, attr) or getattr(self, attr) is None:
2306 setattr(self, attr, value)
2307 return getattr(self, attr)
2309 # Force a checkpoint in fast-import and wait for it to finish
2310 def checkpoint(self):
2311 self.gitStream.write("checkpoint\n\n")
2312 self.gitStream.write("progress checkpoint\n\n")
2313 out = self.gitOutput.readline()
2315 print "checkpoint finished: " + out
2317 def extractFilesFromCommit(self, commit):
2318 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2319 for path in self.cloneExclude]
2322 while commit.has_key("depotFile%s" % fnum):
2323 path = commit["depotFile%s" % fnum]
2325 if [p for p in self.cloneExclude
2326 if p4PathStartsWith(path, p)]:
2329 found = [p for p in self.depotPaths
2330 if p4PathStartsWith(path, p)]
2337 file["rev"] = commit["rev%s" % fnum]
2338 file["action"] = commit["action%s" % fnum]
2339 file["type"] = commit["type%s" % fnum]
2344 def extractJobsFromCommit(self, commit):
2347 while commit.has_key("job%s" % jnum):
2348 job = commit["job%s" % jnum]
2353 def stripRepoPath(self, path, prefixes):
2354 """When streaming files, this is called to map a p4 depot path
2355 to where it should go in git. The prefixes are either
2356 self.depotPaths, or self.branchPrefixes in the case of
2357 branch detection."""
2359 if self.useClientSpec:
2360 # branch detection moves files up a level (the branch name)
2361 # from what client spec interpretation gives
2362 path = self.clientSpecDirs.map_in_client(path)
2363 if self.detectBranches:
2364 for b in self.knownBranches:
2365 if path.startswith(b + "/"):
2366 path = path[len(b)+1:]
2368 elif self.keepRepoPath:
2369 # Preserve everything in relative path name except leading
2370 # //depot/; just look at first prefix as they all should
2371 # be in the same depot.
2372 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2373 if p4PathStartsWith(path, depot):
2374 path = path[len(depot):]
2378 if p4PathStartsWith(path, p):
2379 path = path[len(p):]
2382 path = wildcard_decode(path)
2385 def splitFilesIntoBranches(self, commit):
2386 """Look at each depotFile in the commit to figure out to what
2387 branch it belongs."""
2389 if self.clientSpecDirs:
2390 files = self.extractFilesFromCommit(commit)
2391 self.clientSpecDirs.update_client_spec_path_cache(files)
2395 while commit.has_key("depotFile%s" % fnum):
2396 path = commit["depotFile%s" % fnum]
2397 found = [p for p in self.depotPaths
2398 if p4PathStartsWith(path, p)]
2405 file["rev"] = commit["rev%s" % fnum]
2406 file["action"] = commit["action%s" % fnum]
2407 file["type"] = commit["type%s" % fnum]
2410 # start with the full relative path where this file would
2412 if self.useClientSpec:
2413 relPath = self.clientSpecDirs.map_in_client(path)
2415 relPath = self.stripRepoPath(path, self.depotPaths)
2417 for branch in self.knownBranches.keys():
2418 # add a trailing slash so that a commit into qt/4.2foo
2419 # doesn't end up in qt/4.2, e.g.
2420 if relPath.startswith(branch + "/"):
2421 if branch not in branches:
2422 branches[branch] = []
2423 branches[branch].append(file)
2428 def writeToGitStream(self, gitMode, relPath, contents):
2429 self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2430 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2432 self.gitStream.write(d)
2433 self.gitStream.write('\n')
2435 # output one file from the P4 stream
2436 # - helper for streamP4Files
2438 def streamOneP4File(self, file, contents):
2439 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2441 size = int(self.stream_file['fileSize'])
2442 sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
2445 (type_base, type_mods) = split_p4_type(file["type"])
2448 if "x" in type_mods:
2450 if type_base == "symlink":
2452 # p4 print on a symlink sometimes contains "target\n";
2453 # if it does, remove the newline
2454 data = ''.join(contents)
2456 # Some version of p4 allowed creating a symlink that pointed
2457 # to nothing. This causes p4 errors when checking out such
2458 # a change, and errors here too. Work around it by ignoring
2459 # the bad symlink; hopefully a future change fixes it.
2460 print "\nIgnoring empty symlink in %s" % file['depotFile']
2462 elif data[-1] == '\n':
2463 contents = [data[:-1]]
2467 if type_base == "utf16":
2468 # p4 delivers different text in the python output to -G
2469 # than it does when using "print -o", or normal p4 client
2470 # operations. utf16 is converted to ascii or utf8, perhaps.
2471 # But ascii text saved as -t utf16 is completely mangled.
2472 # Invoke print -o to get the real contents.
2474 # On windows, the newlines will always be mangled by print, so put
2475 # them back too. This is not needed to the cygwin windows version,
2476 # just the native "NT" type.
2479 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2480 except Exception as e:
2481 if 'Translation of file content failed' in str(e):
2482 type_base = 'binary'
2486 if p4_version_string().find('/NT') >= 0:
2487 text = text.replace('\r\n', '\n')
2490 if type_base == "apple":
2491 # Apple filetype files will be streamed as a concatenation of
2492 # its appledouble header and the contents. This is useless
2493 # on both macs and non-macs. If using "print -q -o xx", it
2494 # will create "xx" with the data, and "%xx" with the header.
2495 # This is also not very useful.
2497 # Ideally, someday, this script can learn how to generate
2498 # appledouble files directly and import those to git, but
2499 # non-mac machines can never find a use for apple filetype.
2500 print "\nIgnoring apple filetype file %s" % file['depotFile']
2503 # Note that we do not try to de-mangle keywords on utf16 files,
2504 # even though in theory somebody may want that.
2505 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2507 regexp = re.compile(pattern, re.VERBOSE)
2508 text = ''.join(contents)
2509 text = regexp.sub(r'$\1$', text)
2513 relPath.decode('ascii')
2516 if gitConfig('git-p4.pathEncoding'):
2517 encoding = gitConfig('git-p4.pathEncoding')
2518 relPath = relPath.decode(encoding, 'replace').encode('utf8', 'replace')
2520 print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, relPath)
2522 if self.largeFileSystem:
2523 (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
2525 self.writeToGitStream(git_mode, relPath, contents)
2527 def streamOneP4Deletion(self, file):
2528 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2530 sys.stdout.write("delete %s\n" % relPath)
2532 self.gitStream.write("D %s\n" % relPath)
2534 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2535 self.largeFileSystem.removeLargeFile(relPath)
2537 # handle another chunk of streaming data
2538 def streamP4FilesCb(self, marshalled):
2540 # catch p4 errors and complain
2542 if "code" in marshalled:
2543 if marshalled["code"] == "error":
2544 if "data" in marshalled:
2545 err = marshalled["data"].rstrip()
2547 if not err and 'fileSize' in self.stream_file:
2548 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
2549 if required_bytes > 0:
2550 err = 'Not enough space left on %s! Free at least %i MB.' % (
2551 os.getcwd(), required_bytes/1024/1024
2556 if self.stream_have_file_info:
2557 if "depotFile" in self.stream_file:
2558 f = self.stream_file["depotFile"]
2559 # force a failure in fast-import, else an empty
2560 # commit will be made
2561 self.gitStream.write("\n")
2562 self.gitStream.write("die-now\n")
2563 self.gitStream.close()
2564 # ignore errors, but make sure it exits first
2565 self.importProcess.wait()
2567 die("Error from p4 print for %s: %s" % (f, err))
2569 die("Error from p4 print: %s" % err)
2571 if marshalled.has_key('depotFile') and self.stream_have_file_info:
2572 # start of a new file - output the old one first
2573 self.streamOneP4File(self.stream_file, self.stream_contents)
2574 self.stream_file = {}
2575 self.stream_contents = []
2576 self.stream_have_file_info = False
2578 # pick up the new file information... for the
2579 # 'data' field we need to append to our array
2580 for k in marshalled.keys():
2582 if 'streamContentSize' not in self.stream_file:
2583 self.stream_file['streamContentSize'] = 0
2584 self.stream_file['streamContentSize'] += len(marshalled['data'])
2585 self.stream_contents.append(marshalled['data'])
2587 self.stream_file[k] = marshalled[k]
2590 'streamContentSize' in self.stream_file and
2591 'fileSize' in self.stream_file and
2592 'depotFile' in self.stream_file):
2593 size = int(self.stream_file["fileSize"])
2595 progress = 100*self.stream_file['streamContentSize']/size
2596 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
2599 self.stream_have_file_info = True
2601 # Stream directly from "p4 files" into "git fast-import"
2602 def streamP4Files(self, files):
2608 filesForCommit.append(f)
2609 if f['action'] in self.delete_actions:
2610 filesToDelete.append(f)
2612 filesToRead.append(f)
2615 for f in filesToDelete:
2616 self.streamOneP4Deletion(f)
2618 if len(filesToRead) > 0:
2619 self.stream_file = {}
2620 self.stream_contents = []
2621 self.stream_have_file_info = False
2623 # curry self argument
2624 def streamP4FilesCbSelf(entry):
2625 self.streamP4FilesCb(entry)
2627 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2629 p4CmdList(["-x", "-", "print"],
2631 cb=streamP4FilesCbSelf)
2634 if self.stream_file.has_key('depotFile'):
2635 self.streamOneP4File(self.stream_file, self.stream_contents)
2637 def make_email(self, userid):
2638 if userid in self.users:
2639 return self.users[userid]
2641 return "%s <a@b>" % userid
2643 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2644 """ Stream a p4 tag.
2645 commit is either a git commit, or a fast-import mark, ":<p4commit>"
2649 print "writing tag %s for commit %s" % (labelName, commit)
2650 gitStream.write("tag %s\n" % labelName)
2651 gitStream.write("from %s\n" % commit)
2653 if labelDetails.has_key('Owner'):
2654 owner = labelDetails["Owner"]
2658 # Try to use the owner of the p4 label, or failing that,
2659 # the current p4 user id.
2661 email = self.make_email(owner)
2663 email = self.make_email(self.p4UserId())
2664 tagger = "%s %s %s" % (email, epoch, self.tz)
2666 gitStream.write("tagger %s\n" % tagger)
2668 print "labelDetails=",labelDetails
2669 if labelDetails.has_key('Description'):
2670 description = labelDetails['Description']
2672 description = 'Label from git p4'
2674 gitStream.write("data %d\n" % len(description))
2675 gitStream.write(description)
2676 gitStream.write("\n")
2678 def inClientSpec(self, path):
2679 if not self.clientSpecDirs:
2681 inClientSpec = self.clientSpecDirs.map_in_client(path)
2682 if not inClientSpec and self.verbose:
2683 print('Ignoring file outside of client spec: {0}'.format(path))
2686 def hasBranchPrefix(self, path):
2687 if not self.branchPrefixes:
2689 hasPrefix = [p for p in self.branchPrefixes
2690 if p4PathStartsWith(path, p)]
2691 if not hasPrefix and self.verbose:
2692 print('Ignoring file outside of prefix: {0}'.format(path))
2695 def commit(self, details, files, branch, parent = ""):
2696 epoch = details["time"]
2697 author = details["user"]
2698 jobs = self.extractJobsFromCommit(details)
2701 print('commit into {0}'.format(branch))
2703 if self.clientSpecDirs:
2704 self.clientSpecDirs.update_client_spec_path_cache(files)
2706 files = [f for f in files
2707 if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
2709 if not files and not gitConfigBool('git-p4.keepEmptyCommits'):
2710 print('Ignoring revision {0} as it would produce an empty commit.'
2711 .format(details['change']))
2714 self.gitStream.write("commit %s\n" % branch)
2715 self.gitStream.write("mark :%s\n" % details["change"])
2716 self.committedChanges.add(int(details["change"]))
2718 if author not in self.users:
2719 self.getUserMapFromPerforceServer()
2720 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2722 self.gitStream.write("committer %s\n" % committer)
2724 self.gitStream.write("data <<EOT\n")
2725 self.gitStream.write(details["desc"])
2727 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
2728 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2729 (','.join(self.branchPrefixes), details["change"]))
2730 if len(details['options']) > 0:
2731 self.gitStream.write(": options = %s" % details['options'])
2732 self.gitStream.write("]\nEOT\n\n")
2736 print "parent %s" % parent
2737 self.gitStream.write("from %s\n" % parent)
2739 self.streamP4Files(files)
2740 self.gitStream.write("\n")
2742 change = int(details["change"])
2744 if self.labels.has_key(change):
2745 label = self.labels[change]
2746 labelDetails = label[0]
2747 labelRevisions = label[1]
2749 print "Change %s is labelled %s" % (change, labelDetails)
2751 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2752 for p in self.branchPrefixes])
2754 if len(files) == len(labelRevisions):
2758 if info["action"] in self.delete_actions:
2760 cleanedFiles[info["depotFile"]] = info["rev"]
2762 if cleanedFiles == labelRevisions:
2763 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2767 print ("Tag %s does not match with change %s: files do not match."
2768 % (labelDetails["label"], change))
2772 print ("Tag %s does not match with change %s: file count is different."
2773 % (labelDetails["label"], change))
2775 # Build a dictionary of changelists and labels, for "detect-labels" option.
2776 def getLabels(self):
2779 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2780 if len(l) > 0 and not self.silent:
2781 print "Finding files belonging to labels in %s" % `self.depotPaths`
2784 label = output["label"]
2788 print "Querying files for label %s" % label
2789 for file in p4CmdList(["files"] +
2790 ["%s...@%s" % (p, label)
2791 for p in self.depotPaths]):
2792 revisions[file["depotFile"]] = file["rev"]
2793 change = int(file["change"])
2794 if change > newestChange:
2795 newestChange = change
2797 self.labels[newestChange] = [output, revisions]
2800 print "Label changes: %s" % self.labels.keys()
2802 # Import p4 labels as git tags. A direct mapping does not
2803 # exist, so assume that if all the files are at the same revision
2804 # then we can use that, or it's something more complicated we should
2806 def importP4Labels(self, stream, p4Labels):
2808 print "import p4 labels: " + ' '.join(p4Labels)
2810 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
2811 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
2812 if len(validLabelRegexp) == 0:
2813 validLabelRegexp = defaultLabelRegexp
2814 m = re.compile(validLabelRegexp)
2816 for name in p4Labels:
2819 if not m.match(name):
2821 print "label %s does not match regexp %s" % (name,validLabelRegexp)
2824 if name in ignoredP4Labels:
2827 labelDetails = p4CmdList(['label', "-o", name])[0]
2829 # get the most recent changelist for each file in this label
2830 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2831 for p in self.depotPaths])
2833 if change.has_key('change'):
2834 # find the corresponding git commit; take the oldest commit
2835 changelist = int(change['change'])
2836 if changelist in self.committedChanges:
2837 gitCommit = ":%d" % changelist # use a fast-import mark
2840 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2841 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
2842 if len(gitCommit) == 0:
2843 print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
2846 gitCommit = gitCommit.strip()
2849 # Convert from p4 time format
2851 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2853 print "Could not convert label time %s" % labelDetails['Update']
2856 when = int(time.mktime(tmwhen))
2857 self.streamTag(stream, name, labelDetails, gitCommit, when)
2859 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2862 print "Label %s has no changelists - possibly deleted?" % name
2865 # We can't import this label; don't try again as it will get very
2866 # expensive repeatedly fetching all the files for labels that will
2867 # never be imported. If the label is moved in the future, the
2868 # ignore will need to be removed manually.
2869 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2871 def guessProjectName(self):
2872 for p in self.depotPaths:
2875 p = p[p.strip().rfind("/") + 1:]
2876 if not p.endswith("/"):
2880 def getBranchMapping(self):
2881 lostAndFoundBranches = set()
2883 user = gitConfig("git-p4.branchUser")
2885 command = "branches -u %s" % user
2887 command = "branches"
2889 for info in p4CmdList(command):
2890 details = p4Cmd(["branch", "-o", info["branch"]])
2892 while details.has_key("View%s" % viewIdx):
2893 paths = details["View%s" % viewIdx].split(" ")
2894 viewIdx = viewIdx + 1
2895 # require standard //depot/foo/... //depot/bar/... mapping
2896 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2899 destination = paths[1]
2901 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
2902 source = source[len(self.depotPaths[0]):-4]
2903 destination = destination[len(self.depotPaths[0]):-4]
2905 if destination in self.knownBranches:
2907 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2908 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2911 self.knownBranches[destination] = source
2913 lostAndFoundBranches.discard(destination)
2915 if source not in self.knownBranches:
2916 lostAndFoundBranches.add(source)
2918 # Perforce does not strictly require branches to be defined, so we also
2919 # check git config for a branch list.
2921 # Example of branch definition in git config file:
2923 # branchList=main:branchA
2924 # branchList=main:branchB
2925 # branchList=branchA:branchC
2926 configBranches = gitConfigList("git-p4.branchList")
2927 for branch in configBranches:
2929 (source, destination) = branch.split(":")
2930 self.knownBranches[destination] = source
2932 lostAndFoundBranches.discard(destination)
2934 if source not in self.knownBranches:
2935 lostAndFoundBranches.add(source)
2938 for branch in lostAndFoundBranches:
2939 self.knownBranches[branch] = branch
2941 def getBranchMappingFromGitBranches(self):
2942 branches = p4BranchesInGit(self.importIntoRemotes)
2943 for branch in branches.keys():
2944 if branch == "master":
2947 branch = branch[len(self.projectName):]
2948 self.knownBranches[branch] = branch
2950 def updateOptionDict(self, d):
2952 if self.keepRepoPath:
2953 option_keys['keepRepoPath'] = 1
2955 d["options"] = ' '.join(sorted(option_keys.keys()))
2957 def readOptions(self, d):
2958 self.keepRepoPath = (d.has_key('options')
2959 and ('keepRepoPath' in d['options']))
2961 def gitRefForBranch(self, branch):
2962 if branch == "main":
2963 return self.refPrefix + "master"
2965 if len(branch) <= 0:
2968 return self.refPrefix + self.projectName + branch
2970 def gitCommitByP4Change(self, ref, change):
2972 print "looking in ref " + ref + " for change %s using bisect..." % change
2975 latestCommit = parseRevision(ref)
2979 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2980 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2985 log = extractLogMessageFromGitCommit(next)
2986 settings = extractSettingsGitLog(log)
2987 currentChange = int(settings['change'])
2989 print "current change %s" % currentChange
2991 if currentChange == change:
2993 print "found %s" % next
2996 if currentChange < change:
2997 earliestCommit = "^%s" % next
2999 latestCommit = "%s" % next
3003 def importNewBranch(self, branch, maxChange):
3004 # make fast-import flush all changes to disk and update the refs using the checkpoint
3005 # command so that we can try to find the branch parent in the git history
3006 self.gitStream.write("checkpoint\n\n");
3007 self.gitStream.flush();
3008 branchPrefix = self.depotPaths[0] + branch + "/"
3009 range = "@1,%s" % maxChange
3010 #print "prefix" + branchPrefix
3011 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3012 if len(changes) <= 0:
3014 firstChange = changes[0]
3015 #print "first change in branch: %s" % firstChange
3016 sourceBranch = self.knownBranches[branch]
3017 sourceDepotPath = self.depotPaths[0] + sourceBranch
3018 sourceRef = self.gitRefForBranch(sourceBranch)
3019 #print "source " + sourceBranch
3021 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3022 #print "branch parent: %s" % branchParentChange
3023 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3024 if len(gitParent) > 0:
3025 self.initialParents[self.gitRefForBranch(branch)] = gitParent
3026 #print "parent git commit: %s" % gitParent
3028 self.importChanges(changes)
3031 def searchParent(self, parent, branch, target):
3033 for blob in read_pipe_lines(["git", "rev-list", "--reverse",
3034 "--no-merges", parent]):
3036 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
3039 print "Found parent of %s in commit %s" % (branch, blob)
3046 def importChanges(self, changes):
3048 for change in changes:
3049 description = p4_describe(change)
3050 self.updateOptionDict(description)
3053 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3058 if self.detectBranches:
3059 branches = self.splitFilesIntoBranches(description)
3060 for branch in branches.keys():
3062 branchPrefix = self.depotPaths[0] + branch + "/"
3063 self.branchPrefixes = [ branchPrefix ]
3067 filesForCommit = branches[branch]
3070 print "branch is %s" % branch
3072 self.updatedBranches.add(branch)
3074 if branch not in self.createdBranches:
3075 self.createdBranches.add(branch)
3076 parent = self.knownBranches[branch]
3077 if parent == branch:
3080 fullBranch = self.projectName + branch
3081 if fullBranch not in self.p4BranchesInGit:
3083 print("\n Importing new branch %s" % fullBranch);
3084 if self.importNewBranch(branch, change - 1):
3086 self.p4BranchesInGit.append(fullBranch)
3088 print("\n Resuming with change %s" % change);
3091 print "parent determined through known branches: %s" % parent
3093 branch = self.gitRefForBranch(branch)
3094 parent = self.gitRefForBranch(parent)
3097 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
3099 if len(parent) == 0 and branch in self.initialParents:
3100 parent = self.initialParents[branch]
3101 del self.initialParents[branch]
3105 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3107 print "Creating temporary branch: " + tempBranch
3108 self.commit(description, filesForCommit, tempBranch)
3109 self.tempBranches.append(tempBranch)
3111 blob = self.searchParent(parent, branch, tempBranch)
3113 self.commit(description, filesForCommit, branch, blob)
3116 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
3117 self.commit(description, filesForCommit, branch, parent)
3119 files = self.extractFilesFromCommit(description)
3120 self.commit(description, files, self.branch,
3122 # only needed once, to connect to the previous commit
3123 self.initialParent = ""
3125 print self.gitError.read()
3128 def importHeadRevision(self, revision):
3129 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
3132 details["user"] = "git perforce import user"
3133 details["desc"] = ("Initial import of %s from the state at revision %s\n"
3134 % (' '.join(self.depotPaths), revision))
3135 details["change"] = revision
3139 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3141 for info in p4CmdList(["files"] + fileArgs):
3143 if 'code' in info and info['code'] == 'error':
3144 sys.stderr.write("p4 returned an error: %s\n"
3146 if info['data'].find("must refer to client") >= 0:
3147 sys.stderr.write("This particular p4 error is misleading.\n")
3148 sys.stderr.write("Perhaps the depot path was misspelled.\n");
3149 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
3151 if 'p4ExitCode' in info:
3152 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3156 change = int(info["change"])
3157 if change > newestRevision:
3158 newestRevision = change
3160 if info["action"] in self.delete_actions:
3161 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3162 #fileCnt = fileCnt + 1
3165 for prop in ["depotFile", "rev", "action", "type" ]:
3166 details["%s%s" % (prop, fileCnt)] = info[prop]
3168 fileCnt = fileCnt + 1
3170 details["change"] = newestRevision
3172 # Use time from top-most change so that all git p4 clones of
3173 # the same p4 repo have the same commit SHA1s.
3174 res = p4_describe(newestRevision)
3175 details["time"] = res["time"]
3177 self.updateOptionDict(details)
3179 self.commit(details, self.extractFilesFromCommit(details), self.branch)
3181 print "IO error with git fast-import. Is your git version recent enough?"
3182 print self.gitError.read()
3185 def run(self, args):
3186 self.depotPaths = []
3187 self.changeRange = ""
3188 self.previousDepotPaths = []
3189 self.hasOrigin = False
3191 # map from branch depot path to parent branch
3192 self.knownBranches = {}
3193 self.initialParents = {}
3195 if self.importIntoRemotes:
3196 self.refPrefix = "refs/remotes/p4/"
3198 self.refPrefix = "refs/heads/p4/"
3200 if self.syncWithOrigin:
3201 self.hasOrigin = originP4BranchesExist()
3204 print 'Syncing with origin first, using "git fetch origin"'
3205 system("git fetch origin")
3207 branch_arg_given = bool(self.branch)
3208 if len(self.branch) == 0:
3209 self.branch = self.refPrefix + "master"
3210 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3211 system("git update-ref %s refs/heads/p4" % self.branch)
3212 system("git branch -D p4")
3214 # accept either the command-line option, or the configuration variable
3215 if self.useClientSpec:
3216 # will use this after clone to set the variable
3217 self.useClientSpec_from_options = True
3219 if gitConfigBool("git-p4.useclientspec"):
3220 self.useClientSpec = True
3221 if self.useClientSpec:
3222 self.clientSpecDirs = getClientSpec()
3224 # TODO: should always look at previous commits,
3225 # merge with previous imports, if possible.
3228 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3230 # branches holds mapping from branch name to sha1
3231 branches = p4BranchesInGit(self.importIntoRemotes)
3233 # restrict to just this one, disabling detect-branches
3234 if branch_arg_given:
3235 short = self.branch.split("/")[-1]
3236 if short in branches:
3237 self.p4BranchesInGit = [ short ]
3239 self.p4BranchesInGit = branches.keys()
3241 if len(self.p4BranchesInGit) > 1:
3243 print "Importing from/into multiple branches"
3244 self.detectBranches = True
3245 for branch in branches.keys():
3246 self.initialParents[self.refPrefix + branch] = \
3250 print "branches: %s" % self.p4BranchesInGit
3253 for branch in self.p4BranchesInGit:
3254 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
3256 settings = extractSettingsGitLog(logMsg)
3258 self.readOptions(settings)
3259 if (settings.has_key('depot-paths')
3260 and settings.has_key ('change')):
3261 change = int(settings['change']) + 1
3262 p4Change = max(p4Change, change)
3264 depotPaths = sorted(settings['depot-paths'])
3265 if self.previousDepotPaths == []:
3266 self.previousDepotPaths = depotPaths
3269 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3270 prev_list = prev.split("/")
3271 cur_list = cur.split("/")
3272 for i in range(0, min(len(cur_list), len(prev_list))):
3273 if cur_list[i] <> prev_list[i]:
3277 paths.append ("/".join(cur_list[:i + 1]))
3279 self.previousDepotPaths = paths
3282 self.depotPaths = sorted(self.previousDepotPaths)
3283 self.changeRange = "@%s,#head" % p4Change
3284 if not self.silent and not self.detectBranches:
3285 print "Performing incremental import into %s git branch" % self.branch
3287 # accept multiple ref name abbreviations:
3288 # refs/foo/bar/branch -> use it exactly
3289 # p4/branch -> prepend refs/remotes/ or refs/heads/
3290 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3291 if not self.branch.startswith("refs/"):
3292 if self.importIntoRemotes:
3293 prepend = "refs/remotes/"
3295 prepend = "refs/heads/"
3296 if not self.branch.startswith("p4/"):
3298 self.branch = prepend + self.branch
3300 if len(args) == 0 and self.depotPaths:
3302 print "Depot paths: %s" % ' '.join(self.depotPaths)
3304 if self.depotPaths and self.depotPaths != args:
3305 print ("previous import used depot path %s and now %s was specified. "
3306 "This doesn't work!" % (' '.join (self.depotPaths),
3310 self.depotPaths = sorted(args)
3315 # Make sure no revision specifiers are used when --changesfile
3317 bad_changesfile = False
3318 if len(self.changesFile) > 0:
3319 for p in self.depotPaths:
3320 if p.find("@") >= 0 or p.find("#") >= 0:
3321 bad_changesfile = True
3324 die("Option --changesfile is incompatible with revision specifiers")
3327 for p in self.depotPaths:
3328 if p.find("@") != -1:
3329 atIdx = p.index("@")
3330 self.changeRange = p[atIdx:]
3331 if self.changeRange == "@all":
3332 self.changeRange = ""
3333 elif ',' not in self.changeRange:
3334 revision = self.changeRange
3335 self.changeRange = ""
3337 elif p.find("#") != -1:
3338 hashIdx = p.index("#")
3339 revision = p[hashIdx:]
3341 elif self.previousDepotPaths == []:
3342 # pay attention to changesfile, if given, else import
3343 # the entire p4 tree at the head revision
3344 if len(self.changesFile) == 0:
3347 p = re.sub ("\.\.\.$", "", p)
3348 if not p.endswith("/"):
3353 self.depotPaths = newPaths
3355 # --detect-branches may change this for each branch
3356 self.branchPrefixes = self.depotPaths
3358 self.loadUserMapFromCache()
3360 if self.detectLabels:
3363 if self.detectBranches:
3364 ## FIXME - what's a P4 projectName ?
3365 self.projectName = self.guessProjectName()
3368 self.getBranchMappingFromGitBranches()
3370 self.getBranchMapping()
3372 print "p4-git branches: %s" % self.p4BranchesInGit
3373 print "initial parents: %s" % self.initialParents
3374 for b in self.p4BranchesInGit:
3378 b = b[len(self.projectName):]
3379 self.createdBranches.add(b)
3381 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3383 self.importProcess = subprocess.Popen(["git", "fast-import"],
3384 stdin=subprocess.PIPE,
3385 stdout=subprocess.PIPE,
3386 stderr=subprocess.PIPE);
3387 self.gitOutput = self.importProcess.stdout
3388 self.gitStream = self.importProcess.stdin
3389 self.gitError = self.importProcess.stderr
3392 self.importHeadRevision(revision)
3396 if len(self.changesFile) > 0:
3397 output = open(self.changesFile).readlines()
3400 changeSet.add(int(line))
3402 for change in changeSet:
3403 changes.append(change)
3407 # catch "git p4 sync" with no new branches, in a repo that
3408 # does not have any existing p4 branches
3410 if not self.p4BranchesInGit:
3411 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3413 # The default branch is master, unless --branch is used to
3414 # specify something else. Make sure it exists, or complain
3415 # nicely about how to use --branch.
3416 if not self.detectBranches:
3417 if not branch_exists(self.branch):
3418 if branch_arg_given:
3419 die("Error: branch %s does not exist." % self.branch)
3421 die("Error: no branch %s; perhaps specify one with --branch." %
3425 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3427 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3429 if len(self.maxChanges) > 0:
3430 changes = changes[:min(int(self.maxChanges), len(changes))]
3432 if len(changes) == 0:
3434 print "No changes to import!"
3436 if not self.silent and not self.detectBranches:
3437 print "Import destination: %s" % self.branch
3439 self.updatedBranches = set()
3441 if not self.detectBranches:
3443 # start a new branch
3444 self.initialParent = ""
3446 # build on a previous revision
3447 self.initialParent = parseRevision(self.branch)
3449 self.importChanges(changes)
3453 if len(self.updatedBranches) > 0:
3454 sys.stdout.write("Updated branches: ")
3455 for b in self.updatedBranches:
3456 sys.stdout.write("%s " % b)
3457 sys.stdout.write("\n")
3459 if gitConfigBool("git-p4.importLabels"):
3460 self.importLabels = True
3462 if self.importLabels:
3463 p4Labels = getP4Labels(self.depotPaths)
3464 gitTags = getGitTags()
3466 missingP4Labels = p4Labels - gitTags
3467 self.importP4Labels(self.gitStream, missingP4Labels)
3469 self.gitStream.close()
3470 if self.importProcess.wait() != 0:
3471 die("fast-import failed: %s" % self.gitError.read())
3472 self.gitOutput.close()
3473 self.gitError.close()
3475 # Cleanup temporary branches created during import
3476 if self.tempBranches != []:
3477 for branch in self.tempBranches:
3478 read_pipe("git update-ref -d %s" % branch)
3479 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3481 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3482 # a convenient shortcut refname "p4".
3483 if self.importIntoRemotes:
3484 head_ref = self.refPrefix + "HEAD"
3485 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3486 system(["git", "symbolic-ref", head_ref, self.branch])
3490 class P4Rebase(Command):
3492 Command.__init__(self)
3494 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3496 self.importLabels = False
3497 self.description = ("Fetches the latest revision from perforce and "
3498 + "rebases the current work (branch) against it")
3500 def run(self, args):
3502 sync.importLabels = self.importLabels
3505 return self.rebase()
3508 if os.system("git update-index --refresh") != 0:
3509 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.");
3510 if len(read_pipe("git diff-index HEAD --")) > 0:
3511 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3513 [upstream, settings] = findUpstreamBranchPoint()
3514 if len(upstream) == 0:
3515 die("Cannot find upstream branchpoint for rebase")
3517 # the branchpoint may be p4/foo~3, so strip off the parent
3518 upstream = re.sub("~[0-9]+$", "", upstream)
3520 print "Rebasing the current branch onto %s" % upstream
3521 oldHead = read_pipe("git rev-parse HEAD").strip()
3522 system("git rebase %s" % upstream)
3523 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3526 class P4Clone(P4Sync):
3528 P4Sync.__init__(self)
3529 self.description = "Creates a new git repository and imports from Perforce into it"
3530 self.usage = "usage: %prog [options] //depot/path[@revRange]"
3532 optparse.make_option("--destination", dest="cloneDestination",
3533 action='store', default=None,
3534 help="where to leave result of the clone"),
3535 optparse.make_option("--bare", dest="cloneBare",
3536 action="store_true", default=False),
3538 self.cloneDestination = None
3539 self.needsGit = False
3540 self.cloneBare = False
3542 def defaultDestination(self, args):
3543 ## TODO: use common prefix of args?
3545 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3546 depotDir = re.sub("(#[^#]*)$", "", depotDir)
3547 depotDir = re.sub(r"\.\.\.$", "", depotDir)
3548 depotDir = re.sub(r"/$", "", depotDir)
3549 return os.path.split(depotDir)[1]
3551 def run(self, args):
3555 if self.keepRepoPath and not self.cloneDestination:
3556 sys.stderr.write("Must specify destination for --keep-path\n")
3561 if not self.cloneDestination and len(depotPaths) > 1:
3562 self.cloneDestination = depotPaths[-1]
3563 depotPaths = depotPaths[:-1]
3565 self.cloneExclude = ["/"+p for p in self.cloneExclude]
3566 for p in depotPaths:
3567 if not p.startswith("//"):
3568 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3571 if not self.cloneDestination:
3572 self.cloneDestination = self.defaultDestination(args)
3574 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3576 if not os.path.exists(self.cloneDestination):
3577 os.makedirs(self.cloneDestination)
3578 chdir(self.cloneDestination)
3580 init_cmd = [ "git", "init" ]
3582 init_cmd.append("--bare")
3583 retcode = subprocess.call(init_cmd)
3585 raise CalledProcessError(retcode, init_cmd)
3587 if not P4Sync.run(self, depotPaths):
3590 # create a master branch and check out a work tree
3591 if gitBranchExists(self.branch):
3592 system([ "git", "branch", "master", self.branch ])
3593 if not self.cloneBare:
3594 system([ "git", "checkout", "-f" ])
3596 print 'Not checking out any branch, use ' \
3597 '"git checkout -q -b master <branch>"'
3599 # auto-set this variable if invoked with --use-client-spec
3600 if self.useClientSpec_from_options:
3601 system("git config --bool git-p4.useclientspec true")
3605 class P4Branches(Command):
3607 Command.__init__(self)
3609 self.description = ("Shows the git branches that hold imports and their "
3610 + "corresponding perforce depot paths")
3611 self.verbose = False
3613 def run(self, args):
3614 if originP4BranchesExist():
3615 createOrUpdateBranchesFromOrigin()
3617 cmdline = "git rev-parse --symbolic "
3618 cmdline += " --remotes"
3620 for line in read_pipe_lines(cmdline):
3623 if not line.startswith('p4/') or line == "p4/HEAD":
3627 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3628 settings = extractSettingsGitLog(log)
3630 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3633 class HelpFormatter(optparse.IndentedHelpFormatter):
3635 optparse.IndentedHelpFormatter.__init__(self)
3637 def format_description(self, description):
3639 return description + "\n"
3643 def printUsage(commands):
3644 print "usage: %s <command> [options]" % sys.argv[0]
3646 print "valid commands: %s" % ", ".join(commands)
3648 print "Try %s <command> --help for command specific help." % sys.argv[0]
3653 "submit" : P4Submit,
3654 "commit" : P4Submit,
3656 "rebase" : P4Rebase,
3658 "rollback" : P4RollBack,
3659 "branches" : P4Branches
3664 if len(sys.argv[1:]) == 0:
3665 printUsage(commands.keys())
3668 cmdName = sys.argv[1]
3670 klass = commands[cmdName]
3673 print "unknown command %s" % cmdName
3675 printUsage(commands.keys())
3678 options = cmd.options
3679 cmd.gitdir = os.environ.get("GIT_DIR", None)
3683 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3685 options.append(optparse.make_option("--git-dir", dest="gitdir"))
3687 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3689 description = cmd.description,
3690 formatter = HelpFormatter())
3692 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3694 verbose = cmd.verbose
3696 if cmd.gitdir == None:
3697 cmd.gitdir = os.path.abspath(".git")
3698 if not isValidGitDir(cmd.gitdir):
3699 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3700 if os.path.exists(cmd.gitdir):
3701 cdup = read_pipe("git rev-parse --show-cdup").strip()
3705 if not isValidGitDir(cmd.gitdir):
3706 if isValidGitDir(cmd.gitdir + "/.git"):
3707 cmd.gitdir += "/.git"
3709 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3711 os.environ["GIT_DIR"] = cmd.gitdir
3713 if not cmd.run(args):
3718 if __name__ == '__main__':