3 # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
5 # Author: Simon Hausmann <simon@lst.de>
6 # Copyright: 2007 Simon Hausmann <simon@lst.de>
8 # License: MIT <http://www.opensource.org/licenses/mit-license.php>
10 # pylint: disable=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
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")
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
60 # support basestring in python3
62 if raw_input and input:
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.
89 return '{:d} B'.format(num
)
90 for unit
in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
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.
106 user
= gitConfig("git-p4.user")
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")
116 real_cmd
+= ["-p", port
]
118 host
= gitConfig("git-p4.host")
120 real_cmd
+= ["-H", host
]
122 client
= gitConfig("git-p4.client")
124 real_cmd
+= ["-c", client
]
126 retries
= gitConfigInt("git-p4.retries")
128 # Perform 3 retries by default
131 # Provide a way to not pass this option by setting git-p4.retries to 0
132 real_cmd
+= ["-r", str(retries
)]
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
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:
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.
172 if not is_client_path
:
174 os
.environ
['PWD'] = path
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
184 st
= os
.statvfs(os
.getcwd())
185 return st
.f_bavail
* st
.f_frsize
189 """Terminate execution. Make sure that any running child processes have
190 been wait()ed for before calling this.
195 sys
.stderr
.write(msg
+ "\n")
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
))
208 sys
.stdout
.write(prompt_text
)
210 response
= sys
.stdin
.readline().strip().lower()
213 response
= response
[0]
214 if response
in choices
:
218 # We need different encoding/decoding strategies for text data being passed
219 # around in pipes depending on python version
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
228 # For python2.7, pass read strings as-is, but also allow writing unicode
229 def decode_text_stream(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
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':
263 except UnicodeDecodeError:
264 if encodingStrategy
== 'fallback' and fallbackEncoding
:
265 global encoding_fallback_warning_issued
266 global encoding_escape_warning_issued
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
279 # bytes and strings work very differently in python2 vs python3...
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()
287 escaped_bytes
+= byte
289 for byte_number
in s
:
290 if byte_number
> 127:
291 escaped_bytes
+= b
'%'
292 escaped_bytes
+= hex(byte_number
).upper().encode()[2:]
294 escaped_bytes
+= bytes([byte_number
])
297 raise MetadataDecodingException(s
)
300 def decode_path(path
):
301 """Decode a given string (bytes or otherwise) using configured path
305 encoding
= gitConfig('git-p4.pathEncoding') or 'utf_8'
307 return path
.decode(encoding
, errors
='replace') if isinstance(path
, bytes
) else path
312 path
= path
.decode(encoding
, errors
='replace')
314 print('Path with non-ASCII characters detected. Used {} to decode: {}'.format(encoding
, path
))
318 def run_git_hook(cmd
, param
=[]):
319 """Execute a hook if the hook exists."""
320 args
= ['git', 'hook', 'run', '--ignore-missing', cmd
]
325 return subprocess
.call(args
) == 0
328 def write_pipe(c
, stdin
, *k
, **kw
):
330 sys
.stderr
.write('Writing pipe: {}\n'.format(' '.join(c
)))
332 p
= subprocess
.Popen(c
, stdin
=subprocess
.PIPE
, *k
, **kw
)
334 val
= pipe
.write(stdin
)
337 die('Command failed: {}'.format(' '.join(c
)))
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.
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
)
374 die('Command failed: {}\nError: {}'.format(' '.join(c
), err
))
376 out
= decode_text_stream(out
)
380 def read_pipe_text(c
, *k
, **kw
):
381 """Read output from a command with trailing whitespace stripped. On error,
384 retcode
, out
, err
= read_pipe_full(c
, *k
, **kw
)
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
):
398 sys
.stderr
.write('Reading pipe: {}\n'.format(' '.join(c
)))
400 p
= subprocess
.Popen(c
, stdout
=subprocess
.PIPE
, *k
, **kw
)
402 lines
= pipe
.readlines()
404 lines
= [decode_text_stream(line
) for line
in lines
]
405 if pipe
.close() or p
.wait():
406 die('Command failed: {}'.format(' '.join(c
)))
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
)
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
434 if not p4_has_command("move"):
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:
443 if err
.find("disabled") >= 0:
445 # assume it failed because @... was invalid changelist
449 def system(cmd
, ignore_error
=False, *k
, **kw
):
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
)
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
)
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")
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")
489 # we get here if we couldn't connect and there was nothing to unmarshal
490 die_bad_access("could not connect")
493 expiry
= result
.get("TicketExpiration")
496 if expiry
> min_expiration
:
500 die_bad_access("perforce ticket expires in {0} seconds".format(expiry
))
503 # account without a timeout - all ok
506 elif code
== "error":
507 data
= result
.get("data")
509 die_bad_access("p4 error: {0}".format(data
))
511 die_bad_access("unknown error")
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.
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
)])
546 """Forcibly add file names with wildcards."""
547 if wildcard_present(f
):
548 p4_system(["add", "-f", f
])
550 p4_system(["add", f
])
554 p4_system(["delete", wildcard_encode(f
)])
557 def p4_edit(f
, *options
):
558 p4_system(["edit"] + list(options
) + [wildcard_encode(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
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
587 Return a dict of the results.
590 cmd
= ["describe", "-s"]
595 ds
= p4CmdList(cmd
, skip_info
=True)
597 die("p4 describe -s %d did not return 1 result: %s" % (change
, str(ds
)))
601 if "p4ExitCode" in d
:
602 die("p4 describe -s %d exited with %d: %s" % (change
, d
["p4ExitCode"],
605 if d
["code"] == "error":
606 die("p4 describe -s %d returned error code: %s" % (change
, str(d
)))
609 die("p4 describe -s %d returned no \"time\": %s" % (change
, str(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",
626 "tempobj": "binary+FSw",
627 "ubinary": "binary+F",
628 "uresource": "resource+F",
629 "uxbinary": "binary+Fx",
630 "xbinary": "binary+x",
632 "xtempobj": "binary+Swx",
634 "xunicode": "unicode+x",
637 if p4type
in p4_filetypes_historical
:
638 p4type
= p4_filetypes_historical
[p4type
]
640 s
= p4type
.split("+")
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
:
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):
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.
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
)
706 return match
.group(1)
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."""
715 if not isinstance(depotPaths
, list):
716 depotPaths
= [depotPaths
]
718 for l
in p4CmdList(["labels"] + ["%s..." % p
for p
in depotPaths
]):
726 """Return the set of all git tags."""
729 for line
in read_pipe_lines(["git", "tag"]):
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
)
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)
777 def isModeExec(mode
):
778 """Returns True if the given git mode represents an executable file,
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
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
)
811 class P4CommandException(P4Exception
):
812 """Something went wrong calling p4 which means we have to give up."""
814 def __init__(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
838 def p4KeyContainsFilePaths(key
):
839 """Returns True if the key contains file paths. These are handled by decode_path().
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
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
):
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
)
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.
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
)
878 stdin_file
.write(encode_text_stream(i
))
879 stdin_file
.write(b
'\n')
883 p4
= subprocess
.Popen(
884 cmd
, stdin
=stdin_file
, stdout
=subprocess
.PIPE
, *k
, **kw
)
889 entry
= marshal
.load(p4
.stdout
)
892 # Decode unmarshalled dict to use str keys and values. Special cases are handled below.
894 for key
, value
in entry
.items():
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
904 if 'code' in entry
and entry
['code'] == 'info':
906 for key
in p4KeysContainingNonUtf8Chars():
908 entry
[key
] = metadata_stream_to_writable_bytes(entry
[key
])
917 if errors_as_exceptions
:
919 data
= result
[0].get('data')
921 m
= re
.search(r
'Too many rows scanned \(over (\d+)\)', data
)
923 m
= re
.search(r
'Request too large \(over (\d+)\)', data
)
926 limit
= int(m
.group(1))
927 raise P4RequestSizeException(exitCode
, result
, limit
)
929 raise P4ServerException(exitCode
, result
)
931 raise P4Exception(exitCode
)
934 entry
["p4ExitCode"] = exitCode
940 def p4Cmd(cmd
, *k
, **kw
):
941 list = p4CmdList(cmd
, *k
, **kw
)
948 def p4Where(depotPath
):
949 if not depotPath
.endswith("/"):
951 depotPathLong
= depotPath
+ "..."
952 outputList
= p4CmdList(["where", depotPathLong
])
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:] == "/...":
962 elif "data" in entry
:
963 data
= entry
.get("data")
964 space
= data
.find(" ")
965 if data
[:space
] == depotPath
:
970 if output
["code"] == "error":
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]
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
],
1003 def extractLogMessageFromGitCommit(commit
):
1006 # fixme: title is first line of commit, not 1st paragraph.
1008 for log
in read_pipe_lines(["git", "cat-file", "commit", commit
]):
1018 def extractSettingsGitLog(log
):
1020 for line
in log
.split("\n"):
1022 m
= re
.search(r
"^ *\[git-p4: (.*)\]$", line
)
1026 assignments
= m
.group(1).split(':')
1027 for a
in assignments
:
1029 key
= vals
[0].strip()
1030 val
= ('='.join(vals
[1:])).strip()
1031 if val
.endswith('\"') and val
.startswith('"'):
1036 paths
= values
.get("depot-paths")
1038 paths
= values
.get("depot-path")
1040 values
['depot-paths'] = paths
.split(',')
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
])
1061 def gitConfig(key
, typeSpecifier
=None):
1062 if key
not in _gitConfig
:
1063 cmd
= ["git", "config"]
1065 cmd
+= [typeSpecifier
]
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
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)
1089 _gitConfig
[key
] = int(gitConfig(key
, '--int'))
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/"):
1110 if importIntoRemotes
:
1111 prepend
= "refs/remotes/"
1113 prepend
= "refs/heads/"
1114 if not incomingRef
.startswith("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/"
1126 longprefix
= "refs/heads/p4/"
1127 if incomingRef
.startswith(longprefix
):
1128 return incomingRef
[len(longprefix
):]
1129 if incomingRef
.startswith("p4/"):
1130 return incomingRef
[3:]
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
1143 cmdline
= ["git", "rev-parse", "--symbolic"]
1144 if branchesAreInRemotes
:
1145 cmdline
.append("--remotes")
1147 cmdline
.append("--branches")
1149 for line
in read_pipe_lines(cmdline
):
1152 # only import to p4/
1153 if not line
.startswith('p4/'):
1155 # special symbolic ref to p4/master
1156 if line
== "p4/HEAD":
1159 # strip off p4/ prefix
1160 branch
= line
[len("p4/"):]
1162 branches
[branch
] = parseRevision(line
)
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
)
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
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
]
1213 return ["", settings
]
1216 def createOrUpdateBranchesFromOrigin(localRefPrefix
="refs/remotes/p4/", silent
=True):
1218 print("Creating/updating branch(es) in %s based on origin branch(es)"
1221 originPrefix
= "origin/p4/"
1223 for line
in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
1225 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
1228 headName
= line
[len(originPrefix
):]
1229 remoteHead
= localRefPrefix
+ headName
1232 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
1233 if 'depot-paths' not in original
or 'change' not in original
:
1237 if not gitBranchExists(remoteHead
):
1239 print("creating %s" % remoteHead
)
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
))
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'])))
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()
1272 changeEnd
= int(parts
[1])
1274 return (changeStart
, changeEnd
)
1277 def chooseBlockSize(blockSize
):
1281 return defaultBlockSize
1284 def p4ChangesForPaths(depotPaths
, changeRange
, requestedBlockSize
):
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
== '':
1295 changeEnd
= p4_last_change()
1296 block_size
= chooseBlockSize(requestedBlockSize
)
1298 parts
= changeRange
.split(',')
1299 assert len(parts
) == 2
1301 changeStart
, changeEnd
= p4ParseNumericChangeRange(parts
)
1302 block_size
= chooseBlockSize(requestedBlockSize
)
1304 changeStart
= parts
[0][1:]
1305 changeEnd
= parts
[1]
1306 if requestedBlockSize
:
1307 die("cannot use --changes-block-size with non-numeric revisions")
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
1320 end
= min(changeEnd
, changeStart
+ block_size
)
1321 revisionRange
= "%d,%d" % (changeStart
, end
)
1323 revisionRange
= "%s,%s" % (changeStart
, changeEnd
)
1325 for p
in depotPaths
:
1326 cmd
+= ["%s...@%s" % (p
, revisionRange
)]
1330 result
= p4CmdList(cmd
, errors_as_exceptions
=True)
1331 except P4RequestSizeException
as e
:
1333 block_size
= e
.limit
1334 elif block_size
> e
.limit
:
1335 block_size
= e
.limit
1337 block_size
= max(2, block_size
// 2)
1340 print("block size error, retrying with block size {0}".format(block_size
))
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
:
1349 changes
.add(int(entry
['change']))
1354 if end
>= changeEnd
:
1357 changeStart
= end
+ 1
1359 changes
= sorted(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' %
1387 # dictionary of all client parameters
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
])
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
))
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", "%")
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")
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
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')],
1485 def generateTempFile(self
, contents
):
1486 contentFile
= tempfile
.NamedTemporaryFile(prefix
='git-p4-large-file', delete
=False)
1488 contentFile
.write(d
)
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'):
1497 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1498 contentsSize
= sum(len(d
) for d
in contents
)
1499 if contentsSize
<= gitConfigInt('git-p4.largeFileCompressedThreshold'):
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'):
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
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
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
)
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
1556 with
open(contentFile
, 'r') as f
:
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
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
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')
1612 if not os
.path
.isabs(lfs_path
):
1613 lfs_path
= os
.path
.join(os
.getcwd(), '.git', lfs_path
)
1614 localLargeFile
= os
.path
.join(
1616 'objects', oid
[:2], oid
[2:4],
1619 # LFS Spec states that pointer files should not have the executable bit set.
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
):
1632 self
.baseGitAttributes
+
1636 '# Git LFS (see https://git-lfs.github.com/)\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())
1660 return LargeFileSystem
.processContent(self
, git_mode
, relPath
, contents
)
1664 delete_actions
= ("delete", "move/delete", "purge")
1665 add_actions
= ("add", "branch", "move/add")
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
)
1681 self
.userMapFromPerforceServer
= False
1682 self
.myP4UserId
= None
1686 return self
.myP4UserId
1688 results
= p4CmdList(["user", "-o"])
1691 self
.myP4UserId
= 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
:
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
:
1713 for output
in p4CmdList(["users"]):
1714 if "User" not in output
:
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
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
):
1746 self
.userMapFromPerforceServer
= False
1748 cache
= open(self
.getUserCacheFilename(), 'rb')
1749 lines
= cache
.readlines()
1752 entry
= line
.strip().split(b
"\t")
1753 self
.users
[entry
[0].decode('utf_8')] = entry
[1]
1755 self
.getUserMapFromPerforceServer()
1758 class P4Submit(Command
, P4UserMap
):
1760 conflict_behavior_choices
= ("ask", "skip", "quit")
1763 Command
.__init
__(self
)
1764 P4UserMap
.__init
__(self
)
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]"
1826 self
.detectRenames
= False
1827 self
.preserveUser
= gitConfigBool("git-p4.preserveUser")
1828 self
.dry_run
= False
1830 self
.update_shelve
= list()
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()
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.")
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
1862 Return a tuple (stripped log message, jobs string).
1865 m
= re
.search(r
'^Jobs:', message
, re
.MULTILINE
)
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
1880 inDescriptionSection
= False
1882 for line
in template
.split("\n"):
1883 if line
.startswith("#"):
1884 result
+= line
+ "\n"
1887 if inDescriptionSection
:
1888 if line
.startswith("Files:") or line
.startswith("Jobs:"):
1889 inDescriptionSection
= False
1890 # insert Jobs section
1892 result
+= jobs
+ "\n"
1896 if line
.startswith("Description:"):
1897 inDescriptionSection
= True
1899 for messageLine
in message
.split("\n"):
1900 line
+= "\t" + messageLine
+ "\n"
1902 result
+= line
+ "\n"
1906 def patchRCSKeywords(self
, file, regexp
):
1907 """Attempt to zap the RCS keywords in a p4 controlled file matching the
1910 handle
, outFileName
= tempfile
.mkstemp(dir='.')
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
1917 shutil
.move(outFileName
, file)
1919 # cleanup our temporary file
1920 os
.unlink(outFileName
)
1921 print("Failed to strip RCS keywords in %s" % file)
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
)
1937 return (self
.emails
[gitEmail
], gitEmail
)
1939 def checkValidP4Users(self
, commits
):
1940 """Check if any git authors cannot be mapped to p4 users."""
1942 user
, email
= self
.p4UserForCommit(id)
1944 msg
= "Cannot find p4 user for email %s in commit %s." % (email
, id)
1945 if gitConfigBool("git-p4.allowMissingP4Users"):
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
1957 results
= p4CmdList(["client", "-o"]) # find the current client
1961 client
= r
['Client']
1964 die("could not get client spec")
1965 results
= p4CmdList(["changes", "-c", client
, "-m", "1"])
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
))
1979 if 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)
1989 if r
['code'] == 'error':
1990 die("Could not modify user field of changelist %s to %s:%s" % (changelist
, newUser
, r
['data']))
1992 print("Updated user field for changelist %s to %s" % (changelist
, newUser
))
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
])
2003 if r
['perm'] == 'admin':
2005 if r
['perm'] == 'super':
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()
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.)
2038 inFilesSection
= False
2040 args
= ['change', '-o']
2042 args
.append(str(changelist
))
2043 for entry
in p4CmdList(args
):
2044 if 'code' not in entry
:
2046 if entry
['code'] == 'stat':
2047 change_entry
= entry
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
)]:
2058 if not p4PathStartsWith(value
, self
.depotPath
):
2060 files_list
.append(value
)
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
:
2067 template
+= key
+ ':'
2068 if key
== 'Description':
2070 for field_line
in change_entry
[key
].splitlines():
2071 template
+= '\t'+field_line
+'\n'
2072 if len(files_list
) > 0:
2074 template
+= 'Files:\n'
2075 for path
in files_list
:
2076 template
+= '\t'+path
+'\n'
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"):
2089 # look at the modification time, to check later if the user saved
2091 mtime
= os
.stat(template_file
).st_mtime
2094 if "P4EDITOR" in os
.environ
and (os
.environ
.get("P4EDITOR") != ""):
2095 editor
= os
.environ
.get("P4EDITOR")
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"):
2105 # modification time updated means user saved the file
2106 if os
.stat(template_file
).st_mtime
> mtime
:
2109 response
= prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
2115 def get_diff_description(self
, editedFiles
, filesToAdd
, symlinks
):
2117 if "P4DIFF" in os
.environ
:
2118 del(os
.environ
["P4DIFF"])
2120 for editedFile
in editedFiles
:
2121 diff
+= p4_read_pipe(['diff', '-du',
2122 wildcard_encode(editedFile
)])
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
)
2137 f
= open(newFile
, "r")
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
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])
2160 filesToChangeType
= set()
2161 filesToDelete
= set()
2163 pureRenameCopy
= set()
2165 filesToChangeExecBit
= {}
2169 diff
= parseDiffTreeEntry(line
)
2170 modifier
= diff
['status']
2172 all_files
.append(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:
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']:
2200 pureRenameCopy
.discard(dest
)
2201 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
2203 pureRenameCopy
.discard(dest
)
2204 filesToChangeExecBit
[dest
] = diff
['dst_mode']
2206 # turn off read-only attribute
2207 os
.chmod(dest
, stat
.S_IWRITE
)
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)
2217 p4_integrate(src
, dest
)
2218 if diff
['src_sha1'] != diff
['dst_sha1']:
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
:
2228 os
.chmod(dest
, stat
.S_IWRITE
)
2230 filesToDelete
.add(src
)
2231 editedFiles
.add(dest
)
2232 elif modifier
== "T":
2233 filesToChangeType
.add(path
)
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
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"):
2256 for file in editedFiles | filesToDelete
:
2257 # did this file's delta contain RCS keywords?
2258 regexp
= p4_keywords_regexp_for_file(file)
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],
2264 if regexp
.search(line
):
2266 print("got keyword match on %s in %s in %s" % (regexp
.pattern
, line
, file))
2267 kwfiles
[file] = regexp
2270 for file, regexp
in kwfiles
.items():
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
:
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
:
2300 for f
in filesToDelete
:
2304 # Set/clear executable bits
2305 for f
in filesToChangeExecBit
.keys():
2306 mode
= filesToChangeExecBit
[f
]
2307 setP4ExecBit(f
, mode
)
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")
2341 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
2342 tmpFile
.write(encode_text_stream(submitTemplate
))
2348 # Allow the hook to edit the changelist text before presenting it
2350 if not run_git_hook("p4-prepare-changelist", [fileName
]):
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
2360 print("P4 workspace prepared for submission.")
2361 print("To submit or revert, go to client workspace")
2362 print(" " + self
.clientPath
)
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.")
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
:
2380 print("To revert the changes, use \"p4 revert ...\", and delete")
2381 print("the submit template file \"%s\"" % fileName
)
2383 print("Since the commit adds new files, they must be deleted:")
2384 for f
in filesToAdd
:
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.")
2397 # read the edited message and submit
2398 tmpFile
= open(fileName
, "rb")
2399 message
= decode_text_stream(tmpFile
.read())
2402 message
= message
.replace("\r\n", "\n")
2403 if message
.find(separatorLine
) != -1:
2404 submitTemplate
= message
[:message
.index(separatorLine
)]
2406 submitTemplate
= message
2408 if len(submitTemplate
.strip()) == 0:
2409 print("Changelist is empty, aborting this changelist.")
2414 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate
)
2416 p4_write_pipe(['shelve', '-i'], submitTemplate
)
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
:
2424 if self
.preserveUser
:
2426 # Get last changelist number. Cannot easily get it from
2427 # the submit command output as the output is
2429 changelist
= self
.lastP4Changelist()
2430 self
.modifyChangelistUser(changelist
, p4User
)
2434 run_git_hook("p4-post-changelist")
2436 # Revert changes if we skip this patch
2437 if not submitted
or self
.shelve
:
2439 print("Reverting shelved files.")
2441 print("Submission cancelled, undoing p4 changes.")
2443 for f
in editedFiles | filesToDelete
:
2445 for f
in filesToAdd
:
2449 if not self
.prepare_p4_only
:
2453 def exportGitTags(self
, gitTags
):
2454 """Export git tags as p4 labels. Create a p4 label and then tag with
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
):
2467 print("tag %s does not match regexp %s" % (name
, validLabelRegexp
))
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
2477 print("git tag %s does not give a p4 commit" % name
)
2480 changelist
= values
['change']
2482 # Get the tag details.
2486 for l
in read_pipe_lines(["git", "cat-file", "-p", name
]):
2489 if re
.match(r
'tag\s+', l
):
2491 elif re
.match(r
'\s*$', l
):
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"
2506 labelTemplate
+= "\t" + b
+ "\n"
2507 labelTemplate
+= "View:\n"
2508 for depot_side
in clientSpec
.mappings
:
2509 labelTemplate
+= "\t%s\n" % depot_side
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
)
2517 p4_write_pipe(["label", "-i"], labelTemplate
)
2520 p4_system(["tag", "-l", name
] +
2521 ["%s@%s" % (depot_side
, changelist
) for depot_side
in clientSpec
.mappings
])
2524 print("created p4 label for tag %s" % name
)
2526 def run(self
, args
):
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
)
2536 for i
in self
.update_shelve
:
2538 sys
.exit("invalid changelist %d" % i
)
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:
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")
2561 if val
not in self
.conflict_behavior_choices
:
2562 die("Invalid value '%s' for config git-p4.conflict" % val
)
2565 self
.conflict_behavior
= val
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")
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()
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)
2603 print("Would synchronize p4 checkout in %s" % self
.clientPath
)
2605 print("Synchronizing p4 checkout...")
2607 # old one was destroyed, and maybe nobody told p4
2608 p4_sync("...", "-f")
2615 committish
= self
.master
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())
2626 commits
.append(self
.commit
)
2628 for line
in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self
.origin
, committish
)]):
2629 commits
.append(line
.strip())
2632 if self
.preserveUser
or gitConfigBool("git-p4.skipUserNameCheck"):
2633 self
.checkAuthorship
= False
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"]
2648 # If not explicitly set check the config variable
2649 detectRenames
= gitConfig("git-p4.detectRenames")
2651 if detectRenames
.lower() == "false" or detectRenames
== "":
2653 elif detectRenames
.lower() == "true":
2654 self
.diffOpts
= ["-M"]
2656 self
.diffOpts
= ["-M{}".format(detectRenames
)]
2658 # no command-line arg for -C or --find-copies-harder, just
2660 detectCopies
= gitConfig("git-p4.detectCopies")
2661 if detectCopies
.lower() == "false" or detectCopies
== "":
2663 elif detectCopies
.lower() == "true":
2664 self
.diffOpts
.append("-C")
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
:
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.")
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
))
2689 # Apply the commits, one at a time. On failure, ask if should
2690 # continue to try the rest of the patches, or quit.
2693 print("Would apply")
2695 last
= len(commits
) - 1
2696 for i
, commit
in enumerate(commits
):
2698 print(" ", read_pipe(["git", "show", "-s",
2699 "--format=format:%h %s", commit
]))
2702 ok
= self
.applyCommit(commit
)
2704 applied
.append(commit
)
2705 if self
.prepare_p4_only
:
2707 print("Processing only the first commit due to option"
2708 " --prepare-p4-only")
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":
2718 elif self
.conflict_behavior
== "quit":
2721 die("Unknown conflict_behavior '%s'" %
2722 self
.conflict_behavior
)
2725 print("Skipping this commit, but applying the rest")
2730 chdir(self
.oldWorkingDirectory
)
2731 shelved_applied
= "shelved" if self
.shelve
else "applied"
2734 elif self
.prepare_p4_only
:
2736 elif len(commits
) == len(applied
):
2737 print("All commits {0}!".format(shelved_applied
))
2741 sync
.branch
= self
.branch
2742 if self
.disable_p4sync
:
2743 sync
.sync_origin_only()
2747 if not self
.disable_rebase
:
2752 if len(applied
) == 0:
2753 print("No commits {0}.".format(shelved_applied
))
2755 print("{0} only the commits marked with '*':".format(shelved_applied
.capitalize()))
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
):
2783 """Represent a p4 view ("p4 help views"), and map files in a repo according
2787 def __init__(self
, client_name
):
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
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
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
2831 if depot_side
.startswith("-"):
2833 depot_side
= depot_side
[1:]
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"
2859 if "clientFile" not in res
:
2860 die("No clientFile in 'p4 where' output")
2862 # it will list all of them, but only one not unmap-ped
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
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
)
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
):
2903 Command
.__init
__(self
)
2904 P4UserMap
.__init
__(self
)
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
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]"
2936 self
.createdBranches
= set()
2937 self
.committedChanges
= set()
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))
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()
2987 print("checkpoint finished: " + out
)
2989 def isPathWanted(self
, path
):
2990 for p
in self
.cloneExclude
:
2992 if p4PathStartsWith(path
, p
):
2994 # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2995 elif path
.lower() == p
.lower():
2997 for p
in self
.depotPaths
:
2998 if p4PathStartsWith(path
, decode_path(p
)):
3002 def extractFilesFromCommit(self
, commit
, shelved
=False, shelved_cl
=0):
3005 while "depotFile%s" % fnum
in commit
:
3006 path
= commit
["depotFile%s" % fnum
]
3007 found
= self
.isPathWanted(decode_path(path
))
3014 file["rev"] = commit
["rev%s" % fnum
]
3015 file["action"] = commit
["action%s" % fnum
]
3016 file["type"] = commit
["type%s" % fnum
]
3018 file["shelved_cl"] = int(shelved_cl
)
3023 def extractJobsFromCommit(self
, commit
):
3026 while "job%s" % jnum
in commit
:
3027 job
= commit
["job%s" % jnum
]
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
):]
3057 if p4PathStartsWith(path
, p
):
3058 path
= path
[len(p
):]
3061 path
= wildcard_decode(path
)
3064 def splitFilesIntoBranches(self
, commit
):
3065 """Look at each depotFile in the commit to figure out to what branch it
3069 if self
.clientSpecDirs
:
3070 files
= self
.extractFilesFromCommit(commit
)
3071 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
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
)
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
]
3090 # start with the full relative path where this file would
3092 if self
.useClientSpec
:
3093 relPath
= decode_path(self
.clientSpecDirs
.map_in_client(path
))
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)
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
))
3112 self
.gitStream
.write(d
)
3113 self
.gitStream
.write('\n')
3115 def encodeWithUTF8(self
, path
):
3117 path
.decode('ascii')
3120 if gitConfig('git-p4.pathEncoding'):
3121 encoding
= gitConfig('git-p4.pathEncoding')
3122 path
= path
.decode(encoding
, 'replace').encode('utf8', 'replace')
3124 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding
, 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
)
3137 if 'fileSize' in self
.stream_file
:
3138 size
= int(self
.stream_file
['fileSize'])
3140 # Deleted files don't get a fileSize apparently
3142 sys
.stdout
.write('\r%s --> %s (%s)\n' % (
3143 file_path
, relPath
, format_size_human_readable(size
)))
3146 type_base
, type_mods
= split_p4_type(file["type"])
3149 if "x" in type_mods
:
3151 if type_base
== "symlink":
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
)
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
)
3163 elif data
[-1] == '\n':
3164 contents
= [data
[:-1]]
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.
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'
3187 if p4_version_string().find('/NT') >= 0:
3188 text
= text
.replace(b
'\x0d\x00\x0a\x00', b
'\x0a\x00')
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'])
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
)
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
)
3228 sys
.stdout
.write("delete %s\n" % relPath
)
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
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
))
3253 if self
.stream_have_file_info
:
3254 if "depotFile" in self
.stream_file
:
3255 f
= self
.stream_file
["depotFile"]
3257 # force a failure in fast-import, else an empty
3258 # commit will be made
3259 self
.gitStream
.write("\n")
3260 self
.gitStream
.write("die-now\n")
3261 self
.gitStream
.close()
3262 # ignore errors, but make sure it exits first
3263 self
.importProcess
.wait()
3266 die("Error from p4 print for %s: %s" % (f
, err
))
3268 die("Error from p4 print: %s" % err
)
3270 if 'depotFile' in marshalled
and self
.stream_have_file_info
:
3271 # start of a new file - output the old one first
3272 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
3273 self
.stream_file
= {}
3274 self
.stream_contents
= []
3275 self
.stream_have_file_info
= False
3277 # pick up the new file information... for the
3278 # 'data' field we need to append to our array
3279 for k
in marshalled
.keys():
3281 if 'streamContentSize' not in self
.stream_file
:
3282 self
.stream_file
['streamContentSize'] = 0
3283 self
.stream_file
['streamContentSize'] += len(marshalled
['data'])
3284 self
.stream_contents
.append(marshalled
['data'])
3286 self
.stream_file
[k
] = marshalled
[k
]
3289 'streamContentSize' in self
.stream_file
and
3290 'fileSize' in self
.stream_file
and
3291 'depotFile' in self
.stream_file
):
3292 size
= int(self
.stream_file
["fileSize"])
3294 progress
= 100*self
.stream_file
['streamContentSize']/size
3295 sys
.stdout
.write('\r%s %d%% (%s)' % (
3296 self
.stream_file
['depotFile'], progress
,
3297 format_size_human_readable(size
)))
3300 self
.stream_have_file_info
= True
3302 def streamP4Files(self
, files
):
3303 """Stream directly from "p4 files" into "git fast-import."""
3310 filesForCommit
.append(f
)
3311 if f
['action'] in self
.delete_actions
:
3312 filesToDelete
.append(f
)
3314 filesToRead
.append(f
)
3317 for f
in filesToDelete
:
3318 self
.streamOneP4Deletion(f
)
3320 if len(filesToRead
) > 0:
3321 self
.stream_file
= {}
3322 self
.stream_contents
= []
3323 self
.stream_have_file_info
= False
3325 # curry self argument
3326 def streamP4FilesCbSelf(entry
):
3327 self
.streamP4FilesCb(entry
)
3330 for f
in filesToRead
:
3331 if 'shelved_cl' in f
:
3332 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3334 fileArg
= f
['path'] + encode_text_stream('@={}'.format(f
['shelved_cl']))
3336 fileArg
= f
['path'] + encode_text_stream('#{}'.format(f
['rev']))
3338 fileArgs
.append(fileArg
)
3340 p4CmdList(["-x", "-", "print"],
3342 cb
=streamP4FilesCbSelf
)
3345 if 'depotFile' in self
.stream_file
:
3346 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
3348 def make_email(self
, userid
):
3349 if userid
in self
.users
:
3350 return self
.users
[userid
]
3352 userid_bytes
= metadata_stream_to_writable_bytes(userid
)
3353 return b
"%s <a@b>" % userid_bytes
3355 def streamTag(self
, gitStream
, labelName
, labelDetails
, commit
, epoch
):
3358 Commit is either a git commit, or a fast-import mark, ":<p4commit>".
3362 print("writing tag %s for commit %s" % (labelName
, commit
))
3363 gitStream
.write("tag %s\n" % labelName
)
3364 gitStream
.write("from %s\n" % commit
)
3366 if 'Owner' in labelDetails
:
3367 owner
= labelDetails
["Owner"]
3371 # Try to use the owner of the p4 label, or failing that,
3372 # the current p4 user id.
3374 email
= self
.make_email(owner
)
3376 email
= self
.make_email(self
.p4UserId())
3378 gitStream
.write("tagger ")
3379 gitStream
.write(email
)
3380 gitStream
.write(" %s %s\n" % (epoch
, self
.tz
))
3382 print("labelDetails=", labelDetails
)
3383 if 'Description' in labelDetails
:
3384 description
= labelDetails
['Description']
3386 description
= 'Label from git p4'
3388 gitStream
.write("data %d\n" % len(description
))
3389 gitStream
.write(description
)
3390 gitStream
.write("\n")
3392 def inClientSpec(self
, path
):
3393 if not self
.clientSpecDirs
:
3395 inClientSpec
= self
.clientSpecDirs
.map_in_client(path
)
3396 if not inClientSpec
and self
.verbose
:
3397 print('Ignoring file outside of client spec: {0}'.format(path
))
3400 def hasBranchPrefix(self
, path
):
3401 if not self
.branchPrefixes
:
3403 hasPrefix
= [p
for p
in self
.branchPrefixes
3404 if p4PathStartsWith(path
, p
)]
3405 if not hasPrefix
and self
.verbose
:
3406 print('Ignoring file outside of prefix: {0}'.format(path
))
3409 def findShadowedFiles(self
, files
, change
):
3410 """Perforce allows you commit files and directories with the same name,
3411 so you could have files //depot/foo and //depot/foo/bar both checked
3412 in. A p4 sync of a repository in this state fails. Deleting one of
3413 the files recovers the repository.
3415 Git will not allow the broken state to exist and only the most
3416 recent of the conflicting names is left in the repository. When one
3417 of the conflicting files is deleted we need to re-add the other one
3418 to make sure the git repository recovers in the same way as
3422 deleted
= [f
for f
in files
if f
['action'] in self
.delete_actions
]
3425 path
= decode_path(f
['path'])
3426 to_check
.add(path
+ '/...')
3428 path
= path
.rsplit("/", 1)[0]
3429 if path
== "/" or path
in to_check
:
3432 to_check
= ['%s@%s' % (wildcard_encode(p
), change
) for p
in to_check
3433 if self
.hasBranchPrefix(p
)]
3435 stat_result
= p4CmdList(["-x", "-", "fstat", "-T",
3436 "depotFile,headAction,headRev,headType"], stdin
=to_check
)
3437 for record
in stat_result
:
3438 if record
['code'] != 'stat':
3440 if record
['headAction'] in self
.delete_actions
:
3444 'path': record
['depotFile'],
3445 'rev': record
['headRev'],
3446 'type': record
['headType']})
3448 def commit(self
, details
, files
, branch
, parent
="", allow_empty
=False):
3449 epoch
= details
["time"]
3450 author
= details
["user"]
3451 jobs
= self
.extractJobsFromCommit(details
)
3454 print('commit into {0}'.format(branch
))
3456 files
= [f
for f
in files
3457 if self
.hasBranchPrefix(decode_path(f
['path']))]
3458 self
.findShadowedFiles(files
, details
['change'])
3460 if self
.clientSpecDirs
:
3461 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
3463 files
= [f
for f
in files
if self
.inClientSpec(decode_path(f
['path']))]
3465 if gitConfigBool('git-p4.keepEmptyCommits'):
3468 if not files
and not allow_empty
:
3469 print('Ignoring revision {0} as it would produce an empty commit.'
3470 .format(details
['change']))
3473 self
.gitStream
.write("commit %s\n" % branch
)
3474 self
.gitStream
.write("mark :%s\n" % details
["change"])
3475 self
.committedChanges
.add(int(details
["change"]))
3476 if author
not in self
.users
:
3477 self
.getUserMapFromPerforceServer()
3479 self
.gitStream
.write("committer ")
3480 self
.gitStream
.write(self
.make_email(author
))
3481 self
.gitStream
.write(" %s %s\n" % (epoch
, self
.tz
))
3483 self
.gitStream
.write("data <<EOT\n")
3484 self
.gitStream
.write(details
["desc"])
3486 self
.gitStream
.write("\nJobs: %s" % (' '.join(jobs
)))
3488 if not self
.suppress_meta_comment
:
3489 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3490 (','.join(self
.branchPrefixes
), details
["change"]))
3491 if len(details
['options']) > 0:
3492 self
.gitStream
.write(": options = %s" % details
['options'])
3493 self
.gitStream
.write("]\n")
3495 self
.gitStream
.write("EOT\n\n")
3499 print("parent %s" % parent
)
3500 self
.gitStream
.write("from %s\n" % parent
)
3502 self
.streamP4Files(files
)
3503 self
.gitStream
.write("\n")
3505 change
= int(details
["change"])
3507 if change
in self
.labels
:
3508 label
= self
.labels
[change
]
3509 labelDetails
= label
[0]
3510 labelRevisions
= label
[1]
3512 print("Change %s is labelled %s" % (change
, labelDetails
))
3514 files
= p4CmdList(["files"] + ["%s...@%s" % (p
, change
)
3515 for p
in self
.branchPrefixes
])
3517 if len(files
) == len(labelRevisions
):
3521 if info
["action"] in self
.delete_actions
:
3523 cleanedFiles
[info
["depotFile"]] = info
["rev"]
3525 if cleanedFiles
== labelRevisions
:
3526 self
.streamTag(self
.gitStream
, 'tag_%s' % labelDetails
['label'], labelDetails
, branch
, epoch
)
3530 print("Tag %s does not match with change %s: files do not match."
3531 % (labelDetails
["label"], change
))
3535 print("Tag %s does not match with change %s: file count is different."
3536 % (labelDetails
["label"], change
))
3538 def getLabels(self
):
3539 """Build a dictionary of changelists and labels, for "detect-labels"
3545 l
= p4CmdList(["labels"] + ["%s..." % p
for p
in self
.depotPaths
])
3546 if len(l
) > 0 and not self
.silent
:
3547 print("Finding files belonging to labels in %s" % self
.depotPaths
)
3550 label
= output
["label"]
3554 print("Querying files for label %s" % label
)
3555 for file in p4CmdList(["files"] +
3556 ["%s...@%s" % (p
, label
)
3557 for p
in self
.depotPaths
]):
3558 revisions
[file["depotFile"]] = file["rev"]
3559 change
= int(file["change"])
3560 if change
> newestChange
:
3561 newestChange
= change
3563 self
.labels
[newestChange
] = [output
, revisions
]
3566 print("Label changes: %s" % self
.labels
.keys())
3568 def importP4Labels(self
, stream
, p4Labels
):
3569 """Import p4 labels as git tags. A direct mapping does not exist, so
3570 assume that if all the files are at the same revision then we can
3571 use that, or it's something more complicated we should just ignore.
3575 print("import p4 labels: " + ' '.join(p4Labels
))
3577 ignoredP4Labels
= gitConfigList("git-p4.ignoredP4Labels")
3578 validLabelRegexp
= gitConfig("git-p4.labelImportRegexp")
3579 if len(validLabelRegexp
) == 0:
3580 validLabelRegexp
= defaultLabelRegexp
3581 m
= re
.compile(validLabelRegexp
)
3583 for name
in p4Labels
:
3586 if not m
.match(name
):
3588 print("label %s does not match regexp %s" % (name
, validLabelRegexp
))
3591 if name
in ignoredP4Labels
:
3594 labelDetails
= p4CmdList(['label', "-o", name
])[0]
3596 # get the most recent changelist for each file in this label
3597 change
= p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p
, name
)
3598 for p
in self
.depotPaths
])
3600 if 'change' in change
:
3601 # find the corresponding git commit; take the oldest commit
3602 changelist
= int(change
['change'])
3603 if changelist
in self
.committedChanges
:
3604 gitCommit
= ":%d" % changelist
# use a fast-import mark
3607 gitCommit
= read_pipe(["git", "rev-list", "--max-count=1",
3608 "--reverse", r
":/\[git-p4:.*change = %d\]" % changelist
], ignore_error
=True)
3609 if len(gitCommit
) == 0:
3610 print("importing label %s: could not find git commit for changelist %d" % (name
, changelist
))
3613 gitCommit
= gitCommit
.strip()
3616 # Convert from p4 time format
3618 tmwhen
= time
.strptime(labelDetails
['Update'], "%Y/%m/%d %H:%M:%S")
3620 print("Could not convert label time %s" % labelDetails
['Update'])
3623 when
= int(time
.mktime(tmwhen
))
3624 self
.streamTag(stream
, name
, labelDetails
, gitCommit
, when
)
3626 print("p4 label %s mapped to git commit %s" % (name
, gitCommit
))
3629 print("Label %s has no changelists - possibly deleted?" % name
)
3632 # We can't import this label; don't try again as it will get very
3633 # expensive repeatedly fetching all the files for labels that will
3634 # never be imported. If the label is moved in the future, the
3635 # ignore will need to be removed manually.
3636 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name
])
3638 def guessProjectName(self
):
3639 for p
in self
.depotPaths
:
3642 p
= p
[p
.strip().rfind("/") + 1:]
3643 if not p
.endswith("/"):
3647 def getBranchMapping(self
):
3648 lostAndFoundBranches
= set()
3650 user
= gitConfig("git-p4.branchUser")
3652 for info
in p4CmdList(
3653 ["branches"] + (["-u", user
] if len(user
) > 0 else [])):
3654 details
= p4Cmd(["branch", "-o", info
["branch"]])
3656 while "View%s" % viewIdx
in details
:
3657 paths
= details
["View%s" % viewIdx
].split(" ")
3658 viewIdx
= viewIdx
+ 1
3659 # require standard //depot/foo/... //depot/bar/... mapping
3660 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
3663 destination
= paths
[1]
3665 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
3666 source
= source
[len(self
.depotPaths
[0]):-4]
3667 destination
= destination
[len(self
.depotPaths
[0]):-4]
3669 if destination
in self
.knownBranches
:
3671 print("p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
))
3672 print("but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
))
3675 self
.knownBranches
[destination
] = source
3677 lostAndFoundBranches
.discard(destination
)
3679 if source
not in self
.knownBranches
:
3680 lostAndFoundBranches
.add(source
)
3682 # Perforce does not strictly require branches to be defined, so we also
3683 # check git config for a branch list.
3685 # Example of branch definition in git config file:
3687 # branchList=main:branchA
3688 # branchList=main:branchB
3689 # branchList=branchA:branchC
3690 configBranches
= gitConfigList("git-p4.branchList")
3691 for branch
in configBranches
:
3693 source
, destination
= branch
.split(":")
3694 self
.knownBranches
[destination
] = source
3696 lostAndFoundBranches
.discard(destination
)
3698 if source
not in self
.knownBranches
:
3699 lostAndFoundBranches
.add(source
)
3701 for branch
in lostAndFoundBranches
:
3702 self
.knownBranches
[branch
] = branch
3704 def getBranchMappingFromGitBranches(self
):
3705 branches
= p4BranchesInGit(self
.importIntoRemotes
)
3706 for branch
in branches
.keys():
3707 if branch
== "master":
3710 branch
= branch
[len(self
.projectName
):]
3711 self
.knownBranches
[branch
] = branch
3713 def updateOptionDict(self
, d
):
3715 if self
.keepRepoPath
:
3716 option_keys
['keepRepoPath'] = 1
3718 d
["options"] = ' '.join(sorted(option_keys
.keys()))
3720 def readOptions(self
, d
):
3721 self
.keepRepoPath
= ('options' in d
3722 and ('keepRepoPath' in d
['options']))
3724 def gitRefForBranch(self
, branch
):
3725 if branch
== "main":
3726 return self
.refPrefix
+ "master"
3728 if len(branch
) <= 0:
3731 return self
.refPrefix
+ self
.projectName
+ branch
3733 def gitCommitByP4Change(self
, ref
, change
):
3735 print("looking in ref " + ref
+ " for change %s using bisect..." % change
)
3738 latestCommit
= parseRevision(ref
)
3742 print("trying: earliest %s latest %s" % (earliestCommit
, latestCommit
))
3743 next
= read_pipe(["git", "rev-list", "--bisect",
3744 latestCommit
, earliestCommit
]).strip()
3749 log
= extractLogMessageFromGitCommit(next
)
3750 settings
= extractSettingsGitLog(log
)
3751 currentChange
= int(settings
['change'])
3753 print("current change %s" % currentChange
)
3755 if currentChange
== change
:
3757 print("found %s" % next
)
3760 if currentChange
< change
:
3761 earliestCommit
= "^%s" % next
3763 if next
== latestCommit
:
3764 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref
, change
))
3765 latestCommit
= "%s^@" % next
3769 def importNewBranch(self
, branch
, maxChange
):
3770 # make fast-import flush all changes to disk and update the refs using the checkpoint
3771 # command so that we can try to find the branch parent in the git history
3772 self
.gitStream
.write("checkpoint\n\n")
3773 self
.gitStream
.flush()
3774 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
3775 range = "@1,%s" % maxChange
3776 changes
= p4ChangesForPaths([branchPrefix
], range, self
.changes_block_size
)
3777 if len(changes
) <= 0:
3779 firstChange
= changes
[0]
3780 sourceBranch
= self
.knownBranches
[branch
]
3781 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
3782 sourceRef
= self
.gitRefForBranch(sourceBranch
)
3784 branchParentChange
= int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath
, firstChange
)])["change"])
3785 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
3786 if len(gitParent
) > 0:
3787 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
3789 self
.importChanges(changes
)
3792 def searchParent(self
, parent
, branch
, target
):
3793 targetTree
= read_pipe(["git", "rev-parse",
3794 "{}^{{tree}}".format(target
)]).strip()
3795 for line
in read_pipe_lines(["git", "rev-list", "--format=%H %T",
3796 "--no-merges", parent
]):
3797 if line
.startswith("commit "):
3799 commit
, tree
= line
.strip().split(" ")
3800 if tree
== targetTree
:
3802 print("Found parent of %s in commit %s" % (branch
, commit
))
3806 def importChanges(self
, changes
, origin_revision
=0):
3808 for change
in changes
:
3809 description
= p4_describe(change
)
3810 self
.updateOptionDict(description
)
3813 sys
.stdout
.write("\rImporting revision %s (%d%%)" % (
3814 change
, (cnt
* 100) // len(changes
)))
3819 if self
.detectBranches
:
3820 branches
= self
.splitFilesIntoBranches(description
)
3821 for branch
in branches
.keys():
3823 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
3824 self
.branchPrefixes
= [branchPrefix
]
3828 filesForCommit
= branches
[branch
]
3831 print("branch is %s" % branch
)
3833 self
.updatedBranches
.add(branch
)
3835 if branch
not in self
.createdBranches
:
3836 self
.createdBranches
.add(branch
)
3837 parent
= self
.knownBranches
[branch
]
3838 if parent
== branch
:
3841 fullBranch
= self
.projectName
+ branch
3842 if fullBranch
not in self
.p4BranchesInGit
:
3844 print("\n Importing new branch %s" % fullBranch
)
3845 if self
.importNewBranch(branch
, change
- 1):
3847 self
.p4BranchesInGit
.append(fullBranch
)
3849 print("\n Resuming with change %s" % change
)
3852 print("parent determined through known branches: %s" % parent
)
3854 branch
= self
.gitRefForBranch(branch
)
3855 parent
= self
.gitRefForBranch(parent
)
3858 print("looking for initial parent for %s; current parent is %s" % (branch
, parent
))
3860 if len(parent
) == 0 and branch
in self
.initialParents
:
3861 parent
= self
.initialParents
[branch
]
3862 del self
.initialParents
[branch
]
3866 tempBranch
= "%s/%d" % (self
.tempBranchLocation
, change
)
3868 print("Creating temporary branch: " + tempBranch
)
3869 self
.commit(description
, filesForCommit
, tempBranch
)
3870 self
.tempBranches
.append(tempBranch
)
3872 blob
= self
.searchParent(parent
, branch
, tempBranch
)
3874 self
.commit(description
, filesForCommit
, branch
, blob
)
3877 print("Parent of %s not found. Committing into head of %s" % (branch
, parent
))
3878 self
.commit(description
, filesForCommit
, branch
, parent
)
3880 files
= self
.extractFilesFromCommit(description
)
3881 self
.commit(description
, files
, self
.branch
,
3883 # only needed once, to connect to the previous commit
3884 self
.initialParent
= ""
3886 print(self
.gitError
.read())
3889 def sync_origin_only(self
):
3890 if self
.syncWithOrigin
:
3891 self
.hasOrigin
= originP4BranchesExist()
3894 print('Syncing with origin first, using "git fetch origin"')
3895 system(["git", "fetch", "origin"])
3897 def importHeadRevision(self
, revision
):
3898 print("Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
))
3901 details
["user"] = "git perforce import user"
3902 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
3903 % (' '.join(self
.depotPaths
), revision
))
3904 details
["change"] = revision
3908 fileArgs
= ["%s...%s" % (p
, revision
) for p
in self
.depotPaths
]
3910 for info
in p4CmdList(["files"] + fileArgs
):
3912 if 'code' in info
and info
['code'] == 'error':
3913 sys
.stderr
.write("p4 returned an error: %s\n"
3915 if info
['data'].find("must refer to client") >= 0:
3916 sys
.stderr
.write("This particular p4 error is misleading.\n")
3917 sys
.stderr
.write("Perhaps the depot path was misspelled.\n")
3918 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
3920 if 'p4ExitCode' in info
:
3921 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
3924 change
= int(info
["change"])
3925 if change
> newestRevision
:
3926 newestRevision
= change
3928 if info
["action"] in self
.delete_actions
:
3931 for prop
in ["depotFile", "rev", "action", "type"]:
3932 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
3934 fileCnt
= fileCnt
+ 1
3936 details
["change"] = newestRevision
3938 # Use time from top-most change so that all git p4 clones of
3939 # the same p4 repo have the same commit SHA1s.
3940 res
= p4_describe(newestRevision
)
3941 details
["time"] = res
["time"]
3943 self
.updateOptionDict(details
)
3945 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
)
3946 except IOError as err
:
3947 print("IO error with git fast-import. Is your git version recent enough?")
3948 print("IO error details: {}".format(err
))
3949 print(self
.gitError
.read())
3951 def importRevisions(self
, args
, branch_arg_given
):
3954 if len(self
.changesFile
) > 0:
3955 with
open(self
.changesFile
) as f
:
3956 output
= f
.readlines()
3959 changeSet
.add(int(line
))
3961 for change
in changeSet
:
3962 changes
.append(change
)
3966 # catch "git p4 sync" with no new branches, in a repo that
3967 # does not have any existing p4 branches
3969 if not self
.p4BranchesInGit
:
3970 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3972 # The default branch is master, unless --branch is used to
3973 # specify something else. Make sure it exists, or complain
3974 # nicely about how to use --branch.
3975 if not self
.detectBranches
:
3976 if not branch_exists(self
.branch
):
3977 if branch_arg_given
:
3978 raise P4CommandException("Error: branch %s does not exist." % self
.branch
)
3980 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
3984 print("Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
3986 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
, self
.changes_block_size
)
3988 if len(self
.maxChanges
) > 0:
3989 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
3991 if len(changes
) == 0:
3993 print("No changes to import!")
3995 if not self
.silent
and not self
.detectBranches
:
3996 print("Import destination: %s" % self
.branch
)
3998 self
.updatedBranches
= set()
4000 if not self
.detectBranches
:
4002 # start a new branch
4003 self
.initialParent
= ""
4005 # build on a previous revision
4006 self
.initialParent
= parseRevision(self
.branch
)
4008 self
.importChanges(changes
)
4012 if len(self
.updatedBranches
) > 0:
4013 sys
.stdout
.write("Updated branches: ")
4014 for b
in self
.updatedBranches
:
4015 sys
.stdout
.write("%s " % b
)
4016 sys
.stdout
.write("\n")
4018 def openStreams(self
):
4019 self
.importProcess
= subprocess
.Popen(["git", "fast-import"],
4020 stdin
=subprocess
.PIPE
,
4021 stdout
=subprocess
.PIPE
,
4022 stderr
=subprocess
.PIPE
)
4023 self
.gitOutput
= self
.importProcess
.stdout
4024 self
.gitStream
= self
.importProcess
.stdin
4025 self
.gitError
= self
.importProcess
.stderr
4027 if bytes
is not str:
4028 # Wrap gitStream.write() so that it can be called using `str` arguments
4029 def make_encoded_write(write
):
4030 def encoded_write(s
):
4031 return write(s
.encode() if isinstance(s
, str) else s
)
4032 return encoded_write
4034 self
.gitStream
.write
= make_encoded_write(self
.gitStream
.write
)
4036 def closeStreams(self
):
4037 if self
.gitStream
is None:
4039 self
.gitStream
.close()
4040 if self
.importProcess
.wait() != 0:
4041 die("fast-import failed: %s" % self
.gitError
.read())
4042 self
.gitOutput
.close()
4043 self
.gitError
.close()
4044 self
.gitStream
= None
4046 def run(self
, args
):
4047 if self
.importIntoRemotes
:
4048 self
.refPrefix
= "refs/remotes/p4/"
4050 self
.refPrefix
= "refs/heads/p4/"
4052 self
.sync_origin_only()
4054 branch_arg_given
= bool(self
.branch
)
4055 if len(self
.branch
) == 0:
4056 self
.branch
= self
.refPrefix
+ "master"
4057 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
4058 system(["git", "update-ref", self
.branch
, "refs/heads/p4"])
4059 system(["git", "branch", "-D", "p4"])
4061 # accept either the command-line option, or the configuration variable
4062 if self
.useClientSpec
:
4063 # will use this after clone to set the variable
4064 self
.useClientSpec_from_options
= True
4066 if gitConfigBool("git-p4.useclientspec"):
4067 self
.useClientSpec
= True
4068 if self
.useClientSpec
:
4069 self
.clientSpecDirs
= getClientSpec()
4071 # TODO: should always look at previous commits,
4072 # merge with previous imports, if possible.
4075 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
4077 # branches holds mapping from branch name to sha1
4078 branches
= p4BranchesInGit(self
.importIntoRemotes
)
4080 # restrict to just this one, disabling detect-branches
4081 if branch_arg_given
:
4082 short
= shortP4Ref(self
.branch
, self
.importIntoRemotes
)
4083 if short
in branches
:
4084 self
.p4BranchesInGit
= [short
]
4085 elif self
.branch
.startswith('refs/') and \
4086 branchExists(self
.branch
) and \
4087 '[git-p4:' in extractLogMessageFromGitCommit(self
.branch
):
4088 self
.p4BranchesInGit
= [self
.branch
]
4090 self
.p4BranchesInGit
= branches
.keys()
4092 if len(self
.p4BranchesInGit
) > 1:
4094 print("Importing from/into multiple branches")
4095 self
.detectBranches
= True
4096 for branch
in branches
.keys():
4097 self
.initialParents
[self
.refPrefix
+ branch
] = \
4101 print("branches: %s" % self
.p4BranchesInGit
)
4104 for branch
in self
.p4BranchesInGit
:
4105 logMsg
= extractLogMessageFromGitCommit(fullP4Ref(branch
,
4106 self
.importIntoRemotes
))
4108 settings
= extractSettingsGitLog(logMsg
)
4110 self
.readOptions(settings
)
4111 if 'depot-paths' in settings
and 'change' in settings
:
4112 change
= int(settings
['change']) + 1
4113 p4Change
= max(p4Change
, change
)
4115 depotPaths
= sorted(settings
['depot-paths'])
4116 if self
.previousDepotPaths
== []:
4117 self
.previousDepotPaths
= depotPaths
4120 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
4121 prev_list
= prev
.split("/")
4122 cur_list
= cur
.split("/")
4123 for i
in range(0, min(len(cur_list
), len(prev_list
))):
4124 if cur_list
[i
] != prev_list
[i
]:
4128 paths
.append("/".join(cur_list
[:i
+ 1]))
4130 self
.previousDepotPaths
= paths
4133 self
.depotPaths
= sorted(self
.previousDepotPaths
)
4134 self
.changeRange
= "@%s,#head" % p4Change
4135 if not self
.silent
and not self
.detectBranches
:
4136 print("Performing incremental import into %s git branch" % self
.branch
)
4138 self
.branch
= fullP4Ref(self
.branch
, self
.importIntoRemotes
)
4140 if len(args
) == 0 and self
.depotPaths
:
4142 print("Depot paths: %s" % ' '.join(self
.depotPaths
))
4144 if self
.depotPaths
and self
.depotPaths
!= args
:
4145 print("previous import used depot path %s and now %s was specified. "
4146 "This doesn't work!" % (' '.join(self
.depotPaths
),
4150 self
.depotPaths
= sorted(args
)
4155 # Make sure no revision specifiers are used when --changesfile
4157 bad_changesfile
= False
4158 if len(self
.changesFile
) > 0:
4159 for p
in self
.depotPaths
:
4160 if p
.find("@") >= 0 or p
.find("#") >= 0:
4161 bad_changesfile
= True
4164 die("Option --changesfile is incompatible with revision specifiers")
4167 for p
in self
.depotPaths
:
4168 if p
.find("@") != -1:
4169 atIdx
= p
.index("@")
4170 self
.changeRange
= p
[atIdx
:]
4171 if self
.changeRange
== "@all":
4172 self
.changeRange
= ""
4173 elif ',' not in self
.changeRange
:
4174 revision
= self
.changeRange
4175 self
.changeRange
= ""
4177 elif p
.find("#") != -1:
4178 hashIdx
= p
.index("#")
4179 revision
= p
[hashIdx
:]
4181 elif self
.previousDepotPaths
== []:
4182 # pay attention to changesfile, if given, else import
4183 # the entire p4 tree at the head revision
4184 if len(self
.changesFile
) == 0:
4187 p
= re
.sub(r
"\.\.\.$", "", p
)
4188 if not p
.endswith("/"):
4193 self
.depotPaths
= newPaths
4195 # --detect-branches may change this for each branch
4196 self
.branchPrefixes
= self
.depotPaths
4198 self
.loadUserMapFromCache()
4200 if self
.detectLabels
:
4203 if self
.detectBranches
:
4204 # FIXME - what's a P4 projectName ?
4205 self
.projectName
= self
.guessProjectName()
4208 self
.getBranchMappingFromGitBranches()
4210 self
.getBranchMapping()
4212 print("p4-git branches: %s" % self
.p4BranchesInGit
)
4213 print("initial parents: %s" % self
.initialParents
)
4214 for b
in self
.p4BranchesInGit
:
4218 b
= b
[len(self
.projectName
):]
4219 self
.createdBranches
.add(b
)
4229 self
.importHeadRevision(revision
)
4231 self
.importRevisions(args
, branch_arg_given
)
4233 if gitConfigBool("git-p4.importLabels"):
4234 self
.importLabels
= True
4236 if self
.importLabels
:
4237 p4Labels
= getP4Labels(self
.depotPaths
)
4238 gitTags
= getGitTags()
4240 missingP4Labels
= p4Labels
- gitTags
4241 self
.importP4Labels(self
.gitStream
, missingP4Labels
)
4243 except P4CommandException
as e
:
4252 # Cleanup temporary branches created during import
4253 if self
.tempBranches
!= []:
4254 for branch
in self
.tempBranches
:
4255 read_pipe(["git", "update-ref", "-d", branch
])
4256 if len(read_pipe(["git", "for-each-ref", self
.tempBranchLocation
])) > 0:
4257 die("There are unexpected temporary branches")
4259 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4260 # a convenient shortcut refname "p4".
4261 if self
.importIntoRemotes
:
4262 head_ref
= self
.refPrefix
+ "HEAD"
4263 if not gitBranchExists(head_ref
) and gitBranchExists(self
.branch
):
4264 system(["git", "symbolic-ref", head_ref
, self
.branch
])
4269 class P4Rebase(Command
):
4271 Command
.__init
__(self
)
4273 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
4275 self
.importLabels
= False
4276 self
.description
= ("Fetches the latest revision from perforce and "
4277 + "rebases the current work (branch) against it")
4279 def run(self
, args
):
4281 sync
.importLabels
= self
.importLabels
4284 return self
.rebase()
4287 if os
.system("git update-index --refresh") != 0:
4288 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.")
4289 if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
4290 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.")
4292 upstream
, settings
= findUpstreamBranchPoint()
4293 if len(upstream
) == 0:
4294 die("Cannot find upstream branchpoint for rebase")
4296 # the branchpoint may be p4/foo~3, so strip off the parent
4297 upstream
= re
.sub(r
"~[0-9]+$", "", upstream
)
4299 print("Rebasing the current branch onto %s" % upstream
)
4300 oldHead
= read_pipe(["git", "rev-parse", "HEAD"]).strip()
4301 system(["git", "rebase", upstream
])
4302 system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead
,
4307 class P4Clone(P4Sync
):
4309 P4Sync
.__init
__(self
)
4310 self
.description
= "Creates a new git repository and imports from Perforce into it"
4311 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
4313 optparse
.make_option("--destination", dest
="cloneDestination",
4314 action
='store', default
=None,
4315 help="where to leave result of the clone"),
4316 optparse
.make_option("--bare", dest
="cloneBare",
4317 action
="store_true", default
=False),
4319 self
.cloneDestination
= None
4320 self
.needsGit
= False
4321 self
.cloneBare
= False
4323 def defaultDestination(self
, args
):
4324 # TODO: use common prefix of args?
4326 depotDir
= re
.sub(r
"(@[^@]*)$", "", depotPath
)
4327 depotDir
= re
.sub(r
"(#[^#]*)$", "", depotDir
)
4328 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
4329 depotDir
= re
.sub(r
"/$", "", depotDir
)
4330 return os
.path
.split(depotDir
)[1]
4332 def run(self
, args
):
4336 if self
.keepRepoPath
and not self
.cloneDestination
:
4337 sys
.stderr
.write("Must specify destination for --keep-path\n")
4342 if not self
.cloneDestination
and len(depotPaths
) > 1:
4343 self
.cloneDestination
= depotPaths
[-1]
4344 depotPaths
= depotPaths
[:-1]
4346 for p
in depotPaths
:
4347 if not p
.startswith("//"):
4348 sys
.stderr
.write('Depot paths must start with "//": %s\n' % p
)
4351 if not self
.cloneDestination
:
4352 self
.cloneDestination
= self
.defaultDestination(args
)
4354 print("Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
))
4356 if not os
.path
.exists(self
.cloneDestination
):
4357 os
.makedirs(self
.cloneDestination
)
4358 chdir(self
.cloneDestination
)
4360 init_cmd
= ["git", "init"]
4362 init_cmd
.append("--bare")
4363 retcode
= subprocess
.call(init_cmd
)
4365 raise subprocess
.CalledProcessError(retcode
, init_cmd
)
4367 if not P4Sync
.run(self
, depotPaths
):
4370 # create a master branch and check out a work tree
4371 if gitBranchExists(self
.branch
):
4372 system(["git", "branch", currentGitBranch(), self
.branch
])
4373 if not self
.cloneBare
:
4374 system(["git", "checkout", "-f"])
4376 print('Not checking out any branch, use '
4377 '"git checkout -q -b master <branch>"')
4379 # auto-set this variable if invoked with --use-client-spec
4380 if self
.useClientSpec_from_options
:
4381 system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
4383 # persist any git-p4 encoding-handling config options passed in for clone:
4384 if gitConfig('git-p4.metadataDecodingStrategy'):
4385 system(["git", "config", "git-p4.metadataDecodingStrategy", gitConfig('git-p4.metadataDecodingStrategy')])
4386 if gitConfig('git-p4.metadataFallbackEncoding'):
4387 system(["git", "config", "git-p4.metadataFallbackEncoding", gitConfig('git-p4.metadataFallbackEncoding')])
4388 if gitConfig('git-p4.pathEncoding'):
4389 system(["git", "config", "git-p4.pathEncoding", gitConfig('git-p4.pathEncoding')])
4394 class P4Unshelve(Command
):
4396 Command
.__init
__(self
)
4398 self
.origin
= "HEAD"
4399 self
.description
= "Unshelve a P4 changelist into a git commit"
4400 self
.usage
= "usage: %prog [options] changelist"
4402 optparse
.make_option("--origin", dest
="origin",
4403 help="Use this base revision instead of the default (%s)" % self
.origin
),
4405 self
.verbose
= False
4406 self
.noCommit
= False
4407 self
.destbranch
= "refs/remotes/p4-unshelved"
4409 def renameBranch(self
, branch_name
):
4410 """Rename the existing branch to branch_name.N ."""
4412 for i
in range(0, 1000):
4413 backup_branch_name
= "{0}.{1}".format(branch_name
, i
)
4414 if not gitBranchExists(backup_branch_name
):
4415 # Copy ref to backup
4416 gitUpdateRef(backup_branch_name
, branch_name
)
4417 gitDeleteRef(branch_name
)
4418 print("renamed old unshelve branch to {0}".format(backup_branch_name
))
4421 sys
.exit("gave up trying to rename existing branch {0}".format(branch_name
))
4423 def findLastP4Revision(self
, starting_point
):
4424 """Look back from starting_point for the first commit created by git-p4
4425 to find the P4 commit we are based on, and the depot-paths.
4428 for parent
in (range(65535)):
4429 log
= extractLogMessageFromGitCommit("{0}~{1}".format(starting_point
, parent
))
4430 settings
= extractSettingsGitLog(log
)
4431 if 'change' in settings
:
4434 sys
.exit("could not find git-p4 commits in {0}".format(self
.origin
))
4436 def createShelveParent(self
, change
, branch_name
, sync
, origin
):
4437 """Create a commit matching the parent of the shelved changelist
4440 parent_description
= p4_describe(change
, shelved
=True)
4441 parent_description
['desc'] = 'parent for shelved changelist {}\n'.format(change
)
4442 files
= sync
.extractFilesFromCommit(parent_description
, shelved
=False, shelved_cl
=change
)
4446 # if it was added in the shelved changelist, it won't exist in the parent
4447 if f
['action'] in self
.add_actions
:
4450 # if it was deleted in the shelved changelist it must not be deleted
4451 # in the parent - we might even need to create it if the origin branch
4453 if f
['action'] in self
.delete_actions
:
4456 parent_files
.append(f
)
4458 sync
.commit(parent_description
, parent_files
, branch_name
,
4459 parent
=origin
, allow_empty
=True)
4460 print("created parent commit for {0} based on {1} in {2}".format(
4461 change
, self
.origin
, branch_name
))
4463 def run(self
, args
):
4467 if not gitBranchExists(self
.origin
):
4468 sys
.exit("origin branch {0} does not exist".format(self
.origin
))
4473 # only one change at a time
4476 # if the target branch already exists, rename it
4477 branch_name
= "{0}/{1}".format(self
.destbranch
, change
)
4478 if gitBranchExists(branch_name
):
4479 self
.renameBranch(branch_name
)
4480 sync
.branch
= branch_name
4482 sync
.verbose
= self
.verbose
4483 sync
.suppress_meta_comment
= True
4485 settings
= self
.findLastP4Revision(self
.origin
)
4486 sync
.depotPaths
= settings
['depot-paths']
4487 sync
.branchPrefixes
= sync
.depotPaths
4490 sync
.loadUserMapFromCache()
4493 # create a commit for the parent of the shelved changelist
4494 self
.createShelveParent(change
, branch_name
, sync
, self
.origin
)
4496 # create the commit for the shelved changelist itself
4497 description
= p4_describe(change
, True)
4498 files
= sync
.extractFilesFromCommit(description
, True, change
)
4500 sync
.commit(description
, files
, branch_name
, "")
4503 print("unshelved changelist {0} into {1}".format(change
, branch_name
))
4508 class P4Branches(Command
):
4510 Command
.__init
__(self
)
4512 self
.description
= ("Shows the git branches that hold imports and their "
4513 + "corresponding perforce depot paths")
4514 self
.verbose
= False
4516 def run(self
, args
):
4517 if originP4BranchesExist():
4518 createOrUpdateBranchesFromOrigin()
4520 for line
in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
4523 if not line
.startswith('p4/') or line
== "p4/HEAD":
4527 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
4528 settings
= extractSettingsGitLog(log
)
4530 print("%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"]))
4534 class HelpFormatter(optparse
.IndentedHelpFormatter
):
4536 optparse
.IndentedHelpFormatter
.__init
__(self
)
4538 def format_description(self
, description
):
4540 return description
+ "\n"
4545 def printUsage(commands
):
4546 print("usage: %s <command> [options]" % sys
.argv
[0])
4548 print("valid commands: %s" % ", ".join(commands
))
4550 print("Try %s <command> --help for command specific help." % sys
.argv
[0])
4560 "branches": P4Branches
,
4561 "unshelve": P4Unshelve
,
4566 if len(sys
.argv
[1:]) == 0:
4567 printUsage(commands
.keys())
4570 cmdName
= sys
.argv
[1]
4572 klass
= commands
[cmdName
]
4575 print("unknown command %s" % cmdName
)
4577 printUsage(commands
.keys())
4580 options
= cmd
.options
4581 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
4585 options
.append(optparse
.make_option("--verbose", "-v", dest
="verbose", action
="store_true"))
4587 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
4589 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
4591 description
=cmd
.description
,
4592 formatter
=HelpFormatter())
4595 cmd
, args
= parser
.parse_args(sys
.argv
[2:], cmd
)
4601 verbose
= cmd
.verbose
4603 if cmd
.gitdir
is None:
4604 cmd
.gitdir
= os
.path
.abspath(".git")
4605 if not isValidGitDir(cmd
.gitdir
):
4606 # "rev-parse --git-dir" without arguments will try $PWD/.git
4607 cmd
.gitdir
= read_pipe(["git", "rev-parse", "--git-dir"]).strip()
4608 if os
.path
.exists(cmd
.gitdir
):
4609 cdup
= read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
4613 if not isValidGitDir(cmd
.gitdir
):
4614 if isValidGitDir(cmd
.gitdir
+ "/.git"):
4615 cmd
.gitdir
+= "/.git"
4617 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
4619 # so git commands invoked from the P4 workspace will succeed
4620 os
.environ
["GIT_DIR"] = cmd
.gitdir
4622 if not cmd
.run(args
):
4627 if __name__
== '__main__':