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>
10 # pylint: disable=invalid-name,missing-docstring,too-many-arguments,broad-except
11 # pylint: disable=no-self-use,wrong-import-position,consider-iterating-dictionary
12 # pylint: disable=wrong-import-order,unused-import,too-few-public-methods
13 # pylint: disable=too-many-lines,ungrouped-imports,fixme,too-many-locals
14 # pylint: disable=line-too-long,bad-whitespace,superfluous-parens
15 # pylint: disable=too-many-statements,too-many-instance-attributes
16 # pylint: disable=too-many-branches,too-many-nested-blocks
19 if sys.version_info.major < 3 and sys.version_info.minor < 7:
20 sys.stderr.write("git-p4: requires Python 2.7 or later.\n")
39 # On python2.7 where raw_input() and input() are both availble,
40 # we want raw_input's semantics, but aliased to input for python3
42 # support basestring in python3
44 if raw_input and input:
51 # Only labels/tags matching this will be imported/exported
52 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
54 # The block size is reduced automatically if required
55 defaultBlockSize = 1<<20
57 p4_access_checked = False
59 def p4_build_cmd(cmd):
60 """Build a suitable p4 command line.
62 This consolidates building and returning a p4 command line into one
63 location. It means that hooking into the environment, or other configuration
64 can be done more easily.
68 user = gitConfig("git-p4.user")
70 real_cmd += ["-u",user]
72 password = gitConfig("git-p4.password")
74 real_cmd += ["-P", password]
76 port = gitConfig("git-p4.port")
78 real_cmd += ["-p", port]
80 host = gitConfig("git-p4.host")
82 real_cmd += ["-H", host]
84 client = gitConfig("git-p4.client")
86 real_cmd += ["-c", client]
88 retries = gitConfigInt("git-p4.retries")
90 # Perform 3 retries by default
93 # Provide a way to not pass this option by setting git-p4.retries to 0
94 real_cmd += ["-r", str(retries)]
96 if not isinstance(cmd, list):
97 real_cmd = ' '.join(real_cmd) + ' ' + cmd
101 # now check that we can actually talk to the server
102 global p4_access_checked
103 if not p4_access_checked:
104 p4_access_checked = True # suppress access checks in p4_check_access itself
110 """ Return TRUE if the given path is a git directory (/path/to/dir/.git).
111 This won't automatically add ".git" to a directory.
113 d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
114 if not d or len(d) == 0:
119 def chdir(path, is_client_path=False):
120 """Do chdir to the given path, and set the PWD environment
121 variable for use by P4. It does not look at getcwd() output.
122 Since we're not using the shell, it is necessary to set the
123 PWD environment variable explicitly.
125 Normally, expand the path to force it to be absolute. This
126 addresses the use of relative path names inside P4 settings,
127 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
128 as given; it looks for .p4config using PWD.
130 If is_client_path, the path was handed to us directly by p4,
131 and may be a symbolic link. Do not call os.getcwd() in this
132 case, because it will cause p4 to think that PWD is not inside
137 if not is_client_path:
139 os.environ['PWD'] = path
142 """Return free space in bytes on the disk of the given dirname."""
143 if platform.system() == 'Windows':
144 free_bytes = ctypes.c_ulonglong(0)
145 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
146 return free_bytes.value
148 st = os.statvfs(os.getcwd())
149 return st.f_bavail * st.f_frsize
152 """ Terminate execution. Make sure that any running child processes have been wait()ed for before
158 sys.stderr.write(msg + "\n")
161 def prompt(prompt_text):
162 """ Prompt the user to choose one of the choices
164 Choices are identified in the prompt_text by square brackets around
165 a single letter option.
167 choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text))
170 sys.stdout.write(prompt_text)
172 response=sys.stdin.readline().strip().lower()
175 response = response[0]
176 if response in choices:
179 # We need different encoding/decoding strategies for text data being passed
180 # around in pipes depending on python version
182 # For python3, always encode and decode as appropriate
183 def decode_text_stream(s):
184 return s.decode() if isinstance(s, bytes) else s
185 def encode_text_stream(s):
186 return s.encode() if isinstance(s, str) else s
188 # For python2.7, pass read strings as-is, but also allow writing unicode
189 def decode_text_stream(s):
191 def encode_text_stream(s):
192 return s.encode('utf_8') if isinstance(s, unicode) else s
194 def decode_path(path):
195 """Decode a given string (bytes or otherwise) using configured path encoding options
197 encoding = gitConfig('git-p4.pathEncoding') or 'utf_8'
199 return path.decode(encoding, errors='replace') if isinstance(path, bytes) else path
204 path = path.decode(encoding, errors='replace')
206 print('Path with non-ASCII characters detected. Used {} to decode: {}'.format(encoding, path))
209 def run_git_hook(cmd, param=[]):
210 """args are specified with -a <arg> -a <arg> -a <arg>"""
211 args = ['git', 'hook', 'run', '--ignore-missing', cmd]
216 return subprocess.call(args) == 0
218 def write_pipe(c, stdin):
220 sys.stderr.write('Writing pipe: %s\n' % str(c))
222 expand = not isinstance(c, list)
223 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
225 val = pipe.write(stdin)
228 die('Command failed: %s' % str(c))
232 def p4_write_pipe(c, stdin):
233 real_cmd = p4_build_cmd(c)
234 if bytes is not str and isinstance(stdin, str):
235 stdin = encode_text_stream(stdin)
236 return write_pipe(real_cmd, stdin)
238 def read_pipe_full(c):
239 """ Read output from command. Returns a tuple
240 of the return status, stdout text and stderr
244 sys.stderr.write('Reading pipe: %s\n' % str(c))
246 expand = not isinstance(c, list)
247 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
248 (out, err) = p.communicate()
249 return (p.returncode, out, decode_text_stream(err))
251 def read_pipe(c, ignore_error=False, raw=False):
252 """ Read output from command. Returns the output text on
253 success. On failure, terminates execution, unless
254 ignore_error is True, when it returns an empty string.
256 If raw is True, do not attempt to decode output text.
258 (retcode, out, err) = read_pipe_full(c)
263 die('Command failed: %s\nError: %s' % (str(c), err))
265 out = decode_text_stream(out)
268 def read_pipe_text(c):
269 """ Read output from a command with trailing whitespace stripped.
270 On error, returns None.
272 (retcode, out, err) = read_pipe_full(c)
276 return decode_text_stream(out).rstrip()
278 def p4_read_pipe(c, ignore_error=False, raw=False):
279 real_cmd = p4_build_cmd(c)
280 return read_pipe(real_cmd, ignore_error, raw=raw)
282 def read_pipe_lines(c):
284 sys.stderr.write('Reading pipe: %s\n' % str(c))
286 expand = not isinstance(c, list)
287 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
289 val = [decode_text_stream(line) for line in pipe.readlines()]
290 if pipe.close() or p.wait():
291 die('Command failed: %s' % str(c))
294 def p4_read_pipe_lines(c):
295 """Specifically invoke p4 on the command supplied. """
296 real_cmd = p4_build_cmd(c)
297 return read_pipe_lines(real_cmd)
299 def p4_has_command(cmd):
300 """Ask p4 for help on this command. If it returns an error, the
301 command does not exist in this version of p4."""
302 real_cmd = p4_build_cmd(["help", cmd])
303 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
304 stderr=subprocess.PIPE)
306 return p.returncode == 0
308 def p4_has_move_command():
309 """See if the move command exists, that it supports -k, and that
310 it has not been administratively disabled. The arguments
311 must be correct, but the filenames do not have to exist. Use
312 ones with wildcards so even if they exist, it will fail."""
314 if not p4_has_command("move"):
316 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
317 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
318 (out, err) = p.communicate()
319 err = decode_text_stream(err)
320 # return code will be 1 in either case
321 if err.find("Invalid option") >= 0:
323 if err.find("disabled") >= 0:
325 # assume it failed because @... was invalid changelist
328 def system(cmd, ignore_error=False):
329 expand = not isinstance(cmd, list)
331 sys.stderr.write("executing %s\n" % str(cmd))
332 retcode = subprocess.call(cmd, shell=expand)
333 if retcode and not ignore_error:
334 raise CalledProcessError(retcode, cmd)
339 """Specifically invoke p4 as the system command. """
340 real_cmd = p4_build_cmd(cmd)
341 expand = not isinstance(real_cmd, list)
342 retcode = subprocess.call(real_cmd, shell=expand)
344 raise CalledProcessError(retcode, real_cmd)
346 def die_bad_access(s):
347 die("failure accessing depot: {0}".format(s.rstrip()))
349 def p4_check_access(min_expiration=1):
350 """ Check if we can access Perforce - account still logged in
352 results = p4CmdList(["login", "-s"])
354 if len(results) == 0:
355 # should never get here: always get either some results, or a p4ExitCode
356 assert("could not parse response from perforce")
360 if 'p4ExitCode' in result:
361 # p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
362 die_bad_access("could not run p4")
364 code = result.get("code")
366 # we get here if we couldn't connect and there was nothing to unmarshal
367 die_bad_access("could not connect")
370 expiry = result.get("TicketExpiration")
373 if expiry > min_expiration:
377 die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
380 # account without a timeout - all ok
383 elif code == "error":
384 data = result.get("data")
386 die_bad_access("p4 error: {0}".format(data))
388 die_bad_access("unknown error")
392 die_bad_access("unknown error code {0}".format(code))
394 _p4_version_string = None
395 def p4_version_string():
396 """Read the version string, showing just the last line, which
397 hopefully is the interesting version bit.
400 Perforce - The Fast Software Configuration Management System.
401 Copyright 1995-2011 Perforce Software. All rights reserved.
402 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
404 global _p4_version_string
405 if not _p4_version_string:
406 a = p4_read_pipe_lines(["-V"])
407 _p4_version_string = a[-1].rstrip()
408 return _p4_version_string
410 def p4_integrate(src, dest):
411 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
413 def p4_sync(f, *options):
414 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
417 # forcibly add file names with wildcards
418 if wildcard_present(f):
419 p4_system(["add", "-f", f])
421 p4_system(["add", f])
424 p4_system(["delete", wildcard_encode(f)])
426 def p4_edit(f, *options):
427 p4_system(["edit"] + list(options) + [wildcard_encode(f)])
430 p4_system(["revert", wildcard_encode(f)])
432 def p4_reopen(type, f):
433 p4_system(["reopen", "-t", type, wildcard_encode(f)])
435 def p4_reopen_in_change(changelist, files):
436 cmd = ["reopen", "-c", str(changelist)] + files
439 def p4_move(src, dest):
440 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
442 def p4_last_change():
443 results = p4CmdList(["changes", "-m", "1"], skip_info=True)
444 return int(results[0]['change'])
446 def p4_describe(change, shelved=False):
447 """Make sure it returns a valid result by checking for
448 the presence of field "time". Return a dict of the
451 cmd = ["describe", "-s"]
456 ds = p4CmdList(cmd, skip_info=True)
458 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
462 if "p4ExitCode" in d:
463 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
466 if d["code"] == "error":
467 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
470 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
475 # Canonicalize the p4 type and return a tuple of the
476 # base type, plus any modifiers. See "p4 help filetypes"
477 # for a list and explanation.
479 def split_p4_type(p4type):
481 p4_filetypes_historical = {
482 "ctempobj": "binary+Sw",
488 "tempobj": "binary+FSw",
489 "ubinary": "binary+F",
490 "uresource": "resource+F",
491 "uxbinary": "binary+Fx",
492 "xbinary": "binary+x",
494 "xtempobj": "binary+Swx",
496 "xunicode": "unicode+x",
499 if p4type in p4_filetypes_historical:
500 p4type = p4_filetypes_historical[p4type]
502 s = p4type.split("+")
510 # return the raw p4 type of a file (text, text+ko, etc)
513 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
514 return results[0]['headType']
517 # Given a type base and modifier, return a regexp matching
518 # the keywords that can be expanded in the file
520 def p4_keywords_regexp_for_type(base, type_mods):
521 if base in ("text", "unicode", "binary"):
523 if "ko" in type_mods:
525 elif "k" in type_mods:
526 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
530 \$ # Starts with a dollar, followed by...
531 (%s) # one of the keywords, followed by...
532 (:[^$\n]+)? # possibly an old expansion, followed by...
540 # Given a file, return a regexp matching the possible
541 # RCS keywords that will be expanded, or None for files
542 # with kw expansion turned off.
544 def p4_keywords_regexp_for_file(file):
545 if not os.path.exists(file):
548 (type_base, type_mods) = split_p4_type(p4_type(file))
549 return p4_keywords_regexp_for_type(type_base, type_mods)
551 def setP4ExecBit(file, mode):
552 # Reopens an already open file and changes the execute bit to match
553 # the execute bit setting in the passed in mode.
557 if not isModeExec(mode):
558 p4Type = getP4OpenedType(file)
559 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
560 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
561 if p4Type[-1] == "+":
562 p4Type = p4Type[0:-1]
564 p4_reopen(p4Type, file)
566 def getP4OpenedType(file):
567 # Returns the perforce file type for the given file.
569 result = p4_read_pipe(["opened", wildcard_encode(file)])
570 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
572 return match.group(1)
574 die("Could not determine file type for %s (result: '%s')" % (file, result))
576 # Return the set of all p4 labels
577 def getP4Labels(depotPaths):
579 if not isinstance(depotPaths, list):
580 depotPaths = [depotPaths]
582 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
588 # Return the set of all git tags
591 for line in read_pipe_lines(["git", "tag"]):
596 _diff_tree_pattern = None
598 def parseDiffTreeEntry(entry):
599 """Parses a single diff tree entry into its component elements.
601 See git-diff-tree(1) manpage for details about the format of the diff
602 output. This method returns a dictionary with the following elements:
604 src_mode - The mode of the source file
605 dst_mode - The mode of the destination file
606 src_sha1 - The sha1 for the source file
607 dst_sha1 - The sha1 fr the destination file
608 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
609 status_score - The score for the status (applicable for 'C' and 'R'
610 statuses). This is None if there is no score.
611 src - The path for the source file.
612 dst - The path for the destination file. This is only present for
613 copy or renames. If it is not present, this is None.
615 If the pattern is not matched, None is returned."""
617 global _diff_tree_pattern
618 if not _diff_tree_pattern:
619 _diff_tree_pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
621 match = _diff_tree_pattern.match(entry)
624 'src_mode': match.group(1),
625 'dst_mode': match.group(2),
626 'src_sha1': match.group(3),
627 'dst_sha1': match.group(4),
628 'status': match.group(5),
629 'status_score': match.group(6),
630 'src': match.group(7),
631 'dst': match.group(10)
635 def isModeExec(mode):
636 # Returns True if the given git mode represents an executable file,
638 return mode[-3:] == "755"
640 class P4Exception(Exception):
641 """ Base class for exceptions from the p4 client """
642 def __init__(self, exit_code):
643 self.p4ExitCode = exit_code
645 class P4ServerException(P4Exception):
646 """ Base class for exceptions where we get some kind of marshalled up result from the server """
647 def __init__(self, exit_code, p4_result):
648 super(P4ServerException, self).__init__(exit_code)
649 self.p4_result = p4_result
650 self.code = p4_result[0]['code']
651 self.data = p4_result[0]['data']
653 class P4RequestSizeException(P4ServerException):
654 """ One of the maxresults or maxscanrows errors """
655 def __init__(self, exit_code, p4_result, limit):
656 super(P4RequestSizeException, self).__init__(exit_code, p4_result)
659 class P4CommandException(P4Exception):
660 """ Something went wrong calling p4 which means we have to give up """
661 def __init__(self, msg):
667 def isModeExecChanged(src_mode, dst_mode):
668 return isModeExec(src_mode) != isModeExec(dst_mode)
670 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
671 errors_as_exceptions=False):
673 if not isinstance(cmd, list):
680 cmd = p4_build_cmd(cmd)
682 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
684 # Use a temporary file to avoid deadlocks without
685 # subprocess.communicate(), which would put another copy
686 # of stdout into memory.
688 if stdin is not None:
689 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
690 if not isinstance(stdin, list):
691 stdin_file.write(stdin)
694 stdin_file.write(encode_text_stream(i))
695 stdin_file.write(b'\n')
699 p4 = subprocess.Popen(cmd,
702 stdout=subprocess.PIPE)
707 entry = marshal.load(p4.stdout)
709 # Decode unmarshalled dict to use str keys and values, except for:
710 # - `data` which may contain arbitrary binary data
711 # - `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text
713 for key, value in entry.items():
715 if isinstance(value, bytes) and not (key in ('data', 'path', 'clientFile') or key.startswith('depotFile')):
716 value = value.decode()
717 decoded_entry[key] = value
718 # Parse out data if it's an error response
719 if decoded_entry.get('code') == 'error' and 'data' in decoded_entry:
720 decoded_entry['data'] = decoded_entry['data'].decode()
721 entry = decoded_entry
723 if 'code' in entry and entry['code'] == 'info':
733 if errors_as_exceptions:
735 data = result[0].get('data')
737 m = re.search('Too many rows scanned \(over (\d+)\)', data)
739 m = re.search('Request too large \(over (\d+)\)', data)
742 limit = int(m.group(1))
743 raise P4RequestSizeException(exitCode, result, limit)
745 raise P4ServerException(exitCode, result)
747 raise P4Exception(exitCode)
750 entry["p4ExitCode"] = exitCode
756 list = p4CmdList(cmd)
762 def p4Where(depotPath):
763 if not depotPath.endswith("/"):
765 depotPathLong = depotPath + "..."
766 outputList = p4CmdList(["where", depotPathLong])
768 for entry in outputList:
769 if "depotFile" in entry:
770 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
771 # The base path always ends with "/...".
772 entry_path = decode_path(entry['depotFile'])
773 if entry_path.find(depotPath) == 0 and entry_path[-4:] == "/...":
776 elif "data" in entry:
777 data = entry.get("data")
778 space = data.find(" ")
779 if data[:space] == depotPath:
784 if output["code"] == "error":
788 clientPath = decode_path(output['path'])
789 elif "data" in output:
790 data = output.get("data")
791 lastSpace = data.rfind(b" ")
792 clientPath = decode_path(data[lastSpace + 1:])
794 if clientPath.endswith("..."):
795 clientPath = clientPath[:-3]
798 def currentGitBranch():
799 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
801 def isValidGitDir(path):
802 return git_dir(path) != None
804 def parseRevision(ref):
805 return read_pipe("git rev-parse %s" % ref).strip()
807 def branchExists(ref):
808 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
812 def extractLogMessageFromGitCommit(commit):
815 ## fixme: title is first line of commit, not 1st paragraph.
817 for log in read_pipe_lines(["git", "cat-file", "commit", commit]):
826 def extractSettingsGitLog(log):
828 for line in log.split("\n"):
830 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
834 assignments = m.group(1).split (':')
835 for a in assignments:
837 key = vals[0].strip()
838 val = ('='.join (vals[1:])).strip()
839 if val.endswith ('\"') and val.startswith('"'):
844 paths = values.get("depot-paths")
846 paths = values.get("depot-path")
848 values['depot-paths'] = paths.split(',')
851 def gitBranchExists(branch):
852 proc = subprocess.Popen(["git", "rev-parse", branch],
853 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
854 return proc.wait() == 0;
856 def gitUpdateRef(ref, newvalue):
857 subprocess.check_call(["git", "update-ref", ref, newvalue])
859 def gitDeleteRef(ref):
860 subprocess.check_call(["git", "update-ref", "-d", ref])
864 def gitConfig(key, typeSpecifier=None):
865 if key not in _gitConfig:
866 cmd = [ "git", "config" ]
868 cmd += [ typeSpecifier ]
870 s = read_pipe(cmd, ignore_error=True)
871 _gitConfig[key] = s.strip()
872 return _gitConfig[key]
874 def gitConfigBool(key):
875 """Return a bool, using git config --bool. It is True only if the
876 variable is set to true, and False if set to false or not present
879 if key not in _gitConfig:
880 _gitConfig[key] = gitConfig(key, '--bool') == "true"
881 return _gitConfig[key]
883 def gitConfigInt(key):
884 if key not in _gitConfig:
885 cmd = [ "git", "config", "--int", key ]
886 s = read_pipe(cmd, ignore_error=True)
889 _gitConfig[key] = int(gitConfig(key, '--int'))
891 _gitConfig[key] = None
892 return _gitConfig[key]
894 def gitConfigList(key):
895 if key not in _gitConfig:
896 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
897 _gitConfig[key] = s.strip().splitlines()
898 if _gitConfig[key] == ['']:
900 return _gitConfig[key]
902 def p4BranchesInGit(branchesAreInRemotes=True):
903 """Find all the branches whose names start with "p4/", looking
904 in remotes or heads as specified by the argument. Return
905 a dictionary of { branch: revision } for each one found.
906 The branch names are the short names, without any
911 cmdline = "git rev-parse --symbolic "
912 if branchesAreInRemotes:
913 cmdline += "--remotes"
915 cmdline += "--branches"
917 for line in read_pipe_lines(cmdline):
921 if not line.startswith('p4/'):
923 # special symbolic ref to p4/master
924 if line == "p4/HEAD":
927 # strip off p4/ prefix
928 branch = line[len("p4/"):]
930 branches[branch] = parseRevision(line)
934 def branch_exists(branch):
935 """Make sure that the given ref name really exists."""
937 cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
938 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
939 out, _ = p.communicate()
940 out = decode_text_stream(out)
943 # expect exactly one line of output: the branch name
944 return out.rstrip() == branch
946 def findUpstreamBranchPoint(head = "HEAD"):
947 branches = p4BranchesInGit()
948 # map from depot-path to branch name
949 branchByDepotPath = {}
950 for branch in branches.keys():
951 tip = branches[branch]
952 log = extractLogMessageFromGitCommit(tip)
953 settings = extractSettingsGitLog(log)
954 if "depot-paths" in settings:
955 paths = ",".join(settings["depot-paths"])
956 branchByDepotPath[paths] = "remotes/p4/" + branch
960 while parent < 65535:
961 commit = head + "~%s" % parent
962 log = extractLogMessageFromGitCommit(commit)
963 settings = extractSettingsGitLog(log)
964 if "depot-paths" in settings:
965 paths = ",".join(settings["depot-paths"])
966 if paths in branchByDepotPath:
967 return [branchByDepotPath[paths], settings]
971 return ["", settings]
973 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
975 print("Creating/updating branch(es) in %s based on origin branch(es)"
978 originPrefix = "origin/p4/"
980 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
982 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
985 headName = line[len(originPrefix):]
986 remoteHead = localRefPrefix + headName
989 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
990 if ('depot-paths' not in original
991 or 'change' not in original):
995 if not gitBranchExists(remoteHead):
997 print("creating %s" % remoteHead)
1000 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
1001 if 'change' in settings:
1002 if settings['depot-paths'] == original['depot-paths']:
1003 originP4Change = int(original['change'])
1004 p4Change = int(settings['change'])
1005 if originP4Change > p4Change:
1006 print("%s (%s) is newer than %s (%s). "
1007 "Updating p4 branch from origin."
1008 % (originHead, originP4Change,
1009 remoteHead, p4Change))
1012 print("Ignoring: %s was imported from %s while "
1013 "%s was imported from %s"
1014 % (originHead, ','.join(original['depot-paths']),
1015 remoteHead, ','.join(settings['depot-paths'])))
1018 system("git update-ref %s %s" % (remoteHead, originHead))
1020 def originP4BranchesExist():
1021 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
1024 def p4ParseNumericChangeRange(parts):
1025 changeStart = int(parts[0][1:])
1026 if parts[1] == '#head':
1027 changeEnd = p4_last_change()
1029 changeEnd = int(parts[1])
1031 return (changeStart, changeEnd)
1033 def chooseBlockSize(blockSize):
1037 return defaultBlockSize
1039 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
1042 # Parse the change range into start and end. Try to find integer
1043 # revision ranges as these can be broken up into blocks to avoid
1044 # hitting server-side limits (maxrows, maxscanresults). But if
1045 # that doesn't work, fall back to using the raw revision specifier
1046 # strings, without using block mode.
1048 if changeRange is None or changeRange == '':
1050 changeEnd = p4_last_change()
1051 block_size = chooseBlockSize(requestedBlockSize)
1053 parts = changeRange.split(',')
1054 assert len(parts) == 2
1056 (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
1057 block_size = chooseBlockSize(requestedBlockSize)
1059 changeStart = parts[0][1:]
1060 changeEnd = parts[1]
1061 if requestedBlockSize:
1062 die("cannot use --changes-block-size with non-numeric revisions")
1067 # Retrieve changes a block at a time, to prevent running
1068 # into a MaxResults/MaxScanRows error from the server. If
1069 # we _do_ hit one of those errors, turn down the block size
1075 end = min(changeEnd, changeStart + block_size)
1076 revisionRange = "%d,%d" % (changeStart, end)
1078 revisionRange = "%s,%s" % (changeStart, changeEnd)
1080 for p in depotPaths:
1081 cmd += ["%s...@%s" % (p, revisionRange)]
1085 result = p4CmdList(cmd, errors_as_exceptions=True)
1086 except P4RequestSizeException as e:
1088 block_size = e.limit
1089 elif block_size > e.limit:
1090 block_size = e.limit
1092 block_size = max(2, block_size // 2)
1094 if verbose: print("block size error, retrying with block size {0}".format(block_size))
1096 except P4Exception as e:
1097 die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1099 # Insert changes in chronological order
1100 for entry in reversed(result):
1101 if 'change' not in entry:
1103 changes.add(int(entry['change']))
1108 if end >= changeEnd:
1111 changeStart = end + 1
1113 changes = sorted(changes)
1116 def p4PathStartsWith(path, prefix):
1117 # This method tries to remedy a potential mixed-case issue:
1119 # If UserA adds //depot/DirA/file1
1120 # and UserB adds //depot/dira/file2
1122 # we may or may not have a problem. If you have core.ignorecase=true,
1123 # we treat DirA and dira as the same directory
1124 if gitConfigBool("core.ignorecase"):
1125 return path.lower().startswith(prefix.lower())
1126 return path.startswith(prefix)
1128 def getClientSpec():
1129 """Look at the p4 client spec, create a View() object that contains
1130 all the mappings, and return it."""
1132 specList = p4CmdList("client -o")
1133 if len(specList) != 1:
1134 die('Output from "client -o" is %d lines, expecting 1' %
1137 # dictionary of all client parameters
1140 # the //client/ name
1141 client_name = entry["Client"]
1143 # just the keys that start with "View"
1144 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
1146 # hold this new View
1147 view = View(client_name)
1149 # append the lines, in order, to the view
1150 for view_num in range(len(view_keys)):
1151 k = "View%d" % view_num
1152 if k not in view_keys:
1153 die("Expected view key %s missing" % k)
1154 view.append(entry[k])
1158 def getClientRoot():
1159 """Grab the client directory."""
1161 output = p4CmdList("client -o")
1162 if len(output) != 1:
1163 die('Output from "client -o" is %d lines, expecting 1' % len(output))
1166 if "Root" not in entry:
1167 die('Client has no "Root"')
1169 return entry["Root"]
1172 # P4 wildcards are not allowed in filenames. P4 complains
1173 # if you simply add them, but you can force it with "-f", in
1174 # which case it translates them into %xx encoding internally.
1176 def wildcard_decode(path):
1177 # Search for and fix just these four characters. Do % last so
1178 # that fixing it does not inadvertently create new %-escapes.
1179 # Cannot have * in a filename in windows; untested as to
1180 # what p4 would do in such a case.
1181 if not platform.system() == "Windows":
1182 path = path.replace("%2A", "*")
1183 path = path.replace("%23", "#") \
1184 .replace("%40", "@") \
1185 .replace("%25", "%")
1188 def wildcard_encode(path):
1189 # do % first to avoid double-encoding the %s introduced here
1190 path = path.replace("%", "%25") \
1191 .replace("*", "%2A") \
1192 .replace("#", "%23") \
1193 .replace("@", "%40")
1196 def wildcard_present(path):
1197 m = re.search("[*#@%]", path)
1198 return m is not None
1200 class LargeFileSystem(object):
1201 """Base class for large file system support."""
1203 def __init__(self, writeToGitStream):
1204 self.largeFiles = set()
1205 self.writeToGitStream = writeToGitStream
1207 def generatePointer(self, cloneDestination, contentFile):
1208 """Return the content of a pointer file that is stored in Git instead of
1209 the actual content."""
1210 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1212 def pushFile(self, localLargeFile):
1213 """Push the actual content which is not stored in the Git repository to
1215 assert False, "Method 'pushFile' required in " + self.__class__.__name__
1217 def hasLargeFileExtension(self, relPath):
1218 return functools.reduce(
1219 lambda a, b: a or b,
1220 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1224 def generateTempFile(self, contents):
1225 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1227 contentFile.write(d)
1229 return contentFile.name
1231 def exceedsLargeFileThreshold(self, relPath, contents):
1232 if gitConfigInt('git-p4.largeFileThreshold'):
1233 contentsSize = sum(len(d) for d in contents)
1234 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1236 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1237 contentsSize = sum(len(d) for d in contents)
1238 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1240 contentTempFile = self.generateTempFile(contents)
1241 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True)
1242 with zipfile.ZipFile(compressedContentFile, mode='w') as zf:
1243 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1244 compressedContentsSize = zf.infolist()[0].compress_size
1245 os.remove(contentTempFile)
1246 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1250 def addLargeFile(self, relPath):
1251 self.largeFiles.add(relPath)
1253 def removeLargeFile(self, relPath):
1254 self.largeFiles.remove(relPath)
1256 def isLargeFile(self, relPath):
1257 return relPath in self.largeFiles
1259 def processContent(self, git_mode, relPath, contents):
1260 """Processes the content of git fast import. This method decides if a
1261 file is stored in the large file system and handles all necessary
1263 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1264 contentTempFile = self.generateTempFile(contents)
1265 (pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
1266 if pointer_git_mode:
1267 git_mode = pointer_git_mode
1269 # Move temp file to final location in large file system
1270 largeFileDir = os.path.dirname(localLargeFile)
1271 if not os.path.isdir(largeFileDir):
1272 os.makedirs(largeFileDir)
1273 shutil.move(contentTempFile, localLargeFile)
1274 self.addLargeFile(relPath)
1275 if gitConfigBool('git-p4.largeFilePush'):
1276 self.pushFile(localLargeFile)
1278 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1279 return (git_mode, contents)
1281 class MockLFS(LargeFileSystem):
1282 """Mock large file system for testing."""
1284 def generatePointer(self, contentFile):
1285 """The pointer content is the original content prefixed with "pointer-".
1286 The local filename of the large file storage is derived from the file content.
1288 with open(contentFile, 'r') as f:
1291 pointerContents = 'pointer-' + content
1292 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1293 return (gitMode, pointerContents, localLargeFile)
1295 def pushFile(self, localLargeFile):
1296 """The remote filename of the large file storage is the same as the local
1297 one but in a different directory.
1299 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1300 if not os.path.exists(remotePath):
1301 os.makedirs(remotePath)
1302 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1304 class GitLFS(LargeFileSystem):
1305 """Git LFS as backend for the git-p4 large file system.
1306 See https://git-lfs.github.com/ for details."""
1308 def __init__(self, *args):
1309 LargeFileSystem.__init__(self, *args)
1310 self.baseGitAttributes = []
1312 def generatePointer(self, contentFile):
1313 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1314 mode and content which is stored in the Git repository instead of
1315 the actual content. Return also the new location of the actual
1318 if os.path.getsize(contentFile) == 0:
1319 return (None, '', None)
1321 pointerProcess = subprocess.Popen(
1322 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1323 stdout=subprocess.PIPE
1325 pointerFile = decode_text_stream(pointerProcess.stdout.read())
1326 if pointerProcess.wait():
1327 os.remove(contentFile)
1328 die('git-lfs pointer command failed. Did you install the extension?')
1330 # Git LFS removed the preamble in the output of the 'pointer' command
1331 # starting from version 1.2.0. Check for the preamble here to support
1333 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1334 if pointerFile.startswith('Git LFS pointer for'):
1335 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1337 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
1338 # if someone use external lfs.storage ( not in local repo git )
1339 lfs_path = gitConfig('lfs.storage')
1342 if not os.path.isabs(lfs_path):
1343 lfs_path = os.path.join(os.getcwd(), '.git', lfs_path)
1344 localLargeFile = os.path.join(
1346 'objects', oid[:2], oid[2:4],
1349 # LFS Spec states that pointer files should not have the executable bit set.
1351 return (gitMode, pointerFile, localLargeFile)
1353 def pushFile(self, localLargeFile):
1354 uploadProcess = subprocess.Popen(
1355 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1357 if uploadProcess.wait():
1358 die('git-lfs push command failed. Did you define a remote?')
1360 def generateGitAttributes(self):
1362 self.baseGitAttributes +
1366 '# Git LFS (see https://git-lfs.github.com/)\n',
1369 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1370 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1372 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1373 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1377 def addLargeFile(self, relPath):
1378 LargeFileSystem.addLargeFile(self, relPath)
1379 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1381 def removeLargeFile(self, relPath):
1382 LargeFileSystem.removeLargeFile(self, relPath)
1383 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1385 def processContent(self, git_mode, relPath, contents):
1386 if relPath == '.gitattributes':
1387 self.baseGitAttributes = contents
1388 return (git_mode, self.generateGitAttributes())
1390 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1393 delete_actions = ( "delete", "move/delete", "purge" )
1394 add_actions = ( "add", "branch", "move/add" )
1397 self.usage = "usage: %prog [options]"
1398 self.needsGit = True
1399 self.verbose = False
1401 # This is required for the "append" update_shelve action
1402 def ensure_value(self, attr, value):
1403 if not hasattr(self, attr) or getattr(self, attr) is None:
1404 setattr(self, attr, value)
1405 return getattr(self, attr)
1409 self.userMapFromPerforceServer = False
1410 self.myP4UserId = None
1414 return self.myP4UserId
1416 results = p4CmdList("user -o")
1419 self.myP4UserId = r['User']
1421 die("Could not find your p4 user id")
1423 def p4UserIsMe(self, p4User):
1424 # return True if the given p4 user is actually me
1425 me = self.p4UserId()
1426 if not p4User or p4User != me:
1431 def getUserCacheFilename(self):
1432 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1433 return home + "/.gitp4-usercache.txt"
1435 def getUserMapFromPerforceServer(self):
1436 if self.userMapFromPerforceServer:
1441 for output in p4CmdList("users"):
1442 if "User" not in output:
1444 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1445 self.emails[output["Email"]] = output["User"]
1447 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1448 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1449 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1450 if mapUser and len(mapUser[0]) == 3:
1451 user = mapUser[0][0]
1452 fullname = mapUser[0][1]
1453 email = mapUser[0][2]
1454 self.users[user] = fullname + " <" + email + ">"
1455 self.emails[email] = user
1458 for (key, val) in self.users.items():
1459 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1461 open(self.getUserCacheFilename(), 'w').write(s)
1462 self.userMapFromPerforceServer = True
1464 def loadUserMapFromCache(self):
1466 self.userMapFromPerforceServer = False
1468 cache = open(self.getUserCacheFilename(), 'r')
1469 lines = cache.readlines()
1472 entry = line.strip().split("\t")
1473 self.users[entry[0]] = entry[1]
1475 self.getUserMapFromPerforceServer()
1477 class P4Debug(Command):
1479 Command.__init__(self)
1481 self.description = "A tool to debug the output of p4 -G."
1482 self.needsGit = False
1484 def run(self, args):
1486 for output in p4CmdList(args):
1487 print('Element: %d' % j)
1492 class P4RollBack(Command):
1494 Command.__init__(self)
1496 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1498 self.description = "A tool to debug the multi-branch import. Don't use :)"
1499 self.rollbackLocalBranches = False
1501 def run(self, args):
1504 maxChange = int(args[0])
1506 if "p4ExitCode" in p4Cmd("changes -m 1"):
1507 die("Problems executing p4");
1509 if self.rollbackLocalBranches:
1510 refPrefix = "refs/heads/"
1511 lines = read_pipe_lines("git rev-parse --symbolic --branches")
1513 refPrefix = "refs/remotes/"
1514 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1517 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1519 ref = refPrefix + line
1520 log = extractLogMessageFromGitCommit(ref)
1521 settings = extractSettingsGitLog(log)
1523 depotPaths = settings['depot-paths']
1524 change = settings['change']
1528 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
1529 for p in depotPaths]))) == 0:
1530 print("Branch %s did not exist at change %s, deleting." % (ref, maxChange))
1531 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1534 while change and int(change) > maxChange:
1537 print("%s is at %s ; rewinding towards %s" % (ref, change, maxChange))
1538 system("git update-ref %s \"%s^\"" % (ref, ref))
1539 log = extractLogMessageFromGitCommit(ref)
1540 settings = extractSettingsGitLog(log)
1543 depotPaths = settings['depot-paths']
1544 change = settings['change']
1547 print("%s rewound to %s" % (ref, change))
1551 class P4Submit(Command, P4UserMap):
1553 conflict_behavior_choices = ("ask", "skip", "quit")
1556 Command.__init__(self)
1557 P4UserMap.__init__(self)
1559 optparse.make_option("--origin", dest="origin"),
1560 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1561 # preserve the user, requires relevant p4 permissions
1562 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1563 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1564 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1565 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1566 optparse.make_option("--conflict", dest="conflict_behavior",
1567 choices=self.conflict_behavior_choices),
1568 optparse.make_option("--branch", dest="branch"),
1569 optparse.make_option("--shelve", dest="shelve", action="store_true",
1570 help="Shelve instead of submit. Shelved files are reverted, "
1571 "restoring the workspace to the state before the shelve"),
1572 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
1573 metavar="CHANGELIST",
1574 help="update an existing shelved changelist, implies --shelve, "
1575 "repeat in-order for multiple shelved changelists"),
1576 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1577 help="submit only the specified commit(s), one commit or xxx..xxx"),
1578 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1579 help="Disable rebase after submit is completed. Can be useful if you "
1580 "work from a local git branch that is not master"),
1581 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1582 help="Skip Perforce sync of p4/master after submit or shelve"),
1583 optparse.make_option("--no-verify", dest="no_verify", action="store_true",
1584 help="Bypass p4-pre-submit and p4-changelist hooks"),
1586 self.description = """Submit changes from git to the perforce depot.\n
1587 The `p4-pre-submit` hook is executed if it exists and is executable. It
1588 can be bypassed with the `--no-verify` command line option. The hook takes
1589 no parameters and nothing from standard input. Exiting with a non-zero status
1590 from this script prevents `git-p4 submit` from launching.
1592 One usage scenario is to run unit tests in the hook.
1594 The `p4-prepare-changelist` hook is executed right after preparing the default
1595 changelist message and before the editor is started. It takes one parameter,
1596 the name of the file that contains the changelist text. Exiting with a non-zero
1597 status from the script will abort the process.
1599 The purpose of the hook is to edit the message file in place, and it is not
1600 supressed by the `--no-verify` option. This hook is called even if
1601 `--prepare-p4-only` is set.
1603 The `p4-changelist` hook is executed after the changelist message has been
1604 edited by the user. It can be bypassed with the `--no-verify` option. It
1605 takes a single parameter, the name of the file that holds the proposed
1606 changelist text. Exiting with a non-zero status causes the command to abort.
1608 The hook is allowed to edit the changelist file and can be used to normalize
1609 the text into some project standard format. It can also be used to refuse the
1610 Submit after inspect the message file.
1612 The `p4-post-changelist` hook is invoked after the submit has successfully
1613 occurred in P4. It takes no parameters and is meant primarily for notification
1614 and cannot affect the outcome of the git p4 submit action.
1617 self.usage += " [name of git branch to submit into perforce depot]"
1619 self.detectRenames = False
1620 self.preserveUser = gitConfigBool("git-p4.preserveUser")
1621 self.dry_run = False
1623 self.update_shelve = list()
1625 self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1626 self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
1627 self.prepare_p4_only = False
1628 self.conflict_behavior = None
1629 self.isWindows = (platform.system() == "Windows")
1630 self.exportLabels = False
1631 self.p4HasMoveCommand = p4_has_move_command()
1633 self.no_verify = False
1635 if gitConfig('git-p4.largeFileSystem'):
1636 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1639 if len(p4CmdList("opened ...")) > 0:
1640 die("You have files opened with perforce! Close them before starting the sync.")
1642 def separate_jobs_from_description(self, message):
1643 """Extract and return a possible Jobs field in the commit
1644 message. It goes into a separate section in the p4 change
1647 A jobs line starts with "Jobs:" and looks like a new field
1648 in a form. Values are white-space separated on the same
1649 line or on following lines that start with a tab.
1651 This does not parse and extract the full git commit message
1652 like a p4 form. It just sees the Jobs: line as a marker
1653 to pass everything from then on directly into the p4 form,
1654 but outside the description section.
1656 Return a tuple (stripped log message, jobs string)."""
1658 m = re.search(r'^Jobs:', message, re.MULTILINE)
1660 return (message, None)
1662 jobtext = message[m.start():]
1663 stripped_message = message[:m.start()].rstrip()
1664 return (stripped_message, jobtext)
1666 def prepareLogMessage(self, template, message, jobs):
1667 """Edits the template returned from "p4 change -o" to insert
1668 the message in the Description field, and the jobs text in
1672 inDescriptionSection = False
1674 for line in template.split("\n"):
1675 if line.startswith("#"):
1676 result += line + "\n"
1679 if inDescriptionSection:
1680 if line.startswith("Files:") or line.startswith("Jobs:"):
1681 inDescriptionSection = False
1682 # insert Jobs section
1684 result += jobs + "\n"
1688 if line.startswith("Description:"):
1689 inDescriptionSection = True
1691 for messageLine in message.split("\n"):
1692 line += "\t" + messageLine + "\n"
1694 result += line + "\n"
1698 def patchRCSKeywords(self, file, pattern):
1699 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1700 (handle, outFileName) = tempfile.mkstemp(dir='.')
1702 outFile = os.fdopen(handle, "w+")
1703 inFile = open(file, "r")
1704 regexp = re.compile(pattern, re.VERBOSE)
1705 for line in inFile.readlines():
1706 line = regexp.sub(r'$\1$', line)
1710 # Forcibly overwrite the original file
1712 shutil.move(outFileName, file)
1714 # cleanup our temporary file
1715 os.unlink(outFileName)
1716 print("Failed to strip RCS keywords in %s" % file)
1719 print("Patched up RCS keywords in %s" % file)
1721 def p4UserForCommit(self,id):
1722 # Return the tuple (perforce user,git email) for a given git commit id
1723 self.getUserMapFromPerforceServer()
1724 gitEmail = read_pipe(["git", "log", "--max-count=1",
1725 "--format=%ae", id])
1726 gitEmail = gitEmail.strip()
1727 if gitEmail not in self.emails:
1728 return (None,gitEmail)
1730 return (self.emails[gitEmail],gitEmail)
1732 def checkValidP4Users(self,commits):
1733 # check if any git authors cannot be mapped to p4 users
1735 (user,email) = self.p4UserForCommit(id)
1737 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1738 if gitConfigBool("git-p4.allowMissingP4Users"):
1741 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1743 def lastP4Changelist(self):
1744 # Get back the last changelist number submitted in this client spec. This
1745 # then gets used to patch up the username in the change. If the same
1746 # client spec is being used by multiple processes then this might go
1748 results = p4CmdList("client -o") # find the current client
1752 client = r['Client']
1755 die("could not get client spec")
1756 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1760 die("Could not get changelist number for last submit - cannot patch up user details")
1762 def modifyChangelistUser(self, changelist, newUser):
1763 # fixup the user field of a changelist after it has been submitted.
1764 changes = p4CmdList("change -o %s" % changelist)
1765 if len(changes) != 1:
1766 die("Bad output from p4 change modifying %s to user %s" %
1767 (changelist, newUser))
1770 if c['User'] == newUser: return # nothing to do
1772 # p4 does not understand format version 3 and above
1773 input = marshal.dumps(c, 2)
1775 result = p4CmdList("change -f -i", stdin=input)
1778 if r['code'] == 'error':
1779 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1781 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1783 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1785 def canChangeChangelists(self):
1786 # check to see if we have p4 admin or super-user permissions, either of
1787 # which are required to modify changelists.
1788 results = p4CmdList(["protects", self.depotPath])
1791 if r['perm'] == 'admin':
1793 if r['perm'] == 'super':
1797 def prepareSubmitTemplate(self, changelist=None):
1798 """Run "p4 change -o" to grab a change specification template.
1799 This does not use "p4 -G", as it is nice to keep the submission
1800 template in original order, since a human might edit it.
1802 Remove lines in the Files section that show changes to files
1803 outside the depot path we're committing into."""
1805 [upstream, settings] = findUpstreamBranchPoint()
1808 # A Perforce Change Specification.
1810 # Change: The change number. 'new' on a new changelist.
1811 # Date: The date this specification was last modified.
1812 # Client: The client on which the changelist was created. Read-only.
1813 # User: The user who created the changelist.
1814 # Status: Either 'pending' or 'submitted'. Read-only.
1815 # Type: Either 'public' or 'restricted'. Default is 'public'.
1816 # Description: Comments about the changelist. Required.
1817 # Jobs: What opened jobs are to be closed by this changelist.
1818 # You may delete jobs from this list. (New changelists only.)
1819 # Files: What opened files from the default changelist are to be added
1820 # to this changelist. You may delete files from this list.
1821 # (New changelists only.)
1824 inFilesSection = False
1826 args = ['change', '-o']
1828 args.append(str(changelist))
1829 for entry in p4CmdList(args):
1830 if 'code' not in entry:
1832 if entry['code'] == 'stat':
1833 change_entry = entry
1835 if not change_entry:
1836 die('Failed to decode output of p4 change -o')
1837 for key, value in change_entry.items():
1838 if key.startswith('File'):
1839 if 'depot-paths' in settings:
1840 if not [p for p in settings['depot-paths']
1841 if p4PathStartsWith(value, p)]:
1844 if not p4PathStartsWith(value, self.depotPath):
1846 files_list.append(value)
1848 # Output in the order expected by prepareLogMessage
1849 for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
1850 if key not in change_entry:
1853 template += key + ':'
1854 if key == 'Description':
1856 for field_line in change_entry[key].splitlines():
1857 template += '\t'+field_line+'\n'
1858 if len(files_list) > 0:
1860 template += 'Files:\n'
1861 for path in files_list:
1862 template += '\t'+path+'\n'
1865 def edit_template(self, template_file):
1866 """Invoke the editor to let the user change the submission
1867 message. Return true if okay to continue with the submit."""
1869 # if configured to skip the editing part, just submit
1870 if gitConfigBool("git-p4.skipSubmitEdit"):
1873 # look at the modification time, to check later if the user saved
1875 mtime = os.stat(template_file).st_mtime
1878 if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
1879 editor = os.environ.get("P4EDITOR")
1881 editor = read_pipe("git var GIT_EDITOR").strip()
1882 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1884 # If the file was not saved, prompt to see if this patch should
1885 # be skipped. But skip this verification step if configured so.
1886 if gitConfigBool("git-p4.skipSubmitEditCheck"):
1889 # modification time updated means user saved the file
1890 if os.stat(template_file).st_mtime > mtime:
1893 response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1899 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
1901 if "P4DIFF" in os.environ:
1902 del(os.environ["P4DIFF"])
1904 for editedFile in editedFiles:
1905 diff += p4_read_pipe(['diff', '-du',
1906 wildcard_encode(editedFile)])
1910 for newFile in filesToAdd:
1911 newdiff += "==== new file ====\n"
1912 newdiff += "--- /dev/null\n"
1913 newdiff += "+++ %s\n" % newFile
1915 is_link = os.path.islink(newFile)
1916 expect_link = newFile in symlinks
1918 if is_link and expect_link:
1919 newdiff += "+%s\n" % os.readlink(newFile)
1921 f = open(newFile, "r")
1922 for line in f.readlines():
1923 newdiff += "+" + line
1926 return (diff + newdiff).replace('\r\n', '\n')
1928 def applyCommit(self, id):
1929 """Apply one commit, return True if it succeeded."""
1931 print("Applying", read_pipe(["git", "show", "-s",
1932 "--format=format:%h %s", id]))
1934 (p4User, gitEmail) = self.p4UserForCommit(id)
1936 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1938 filesToChangeType = set()
1939 filesToDelete = set()
1941 pureRenameCopy = set()
1943 filesToChangeExecBit = {}
1947 diff = parseDiffTreeEntry(line)
1948 modifier = diff['status']
1950 all_files.append(path)
1954 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1955 filesToChangeExecBit[path] = diff['dst_mode']
1956 editedFiles.add(path)
1957 elif modifier == "A":
1958 filesToAdd.add(path)
1959 filesToChangeExecBit[path] = diff['dst_mode']
1960 if path in filesToDelete:
1961 filesToDelete.remove(path)
1963 dst_mode = int(diff['dst_mode'], 8)
1964 if dst_mode == 0o120000:
1967 elif modifier == "D":
1968 filesToDelete.add(path)
1969 if path in filesToAdd:
1970 filesToAdd.remove(path)
1971 elif modifier == "C":
1972 src, dest = diff['src'], diff['dst']
1973 all_files.append(dest)
1974 p4_integrate(src, dest)
1975 pureRenameCopy.add(dest)
1976 if diff['src_sha1'] != diff['dst_sha1']:
1978 pureRenameCopy.discard(dest)
1979 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1981 pureRenameCopy.discard(dest)
1982 filesToChangeExecBit[dest] = diff['dst_mode']
1984 # turn off read-only attribute
1985 os.chmod(dest, stat.S_IWRITE)
1987 editedFiles.add(dest)
1988 elif modifier == "R":
1989 src, dest = diff['src'], diff['dst']
1990 all_files.append(dest)
1991 if self.p4HasMoveCommand:
1992 p4_edit(src) # src must be open before move
1993 p4_move(src, dest) # opens for (move/delete, move/add)
1995 p4_integrate(src, dest)
1996 if diff['src_sha1'] != diff['dst_sha1']:
1999 pureRenameCopy.add(dest)
2000 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2001 if not self.p4HasMoveCommand:
2002 p4_edit(dest) # with move: already open, writable
2003 filesToChangeExecBit[dest] = diff['dst_mode']
2004 if not self.p4HasMoveCommand:
2006 os.chmod(dest, stat.S_IWRITE)
2008 filesToDelete.add(src)
2009 editedFiles.add(dest)
2010 elif modifier == "T":
2011 filesToChangeType.add(path)
2013 die("unknown modifier %s for %s" % (modifier, path))
2015 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
2016 patchcmd = diffcmd + " | git apply "
2017 tryPatchCmd = patchcmd + "--check -"
2018 applyPatchCmd = patchcmd + "--check --apply -"
2019 patch_succeeded = True
2022 print("TryPatch: %s" % tryPatchCmd)
2024 if os.system(tryPatchCmd) != 0:
2025 fixed_rcs_keywords = False
2026 patch_succeeded = False
2027 print("Unfortunately applying the change failed!")
2029 # Patch failed, maybe it's just RCS keyword woes. Look through
2030 # the patch to see if that's possible.
2031 if gitConfigBool("git-p4.attemptRCSCleanup"):
2035 for file in editedFiles | filesToDelete:
2036 # did this file's delta contain RCS keywords?
2037 pattern = p4_keywords_regexp_for_file(file)
2040 # this file is a possibility...look for RCS keywords.
2041 regexp = re.compile(pattern, re.VERBOSE)
2042 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
2043 if regexp.search(line):
2045 print("got keyword match on %s in %s in %s" % (pattern, line, file))
2046 kwfiles[file] = pattern
2049 for file in kwfiles:
2051 print("zapping %s with %s" % (line,pattern))
2052 # File is being deleted, so not open in p4. Must
2053 # disable the read-only bit on windows.
2054 if self.isWindows and file not in editedFiles:
2055 os.chmod(file, stat.S_IWRITE)
2056 self.patchRCSKeywords(file, kwfiles[file])
2057 fixed_rcs_keywords = True
2059 if fixed_rcs_keywords:
2060 print("Retrying the patch with RCS keywords cleaned up")
2061 if os.system(tryPatchCmd) == 0:
2062 patch_succeeded = True
2063 print("Patch succeesed this time with RCS keywords cleaned")
2065 if not patch_succeeded:
2066 for f in editedFiles:
2071 # Apply the patch for real, and do add/delete/+x handling.
2073 system(applyPatchCmd)
2075 for f in filesToChangeType:
2076 p4_edit(f, "-t", "auto")
2077 for f in filesToAdd:
2079 for f in filesToDelete:
2083 # Set/clear executable bits
2084 for f in filesToChangeExecBit.keys():
2085 mode = filesToChangeExecBit[f]
2086 setP4ExecBit(f, mode)
2089 if len(self.update_shelve) > 0:
2090 update_shelve = self.update_shelve.pop(0)
2091 p4_reopen_in_change(update_shelve, all_files)
2094 # Build p4 change description, starting with the contents
2095 # of the git commit message.
2097 logMessage = extractLogMessageFromGitCommit(id)
2098 logMessage = logMessage.strip()
2099 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
2101 template = self.prepareSubmitTemplate(update_shelve)
2102 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
2104 if self.preserveUser:
2105 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
2107 if self.checkAuthorship and not self.p4UserIsMe(p4User):
2108 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2109 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2110 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2112 separatorLine = "######## everything below this line is just the diff #######\n"
2113 if not self.prepare_p4_only:
2114 submitTemplate += separatorLine
2115 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
2117 (handle, fileName) = tempfile.mkstemp()
2118 tmpFile = os.fdopen(handle, "w+b")
2120 submitTemplate = submitTemplate.replace("\n", "\r\n")
2121 tmpFile.write(encode_text_stream(submitTemplate))
2127 # Allow the hook to edit the changelist text before presenting it
2129 if not run_git_hook("p4-prepare-changelist", [fileName]):
2132 if self.prepare_p4_only:
2134 # Leave the p4 tree prepared, and the submit template around
2135 # and let the user decide what to do next
2139 print("P4 workspace prepared for submission.")
2140 print("To submit or revert, go to client workspace")
2141 print(" " + self.clientPath)
2143 print("To submit, use \"p4 submit\" to write a new description,")
2144 print("or \"p4 submit -i <%s\" to use the one prepared by" \
2145 " \"git p4\"." % fileName)
2146 print("You can delete the file \"%s\" when finished." % fileName)
2148 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
2149 print("To preserve change ownership by user %s, you must\n" \
2150 "do \"p4 change -f <change>\" after submitting and\n" \
2151 "edit the User field.")
2153 print("After submitting, renamed files must be re-synced.")
2154 print("Invoke \"p4 sync -f\" on each of these files:")
2155 for f in pureRenameCopy:
2159 print("To revert the changes, use \"p4 revert ...\", and delete")
2160 print("the submit template file \"%s\"" % fileName)
2162 print("Since the commit adds new files, they must be deleted:")
2163 for f in filesToAdd:
2169 if self.edit_template(fileName):
2170 if not self.no_verify:
2171 if not run_git_hook("p4-changelist", [fileName]):
2172 print("The p4-changelist hook failed.")
2176 # read the edited message and submit
2177 tmpFile = open(fileName, "rb")
2178 message = decode_text_stream(tmpFile.read())
2181 message = message.replace("\r\n", "\n")
2182 if message.find(separatorLine) != -1:
2183 submitTemplate = message[:message.index(separatorLine)]
2185 submitTemplate = message
2187 if len(submitTemplate.strip()) == 0:
2188 print("Changelist is empty, aborting this changelist.")
2193 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2195 p4_write_pipe(['shelve', '-i'], submitTemplate)
2197 p4_write_pipe(['submit', '-i'], submitTemplate)
2198 # The rename/copy happened by applying a patch that created a
2199 # new file. This leaves it writable, which confuses p4.
2200 for f in pureRenameCopy:
2203 if self.preserveUser:
2205 # Get last changelist number. Cannot easily get it from
2206 # the submit command output as the output is
2208 changelist = self.lastP4Changelist()
2209 self.modifyChangelistUser(changelist, p4User)
2213 run_git_hook("p4-post-changelist")
2215 # Revert changes if we skip this patch
2216 if not submitted or self.shelve:
2218 print ("Reverting shelved files.")
2220 print ("Submission cancelled, undoing p4 changes.")
2222 for f in editedFiles | filesToDelete:
2224 for f in filesToAdd:
2228 if not self.prepare_p4_only:
2232 # Export git tags as p4 labels. Create a p4 label and then tag
2234 def exportGitTags(self, gitTags):
2235 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2236 if len(validLabelRegexp) == 0:
2237 validLabelRegexp = defaultLabelRegexp
2238 m = re.compile(validLabelRegexp)
2240 for name in gitTags:
2242 if not m.match(name):
2244 print("tag %s does not match regexp %s" % (name, validLabelRegexp))
2247 # Get the p4 commit this corresponds to
2248 logMessage = extractLogMessageFromGitCommit(name)
2249 values = extractSettingsGitLog(logMessage)
2251 if 'change' not in values:
2252 # a tag pointing to something not sent to p4; ignore
2254 print("git tag %s does not give a p4 commit" % name)
2257 changelist = values['change']
2259 # Get the tag details.
2263 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2266 if re.match(r'tag\s+', l):
2268 elif re.match(r'\s*$', l):
2275 body = ["lightweight tag imported by git p4\n"]
2277 # Create the label - use the same view as the client spec we are using
2278 clientSpec = getClientSpec()
2280 labelTemplate = "Label: %s\n" % name
2281 labelTemplate += "Description:\n"
2283 labelTemplate += "\t" + b + "\n"
2284 labelTemplate += "View:\n"
2285 for depot_side in clientSpec.mappings:
2286 labelTemplate += "\t%s\n" % depot_side
2289 print("Would create p4 label %s for tag" % name)
2290 elif self.prepare_p4_only:
2291 print("Not creating p4 label %s for tag due to option" \
2292 " --prepare-p4-only" % name)
2294 p4_write_pipe(["label", "-i"], labelTemplate)
2297 p4_system(["tag", "-l", name] +
2298 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2301 print("created p4 label for tag %s" % name)
2303 def run(self, args):
2305 self.master = currentGitBranch()
2306 elif len(args) == 1:
2307 self.master = args[0]
2308 if not branchExists(self.master):
2309 die("Branch %s does not exist" % self.master)
2313 for i in self.update_shelve:
2315 sys.exit("invalid changelist %d" % i)
2318 allowSubmit = gitConfig("git-p4.allowSubmit")
2319 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2320 die("%s is not in git-p4.allowSubmit" % self.master)
2322 [upstream, settings] = findUpstreamBranchPoint()
2323 self.depotPath = settings['depot-paths'][0]
2324 if len(self.origin) == 0:
2325 self.origin = upstream
2327 if len(self.update_shelve) > 0:
2330 if self.preserveUser:
2331 if not self.canChangeChangelists():
2332 die("Cannot preserve user names without p4 super-user or admin permissions")
2334 # if not set from the command line, try the config file
2335 if self.conflict_behavior is None:
2336 val = gitConfig("git-p4.conflict")
2338 if val not in self.conflict_behavior_choices:
2339 die("Invalid value '%s' for config git-p4.conflict" % val)
2342 self.conflict_behavior = val
2345 print("Origin branch is " + self.origin)
2347 if len(self.depotPath) == 0:
2348 print("Internal error: cannot locate perforce depot path from existing branches")
2351 self.useClientSpec = False
2352 if gitConfigBool("git-p4.useclientspec"):
2353 self.useClientSpec = True
2354 if self.useClientSpec:
2355 self.clientSpecDirs = getClientSpec()
2357 # Check for the existence of P4 branches
2358 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2360 if self.useClientSpec and not branchesDetected:
2361 # all files are relative to the client spec
2362 self.clientPath = getClientRoot()
2364 self.clientPath = p4Where(self.depotPath)
2366 if self.clientPath == "":
2367 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2369 print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
2370 self.oldWorkingDirectory = os.getcwd()
2372 # ensure the clientPath exists
2373 new_client_dir = False
2374 if not os.path.exists(self.clientPath):
2375 new_client_dir = True
2376 os.makedirs(self.clientPath)
2378 chdir(self.clientPath, is_client_path=True)
2380 print("Would synchronize p4 checkout in %s" % self.clientPath)
2382 print("Synchronizing p4 checkout...")
2384 # old one was destroyed, and maybe nobody told p4
2385 p4_sync("...", "-f")
2392 committish = self.master
2396 if self.commit != "":
2397 if self.commit.find("..") != -1:
2398 limits_ish = self.commit.split("..")
2399 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2400 commits.append(line.strip())
2403 commits.append(self.commit)
2405 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2406 commits.append(line.strip())
2409 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2410 self.checkAuthorship = False
2412 self.checkAuthorship = True
2414 if self.preserveUser:
2415 self.checkValidP4Users(commits)
2418 # Build up a set of options to be passed to diff when
2419 # submitting each commit to p4.
2421 if self.detectRenames:
2422 # command-line -M arg
2423 self.diffOpts = "-M"
2425 # If not explicitly set check the config variable
2426 detectRenames = gitConfig("git-p4.detectRenames")
2428 if detectRenames.lower() == "false" or detectRenames == "":
2430 elif detectRenames.lower() == "true":
2431 self.diffOpts = "-M"
2433 self.diffOpts = "-M%s" % detectRenames
2435 # no command-line arg for -C or --find-copies-harder, just
2437 detectCopies = gitConfig("git-p4.detectCopies")
2438 if detectCopies.lower() == "false" or detectCopies == "":
2440 elif detectCopies.lower() == "true":
2441 self.diffOpts += " -C"
2443 self.diffOpts += " -C%s" % detectCopies
2445 if gitConfigBool("git-p4.detectCopiesHarder"):
2446 self.diffOpts += " --find-copies-harder"
2448 num_shelves = len(self.update_shelve)
2449 if num_shelves > 0 and num_shelves != len(commits):
2450 sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2451 (len(commits), num_shelves))
2453 if not self.no_verify:
2455 if not run_git_hook("p4-pre-submit"):
2456 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip " \
2457 "this pre-submission check by adding\nthe command line option '--no-verify', " \
2458 "however,\nthis will also skip the p4-changelist hook as well.")
2460 except Exception as e:
2461 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "\
2462 "with the error '{0}'".format(e.message) )
2466 # Apply the commits, one at a time. On failure, ask if should
2467 # continue to try the rest of the patches, or quit.
2470 print("Would apply")
2472 last = len(commits) - 1
2473 for i, commit in enumerate(commits):
2475 print(" ", read_pipe(["git", "show", "-s",
2476 "--format=format:%h %s", commit]))
2479 ok = self.applyCommit(commit)
2481 applied.append(commit)
2482 if self.prepare_p4_only:
2484 print("Processing only the first commit due to option" \
2485 " --prepare-p4-only")
2489 # prompt for what to do, or use the option/variable
2490 if self.conflict_behavior == "ask":
2491 print("What do you want to do?")
2492 response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2493 elif self.conflict_behavior == "skip":
2495 elif self.conflict_behavior == "quit":
2498 die("Unknown conflict_behavior '%s'" %
2499 self.conflict_behavior)
2502 print("Skipping this commit, but applying the rest")
2507 chdir(self.oldWorkingDirectory)
2508 shelved_applied = "shelved" if self.shelve else "applied"
2511 elif self.prepare_p4_only:
2513 elif len(commits) == len(applied):
2514 print("All commits {0}!".format(shelved_applied))
2518 sync.branch = self.branch
2519 if self.disable_p4sync:
2520 sync.sync_origin_only()
2524 if not self.disable_rebase:
2529 if len(applied) == 0:
2530 print("No commits {0}.".format(shelved_applied))
2532 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2538 print(star, read_pipe(["git", "show", "-s",
2539 "--format=format:%h %s", c]))
2540 print("You will have to do 'git p4 sync' and rebase.")
2542 if gitConfigBool("git-p4.exportLabels"):
2543 self.exportLabels = True
2545 if self.exportLabels:
2546 p4Labels = getP4Labels(self.depotPath)
2547 gitTags = getGitTags()
2549 missingGitTags = gitTags - p4Labels
2550 self.exportGitTags(missingGitTags)
2552 # exit with error unless everything applied perfectly
2553 if len(commits) != len(applied):
2559 """Represent a p4 view ("p4 help views"), and map files in a
2560 repo according to the view."""
2562 def __init__(self, client_name):
2564 self.client_prefix = "//%s/" % client_name
2565 # cache results of "p4 where" to lookup client file locations
2566 self.client_spec_path_cache = {}
2568 def append(self, view_line):
2569 """Parse a view line, splitting it into depot and client
2570 sides. Append to self.mappings, preserving order. This
2571 is only needed for tag creation."""
2573 # Split the view line into exactly two words. P4 enforces
2574 # structure on these lines that simplifies this quite a bit.
2576 # Either or both words may be double-quoted.
2577 # Single quotes do not matter.
2578 # Double-quote marks cannot occur inside the words.
2579 # A + or - prefix is also inside the quotes.
2580 # There are no quotes unless they contain a space.
2581 # The line is already white-space stripped.
2582 # The two words are separated by a single space.
2584 if view_line[0] == '"':
2585 # First word is double quoted. Find its end.
2586 close_quote_index = view_line.find('"', 1)
2587 if close_quote_index <= 0:
2588 die("No first-word closing quote found: %s" % view_line)
2589 depot_side = view_line[1:close_quote_index]
2590 # skip closing quote and space
2591 rhs_index = close_quote_index + 1 + 1
2593 space_index = view_line.find(" ")
2594 if space_index <= 0:
2595 die("No word-splitting space found: %s" % view_line)
2596 depot_side = view_line[0:space_index]
2597 rhs_index = space_index + 1
2599 # prefix + means overlay on previous mapping
2600 if depot_side.startswith("+"):
2601 depot_side = depot_side[1:]
2603 # prefix - means exclude this path, leave out of mappings
2605 if depot_side.startswith("-"):
2607 depot_side = depot_side[1:]
2610 self.mappings.append(depot_side)
2612 def convert_client_path(self, clientFile):
2613 # chop off //client/ part to make it relative
2614 if not decode_path(clientFile).startswith(self.client_prefix):
2615 die("No prefix '%s' on clientFile '%s'" %
2616 (self.client_prefix, clientFile))
2617 return clientFile[len(self.client_prefix):]
2619 def update_client_spec_path_cache(self, files):
2620 """ Caching file paths by "p4 where" batch query """
2622 # List depot file paths exclude that already cached
2623 fileArgs = [f['path'] for f in files if decode_path(f['path']) not in self.client_spec_path_cache]
2625 if len(fileArgs) == 0:
2626 return # All files in cache
2628 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2629 for res in where_result:
2630 if "code" in res and res["code"] == "error":
2631 # assume error is "... file(s) not in client view"
2633 if "clientFile" not in res:
2634 die("No clientFile in 'p4 where' output")
2636 # it will list all of them, but only one not unmap-ped
2638 depot_path = decode_path(res['depotFile'])
2639 if gitConfigBool("core.ignorecase"):
2640 depot_path = depot_path.lower()
2641 self.client_spec_path_cache[depot_path] = self.convert_client_path(res["clientFile"])
2643 # not found files or unmap files set to ""
2644 for depotFile in fileArgs:
2645 depotFile = decode_path(depotFile)
2646 if gitConfigBool("core.ignorecase"):
2647 depotFile = depotFile.lower()
2648 if depotFile not in self.client_spec_path_cache:
2649 self.client_spec_path_cache[depotFile] = b''
2651 def map_in_client(self, depot_path):
2652 """Return the relative location in the client where this
2653 depot file should live. Returns "" if the file should
2654 not be mapped in the client."""
2656 if gitConfigBool("core.ignorecase"):
2657 depot_path = depot_path.lower()
2659 if depot_path in self.client_spec_path_cache:
2660 return self.client_spec_path_cache[depot_path]
2662 die( "Error: %s is not found in client spec path" % depot_path )
2665 def cloneExcludeCallback(option, opt_str, value, parser):
2666 # prepend "/" because the first "/" was consumed as part of the option itself.
2667 # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2668 parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2670 class P4Sync(Command, P4UserMap):
2673 Command.__init__(self)
2674 P4UserMap.__init__(self)
2676 optparse.make_option("--branch", dest="branch"),
2677 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2678 optparse.make_option("--changesfile", dest="changesFile"),
2679 optparse.make_option("--silent", dest="silent", action="store_true"),
2680 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2681 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2682 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2683 help="Import into refs/heads/ , not refs/remotes"),
2684 optparse.make_option("--max-changes", dest="maxChanges",
2685 help="Maximum number of changes to import"),
2686 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2687 help="Internal block size to use when iteratively calling p4 changes"),
2688 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2689 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2690 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2691 help="Only sync files that are included in the Perforce Client Spec"),
2692 optparse.make_option("-/", dest="cloneExclude",
2693 action="callback", callback=cloneExcludeCallback, type="string",
2694 help="exclude depot path"),
2696 self.description = """Imports from Perforce into a git repository.\n
2698 //depot/my/project/ -- to import the current head
2699 //depot/my/project/@all -- to import everything
2700 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2702 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2704 self.usage += " //depot/path[@revRange]"
2706 self.createdBranches = set()
2707 self.committedChanges = set()
2709 self.detectBranches = False
2710 self.detectLabels = False
2711 self.importLabels = False
2712 self.changesFile = ""
2713 self.syncWithOrigin = True
2714 self.importIntoRemotes = True
2715 self.maxChanges = ""
2716 self.changes_block_size = None
2717 self.keepRepoPath = False
2718 self.depotPaths = None
2719 self.p4BranchesInGit = []
2720 self.cloneExclude = []
2721 self.useClientSpec = False
2722 self.useClientSpec_from_options = False
2723 self.clientSpecDirs = None
2724 self.tempBranches = []
2725 self.tempBranchLocation = "refs/git-p4-tmp"
2726 self.largeFileSystem = None
2727 self.suppress_meta_comment = False
2729 if gitConfig('git-p4.largeFileSystem'):
2730 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2731 self.largeFileSystem = largeFileSystemConstructor(
2732 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2735 if gitConfig("git-p4.syncFromOrigin") == "false":
2736 self.syncWithOrigin = False
2738 self.depotPaths = []
2739 self.changeRange = ""
2740 self.previousDepotPaths = []
2741 self.hasOrigin = False
2743 # map from branch depot path to parent branch
2744 self.knownBranches = {}
2745 self.initialParents = {}
2747 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2750 # Force a checkpoint in fast-import and wait for it to finish
2751 def checkpoint(self):
2752 self.gitStream.write("checkpoint\n\n")
2753 self.gitStream.write("progress checkpoint\n\n")
2754 self.gitStream.flush()
2755 out = self.gitOutput.readline()
2757 print("checkpoint finished: " + out)
2759 def isPathWanted(self, path):
2760 for p in self.cloneExclude:
2762 if p4PathStartsWith(path, p):
2764 # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2765 elif path.lower() == p.lower():
2767 for p in self.depotPaths:
2768 if p4PathStartsWith(path, decode_path(p)):
2772 def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0):
2775 while "depotFile%s" % fnum in commit:
2776 path = commit["depotFile%s" % fnum]
2777 found = self.isPathWanted(decode_path(path))
2784 file["rev"] = commit["rev%s" % fnum]
2785 file["action"] = commit["action%s" % fnum]
2786 file["type"] = commit["type%s" % fnum]
2788 file["shelved_cl"] = int(shelved_cl)
2793 def extractJobsFromCommit(self, commit):
2796 while "job%s" % jnum in commit:
2797 job = commit["job%s" % jnum]
2802 def stripRepoPath(self, path, prefixes):
2803 """When streaming files, this is called to map a p4 depot path
2804 to where it should go in git. The prefixes are either
2805 self.depotPaths, or self.branchPrefixes in the case of
2806 branch detection."""
2808 if self.useClientSpec:
2809 # branch detection moves files up a level (the branch name)
2810 # from what client spec interpretation gives
2811 path = decode_path(self.clientSpecDirs.map_in_client(path))
2812 if self.detectBranches:
2813 for b in self.knownBranches:
2814 if p4PathStartsWith(path, b + "/"):
2815 path = path[len(b)+1:]
2817 elif self.keepRepoPath:
2818 # Preserve everything in relative path name except leading
2819 # //depot/; just look at first prefix as they all should
2820 # be in the same depot.
2821 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2822 if p4PathStartsWith(path, depot):
2823 path = path[len(depot):]
2827 if p4PathStartsWith(path, p):
2828 path = path[len(p):]
2831 path = wildcard_decode(path)
2834 def splitFilesIntoBranches(self, commit):
2835 """Look at each depotFile in the commit to figure out to what
2836 branch it belongs."""
2838 if self.clientSpecDirs:
2839 files = self.extractFilesFromCommit(commit)
2840 self.clientSpecDirs.update_client_spec_path_cache(files)
2844 while "depotFile%s" % fnum in commit:
2845 raw_path = commit["depotFile%s" % fnum]
2846 path = decode_path(raw_path)
2847 found = self.isPathWanted(path)
2853 file["path"] = raw_path
2854 file["rev"] = commit["rev%s" % fnum]
2855 file["action"] = commit["action%s" % fnum]
2856 file["type"] = commit["type%s" % fnum]
2859 # start with the full relative path where this file would
2861 if self.useClientSpec:
2862 relPath = decode_path(self.clientSpecDirs.map_in_client(path))
2864 relPath = self.stripRepoPath(path, self.depotPaths)
2866 for branch in self.knownBranches.keys():
2867 # add a trailing slash so that a commit into qt/4.2foo
2868 # doesn't end up in qt/4.2, e.g.
2869 if p4PathStartsWith(relPath, branch + "/"):
2870 if branch not in branches:
2871 branches[branch] = []
2872 branches[branch].append(file)
2877 def writeToGitStream(self, gitMode, relPath, contents):
2878 self.gitStream.write(encode_text_stream(u'M {} inline {}\n'.format(gitMode, relPath)))
2879 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2881 self.gitStream.write(d)
2882 self.gitStream.write('\n')
2884 def encodeWithUTF8(self, path):
2886 path.decode('ascii')
2889 if gitConfig('git-p4.pathEncoding'):
2890 encoding = gitConfig('git-p4.pathEncoding')
2891 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2893 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
2896 # output one file from the P4 stream
2897 # - helper for streamP4Files
2899 def streamOneP4File(self, file, contents):
2900 file_path = file['depotFile']
2901 relPath = self.stripRepoPath(decode_path(file_path), self.branchPrefixes)
2904 if 'fileSize' in self.stream_file:
2905 size = int(self.stream_file['fileSize'])
2907 size = 0 # deleted files don't get a fileSize apparently
2908 sys.stdout.write('\r%s --> %s (%i MB)\n' % (file_path, relPath, size/1024/1024))
2911 (type_base, type_mods) = split_p4_type(file["type"])
2914 if "x" in type_mods:
2916 if type_base == "symlink":
2918 # p4 print on a symlink sometimes contains "target\n";
2919 # if it does, remove the newline
2920 data = ''.join(decode_text_stream(c) for c in contents)
2922 # Some version of p4 allowed creating a symlink that pointed
2923 # to nothing. This causes p4 errors when checking out such
2924 # a change, and errors here too. Work around it by ignoring
2925 # the bad symlink; hopefully a future change fixes it.
2926 print("\nIgnoring empty symlink in %s" % file_path)
2928 elif data[-1] == '\n':
2929 contents = [data[:-1]]
2933 if type_base == "utf16":
2934 # p4 delivers different text in the python output to -G
2935 # than it does when using "print -o", or normal p4 client
2936 # operations. utf16 is converted to ascii or utf8, perhaps.
2937 # But ascii text saved as -t utf16 is completely mangled.
2938 # Invoke print -o to get the real contents.
2940 # On windows, the newlines will always be mangled by print, so put
2941 # them back too. This is not needed to the cygwin windows version,
2942 # just the native "NT" type.
2945 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw=True)
2946 except Exception as e:
2947 if 'Translation of file content failed' in str(e):
2948 type_base = 'binary'
2952 if p4_version_string().find('/NT') >= 0:
2953 text = text.replace(b'\r\n', b'\n')
2956 if type_base == "apple":
2957 # Apple filetype files will be streamed as a concatenation of
2958 # its appledouble header and the contents. This is useless
2959 # on both macs and non-macs. If using "print -q -o xx", it
2960 # will create "xx" with the data, and "%xx" with the header.
2961 # This is also not very useful.
2963 # Ideally, someday, this script can learn how to generate
2964 # appledouble files directly and import those to git, but
2965 # non-mac machines can never find a use for apple filetype.
2966 print("\nIgnoring apple filetype file %s" % file['depotFile'])
2969 # Note that we do not try to de-mangle keywords on utf16 files,
2970 # even though in theory somebody may want that.
2971 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2973 regexp = re.compile(pattern, re.VERBOSE)
2974 text = ''.join(decode_text_stream(c) for c in contents)
2975 text = regexp.sub(r'$\1$', text)
2976 contents = [ encode_text_stream(text) ]
2978 if self.largeFileSystem:
2979 (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
2981 self.writeToGitStream(git_mode, relPath, contents)
2983 def streamOneP4Deletion(self, file):
2984 relPath = self.stripRepoPath(decode_path(file['path']), self.branchPrefixes)
2986 sys.stdout.write("delete %s\n" % relPath)
2988 self.gitStream.write(encode_text_stream(u'D {}\n'.format(relPath)))
2990 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2991 self.largeFileSystem.removeLargeFile(relPath)
2993 # handle another chunk of streaming data
2994 def streamP4FilesCb(self, marshalled):
2996 # catch p4 errors and complain
2998 if "code" in marshalled:
2999 if marshalled["code"] == "error":
3000 if "data" in marshalled:
3001 err = marshalled["data"].rstrip()
3003 if not err and 'fileSize' in self.stream_file:
3004 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
3005 if required_bytes > 0:
3006 err = 'Not enough space left on %s! Free at least %i MB.' % (
3007 os.getcwd(), required_bytes/1024/1024
3012 if self.stream_have_file_info:
3013 if "depotFile" in self.stream_file:
3014 f = self.stream_file["depotFile"]
3015 # force a failure in fast-import, else an empty
3016 # commit will be made
3017 self.gitStream.write("\n")
3018 self.gitStream.write("die-now\n")
3019 self.gitStream.close()
3020 # ignore errors, but make sure it exits first
3021 self.importProcess.wait()
3023 die("Error from p4 print for %s: %s" % (f, err))
3025 die("Error from p4 print: %s" % err)
3027 if 'depotFile' in marshalled and self.stream_have_file_info:
3028 # start of a new file - output the old one first
3029 self.streamOneP4File(self.stream_file, self.stream_contents)
3030 self.stream_file = {}
3031 self.stream_contents = []
3032 self.stream_have_file_info = False
3034 # pick up the new file information... for the
3035 # 'data' field we need to append to our array
3036 for k in marshalled.keys():
3038 if 'streamContentSize' not in self.stream_file:
3039 self.stream_file['streamContentSize'] = 0
3040 self.stream_file['streamContentSize'] += len(marshalled['data'])
3041 self.stream_contents.append(marshalled['data'])
3043 self.stream_file[k] = marshalled[k]
3046 'streamContentSize' in self.stream_file and
3047 'fileSize' in self.stream_file and
3048 'depotFile' in self.stream_file):
3049 size = int(self.stream_file["fileSize"])
3051 progress = 100*self.stream_file['streamContentSize']/size
3052 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
3055 self.stream_have_file_info = True
3057 # Stream directly from "p4 files" into "git fast-import"
3058 def streamP4Files(self, files):
3064 filesForCommit.append(f)
3065 if f['action'] in self.delete_actions:
3066 filesToDelete.append(f)
3068 filesToRead.append(f)
3071 for f in filesToDelete:
3072 self.streamOneP4Deletion(f)
3074 if len(filesToRead) > 0:
3075 self.stream_file = {}
3076 self.stream_contents = []
3077 self.stream_have_file_info = False
3079 # curry self argument
3080 def streamP4FilesCbSelf(entry):
3081 self.streamP4FilesCb(entry)
3084 for f in filesToRead:
3085 if 'shelved_cl' in f:
3086 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3088 fileArg = f['path'] + encode_text_stream('@={}'.format(f['shelved_cl']))
3090 fileArg = f['path'] + encode_text_stream('#{}'.format(f['rev']))
3092 fileArgs.append(fileArg)
3094 p4CmdList(["-x", "-", "print"],
3096 cb=streamP4FilesCbSelf)
3099 if 'depotFile' in self.stream_file:
3100 self.streamOneP4File(self.stream_file, self.stream_contents)
3102 def make_email(self, userid):
3103 if userid in self.users:
3104 return self.users[userid]
3106 return "%s <a@b>" % userid
3108 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
3109 """ Stream a p4 tag.
3110 commit is either a git commit, or a fast-import mark, ":<p4commit>"
3114 print("writing tag %s for commit %s" % (labelName, commit))
3115 gitStream.write("tag %s\n" % labelName)
3116 gitStream.write("from %s\n" % commit)
3118 if 'Owner' in labelDetails:
3119 owner = labelDetails["Owner"]
3123 # Try to use the owner of the p4 label, or failing that,
3124 # the current p4 user id.
3126 email = self.make_email(owner)
3128 email = self.make_email(self.p4UserId())
3129 tagger = "%s %s %s" % (email, epoch, self.tz)
3131 gitStream.write("tagger %s\n" % tagger)
3133 print("labelDetails=",labelDetails)
3134 if 'Description' in labelDetails:
3135 description = labelDetails['Description']
3137 description = 'Label from git p4'
3139 gitStream.write("data %d\n" % len(description))
3140 gitStream.write(description)
3141 gitStream.write("\n")
3143 def inClientSpec(self, path):
3144 if not self.clientSpecDirs:
3146 inClientSpec = self.clientSpecDirs.map_in_client(path)
3147 if not inClientSpec and self.verbose:
3148 print('Ignoring file outside of client spec: {0}'.format(path))
3151 def hasBranchPrefix(self, path):
3152 if not self.branchPrefixes:
3154 hasPrefix = [p for p in self.branchPrefixes
3155 if p4PathStartsWith(path, p)]
3156 if not hasPrefix and self.verbose:
3157 print('Ignoring file outside of prefix: {0}'.format(path))
3160 def findShadowedFiles(self, files, change):
3161 # Perforce allows you commit files and directories with the same name,
3162 # so you could have files //depot/foo and //depot/foo/bar both checked
3163 # in. A p4 sync of a repository in this state fails. Deleting one of
3164 # the files recovers the repository.
3166 # Git will not allow the broken state to exist and only the most recent
3167 # of the conflicting names is left in the repository. When one of the
3168 # conflicting files is deleted we need to re-add the other one to make
3169 # sure the git repository recovers in the same way as perforce.
3170 deleted = [f for f in files if f['action'] in self.delete_actions]
3173 path = decode_path(f['path'])
3174 to_check.add(path + '/...')
3176 path = path.rsplit("/", 1)[0]
3177 if path == "/" or path in to_check:
3180 to_check = ['%s@%s' % (wildcard_encode(p), change) for p in to_check
3181 if self.hasBranchPrefix(p)]
3183 stat_result = p4CmdList(["-x", "-", "fstat", "-T",
3184 "depotFile,headAction,headRev,headType"], stdin=to_check)
3185 for record in stat_result:
3186 if record['code'] != 'stat':
3188 if record['headAction'] in self.delete_actions:
3192 'path': record['depotFile'],
3193 'rev': record['headRev'],
3194 'type': record['headType']})
3196 def commit(self, details, files, branch, parent = "", allow_empty=False):
3197 epoch = details["time"]
3198 author = details["user"]
3199 jobs = self.extractJobsFromCommit(details)
3202 print('commit into {0}'.format(branch))
3204 files = [f for f in files
3205 if self.hasBranchPrefix(decode_path(f['path']))]
3206 self.findShadowedFiles(files, details['change'])
3208 if self.clientSpecDirs:
3209 self.clientSpecDirs.update_client_spec_path_cache(files)
3211 files = [f for f in files if self.inClientSpec(decode_path(f['path']))]
3213 if gitConfigBool('git-p4.keepEmptyCommits'):
3216 if not files and not allow_empty:
3217 print('Ignoring revision {0} as it would produce an empty commit.'
3218 .format(details['change']))
3221 self.gitStream.write("commit %s\n" % branch)
3222 self.gitStream.write("mark :%s\n" % details["change"])
3223 self.committedChanges.add(int(details["change"]))
3225 if author not in self.users:
3226 self.getUserMapFromPerforceServer()
3227 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
3229 self.gitStream.write("committer %s\n" % committer)
3231 self.gitStream.write("data <<EOT\n")
3232 self.gitStream.write(details["desc"])
3234 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3236 if not self.suppress_meta_comment:
3237 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3238 (','.join(self.branchPrefixes), details["change"]))
3239 if len(details['options']) > 0:
3240 self.gitStream.write(": options = %s" % details['options'])
3241 self.gitStream.write("]\n")
3243 self.gitStream.write("EOT\n\n")
3247 print("parent %s" % parent)
3248 self.gitStream.write("from %s\n" % parent)
3250 self.streamP4Files(files)
3251 self.gitStream.write("\n")
3253 change = int(details["change"])
3255 if change in self.labels:
3256 label = self.labels[change]
3257 labelDetails = label[0]
3258 labelRevisions = label[1]
3260 print("Change %s is labelled %s" % (change, labelDetails))
3262 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3263 for p in self.branchPrefixes])
3265 if len(files) == len(labelRevisions):
3269 if info["action"] in self.delete_actions:
3271 cleanedFiles[info["depotFile"]] = info["rev"]
3273 if cleanedFiles == labelRevisions:
3274 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3278 print("Tag %s does not match with change %s: files do not match."
3279 % (labelDetails["label"], change))
3283 print("Tag %s does not match with change %s: file count is different."
3284 % (labelDetails["label"], change))
3286 # Build a dictionary of changelists and labels, for "detect-labels" option.
3287 def getLabels(self):
3290 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3291 if len(l) > 0 and not self.silent:
3292 print("Finding files belonging to labels in %s" % self.depotPaths)
3295 label = output["label"]
3299 print("Querying files for label %s" % label)
3300 for file in p4CmdList(["files"] +
3301 ["%s...@%s" % (p, label)
3302 for p in self.depotPaths]):
3303 revisions[file["depotFile"]] = file["rev"]
3304 change = int(file["change"])
3305 if change > newestChange:
3306 newestChange = change
3308 self.labels[newestChange] = [output, revisions]
3311 print("Label changes: %s" % self.labels.keys())
3313 # Import p4 labels as git tags. A direct mapping does not
3314 # exist, so assume that if all the files are at the same revision
3315 # then we can use that, or it's something more complicated we should
3317 def importP4Labels(self, stream, p4Labels):
3319 print("import p4 labels: " + ' '.join(p4Labels))
3321 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3322 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3323 if len(validLabelRegexp) == 0:
3324 validLabelRegexp = defaultLabelRegexp
3325 m = re.compile(validLabelRegexp)
3327 for name in p4Labels:
3330 if not m.match(name):
3332 print("label %s does not match regexp %s" % (name,validLabelRegexp))
3335 if name in ignoredP4Labels:
3338 labelDetails = p4CmdList(['label', "-o", name])[0]
3340 # get the most recent changelist for each file in this label
3341 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3342 for p in self.depotPaths])
3344 if 'change' in change:
3345 # find the corresponding git commit; take the oldest commit
3346 changelist = int(change['change'])
3347 if changelist in self.committedChanges:
3348 gitCommit = ":%d" % changelist # use a fast-import mark
3351 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3352 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3353 if len(gitCommit) == 0:
3354 print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3357 gitCommit = gitCommit.strip()
3360 # Convert from p4 time format
3362 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3364 print("Could not convert label time %s" % labelDetails['Update'])
3367 when = int(time.mktime(tmwhen))
3368 self.streamTag(stream, name, labelDetails, gitCommit, when)
3370 print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3373 print("Label %s has no changelists - possibly deleted?" % name)
3376 # We can't import this label; don't try again as it will get very
3377 # expensive repeatedly fetching all the files for labels that will
3378 # never be imported. If the label is moved in the future, the
3379 # ignore will need to be removed manually.
3380 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3382 def guessProjectName(self):
3383 for p in self.depotPaths:
3386 p = p[p.strip().rfind("/") + 1:]
3387 if not p.endswith("/"):
3391 def getBranchMapping(self):
3392 lostAndFoundBranches = set()
3394 user = gitConfig("git-p4.branchUser")
3396 command = "branches -u %s" % user
3398 command = "branches"
3400 for info in p4CmdList(command):
3401 details = p4Cmd(["branch", "-o", info["branch"]])
3403 while "View%s" % viewIdx in details:
3404 paths = details["View%s" % viewIdx].split(" ")
3405 viewIdx = viewIdx + 1
3406 # require standard //depot/foo/... //depot/bar/... mapping
3407 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3410 destination = paths[1]
3412 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3413 source = source[len(self.depotPaths[0]):-4]
3414 destination = destination[len(self.depotPaths[0]):-4]
3416 if destination in self.knownBranches:
3418 print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3419 print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3422 self.knownBranches[destination] = source
3424 lostAndFoundBranches.discard(destination)
3426 if source not in self.knownBranches:
3427 lostAndFoundBranches.add(source)
3429 # Perforce does not strictly require branches to be defined, so we also
3430 # check git config for a branch list.
3432 # Example of branch definition in git config file:
3434 # branchList=main:branchA
3435 # branchList=main:branchB
3436 # branchList=branchA:branchC
3437 configBranches = gitConfigList("git-p4.branchList")
3438 for branch in configBranches:
3440 (source, destination) = branch.split(":")
3441 self.knownBranches[destination] = source
3443 lostAndFoundBranches.discard(destination)
3445 if source not in self.knownBranches:
3446 lostAndFoundBranches.add(source)
3449 for branch in lostAndFoundBranches:
3450 self.knownBranches[branch] = branch
3452 def getBranchMappingFromGitBranches(self):
3453 branches = p4BranchesInGit(self.importIntoRemotes)
3454 for branch in branches.keys():
3455 if branch == "master":
3458 branch = branch[len(self.projectName):]
3459 self.knownBranches[branch] = branch
3461 def updateOptionDict(self, d):
3463 if self.keepRepoPath:
3464 option_keys['keepRepoPath'] = 1
3466 d["options"] = ' '.join(sorted(option_keys.keys()))
3468 def readOptions(self, d):
3469 self.keepRepoPath = ('options' in d
3470 and ('keepRepoPath' in d['options']))
3472 def gitRefForBranch(self, branch):
3473 if branch == "main":
3474 return self.refPrefix + "master"
3476 if len(branch) <= 0:
3479 return self.refPrefix + self.projectName + branch
3481 def gitCommitByP4Change(self, ref, change):
3483 print("looking in ref " + ref + " for change %s using bisect..." % change)
3486 latestCommit = parseRevision(ref)
3490 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3491 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3496 log = extractLogMessageFromGitCommit(next)
3497 settings = extractSettingsGitLog(log)
3498 currentChange = int(settings['change'])
3500 print("current change %s" % currentChange)
3502 if currentChange == change:
3504 print("found %s" % next)
3507 if currentChange < change:
3508 earliestCommit = "^%s" % next
3510 if next == latestCommit:
3511 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3512 latestCommit = "%s^@" % next
3516 def importNewBranch(self, branch, maxChange):
3517 # make fast-import flush all changes to disk and update the refs using the checkpoint
3518 # command so that we can try to find the branch parent in the git history
3519 self.gitStream.write("checkpoint\n\n");
3520 self.gitStream.flush();
3521 branchPrefix = self.depotPaths[0] + branch + "/"
3522 range = "@1,%s" % maxChange
3523 #print "prefix" + branchPrefix
3524 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3525 if len(changes) <= 0:
3527 firstChange = changes[0]
3528 #print "first change in branch: %s" % firstChange
3529 sourceBranch = self.knownBranches[branch]
3530 sourceDepotPath = self.depotPaths[0] + sourceBranch
3531 sourceRef = self.gitRefForBranch(sourceBranch)
3532 #print "source " + sourceBranch
3534 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3535 #print "branch parent: %s" % branchParentChange
3536 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3537 if len(gitParent) > 0:
3538 self.initialParents[self.gitRefForBranch(branch)] = gitParent
3539 #print "parent git commit: %s" % gitParent
3541 self.importChanges(changes)
3544 def searchParent(self, parent, branch, target):
3545 targetTree = read_pipe(["git", "rev-parse",
3546 "{}^{{tree}}".format(target)]).strip()
3547 for line in read_pipe_lines(["git", "rev-list", "--format=%H %T",
3548 "--no-merges", parent]):
3549 if line.startswith("commit "):
3551 commit, tree = line.strip().split(" ")
3552 if tree == targetTree:
3554 print("Found parent of %s in commit %s" % (branch, commit))
3558 def importChanges(self, changes, origin_revision=0):
3560 for change in changes:
3561 description = p4_describe(change)
3562 self.updateOptionDict(description)
3565 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3570 if self.detectBranches:
3571 branches = self.splitFilesIntoBranches(description)
3572 for branch in branches.keys():
3574 branchPrefix = self.depotPaths[0] + branch + "/"
3575 self.branchPrefixes = [ branchPrefix ]
3579 filesForCommit = branches[branch]
3582 print("branch is %s" % branch)
3584 self.updatedBranches.add(branch)
3586 if branch not in self.createdBranches:
3587 self.createdBranches.add(branch)
3588 parent = self.knownBranches[branch]
3589 if parent == branch:
3592 fullBranch = self.projectName + branch
3593 if fullBranch not in self.p4BranchesInGit:
3595 print("\n Importing new branch %s" % fullBranch);
3596 if self.importNewBranch(branch, change - 1):
3598 self.p4BranchesInGit.append(fullBranch)
3600 print("\n Resuming with change %s" % change);
3603 print("parent determined through known branches: %s" % parent)
3605 branch = self.gitRefForBranch(branch)
3606 parent = self.gitRefForBranch(parent)
3609 print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3611 if len(parent) == 0 and branch in self.initialParents:
3612 parent = self.initialParents[branch]
3613 del self.initialParents[branch]
3617 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3619 print("Creating temporary branch: " + tempBranch)
3620 self.commit(description, filesForCommit, tempBranch)
3621 self.tempBranches.append(tempBranch)
3623 blob = self.searchParent(parent, branch, tempBranch)
3625 self.commit(description, filesForCommit, branch, blob)
3628 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3629 self.commit(description, filesForCommit, branch, parent)
3631 files = self.extractFilesFromCommit(description)
3632 self.commit(description, files, self.branch,
3634 # only needed once, to connect to the previous commit
3635 self.initialParent = ""
3637 print(self.gitError.read())
3640 def sync_origin_only(self):
3641 if self.syncWithOrigin:
3642 self.hasOrigin = originP4BranchesExist()
3645 print('Syncing with origin first, using "git fetch origin"')
3646 system("git fetch origin")
3648 def importHeadRevision(self, revision):
3649 print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3652 details["user"] = "git perforce import user"
3653 details["desc"] = ("Initial import of %s from the state at revision %s\n"
3654 % (' '.join(self.depotPaths), revision))
3655 details["change"] = revision
3659 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3661 for info in p4CmdList(["files"] + fileArgs):
3663 if 'code' in info and info['code'] == 'error':
3664 sys.stderr.write("p4 returned an error: %s\n"
3666 if info['data'].find("must refer to client") >= 0:
3667 sys.stderr.write("This particular p4 error is misleading.\n")
3668 sys.stderr.write("Perhaps the depot path was misspelled.\n");
3669 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
3671 if 'p4ExitCode' in info:
3672 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3676 change = int(info["change"])
3677 if change > newestRevision:
3678 newestRevision = change
3680 if info["action"] in self.delete_actions:
3681 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3682 #fileCnt = fileCnt + 1
3685 for prop in ["depotFile", "rev", "action", "type" ]:
3686 details["%s%s" % (prop, fileCnt)] = info[prop]
3688 fileCnt = fileCnt + 1
3690 details["change"] = newestRevision
3692 # Use time from top-most change so that all git p4 clones of
3693 # the same p4 repo have the same commit SHA1s.
3694 res = p4_describe(newestRevision)
3695 details["time"] = res["time"]
3697 self.updateOptionDict(details)
3699 self.commit(details, self.extractFilesFromCommit(details), self.branch)
3700 except IOError as err:
3701 print("IO error with git fast-import. Is your git version recent enough?")
3702 print("IO error details: {}".format(err))
3703 print(self.gitError.read())
3706 def importRevisions(self, args, branch_arg_given):
3709 if len(self.changesFile) > 0:
3710 with open(self.changesFile) as f:
3711 output = f.readlines()
3714 changeSet.add(int(line))
3716 for change in changeSet:
3717 changes.append(change)
3721 # catch "git p4 sync" with no new branches, in a repo that
3722 # does not have any existing p4 branches
3724 if not self.p4BranchesInGit:
3725 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3727 # The default branch is master, unless --branch is used to
3728 # specify something else. Make sure it exists, or complain
3729 # nicely about how to use --branch.
3730 if not self.detectBranches:
3731 if not branch_exists(self.branch):
3732 if branch_arg_given:
3733 raise P4CommandException("Error: branch %s does not exist." % self.branch)
3735 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
3739 print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3741 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3743 if len(self.maxChanges) > 0:
3744 changes = changes[:min(int(self.maxChanges), len(changes))]
3746 if len(changes) == 0:
3748 print("No changes to import!")
3750 if not self.silent and not self.detectBranches:
3751 print("Import destination: %s" % self.branch)
3753 self.updatedBranches = set()
3755 if not self.detectBranches:
3757 # start a new branch
3758 self.initialParent = ""
3760 # build on a previous revision
3761 self.initialParent = parseRevision(self.branch)
3763 self.importChanges(changes)
3767 if len(self.updatedBranches) > 0:
3768 sys.stdout.write("Updated branches: ")
3769 for b in self.updatedBranches:
3770 sys.stdout.write("%s " % b)
3771 sys.stdout.write("\n")
3773 def openStreams(self):
3774 self.importProcess = subprocess.Popen(["git", "fast-import"],
3775 stdin=subprocess.PIPE,
3776 stdout=subprocess.PIPE,
3777 stderr=subprocess.PIPE);
3778 self.gitOutput = self.importProcess.stdout
3779 self.gitStream = self.importProcess.stdin
3780 self.gitError = self.importProcess.stderr
3782 if bytes is not str:
3783 # Wrap gitStream.write() so that it can be called using `str` arguments
3784 def make_encoded_write(write):
3785 def encoded_write(s):
3786 return write(s.encode() if isinstance(s, str) else s)
3787 return encoded_write
3789 self.gitStream.write = make_encoded_write(self.gitStream.write)
3791 def closeStreams(self):
3792 if self.gitStream is None:
3794 self.gitStream.close()
3795 if self.importProcess.wait() != 0:
3796 die("fast-import failed: %s" % self.gitError.read())
3797 self.gitOutput.close()
3798 self.gitError.close()
3799 self.gitStream = None
3801 def run(self, args):
3802 if self.importIntoRemotes:
3803 self.refPrefix = "refs/remotes/p4/"
3805 self.refPrefix = "refs/heads/p4/"
3807 self.sync_origin_only()
3809 branch_arg_given = bool(self.branch)
3810 if len(self.branch) == 0:
3811 self.branch = self.refPrefix + "master"
3812 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3813 system("git update-ref %s refs/heads/p4" % self.branch)
3814 system("git branch -D p4")
3816 # accept either the command-line option, or the configuration variable
3817 if self.useClientSpec:
3818 # will use this after clone to set the variable
3819 self.useClientSpec_from_options = True
3821 if gitConfigBool("git-p4.useclientspec"):
3822 self.useClientSpec = True
3823 if self.useClientSpec:
3824 self.clientSpecDirs = getClientSpec()
3826 # TODO: should always look at previous commits,
3827 # merge with previous imports, if possible.
3830 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3832 # branches holds mapping from branch name to sha1
3833 branches = p4BranchesInGit(self.importIntoRemotes)
3835 # restrict to just this one, disabling detect-branches
3836 if branch_arg_given:
3837 short = self.branch.split("/")[-1]
3838 if short in branches:
3839 self.p4BranchesInGit = [ short ]
3841 self.p4BranchesInGit = branches.keys()
3843 if len(self.p4BranchesInGit) > 1:
3845 print("Importing from/into multiple branches")
3846 self.detectBranches = True
3847 for branch in branches.keys():
3848 self.initialParents[self.refPrefix + branch] = \
3852 print("branches: %s" % self.p4BranchesInGit)
3855 for branch in self.p4BranchesInGit:
3856 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
3858 settings = extractSettingsGitLog(logMsg)
3860 self.readOptions(settings)
3861 if ('depot-paths' in settings
3862 and 'change' in settings):
3863 change = int(settings['change']) + 1
3864 p4Change = max(p4Change, change)
3866 depotPaths = sorted(settings['depot-paths'])
3867 if self.previousDepotPaths == []:
3868 self.previousDepotPaths = depotPaths
3871 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3872 prev_list = prev.split("/")
3873 cur_list = cur.split("/")
3874 for i in range(0, min(len(cur_list), len(prev_list))):
3875 if cur_list[i] != prev_list[i]:
3879 paths.append ("/".join(cur_list[:i + 1]))
3881 self.previousDepotPaths = paths
3884 self.depotPaths = sorted(self.previousDepotPaths)
3885 self.changeRange = "@%s,#head" % p4Change
3886 if not self.silent and not self.detectBranches:
3887 print("Performing incremental import into %s git branch" % self.branch)
3889 # accept multiple ref name abbreviations:
3890 # refs/foo/bar/branch -> use it exactly
3891 # p4/branch -> prepend refs/remotes/ or refs/heads/
3892 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3893 if not self.branch.startswith("refs/"):
3894 if self.importIntoRemotes:
3895 prepend = "refs/remotes/"
3897 prepend = "refs/heads/"
3898 if not self.branch.startswith("p4/"):
3900 self.branch = prepend + self.branch
3902 if len(args) == 0 and self.depotPaths:
3904 print("Depot paths: %s" % ' '.join(self.depotPaths))
3906 if self.depotPaths and self.depotPaths != args:
3907 print("previous import used depot path %s and now %s was specified. "
3908 "This doesn't work!" % (' '.join (self.depotPaths),
3912 self.depotPaths = sorted(args)
3917 # Make sure no revision specifiers are used when --changesfile
3919 bad_changesfile = False
3920 if len(self.changesFile) > 0:
3921 for p in self.depotPaths:
3922 if p.find("@") >= 0 or p.find("#") >= 0:
3923 bad_changesfile = True
3926 die("Option --changesfile is incompatible with revision specifiers")
3929 for p in self.depotPaths:
3930 if p.find("@") != -1:
3931 atIdx = p.index("@")
3932 self.changeRange = p[atIdx:]
3933 if self.changeRange == "@all":
3934 self.changeRange = ""
3935 elif ',' not in self.changeRange:
3936 revision = self.changeRange
3937 self.changeRange = ""
3939 elif p.find("#") != -1:
3940 hashIdx = p.index("#")
3941 revision = p[hashIdx:]
3943 elif self.previousDepotPaths == []:
3944 # pay attention to changesfile, if given, else import
3945 # the entire p4 tree at the head revision
3946 if len(self.changesFile) == 0:
3949 p = re.sub ("\.\.\.$", "", p)
3950 if not p.endswith("/"):
3955 self.depotPaths = newPaths
3957 # --detect-branches may change this for each branch
3958 self.branchPrefixes = self.depotPaths
3960 self.loadUserMapFromCache()
3962 if self.detectLabels:
3965 if self.detectBranches:
3966 ## FIXME - what's a P4 projectName ?
3967 self.projectName = self.guessProjectName()
3970 self.getBranchMappingFromGitBranches()
3972 self.getBranchMapping()
3974 print("p4-git branches: %s" % self.p4BranchesInGit)
3975 print("initial parents: %s" % self.initialParents)
3976 for b in self.p4BranchesInGit:
3980 b = b[len(self.projectName):]
3981 self.createdBranches.add(b)
3991 self.importHeadRevision(revision)
3993 self.importRevisions(args, branch_arg_given)
3995 if gitConfigBool("git-p4.importLabels"):
3996 self.importLabels = True
3998 if self.importLabels:
3999 p4Labels = getP4Labels(self.depotPaths)
4000 gitTags = getGitTags()
4002 missingP4Labels = p4Labels - gitTags
4003 self.importP4Labels(self.gitStream, missingP4Labels)
4005 except P4CommandException as e:
4014 # Cleanup temporary branches created during import
4015 if self.tempBranches != []:
4016 for branch in self.tempBranches:
4017 read_pipe("git update-ref -d %s" % branch)
4018 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
4020 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4021 # a convenient shortcut refname "p4".
4022 if self.importIntoRemotes:
4023 head_ref = self.refPrefix + "HEAD"
4024 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
4025 system(["git", "symbolic-ref", head_ref, self.branch])
4029 class P4Rebase(Command):
4031 Command.__init__(self)
4033 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
4035 self.importLabels = False
4036 self.description = ("Fetches the latest revision from perforce and "
4037 + "rebases the current work (branch) against it")
4039 def run(self, args):
4041 sync.importLabels = self.importLabels
4044 return self.rebase()
4047 if os.system("git update-index --refresh") != 0:
4048 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.");
4049 if len(read_pipe("git diff-index HEAD --")) > 0:
4050 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
4052 [upstream, settings] = findUpstreamBranchPoint()
4053 if len(upstream) == 0:
4054 die("Cannot find upstream branchpoint for rebase")
4056 # the branchpoint may be p4/foo~3, so strip off the parent
4057 upstream = re.sub("~[0-9]+$", "", upstream)
4059 print("Rebasing the current branch onto %s" % upstream)
4060 oldHead = read_pipe("git rev-parse HEAD").strip()
4061 system("git rebase %s" % upstream)
4062 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
4065 class P4Clone(P4Sync):
4067 P4Sync.__init__(self)
4068 self.description = "Creates a new git repository and imports from Perforce into it"
4069 self.usage = "usage: %prog [options] //depot/path[@revRange]"
4071 optparse.make_option("--destination", dest="cloneDestination",
4072 action='store', default=None,
4073 help="where to leave result of the clone"),
4074 optparse.make_option("--bare", dest="cloneBare",
4075 action="store_true", default=False),
4077 self.cloneDestination = None
4078 self.needsGit = False
4079 self.cloneBare = False
4081 def defaultDestination(self, args):
4082 ## TODO: use common prefix of args?
4084 depotDir = re.sub("(@[^@]*)$", "", depotPath)
4085 depotDir = re.sub("(#[^#]*)$", "", depotDir)
4086 depotDir = re.sub(r"\.\.\.$", "", depotDir)
4087 depotDir = re.sub(r"/$", "", depotDir)
4088 return os.path.split(depotDir)[1]
4090 def run(self, args):
4094 if self.keepRepoPath and not self.cloneDestination:
4095 sys.stderr.write("Must specify destination for --keep-path\n")
4100 if not self.cloneDestination and len(depotPaths) > 1:
4101 self.cloneDestination = depotPaths[-1]
4102 depotPaths = depotPaths[:-1]
4104 for p in depotPaths:
4105 if not p.startswith("//"):
4106 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
4109 if not self.cloneDestination:
4110 self.cloneDestination = self.defaultDestination(args)
4112 print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
4114 if not os.path.exists(self.cloneDestination):
4115 os.makedirs(self.cloneDestination)
4116 chdir(self.cloneDestination)
4118 init_cmd = [ "git", "init" ]
4120 init_cmd.append("--bare")
4121 retcode = subprocess.call(init_cmd)
4123 raise CalledProcessError(retcode, init_cmd)
4125 if not P4Sync.run(self, depotPaths):
4128 # create a master branch and check out a work tree
4129 if gitBranchExists(self.branch):
4130 system([ "git", "branch", currentGitBranch(), self.branch ])
4131 if not self.cloneBare:
4132 system([ "git", "checkout", "-f" ])
4134 print('Not checking out any branch, use ' \
4135 '"git checkout -q -b master <branch>"')
4137 # auto-set this variable if invoked with --use-client-spec
4138 if self.useClientSpec_from_options:
4139 system("git config --bool git-p4.useclientspec true")
4143 class P4Unshelve(Command):
4145 Command.__init__(self)
4147 self.origin = "HEAD"
4148 self.description = "Unshelve a P4 changelist into a git commit"
4149 self.usage = "usage: %prog [options] changelist"
4151 optparse.make_option("--origin", dest="origin",
4152 help="Use this base revision instead of the default (%s)" % self.origin),
4154 self.verbose = False
4155 self.noCommit = False
4156 self.destbranch = "refs/remotes/p4-unshelved"
4158 def renameBranch(self, branch_name):
4159 """ Rename the existing branch to branch_name.N
4163 for i in range(0,1000):
4164 backup_branch_name = "{0}.{1}".format(branch_name, i)
4165 if not gitBranchExists(backup_branch_name):
4166 gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
4167 gitDeleteRef(branch_name)
4169 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4173 sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4175 def findLastP4Revision(self, starting_point):
4176 """ Look back from starting_point for the first commit created by git-p4
4177 to find the P4 commit we are based on, and the depot-paths.
4180 for parent in (range(65535)):
4181 log = extractLogMessageFromGitCommit("{0}~{1}".format(starting_point, parent))
4182 settings = extractSettingsGitLog(log)
4183 if 'change' in settings:
4186 sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4188 def createShelveParent(self, change, branch_name, sync, origin):
4189 """ Create a commit matching the parent of the shelved changelist 'change'
4191 parent_description = p4_describe(change, shelved=True)
4192 parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4193 files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4197 # if it was added in the shelved changelist, it won't exist in the parent
4198 if f['action'] in self.add_actions:
4201 # if it was deleted in the shelved changelist it must not be deleted
4202 # in the parent - we might even need to create it if the origin branch
4204 if f['action'] in self.delete_actions:
4207 parent_files.append(f)
4209 sync.commit(parent_description, parent_files, branch_name,
4210 parent=origin, allow_empty=True)
4211 print("created parent commit for {0} based on {1} in {2}".format(
4212 change, self.origin, branch_name))
4214 def run(self, args):
4218 if not gitBranchExists(self.origin):
4219 sys.exit("origin branch {0} does not exist".format(self.origin))
4224 # only one change at a time
4227 # if the target branch already exists, rename it
4228 branch_name = "{0}/{1}".format(self.destbranch, change)
4229 if gitBranchExists(branch_name):
4230 self.renameBranch(branch_name)
4231 sync.branch = branch_name
4233 sync.verbose = self.verbose
4234 sync.suppress_meta_comment = True
4236 settings = self.findLastP4Revision(self.origin)
4237 sync.depotPaths = settings['depot-paths']
4238 sync.branchPrefixes = sync.depotPaths
4241 sync.loadUserMapFromCache()
4244 # create a commit for the parent of the shelved changelist
4245 self.createShelveParent(change, branch_name, sync, self.origin)
4247 # create the commit for the shelved changelist itself
4248 description = p4_describe(change, True)
4249 files = sync.extractFilesFromCommit(description, True, change)
4251 sync.commit(description, files, branch_name, "")
4254 print("unshelved changelist {0} into {1}".format(change, branch_name))
4258 class P4Branches(Command):
4260 Command.__init__(self)
4262 self.description = ("Shows the git branches that hold imports and their "
4263 + "corresponding perforce depot paths")
4264 self.verbose = False
4266 def run(self, args):
4267 if originP4BranchesExist():
4268 createOrUpdateBranchesFromOrigin()
4270 cmdline = "git rev-parse --symbolic "
4271 cmdline += " --remotes"
4273 for line in read_pipe_lines(cmdline):
4276 if not line.startswith('p4/') or line == "p4/HEAD":
4280 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4281 settings = extractSettingsGitLog(log)
4283 print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4286 class HelpFormatter(optparse.IndentedHelpFormatter):
4288 optparse.IndentedHelpFormatter.__init__(self)
4290 def format_description(self, description):
4292 return description + "\n"
4296 def printUsage(commands):
4297 print("usage: %s <command> [options]" % sys.argv[0])
4299 print("valid commands: %s" % ", ".join(commands))
4301 print("Try %s <command> --help for command specific help." % sys.argv[0])
4306 "submit" : P4Submit,
4307 "commit" : P4Submit,
4309 "rebase" : P4Rebase,
4311 "rollback" : P4RollBack,
4312 "branches" : P4Branches,
4313 "unshelve" : P4Unshelve,
4317 if len(sys.argv[1:]) == 0:
4318 printUsage(commands.keys())
4321 cmdName = sys.argv[1]
4323 klass = commands[cmdName]
4326 print("unknown command %s" % cmdName)
4328 printUsage(commands.keys())
4331 options = cmd.options
4332 cmd.gitdir = os.environ.get("GIT_DIR", None)
4336 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4338 options.append(optparse.make_option("--git-dir", dest="gitdir"))
4340 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4342 description = cmd.description,
4343 formatter = HelpFormatter())
4346 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
4352 verbose = cmd.verbose
4354 if cmd.gitdir == None:
4355 cmd.gitdir = os.path.abspath(".git")
4356 if not isValidGitDir(cmd.gitdir):
4357 # "rev-parse --git-dir" without arguments will try $PWD/.git
4358 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
4359 if os.path.exists(cmd.gitdir):
4360 cdup = read_pipe("git rev-parse --show-cdup").strip()
4364 if not isValidGitDir(cmd.gitdir):
4365 if isValidGitDir(cmd.gitdir + "/.git"):
4366 cmd.gitdir += "/.git"
4368 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4370 # so git commands invoked from the P4 workspace will succeed
4371 os.environ["GIT_DIR"] = cmd.gitdir
4373 if not cmd.run(args):
4378 if __name__ == '__main__':