Merge branch 'ph/diff-src-dst-prefix-config'
[alt-git.git] / git-p4.py
blob28ab12c72b6561bf6f583391acf8daa300a6211b
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(r'^([cku]?)x(.*)', r'\1\2', p4Type)
693 p4Type = re.sub(r'(.*?\+.*?)x(.*?)', r'\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(r".*\((.+)\)( \*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(r':(\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 p4KeysContainingNonUtf8Chars():
826 """Returns all keys which may contain non UTF-8 encoded strings
827 for which a fallback strategy has to be applied.
829 return ['desc', 'client', 'FullName']
832 def p4KeysContainingBinaryData():
833 """Returns all keys which may contain arbitrary binary data
835 return ['data']
838 def p4KeyContainsFilePaths(key):
839 """Returns True if the key contains file paths. These are handled by decode_path().
840 Otherwise False.
842 return key.startswith('depotFile') or key in ['path', 'clientFile']
845 def p4KeyWhichCanBeDirectlyDecoded(key):
846 """Returns True if the key can be directly decoded as UTF-8 string
847 Otherwise False.
849 Keys which can not be encoded directly:
850 - `data` which may contain arbitrary binary data
851 - `desc` or `client` or `FullName` which may contain non-UTF8 encoded text
852 - `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text, handled by decode_path()
854 if key in p4KeysContainingNonUtf8Chars() or \
855 key in p4KeysContainingBinaryData() or \
856 p4KeyContainsFilePaths(key):
857 return False
858 return True
861 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
862 errors_as_exceptions=False, *k, **kw):
864 cmd = p4_build_cmd(["-G"] + cmd)
865 if verbose:
866 sys.stderr.write("Opening pipe: {}\n".format(' '.join(cmd)))
868 # Use a temporary file to avoid deadlocks without
869 # subprocess.communicate(), which would put another copy
870 # of stdout into memory.
871 stdin_file = None
872 if stdin is not None:
873 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
874 if not isinstance(stdin, list):
875 stdin_file.write(stdin)
876 else:
877 for i in stdin:
878 stdin_file.write(encode_text_stream(i))
879 stdin_file.write(b'\n')
880 stdin_file.flush()
881 stdin_file.seek(0)
883 p4 = subprocess.Popen(
884 cmd, stdin=stdin_file, stdout=subprocess.PIPE, *k, **kw)
886 result = []
887 try:
888 while True:
889 entry = marshal.load(p4.stdout)
891 if bytes is not str:
892 # Decode unmarshalled dict to use str keys and values. Special cases are handled below.
893 decoded_entry = {}
894 for key, value in entry.items():
895 key = key.decode()
896 if isinstance(value, bytes) and p4KeyWhichCanBeDirectlyDecoded(key):
897 value = value.decode()
898 decoded_entry[key] = value
899 # Parse out data if it's an error response
900 if decoded_entry.get('code') == 'error' and 'data' in decoded_entry:
901 decoded_entry['data'] = decoded_entry['data'].decode()
902 entry = decoded_entry
903 if skip_info:
904 if 'code' in entry and entry['code'] == 'info':
905 continue
906 for key in p4KeysContainingNonUtf8Chars():
907 if key in entry:
908 entry[key] = metadata_stream_to_writable_bytes(entry[key])
909 if cb is not None:
910 cb(entry)
911 else:
912 result.append(entry)
913 except EOFError:
914 pass
915 exitCode = p4.wait()
916 if exitCode != 0:
917 if errors_as_exceptions:
918 if len(result) > 0:
919 data = result[0].get('data')
920 if data:
921 m = re.search(r'Too many rows scanned \(over (\d+)\)', data)
922 if not m:
923 m = re.search(r'Request too large \(over (\d+)\)', data)
925 if m:
926 limit = int(m.group(1))
927 raise P4RequestSizeException(exitCode, result, limit)
929 raise P4ServerException(exitCode, result)
930 else:
931 raise P4Exception(exitCode)
932 else:
933 entry = {}
934 entry["p4ExitCode"] = exitCode
935 result.append(entry)
937 return result
940 def p4Cmd(cmd, *k, **kw):
941 list = p4CmdList(cmd, *k, **kw)
942 result = {}
943 for entry in list:
944 result.update(entry)
945 return result
948 def p4Where(depotPath):
949 if not depotPath.endswith("/"):
950 depotPath += "/"
951 depotPathLong = depotPath + "..."
952 outputList = p4CmdList(["where", depotPathLong])
953 output = None
954 for entry in outputList:
955 if "depotFile" in entry:
956 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
957 # The base path always ends with "/...".
958 entry_path = decode_path(entry['depotFile'])
959 if entry_path.find(depotPath) == 0 and entry_path[-4:] == "/...":
960 output = entry
961 break
962 elif "data" in entry:
963 data = entry.get("data")
964 space = data.find(" ")
965 if data[:space] == depotPath:
966 output = entry
967 break
968 if output is None:
969 return ""
970 if output["code"] == "error":
971 return ""
972 clientPath = ""
973 if "path" in output:
974 clientPath = decode_path(output['path'])
975 elif "data" in output:
976 data = output.get("data")
977 lastSpace = data.rfind(b" ")
978 clientPath = decode_path(data[lastSpace + 1:])
980 if clientPath.endswith("..."):
981 clientPath = clientPath[:-3]
982 return clientPath
985 def currentGitBranch():
986 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
989 def isValidGitDir(path):
990 return git_dir(path) is not None
993 def parseRevision(ref):
994 return read_pipe(["git", "rev-parse", ref]).strip()
997 def branchExists(ref):
998 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
999 ignore_error=True)
1000 return len(rev) > 0
1003 def extractLogMessageFromGitCommit(commit):
1004 logMessage = ""
1006 # fixme: title is first line of commit, not 1st paragraph.
1007 foundTitle = False
1008 for log in read_pipe_lines(["git", "cat-file", "commit", commit]):
1009 if not foundTitle:
1010 if len(log) == 1:
1011 foundTitle = True
1012 continue
1014 logMessage += log
1015 return logMessage
1018 def extractSettingsGitLog(log):
1019 values = {}
1020 for line in log.split("\n"):
1021 line = line.strip()
1022 m = re.search(r"^ *\[git-p4: (.*)\]$", line)
1023 if not m:
1024 continue
1026 assignments = m.group(1).split(':')
1027 for a in assignments:
1028 vals = a.split('=')
1029 key = vals[0].strip()
1030 val = ('='.join(vals[1:])).strip()
1031 if val.endswith('\"') and val.startswith('"'):
1032 val = val[1:-1]
1034 values[key] = val
1036 paths = values.get("depot-paths")
1037 if not paths:
1038 paths = values.get("depot-path")
1039 if paths:
1040 values['depot-paths'] = paths.split(',')
1041 return values
1044 def gitBranchExists(branch):
1045 proc = subprocess.Popen(["git", "rev-parse", branch],
1046 stderr=subprocess.PIPE, stdout=subprocess.PIPE)
1047 return proc.wait() == 0
1050 def gitUpdateRef(ref, newvalue):
1051 subprocess.check_call(["git", "update-ref", ref, newvalue])
1054 def gitDeleteRef(ref):
1055 subprocess.check_call(["git", "update-ref", "-d", ref])
1058 _gitConfig = {}
1061 def gitConfig(key, typeSpecifier=None):
1062 if key not in _gitConfig:
1063 cmd = ["git", "config"]
1064 if typeSpecifier:
1065 cmd += [typeSpecifier]
1066 cmd += [key]
1067 s = read_pipe(cmd, ignore_error=True)
1068 _gitConfig[key] = s.strip()
1069 return _gitConfig[key]
1072 def gitConfigBool(key):
1073 """Return a bool, using git config --bool. It is True only if the
1074 variable is set to true, and False if set to false or not present
1075 in the config.
1078 if key not in _gitConfig:
1079 _gitConfig[key] = gitConfig(key, '--bool') == "true"
1080 return _gitConfig[key]
1083 def gitConfigInt(key):
1084 if key not in _gitConfig:
1085 cmd = ["git", "config", "--int", key]
1086 s = read_pipe(cmd, ignore_error=True)
1087 v = s.strip()
1088 try:
1089 _gitConfig[key] = int(gitConfig(key, '--int'))
1090 except ValueError:
1091 _gitConfig[key] = None
1092 return _gitConfig[key]
1095 def gitConfigList(key):
1096 if key not in _gitConfig:
1097 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
1098 _gitConfig[key] = s.strip().splitlines()
1099 if _gitConfig[key] == ['']:
1100 _gitConfig[key] = []
1101 return _gitConfig[key]
1103 def fullP4Ref(incomingRef, importIntoRemotes=True):
1104 """Standardize a given provided p4 ref value to a full git ref:
1105 refs/foo/bar/branch -> use it exactly
1106 p4/branch -> prepend refs/remotes/ or refs/heads/
1107 branch -> prepend refs/remotes/p4/ or refs/heads/p4/"""
1108 if incomingRef.startswith("refs/"):
1109 return incomingRef
1110 if importIntoRemotes:
1111 prepend = "refs/remotes/"
1112 else:
1113 prepend = "refs/heads/"
1114 if not incomingRef.startswith("p4/"):
1115 prepend += "p4/"
1116 return prepend + incomingRef
1118 def shortP4Ref(incomingRef, importIntoRemotes=True):
1119 """Standardize to a "short ref" if possible:
1120 refs/foo/bar/branch -> ignore
1121 refs/remotes/p4/branch or refs/heads/p4/branch -> shorten
1122 p4/branch -> shorten"""
1123 if importIntoRemotes:
1124 longprefix = "refs/remotes/p4/"
1125 else:
1126 longprefix = "refs/heads/p4/"
1127 if incomingRef.startswith(longprefix):
1128 return incomingRef[len(longprefix):]
1129 if incomingRef.startswith("p4/"):
1130 return incomingRef[3:]
1131 return incomingRef
1133 def p4BranchesInGit(branchesAreInRemotes=True):
1134 """Find all the branches whose names start with "p4/", looking
1135 in remotes or heads as specified by the argument. Return
1136 a dictionary of { branch: revision } for each one found.
1137 The branch names are the short names, without any
1138 "p4/" prefix.
1141 branches = {}
1143 cmdline = ["git", "rev-parse", "--symbolic"]
1144 if branchesAreInRemotes:
1145 cmdline.append("--remotes")
1146 else:
1147 cmdline.append("--branches")
1149 for line in read_pipe_lines(cmdline):
1150 line = line.strip()
1152 # only import to p4/
1153 if not line.startswith('p4/'):
1154 continue
1155 # special symbolic ref to p4/master
1156 if line == "p4/HEAD":
1157 continue
1159 # strip off p4/ prefix
1160 branch = line[len("p4/"):]
1162 branches[branch] = parseRevision(line)
1164 return branches
1167 def branch_exists(branch):
1168 """Make sure that the given ref name really exists."""
1170 cmd = ["git", "rev-parse", "--symbolic", "--verify", branch]
1171 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1172 out, _ = p.communicate()
1173 out = decode_text_stream(out)
1174 if p.returncode:
1175 return False
1176 # expect exactly one line of output: the branch name
1177 return out.rstrip() == branch
1180 def findUpstreamBranchPoint(head="HEAD"):
1181 branches = p4BranchesInGit()
1182 # map from depot-path to branch name
1183 branchByDepotPath = {}
1184 for branch in branches.keys():
1185 tip = branches[branch]
1186 log = extractLogMessageFromGitCommit(tip)
1187 settings = extractSettingsGitLog(log)
1188 if "depot-paths" in settings:
1189 git_branch = "remotes/p4/" + branch
1190 paths = ",".join(settings["depot-paths"])
1191 branchByDepotPath[paths] = git_branch
1192 if "change" in settings:
1193 paths = paths + ";" + settings["change"]
1194 branchByDepotPath[paths] = git_branch
1196 settings = None
1197 parent = 0
1198 while parent < 65535:
1199 commit = head + "~%s" % parent
1200 log = extractLogMessageFromGitCommit(commit)
1201 settings = extractSettingsGitLog(log)
1202 if "depot-paths" in settings:
1203 paths = ",".join(settings["depot-paths"])
1204 if "change" in settings:
1205 expaths = paths + ";" + settings["change"]
1206 if expaths in branchByDepotPath:
1207 return [branchByDepotPath[expaths], settings]
1208 if paths in branchByDepotPath:
1209 return [branchByDepotPath[paths], settings]
1211 parent = parent + 1
1213 return ["", settings]
1216 def createOrUpdateBranchesFromOrigin(localRefPrefix="refs/remotes/p4/", silent=True):
1217 if not silent:
1218 print("Creating/updating branch(es) in %s based on origin branch(es)"
1219 % localRefPrefix)
1221 originPrefix = "origin/p4/"
1223 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
1224 line = line.strip()
1225 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
1226 continue
1228 headName = line[len(originPrefix):]
1229 remoteHead = localRefPrefix + headName
1230 originHead = line
1232 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
1233 if 'depot-paths' not in original or 'change' not in original:
1234 continue
1236 update = False
1237 if not gitBranchExists(remoteHead):
1238 if verbose:
1239 print("creating %s" % remoteHead)
1240 update = True
1241 else:
1242 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
1243 if 'change' in settings:
1244 if settings['depot-paths'] == original['depot-paths']:
1245 originP4Change = int(original['change'])
1246 p4Change = int(settings['change'])
1247 if originP4Change > p4Change:
1248 print("%s (%s) is newer than %s (%s). "
1249 "Updating p4 branch from origin."
1250 % (originHead, originP4Change,
1251 remoteHead, p4Change))
1252 update = True
1253 else:
1254 print("Ignoring: %s was imported from %s while "
1255 "%s was imported from %s"
1256 % (originHead, ','.join(original['depot-paths']),
1257 remoteHead, ','.join(settings['depot-paths'])))
1259 if update:
1260 system(["git", "update-ref", remoteHead, originHead])
1263 def originP4BranchesExist():
1264 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
1267 def p4ParseNumericChangeRange(parts):
1268 changeStart = int(parts[0][1:])
1269 if parts[1] == '#head':
1270 changeEnd = p4_last_change()
1271 else:
1272 changeEnd = int(parts[1])
1274 return (changeStart, changeEnd)
1277 def chooseBlockSize(blockSize):
1278 if blockSize:
1279 return blockSize
1280 else:
1281 return defaultBlockSize
1284 def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
1285 assert depotPaths
1287 # Parse the change range into start and end. Try to find integer
1288 # revision ranges as these can be broken up into blocks to avoid
1289 # hitting server-side limits (maxrows, maxscanresults). But if
1290 # that doesn't work, fall back to using the raw revision specifier
1291 # strings, without using block mode.
1293 if changeRange is None or changeRange == '':
1294 changeStart = 1
1295 changeEnd = p4_last_change()
1296 block_size = chooseBlockSize(requestedBlockSize)
1297 else:
1298 parts = changeRange.split(',')
1299 assert len(parts) == 2
1300 try:
1301 changeStart, changeEnd = p4ParseNumericChangeRange(parts)
1302 block_size = chooseBlockSize(requestedBlockSize)
1303 except ValueError:
1304 changeStart = parts[0][1:]
1305 changeEnd = parts[1]
1306 if requestedBlockSize:
1307 die("cannot use --changes-block-size with non-numeric revisions")
1308 block_size = None
1310 changes = set()
1312 # Retrieve changes a block at a time, to prevent running
1313 # into a MaxResults/MaxScanRows error from the server. If
1314 # we _do_ hit one of those errors, turn down the block size
1316 while True:
1317 cmd = ['changes']
1319 if block_size:
1320 end = min(changeEnd, changeStart + block_size)
1321 revisionRange = "%d,%d" % (changeStart, end)
1322 else:
1323 revisionRange = "%s,%s" % (changeStart, changeEnd)
1325 for p in depotPaths:
1326 cmd += ["%s...@%s" % (p, revisionRange)]
1328 # fetch the changes
1329 try:
1330 result = p4CmdList(cmd, errors_as_exceptions=True)
1331 except P4RequestSizeException as e:
1332 if not block_size:
1333 block_size = e.limit
1334 elif block_size > e.limit:
1335 block_size = e.limit
1336 else:
1337 block_size = max(2, block_size // 2)
1339 if verbose:
1340 print("block size error, retrying with block size {0}".format(block_size))
1341 continue
1342 except P4Exception as e:
1343 die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1345 # Insert changes in chronological order
1346 for entry in reversed(result):
1347 if 'change' not in entry:
1348 continue
1349 changes.add(int(entry['change']))
1351 if not block_size:
1352 break
1354 if end >= changeEnd:
1355 break
1357 changeStart = end + 1
1359 changes = sorted(changes)
1360 return changes
1363 def p4PathStartsWith(path, prefix):
1364 """This method tries to remedy a potential mixed-case issue:
1366 If UserA adds //depot/DirA/file1
1367 and UserB adds //depot/dira/file2
1369 we may or may not have a problem. If you have core.ignorecase=true,
1370 we treat DirA and dira as the same directory.
1372 if gitConfigBool("core.ignorecase"):
1373 return path.lower().startswith(prefix.lower())
1374 return path.startswith(prefix)
1377 def getClientSpec():
1378 """Look at the p4 client spec, create a View() object that contains
1379 all the mappings, and return it.
1382 specList = p4CmdList(["client", "-o"])
1383 if len(specList) != 1:
1384 die('Output from "client -o" is %d lines, expecting 1' %
1385 len(specList))
1387 # dictionary of all client parameters
1388 entry = specList[0]
1390 # the //client/ name
1391 client_name = entry["Client"]
1393 # just the keys that start with "View"
1394 view_keys = [k for k in entry.keys() if k.startswith("View")]
1396 # hold this new View
1397 view = View(client_name)
1399 # append the lines, in order, to the view
1400 for view_num in range(len(view_keys)):
1401 k = "View%d" % view_num
1402 if k not in view_keys:
1403 die("Expected view key %s missing" % k)
1404 view.append(entry[k])
1406 return view
1409 def getClientRoot():
1410 """Grab the client directory."""
1412 output = p4CmdList(["client", "-o"])
1413 if len(output) != 1:
1414 die('Output from "client -o" is %d lines, expecting 1' % len(output))
1416 entry = output[0]
1417 if "Root" not in entry:
1418 die('Client has no "Root"')
1420 return entry["Root"]
1423 def wildcard_decode(path):
1424 """Decode P4 wildcards into %xx encoding
1426 P4 wildcards are not allowed in filenames. P4 complains if you simply
1427 add them, but you can force it with "-f", in which case it translates
1428 them into %xx encoding internally.
1431 # Search for and fix just these four characters. Do % last so
1432 # that fixing it does not inadvertently create new %-escapes.
1433 # Cannot have * in a filename in windows; untested as to
1434 # what p4 would do in such a case.
1435 if not platform.system() == "Windows":
1436 path = path.replace("%2A", "*")
1437 path = path.replace("%23", "#") \
1438 .replace("%40", "@") \
1439 .replace("%25", "%")
1440 return path
1443 def wildcard_encode(path):
1444 """Encode %xx coded wildcards into P4 coding."""
1446 # do % first to avoid double-encoding the %s introduced here
1447 path = path.replace("%", "%25") \
1448 .replace("*", "%2A") \
1449 .replace("#", "%23") \
1450 .replace("@", "%40")
1451 return path
1454 def wildcard_present(path):
1455 m = re.search(r"[*#@%]", path)
1456 return m is not None
1459 class LargeFileSystem(object):
1460 """Base class for large file system support."""
1462 def __init__(self, writeToGitStream):
1463 self.largeFiles = set()
1464 self.writeToGitStream = writeToGitStream
1466 def generatePointer(self, cloneDestination, contentFile):
1467 """Return the content of a pointer file that is stored in Git instead
1468 of the actual content.
1470 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1472 def pushFile(self, localLargeFile):
1473 """Push the actual content which is not stored in the Git repository to
1474 a server.
1476 assert False, "Method 'pushFile' required in " + self.__class__.__name__
1478 def hasLargeFileExtension(self, relPath):
1479 return functools.reduce(
1480 lambda a, b: a or b,
1481 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1482 False
1485 def generateTempFile(self, contents):
1486 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1487 for d in contents:
1488 contentFile.write(d)
1489 contentFile.close()
1490 return contentFile.name
1492 def exceedsLargeFileThreshold(self, relPath, contents):
1493 if gitConfigInt('git-p4.largeFileThreshold'):
1494 contentsSize = sum(len(d) for d in contents)
1495 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1496 return True
1497 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1498 contentsSize = sum(len(d) for d in contents)
1499 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1500 return False
1501 contentTempFile = self.generateTempFile(contents)
1502 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True)
1503 with zipfile.ZipFile(compressedContentFile, mode='w') as zf:
1504 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1505 compressedContentsSize = zf.infolist()[0].compress_size
1506 os.remove(contentTempFile)
1507 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1508 return True
1509 return False
1511 def addLargeFile(self, relPath):
1512 self.largeFiles.add(relPath)
1514 def removeLargeFile(self, relPath):
1515 self.largeFiles.remove(relPath)
1517 def isLargeFile(self, relPath):
1518 return relPath in self.largeFiles
1520 def processContent(self, git_mode, relPath, contents):
1521 """Processes the content of git fast import. This method decides if a
1522 file is stored in the large file system and handles all necessary
1523 steps.
1525 # symlinks aren't processed by smudge/clean filters
1526 if git_mode == "120000":
1527 return (git_mode, contents)
1529 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1530 contentTempFile = self.generateTempFile(contents)
1531 pointer_git_mode, contents, localLargeFile = self.generatePointer(contentTempFile)
1532 if pointer_git_mode:
1533 git_mode = pointer_git_mode
1534 if localLargeFile:
1535 # Move temp file to final location in large file system
1536 largeFileDir = os.path.dirname(localLargeFile)
1537 if not os.path.isdir(largeFileDir):
1538 os.makedirs(largeFileDir)
1539 shutil.move(contentTempFile, localLargeFile)
1540 self.addLargeFile(relPath)
1541 if gitConfigBool('git-p4.largeFilePush'):
1542 self.pushFile(localLargeFile)
1543 if verbose:
1544 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1545 return (git_mode, contents)
1548 class MockLFS(LargeFileSystem):
1549 """Mock large file system for testing."""
1551 def generatePointer(self, contentFile):
1552 """The pointer content is the original content prefixed with "pointer-".
1553 The local filename of the large file storage is derived from the
1554 file content.
1556 with open(contentFile, 'r') as f:
1557 content = next(f)
1558 gitMode = '100644'
1559 pointerContents = 'pointer-' + content
1560 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1561 return (gitMode, pointerContents, localLargeFile)
1563 def pushFile(self, localLargeFile):
1564 """The remote filename of the large file storage is the same as the
1565 local one but in a different directory.
1567 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1568 if not os.path.exists(remotePath):
1569 os.makedirs(remotePath)
1570 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1573 class GitLFS(LargeFileSystem):
1574 """Git LFS as backend for the git-p4 large file system.
1575 See https://git-lfs.github.com/ for details.
1578 def __init__(self, *args):
1579 LargeFileSystem.__init__(self, *args)
1580 self.baseGitAttributes = []
1582 def generatePointer(self, contentFile):
1583 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1584 mode and content which is stored in the Git repository instead of
1585 the actual content. Return also the new location of the actual
1586 content.
1588 if os.path.getsize(contentFile) == 0:
1589 return (None, '', None)
1591 pointerProcess = subprocess.Popen(
1592 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1593 stdout=subprocess.PIPE
1595 pointerFile = decode_text_stream(pointerProcess.stdout.read())
1596 if pointerProcess.wait():
1597 os.remove(contentFile)
1598 die('git-lfs pointer command failed. Did you install the extension?')
1600 # Git LFS removed the preamble in the output of the 'pointer' command
1601 # starting from version 1.2.0. Check for the preamble here to support
1602 # earlier versions.
1603 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1604 if pointerFile.startswith('Git LFS pointer for'):
1605 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1607 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
1608 # if someone use external lfs.storage ( not in local repo git )
1609 lfs_path = gitConfig('lfs.storage')
1610 if not lfs_path:
1611 lfs_path = 'lfs'
1612 if not os.path.isabs(lfs_path):
1613 lfs_path = os.path.join(os.getcwd(), '.git', lfs_path)
1614 localLargeFile = os.path.join(
1615 lfs_path,
1616 'objects', oid[:2], oid[2:4],
1617 oid,
1619 # LFS Spec states that pointer files should not have the executable bit set.
1620 gitMode = '100644'
1621 return (gitMode, pointerFile, localLargeFile)
1623 def pushFile(self, localLargeFile):
1624 uploadProcess = subprocess.Popen(
1625 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1627 if uploadProcess.wait():
1628 die('git-lfs push command failed. Did you define a remote?')
1630 def generateGitAttributes(self):
1631 return (
1632 self.baseGitAttributes +
1634 '\n',
1635 '#\n',
1636 '# Git LFS (see https://git-lfs.github.com/)\n',
1637 '#\n',
1639 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1640 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1642 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1643 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1647 def addLargeFile(self, relPath):
1648 LargeFileSystem.addLargeFile(self, relPath)
1649 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1651 def removeLargeFile(self, relPath):
1652 LargeFileSystem.removeLargeFile(self, relPath)
1653 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1655 def processContent(self, git_mode, relPath, contents):
1656 if relPath == '.gitattributes':
1657 self.baseGitAttributes = contents
1658 return (git_mode, self.generateGitAttributes())
1659 else:
1660 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1663 class Command:
1664 delete_actions = ("delete", "move/delete", "purge")
1665 add_actions = ("add", "branch", "move/add")
1667 def __init__(self):
1668 self.usage = "usage: %prog [options]"
1669 self.needsGit = True
1670 self.verbose = False
1672 # This is required for the "append" update_shelve action
1673 def ensure_value(self, attr, value):
1674 if not hasattr(self, attr) or getattr(self, attr) is None:
1675 setattr(self, attr, value)
1676 return getattr(self, attr)
1679 class P4UserMap:
1680 def __init__(self):
1681 self.userMapFromPerforceServer = False
1682 self.myP4UserId = None
1684 def p4UserId(self):
1685 if self.myP4UserId:
1686 return self.myP4UserId
1688 results = p4CmdList(["user", "-o"])
1689 for r in results:
1690 if 'User' in r:
1691 self.myP4UserId = r['User']
1692 return r['User']
1693 die("Could not find your p4 user id")
1695 def p4UserIsMe(self, p4User):
1696 """Return True if the given p4 user is actually me."""
1697 me = self.p4UserId()
1698 if not p4User or p4User != me:
1699 return False
1700 else:
1701 return True
1703 def getUserCacheFilename(self):
1704 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1705 return home + "/.gitp4-usercache.txt"
1707 def getUserMapFromPerforceServer(self):
1708 if self.userMapFromPerforceServer:
1709 return
1710 self.users = {}
1711 self.emails = {}
1713 for output in p4CmdList(["users"]):
1714 if "User" not in output:
1715 continue
1716 # "FullName" is bytes. "Email" on the other hand might be bytes
1717 # or unicode string depending on whether we are running under
1718 # python2 or python3. To support
1719 # git-p4.metadataDecodingStrategy=fallback, self.users dict values
1720 # are always bytes, ready to be written to git.
1721 emailbytes = metadata_stream_to_writable_bytes(output["Email"])
1722 self.users[output["User"]] = output["FullName"] + b" <" + emailbytes + b">"
1723 self.emails[output["Email"]] = output["User"]
1725 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1726 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1727 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1728 if mapUser and len(mapUser[0]) == 3:
1729 user = mapUser[0][0]
1730 fullname = mapUser[0][1]
1731 email = mapUser[0][2]
1732 fulluser = fullname + " <" + email + ">"
1733 self.users[user] = metadata_stream_to_writable_bytes(fulluser)
1734 self.emails[email] = user
1736 s = b''
1737 for (key, val) in self.users.items():
1738 keybytes = metadata_stream_to_writable_bytes(key)
1739 s += b"%s\t%s\n" % (keybytes.expandtabs(1), val.expandtabs(1))
1741 open(self.getUserCacheFilename(), 'wb').write(s)
1742 self.userMapFromPerforceServer = True
1744 def loadUserMapFromCache(self):
1745 self.users = {}
1746 self.userMapFromPerforceServer = False
1747 try:
1748 cache = open(self.getUserCacheFilename(), 'rb')
1749 lines = cache.readlines()
1750 cache.close()
1751 for line in lines:
1752 entry = line.strip().split(b"\t")
1753 self.users[entry[0].decode('utf_8')] = entry[1]
1754 except IOError:
1755 self.getUserMapFromPerforceServer()
1758 class P4Submit(Command, P4UserMap):
1760 conflict_behavior_choices = ("ask", "skip", "quit")
1762 def __init__(self):
1763 Command.__init__(self)
1764 P4UserMap.__init__(self)
1765 self.options = [
1766 optparse.make_option("--origin", dest="origin"),
1767 optparse.make_option("-M", dest="detectRenames", action="store_true"),
1768 # preserve the user, requires relevant p4 permissions
1769 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1770 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1771 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1772 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1773 optparse.make_option("--conflict", dest="conflict_behavior",
1774 choices=self.conflict_behavior_choices),
1775 optparse.make_option("--branch", dest="branch"),
1776 optparse.make_option("--shelve", dest="shelve", action="store_true",
1777 help="Shelve instead of submit. Shelved files are reverted, "
1778 "restoring the workspace to the state before the shelve"),
1779 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
1780 metavar="CHANGELIST",
1781 help="update an existing shelved changelist, implies --shelve, "
1782 "repeat in-order for multiple shelved changelists"),
1783 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1784 help="submit only the specified commit(s), one commit or xxx..xxx"),
1785 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1786 help="Disable rebase after submit is completed. Can be useful if you "
1787 "work from a local git branch that is not master"),
1788 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1789 help="Skip Perforce sync of p4/master after submit or shelve"),
1790 optparse.make_option("--no-verify", dest="no_verify", action="store_true",
1791 help="Bypass p4-pre-submit and p4-changelist hooks"),
1793 self.description = """Submit changes from git to the perforce depot.\n
1794 The `p4-pre-submit` hook is executed if it exists and is executable. It
1795 can be bypassed with the `--no-verify` command line option. The hook takes
1796 no parameters and nothing from standard input. Exiting with a non-zero status
1797 from this script prevents `git-p4 submit` from launching.
1799 One usage scenario is to run unit tests in the hook.
1801 The `p4-prepare-changelist` hook is executed right after preparing the default
1802 changelist message and before the editor is started. It takes one parameter,
1803 the name of the file that contains the changelist text. Exiting with a non-zero
1804 status from the script will abort the process.
1806 The purpose of the hook is to edit the message file in place, and it is not
1807 supressed by the `--no-verify` option. This hook is called even if
1808 `--prepare-p4-only` is set.
1810 The `p4-changelist` hook is executed after the changelist message has been
1811 edited by the user. It can be bypassed with the `--no-verify` option. It
1812 takes a single parameter, the name of the file that holds the proposed
1813 changelist text. Exiting with a non-zero status causes the command to abort.
1815 The hook is allowed to edit the changelist file and can be used to normalize
1816 the text into some project standard format. It can also be used to refuse the
1817 Submit after inspect the message file.
1819 The `p4-post-changelist` hook is invoked after the submit has successfully
1820 occurred in P4. It takes no parameters and is meant primarily for notification
1821 and cannot affect the outcome of the git p4 submit action.
1824 self.usage += " [name of git branch to submit into perforce depot]"
1825 self.origin = ""
1826 self.detectRenames = False
1827 self.preserveUser = gitConfigBool("git-p4.preserveUser")
1828 self.dry_run = False
1829 self.shelve = False
1830 self.update_shelve = list()
1831 self.commit = ""
1832 self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1833 self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
1834 self.prepare_p4_only = False
1835 self.conflict_behavior = None
1836 self.isWindows = (platform.system() == "Windows")
1837 self.exportLabels = False
1838 self.p4HasMoveCommand = p4_has_move_command()
1839 self.branch = None
1840 self.no_verify = False
1842 if gitConfig('git-p4.largeFileSystem'):
1843 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1845 def check(self):
1846 if len(p4CmdList(["opened", "..."])) > 0:
1847 die("You have files opened with perforce! Close them before starting the sync.")
1849 def separate_jobs_from_description(self, message):
1850 """Extract and return a possible Jobs field in the commit message. It
1851 goes into a separate section in the p4 change specification.
1853 A jobs line starts with "Jobs:" and looks like a new field in a
1854 form. Values are white-space separated on the same line or on
1855 following lines that start with a tab.
1857 This does not parse and extract the full git commit message like a
1858 p4 form. It just sees the Jobs: line as a marker to pass everything
1859 from then on directly into the p4 form, but outside the description
1860 section.
1862 Return a tuple (stripped log message, jobs string).
1865 m = re.search(r'^Jobs:', message, re.MULTILINE)
1866 if m is None:
1867 return (message, None)
1869 jobtext = message[m.start():]
1870 stripped_message = message[:m.start()].rstrip()
1871 return (stripped_message, jobtext)
1873 def prepareLogMessage(self, template, message, jobs):
1874 """Edits the template returned from "p4 change -o" to insert the
1875 message in the Description field, and the jobs text in the Jobs
1876 field.
1878 result = ""
1880 inDescriptionSection = False
1882 for line in template.split("\n"):
1883 if line.startswith("#"):
1884 result += line + "\n"
1885 continue
1887 if inDescriptionSection:
1888 if line.startswith("Files:") or line.startswith("Jobs:"):
1889 inDescriptionSection = False
1890 # insert Jobs section
1891 if jobs:
1892 result += jobs + "\n"
1893 else:
1894 continue
1895 else:
1896 if line.startswith("Description:"):
1897 inDescriptionSection = True
1898 line += "\n"
1899 for messageLine in message.split("\n"):
1900 line += "\t" + messageLine + "\n"
1902 result += line + "\n"
1904 return result
1906 def patchRCSKeywords(self, file, regexp):
1907 """Attempt to zap the RCS keywords in a p4 controlled file matching the
1908 given regex.
1910 handle, outFileName = tempfile.mkstemp(dir='.')
1911 try:
1912 with os.fdopen(handle, "wb") as outFile, open(file, "rb") as inFile:
1913 for line in inFile.readlines():
1914 outFile.write(regexp.sub(br'$\1$', line))
1915 # Forcibly overwrite the original file
1916 os.unlink(file)
1917 shutil.move(outFileName, file)
1918 except:
1919 # cleanup our temporary file
1920 os.unlink(outFileName)
1921 print("Failed to strip RCS keywords in %s" % file)
1922 raise
1924 print("Patched up RCS keywords in %s" % file)
1926 def p4UserForCommit(self, id):
1927 """Return the tuple (perforce user,git email) for a given git commit
1930 self.getUserMapFromPerforceServer()
1931 gitEmail = read_pipe(["git", "log", "--max-count=1",
1932 "--format=%ae", id])
1933 gitEmail = gitEmail.strip()
1934 if gitEmail not in self.emails:
1935 return (None, gitEmail)
1936 else:
1937 return (self.emails[gitEmail], gitEmail)
1939 def checkValidP4Users(self, commits):
1940 """Check if any git authors cannot be mapped to p4 users."""
1941 for id in commits:
1942 user, email = self.p4UserForCommit(id)
1943 if not user:
1944 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1945 if gitConfigBool("git-p4.allowMissingP4Users"):
1946 print("%s" % msg)
1947 else:
1948 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1950 def lastP4Changelist(self):
1951 """Get back the last changelist number submitted in this client spec.
1953 This then gets used to patch up the username in the change. If the
1954 same client spec is being used by multiple processes then this might
1955 go wrong.
1957 results = p4CmdList(["client", "-o"]) # find the current client
1958 client = None
1959 for r in results:
1960 if 'Client' in r:
1961 client = r['Client']
1962 break
1963 if not client:
1964 die("could not get client spec")
1965 results = p4CmdList(["changes", "-c", client, "-m", "1"])
1966 for r in results:
1967 if 'change' in r:
1968 return r['change']
1969 die("Could not get changelist number for last submit - cannot patch up user details")
1971 def modifyChangelistUser(self, changelist, newUser):
1972 """Fixup the user field of a changelist after it has been submitted."""
1973 changes = p4CmdList(["change", "-o", changelist])
1974 if len(changes) != 1:
1975 die("Bad output from p4 change modifying %s to user %s" %
1976 (changelist, newUser))
1978 c = changes[0]
1979 if c['User'] == newUser:
1980 # Nothing to do
1981 return
1982 c['User'] = newUser
1983 # p4 does not understand format version 3 and above
1984 input = marshal.dumps(c, 2)
1986 result = p4CmdList(["change", "-f", "-i"], stdin=input)
1987 for r in result:
1988 if 'code' in r:
1989 if r['code'] == 'error':
1990 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1991 if 'data' in r:
1992 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1993 return
1994 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1996 def canChangeChangelists(self):
1997 """Check to see if we have p4 admin or super-user permissions, either
1998 of which are required to modify changelists.
2000 results = p4CmdList(["protects", self.depotPath])
2001 for r in results:
2002 if 'perm' in r:
2003 if r['perm'] == 'admin':
2004 return 1
2005 if r['perm'] == 'super':
2006 return 1
2007 return 0
2009 def prepareSubmitTemplate(self, changelist=None):
2010 """Run "p4 change -o" to grab a change specification template.
2012 This does not use "p4 -G", as it is nice to keep the submission
2013 template in original order, since a human might edit it.
2015 Remove lines in the Files section that show changes to files
2016 outside the depot path we're committing into.
2019 upstream, settings = findUpstreamBranchPoint()
2021 template = """\
2022 # A Perforce Change Specification.
2024 # Change: The change number. 'new' on a new changelist.
2025 # Date: The date this specification was last modified.
2026 # Client: The client on which the changelist was created. Read-only.
2027 # User: The user who created the changelist.
2028 # Status: Either 'pending' or 'submitted'. Read-only.
2029 # Type: Either 'public' or 'restricted'. Default is 'public'.
2030 # Description: Comments about the changelist. Required.
2031 # Jobs: What opened jobs are to be closed by this changelist.
2032 # You may delete jobs from this list. (New changelists only.)
2033 # Files: What opened files from the default changelist are to be added
2034 # to this changelist. You may delete files from this list.
2035 # (New changelists only.)
2037 files_list = []
2038 inFilesSection = False
2039 change_entry = None
2040 args = ['change', '-o']
2041 if changelist:
2042 args.append(str(changelist))
2043 for entry in p4CmdList(args):
2044 if 'code' not in entry:
2045 continue
2046 if entry['code'] == 'stat':
2047 change_entry = entry
2048 break
2049 if not change_entry:
2050 die('Failed to decode output of p4 change -o')
2051 for key, value in change_entry.items():
2052 if key.startswith('File'):
2053 if 'depot-paths' in settings:
2054 if not [p for p in settings['depot-paths']
2055 if p4PathStartsWith(value, p)]:
2056 continue
2057 else:
2058 if not p4PathStartsWith(value, self.depotPath):
2059 continue
2060 files_list.append(value)
2061 continue
2062 # Output in the order expected by prepareLogMessage
2063 for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
2064 if key not in change_entry:
2065 continue
2066 template += '\n'
2067 template += key + ':'
2068 if key == 'Description':
2069 template += '\n'
2070 for field_line in change_entry[key].splitlines():
2071 template += '\t'+field_line+'\n'
2072 if len(files_list) > 0:
2073 template += '\n'
2074 template += 'Files:\n'
2075 for path in files_list:
2076 template += '\t'+path+'\n'
2077 return template
2079 def edit_template(self, template_file):
2080 """Invoke the editor to let the user change the submission message.
2082 Return true if okay to continue with the submit.
2085 # if configured to skip the editing part, just submit
2086 if gitConfigBool("git-p4.skipSubmitEdit"):
2087 return True
2089 # look at the modification time, to check later if the user saved
2090 # the file
2091 mtime = os.stat(template_file).st_mtime
2093 # invoke the editor
2094 if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
2095 editor = os.environ.get("P4EDITOR")
2096 else:
2097 editor = read_pipe(["git", "var", "GIT_EDITOR"]).strip()
2098 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
2100 # If the file was not saved, prompt to see if this patch should
2101 # be skipped. But skip this verification step if configured so.
2102 if gitConfigBool("git-p4.skipSubmitEditCheck"):
2103 return True
2105 # modification time updated means user saved the file
2106 if os.stat(template_file).st_mtime > mtime:
2107 return True
2109 response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
2110 if response == 'y':
2111 return True
2112 if response == 'n':
2113 return False
2115 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
2116 # diff
2117 if "P4DIFF" in os.environ:
2118 del(os.environ["P4DIFF"])
2119 diff = ""
2120 for editedFile in editedFiles:
2121 diff += p4_read_pipe(['diff', '-du',
2122 wildcard_encode(editedFile)])
2124 # new file diff
2125 newdiff = ""
2126 for newFile in filesToAdd:
2127 newdiff += "==== new file ====\n"
2128 newdiff += "--- /dev/null\n"
2129 newdiff += "+++ %s\n" % newFile
2131 is_link = os.path.islink(newFile)
2132 expect_link = newFile in symlinks
2134 if is_link and expect_link:
2135 newdiff += "+%s\n" % os.readlink(newFile)
2136 else:
2137 f = open(newFile, "r")
2138 try:
2139 for line in f.readlines():
2140 newdiff += "+" + line
2141 except UnicodeDecodeError:
2142 # Found non-text data and skip, since diff description
2143 # should only include text
2144 pass
2145 f.close()
2147 return (diff + newdiff).replace('\r\n', '\n')
2149 def applyCommit(self, id):
2150 """Apply one commit, return True if it succeeded."""
2152 print("Applying", read_pipe(["git", "show", "-s",
2153 "--format=format:%h %s", id]))
2155 p4User, gitEmail = self.p4UserForCommit(id)
2157 diff = read_pipe_lines(
2158 ["git", "diff-tree", "-r"] + self.diffOpts + ["{}^".format(id), id])
2159 filesToAdd = set()
2160 filesToChangeType = set()
2161 filesToDelete = set()
2162 editedFiles = set()
2163 pureRenameCopy = set()
2164 symlinks = set()
2165 filesToChangeExecBit = {}
2166 all_files = list()
2168 for line in diff:
2169 diff = parseDiffTreeEntry(line)
2170 modifier = diff['status']
2171 path = diff['src']
2172 all_files.append(path)
2174 if modifier == "M":
2175 p4_edit(path)
2176 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2177 filesToChangeExecBit[path] = diff['dst_mode']
2178 editedFiles.add(path)
2179 elif modifier == "A":
2180 filesToAdd.add(path)
2181 filesToChangeExecBit[path] = diff['dst_mode']
2182 if path in filesToDelete:
2183 filesToDelete.remove(path)
2185 dst_mode = int(diff['dst_mode'], 8)
2186 if dst_mode == 0o120000:
2187 symlinks.add(path)
2189 elif modifier == "D":
2190 filesToDelete.add(path)
2191 if path in filesToAdd:
2192 filesToAdd.remove(path)
2193 elif modifier == "C":
2194 src, dest = diff['src'], diff['dst']
2195 all_files.append(dest)
2196 p4_integrate(src, dest)
2197 pureRenameCopy.add(dest)
2198 if diff['src_sha1'] != diff['dst_sha1']:
2199 p4_edit(dest)
2200 pureRenameCopy.discard(dest)
2201 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2202 p4_edit(dest)
2203 pureRenameCopy.discard(dest)
2204 filesToChangeExecBit[dest] = diff['dst_mode']
2205 if self.isWindows:
2206 # turn off read-only attribute
2207 os.chmod(dest, stat.S_IWRITE)
2208 os.unlink(dest)
2209 editedFiles.add(dest)
2210 elif modifier == "R":
2211 src, dest = diff['src'], diff['dst']
2212 all_files.append(dest)
2213 if self.p4HasMoveCommand:
2214 p4_edit(src) # src must be open before move
2215 p4_move(src, dest) # opens for (move/delete, move/add)
2216 else:
2217 p4_integrate(src, dest)
2218 if diff['src_sha1'] != diff['dst_sha1']:
2219 p4_edit(dest)
2220 else:
2221 pureRenameCopy.add(dest)
2222 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2223 if not self.p4HasMoveCommand:
2224 p4_edit(dest) # with move: already open, writable
2225 filesToChangeExecBit[dest] = diff['dst_mode']
2226 if not self.p4HasMoveCommand:
2227 if self.isWindows:
2228 os.chmod(dest, stat.S_IWRITE)
2229 os.unlink(dest)
2230 filesToDelete.add(src)
2231 editedFiles.add(dest)
2232 elif modifier == "T":
2233 filesToChangeType.add(path)
2234 else:
2235 die("unknown modifier %s for %s" % (modifier, path))
2237 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
2238 patchcmd = diffcmd + " | git apply "
2239 tryPatchCmd = patchcmd + "--check -"
2240 applyPatchCmd = patchcmd + "--check --apply -"
2241 patch_succeeded = True
2243 if verbose:
2244 print("TryPatch: %s" % tryPatchCmd)
2246 if os.system(tryPatchCmd) != 0:
2247 fixed_rcs_keywords = False
2248 patch_succeeded = False
2249 print("Unfortunately applying the change failed!")
2251 # Patch failed, maybe it's just RCS keyword woes. Look through
2252 # the patch to see if that's possible.
2253 if gitConfigBool("git-p4.attemptRCSCleanup"):
2254 file = None
2255 kwfiles = {}
2256 for file in editedFiles | filesToDelete:
2257 # did this file's delta contain RCS keywords?
2258 regexp = p4_keywords_regexp_for_file(file)
2259 if regexp:
2260 # this file is a possibility...look for RCS keywords.
2261 for line in read_pipe_lines(
2262 ["git", "diff", "%s^..%s" % (id, id), file],
2263 raw=True):
2264 if regexp.search(line):
2265 if verbose:
2266 print("got keyword match on %s in %s in %s" % (regexp.pattern, line, file))
2267 kwfiles[file] = regexp
2268 break
2270 for file, regexp in kwfiles.items():
2271 if verbose:
2272 print("zapping %s with %s" % (line, regexp.pattern))
2273 # File is being deleted, so not open in p4. Must
2274 # disable the read-only bit on windows.
2275 if self.isWindows and file not in editedFiles:
2276 os.chmod(file, stat.S_IWRITE)
2277 self.patchRCSKeywords(file, kwfiles[file])
2278 fixed_rcs_keywords = True
2280 if fixed_rcs_keywords:
2281 print("Retrying the patch with RCS keywords cleaned up")
2282 if os.system(tryPatchCmd) == 0:
2283 patch_succeeded = True
2284 print("Patch succeesed this time with RCS keywords cleaned")
2286 if not patch_succeeded:
2287 for f in editedFiles:
2288 p4_revert(f)
2289 return False
2292 # Apply the patch for real, and do add/delete/+x handling.
2294 system(applyPatchCmd, shell=True)
2296 for f in filesToChangeType:
2297 p4_edit(f, "-t", "auto")
2298 for f in filesToAdd:
2299 p4_add(f)
2300 for f in filesToDelete:
2301 p4_revert(f)
2302 p4_delete(f)
2304 # Set/clear executable bits
2305 for f in filesToChangeExecBit.keys():
2306 mode = filesToChangeExecBit[f]
2307 setP4ExecBit(f, mode)
2309 update_shelve = 0
2310 if len(self.update_shelve) > 0:
2311 update_shelve = self.update_shelve.pop(0)
2312 p4_reopen_in_change(update_shelve, all_files)
2315 # Build p4 change description, starting with the contents
2316 # of the git commit message.
2318 logMessage = extractLogMessageFromGitCommit(id)
2319 logMessage = logMessage.strip()
2320 logMessage, jobs = self.separate_jobs_from_description(logMessage)
2322 template = self.prepareSubmitTemplate(update_shelve)
2323 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
2325 if self.preserveUser:
2326 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
2328 if self.checkAuthorship and not self.p4UserIsMe(p4User):
2329 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2330 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2331 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2333 separatorLine = "######## everything below this line is just the diff #######\n"
2334 if not self.prepare_p4_only:
2335 submitTemplate += separatorLine
2336 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
2338 handle, fileName = tempfile.mkstemp()
2339 tmpFile = os.fdopen(handle, "w+b")
2340 if self.isWindows:
2341 submitTemplate = submitTemplate.replace("\n", "\r\n")
2342 tmpFile.write(encode_text_stream(submitTemplate))
2343 tmpFile.close()
2345 submitted = False
2347 try:
2348 # Allow the hook to edit the changelist text before presenting it
2349 # to the user.
2350 if not run_git_hook("p4-prepare-changelist", [fileName]):
2351 return False
2353 if self.prepare_p4_only:
2355 # Leave the p4 tree prepared, and the submit template around
2356 # and let the user decide what to do next
2358 submitted = True
2359 print("")
2360 print("P4 workspace prepared for submission.")
2361 print("To submit or revert, go to client workspace")
2362 print(" " + self.clientPath)
2363 print("")
2364 print("To submit, use \"p4 submit\" to write a new description,")
2365 print("or \"p4 submit -i <%s\" to use the one prepared by"
2366 " \"git p4\"." % fileName)
2367 print("You can delete the file \"%s\" when finished." % fileName)
2369 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
2370 print("To preserve change ownership by user %s, you must\n"
2371 "do \"p4 change -f <change>\" after submitting and\n"
2372 "edit the User field.")
2373 if pureRenameCopy:
2374 print("After submitting, renamed files must be re-synced.")
2375 print("Invoke \"p4 sync -f\" on each of these files:")
2376 for f in pureRenameCopy:
2377 print(" " + f)
2379 print("")
2380 print("To revert the changes, use \"p4 revert ...\", and delete")
2381 print("the submit template file \"%s\"" % fileName)
2382 if filesToAdd:
2383 print("Since the commit adds new files, they must be deleted:")
2384 for f in filesToAdd:
2385 print(" " + f)
2386 print("")
2387 sys.stdout.flush()
2388 return True
2390 if self.edit_template(fileName):
2391 if not self.no_verify:
2392 if not run_git_hook("p4-changelist", [fileName]):
2393 print("The p4-changelist hook failed.")
2394 sys.stdout.flush()
2395 return False
2397 # read the edited message and submit
2398 tmpFile = open(fileName, "rb")
2399 message = decode_text_stream(tmpFile.read())
2400 tmpFile.close()
2401 if self.isWindows:
2402 message = message.replace("\r\n", "\n")
2403 if message.find(separatorLine) != -1:
2404 submitTemplate = message[:message.index(separatorLine)]
2405 else:
2406 submitTemplate = message
2408 if len(submitTemplate.strip()) == 0:
2409 print("Changelist is empty, aborting this changelist.")
2410 sys.stdout.flush()
2411 return False
2413 if update_shelve:
2414 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2415 elif self.shelve:
2416 p4_write_pipe(['shelve', '-i'], submitTemplate)
2417 else:
2418 p4_write_pipe(['submit', '-i'], submitTemplate)
2419 # The rename/copy happened by applying a patch that created a
2420 # new file. This leaves it writable, which confuses p4.
2421 for f in pureRenameCopy:
2422 p4_sync(f, "-f")
2424 if self.preserveUser:
2425 if p4User:
2426 # Get last changelist number. Cannot easily get it from
2427 # the submit command output as the output is
2428 # unmarshalled.
2429 changelist = self.lastP4Changelist()
2430 self.modifyChangelistUser(changelist, p4User)
2432 submitted = True
2434 run_git_hook("p4-post-changelist")
2435 finally:
2436 # Revert changes if we skip this patch
2437 if not submitted or self.shelve:
2438 if self.shelve:
2439 print("Reverting shelved files.")
2440 else:
2441 print("Submission cancelled, undoing p4 changes.")
2442 sys.stdout.flush()
2443 for f in editedFiles | filesToDelete:
2444 p4_revert(f)
2445 for f in filesToAdd:
2446 p4_revert(f)
2447 os.remove(f)
2449 if not self.prepare_p4_only:
2450 os.remove(fileName)
2451 return submitted
2453 def exportGitTags(self, gitTags):
2454 """Export git tags as p4 labels. Create a p4 label and then tag with
2455 that.
2458 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2459 if len(validLabelRegexp) == 0:
2460 validLabelRegexp = defaultLabelRegexp
2461 m = re.compile(validLabelRegexp)
2463 for name in gitTags:
2465 if not m.match(name):
2466 if verbose:
2467 print("tag %s does not match regexp %s" % (name, validLabelRegexp))
2468 continue
2470 # Get the p4 commit this corresponds to
2471 logMessage = extractLogMessageFromGitCommit(name)
2472 values = extractSettingsGitLog(logMessage)
2474 if 'change' not in values:
2475 # a tag pointing to something not sent to p4; ignore
2476 if verbose:
2477 print("git tag %s does not give a p4 commit" % name)
2478 continue
2479 else:
2480 changelist = values['change']
2482 # Get the tag details.
2483 inHeader = True
2484 isAnnotated = False
2485 body = []
2486 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2487 l = l.strip()
2488 if inHeader:
2489 if re.match(r'tag\s+', l):
2490 isAnnotated = True
2491 elif re.match(r'\s*$', l):
2492 inHeader = False
2493 continue
2494 else:
2495 body.append(l)
2497 if not isAnnotated:
2498 body = ["lightweight tag imported by git p4\n"]
2500 # Create the label - use the same view as the client spec we are using
2501 clientSpec = getClientSpec()
2503 labelTemplate = "Label: %s\n" % name
2504 labelTemplate += "Description:\n"
2505 for b in body:
2506 labelTemplate += "\t" + b + "\n"
2507 labelTemplate += "View:\n"
2508 for depot_side in clientSpec.mappings:
2509 labelTemplate += "\t%s\n" % depot_side
2511 if self.dry_run:
2512 print("Would create p4 label %s for tag" % name)
2513 elif self.prepare_p4_only:
2514 print("Not creating p4 label %s for tag due to option"
2515 " --prepare-p4-only" % name)
2516 else:
2517 p4_write_pipe(["label", "-i"], labelTemplate)
2519 # Use the label
2520 p4_system(["tag", "-l", name] +
2521 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2523 if verbose:
2524 print("created p4 label for tag %s" % name)
2526 def run(self, args):
2527 if len(args) == 0:
2528 self.master = currentGitBranch()
2529 elif len(args) == 1:
2530 self.master = args[0]
2531 if not branchExists(self.master):
2532 die("Branch %s does not exist" % self.master)
2533 else:
2534 return False
2536 for i in self.update_shelve:
2537 if i <= 0:
2538 sys.exit("invalid changelist %d" % i)
2540 if self.master:
2541 allowSubmit = gitConfig("git-p4.allowSubmit")
2542 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2543 die("%s is not in git-p4.allowSubmit" % self.master)
2545 upstream, settings = findUpstreamBranchPoint()
2546 self.depotPath = settings['depot-paths'][0]
2547 if len(self.origin) == 0:
2548 self.origin = upstream
2550 if len(self.update_shelve) > 0:
2551 self.shelve = True
2553 if self.preserveUser:
2554 if not self.canChangeChangelists():
2555 die("Cannot preserve user names without p4 super-user or admin permissions")
2557 # if not set from the command line, try the config file
2558 if self.conflict_behavior is None:
2559 val = gitConfig("git-p4.conflict")
2560 if val:
2561 if val not in self.conflict_behavior_choices:
2562 die("Invalid value '%s' for config git-p4.conflict" % val)
2563 else:
2564 val = "ask"
2565 self.conflict_behavior = val
2567 if self.verbose:
2568 print("Origin branch is " + self.origin)
2570 if len(self.depotPath) == 0:
2571 print("Internal error: cannot locate perforce depot path from existing branches")
2572 sys.exit(128)
2574 self.useClientSpec = False
2575 if gitConfigBool("git-p4.useclientspec"):
2576 self.useClientSpec = True
2577 if self.useClientSpec:
2578 self.clientSpecDirs = getClientSpec()
2580 # Check for the existence of P4 branches
2581 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2583 if self.useClientSpec and not branchesDetected:
2584 # all files are relative to the client spec
2585 self.clientPath = getClientRoot()
2586 else:
2587 self.clientPath = p4Where(self.depotPath)
2589 if self.clientPath == "":
2590 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2592 print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
2593 self.oldWorkingDirectory = os.getcwd()
2595 # ensure the clientPath exists
2596 new_client_dir = False
2597 if not os.path.exists(self.clientPath):
2598 new_client_dir = True
2599 os.makedirs(self.clientPath)
2601 chdir(self.clientPath, is_client_path=True)
2602 if self.dry_run:
2603 print("Would synchronize p4 checkout in %s" % self.clientPath)
2604 else:
2605 print("Synchronizing p4 checkout...")
2606 if new_client_dir:
2607 # old one was destroyed, and maybe nobody told p4
2608 p4_sync("...", "-f")
2609 else:
2610 p4_sync("...")
2611 self.check()
2613 commits = []
2614 if self.master:
2615 committish = self.master
2616 else:
2617 committish = 'HEAD'
2619 if self.commit != "":
2620 if self.commit.find("..") != -1:
2621 limits_ish = self.commit.split("..")
2622 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2623 commits.append(line.strip())
2624 commits.reverse()
2625 else:
2626 commits.append(self.commit)
2627 else:
2628 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2629 commits.append(line.strip())
2630 commits.reverse()
2632 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2633 self.checkAuthorship = False
2634 else:
2635 self.checkAuthorship = True
2637 if self.preserveUser:
2638 self.checkValidP4Users(commits)
2641 # Build up a set of options to be passed to diff when
2642 # submitting each commit to p4.
2644 if self.detectRenames:
2645 # command-line -M arg
2646 self.diffOpts = ["-M"]
2647 else:
2648 # If not explicitly set check the config variable
2649 detectRenames = gitConfig("git-p4.detectRenames")
2651 if detectRenames.lower() == "false" or detectRenames == "":
2652 self.diffOpts = []
2653 elif detectRenames.lower() == "true":
2654 self.diffOpts = ["-M"]
2655 else:
2656 self.diffOpts = ["-M{}".format(detectRenames)]
2658 # no command-line arg for -C or --find-copies-harder, just
2659 # config variables
2660 detectCopies = gitConfig("git-p4.detectCopies")
2661 if detectCopies.lower() == "false" or detectCopies == "":
2662 pass
2663 elif detectCopies.lower() == "true":
2664 self.diffOpts.append("-C")
2665 else:
2666 self.diffOpts.append("-C{}".format(detectCopies))
2668 if gitConfigBool("git-p4.detectCopiesHarder"):
2669 self.diffOpts.append("--find-copies-harder")
2671 num_shelves = len(self.update_shelve)
2672 if num_shelves > 0 and num_shelves != len(commits):
2673 sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2674 (len(commits), num_shelves))
2676 if not self.no_verify:
2677 try:
2678 if not run_git_hook("p4-pre-submit"):
2679 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip "
2680 "this pre-submission check by adding\nthe command line option '--no-verify', "
2681 "however,\nthis will also skip the p4-changelist hook as well.")
2682 sys.exit(1)
2683 except Exception as e:
2684 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "
2685 "with the error '{0}'".format(e.message))
2686 sys.exit(1)
2689 # Apply the commits, one at a time. On failure, ask if should
2690 # continue to try the rest of the patches, or quit.
2692 if self.dry_run:
2693 print("Would apply")
2694 applied = []
2695 last = len(commits) - 1
2696 for i, commit in enumerate(commits):
2697 if self.dry_run:
2698 print(" ", read_pipe(["git", "show", "-s",
2699 "--format=format:%h %s", commit]))
2700 ok = True
2701 else:
2702 ok = self.applyCommit(commit)
2703 if ok:
2704 applied.append(commit)
2705 if self.prepare_p4_only:
2706 if i < last:
2707 print("Processing only the first commit due to option"
2708 " --prepare-p4-only")
2709 break
2710 else:
2711 if i < last:
2712 # prompt for what to do, or use the option/variable
2713 if self.conflict_behavior == "ask":
2714 print("What do you want to do?")
2715 response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2716 elif self.conflict_behavior == "skip":
2717 response = "s"
2718 elif self.conflict_behavior == "quit":
2719 response = "q"
2720 else:
2721 die("Unknown conflict_behavior '%s'" %
2722 self.conflict_behavior)
2724 if response == "s":
2725 print("Skipping this commit, but applying the rest")
2726 if response == "q":
2727 print("Quitting")
2728 break
2730 chdir(self.oldWorkingDirectory)
2731 shelved_applied = "shelved" if self.shelve else "applied"
2732 if self.dry_run:
2733 pass
2734 elif self.prepare_p4_only:
2735 pass
2736 elif len(commits) == len(applied):
2737 print("All commits {0}!".format(shelved_applied))
2739 sync = P4Sync()
2740 if self.branch:
2741 sync.branch = self.branch
2742 if self.disable_p4sync:
2743 sync.sync_origin_only()
2744 else:
2745 sync.run([])
2747 if not self.disable_rebase:
2748 rebase = P4Rebase()
2749 rebase.rebase()
2751 else:
2752 if len(applied) == 0:
2753 print("No commits {0}.".format(shelved_applied))
2754 else:
2755 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2756 for c in commits:
2757 if c in applied:
2758 star = "*"
2759 else:
2760 star = " "
2761 print(star, read_pipe(["git", "show", "-s",
2762 "--format=format:%h %s", c]))
2763 print("You will have to do 'git p4 sync' and rebase.")
2765 if gitConfigBool("git-p4.exportLabels"):
2766 self.exportLabels = True
2768 if self.exportLabels:
2769 p4Labels = getP4Labels(self.depotPath)
2770 gitTags = getGitTags()
2772 missingGitTags = gitTags - p4Labels
2773 self.exportGitTags(missingGitTags)
2775 # exit with error unless everything applied perfectly
2776 if len(commits) != len(applied):
2777 sys.exit(1)
2779 return True
2782 class View(object):
2783 """Represent a p4 view ("p4 help views"), and map files in a repo according
2784 to the view.
2787 def __init__(self, client_name):
2788 self.mappings = []
2789 self.client_prefix = "//%s/" % client_name
2790 # cache results of "p4 where" to lookup client file locations
2791 self.client_spec_path_cache = {}
2793 def append(self, view_line):
2794 """Parse a view line, splitting it into depot and client sides. Append
2795 to self.mappings, preserving order. This is only needed for tag
2796 creation.
2799 # Split the view line into exactly two words. P4 enforces
2800 # structure on these lines that simplifies this quite a bit.
2802 # Either or both words may be double-quoted.
2803 # Single quotes do not matter.
2804 # Double-quote marks cannot occur inside the words.
2805 # A + or - prefix is also inside the quotes.
2806 # There are no quotes unless they contain a space.
2807 # The line is already white-space stripped.
2808 # The two words are separated by a single space.
2810 if view_line[0] == '"':
2811 # First word is double quoted. Find its end.
2812 close_quote_index = view_line.find('"', 1)
2813 if close_quote_index <= 0:
2814 die("No first-word closing quote found: %s" % view_line)
2815 depot_side = view_line[1:close_quote_index]
2816 # skip closing quote and space
2817 rhs_index = close_quote_index + 1 + 1
2818 else:
2819 space_index = view_line.find(" ")
2820 if space_index <= 0:
2821 die("No word-splitting space found: %s" % view_line)
2822 depot_side = view_line[0:space_index]
2823 rhs_index = space_index + 1
2825 # prefix + means overlay on previous mapping
2826 if depot_side.startswith("+"):
2827 depot_side = depot_side[1:]
2829 # prefix - means exclude this path, leave out of mappings
2830 exclude = False
2831 if depot_side.startswith("-"):
2832 exclude = True
2833 depot_side = depot_side[1:]
2835 if not exclude:
2836 self.mappings.append(depot_side)
2838 def convert_client_path(self, clientFile):
2839 # chop off //client/ part to make it relative
2840 if not decode_path(clientFile).startswith(self.client_prefix):
2841 die("No prefix '%s' on clientFile '%s'" %
2842 (self.client_prefix, clientFile))
2843 return clientFile[len(self.client_prefix):]
2845 def update_client_spec_path_cache(self, files):
2846 """Caching file paths by "p4 where" batch query."""
2848 # List depot file paths exclude that already cached
2849 fileArgs = [f['path'] for f in files if decode_path(f['path']) not in self.client_spec_path_cache]
2851 if len(fileArgs) == 0:
2852 return # All files in cache
2854 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2855 for res in where_result:
2856 if "code" in res and res["code"] == "error":
2857 # assume error is "... file(s) not in client view"
2858 continue
2859 if "clientFile" not in res:
2860 die("No clientFile in 'p4 where' output")
2861 if "unmap" in res:
2862 # it will list all of them, but only one not unmap-ped
2863 continue
2864 depot_path = decode_path(res['depotFile'])
2865 if gitConfigBool("core.ignorecase"):
2866 depot_path = depot_path.lower()
2867 self.client_spec_path_cache[depot_path] = self.convert_client_path(res["clientFile"])
2869 # not found files or unmap files set to ""
2870 for depotFile in fileArgs:
2871 depotFile = decode_path(depotFile)
2872 if gitConfigBool("core.ignorecase"):
2873 depotFile = depotFile.lower()
2874 if depotFile not in self.client_spec_path_cache:
2875 self.client_spec_path_cache[depotFile] = b''
2877 def map_in_client(self, depot_path):
2878 """Return the relative location in the client where this depot file
2879 should live.
2881 Returns "" if the file should not be mapped in the client.
2884 if gitConfigBool("core.ignorecase"):
2885 depot_path = depot_path.lower()
2887 if depot_path in self.client_spec_path_cache:
2888 return self.client_spec_path_cache[depot_path]
2890 die("Error: %s is not found in client spec path" % depot_path)
2891 return ""
2894 def cloneExcludeCallback(option, opt_str, value, parser):
2895 # prepend "/" because the first "/" was consumed as part of the option itself.
2896 # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2897 parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2900 class P4Sync(Command, P4UserMap):
2902 def __init__(self):
2903 Command.__init__(self)
2904 P4UserMap.__init__(self)
2905 self.options = [
2906 optparse.make_option("--branch", dest="branch"),
2907 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2908 optparse.make_option("--changesfile", dest="changesFile"),
2909 optparse.make_option("--silent", dest="silent", action="store_true"),
2910 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2911 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2912 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2913 help="Import into refs/heads/ , not refs/remotes"),
2914 optparse.make_option("--max-changes", dest="maxChanges",
2915 help="Maximum number of changes to import"),
2916 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2917 help="Internal block size to use when iteratively calling p4 changes"),
2918 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2919 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2920 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2921 help="Only sync files that are included in the Perforce Client Spec"),
2922 optparse.make_option("-/", dest="cloneExclude",
2923 action="callback", callback=cloneExcludeCallback, type="string",
2924 help="exclude depot path"),
2926 self.description = """Imports from Perforce into a git repository.\n
2927 example:
2928 //depot/my/project/ -- to import the current head
2929 //depot/my/project/@all -- to import everything
2930 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2932 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2934 self.usage += " //depot/path[@revRange]"
2935 self.silent = False
2936 self.createdBranches = set()
2937 self.committedChanges = set()
2938 self.branch = ""
2939 self.detectBranches = False
2940 self.detectLabels = False
2941 self.importLabels = False
2942 self.changesFile = ""
2943 self.syncWithOrigin = True
2944 self.importIntoRemotes = True
2945 self.maxChanges = ""
2946 self.changes_block_size = None
2947 self.keepRepoPath = False
2948 self.depotPaths = None
2949 self.p4BranchesInGit = []
2950 self.cloneExclude = []
2951 self.useClientSpec = False
2952 self.useClientSpec_from_options = False
2953 self.clientSpecDirs = None
2954 self.tempBranches = []
2955 self.tempBranchLocation = "refs/git-p4-tmp"
2956 self.largeFileSystem = None
2957 self.suppress_meta_comment = False
2959 if gitConfig('git-p4.largeFileSystem'):
2960 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2961 self.largeFileSystem = largeFileSystemConstructor(
2962 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2965 if gitConfig("git-p4.syncFromOrigin") == "false":
2966 self.syncWithOrigin = False
2968 self.depotPaths = []
2969 self.changeRange = ""
2970 self.previousDepotPaths = []
2971 self.hasOrigin = False
2973 # map from branch depot path to parent branch
2974 self.knownBranches = {}
2975 self.initialParents = {}
2977 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2978 self.labels = {}
2980 def checkpoint(self):
2981 """Force a checkpoint in fast-import and wait for it to finish."""
2982 self.gitStream.write("checkpoint\n\n")
2983 self.gitStream.write("progress checkpoint\n\n")
2984 self.gitStream.flush()
2985 out = self.gitOutput.readline()
2986 if self.verbose:
2987 print("checkpoint finished: " + out)
2989 def isPathWanted(self, path):
2990 for p in self.cloneExclude:
2991 if p.endswith("/"):
2992 if p4PathStartsWith(path, p):
2993 return False
2994 # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2995 elif path.lower() == p.lower():
2996 return False
2997 for p in self.depotPaths:
2998 if p4PathStartsWith(path, decode_path(p)):
2999 return True
3000 return False
3002 def extractFilesFromCommit(self, commit, shelved=False, shelved_cl=0):
3003 files = []
3004 fnum = 0
3005 while "depotFile%s" % fnum in commit:
3006 path = commit["depotFile%s" % fnum]
3007 found = self.isPathWanted(decode_path(path))
3008 if not found:
3009 fnum = fnum + 1
3010 continue
3012 file = {}
3013 file["path"] = path
3014 file["rev"] = commit["rev%s" % fnum]
3015 file["action"] = commit["action%s" % fnum]
3016 file["type"] = commit["type%s" % fnum]
3017 if shelved:
3018 file["shelved_cl"] = int(shelved_cl)
3019 files.append(file)
3020 fnum = fnum + 1
3021 return files
3023 def extractJobsFromCommit(self, commit):
3024 jobs = []
3025 jnum = 0
3026 while "job%s" % jnum in commit:
3027 job = commit["job%s" % jnum]
3028 jobs.append(job)
3029 jnum = jnum + 1
3030 return jobs
3032 def stripRepoPath(self, path, prefixes):
3033 """When streaming files, this is called to map a p4 depot path to where
3034 it should go in git. The prefixes are either self.depotPaths, or
3035 self.branchPrefixes in the case of branch detection.
3038 if self.useClientSpec:
3039 # branch detection moves files up a level (the branch name)
3040 # from what client spec interpretation gives
3041 path = decode_path(self.clientSpecDirs.map_in_client(path))
3042 if self.detectBranches:
3043 for b in self.knownBranches:
3044 if p4PathStartsWith(path, b + "/"):
3045 path = path[len(b)+1:]
3047 elif self.keepRepoPath:
3048 # Preserve everything in relative path name except leading
3049 # //depot/; just look at first prefix as they all should
3050 # be in the same depot.
3051 depot = re.sub(r"^(//[^/]+/).*", r'\1', prefixes[0])
3052 if p4PathStartsWith(path, depot):
3053 path = path[len(depot):]
3055 else:
3056 for p in prefixes:
3057 if p4PathStartsWith(path, p):
3058 path = path[len(p):]
3059 break
3061 path = wildcard_decode(path)
3062 return path
3064 def splitFilesIntoBranches(self, commit):
3065 """Look at each depotFile in the commit to figure out to what branch it
3066 belongs.
3069 if self.clientSpecDirs:
3070 files = self.extractFilesFromCommit(commit)
3071 self.clientSpecDirs.update_client_spec_path_cache(files)
3073 branches = {}
3074 fnum = 0
3075 while "depotFile%s" % fnum in commit:
3076 raw_path = commit["depotFile%s" % fnum]
3077 path = decode_path(raw_path)
3078 found = self.isPathWanted(path)
3079 if not found:
3080 fnum = fnum + 1
3081 continue
3083 file = {}
3084 file["path"] = raw_path
3085 file["rev"] = commit["rev%s" % fnum]
3086 file["action"] = commit["action%s" % fnum]
3087 file["type"] = commit["type%s" % fnum]
3088 fnum = fnum + 1
3090 # start with the full relative path where this file would
3091 # go in a p4 client
3092 if self.useClientSpec:
3093 relPath = decode_path(self.clientSpecDirs.map_in_client(path))
3094 else:
3095 relPath = self.stripRepoPath(path, self.depotPaths)
3097 for branch in self.knownBranches.keys():
3098 # add a trailing slash so that a commit into qt/4.2foo
3099 # doesn't end up in qt/4.2, e.g.
3100 if p4PathStartsWith(relPath, branch + "/"):
3101 if branch not in branches:
3102 branches[branch] = []
3103 branches[branch].append(file)
3104 break
3106 return branches
3108 def writeToGitStream(self, gitMode, relPath, contents):
3109 self.gitStream.write(encode_text_stream(u'M {} inline {}\n'.format(gitMode, relPath)))
3110 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
3111 for d in contents:
3112 self.gitStream.write(d)
3113 self.gitStream.write('\n')
3115 def encodeWithUTF8(self, path):
3116 try:
3117 path.decode('ascii')
3118 except:
3119 encoding = 'utf8'
3120 if gitConfig('git-p4.pathEncoding'):
3121 encoding = gitConfig('git-p4.pathEncoding')
3122 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
3123 if self.verbose:
3124 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
3125 return path
3127 def streamOneP4File(self, file, contents):
3128 """Output one file from the P4 stream.
3130 This is a helper for streamP4Files().
3133 file_path = file['depotFile']
3134 relPath = self.stripRepoPath(decode_path(file_path), self.branchPrefixes)
3136 if verbose:
3137 if 'fileSize' in self.stream_file:
3138 size = int(self.stream_file['fileSize'])
3139 else:
3140 # Deleted files don't get a fileSize apparently
3141 size = 0
3142 sys.stdout.write('\r%s --> %s (%s)\n' % (
3143 file_path, relPath, format_size_human_readable(size)))
3144 sys.stdout.flush()
3146 type_base, type_mods = split_p4_type(file["type"])
3148 git_mode = "100644"
3149 if "x" in type_mods:
3150 git_mode = "100755"
3151 if type_base == "symlink":
3152 git_mode = "120000"
3153 # p4 print on a symlink sometimes contains "target\n";
3154 # if it does, remove the newline
3155 data = ''.join(decode_text_stream(c) for c in contents)
3156 if not data:
3157 # Some version of p4 allowed creating a symlink that pointed
3158 # to nothing. This causes p4 errors when checking out such
3159 # a change, and errors here too. Work around it by ignoring
3160 # the bad symlink; hopefully a future change fixes it.
3161 print("\nIgnoring empty symlink in %s" % file_path)
3162 return
3163 elif data[-1] == '\n':
3164 contents = [data[:-1]]
3165 else:
3166 contents = [data]
3168 if type_base == "utf16":
3169 # p4 delivers different text in the python output to -G
3170 # than it does when using "print -o", or normal p4 client
3171 # operations. utf16 is converted to ascii or utf8, perhaps.
3172 # But ascii text saved as -t utf16 is completely mangled.
3173 # Invoke print -o to get the real contents.
3175 # On windows, the newlines will always be mangled by print, so put
3176 # them back too. This is not needed to the cygwin windows version,
3177 # just the native "NT" type.
3179 try:
3180 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw=True)
3181 except Exception as e:
3182 if 'Translation of file content failed' in str(e):
3183 type_base = 'binary'
3184 else:
3185 raise e
3186 else:
3187 if p4_version_string().find('/NT') >= 0:
3188 text = text.replace(b'\x0d\x00\x0a\x00', b'\x0a\x00')
3189 contents = [text]
3191 if type_base == "apple":
3192 # Apple filetype files will be streamed as a concatenation of
3193 # its appledouble header and the contents. This is useless
3194 # on both macs and non-macs. If using "print -q -o xx", it
3195 # will create "xx" with the data, and "%xx" with the header.
3196 # This is also not very useful.
3198 # Ideally, someday, this script can learn how to generate
3199 # appledouble files directly and import those to git, but
3200 # non-mac machines can never find a use for apple filetype.
3201 print("\nIgnoring apple filetype file %s" % file['depotFile'])
3202 return
3204 if type_base == "utf8":
3205 # The type utf8 explicitly means utf8 *with BOM*. These are
3206 # streamed just like regular text files, however, without
3207 # the BOM in the stream.
3208 # Therefore, to accurately import these files into git, we
3209 # need to explicitly re-add the BOM before writing.
3210 # 'contents' is a set of bytes in this case, so create the
3211 # BOM prefix as a b'' literal.
3212 contents = [b'\xef\xbb\xbf' + contents[0]] + contents[1:]
3214 # Note that we do not try to de-mangle keywords on utf16 files,
3215 # even though in theory somebody may want that.
3216 regexp = p4_keywords_regexp_for_type(type_base, type_mods)
3217 if regexp:
3218 contents = [regexp.sub(br'$\1$', c) for c in contents]
3220 if self.largeFileSystem:
3221 git_mode, contents = self.largeFileSystem.processContent(git_mode, relPath, contents)
3223 self.writeToGitStream(git_mode, relPath, contents)
3225 def streamOneP4Deletion(self, file):
3226 relPath = self.stripRepoPath(decode_path(file['path']), self.branchPrefixes)
3227 if verbose:
3228 sys.stdout.write("delete %s\n" % relPath)
3229 sys.stdout.flush()
3230 self.gitStream.write(encode_text_stream(u'D {}\n'.format(relPath)))
3232 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
3233 self.largeFileSystem.removeLargeFile(relPath)
3235 def streamP4FilesCb(self, marshalled):
3236 """Handle another chunk of streaming data."""
3238 # catch p4 errors and complain
3239 err = None
3240 if "code" in marshalled:
3241 if marshalled["code"] == "error":
3242 if "data" in marshalled:
3243 err = marshalled["data"].rstrip()
3245 if not err and 'fileSize' in self.stream_file:
3246 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
3247 if required_bytes > 0:
3248 err = 'Not enough space left on %s! Free at least %s.' % (
3249 os.getcwd(), format_size_human_readable(required_bytes))
3251 if err:
3252 f = None
3253 if self.stream_have_file_info:
3254 if "depotFile" in self.stream_file:
3255 f = self.stream_file["depotFile"]
3256 # force a failure in fast-import, else an empty
3257 # commit will be made
3258 self.gitStream.write("\n")
3259 self.gitStream.write("die-now\n")
3260 self.gitStream.close()
3261 # ignore errors, but make sure it exits first
3262 self.importProcess.wait()
3263 if f:
3264 die("Error from p4 print for %s: %s" % (f, err))
3265 else:
3266 die("Error from p4 print: %s" % err)
3268 if 'depotFile' in marshalled and self.stream_have_file_info:
3269 # start of a new file - output the old one first
3270 self.streamOneP4File(self.stream_file, self.stream_contents)
3271 self.stream_file = {}
3272 self.stream_contents = []
3273 self.stream_have_file_info = False
3275 # pick up the new file information... for the
3276 # 'data' field we need to append to our array
3277 for k in marshalled.keys():
3278 if k == 'data':
3279 if 'streamContentSize' not in self.stream_file:
3280 self.stream_file['streamContentSize'] = 0
3281 self.stream_file['streamContentSize'] += len(marshalled['data'])
3282 self.stream_contents.append(marshalled['data'])
3283 else:
3284 self.stream_file[k] = marshalled[k]
3286 if (verbose and
3287 'streamContentSize' in self.stream_file and
3288 'fileSize' in self.stream_file and
3289 'depotFile' in self.stream_file):
3290 size = int(self.stream_file["fileSize"])
3291 if size > 0:
3292 progress = 100*self.stream_file['streamContentSize']/size
3293 sys.stdout.write('\r%s %d%% (%s)' % (
3294 self.stream_file['depotFile'], progress,
3295 format_size_human_readable(size)))
3296 sys.stdout.flush()
3298 self.stream_have_file_info = True
3300 def streamP4Files(self, files):
3301 """Stream directly from "p4 files" into "git fast-import."""
3303 filesForCommit = []
3304 filesToRead = []
3305 filesToDelete = []
3307 for f in files:
3308 filesForCommit.append(f)
3309 if f['action'] in self.delete_actions:
3310 filesToDelete.append(f)
3311 else:
3312 filesToRead.append(f)
3314 # deleted files...
3315 for f in filesToDelete:
3316 self.streamOneP4Deletion(f)
3318 if len(filesToRead) > 0:
3319 self.stream_file = {}
3320 self.stream_contents = []
3321 self.stream_have_file_info = False
3323 # curry self argument
3324 def streamP4FilesCbSelf(entry):
3325 self.streamP4FilesCb(entry)
3327 fileArgs = []
3328 for f in filesToRead:
3329 if 'shelved_cl' in f:
3330 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3331 # the contents
3332 fileArg = f['path'] + encode_text_stream('@={}'.format(f['shelved_cl']))
3333 else:
3334 fileArg = f['path'] + encode_text_stream('#{}'.format(f['rev']))
3336 fileArgs.append(fileArg)
3338 p4CmdList(["-x", "-", "print"],
3339 stdin=fileArgs,
3340 cb=streamP4FilesCbSelf)
3342 # do the last chunk
3343 if 'depotFile' in self.stream_file:
3344 self.streamOneP4File(self.stream_file, self.stream_contents)
3346 def make_email(self, userid):
3347 if userid in self.users:
3348 return self.users[userid]
3349 else:
3350 userid_bytes = metadata_stream_to_writable_bytes(userid)
3351 return b"%s <a@b>" % userid_bytes
3353 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
3354 """Stream a p4 tag.
3356 Commit is either a git commit, or a fast-import mark, ":<p4commit>".
3359 if verbose:
3360 print("writing tag %s for commit %s" % (labelName, commit))
3361 gitStream.write("tag %s\n" % labelName)
3362 gitStream.write("from %s\n" % commit)
3364 if 'Owner' in labelDetails:
3365 owner = labelDetails["Owner"]
3366 else:
3367 owner = None
3369 # Try to use the owner of the p4 label, or failing that,
3370 # the current p4 user id.
3371 if owner:
3372 email = self.make_email(owner)
3373 else:
3374 email = self.make_email(self.p4UserId())
3376 gitStream.write("tagger ")
3377 gitStream.write(email)
3378 gitStream.write(" %s %s\n" % (epoch, self.tz))
3380 print("labelDetails=", labelDetails)
3381 if 'Description' in labelDetails:
3382 description = labelDetails['Description']
3383 else:
3384 description = 'Label from git p4'
3386 gitStream.write("data %d\n" % len(description))
3387 gitStream.write(description)
3388 gitStream.write("\n")
3390 def inClientSpec(self, path):
3391 if not self.clientSpecDirs:
3392 return True
3393 inClientSpec = self.clientSpecDirs.map_in_client(path)
3394 if not inClientSpec and self.verbose:
3395 print('Ignoring file outside of client spec: {0}'.format(path))
3396 return inClientSpec
3398 def hasBranchPrefix(self, path):
3399 if not self.branchPrefixes:
3400 return True
3401 hasPrefix = [p for p in self.branchPrefixes
3402 if p4PathStartsWith(path, p)]
3403 if not hasPrefix and self.verbose:
3404 print('Ignoring file outside of prefix: {0}'.format(path))
3405 return hasPrefix
3407 def findShadowedFiles(self, files, change):
3408 """Perforce allows you commit files and directories with the same name,
3409 so you could have files //depot/foo and //depot/foo/bar both checked
3410 in. A p4 sync of a repository in this state fails. Deleting one of
3411 the files recovers the repository.
3413 Git will not allow the broken state to exist and only the most
3414 recent of the conflicting names is left in the repository. When one
3415 of the conflicting files is deleted we need to re-add the other one
3416 to make sure the git repository recovers in the same way as
3417 perforce.
3420 deleted = [f for f in files if f['action'] in self.delete_actions]
3421 to_check = set()
3422 for f in deleted:
3423 path = decode_path(f['path'])
3424 to_check.add(path + '/...')
3425 while True:
3426 path = path.rsplit("/", 1)[0]
3427 if path == "/" or path in to_check:
3428 break
3429 to_check.add(path)
3430 to_check = ['%s@%s' % (wildcard_encode(p), change) for p in to_check
3431 if self.hasBranchPrefix(p)]
3432 if to_check:
3433 stat_result = p4CmdList(["-x", "-", "fstat", "-T",
3434 "depotFile,headAction,headRev,headType"], stdin=to_check)
3435 for record in stat_result:
3436 if record['code'] != 'stat':
3437 continue
3438 if record['headAction'] in self.delete_actions:
3439 continue
3440 files.append({
3441 'action': 'add',
3442 'path': record['depotFile'],
3443 'rev': record['headRev'],
3444 'type': record['headType']})
3446 def commit(self, details, files, branch, parent="", allow_empty=False):
3447 epoch = details["time"]
3448 author = details["user"]
3449 jobs = self.extractJobsFromCommit(details)
3451 if self.verbose:
3452 print('commit into {0}'.format(branch))
3454 files = [f for f in files
3455 if self.hasBranchPrefix(decode_path(f['path']))]
3456 self.findShadowedFiles(files, details['change'])
3458 if self.clientSpecDirs:
3459 self.clientSpecDirs.update_client_spec_path_cache(files)
3461 files = [f for f in files if self.inClientSpec(decode_path(f['path']))]
3463 if gitConfigBool('git-p4.keepEmptyCommits'):
3464 allow_empty = True
3466 if not files and not allow_empty:
3467 print('Ignoring revision {0} as it would produce an empty commit.'
3468 .format(details['change']))
3469 return
3471 self.gitStream.write("commit %s\n" % branch)
3472 self.gitStream.write("mark :%s\n" % details["change"])
3473 self.committedChanges.add(int(details["change"]))
3474 if author not in self.users:
3475 self.getUserMapFromPerforceServer()
3477 self.gitStream.write("committer ")
3478 self.gitStream.write(self.make_email(author))
3479 self.gitStream.write(" %s %s\n" % (epoch, self.tz))
3481 self.gitStream.write("data <<EOT\n")
3482 self.gitStream.write(details["desc"])
3483 if len(jobs) > 0:
3484 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3486 if not self.suppress_meta_comment:
3487 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3488 (','.join(self.branchPrefixes), details["change"]))
3489 if len(details['options']) > 0:
3490 self.gitStream.write(": options = %s" % details['options'])
3491 self.gitStream.write("]\n")
3493 self.gitStream.write("EOT\n\n")
3495 if len(parent) > 0:
3496 if self.verbose:
3497 print("parent %s" % parent)
3498 self.gitStream.write("from %s\n" % parent)
3500 self.streamP4Files(files)
3501 self.gitStream.write("\n")
3503 change = int(details["change"])
3505 if change in self.labels:
3506 label = self.labels[change]
3507 labelDetails = label[0]
3508 labelRevisions = label[1]
3509 if self.verbose:
3510 print("Change %s is labelled %s" % (change, labelDetails))
3512 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3513 for p in self.branchPrefixes])
3515 if len(files) == len(labelRevisions):
3517 cleanedFiles = {}
3518 for info in files:
3519 if info["action"] in self.delete_actions:
3520 continue
3521 cleanedFiles[info["depotFile"]] = info["rev"]
3523 if cleanedFiles == labelRevisions:
3524 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3526 else:
3527 if not self.silent:
3528 print("Tag %s does not match with change %s: files do not match."
3529 % (labelDetails["label"], change))
3531 else:
3532 if not self.silent:
3533 print("Tag %s does not match with change %s: file count is different."
3534 % (labelDetails["label"], change))
3536 def getLabels(self):
3537 """Build a dictionary of changelists and labels, for "detect-labels"
3538 option.
3541 self.labels = {}
3543 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3544 if len(l) > 0 and not self.silent:
3545 print("Finding files belonging to labels in %s" % self.depotPaths)
3547 for output in l:
3548 label = output["label"]
3549 revisions = {}
3550 newestChange = 0
3551 if self.verbose:
3552 print("Querying files for label %s" % label)
3553 for file in p4CmdList(["files"] +
3554 ["%s...@%s" % (p, label)
3555 for p in self.depotPaths]):
3556 revisions[file["depotFile"]] = file["rev"]
3557 change = int(file["change"])
3558 if change > newestChange:
3559 newestChange = change
3561 self.labels[newestChange] = [output, revisions]
3563 if self.verbose:
3564 print("Label changes: %s" % self.labels.keys())
3566 def importP4Labels(self, stream, p4Labels):
3567 """Import p4 labels as git tags. A direct mapping does not exist, so
3568 assume that if all the files are at the same revision then we can
3569 use that, or it's something more complicated we should just ignore.
3572 if verbose:
3573 print("import p4 labels: " + ' '.join(p4Labels))
3575 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3576 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3577 if len(validLabelRegexp) == 0:
3578 validLabelRegexp = defaultLabelRegexp
3579 m = re.compile(validLabelRegexp)
3581 for name in p4Labels:
3582 commitFound = False
3584 if not m.match(name):
3585 if verbose:
3586 print("label %s does not match regexp %s" % (name, validLabelRegexp))
3587 continue
3589 if name in ignoredP4Labels:
3590 continue
3592 labelDetails = p4CmdList(['label', "-o", name])[0]
3594 # get the most recent changelist for each file in this label
3595 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3596 for p in self.depotPaths])
3598 if 'change' in change:
3599 # find the corresponding git commit; take the oldest commit
3600 changelist = int(change['change'])
3601 if changelist in self.committedChanges:
3602 gitCommit = ":%d" % changelist # use a fast-import mark
3603 commitFound = True
3604 else:
3605 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3606 "--reverse", r":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3607 if len(gitCommit) == 0:
3608 print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3609 else:
3610 commitFound = True
3611 gitCommit = gitCommit.strip()
3613 if commitFound:
3614 # Convert from p4 time format
3615 try:
3616 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3617 except ValueError:
3618 print("Could not convert label time %s" % labelDetails['Update'])
3619 tmwhen = 1
3621 when = int(time.mktime(tmwhen))
3622 self.streamTag(stream, name, labelDetails, gitCommit, when)
3623 if verbose:
3624 print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3625 else:
3626 if verbose:
3627 print("Label %s has no changelists - possibly deleted?" % name)
3629 if not commitFound:
3630 # We can't import this label; don't try again as it will get very
3631 # expensive repeatedly fetching all the files for labels that will
3632 # never be imported. If the label is moved in the future, the
3633 # ignore will need to be removed manually.
3634 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3636 def guessProjectName(self):
3637 for p in self.depotPaths:
3638 if p.endswith("/"):
3639 p = p[:-1]
3640 p = p[p.strip().rfind("/") + 1:]
3641 if not p.endswith("/"):
3642 p += "/"
3643 return p
3645 def getBranchMapping(self):
3646 lostAndFoundBranches = set()
3648 user = gitConfig("git-p4.branchUser")
3650 for info in p4CmdList(
3651 ["branches"] + (["-u", user] if len(user) > 0 else [])):
3652 details = p4Cmd(["branch", "-o", info["branch"]])
3653 viewIdx = 0
3654 while "View%s" % viewIdx in details:
3655 paths = details["View%s" % viewIdx].split(" ")
3656 viewIdx = viewIdx + 1
3657 # require standard //depot/foo/... //depot/bar/... mapping
3658 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3659 continue
3660 source = paths[0]
3661 destination = paths[1]
3662 # HACK
3663 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3664 source = source[len(self.depotPaths[0]):-4]
3665 destination = destination[len(self.depotPaths[0]):-4]
3667 if destination in self.knownBranches:
3668 if not self.silent:
3669 print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3670 print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3671 continue
3673 self.knownBranches[destination] = source
3675 lostAndFoundBranches.discard(destination)
3677 if source not in self.knownBranches:
3678 lostAndFoundBranches.add(source)
3680 # Perforce does not strictly require branches to be defined, so we also
3681 # check git config for a branch list.
3683 # Example of branch definition in git config file:
3684 # [git-p4]
3685 # branchList=main:branchA
3686 # branchList=main:branchB
3687 # branchList=branchA:branchC
3688 configBranches = gitConfigList("git-p4.branchList")
3689 for branch in configBranches:
3690 if branch:
3691 source, destination = branch.split(":")
3692 self.knownBranches[destination] = source
3694 lostAndFoundBranches.discard(destination)
3696 if source not in self.knownBranches:
3697 lostAndFoundBranches.add(source)
3699 for branch in lostAndFoundBranches:
3700 self.knownBranches[branch] = branch
3702 def getBranchMappingFromGitBranches(self):
3703 branches = p4BranchesInGit(self.importIntoRemotes)
3704 for branch in branches.keys():
3705 if branch == "master":
3706 branch = "main"
3707 else:
3708 branch = branch[len(self.projectName):]
3709 self.knownBranches[branch] = branch
3711 def updateOptionDict(self, d):
3712 option_keys = {}
3713 if self.keepRepoPath:
3714 option_keys['keepRepoPath'] = 1
3716 d["options"] = ' '.join(sorted(option_keys.keys()))
3718 def readOptions(self, d):
3719 self.keepRepoPath = ('options' in d
3720 and ('keepRepoPath' in d['options']))
3722 def gitRefForBranch(self, branch):
3723 if branch == "main":
3724 return self.refPrefix + "master"
3726 if len(branch) <= 0:
3727 return branch
3729 return self.refPrefix + self.projectName + branch
3731 def gitCommitByP4Change(self, ref, change):
3732 if self.verbose:
3733 print("looking in ref " + ref + " for change %s using bisect..." % change)
3735 earliestCommit = ""
3736 latestCommit = parseRevision(ref)
3738 while True:
3739 if self.verbose:
3740 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3741 next = read_pipe(["git", "rev-list", "--bisect",
3742 latestCommit, earliestCommit]).strip()
3743 if len(next) == 0:
3744 if self.verbose:
3745 print("argh")
3746 return ""
3747 log = extractLogMessageFromGitCommit(next)
3748 settings = extractSettingsGitLog(log)
3749 currentChange = int(settings['change'])
3750 if self.verbose:
3751 print("current change %s" % currentChange)
3753 if currentChange == change:
3754 if self.verbose:
3755 print("found %s" % next)
3756 return next
3758 if currentChange < change:
3759 earliestCommit = "^%s" % next
3760 else:
3761 if next == latestCommit:
3762 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3763 latestCommit = "%s^@" % next
3765 return ""
3767 def importNewBranch(self, branch, maxChange):
3768 # make fast-import flush all changes to disk and update the refs using the checkpoint
3769 # command so that we can try to find the branch parent in the git history
3770 self.gitStream.write("checkpoint\n\n")
3771 self.gitStream.flush()
3772 branchPrefix = self.depotPaths[0] + branch + "/"
3773 range = "@1,%s" % maxChange
3774 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3775 if len(changes) <= 0:
3776 return False
3777 firstChange = changes[0]
3778 sourceBranch = self.knownBranches[branch]
3779 sourceDepotPath = self.depotPaths[0] + sourceBranch
3780 sourceRef = self.gitRefForBranch(sourceBranch)
3782 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3783 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3784 if len(gitParent) > 0:
3785 self.initialParents[self.gitRefForBranch(branch)] = gitParent
3787 self.importChanges(changes)
3788 return True
3790 def searchParent(self, parent, branch, target):
3791 targetTree = read_pipe(["git", "rev-parse",
3792 "{}^{{tree}}".format(target)]).strip()
3793 for line in read_pipe_lines(["git", "rev-list", "--format=%H %T",
3794 "--no-merges", parent]):
3795 if line.startswith("commit "):
3796 continue
3797 commit, tree = line.strip().split(" ")
3798 if tree == targetTree:
3799 if self.verbose:
3800 print("Found parent of %s in commit %s" % (branch, commit))
3801 return commit
3802 return None
3804 def importChanges(self, changes, origin_revision=0):
3805 cnt = 1
3806 for change in changes:
3807 description = p4_describe(change)
3808 self.updateOptionDict(description)
3810 if not self.silent:
3811 sys.stdout.write("\rImporting revision %s (%d%%)" % (
3812 change, (cnt * 100) // len(changes)))
3813 sys.stdout.flush()
3814 cnt = cnt + 1
3816 try:
3817 if self.detectBranches:
3818 branches = self.splitFilesIntoBranches(description)
3819 for branch in branches.keys():
3820 # HACK --hwn
3821 branchPrefix = self.depotPaths[0] + branch + "/"
3822 self.branchPrefixes = [branchPrefix]
3824 parent = ""
3826 filesForCommit = branches[branch]
3828 if self.verbose:
3829 print("branch is %s" % branch)
3831 self.updatedBranches.add(branch)
3833 if branch not in self.createdBranches:
3834 self.createdBranches.add(branch)
3835 parent = self.knownBranches[branch]
3836 if parent == branch:
3837 parent = ""
3838 else:
3839 fullBranch = self.projectName + branch
3840 if fullBranch not in self.p4BranchesInGit:
3841 if not self.silent:
3842 print("\n Importing new branch %s" % fullBranch)
3843 if self.importNewBranch(branch, change - 1):
3844 parent = ""
3845 self.p4BranchesInGit.append(fullBranch)
3846 if not self.silent:
3847 print("\n Resuming with change %s" % change)
3849 if self.verbose:
3850 print("parent determined through known branches: %s" % parent)
3852 branch = self.gitRefForBranch(branch)
3853 parent = self.gitRefForBranch(parent)
3855 if self.verbose:
3856 print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3858 if len(parent) == 0 and branch in self.initialParents:
3859 parent = self.initialParents[branch]
3860 del self.initialParents[branch]
3862 blob = None
3863 if len(parent) > 0:
3864 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3865 if self.verbose:
3866 print("Creating temporary branch: " + tempBranch)
3867 self.commit(description, filesForCommit, tempBranch)
3868 self.tempBranches.append(tempBranch)
3869 self.checkpoint()
3870 blob = self.searchParent(parent, branch, tempBranch)
3871 if blob:
3872 self.commit(description, filesForCommit, branch, blob)
3873 else:
3874 if self.verbose:
3875 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3876 self.commit(description, filesForCommit, branch, parent)
3877 else:
3878 files = self.extractFilesFromCommit(description)
3879 self.commit(description, files, self.branch,
3880 self.initialParent)
3881 # only needed once, to connect to the previous commit
3882 self.initialParent = ""
3883 except IOError:
3884 print(self.gitError.read())
3885 sys.exit(1)
3887 def sync_origin_only(self):
3888 if self.syncWithOrigin:
3889 self.hasOrigin = originP4BranchesExist()
3890 if self.hasOrigin:
3891 if not self.silent:
3892 print('Syncing with origin first, using "git fetch origin"')
3893 system(["git", "fetch", "origin"])
3895 def importHeadRevision(self, revision):
3896 print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3898 details = {}
3899 details["user"] = "git perforce import user"
3900 details["desc"] = ("Initial import of %s from the state at revision %s\n"
3901 % (' '.join(self.depotPaths), revision))
3902 details["change"] = revision
3903 newestRevision = 0
3905 fileCnt = 0
3906 fileArgs = ["%s...%s" % (p, revision) for p in self.depotPaths]
3908 for info in p4CmdList(["files"] + fileArgs):
3910 if 'code' in info and info['code'] == 'error':
3911 sys.stderr.write("p4 returned an error: %s\n"
3912 % info['data'])
3913 if info['data'].find("must refer to client") >= 0:
3914 sys.stderr.write("This particular p4 error is misleading.\n")
3915 sys.stderr.write("Perhaps the depot path was misspelled.\n")
3916 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
3917 sys.exit(1)
3918 if 'p4ExitCode' in info:
3919 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3920 sys.exit(1)
3922 change = int(info["change"])
3923 if change > newestRevision:
3924 newestRevision = change
3926 if info["action"] in self.delete_actions:
3927 continue
3929 for prop in ["depotFile", "rev", "action", "type"]:
3930 details["%s%s" % (prop, fileCnt)] = info[prop]
3932 fileCnt = fileCnt + 1
3934 details["change"] = newestRevision
3936 # Use time from top-most change so that all git p4 clones of
3937 # the same p4 repo have the same commit SHA1s.
3938 res = p4_describe(newestRevision)
3939 details["time"] = res["time"]
3941 self.updateOptionDict(details)
3942 try:
3943 self.commit(details, self.extractFilesFromCommit(details), self.branch)
3944 except IOError as err:
3945 print("IO error with git fast-import. Is your git version recent enough?")
3946 print("IO error details: {}".format(err))
3947 print(self.gitError.read())
3949 def importRevisions(self, args, branch_arg_given):
3950 changes = []
3952 if len(self.changesFile) > 0:
3953 with open(self.changesFile) as f:
3954 output = f.readlines()
3955 changeSet = set()
3956 for line in output:
3957 changeSet.add(int(line))
3959 for change in changeSet:
3960 changes.append(change)
3962 changes.sort()
3963 else:
3964 # catch "git p4 sync" with no new branches, in a repo that
3965 # does not have any existing p4 branches
3966 if len(args) == 0:
3967 if not self.p4BranchesInGit:
3968 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3970 # The default branch is master, unless --branch is used to
3971 # specify something else. Make sure it exists, or complain
3972 # nicely about how to use --branch.
3973 if not self.detectBranches:
3974 if not branch_exists(self.branch):
3975 if branch_arg_given:
3976 raise P4CommandException("Error: branch %s does not exist." % self.branch)
3977 else:
3978 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
3979 self.branch)
3981 if self.verbose:
3982 print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3983 self.changeRange))
3984 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3986 if len(self.maxChanges) > 0:
3987 changes = changes[:min(int(self.maxChanges), len(changes))]
3989 if len(changes) == 0:
3990 if not self.silent:
3991 print("No changes to import!")
3992 else:
3993 if not self.silent and not self.detectBranches:
3994 print("Import destination: %s" % self.branch)
3996 self.updatedBranches = set()
3998 if not self.detectBranches:
3999 if args:
4000 # start a new branch
4001 self.initialParent = ""
4002 else:
4003 # build on a previous revision
4004 self.initialParent = parseRevision(self.branch)
4006 self.importChanges(changes)
4008 if not self.silent:
4009 print("")
4010 if len(self.updatedBranches) > 0:
4011 sys.stdout.write("Updated branches: ")
4012 for b in self.updatedBranches:
4013 sys.stdout.write("%s " % b)
4014 sys.stdout.write("\n")
4016 def openStreams(self):
4017 self.importProcess = subprocess.Popen(["git", "fast-import"],
4018 stdin=subprocess.PIPE,
4019 stdout=subprocess.PIPE,
4020 stderr=subprocess.PIPE)
4021 self.gitOutput = self.importProcess.stdout
4022 self.gitStream = self.importProcess.stdin
4023 self.gitError = self.importProcess.stderr
4025 if bytes is not str:
4026 # Wrap gitStream.write() so that it can be called using `str` arguments
4027 def make_encoded_write(write):
4028 def encoded_write(s):
4029 return write(s.encode() if isinstance(s, str) else s)
4030 return encoded_write
4032 self.gitStream.write = make_encoded_write(self.gitStream.write)
4034 def closeStreams(self):
4035 if self.gitStream is None:
4036 return
4037 self.gitStream.close()
4038 if self.importProcess.wait() != 0:
4039 die("fast-import failed: %s" % self.gitError.read())
4040 self.gitOutput.close()
4041 self.gitError.close()
4042 self.gitStream = None
4044 def run(self, args):
4045 if self.importIntoRemotes:
4046 self.refPrefix = "refs/remotes/p4/"
4047 else:
4048 self.refPrefix = "refs/heads/p4/"
4050 self.sync_origin_only()
4052 branch_arg_given = bool(self.branch)
4053 if len(self.branch) == 0:
4054 self.branch = self.refPrefix + "master"
4055 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
4056 system(["git", "update-ref", self.branch, "refs/heads/p4"])
4057 system(["git", "branch", "-D", "p4"])
4059 # accept either the command-line option, or the configuration variable
4060 if self.useClientSpec:
4061 # will use this after clone to set the variable
4062 self.useClientSpec_from_options = True
4063 else:
4064 if gitConfigBool("git-p4.useclientspec"):
4065 self.useClientSpec = True
4066 if self.useClientSpec:
4067 self.clientSpecDirs = getClientSpec()
4069 # TODO: should always look at previous commits,
4070 # merge with previous imports, if possible.
4071 if args == []:
4072 if self.hasOrigin:
4073 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
4075 # branches holds mapping from branch name to sha1
4076 branches = p4BranchesInGit(self.importIntoRemotes)
4078 # restrict to just this one, disabling detect-branches
4079 if branch_arg_given:
4080 short = shortP4Ref(self.branch, self.importIntoRemotes)
4081 if short in branches:
4082 self.p4BranchesInGit = [short]
4083 elif self.branch.startswith('refs/') and \
4084 branchExists(self.branch) and \
4085 '[git-p4:' in extractLogMessageFromGitCommit(self.branch):
4086 self.p4BranchesInGit = [self.branch]
4087 else:
4088 self.p4BranchesInGit = branches.keys()
4090 if len(self.p4BranchesInGit) > 1:
4091 if not self.silent:
4092 print("Importing from/into multiple branches")
4093 self.detectBranches = True
4094 for branch in branches.keys():
4095 self.initialParents[self.refPrefix + branch] = \
4096 branches[branch]
4098 if self.verbose:
4099 print("branches: %s" % self.p4BranchesInGit)
4101 p4Change = 0
4102 for branch in self.p4BranchesInGit:
4103 logMsg = extractLogMessageFromGitCommit(fullP4Ref(branch,
4104 self.importIntoRemotes))
4106 settings = extractSettingsGitLog(logMsg)
4108 self.readOptions(settings)
4109 if 'depot-paths' in settings and 'change' in settings:
4110 change = int(settings['change']) + 1
4111 p4Change = max(p4Change, change)
4113 depotPaths = sorted(settings['depot-paths'])
4114 if self.previousDepotPaths == []:
4115 self.previousDepotPaths = depotPaths
4116 else:
4117 paths = []
4118 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
4119 prev_list = prev.split("/")
4120 cur_list = cur.split("/")
4121 for i in range(0, min(len(cur_list), len(prev_list))):
4122 if cur_list[i] != prev_list[i]:
4123 i = i - 1
4124 break
4126 paths.append("/".join(cur_list[:i + 1]))
4128 self.previousDepotPaths = paths
4130 if p4Change > 0:
4131 self.depotPaths = sorted(self.previousDepotPaths)
4132 self.changeRange = "@%s,#head" % p4Change
4133 if not self.silent and not self.detectBranches:
4134 print("Performing incremental import into %s git branch" % self.branch)
4136 self.branch = fullP4Ref(self.branch, self.importIntoRemotes)
4138 if len(args) == 0 and self.depotPaths:
4139 if not self.silent:
4140 print("Depot paths: %s" % ' '.join(self.depotPaths))
4141 else:
4142 if self.depotPaths and self.depotPaths != args:
4143 print("previous import used depot path %s and now %s was specified. "
4144 "This doesn't work!" % (' '.join(self.depotPaths),
4145 ' '.join(args)))
4146 sys.exit(1)
4148 self.depotPaths = sorted(args)
4150 revision = ""
4151 self.users = {}
4153 # Make sure no revision specifiers are used when --changesfile
4154 # is specified.
4155 bad_changesfile = False
4156 if len(self.changesFile) > 0:
4157 for p in self.depotPaths:
4158 if p.find("@") >= 0 or p.find("#") >= 0:
4159 bad_changesfile = True
4160 break
4161 if bad_changesfile:
4162 die("Option --changesfile is incompatible with revision specifiers")
4164 newPaths = []
4165 for p in self.depotPaths:
4166 if p.find("@") != -1:
4167 atIdx = p.index("@")
4168 self.changeRange = p[atIdx:]
4169 if self.changeRange == "@all":
4170 self.changeRange = ""
4171 elif ',' not in self.changeRange:
4172 revision = self.changeRange
4173 self.changeRange = ""
4174 p = p[:atIdx]
4175 elif p.find("#") != -1:
4176 hashIdx = p.index("#")
4177 revision = p[hashIdx:]
4178 p = p[:hashIdx]
4179 elif self.previousDepotPaths == []:
4180 # pay attention to changesfile, if given, else import
4181 # the entire p4 tree at the head revision
4182 if len(self.changesFile) == 0:
4183 revision = "#head"
4185 p = re.sub(r"\.\.\.$", "", p)
4186 if not p.endswith("/"):
4187 p += "/"
4189 newPaths.append(p)
4191 self.depotPaths = newPaths
4193 # --detect-branches may change this for each branch
4194 self.branchPrefixes = self.depotPaths
4196 self.loadUserMapFromCache()
4197 self.labels = {}
4198 if self.detectLabels:
4199 self.getLabels()
4201 if self.detectBranches:
4202 # FIXME - what's a P4 projectName ?
4203 self.projectName = self.guessProjectName()
4205 if self.hasOrigin:
4206 self.getBranchMappingFromGitBranches()
4207 else:
4208 self.getBranchMapping()
4209 if self.verbose:
4210 print("p4-git branches: %s" % self.p4BranchesInGit)
4211 print("initial parents: %s" % self.initialParents)
4212 for b in self.p4BranchesInGit:
4213 if b != "master":
4215 # FIXME
4216 b = b[len(self.projectName):]
4217 self.createdBranches.add(b)
4219 p4_check_access()
4221 self.openStreams()
4223 err = None
4225 try:
4226 if revision:
4227 self.importHeadRevision(revision)
4228 else:
4229 self.importRevisions(args, branch_arg_given)
4231 if gitConfigBool("git-p4.importLabels"):
4232 self.importLabels = True
4234 if self.importLabels:
4235 p4Labels = getP4Labels(self.depotPaths)
4236 gitTags = getGitTags()
4238 missingP4Labels = p4Labels - gitTags
4239 self.importP4Labels(self.gitStream, missingP4Labels)
4241 except P4CommandException as e:
4242 err = e
4244 finally:
4245 self.closeStreams()
4247 if err:
4248 die(str(err))
4250 # Cleanup temporary branches created during import
4251 if self.tempBranches != []:
4252 for branch in self.tempBranches:
4253 read_pipe(["git", "update-ref", "-d", branch])
4254 if len(read_pipe(["git", "for-each-ref", self.tempBranchLocation])) > 0:
4255 die("There are unexpected temporary branches")
4257 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4258 # a convenient shortcut refname "p4".
4259 if self.importIntoRemotes:
4260 head_ref = self.refPrefix + "HEAD"
4261 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
4262 system(["git", "symbolic-ref", head_ref, self.branch])
4264 return True
4267 class P4Rebase(Command):
4268 def __init__(self):
4269 Command.__init__(self)
4270 self.options = [
4271 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
4273 self.importLabels = False
4274 self.description = ("Fetches the latest revision from perforce and "
4275 + "rebases the current work (branch) against it")
4277 def run(self, args):
4278 sync = P4Sync()
4279 sync.importLabels = self.importLabels
4280 sync.run([])
4282 return self.rebase()
4284 def rebase(self):
4285 if os.system("git update-index --refresh") != 0:
4286 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.")
4287 if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
4288 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.")
4290 upstream, settings = findUpstreamBranchPoint()
4291 if len(upstream) == 0:
4292 die("Cannot find upstream branchpoint for rebase")
4294 # the branchpoint may be p4/foo~3, so strip off the parent
4295 upstream = re.sub(r"~[0-9]+$", "", upstream)
4297 print("Rebasing the current branch onto %s" % upstream)
4298 oldHead = read_pipe(["git", "rev-parse", "HEAD"]).strip()
4299 system(["git", "rebase", upstream])
4300 system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead,
4301 "HEAD", "--"])
4302 return True
4305 class P4Clone(P4Sync):
4306 def __init__(self):
4307 P4Sync.__init__(self)
4308 self.description = "Creates a new git repository and imports from Perforce into it"
4309 self.usage = "usage: %prog [options] //depot/path[@revRange]"
4310 self.options += [
4311 optparse.make_option("--destination", dest="cloneDestination",
4312 action='store', default=None,
4313 help="where to leave result of the clone"),
4314 optparse.make_option("--bare", dest="cloneBare",
4315 action="store_true", default=False),
4317 self.cloneDestination = None
4318 self.needsGit = False
4319 self.cloneBare = False
4321 def defaultDestination(self, args):
4322 # TODO: use common prefix of args?
4323 depotPath = args[0]
4324 depotDir = re.sub(r"(@[^@]*)$", "", depotPath)
4325 depotDir = re.sub(r"(#[^#]*)$", "", depotDir)
4326 depotDir = re.sub(r"\.\.\.$", "", depotDir)
4327 depotDir = re.sub(r"/$", "", depotDir)
4328 return os.path.split(depotDir)[1]
4330 def run(self, args):
4331 if len(args) < 1:
4332 return False
4334 if self.keepRepoPath and not self.cloneDestination:
4335 sys.stderr.write("Must specify destination for --keep-path\n")
4336 sys.exit(1)
4338 depotPaths = args
4340 if not self.cloneDestination and len(depotPaths) > 1:
4341 self.cloneDestination = depotPaths[-1]
4342 depotPaths = depotPaths[:-1]
4344 for p in depotPaths:
4345 if not p.startswith("//"):
4346 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
4347 return False
4349 if not self.cloneDestination:
4350 self.cloneDestination = self.defaultDestination(args)
4352 print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
4354 if not os.path.exists(self.cloneDestination):
4355 os.makedirs(self.cloneDestination)
4356 chdir(self.cloneDestination)
4358 init_cmd = ["git", "init"]
4359 if self.cloneBare:
4360 init_cmd.append("--bare")
4361 retcode = subprocess.call(init_cmd)
4362 if retcode:
4363 raise subprocess.CalledProcessError(retcode, init_cmd)
4365 if not P4Sync.run(self, depotPaths):
4366 return False
4368 # create a master branch and check out a work tree
4369 if gitBranchExists(self.branch):
4370 system(["git", "branch", currentGitBranch(), self.branch])
4371 if not self.cloneBare:
4372 system(["git", "checkout", "-f"])
4373 else:
4374 print('Not checking out any branch, use '
4375 '"git checkout -q -b master <branch>"')
4377 # auto-set this variable if invoked with --use-client-spec
4378 if self.useClientSpec_from_options:
4379 system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
4381 # persist any git-p4 encoding-handling config options passed in for clone:
4382 if gitConfig('git-p4.metadataDecodingStrategy'):
4383 system(["git", "config", "git-p4.metadataDecodingStrategy", gitConfig('git-p4.metadataDecodingStrategy')])
4384 if gitConfig('git-p4.metadataFallbackEncoding'):
4385 system(["git", "config", "git-p4.metadataFallbackEncoding", gitConfig('git-p4.metadataFallbackEncoding')])
4386 if gitConfig('git-p4.pathEncoding'):
4387 system(["git", "config", "git-p4.pathEncoding", gitConfig('git-p4.pathEncoding')])
4389 return True
4392 class P4Unshelve(Command):
4393 def __init__(self):
4394 Command.__init__(self)
4395 self.options = []
4396 self.origin = "HEAD"
4397 self.description = "Unshelve a P4 changelist into a git commit"
4398 self.usage = "usage: %prog [options] changelist"
4399 self.options += [
4400 optparse.make_option("--origin", dest="origin",
4401 help="Use this base revision instead of the default (%s)" % self.origin),
4403 self.verbose = False
4404 self.noCommit = False
4405 self.destbranch = "refs/remotes/p4-unshelved"
4407 def renameBranch(self, branch_name):
4408 """Rename the existing branch to branch_name.N ."""
4410 for i in range(0, 1000):
4411 backup_branch_name = "{0}.{1}".format(branch_name, i)
4412 if not gitBranchExists(backup_branch_name):
4413 # Copy ref to backup
4414 gitUpdateRef(backup_branch_name, branch_name)
4415 gitDeleteRef(branch_name)
4416 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4417 break
4418 else:
4419 sys.exit("gave up trying to rename existing branch {0}".format(branch_name))
4421 def findLastP4Revision(self, starting_point):
4422 """Look back from starting_point for the first commit created by git-p4
4423 to find the P4 commit we are based on, and the depot-paths.
4426 for parent in (range(65535)):
4427 log = extractLogMessageFromGitCommit("{0}~{1}".format(starting_point, parent))
4428 settings = extractSettingsGitLog(log)
4429 if 'change' in settings:
4430 return settings
4432 sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4434 def createShelveParent(self, change, branch_name, sync, origin):
4435 """Create a commit matching the parent of the shelved changelist
4436 'change'.
4438 parent_description = p4_describe(change, shelved=True)
4439 parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4440 files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4442 parent_files = []
4443 for f in files:
4444 # if it was added in the shelved changelist, it won't exist in the parent
4445 if f['action'] in self.add_actions:
4446 continue
4448 # if it was deleted in the shelved changelist it must not be deleted
4449 # in the parent - we might even need to create it if the origin branch
4450 # does not have it
4451 if f['action'] in self.delete_actions:
4452 f['action'] = 'add'
4454 parent_files.append(f)
4456 sync.commit(parent_description, parent_files, branch_name,
4457 parent=origin, allow_empty=True)
4458 print("created parent commit for {0} based on {1} in {2}".format(
4459 change, self.origin, branch_name))
4461 def run(self, args):
4462 if len(args) != 1:
4463 return False
4465 if not gitBranchExists(self.origin):
4466 sys.exit("origin branch {0} does not exist".format(self.origin))
4468 sync = P4Sync()
4469 changes = args
4471 # only one change at a time
4472 change = changes[0]
4474 # if the target branch already exists, rename it
4475 branch_name = "{0}/{1}".format(self.destbranch, change)
4476 if gitBranchExists(branch_name):
4477 self.renameBranch(branch_name)
4478 sync.branch = branch_name
4480 sync.verbose = self.verbose
4481 sync.suppress_meta_comment = True
4483 settings = self.findLastP4Revision(self.origin)
4484 sync.depotPaths = settings['depot-paths']
4485 sync.branchPrefixes = sync.depotPaths
4487 sync.openStreams()
4488 sync.loadUserMapFromCache()
4489 sync.silent = True
4491 # create a commit for the parent of the shelved changelist
4492 self.createShelveParent(change, branch_name, sync, self.origin)
4494 # create the commit for the shelved changelist itself
4495 description = p4_describe(change, True)
4496 files = sync.extractFilesFromCommit(description, True, change)
4498 sync.commit(description, files, branch_name, "")
4499 sync.closeStreams()
4501 print("unshelved changelist {0} into {1}".format(change, branch_name))
4503 return True
4506 class P4Branches(Command):
4507 def __init__(self):
4508 Command.__init__(self)
4509 self.options = []
4510 self.description = ("Shows the git branches that hold imports and their "
4511 + "corresponding perforce depot paths")
4512 self.verbose = False
4514 def run(self, args):
4515 if originP4BranchesExist():
4516 createOrUpdateBranchesFromOrigin()
4518 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
4519 line = line.strip()
4521 if not line.startswith('p4/') or line == "p4/HEAD":
4522 continue
4523 branch = line
4525 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4526 settings = extractSettingsGitLog(log)
4528 print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4529 return True
4532 class HelpFormatter(optparse.IndentedHelpFormatter):
4533 def __init__(self):
4534 optparse.IndentedHelpFormatter.__init__(self)
4536 def format_description(self, description):
4537 if description:
4538 return description + "\n"
4539 else:
4540 return ""
4543 def printUsage(commands):
4544 print("usage: %s <command> [options]" % sys.argv[0])
4545 print("")
4546 print("valid commands: %s" % ", ".join(commands))
4547 print("")
4548 print("Try %s <command> --help for command specific help." % sys.argv[0])
4549 print("")
4552 commands = {
4553 "submit": P4Submit,
4554 "commit": P4Submit,
4555 "sync": P4Sync,
4556 "rebase": P4Rebase,
4557 "clone": P4Clone,
4558 "branches": P4Branches,
4559 "unshelve": P4Unshelve,
4563 def main():
4564 if len(sys.argv[1:]) == 0:
4565 printUsage(commands.keys())
4566 sys.exit(2)
4568 cmdName = sys.argv[1]
4569 try:
4570 klass = commands[cmdName]
4571 cmd = klass()
4572 except KeyError:
4573 print("unknown command %s" % cmdName)
4574 print("")
4575 printUsage(commands.keys())
4576 sys.exit(2)
4578 options = cmd.options
4579 cmd.gitdir = os.environ.get("GIT_DIR", None)
4581 args = sys.argv[2:]
4583 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4584 if cmd.needsGit:
4585 options.append(optparse.make_option("--git-dir", dest="gitdir"))
4587 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4588 options,
4589 description=cmd.description,
4590 formatter=HelpFormatter())
4592 try:
4593 cmd, args = parser.parse_args(sys.argv[2:], cmd)
4594 except:
4595 parser.print_help()
4596 raise
4598 global verbose
4599 verbose = cmd.verbose
4600 if cmd.needsGit:
4601 if cmd.gitdir is None:
4602 cmd.gitdir = os.path.abspath(".git")
4603 if not isValidGitDir(cmd.gitdir):
4604 # "rev-parse --git-dir" without arguments will try $PWD/.git
4605 cmd.gitdir = read_pipe(["git", "rev-parse", "--git-dir"]).strip()
4606 if os.path.exists(cmd.gitdir):
4607 cdup = read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
4608 if len(cdup) > 0:
4609 chdir(cdup)
4611 if not isValidGitDir(cmd.gitdir):
4612 if isValidGitDir(cmd.gitdir + "/.git"):
4613 cmd.gitdir += "/.git"
4614 else:
4615 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4617 # so git commands invoked from the P4 workspace will succeed
4618 os.environ["GIT_DIR"] = cmd.gitdir
4620 if not cmd.run(args):
4621 parser.print_help()
4622 sys.exit(2)
4625 if __name__ == '__main__':
4626 main()