Merge branch 'ep/maint-equals-null-cocci'
[git/debian.git] / git-p4.py
blobc47abb4bff999838aa195b52f5583280f1142a0e
1 #!/usr/bin/env python
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>
7 # 2007 Trolltech ASA
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
10 # pylint: disable=bad-whitespace
11 # pylint: disable=broad-except
12 # pylint: disable=consider-iterating-dictionary
13 # pylint: disable=disable
14 # pylint: disable=fixme
15 # pylint: disable=invalid-name
16 # pylint: disable=line-too-long
17 # pylint: disable=missing-docstring
18 # pylint: disable=no-self-use
19 # pylint: disable=superfluous-parens
20 # pylint: disable=too-few-public-methods
21 # pylint: disable=too-many-arguments
22 # pylint: disable=too-many-branches
23 # pylint: disable=too-many-instance-attributes
24 # pylint: disable=too-many-lines
25 # pylint: disable=too-many-locals
26 # pylint: disable=too-many-nested-blocks
27 # pylint: disable=too-many-statements
28 # pylint: disable=ungrouped-imports
29 # pylint: disable=unused-import
30 # pylint: disable=wrong-import-order
31 # pylint: disable=wrong-import-position
34 import sys
35 if sys.version_info.major < 3 and sys.version_info.minor < 7:
36 sys.stderr.write("git-p4: requires Python 2.7 or later.\n")
37 sys.exit(1)
39 import ctypes
40 import errno
41 import functools
42 import glob
43 import marshal
44 import optparse
45 import os
46 import platform
47 import re
48 import shutil
49 import stat
50 import subprocess
51 import tempfile
52 import time
53 import zipfile
54 import zlib
56 # On python2.7 where raw_input() and input() are both availble,
57 # we want raw_input's semantics, but aliased to input for python3
58 # compatibility
59 # support basestring in python3
60 try:
61 if raw_input and input:
62 input = raw_input
63 except:
64 pass
66 verbose = False
68 # Only labels/tags matching this will be imported/exported
69 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
71 # The block size is reduced automatically if required
72 defaultBlockSize = 1 << 20
74 p4_access_checked = False
76 re_ko_keywords = re.compile(br'\$(Id|Header)(:[^$\n]+)?\$')
77 re_k_keywords = re.compile(br'\$(Id|Header|Author|Date|DateTime|Change|File|Revision)(:[^$\n]+)?\$')
80 def format_size_human_readable(num):
81 """Returns a number of units (typically bytes) formatted as a
82 human-readable string.
83 """
84 if num < 1024:
85 return '{:d} B'.format(num)
86 for unit in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
87 num /= 1024.0
88 if num < 1024.0:
89 return "{:3.1f} {}B".format(num, unit)
90 return "{:.1f} YiB".format(num)
93 def p4_build_cmd(cmd):
94 """Build a suitable p4 command line.
96 This consolidates building and returning a p4 command line into one
97 location. It means that hooking into the environment, or other
98 configuration can be done more easily.
99 """
100 real_cmd = ["p4"]
102 user = gitConfig("git-p4.user")
103 if len(user) > 0:
104 real_cmd += ["-u", user]
106 password = gitConfig("git-p4.password")
107 if len(password) > 0:
108 real_cmd += ["-P", password]
110 port = gitConfig("git-p4.port")
111 if len(port) > 0:
112 real_cmd += ["-p", port]
114 host = gitConfig("git-p4.host")
115 if len(host) > 0:
116 real_cmd += ["-H", host]
118 client = gitConfig("git-p4.client")
119 if len(client) > 0:
120 real_cmd += ["-c", client]
122 retries = gitConfigInt("git-p4.retries")
123 if retries is None:
124 # Perform 3 retries by default
125 retries = 3
126 if retries > 0:
127 # Provide a way to not pass this option by setting git-p4.retries to 0
128 real_cmd += ["-r", str(retries)]
130 real_cmd += cmd
132 # now check that we can actually talk to the server
133 global p4_access_checked
134 if not p4_access_checked:
135 p4_access_checked = True # suppress access checks in p4_check_access itself
136 p4_check_access()
138 return real_cmd
141 def git_dir(path):
142 """Return TRUE if the given path is a git directory (/path/to/dir/.git).
143 This won't automatically add ".git" to a directory.
145 d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
146 if not d or len(d) == 0:
147 return None
148 else:
149 return d
152 def chdir(path, is_client_path=False):
153 """Do chdir to the given path, and set the PWD environment variable for use
154 by P4. It does not look at getcwd() output. Since we're not using the
155 shell, it is necessary to set the PWD environment variable explicitly.
157 Normally, expand the path to force it to be absolute. This addresses
158 the use of relative path names inside P4 settings, e.g.
159 P4CONFIG=.p4config. P4 does not simply open the filename as given; it
160 looks for .p4config using PWD.
162 If is_client_path, the path was handed to us directly by p4, and may be
163 a symbolic link. Do not call os.getcwd() in this case, because it will
164 cause p4 to think that PWD is not inside the client path.
167 os.chdir(path)
168 if not is_client_path:
169 path = os.getcwd()
170 os.environ['PWD'] = path
173 def calcDiskFree():
174 """Return free space in bytes on the disk of the given dirname."""
175 if platform.system() == 'Windows':
176 free_bytes = ctypes.c_ulonglong(0)
177 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
178 return free_bytes.value
179 else:
180 st = os.statvfs(os.getcwd())
181 return st.f_bavail * st.f_frsize
184 def die(msg):
185 """Terminate execution. Make sure that any running child processes have
186 been wait()ed for before calling this.
188 if verbose:
189 raise Exception(msg)
190 else:
191 sys.stderr.write(msg + "\n")
192 sys.exit(1)
195 def prompt(prompt_text):
196 """Prompt the user to choose one of the choices.
198 Choices are identified in the prompt_text by square brackets around a
199 single letter option.
201 choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text))
202 while True:
203 sys.stderr.flush()
204 sys.stdout.write(prompt_text)
205 sys.stdout.flush()
206 response = sys.stdin.readline().strip().lower()
207 if not response:
208 continue
209 response = response[0]
210 if response in choices:
211 return response
214 # We need different encoding/decoding strategies for text data being passed
215 # around in pipes depending on python version
216 if bytes is not str:
217 # For python3, always encode and decode as appropriate
218 def decode_text_stream(s):
219 return s.decode() if isinstance(s, bytes) else s
221 def encode_text_stream(s):
222 return s.encode() if isinstance(s, str) else s
223 else:
224 # For python2.7, pass read strings as-is, but also allow writing unicode
225 def decode_text_stream(s):
226 return s
228 def encode_text_stream(s):
229 return s.encode('utf_8') if isinstance(s, unicode) else s
232 def decode_path(path):
233 """Decode a given string (bytes or otherwise) using configured path
234 encoding options.
237 encoding = gitConfig('git-p4.pathEncoding') or 'utf_8'
238 if bytes is not str:
239 return path.decode(encoding, errors='replace') if isinstance(path, bytes) else path
240 else:
241 try:
242 path.decode('ascii')
243 except:
244 path = path.decode(encoding, errors='replace')
245 if verbose:
246 print('Path with non-ASCII characters detected. Used {} to decode: {}'.format(encoding, path))
247 return path
250 def run_git_hook(cmd, param=[]):
251 """Execute a hook if the hook exists."""
252 args = ['git', 'hook', 'run', '--ignore-missing', cmd]
253 if param:
254 args.append("--")
255 for p in param:
256 args.append(p)
257 return subprocess.call(args) == 0
260 def write_pipe(c, stdin, *k, **kw):
261 if verbose:
262 sys.stderr.write('Writing pipe: {}\n'.format(' '.join(c)))
264 p = subprocess.Popen(c, stdin=subprocess.PIPE, *k, **kw)
265 pipe = p.stdin
266 val = pipe.write(stdin)
267 pipe.close()
268 if p.wait():
269 die('Command failed: {}'.format(' '.join(c)))
271 return val
274 def p4_write_pipe(c, stdin, *k, **kw):
275 real_cmd = p4_build_cmd(c)
276 if bytes is not str and isinstance(stdin, str):
277 stdin = encode_text_stream(stdin)
278 return write_pipe(real_cmd, stdin, *k, **kw)
281 def read_pipe_full(c, *k, **kw):
282 """Read output from command. Returns a tuple of the return status, stdout
283 text and stderr text.
285 if verbose:
286 sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c)))
288 p = subprocess.Popen(
289 c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, *k, **kw)
290 out, err = p.communicate()
291 return (p.returncode, out, decode_text_stream(err))
294 def read_pipe(c, ignore_error=False, raw=False, *k, **kw):
295 """Read output from command. Returns the output text on success. On
296 failure, terminates execution, unless ignore_error is True, when it
297 returns an empty string.
299 If raw is True, do not attempt to decode output text.
301 retcode, out, err = read_pipe_full(c, *k, **kw)
302 if retcode != 0:
303 if ignore_error:
304 out = ""
305 else:
306 die('Command failed: {}\nError: {}'.format(' '.join(c), err))
307 if not raw:
308 out = decode_text_stream(out)
309 return out
312 def read_pipe_text(c, *k, **kw):
313 """Read output from a command with trailing whitespace stripped. On error,
314 returns None.
316 retcode, out, err = read_pipe_full(c, *k, **kw)
317 if retcode != 0:
318 return None
319 else:
320 return decode_text_stream(out).rstrip()
323 def p4_read_pipe(c, ignore_error=False, raw=False, *k, **kw):
324 real_cmd = p4_build_cmd(c)
325 return read_pipe(real_cmd, ignore_error, raw=raw, *k, **kw)
328 def read_pipe_lines(c, raw=False, *k, **kw):
329 if verbose:
330 sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c)))
332 p = subprocess.Popen(c, stdout=subprocess.PIPE, *k, **kw)
333 pipe = p.stdout
334 lines = pipe.readlines()
335 if not raw:
336 lines = [decode_text_stream(line) for line in lines]
337 if pipe.close() or p.wait():
338 die('Command failed: {}'.format(' '.join(c)))
339 return lines
342 def p4_read_pipe_lines(c, *k, **kw):
343 """Specifically invoke p4 on the command supplied."""
344 real_cmd = p4_build_cmd(c)
345 return read_pipe_lines(real_cmd, *k, **kw)
348 def p4_has_command(cmd):
349 """Ask p4 for help on this command. If it returns an error, the command
350 does not exist in this version of p4.
352 real_cmd = p4_build_cmd(["help", cmd])
353 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
354 stderr=subprocess.PIPE)
355 p.communicate()
356 return p.returncode == 0
359 def p4_has_move_command():
360 """See if the move command exists, that it supports -k, and that it has not
361 been administratively disabled. The arguments must be correct, but the
362 filenames do not have to exist. Use ones with wildcards so even if they
363 exist, it will fail.
366 if not p4_has_command("move"):
367 return False
368 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
369 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
370 out, err = p.communicate()
371 err = decode_text_stream(err)
372 # return code will be 1 in either case
373 if err.find("Invalid option") >= 0:
374 return False
375 if err.find("disabled") >= 0:
376 return False
377 # assume it failed because @... was invalid changelist
378 return True
381 def system(cmd, ignore_error=False, *k, **kw):
382 if verbose:
383 sys.stderr.write("executing {}\n".format(
384 ' '.join(cmd) if isinstance(cmd, list) else cmd))
385 retcode = subprocess.call(cmd, *k, **kw)
386 if retcode and not ignore_error:
387 raise subprocess.CalledProcessError(retcode, cmd)
389 return retcode
392 def p4_system(cmd, *k, **kw):
393 """Specifically invoke p4 as the system command."""
394 real_cmd = p4_build_cmd(cmd)
395 retcode = subprocess.call(real_cmd, *k, **kw)
396 if retcode:
397 raise subprocess.CalledProcessError(retcode, real_cmd)
400 def die_bad_access(s):
401 die("failure accessing depot: {0}".format(s.rstrip()))
404 def p4_check_access(min_expiration=1):
405 """Check if we can access Perforce - account still logged in."""
407 results = p4CmdList(["login", "-s"])
409 if len(results) == 0:
410 # should never get here: always get either some results, or a p4ExitCode
411 assert("could not parse response from perforce")
413 result = results[0]
415 if 'p4ExitCode' in result:
416 # p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
417 die_bad_access("could not run p4")
419 code = result.get("code")
420 if not code:
421 # we get here if we couldn't connect and there was nothing to unmarshal
422 die_bad_access("could not connect")
424 elif code == "stat":
425 expiry = result.get("TicketExpiration")
426 if expiry:
427 expiry = int(expiry)
428 if expiry > min_expiration:
429 # ok to carry on
430 return
431 else:
432 die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
434 else:
435 # account without a timeout - all ok
436 return
438 elif code == "error":
439 data = result.get("data")
440 if data:
441 die_bad_access("p4 error: {0}".format(data))
442 else:
443 die_bad_access("unknown error")
444 elif code == "info":
445 return
446 else:
447 die_bad_access("unknown error code {0}".format(code))
450 _p4_version_string = None
453 def p4_version_string():
454 """Read the version string, showing just the last line, which hopefully is
455 the interesting version bit.
457 $ p4 -V
458 Perforce - The Fast Software Configuration Management System.
459 Copyright 1995-2011 Perforce Software. All rights reserved.
460 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
462 global _p4_version_string
463 if not _p4_version_string:
464 a = p4_read_pipe_lines(["-V"])
465 _p4_version_string = a[-1].rstrip()
466 return _p4_version_string
469 def p4_integrate(src, dest):
470 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
473 def p4_sync(f, *options):
474 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
477 def p4_add(f):
478 """Forcibly add file names with wildcards."""
479 if wildcard_present(f):
480 p4_system(["add", "-f", f])
481 else:
482 p4_system(["add", f])
485 def p4_delete(f):
486 p4_system(["delete", wildcard_encode(f)])
489 def p4_edit(f, *options):
490 p4_system(["edit"] + list(options) + [wildcard_encode(f)])
493 def p4_revert(f):
494 p4_system(["revert", wildcard_encode(f)])
497 def p4_reopen(type, f):
498 p4_system(["reopen", "-t", type, wildcard_encode(f)])
501 def p4_reopen_in_change(changelist, files):
502 cmd = ["reopen", "-c", str(changelist)] + files
503 p4_system(cmd)
506 def p4_move(src, dest):
507 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
510 def p4_last_change():
511 results = p4CmdList(["changes", "-m", "1"], skip_info=True)
512 return int(results[0]['change'])
515 def p4_describe(change, shelved=False):
516 """Make sure it returns a valid result by checking for the presence of
517 field "time".
519 Return a dict of the results.
522 cmd = ["describe", "-s"]
523 if shelved:
524 cmd += ["-S"]
525 cmd += [str(change)]
527 ds = p4CmdList(cmd, skip_info=True)
528 if len(ds) != 1:
529 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
531 d = ds[0]
533 if "p4ExitCode" in d:
534 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
535 str(d)))
536 if "code" in d:
537 if d["code"] == "error":
538 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
540 if "time" not in d:
541 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
543 return d
546 def split_p4_type(p4type):
547 """Canonicalize the p4 type and return a tuple of the base type, plus any
548 modifiers. See "p4 help filetypes" for a list and explanation.
551 p4_filetypes_historical = {
552 "ctempobj": "binary+Sw",
553 "ctext": "text+C",
554 "cxtext": "text+Cx",
555 "ktext": "text+k",
556 "kxtext": "text+kx",
557 "ltext": "text+F",
558 "tempobj": "binary+FSw",
559 "ubinary": "binary+F",
560 "uresource": "resource+F",
561 "uxbinary": "binary+Fx",
562 "xbinary": "binary+x",
563 "xltext": "text+Fx",
564 "xtempobj": "binary+Swx",
565 "xtext": "text+x",
566 "xunicode": "unicode+x",
567 "xutf16": "utf16+x",
569 if p4type in p4_filetypes_historical:
570 p4type = p4_filetypes_historical[p4type]
571 mods = ""
572 s = p4type.split("+")
573 base = s[0]
574 mods = ""
575 if len(s) > 1:
576 mods = s[1]
577 return (base, mods)
580 def p4_type(f):
581 """Return the raw p4 type of a file (text, text+ko, etc)."""
583 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
584 return results[0]['headType']
587 def p4_keywords_regexp_for_type(base, type_mods):
588 """Given a type base and modifier, return a regexp matching the keywords
589 that can be expanded in the file.
592 if base in ("text", "unicode", "binary"):
593 if "ko" in type_mods:
594 return re_ko_keywords
595 elif "k" in type_mods:
596 return re_k_keywords
597 else:
598 return None
599 else:
600 return None
603 def p4_keywords_regexp_for_file(file):
604 """Given a file, return a regexp matching the possible RCS keywords that
605 will be expanded, or None for files with kw expansion turned off.
608 if not os.path.exists(file):
609 return None
610 else:
611 type_base, type_mods = split_p4_type(p4_type(file))
612 return p4_keywords_regexp_for_type(type_base, type_mods)
615 def setP4ExecBit(file, mode):
616 """Reopens an already open file and changes the execute bit to match the
617 execute bit setting in the passed in mode.
620 p4Type = "+x"
622 if not isModeExec(mode):
623 p4Type = getP4OpenedType(file)
624 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
625 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
626 if p4Type[-1] == "+":
627 p4Type = p4Type[0:-1]
629 p4_reopen(p4Type, file)
632 def getP4OpenedType(file):
633 """Returns the perforce file type for the given file."""
635 result = p4_read_pipe(["opened", wildcard_encode(file)])
636 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
637 if match:
638 return match.group(1)
639 else:
640 die("Could not determine file type for %s (result: '%s')" % (file, result))
643 def getP4Labels(depotPaths):
644 """Return the set of all p4 labels."""
646 labels = set()
647 if not isinstance(depotPaths, list):
648 depotPaths = [depotPaths]
650 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
651 label = l['label']
652 labels.add(label)
654 return labels
657 def getGitTags():
658 """Return the set of all git tags."""
660 gitTags = set()
661 for line in read_pipe_lines(["git", "tag"]):
662 tag = line.strip()
663 gitTags.add(tag)
664 return gitTags
667 _diff_tree_pattern = None
670 def parseDiffTreeEntry(entry):
671 """Parses a single diff tree entry into its component elements.
673 See git-diff-tree(1) manpage for details about the format of the diff
674 output. This method returns a dictionary with the following elements:
676 src_mode - The mode of the source file
677 dst_mode - The mode of the destination file
678 src_sha1 - The sha1 for the source file
679 dst_sha1 - The sha1 fr the destination file
680 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
681 status_score - The score for the status (applicable for 'C' and 'R'
682 statuses). This is None if there is no score.
683 src - The path for the source file.
684 dst - The path for the destination file. This is only present for
685 copy or renames. If it is not present, this is None.
687 If the pattern is not matched, None is returned.
690 global _diff_tree_pattern
691 if not _diff_tree_pattern:
692 _diff_tree_pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
694 match = _diff_tree_pattern.match(entry)
695 if match:
696 return {
697 'src_mode': match.group(1),
698 'dst_mode': match.group(2),
699 'src_sha1': match.group(3),
700 'dst_sha1': match.group(4),
701 'status': match.group(5),
702 'status_score': match.group(6),
703 'src': match.group(7),
704 'dst': match.group(10)
706 return None
709 def isModeExec(mode):
710 """Returns True if the given git mode represents an executable file,
711 otherwise False.
713 return mode[-3:] == "755"
716 class P4Exception(Exception):
717 """Base class for exceptions from the p4 client."""
719 def __init__(self, exit_code):
720 self.p4ExitCode = exit_code
723 class P4ServerException(P4Exception):
724 """Base class for exceptions where we get some kind of marshalled up result
725 from the server.
728 def __init__(self, exit_code, p4_result):
729 super(P4ServerException, self).__init__(exit_code)
730 self.p4_result = p4_result
731 self.code = p4_result[0]['code']
732 self.data = p4_result[0]['data']
735 class P4RequestSizeException(P4ServerException):
736 """One of the maxresults or maxscanrows errors."""
738 def __init__(self, exit_code, p4_result, limit):
739 super(P4RequestSizeException, self).__init__(exit_code, p4_result)
740 self.limit = limit
743 class P4CommandException(P4Exception):
744 """Something went wrong calling p4 which means we have to give up."""
746 def __init__(self, msg):
747 self.msg = msg
749 def __str__(self):
750 return self.msg
753 def isModeExecChanged(src_mode, dst_mode):
754 return isModeExec(src_mode) != isModeExec(dst_mode)
757 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
758 errors_as_exceptions=False, *k, **kw):
760 cmd = p4_build_cmd(["-G"] + cmd)
761 if verbose:
762 sys.stderr.write("Opening pipe: {}\n".format(' '.join(cmd)))
764 # Use a temporary file to avoid deadlocks without
765 # subprocess.communicate(), which would put another copy
766 # of stdout into memory.
767 stdin_file = None
768 if stdin is not None:
769 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
770 if not isinstance(stdin, list):
771 stdin_file.write(stdin)
772 else:
773 for i in stdin:
774 stdin_file.write(encode_text_stream(i))
775 stdin_file.write(b'\n')
776 stdin_file.flush()
777 stdin_file.seek(0)
779 p4 = subprocess.Popen(
780 cmd, stdin=stdin_file, stdout=subprocess.PIPE, *k, **kw)
782 result = []
783 try:
784 while True:
785 entry = marshal.load(p4.stdout)
786 if bytes is not str:
787 # Decode unmarshalled dict to use str keys and values, except for:
788 # - `data` which may contain arbitrary binary data
789 # - `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text
790 decoded_entry = {}
791 for key, value in entry.items():
792 key = key.decode()
793 if isinstance(value, bytes) and not (key in ('data', 'path', 'clientFile') or key.startswith('depotFile')):
794 value = value.decode()
795 decoded_entry[key] = value
796 # Parse out data if it's an error response
797 if decoded_entry.get('code') == 'error' and 'data' in decoded_entry:
798 decoded_entry['data'] = decoded_entry['data'].decode()
799 entry = decoded_entry
800 if skip_info:
801 if 'code' in entry and entry['code'] == 'info':
802 continue
803 if cb is not None:
804 cb(entry)
805 else:
806 result.append(entry)
807 except EOFError:
808 pass
809 exitCode = p4.wait()
810 if exitCode != 0:
811 if errors_as_exceptions:
812 if len(result) > 0:
813 data = result[0].get('data')
814 if data:
815 m = re.search('Too many rows scanned \(over (\d+)\)', data)
816 if not m:
817 m = re.search('Request too large \(over (\d+)\)', data)
819 if m:
820 limit = int(m.group(1))
821 raise P4RequestSizeException(exitCode, result, limit)
823 raise P4ServerException(exitCode, result)
824 else:
825 raise P4Exception(exitCode)
826 else:
827 entry = {}
828 entry["p4ExitCode"] = exitCode
829 result.append(entry)
831 return result
834 def p4Cmd(cmd, *k, **kw):
835 list = p4CmdList(cmd, *k, **kw)
836 result = {}
837 for entry in list:
838 result.update(entry)
839 return result
842 def p4Where(depotPath):
843 if not depotPath.endswith("/"):
844 depotPath += "/"
845 depotPathLong = depotPath + "..."
846 outputList = p4CmdList(["where", depotPathLong])
847 output = None
848 for entry in outputList:
849 if "depotFile" in entry:
850 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
851 # The base path always ends with "/...".
852 entry_path = decode_path(entry['depotFile'])
853 if entry_path.find(depotPath) == 0 and entry_path[-4:] == "/...":
854 output = entry
855 break
856 elif "data" in entry:
857 data = entry.get("data")
858 space = data.find(" ")
859 if data[:space] == depotPath:
860 output = entry
861 break
862 if output is None:
863 return ""
864 if output["code"] == "error":
865 return ""
866 clientPath = ""
867 if "path" in output:
868 clientPath = decode_path(output['path'])
869 elif "data" in output:
870 data = output.get("data")
871 lastSpace = data.rfind(b" ")
872 clientPath = decode_path(data[lastSpace + 1:])
874 if clientPath.endswith("..."):
875 clientPath = clientPath[:-3]
876 return clientPath
879 def currentGitBranch():
880 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
883 def isValidGitDir(path):
884 return git_dir(path) is not None
887 def parseRevision(ref):
888 return read_pipe(["git", "rev-parse", ref]).strip()
891 def branchExists(ref):
892 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
893 ignore_error=True)
894 return len(rev) > 0
897 def extractLogMessageFromGitCommit(commit):
898 logMessage = ""
900 # fixme: title is first line of commit, not 1st paragraph.
901 foundTitle = False
902 for log in read_pipe_lines(["git", "cat-file", "commit", commit]):
903 if not foundTitle:
904 if len(log) == 1:
905 foundTitle = True
906 continue
908 logMessage += log
909 return logMessage
912 def extractSettingsGitLog(log):
913 values = {}
914 for line in log.split("\n"):
915 line = line.strip()
916 m = re.search(r"^ *\[git-p4: (.*)\]$", line)
917 if not m:
918 continue
920 assignments = m.group(1).split(':')
921 for a in assignments:
922 vals = a.split('=')
923 key = vals[0].strip()
924 val = ('='.join(vals[1:])).strip()
925 if val.endswith('\"') and val.startswith('"'):
926 val = val[1:-1]
928 values[key] = val
930 paths = values.get("depot-paths")
931 if not paths:
932 paths = values.get("depot-path")
933 if paths:
934 values['depot-paths'] = paths.split(',')
935 return values
938 def gitBranchExists(branch):
939 proc = subprocess.Popen(["git", "rev-parse", branch],
940 stderr=subprocess.PIPE, stdout=subprocess.PIPE)
941 return proc.wait() == 0
944 def gitUpdateRef(ref, newvalue):
945 subprocess.check_call(["git", "update-ref", ref, newvalue])
948 def gitDeleteRef(ref):
949 subprocess.check_call(["git", "update-ref", "-d", ref])
952 _gitConfig = {}
955 def gitConfig(key, typeSpecifier=None):
956 if key not in _gitConfig:
957 cmd = ["git", "config"]
958 if typeSpecifier:
959 cmd += [typeSpecifier]
960 cmd += [key]
961 s = read_pipe(cmd, ignore_error=True)
962 _gitConfig[key] = s.strip()
963 return _gitConfig[key]
966 def gitConfigBool(key):
967 """Return a bool, using git config --bool. It is True only if the
968 variable is set to true, and False if set to false or not present
969 in the config.
972 if key not in _gitConfig:
973 _gitConfig[key] = gitConfig(key, '--bool') == "true"
974 return _gitConfig[key]
977 def gitConfigInt(key):
978 if key not in _gitConfig:
979 cmd = ["git", "config", "--int", key]
980 s = read_pipe(cmd, ignore_error=True)
981 v = s.strip()
982 try:
983 _gitConfig[key] = int(gitConfig(key, '--int'))
984 except ValueError:
985 _gitConfig[key] = None
986 return _gitConfig[key]
989 def gitConfigList(key):
990 if key not in _gitConfig:
991 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
992 _gitConfig[key] = s.strip().splitlines()
993 if _gitConfig[key] == ['']:
994 _gitConfig[key] = []
995 return _gitConfig[key]
997 def fullP4Ref(incomingRef, importIntoRemotes=True):
998 """Standardize a given provided p4 ref value to a full git ref:
999 refs/foo/bar/branch -> use it exactly
1000 p4/branch -> prepend refs/remotes/ or refs/heads/
1001 branch -> prepend refs/remotes/p4/ or refs/heads/p4/"""
1002 if incomingRef.startswith("refs/"):
1003 return incomingRef
1004 if importIntoRemotes:
1005 prepend = "refs/remotes/"
1006 else:
1007 prepend = "refs/heads/"
1008 if not incomingRef.startswith("p4/"):
1009 prepend += "p4/"
1010 return prepend + incomingRef
1012 def shortP4Ref(incomingRef, importIntoRemotes=True):
1013 """Standardize to a "short ref" if possible:
1014 refs/foo/bar/branch -> ignore
1015 refs/remotes/p4/branch or refs/heads/p4/branch -> shorten
1016 p4/branch -> shorten"""
1017 if importIntoRemotes:
1018 longprefix = "refs/remotes/p4/"
1019 else:
1020 longprefix = "refs/heads/p4/"
1021 if incomingRef.startswith(longprefix):
1022 return incomingRef[len(longprefix):]
1023 if incomingRef.startswith("p4/"):
1024 return incomingRef[3:]
1025 return incomingRef
1027 def p4BranchesInGit(branchesAreInRemotes=True):
1028 """Find all the branches whose names start with "p4/", looking
1029 in remotes or heads as specified by the argument. Return
1030 a dictionary of { branch: revision } for each one found.
1031 The branch names are the short names, without any
1032 "p4/" prefix.
1035 branches = {}
1037 cmdline = ["git", "rev-parse", "--symbolic"]
1038 if branchesAreInRemotes:
1039 cmdline.append("--remotes")
1040 else:
1041 cmdline.append("--branches")
1043 for line in read_pipe_lines(cmdline):
1044 line = line.strip()
1046 # only import to p4/
1047 if not line.startswith('p4/'):
1048 continue
1049 # special symbolic ref to p4/master
1050 if line == "p4/HEAD":
1051 continue
1053 # strip off p4/ prefix
1054 branch = line[len("p4/"):]
1056 branches[branch] = parseRevision(line)
1058 return branches
1061 def branch_exists(branch):
1062 """Make sure that the given ref name really exists."""
1064 cmd = ["git", "rev-parse", "--symbolic", "--verify", branch]
1065 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1066 out, _ = p.communicate()
1067 out = decode_text_stream(out)
1068 if p.returncode:
1069 return False
1070 # expect exactly one line of output: the branch name
1071 return out.rstrip() == branch
1074 def findUpstreamBranchPoint(head="HEAD"):
1075 branches = p4BranchesInGit()
1076 # map from depot-path to branch name
1077 branchByDepotPath = {}
1078 for branch in branches.keys():
1079 tip = branches[branch]
1080 log = extractLogMessageFromGitCommit(tip)
1081 settings = extractSettingsGitLog(log)
1082 if "depot-paths" in settings:
1083 git_branch = "remotes/p4/" + branch
1084 paths = ",".join(settings["depot-paths"])
1085 branchByDepotPath[paths] = git_branch
1086 if "change" in settings:
1087 paths = paths + ";" + settings["change"]
1088 branchByDepotPath[paths] = git_branch
1090 settings = None
1091 parent = 0
1092 while parent < 65535:
1093 commit = head + "~%s" % parent
1094 log = extractLogMessageFromGitCommit(commit)
1095 settings = extractSettingsGitLog(log)
1096 if "depot-paths" in settings:
1097 paths = ",".join(settings["depot-paths"])
1098 if "change" in settings:
1099 expaths = paths + ";" + settings["change"]
1100 if expaths in branchByDepotPath:
1101 return [branchByDepotPath[expaths], settings]
1102 if paths in branchByDepotPath:
1103 return [branchByDepotPath[paths], settings]
1105 parent = parent + 1
1107 return ["", settings]
1110 def createOrUpdateBranchesFromOrigin(localRefPrefix="refs/remotes/p4/", silent=True):
1111 if not silent:
1112 print("Creating/updating branch(es) in %s based on origin branch(es)"
1113 % localRefPrefix)
1115 originPrefix = "origin/p4/"
1117 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
1118 line = line.strip()
1119 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
1120 continue
1122 headName = line[len(originPrefix):]
1123 remoteHead = localRefPrefix + headName
1124 originHead = line
1126 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
1127 if 'depot-paths' not in original or 'change' not in original:
1128 continue
1130 update = False
1131 if not gitBranchExists(remoteHead):
1132 if verbose:
1133 print("creating %s" % remoteHead)
1134 update = True
1135 else:
1136 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
1137 if 'change' in settings:
1138 if settings['depot-paths'] == original['depot-paths']:
1139 originP4Change = int(original['change'])
1140 p4Change = int(settings['change'])
1141 if originP4Change > p4Change:
1142 print("%s (%s) is newer than %s (%s). "
1143 "Updating p4 branch from origin."
1144 % (originHead, originP4Change,
1145 remoteHead, p4Change))
1146 update = True
1147 else:
1148 print("Ignoring: %s was imported from %s while "
1149 "%s was imported from %s"
1150 % (originHead, ','.join(original['depot-paths']),
1151 remoteHead, ','.join(settings['depot-paths'])))
1153 if update:
1154 system(["git", "update-ref", remoteHead, originHead])
1157 def originP4BranchesExist():
1158 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
1161 def p4ParseNumericChangeRange(parts):
1162 changeStart = int(parts[0][1:])
1163 if parts[1] == '#head':
1164 changeEnd = p4_last_change()
1165 else:
1166 changeEnd = int(parts[1])
1168 return (changeStart, changeEnd)
1171 def chooseBlockSize(blockSize):
1172 if blockSize:
1173 return blockSize
1174 else:
1175 return defaultBlockSize
1178 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
1179 assert depotPaths
1181 # Parse the change range into start and end. Try to find integer
1182 # revision ranges as these can be broken up into blocks to avoid
1183 # hitting server-side limits (maxrows, maxscanresults). But if
1184 # that doesn't work, fall back to using the raw revision specifier
1185 # strings, without using block mode.
1187 if changeRange is None or changeRange == '':
1188 changeStart = 1
1189 changeEnd = p4_last_change()
1190 block_size = chooseBlockSize(requestedBlockSize)
1191 else:
1192 parts = changeRange.split(',')
1193 assert len(parts) == 2
1194 try:
1195 changeStart, changeEnd = p4ParseNumericChangeRange(parts)
1196 block_size = chooseBlockSize(requestedBlockSize)
1197 except ValueError:
1198 changeStart = parts[0][1:]
1199 changeEnd = parts[1]
1200 if requestedBlockSize:
1201 die("cannot use --changes-block-size with non-numeric revisions")
1202 block_size = None
1204 changes = set()
1206 # Retrieve changes a block at a time, to prevent running
1207 # into a MaxResults/MaxScanRows error from the server. If
1208 # we _do_ hit one of those errors, turn down the block size
1210 while True:
1211 cmd = ['changes']
1213 if block_size:
1214 end = min(changeEnd, changeStart + block_size)
1215 revisionRange = "%d,%d" % (changeStart, end)
1216 else:
1217 revisionRange = "%s,%s" % (changeStart, changeEnd)
1219 for p in depotPaths:
1220 cmd += ["%s...@%s" % (p, revisionRange)]
1222 # fetch the changes
1223 try:
1224 result = p4CmdList(cmd, errors_as_exceptions=True)
1225 except P4RequestSizeException as e:
1226 if not block_size:
1227 block_size = e.limit
1228 elif block_size > e.limit:
1229 block_size = e.limit
1230 else:
1231 block_size = max(2, block_size // 2)
1233 if verbose:
1234 print("block size error, retrying with block size {0}".format(block_size))
1235 continue
1236 except P4Exception as e:
1237 die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1239 # Insert changes in chronological order
1240 for entry in reversed(result):
1241 if 'change' not in entry:
1242 continue
1243 changes.add(int(entry['change']))
1245 if not block_size:
1246 break
1248 if end >= changeEnd:
1249 break
1251 changeStart = end + 1
1253 changes = sorted(changes)
1254 return changes
1257 def p4PathStartsWith(path, prefix):
1258 """This method tries to remedy a potential mixed-case issue:
1260 If UserA adds //depot/DirA/file1
1261 and UserB adds //depot/dira/file2
1263 we may or may not have a problem. If you have core.ignorecase=true,
1264 we treat DirA and dira as the same directory.
1266 if gitConfigBool("core.ignorecase"):
1267 return path.lower().startswith(prefix.lower())
1268 return path.startswith(prefix)
1271 def getClientSpec():
1272 """Look at the p4 client spec, create a View() object that contains
1273 all the mappings, and return it.
1276 specList = p4CmdList(["client", "-o"])
1277 if len(specList) != 1:
1278 die('Output from "client -o" is %d lines, expecting 1' %
1279 len(specList))
1281 # dictionary of all client parameters
1282 entry = specList[0]
1284 # the //client/ name
1285 client_name = entry["Client"]
1287 # just the keys that start with "View"
1288 view_keys = [k for k in entry.keys() if k.startswith("View")]
1290 # hold this new View
1291 view = View(client_name)
1293 # append the lines, in order, to the view
1294 for view_num in range(len(view_keys)):
1295 k = "View%d" % view_num
1296 if k not in view_keys:
1297 die("Expected view key %s missing" % k)
1298 view.append(entry[k])
1300 return view
1303 def getClientRoot():
1304 """Grab the client directory."""
1306 output = p4CmdList(["client", "-o"])
1307 if len(output) != 1:
1308 die('Output from "client -o" is %d lines, expecting 1' % len(output))
1310 entry = output[0]
1311 if "Root" not in entry:
1312 die('Client has no "Root"')
1314 return entry["Root"]
1317 def wildcard_decode(path):
1318 """Decode P4 wildcards into %xx encoding
1320 P4 wildcards are not allowed in filenames. P4 complains if you simply
1321 add them, but you can force it with "-f", in which case it translates
1322 them into %xx encoding internally.
1325 # Search for and fix just these four characters. Do % last so
1326 # that fixing it does not inadvertently create new %-escapes.
1327 # Cannot have * in a filename in windows; untested as to
1328 # what p4 would do in such a case.
1329 if not platform.system() == "Windows":
1330 path = path.replace("%2A", "*")
1331 path = path.replace("%23", "#") \
1332 .replace("%40", "@") \
1333 .replace("%25", "%")
1334 return path
1337 def wildcard_encode(path):
1338 """Encode %xx coded wildcards into P4 coding."""
1340 # do % first to avoid double-encoding the %s introduced here
1341 path = path.replace("%", "%25") \
1342 .replace("*", "%2A") \
1343 .replace("#", "%23") \
1344 .replace("@", "%40")
1345 return path
1348 def wildcard_present(path):
1349 m = re.search("[*#@%]", path)
1350 return m is not None
1353 class LargeFileSystem(object):
1354 """Base class for large file system support."""
1356 def __init__(self, writeToGitStream):
1357 self.largeFiles = set()
1358 self.writeToGitStream = writeToGitStream
1360 def generatePointer(self, cloneDestination, contentFile):
1361 """Return the content of a pointer file that is stored in Git instead
1362 of the actual content.
1364 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1366 def pushFile(self, localLargeFile):
1367 """Push the actual content which is not stored in the Git repository to
1368 a server.
1370 assert False, "Method 'pushFile' required in " + self.__class__.__name__
1372 def hasLargeFileExtension(self, relPath):
1373 return functools.reduce(
1374 lambda a, b: a or b,
1375 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1376 False
1379 def generateTempFile(self, contents):
1380 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1381 for d in contents:
1382 contentFile.write(d)
1383 contentFile.close()
1384 return contentFile.name
1386 def exceedsLargeFileThreshold(self, relPath, contents):
1387 if gitConfigInt('git-p4.largeFileThreshold'):
1388 contentsSize = sum(len(d) for d in contents)
1389 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1390 return True
1391 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1392 contentsSize = sum(len(d) for d in contents)
1393 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1394 return False
1395 contentTempFile = self.generateTempFile(contents)
1396 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True)
1397 with zipfile.ZipFile(compressedContentFile, mode='w') as zf:
1398 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1399 compressedContentsSize = zf.infolist()[0].compress_size
1400 os.remove(contentTempFile)
1401 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1402 return True
1403 return False
1405 def addLargeFile(self, relPath):
1406 self.largeFiles.add(relPath)
1408 def removeLargeFile(self, relPath):
1409 self.largeFiles.remove(relPath)
1411 def isLargeFile(self, relPath):
1412 return relPath in self.largeFiles
1414 def processContent(self, git_mode, relPath, contents):
1415 """Processes the content of git fast import. This method decides if a
1416 file is stored in the large file system and handles all necessary
1417 steps.
1419 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1420 contentTempFile = self.generateTempFile(contents)
1421 pointer_git_mode, contents, localLargeFile = self.generatePointer(contentTempFile)
1422 if pointer_git_mode:
1423 git_mode = pointer_git_mode
1424 if localLargeFile:
1425 # Move temp file to final location in large file system
1426 largeFileDir = os.path.dirname(localLargeFile)
1427 if not os.path.isdir(largeFileDir):
1428 os.makedirs(largeFileDir)
1429 shutil.move(contentTempFile, localLargeFile)
1430 self.addLargeFile(relPath)
1431 if gitConfigBool('git-p4.largeFilePush'):
1432 self.pushFile(localLargeFile)
1433 if verbose:
1434 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1435 return (git_mode, contents)
1438 class MockLFS(LargeFileSystem):
1439 """Mock large file system for testing."""
1441 def generatePointer(self, contentFile):
1442 """The pointer content is the original content prefixed with "pointer-".
1443 The local filename of the large file storage is derived from the
1444 file content.
1446 with open(contentFile, 'r') as f:
1447 content = next(f)
1448 gitMode = '100644'
1449 pointerContents = 'pointer-' + content
1450 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1451 return (gitMode, pointerContents, localLargeFile)
1453 def pushFile(self, localLargeFile):
1454 """The remote filename of the large file storage is the same as the
1455 local one but in a different directory.
1457 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1458 if not os.path.exists(remotePath):
1459 os.makedirs(remotePath)
1460 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1463 class GitLFS(LargeFileSystem):
1464 """Git LFS as backend for the git-p4 large file system.
1465 See https://git-lfs.github.com/ for details.
1468 def __init__(self, *args):
1469 LargeFileSystem.__init__(self, *args)
1470 self.baseGitAttributes = []
1472 def generatePointer(self, contentFile):
1473 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1474 mode and content which is stored in the Git repository instead of
1475 the actual content. Return also the new location of the actual
1476 content.
1478 if os.path.getsize(contentFile) == 0:
1479 return (None, '', None)
1481 pointerProcess = subprocess.Popen(
1482 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1483 stdout=subprocess.PIPE
1485 pointerFile = decode_text_stream(pointerProcess.stdout.read())
1486 if pointerProcess.wait():
1487 os.remove(contentFile)
1488 die('git-lfs pointer command failed. Did you install the extension?')
1490 # Git LFS removed the preamble in the output of the 'pointer' command
1491 # starting from version 1.2.0. Check for the preamble here to support
1492 # earlier versions.
1493 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1494 if pointerFile.startswith('Git LFS pointer for'):
1495 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1497 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
1498 # if someone use external lfs.storage ( not in local repo git )
1499 lfs_path = gitConfig('lfs.storage')
1500 if not lfs_path:
1501 lfs_path = 'lfs'
1502 if not os.path.isabs(lfs_path):
1503 lfs_path = os.path.join(os.getcwd(), '.git', lfs_path)
1504 localLargeFile = os.path.join(
1505 lfs_path,
1506 'objects', oid[:2], oid[2:4],
1507 oid,
1509 # LFS Spec states that pointer files should not have the executable bit set.
1510 gitMode = '100644'
1511 return (gitMode, pointerFile, localLargeFile)
1513 def pushFile(self, localLargeFile):
1514 uploadProcess = subprocess.Popen(
1515 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1517 if uploadProcess.wait():
1518 die('git-lfs push command failed. Did you define a remote?')
1520 def generateGitAttributes(self):
1521 return (
1522 self.baseGitAttributes +
1524 '\n',
1525 '#\n',
1526 '# Git LFS (see https://git-lfs.github.com/)\n',
1527 '#\n',
1529 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1530 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1532 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1533 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1537 def addLargeFile(self, relPath):
1538 LargeFileSystem.addLargeFile(self, relPath)
1539 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1541 def removeLargeFile(self, relPath):
1542 LargeFileSystem.removeLargeFile(self, relPath)
1543 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1545 def processContent(self, git_mode, relPath, contents):
1546 if relPath == '.gitattributes':
1547 self.baseGitAttributes = contents
1548 return (git_mode, self.generateGitAttributes())
1549 else:
1550 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1553 class Command:
1554 delete_actions = ("delete", "move/delete", "purge")
1555 add_actions = ("add", "branch", "move/add")
1557 def __init__(self):
1558 self.usage = "usage: %prog [options]"
1559 self.needsGit = True
1560 self.verbose = False
1562 # This is required for the "append" update_shelve action
1563 def ensure_value(self, attr, value):
1564 if not hasattr(self, attr) or getattr(self, attr) is None:
1565 setattr(self, attr, value)
1566 return getattr(self, attr)
1569 class P4UserMap:
1570 def __init__(self):
1571 self.userMapFromPerforceServer = False
1572 self.myP4UserId = None
1574 def p4UserId(self):
1575 if self.myP4UserId:
1576 return self.myP4UserId
1578 results = p4CmdList(["user", "-o"])
1579 for r in results:
1580 if 'User' in r:
1581 self.myP4UserId = r['User']
1582 return r['User']
1583 die("Could not find your p4 user id")
1585 def p4UserIsMe(self, p4User):
1586 """Return True if the given p4 user is actually me."""
1587 me = self.p4UserId()
1588 if not p4User or p4User != me:
1589 return False
1590 else:
1591 return True
1593 def getUserCacheFilename(self):
1594 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1595 return home + "/.gitp4-usercache.txt"
1597 def getUserMapFromPerforceServer(self):
1598 if self.userMapFromPerforceServer:
1599 return
1600 self.users = {}
1601 self.emails = {}
1603 for output in p4CmdList(["users"]):
1604 if "User" not in output:
1605 continue
1606 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1607 self.emails[output["Email"]] = output["User"]
1609 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1610 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1611 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1612 if mapUser and len(mapUser[0]) == 3:
1613 user = mapUser[0][0]
1614 fullname = mapUser[0][1]
1615 email = mapUser[0][2]
1616 self.users[user] = fullname + " <" + email + ">"
1617 self.emails[email] = user
1619 s = ''
1620 for (key, val) in self.users.items():
1621 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1623 open(self.getUserCacheFilename(), 'w').write(s)
1624 self.userMapFromPerforceServer = True
1626 def loadUserMapFromCache(self):
1627 self.users = {}
1628 self.userMapFromPerforceServer = False
1629 try:
1630 cache = open(self.getUserCacheFilename(), 'r')
1631 lines = cache.readlines()
1632 cache.close()
1633 for line in lines:
1634 entry = line.strip().split("\t")
1635 self.users[entry[0]] = entry[1]
1636 except IOError:
1637 self.getUserMapFromPerforceServer()
1640 class P4Submit(Command, P4UserMap):
1642 conflict_behavior_choices = ("ask", "skip", "quit")
1644 def __init__(self):
1645 Command.__init__(self)
1646 P4UserMap.__init__(self)
1647 self.options = [
1648 optparse.make_option("--origin", dest="origin"),
1649 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1650 # preserve the user, requires relevant p4 permissions
1651 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1652 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1653 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1654 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1655 optparse.make_option("--conflict", dest="conflict_behavior",
1656 choices=self.conflict_behavior_choices),
1657 optparse.make_option("--branch", dest="branch"),
1658 optparse.make_option("--shelve", dest="shelve", action="store_true",
1659 help="Shelve instead of submit. Shelved files are reverted, "
1660 "restoring the workspace to the state before the shelve"),
1661 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
1662 metavar="CHANGELIST",
1663 help="update an existing shelved changelist, implies --shelve, "
1664 "repeat in-order for multiple shelved changelists"),
1665 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1666 help="submit only the specified commit(s), one commit or xxx..xxx"),
1667 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1668 help="Disable rebase after submit is completed. Can be useful if you "
1669 "work from a local git branch that is not master"),
1670 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1671 help="Skip Perforce sync of p4/master after submit or shelve"),
1672 optparse.make_option("--no-verify", dest="no_verify", action="store_true",
1673 help="Bypass p4-pre-submit and p4-changelist hooks"),
1675 self.description = """Submit changes from git to the perforce depot.\n
1676 The `p4-pre-submit` hook is executed if it exists and is executable. It
1677 can be bypassed with the `--no-verify` command line option. The hook takes
1678 no parameters and nothing from standard input. Exiting with a non-zero status
1679 from this script prevents `git-p4 submit` from launching.
1681 One usage scenario is to run unit tests in the hook.
1683 The `p4-prepare-changelist` hook is executed right after preparing the default
1684 changelist message and before the editor is started. It takes one parameter,
1685 the name of the file that contains the changelist text. Exiting with a non-zero
1686 status from the script will abort the process.
1688 The purpose of the hook is to edit the message file in place, and it is not
1689 supressed by the `--no-verify` option. This hook is called even if
1690 `--prepare-p4-only` is set.
1692 The `p4-changelist` hook is executed after the changelist message has been
1693 edited by the user. It can be bypassed with the `--no-verify` option. It
1694 takes a single parameter, the name of the file that holds the proposed
1695 changelist text. Exiting with a non-zero status causes the command to abort.
1697 The hook is allowed to edit the changelist file and can be used to normalize
1698 the text into some project standard format. It can also be used to refuse the
1699 Submit after inspect the message file.
1701 The `p4-post-changelist` hook is invoked after the submit has successfully
1702 occurred in P4. It takes no parameters and is meant primarily for notification
1703 and cannot affect the outcome of the git p4 submit action.
1706 self.usage += " [name of git branch to submit into perforce depot]"
1707 self.origin = ""
1708 self.detectRenames = False
1709 self.preserveUser = gitConfigBool("git-p4.preserveUser")
1710 self.dry_run = False
1711 self.shelve = False
1712 self.update_shelve = list()
1713 self.commit = ""
1714 self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1715 self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
1716 self.prepare_p4_only = False
1717 self.conflict_behavior = None
1718 self.isWindows = (platform.system() == "Windows")
1719 self.exportLabels = False
1720 self.p4HasMoveCommand = p4_has_move_command()
1721 self.branch = None
1722 self.no_verify = False
1724 if gitConfig('git-p4.largeFileSystem'):
1725 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1727 def check(self):
1728 if len(p4CmdList(["opened", "..."])) > 0:
1729 die("You have files opened with perforce! Close them before starting the sync.")
1731 def separate_jobs_from_description(self, message):
1732 """Extract and return a possible Jobs field in the commit message. It
1733 goes into a separate section in the p4 change specification.
1735 A jobs line starts with "Jobs:" and looks like a new field in a
1736 form. Values are white-space separated on the same line or on
1737 following lines that start with a tab.
1739 This does not parse and extract the full git commit message like a
1740 p4 form. It just sees the Jobs: line as a marker to pass everything
1741 from then on directly into the p4 form, but outside the description
1742 section.
1744 Return a tuple (stripped log message, jobs string).
1747 m = re.search(r'^Jobs:', message, re.MULTILINE)
1748 if m is None:
1749 return (message, None)
1751 jobtext = message[m.start():]
1752 stripped_message = message[:m.start()].rstrip()
1753 return (stripped_message, jobtext)
1755 def prepareLogMessage(self, template, message, jobs):
1756 """Edits the template returned from "p4 change -o" to insert the
1757 message in the Description field, and the jobs text in the Jobs
1758 field.
1760 result = ""
1762 inDescriptionSection = False
1764 for line in template.split("\n"):
1765 if line.startswith("#"):
1766 result += line + "\n"
1767 continue
1769 if inDescriptionSection:
1770 if line.startswith("Files:") or line.startswith("Jobs:"):
1771 inDescriptionSection = False
1772 # insert Jobs section
1773 if jobs:
1774 result += jobs + "\n"
1775 else:
1776 continue
1777 else:
1778 if line.startswith("Description:"):
1779 inDescriptionSection = True
1780 line += "\n"
1781 for messageLine in message.split("\n"):
1782 line += "\t" + messageLine + "\n"
1784 result += line + "\n"
1786 return result
1788 def patchRCSKeywords(self, file, regexp):
1789 """Attempt to zap the RCS keywords in a p4 controlled file matching the
1790 given regex.
1792 handle, outFileName = tempfile.mkstemp(dir='.')
1793 try:
1794 with os.fdopen(handle, "wb") as outFile, open(file, "rb") as inFile:
1795 for line in inFile.readlines():
1796 outFile.write(regexp.sub(br'$\1$', line))
1797 # Forcibly overwrite the original file
1798 os.unlink(file)
1799 shutil.move(outFileName, file)
1800 except:
1801 # cleanup our temporary file
1802 os.unlink(outFileName)
1803 print("Failed to strip RCS keywords in %s" % file)
1804 raise
1806 print("Patched up RCS keywords in %s" % file)
1808 def p4UserForCommit(self, id):
1809 """Return the tuple (perforce user,git email) for a given git commit
1812 self.getUserMapFromPerforceServer()
1813 gitEmail = read_pipe(["git", "log", "--max-count=1",
1814 "--format=%ae", id])
1815 gitEmail = gitEmail.strip()
1816 if gitEmail not in self.emails:
1817 return (None, gitEmail)
1818 else:
1819 return (self.emails[gitEmail], gitEmail)
1821 def checkValidP4Users(self, commits):
1822 """Check if any git authors cannot be mapped to p4 users."""
1823 for id in commits:
1824 user, email = self.p4UserForCommit(id)
1825 if not user:
1826 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1827 if gitConfigBool("git-p4.allowMissingP4Users"):
1828 print("%s" % msg)
1829 else:
1830 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1832 def lastP4Changelist(self):
1833 """Get back the last changelist number submitted in this client spec.
1835 This then gets used to patch up the username in the change. If the
1836 same client spec is being used by multiple processes then this might
1837 go wrong.
1839 results = p4CmdList(["client", "-o"]) # find the current client
1840 client = None
1841 for r in results:
1842 if 'Client' in r:
1843 client = r['Client']
1844 break
1845 if not client:
1846 die("could not get client spec")
1847 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1848 for r in results:
1849 if 'change' in r:
1850 return r['change']
1851 die("Could not get changelist number for last submit - cannot patch up user details")
1853 def modifyChangelistUser(self, changelist, newUser):
1854 """Fixup the user field of a changelist after it has been submitted."""
1855 changes = p4CmdList(["change", "-o", changelist])
1856 if len(changes) != 1:
1857 die("Bad output from p4 change modifying %s to user %s" %
1858 (changelist, newUser))
1860 c = changes[0]
1861 if c['User'] == newUser:
1862 # Nothing to do
1863 return
1864 c['User'] = newUser
1865 # p4 does not understand format version 3 and above
1866 input = marshal.dumps(c, 2)
1868 result = p4CmdList(["change", "-f", "-i"], stdin=input)
1869 for r in result:
1870 if 'code' in r:
1871 if r['code'] == 'error':
1872 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1873 if 'data' in r:
1874 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1875 return
1876 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1878 def canChangeChangelists(self):
1879 """Check to see if we have p4 admin or super-user permissions, either
1880 of which are required to modify changelists.
1882 results = p4CmdList(["protects", self.depotPath])
1883 for r in results:
1884 if 'perm' in r:
1885 if r['perm'] == 'admin':
1886 return 1
1887 if r['perm'] == 'super':
1888 return 1
1889 return 0
1891 def prepareSubmitTemplate(self, changelist=None):
1892 """Run "p4 change -o" to grab a change specification template.
1894 This does not use "p4 -G", as it is nice to keep the submission
1895 template in original order, since a human might edit it.
1897 Remove lines in the Files section that show changes to files
1898 outside the depot path we're committing into.
1901 upstream, settings = findUpstreamBranchPoint()
1903 template = """\
1904 # A Perforce Change Specification.
1906 # Change: The change number. 'new' on a new changelist.
1907 # Date: The date this specification was last modified.
1908 # Client: The client on which the changelist was created. Read-only.
1909 # User: The user who created the changelist.
1910 # Status: Either 'pending' or 'submitted'. Read-only.
1911 # Type: Either 'public' or 'restricted'. Default is 'public'.
1912 # Description: Comments about the changelist. Required.
1913 # Jobs: What opened jobs are to be closed by this changelist.
1914 # You may delete jobs from this list. (New changelists only.)
1915 # Files: What opened files from the default changelist are to be added
1916 # to this changelist. You may delete files from this list.
1917 # (New changelists only.)
1919 files_list = []
1920 inFilesSection = False
1921 change_entry = None
1922 args = ['change', '-o']
1923 if changelist:
1924 args.append(str(changelist))
1925 for entry in p4CmdList(args):
1926 if 'code' not in entry:
1927 continue
1928 if entry['code'] == 'stat':
1929 change_entry = entry
1930 break
1931 if not change_entry:
1932 die('Failed to decode output of p4 change -o')
1933 for key, value in change_entry.items():
1934 if key.startswith('File'):
1935 if 'depot-paths' in settings:
1936 if not [p for p in settings['depot-paths']
1937 if p4PathStartsWith(value, p)]:
1938 continue
1939 else:
1940 if not p4PathStartsWith(value, self.depotPath):
1941 continue
1942 files_list.append(value)
1943 continue
1944 # Output in the order expected by prepareLogMessage
1945 for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
1946 if key not in change_entry:
1947 continue
1948 template += '\n'
1949 template += key + ':'
1950 if key == 'Description':
1951 template += '\n'
1952 for field_line in change_entry[key].splitlines():
1953 template += '\t'+field_line+'\n'
1954 if len(files_list) > 0:
1955 template += '\n'
1956 template += 'Files:\n'
1957 for path in files_list:
1958 template += '\t'+path+'\n'
1959 return template
1961 def edit_template(self, template_file):
1962 """Invoke the editor to let the user change the submission message.
1964 Return true if okay to continue with the submit.
1967 # if configured to skip the editing part, just submit
1968 if gitConfigBool("git-p4.skipSubmitEdit"):
1969 return True
1971 # look at the modification time, to check later if the user saved
1972 # the file
1973 mtime = os.stat(template_file).st_mtime
1975 # invoke the editor
1976 if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
1977 editor = os.environ.get("P4EDITOR")
1978 else:
1979 editor = read_pipe(["git", "var", "GIT_EDITOR"]).strip()
1980 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1982 # If the file was not saved, prompt to see if this patch should
1983 # be skipped. But skip this verification step if configured so.
1984 if gitConfigBool("git-p4.skipSubmitEditCheck"):
1985 return True
1987 # modification time updated means user saved the file
1988 if os.stat(template_file).st_mtime > mtime:
1989 return True
1991 response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1992 if response == 'y':
1993 return True
1994 if response == 'n':
1995 return False
1997 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
1998 # diff
1999 if "P4DIFF" in os.environ:
2000 del(os.environ["P4DIFF"])
2001 diff = ""
2002 for editedFile in editedFiles:
2003 diff += p4_read_pipe(['diff', '-du',
2004 wildcard_encode(editedFile)])
2006 # new file diff
2007 newdiff = ""
2008 for newFile in filesToAdd:
2009 newdiff += "==== new file ====\n"
2010 newdiff += "--- /dev/null\n"
2011 newdiff += "+++ %s\n" % newFile
2013 is_link = os.path.islink(newFile)
2014 expect_link = newFile in symlinks
2016 if is_link and expect_link:
2017 newdiff += "+%s\n" % os.readlink(newFile)
2018 else:
2019 f = open(newFile, "r")
2020 try:
2021 for line in f.readlines():
2022 newdiff += "+" + line
2023 except UnicodeDecodeError:
2024 # Found non-text data and skip, since diff description
2025 # should only include text
2026 pass
2027 f.close()
2029 return (diff + newdiff).replace('\r\n', '\n')
2031 def applyCommit(self, id):
2032 """Apply one commit, return True if it succeeded."""
2034 print("Applying", read_pipe(["git", "show", "-s",
2035 "--format=format:%h %s", id]))
2037 p4User, gitEmail = self.p4UserForCommit(id)
2039 diff = read_pipe_lines(
2040 ["git", "diff-tree", "-r"] + self.diffOpts + ["{}^".format(id), id])
2041 filesToAdd = set()
2042 filesToChangeType = set()
2043 filesToDelete = set()
2044 editedFiles = set()
2045 pureRenameCopy = set()
2046 symlinks = set()
2047 filesToChangeExecBit = {}
2048 all_files = list()
2050 for line in diff:
2051 diff = parseDiffTreeEntry(line)
2052 modifier = diff['status']
2053 path = diff['src']
2054 all_files.append(path)
2056 if modifier == "M":
2057 p4_edit(path)
2058 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2059 filesToChangeExecBit[path] = diff['dst_mode']
2060 editedFiles.add(path)
2061 elif modifier == "A":
2062 filesToAdd.add(path)
2063 filesToChangeExecBit[path] = diff['dst_mode']
2064 if path in filesToDelete:
2065 filesToDelete.remove(path)
2067 dst_mode = int(diff['dst_mode'], 8)
2068 if dst_mode == 0o120000:
2069 symlinks.add(path)
2071 elif modifier == "D":
2072 filesToDelete.add(path)
2073 if path in filesToAdd:
2074 filesToAdd.remove(path)
2075 elif modifier == "C":
2076 src, dest = diff['src'], diff['dst']
2077 all_files.append(dest)
2078 p4_integrate(src, dest)
2079 pureRenameCopy.add(dest)
2080 if diff['src_sha1'] != diff['dst_sha1']:
2081 p4_edit(dest)
2082 pureRenameCopy.discard(dest)
2083 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2084 p4_edit(dest)
2085 pureRenameCopy.discard(dest)
2086 filesToChangeExecBit[dest] = diff['dst_mode']
2087 if self.isWindows:
2088 # turn off read-only attribute
2089 os.chmod(dest, stat.S_IWRITE)
2090 os.unlink(dest)
2091 editedFiles.add(dest)
2092 elif modifier == "R":
2093 src, dest = diff['src'], diff['dst']
2094 all_files.append(dest)
2095 if self.p4HasMoveCommand:
2096 p4_edit(src) # src must be open before move
2097 p4_move(src, dest) # opens for (move/delete, move/add)
2098 else:
2099 p4_integrate(src, dest)
2100 if diff['src_sha1'] != diff['dst_sha1']:
2101 p4_edit(dest)
2102 else:
2103 pureRenameCopy.add(dest)
2104 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2105 if not self.p4HasMoveCommand:
2106 p4_edit(dest) # with move: already open, writable
2107 filesToChangeExecBit[dest] = diff['dst_mode']
2108 if not self.p4HasMoveCommand:
2109 if self.isWindows:
2110 os.chmod(dest, stat.S_IWRITE)
2111 os.unlink(dest)
2112 filesToDelete.add(src)
2113 editedFiles.add(dest)
2114 elif modifier == "T":
2115 filesToChangeType.add(path)
2116 else:
2117 die("unknown modifier %s for %s" % (modifier, path))
2119 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
2120 patchcmd = diffcmd + " | git apply "
2121 tryPatchCmd = patchcmd + "--check -"
2122 applyPatchCmd = patchcmd + "--check --apply -"
2123 patch_succeeded = True
2125 if verbose:
2126 print("TryPatch: %s" % tryPatchCmd)
2128 if os.system(tryPatchCmd) != 0:
2129 fixed_rcs_keywords = False
2130 patch_succeeded = False
2131 print("Unfortunately applying the change failed!")
2133 # Patch failed, maybe it's just RCS keyword woes. Look through
2134 # the patch to see if that's possible.
2135 if gitConfigBool("git-p4.attemptRCSCleanup"):
2136 file = None
2137 kwfiles = {}
2138 for file in editedFiles | filesToDelete:
2139 # did this file's delta contain RCS keywords?
2140 regexp = p4_keywords_regexp_for_file(file)
2141 if regexp:
2142 # this file is a possibility...look for RCS keywords.
2143 for line in read_pipe_lines(
2144 ["git", "diff", "%s^..%s" % (id, id), file],
2145 raw=True):
2146 if regexp.search(line):
2147 if verbose:
2148 print("got keyword match on %s in %s in %s" % (regex.pattern, line, file))
2149 kwfiles[file] = regexp
2150 break
2152 for file, regexp in kwfiles.items():
2153 if verbose:
2154 print("zapping %s with %s" % (line, regexp.pattern))
2155 # File is being deleted, so not open in p4. Must
2156 # disable the read-only bit on windows.
2157 if self.isWindows and file not in editedFiles:
2158 os.chmod(file, stat.S_IWRITE)
2159 self.patchRCSKeywords(file, kwfiles[file])
2160 fixed_rcs_keywords = True
2162 if fixed_rcs_keywords:
2163 print("Retrying the patch with RCS keywords cleaned up")
2164 if os.system(tryPatchCmd) == 0:
2165 patch_succeeded = True
2166 print("Patch succeesed this time with RCS keywords cleaned")
2168 if not patch_succeeded:
2169 for f in editedFiles:
2170 p4_revert(f)
2171 return False
2174 # Apply the patch for real, and do add/delete/+x handling.
2176 system(applyPatchCmd, shell=True)
2178 for f in filesToChangeType:
2179 p4_edit(f, "-t", "auto")
2180 for f in filesToAdd:
2181 p4_add(f)
2182 for f in filesToDelete:
2183 p4_revert(f)
2184 p4_delete(f)
2186 # Set/clear executable bits
2187 for f in filesToChangeExecBit.keys():
2188 mode = filesToChangeExecBit[f]
2189 setP4ExecBit(f, mode)
2191 update_shelve = 0
2192 if len(self.update_shelve) > 0:
2193 update_shelve = self.update_shelve.pop(0)
2194 p4_reopen_in_change(update_shelve, all_files)
2197 # Build p4 change description, starting with the contents
2198 # of the git commit message.
2200 logMessage = extractLogMessageFromGitCommit(id)
2201 logMessage = logMessage.strip()
2202 logMessage, jobs = self.separate_jobs_from_description(logMessage)
2204 template = self.prepareSubmitTemplate(update_shelve)
2205 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
2207 if self.preserveUser:
2208 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
2210 if self.checkAuthorship and not self.p4UserIsMe(p4User):
2211 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2212 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2213 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2215 separatorLine = "######## everything below this line is just the diff #######\n"
2216 if not self.prepare_p4_only:
2217 submitTemplate += separatorLine
2218 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
2220 handle, fileName = tempfile.mkstemp()
2221 tmpFile = os.fdopen(handle, "w+b")
2222 if self.isWindows:
2223 submitTemplate = submitTemplate.replace("\n", "\r\n")
2224 tmpFile.write(encode_text_stream(submitTemplate))
2225 tmpFile.close()
2227 submitted = False
2229 try:
2230 # Allow the hook to edit the changelist text before presenting it
2231 # to the user.
2232 if not run_git_hook("p4-prepare-changelist", [fileName]):
2233 return False
2235 if self.prepare_p4_only:
2237 # Leave the p4 tree prepared, and the submit template around
2238 # and let the user decide what to do next
2240 submitted = True
2241 print("")
2242 print("P4 workspace prepared for submission.")
2243 print("To submit or revert, go to client workspace")
2244 print(" " + self.clientPath)
2245 print("")
2246 print("To submit, use \"p4 submit\" to write a new description,")
2247 print("or \"p4 submit -i <%s\" to use the one prepared by"
2248 " \"git p4\"." % fileName)
2249 print("You can delete the file \"%s\" when finished." % fileName)
2251 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
2252 print("To preserve change ownership by user %s, you must\n"
2253 "do \"p4 change -f <change>\" after submitting and\n"
2254 "edit the User field.")
2255 if pureRenameCopy:
2256 print("After submitting, renamed files must be re-synced.")
2257 print("Invoke \"p4 sync -f\" on each of these files:")
2258 for f in pureRenameCopy:
2259 print(" " + f)
2261 print("")
2262 print("To revert the changes, use \"p4 revert ...\", and delete")
2263 print("the submit template file \"%s\"" % fileName)
2264 if filesToAdd:
2265 print("Since the commit adds new files, they must be deleted:")
2266 for f in filesToAdd:
2267 print(" " + f)
2268 print("")
2269 sys.stdout.flush()
2270 return True
2272 if self.edit_template(fileName):
2273 if not self.no_verify:
2274 if not run_git_hook("p4-changelist", [fileName]):
2275 print("The p4-changelist hook failed.")
2276 sys.stdout.flush()
2277 return False
2279 # read the edited message and submit
2280 tmpFile = open(fileName, "rb")
2281 message = decode_text_stream(tmpFile.read())
2282 tmpFile.close()
2283 if self.isWindows:
2284 message = message.replace("\r\n", "\n")
2285 if message.find(separatorLine) != -1:
2286 submitTemplate = message[:message.index(separatorLine)]
2287 else:
2288 submitTemplate = message
2290 if len(submitTemplate.strip()) == 0:
2291 print("Changelist is empty, aborting this changelist.")
2292 sys.stdout.flush()
2293 return False
2295 if update_shelve:
2296 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2297 elif self.shelve:
2298 p4_write_pipe(['shelve', '-i'], submitTemplate)
2299 else:
2300 p4_write_pipe(['submit', '-i'], submitTemplate)
2301 # The rename/copy happened by applying a patch that created a
2302 # new file. This leaves it writable, which confuses p4.
2303 for f in pureRenameCopy:
2304 p4_sync(f, "-f")
2306 if self.preserveUser:
2307 if p4User:
2308 # Get last changelist number. Cannot easily get it from
2309 # the submit command output as the output is
2310 # unmarshalled.
2311 changelist = self.lastP4Changelist()
2312 self.modifyChangelistUser(changelist, p4User)
2314 submitted = True
2316 run_git_hook("p4-post-changelist")
2317 finally:
2318 # Revert changes if we skip this patch
2319 if not submitted or self.shelve:
2320 if self.shelve:
2321 print("Reverting shelved files.")
2322 else:
2323 print("Submission cancelled, undoing p4 changes.")
2324 sys.stdout.flush()
2325 for f in editedFiles | filesToDelete:
2326 p4_revert(f)
2327 for f in filesToAdd:
2328 p4_revert(f)
2329 os.remove(f)
2331 if not self.prepare_p4_only:
2332 os.remove(fileName)
2333 return submitted
2335 def exportGitTags(self, gitTags):
2336 """Export git tags as p4 labels. Create a p4 label and then tag with
2337 that.
2340 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2341 if len(validLabelRegexp) == 0:
2342 validLabelRegexp = defaultLabelRegexp
2343 m = re.compile(validLabelRegexp)
2345 for name in gitTags:
2347 if not m.match(name):
2348 if verbose:
2349 print("tag %s does not match regexp %s" % (name, validLabelRegexp))
2350 continue
2352 # Get the p4 commit this corresponds to
2353 logMessage = extractLogMessageFromGitCommit(name)
2354 values = extractSettingsGitLog(logMessage)
2356 if 'change' not in values:
2357 # a tag pointing to something not sent to p4; ignore
2358 if verbose:
2359 print("git tag %s does not give a p4 commit" % name)
2360 continue
2361 else:
2362 changelist = values['change']
2364 # Get the tag details.
2365 inHeader = True
2366 isAnnotated = False
2367 body = []
2368 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2369 l = l.strip()
2370 if inHeader:
2371 if re.match(r'tag\s+', l):
2372 isAnnotated = True
2373 elif re.match(r'\s*$', l):
2374 inHeader = False
2375 continue
2376 else:
2377 body.append(l)
2379 if not isAnnotated:
2380 body = ["lightweight tag imported by git p4\n"]
2382 # Create the label - use the same view as the client spec we are using
2383 clientSpec = getClientSpec()
2385 labelTemplate = "Label: %s\n" % name
2386 labelTemplate += "Description:\n"
2387 for b in body:
2388 labelTemplate += "\t" + b + "\n"
2389 labelTemplate += "View:\n"
2390 for depot_side in clientSpec.mappings:
2391 labelTemplate += "\t%s\n" % depot_side
2393 if self.dry_run:
2394 print("Would create p4 label %s for tag" % name)
2395 elif self.prepare_p4_only:
2396 print("Not creating p4 label %s for tag due to option"
2397 " --prepare-p4-only" % name)
2398 else:
2399 p4_write_pipe(["label", "-i"], labelTemplate)
2401 # Use the label
2402 p4_system(["tag", "-l", name] +
2403 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2405 if verbose:
2406 print("created p4 label for tag %s" % name)
2408 def run(self, args):
2409 if len(args) == 0:
2410 self.master = currentGitBranch()
2411 elif len(args) == 1:
2412 self.master = args[0]
2413 if not branchExists(self.master):
2414 die("Branch %s does not exist" % self.master)
2415 else:
2416 return False
2418 for i in self.update_shelve:
2419 if i <= 0:
2420 sys.exit("invalid changelist %d" % i)
2422 if self.master:
2423 allowSubmit = gitConfig("git-p4.allowSubmit")
2424 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2425 die("%s is not in git-p4.allowSubmit" % self.master)
2427 upstream, settings = findUpstreamBranchPoint()
2428 self.depotPath = settings['depot-paths'][0]
2429 if len(self.origin) == 0:
2430 self.origin = upstream
2432 if len(self.update_shelve) > 0:
2433 self.shelve = True
2435 if self.preserveUser:
2436 if not self.canChangeChangelists():
2437 die("Cannot preserve user names without p4 super-user or admin permissions")
2439 # if not set from the command line, try the config file
2440 if self.conflict_behavior is None:
2441 val = gitConfig("git-p4.conflict")
2442 if val:
2443 if val not in self.conflict_behavior_choices:
2444 die("Invalid value '%s' for config git-p4.conflict" % val)
2445 else:
2446 val = "ask"
2447 self.conflict_behavior = val
2449 if self.verbose:
2450 print("Origin branch is " + self.origin)
2452 if len(self.depotPath) == 0:
2453 print("Internal error: cannot locate perforce depot path from existing branches")
2454 sys.exit(128)
2456 self.useClientSpec = False
2457 if gitConfigBool("git-p4.useclientspec"):
2458 self.useClientSpec = True
2459 if self.useClientSpec:
2460 self.clientSpecDirs = getClientSpec()
2462 # Check for the existence of P4 branches
2463 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2465 if self.useClientSpec and not branchesDetected:
2466 # all files are relative to the client spec
2467 self.clientPath = getClientRoot()
2468 else:
2469 self.clientPath = p4Where(self.depotPath)
2471 if self.clientPath == "":
2472 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2474 print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
2475 self.oldWorkingDirectory = os.getcwd()
2477 # ensure the clientPath exists
2478 new_client_dir = False
2479 if not os.path.exists(self.clientPath):
2480 new_client_dir = True
2481 os.makedirs(self.clientPath)
2483 chdir(self.clientPath, is_client_path=True)
2484 if self.dry_run:
2485 print("Would synchronize p4 checkout in %s" % self.clientPath)
2486 else:
2487 print("Synchronizing p4 checkout...")
2488 if new_client_dir:
2489 # old one was destroyed, and maybe nobody told p4
2490 p4_sync("...", "-f")
2491 else:
2492 p4_sync("...")
2493 self.check()
2495 commits = []
2496 if self.master:
2497 committish = self.master
2498 else:
2499 committish = 'HEAD'
2501 if self.commit != "":
2502 if self.commit.find("..") != -1:
2503 limits_ish = self.commit.split("..")
2504 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2505 commits.append(line.strip())
2506 commits.reverse()
2507 else:
2508 commits.append(self.commit)
2509 else:
2510 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2511 commits.append(line.strip())
2512 commits.reverse()
2514 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2515 self.checkAuthorship = False
2516 else:
2517 self.checkAuthorship = True
2519 if self.preserveUser:
2520 self.checkValidP4Users(commits)
2523 # Build up a set of options to be passed to diff when
2524 # submitting each commit to p4.
2526 if self.detectRenames:
2527 # command-line -M arg
2528 self.diffOpts = ["-M"]
2529 else:
2530 # If not explicitly set check the config variable
2531 detectRenames = gitConfig("git-p4.detectRenames")
2533 if detectRenames.lower() == "false" or detectRenames == "":
2534 self.diffOpts = []
2535 elif detectRenames.lower() == "true":
2536 self.diffOpts = ["-M"]
2537 else:
2538 self.diffOpts = ["-M{}".format(detectRenames)]
2540 # no command-line arg for -C or --find-copies-harder, just
2541 # config variables
2542 detectCopies = gitConfig("git-p4.detectCopies")
2543 if detectCopies.lower() == "false" or detectCopies == "":
2544 pass
2545 elif detectCopies.lower() == "true":
2546 self.diffOpts.append("-C")
2547 else:
2548 self.diffOpts.append("-C{}".format(detectCopies))
2550 if gitConfigBool("git-p4.detectCopiesHarder"):
2551 self.diffOpts.append("--find-copies-harder")
2553 num_shelves = len(self.update_shelve)
2554 if num_shelves > 0 and num_shelves != len(commits):
2555 sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2556 (len(commits), num_shelves))
2558 if not self.no_verify:
2559 try:
2560 if not run_git_hook("p4-pre-submit"):
2561 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip "
2562 "this pre-submission check by adding\nthe command line option '--no-verify', "
2563 "however,\nthis will also skip the p4-changelist hook as well.")
2564 sys.exit(1)
2565 except Exception as e:
2566 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "
2567 "with the error '{0}'".format(e.message))
2568 sys.exit(1)
2571 # Apply the commits, one at a time. On failure, ask if should
2572 # continue to try the rest of the patches, or quit.
2574 if self.dry_run:
2575 print("Would apply")
2576 applied = []
2577 last = len(commits) - 1
2578 for i, commit in enumerate(commits):
2579 if self.dry_run:
2580 print(" ", read_pipe(["git", "show", "-s",
2581 "--format=format:%h %s", commit]))
2582 ok = True
2583 else:
2584 ok = self.applyCommit(commit)
2585 if ok:
2586 applied.append(commit)
2587 if self.prepare_p4_only:
2588 if i < last:
2589 print("Processing only the first commit due to option"
2590 " --prepare-p4-only")
2591 break
2592 else:
2593 if i < last:
2594 # prompt for what to do, or use the option/variable
2595 if self.conflict_behavior == "ask":
2596 print("What do you want to do?")
2597 response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2598 elif self.conflict_behavior == "skip":
2599 response = "s"
2600 elif self.conflict_behavior == "quit":
2601 response = "q"
2602 else:
2603 die("Unknown conflict_behavior '%s'" %
2604 self.conflict_behavior)
2606 if response == "s":
2607 print("Skipping this commit, but applying the rest")
2608 if response == "q":
2609 print("Quitting")
2610 break
2612 chdir(self.oldWorkingDirectory)
2613 shelved_applied = "shelved" if self.shelve else "applied"
2614 if self.dry_run:
2615 pass
2616 elif self.prepare_p4_only:
2617 pass
2618 elif len(commits) == len(applied):
2619 print("All commits {0}!".format(shelved_applied))
2621 sync = P4Sync()
2622 if self.branch:
2623 sync.branch = self.branch
2624 if self.disable_p4sync:
2625 sync.sync_origin_only()
2626 else:
2627 sync.run([])
2629 if not self.disable_rebase:
2630 rebase = P4Rebase()
2631 rebase.rebase()
2633 else:
2634 if len(applied) == 0:
2635 print("No commits {0}.".format(shelved_applied))
2636 else:
2637 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2638 for c in commits:
2639 if c in applied:
2640 star = "*"
2641 else:
2642 star = " "
2643 print(star, read_pipe(["git", "show", "-s",
2644 "--format=format:%h %s", c]))
2645 print("You will have to do 'git p4 sync' and rebase.")
2647 if gitConfigBool("git-p4.exportLabels"):
2648 self.exportLabels = True
2650 if self.exportLabels:
2651 p4Labels = getP4Labels(self.depotPath)
2652 gitTags = getGitTags()
2654 missingGitTags = gitTags - p4Labels
2655 self.exportGitTags(missingGitTags)
2657 # exit with error unless everything applied perfectly
2658 if len(commits) != len(applied):
2659 sys.exit(1)
2661 return True
2664 class View(object):
2665 """Represent a p4 view ("p4 help views"), and map files in a repo according
2666 to the view.
2669 def __init__(self, client_name):
2670 self.mappings = []
2671 self.client_prefix = "//%s/" % client_name
2672 # cache results of "p4 where" to lookup client file locations
2673 self.client_spec_path_cache = {}
2675 def append(self, view_line):
2676 """Parse a view line, splitting it into depot and client sides. Append
2677 to self.mappings, preserving order. This is only needed for tag
2678 creation.
2681 # Split the view line into exactly two words. P4 enforces
2682 # structure on these lines that simplifies this quite a bit.
2684 # Either or both words may be double-quoted.
2685 # Single quotes do not matter.
2686 # Double-quote marks cannot occur inside the words.
2687 # A + or - prefix is also inside the quotes.
2688 # There are no quotes unless they contain a space.
2689 # The line is already white-space stripped.
2690 # The two words are separated by a single space.
2692 if view_line[0] == '"':
2693 # First word is double quoted. Find its end.
2694 close_quote_index = view_line.find('"', 1)
2695 if close_quote_index <= 0:
2696 die("No first-word closing quote found: %s" % view_line)
2697 depot_side = view_line[1:close_quote_index]
2698 # skip closing quote and space
2699 rhs_index = close_quote_index + 1 + 1
2700 else:
2701 space_index = view_line.find(" ")
2702 if space_index <= 0:
2703 die("No word-splitting space found: %s" % view_line)
2704 depot_side = view_line[0:space_index]
2705 rhs_index = space_index + 1
2707 # prefix + means overlay on previous mapping
2708 if depot_side.startswith("+"):
2709 depot_side = depot_side[1:]
2711 # prefix - means exclude this path, leave out of mappings
2712 exclude = False
2713 if depot_side.startswith("-"):
2714 exclude = True
2715 depot_side = depot_side[1:]
2717 if not exclude:
2718 self.mappings.append(depot_side)
2720 def convert_client_path(self, clientFile):
2721 # chop off //client/ part to make it relative
2722 if not decode_path(clientFile).startswith(self.client_prefix):
2723 die("No prefix '%s' on clientFile '%s'" %
2724 (self.client_prefix, clientFile))
2725 return clientFile[len(self.client_prefix):]
2727 def update_client_spec_path_cache(self, files):
2728 """Caching file paths by "p4 where" batch query."""
2730 # List depot file paths exclude that already cached
2731 fileArgs = [f['path'] for f in files if decode_path(f['path']) not in self.client_spec_path_cache]
2733 if len(fileArgs) == 0:
2734 return # All files in cache
2736 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2737 for res in where_result:
2738 if "code" in res and res["code"] == "error":
2739 # assume error is "... file(s) not in client view"
2740 continue
2741 if "clientFile" not in res:
2742 die("No clientFile in 'p4 where' output")
2743 if "unmap" in res:
2744 # it will list all of them, but only one not unmap-ped
2745 continue
2746 depot_path = decode_path(res['depotFile'])
2747 if gitConfigBool("core.ignorecase"):
2748 depot_path = depot_path.lower()
2749 self.client_spec_path_cache[depot_path] = self.convert_client_path(res["clientFile"])
2751 # not found files or unmap files set to ""
2752 for depotFile in fileArgs:
2753 depotFile = decode_path(depotFile)
2754 if gitConfigBool("core.ignorecase"):
2755 depotFile = depotFile.lower()
2756 if depotFile not in self.client_spec_path_cache:
2757 self.client_spec_path_cache[depotFile] = b''
2759 def map_in_client(self, depot_path):
2760 """Return the relative location in the client where this depot file
2761 should live.
2763 Returns "" if the file should not be mapped in the client.
2766 if gitConfigBool("core.ignorecase"):
2767 depot_path = depot_path.lower()
2769 if depot_path in self.client_spec_path_cache:
2770 return self.client_spec_path_cache[depot_path]
2772 die("Error: %s is not found in client spec path" % depot_path)
2773 return ""
2776 def cloneExcludeCallback(option, opt_str, value, parser):
2777 # prepend "/" because the first "/" was consumed as part of the option itself.
2778 # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2779 parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2782 class P4Sync(Command, P4UserMap):
2784 def __init__(self):
2785 Command.__init__(self)
2786 P4UserMap.__init__(self)
2787 self.options = [
2788 optparse.make_option("--branch", dest="branch"),
2789 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2790 optparse.make_option("--changesfile", dest="changesFile"),
2791 optparse.make_option("--silent", dest="silent", action="store_true"),
2792 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2793 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2794 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2795 help="Import into refs/heads/ , not refs/remotes"),
2796 optparse.make_option("--max-changes", dest="maxChanges",
2797 help="Maximum number of changes to import"),
2798 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2799 help="Internal block size to use when iteratively calling p4 changes"),
2800 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2801 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2802 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2803 help="Only sync files that are included in the Perforce Client Spec"),
2804 optparse.make_option("-/", dest="cloneExclude",
2805 action="callback", callback=cloneExcludeCallback, type="string",
2806 help="exclude depot path"),
2808 self.description = """Imports from Perforce into a git repository.\n
2809 example:
2810 //depot/my/project/ -- to import the current head
2811 //depot/my/project/@all -- to import everything
2812 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2814 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2816 self.usage += " //depot/path[@revRange]"
2817 self.silent = False
2818 self.createdBranches = set()
2819 self.committedChanges = set()
2820 self.branch = ""
2821 self.detectBranches = False
2822 self.detectLabels = False
2823 self.importLabels = False
2824 self.changesFile = ""
2825 self.syncWithOrigin = True
2826 self.importIntoRemotes = True
2827 self.maxChanges = ""
2828 self.changes_block_size = None
2829 self.keepRepoPath = False
2830 self.depotPaths = None
2831 self.p4BranchesInGit = []
2832 self.cloneExclude = []
2833 self.useClientSpec = False
2834 self.useClientSpec_from_options = False
2835 self.clientSpecDirs = None
2836 self.tempBranches = []
2837 self.tempBranchLocation = "refs/git-p4-tmp"
2838 self.largeFileSystem = None
2839 self.suppress_meta_comment = False
2841 if gitConfig('git-p4.largeFileSystem'):
2842 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2843 self.largeFileSystem = largeFileSystemConstructor(
2844 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2847 if gitConfig("git-p4.syncFromOrigin") == "false":
2848 self.syncWithOrigin = False
2850 self.depotPaths = []
2851 self.changeRange = ""
2852 self.previousDepotPaths = []
2853 self.hasOrigin = False
2855 # map from branch depot path to parent branch
2856 self.knownBranches = {}
2857 self.initialParents = {}
2859 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2860 self.labels = {}
2862 def checkpoint(self):
2863 """Force a checkpoint in fast-import and wait for it to finish."""
2864 self.gitStream.write("checkpoint\n\n")
2865 self.gitStream.write("progress checkpoint\n\n")
2866 self.gitStream.flush()
2867 out = self.gitOutput.readline()
2868 if self.verbose:
2869 print("checkpoint finished: " + out)
2871 def isPathWanted(self, path):
2872 for p in self.cloneExclude:
2873 if p.endswith("/"):
2874 if p4PathStartsWith(path, p):
2875 return False
2876 # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2877 elif path.lower() == p.lower():
2878 return False
2879 for p in self.depotPaths:
2880 if p4PathStartsWith(path, decode_path(p)):
2881 return True
2882 return False
2884 def extractFilesFromCommit(self, commit, shelved=False, shelved_cl=0):
2885 files = []
2886 fnum = 0
2887 while "depotFile%s" % fnum in commit:
2888 path = commit["depotFile%s" % fnum]
2889 found = self.isPathWanted(decode_path(path))
2890 if not found:
2891 fnum = fnum + 1
2892 continue
2894 file = {}
2895 file["path"] = path
2896 file["rev"] = commit["rev%s" % fnum]
2897 file["action"] = commit["action%s" % fnum]
2898 file["type"] = commit["type%s" % fnum]
2899 if shelved:
2900 file["shelved_cl"] = int(shelved_cl)
2901 files.append(file)
2902 fnum = fnum + 1
2903 return files
2905 def extractJobsFromCommit(self, commit):
2906 jobs = []
2907 jnum = 0
2908 while "job%s" % jnum in commit:
2909 job = commit["job%s" % jnum]
2910 jobs.append(job)
2911 jnum = jnum + 1
2912 return jobs
2914 def stripRepoPath(self, path, prefixes):
2915 """When streaming files, this is called to map a p4 depot path to where
2916 it should go in git. The prefixes are either self.depotPaths, or
2917 self.branchPrefixes in the case of branch detection.
2920 if self.useClientSpec:
2921 # branch detection moves files up a level (the branch name)
2922 # from what client spec interpretation gives
2923 path = decode_path(self.clientSpecDirs.map_in_client(path))
2924 if self.detectBranches:
2925 for b in self.knownBranches:
2926 if p4PathStartsWith(path, b + "/"):
2927 path = path[len(b)+1:]
2929 elif self.keepRepoPath:
2930 # Preserve everything in relative path name except leading
2931 # //depot/; just look at first prefix as they all should
2932 # be in the same depot.
2933 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2934 if p4PathStartsWith(path, depot):
2935 path = path[len(depot):]
2937 else:
2938 for p in prefixes:
2939 if p4PathStartsWith(path, p):
2940 path = path[len(p):]
2941 break
2943 path = wildcard_decode(path)
2944 return path
2946 def splitFilesIntoBranches(self, commit):
2947 """Look at each depotFile in the commit to figure out to what branch it
2948 belongs.
2951 if self.clientSpecDirs:
2952 files = self.extractFilesFromCommit(commit)
2953 self.clientSpecDirs.update_client_spec_path_cache(files)
2955 branches = {}
2956 fnum = 0
2957 while "depotFile%s" % fnum in commit:
2958 raw_path = commit["depotFile%s" % fnum]
2959 path = decode_path(raw_path)
2960 found = self.isPathWanted(path)
2961 if not found:
2962 fnum = fnum + 1
2963 continue
2965 file = {}
2966 file["path"] = raw_path
2967 file["rev"] = commit["rev%s" % fnum]
2968 file["action"] = commit["action%s" % fnum]
2969 file["type"] = commit["type%s" % fnum]
2970 fnum = fnum + 1
2972 # start with the full relative path where this file would
2973 # go in a p4 client
2974 if self.useClientSpec:
2975 relPath = decode_path(self.clientSpecDirs.map_in_client(path))
2976 else:
2977 relPath = self.stripRepoPath(path, self.depotPaths)
2979 for branch in self.knownBranches.keys():
2980 # add a trailing slash so that a commit into qt/4.2foo
2981 # doesn't end up in qt/4.2, e.g.
2982 if p4PathStartsWith(relPath, branch + "/"):
2983 if branch not in branches:
2984 branches[branch] = []
2985 branches[branch].append(file)
2986 break
2988 return branches
2990 def writeToGitStream(self, gitMode, relPath, contents):
2991 self.gitStream.write(encode_text_stream(u'M {} inline {}\n'.format(gitMode, relPath)))
2992 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2993 for d in contents:
2994 self.gitStream.write(d)
2995 self.gitStream.write('\n')
2997 def encodeWithUTF8(self, path):
2998 try:
2999 path.decode('ascii')
3000 except:
3001 encoding = 'utf8'
3002 if gitConfig('git-p4.pathEncoding'):
3003 encoding = gitConfig('git-p4.pathEncoding')
3004 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
3005 if self.verbose:
3006 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
3007 return path
3009 def streamOneP4File(self, file, contents):
3010 """Output one file from the P4 stream.
3012 This is a helper for streamP4Files().
3015 file_path = file['depotFile']
3016 relPath = self.stripRepoPath(decode_path(file_path), self.branchPrefixes)
3018 if verbose:
3019 if 'fileSize' in self.stream_file:
3020 size = int(self.stream_file['fileSize'])
3021 else:
3022 # Deleted files don't get a fileSize apparently
3023 size = 0
3024 sys.stdout.write('\r%s --> %s (%s)\n' % (
3025 file_path, relPath, format_size_human_readable(size)))
3026 sys.stdout.flush()
3028 type_base, type_mods = split_p4_type(file["type"])
3030 git_mode = "100644"
3031 if "x" in type_mods:
3032 git_mode = "100755"
3033 if type_base == "symlink":
3034 git_mode = "120000"
3035 # p4 print on a symlink sometimes contains "target\n";
3036 # if it does, remove the newline
3037 data = ''.join(decode_text_stream(c) for c in contents)
3038 if not data:
3039 # Some version of p4 allowed creating a symlink that pointed
3040 # to nothing. This causes p4 errors when checking out such
3041 # a change, and errors here too. Work around it by ignoring
3042 # the bad symlink; hopefully a future change fixes it.
3043 print("\nIgnoring empty symlink in %s" % file_path)
3044 return
3045 elif data[-1] == '\n':
3046 contents = [data[:-1]]
3047 else:
3048 contents = [data]
3050 if type_base == "utf16":
3051 # p4 delivers different text in the python output to -G
3052 # than it does when using "print -o", or normal p4 client
3053 # operations. utf16 is converted to ascii or utf8, perhaps.
3054 # But ascii text saved as -t utf16 is completely mangled.
3055 # Invoke print -o to get the real contents.
3057 # On windows, the newlines will always be mangled by print, so put
3058 # them back too. This is not needed to the cygwin windows version,
3059 # just the native "NT" type.
3061 try:
3062 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw=True)
3063 except Exception as e:
3064 if 'Translation of file content failed' in str(e):
3065 type_base = 'binary'
3066 else:
3067 raise e
3068 else:
3069 if p4_version_string().find('/NT') >= 0:
3070 text = text.replace(b'\r\n', b'\n')
3071 contents = [text]
3073 if type_base == "apple":
3074 # Apple filetype files will be streamed as a concatenation of
3075 # its appledouble header and the contents. This is useless
3076 # on both macs and non-macs. If using "print -q -o xx", it
3077 # will create "xx" with the data, and "%xx" with the header.
3078 # This is also not very useful.
3080 # Ideally, someday, this script can learn how to generate
3081 # appledouble files directly and import those to git, but
3082 # non-mac machines can never find a use for apple filetype.
3083 print("\nIgnoring apple filetype file %s" % file['depotFile'])
3084 return
3086 if type_base == "utf8":
3087 # The type utf8 explicitly means utf8 *with BOM*. These are
3088 # streamed just like regular text files, however, without
3089 # the BOM in the stream.
3090 # Therefore, to accurately import these files into git, we
3091 # need to explicitly re-add the BOM before writing.
3092 # 'contents' is a set of bytes in this case, so create the
3093 # BOM prefix as a b'' literal.
3094 contents = [b'\xef\xbb\xbf' + contents[0]] + contents[1:]
3096 # Note that we do not try to de-mangle keywords on utf16 files,
3097 # even though in theory somebody may want that.
3098 regexp = p4_keywords_regexp_for_type(type_base, type_mods)
3099 if regexp:
3100 contents = [regexp.sub(br'$\1$', c) for c in contents]
3102 if self.largeFileSystem:
3103 git_mode, contents = self.largeFileSystem.processContent(git_mode, relPath, contents)
3105 self.writeToGitStream(git_mode, relPath, contents)
3107 def streamOneP4Deletion(self, file):
3108 relPath = self.stripRepoPath(decode_path(file['path']), self.branchPrefixes)
3109 if verbose:
3110 sys.stdout.write("delete %s\n" % relPath)
3111 sys.stdout.flush()
3112 self.gitStream.write(encode_text_stream(u'D {}\n'.format(relPath)))
3114 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
3115 self.largeFileSystem.removeLargeFile(relPath)
3117 def streamP4FilesCb(self, marshalled):
3118 """Handle another chunk of streaming data."""
3120 # catch p4 errors and complain
3121 err = None
3122 if "code" in marshalled:
3123 if marshalled["code"] == "error":
3124 if "data" in marshalled:
3125 err = marshalled["data"].rstrip()
3127 if not err and 'fileSize' in self.stream_file:
3128 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
3129 if required_bytes > 0:
3130 err = 'Not enough space left on %s! Free at least %s.' % (
3131 os.getcwd(), format_size_human_readable(required_bytes))
3133 if err:
3134 f = None
3135 if self.stream_have_file_info:
3136 if "depotFile" in self.stream_file:
3137 f = self.stream_file["depotFile"]
3138 # force a failure in fast-import, else an empty
3139 # commit will be made
3140 self.gitStream.write("\n")
3141 self.gitStream.write("die-now\n")
3142 self.gitStream.close()
3143 # ignore errors, but make sure it exits first
3144 self.importProcess.wait()
3145 if f:
3146 die("Error from p4 print for %s: %s" % (f, err))
3147 else:
3148 die("Error from p4 print: %s" % err)
3150 if 'depotFile' in marshalled and self.stream_have_file_info:
3151 # start of a new file - output the old one first
3152 self.streamOneP4File(self.stream_file, self.stream_contents)
3153 self.stream_file = {}
3154 self.stream_contents = []
3155 self.stream_have_file_info = False
3157 # pick up the new file information... for the
3158 # 'data' field we need to append to our array
3159 for k in marshalled.keys():
3160 if k == 'data':
3161 if 'streamContentSize' not in self.stream_file:
3162 self.stream_file['streamContentSize'] = 0
3163 self.stream_file['streamContentSize'] += len(marshalled['data'])
3164 self.stream_contents.append(marshalled['data'])
3165 else:
3166 self.stream_file[k] = marshalled[k]
3168 if (verbose and
3169 'streamContentSize' in self.stream_file and
3170 'fileSize' in self.stream_file and
3171 'depotFile' in self.stream_file):
3172 size = int(self.stream_file["fileSize"])
3173 if size > 0:
3174 progress = 100*self.stream_file['streamContentSize']/size
3175 sys.stdout.write('\r%s %d%% (%s)' % (
3176 self.stream_file['depotFile'], progress,
3177 format_size_human_readable(size)))
3178 sys.stdout.flush()
3180 self.stream_have_file_info = True
3182 def streamP4Files(self, files):
3183 """Stream directly from "p4 files" into "git fast-import."""
3185 filesForCommit = []
3186 filesToRead = []
3187 filesToDelete = []
3189 for f in files:
3190 filesForCommit.append(f)
3191 if f['action'] in self.delete_actions:
3192 filesToDelete.append(f)
3193 else:
3194 filesToRead.append(f)
3196 # deleted files...
3197 for f in filesToDelete:
3198 self.streamOneP4Deletion(f)
3200 if len(filesToRead) > 0:
3201 self.stream_file = {}
3202 self.stream_contents = []
3203 self.stream_have_file_info = False
3205 # curry self argument
3206 def streamP4FilesCbSelf(entry):
3207 self.streamP4FilesCb(entry)
3209 fileArgs = []
3210 for f in filesToRead:
3211 if 'shelved_cl' in f:
3212 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3213 # the contents
3214 fileArg = f['path'] + encode_text_stream('@={}'.format(f['shelved_cl']))
3215 else:
3216 fileArg = f['path'] + encode_text_stream('#{}'.format(f['rev']))
3218 fileArgs.append(fileArg)
3220 p4CmdList(["-x", "-", "print"],
3221 stdin=fileArgs,
3222 cb=streamP4FilesCbSelf)
3224 # do the last chunk
3225 if 'depotFile' in self.stream_file:
3226 self.streamOneP4File(self.stream_file, self.stream_contents)
3228 def make_email(self, userid):
3229 if userid in self.users:
3230 return self.users[userid]
3231 else:
3232 return "%s <a@b>" % userid
3234 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
3235 """Stream a p4 tag.
3237 Commit is either a git commit, or a fast-import mark, ":<p4commit>".
3240 if verbose:
3241 print("writing tag %s for commit %s" % (labelName, commit))
3242 gitStream.write("tag %s\n" % labelName)
3243 gitStream.write("from %s\n" % commit)
3245 if 'Owner' in labelDetails:
3246 owner = labelDetails["Owner"]
3247 else:
3248 owner = None
3250 # Try to use the owner of the p4 label, or failing that,
3251 # the current p4 user id.
3252 if owner:
3253 email = self.make_email(owner)
3254 else:
3255 email = self.make_email(self.p4UserId())
3256 tagger = "%s %s %s" % (email, epoch, self.tz)
3258 gitStream.write("tagger %s\n" % tagger)
3260 print("labelDetails=", labelDetails)
3261 if 'Description' in labelDetails:
3262 description = labelDetails['Description']
3263 else:
3264 description = 'Label from git p4'
3266 gitStream.write("data %d\n" % len(description))
3267 gitStream.write(description)
3268 gitStream.write("\n")
3270 def inClientSpec(self, path):
3271 if not self.clientSpecDirs:
3272 return True
3273 inClientSpec = self.clientSpecDirs.map_in_client(path)
3274 if not inClientSpec and self.verbose:
3275 print('Ignoring file outside of client spec: {0}'.format(path))
3276 return inClientSpec
3278 def hasBranchPrefix(self, path):
3279 if not self.branchPrefixes:
3280 return True
3281 hasPrefix = [p for p in self.branchPrefixes
3282 if p4PathStartsWith(path, p)]
3283 if not hasPrefix and self.verbose:
3284 print('Ignoring file outside of prefix: {0}'.format(path))
3285 return hasPrefix
3287 def findShadowedFiles(self, files, change):
3288 """Perforce allows you commit files and directories with the same name,
3289 so you could have files //depot/foo and //depot/foo/bar both checked
3290 in. A p4 sync of a repository in this state fails. Deleting one of
3291 the files recovers the repository.
3293 Git will not allow the broken state to exist and only the most
3294 recent of the conflicting names is left in the repository. When one
3295 of the conflicting files is deleted we need to re-add the other one
3296 to make sure the git repository recovers in the same way as
3297 perforce.
3300 deleted = [f for f in files if f['action'] in self.delete_actions]
3301 to_check = set()
3302 for f in deleted:
3303 path = decode_path(f['path'])
3304 to_check.add(path + '/...')
3305 while True:
3306 path = path.rsplit("/", 1)[0]
3307 if path == "/" or path in to_check:
3308 break
3309 to_check.add(path)
3310 to_check = ['%s@%s' % (wildcard_encode(p), change) for p in to_check
3311 if self.hasBranchPrefix(p)]
3312 if to_check:
3313 stat_result = p4CmdList(["-x", "-", "fstat", "-T",
3314 "depotFile,headAction,headRev,headType"], stdin=to_check)
3315 for record in stat_result:
3316 if record['code'] != 'stat':
3317 continue
3318 if record['headAction'] in self.delete_actions:
3319 continue
3320 files.append({
3321 'action': 'add',
3322 'path': record['depotFile'],
3323 'rev': record['headRev'],
3324 'type': record['headType']})
3326 def commit(self, details, files, branch, parent="", allow_empty=False):
3327 epoch = details["time"]
3328 author = details["user"]
3329 jobs = self.extractJobsFromCommit(details)
3331 if self.verbose:
3332 print('commit into {0}'.format(branch))
3334 files = [f for f in files
3335 if self.hasBranchPrefix(decode_path(f['path']))]
3336 self.findShadowedFiles(files, details['change'])
3338 if self.clientSpecDirs:
3339 self.clientSpecDirs.update_client_spec_path_cache(files)
3341 files = [f for f in files if self.inClientSpec(decode_path(f['path']))]
3343 if gitConfigBool('git-p4.keepEmptyCommits'):
3344 allow_empty = True
3346 if not files and not allow_empty:
3347 print('Ignoring revision {0} as it would produce an empty commit.'
3348 .format(details['change']))
3349 return
3351 self.gitStream.write("commit %s\n" % branch)
3352 self.gitStream.write("mark :%s\n" % details["change"])
3353 self.committedChanges.add(int(details["change"]))
3354 committer = ""
3355 if author not in self.users:
3356 self.getUserMapFromPerforceServer()
3357 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
3359 self.gitStream.write("committer %s\n" % committer)
3361 self.gitStream.write("data <<EOT\n")
3362 self.gitStream.write(details["desc"])
3363 if len(jobs) > 0:
3364 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3366 if not self.suppress_meta_comment:
3367 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3368 (','.join(self.branchPrefixes), details["change"]))
3369 if len(details['options']) > 0:
3370 self.gitStream.write(": options = %s" % details['options'])
3371 self.gitStream.write("]\n")
3373 self.gitStream.write("EOT\n\n")
3375 if len(parent) > 0:
3376 if self.verbose:
3377 print("parent %s" % parent)
3378 self.gitStream.write("from %s\n" % parent)
3380 self.streamP4Files(files)
3381 self.gitStream.write("\n")
3383 change = int(details["change"])
3385 if change in self.labels:
3386 label = self.labels[change]
3387 labelDetails = label[0]
3388 labelRevisions = label[1]
3389 if self.verbose:
3390 print("Change %s is labelled %s" % (change, labelDetails))
3392 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3393 for p in self.branchPrefixes])
3395 if len(files) == len(labelRevisions):
3397 cleanedFiles = {}
3398 for info in files:
3399 if info["action"] in self.delete_actions:
3400 continue
3401 cleanedFiles[info["depotFile"]] = info["rev"]
3403 if cleanedFiles == labelRevisions:
3404 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3406 else:
3407 if not self.silent:
3408 print("Tag %s does not match with change %s: files do not match."
3409 % (labelDetails["label"], change))
3411 else:
3412 if not self.silent:
3413 print("Tag %s does not match with change %s: file count is different."
3414 % (labelDetails["label"], change))
3416 def getLabels(self):
3417 """Build a dictionary of changelists and labels, for "detect-labels"
3418 option.
3421 self.labels = {}
3423 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3424 if len(l) > 0 and not self.silent:
3425 print("Finding files belonging to labels in %s" % self.depotPaths)
3427 for output in l:
3428 label = output["label"]
3429 revisions = {}
3430 newestChange = 0
3431 if self.verbose:
3432 print("Querying files for label %s" % label)
3433 for file in p4CmdList(["files"] +
3434 ["%s...@%s" % (p, label)
3435 for p in self.depotPaths]):
3436 revisions[file["depotFile"]] = file["rev"]
3437 change = int(file["change"])
3438 if change > newestChange:
3439 newestChange = change
3441 self.labels[newestChange] = [output, revisions]
3443 if self.verbose:
3444 print("Label changes: %s" % self.labels.keys())
3446 def importP4Labels(self, stream, p4Labels):
3447 """Import p4 labels as git tags. A direct mapping does not exist, so
3448 assume that if all the files are at the same revision then we can
3449 use that, or it's something more complicated we should just ignore.
3452 if verbose:
3453 print("import p4 labels: " + ' '.join(p4Labels))
3455 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3456 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3457 if len(validLabelRegexp) == 0:
3458 validLabelRegexp = defaultLabelRegexp
3459 m = re.compile(validLabelRegexp)
3461 for name in p4Labels:
3462 commitFound = False
3464 if not m.match(name):
3465 if verbose:
3466 print("label %s does not match regexp %s" % (name, validLabelRegexp))
3467 continue
3469 if name in ignoredP4Labels:
3470 continue
3472 labelDetails = p4CmdList(['label', "-o", name])[0]
3474 # get the most recent changelist for each file in this label
3475 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3476 for p in self.depotPaths])
3478 if 'change' in change:
3479 # find the corresponding git commit; take the oldest commit
3480 changelist = int(change['change'])
3481 if changelist in self.committedChanges:
3482 gitCommit = ":%d" % changelist # use a fast-import mark
3483 commitFound = True
3484 else:
3485 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3486 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3487 if len(gitCommit) == 0:
3488 print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3489 else:
3490 commitFound = True
3491 gitCommit = gitCommit.strip()
3493 if commitFound:
3494 # Convert from p4 time format
3495 try:
3496 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3497 except ValueError:
3498 print("Could not convert label time %s" % labelDetails['Update'])
3499 tmwhen = 1
3501 when = int(time.mktime(tmwhen))
3502 self.streamTag(stream, name, labelDetails, gitCommit, when)
3503 if verbose:
3504 print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3505 else:
3506 if verbose:
3507 print("Label %s has no changelists - possibly deleted?" % name)
3509 if not commitFound:
3510 # We can't import this label; don't try again as it will get very
3511 # expensive repeatedly fetching all the files for labels that will
3512 # never be imported. If the label is moved in the future, the
3513 # ignore will need to be removed manually.
3514 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3516 def guessProjectName(self):
3517 for p in self.depotPaths:
3518 if p.endswith("/"):
3519 p = p[:-1]
3520 p = p[p.strip().rfind("/") + 1:]
3521 if not p.endswith("/"):
3522 p += "/"
3523 return p
3525 def getBranchMapping(self):
3526 lostAndFoundBranches = set()
3528 user = gitConfig("git-p4.branchUser")
3530 for info in p4CmdList(
3531 ["branches"] + (["-u", user] if len(user) > 0 else [])):
3532 details = p4Cmd(["branch", "-o", info["branch"]])
3533 viewIdx = 0
3534 while "View%s" % viewIdx in details:
3535 paths = details["View%s" % viewIdx].split(" ")
3536 viewIdx = viewIdx + 1
3537 # require standard //depot/foo/... //depot/bar/... mapping
3538 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3539 continue
3540 source = paths[0]
3541 destination = paths[1]
3542 # HACK
3543 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3544 source = source[len(self.depotPaths[0]):-4]
3545 destination = destination[len(self.depotPaths[0]):-4]
3547 if destination in self.knownBranches:
3548 if not self.silent:
3549 print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3550 print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3551 continue
3553 self.knownBranches[destination] = source
3555 lostAndFoundBranches.discard(destination)
3557 if source not in self.knownBranches:
3558 lostAndFoundBranches.add(source)
3560 # Perforce does not strictly require branches to be defined, so we also
3561 # check git config for a branch list.
3563 # Example of branch definition in git config file:
3564 # [git-p4]
3565 # branchList=main:branchA
3566 # branchList=main:branchB
3567 # branchList=branchA:branchC
3568 configBranches = gitConfigList("git-p4.branchList")
3569 for branch in configBranches:
3570 if branch:
3571 source, destination = branch.split(":")
3572 self.knownBranches[destination] = source
3574 lostAndFoundBranches.discard(destination)
3576 if source not in self.knownBranches:
3577 lostAndFoundBranches.add(source)
3579 for branch in lostAndFoundBranches:
3580 self.knownBranches[branch] = branch
3582 def getBranchMappingFromGitBranches(self):
3583 branches = p4BranchesInGit(self.importIntoRemotes)
3584 for branch in branches.keys():
3585 if branch == "master":
3586 branch = "main"
3587 else:
3588 branch = branch[len(self.projectName):]
3589 self.knownBranches[branch] = branch
3591 def updateOptionDict(self, d):
3592 option_keys = {}
3593 if self.keepRepoPath:
3594 option_keys['keepRepoPath'] = 1
3596 d["options"] = ' '.join(sorted(option_keys.keys()))
3598 def readOptions(self, d):
3599 self.keepRepoPath = ('options' in d
3600 and ('keepRepoPath' in d['options']))
3602 def gitRefForBranch(self, branch):
3603 if branch == "main":
3604 return self.refPrefix + "master"
3606 if len(branch) <= 0:
3607 return branch
3609 return self.refPrefix + self.projectName + branch
3611 def gitCommitByP4Change(self, ref, change):
3612 if self.verbose:
3613 print("looking in ref " + ref + " for change %s using bisect..." % change)
3615 earliestCommit = ""
3616 latestCommit = parseRevision(ref)
3618 while True:
3619 if self.verbose:
3620 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3621 next = read_pipe(["git", "rev-list", "--bisect",
3622 latestCommit, earliestCommit]).strip()
3623 if len(next) == 0:
3624 if self.verbose:
3625 print("argh")
3626 return ""
3627 log = extractLogMessageFromGitCommit(next)
3628 settings = extractSettingsGitLog(log)
3629 currentChange = int(settings['change'])
3630 if self.verbose:
3631 print("current change %s" % currentChange)
3633 if currentChange == change:
3634 if self.verbose:
3635 print("found %s" % next)
3636 return next
3638 if currentChange < change:
3639 earliestCommit = "^%s" % next
3640 else:
3641 if next == latestCommit:
3642 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3643 latestCommit = "%s^@" % next
3645 return ""
3647 def importNewBranch(self, branch, maxChange):
3648 # make fast-import flush all changes to disk and update the refs using the checkpoint
3649 # command so that we can try to find the branch parent in the git history
3650 self.gitStream.write("checkpoint\n\n")
3651 self.gitStream.flush()
3652 branchPrefix = self.depotPaths[0] + branch + "/"
3653 range = "@1,%s" % maxChange
3654 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3655 if len(changes) <= 0:
3656 return False
3657 firstChange = changes[0]
3658 sourceBranch = self.knownBranches[branch]
3659 sourceDepotPath = self.depotPaths[0] + sourceBranch
3660 sourceRef = self.gitRefForBranch(sourceBranch)
3662 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3663 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3664 if len(gitParent) > 0:
3665 self.initialParents[self.gitRefForBranch(branch)] = gitParent
3667 self.importChanges(changes)
3668 return True
3670 def searchParent(self, parent, branch, target):
3671 targetTree = read_pipe(["git", "rev-parse",
3672 "{}^{{tree}}".format(target)]).strip()
3673 for line in read_pipe_lines(["git", "rev-list", "--format=%H %T",
3674 "--no-merges", parent]):
3675 if line.startswith("commit "):
3676 continue
3677 commit, tree = line.strip().split(" ")
3678 if tree == targetTree:
3679 if self.verbose:
3680 print("Found parent of %s in commit %s" % (branch, commit))
3681 return commit
3682 return None
3684 def importChanges(self, changes, origin_revision=0):
3685 cnt = 1
3686 for change in changes:
3687 description = p4_describe(change)
3688 self.updateOptionDict(description)
3690 if not self.silent:
3691 sys.stdout.write("\rImporting revision %s (%d%%)" % (
3692 change, (cnt * 100) // len(changes)))
3693 sys.stdout.flush()
3694 cnt = cnt + 1
3696 try:
3697 if self.detectBranches:
3698 branches = self.splitFilesIntoBranches(description)
3699 for branch in branches.keys():
3700 # HACK --hwn
3701 branchPrefix = self.depotPaths[0] + branch + "/"
3702 self.branchPrefixes = [branchPrefix]
3704 parent = ""
3706 filesForCommit = branches[branch]
3708 if self.verbose:
3709 print("branch is %s" % branch)
3711 self.updatedBranches.add(branch)
3713 if branch not in self.createdBranches:
3714 self.createdBranches.add(branch)
3715 parent = self.knownBranches[branch]
3716 if parent == branch:
3717 parent = ""
3718 else:
3719 fullBranch = self.projectName + branch
3720 if fullBranch not in self.p4BranchesInGit:
3721 if not self.silent:
3722 print("\n Importing new branch %s" % fullBranch)
3723 if self.importNewBranch(branch, change - 1):
3724 parent = ""
3725 self.p4BranchesInGit.append(fullBranch)
3726 if not self.silent:
3727 print("\n Resuming with change %s" % change)
3729 if self.verbose:
3730 print("parent determined through known branches: %s" % parent)
3732 branch = self.gitRefForBranch(branch)
3733 parent = self.gitRefForBranch(parent)
3735 if self.verbose:
3736 print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3738 if len(parent) == 0 and branch in self.initialParents:
3739 parent = self.initialParents[branch]
3740 del self.initialParents[branch]
3742 blob = None
3743 if len(parent) > 0:
3744 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3745 if self.verbose:
3746 print("Creating temporary branch: " + tempBranch)
3747 self.commit(description, filesForCommit, tempBranch)
3748 self.tempBranches.append(tempBranch)
3749 self.checkpoint()
3750 blob = self.searchParent(parent, branch, tempBranch)
3751 if blob:
3752 self.commit(description, filesForCommit, branch, blob)
3753 else:
3754 if self.verbose:
3755 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3756 self.commit(description, filesForCommit, branch, parent)
3757 else:
3758 files = self.extractFilesFromCommit(description)
3759 self.commit(description, files, self.branch,
3760 self.initialParent)
3761 # only needed once, to connect to the previous commit
3762 self.initialParent = ""
3763 except IOError:
3764 print(self.gitError.read())
3765 sys.exit(1)
3767 def sync_origin_only(self):
3768 if self.syncWithOrigin:
3769 self.hasOrigin = originP4BranchesExist()
3770 if self.hasOrigin:
3771 if not self.silent:
3772 print('Syncing with origin first, using "git fetch origin"')
3773 system(["git", "fetch", "origin"])
3775 def importHeadRevision(self, revision):
3776 print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3778 details = {}
3779 details["user"] = "git perforce import user"
3780 details["desc"] = ("Initial import of %s from the state at revision %s\n"
3781 % (' '.join(self.depotPaths), revision))
3782 details["change"] = revision
3783 newestRevision = 0
3785 fileCnt = 0
3786 fileArgs = ["%s...%s" % (p, revision) for p in self.depotPaths]
3788 for info in p4CmdList(["files"] + fileArgs):
3790 if 'code' in info and info['code'] == 'error':
3791 sys.stderr.write("p4 returned an error: %s\n"
3792 % info['data'])
3793 if info['data'].find("must refer to client") >= 0:
3794 sys.stderr.write("This particular p4 error is misleading.\n")
3795 sys.stderr.write("Perhaps the depot path was misspelled.\n")
3796 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
3797 sys.exit(1)
3798 if 'p4ExitCode' in info:
3799 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3800 sys.exit(1)
3802 change = int(info["change"])
3803 if change > newestRevision:
3804 newestRevision = change
3806 if info["action"] in self.delete_actions:
3807 continue
3809 for prop in ["depotFile", "rev", "action", "type"]:
3810 details["%s%s" % (prop, fileCnt)] = info[prop]
3812 fileCnt = fileCnt + 1
3814 details["change"] = newestRevision
3816 # Use time from top-most change so that all git p4 clones of
3817 # the same p4 repo have the same commit SHA1s.
3818 res = p4_describe(newestRevision)
3819 details["time"] = res["time"]
3821 self.updateOptionDict(details)
3822 try:
3823 self.commit(details, self.extractFilesFromCommit(details), self.branch)
3824 except IOError as err:
3825 print("IO error with git fast-import. Is your git version recent enough?")
3826 print("IO error details: {}".format(err))
3827 print(self.gitError.read())
3829 def importRevisions(self, args, branch_arg_given):
3830 changes = []
3832 if len(self.changesFile) > 0:
3833 with open(self.changesFile) as f:
3834 output = f.readlines()
3835 changeSet = set()
3836 for line in output:
3837 changeSet.add(int(line))
3839 for change in changeSet:
3840 changes.append(change)
3842 changes.sort()
3843 else:
3844 # catch "git p4 sync" with no new branches, in a repo that
3845 # does not have any existing p4 branches
3846 if len(args) == 0:
3847 if not self.p4BranchesInGit:
3848 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3850 # The default branch is master, unless --branch is used to
3851 # specify something else. Make sure it exists, or complain
3852 # nicely about how to use --branch.
3853 if not self.detectBranches:
3854 if not branch_exists(self.branch):
3855 if branch_arg_given:
3856 raise P4CommandException("Error: branch %s does not exist." % self.branch)
3857 else:
3858 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
3859 self.branch)
3861 if self.verbose:
3862 print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3863 self.changeRange))
3864 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3866 if len(self.maxChanges) > 0:
3867 changes = changes[:min(int(self.maxChanges), len(changes))]
3869 if len(changes) == 0:
3870 if not self.silent:
3871 print("No changes to import!")
3872 else:
3873 if not self.silent and not self.detectBranches:
3874 print("Import destination: %s" % self.branch)
3876 self.updatedBranches = set()
3878 if not self.detectBranches:
3879 if args:
3880 # start a new branch
3881 self.initialParent = ""
3882 else:
3883 # build on a previous revision
3884 self.initialParent = parseRevision(self.branch)
3886 self.importChanges(changes)
3888 if not self.silent:
3889 print("")
3890 if len(self.updatedBranches) > 0:
3891 sys.stdout.write("Updated branches: ")
3892 for b in self.updatedBranches:
3893 sys.stdout.write("%s " % b)
3894 sys.stdout.write("\n")
3896 def openStreams(self):
3897 self.importProcess = subprocess.Popen(["git", "fast-import"],
3898 stdin=subprocess.PIPE,
3899 stdout=subprocess.PIPE,
3900 stderr=subprocess.PIPE)
3901 self.gitOutput = self.importProcess.stdout
3902 self.gitStream = self.importProcess.stdin
3903 self.gitError = self.importProcess.stderr
3905 if bytes is not str:
3906 # Wrap gitStream.write() so that it can be called using `str` arguments
3907 def make_encoded_write(write):
3908 def encoded_write(s):
3909 return write(s.encode() if isinstance(s, str) else s)
3910 return encoded_write
3912 self.gitStream.write = make_encoded_write(self.gitStream.write)
3914 def closeStreams(self):
3915 if self.gitStream is None:
3916 return
3917 self.gitStream.close()
3918 if self.importProcess.wait() != 0:
3919 die("fast-import failed: %s" % self.gitError.read())
3920 self.gitOutput.close()
3921 self.gitError.close()
3922 self.gitStream = None
3924 def run(self, args):
3925 if self.importIntoRemotes:
3926 self.refPrefix = "refs/remotes/p4/"
3927 else:
3928 self.refPrefix = "refs/heads/p4/"
3930 self.sync_origin_only()
3932 branch_arg_given = bool(self.branch)
3933 if len(self.branch) == 0:
3934 self.branch = self.refPrefix + "master"
3935 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3936 system(["git", "update-ref", self.branch, "refs/heads/p4"])
3937 system(["git", "branch", "-D", "p4"])
3939 # accept either the command-line option, or the configuration variable
3940 if self.useClientSpec:
3941 # will use this after clone to set the variable
3942 self.useClientSpec_from_options = True
3943 else:
3944 if gitConfigBool("git-p4.useclientspec"):
3945 self.useClientSpec = True
3946 if self.useClientSpec:
3947 self.clientSpecDirs = getClientSpec()
3949 # TODO: should always look at previous commits,
3950 # merge with previous imports, if possible.
3951 if args == []:
3952 if self.hasOrigin:
3953 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3955 # branches holds mapping from branch name to sha1
3956 branches = p4BranchesInGit(self.importIntoRemotes)
3958 # restrict to just this one, disabling detect-branches
3959 if branch_arg_given:
3960 short = shortP4Ref(self.branch, self.importIntoRemotes)
3961 if short in branches:
3962 self.p4BranchesInGit = [short]
3963 elif self.branch.startswith('refs/') and \
3964 branchExists(self.branch) and \
3965 '[git-p4:' in extractLogMessageFromGitCommit(self.branch):
3966 self.p4BranchesInGit = [self.branch]
3967 else:
3968 self.p4BranchesInGit = branches.keys()
3970 if len(self.p4BranchesInGit) > 1:
3971 if not self.silent:
3972 print("Importing from/into multiple branches")
3973 self.detectBranches = True
3974 for branch in branches.keys():
3975 self.initialParents[self.refPrefix + branch] = \
3976 branches[branch]
3978 if self.verbose:
3979 print("branches: %s" % self.p4BranchesInGit)
3981 p4Change = 0
3982 for branch in self.p4BranchesInGit:
3983 logMsg = extractLogMessageFromGitCommit(fullP4Ref(branch,
3984 self.importIntoRemotes))
3986 settings = extractSettingsGitLog(logMsg)
3988 self.readOptions(settings)
3989 if 'depot-paths' in settings and 'change' in settings:
3990 change = int(settings['change']) + 1
3991 p4Change = max(p4Change, change)
3993 depotPaths = sorted(settings['depot-paths'])
3994 if self.previousDepotPaths == []:
3995 self.previousDepotPaths = depotPaths
3996 else:
3997 paths = []
3998 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3999 prev_list = prev.split("/")
4000 cur_list = cur.split("/")
4001 for i in range(0, min(len(cur_list), len(prev_list))):
4002 if cur_list[i] != prev_list[i]:
4003 i = i - 1
4004 break
4006 paths.append("/".join(cur_list[:i + 1]))
4008 self.previousDepotPaths = paths
4010 if p4Change > 0:
4011 self.depotPaths = sorted(self.previousDepotPaths)
4012 self.changeRange = "@%s,#head" % p4Change
4013 if not self.silent and not self.detectBranches:
4014 print("Performing incremental import into %s git branch" % self.branch)
4016 self.branch = fullP4Ref(self.branch, self.importIntoRemotes)
4018 if len(args) == 0 and self.depotPaths:
4019 if not self.silent:
4020 print("Depot paths: %s" % ' '.join(self.depotPaths))
4021 else:
4022 if self.depotPaths and self.depotPaths != args:
4023 print("previous import used depot path %s and now %s was specified. "
4024 "This doesn't work!" % (' '.join(self.depotPaths),
4025 ' '.join(args)))
4026 sys.exit(1)
4028 self.depotPaths = sorted(args)
4030 revision = ""
4031 self.users = {}
4033 # Make sure no revision specifiers are used when --changesfile
4034 # is specified.
4035 bad_changesfile = False
4036 if len(self.changesFile) > 0:
4037 for p in self.depotPaths:
4038 if p.find("@") >= 0 or p.find("#") >= 0:
4039 bad_changesfile = True
4040 break
4041 if bad_changesfile:
4042 die("Option --changesfile is incompatible with revision specifiers")
4044 newPaths = []
4045 for p in self.depotPaths:
4046 if p.find("@") != -1:
4047 atIdx = p.index("@")
4048 self.changeRange = p[atIdx:]
4049 if self.changeRange == "@all":
4050 self.changeRange = ""
4051 elif ',' not in self.changeRange:
4052 revision = self.changeRange
4053 self.changeRange = ""
4054 p = p[:atIdx]
4055 elif p.find("#") != -1:
4056 hashIdx = p.index("#")
4057 revision = p[hashIdx:]
4058 p = p[:hashIdx]
4059 elif self.previousDepotPaths == []:
4060 # pay attention to changesfile, if given, else import
4061 # the entire p4 tree at the head revision
4062 if len(self.changesFile) == 0:
4063 revision = "#head"
4065 p = re.sub("\.\.\.$", "", p)
4066 if not p.endswith("/"):
4067 p += "/"
4069 newPaths.append(p)
4071 self.depotPaths = newPaths
4073 # --detect-branches may change this for each branch
4074 self.branchPrefixes = self.depotPaths
4076 self.loadUserMapFromCache()
4077 self.labels = {}
4078 if self.detectLabels:
4079 self.getLabels()
4081 if self.detectBranches:
4082 # FIXME - what's a P4 projectName ?
4083 self.projectName = self.guessProjectName()
4085 if self.hasOrigin:
4086 self.getBranchMappingFromGitBranches()
4087 else:
4088 self.getBranchMapping()
4089 if self.verbose:
4090 print("p4-git branches: %s" % self.p4BranchesInGit)
4091 print("initial parents: %s" % self.initialParents)
4092 for b in self.p4BranchesInGit:
4093 if b != "master":
4095 # FIXME
4096 b = b[len(self.projectName):]
4097 self.createdBranches.add(b)
4099 p4_check_access()
4101 self.openStreams()
4103 err = None
4105 try:
4106 if revision:
4107 self.importHeadRevision(revision)
4108 else:
4109 self.importRevisions(args, branch_arg_given)
4111 if gitConfigBool("git-p4.importLabels"):
4112 self.importLabels = True
4114 if self.importLabels:
4115 p4Labels = getP4Labels(self.depotPaths)
4116 gitTags = getGitTags()
4118 missingP4Labels = p4Labels - gitTags
4119 self.importP4Labels(self.gitStream, missingP4Labels)
4121 except P4CommandException as e:
4122 err = e
4124 finally:
4125 self.closeStreams()
4127 if err:
4128 die(str(err))
4130 # Cleanup temporary branches created during import
4131 if self.tempBranches != []:
4132 for branch in self.tempBranches:
4133 read_pipe(["git", "update-ref", "-d", branch])
4134 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
4136 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4137 # a convenient shortcut refname "p4".
4138 if self.importIntoRemotes:
4139 head_ref = self.refPrefix + "HEAD"
4140 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
4141 system(["git", "symbolic-ref", head_ref, self.branch])
4143 return True
4146 class P4Rebase(Command):
4147 def __init__(self):
4148 Command.__init__(self)
4149 self.options = [
4150 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
4152 self.importLabels = False
4153 self.description = ("Fetches the latest revision from perforce and "
4154 + "rebases the current work (branch) against it")
4156 def run(self, args):
4157 sync = P4Sync()
4158 sync.importLabels = self.importLabels
4159 sync.run([])
4161 return self.rebase()
4163 def rebase(self):
4164 if os.system("git update-index --refresh") != 0:
4165 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.")
4166 if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
4167 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.")
4169 upstream, settings = findUpstreamBranchPoint()
4170 if len(upstream) == 0:
4171 die("Cannot find upstream branchpoint for rebase")
4173 # the branchpoint may be p4/foo~3, so strip off the parent
4174 upstream = re.sub("~[0-9]+$", "", upstream)
4176 print("Rebasing the current branch onto %s" % upstream)
4177 oldHead = read_pipe(["git", "rev-parse", "HEAD"]).strip()
4178 system(["git", "rebase", upstream])
4179 system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead,
4180 "HEAD", "--"])
4181 return True
4184 class P4Clone(P4Sync):
4185 def __init__(self):
4186 P4Sync.__init__(self)
4187 self.description = "Creates a new git repository and imports from Perforce into it"
4188 self.usage = "usage: %prog [options] //depot/path[@revRange]"
4189 self.options += [
4190 optparse.make_option("--destination", dest="cloneDestination",
4191 action='store', default=None,
4192 help="where to leave result of the clone"),
4193 optparse.make_option("--bare", dest="cloneBare",
4194 action="store_true", default=False),
4196 self.cloneDestination = None
4197 self.needsGit = False
4198 self.cloneBare = False
4200 def defaultDestination(self, args):
4201 # TODO: use common prefix of args?
4202 depotPath = args[0]
4203 depotDir = re.sub("(@[^@]*)$", "", depotPath)
4204 depotDir = re.sub("(#[^#]*)$", "", depotDir)
4205 depotDir = re.sub(r"\.\.\.$", "", depotDir)
4206 depotDir = re.sub(r"/$", "", depotDir)
4207 return os.path.split(depotDir)[1]
4209 def run(self, args):
4210 if len(args) < 1:
4211 return False
4213 if self.keepRepoPath and not self.cloneDestination:
4214 sys.stderr.write("Must specify destination for --keep-path\n")
4215 sys.exit(1)
4217 depotPaths = args
4219 if not self.cloneDestination and len(depotPaths) > 1:
4220 self.cloneDestination = depotPaths[-1]
4221 depotPaths = depotPaths[:-1]
4223 for p in depotPaths:
4224 if not p.startswith("//"):
4225 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
4226 return False
4228 if not self.cloneDestination:
4229 self.cloneDestination = self.defaultDestination(args)
4231 print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
4233 if not os.path.exists(self.cloneDestination):
4234 os.makedirs(self.cloneDestination)
4235 chdir(self.cloneDestination)
4237 init_cmd = ["git", "init"]
4238 if self.cloneBare:
4239 init_cmd.append("--bare")
4240 retcode = subprocess.call(init_cmd)
4241 if retcode:
4242 raise subprocess.CalledProcessError(retcode, init_cmd)
4244 if not P4Sync.run(self, depotPaths):
4245 return False
4247 # create a master branch and check out a work tree
4248 if gitBranchExists(self.branch):
4249 system(["git", "branch", currentGitBranch(), self.branch])
4250 if not self.cloneBare:
4251 system(["git", "checkout", "-f"])
4252 else:
4253 print('Not checking out any branch, use '
4254 '"git checkout -q -b master <branch>"')
4256 # auto-set this variable if invoked with --use-client-spec
4257 if self.useClientSpec_from_options:
4258 system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
4260 return True
4263 class P4Unshelve(Command):
4264 def __init__(self):
4265 Command.__init__(self)
4266 self.options = []
4267 self.origin = "HEAD"
4268 self.description = "Unshelve a P4 changelist into a git commit"
4269 self.usage = "usage: %prog [options] changelist"
4270 self.options += [
4271 optparse.make_option("--origin", dest="origin",
4272 help="Use this base revision instead of the default (%s)" % self.origin),
4274 self.verbose = False
4275 self.noCommit = False
4276 self.destbranch = "refs/remotes/p4-unshelved"
4278 def renameBranch(self, branch_name):
4279 """Rename the existing branch to branch_name.N ."""
4281 found = True
4282 for i in range(0, 1000):
4283 backup_branch_name = "{0}.{1}".format(branch_name, i)
4284 if not gitBranchExists(backup_branch_name):
4285 # Copy ref to backup
4286 gitUpdateRef(backup_branch_name, branch_name)
4287 gitDeleteRef(branch_name)
4288 found = True
4289 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4290 break
4292 if not found:
4293 sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4295 def findLastP4Revision(self, starting_point):
4296 """Look back from starting_point for the first commit created by git-p4
4297 to find the P4 commit we are based on, and the depot-paths.
4300 for parent in (range(65535)):
4301 log = extractLogMessageFromGitCommit("{0}~{1}".format(starting_point, parent))
4302 settings = extractSettingsGitLog(log)
4303 if 'change' in settings:
4304 return settings
4306 sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4308 def createShelveParent(self, change, branch_name, sync, origin):
4309 """Create a commit matching the parent of the shelved changelist
4310 'change'.
4312 parent_description = p4_describe(change, shelved=True)
4313 parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4314 files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4316 parent_files = []
4317 for f in files:
4318 # if it was added in the shelved changelist, it won't exist in the parent
4319 if f['action'] in self.add_actions:
4320 continue
4322 # if it was deleted in the shelved changelist it must not be deleted
4323 # in the parent - we might even need to create it if the origin branch
4324 # does not have it
4325 if f['action'] in self.delete_actions:
4326 f['action'] = 'add'
4328 parent_files.append(f)
4330 sync.commit(parent_description, parent_files, branch_name,
4331 parent=origin, allow_empty=True)
4332 print("created parent commit for {0} based on {1} in {2}".format(
4333 change, self.origin, branch_name))
4335 def run(self, args):
4336 if len(args) != 1:
4337 return False
4339 if not gitBranchExists(self.origin):
4340 sys.exit("origin branch {0} does not exist".format(self.origin))
4342 sync = P4Sync()
4343 changes = args
4345 # only one change at a time
4346 change = changes[0]
4348 # if the target branch already exists, rename it
4349 branch_name = "{0}/{1}".format(self.destbranch, change)
4350 if gitBranchExists(branch_name):
4351 self.renameBranch(branch_name)
4352 sync.branch = branch_name
4354 sync.verbose = self.verbose
4355 sync.suppress_meta_comment = True
4357 settings = self.findLastP4Revision(self.origin)
4358 sync.depotPaths = settings['depot-paths']
4359 sync.branchPrefixes = sync.depotPaths
4361 sync.openStreams()
4362 sync.loadUserMapFromCache()
4363 sync.silent = True
4365 # create a commit for the parent of the shelved changelist
4366 self.createShelveParent(change, branch_name, sync, self.origin)
4368 # create the commit for the shelved changelist itself
4369 description = p4_describe(change, True)
4370 files = sync.extractFilesFromCommit(description, True, change)
4372 sync.commit(description, files, branch_name, "")
4373 sync.closeStreams()
4375 print("unshelved changelist {0} into {1}".format(change, branch_name))
4377 return True
4380 class P4Branches(Command):
4381 def __init__(self):
4382 Command.__init__(self)
4383 self.options = []
4384 self.description = ("Shows the git branches that hold imports and their "
4385 + "corresponding perforce depot paths")
4386 self.verbose = False
4388 def run(self, args):
4389 if originP4BranchesExist():
4390 createOrUpdateBranchesFromOrigin()
4392 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
4393 line = line.strip()
4395 if not line.startswith('p4/') or line == "p4/HEAD":
4396 continue
4397 branch = line
4399 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4400 settings = extractSettingsGitLog(log)
4402 print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4403 return True
4406 class HelpFormatter(optparse.IndentedHelpFormatter):
4407 def __init__(self):
4408 optparse.IndentedHelpFormatter.__init__(self)
4410 def format_description(self, description):
4411 if description:
4412 return description + "\n"
4413 else:
4414 return ""
4417 def printUsage(commands):
4418 print("usage: %s <command> [options]" % sys.argv[0])
4419 print("")
4420 print("valid commands: %s" % ", ".join(commands))
4421 print("")
4422 print("Try %s <command> --help for command specific help." % sys.argv[0])
4423 print("")
4426 commands = {
4427 "submit": P4Submit,
4428 "commit": P4Submit,
4429 "sync": P4Sync,
4430 "rebase": P4Rebase,
4431 "clone": P4Clone,
4432 "branches": P4Branches,
4433 "unshelve": P4Unshelve,
4437 def main():
4438 if len(sys.argv[1:]) == 0:
4439 printUsage(commands.keys())
4440 sys.exit(2)
4442 cmdName = sys.argv[1]
4443 try:
4444 klass = commands[cmdName]
4445 cmd = klass()
4446 except KeyError:
4447 print("unknown command %s" % cmdName)
4448 print("")
4449 printUsage(commands.keys())
4450 sys.exit(2)
4452 options = cmd.options
4453 cmd.gitdir = os.environ.get("GIT_DIR", None)
4455 args = sys.argv[2:]
4457 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4458 if cmd.needsGit:
4459 options.append(optparse.make_option("--git-dir", dest="gitdir"))
4461 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4462 options,
4463 description=cmd.description,
4464 formatter=HelpFormatter())
4466 try:
4467 cmd, args = parser.parse_args(sys.argv[2:], cmd)
4468 except:
4469 parser.print_help()
4470 raise
4472 global verbose
4473 verbose = cmd.verbose
4474 if cmd.needsGit:
4475 if cmd.gitdir is None:
4476 cmd.gitdir = os.path.abspath(".git")
4477 if not isValidGitDir(cmd.gitdir):
4478 # "rev-parse --git-dir" without arguments will try $PWD/.git
4479 cmd.gitdir = read_pipe(["git", "rev-parse", "--git-dir"]).strip()
4480 if os.path.exists(cmd.gitdir):
4481 cdup = read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
4482 if len(cdup) > 0:
4483 chdir(cdup)
4485 if not isValidGitDir(cmd.gitdir):
4486 if isValidGitDir(cmd.gitdir + "/.git"):
4487 cmd.gitdir += "/.git"
4488 else:
4489 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4491 # so git commands invoked from the P4 workspace will succeed
4492 os.environ["GIT_DIR"] = cmd.gitdir
4494 if not cmd.run(args):
4495 parser.print_help()
4496 sys.exit(2)
4499 if __name__ == '__main__':
4500 main()