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 p4_access_checked = False
55 def p4_build_cmd(cmd):
56 """Build a suitable p4 command line.
58 This consolidates building and returning a p4 command line into one
59 location. It means that hooking into the environment, or other configuration
60 can be done more easily.
64 user = gitConfig("git-p4.user")
66 real_cmd += ["-u",user]
68 password = gitConfig("git-p4.password")
70 real_cmd += ["-P", password]
72 port = gitConfig("git-p4.port")
74 real_cmd += ["-p", port]
76 host = gitConfig("git-p4.host")
78 real_cmd += ["-H", host]
80 client = gitConfig("git-p4.client")
82 real_cmd += ["-c", client]
84 retries = gitConfigInt("git-p4.retries")
86 # Perform 3 retries by default
89 # Provide a way to not pass this option by setting git-p4.retries to 0
90 real_cmd += ["-r", str(retries)]
92 if isinstance(cmd,basestring):
93 real_cmd = ' '.join(real_cmd) + ' ' + cmd
97 # now check that we can actually talk to the server
98 global p4_access_checked
99 if not p4_access_checked:
100 p4_access_checked = True # suppress access checks in p4_check_access itself
106 """ Return TRUE if the given path is a git directory (/path/to/dir/.git).
107 This won't automatically add ".git" to a directory.
109 d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
110 if not d or len(d) == 0:
115 def chdir(path, is_client_path=False):
116 """Do chdir to the given path, and set the PWD environment
117 variable for use by P4. It does not look at getcwd() output.
118 Since we're not using the shell, it is necessary to set the
119 PWD environment variable explicitly.
121 Normally, expand the path to force it to be absolute. This
122 addresses the use of relative path names inside P4 settings,
123 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
124 as given; it looks for .p4config using PWD.
126 If is_client_path, the path was handed to us directly by p4,
127 and may be a symbolic link. Do not call os.getcwd() in this
128 case, because it will cause p4 to think that PWD is not inside
133 if not is_client_path:
135 os.environ['PWD'] = path
138 """Return free space in bytes on the disk of the given dirname."""
139 if platform.system() == 'Windows':
140 free_bytes = ctypes.c_ulonglong(0)
141 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
142 return free_bytes.value
144 st = os.statvfs(os.getcwd())
145 return st.f_bavail * st.f_frsize
151 sys.stderr.write(msg + "\n")
154 def write_pipe(c, stdin):
156 sys.stderr.write('Writing pipe: %s\n' % str(c))
158 expand = isinstance(c,basestring)
159 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
161 val = pipe.write(stdin)
164 die('Command failed: %s' % str(c))
168 def p4_write_pipe(c, stdin):
169 real_cmd = p4_build_cmd(c)
170 return write_pipe(real_cmd, stdin)
172 def read_pipe_full(c):
173 """ Read output from command. Returns a tuple
174 of the return status, stdout text and stderr
178 sys.stderr.write('Reading pipe: %s\n' % str(c))
180 expand = isinstance(c,basestring)
181 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
182 (out, err) = p.communicate()
183 return (p.returncode, out, err)
185 def read_pipe(c, ignore_error=False):
186 """ Read output from command. Returns the output text on
187 success. On failure, terminates execution, unless
188 ignore_error is True, when it returns an empty string.
190 (retcode, out, err) = read_pipe_full(c)
195 die('Command failed: %s\nError: %s' % (str(c), err))
198 def read_pipe_text(c):
199 """ Read output from a command with trailing whitespace stripped.
200 On error, returns None.
202 (retcode, out, err) = read_pipe_full(c)
208 def p4_read_pipe(c, ignore_error=False):
209 real_cmd = p4_build_cmd(c)
210 return read_pipe(real_cmd, ignore_error)
212 def read_pipe_lines(c):
214 sys.stderr.write('Reading pipe: %s\n' % str(c))
216 expand = isinstance(c, basestring)
217 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
219 val = pipe.readlines()
220 if pipe.close() or p.wait():
221 die('Command failed: %s' % str(c))
225 def p4_read_pipe_lines(c):
226 """Specifically invoke p4 on the command supplied. """
227 real_cmd = p4_build_cmd(c)
228 return read_pipe_lines(real_cmd)
230 def p4_has_command(cmd):
231 """Ask p4 for help on this command. If it returns an error, the
232 command does not exist in this version of p4."""
233 real_cmd = p4_build_cmd(["help", cmd])
234 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
235 stderr=subprocess.PIPE)
237 return p.returncode == 0
239 def p4_has_move_command():
240 """See if the move command exists, that it supports -k, and that
241 it has not been administratively disabled. The arguments
242 must be correct, but the filenames do not have to exist. Use
243 ones with wildcards so even if they exist, it will fail."""
245 if not p4_has_command("move"):
247 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
248 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
249 (out, err) = p.communicate()
250 # return code will be 1 in either case
251 if err.find("Invalid option") >= 0:
253 if err.find("disabled") >= 0:
255 # assume it failed because @... was invalid changelist
258 def system(cmd, ignore_error=False):
259 expand = isinstance(cmd,basestring)
261 sys.stderr.write("executing %s\n" % str(cmd))
262 retcode = subprocess.call(cmd, shell=expand)
263 if retcode and not ignore_error:
264 raise CalledProcessError(retcode, cmd)
269 """Specifically invoke p4 as the system command. """
270 real_cmd = p4_build_cmd(cmd)
271 expand = isinstance(real_cmd, basestring)
272 retcode = subprocess.call(real_cmd, shell=expand)
274 raise CalledProcessError(retcode, real_cmd)
276 def die_bad_access(s):
277 die("failure accessing depot: {0}".format(s.rstrip()))
279 def p4_check_access(min_expiration=1):
280 """ Check if we can access Perforce - account still logged in
282 results = p4CmdList(["login", "-s"])
284 if len(results) == 0:
285 # should never get here: always get either some results, or a p4ExitCode
286 assert("could not parse response from perforce")
290 if 'p4ExitCode' in result:
291 # p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
292 die_bad_access("could not run p4")
294 code = result.get("code")
296 # we get here if we couldn't connect and there was nothing to unmarshal
297 die_bad_access("could not connect")
300 expiry = result.get("TicketExpiration")
303 if expiry > min_expiration:
307 die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
310 # account without a timeout - all ok
313 elif code == "error":
314 data = result.get("data")
316 die_bad_access("p4 error: {0}".format(data))
318 die_bad_access("unknown error")
320 die_bad_access("unknown error code {0}".format(code))
322 _p4_version_string = None
323 def p4_version_string():
324 """Read the version string, showing just the last line, which
325 hopefully is the interesting version bit.
328 Perforce - The Fast Software Configuration Management System.
329 Copyright 1995-2011 Perforce Software. All rights reserved.
330 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
332 global _p4_version_string
333 if not _p4_version_string:
334 a = p4_read_pipe_lines(["-V"])
335 _p4_version_string = a[-1].rstrip()
336 return _p4_version_string
338 def p4_integrate(src, dest):
339 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
341 def p4_sync(f, *options):
342 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
345 # forcibly add file names with wildcards
346 if wildcard_present(f):
347 p4_system(["add", "-f", f])
349 p4_system(["add", f])
352 p4_system(["delete", wildcard_encode(f)])
354 def p4_edit(f, *options):
355 p4_system(["edit"] + list(options) + [wildcard_encode(f)])
358 p4_system(["revert", wildcard_encode(f)])
360 def p4_reopen(type, f):
361 p4_system(["reopen", "-t", type, wildcard_encode(f)])
363 def p4_reopen_in_change(changelist, files):
364 cmd = ["reopen", "-c", str(changelist)] + files
367 def p4_move(src, dest):
368 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
370 def p4_last_change():
371 results = p4CmdList(["changes", "-m", "1"], skip_info=True)
372 return int(results[0]['change'])
374 def p4_describe(change):
375 """Make sure it returns a valid result by checking for
376 the presence of field "time". Return a dict of the
379 ds = p4CmdList(["describe", "-s", str(change)], skip_info=True)
381 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
385 if "p4ExitCode" in d:
386 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
389 if d["code"] == "error":
390 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
393 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
398 # Canonicalize the p4 type and return a tuple of the
399 # base type, plus any modifiers. See "p4 help filetypes"
400 # for a list and explanation.
402 def split_p4_type(p4type):
404 p4_filetypes_historical = {
405 "ctempobj": "binary+Sw",
411 "tempobj": "binary+FSw",
412 "ubinary": "binary+F",
413 "uresource": "resource+F",
414 "uxbinary": "binary+Fx",
415 "xbinary": "binary+x",
417 "xtempobj": "binary+Swx",
419 "xunicode": "unicode+x",
422 if p4type in p4_filetypes_historical:
423 p4type = p4_filetypes_historical[p4type]
425 s = p4type.split("+")
433 # return the raw p4 type of a file (text, text+ko, etc)
436 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
437 return results[0]['headType']
440 # Given a type base and modifier, return a regexp matching
441 # the keywords that can be expanded in the file
443 def p4_keywords_regexp_for_type(base, type_mods):
444 if base in ("text", "unicode", "binary"):
446 if "ko" in type_mods:
448 elif "k" in type_mods:
449 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
453 \$ # Starts with a dollar, followed by...
454 (%s) # one of the keywords, followed by...
455 (:[^$\n]+)? # possibly an old expansion, followed by...
463 # Given a file, return a regexp matching the possible
464 # RCS keywords that will be expanded, or None for files
465 # with kw expansion turned off.
467 def p4_keywords_regexp_for_file(file):
468 if not os.path.exists(file):
471 (type_base, type_mods) = split_p4_type(p4_type(file))
472 return p4_keywords_regexp_for_type(type_base, type_mods)
474 def setP4ExecBit(file, mode):
475 # Reopens an already open file and changes the execute bit to match
476 # the execute bit setting in the passed in mode.
480 if not isModeExec(mode):
481 p4Type = getP4OpenedType(file)
482 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
483 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
484 if p4Type[-1] == "+":
485 p4Type = p4Type[0:-1]
487 p4_reopen(p4Type, file)
489 def getP4OpenedType(file):
490 # Returns the perforce file type for the given file.
492 result = p4_read_pipe(["opened", wildcard_encode(file)])
493 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
495 return match.group(1)
497 die("Could not determine file type for %s (result: '%s')" % (file, result))
499 # Return the set of all p4 labels
500 def getP4Labels(depotPaths):
502 if isinstance(depotPaths,basestring):
503 depotPaths = [depotPaths]
505 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
511 # Return the set of all git tags
514 for line in read_pipe_lines(["git", "tag"]):
519 def diffTreePattern():
520 # This is a simple generator for the diff tree regex pattern. This could be
521 # a class variable if this and parseDiffTreeEntry were a part of a class.
522 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
526 def parseDiffTreeEntry(entry):
527 """Parses a single diff tree entry into its component elements.
529 See git-diff-tree(1) manpage for details about the format of the diff
530 output. This method returns a dictionary with the following elements:
532 src_mode - The mode of the source file
533 dst_mode - The mode of the destination file
534 src_sha1 - The sha1 for the source file
535 dst_sha1 - The sha1 fr the destination file
536 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
537 status_score - The score for the status (applicable for 'C' and 'R'
538 statuses). This is None if there is no score.
539 src - The path for the source file.
540 dst - The path for the destination file. This is only present for
541 copy or renames. If it is not present, this is None.
543 If the pattern is not matched, None is returned."""
545 match = diffTreePattern().next().match(entry)
548 'src_mode': match.group(1),
549 'dst_mode': match.group(2),
550 'src_sha1': match.group(3),
551 'dst_sha1': match.group(4),
552 'status': match.group(5),
553 'status_score': match.group(6),
554 'src': match.group(7),
555 'dst': match.group(10)
559 def isModeExec(mode):
560 # Returns True if the given git mode represents an executable file,
562 return mode[-3:] == "755"
564 def isModeExecChanged(src_mode, dst_mode):
565 return isModeExec(src_mode) != isModeExec(dst_mode)
567 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False):
569 if isinstance(cmd,basestring):
576 cmd = p4_build_cmd(cmd)
578 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
580 # Use a temporary file to avoid deadlocks without
581 # subprocess.communicate(), which would put another copy
582 # of stdout into memory.
584 if stdin is not None:
585 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
586 if isinstance(stdin,basestring):
587 stdin_file.write(stdin)
590 stdin_file.write(i + '\n')
594 p4 = subprocess.Popen(cmd,
597 stdout=subprocess.PIPE)
602 entry = marshal.load(p4.stdout)
604 if 'code' in entry and entry['code'] == 'info':
615 entry["p4ExitCode"] = exitCode
621 list = p4CmdList(cmd)
627 def p4Where(depotPath):
628 if not depotPath.endswith("/"):
630 depotPathLong = depotPath + "..."
631 outputList = p4CmdList(["where", depotPathLong])
633 for entry in outputList:
634 if "depotFile" in entry:
635 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
636 # The base path always ends with "/...".
637 if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
640 elif "data" in entry:
641 data = entry.get("data")
642 space = data.find(" ")
643 if data[:space] == depotPath:
648 if output["code"] == "error":
652 clientPath = output.get("path")
653 elif "data" in output:
654 data = output.get("data")
655 lastSpace = data.rfind(" ")
656 clientPath = data[lastSpace + 1:]
658 if clientPath.endswith("..."):
659 clientPath = clientPath[:-3]
662 def currentGitBranch():
663 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
665 def isValidGitDir(path):
666 return git_dir(path) != None
668 def parseRevision(ref):
669 return read_pipe("git rev-parse %s" % ref).strip()
671 def branchExists(ref):
672 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
676 def extractLogMessageFromGitCommit(commit):
679 ## fixme: title is first line of commit, not 1st paragraph.
681 for log in read_pipe_lines("git cat-file commit %s" % commit):
690 def extractSettingsGitLog(log):
692 for line in log.split("\n"):
694 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
698 assignments = m.group(1).split (':')
699 for a in assignments:
701 key = vals[0].strip()
702 val = ('='.join (vals[1:])).strip()
703 if val.endswith ('\"') and val.startswith('"'):
708 paths = values.get("depot-paths")
710 paths = values.get("depot-path")
712 values['depot-paths'] = paths.split(',')
715 def gitBranchExists(branch):
716 proc = subprocess.Popen(["git", "rev-parse", branch],
717 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
718 return proc.wait() == 0;
722 def gitConfig(key, typeSpecifier=None):
723 if not _gitConfig.has_key(key):
724 cmd = [ "git", "config" ]
726 cmd += [ typeSpecifier ]
728 s = read_pipe(cmd, ignore_error=True)
729 _gitConfig[key] = s.strip()
730 return _gitConfig[key]
732 def gitConfigBool(key):
733 """Return a bool, using git config --bool. It is True only if the
734 variable is set to true, and False if set to false or not present
737 if not _gitConfig.has_key(key):
738 _gitConfig[key] = gitConfig(key, '--bool') == "true"
739 return _gitConfig[key]
741 def gitConfigInt(key):
742 if not _gitConfig.has_key(key):
743 cmd = [ "git", "config", "--int", key ]
744 s = read_pipe(cmd, ignore_error=True)
747 _gitConfig[key] = int(gitConfig(key, '--int'))
749 _gitConfig[key] = None
750 return _gitConfig[key]
752 def gitConfigList(key):
753 if not _gitConfig.has_key(key):
754 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
755 _gitConfig[key] = s.strip().splitlines()
756 if _gitConfig[key] == ['']:
758 return _gitConfig[key]
760 def p4BranchesInGit(branchesAreInRemotes=True):
761 """Find all the branches whose names start with "p4/", looking
762 in remotes or heads as specified by the argument. Return
763 a dictionary of { branch: revision } for each one found.
764 The branch names are the short names, without any
769 cmdline = "git rev-parse --symbolic "
770 if branchesAreInRemotes:
771 cmdline += "--remotes"
773 cmdline += "--branches"
775 for line in read_pipe_lines(cmdline):
779 if not line.startswith('p4/'):
781 # special symbolic ref to p4/master
782 if line == "p4/HEAD":
785 # strip off p4/ prefix
786 branch = line[len("p4/"):]
788 branches[branch] = parseRevision(line)
792 def branch_exists(branch):
793 """Make sure that the given ref name really exists."""
795 cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
796 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
797 out, _ = p.communicate()
800 # expect exactly one line of output: the branch name
801 return out.rstrip() == branch
803 def findUpstreamBranchPoint(head = "HEAD"):
804 branches = p4BranchesInGit()
805 # map from depot-path to branch name
806 branchByDepotPath = {}
807 for branch in branches.keys():
808 tip = branches[branch]
809 log = extractLogMessageFromGitCommit(tip)
810 settings = extractSettingsGitLog(log)
811 if settings.has_key("depot-paths"):
812 paths = ",".join(settings["depot-paths"])
813 branchByDepotPath[paths] = "remotes/p4/" + branch
817 while parent < 65535:
818 commit = head + "~%s" % parent
819 log = extractLogMessageFromGitCommit(commit)
820 settings = extractSettingsGitLog(log)
821 if settings.has_key("depot-paths"):
822 paths = ",".join(settings["depot-paths"])
823 if branchByDepotPath.has_key(paths):
824 return [branchByDepotPath[paths], settings]
828 return ["", settings]
830 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
832 print ("Creating/updating branch(es) in %s based on origin branch(es)"
835 originPrefix = "origin/p4/"
837 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
839 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
842 headName = line[len(originPrefix):]
843 remoteHead = localRefPrefix + headName
846 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
847 if (not original.has_key('depot-paths')
848 or not original.has_key('change')):
852 if not gitBranchExists(remoteHead):
854 print "creating %s" % remoteHead
857 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
858 if settings.has_key('change') > 0:
859 if settings['depot-paths'] == original['depot-paths']:
860 originP4Change = int(original['change'])
861 p4Change = int(settings['change'])
862 if originP4Change > p4Change:
863 print ("%s (%s) is newer than %s (%s). "
864 "Updating p4 branch from origin."
865 % (originHead, originP4Change,
866 remoteHead, p4Change))
869 print ("Ignoring: %s was imported from %s while "
870 "%s was imported from %s"
871 % (originHead, ','.join(original['depot-paths']),
872 remoteHead, ','.join(settings['depot-paths'])))
875 system("git update-ref %s %s" % (remoteHead, originHead))
877 def originP4BranchesExist():
878 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
881 def p4ParseNumericChangeRange(parts):
882 changeStart = int(parts[0][1:])
883 if parts[1] == '#head':
884 changeEnd = p4_last_change()
886 changeEnd = int(parts[1])
888 return (changeStart, changeEnd)
890 def chooseBlockSize(blockSize):
894 return defaultBlockSize
896 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
899 # Parse the change range into start and end. Try to find integer
900 # revision ranges as these can be broken up into blocks to avoid
901 # hitting server-side limits (maxrows, maxscanresults). But if
902 # that doesn't work, fall back to using the raw revision specifier
903 # strings, without using block mode.
905 if changeRange is None or changeRange == '':
907 changeEnd = p4_last_change()
908 block_size = chooseBlockSize(requestedBlockSize)
910 parts = changeRange.split(',')
911 assert len(parts) == 2
913 (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
914 block_size = chooseBlockSize(requestedBlockSize)
916 changeStart = parts[0][1:]
918 if requestedBlockSize:
919 die("cannot use --changes-block-size with non-numeric revisions")
924 # Retrieve changes a block at a time, to prevent running
925 # into a MaxResults/MaxScanRows error from the server.
931 end = min(changeEnd, changeStart + block_size)
932 revisionRange = "%d,%d" % (changeStart, end)
934 revisionRange = "%s,%s" % (changeStart, changeEnd)
937 cmd += ["%s...@%s" % (p, revisionRange)]
939 # Insert changes in chronological order
940 for entry in reversed(p4CmdList(cmd)):
941 if entry.has_key('p4ExitCode'):
942 die('Error retrieving changes descriptions ({})'.format(entry['p4ExitCode']))
943 if not entry.has_key('change'):
945 changes.add(int(entry['change']))
953 changeStart = end + 1
955 changes = sorted(changes)
958 def p4PathStartsWith(path, prefix):
959 # This method tries to remedy a potential mixed-case issue:
961 # If UserA adds //depot/DirA/file1
962 # and UserB adds //depot/dira/file2
964 # we may or may not have a problem. If you have core.ignorecase=true,
965 # we treat DirA and dira as the same directory
966 if gitConfigBool("core.ignorecase"):
967 return path.lower().startswith(prefix.lower())
968 return path.startswith(prefix)
971 """Look at the p4 client spec, create a View() object that contains
972 all the mappings, and return it."""
974 specList = p4CmdList("client -o")
975 if len(specList) != 1:
976 die('Output from "client -o" is %d lines, expecting 1' %
979 # dictionary of all client parameters
983 client_name = entry["Client"]
985 # just the keys that start with "View"
986 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
989 view = View(client_name)
991 # append the lines, in order, to the view
992 for view_num in range(len(view_keys)):
993 k = "View%d" % view_num
994 if k not in view_keys:
995 die("Expected view key %s missing" % k)
996 view.append(entry[k])
1000 def getClientRoot():
1001 """Grab the client directory."""
1003 output = p4CmdList("client -o")
1004 if len(output) != 1:
1005 die('Output from "client -o" is %d lines, expecting 1' % len(output))
1008 if "Root" not in entry:
1009 die('Client has no "Root"')
1011 return entry["Root"]
1014 # P4 wildcards are not allowed in filenames. P4 complains
1015 # if you simply add them, but you can force it with "-f", in
1016 # which case it translates them into %xx encoding internally.
1018 def wildcard_decode(path):
1019 # Search for and fix just these four characters. Do % last so
1020 # that fixing it does not inadvertently create new %-escapes.
1021 # Cannot have * in a filename in windows; untested as to
1022 # what p4 would do in such a case.
1023 if not platform.system() == "Windows":
1024 path = path.replace("%2A", "*")
1025 path = path.replace("%23", "#") \
1026 .replace("%40", "@") \
1027 .replace("%25", "%")
1030 def wildcard_encode(path):
1031 # do % first to avoid double-encoding the %s introduced here
1032 path = path.replace("%", "%25") \
1033 .replace("*", "%2A") \
1034 .replace("#", "%23") \
1035 .replace("@", "%40")
1038 def wildcard_present(path):
1039 m = re.search("[*#@%]", path)
1040 return m is not None
1042 class LargeFileSystem(object):
1043 """Base class for large file system support."""
1045 def __init__(self, writeToGitStream):
1046 self.largeFiles = set()
1047 self.writeToGitStream = writeToGitStream
1049 def generatePointer(self, cloneDestination, contentFile):
1050 """Return the content of a pointer file that is stored in Git instead of
1051 the actual content."""
1052 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1054 def pushFile(self, localLargeFile):
1055 """Push the actual content which is not stored in the Git repository to
1057 assert False, "Method 'pushFile' required in " + self.__class__.__name__
1059 def hasLargeFileExtension(self, relPath):
1061 lambda a, b: a or b,
1062 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1066 def generateTempFile(self, contents):
1067 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1069 contentFile.write(d)
1071 return contentFile.name
1073 def exceedsLargeFileThreshold(self, relPath, contents):
1074 if gitConfigInt('git-p4.largeFileThreshold'):
1075 contentsSize = sum(len(d) for d in contents)
1076 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1078 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1079 contentsSize = sum(len(d) for d in contents)
1080 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1082 contentTempFile = self.generateTempFile(contents)
1083 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1084 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')
1085 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1087 compressedContentsSize = zf.infolist()[0].compress_size
1088 os.remove(contentTempFile)
1089 os.remove(compressedContentFile.name)
1090 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1094 def addLargeFile(self, relPath):
1095 self.largeFiles.add(relPath)
1097 def removeLargeFile(self, relPath):
1098 self.largeFiles.remove(relPath)
1100 def isLargeFile(self, relPath):
1101 return relPath in self.largeFiles
1103 def processContent(self, git_mode, relPath, contents):
1104 """Processes the content of git fast import. This method decides if a
1105 file is stored in the large file system and handles all necessary
1107 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1108 contentTempFile = self.generateTempFile(contents)
1109 (pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
1110 if pointer_git_mode:
1111 git_mode = pointer_git_mode
1113 # Move temp file to final location in large file system
1114 largeFileDir = os.path.dirname(localLargeFile)
1115 if not os.path.isdir(largeFileDir):
1116 os.makedirs(largeFileDir)
1117 shutil.move(contentTempFile, localLargeFile)
1118 self.addLargeFile(relPath)
1119 if gitConfigBool('git-p4.largeFilePush'):
1120 self.pushFile(localLargeFile)
1122 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1123 return (git_mode, contents)
1125 class MockLFS(LargeFileSystem):
1126 """Mock large file system for testing."""
1128 def generatePointer(self, contentFile):
1129 """The pointer content is the original content prefixed with "pointer-".
1130 The local filename of the large file storage is derived from the file content.
1132 with open(contentFile, 'r') as f:
1135 pointerContents = 'pointer-' + content
1136 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1137 return (gitMode, pointerContents, localLargeFile)
1139 def pushFile(self, localLargeFile):
1140 """The remote filename of the large file storage is the same as the local
1141 one but in a different directory.
1143 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1144 if not os.path.exists(remotePath):
1145 os.makedirs(remotePath)
1146 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1148 class GitLFS(LargeFileSystem):
1149 """Git LFS as backend for the git-p4 large file system.
1150 See https://git-lfs.github.com/ for details."""
1152 def __init__(self, *args):
1153 LargeFileSystem.__init__(self, *args)
1154 self.baseGitAttributes = []
1156 def generatePointer(self, contentFile):
1157 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1158 mode and content which is stored in the Git repository instead of
1159 the actual content. Return also the new location of the actual
1162 if os.path.getsize(contentFile) == 0:
1163 return (None, '', None)
1165 pointerProcess = subprocess.Popen(
1166 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1167 stdout=subprocess.PIPE
1169 pointerFile = pointerProcess.stdout.read()
1170 if pointerProcess.wait():
1171 os.remove(contentFile)
1172 die('git-lfs pointer command failed. Did you install the extension?')
1174 # Git LFS removed the preamble in the output of the 'pointer' command
1175 # starting from version 1.2.0. Check for the preamble here to support
1177 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1178 if pointerFile.startswith('Git LFS pointer for'):
1179 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1181 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
1182 localLargeFile = os.path.join(
1184 '.git', 'lfs', 'objects', oid[:2], oid[2:4],
1187 # LFS Spec states that pointer files should not have the executable bit set.
1189 return (gitMode, pointerFile, localLargeFile)
1191 def pushFile(self, localLargeFile):
1192 uploadProcess = subprocess.Popen(
1193 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1195 if uploadProcess.wait():
1196 die('git-lfs push command failed. Did you define a remote?')
1198 def generateGitAttributes(self):
1200 self.baseGitAttributes +
1204 '# Git LFS (see https://git-lfs.github.com/)\n',
1207 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1208 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1210 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1211 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1215 def addLargeFile(self, relPath):
1216 LargeFileSystem.addLargeFile(self, relPath)
1217 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1219 def removeLargeFile(self, relPath):
1220 LargeFileSystem.removeLargeFile(self, relPath)
1221 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1223 def processContent(self, git_mode, relPath, contents):
1224 if relPath == '.gitattributes':
1225 self.baseGitAttributes = contents
1226 return (git_mode, self.generateGitAttributes())
1228 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1232 self.usage = "usage: %prog [options]"
1233 self.needsGit = True
1234 self.verbose = False
1236 # This is required for the "append" cloneExclude action
1237 def ensure_value(self, attr, value):
1238 if not hasattr(self, attr) or getattr(self, attr) is None:
1239 setattr(self, attr, value)
1240 return getattr(self, attr)
1244 self.userMapFromPerforceServer = False
1245 self.myP4UserId = None
1249 return self.myP4UserId
1251 results = p4CmdList("user -o")
1253 if r.has_key('User'):
1254 self.myP4UserId = r['User']
1256 die("Could not find your p4 user id")
1258 def p4UserIsMe(self, p4User):
1259 # return True if the given p4 user is actually me
1260 me = self.p4UserId()
1261 if not p4User or p4User != me:
1266 def getUserCacheFilename(self):
1267 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1268 return home + "/.gitp4-usercache.txt"
1270 def getUserMapFromPerforceServer(self):
1271 if self.userMapFromPerforceServer:
1276 for output in p4CmdList("users"):
1277 if not output.has_key("User"):
1279 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1280 self.emails[output["Email"]] = output["User"]
1282 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1283 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1284 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1285 if mapUser and len(mapUser[0]) == 3:
1286 user = mapUser[0][0]
1287 fullname = mapUser[0][1]
1288 email = mapUser[0][2]
1289 self.users[user] = fullname + " <" + email + ">"
1290 self.emails[email] = user
1293 for (key, val) in self.users.items():
1294 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1296 open(self.getUserCacheFilename(), "wb").write(s)
1297 self.userMapFromPerforceServer = True
1299 def loadUserMapFromCache(self):
1301 self.userMapFromPerforceServer = False
1303 cache = open(self.getUserCacheFilename(), "rb")
1304 lines = cache.readlines()
1307 entry = line.strip().split("\t")
1308 self.users[entry[0]] = entry[1]
1310 self.getUserMapFromPerforceServer()
1312 class P4Debug(Command):
1314 Command.__init__(self)
1316 self.description = "A tool to debug the output of p4 -G."
1317 self.needsGit = False
1319 def run(self, args):
1321 for output in p4CmdList(args):
1322 print 'Element: %d' % j
1327 class P4RollBack(Command):
1329 Command.__init__(self)
1331 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1333 self.description = "A tool to debug the multi-branch import. Don't use :)"
1334 self.rollbackLocalBranches = False
1336 def run(self, args):
1339 maxChange = int(args[0])
1341 if "p4ExitCode" in p4Cmd("changes -m 1"):
1342 die("Problems executing p4");
1344 if self.rollbackLocalBranches:
1345 refPrefix = "refs/heads/"
1346 lines = read_pipe_lines("git rev-parse --symbolic --branches")
1348 refPrefix = "refs/remotes/"
1349 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1352 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1354 ref = refPrefix + line
1355 log = extractLogMessageFromGitCommit(ref)
1356 settings = extractSettingsGitLog(log)
1358 depotPaths = settings['depot-paths']
1359 change = settings['change']
1363 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
1364 for p in depotPaths]))) == 0:
1365 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1366 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1369 while change and int(change) > maxChange:
1372 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
1373 system("git update-ref %s \"%s^\"" % (ref, ref))
1374 log = extractLogMessageFromGitCommit(ref)
1375 settings = extractSettingsGitLog(log)
1378 depotPaths = settings['depot-paths']
1379 change = settings['change']
1382 print "%s rewound to %s" % (ref, change)
1386 class P4Submit(Command, P4UserMap):
1388 conflict_behavior_choices = ("ask", "skip", "quit")
1391 Command.__init__(self)
1392 P4UserMap.__init__(self)
1394 optparse.make_option("--origin", dest="origin"),
1395 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1396 # preserve the user, requires relevant p4 permissions
1397 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1398 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1399 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1400 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1401 optparse.make_option("--conflict", dest="conflict_behavior",
1402 choices=self.conflict_behavior_choices),
1403 optparse.make_option("--branch", dest="branch"),
1404 optparse.make_option("--shelve", dest="shelve", action="store_true",
1405 help="Shelve instead of submit. Shelved files are reverted, "
1406 "restoring the workspace to the state before the shelve"),
1407 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
1408 metavar="CHANGELIST",
1409 help="update an existing shelved changelist, implies --shelve, "
1410 "repeat in-order for multiple shelved changelists"),
1411 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1412 help="submit only the specified commit(s), one commit or xxx..xxx"),
1413 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1414 help="Disable rebase after submit is completed. Can be useful if you "
1415 "work from a local git branch that is not master"),
1416 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1417 help="Skip Perforce sync of p4/master after submit or shelve"),
1419 self.description = "Submit changes from git to the perforce depot."
1420 self.usage += " [name of git branch to submit into perforce depot]"
1422 self.detectRenames = False
1423 self.preserveUser = gitConfigBool("git-p4.preserveUser")
1424 self.dry_run = False
1426 self.update_shelve = list()
1428 self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1429 self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
1430 self.prepare_p4_only = False
1431 self.conflict_behavior = None
1432 self.isWindows = (platform.system() == "Windows")
1433 self.exportLabels = False
1434 self.p4HasMoveCommand = p4_has_move_command()
1437 if gitConfig('git-p4.largeFileSystem'):
1438 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1441 if len(p4CmdList("opened ...")) > 0:
1442 die("You have files opened with perforce! Close them before starting the sync.")
1444 def separate_jobs_from_description(self, message):
1445 """Extract and return a possible Jobs field in the commit
1446 message. It goes into a separate section in the p4 change
1449 A jobs line starts with "Jobs:" and looks like a new field
1450 in a form. Values are white-space separated on the same
1451 line or on following lines that start with a tab.
1453 This does not parse and extract the full git commit message
1454 like a p4 form. It just sees the Jobs: line as a marker
1455 to pass everything from then on directly into the p4 form,
1456 but outside the description section.
1458 Return a tuple (stripped log message, jobs string)."""
1460 m = re.search(r'^Jobs:', message, re.MULTILINE)
1462 return (message, None)
1464 jobtext = message[m.start():]
1465 stripped_message = message[:m.start()].rstrip()
1466 return (stripped_message, jobtext)
1468 def prepareLogMessage(self, template, message, jobs):
1469 """Edits the template returned from "p4 change -o" to insert
1470 the message in the Description field, and the jobs text in
1474 inDescriptionSection = False
1476 for line in template.split("\n"):
1477 if line.startswith("#"):
1478 result += line + "\n"
1481 if inDescriptionSection:
1482 if line.startswith("Files:") or line.startswith("Jobs:"):
1483 inDescriptionSection = False
1484 # insert Jobs section
1486 result += jobs + "\n"
1490 if line.startswith("Description:"):
1491 inDescriptionSection = True
1493 for messageLine in message.split("\n"):
1494 line += "\t" + messageLine + "\n"
1496 result += line + "\n"
1500 def patchRCSKeywords(self, file, pattern):
1501 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1502 (handle, outFileName) = tempfile.mkstemp(dir='.')
1504 outFile = os.fdopen(handle, "w+")
1505 inFile = open(file, "r")
1506 regexp = re.compile(pattern, re.VERBOSE)
1507 for line in inFile.readlines():
1508 line = regexp.sub(r'$\1$', line)
1512 # Forcibly overwrite the original file
1514 shutil.move(outFileName, file)
1516 # cleanup our temporary file
1517 os.unlink(outFileName)
1518 print "Failed to strip RCS keywords in %s" % file
1521 print "Patched up RCS keywords in %s" % file
1523 def p4UserForCommit(self,id):
1524 # Return the tuple (perforce user,git email) for a given git commit id
1525 self.getUserMapFromPerforceServer()
1526 gitEmail = read_pipe(["git", "log", "--max-count=1",
1527 "--format=%ae", id])
1528 gitEmail = gitEmail.strip()
1529 if not self.emails.has_key(gitEmail):
1530 return (None,gitEmail)
1532 return (self.emails[gitEmail],gitEmail)
1534 def checkValidP4Users(self,commits):
1535 # check if any git authors cannot be mapped to p4 users
1537 (user,email) = self.p4UserForCommit(id)
1539 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1540 if gitConfigBool("git-p4.allowMissingP4Users"):
1543 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1545 def lastP4Changelist(self):
1546 # Get back the last changelist number submitted in this client spec. This
1547 # then gets used to patch up the username in the change. If the same
1548 # client spec is being used by multiple processes then this might go
1550 results = p4CmdList("client -o") # find the current client
1553 if r.has_key('Client'):
1554 client = r['Client']
1557 die("could not get client spec")
1558 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1560 if r.has_key('change'):
1562 die("Could not get changelist number for last submit - cannot patch up user details")
1564 def modifyChangelistUser(self, changelist, newUser):
1565 # fixup the user field of a changelist after it has been submitted.
1566 changes = p4CmdList("change -o %s" % changelist)
1567 if len(changes) != 1:
1568 die("Bad output from p4 change modifying %s to user %s" %
1569 (changelist, newUser))
1572 if c['User'] == newUser: return # nothing to do
1574 input = marshal.dumps(c)
1576 result = p4CmdList("change -f -i", stdin=input)
1578 if r.has_key('code'):
1579 if r['code'] == 'error':
1580 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1581 if r.has_key('data'):
1582 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1584 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1586 def canChangeChangelists(self):
1587 # check to see if we have p4 admin or super-user permissions, either of
1588 # which are required to modify changelists.
1589 results = p4CmdList(["protects", self.depotPath])
1591 if r.has_key('perm'):
1592 if r['perm'] == 'admin':
1594 if r['perm'] == 'super':
1598 def prepareSubmitTemplate(self, changelist=None):
1599 """Run "p4 change -o" to grab a change specification template.
1600 This does not use "p4 -G", as it is nice to keep the submission
1601 template in original order, since a human might edit it.
1603 Remove lines in the Files section that show changes to files
1604 outside the depot path we're committing into."""
1606 [upstream, settings] = findUpstreamBranchPoint()
1609 # A Perforce Change Specification.
1611 # Change: The change number. 'new' on a new changelist.
1612 # Date: The date this specification was last modified.
1613 # Client: The client on which the changelist was created. Read-only.
1614 # User: The user who created the changelist.
1615 # Status: Either 'pending' or 'submitted'. Read-only.
1616 # Type: Either 'public' or 'restricted'. Default is 'public'.
1617 # Description: Comments about the changelist. Required.
1618 # Jobs: What opened jobs are to be closed by this changelist.
1619 # You may delete jobs from this list. (New changelists only.)
1620 # Files: What opened files from the default changelist are to be added
1621 # to this changelist. You may delete files from this list.
1622 # (New changelists only.)
1625 inFilesSection = False
1627 args = ['change', '-o']
1629 args.append(str(changelist))
1630 for entry in p4CmdList(args):
1631 if not entry.has_key('code'):
1633 if entry['code'] == 'stat':
1634 change_entry = entry
1636 if not change_entry:
1637 die('Failed to decode output of p4 change -o')
1638 for key, value in change_entry.iteritems():
1639 if key.startswith('File'):
1640 if settings.has_key('depot-paths'):
1641 if not [p for p in settings['depot-paths']
1642 if p4PathStartsWith(value, p)]:
1645 if not p4PathStartsWith(value, self.depotPath):
1647 files_list.append(value)
1649 # Output in the order expected by prepareLogMessage
1650 for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
1651 if not change_entry.has_key(key):
1654 template += key + ':'
1655 if key == 'Description':
1657 for field_line in change_entry[key].splitlines():
1658 template += '\t'+field_line+'\n'
1659 if len(files_list) > 0:
1661 template += 'Files:\n'
1662 for path in files_list:
1663 template += '\t'+path+'\n'
1666 def edit_template(self, template_file):
1667 """Invoke the editor to let the user change the submission
1668 message. Return true if okay to continue with the submit."""
1670 # if configured to skip the editing part, just submit
1671 if gitConfigBool("git-p4.skipSubmitEdit"):
1674 # look at the modification time, to check later if the user saved
1676 mtime = os.stat(template_file).st_mtime
1679 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
1680 editor = os.environ.get("P4EDITOR")
1682 editor = read_pipe("git var GIT_EDITOR").strip()
1683 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1685 # If the file was not saved, prompt to see if this patch should
1686 # be skipped. But skip this verification step if configured so.
1687 if gitConfigBool("git-p4.skipSubmitEditCheck"):
1690 # modification time updated means user saved the file
1691 if os.stat(template_file).st_mtime > mtime:
1695 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1701 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
1703 if os.environ.has_key("P4DIFF"):
1704 del(os.environ["P4DIFF"])
1706 for editedFile in editedFiles:
1707 diff += p4_read_pipe(['diff', '-du',
1708 wildcard_encode(editedFile)])
1712 for newFile in filesToAdd:
1713 newdiff += "==== new file ====\n"
1714 newdiff += "--- /dev/null\n"
1715 newdiff += "+++ %s\n" % newFile
1717 is_link = os.path.islink(newFile)
1718 expect_link = newFile in symlinks
1720 if is_link and expect_link:
1721 newdiff += "+%s\n" % os.readlink(newFile)
1723 f = open(newFile, "r")
1724 for line in f.readlines():
1725 newdiff += "+" + line
1728 return (diff + newdiff).replace('\r\n', '\n')
1730 def applyCommit(self, id):
1731 """Apply one commit, return True if it succeeded."""
1733 print "Applying", read_pipe(["git", "show", "-s",
1734 "--format=format:%h %s", id])
1736 (p4User, gitEmail) = self.p4UserForCommit(id)
1738 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1740 filesToChangeType = set()
1741 filesToDelete = set()
1743 pureRenameCopy = set()
1745 filesToChangeExecBit = {}
1749 diff = parseDiffTreeEntry(line)
1750 modifier = diff['status']
1752 all_files.append(path)
1756 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1757 filesToChangeExecBit[path] = diff['dst_mode']
1758 editedFiles.add(path)
1759 elif modifier == "A":
1760 filesToAdd.add(path)
1761 filesToChangeExecBit[path] = diff['dst_mode']
1762 if path in filesToDelete:
1763 filesToDelete.remove(path)
1765 dst_mode = int(diff['dst_mode'], 8)
1766 if dst_mode == 0120000:
1769 elif modifier == "D":
1770 filesToDelete.add(path)
1771 if path in filesToAdd:
1772 filesToAdd.remove(path)
1773 elif modifier == "C":
1774 src, dest = diff['src'], diff['dst']
1775 p4_integrate(src, dest)
1776 pureRenameCopy.add(dest)
1777 if diff['src_sha1'] != diff['dst_sha1']:
1779 pureRenameCopy.discard(dest)
1780 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1782 pureRenameCopy.discard(dest)
1783 filesToChangeExecBit[dest] = diff['dst_mode']
1785 # turn off read-only attribute
1786 os.chmod(dest, stat.S_IWRITE)
1788 editedFiles.add(dest)
1789 elif modifier == "R":
1790 src, dest = diff['src'], diff['dst']
1791 if self.p4HasMoveCommand:
1792 p4_edit(src) # src must be open before move
1793 p4_move(src, dest) # opens for (move/delete, move/add)
1795 p4_integrate(src, dest)
1796 if diff['src_sha1'] != diff['dst_sha1']:
1799 pureRenameCopy.add(dest)
1800 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1801 if not self.p4HasMoveCommand:
1802 p4_edit(dest) # with move: already open, writable
1803 filesToChangeExecBit[dest] = diff['dst_mode']
1804 if not self.p4HasMoveCommand:
1806 os.chmod(dest, stat.S_IWRITE)
1808 filesToDelete.add(src)
1809 editedFiles.add(dest)
1810 elif modifier == "T":
1811 filesToChangeType.add(path)
1813 die("unknown modifier %s for %s" % (modifier, path))
1815 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
1816 patchcmd = diffcmd + " | git apply "
1817 tryPatchCmd = patchcmd + "--check -"
1818 applyPatchCmd = patchcmd + "--check --apply -"
1819 patch_succeeded = True
1821 if os.system(tryPatchCmd) != 0:
1822 fixed_rcs_keywords = False
1823 patch_succeeded = False
1824 print "Unfortunately applying the change failed!"
1826 # Patch failed, maybe it's just RCS keyword woes. Look through
1827 # the patch to see if that's possible.
1828 if gitConfigBool("git-p4.attemptRCSCleanup"):
1832 for file in editedFiles | filesToDelete:
1833 # did this file's delta contain RCS keywords?
1834 pattern = p4_keywords_regexp_for_file(file)
1837 # this file is a possibility...look for RCS keywords.
1838 regexp = re.compile(pattern, re.VERBOSE)
1839 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1840 if regexp.search(line):
1842 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1843 kwfiles[file] = pattern
1846 for file in kwfiles:
1848 print "zapping %s with %s" % (line,pattern)
1849 # File is being deleted, so not open in p4. Must
1850 # disable the read-only bit on windows.
1851 if self.isWindows and file not in editedFiles:
1852 os.chmod(file, stat.S_IWRITE)
1853 self.patchRCSKeywords(file, kwfiles[file])
1854 fixed_rcs_keywords = True
1856 if fixed_rcs_keywords:
1857 print "Retrying the patch with RCS keywords cleaned up"
1858 if os.system(tryPatchCmd) == 0:
1859 patch_succeeded = True
1861 if not patch_succeeded:
1862 for f in editedFiles:
1867 # Apply the patch for real, and do add/delete/+x handling.
1869 system(applyPatchCmd)
1871 for f in filesToChangeType:
1872 p4_edit(f, "-t", "auto")
1873 for f in filesToAdd:
1875 for f in filesToDelete:
1879 # Set/clear executable bits
1880 for f in filesToChangeExecBit.keys():
1881 mode = filesToChangeExecBit[f]
1882 setP4ExecBit(f, mode)
1885 if len(self.update_shelve) > 0:
1886 update_shelve = self.update_shelve.pop(0)
1887 p4_reopen_in_change(update_shelve, all_files)
1890 # Build p4 change description, starting with the contents
1891 # of the git commit message.
1893 logMessage = extractLogMessageFromGitCommit(id)
1894 logMessage = logMessage.strip()
1895 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
1897 template = self.prepareSubmitTemplate(update_shelve)
1898 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
1900 if self.preserveUser:
1901 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
1903 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1904 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1905 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1906 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1908 separatorLine = "######## everything below this line is just the diff #######\n"
1909 if not self.prepare_p4_only:
1910 submitTemplate += separatorLine
1911 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
1913 (handle, fileName) = tempfile.mkstemp()
1914 tmpFile = os.fdopen(handle, "w+b")
1916 submitTemplate = submitTemplate.replace("\n", "\r\n")
1917 tmpFile.write(submitTemplate)
1920 if self.prepare_p4_only:
1922 # Leave the p4 tree prepared, and the submit template around
1923 # and let the user decide what to do next
1926 print "P4 workspace prepared for submission."
1927 print "To submit or revert, go to client workspace"
1928 print " " + self.clientPath
1930 print "To submit, use \"p4 submit\" to write a new description,"
1931 print "or \"p4 submit -i <%s\" to use the one prepared by" \
1932 " \"git p4\"." % fileName
1933 print "You can delete the file \"%s\" when finished." % fileName
1935 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1936 print "To preserve change ownership by user %s, you must\n" \
1937 "do \"p4 change -f <change>\" after submitting and\n" \
1938 "edit the User field."
1940 print "After submitting, renamed files must be re-synced."
1941 print "Invoke \"p4 sync -f\" on each of these files:"
1942 for f in pureRenameCopy:
1946 print "To revert the changes, use \"p4 revert ...\", and delete"
1947 print "the submit template file \"%s\"" % fileName
1949 print "Since the commit adds new files, they must be deleted:"
1950 for f in filesToAdd:
1956 # Let the user edit the change description, then submit it.
1961 if self.edit_template(fileName):
1962 # read the edited message and submit
1963 tmpFile = open(fileName, "rb")
1964 message = tmpFile.read()
1967 message = message.replace("\r\n", "\n")
1968 submitTemplate = message[:message.index(separatorLine)]
1971 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
1973 p4_write_pipe(['shelve', '-i'], submitTemplate)
1975 p4_write_pipe(['submit', '-i'], submitTemplate)
1976 # The rename/copy happened by applying a patch that created a
1977 # new file. This leaves it writable, which confuses p4.
1978 for f in pureRenameCopy:
1981 if self.preserveUser:
1983 # Get last changelist number. Cannot easily get it from
1984 # the submit command output as the output is
1986 changelist = self.lastP4Changelist()
1987 self.modifyChangelistUser(changelist, p4User)
1993 if not submitted or self.shelve:
1995 print ("Reverting shelved files.")
1997 print ("Submission cancelled, undoing p4 changes.")
1998 for f in editedFiles | filesToDelete:
2000 for f in filesToAdd:
2007 # Export git tags as p4 labels. Create a p4 label and then tag
2009 def exportGitTags(self, gitTags):
2010 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2011 if len(validLabelRegexp) == 0:
2012 validLabelRegexp = defaultLabelRegexp
2013 m = re.compile(validLabelRegexp)
2015 for name in gitTags:
2017 if not m.match(name):
2019 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
2022 # Get the p4 commit this corresponds to
2023 logMessage = extractLogMessageFromGitCommit(name)
2024 values = extractSettingsGitLog(logMessage)
2026 if not values.has_key('change'):
2027 # a tag pointing to something not sent to p4; ignore
2029 print "git tag %s does not give a p4 commit" % name
2032 changelist = values['change']
2034 # Get the tag details.
2038 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2041 if re.match(r'tag\s+', l):
2043 elif re.match(r'\s*$', l):
2050 body = ["lightweight tag imported by git p4\n"]
2052 # Create the label - use the same view as the client spec we are using
2053 clientSpec = getClientSpec()
2055 labelTemplate = "Label: %s\n" % name
2056 labelTemplate += "Description:\n"
2058 labelTemplate += "\t" + b + "\n"
2059 labelTemplate += "View:\n"
2060 for depot_side in clientSpec.mappings:
2061 labelTemplate += "\t%s\n" % depot_side
2064 print "Would create p4 label %s for tag" % name
2065 elif self.prepare_p4_only:
2066 print "Not creating p4 label %s for tag due to option" \
2067 " --prepare-p4-only" % name
2069 p4_write_pipe(["label", "-i"], labelTemplate)
2072 p4_system(["tag", "-l", name] +
2073 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2076 print "created p4 label for tag %s" % name
2078 def run(self, args):
2080 self.master = currentGitBranch()
2081 elif len(args) == 1:
2082 self.master = args[0]
2083 if not branchExists(self.master):
2084 die("Branch %s does not exist" % self.master)
2088 for i in self.update_shelve:
2090 sys.exit("invalid changelist %d" % i)
2093 allowSubmit = gitConfig("git-p4.allowSubmit")
2094 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2095 die("%s is not in git-p4.allowSubmit" % self.master)
2097 [upstream, settings] = findUpstreamBranchPoint()
2098 self.depotPath = settings['depot-paths'][0]
2099 if len(self.origin) == 0:
2100 self.origin = upstream
2102 if len(self.update_shelve) > 0:
2105 if self.preserveUser:
2106 if not self.canChangeChangelists():
2107 die("Cannot preserve user names without p4 super-user or admin permissions")
2109 # if not set from the command line, try the config file
2110 if self.conflict_behavior is None:
2111 val = gitConfig("git-p4.conflict")
2113 if val not in self.conflict_behavior_choices:
2114 die("Invalid value '%s' for config git-p4.conflict" % val)
2117 self.conflict_behavior = val
2120 print "Origin branch is " + self.origin
2122 if len(self.depotPath) == 0:
2123 print "Internal error: cannot locate perforce depot path from existing branches"
2126 self.useClientSpec = False
2127 if gitConfigBool("git-p4.useclientspec"):
2128 self.useClientSpec = True
2129 if self.useClientSpec:
2130 self.clientSpecDirs = getClientSpec()
2132 # Check for the existence of P4 branches
2133 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2135 if self.useClientSpec and not branchesDetected:
2136 # all files are relative to the client spec
2137 self.clientPath = getClientRoot()
2139 self.clientPath = p4Where(self.depotPath)
2141 if self.clientPath == "":
2142 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2144 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
2145 self.oldWorkingDirectory = os.getcwd()
2147 # ensure the clientPath exists
2148 new_client_dir = False
2149 if not os.path.exists(self.clientPath):
2150 new_client_dir = True
2151 os.makedirs(self.clientPath)
2153 chdir(self.clientPath, is_client_path=True)
2155 print "Would synchronize p4 checkout in %s" % self.clientPath
2157 print "Synchronizing p4 checkout..."
2159 # old one was destroyed, and maybe nobody told p4
2160 p4_sync("...", "-f")
2167 commitish = self.master
2171 if self.commit != "":
2172 if self.commit.find("..") != -1:
2173 limits_ish = self.commit.split("..")
2174 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2175 commits.append(line.strip())
2178 commits.append(self.commit)
2180 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, commitish)]):
2181 commits.append(line.strip())
2184 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2185 self.checkAuthorship = False
2187 self.checkAuthorship = True
2189 if self.preserveUser:
2190 self.checkValidP4Users(commits)
2193 # Build up a set of options to be passed to diff when
2194 # submitting each commit to p4.
2196 if self.detectRenames:
2197 # command-line -M arg
2198 self.diffOpts = "-M"
2200 # If not explicitly set check the config variable
2201 detectRenames = gitConfig("git-p4.detectRenames")
2203 if detectRenames.lower() == "false" or detectRenames == "":
2205 elif detectRenames.lower() == "true":
2206 self.diffOpts = "-M"
2208 self.diffOpts = "-M%s" % detectRenames
2210 # no command-line arg for -C or --find-copies-harder, just
2212 detectCopies = gitConfig("git-p4.detectCopies")
2213 if detectCopies.lower() == "false" or detectCopies == "":
2215 elif detectCopies.lower() == "true":
2216 self.diffOpts += " -C"
2218 self.diffOpts += " -C%s" % detectCopies
2220 if gitConfigBool("git-p4.detectCopiesHarder"):
2221 self.diffOpts += " --find-copies-harder"
2223 num_shelves = len(self.update_shelve)
2224 if num_shelves > 0 and num_shelves != len(commits):
2225 sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2226 (len(commits), num_shelves))
2229 # Apply the commits, one at a time. On failure, ask if should
2230 # continue to try the rest of the patches, or quit.
2235 last = len(commits) - 1
2236 for i, commit in enumerate(commits):
2238 print " ", read_pipe(["git", "show", "-s",
2239 "--format=format:%h %s", commit])
2242 ok = self.applyCommit(commit)
2244 applied.append(commit)
2246 if self.prepare_p4_only and i < last:
2247 print "Processing only the first commit due to option" \
2248 " --prepare-p4-only"
2253 # prompt for what to do, or use the option/variable
2254 if self.conflict_behavior == "ask":
2255 print "What do you want to do?"
2256 response = raw_input("[s]kip this commit but apply"
2257 " the rest, or [q]uit? ")
2260 elif self.conflict_behavior == "skip":
2262 elif self.conflict_behavior == "quit":
2265 die("Unknown conflict_behavior '%s'" %
2266 self.conflict_behavior)
2268 if response[0] == "s":
2269 print "Skipping this commit, but applying the rest"
2271 if response[0] == "q":
2278 chdir(self.oldWorkingDirectory)
2279 shelved_applied = "shelved" if self.shelve else "applied"
2282 elif self.prepare_p4_only:
2284 elif len(commits) == len(applied):
2285 print ("All commits {0}!".format(shelved_applied))
2289 sync.branch = self.branch
2290 if self.disable_p4sync:
2291 sync.sync_origin_only()
2295 if not self.disable_rebase:
2300 if len(applied) == 0:
2301 print ("No commits {0}.".format(shelved_applied))
2303 print ("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2309 print star, read_pipe(["git", "show", "-s",
2310 "--format=format:%h %s", c])
2311 print "You will have to do 'git p4 sync' and rebase."
2313 if gitConfigBool("git-p4.exportLabels"):
2314 self.exportLabels = True
2316 if self.exportLabels:
2317 p4Labels = getP4Labels(self.depotPath)
2318 gitTags = getGitTags()
2320 missingGitTags = gitTags - p4Labels
2321 self.exportGitTags(missingGitTags)
2323 # exit with error unless everything applied perfectly
2324 if len(commits) != len(applied):
2330 """Represent a p4 view ("p4 help views"), and map files in a
2331 repo according to the view."""
2333 def __init__(self, client_name):
2335 self.client_prefix = "//%s/" % client_name
2336 # cache results of "p4 where" to lookup client file locations
2337 self.client_spec_path_cache = {}
2339 def append(self, view_line):
2340 """Parse a view line, splitting it into depot and client
2341 sides. Append to self.mappings, preserving order. This
2342 is only needed for tag creation."""
2344 # Split the view line into exactly two words. P4 enforces
2345 # structure on these lines that simplifies this quite a bit.
2347 # Either or both words may be double-quoted.
2348 # Single quotes do not matter.
2349 # Double-quote marks cannot occur inside the words.
2350 # A + or - prefix is also inside the quotes.
2351 # There are no quotes unless they contain a space.
2352 # The line is already white-space stripped.
2353 # The two words are separated by a single space.
2355 if view_line[0] == '"':
2356 # First word is double quoted. Find its end.
2357 close_quote_index = view_line.find('"', 1)
2358 if close_quote_index <= 0:
2359 die("No first-word closing quote found: %s" % view_line)
2360 depot_side = view_line[1:close_quote_index]
2361 # skip closing quote and space
2362 rhs_index = close_quote_index + 1 + 1
2364 space_index = view_line.find(" ")
2365 if space_index <= 0:
2366 die("No word-splitting space found: %s" % view_line)
2367 depot_side = view_line[0:space_index]
2368 rhs_index = space_index + 1
2370 # prefix + means overlay on previous mapping
2371 if depot_side.startswith("+"):
2372 depot_side = depot_side[1:]
2374 # prefix - means exclude this path, leave out of mappings
2376 if depot_side.startswith("-"):
2378 depot_side = depot_side[1:]
2381 self.mappings.append(depot_side)
2383 def convert_client_path(self, clientFile):
2384 # chop off //client/ part to make it relative
2385 if not clientFile.startswith(self.client_prefix):
2386 die("No prefix '%s' on clientFile '%s'" %
2387 (self.client_prefix, clientFile))
2388 return clientFile[len(self.client_prefix):]
2390 def update_client_spec_path_cache(self, files):
2391 """ Caching file paths by "p4 where" batch query """
2393 # List depot file paths exclude that already cached
2394 fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
2396 if len(fileArgs) == 0:
2397 return # All files in cache
2399 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2400 for res in where_result:
2401 if "code" in res and res["code"] == "error":
2402 # assume error is "... file(s) not in client view"
2404 if "clientFile" not in res:
2405 die("No clientFile in 'p4 where' output")
2407 # it will list all of them, but only one not unmap-ped
2409 if gitConfigBool("core.ignorecase"):
2410 res['depotFile'] = res['depotFile'].lower()
2411 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
2413 # not found files or unmap files set to ""
2414 for depotFile in fileArgs:
2415 if gitConfigBool("core.ignorecase"):
2416 depotFile = depotFile.lower()
2417 if depotFile not in self.client_spec_path_cache:
2418 self.client_spec_path_cache[depotFile] = ""
2420 def map_in_client(self, depot_path):
2421 """Return the relative location in the client where this
2422 depot file should live. Returns "" if the file should
2423 not be mapped in the client."""
2425 if gitConfigBool("core.ignorecase"):
2426 depot_path = depot_path.lower()
2428 if depot_path in self.client_spec_path_cache:
2429 return self.client_spec_path_cache[depot_path]
2431 die( "Error: %s is not found in client spec path" % depot_path )
2434 class P4Sync(Command, P4UserMap):
2435 delete_actions = ( "delete", "move/delete", "purge" )
2438 Command.__init__(self)
2439 P4UserMap.__init__(self)
2441 optparse.make_option("--branch", dest="branch"),
2442 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2443 optparse.make_option("--changesfile", dest="changesFile"),
2444 optparse.make_option("--silent", dest="silent", action="store_true"),
2445 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2446 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2447 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2448 help="Import into refs/heads/ , not refs/remotes"),
2449 optparse.make_option("--max-changes", dest="maxChanges",
2450 help="Maximum number of changes to import"),
2451 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2452 help="Internal block size to use when iteratively calling p4 changes"),
2453 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2454 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2455 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2456 help="Only sync files that are included in the Perforce Client Spec"),
2457 optparse.make_option("-/", dest="cloneExclude",
2458 action="append", type="string",
2459 help="exclude depot path"),
2461 self.description = """Imports from Perforce into a git repository.\n
2463 //depot/my/project/ -- to import the current head
2464 //depot/my/project/@all -- to import everything
2465 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2467 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2469 self.usage += " //depot/path[@revRange]"
2471 self.createdBranches = set()
2472 self.committedChanges = set()
2474 self.detectBranches = False
2475 self.detectLabels = False
2476 self.importLabels = False
2477 self.changesFile = ""
2478 self.syncWithOrigin = True
2479 self.importIntoRemotes = True
2480 self.maxChanges = ""
2481 self.changes_block_size = None
2482 self.keepRepoPath = False
2483 self.depotPaths = None
2484 self.p4BranchesInGit = []
2485 self.cloneExclude = []
2486 self.useClientSpec = False
2487 self.useClientSpec_from_options = False
2488 self.clientSpecDirs = None
2489 self.tempBranches = []
2490 self.tempBranchLocation = "refs/git-p4-tmp"
2491 self.largeFileSystem = None
2493 if gitConfig('git-p4.largeFileSystem'):
2494 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2495 self.largeFileSystem = largeFileSystemConstructor(
2496 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2499 if gitConfig("git-p4.syncFromOrigin") == "false":
2500 self.syncWithOrigin = False
2502 # Force a checkpoint in fast-import and wait for it to finish
2503 def checkpoint(self):
2504 self.gitStream.write("checkpoint\n\n")
2505 self.gitStream.write("progress checkpoint\n\n")
2506 out = self.gitOutput.readline()
2508 print "checkpoint finished: " + out
2510 def extractFilesFromCommit(self, commit):
2511 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2512 for path in self.cloneExclude]
2515 while commit.has_key("depotFile%s" % fnum):
2516 path = commit["depotFile%s" % fnum]
2518 if [p for p in self.cloneExclude
2519 if p4PathStartsWith(path, p)]:
2522 found = [p for p in self.depotPaths
2523 if p4PathStartsWith(path, p)]
2530 file["rev"] = commit["rev%s" % fnum]
2531 file["action"] = commit["action%s" % fnum]
2532 file["type"] = commit["type%s" % fnum]
2537 def extractJobsFromCommit(self, commit):
2540 while commit.has_key("job%s" % jnum):
2541 job = commit["job%s" % jnum]
2546 def stripRepoPath(self, path, prefixes):
2547 """When streaming files, this is called to map a p4 depot path
2548 to where it should go in git. The prefixes are either
2549 self.depotPaths, or self.branchPrefixes in the case of
2550 branch detection."""
2552 if self.useClientSpec:
2553 # branch detection moves files up a level (the branch name)
2554 # from what client spec interpretation gives
2555 path = self.clientSpecDirs.map_in_client(path)
2556 if self.detectBranches:
2557 for b in self.knownBranches:
2558 if path.startswith(b + "/"):
2559 path = path[len(b)+1:]
2561 elif self.keepRepoPath:
2562 # Preserve everything in relative path name except leading
2563 # //depot/; just look at first prefix as they all should
2564 # be in the same depot.
2565 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2566 if p4PathStartsWith(path, depot):
2567 path = path[len(depot):]
2571 if p4PathStartsWith(path, p):
2572 path = path[len(p):]
2575 path = wildcard_decode(path)
2578 def splitFilesIntoBranches(self, commit):
2579 """Look at each depotFile in the commit to figure out to what
2580 branch it belongs."""
2582 if self.clientSpecDirs:
2583 files = self.extractFilesFromCommit(commit)
2584 self.clientSpecDirs.update_client_spec_path_cache(files)
2588 while commit.has_key("depotFile%s" % fnum):
2589 path = commit["depotFile%s" % fnum]
2590 found = [p for p in self.depotPaths
2591 if p4PathStartsWith(path, p)]
2598 file["rev"] = commit["rev%s" % fnum]
2599 file["action"] = commit["action%s" % fnum]
2600 file["type"] = commit["type%s" % fnum]
2603 # start with the full relative path where this file would
2605 if self.useClientSpec:
2606 relPath = self.clientSpecDirs.map_in_client(path)
2608 relPath = self.stripRepoPath(path, self.depotPaths)
2610 for branch in self.knownBranches.keys():
2611 # add a trailing slash so that a commit into qt/4.2foo
2612 # doesn't end up in qt/4.2, e.g.
2613 if relPath.startswith(branch + "/"):
2614 if branch not in branches:
2615 branches[branch] = []
2616 branches[branch].append(file)
2621 def writeToGitStream(self, gitMode, relPath, contents):
2622 self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2623 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2625 self.gitStream.write(d)
2626 self.gitStream.write('\n')
2628 def encodeWithUTF8(self, path):
2630 path.decode('ascii')
2633 if gitConfig('git-p4.pathEncoding'):
2634 encoding = gitConfig('git-p4.pathEncoding')
2635 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2637 print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path)
2640 # output one file from the P4 stream
2641 # - helper for streamP4Files
2643 def streamOneP4File(self, file, contents):
2644 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2645 relPath = self.encodeWithUTF8(relPath)
2647 size = int(self.stream_file['fileSize'])
2648 sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
2651 (type_base, type_mods) = split_p4_type(file["type"])
2654 if "x" in type_mods:
2656 if type_base == "symlink":
2658 # p4 print on a symlink sometimes contains "target\n";
2659 # if it does, remove the newline
2660 data = ''.join(contents)
2662 # Some version of p4 allowed creating a symlink that pointed
2663 # to nothing. This causes p4 errors when checking out such
2664 # a change, and errors here too. Work around it by ignoring
2665 # the bad symlink; hopefully a future change fixes it.
2666 print "\nIgnoring empty symlink in %s" % file['depotFile']
2668 elif data[-1] == '\n':
2669 contents = [data[:-1]]
2673 if type_base == "utf16":
2674 # p4 delivers different text in the python output to -G
2675 # than it does when using "print -o", or normal p4 client
2676 # operations. utf16 is converted to ascii or utf8, perhaps.
2677 # But ascii text saved as -t utf16 is completely mangled.
2678 # Invoke print -o to get the real contents.
2680 # On windows, the newlines will always be mangled by print, so put
2681 # them back too. This is not needed to the cygwin windows version,
2682 # just the native "NT" type.
2685 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2686 except Exception as e:
2687 if 'Translation of file content failed' in str(e):
2688 type_base = 'binary'
2692 if p4_version_string().find('/NT') >= 0:
2693 text = text.replace('\r\n', '\n')
2696 if type_base == "apple":
2697 # Apple filetype files will be streamed as a concatenation of
2698 # its appledouble header and the contents. This is useless
2699 # on both macs and non-macs. If using "print -q -o xx", it
2700 # will create "xx" with the data, and "%xx" with the header.
2701 # This is also not very useful.
2703 # Ideally, someday, this script can learn how to generate
2704 # appledouble files directly and import those to git, but
2705 # non-mac machines can never find a use for apple filetype.
2706 print "\nIgnoring apple filetype file %s" % file['depotFile']
2709 # Note that we do not try to de-mangle keywords on utf16 files,
2710 # even though in theory somebody may want that.
2711 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2713 regexp = re.compile(pattern, re.VERBOSE)
2714 text = ''.join(contents)
2715 text = regexp.sub(r'$\1$', text)
2718 if self.largeFileSystem:
2719 (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
2721 self.writeToGitStream(git_mode, relPath, contents)
2723 def streamOneP4Deletion(self, file):
2724 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2725 relPath = self.encodeWithUTF8(relPath)
2727 sys.stdout.write("delete %s\n" % relPath)
2729 self.gitStream.write("D %s\n" % relPath)
2731 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2732 self.largeFileSystem.removeLargeFile(relPath)
2734 # handle another chunk of streaming data
2735 def streamP4FilesCb(self, marshalled):
2737 # catch p4 errors and complain
2739 if "code" in marshalled:
2740 if marshalled["code"] == "error":
2741 if "data" in marshalled:
2742 err = marshalled["data"].rstrip()
2744 if not err and 'fileSize' in self.stream_file:
2745 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
2746 if required_bytes > 0:
2747 err = 'Not enough space left on %s! Free at least %i MB.' % (
2748 os.getcwd(), required_bytes/1024/1024
2753 if self.stream_have_file_info:
2754 if "depotFile" in self.stream_file:
2755 f = self.stream_file["depotFile"]
2756 # force a failure in fast-import, else an empty
2757 # commit will be made
2758 self.gitStream.write("\n")
2759 self.gitStream.write("die-now\n")
2760 self.gitStream.close()
2761 # ignore errors, but make sure it exits first
2762 self.importProcess.wait()
2764 die("Error from p4 print for %s: %s" % (f, err))
2766 die("Error from p4 print: %s" % err)
2768 if marshalled.has_key('depotFile') and self.stream_have_file_info:
2769 # start of a new file - output the old one first
2770 self.streamOneP4File(self.stream_file, self.stream_contents)
2771 self.stream_file = {}
2772 self.stream_contents = []
2773 self.stream_have_file_info = False
2775 # pick up the new file information... for the
2776 # 'data' field we need to append to our array
2777 for k in marshalled.keys():
2779 if 'streamContentSize' not in self.stream_file:
2780 self.stream_file['streamContentSize'] = 0
2781 self.stream_file['streamContentSize'] += len(marshalled['data'])
2782 self.stream_contents.append(marshalled['data'])
2784 self.stream_file[k] = marshalled[k]
2787 'streamContentSize' in self.stream_file and
2788 'fileSize' in self.stream_file and
2789 'depotFile' in self.stream_file):
2790 size = int(self.stream_file["fileSize"])
2792 progress = 100*self.stream_file['streamContentSize']/size
2793 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
2796 self.stream_have_file_info = True
2798 # Stream directly from "p4 files" into "git fast-import"
2799 def streamP4Files(self, files):
2805 filesForCommit.append(f)
2806 if f['action'] in self.delete_actions:
2807 filesToDelete.append(f)
2809 filesToRead.append(f)
2812 for f in filesToDelete:
2813 self.streamOneP4Deletion(f)
2815 if len(filesToRead) > 0:
2816 self.stream_file = {}
2817 self.stream_contents = []
2818 self.stream_have_file_info = False
2820 # curry self argument
2821 def streamP4FilesCbSelf(entry):
2822 self.streamP4FilesCb(entry)
2824 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2826 p4CmdList(["-x", "-", "print"],
2828 cb=streamP4FilesCbSelf)
2831 if self.stream_file.has_key('depotFile'):
2832 self.streamOneP4File(self.stream_file, self.stream_contents)
2834 def make_email(self, userid):
2835 if userid in self.users:
2836 return self.users[userid]
2838 return "%s <a@b>" % userid
2840 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2841 """ Stream a p4 tag.
2842 commit is either a git commit, or a fast-import mark, ":<p4commit>"
2846 print "writing tag %s for commit %s" % (labelName, commit)
2847 gitStream.write("tag %s\n" % labelName)
2848 gitStream.write("from %s\n" % commit)
2850 if labelDetails.has_key('Owner'):
2851 owner = labelDetails["Owner"]
2855 # Try to use the owner of the p4 label, or failing that,
2856 # the current p4 user id.
2858 email = self.make_email(owner)
2860 email = self.make_email(self.p4UserId())
2861 tagger = "%s %s %s" % (email, epoch, self.tz)
2863 gitStream.write("tagger %s\n" % tagger)
2865 print "labelDetails=",labelDetails
2866 if labelDetails.has_key('Description'):
2867 description = labelDetails['Description']
2869 description = 'Label from git p4'
2871 gitStream.write("data %d\n" % len(description))
2872 gitStream.write(description)
2873 gitStream.write("\n")
2875 def inClientSpec(self, path):
2876 if not self.clientSpecDirs:
2878 inClientSpec = self.clientSpecDirs.map_in_client(path)
2879 if not inClientSpec and self.verbose:
2880 print('Ignoring file outside of client spec: {0}'.format(path))
2883 def hasBranchPrefix(self, path):
2884 if not self.branchPrefixes:
2886 hasPrefix = [p for p in self.branchPrefixes
2887 if p4PathStartsWith(path, p)]
2888 if not hasPrefix and self.verbose:
2889 print('Ignoring file outside of prefix: {0}'.format(path))
2892 def commit(self, details, files, branch, parent = ""):
2893 epoch = details["time"]
2894 author = details["user"]
2895 jobs = self.extractJobsFromCommit(details)
2898 print('commit into {0}'.format(branch))
2900 if self.clientSpecDirs:
2901 self.clientSpecDirs.update_client_spec_path_cache(files)
2903 files = [f for f in files
2904 if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
2906 if not files and not gitConfigBool('git-p4.keepEmptyCommits'):
2907 print('Ignoring revision {0} as it would produce an empty commit.'
2908 .format(details['change']))
2911 self.gitStream.write("commit %s\n" % branch)
2912 self.gitStream.write("mark :%s\n" % details["change"])
2913 self.committedChanges.add(int(details["change"]))
2915 if author not in self.users:
2916 self.getUserMapFromPerforceServer()
2917 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
2919 self.gitStream.write("committer %s\n" % committer)
2921 self.gitStream.write("data <<EOT\n")
2922 self.gitStream.write(details["desc"])
2924 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
2925 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2926 (','.join(self.branchPrefixes), details["change"]))
2927 if len(details['options']) > 0:
2928 self.gitStream.write(": options = %s" % details['options'])
2929 self.gitStream.write("]\nEOT\n\n")
2933 print "parent %s" % parent
2934 self.gitStream.write("from %s\n" % parent)
2936 self.streamP4Files(files)
2937 self.gitStream.write("\n")
2939 change = int(details["change"])
2941 if self.labels.has_key(change):
2942 label = self.labels[change]
2943 labelDetails = label[0]
2944 labelRevisions = label[1]
2946 print "Change %s is labelled %s" % (change, labelDetails)
2948 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
2949 for p in self.branchPrefixes])
2951 if len(files) == len(labelRevisions):
2955 if info["action"] in self.delete_actions:
2957 cleanedFiles[info["depotFile"]] = info["rev"]
2959 if cleanedFiles == labelRevisions:
2960 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
2964 print ("Tag %s does not match with change %s: files do not match."
2965 % (labelDetails["label"], change))
2969 print ("Tag %s does not match with change %s: file count is different."
2970 % (labelDetails["label"], change))
2972 # Build a dictionary of changelists and labels, for "detect-labels" option.
2973 def getLabels(self):
2976 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
2977 if len(l) > 0 and not self.silent:
2978 print "Finding files belonging to labels in %s" % `self.depotPaths`
2981 label = output["label"]
2985 print "Querying files for label %s" % label
2986 for file in p4CmdList(["files"] +
2987 ["%s...@%s" % (p, label)
2988 for p in self.depotPaths]):
2989 revisions[file["depotFile"]] = file["rev"]
2990 change = int(file["change"])
2991 if change > newestChange:
2992 newestChange = change
2994 self.labels[newestChange] = [output, revisions]
2997 print "Label changes: %s" % self.labels.keys()
2999 # Import p4 labels as git tags. A direct mapping does not
3000 # exist, so assume that if all the files are at the same revision
3001 # then we can use that, or it's something more complicated we should
3003 def importP4Labels(self, stream, p4Labels):
3005 print "import p4 labels: " + ' '.join(p4Labels)
3007 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3008 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3009 if len(validLabelRegexp) == 0:
3010 validLabelRegexp = defaultLabelRegexp
3011 m = re.compile(validLabelRegexp)
3013 for name in p4Labels:
3016 if not m.match(name):
3018 print "label %s does not match regexp %s" % (name,validLabelRegexp)
3021 if name in ignoredP4Labels:
3024 labelDetails = p4CmdList(['label', "-o", name])[0]
3026 # get the most recent changelist for each file in this label
3027 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3028 for p in self.depotPaths])
3030 if change.has_key('change'):
3031 # find the corresponding git commit; take the oldest commit
3032 changelist = int(change['change'])
3033 if changelist in self.committedChanges:
3034 gitCommit = ":%d" % changelist # use a fast-import mark
3037 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3038 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3039 if len(gitCommit) == 0:
3040 print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
3043 gitCommit = gitCommit.strip()
3046 # Convert from p4 time format
3048 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3050 print "Could not convert label time %s" % labelDetails['Update']
3053 when = int(time.mktime(tmwhen))
3054 self.streamTag(stream, name, labelDetails, gitCommit, when)
3056 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
3059 print "Label %s has no changelists - possibly deleted?" % name
3062 # We can't import this label; don't try again as it will get very
3063 # expensive repeatedly fetching all the files for labels that will
3064 # never be imported. If the label is moved in the future, the
3065 # ignore will need to be removed manually.
3066 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3068 def guessProjectName(self):
3069 for p in self.depotPaths:
3072 p = p[p.strip().rfind("/") + 1:]
3073 if not p.endswith("/"):
3077 def getBranchMapping(self):
3078 lostAndFoundBranches = set()
3080 user = gitConfig("git-p4.branchUser")
3082 command = "branches -u %s" % user
3084 command = "branches"
3086 for info in p4CmdList(command):
3087 details = p4Cmd(["branch", "-o", info["branch"]])
3089 while details.has_key("View%s" % viewIdx):
3090 paths = details["View%s" % viewIdx].split(" ")
3091 viewIdx = viewIdx + 1
3092 # require standard //depot/foo/... //depot/bar/... mapping
3093 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3096 destination = paths[1]
3098 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3099 source = source[len(self.depotPaths[0]):-4]
3100 destination = destination[len(self.depotPaths[0]):-4]
3102 if destination in self.knownBranches:
3104 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
3105 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
3108 self.knownBranches[destination] = source
3110 lostAndFoundBranches.discard(destination)
3112 if source not in self.knownBranches:
3113 lostAndFoundBranches.add(source)
3115 # Perforce does not strictly require branches to be defined, so we also
3116 # check git config for a branch list.
3118 # Example of branch definition in git config file:
3120 # branchList=main:branchA
3121 # branchList=main:branchB
3122 # branchList=branchA:branchC
3123 configBranches = gitConfigList("git-p4.branchList")
3124 for branch in configBranches:
3126 (source, destination) = branch.split(":")
3127 self.knownBranches[destination] = source
3129 lostAndFoundBranches.discard(destination)
3131 if source not in self.knownBranches:
3132 lostAndFoundBranches.add(source)
3135 for branch in lostAndFoundBranches:
3136 self.knownBranches[branch] = branch
3138 def getBranchMappingFromGitBranches(self):
3139 branches = p4BranchesInGit(self.importIntoRemotes)
3140 for branch in branches.keys():
3141 if branch == "master":
3144 branch = branch[len(self.projectName):]
3145 self.knownBranches[branch] = branch
3147 def updateOptionDict(self, d):
3149 if self.keepRepoPath:
3150 option_keys['keepRepoPath'] = 1
3152 d["options"] = ' '.join(sorted(option_keys.keys()))
3154 def readOptions(self, d):
3155 self.keepRepoPath = (d.has_key('options')
3156 and ('keepRepoPath' in d['options']))
3158 def gitRefForBranch(self, branch):
3159 if branch == "main":
3160 return self.refPrefix + "master"
3162 if len(branch) <= 0:
3165 return self.refPrefix + self.projectName + branch
3167 def gitCommitByP4Change(self, ref, change):
3169 print "looking in ref " + ref + " for change %s using bisect..." % change
3172 latestCommit = parseRevision(ref)
3176 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
3177 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3182 log = extractLogMessageFromGitCommit(next)
3183 settings = extractSettingsGitLog(log)
3184 currentChange = int(settings['change'])
3186 print "current change %s" % currentChange
3188 if currentChange == change:
3190 print "found %s" % next
3193 if currentChange < change:
3194 earliestCommit = "^%s" % next
3196 latestCommit = "%s" % next
3200 def importNewBranch(self, branch, maxChange):
3201 # make fast-import flush all changes to disk and update the refs using the checkpoint
3202 # command so that we can try to find the branch parent in the git history
3203 self.gitStream.write("checkpoint\n\n");
3204 self.gitStream.flush();
3205 branchPrefix = self.depotPaths[0] + branch + "/"
3206 range = "@1,%s" % maxChange
3207 #print "prefix" + branchPrefix
3208 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3209 if len(changes) <= 0:
3211 firstChange = changes[0]
3212 #print "first change in branch: %s" % firstChange
3213 sourceBranch = self.knownBranches[branch]
3214 sourceDepotPath = self.depotPaths[0] + sourceBranch
3215 sourceRef = self.gitRefForBranch(sourceBranch)
3216 #print "source " + sourceBranch
3218 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3219 #print "branch parent: %s" % branchParentChange
3220 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3221 if len(gitParent) > 0:
3222 self.initialParents[self.gitRefForBranch(branch)] = gitParent
3223 #print "parent git commit: %s" % gitParent
3225 self.importChanges(changes)
3228 def searchParent(self, parent, branch, target):
3230 for blob in read_pipe_lines(["git", "rev-list", "--reverse",
3231 "--no-merges", parent]):
3233 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
3236 print "Found parent of %s in commit %s" % (branch, blob)
3243 def importChanges(self, changes):
3245 for change in changes:
3246 description = p4_describe(change)
3247 self.updateOptionDict(description)
3250 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3255 if self.detectBranches:
3256 branches = self.splitFilesIntoBranches(description)
3257 for branch in branches.keys():
3259 branchPrefix = self.depotPaths[0] + branch + "/"
3260 self.branchPrefixes = [ branchPrefix ]
3264 filesForCommit = branches[branch]
3267 print "branch is %s" % branch
3269 self.updatedBranches.add(branch)
3271 if branch not in self.createdBranches:
3272 self.createdBranches.add(branch)
3273 parent = self.knownBranches[branch]
3274 if parent == branch:
3277 fullBranch = self.projectName + branch
3278 if fullBranch not in self.p4BranchesInGit:
3280 print("\n Importing new branch %s" % fullBranch);
3281 if self.importNewBranch(branch, change - 1):
3283 self.p4BranchesInGit.append(fullBranch)
3285 print("\n Resuming with change %s" % change);
3288 print "parent determined through known branches: %s" % parent
3290 branch = self.gitRefForBranch(branch)
3291 parent = self.gitRefForBranch(parent)
3294 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
3296 if len(parent) == 0 and branch in self.initialParents:
3297 parent = self.initialParents[branch]
3298 del self.initialParents[branch]
3302 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3304 print "Creating temporary branch: " + tempBranch
3305 self.commit(description, filesForCommit, tempBranch)
3306 self.tempBranches.append(tempBranch)
3308 blob = self.searchParent(parent, branch, tempBranch)
3310 self.commit(description, filesForCommit, branch, blob)
3313 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
3314 self.commit(description, filesForCommit, branch, parent)
3316 files = self.extractFilesFromCommit(description)
3317 self.commit(description, files, self.branch,
3319 # only needed once, to connect to the previous commit
3320 self.initialParent = ""
3322 print self.gitError.read()
3325 def sync_origin_only(self):
3326 if self.syncWithOrigin:
3327 self.hasOrigin = originP4BranchesExist()
3330 print 'Syncing with origin first, using "git fetch origin"'
3331 system("git fetch origin")
3333 def importHeadRevision(self, revision):
3334 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
3337 details["user"] = "git perforce import user"
3338 details["desc"] = ("Initial import of %s from the state at revision %s\n"
3339 % (' '.join(self.depotPaths), revision))
3340 details["change"] = revision
3344 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3346 for info in p4CmdList(["files"] + fileArgs):
3348 if 'code' in info and info['code'] == 'error':
3349 sys.stderr.write("p4 returned an error: %s\n"
3351 if info['data'].find("must refer to client") >= 0:
3352 sys.stderr.write("This particular p4 error is misleading.\n")
3353 sys.stderr.write("Perhaps the depot path was misspelled.\n");
3354 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
3356 if 'p4ExitCode' in info:
3357 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3361 change = int(info["change"])
3362 if change > newestRevision:
3363 newestRevision = change
3365 if info["action"] in self.delete_actions:
3366 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3367 #fileCnt = fileCnt + 1
3370 for prop in ["depotFile", "rev", "action", "type" ]:
3371 details["%s%s" % (prop, fileCnt)] = info[prop]
3373 fileCnt = fileCnt + 1
3375 details["change"] = newestRevision
3377 # Use time from top-most change so that all git p4 clones of
3378 # the same p4 repo have the same commit SHA1s.
3379 res = p4_describe(newestRevision)
3380 details["time"] = res["time"]
3382 self.updateOptionDict(details)
3384 self.commit(details, self.extractFilesFromCommit(details), self.branch)
3386 print "IO error with git fast-import. Is your git version recent enough?"
3387 print self.gitError.read()
3390 def run(self, args):
3391 self.depotPaths = []
3392 self.changeRange = ""
3393 self.previousDepotPaths = []
3394 self.hasOrigin = False
3396 # map from branch depot path to parent branch
3397 self.knownBranches = {}
3398 self.initialParents = {}
3400 if self.importIntoRemotes:
3401 self.refPrefix = "refs/remotes/p4/"
3403 self.refPrefix = "refs/heads/p4/"
3405 self.sync_origin_only()
3407 branch_arg_given = bool(self.branch)
3408 if len(self.branch) == 0:
3409 self.branch = self.refPrefix + "master"
3410 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3411 system("git update-ref %s refs/heads/p4" % self.branch)
3412 system("git branch -D p4")
3414 # accept either the command-line option, or the configuration variable
3415 if self.useClientSpec:
3416 # will use this after clone to set the variable
3417 self.useClientSpec_from_options = True
3419 if gitConfigBool("git-p4.useclientspec"):
3420 self.useClientSpec = True
3421 if self.useClientSpec:
3422 self.clientSpecDirs = getClientSpec()
3424 # TODO: should always look at previous commits,
3425 # merge with previous imports, if possible.
3428 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3430 # branches holds mapping from branch name to sha1
3431 branches = p4BranchesInGit(self.importIntoRemotes)
3433 # restrict to just this one, disabling detect-branches
3434 if branch_arg_given:
3435 short = self.branch.split("/")[-1]
3436 if short in branches:
3437 self.p4BranchesInGit = [ short ]
3439 self.p4BranchesInGit = branches.keys()
3441 if len(self.p4BranchesInGit) > 1:
3443 print "Importing from/into multiple branches"
3444 self.detectBranches = True
3445 for branch in branches.keys():
3446 self.initialParents[self.refPrefix + branch] = \
3450 print "branches: %s" % self.p4BranchesInGit
3453 for branch in self.p4BranchesInGit:
3454 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
3456 settings = extractSettingsGitLog(logMsg)
3458 self.readOptions(settings)
3459 if (settings.has_key('depot-paths')
3460 and settings.has_key ('change')):
3461 change = int(settings['change']) + 1
3462 p4Change = max(p4Change, change)
3464 depotPaths = sorted(settings['depot-paths'])
3465 if self.previousDepotPaths == []:
3466 self.previousDepotPaths = depotPaths
3469 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3470 prev_list = prev.split("/")
3471 cur_list = cur.split("/")
3472 for i in range(0, min(len(cur_list), len(prev_list))):
3473 if cur_list[i] <> prev_list[i]:
3477 paths.append ("/".join(cur_list[:i + 1]))
3479 self.previousDepotPaths = paths
3482 self.depotPaths = sorted(self.previousDepotPaths)
3483 self.changeRange = "@%s,#head" % p4Change
3484 if not self.silent and not self.detectBranches:
3485 print "Performing incremental import into %s git branch" % self.branch
3487 # accept multiple ref name abbreviations:
3488 # refs/foo/bar/branch -> use it exactly
3489 # p4/branch -> prepend refs/remotes/ or refs/heads/
3490 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3491 if not self.branch.startswith("refs/"):
3492 if self.importIntoRemotes:
3493 prepend = "refs/remotes/"
3495 prepend = "refs/heads/"
3496 if not self.branch.startswith("p4/"):
3498 self.branch = prepend + self.branch
3500 if len(args) == 0 and self.depotPaths:
3502 print "Depot paths: %s" % ' '.join(self.depotPaths)
3504 if self.depotPaths and self.depotPaths != args:
3505 print ("previous import used depot path %s and now %s was specified. "
3506 "This doesn't work!" % (' '.join (self.depotPaths),
3510 self.depotPaths = sorted(args)
3515 # Make sure no revision specifiers are used when --changesfile
3517 bad_changesfile = False
3518 if len(self.changesFile) > 0:
3519 for p in self.depotPaths:
3520 if p.find("@") >= 0 or p.find("#") >= 0:
3521 bad_changesfile = True
3524 die("Option --changesfile is incompatible with revision specifiers")
3527 for p in self.depotPaths:
3528 if p.find("@") != -1:
3529 atIdx = p.index("@")
3530 self.changeRange = p[atIdx:]
3531 if self.changeRange == "@all":
3532 self.changeRange = ""
3533 elif ',' not in self.changeRange:
3534 revision = self.changeRange
3535 self.changeRange = ""
3537 elif p.find("#") != -1:
3538 hashIdx = p.index("#")
3539 revision = p[hashIdx:]
3541 elif self.previousDepotPaths == []:
3542 # pay attention to changesfile, if given, else import
3543 # the entire p4 tree at the head revision
3544 if len(self.changesFile) == 0:
3547 p = re.sub ("\.\.\.$", "", p)
3548 if not p.endswith("/"):
3553 self.depotPaths = newPaths
3555 # --detect-branches may change this for each branch
3556 self.branchPrefixes = self.depotPaths
3558 self.loadUserMapFromCache()
3560 if self.detectLabels:
3563 if self.detectBranches:
3564 ## FIXME - what's a P4 projectName ?
3565 self.projectName = self.guessProjectName()
3568 self.getBranchMappingFromGitBranches()
3570 self.getBranchMapping()
3572 print "p4-git branches: %s" % self.p4BranchesInGit
3573 print "initial parents: %s" % self.initialParents
3574 for b in self.p4BranchesInGit:
3578 b = b[len(self.projectName):]
3579 self.createdBranches.add(b)
3581 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
3583 self.importProcess = subprocess.Popen(["git", "fast-import"],
3584 stdin=subprocess.PIPE,
3585 stdout=subprocess.PIPE,
3586 stderr=subprocess.PIPE);
3587 self.gitOutput = self.importProcess.stdout
3588 self.gitStream = self.importProcess.stdin
3589 self.gitError = self.importProcess.stderr
3592 self.importHeadRevision(revision)
3596 if len(self.changesFile) > 0:
3597 output = open(self.changesFile).readlines()
3600 changeSet.add(int(line))
3602 for change in changeSet:
3603 changes.append(change)
3607 # catch "git p4 sync" with no new branches, in a repo that
3608 # does not have any existing p4 branches
3610 if not self.p4BranchesInGit:
3611 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3613 # The default branch is master, unless --branch is used to
3614 # specify something else. Make sure it exists, or complain
3615 # nicely about how to use --branch.
3616 if not self.detectBranches:
3617 if not branch_exists(self.branch):
3618 if branch_arg_given:
3619 die("Error: branch %s does not exist." % self.branch)
3621 die("Error: no branch %s; perhaps specify one with --branch." %
3625 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3627 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3629 if len(self.maxChanges) > 0:
3630 changes = changes[:min(int(self.maxChanges), len(changes))]
3632 if len(changes) == 0:
3634 print "No changes to import!"
3636 if not self.silent and not self.detectBranches:
3637 print "Import destination: %s" % self.branch
3639 self.updatedBranches = set()
3641 if not self.detectBranches:
3643 # start a new branch
3644 self.initialParent = ""
3646 # build on a previous revision
3647 self.initialParent = parseRevision(self.branch)
3649 self.importChanges(changes)
3653 if len(self.updatedBranches) > 0:
3654 sys.stdout.write("Updated branches: ")
3655 for b in self.updatedBranches:
3656 sys.stdout.write("%s " % b)
3657 sys.stdout.write("\n")
3659 if gitConfigBool("git-p4.importLabels"):
3660 self.importLabels = True
3662 if self.importLabels:
3663 p4Labels = getP4Labels(self.depotPaths)
3664 gitTags = getGitTags()
3666 missingP4Labels = p4Labels - gitTags
3667 self.importP4Labels(self.gitStream, missingP4Labels)
3669 self.gitStream.close()
3670 if self.importProcess.wait() != 0:
3671 die("fast-import failed: %s" % self.gitError.read())
3672 self.gitOutput.close()
3673 self.gitError.close()
3675 # Cleanup temporary branches created during import
3676 if self.tempBranches != []:
3677 for branch in self.tempBranches:
3678 read_pipe("git update-ref -d %s" % branch)
3679 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3681 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3682 # a convenient shortcut refname "p4".
3683 if self.importIntoRemotes:
3684 head_ref = self.refPrefix + "HEAD"
3685 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3686 system(["git", "symbolic-ref", head_ref, self.branch])
3690 class P4Rebase(Command):
3692 Command.__init__(self)
3694 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
3696 self.importLabels = False
3697 self.description = ("Fetches the latest revision from perforce and "
3698 + "rebases the current work (branch) against it")
3700 def run(self, args):
3702 sync.importLabels = self.importLabels
3705 return self.rebase()
3708 if os.system("git update-index --refresh") != 0:
3709 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.");
3710 if len(read_pipe("git diff-index HEAD --")) > 0:
3711 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
3713 [upstream, settings] = findUpstreamBranchPoint()
3714 if len(upstream) == 0:
3715 die("Cannot find upstream branchpoint for rebase")
3717 # the branchpoint may be p4/foo~3, so strip off the parent
3718 upstream = re.sub("~[0-9]+$", "", upstream)
3720 print "Rebasing the current branch onto %s" % upstream
3721 oldHead = read_pipe("git rev-parse HEAD").strip()
3722 system("git rebase %s" % upstream)
3723 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
3726 class P4Clone(P4Sync):
3728 P4Sync.__init__(self)
3729 self.description = "Creates a new git repository and imports from Perforce into it"
3730 self.usage = "usage: %prog [options] //depot/path[@revRange]"
3732 optparse.make_option("--destination", dest="cloneDestination",
3733 action='store', default=None,
3734 help="where to leave result of the clone"),
3735 optparse.make_option("--bare", dest="cloneBare",
3736 action="store_true", default=False),
3738 self.cloneDestination = None
3739 self.needsGit = False
3740 self.cloneBare = False
3742 def defaultDestination(self, args):
3743 ## TODO: use common prefix of args?
3745 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3746 depotDir = re.sub("(#[^#]*)$", "", depotDir)
3747 depotDir = re.sub(r"\.\.\.$", "", depotDir)
3748 depotDir = re.sub(r"/$", "", depotDir)
3749 return os.path.split(depotDir)[1]
3751 def run(self, args):
3755 if self.keepRepoPath and not self.cloneDestination:
3756 sys.stderr.write("Must specify destination for --keep-path\n")
3761 if not self.cloneDestination and len(depotPaths) > 1:
3762 self.cloneDestination = depotPaths[-1]
3763 depotPaths = depotPaths[:-1]
3765 self.cloneExclude = ["/"+p for p in self.cloneExclude]
3766 for p in depotPaths:
3767 if not p.startswith("//"):
3768 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
3771 if not self.cloneDestination:
3772 self.cloneDestination = self.defaultDestination(args)
3774 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
3776 if not os.path.exists(self.cloneDestination):
3777 os.makedirs(self.cloneDestination)
3778 chdir(self.cloneDestination)
3780 init_cmd = [ "git", "init" ]
3782 init_cmd.append("--bare")
3783 retcode = subprocess.call(init_cmd)
3785 raise CalledProcessError(retcode, init_cmd)
3787 if not P4Sync.run(self, depotPaths):
3790 # create a master branch and check out a work tree
3791 if gitBranchExists(self.branch):
3792 system([ "git", "branch", "master", self.branch ])
3793 if not self.cloneBare:
3794 system([ "git", "checkout", "-f" ])
3796 print 'Not checking out any branch, use ' \
3797 '"git checkout -q -b master <branch>"'
3799 # auto-set this variable if invoked with --use-client-spec
3800 if self.useClientSpec_from_options:
3801 system("git config --bool git-p4.useclientspec true")
3805 class P4Branches(Command):
3807 Command.__init__(self)
3809 self.description = ("Shows the git branches that hold imports and their "
3810 + "corresponding perforce depot paths")
3811 self.verbose = False
3813 def run(self, args):
3814 if originP4BranchesExist():
3815 createOrUpdateBranchesFromOrigin()
3817 cmdline = "git rev-parse --symbolic "
3818 cmdline += " --remotes"
3820 for line in read_pipe_lines(cmdline):
3823 if not line.startswith('p4/') or line == "p4/HEAD":
3827 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3828 settings = extractSettingsGitLog(log)
3830 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3833 class HelpFormatter(optparse.IndentedHelpFormatter):
3835 optparse.IndentedHelpFormatter.__init__(self)
3837 def format_description(self, description):
3839 return description + "\n"
3843 def printUsage(commands):
3844 print "usage: %s <command> [options]" % sys.argv[0]
3846 print "valid commands: %s" % ", ".join(commands)
3848 print "Try %s <command> --help for command specific help." % sys.argv[0]
3853 "submit" : P4Submit,
3854 "commit" : P4Submit,
3856 "rebase" : P4Rebase,
3858 "rollback" : P4RollBack,
3859 "branches" : P4Branches
3864 if len(sys.argv[1:]) == 0:
3865 printUsage(commands.keys())
3868 cmdName = sys.argv[1]
3870 klass = commands[cmdName]
3873 print "unknown command %s" % cmdName
3875 printUsage(commands.keys())
3878 options = cmd.options
3879 cmd.gitdir = os.environ.get("GIT_DIR", None)
3883 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
3885 options.append(optparse.make_option("--git-dir", dest="gitdir"))
3887 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3889 description = cmd.description,
3890 formatter = HelpFormatter())
3892 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
3894 verbose = cmd.verbose
3896 if cmd.gitdir == None:
3897 cmd.gitdir = os.path.abspath(".git")
3898 if not isValidGitDir(cmd.gitdir):
3899 # "rev-parse --git-dir" without arguments will try $PWD/.git
3900 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3901 if os.path.exists(cmd.gitdir):
3902 cdup = read_pipe("git rev-parse --show-cdup").strip()
3906 if not isValidGitDir(cmd.gitdir):
3907 if isValidGitDir(cmd.gitdir + "/.git"):
3908 cmd.gitdir += "/.git"
3910 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
3912 # so git commands invoked from the P4 workspace will succeed
3913 os.environ["GIT_DIR"] = cmd.gitdir
3915 if not cmd.run(args):
3920 if __name__ == '__main__':