Merge branch 'jc/resolve-undo' into maint
[git/debian.git] / git-p4.py
blob8fbf6eb1fe385090489f55c87913d0867ad8c349
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 struct
35 import sys
36 if sys.version_info.major < 3 and sys.version_info.minor < 7:
37 sys.stderr.write("git-p4: requires Python 2.7 or later.\n")
38 sys.exit(1)
40 import ctypes
41 import errno
42 import functools
43 import glob
44 import marshal
45 import optparse
46 import os
47 import platform
48 import re
49 import shutil
50 import stat
51 import subprocess
52 import tempfile
53 import time
54 import zipfile
55 import zlib
57 # On python2.7 where raw_input() and input() are both availble,
58 # we want raw_input's semantics, but aliased to input for python3
59 # compatibility
60 # support basestring in python3
61 try:
62 if raw_input and input:
63 input = raw_input
64 except:
65 pass
67 verbose = False
69 # Only labels/tags matching this will be imported/exported
70 defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
72 # The block size is reduced automatically if required
73 defaultBlockSize = 1 << 20
75 defaultMetadataDecodingStrategy = 'passthrough' if sys.version_info.major == 2 else 'fallback'
76 defaultFallbackMetadataEncoding = 'cp1252'
78 p4_access_checked = False
80 re_ko_keywords = re.compile(br'\$(Id|Header)(:[^$\n]+)?\$')
81 re_k_keywords = re.compile(br'\$(Id|Header|Author|Date|DateTime|Change|File|Revision)(:[^$\n]+)?\$')
84 def format_size_human_readable(num):
85 """Returns a number of units (typically bytes) formatted as a
86 human-readable string.
87 """
88 if num < 1024:
89 return '{:d} B'.format(num)
90 for unit in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
91 num /= 1024.0
92 if num < 1024.0:
93 return "{:3.1f} {}B".format(num, unit)
94 return "{:.1f} YiB".format(num)
97 def p4_build_cmd(cmd):
98 """Build a suitable p4 command line.
100 This consolidates building and returning a p4 command line into one
101 location. It means that hooking into the environment, or other
102 configuration can be done more easily.
104 real_cmd = ["p4"]
106 user = gitConfig("git-p4.user")
107 if len(user) > 0:
108 real_cmd += ["-u", user]
110 password = gitConfig("git-p4.password")
111 if len(password) > 0:
112 real_cmd += ["-P", password]
114 port = gitConfig("git-p4.port")
115 if len(port) > 0:
116 real_cmd += ["-p", port]
118 host = gitConfig("git-p4.host")
119 if len(host) > 0:
120 real_cmd += ["-H", host]
122 client = gitConfig("git-p4.client")
123 if len(client) > 0:
124 real_cmd += ["-c", client]
126 retries = gitConfigInt("git-p4.retries")
127 if retries is None:
128 # Perform 3 retries by default
129 retries = 3
130 if retries > 0:
131 # Provide a way to not pass this option by setting git-p4.retries to 0
132 real_cmd += ["-r", str(retries)]
134 real_cmd += cmd
136 # now check that we can actually talk to the server
137 global p4_access_checked
138 if not p4_access_checked:
139 p4_access_checked = True # suppress access checks in p4_check_access itself
140 p4_check_access()
142 return real_cmd
145 def git_dir(path):
146 """Return TRUE if the given path is a git directory (/path/to/dir/.git).
147 This won't automatically add ".git" to a directory.
149 d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
150 if not d or len(d) == 0:
151 return None
152 else:
153 return d
156 def chdir(path, is_client_path=False):
157 """Do chdir to the given path, and set the PWD environment variable for use
158 by P4. It does not look at getcwd() output. Since we're not using the
159 shell, it is necessary to set the PWD environment variable explicitly.
161 Normally, expand the path to force it to be absolute. This addresses
162 the use of relative path names inside P4 settings, e.g.
163 P4CONFIG=.p4config. P4 does not simply open the filename as given; it
164 looks for .p4config using PWD.
166 If is_client_path, the path was handed to us directly by p4, and may be
167 a symbolic link. Do not call os.getcwd() in this case, because it will
168 cause p4 to think that PWD is not inside the client path.
171 os.chdir(path)
172 if not is_client_path:
173 path = os.getcwd()
174 os.environ['PWD'] = path
177 def calcDiskFree():
178 """Return free space in bytes on the disk of the given dirname."""
179 if platform.system() == 'Windows':
180 free_bytes = ctypes.c_ulonglong(0)
181 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
182 return free_bytes.value
183 else:
184 st = os.statvfs(os.getcwd())
185 return st.f_bavail * st.f_frsize
188 def die(msg):
189 """Terminate execution. Make sure that any running child processes have
190 been wait()ed for before calling this.
192 if verbose:
193 raise Exception(msg)
194 else:
195 sys.stderr.write(msg + "\n")
196 sys.exit(1)
199 def prompt(prompt_text):
200 """Prompt the user to choose one of the choices.
202 Choices are identified in the prompt_text by square brackets around a
203 single letter option.
205 choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text))
206 while True:
207 sys.stderr.flush()
208 sys.stdout.write(prompt_text)
209 sys.stdout.flush()
210 response = sys.stdin.readline().strip().lower()
211 if not response:
212 continue
213 response = response[0]
214 if response in choices:
215 return response
218 # We need different encoding/decoding strategies for text data being passed
219 # around in pipes depending on python version
220 if bytes is not str:
221 # For python3, always encode and decode as appropriate
222 def decode_text_stream(s):
223 return s.decode() if isinstance(s, bytes) else s
225 def encode_text_stream(s):
226 return s.encode() if isinstance(s, str) else s
227 else:
228 # For python2.7, pass read strings as-is, but also allow writing unicode
229 def decode_text_stream(s):
230 return s
232 def encode_text_stream(s):
233 return s.encode('utf_8') if isinstance(s, unicode) else s
236 class MetadataDecodingException(Exception):
237 def __init__(self, input_string):
238 self.input_string = input_string
240 def __str__(self):
241 return """Decoding perforce metadata failed!
242 The failing string was:
246 Consider setting the git-p4.metadataDecodingStrategy config option to
247 'fallback', to allow metadata to be decoded using a fallback encoding,
248 defaulting to cp1252.""".format(self.input_string)
251 encoding_fallback_warning_issued = False
252 encoding_escape_warning_issued = False
253 def metadata_stream_to_writable_bytes(s):
254 encodingStrategy = gitConfig('git-p4.metadataDecodingStrategy') or defaultMetadataDecodingStrategy
255 fallbackEncoding = gitConfig('git-p4.metadataFallbackEncoding') or defaultFallbackMetadataEncoding
256 if not isinstance(s, bytes):
257 return s.encode('utf_8')
258 if encodingStrategy == 'passthrough':
259 return s
260 try:
261 s.decode('utf_8')
262 return s
263 except UnicodeDecodeError:
264 if encodingStrategy == 'fallback' and fallbackEncoding:
265 global encoding_fallback_warning_issued
266 global encoding_escape_warning_issued
267 try:
268 if not encoding_fallback_warning_issued:
269 print("\nCould not decode value as utf-8; using configured fallback encoding %s: %s" % (fallbackEncoding, s))
270 print("\n(this warning is only displayed once during an import)")
271 encoding_fallback_warning_issued = True
272 return s.decode(fallbackEncoding).encode('utf_8')
273 except Exception as exc:
274 if not encoding_escape_warning_issued:
275 print("\nCould not decode value with configured fallback encoding %s; escaping bytes over 127: %s" % (fallbackEncoding, s))
276 print("\n(this warning is only displayed once during an import)")
277 encoding_escape_warning_issued = True
278 escaped_bytes = b''
279 # bytes and strings work very differently in python2 vs python3...
280 if str is bytes:
281 for byte in s:
282 byte_number = struct.unpack('>B', byte)[0]
283 if byte_number > 127:
284 escaped_bytes += b'%'
285 escaped_bytes += hex(byte_number)[2:].upper()
286 else:
287 escaped_bytes += byte
288 else:
289 for byte_number in s:
290 if byte_number > 127:
291 escaped_bytes += b'%'
292 escaped_bytes += hex(byte_number).upper().encode()[2:]
293 else:
294 escaped_bytes += bytes([byte_number])
295 return escaped_bytes
297 raise MetadataDecodingException(s)
300 def decode_path(path):
301 """Decode a given string (bytes or otherwise) using configured path
302 encoding options.
305 encoding = gitConfig('git-p4.pathEncoding') or 'utf_8'
306 if bytes is not str:
307 return path.decode(encoding, errors='replace') if isinstance(path, bytes) else path
308 else:
309 try:
310 path.decode('ascii')
311 except:
312 path = path.decode(encoding, errors='replace')
313 if verbose:
314 print('Path with non-ASCII characters detected. Used {} to decode: {}'.format(encoding, path))
315 return path
318 def run_git_hook(cmd, param=[]):
319 """Execute a hook if the hook exists."""
320 args = ['git', 'hook', 'run', '--ignore-missing', cmd]
321 if param:
322 args.append("--")
323 for p in param:
324 args.append(p)
325 return subprocess.call(args) == 0
328 def write_pipe(c, stdin, *k, **kw):
329 if verbose:
330 sys.stderr.write('Writing pipe: {}\n'.format(' '.join(c)))
332 p = subprocess.Popen(c, stdin=subprocess.PIPE, *k, **kw)
333 pipe = p.stdin
334 val = pipe.write(stdin)
335 pipe.close()
336 if p.wait():
337 die('Command failed: {}'.format(' '.join(c)))
339 return val
342 def p4_write_pipe(c, stdin, *k, **kw):
343 real_cmd = p4_build_cmd(c)
344 if bytes is not str and isinstance(stdin, str):
345 stdin = encode_text_stream(stdin)
346 return write_pipe(real_cmd, stdin, *k, **kw)
349 def read_pipe_full(c, *k, **kw):
350 """Read output from command. Returns a tuple of the return status, stdout
351 text and stderr text.
353 if verbose:
354 sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c)))
356 p = subprocess.Popen(
357 c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, *k, **kw)
358 out, err = p.communicate()
359 return (p.returncode, out, decode_text_stream(err))
362 def read_pipe(c, ignore_error=False, raw=False, *k, **kw):
363 """Read output from command. Returns the output text on success. On
364 failure, terminates execution, unless ignore_error is True, when it
365 returns an empty string.
367 If raw is True, do not attempt to decode output text.
369 retcode, out, err = read_pipe_full(c, *k, **kw)
370 if retcode != 0:
371 if ignore_error:
372 out = ""
373 else:
374 die('Command failed: {}\nError: {}'.format(' '.join(c), err))
375 if not raw:
376 out = decode_text_stream(out)
377 return out
380 def read_pipe_text(c, *k, **kw):
381 """Read output from a command with trailing whitespace stripped. On error,
382 returns None.
384 retcode, out, err = read_pipe_full(c, *k, **kw)
385 if retcode != 0:
386 return None
387 else:
388 return decode_text_stream(out).rstrip()
391 def p4_read_pipe(c, ignore_error=False, raw=False, *k, **kw):
392 real_cmd = p4_build_cmd(c)
393 return read_pipe(real_cmd, ignore_error, raw=raw, *k, **kw)
396 def read_pipe_lines(c, raw=False, *k, **kw):
397 if verbose:
398 sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c)))
400 p = subprocess.Popen(c, stdout=subprocess.PIPE, *k, **kw)
401 pipe = p.stdout
402 lines = pipe.readlines()
403 if not raw:
404 lines = [decode_text_stream(line) for line in lines]
405 if pipe.close() or p.wait():
406 die('Command failed: {}'.format(' '.join(c)))
407 return lines
410 def p4_read_pipe_lines(c, *k, **kw):
411 """Specifically invoke p4 on the command supplied."""
412 real_cmd = p4_build_cmd(c)
413 return read_pipe_lines(real_cmd, *k, **kw)
416 def p4_has_command(cmd):
417 """Ask p4 for help on this command. If it returns an error, the command
418 does not exist in this version of p4.
420 real_cmd = p4_build_cmd(["help", cmd])
421 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
422 stderr=subprocess.PIPE)
423 p.communicate()
424 return p.returncode == 0
427 def p4_has_move_command():
428 """See if the move command exists, that it supports -k, and that it has not
429 been administratively disabled. The arguments must be correct, but the
430 filenames do not have to exist. Use ones with wildcards so even if they
431 exist, it will fail.
434 if not p4_has_command("move"):
435 return False
436 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
437 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
438 out, err = p.communicate()
439 err = decode_text_stream(err)
440 # return code will be 1 in either case
441 if err.find("Invalid option") >= 0:
442 return False
443 if err.find("disabled") >= 0:
444 return False
445 # assume it failed because @... was invalid changelist
446 return True
449 def system(cmd, ignore_error=False, *k, **kw):
450 if verbose:
451 sys.stderr.write("executing {}\n".format(
452 ' '.join(cmd) if isinstance(cmd, list) else cmd))
453 retcode = subprocess.call(cmd, *k, **kw)
454 if retcode and not ignore_error:
455 raise subprocess.CalledProcessError(retcode, cmd)
457 return retcode
460 def p4_system(cmd, *k, **kw):
461 """Specifically invoke p4 as the system command."""
462 real_cmd = p4_build_cmd(cmd)
463 retcode = subprocess.call(real_cmd, *k, **kw)
464 if retcode:
465 raise subprocess.CalledProcessError(retcode, real_cmd)
468 def die_bad_access(s):
469 die("failure accessing depot: {0}".format(s.rstrip()))
472 def p4_check_access(min_expiration=1):
473 """Check if we can access Perforce - account still logged in."""
475 results = p4CmdList(["login", "-s"])
477 if len(results) == 0:
478 # should never get here: always get either some results, or a p4ExitCode
479 assert("could not parse response from perforce")
481 result = results[0]
483 if 'p4ExitCode' in result:
484 # p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
485 die_bad_access("could not run p4")
487 code = result.get("code")
488 if not code:
489 # we get here if we couldn't connect and there was nothing to unmarshal
490 die_bad_access("could not connect")
492 elif code == "stat":
493 expiry = result.get("TicketExpiration")
494 if expiry:
495 expiry = int(expiry)
496 if expiry > min_expiration:
497 # ok to carry on
498 return
499 else:
500 die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
502 else:
503 # account without a timeout - all ok
504 return
506 elif code == "error":
507 data = result.get("data")
508 if data:
509 die_bad_access("p4 error: {0}".format(data))
510 else:
511 die_bad_access("unknown error")
512 elif code == "info":
513 return
514 else:
515 die_bad_access("unknown error code {0}".format(code))
518 _p4_version_string = None
521 def p4_version_string():
522 """Read the version string, showing just the last line, which hopefully is
523 the interesting version bit.
525 $ p4 -V
526 Perforce - The Fast Software Configuration Management System.
527 Copyright 1995-2011 Perforce Software. All rights reserved.
528 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
530 global _p4_version_string
531 if not _p4_version_string:
532 a = p4_read_pipe_lines(["-V"])
533 _p4_version_string = a[-1].rstrip()
534 return _p4_version_string
537 def p4_integrate(src, dest):
538 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
541 def p4_sync(f, *options):
542 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
545 def p4_add(f):
546 """Forcibly add file names with wildcards."""
547 if wildcard_present(f):
548 p4_system(["add", "-f", f])
549 else:
550 p4_system(["add", f])
553 def p4_delete(f):
554 p4_system(["delete", wildcard_encode(f)])
557 def p4_edit(f, *options):
558 p4_system(["edit"] + list(options) + [wildcard_encode(f)])
561 def p4_revert(f):
562 p4_system(["revert", wildcard_encode(f)])
565 def p4_reopen(type, f):
566 p4_system(["reopen", "-t", type, wildcard_encode(f)])
569 def p4_reopen_in_change(changelist, files):
570 cmd = ["reopen", "-c", str(changelist)] + files
571 p4_system(cmd)
574 def p4_move(src, dest):
575 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
578 def p4_last_change():
579 results = p4CmdList(["changes", "-m", "1"], skip_info=True)
580 return int(results[0]['change'])
583 def p4_describe(change, shelved=False):
584 """Make sure it returns a valid result by checking for the presence of
585 field "time".
587 Return a dict of the results.
590 cmd = ["describe", "-s"]
591 if shelved:
592 cmd += ["-S"]
593 cmd += [str(change)]
595 ds = p4CmdList(cmd, skip_info=True)
596 if len(ds) != 1:
597 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
599 d = ds[0]
601 if "p4ExitCode" in d:
602 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
603 str(d)))
604 if "code" in d:
605 if d["code"] == "error":
606 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
608 if "time" not in d:
609 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
611 return d
614 def split_p4_type(p4type):
615 """Canonicalize the p4 type and return a tuple of the base type, plus any
616 modifiers. See "p4 help filetypes" for a list and explanation.
619 p4_filetypes_historical = {
620 "ctempobj": "binary+Sw",
621 "ctext": "text+C",
622 "cxtext": "text+Cx",
623 "ktext": "text+k",
624 "kxtext": "text+kx",
625 "ltext": "text+F",
626 "tempobj": "binary+FSw",
627 "ubinary": "binary+F",
628 "uresource": "resource+F",
629 "uxbinary": "binary+Fx",
630 "xbinary": "binary+x",
631 "xltext": "text+Fx",
632 "xtempobj": "binary+Swx",
633 "xtext": "text+x",
634 "xunicode": "unicode+x",
635 "xutf16": "utf16+x",
637 if p4type in p4_filetypes_historical:
638 p4type = p4_filetypes_historical[p4type]
639 mods = ""
640 s = p4type.split("+")
641 base = s[0]
642 mods = ""
643 if len(s) > 1:
644 mods = s[1]
645 return (base, mods)
648 def p4_type(f):
649 """Return the raw p4 type of a file (text, text+ko, etc)."""
651 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
652 return results[0]['headType']
655 def p4_keywords_regexp_for_type(base, type_mods):
656 """Given a type base and modifier, return a regexp matching the keywords
657 that can be expanded in the file.
660 if base in ("text", "unicode", "binary"):
661 if "ko" in type_mods:
662 return re_ko_keywords
663 elif "k" in type_mods:
664 return re_k_keywords
665 else:
666 return None
667 else:
668 return None
671 def p4_keywords_regexp_for_file(file):
672 """Given a file, return a regexp matching the possible RCS keywords that
673 will be expanded, or None for files with kw expansion turned off.
676 if not os.path.exists(file):
677 return None
678 else:
679 type_base, type_mods = split_p4_type(p4_type(file))
680 return p4_keywords_regexp_for_type(type_base, type_mods)
683 def setP4ExecBit(file, mode):
684 """Reopens an already open file and changes the execute bit to match the
685 execute bit setting in the passed in mode.
688 p4Type = "+x"
690 if not isModeExec(mode):
691 p4Type = getP4OpenedType(file)
692 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
693 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
694 if p4Type[-1] == "+":
695 p4Type = p4Type[0:-1]
697 p4_reopen(p4Type, file)
700 def getP4OpenedType(file):
701 """Returns the perforce file type for the given file."""
703 result = p4_read_pipe(["opened", wildcard_encode(file)])
704 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
705 if match:
706 return match.group(1)
707 else:
708 die("Could not determine file type for %s (result: '%s')" % (file, result))
711 def getP4Labels(depotPaths):
712 """Return the set of all p4 labels."""
714 labels = set()
715 if not isinstance(depotPaths, list):
716 depotPaths = [depotPaths]
718 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
719 label = l['label']
720 labels.add(label)
722 return labels
725 def getGitTags():
726 """Return the set of all git tags."""
728 gitTags = set()
729 for line in read_pipe_lines(["git", "tag"]):
730 tag = line.strip()
731 gitTags.add(tag)
732 return gitTags
735 _diff_tree_pattern = None
738 def parseDiffTreeEntry(entry):
739 """Parses a single diff tree entry into its component elements.
741 See git-diff-tree(1) manpage for details about the format of the diff
742 output. This method returns a dictionary with the following elements:
744 src_mode - The mode of the source file
745 dst_mode - The mode of the destination file
746 src_sha1 - The sha1 for the source file
747 dst_sha1 - The sha1 fr the destination file
748 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
749 status_score - The score for the status (applicable for 'C' and 'R'
750 statuses). This is None if there is no score.
751 src - The path for the source file.
752 dst - The path for the destination file. This is only present for
753 copy or renames. If it is not present, this is None.
755 If the pattern is not matched, None is returned.
758 global _diff_tree_pattern
759 if not _diff_tree_pattern:
760 _diff_tree_pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
762 match = _diff_tree_pattern.match(entry)
763 if match:
764 return {
765 'src_mode': match.group(1),
766 'dst_mode': match.group(2),
767 'src_sha1': match.group(3),
768 'dst_sha1': match.group(4),
769 'status': match.group(5),
770 'status_score': match.group(6),
771 'src': match.group(7),
772 'dst': match.group(10)
774 return None
777 def isModeExec(mode):
778 """Returns True if the given git mode represents an executable file,
779 otherwise False.
781 return mode[-3:] == "755"
784 class P4Exception(Exception):
785 """Base class for exceptions from the p4 client."""
787 def __init__(self, exit_code):
788 self.p4ExitCode = exit_code
791 class P4ServerException(P4Exception):
792 """Base class for exceptions where we get some kind of marshalled up result
793 from the server.
796 def __init__(self, exit_code, p4_result):
797 super(P4ServerException, self).__init__(exit_code)
798 self.p4_result = p4_result
799 self.code = p4_result[0]['code']
800 self.data = p4_result[0]['data']
803 class P4RequestSizeException(P4ServerException):
804 """One of the maxresults or maxscanrows errors."""
806 def __init__(self, exit_code, p4_result, limit):
807 super(P4RequestSizeException, self).__init__(exit_code, p4_result)
808 self.limit = limit
811 class P4CommandException(P4Exception):
812 """Something went wrong calling p4 which means we have to give up."""
814 def __init__(self, msg):
815 self.msg = msg
817 def __str__(self):
818 return self.msg
821 def isModeExecChanged(src_mode, dst_mode):
822 return isModeExec(src_mode) != isModeExec(dst_mode)
825 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
826 errors_as_exceptions=False, *k, **kw):
828 cmd = p4_build_cmd(["-G"] + cmd)
829 if verbose:
830 sys.stderr.write("Opening pipe: {}\n".format(' '.join(cmd)))
832 # Use a temporary file to avoid deadlocks without
833 # subprocess.communicate(), which would put another copy
834 # of stdout into memory.
835 stdin_file = None
836 if stdin is not None:
837 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
838 if not isinstance(stdin, list):
839 stdin_file.write(stdin)
840 else:
841 for i in stdin:
842 stdin_file.write(encode_text_stream(i))
843 stdin_file.write(b'\n')
844 stdin_file.flush()
845 stdin_file.seek(0)
847 p4 = subprocess.Popen(
848 cmd, stdin=stdin_file, stdout=subprocess.PIPE, *k, **kw)
850 result = []
851 try:
852 while True:
853 entry = marshal.load(p4.stdout)
854 if bytes is not str:
855 # Decode unmarshalled dict to use str keys and values, except for:
856 # - `data` which may contain arbitrary binary data
857 # - `desc` or `FullName` which may contain non-UTF8 encoded text handled below, eagerly converted to bytes
858 # - `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text, handled by decode_path()
859 decoded_entry = {}
860 for key, value in entry.items():
861 key = key.decode()
862 if isinstance(value, bytes) and not (key in ('data', 'desc', 'FullName', 'path', 'clientFile') or key.startswith('depotFile')):
863 value = value.decode()
864 decoded_entry[key] = value
865 # Parse out data if it's an error response
866 if decoded_entry.get('code') == 'error' and 'data' in decoded_entry:
867 decoded_entry['data'] = decoded_entry['data'].decode()
868 entry = decoded_entry
869 if skip_info:
870 if 'code' in entry and entry['code'] == 'info':
871 continue
872 if 'desc' in entry:
873 entry['desc'] = metadata_stream_to_writable_bytes(entry['desc'])
874 if 'FullName' in entry:
875 entry['FullName'] = metadata_stream_to_writable_bytes(entry['FullName'])
876 if cb is not None:
877 cb(entry)
878 else:
879 result.append(entry)
880 except EOFError:
881 pass
882 exitCode = p4.wait()
883 if exitCode != 0:
884 if errors_as_exceptions:
885 if len(result) > 0:
886 data = result[0].get('data')
887 if data:
888 m = re.search('Too many rows scanned \(over (\d+)\)', data)
889 if not m:
890 m = re.search('Request too large \(over (\d+)\)', data)
892 if m:
893 limit = int(m.group(1))
894 raise P4RequestSizeException(exitCode, result, limit)
896 raise P4ServerException(exitCode, result)
897 else:
898 raise P4Exception(exitCode)
899 else:
900 entry = {}
901 entry["p4ExitCode"] = exitCode
902 result.append(entry)
904 return result
907 def p4Cmd(cmd, *k, **kw):
908 list = p4CmdList(cmd, *k, **kw)
909 result = {}
910 for entry in list:
911 result.update(entry)
912 return result
915 def p4Where(depotPath):
916 if not depotPath.endswith("/"):
917 depotPath += "/"
918 depotPathLong = depotPath + "..."
919 outputList = p4CmdList(["where", depotPathLong])
920 output = None
921 for entry in outputList:
922 if "depotFile" in entry:
923 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
924 # The base path always ends with "/...".
925 entry_path = decode_path(entry['depotFile'])
926 if entry_path.find(depotPath) == 0 and entry_path[-4:] == "/...":
927 output = entry
928 break
929 elif "data" in entry:
930 data = entry.get("data")
931 space = data.find(" ")
932 if data[:space] == depotPath:
933 output = entry
934 break
935 if output is None:
936 return ""
937 if output["code"] == "error":
938 return ""
939 clientPath = ""
940 if "path" in output:
941 clientPath = decode_path(output['path'])
942 elif "data" in output:
943 data = output.get("data")
944 lastSpace = data.rfind(b" ")
945 clientPath = decode_path(data[lastSpace + 1:])
947 if clientPath.endswith("..."):
948 clientPath = clientPath[:-3]
949 return clientPath
952 def currentGitBranch():
953 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
956 def isValidGitDir(path):
957 return git_dir(path) is not None
960 def parseRevision(ref):
961 return read_pipe(["git", "rev-parse", ref]).strip()
964 def branchExists(ref):
965 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
966 ignore_error=True)
967 return len(rev) > 0
970 def extractLogMessageFromGitCommit(commit):
971 logMessage = ""
973 # fixme: title is first line of commit, not 1st paragraph.
974 foundTitle = False
975 for log in read_pipe_lines(["git", "cat-file", "commit", commit]):
976 if not foundTitle:
977 if len(log) == 1:
978 foundTitle = True
979 continue
981 logMessage += log
982 return logMessage
985 def extractSettingsGitLog(log):
986 values = {}
987 for line in log.split("\n"):
988 line = line.strip()
989 m = re.search(r"^ *\[git-p4: (.*)\]$", line)
990 if not m:
991 continue
993 assignments = m.group(1).split(':')
994 for a in assignments:
995 vals = a.split('=')
996 key = vals[0].strip()
997 val = ('='.join(vals[1:])).strip()
998 if val.endswith('\"') and val.startswith('"'):
999 val = val[1:-1]
1001 values[key] = val
1003 paths = values.get("depot-paths")
1004 if not paths:
1005 paths = values.get("depot-path")
1006 if paths:
1007 values['depot-paths'] = paths.split(',')
1008 return values
1011 def gitBranchExists(branch):
1012 proc = subprocess.Popen(["git", "rev-parse", branch],
1013 stderr=subprocess.PIPE, stdout=subprocess.PIPE)
1014 return proc.wait() == 0
1017 def gitUpdateRef(ref, newvalue):
1018 subprocess.check_call(["git", "update-ref", ref, newvalue])
1021 def gitDeleteRef(ref):
1022 subprocess.check_call(["git", "update-ref", "-d", ref])
1025 _gitConfig = {}
1028 def gitConfig(key, typeSpecifier=None):
1029 if key not in _gitConfig:
1030 cmd = ["git", "config"]
1031 if typeSpecifier:
1032 cmd += [typeSpecifier]
1033 cmd += [key]
1034 s = read_pipe(cmd, ignore_error=True)
1035 _gitConfig[key] = s.strip()
1036 return _gitConfig[key]
1039 def gitConfigBool(key):
1040 """Return a bool, using git config --bool. It is True only if the
1041 variable is set to true, and False if set to false or not present
1042 in the config.
1045 if key not in _gitConfig:
1046 _gitConfig[key] = gitConfig(key, '--bool') == "true"
1047 return _gitConfig[key]
1050 def gitConfigInt(key):
1051 if key not in _gitConfig:
1052 cmd = ["git", "config", "--int", key]
1053 s = read_pipe(cmd, ignore_error=True)
1054 v = s.strip()
1055 try:
1056 _gitConfig[key] = int(gitConfig(key, '--int'))
1057 except ValueError:
1058 _gitConfig[key] = None
1059 return _gitConfig[key]
1062 def gitConfigList(key):
1063 if key not in _gitConfig:
1064 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
1065 _gitConfig[key] = s.strip().splitlines()
1066 if _gitConfig[key] == ['']:
1067 _gitConfig[key] = []
1068 return _gitConfig[key]
1070 def fullP4Ref(incomingRef, importIntoRemotes=True):
1071 """Standardize a given provided p4 ref value to a full git ref:
1072 refs/foo/bar/branch -> use it exactly
1073 p4/branch -> prepend refs/remotes/ or refs/heads/
1074 branch -> prepend refs/remotes/p4/ or refs/heads/p4/"""
1075 if incomingRef.startswith("refs/"):
1076 return incomingRef
1077 if importIntoRemotes:
1078 prepend = "refs/remotes/"
1079 else:
1080 prepend = "refs/heads/"
1081 if not incomingRef.startswith("p4/"):
1082 prepend += "p4/"
1083 return prepend + incomingRef
1085 def shortP4Ref(incomingRef, importIntoRemotes=True):
1086 """Standardize to a "short ref" if possible:
1087 refs/foo/bar/branch -> ignore
1088 refs/remotes/p4/branch or refs/heads/p4/branch -> shorten
1089 p4/branch -> shorten"""
1090 if importIntoRemotes:
1091 longprefix = "refs/remotes/p4/"
1092 else:
1093 longprefix = "refs/heads/p4/"
1094 if incomingRef.startswith(longprefix):
1095 return incomingRef[len(longprefix):]
1096 if incomingRef.startswith("p4/"):
1097 return incomingRef[3:]
1098 return incomingRef
1100 def p4BranchesInGit(branchesAreInRemotes=True):
1101 """Find all the branches whose names start with "p4/", looking
1102 in remotes or heads as specified by the argument. Return
1103 a dictionary of { branch: revision } for each one found.
1104 The branch names are the short names, without any
1105 "p4/" prefix.
1108 branches = {}
1110 cmdline = ["git", "rev-parse", "--symbolic"]
1111 if branchesAreInRemotes:
1112 cmdline.append("--remotes")
1113 else:
1114 cmdline.append("--branches")
1116 for line in read_pipe_lines(cmdline):
1117 line = line.strip()
1119 # only import to p4/
1120 if not line.startswith('p4/'):
1121 continue
1122 # special symbolic ref to p4/master
1123 if line == "p4/HEAD":
1124 continue
1126 # strip off p4/ prefix
1127 branch = line[len("p4/"):]
1129 branches[branch] = parseRevision(line)
1131 return branches
1134 def branch_exists(branch):
1135 """Make sure that the given ref name really exists."""
1137 cmd = ["git", "rev-parse", "--symbolic", "--verify", branch]
1138 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1139 out, _ = p.communicate()
1140 out = decode_text_stream(out)
1141 if p.returncode:
1142 return False
1143 # expect exactly one line of output: the branch name
1144 return out.rstrip() == branch
1147 def findUpstreamBranchPoint(head="HEAD"):
1148 branches = p4BranchesInGit()
1149 # map from depot-path to branch name
1150 branchByDepotPath = {}
1151 for branch in branches.keys():
1152 tip = branches[branch]
1153 log = extractLogMessageFromGitCommit(tip)
1154 settings = extractSettingsGitLog(log)
1155 if "depot-paths" in settings:
1156 git_branch = "remotes/p4/" + branch
1157 paths = ",".join(settings["depot-paths"])
1158 branchByDepotPath[paths] = git_branch
1159 if "change" in settings:
1160 paths = paths + ";" + settings["change"]
1161 branchByDepotPath[paths] = git_branch
1163 settings = None
1164 parent = 0
1165 while parent < 65535:
1166 commit = head + "~%s" % parent
1167 log = extractLogMessageFromGitCommit(commit)
1168 settings = extractSettingsGitLog(log)
1169 if "depot-paths" in settings:
1170 paths = ",".join(settings["depot-paths"])
1171 if "change" in settings:
1172 expaths = paths + ";" + settings["change"]
1173 if expaths in branchByDepotPath:
1174 return [branchByDepotPath[expaths], settings]
1175 if paths in branchByDepotPath:
1176 return [branchByDepotPath[paths], settings]
1178 parent = parent + 1
1180 return ["", settings]
1183 def createOrUpdateBranchesFromOrigin(localRefPrefix="refs/remotes/p4/", silent=True):
1184 if not silent:
1185 print("Creating/updating branch(es) in %s based on origin branch(es)"
1186 % localRefPrefix)
1188 originPrefix = "origin/p4/"
1190 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
1191 line = line.strip()
1192 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
1193 continue
1195 headName = line[len(originPrefix):]
1196 remoteHead = localRefPrefix + headName
1197 originHead = line
1199 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
1200 if 'depot-paths' not in original or 'change' not in original:
1201 continue
1203 update = False
1204 if not gitBranchExists(remoteHead):
1205 if verbose:
1206 print("creating %s" % remoteHead)
1207 update = True
1208 else:
1209 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
1210 if 'change' in settings:
1211 if settings['depot-paths'] == original['depot-paths']:
1212 originP4Change = int(original['change'])
1213 p4Change = int(settings['change'])
1214 if originP4Change > p4Change:
1215 print("%s (%s) is newer than %s (%s). "
1216 "Updating p4 branch from origin."
1217 % (originHead, originP4Change,
1218 remoteHead, p4Change))
1219 update = True
1220 else:
1221 print("Ignoring: %s was imported from %s while "
1222 "%s was imported from %s"
1223 % (originHead, ','.join(original['depot-paths']),
1224 remoteHead, ','.join(settings['depot-paths'])))
1226 if update:
1227 system(["git", "update-ref", remoteHead, originHead])
1230 def originP4BranchesExist():
1231 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
1234 def p4ParseNumericChangeRange(parts):
1235 changeStart = int(parts[0][1:])
1236 if parts[1] == '#head':
1237 changeEnd = p4_last_change()
1238 else:
1239 changeEnd = int(parts[1])
1241 return (changeStart, changeEnd)
1244 def chooseBlockSize(blockSize):
1245 if blockSize:
1246 return blockSize
1247 else:
1248 return defaultBlockSize
1251 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
1252 assert depotPaths
1254 # Parse the change range into start and end. Try to find integer
1255 # revision ranges as these can be broken up into blocks to avoid
1256 # hitting server-side limits (maxrows, maxscanresults). But if
1257 # that doesn't work, fall back to using the raw revision specifier
1258 # strings, without using block mode.
1260 if changeRange is None or changeRange == '':
1261 changeStart = 1
1262 changeEnd = p4_last_change()
1263 block_size = chooseBlockSize(requestedBlockSize)
1264 else:
1265 parts = changeRange.split(',')
1266 assert len(parts) == 2
1267 try:
1268 changeStart, changeEnd = p4ParseNumericChangeRange(parts)
1269 block_size = chooseBlockSize(requestedBlockSize)
1270 except ValueError:
1271 changeStart = parts[0][1:]
1272 changeEnd = parts[1]
1273 if requestedBlockSize:
1274 die("cannot use --changes-block-size with non-numeric revisions")
1275 block_size = None
1277 changes = set()
1279 # Retrieve changes a block at a time, to prevent running
1280 # into a MaxResults/MaxScanRows error from the server. If
1281 # we _do_ hit one of those errors, turn down the block size
1283 while True:
1284 cmd = ['changes']
1286 if block_size:
1287 end = min(changeEnd, changeStart + block_size)
1288 revisionRange = "%d,%d" % (changeStart, end)
1289 else:
1290 revisionRange = "%s,%s" % (changeStart, changeEnd)
1292 for p in depotPaths:
1293 cmd += ["%s...@%s" % (p, revisionRange)]
1295 # fetch the changes
1296 try:
1297 result = p4CmdList(cmd, errors_as_exceptions=True)
1298 except P4RequestSizeException as e:
1299 if not block_size:
1300 block_size = e.limit
1301 elif block_size > e.limit:
1302 block_size = e.limit
1303 else:
1304 block_size = max(2, block_size // 2)
1306 if verbose:
1307 print("block size error, retrying with block size {0}".format(block_size))
1308 continue
1309 except P4Exception as e:
1310 die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1312 # Insert changes in chronological order
1313 for entry in reversed(result):
1314 if 'change' not in entry:
1315 continue
1316 changes.add(int(entry['change']))
1318 if not block_size:
1319 break
1321 if end >= changeEnd:
1322 break
1324 changeStart = end + 1
1326 changes = sorted(changes)
1327 return changes
1330 def p4PathStartsWith(path, prefix):
1331 """This method tries to remedy a potential mixed-case issue:
1333 If UserA adds //depot/DirA/file1
1334 and UserB adds //depot/dira/file2
1336 we may or may not have a problem. If you have core.ignorecase=true,
1337 we treat DirA and dira as the same directory.
1339 if gitConfigBool("core.ignorecase"):
1340 return path.lower().startswith(prefix.lower())
1341 return path.startswith(prefix)
1344 def getClientSpec():
1345 """Look at the p4 client spec, create a View() object that contains
1346 all the mappings, and return it.
1349 specList = p4CmdList(["client", "-o"])
1350 if len(specList) != 1:
1351 die('Output from "client -o" is %d lines, expecting 1' %
1352 len(specList))
1354 # dictionary of all client parameters
1355 entry = specList[0]
1357 # the //client/ name
1358 client_name = entry["Client"]
1360 # just the keys that start with "View"
1361 view_keys = [k for k in entry.keys() if k.startswith("View")]
1363 # hold this new View
1364 view = View(client_name)
1366 # append the lines, in order, to the view
1367 for view_num in range(len(view_keys)):
1368 k = "View%d" % view_num
1369 if k not in view_keys:
1370 die("Expected view key %s missing" % k)
1371 view.append(entry[k])
1373 return view
1376 def getClientRoot():
1377 """Grab the client directory."""
1379 output = p4CmdList(["client", "-o"])
1380 if len(output) != 1:
1381 die('Output from "client -o" is %d lines, expecting 1' % len(output))
1383 entry = output[0]
1384 if "Root" not in entry:
1385 die('Client has no "Root"')
1387 return entry["Root"]
1390 def wildcard_decode(path):
1391 """Decode P4 wildcards into %xx encoding
1393 P4 wildcards are not allowed in filenames. P4 complains if you simply
1394 add them, but you can force it with "-f", in which case it translates
1395 them into %xx encoding internally.
1398 # Search for and fix just these four characters. Do % last so
1399 # that fixing it does not inadvertently create new %-escapes.
1400 # Cannot have * in a filename in windows; untested as to
1401 # what p4 would do in such a case.
1402 if not platform.system() == "Windows":
1403 path = path.replace("%2A", "*")
1404 path = path.replace("%23", "#") \
1405 .replace("%40", "@") \
1406 .replace("%25", "%")
1407 return path
1410 def wildcard_encode(path):
1411 """Encode %xx coded wildcards into P4 coding."""
1413 # do % first to avoid double-encoding the %s introduced here
1414 path = path.replace("%", "%25") \
1415 .replace("*", "%2A") \
1416 .replace("#", "%23") \
1417 .replace("@", "%40")
1418 return path
1421 def wildcard_present(path):
1422 m = re.search("[*#@%]", path)
1423 return m is not None
1426 class LargeFileSystem(object):
1427 """Base class for large file system support."""
1429 def __init__(self, writeToGitStream):
1430 self.largeFiles = set()
1431 self.writeToGitStream = writeToGitStream
1433 def generatePointer(self, cloneDestination, contentFile):
1434 """Return the content of a pointer file that is stored in Git instead
1435 of the actual content.
1437 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1439 def pushFile(self, localLargeFile):
1440 """Push the actual content which is not stored in the Git repository to
1441 a server.
1443 assert False, "Method 'pushFile' required in " + self.__class__.__name__
1445 def hasLargeFileExtension(self, relPath):
1446 return functools.reduce(
1447 lambda a, b: a or b,
1448 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1449 False
1452 def generateTempFile(self, contents):
1453 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1454 for d in contents:
1455 contentFile.write(d)
1456 contentFile.close()
1457 return contentFile.name
1459 def exceedsLargeFileThreshold(self, relPath, contents):
1460 if gitConfigInt('git-p4.largeFileThreshold'):
1461 contentsSize = sum(len(d) for d in contents)
1462 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1463 return True
1464 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1465 contentsSize = sum(len(d) for d in contents)
1466 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1467 return False
1468 contentTempFile = self.generateTempFile(contents)
1469 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True)
1470 with zipfile.ZipFile(compressedContentFile, mode='w') as zf:
1471 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1472 compressedContentsSize = zf.infolist()[0].compress_size
1473 os.remove(contentTempFile)
1474 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1475 return True
1476 return False
1478 def addLargeFile(self, relPath):
1479 self.largeFiles.add(relPath)
1481 def removeLargeFile(self, relPath):
1482 self.largeFiles.remove(relPath)
1484 def isLargeFile(self, relPath):
1485 return relPath in self.largeFiles
1487 def processContent(self, git_mode, relPath, contents):
1488 """Processes the content of git fast import. This method decides if a
1489 file is stored in the large file system and handles all necessary
1490 steps.
1492 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1493 contentTempFile = self.generateTempFile(contents)
1494 pointer_git_mode, contents, localLargeFile = self.generatePointer(contentTempFile)
1495 if pointer_git_mode:
1496 git_mode = pointer_git_mode
1497 if localLargeFile:
1498 # Move temp file to final location in large file system
1499 largeFileDir = os.path.dirname(localLargeFile)
1500 if not os.path.isdir(largeFileDir):
1501 os.makedirs(largeFileDir)
1502 shutil.move(contentTempFile, localLargeFile)
1503 self.addLargeFile(relPath)
1504 if gitConfigBool('git-p4.largeFilePush'):
1505 self.pushFile(localLargeFile)
1506 if verbose:
1507 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1508 return (git_mode, contents)
1511 class MockLFS(LargeFileSystem):
1512 """Mock large file system for testing."""
1514 def generatePointer(self, contentFile):
1515 """The pointer content is the original content prefixed with "pointer-".
1516 The local filename of the large file storage is derived from the
1517 file content.
1519 with open(contentFile, 'r') as f:
1520 content = next(f)
1521 gitMode = '100644'
1522 pointerContents = 'pointer-' + content
1523 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1524 return (gitMode, pointerContents, localLargeFile)
1526 def pushFile(self, localLargeFile):
1527 """The remote filename of the large file storage is the same as the
1528 local one but in a different directory.
1530 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1531 if not os.path.exists(remotePath):
1532 os.makedirs(remotePath)
1533 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1536 class GitLFS(LargeFileSystem):
1537 """Git LFS as backend for the git-p4 large file system.
1538 See https://git-lfs.github.com/ for details.
1541 def __init__(self, *args):
1542 LargeFileSystem.__init__(self, *args)
1543 self.baseGitAttributes = []
1545 def generatePointer(self, contentFile):
1546 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1547 mode and content which is stored in the Git repository instead of
1548 the actual content. Return also the new location of the actual
1549 content.
1551 if os.path.getsize(contentFile) == 0:
1552 return (None, '', None)
1554 pointerProcess = subprocess.Popen(
1555 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1556 stdout=subprocess.PIPE
1558 pointerFile = decode_text_stream(pointerProcess.stdout.read())
1559 if pointerProcess.wait():
1560 os.remove(contentFile)
1561 die('git-lfs pointer command failed. Did you install the extension?')
1563 # Git LFS removed the preamble in the output of the 'pointer' command
1564 # starting from version 1.2.0. Check for the preamble here to support
1565 # earlier versions.
1566 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1567 if pointerFile.startswith('Git LFS pointer for'):
1568 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1570 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
1571 # if someone use external lfs.storage ( not in local repo git )
1572 lfs_path = gitConfig('lfs.storage')
1573 if not lfs_path:
1574 lfs_path = 'lfs'
1575 if not os.path.isabs(lfs_path):
1576 lfs_path = os.path.join(os.getcwd(), '.git', lfs_path)
1577 localLargeFile = os.path.join(
1578 lfs_path,
1579 'objects', oid[:2], oid[2:4],
1580 oid,
1582 # LFS Spec states that pointer files should not have the executable bit set.
1583 gitMode = '100644'
1584 return (gitMode, pointerFile, localLargeFile)
1586 def pushFile(self, localLargeFile):
1587 uploadProcess = subprocess.Popen(
1588 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1590 if uploadProcess.wait():
1591 die('git-lfs push command failed. Did you define a remote?')
1593 def generateGitAttributes(self):
1594 return (
1595 self.baseGitAttributes +
1597 '\n',
1598 '#\n',
1599 '# Git LFS (see https://git-lfs.github.com/)\n',
1600 '#\n',
1602 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1603 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1605 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1606 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1610 def addLargeFile(self, relPath):
1611 LargeFileSystem.addLargeFile(self, relPath)
1612 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1614 def removeLargeFile(self, relPath):
1615 LargeFileSystem.removeLargeFile(self, relPath)
1616 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1618 def processContent(self, git_mode, relPath, contents):
1619 if relPath == '.gitattributes':
1620 self.baseGitAttributes = contents
1621 return (git_mode, self.generateGitAttributes())
1622 else:
1623 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1626 class Command:
1627 delete_actions = ("delete", "move/delete", "purge")
1628 add_actions = ("add", "branch", "move/add")
1630 def __init__(self):
1631 self.usage = "usage: %prog [options]"
1632 self.needsGit = True
1633 self.verbose = False
1635 # This is required for the "append" update_shelve action
1636 def ensure_value(self, attr, value):
1637 if not hasattr(self, attr) or getattr(self, attr) is None:
1638 setattr(self, attr, value)
1639 return getattr(self, attr)
1642 class P4UserMap:
1643 def __init__(self):
1644 self.userMapFromPerforceServer = False
1645 self.myP4UserId = None
1647 def p4UserId(self):
1648 if self.myP4UserId:
1649 return self.myP4UserId
1651 results = p4CmdList(["user", "-o"])
1652 for r in results:
1653 if 'User' in r:
1654 self.myP4UserId = r['User']
1655 return r['User']
1656 die("Could not find your p4 user id")
1658 def p4UserIsMe(self, p4User):
1659 """Return True if the given p4 user is actually me."""
1660 me = self.p4UserId()
1661 if not p4User or p4User != me:
1662 return False
1663 else:
1664 return True
1666 def getUserCacheFilename(self):
1667 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1668 return home + "/.gitp4-usercache.txt"
1670 def getUserMapFromPerforceServer(self):
1671 if self.userMapFromPerforceServer:
1672 return
1673 self.users = {}
1674 self.emails = {}
1676 for output in p4CmdList(["users"]):
1677 if "User" not in output:
1678 continue
1679 # "FullName" is bytes. "Email" on the other hand might be bytes
1680 # or unicode string depending on whether we are running under
1681 # python2 or python3. To support
1682 # git-p4.metadataDecodingStrategy=fallback, self.users dict values
1683 # are always bytes, ready to be written to git.
1684 emailbytes = metadata_stream_to_writable_bytes(output["Email"])
1685 self.users[output["User"]] = output["FullName"] + b" <" + emailbytes + b">"
1686 self.emails[output["Email"]] = output["User"]
1688 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1689 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1690 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1691 if mapUser and len(mapUser[0]) == 3:
1692 user = mapUser[0][0]
1693 fullname = mapUser[0][1]
1694 email = mapUser[0][2]
1695 fulluser = fullname + " <" + email + ">"
1696 self.users[user] = metadata_stream_to_writable_bytes(fulluser)
1697 self.emails[email] = user
1699 s = b''
1700 for (key, val) in self.users.items():
1701 keybytes = metadata_stream_to_writable_bytes(key)
1702 s += b"%s\t%s\n" % (keybytes.expandtabs(1), val.expandtabs(1))
1704 open(self.getUserCacheFilename(), 'wb').write(s)
1705 self.userMapFromPerforceServer = True
1707 def loadUserMapFromCache(self):
1708 self.users = {}
1709 self.userMapFromPerforceServer = False
1710 try:
1711 cache = open(self.getUserCacheFilename(), 'rb')
1712 lines = cache.readlines()
1713 cache.close()
1714 for line in lines:
1715 entry = line.strip().split(b"\t")
1716 self.users[entry[0].decode('utf_8')] = entry[1]
1717 except IOError:
1718 self.getUserMapFromPerforceServer()
1721 class P4Submit(Command, P4UserMap):
1723 conflict_behavior_choices = ("ask", "skip", "quit")
1725 def __init__(self):
1726 Command.__init__(self)
1727 P4UserMap.__init__(self)
1728 self.options = [
1729 optparse.make_option("--origin", dest="origin"),
1730 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1731 # preserve the user, requires relevant p4 permissions
1732 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1733 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1734 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1735 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1736 optparse.make_option("--conflict", dest="conflict_behavior",
1737 choices=self.conflict_behavior_choices),
1738 optparse.make_option("--branch", dest="branch"),
1739 optparse.make_option("--shelve", dest="shelve", action="store_true",
1740 help="Shelve instead of submit. Shelved files are reverted, "
1741 "restoring the workspace to the state before the shelve"),
1742 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
1743 metavar="CHANGELIST",
1744 help="update an existing shelved changelist, implies --shelve, "
1745 "repeat in-order for multiple shelved changelists"),
1746 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1747 help="submit only the specified commit(s), one commit or xxx..xxx"),
1748 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1749 help="Disable rebase after submit is completed. Can be useful if you "
1750 "work from a local git branch that is not master"),
1751 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1752 help="Skip Perforce sync of p4/master after submit or shelve"),
1753 optparse.make_option("--no-verify", dest="no_verify", action="store_true",
1754 help="Bypass p4-pre-submit and p4-changelist hooks"),
1756 self.description = """Submit changes from git to the perforce depot.\n
1757 The `p4-pre-submit` hook is executed if it exists and is executable. It
1758 can be bypassed with the `--no-verify` command line option. The hook takes
1759 no parameters and nothing from standard input. Exiting with a non-zero status
1760 from this script prevents `git-p4 submit` from launching.
1762 One usage scenario is to run unit tests in the hook.
1764 The `p4-prepare-changelist` hook is executed right after preparing the default
1765 changelist message and before the editor is started. It takes one parameter,
1766 the name of the file that contains the changelist text. Exiting with a non-zero
1767 status from the script will abort the process.
1769 The purpose of the hook is to edit the message file in place, and it is not
1770 supressed by the `--no-verify` option. This hook is called even if
1771 `--prepare-p4-only` is set.
1773 The `p4-changelist` hook is executed after the changelist message has been
1774 edited by the user. It can be bypassed with the `--no-verify` option. It
1775 takes a single parameter, the name of the file that holds the proposed
1776 changelist text. Exiting with a non-zero status causes the command to abort.
1778 The hook is allowed to edit the changelist file and can be used to normalize
1779 the text into some project standard format. It can also be used to refuse the
1780 Submit after inspect the message file.
1782 The `p4-post-changelist` hook is invoked after the submit has successfully
1783 occurred in P4. It takes no parameters and is meant primarily for notification
1784 and cannot affect the outcome of the git p4 submit action.
1787 self.usage += " [name of git branch to submit into perforce depot]"
1788 self.origin = ""
1789 self.detectRenames = False
1790 self.preserveUser = gitConfigBool("git-p4.preserveUser")
1791 self.dry_run = False
1792 self.shelve = False
1793 self.update_shelve = list()
1794 self.commit = ""
1795 self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1796 self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
1797 self.prepare_p4_only = False
1798 self.conflict_behavior = None
1799 self.isWindows = (platform.system() == "Windows")
1800 self.exportLabels = False
1801 self.p4HasMoveCommand = p4_has_move_command()
1802 self.branch = None
1803 self.no_verify = False
1805 if gitConfig('git-p4.largeFileSystem'):
1806 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1808 def check(self):
1809 if len(p4CmdList(["opened", "..."])) > 0:
1810 die("You have files opened with perforce! Close them before starting the sync.")
1812 def separate_jobs_from_description(self, message):
1813 """Extract and return a possible Jobs field in the commit message. It
1814 goes into a separate section in the p4 change specification.
1816 A jobs line starts with "Jobs:" and looks like a new field in a
1817 form. Values are white-space separated on the same line or on
1818 following lines that start with a tab.
1820 This does not parse and extract the full git commit message like a
1821 p4 form. It just sees the Jobs: line as a marker to pass everything
1822 from then on directly into the p4 form, but outside the description
1823 section.
1825 Return a tuple (stripped log message, jobs string).
1828 m = re.search(r'^Jobs:', message, re.MULTILINE)
1829 if m is None:
1830 return (message, None)
1832 jobtext = message[m.start():]
1833 stripped_message = message[:m.start()].rstrip()
1834 return (stripped_message, jobtext)
1836 def prepareLogMessage(self, template, message, jobs):
1837 """Edits the template returned from "p4 change -o" to insert the
1838 message in the Description field, and the jobs text in the Jobs
1839 field.
1841 result = ""
1843 inDescriptionSection = False
1845 for line in template.split("\n"):
1846 if line.startswith("#"):
1847 result += line + "\n"
1848 continue
1850 if inDescriptionSection:
1851 if line.startswith("Files:") or line.startswith("Jobs:"):
1852 inDescriptionSection = False
1853 # insert Jobs section
1854 if jobs:
1855 result += jobs + "\n"
1856 else:
1857 continue
1858 else:
1859 if line.startswith("Description:"):
1860 inDescriptionSection = True
1861 line += "\n"
1862 for messageLine in message.split("\n"):
1863 line += "\t" + messageLine + "\n"
1865 result += line + "\n"
1867 return result
1869 def patchRCSKeywords(self, file, regexp):
1870 """Attempt to zap the RCS keywords in a p4 controlled file matching the
1871 given regex.
1873 handle, outFileName = tempfile.mkstemp(dir='.')
1874 try:
1875 with os.fdopen(handle, "wb") as outFile, open(file, "rb") as inFile:
1876 for line in inFile.readlines():
1877 outFile.write(regexp.sub(br'$\1$', line))
1878 # Forcibly overwrite the original file
1879 os.unlink(file)
1880 shutil.move(outFileName, file)
1881 except:
1882 # cleanup our temporary file
1883 os.unlink(outFileName)
1884 print("Failed to strip RCS keywords in %s" % file)
1885 raise
1887 print("Patched up RCS keywords in %s" % file)
1889 def p4UserForCommit(self, id):
1890 """Return the tuple (perforce user,git email) for a given git commit
1893 self.getUserMapFromPerforceServer()
1894 gitEmail = read_pipe(["git", "log", "--max-count=1",
1895 "--format=%ae", id])
1896 gitEmail = gitEmail.strip()
1897 if gitEmail not in self.emails:
1898 return (None, gitEmail)
1899 else:
1900 return (self.emails[gitEmail], gitEmail)
1902 def checkValidP4Users(self, commits):
1903 """Check if any git authors cannot be mapped to p4 users."""
1904 for id in commits:
1905 user, email = self.p4UserForCommit(id)
1906 if not user:
1907 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1908 if gitConfigBool("git-p4.allowMissingP4Users"):
1909 print("%s" % msg)
1910 else:
1911 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1913 def lastP4Changelist(self):
1914 """Get back the last changelist number submitted in this client spec.
1916 This then gets used to patch up the username in the change. If the
1917 same client spec is being used by multiple processes then this might
1918 go wrong.
1920 results = p4CmdList(["client", "-o"]) # find the current client
1921 client = None
1922 for r in results:
1923 if 'Client' in r:
1924 client = r['Client']
1925 break
1926 if not client:
1927 die("could not get client spec")
1928 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1929 for r in results:
1930 if 'change' in r:
1931 return r['change']
1932 die("Could not get changelist number for last submit - cannot patch up user details")
1934 def modifyChangelistUser(self, changelist, newUser):
1935 """Fixup the user field of a changelist after it has been submitted."""
1936 changes = p4CmdList(["change", "-o", changelist])
1937 if len(changes) != 1:
1938 die("Bad output from p4 change modifying %s to user %s" %
1939 (changelist, newUser))
1941 c = changes[0]
1942 if c['User'] == newUser:
1943 # Nothing to do
1944 return
1945 c['User'] = newUser
1946 # p4 does not understand format version 3 and above
1947 input = marshal.dumps(c, 2)
1949 result = p4CmdList(["change", "-f", "-i"], stdin=input)
1950 for r in result:
1951 if 'code' in r:
1952 if r['code'] == 'error':
1953 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1954 if 'data' in r:
1955 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1956 return
1957 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1959 def canChangeChangelists(self):
1960 """Check to see if we have p4 admin or super-user permissions, either
1961 of which are required to modify changelists.
1963 results = p4CmdList(["protects", self.depotPath])
1964 for r in results:
1965 if 'perm' in r:
1966 if r['perm'] == 'admin':
1967 return 1
1968 if r['perm'] == 'super':
1969 return 1
1970 return 0
1972 def prepareSubmitTemplate(self, changelist=None):
1973 """Run "p4 change -o" to grab a change specification template.
1975 This does not use "p4 -G", as it is nice to keep the submission
1976 template in original order, since a human might edit it.
1978 Remove lines in the Files section that show changes to files
1979 outside the depot path we're committing into.
1982 upstream, settings = findUpstreamBranchPoint()
1984 template = """\
1985 # A Perforce Change Specification.
1987 # Change: The change number. 'new' on a new changelist.
1988 # Date: The date this specification was last modified.
1989 # Client: The client on which the changelist was created. Read-only.
1990 # User: The user who created the changelist.
1991 # Status: Either 'pending' or 'submitted'. Read-only.
1992 # Type: Either 'public' or 'restricted'. Default is 'public'.
1993 # Description: Comments about the changelist. Required.
1994 # Jobs: What opened jobs are to be closed by this changelist.
1995 # You may delete jobs from this list. (New changelists only.)
1996 # Files: What opened files from the default changelist are to be added
1997 # to this changelist. You may delete files from this list.
1998 # (New changelists only.)
2000 files_list = []
2001 inFilesSection = False
2002 change_entry = None
2003 args = ['change', '-o']
2004 if changelist:
2005 args.append(str(changelist))
2006 for entry in p4CmdList(args):
2007 if 'code' not in entry:
2008 continue
2009 if entry['code'] == 'stat':
2010 change_entry = entry
2011 break
2012 if not change_entry:
2013 die('Failed to decode output of p4 change -o')
2014 for key, value in change_entry.items():
2015 if key.startswith('File'):
2016 if 'depot-paths' in settings:
2017 if not [p for p in settings['depot-paths']
2018 if p4PathStartsWith(value, p)]:
2019 continue
2020 else:
2021 if not p4PathStartsWith(value, self.depotPath):
2022 continue
2023 files_list.append(value)
2024 continue
2025 # Output in the order expected by prepareLogMessage
2026 for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
2027 if key not in change_entry:
2028 continue
2029 template += '\n'
2030 template += key + ':'
2031 if key == 'Description':
2032 template += '\n'
2033 for field_line in change_entry[key].splitlines():
2034 template += '\t'+field_line+'\n'
2035 if len(files_list) > 0:
2036 template += '\n'
2037 template += 'Files:\n'
2038 for path in files_list:
2039 template += '\t'+path+'\n'
2040 return template
2042 def edit_template(self, template_file):
2043 """Invoke the editor to let the user change the submission message.
2045 Return true if okay to continue with the submit.
2048 # if configured to skip the editing part, just submit
2049 if gitConfigBool("git-p4.skipSubmitEdit"):
2050 return True
2052 # look at the modification time, to check later if the user saved
2053 # the file
2054 mtime = os.stat(template_file).st_mtime
2056 # invoke the editor
2057 if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
2058 editor = os.environ.get("P4EDITOR")
2059 else:
2060 editor = read_pipe(["git", "var", "GIT_EDITOR"]).strip()
2061 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
2063 # If the file was not saved, prompt to see if this patch should
2064 # be skipped. But skip this verification step if configured so.
2065 if gitConfigBool("git-p4.skipSubmitEditCheck"):
2066 return True
2068 # modification time updated means user saved the file
2069 if os.stat(template_file).st_mtime > mtime:
2070 return True
2072 response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
2073 if response == 'y':
2074 return True
2075 if response == 'n':
2076 return False
2078 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
2079 # diff
2080 if "P4DIFF" in os.environ:
2081 del(os.environ["P4DIFF"])
2082 diff = ""
2083 for editedFile in editedFiles:
2084 diff += p4_read_pipe(['diff', '-du',
2085 wildcard_encode(editedFile)])
2087 # new file diff
2088 newdiff = ""
2089 for newFile in filesToAdd:
2090 newdiff += "==== new file ====\n"
2091 newdiff += "--- /dev/null\n"
2092 newdiff += "+++ %s\n" % newFile
2094 is_link = os.path.islink(newFile)
2095 expect_link = newFile in symlinks
2097 if is_link and expect_link:
2098 newdiff += "+%s\n" % os.readlink(newFile)
2099 else:
2100 f = open(newFile, "r")
2101 try:
2102 for line in f.readlines():
2103 newdiff += "+" + line
2104 except UnicodeDecodeError:
2105 # Found non-text data and skip, since diff description
2106 # should only include text
2107 pass
2108 f.close()
2110 return (diff + newdiff).replace('\r\n', '\n')
2112 def applyCommit(self, id):
2113 """Apply one commit, return True if it succeeded."""
2115 print("Applying", read_pipe(["git", "show", "-s",
2116 "--format=format:%h %s", id]))
2118 p4User, gitEmail = self.p4UserForCommit(id)
2120 diff = read_pipe_lines(
2121 ["git", "diff-tree", "-r"] + self.diffOpts + ["{}^".format(id), id])
2122 filesToAdd = set()
2123 filesToChangeType = set()
2124 filesToDelete = set()
2125 editedFiles = set()
2126 pureRenameCopy = set()
2127 symlinks = set()
2128 filesToChangeExecBit = {}
2129 all_files = list()
2131 for line in diff:
2132 diff = parseDiffTreeEntry(line)
2133 modifier = diff['status']
2134 path = diff['src']
2135 all_files.append(path)
2137 if modifier == "M":
2138 p4_edit(path)
2139 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2140 filesToChangeExecBit[path] = diff['dst_mode']
2141 editedFiles.add(path)
2142 elif modifier == "A":
2143 filesToAdd.add(path)
2144 filesToChangeExecBit[path] = diff['dst_mode']
2145 if path in filesToDelete:
2146 filesToDelete.remove(path)
2148 dst_mode = int(diff['dst_mode'], 8)
2149 if dst_mode == 0o120000:
2150 symlinks.add(path)
2152 elif modifier == "D":
2153 filesToDelete.add(path)
2154 if path in filesToAdd:
2155 filesToAdd.remove(path)
2156 elif modifier == "C":
2157 src, dest = diff['src'], diff['dst']
2158 all_files.append(dest)
2159 p4_integrate(src, dest)
2160 pureRenameCopy.add(dest)
2161 if diff['src_sha1'] != diff['dst_sha1']:
2162 p4_edit(dest)
2163 pureRenameCopy.discard(dest)
2164 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2165 p4_edit(dest)
2166 pureRenameCopy.discard(dest)
2167 filesToChangeExecBit[dest] = diff['dst_mode']
2168 if self.isWindows:
2169 # turn off read-only attribute
2170 os.chmod(dest, stat.S_IWRITE)
2171 os.unlink(dest)
2172 editedFiles.add(dest)
2173 elif modifier == "R":
2174 src, dest = diff['src'], diff['dst']
2175 all_files.append(dest)
2176 if self.p4HasMoveCommand:
2177 p4_edit(src) # src must be open before move
2178 p4_move(src, dest) # opens for (move/delete, move/add)
2179 else:
2180 p4_integrate(src, dest)
2181 if diff['src_sha1'] != diff['dst_sha1']:
2182 p4_edit(dest)
2183 else:
2184 pureRenameCopy.add(dest)
2185 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2186 if not self.p4HasMoveCommand:
2187 p4_edit(dest) # with move: already open, writable
2188 filesToChangeExecBit[dest] = diff['dst_mode']
2189 if not self.p4HasMoveCommand:
2190 if self.isWindows:
2191 os.chmod(dest, stat.S_IWRITE)
2192 os.unlink(dest)
2193 filesToDelete.add(src)
2194 editedFiles.add(dest)
2195 elif modifier == "T":
2196 filesToChangeType.add(path)
2197 else:
2198 die("unknown modifier %s for %s" % (modifier, path))
2200 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
2201 patchcmd = diffcmd + " | git apply "
2202 tryPatchCmd = patchcmd + "--check -"
2203 applyPatchCmd = patchcmd + "--check --apply -"
2204 patch_succeeded = True
2206 if verbose:
2207 print("TryPatch: %s" % tryPatchCmd)
2209 if os.system(tryPatchCmd) != 0:
2210 fixed_rcs_keywords = False
2211 patch_succeeded = False
2212 print("Unfortunately applying the change failed!")
2214 # Patch failed, maybe it's just RCS keyword woes. Look through
2215 # the patch to see if that's possible.
2216 if gitConfigBool("git-p4.attemptRCSCleanup"):
2217 file = None
2218 kwfiles = {}
2219 for file in editedFiles | filesToDelete:
2220 # did this file's delta contain RCS keywords?
2221 regexp = p4_keywords_regexp_for_file(file)
2222 if regexp:
2223 # this file is a possibility...look for RCS keywords.
2224 for line in read_pipe_lines(
2225 ["git", "diff", "%s^..%s" % (id, id), file],
2226 raw=True):
2227 if regexp.search(line):
2228 if verbose:
2229 print("got keyword match on %s in %s in %s" % (regex.pattern, line, file))
2230 kwfiles[file] = regexp
2231 break
2233 for file, regexp in kwfiles.items():
2234 if verbose:
2235 print("zapping %s with %s" % (line, regexp.pattern))
2236 # File is being deleted, so not open in p4. Must
2237 # disable the read-only bit on windows.
2238 if self.isWindows and file not in editedFiles:
2239 os.chmod(file, stat.S_IWRITE)
2240 self.patchRCSKeywords(file, kwfiles[file])
2241 fixed_rcs_keywords = True
2243 if fixed_rcs_keywords:
2244 print("Retrying the patch with RCS keywords cleaned up")
2245 if os.system(tryPatchCmd) == 0:
2246 patch_succeeded = True
2247 print("Patch succeesed this time with RCS keywords cleaned")
2249 if not patch_succeeded:
2250 for f in editedFiles:
2251 p4_revert(f)
2252 return False
2255 # Apply the patch for real, and do add/delete/+x handling.
2257 system(applyPatchCmd, shell=True)
2259 for f in filesToChangeType:
2260 p4_edit(f, "-t", "auto")
2261 for f in filesToAdd:
2262 p4_add(f)
2263 for f in filesToDelete:
2264 p4_revert(f)
2265 p4_delete(f)
2267 # Set/clear executable bits
2268 for f in filesToChangeExecBit.keys():
2269 mode = filesToChangeExecBit[f]
2270 setP4ExecBit(f, mode)
2272 update_shelve = 0
2273 if len(self.update_shelve) > 0:
2274 update_shelve = self.update_shelve.pop(0)
2275 p4_reopen_in_change(update_shelve, all_files)
2278 # Build p4 change description, starting with the contents
2279 # of the git commit message.
2281 logMessage = extractLogMessageFromGitCommit(id)
2282 logMessage = logMessage.strip()
2283 logMessage, jobs = self.separate_jobs_from_description(logMessage)
2285 template = self.prepareSubmitTemplate(update_shelve)
2286 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
2288 if self.preserveUser:
2289 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
2291 if self.checkAuthorship and not self.p4UserIsMe(p4User):
2292 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2293 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2294 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2296 separatorLine = "######## everything below this line is just the diff #######\n"
2297 if not self.prepare_p4_only:
2298 submitTemplate += separatorLine
2299 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
2301 handle, fileName = tempfile.mkstemp()
2302 tmpFile = os.fdopen(handle, "w+b")
2303 if self.isWindows:
2304 submitTemplate = submitTemplate.replace("\n", "\r\n")
2305 tmpFile.write(encode_text_stream(submitTemplate))
2306 tmpFile.close()
2308 submitted = False
2310 try:
2311 # Allow the hook to edit the changelist text before presenting it
2312 # to the user.
2313 if not run_git_hook("p4-prepare-changelist", [fileName]):
2314 return False
2316 if self.prepare_p4_only:
2318 # Leave the p4 tree prepared, and the submit template around
2319 # and let the user decide what to do next
2321 submitted = True
2322 print("")
2323 print("P4 workspace prepared for submission.")
2324 print("To submit or revert, go to client workspace")
2325 print(" " + self.clientPath)
2326 print("")
2327 print("To submit, use \"p4 submit\" to write a new description,")
2328 print("or \"p4 submit -i <%s\" to use the one prepared by"
2329 " \"git p4\"." % fileName)
2330 print("You can delete the file \"%s\" when finished." % fileName)
2332 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
2333 print("To preserve change ownership by user %s, you must\n"
2334 "do \"p4 change -f <change>\" after submitting and\n"
2335 "edit the User field.")
2336 if pureRenameCopy:
2337 print("After submitting, renamed files must be re-synced.")
2338 print("Invoke \"p4 sync -f\" on each of these files:")
2339 for f in pureRenameCopy:
2340 print(" " + f)
2342 print("")
2343 print("To revert the changes, use \"p4 revert ...\", and delete")
2344 print("the submit template file \"%s\"" % fileName)
2345 if filesToAdd:
2346 print("Since the commit adds new files, they must be deleted:")
2347 for f in filesToAdd:
2348 print(" " + f)
2349 print("")
2350 sys.stdout.flush()
2351 return True
2353 if self.edit_template(fileName):
2354 if not self.no_verify:
2355 if not run_git_hook("p4-changelist", [fileName]):
2356 print("The p4-changelist hook failed.")
2357 sys.stdout.flush()
2358 return False
2360 # read the edited message and submit
2361 tmpFile = open(fileName, "rb")
2362 message = decode_text_stream(tmpFile.read())
2363 tmpFile.close()
2364 if self.isWindows:
2365 message = message.replace("\r\n", "\n")
2366 if message.find(separatorLine) != -1:
2367 submitTemplate = message[:message.index(separatorLine)]
2368 else:
2369 submitTemplate = message
2371 if len(submitTemplate.strip()) == 0:
2372 print("Changelist is empty, aborting this changelist.")
2373 sys.stdout.flush()
2374 return False
2376 if update_shelve:
2377 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2378 elif self.shelve:
2379 p4_write_pipe(['shelve', '-i'], submitTemplate)
2380 else:
2381 p4_write_pipe(['submit', '-i'], submitTemplate)
2382 # The rename/copy happened by applying a patch that created a
2383 # new file. This leaves it writable, which confuses p4.
2384 for f in pureRenameCopy:
2385 p4_sync(f, "-f")
2387 if self.preserveUser:
2388 if p4User:
2389 # Get last changelist number. Cannot easily get it from
2390 # the submit command output as the output is
2391 # unmarshalled.
2392 changelist = self.lastP4Changelist()
2393 self.modifyChangelistUser(changelist, p4User)
2395 submitted = True
2397 run_git_hook("p4-post-changelist")
2398 finally:
2399 # Revert changes if we skip this patch
2400 if not submitted or self.shelve:
2401 if self.shelve:
2402 print("Reverting shelved files.")
2403 else:
2404 print("Submission cancelled, undoing p4 changes.")
2405 sys.stdout.flush()
2406 for f in editedFiles | filesToDelete:
2407 p4_revert(f)
2408 for f in filesToAdd:
2409 p4_revert(f)
2410 os.remove(f)
2412 if not self.prepare_p4_only:
2413 os.remove(fileName)
2414 return submitted
2416 def exportGitTags(self, gitTags):
2417 """Export git tags as p4 labels. Create a p4 label and then tag with
2418 that.
2421 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2422 if len(validLabelRegexp) == 0:
2423 validLabelRegexp = defaultLabelRegexp
2424 m = re.compile(validLabelRegexp)
2426 for name in gitTags:
2428 if not m.match(name):
2429 if verbose:
2430 print("tag %s does not match regexp %s" % (name, validLabelRegexp))
2431 continue
2433 # Get the p4 commit this corresponds to
2434 logMessage = extractLogMessageFromGitCommit(name)
2435 values = extractSettingsGitLog(logMessage)
2437 if 'change' not in values:
2438 # a tag pointing to something not sent to p4; ignore
2439 if verbose:
2440 print("git tag %s does not give a p4 commit" % name)
2441 continue
2442 else:
2443 changelist = values['change']
2445 # Get the tag details.
2446 inHeader = True
2447 isAnnotated = False
2448 body = []
2449 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2450 l = l.strip()
2451 if inHeader:
2452 if re.match(r'tag\s+', l):
2453 isAnnotated = True
2454 elif re.match(r'\s*$', l):
2455 inHeader = False
2456 continue
2457 else:
2458 body.append(l)
2460 if not isAnnotated:
2461 body = ["lightweight tag imported by git p4\n"]
2463 # Create the label - use the same view as the client spec we are using
2464 clientSpec = getClientSpec()
2466 labelTemplate = "Label: %s\n" % name
2467 labelTemplate += "Description:\n"
2468 for b in body:
2469 labelTemplate += "\t" + b + "\n"
2470 labelTemplate += "View:\n"
2471 for depot_side in clientSpec.mappings:
2472 labelTemplate += "\t%s\n" % depot_side
2474 if self.dry_run:
2475 print("Would create p4 label %s for tag" % name)
2476 elif self.prepare_p4_only:
2477 print("Not creating p4 label %s for tag due to option"
2478 " --prepare-p4-only" % name)
2479 else:
2480 p4_write_pipe(["label", "-i"], labelTemplate)
2482 # Use the label
2483 p4_system(["tag", "-l", name] +
2484 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2486 if verbose:
2487 print("created p4 label for tag %s" % name)
2489 def run(self, args):
2490 if len(args) == 0:
2491 self.master = currentGitBranch()
2492 elif len(args) == 1:
2493 self.master = args[0]
2494 if not branchExists(self.master):
2495 die("Branch %s does not exist" % self.master)
2496 else:
2497 return False
2499 for i in self.update_shelve:
2500 if i <= 0:
2501 sys.exit("invalid changelist %d" % i)
2503 if self.master:
2504 allowSubmit = gitConfig("git-p4.allowSubmit")
2505 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2506 die("%s is not in git-p4.allowSubmit" % self.master)
2508 upstream, settings = findUpstreamBranchPoint()
2509 self.depotPath = settings['depot-paths'][0]
2510 if len(self.origin) == 0:
2511 self.origin = upstream
2513 if len(self.update_shelve) > 0:
2514 self.shelve = True
2516 if self.preserveUser:
2517 if not self.canChangeChangelists():
2518 die("Cannot preserve user names without p4 super-user or admin permissions")
2520 # if not set from the command line, try the config file
2521 if self.conflict_behavior is None:
2522 val = gitConfig("git-p4.conflict")
2523 if val:
2524 if val not in self.conflict_behavior_choices:
2525 die("Invalid value '%s' for config git-p4.conflict" % val)
2526 else:
2527 val = "ask"
2528 self.conflict_behavior = val
2530 if self.verbose:
2531 print("Origin branch is " + self.origin)
2533 if len(self.depotPath) == 0:
2534 print("Internal error: cannot locate perforce depot path from existing branches")
2535 sys.exit(128)
2537 self.useClientSpec = False
2538 if gitConfigBool("git-p4.useclientspec"):
2539 self.useClientSpec = True
2540 if self.useClientSpec:
2541 self.clientSpecDirs = getClientSpec()
2543 # Check for the existence of P4 branches
2544 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2546 if self.useClientSpec and not branchesDetected:
2547 # all files are relative to the client spec
2548 self.clientPath = getClientRoot()
2549 else:
2550 self.clientPath = p4Where(self.depotPath)
2552 if self.clientPath == "":
2553 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2555 print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
2556 self.oldWorkingDirectory = os.getcwd()
2558 # ensure the clientPath exists
2559 new_client_dir = False
2560 if not os.path.exists(self.clientPath):
2561 new_client_dir = True
2562 os.makedirs(self.clientPath)
2564 chdir(self.clientPath, is_client_path=True)
2565 if self.dry_run:
2566 print("Would synchronize p4 checkout in %s" % self.clientPath)
2567 else:
2568 print("Synchronizing p4 checkout...")
2569 if new_client_dir:
2570 # old one was destroyed, and maybe nobody told p4
2571 p4_sync("...", "-f")
2572 else:
2573 p4_sync("...")
2574 self.check()
2576 commits = []
2577 if self.master:
2578 committish = self.master
2579 else:
2580 committish = 'HEAD'
2582 if self.commit != "":
2583 if self.commit.find("..") != -1:
2584 limits_ish = self.commit.split("..")
2585 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2586 commits.append(line.strip())
2587 commits.reverse()
2588 else:
2589 commits.append(self.commit)
2590 else:
2591 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2592 commits.append(line.strip())
2593 commits.reverse()
2595 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2596 self.checkAuthorship = False
2597 else:
2598 self.checkAuthorship = True
2600 if self.preserveUser:
2601 self.checkValidP4Users(commits)
2604 # Build up a set of options to be passed to diff when
2605 # submitting each commit to p4.
2607 if self.detectRenames:
2608 # command-line -M arg
2609 self.diffOpts = ["-M"]
2610 else:
2611 # If not explicitly set check the config variable
2612 detectRenames = gitConfig("git-p4.detectRenames")
2614 if detectRenames.lower() == "false" or detectRenames == "":
2615 self.diffOpts = []
2616 elif detectRenames.lower() == "true":
2617 self.diffOpts = ["-M"]
2618 else:
2619 self.diffOpts = ["-M{}".format(detectRenames)]
2621 # no command-line arg for -C or --find-copies-harder, just
2622 # config variables
2623 detectCopies = gitConfig("git-p4.detectCopies")
2624 if detectCopies.lower() == "false" or detectCopies == "":
2625 pass
2626 elif detectCopies.lower() == "true":
2627 self.diffOpts.append("-C")
2628 else:
2629 self.diffOpts.append("-C{}".format(detectCopies))
2631 if gitConfigBool("git-p4.detectCopiesHarder"):
2632 self.diffOpts.append("--find-copies-harder")
2634 num_shelves = len(self.update_shelve)
2635 if num_shelves > 0 and num_shelves != len(commits):
2636 sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2637 (len(commits), num_shelves))
2639 if not self.no_verify:
2640 try:
2641 if not run_git_hook("p4-pre-submit"):
2642 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip "
2643 "this pre-submission check by adding\nthe command line option '--no-verify', "
2644 "however,\nthis will also skip the p4-changelist hook as well.")
2645 sys.exit(1)
2646 except Exception as e:
2647 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "
2648 "with the error '{0}'".format(e.message))
2649 sys.exit(1)
2652 # Apply the commits, one at a time. On failure, ask if should
2653 # continue to try the rest of the patches, or quit.
2655 if self.dry_run:
2656 print("Would apply")
2657 applied = []
2658 last = len(commits) - 1
2659 for i, commit in enumerate(commits):
2660 if self.dry_run:
2661 print(" ", read_pipe(["git", "show", "-s",
2662 "--format=format:%h %s", commit]))
2663 ok = True
2664 else:
2665 ok = self.applyCommit(commit)
2666 if ok:
2667 applied.append(commit)
2668 if self.prepare_p4_only:
2669 if i < last:
2670 print("Processing only the first commit due to option"
2671 " --prepare-p4-only")
2672 break
2673 else:
2674 if i < last:
2675 # prompt for what to do, or use the option/variable
2676 if self.conflict_behavior == "ask":
2677 print("What do you want to do?")
2678 response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2679 elif self.conflict_behavior == "skip":
2680 response = "s"
2681 elif self.conflict_behavior == "quit":
2682 response = "q"
2683 else:
2684 die("Unknown conflict_behavior '%s'" %
2685 self.conflict_behavior)
2687 if response == "s":
2688 print("Skipping this commit, but applying the rest")
2689 if response == "q":
2690 print("Quitting")
2691 break
2693 chdir(self.oldWorkingDirectory)
2694 shelved_applied = "shelved" if self.shelve else "applied"
2695 if self.dry_run:
2696 pass
2697 elif self.prepare_p4_only:
2698 pass
2699 elif len(commits) == len(applied):
2700 print("All commits {0}!".format(shelved_applied))
2702 sync = P4Sync()
2703 if self.branch:
2704 sync.branch = self.branch
2705 if self.disable_p4sync:
2706 sync.sync_origin_only()
2707 else:
2708 sync.run([])
2710 if not self.disable_rebase:
2711 rebase = P4Rebase()
2712 rebase.rebase()
2714 else:
2715 if len(applied) == 0:
2716 print("No commits {0}.".format(shelved_applied))
2717 else:
2718 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2719 for c in commits:
2720 if c in applied:
2721 star = "*"
2722 else:
2723 star = " "
2724 print(star, read_pipe(["git", "show", "-s",
2725 "--format=format:%h %s", c]))
2726 print("You will have to do 'git p4 sync' and rebase.")
2728 if gitConfigBool("git-p4.exportLabels"):
2729 self.exportLabels = True
2731 if self.exportLabels:
2732 p4Labels = getP4Labels(self.depotPath)
2733 gitTags = getGitTags()
2735 missingGitTags = gitTags - p4Labels
2736 self.exportGitTags(missingGitTags)
2738 # exit with error unless everything applied perfectly
2739 if len(commits) != len(applied):
2740 sys.exit(1)
2742 return True
2745 class View(object):
2746 """Represent a p4 view ("p4 help views"), and map files in a repo according
2747 to the view.
2750 def __init__(self, client_name):
2751 self.mappings = []
2752 self.client_prefix = "//%s/" % client_name
2753 # cache results of "p4 where" to lookup client file locations
2754 self.client_spec_path_cache = {}
2756 def append(self, view_line):
2757 """Parse a view line, splitting it into depot and client sides. Append
2758 to self.mappings, preserving order. This is only needed for tag
2759 creation.
2762 # Split the view line into exactly two words. P4 enforces
2763 # structure on these lines that simplifies this quite a bit.
2765 # Either or both words may be double-quoted.
2766 # Single quotes do not matter.
2767 # Double-quote marks cannot occur inside the words.
2768 # A + or - prefix is also inside the quotes.
2769 # There are no quotes unless they contain a space.
2770 # The line is already white-space stripped.
2771 # The two words are separated by a single space.
2773 if view_line[0] == '"':
2774 # First word is double quoted. Find its end.
2775 close_quote_index = view_line.find('"', 1)
2776 if close_quote_index <= 0:
2777 die("No first-word closing quote found: %s" % view_line)
2778 depot_side = view_line[1:close_quote_index]
2779 # skip closing quote and space
2780 rhs_index = close_quote_index + 1 + 1
2781 else:
2782 space_index = view_line.find(" ")
2783 if space_index <= 0:
2784 die("No word-splitting space found: %s" % view_line)
2785 depot_side = view_line[0:space_index]
2786 rhs_index = space_index + 1
2788 # prefix + means overlay on previous mapping
2789 if depot_side.startswith("+"):
2790 depot_side = depot_side[1:]
2792 # prefix - means exclude this path, leave out of mappings
2793 exclude = False
2794 if depot_side.startswith("-"):
2795 exclude = True
2796 depot_side = depot_side[1:]
2798 if not exclude:
2799 self.mappings.append(depot_side)
2801 def convert_client_path(self, clientFile):
2802 # chop off //client/ part to make it relative
2803 if not decode_path(clientFile).startswith(self.client_prefix):
2804 die("No prefix '%s' on clientFile '%s'" %
2805 (self.client_prefix, clientFile))
2806 return clientFile[len(self.client_prefix):]
2808 def update_client_spec_path_cache(self, files):
2809 """Caching file paths by "p4 where" batch query."""
2811 # List depot file paths exclude that already cached
2812 fileArgs = [f['path'] for f in files if decode_path(f['path']) not in self.client_spec_path_cache]
2814 if len(fileArgs) == 0:
2815 return # All files in cache
2817 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2818 for res in where_result:
2819 if "code" in res and res["code"] == "error":
2820 # assume error is "... file(s) not in client view"
2821 continue
2822 if "clientFile" not in res:
2823 die("No clientFile in 'p4 where' output")
2824 if "unmap" in res:
2825 # it will list all of them, but only one not unmap-ped
2826 continue
2827 depot_path = decode_path(res['depotFile'])
2828 if gitConfigBool("core.ignorecase"):
2829 depot_path = depot_path.lower()
2830 self.client_spec_path_cache[depot_path] = self.convert_client_path(res["clientFile"])
2832 # not found files or unmap files set to ""
2833 for depotFile in fileArgs:
2834 depotFile = decode_path(depotFile)
2835 if gitConfigBool("core.ignorecase"):
2836 depotFile = depotFile.lower()
2837 if depotFile not in self.client_spec_path_cache:
2838 self.client_spec_path_cache[depotFile] = b''
2840 def map_in_client(self, depot_path):
2841 """Return the relative location in the client where this depot file
2842 should live.
2844 Returns "" if the file should not be mapped in the client.
2847 if gitConfigBool("core.ignorecase"):
2848 depot_path = depot_path.lower()
2850 if depot_path in self.client_spec_path_cache:
2851 return self.client_spec_path_cache[depot_path]
2853 die("Error: %s is not found in client spec path" % depot_path)
2854 return ""
2857 def cloneExcludeCallback(option, opt_str, value, parser):
2858 # prepend "/" because the first "/" was consumed as part of the option itself.
2859 # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2860 parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2863 class P4Sync(Command, P4UserMap):
2865 def __init__(self):
2866 Command.__init__(self)
2867 P4UserMap.__init__(self)
2868 self.options = [
2869 optparse.make_option("--branch", dest="branch"),
2870 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2871 optparse.make_option("--changesfile", dest="changesFile"),
2872 optparse.make_option("--silent", dest="silent", action="store_true"),
2873 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2874 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2875 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2876 help="Import into refs/heads/ , not refs/remotes"),
2877 optparse.make_option("--max-changes", dest="maxChanges",
2878 help="Maximum number of changes to import"),
2879 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2880 help="Internal block size to use when iteratively calling p4 changes"),
2881 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2882 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2883 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2884 help="Only sync files that are included in the Perforce Client Spec"),
2885 optparse.make_option("-/", dest="cloneExclude",
2886 action="callback", callback=cloneExcludeCallback, type="string",
2887 help="exclude depot path"),
2889 self.description = """Imports from Perforce into a git repository.\n
2890 example:
2891 //depot/my/project/ -- to import the current head
2892 //depot/my/project/@all -- to import everything
2893 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2895 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2897 self.usage += " //depot/path[@revRange]"
2898 self.silent = False
2899 self.createdBranches = set()
2900 self.committedChanges = set()
2901 self.branch = ""
2902 self.detectBranches = False
2903 self.detectLabels = False
2904 self.importLabels = False
2905 self.changesFile = ""
2906 self.syncWithOrigin = True
2907 self.importIntoRemotes = True
2908 self.maxChanges = ""
2909 self.changes_block_size = None
2910 self.keepRepoPath = False
2911 self.depotPaths = None
2912 self.p4BranchesInGit = []
2913 self.cloneExclude = []
2914 self.useClientSpec = False
2915 self.useClientSpec_from_options = False
2916 self.clientSpecDirs = None
2917 self.tempBranches = []
2918 self.tempBranchLocation = "refs/git-p4-tmp"
2919 self.largeFileSystem = None
2920 self.suppress_meta_comment = False
2922 if gitConfig('git-p4.largeFileSystem'):
2923 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2924 self.largeFileSystem = largeFileSystemConstructor(
2925 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2928 if gitConfig("git-p4.syncFromOrigin") == "false":
2929 self.syncWithOrigin = False
2931 self.depotPaths = []
2932 self.changeRange = ""
2933 self.previousDepotPaths = []
2934 self.hasOrigin = False
2936 # map from branch depot path to parent branch
2937 self.knownBranches = {}
2938 self.initialParents = {}
2940 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2941 self.labels = {}
2943 def checkpoint(self):
2944 """Force a checkpoint in fast-import and wait for it to finish."""
2945 self.gitStream.write("checkpoint\n\n")
2946 self.gitStream.write("progress checkpoint\n\n")
2947 self.gitStream.flush()
2948 out = self.gitOutput.readline()
2949 if self.verbose:
2950 print("checkpoint finished: " + out)
2952 def isPathWanted(self, path):
2953 for p in self.cloneExclude:
2954 if p.endswith("/"):
2955 if p4PathStartsWith(path, p):
2956 return False
2957 # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2958 elif path.lower() == p.lower():
2959 return False
2960 for p in self.depotPaths:
2961 if p4PathStartsWith(path, decode_path(p)):
2962 return True
2963 return False
2965 def extractFilesFromCommit(self, commit, shelved=False, shelved_cl=0):
2966 files = []
2967 fnum = 0
2968 while "depotFile%s" % fnum in commit:
2969 path = commit["depotFile%s" % fnum]
2970 found = self.isPathWanted(decode_path(path))
2971 if not found:
2972 fnum = fnum + 1
2973 continue
2975 file = {}
2976 file["path"] = path
2977 file["rev"] = commit["rev%s" % fnum]
2978 file["action"] = commit["action%s" % fnum]
2979 file["type"] = commit["type%s" % fnum]
2980 if shelved:
2981 file["shelved_cl"] = int(shelved_cl)
2982 files.append(file)
2983 fnum = fnum + 1
2984 return files
2986 def extractJobsFromCommit(self, commit):
2987 jobs = []
2988 jnum = 0
2989 while "job%s" % jnum in commit:
2990 job = commit["job%s" % jnum]
2991 jobs.append(job)
2992 jnum = jnum + 1
2993 return jobs
2995 def stripRepoPath(self, path, prefixes):
2996 """When streaming files, this is called to map a p4 depot path to where
2997 it should go in git. The prefixes are either self.depotPaths, or
2998 self.branchPrefixes in the case of branch detection.
3001 if self.useClientSpec:
3002 # branch detection moves files up a level (the branch name)
3003 # from what client spec interpretation gives
3004 path = decode_path(self.clientSpecDirs.map_in_client(path))
3005 if self.detectBranches:
3006 for b in self.knownBranches:
3007 if p4PathStartsWith(path, b + "/"):
3008 path = path[len(b)+1:]
3010 elif self.keepRepoPath:
3011 # Preserve everything in relative path name except leading
3012 # //depot/; just look at first prefix as they all should
3013 # be in the same depot.
3014 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
3015 if p4PathStartsWith(path, depot):
3016 path = path[len(depot):]
3018 else:
3019 for p in prefixes:
3020 if p4PathStartsWith(path, p):
3021 path = path[len(p):]
3022 break
3024 path = wildcard_decode(path)
3025 return path
3027 def splitFilesIntoBranches(self, commit):
3028 """Look at each depotFile in the commit to figure out to what branch it
3029 belongs.
3032 if self.clientSpecDirs:
3033 files = self.extractFilesFromCommit(commit)
3034 self.clientSpecDirs.update_client_spec_path_cache(files)
3036 branches = {}
3037 fnum = 0
3038 while "depotFile%s" % fnum in commit:
3039 raw_path = commit["depotFile%s" % fnum]
3040 path = decode_path(raw_path)
3041 found = self.isPathWanted(path)
3042 if not found:
3043 fnum = fnum + 1
3044 continue
3046 file = {}
3047 file["path"] = raw_path
3048 file["rev"] = commit["rev%s" % fnum]
3049 file["action"] = commit["action%s" % fnum]
3050 file["type"] = commit["type%s" % fnum]
3051 fnum = fnum + 1
3053 # start with the full relative path where this file would
3054 # go in a p4 client
3055 if self.useClientSpec:
3056 relPath = decode_path(self.clientSpecDirs.map_in_client(path))
3057 else:
3058 relPath = self.stripRepoPath(path, self.depotPaths)
3060 for branch in self.knownBranches.keys():
3061 # add a trailing slash so that a commit into qt/4.2foo
3062 # doesn't end up in qt/4.2, e.g.
3063 if p4PathStartsWith(relPath, branch + "/"):
3064 if branch not in branches:
3065 branches[branch] = []
3066 branches[branch].append(file)
3067 break
3069 return branches
3071 def writeToGitStream(self, gitMode, relPath, contents):
3072 self.gitStream.write(encode_text_stream(u'M {} inline {}\n'.format(gitMode, relPath)))
3073 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
3074 for d in contents:
3075 self.gitStream.write(d)
3076 self.gitStream.write('\n')
3078 def encodeWithUTF8(self, path):
3079 try:
3080 path.decode('ascii')
3081 except:
3082 encoding = 'utf8'
3083 if gitConfig('git-p4.pathEncoding'):
3084 encoding = gitConfig('git-p4.pathEncoding')
3085 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
3086 if self.verbose:
3087 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
3088 return path
3090 def streamOneP4File(self, file, contents):
3091 """Output one file from the P4 stream.
3093 This is a helper for streamP4Files().
3096 file_path = file['depotFile']
3097 relPath = self.stripRepoPath(decode_path(file_path), self.branchPrefixes)
3099 if verbose:
3100 if 'fileSize' in self.stream_file:
3101 size = int(self.stream_file['fileSize'])
3102 else:
3103 # Deleted files don't get a fileSize apparently
3104 size = 0
3105 sys.stdout.write('\r%s --> %s (%s)\n' % (
3106 file_path, relPath, format_size_human_readable(size)))
3107 sys.stdout.flush()
3109 type_base, type_mods = split_p4_type(file["type"])
3111 git_mode = "100644"
3112 if "x" in type_mods:
3113 git_mode = "100755"
3114 if type_base == "symlink":
3115 git_mode = "120000"
3116 # p4 print on a symlink sometimes contains "target\n";
3117 # if it does, remove the newline
3118 data = ''.join(decode_text_stream(c) for c in contents)
3119 if not data:
3120 # Some version of p4 allowed creating a symlink that pointed
3121 # to nothing. This causes p4 errors when checking out such
3122 # a change, and errors here too. Work around it by ignoring
3123 # the bad symlink; hopefully a future change fixes it.
3124 print("\nIgnoring empty symlink in %s" % file_path)
3125 return
3126 elif data[-1] == '\n':
3127 contents = [data[:-1]]
3128 else:
3129 contents = [data]
3131 if type_base == "utf16":
3132 # p4 delivers different text in the python output to -G
3133 # than it does when using "print -o", or normal p4 client
3134 # operations. utf16 is converted to ascii or utf8, perhaps.
3135 # But ascii text saved as -t utf16 is completely mangled.
3136 # Invoke print -o to get the real contents.
3138 # On windows, the newlines will always be mangled by print, so put
3139 # them back too. This is not needed to the cygwin windows version,
3140 # just the native "NT" type.
3142 try:
3143 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw=True)
3144 except Exception as e:
3145 if 'Translation of file content failed' in str(e):
3146 type_base = 'binary'
3147 else:
3148 raise e
3149 else:
3150 if p4_version_string().find('/NT') >= 0:
3151 text = text.replace(b'\r\n', b'\n')
3152 contents = [text]
3154 if type_base == "apple":
3155 # Apple filetype files will be streamed as a concatenation of
3156 # its appledouble header and the contents. This is useless
3157 # on both macs and non-macs. If using "print -q -o xx", it
3158 # will create "xx" with the data, and "%xx" with the header.
3159 # This is also not very useful.
3161 # Ideally, someday, this script can learn how to generate
3162 # appledouble files directly and import those to git, but
3163 # non-mac machines can never find a use for apple filetype.
3164 print("\nIgnoring apple filetype file %s" % file['depotFile'])
3165 return
3167 if type_base == "utf8":
3168 # The type utf8 explicitly means utf8 *with BOM*. These are
3169 # streamed just like regular text files, however, without
3170 # the BOM in the stream.
3171 # Therefore, to accurately import these files into git, we
3172 # need to explicitly re-add the BOM before writing.
3173 # 'contents' is a set of bytes in this case, so create the
3174 # BOM prefix as a b'' literal.
3175 contents = [b'\xef\xbb\xbf' + contents[0]] + contents[1:]
3177 # Note that we do not try to de-mangle keywords on utf16 files,
3178 # even though in theory somebody may want that.
3179 regexp = p4_keywords_regexp_for_type(type_base, type_mods)
3180 if regexp:
3181 contents = [regexp.sub(br'$\1$', c) for c in contents]
3183 if self.largeFileSystem:
3184 git_mode, contents = self.largeFileSystem.processContent(git_mode, relPath, contents)
3186 self.writeToGitStream(git_mode, relPath, contents)
3188 def streamOneP4Deletion(self, file):
3189 relPath = self.stripRepoPath(decode_path(file['path']), self.branchPrefixes)
3190 if verbose:
3191 sys.stdout.write("delete %s\n" % relPath)
3192 sys.stdout.flush()
3193 self.gitStream.write(encode_text_stream(u'D {}\n'.format(relPath)))
3195 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
3196 self.largeFileSystem.removeLargeFile(relPath)
3198 def streamP4FilesCb(self, marshalled):
3199 """Handle another chunk of streaming data."""
3201 # catch p4 errors and complain
3202 err = None
3203 if "code" in marshalled:
3204 if marshalled["code"] == "error":
3205 if "data" in marshalled:
3206 err = marshalled["data"].rstrip()
3208 if not err and 'fileSize' in self.stream_file:
3209 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
3210 if required_bytes > 0:
3211 err = 'Not enough space left on %s! Free at least %s.' % (
3212 os.getcwd(), format_size_human_readable(required_bytes))
3214 if err:
3215 f = None
3216 if self.stream_have_file_info:
3217 if "depotFile" in self.stream_file:
3218 f = self.stream_file["depotFile"]
3219 # force a failure in fast-import, else an empty
3220 # commit will be made
3221 self.gitStream.write("\n")
3222 self.gitStream.write("die-now\n")
3223 self.gitStream.close()
3224 # ignore errors, but make sure it exits first
3225 self.importProcess.wait()
3226 if f:
3227 die("Error from p4 print for %s: %s" % (f, err))
3228 else:
3229 die("Error from p4 print: %s" % err)
3231 if 'depotFile' in marshalled and self.stream_have_file_info:
3232 # start of a new file - output the old one first
3233 self.streamOneP4File(self.stream_file, self.stream_contents)
3234 self.stream_file = {}
3235 self.stream_contents = []
3236 self.stream_have_file_info = False
3238 # pick up the new file information... for the
3239 # 'data' field we need to append to our array
3240 for k in marshalled.keys():
3241 if k == 'data':
3242 if 'streamContentSize' not in self.stream_file:
3243 self.stream_file['streamContentSize'] = 0
3244 self.stream_file['streamContentSize'] += len(marshalled['data'])
3245 self.stream_contents.append(marshalled['data'])
3246 else:
3247 self.stream_file[k] = marshalled[k]
3249 if (verbose and
3250 'streamContentSize' in self.stream_file and
3251 'fileSize' in self.stream_file and
3252 'depotFile' in self.stream_file):
3253 size = int(self.stream_file["fileSize"])
3254 if size > 0:
3255 progress = 100*self.stream_file['streamContentSize']/size
3256 sys.stdout.write('\r%s %d%% (%s)' % (
3257 self.stream_file['depotFile'], progress,
3258 format_size_human_readable(size)))
3259 sys.stdout.flush()
3261 self.stream_have_file_info = True
3263 def streamP4Files(self, files):
3264 """Stream directly from "p4 files" into "git fast-import."""
3266 filesForCommit = []
3267 filesToRead = []
3268 filesToDelete = []
3270 for f in files:
3271 filesForCommit.append(f)
3272 if f['action'] in self.delete_actions:
3273 filesToDelete.append(f)
3274 else:
3275 filesToRead.append(f)
3277 # deleted files...
3278 for f in filesToDelete:
3279 self.streamOneP4Deletion(f)
3281 if len(filesToRead) > 0:
3282 self.stream_file = {}
3283 self.stream_contents = []
3284 self.stream_have_file_info = False
3286 # curry self argument
3287 def streamP4FilesCbSelf(entry):
3288 self.streamP4FilesCb(entry)
3290 fileArgs = []
3291 for f in filesToRead:
3292 if 'shelved_cl' in f:
3293 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3294 # the contents
3295 fileArg = f['path'] + encode_text_stream('@={}'.format(f['shelved_cl']))
3296 else:
3297 fileArg = f['path'] + encode_text_stream('#{}'.format(f['rev']))
3299 fileArgs.append(fileArg)
3301 p4CmdList(["-x", "-", "print"],
3302 stdin=fileArgs,
3303 cb=streamP4FilesCbSelf)
3305 # do the last chunk
3306 if 'depotFile' in self.stream_file:
3307 self.streamOneP4File(self.stream_file, self.stream_contents)
3309 def make_email(self, userid):
3310 if userid in self.users:
3311 return self.users[userid]
3312 else:
3313 userid_bytes = metadata_stream_to_writable_bytes(userid)
3314 return b"%s <a@b>" % userid_bytes
3316 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
3317 """Stream a p4 tag.
3319 Commit is either a git commit, or a fast-import mark, ":<p4commit>".
3322 if verbose:
3323 print("writing tag %s for commit %s" % (labelName, commit))
3324 gitStream.write("tag %s\n" % labelName)
3325 gitStream.write("from %s\n" % commit)
3327 if 'Owner' in labelDetails:
3328 owner = labelDetails["Owner"]
3329 else:
3330 owner = None
3332 # Try to use the owner of the p4 label, or failing that,
3333 # the current p4 user id.
3334 if owner:
3335 email = self.make_email(owner)
3336 else:
3337 email = self.make_email(self.p4UserId())
3339 gitStream.write("tagger ")
3340 gitStream.write(email)
3341 gitStream.write(" %s %s\n" % (epoch, self.tz))
3343 print("labelDetails=", labelDetails)
3344 if 'Description' in labelDetails:
3345 description = labelDetails['Description']
3346 else:
3347 description = 'Label from git p4'
3349 gitStream.write("data %d\n" % len(description))
3350 gitStream.write(description)
3351 gitStream.write("\n")
3353 def inClientSpec(self, path):
3354 if not self.clientSpecDirs:
3355 return True
3356 inClientSpec = self.clientSpecDirs.map_in_client(path)
3357 if not inClientSpec and self.verbose:
3358 print('Ignoring file outside of client spec: {0}'.format(path))
3359 return inClientSpec
3361 def hasBranchPrefix(self, path):
3362 if not self.branchPrefixes:
3363 return True
3364 hasPrefix = [p for p in self.branchPrefixes
3365 if p4PathStartsWith(path, p)]
3366 if not hasPrefix and self.verbose:
3367 print('Ignoring file outside of prefix: {0}'.format(path))
3368 return hasPrefix
3370 def findShadowedFiles(self, files, change):
3371 """Perforce allows you commit files and directories with the same name,
3372 so you could have files //depot/foo and //depot/foo/bar both checked
3373 in. A p4 sync of a repository in this state fails. Deleting one of
3374 the files recovers the repository.
3376 Git will not allow the broken state to exist and only the most
3377 recent of the conflicting names is left in the repository. When one
3378 of the conflicting files is deleted we need to re-add the other one
3379 to make sure the git repository recovers in the same way as
3380 perforce.
3383 deleted = [f for f in files if f['action'] in self.delete_actions]
3384 to_check = set()
3385 for f in deleted:
3386 path = decode_path(f['path'])
3387 to_check.add(path + '/...')
3388 while True:
3389 path = path.rsplit("/", 1)[0]
3390 if path == "/" or path in to_check:
3391 break
3392 to_check.add(path)
3393 to_check = ['%s@%s' % (wildcard_encode(p), change) for p in to_check
3394 if self.hasBranchPrefix(p)]
3395 if to_check:
3396 stat_result = p4CmdList(["-x", "-", "fstat", "-T",
3397 "depotFile,headAction,headRev,headType"], stdin=to_check)
3398 for record in stat_result:
3399 if record['code'] != 'stat':
3400 continue
3401 if record['headAction'] in self.delete_actions:
3402 continue
3403 files.append({
3404 'action': 'add',
3405 'path': record['depotFile'],
3406 'rev': record['headRev'],
3407 'type': record['headType']})
3409 def commit(self, details, files, branch, parent="", allow_empty=False):
3410 epoch = details["time"]
3411 author = details["user"]
3412 jobs = self.extractJobsFromCommit(details)
3414 if self.verbose:
3415 print('commit into {0}'.format(branch))
3417 files = [f for f in files
3418 if self.hasBranchPrefix(decode_path(f['path']))]
3419 self.findShadowedFiles(files, details['change'])
3421 if self.clientSpecDirs:
3422 self.clientSpecDirs.update_client_spec_path_cache(files)
3424 files = [f for f in files if self.inClientSpec(decode_path(f['path']))]
3426 if gitConfigBool('git-p4.keepEmptyCommits'):
3427 allow_empty = True
3429 if not files and not allow_empty:
3430 print('Ignoring revision {0} as it would produce an empty commit.'
3431 .format(details['change']))
3432 return
3434 self.gitStream.write("commit %s\n" % branch)
3435 self.gitStream.write("mark :%s\n" % details["change"])
3436 self.committedChanges.add(int(details["change"]))
3437 if author not in self.users:
3438 self.getUserMapFromPerforceServer()
3440 self.gitStream.write("committer ")
3441 self.gitStream.write(self.make_email(author))
3442 self.gitStream.write(" %s %s\n" % (epoch, self.tz))
3444 self.gitStream.write("data <<EOT\n")
3445 self.gitStream.write(details["desc"])
3446 if len(jobs) > 0:
3447 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3449 if not self.suppress_meta_comment:
3450 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3451 (','.join(self.branchPrefixes), details["change"]))
3452 if len(details['options']) > 0:
3453 self.gitStream.write(": options = %s" % details['options'])
3454 self.gitStream.write("]\n")
3456 self.gitStream.write("EOT\n\n")
3458 if len(parent) > 0:
3459 if self.verbose:
3460 print("parent %s" % parent)
3461 self.gitStream.write("from %s\n" % parent)
3463 self.streamP4Files(files)
3464 self.gitStream.write("\n")
3466 change = int(details["change"])
3468 if change in self.labels:
3469 label = self.labels[change]
3470 labelDetails = label[0]
3471 labelRevisions = label[1]
3472 if self.verbose:
3473 print("Change %s is labelled %s" % (change, labelDetails))
3475 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3476 for p in self.branchPrefixes])
3478 if len(files) == len(labelRevisions):
3480 cleanedFiles = {}
3481 for info in files:
3482 if info["action"] in self.delete_actions:
3483 continue
3484 cleanedFiles[info["depotFile"]] = info["rev"]
3486 if cleanedFiles == labelRevisions:
3487 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3489 else:
3490 if not self.silent:
3491 print("Tag %s does not match with change %s: files do not match."
3492 % (labelDetails["label"], change))
3494 else:
3495 if not self.silent:
3496 print("Tag %s does not match with change %s: file count is different."
3497 % (labelDetails["label"], change))
3499 def getLabels(self):
3500 """Build a dictionary of changelists and labels, for "detect-labels"
3501 option.
3504 self.labels = {}
3506 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3507 if len(l) > 0 and not self.silent:
3508 print("Finding files belonging to labels in %s" % self.depotPaths)
3510 for output in l:
3511 label = output["label"]
3512 revisions = {}
3513 newestChange = 0
3514 if self.verbose:
3515 print("Querying files for label %s" % label)
3516 for file in p4CmdList(["files"] +
3517 ["%s...@%s" % (p, label)
3518 for p in self.depotPaths]):
3519 revisions[file["depotFile"]] = file["rev"]
3520 change = int(file["change"])
3521 if change > newestChange:
3522 newestChange = change
3524 self.labels[newestChange] = [output, revisions]
3526 if self.verbose:
3527 print("Label changes: %s" % self.labels.keys())
3529 def importP4Labels(self, stream, p4Labels):
3530 """Import p4 labels as git tags. A direct mapping does not exist, so
3531 assume that if all the files are at the same revision then we can
3532 use that, or it's something more complicated we should just ignore.
3535 if verbose:
3536 print("import p4 labels: " + ' '.join(p4Labels))
3538 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3539 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3540 if len(validLabelRegexp) == 0:
3541 validLabelRegexp = defaultLabelRegexp
3542 m = re.compile(validLabelRegexp)
3544 for name in p4Labels:
3545 commitFound = False
3547 if not m.match(name):
3548 if verbose:
3549 print("label %s does not match regexp %s" % (name, validLabelRegexp))
3550 continue
3552 if name in ignoredP4Labels:
3553 continue
3555 labelDetails = p4CmdList(['label', "-o", name])[0]
3557 # get the most recent changelist for each file in this label
3558 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3559 for p in self.depotPaths])
3561 if 'change' in change:
3562 # find the corresponding git commit; take the oldest commit
3563 changelist = int(change['change'])
3564 if changelist in self.committedChanges:
3565 gitCommit = ":%d" % changelist # use a fast-import mark
3566 commitFound = True
3567 else:
3568 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3569 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3570 if len(gitCommit) == 0:
3571 print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3572 else:
3573 commitFound = True
3574 gitCommit = gitCommit.strip()
3576 if commitFound:
3577 # Convert from p4 time format
3578 try:
3579 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3580 except ValueError:
3581 print("Could not convert label time %s" % labelDetails['Update'])
3582 tmwhen = 1
3584 when = int(time.mktime(tmwhen))
3585 self.streamTag(stream, name, labelDetails, gitCommit, when)
3586 if verbose:
3587 print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3588 else:
3589 if verbose:
3590 print("Label %s has no changelists - possibly deleted?" % name)
3592 if not commitFound:
3593 # We can't import this label; don't try again as it will get very
3594 # expensive repeatedly fetching all the files for labels that will
3595 # never be imported. If the label is moved in the future, the
3596 # ignore will need to be removed manually.
3597 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3599 def guessProjectName(self):
3600 for p in self.depotPaths:
3601 if p.endswith("/"):
3602 p = p[:-1]
3603 p = p[p.strip().rfind("/") + 1:]
3604 if not p.endswith("/"):
3605 p += "/"
3606 return p
3608 def getBranchMapping(self):
3609 lostAndFoundBranches = set()
3611 user = gitConfig("git-p4.branchUser")
3613 for info in p4CmdList(
3614 ["branches"] + (["-u", user] if len(user) > 0 else [])):
3615 details = p4Cmd(["branch", "-o", info["branch"]])
3616 viewIdx = 0
3617 while "View%s" % viewIdx in details:
3618 paths = details["View%s" % viewIdx].split(" ")
3619 viewIdx = viewIdx + 1
3620 # require standard //depot/foo/... //depot/bar/... mapping
3621 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3622 continue
3623 source = paths[0]
3624 destination = paths[1]
3625 # HACK
3626 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3627 source = source[len(self.depotPaths[0]):-4]
3628 destination = destination[len(self.depotPaths[0]):-4]
3630 if destination in self.knownBranches:
3631 if not self.silent:
3632 print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3633 print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3634 continue
3636 self.knownBranches[destination] = source
3638 lostAndFoundBranches.discard(destination)
3640 if source not in self.knownBranches:
3641 lostAndFoundBranches.add(source)
3643 # Perforce does not strictly require branches to be defined, so we also
3644 # check git config for a branch list.
3646 # Example of branch definition in git config file:
3647 # [git-p4]
3648 # branchList=main:branchA
3649 # branchList=main:branchB
3650 # branchList=branchA:branchC
3651 configBranches = gitConfigList("git-p4.branchList")
3652 for branch in configBranches:
3653 if branch:
3654 source, destination = branch.split(":")
3655 self.knownBranches[destination] = source
3657 lostAndFoundBranches.discard(destination)
3659 if source not in self.knownBranches:
3660 lostAndFoundBranches.add(source)
3662 for branch in lostAndFoundBranches:
3663 self.knownBranches[branch] = branch
3665 def getBranchMappingFromGitBranches(self):
3666 branches = p4BranchesInGit(self.importIntoRemotes)
3667 for branch in branches.keys():
3668 if branch == "master":
3669 branch = "main"
3670 else:
3671 branch = branch[len(self.projectName):]
3672 self.knownBranches[branch] = branch
3674 def updateOptionDict(self, d):
3675 option_keys = {}
3676 if self.keepRepoPath:
3677 option_keys['keepRepoPath'] = 1
3679 d["options"] = ' '.join(sorted(option_keys.keys()))
3681 def readOptions(self, d):
3682 self.keepRepoPath = ('options' in d
3683 and ('keepRepoPath' in d['options']))
3685 def gitRefForBranch(self, branch):
3686 if branch == "main":
3687 return self.refPrefix + "master"
3689 if len(branch) <= 0:
3690 return branch
3692 return self.refPrefix + self.projectName + branch
3694 def gitCommitByP4Change(self, ref, change):
3695 if self.verbose:
3696 print("looking in ref " + ref + " for change %s using bisect..." % change)
3698 earliestCommit = ""
3699 latestCommit = parseRevision(ref)
3701 while True:
3702 if self.verbose:
3703 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3704 next = read_pipe(["git", "rev-list", "--bisect",
3705 latestCommit, earliestCommit]).strip()
3706 if len(next) == 0:
3707 if self.verbose:
3708 print("argh")
3709 return ""
3710 log = extractLogMessageFromGitCommit(next)
3711 settings = extractSettingsGitLog(log)
3712 currentChange = int(settings['change'])
3713 if self.verbose:
3714 print("current change %s" % currentChange)
3716 if currentChange == change:
3717 if self.verbose:
3718 print("found %s" % next)
3719 return next
3721 if currentChange < change:
3722 earliestCommit = "^%s" % next
3723 else:
3724 if next == latestCommit:
3725 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3726 latestCommit = "%s^@" % next
3728 return ""
3730 def importNewBranch(self, branch, maxChange):
3731 # make fast-import flush all changes to disk and update the refs using the checkpoint
3732 # command so that we can try to find the branch parent in the git history
3733 self.gitStream.write("checkpoint\n\n")
3734 self.gitStream.flush()
3735 branchPrefix = self.depotPaths[0] + branch + "/"
3736 range = "@1,%s" % maxChange
3737 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3738 if len(changes) <= 0:
3739 return False
3740 firstChange = changes[0]
3741 sourceBranch = self.knownBranches[branch]
3742 sourceDepotPath = self.depotPaths[0] + sourceBranch
3743 sourceRef = self.gitRefForBranch(sourceBranch)
3745 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3746 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3747 if len(gitParent) > 0:
3748 self.initialParents[self.gitRefForBranch(branch)] = gitParent
3750 self.importChanges(changes)
3751 return True
3753 def searchParent(self, parent, branch, target):
3754 targetTree = read_pipe(["git", "rev-parse",
3755 "{}^{{tree}}".format(target)]).strip()
3756 for line in read_pipe_lines(["git", "rev-list", "--format=%H %T",
3757 "--no-merges", parent]):
3758 if line.startswith("commit "):
3759 continue
3760 commit, tree = line.strip().split(" ")
3761 if tree == targetTree:
3762 if self.verbose:
3763 print("Found parent of %s in commit %s" % (branch, commit))
3764 return commit
3765 return None
3767 def importChanges(self, changes, origin_revision=0):
3768 cnt = 1
3769 for change in changes:
3770 description = p4_describe(change)
3771 self.updateOptionDict(description)
3773 if not self.silent:
3774 sys.stdout.write("\rImporting revision %s (%d%%)" % (
3775 change, (cnt * 100) // len(changes)))
3776 sys.stdout.flush()
3777 cnt = cnt + 1
3779 try:
3780 if self.detectBranches:
3781 branches = self.splitFilesIntoBranches(description)
3782 for branch in branches.keys():
3783 # HACK --hwn
3784 branchPrefix = self.depotPaths[0] + branch + "/"
3785 self.branchPrefixes = [branchPrefix]
3787 parent = ""
3789 filesForCommit = branches[branch]
3791 if self.verbose:
3792 print("branch is %s" % branch)
3794 self.updatedBranches.add(branch)
3796 if branch not in self.createdBranches:
3797 self.createdBranches.add(branch)
3798 parent = self.knownBranches[branch]
3799 if parent == branch:
3800 parent = ""
3801 else:
3802 fullBranch = self.projectName + branch
3803 if fullBranch not in self.p4BranchesInGit:
3804 if not self.silent:
3805 print("\n Importing new branch %s" % fullBranch)
3806 if self.importNewBranch(branch, change - 1):
3807 parent = ""
3808 self.p4BranchesInGit.append(fullBranch)
3809 if not self.silent:
3810 print("\n Resuming with change %s" % change)
3812 if self.verbose:
3813 print("parent determined through known branches: %s" % parent)
3815 branch = self.gitRefForBranch(branch)
3816 parent = self.gitRefForBranch(parent)
3818 if self.verbose:
3819 print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3821 if len(parent) == 0 and branch in self.initialParents:
3822 parent = self.initialParents[branch]
3823 del self.initialParents[branch]
3825 blob = None
3826 if len(parent) > 0:
3827 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3828 if self.verbose:
3829 print("Creating temporary branch: " + tempBranch)
3830 self.commit(description, filesForCommit, tempBranch)
3831 self.tempBranches.append(tempBranch)
3832 self.checkpoint()
3833 blob = self.searchParent(parent, branch, tempBranch)
3834 if blob:
3835 self.commit(description, filesForCommit, branch, blob)
3836 else:
3837 if self.verbose:
3838 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3839 self.commit(description, filesForCommit, branch, parent)
3840 else:
3841 files = self.extractFilesFromCommit(description)
3842 self.commit(description, files, self.branch,
3843 self.initialParent)
3844 # only needed once, to connect to the previous commit
3845 self.initialParent = ""
3846 except IOError:
3847 print(self.gitError.read())
3848 sys.exit(1)
3850 def sync_origin_only(self):
3851 if self.syncWithOrigin:
3852 self.hasOrigin = originP4BranchesExist()
3853 if self.hasOrigin:
3854 if not self.silent:
3855 print('Syncing with origin first, using "git fetch origin"')
3856 system(["git", "fetch", "origin"])
3858 def importHeadRevision(self, revision):
3859 print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3861 details = {}
3862 details["user"] = "git perforce import user"
3863 details["desc"] = ("Initial import of %s from the state at revision %s\n"
3864 % (' '.join(self.depotPaths), revision))
3865 details["change"] = revision
3866 newestRevision = 0
3868 fileCnt = 0
3869 fileArgs = ["%s...%s" % (p, revision) for p in self.depotPaths]
3871 for info in p4CmdList(["files"] + fileArgs):
3873 if 'code' in info and info['code'] == 'error':
3874 sys.stderr.write("p4 returned an error: %s\n"
3875 % info['data'])
3876 if info['data'].find("must refer to client") >= 0:
3877 sys.stderr.write("This particular p4 error is misleading.\n")
3878 sys.stderr.write("Perhaps the depot path was misspelled.\n")
3879 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
3880 sys.exit(1)
3881 if 'p4ExitCode' in info:
3882 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3883 sys.exit(1)
3885 change = int(info["change"])
3886 if change > newestRevision:
3887 newestRevision = change
3889 if info["action"] in self.delete_actions:
3890 continue
3892 for prop in ["depotFile", "rev", "action", "type"]:
3893 details["%s%s" % (prop, fileCnt)] = info[prop]
3895 fileCnt = fileCnt + 1
3897 details["change"] = newestRevision
3899 # Use time from top-most change so that all git p4 clones of
3900 # the same p4 repo have the same commit SHA1s.
3901 res = p4_describe(newestRevision)
3902 details["time"] = res["time"]
3904 self.updateOptionDict(details)
3905 try:
3906 self.commit(details, self.extractFilesFromCommit(details), self.branch)
3907 except IOError as err:
3908 print("IO error with git fast-import. Is your git version recent enough?")
3909 print("IO error details: {}".format(err))
3910 print(self.gitError.read())
3912 def importRevisions(self, args, branch_arg_given):
3913 changes = []
3915 if len(self.changesFile) > 0:
3916 with open(self.changesFile) as f:
3917 output = f.readlines()
3918 changeSet = set()
3919 for line in output:
3920 changeSet.add(int(line))
3922 for change in changeSet:
3923 changes.append(change)
3925 changes.sort()
3926 else:
3927 # catch "git p4 sync" with no new branches, in a repo that
3928 # does not have any existing p4 branches
3929 if len(args) == 0:
3930 if not self.p4BranchesInGit:
3931 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3933 # The default branch is master, unless --branch is used to
3934 # specify something else. Make sure it exists, or complain
3935 # nicely about how to use --branch.
3936 if not self.detectBranches:
3937 if not branch_exists(self.branch):
3938 if branch_arg_given:
3939 raise P4CommandException("Error: branch %s does not exist." % self.branch)
3940 else:
3941 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
3942 self.branch)
3944 if self.verbose:
3945 print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3946 self.changeRange))
3947 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3949 if len(self.maxChanges) > 0:
3950 changes = changes[:min(int(self.maxChanges), len(changes))]
3952 if len(changes) == 0:
3953 if not self.silent:
3954 print("No changes to import!")
3955 else:
3956 if not self.silent and not self.detectBranches:
3957 print("Import destination: %s" % self.branch)
3959 self.updatedBranches = set()
3961 if not self.detectBranches:
3962 if args:
3963 # start a new branch
3964 self.initialParent = ""
3965 else:
3966 # build on a previous revision
3967 self.initialParent = parseRevision(self.branch)
3969 self.importChanges(changes)
3971 if not self.silent:
3972 print("")
3973 if len(self.updatedBranches) > 0:
3974 sys.stdout.write("Updated branches: ")
3975 for b in self.updatedBranches:
3976 sys.stdout.write("%s " % b)
3977 sys.stdout.write("\n")
3979 def openStreams(self):
3980 self.importProcess = subprocess.Popen(["git", "fast-import"],
3981 stdin=subprocess.PIPE,
3982 stdout=subprocess.PIPE,
3983 stderr=subprocess.PIPE)
3984 self.gitOutput = self.importProcess.stdout
3985 self.gitStream = self.importProcess.stdin
3986 self.gitError = self.importProcess.stderr
3988 if bytes is not str:
3989 # Wrap gitStream.write() so that it can be called using `str` arguments
3990 def make_encoded_write(write):
3991 def encoded_write(s):
3992 return write(s.encode() if isinstance(s, str) else s)
3993 return encoded_write
3995 self.gitStream.write = make_encoded_write(self.gitStream.write)
3997 def closeStreams(self):
3998 if self.gitStream is None:
3999 return
4000 self.gitStream.close()
4001 if self.importProcess.wait() != 0:
4002 die("fast-import failed: %s" % self.gitError.read())
4003 self.gitOutput.close()
4004 self.gitError.close()
4005 self.gitStream = None
4007 def run(self, args):
4008 if self.importIntoRemotes:
4009 self.refPrefix = "refs/remotes/p4/"
4010 else:
4011 self.refPrefix = "refs/heads/p4/"
4013 self.sync_origin_only()
4015 branch_arg_given = bool(self.branch)
4016 if len(self.branch) == 0:
4017 self.branch = self.refPrefix + "master"
4018 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
4019 system(["git", "update-ref", self.branch, "refs/heads/p4"])
4020 system(["git", "branch", "-D", "p4"])
4022 # accept either the command-line option, or the configuration variable
4023 if self.useClientSpec:
4024 # will use this after clone to set the variable
4025 self.useClientSpec_from_options = True
4026 else:
4027 if gitConfigBool("git-p4.useclientspec"):
4028 self.useClientSpec = True
4029 if self.useClientSpec:
4030 self.clientSpecDirs = getClientSpec()
4032 # TODO: should always look at previous commits,
4033 # merge with previous imports, if possible.
4034 if args == []:
4035 if self.hasOrigin:
4036 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
4038 # branches holds mapping from branch name to sha1
4039 branches = p4BranchesInGit(self.importIntoRemotes)
4041 # restrict to just this one, disabling detect-branches
4042 if branch_arg_given:
4043 short = shortP4Ref(self.branch, self.importIntoRemotes)
4044 if short in branches:
4045 self.p4BranchesInGit = [short]
4046 elif self.branch.startswith('refs/') and \
4047 branchExists(self.branch) and \
4048 '[git-p4:' in extractLogMessageFromGitCommit(self.branch):
4049 self.p4BranchesInGit = [self.branch]
4050 else:
4051 self.p4BranchesInGit = branches.keys()
4053 if len(self.p4BranchesInGit) > 1:
4054 if not self.silent:
4055 print("Importing from/into multiple branches")
4056 self.detectBranches = True
4057 for branch in branches.keys():
4058 self.initialParents[self.refPrefix + branch] = \
4059 branches[branch]
4061 if self.verbose:
4062 print("branches: %s" % self.p4BranchesInGit)
4064 p4Change = 0
4065 for branch in self.p4BranchesInGit:
4066 logMsg = extractLogMessageFromGitCommit(fullP4Ref(branch,
4067 self.importIntoRemotes))
4069 settings = extractSettingsGitLog(logMsg)
4071 self.readOptions(settings)
4072 if 'depot-paths' in settings and 'change' in settings:
4073 change = int(settings['change']) + 1
4074 p4Change = max(p4Change, change)
4076 depotPaths = sorted(settings['depot-paths'])
4077 if self.previousDepotPaths == []:
4078 self.previousDepotPaths = depotPaths
4079 else:
4080 paths = []
4081 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
4082 prev_list = prev.split("/")
4083 cur_list = cur.split("/")
4084 for i in range(0, min(len(cur_list), len(prev_list))):
4085 if cur_list[i] != prev_list[i]:
4086 i = i - 1
4087 break
4089 paths.append("/".join(cur_list[:i + 1]))
4091 self.previousDepotPaths = paths
4093 if p4Change > 0:
4094 self.depotPaths = sorted(self.previousDepotPaths)
4095 self.changeRange = "@%s,#head" % p4Change
4096 if not self.silent and not self.detectBranches:
4097 print("Performing incremental import into %s git branch" % self.branch)
4099 self.branch = fullP4Ref(self.branch, self.importIntoRemotes)
4101 if len(args) == 0 and self.depotPaths:
4102 if not self.silent:
4103 print("Depot paths: %s" % ' '.join(self.depotPaths))
4104 else:
4105 if self.depotPaths and self.depotPaths != args:
4106 print("previous import used depot path %s and now %s was specified. "
4107 "This doesn't work!" % (' '.join(self.depotPaths),
4108 ' '.join(args)))
4109 sys.exit(1)
4111 self.depotPaths = sorted(args)
4113 revision = ""
4114 self.users = {}
4116 # Make sure no revision specifiers are used when --changesfile
4117 # is specified.
4118 bad_changesfile = False
4119 if len(self.changesFile) > 0:
4120 for p in self.depotPaths:
4121 if p.find("@") >= 0 or p.find("#") >= 0:
4122 bad_changesfile = True
4123 break
4124 if bad_changesfile:
4125 die("Option --changesfile is incompatible with revision specifiers")
4127 newPaths = []
4128 for p in self.depotPaths:
4129 if p.find("@") != -1:
4130 atIdx = p.index("@")
4131 self.changeRange = p[atIdx:]
4132 if self.changeRange == "@all":
4133 self.changeRange = ""
4134 elif ',' not in self.changeRange:
4135 revision = self.changeRange
4136 self.changeRange = ""
4137 p = p[:atIdx]
4138 elif p.find("#") != -1:
4139 hashIdx = p.index("#")
4140 revision = p[hashIdx:]
4141 p = p[:hashIdx]
4142 elif self.previousDepotPaths == []:
4143 # pay attention to changesfile, if given, else import
4144 # the entire p4 tree at the head revision
4145 if len(self.changesFile) == 0:
4146 revision = "#head"
4148 p = re.sub("\.\.\.$", "", p)
4149 if not p.endswith("/"):
4150 p += "/"
4152 newPaths.append(p)
4154 self.depotPaths = newPaths
4156 # --detect-branches may change this for each branch
4157 self.branchPrefixes = self.depotPaths
4159 self.loadUserMapFromCache()
4160 self.labels = {}
4161 if self.detectLabels:
4162 self.getLabels()
4164 if self.detectBranches:
4165 # FIXME - what's a P4 projectName ?
4166 self.projectName = self.guessProjectName()
4168 if self.hasOrigin:
4169 self.getBranchMappingFromGitBranches()
4170 else:
4171 self.getBranchMapping()
4172 if self.verbose:
4173 print("p4-git branches: %s" % self.p4BranchesInGit)
4174 print("initial parents: %s" % self.initialParents)
4175 for b in self.p4BranchesInGit:
4176 if b != "master":
4178 # FIXME
4179 b = b[len(self.projectName):]
4180 self.createdBranches.add(b)
4182 p4_check_access()
4184 self.openStreams()
4186 err = None
4188 try:
4189 if revision:
4190 self.importHeadRevision(revision)
4191 else:
4192 self.importRevisions(args, branch_arg_given)
4194 if gitConfigBool("git-p4.importLabels"):
4195 self.importLabels = True
4197 if self.importLabels:
4198 p4Labels = getP4Labels(self.depotPaths)
4199 gitTags = getGitTags()
4201 missingP4Labels = p4Labels - gitTags
4202 self.importP4Labels(self.gitStream, missingP4Labels)
4204 except P4CommandException as e:
4205 err = e
4207 finally:
4208 self.closeStreams()
4210 if err:
4211 die(str(err))
4213 # Cleanup temporary branches created during import
4214 if self.tempBranches != []:
4215 for branch in self.tempBranches:
4216 read_pipe(["git", "update-ref", "-d", branch])
4217 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
4219 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4220 # a convenient shortcut refname "p4".
4221 if self.importIntoRemotes:
4222 head_ref = self.refPrefix + "HEAD"
4223 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
4224 system(["git", "symbolic-ref", head_ref, self.branch])
4226 return True
4229 class P4Rebase(Command):
4230 def __init__(self):
4231 Command.__init__(self)
4232 self.options = [
4233 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
4235 self.importLabels = False
4236 self.description = ("Fetches the latest revision from perforce and "
4237 + "rebases the current work (branch) against it")
4239 def run(self, args):
4240 sync = P4Sync()
4241 sync.importLabels = self.importLabels
4242 sync.run([])
4244 return self.rebase()
4246 def rebase(self):
4247 if os.system("git update-index --refresh") != 0:
4248 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.")
4249 if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
4250 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.")
4252 upstream, settings = findUpstreamBranchPoint()
4253 if len(upstream) == 0:
4254 die("Cannot find upstream branchpoint for rebase")
4256 # the branchpoint may be p4/foo~3, so strip off the parent
4257 upstream = re.sub("~[0-9]+$", "", upstream)
4259 print("Rebasing the current branch onto %s" % upstream)
4260 oldHead = read_pipe(["git", "rev-parse", "HEAD"]).strip()
4261 system(["git", "rebase", upstream])
4262 system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead,
4263 "HEAD", "--"])
4264 return True
4267 class P4Clone(P4Sync):
4268 def __init__(self):
4269 P4Sync.__init__(self)
4270 self.description = "Creates a new git repository and imports from Perforce into it"
4271 self.usage = "usage: %prog [options] //depot/path[@revRange]"
4272 self.options += [
4273 optparse.make_option("--destination", dest="cloneDestination",
4274 action='store', default=None,
4275 help="where to leave result of the clone"),
4276 optparse.make_option("--bare", dest="cloneBare",
4277 action="store_true", default=False),
4279 self.cloneDestination = None
4280 self.needsGit = False
4281 self.cloneBare = False
4283 def defaultDestination(self, args):
4284 # TODO: use common prefix of args?
4285 depotPath = args[0]
4286 depotDir = re.sub("(@[^@]*)$", "", depotPath)
4287 depotDir = re.sub("(#[^#]*)$", "", depotDir)
4288 depotDir = re.sub(r"\.\.\.$", "", depotDir)
4289 depotDir = re.sub(r"/$", "", depotDir)
4290 return os.path.split(depotDir)[1]
4292 def run(self, args):
4293 if len(args) < 1:
4294 return False
4296 if self.keepRepoPath and not self.cloneDestination:
4297 sys.stderr.write("Must specify destination for --keep-path\n")
4298 sys.exit(1)
4300 depotPaths = args
4302 if not self.cloneDestination and len(depotPaths) > 1:
4303 self.cloneDestination = depotPaths[-1]
4304 depotPaths = depotPaths[:-1]
4306 for p in depotPaths:
4307 if not p.startswith("//"):
4308 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
4309 return False
4311 if not self.cloneDestination:
4312 self.cloneDestination = self.defaultDestination(args)
4314 print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
4316 if not os.path.exists(self.cloneDestination):
4317 os.makedirs(self.cloneDestination)
4318 chdir(self.cloneDestination)
4320 init_cmd = ["git", "init"]
4321 if self.cloneBare:
4322 init_cmd.append("--bare")
4323 retcode = subprocess.call(init_cmd)
4324 if retcode:
4325 raise subprocess.CalledProcessError(retcode, init_cmd)
4327 if not P4Sync.run(self, depotPaths):
4328 return False
4330 # create a master branch and check out a work tree
4331 if gitBranchExists(self.branch):
4332 system(["git", "branch", currentGitBranch(), self.branch])
4333 if not self.cloneBare:
4334 system(["git", "checkout", "-f"])
4335 else:
4336 print('Not checking out any branch, use '
4337 '"git checkout -q -b master <branch>"')
4339 # auto-set this variable if invoked with --use-client-spec
4340 if self.useClientSpec_from_options:
4341 system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
4343 # persist any git-p4 encoding-handling config options passed in for clone:
4344 if gitConfig('git-p4.metadataDecodingStrategy'):
4345 system(["git", "config", "git-p4.metadataDecodingStrategy", gitConfig('git-p4.metadataDecodingStrategy')])
4346 if gitConfig('git-p4.metadataFallbackEncoding'):
4347 system(["git", "config", "git-p4.metadataFallbackEncoding", gitConfig('git-p4.metadataFallbackEncoding')])
4348 if gitConfig('git-p4.pathEncoding'):
4349 system(["git", "config", "git-p4.pathEncoding", gitConfig('git-p4.pathEncoding')])
4351 return True
4354 class P4Unshelve(Command):
4355 def __init__(self):
4356 Command.__init__(self)
4357 self.options = []
4358 self.origin = "HEAD"
4359 self.description = "Unshelve a P4 changelist into a git commit"
4360 self.usage = "usage: %prog [options] changelist"
4361 self.options += [
4362 optparse.make_option("--origin", dest="origin",
4363 help="Use this base revision instead of the default (%s)" % self.origin),
4365 self.verbose = False
4366 self.noCommit = False
4367 self.destbranch = "refs/remotes/p4-unshelved"
4369 def renameBranch(self, branch_name):
4370 """Rename the existing branch to branch_name.N ."""
4372 found = True
4373 for i in range(0, 1000):
4374 backup_branch_name = "{0}.{1}".format(branch_name, i)
4375 if not gitBranchExists(backup_branch_name):
4376 # Copy ref to backup
4377 gitUpdateRef(backup_branch_name, branch_name)
4378 gitDeleteRef(branch_name)
4379 found = True
4380 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4381 break
4383 if not found:
4384 sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4386 def findLastP4Revision(self, starting_point):
4387 """Look back from starting_point for the first commit created by git-p4
4388 to find the P4 commit we are based on, and the depot-paths.
4391 for parent in (range(65535)):
4392 log = extractLogMessageFromGitCommit("{0}~{1}".format(starting_point, parent))
4393 settings = extractSettingsGitLog(log)
4394 if 'change' in settings:
4395 return settings
4397 sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4399 def createShelveParent(self, change, branch_name, sync, origin):
4400 """Create a commit matching the parent of the shelved changelist
4401 'change'.
4403 parent_description = p4_describe(change, shelved=True)
4404 parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4405 files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4407 parent_files = []
4408 for f in files:
4409 # if it was added in the shelved changelist, it won't exist in the parent
4410 if f['action'] in self.add_actions:
4411 continue
4413 # if it was deleted in the shelved changelist it must not be deleted
4414 # in the parent - we might even need to create it if the origin branch
4415 # does not have it
4416 if f['action'] in self.delete_actions:
4417 f['action'] = 'add'
4419 parent_files.append(f)
4421 sync.commit(parent_description, parent_files, branch_name,
4422 parent=origin, allow_empty=True)
4423 print("created parent commit for {0} based on {1} in {2}".format(
4424 change, self.origin, branch_name))
4426 def run(self, args):
4427 if len(args) != 1:
4428 return False
4430 if not gitBranchExists(self.origin):
4431 sys.exit("origin branch {0} does not exist".format(self.origin))
4433 sync = P4Sync()
4434 changes = args
4436 # only one change at a time
4437 change = changes[0]
4439 # if the target branch already exists, rename it
4440 branch_name = "{0}/{1}".format(self.destbranch, change)
4441 if gitBranchExists(branch_name):
4442 self.renameBranch(branch_name)
4443 sync.branch = branch_name
4445 sync.verbose = self.verbose
4446 sync.suppress_meta_comment = True
4448 settings = self.findLastP4Revision(self.origin)
4449 sync.depotPaths = settings['depot-paths']
4450 sync.branchPrefixes = sync.depotPaths
4452 sync.openStreams()
4453 sync.loadUserMapFromCache()
4454 sync.silent = True
4456 # create a commit for the parent of the shelved changelist
4457 self.createShelveParent(change, branch_name, sync, self.origin)
4459 # create the commit for the shelved changelist itself
4460 description = p4_describe(change, True)
4461 files = sync.extractFilesFromCommit(description, True, change)
4463 sync.commit(description, files, branch_name, "")
4464 sync.closeStreams()
4466 print("unshelved changelist {0} into {1}".format(change, branch_name))
4468 return True
4471 class P4Branches(Command):
4472 def __init__(self):
4473 Command.__init__(self)
4474 self.options = []
4475 self.description = ("Shows the git branches that hold imports and their "
4476 + "corresponding perforce depot paths")
4477 self.verbose = False
4479 def run(self, args):
4480 if originP4BranchesExist():
4481 createOrUpdateBranchesFromOrigin()
4483 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
4484 line = line.strip()
4486 if not line.startswith('p4/') or line == "p4/HEAD":
4487 continue
4488 branch = line
4490 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4491 settings = extractSettingsGitLog(log)
4493 print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4494 return True
4497 class HelpFormatter(optparse.IndentedHelpFormatter):
4498 def __init__(self):
4499 optparse.IndentedHelpFormatter.__init__(self)
4501 def format_description(self, description):
4502 if description:
4503 return description + "\n"
4504 else:
4505 return ""
4508 def printUsage(commands):
4509 print("usage: %s <command> [options]" % sys.argv[0])
4510 print("")
4511 print("valid commands: %s" % ", ".join(commands))
4512 print("")
4513 print("Try %s <command> --help for command specific help." % sys.argv[0])
4514 print("")
4517 commands = {
4518 "submit": P4Submit,
4519 "commit": P4Submit,
4520 "sync": P4Sync,
4521 "rebase": P4Rebase,
4522 "clone": P4Clone,
4523 "branches": P4Branches,
4524 "unshelve": P4Unshelve,
4528 def main():
4529 if len(sys.argv[1:]) == 0:
4530 printUsage(commands.keys())
4531 sys.exit(2)
4533 cmdName = sys.argv[1]
4534 try:
4535 klass = commands[cmdName]
4536 cmd = klass()
4537 except KeyError:
4538 print("unknown command %s" % cmdName)
4539 print("")
4540 printUsage(commands.keys())
4541 sys.exit(2)
4543 options = cmd.options
4544 cmd.gitdir = os.environ.get("GIT_DIR", None)
4546 args = sys.argv[2:]
4548 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4549 if cmd.needsGit:
4550 options.append(optparse.make_option("--git-dir", dest="gitdir"))
4552 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4553 options,
4554 description=cmd.description,
4555 formatter=HelpFormatter())
4557 try:
4558 cmd, args = parser.parse_args(sys.argv[2:], cmd)
4559 except:
4560 parser.print_help()
4561 raise
4563 global verbose
4564 verbose = cmd.verbose
4565 if cmd.needsGit:
4566 if cmd.gitdir is None:
4567 cmd.gitdir = os.path.abspath(".git")
4568 if not isValidGitDir(cmd.gitdir):
4569 # "rev-parse --git-dir" without arguments will try $PWD/.git
4570 cmd.gitdir = read_pipe(["git", "rev-parse", "--git-dir"]).strip()
4571 if os.path.exists(cmd.gitdir):
4572 cdup = read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
4573 if len(cdup) > 0:
4574 chdir(cdup)
4576 if not isValidGitDir(cmd.gitdir):
4577 if isValidGitDir(cmd.gitdir + "/.git"):
4578 cmd.gitdir += "/.git"
4579 else:
4580 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4582 # so git commands invoked from the P4 workspace will succeed
4583 os.environ["GIT_DIR"] = cmd.gitdir
4585 if not cmd.run(args):
4586 parser.print_help()
4587 sys.exit(2)
4590 if __name__ == '__main__':
4591 main()