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('^([cku]?)x(.*)', '\\1\\2', p4Type
)
693 p4Type
= re
.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type
)
694 if p4Type
[-1] == "+":
695 p4Type
= p4Type
[0:-1]
697 p4_reopen(p4Type
, file)
700 def getP4OpenedType(file):
701 """Returns the perforce file type for the given file."""
703 result
= p4_read_pipe(["opened", wildcard_encode(file)])
704 match
= re
.match(".*\((.+)\)( \*exclusive\*)?\r?$", result
)
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(':(\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('Too many rows scanned \(over (\d+)\)', data
)
923 m
= re
.search('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("[*#@%]", 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
'\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"]
3256 # force a failure in fast-import, else an empty
3257 # commit will be made
3258 self
.gitStream
.write("\n")
3259 self
.gitStream
.write("die-now\n")
3260 self
.gitStream
.close()
3261 # ignore errors, but make sure it exits first
3262 self
.importProcess
.wait()
3264 die("Error from p4 print for %s: %s" % (f
, err
))
3266 die("Error from p4 print: %s" % err
)
3268 if 'depotFile' in marshalled
and self
.stream_have_file_info
:
3269 # start of a new file - output the old one first
3270 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
3271 self
.stream_file
= {}
3272 self
.stream_contents
= []
3273 self
.stream_have_file_info
= False
3275 # pick up the new file information... for the
3276 # 'data' field we need to append to our array
3277 for k
in marshalled
.keys():
3279 if 'streamContentSize' not in self
.stream_file
:
3280 self
.stream_file
['streamContentSize'] = 0
3281 self
.stream_file
['streamContentSize'] += len(marshalled
['data'])
3282 self
.stream_contents
.append(marshalled
['data'])
3284 self
.stream_file
[k
] = marshalled
[k
]
3287 'streamContentSize' in self
.stream_file
and
3288 'fileSize' in self
.stream_file
and
3289 'depotFile' in self
.stream_file
):
3290 size
= int(self
.stream_file
["fileSize"])
3292 progress
= 100*self
.stream_file
['streamContentSize']/size
3293 sys
.stdout
.write('\r%s %d%% (%s)' % (
3294 self
.stream_file
['depotFile'], progress
,
3295 format_size_human_readable(size
)))
3298 self
.stream_have_file_info
= True
3300 def streamP4Files(self
, files
):
3301 """Stream directly from "p4 files" into "git fast-import."""
3308 filesForCommit
.append(f
)
3309 if f
['action'] in self
.delete_actions
:
3310 filesToDelete
.append(f
)
3312 filesToRead
.append(f
)
3315 for f
in filesToDelete
:
3316 self
.streamOneP4Deletion(f
)
3318 if len(filesToRead
) > 0:
3319 self
.stream_file
= {}
3320 self
.stream_contents
= []
3321 self
.stream_have_file_info
= False
3323 # curry self argument
3324 def streamP4FilesCbSelf(entry
):
3325 self
.streamP4FilesCb(entry
)
3328 for f
in filesToRead
:
3329 if 'shelved_cl' in f
:
3330 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3332 fileArg
= f
['path'] + encode_text_stream('@={}'.format(f
['shelved_cl']))
3334 fileArg
= f
['path'] + encode_text_stream('#{}'.format(f
['rev']))
3336 fileArgs
.append(fileArg
)
3338 p4CmdList(["-x", "-", "print"],
3340 cb
=streamP4FilesCbSelf
)
3343 if 'depotFile' in self
.stream_file
:
3344 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
3346 def make_email(self
, userid
):
3347 if userid
in self
.users
:
3348 return self
.users
[userid
]
3350 userid_bytes
= metadata_stream_to_writable_bytes(userid
)
3351 return b
"%s <a@b>" % userid_bytes
3353 def streamTag(self
, gitStream
, labelName
, labelDetails
, commit
, epoch
):
3356 Commit is either a git commit, or a fast-import mark, ":<p4commit>".
3360 print("writing tag %s for commit %s" % (labelName
, commit
))
3361 gitStream
.write("tag %s\n" % labelName
)
3362 gitStream
.write("from %s\n" % commit
)
3364 if 'Owner' in labelDetails
:
3365 owner
= labelDetails
["Owner"]
3369 # Try to use the owner of the p4 label, or failing that,
3370 # the current p4 user id.
3372 email
= self
.make_email(owner
)
3374 email
= self
.make_email(self
.p4UserId())
3376 gitStream
.write("tagger ")
3377 gitStream
.write(email
)
3378 gitStream
.write(" %s %s\n" % (epoch
, self
.tz
))
3380 print("labelDetails=", labelDetails
)
3381 if 'Description' in labelDetails
:
3382 description
= labelDetails
['Description']
3384 description
= 'Label from git p4'
3386 gitStream
.write("data %d\n" % len(description
))
3387 gitStream
.write(description
)
3388 gitStream
.write("\n")
3390 def inClientSpec(self
, path
):
3391 if not self
.clientSpecDirs
:
3393 inClientSpec
= self
.clientSpecDirs
.map_in_client(path
)
3394 if not inClientSpec
and self
.verbose
:
3395 print('Ignoring file outside of client spec: {0}'.format(path
))
3398 def hasBranchPrefix(self
, path
):
3399 if not self
.branchPrefixes
:
3401 hasPrefix
= [p
for p
in self
.branchPrefixes
3402 if p4PathStartsWith(path
, p
)]
3403 if not hasPrefix
and self
.verbose
:
3404 print('Ignoring file outside of prefix: {0}'.format(path
))
3407 def findShadowedFiles(self
, files
, change
):
3408 """Perforce allows you commit files and directories with the same name,
3409 so you could have files //depot/foo and //depot/foo/bar both checked
3410 in. A p4 sync of a repository in this state fails. Deleting one of
3411 the files recovers the repository.
3413 Git will not allow the broken state to exist and only the most
3414 recent of the conflicting names is left in the repository. When one
3415 of the conflicting files is deleted we need to re-add the other one
3416 to make sure the git repository recovers in the same way as
3420 deleted
= [f
for f
in files
if f
['action'] in self
.delete_actions
]
3423 path
= decode_path(f
['path'])
3424 to_check
.add(path
+ '/...')
3426 path
= path
.rsplit("/", 1)[0]
3427 if path
== "/" or path
in to_check
:
3430 to_check
= ['%s@%s' % (wildcard_encode(p
), change
) for p
in to_check
3431 if self
.hasBranchPrefix(p
)]
3433 stat_result
= p4CmdList(["-x", "-", "fstat", "-T",
3434 "depotFile,headAction,headRev,headType"], stdin
=to_check
)
3435 for record
in stat_result
:
3436 if record
['code'] != 'stat':
3438 if record
['headAction'] in self
.delete_actions
:
3442 'path': record
['depotFile'],
3443 'rev': record
['headRev'],
3444 'type': record
['headType']})
3446 def commit(self
, details
, files
, branch
, parent
="", allow_empty
=False):
3447 epoch
= details
["time"]
3448 author
= details
["user"]
3449 jobs
= self
.extractJobsFromCommit(details
)
3452 print('commit into {0}'.format(branch
))
3454 files
= [f
for f
in files
3455 if self
.hasBranchPrefix(decode_path(f
['path']))]
3456 self
.findShadowedFiles(files
, details
['change'])
3458 if self
.clientSpecDirs
:
3459 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
3461 files
= [f
for f
in files
if self
.inClientSpec(decode_path(f
['path']))]
3463 if gitConfigBool('git-p4.keepEmptyCommits'):
3466 if not files
and not allow_empty
:
3467 print('Ignoring revision {0} as it would produce an empty commit.'
3468 .format(details
['change']))
3471 self
.gitStream
.write("commit %s\n" % branch
)
3472 self
.gitStream
.write("mark :%s\n" % details
["change"])
3473 self
.committedChanges
.add(int(details
["change"]))
3474 if author
not in self
.users
:
3475 self
.getUserMapFromPerforceServer()
3477 self
.gitStream
.write("committer ")
3478 self
.gitStream
.write(self
.make_email(author
))
3479 self
.gitStream
.write(" %s %s\n" % (epoch
, self
.tz
))
3481 self
.gitStream
.write("data <<EOT\n")
3482 self
.gitStream
.write(details
["desc"])
3484 self
.gitStream
.write("\nJobs: %s" % (' '.join(jobs
)))
3486 if not self
.suppress_meta_comment
:
3487 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3488 (','.join(self
.branchPrefixes
), details
["change"]))
3489 if len(details
['options']) > 0:
3490 self
.gitStream
.write(": options = %s" % details
['options'])
3491 self
.gitStream
.write("]\n")
3493 self
.gitStream
.write("EOT\n\n")
3497 print("parent %s" % parent
)
3498 self
.gitStream
.write("from %s\n" % parent
)
3500 self
.streamP4Files(files
)
3501 self
.gitStream
.write("\n")
3503 change
= int(details
["change"])
3505 if change
in self
.labels
:
3506 label
= self
.labels
[change
]
3507 labelDetails
= label
[0]
3508 labelRevisions
= label
[1]
3510 print("Change %s is labelled %s" % (change
, labelDetails
))
3512 files
= p4CmdList(["files"] + ["%s...@%s" % (p
, change
)
3513 for p
in self
.branchPrefixes
])
3515 if len(files
) == len(labelRevisions
):
3519 if info
["action"] in self
.delete_actions
:
3521 cleanedFiles
[info
["depotFile"]] = info
["rev"]
3523 if cleanedFiles
== labelRevisions
:
3524 self
.streamTag(self
.gitStream
, 'tag_%s' % labelDetails
['label'], labelDetails
, branch
, epoch
)
3528 print("Tag %s does not match with change %s: files do not match."
3529 % (labelDetails
["label"], change
))
3533 print("Tag %s does not match with change %s: file count is different."
3534 % (labelDetails
["label"], change
))
3536 def getLabels(self
):
3537 """Build a dictionary of changelists and labels, for "detect-labels"
3543 l
= p4CmdList(["labels"] + ["%s..." % p
for p
in self
.depotPaths
])
3544 if len(l
) > 0 and not self
.silent
:
3545 print("Finding files belonging to labels in %s" % self
.depotPaths
)
3548 label
= output
["label"]
3552 print("Querying files for label %s" % label
)
3553 for file in p4CmdList(["files"] +
3554 ["%s...@%s" % (p
, label
)
3555 for p
in self
.depotPaths
]):
3556 revisions
[file["depotFile"]] = file["rev"]
3557 change
= int(file["change"])
3558 if change
> newestChange
:
3559 newestChange
= change
3561 self
.labels
[newestChange
] = [output
, revisions
]
3564 print("Label changes: %s" % self
.labels
.keys())
3566 def importP4Labels(self
, stream
, p4Labels
):
3567 """Import p4 labels as git tags. A direct mapping does not exist, so
3568 assume that if all the files are at the same revision then we can
3569 use that, or it's something more complicated we should just ignore.
3573 print("import p4 labels: " + ' '.join(p4Labels
))
3575 ignoredP4Labels
= gitConfigList("git-p4.ignoredP4Labels")
3576 validLabelRegexp
= gitConfig("git-p4.labelImportRegexp")
3577 if len(validLabelRegexp
) == 0:
3578 validLabelRegexp
= defaultLabelRegexp
3579 m
= re
.compile(validLabelRegexp
)
3581 for name
in p4Labels
:
3584 if not m
.match(name
):
3586 print("label %s does not match regexp %s" % (name
, validLabelRegexp
))
3589 if name
in ignoredP4Labels
:
3592 labelDetails
= p4CmdList(['label', "-o", name
])[0]
3594 # get the most recent changelist for each file in this label
3595 change
= p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p
, name
)
3596 for p
in self
.depotPaths
])
3598 if 'change' in change
:
3599 # find the corresponding git commit; take the oldest commit
3600 changelist
= int(change
['change'])
3601 if changelist
in self
.committedChanges
:
3602 gitCommit
= ":%d" % changelist
# use a fast-import mark
3605 gitCommit
= read_pipe(["git", "rev-list", "--max-count=1",
3606 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist
], ignore_error
=True)
3607 if len(gitCommit
) == 0:
3608 print("importing label %s: could not find git commit for changelist %d" % (name
, changelist
))
3611 gitCommit
= gitCommit
.strip()
3614 # Convert from p4 time format
3616 tmwhen
= time
.strptime(labelDetails
['Update'], "%Y/%m/%d %H:%M:%S")
3618 print("Could not convert label time %s" % labelDetails
['Update'])
3621 when
= int(time
.mktime(tmwhen
))
3622 self
.streamTag(stream
, name
, labelDetails
, gitCommit
, when
)
3624 print("p4 label %s mapped to git commit %s" % (name
, gitCommit
))
3627 print("Label %s has no changelists - possibly deleted?" % name
)
3630 # We can't import this label; don't try again as it will get very
3631 # expensive repeatedly fetching all the files for labels that will
3632 # never be imported. If the label is moved in the future, the
3633 # ignore will need to be removed manually.
3634 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name
])
3636 def guessProjectName(self
):
3637 for p
in self
.depotPaths
:
3640 p
= p
[p
.strip().rfind("/") + 1:]
3641 if not p
.endswith("/"):
3645 def getBranchMapping(self
):
3646 lostAndFoundBranches
= set()
3648 user
= gitConfig("git-p4.branchUser")
3650 for info
in p4CmdList(
3651 ["branches"] + (["-u", user
] if len(user
) > 0 else [])):
3652 details
= p4Cmd(["branch", "-o", info
["branch"]])
3654 while "View%s" % viewIdx
in details
:
3655 paths
= details
["View%s" % viewIdx
].split(" ")
3656 viewIdx
= viewIdx
+ 1
3657 # require standard //depot/foo/... //depot/bar/... mapping
3658 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
3661 destination
= paths
[1]
3663 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
3664 source
= source
[len(self
.depotPaths
[0]):-4]
3665 destination
= destination
[len(self
.depotPaths
[0]):-4]
3667 if destination
in self
.knownBranches
:
3669 print("p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
))
3670 print("but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
))
3673 self
.knownBranches
[destination
] = source
3675 lostAndFoundBranches
.discard(destination
)
3677 if source
not in self
.knownBranches
:
3678 lostAndFoundBranches
.add(source
)
3680 # Perforce does not strictly require branches to be defined, so we also
3681 # check git config for a branch list.
3683 # Example of branch definition in git config file:
3685 # branchList=main:branchA
3686 # branchList=main:branchB
3687 # branchList=branchA:branchC
3688 configBranches
= gitConfigList("git-p4.branchList")
3689 for branch
in configBranches
:
3691 source
, destination
= branch
.split(":")
3692 self
.knownBranches
[destination
] = source
3694 lostAndFoundBranches
.discard(destination
)
3696 if source
not in self
.knownBranches
:
3697 lostAndFoundBranches
.add(source
)
3699 for branch
in lostAndFoundBranches
:
3700 self
.knownBranches
[branch
] = branch
3702 def getBranchMappingFromGitBranches(self
):
3703 branches
= p4BranchesInGit(self
.importIntoRemotes
)
3704 for branch
in branches
.keys():
3705 if branch
== "master":
3708 branch
= branch
[len(self
.projectName
):]
3709 self
.knownBranches
[branch
] = branch
3711 def updateOptionDict(self
, d
):
3713 if self
.keepRepoPath
:
3714 option_keys
['keepRepoPath'] = 1
3716 d
["options"] = ' '.join(sorted(option_keys
.keys()))
3718 def readOptions(self
, d
):
3719 self
.keepRepoPath
= ('options' in d
3720 and ('keepRepoPath' in d
['options']))
3722 def gitRefForBranch(self
, branch
):
3723 if branch
== "main":
3724 return self
.refPrefix
+ "master"
3726 if len(branch
) <= 0:
3729 return self
.refPrefix
+ self
.projectName
+ branch
3731 def gitCommitByP4Change(self
, ref
, change
):
3733 print("looking in ref " + ref
+ " for change %s using bisect..." % change
)
3736 latestCommit
= parseRevision(ref
)
3740 print("trying: earliest %s latest %s" % (earliestCommit
, latestCommit
))
3741 next
= read_pipe(["git", "rev-list", "--bisect",
3742 latestCommit
, earliestCommit
]).strip()
3747 log
= extractLogMessageFromGitCommit(next
)
3748 settings
= extractSettingsGitLog(log
)
3749 currentChange
= int(settings
['change'])
3751 print("current change %s" % currentChange
)
3753 if currentChange
== change
:
3755 print("found %s" % next
)
3758 if currentChange
< change
:
3759 earliestCommit
= "^%s" % next
3761 if next
== latestCommit
:
3762 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref
, change
))
3763 latestCommit
= "%s^@" % next
3767 def importNewBranch(self
, branch
, maxChange
):
3768 # make fast-import flush all changes to disk and update the refs using the checkpoint
3769 # command so that we can try to find the branch parent in the git history
3770 self
.gitStream
.write("checkpoint\n\n")
3771 self
.gitStream
.flush()
3772 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
3773 range = "@1,%s" % maxChange
3774 changes
= p4ChangesForPaths([branchPrefix
], range, self
.changes_block_size
)
3775 if len(changes
) <= 0:
3777 firstChange
= changes
[0]
3778 sourceBranch
= self
.knownBranches
[branch
]
3779 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
3780 sourceRef
= self
.gitRefForBranch(sourceBranch
)
3782 branchParentChange
= int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath
, firstChange
)])["change"])
3783 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
3784 if len(gitParent
) > 0:
3785 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
3787 self
.importChanges(changes
)
3790 def searchParent(self
, parent
, branch
, target
):
3791 targetTree
= read_pipe(["git", "rev-parse",
3792 "{}^{{tree}}".format(target
)]).strip()
3793 for line
in read_pipe_lines(["git", "rev-list", "--format=%H %T",
3794 "--no-merges", parent
]):
3795 if line
.startswith("commit "):
3797 commit
, tree
= line
.strip().split(" ")
3798 if tree
== targetTree
:
3800 print("Found parent of %s in commit %s" % (branch
, commit
))
3804 def importChanges(self
, changes
, origin_revision
=0):
3806 for change
in changes
:
3807 description
= p4_describe(change
)
3808 self
.updateOptionDict(description
)
3811 sys
.stdout
.write("\rImporting revision %s (%d%%)" % (
3812 change
, (cnt
* 100) // len(changes
)))
3817 if self
.detectBranches
:
3818 branches
= self
.splitFilesIntoBranches(description
)
3819 for branch
in branches
.keys():
3821 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
3822 self
.branchPrefixes
= [branchPrefix
]
3826 filesForCommit
= branches
[branch
]
3829 print("branch is %s" % branch
)
3831 self
.updatedBranches
.add(branch
)
3833 if branch
not in self
.createdBranches
:
3834 self
.createdBranches
.add(branch
)
3835 parent
= self
.knownBranches
[branch
]
3836 if parent
== branch
:
3839 fullBranch
= self
.projectName
+ branch
3840 if fullBranch
not in self
.p4BranchesInGit
:
3842 print("\n Importing new branch %s" % fullBranch
)
3843 if self
.importNewBranch(branch
, change
- 1):
3845 self
.p4BranchesInGit
.append(fullBranch
)
3847 print("\n Resuming with change %s" % change
)
3850 print("parent determined through known branches: %s" % parent
)
3852 branch
= self
.gitRefForBranch(branch
)
3853 parent
= self
.gitRefForBranch(parent
)
3856 print("looking for initial parent for %s; current parent is %s" % (branch
, parent
))
3858 if len(parent
) == 0 and branch
in self
.initialParents
:
3859 parent
= self
.initialParents
[branch
]
3860 del self
.initialParents
[branch
]
3864 tempBranch
= "%s/%d" % (self
.tempBranchLocation
, change
)
3866 print("Creating temporary branch: " + tempBranch
)
3867 self
.commit(description
, filesForCommit
, tempBranch
)
3868 self
.tempBranches
.append(tempBranch
)
3870 blob
= self
.searchParent(parent
, branch
, tempBranch
)
3872 self
.commit(description
, filesForCommit
, branch
, blob
)
3875 print("Parent of %s not found. Committing into head of %s" % (branch
, parent
))
3876 self
.commit(description
, filesForCommit
, branch
, parent
)
3878 files
= self
.extractFilesFromCommit(description
)
3879 self
.commit(description
, files
, self
.branch
,
3881 # only needed once, to connect to the previous commit
3882 self
.initialParent
= ""
3884 print(self
.gitError
.read())
3887 def sync_origin_only(self
):
3888 if self
.syncWithOrigin
:
3889 self
.hasOrigin
= originP4BranchesExist()
3892 print('Syncing with origin first, using "git fetch origin"')
3893 system(["git", "fetch", "origin"])
3895 def importHeadRevision(self
, revision
):
3896 print("Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
))
3899 details
["user"] = "git perforce import user"
3900 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
3901 % (' '.join(self
.depotPaths
), revision
))
3902 details
["change"] = revision
3906 fileArgs
= ["%s...%s" % (p
, revision
) for p
in self
.depotPaths
]
3908 for info
in p4CmdList(["files"] + fileArgs
):
3910 if 'code' in info
and info
['code'] == 'error':
3911 sys
.stderr
.write("p4 returned an error: %s\n"
3913 if info
['data'].find("must refer to client") >= 0:
3914 sys
.stderr
.write("This particular p4 error is misleading.\n")
3915 sys
.stderr
.write("Perhaps the depot path was misspelled.\n")
3916 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
3918 if 'p4ExitCode' in info
:
3919 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
3922 change
= int(info
["change"])
3923 if change
> newestRevision
:
3924 newestRevision
= change
3926 if info
["action"] in self
.delete_actions
:
3929 for prop
in ["depotFile", "rev", "action", "type"]:
3930 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
3932 fileCnt
= fileCnt
+ 1
3934 details
["change"] = newestRevision
3936 # Use time from top-most change so that all git p4 clones of
3937 # the same p4 repo have the same commit SHA1s.
3938 res
= p4_describe(newestRevision
)
3939 details
["time"] = res
["time"]
3941 self
.updateOptionDict(details
)
3943 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
)
3944 except IOError as err
:
3945 print("IO error with git fast-import. Is your git version recent enough?")
3946 print("IO error details: {}".format(err
))
3947 print(self
.gitError
.read())
3949 def importRevisions(self
, args
, branch_arg_given
):
3952 if len(self
.changesFile
) > 0:
3953 with
open(self
.changesFile
) as f
:
3954 output
= f
.readlines()
3957 changeSet
.add(int(line
))
3959 for change
in changeSet
:
3960 changes
.append(change
)
3964 # catch "git p4 sync" with no new branches, in a repo that
3965 # does not have any existing p4 branches
3967 if not self
.p4BranchesInGit
:
3968 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3970 # The default branch is master, unless --branch is used to
3971 # specify something else. Make sure it exists, or complain
3972 # nicely about how to use --branch.
3973 if not self
.detectBranches
:
3974 if not branch_exists(self
.branch
):
3975 if branch_arg_given
:
3976 raise P4CommandException("Error: branch %s does not exist." % self
.branch
)
3978 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
3982 print("Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
3984 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
, self
.changes_block_size
)
3986 if len(self
.maxChanges
) > 0:
3987 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
3989 if len(changes
) == 0:
3991 print("No changes to import!")
3993 if not self
.silent
and not self
.detectBranches
:
3994 print("Import destination: %s" % self
.branch
)
3996 self
.updatedBranches
= set()
3998 if not self
.detectBranches
:
4000 # start a new branch
4001 self
.initialParent
= ""
4003 # build on a previous revision
4004 self
.initialParent
= parseRevision(self
.branch
)
4006 self
.importChanges(changes
)
4010 if len(self
.updatedBranches
) > 0:
4011 sys
.stdout
.write("Updated branches: ")
4012 for b
in self
.updatedBranches
:
4013 sys
.stdout
.write("%s " % b
)
4014 sys
.stdout
.write("\n")
4016 def openStreams(self
):
4017 self
.importProcess
= subprocess
.Popen(["git", "fast-import"],
4018 stdin
=subprocess
.PIPE
,
4019 stdout
=subprocess
.PIPE
,
4020 stderr
=subprocess
.PIPE
)
4021 self
.gitOutput
= self
.importProcess
.stdout
4022 self
.gitStream
= self
.importProcess
.stdin
4023 self
.gitError
= self
.importProcess
.stderr
4025 if bytes
is not str:
4026 # Wrap gitStream.write() so that it can be called using `str` arguments
4027 def make_encoded_write(write
):
4028 def encoded_write(s
):
4029 return write(s
.encode() if isinstance(s
, str) else s
)
4030 return encoded_write
4032 self
.gitStream
.write
= make_encoded_write(self
.gitStream
.write
)
4034 def closeStreams(self
):
4035 if self
.gitStream
is None:
4037 self
.gitStream
.close()
4038 if self
.importProcess
.wait() != 0:
4039 die("fast-import failed: %s" % self
.gitError
.read())
4040 self
.gitOutput
.close()
4041 self
.gitError
.close()
4042 self
.gitStream
= None
4044 def run(self
, args
):
4045 if self
.importIntoRemotes
:
4046 self
.refPrefix
= "refs/remotes/p4/"
4048 self
.refPrefix
= "refs/heads/p4/"
4050 self
.sync_origin_only()
4052 branch_arg_given
= bool(self
.branch
)
4053 if len(self
.branch
) == 0:
4054 self
.branch
= self
.refPrefix
+ "master"
4055 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
4056 system(["git", "update-ref", self
.branch
, "refs/heads/p4"])
4057 system(["git", "branch", "-D", "p4"])
4059 # accept either the command-line option, or the configuration variable
4060 if self
.useClientSpec
:
4061 # will use this after clone to set the variable
4062 self
.useClientSpec_from_options
= True
4064 if gitConfigBool("git-p4.useclientspec"):
4065 self
.useClientSpec
= True
4066 if self
.useClientSpec
:
4067 self
.clientSpecDirs
= getClientSpec()
4069 # TODO: should always look at previous commits,
4070 # merge with previous imports, if possible.
4073 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
4075 # branches holds mapping from branch name to sha1
4076 branches
= p4BranchesInGit(self
.importIntoRemotes
)
4078 # restrict to just this one, disabling detect-branches
4079 if branch_arg_given
:
4080 short
= shortP4Ref(self
.branch
, self
.importIntoRemotes
)
4081 if short
in branches
:
4082 self
.p4BranchesInGit
= [short
]
4083 elif self
.branch
.startswith('refs/') and \
4084 branchExists(self
.branch
) and \
4085 '[git-p4:' in extractLogMessageFromGitCommit(self
.branch
):
4086 self
.p4BranchesInGit
= [self
.branch
]
4088 self
.p4BranchesInGit
= branches
.keys()
4090 if len(self
.p4BranchesInGit
) > 1:
4092 print("Importing from/into multiple branches")
4093 self
.detectBranches
= True
4094 for branch
in branches
.keys():
4095 self
.initialParents
[self
.refPrefix
+ branch
] = \
4099 print("branches: %s" % self
.p4BranchesInGit
)
4102 for branch
in self
.p4BranchesInGit
:
4103 logMsg
= extractLogMessageFromGitCommit(fullP4Ref(branch
,
4104 self
.importIntoRemotes
))
4106 settings
= extractSettingsGitLog(logMsg
)
4108 self
.readOptions(settings
)
4109 if 'depot-paths' in settings
and 'change' in settings
:
4110 change
= int(settings
['change']) + 1
4111 p4Change
= max(p4Change
, change
)
4113 depotPaths
= sorted(settings
['depot-paths'])
4114 if self
.previousDepotPaths
== []:
4115 self
.previousDepotPaths
= depotPaths
4118 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
4119 prev_list
= prev
.split("/")
4120 cur_list
= cur
.split("/")
4121 for i
in range(0, min(len(cur_list
), len(prev_list
))):
4122 if cur_list
[i
] != prev_list
[i
]:
4126 paths
.append("/".join(cur_list
[:i
+ 1]))
4128 self
.previousDepotPaths
= paths
4131 self
.depotPaths
= sorted(self
.previousDepotPaths
)
4132 self
.changeRange
= "@%s,#head" % p4Change
4133 if not self
.silent
and not self
.detectBranches
:
4134 print("Performing incremental import into %s git branch" % self
.branch
)
4136 self
.branch
= fullP4Ref(self
.branch
, self
.importIntoRemotes
)
4138 if len(args
) == 0 and self
.depotPaths
:
4140 print("Depot paths: %s" % ' '.join(self
.depotPaths
))
4142 if self
.depotPaths
and self
.depotPaths
!= args
:
4143 print("previous import used depot path %s and now %s was specified. "
4144 "This doesn't work!" % (' '.join(self
.depotPaths
),
4148 self
.depotPaths
= sorted(args
)
4153 # Make sure no revision specifiers are used when --changesfile
4155 bad_changesfile
= False
4156 if len(self
.changesFile
) > 0:
4157 for p
in self
.depotPaths
:
4158 if p
.find("@") >= 0 or p
.find("#") >= 0:
4159 bad_changesfile
= True
4162 die("Option --changesfile is incompatible with revision specifiers")
4165 for p
in self
.depotPaths
:
4166 if p
.find("@") != -1:
4167 atIdx
= p
.index("@")
4168 self
.changeRange
= p
[atIdx
:]
4169 if self
.changeRange
== "@all":
4170 self
.changeRange
= ""
4171 elif ',' not in self
.changeRange
:
4172 revision
= self
.changeRange
4173 self
.changeRange
= ""
4175 elif p
.find("#") != -1:
4176 hashIdx
= p
.index("#")
4177 revision
= p
[hashIdx
:]
4179 elif self
.previousDepotPaths
== []:
4180 # pay attention to changesfile, if given, else import
4181 # the entire p4 tree at the head revision
4182 if len(self
.changesFile
) == 0:
4185 p
= re
.sub("\.\.\.$", "", p
)
4186 if not p
.endswith("/"):
4191 self
.depotPaths
= newPaths
4193 # --detect-branches may change this for each branch
4194 self
.branchPrefixes
= self
.depotPaths
4196 self
.loadUserMapFromCache()
4198 if self
.detectLabels
:
4201 if self
.detectBranches
:
4202 # FIXME - what's a P4 projectName ?
4203 self
.projectName
= self
.guessProjectName()
4206 self
.getBranchMappingFromGitBranches()
4208 self
.getBranchMapping()
4210 print("p4-git branches: %s" % self
.p4BranchesInGit
)
4211 print("initial parents: %s" % self
.initialParents
)
4212 for b
in self
.p4BranchesInGit
:
4216 b
= b
[len(self
.projectName
):]
4217 self
.createdBranches
.add(b
)
4227 self
.importHeadRevision(revision
)
4229 self
.importRevisions(args
, branch_arg_given
)
4231 if gitConfigBool("git-p4.importLabels"):
4232 self
.importLabels
= True
4234 if self
.importLabels
:
4235 p4Labels
= getP4Labels(self
.depotPaths
)
4236 gitTags
= getGitTags()
4238 missingP4Labels
= p4Labels
- gitTags
4239 self
.importP4Labels(self
.gitStream
, missingP4Labels
)
4241 except P4CommandException
as e
:
4250 # Cleanup temporary branches created during import
4251 if self
.tempBranches
!= []:
4252 for branch
in self
.tempBranches
:
4253 read_pipe(["git", "update-ref", "-d", branch
])
4254 if len(read_pipe(["git", "for-each-ref", self
.tempBranchLocation
])) > 0:
4255 die("There are unexpected temporary branches")
4257 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4258 # a convenient shortcut refname "p4".
4259 if self
.importIntoRemotes
:
4260 head_ref
= self
.refPrefix
+ "HEAD"
4261 if not gitBranchExists(head_ref
) and gitBranchExists(self
.branch
):
4262 system(["git", "symbolic-ref", head_ref
, self
.branch
])
4267 class P4Rebase(Command
):
4269 Command
.__init
__(self
)
4271 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
4273 self
.importLabels
= False
4274 self
.description
= ("Fetches the latest revision from perforce and "
4275 + "rebases the current work (branch) against it")
4277 def run(self
, args
):
4279 sync
.importLabels
= self
.importLabels
4282 return self
.rebase()
4285 if os
.system("git update-index --refresh") != 0:
4286 die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up to date or stash away all your changes with git stash.")
4287 if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
4288 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.")
4290 upstream
, settings
= findUpstreamBranchPoint()
4291 if len(upstream
) == 0:
4292 die("Cannot find upstream branchpoint for rebase")
4294 # the branchpoint may be p4/foo~3, so strip off the parent
4295 upstream
= re
.sub("~[0-9]+$", "", upstream
)
4297 print("Rebasing the current branch onto %s" % upstream
)
4298 oldHead
= read_pipe(["git", "rev-parse", "HEAD"]).strip()
4299 system(["git", "rebase", upstream
])
4300 system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead
,
4305 class P4Clone(P4Sync
):
4307 P4Sync
.__init
__(self
)
4308 self
.description
= "Creates a new git repository and imports from Perforce into it"
4309 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
4311 optparse
.make_option("--destination", dest
="cloneDestination",
4312 action
='store', default
=None,
4313 help="where to leave result of the clone"),
4314 optparse
.make_option("--bare", dest
="cloneBare",
4315 action
="store_true", default
=False),
4317 self
.cloneDestination
= None
4318 self
.needsGit
= False
4319 self
.cloneBare
= False
4321 def defaultDestination(self
, args
):
4322 # TODO: use common prefix of args?
4324 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
4325 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
4326 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
4327 depotDir
= re
.sub(r
"/$", "", depotDir
)
4328 return os
.path
.split(depotDir
)[1]
4330 def run(self
, args
):
4334 if self
.keepRepoPath
and not self
.cloneDestination
:
4335 sys
.stderr
.write("Must specify destination for --keep-path\n")
4340 if not self
.cloneDestination
and len(depotPaths
) > 1:
4341 self
.cloneDestination
= depotPaths
[-1]
4342 depotPaths
= depotPaths
[:-1]
4344 for p
in depotPaths
:
4345 if not p
.startswith("//"):
4346 sys
.stderr
.write('Depot paths must start with "//": %s\n' % p
)
4349 if not self
.cloneDestination
:
4350 self
.cloneDestination
= self
.defaultDestination(args
)
4352 print("Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
))
4354 if not os
.path
.exists(self
.cloneDestination
):
4355 os
.makedirs(self
.cloneDestination
)
4356 chdir(self
.cloneDestination
)
4358 init_cmd
= ["git", "init"]
4360 init_cmd
.append("--bare")
4361 retcode
= subprocess
.call(init_cmd
)
4363 raise subprocess
.CalledProcessError(retcode
, init_cmd
)
4365 if not P4Sync
.run(self
, depotPaths
):
4368 # create a master branch and check out a work tree
4369 if gitBranchExists(self
.branch
):
4370 system(["git", "branch", currentGitBranch(), self
.branch
])
4371 if not self
.cloneBare
:
4372 system(["git", "checkout", "-f"])
4374 print('Not checking out any branch, use '
4375 '"git checkout -q -b master <branch>"')
4377 # auto-set this variable if invoked with --use-client-spec
4378 if self
.useClientSpec_from_options
:
4379 system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
4381 # persist any git-p4 encoding-handling config options passed in for clone:
4382 if gitConfig('git-p4.metadataDecodingStrategy'):
4383 system(["git", "config", "git-p4.metadataDecodingStrategy", gitConfig('git-p4.metadataDecodingStrategy')])
4384 if gitConfig('git-p4.metadataFallbackEncoding'):
4385 system(["git", "config", "git-p4.metadataFallbackEncoding", gitConfig('git-p4.metadataFallbackEncoding')])
4386 if gitConfig('git-p4.pathEncoding'):
4387 system(["git", "config", "git-p4.pathEncoding", gitConfig('git-p4.pathEncoding')])
4392 class P4Unshelve(Command
):
4394 Command
.__init
__(self
)
4396 self
.origin
= "HEAD"
4397 self
.description
= "Unshelve a P4 changelist into a git commit"
4398 self
.usage
= "usage: %prog [options] changelist"
4400 optparse
.make_option("--origin", dest
="origin",
4401 help="Use this base revision instead of the default (%s)" % self
.origin
),
4403 self
.verbose
= False
4404 self
.noCommit
= False
4405 self
.destbranch
= "refs/remotes/p4-unshelved"
4407 def renameBranch(self
, branch_name
):
4408 """Rename the existing branch to branch_name.N ."""
4410 for i
in range(0, 1000):
4411 backup_branch_name
= "{0}.{1}".format(branch_name
, i
)
4412 if not gitBranchExists(backup_branch_name
):
4413 # Copy ref to backup
4414 gitUpdateRef(backup_branch_name
, branch_name
)
4415 gitDeleteRef(branch_name
)
4416 print("renamed old unshelve branch to {0}".format(backup_branch_name
))
4419 sys
.exit("gave up trying to rename existing branch {0}".format(branch_name
))
4421 def findLastP4Revision(self
, starting_point
):
4422 """Look back from starting_point for the first commit created by git-p4
4423 to find the P4 commit we are based on, and the depot-paths.
4426 for parent
in (range(65535)):
4427 log
= extractLogMessageFromGitCommit("{0}~{1}".format(starting_point
, parent
))
4428 settings
= extractSettingsGitLog(log
)
4429 if 'change' in settings
:
4432 sys
.exit("could not find git-p4 commits in {0}".format(self
.origin
))
4434 def createShelveParent(self
, change
, branch_name
, sync
, origin
):
4435 """Create a commit matching the parent of the shelved changelist
4438 parent_description
= p4_describe(change
, shelved
=True)
4439 parent_description
['desc'] = 'parent for shelved changelist {}\n'.format(change
)
4440 files
= sync
.extractFilesFromCommit(parent_description
, shelved
=False, shelved_cl
=change
)
4444 # if it was added in the shelved changelist, it won't exist in the parent
4445 if f
['action'] in self
.add_actions
:
4448 # if it was deleted in the shelved changelist it must not be deleted
4449 # in the parent - we might even need to create it if the origin branch
4451 if f
['action'] in self
.delete_actions
:
4454 parent_files
.append(f
)
4456 sync
.commit(parent_description
, parent_files
, branch_name
,
4457 parent
=origin
, allow_empty
=True)
4458 print("created parent commit for {0} based on {1} in {2}".format(
4459 change
, self
.origin
, branch_name
))
4461 def run(self
, args
):
4465 if not gitBranchExists(self
.origin
):
4466 sys
.exit("origin branch {0} does not exist".format(self
.origin
))
4471 # only one change at a time
4474 # if the target branch already exists, rename it
4475 branch_name
= "{0}/{1}".format(self
.destbranch
, change
)
4476 if gitBranchExists(branch_name
):
4477 self
.renameBranch(branch_name
)
4478 sync
.branch
= branch_name
4480 sync
.verbose
= self
.verbose
4481 sync
.suppress_meta_comment
= True
4483 settings
= self
.findLastP4Revision(self
.origin
)
4484 sync
.depotPaths
= settings
['depot-paths']
4485 sync
.branchPrefixes
= sync
.depotPaths
4488 sync
.loadUserMapFromCache()
4491 # create a commit for the parent of the shelved changelist
4492 self
.createShelveParent(change
, branch_name
, sync
, self
.origin
)
4494 # create the commit for the shelved changelist itself
4495 description
= p4_describe(change
, True)
4496 files
= sync
.extractFilesFromCommit(description
, True, change
)
4498 sync
.commit(description
, files
, branch_name
, "")
4501 print("unshelved changelist {0} into {1}".format(change
, branch_name
))
4506 class P4Branches(Command
):
4508 Command
.__init
__(self
)
4510 self
.description
= ("Shows the git branches that hold imports and their "
4511 + "corresponding perforce depot paths")
4512 self
.verbose
= False
4514 def run(self
, args
):
4515 if originP4BranchesExist():
4516 createOrUpdateBranchesFromOrigin()
4518 for line
in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
4521 if not line
.startswith('p4/') or line
== "p4/HEAD":
4525 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
4526 settings
= extractSettingsGitLog(log
)
4528 print("%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"]))
4532 class HelpFormatter(optparse
.IndentedHelpFormatter
):
4534 optparse
.IndentedHelpFormatter
.__init
__(self
)
4536 def format_description(self
, description
):
4538 return description
+ "\n"
4543 def printUsage(commands
):
4544 print("usage: %s <command> [options]" % sys
.argv
[0])
4546 print("valid commands: %s" % ", ".join(commands
))
4548 print("Try %s <command> --help for command specific help." % sys
.argv
[0])
4558 "branches": P4Branches
,
4559 "unshelve": P4Unshelve
,
4564 if len(sys
.argv
[1:]) == 0:
4565 printUsage(commands
.keys())
4568 cmdName
= sys
.argv
[1]
4570 klass
= commands
[cmdName
]
4573 print("unknown command %s" % cmdName
)
4575 printUsage(commands
.keys())
4578 options
= cmd
.options
4579 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
4583 options
.append(optparse
.make_option("--verbose", "-v", dest
="verbose", action
="store_true"))
4585 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
4587 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
4589 description
=cmd
.description
,
4590 formatter
=HelpFormatter())
4593 cmd
, args
= parser
.parse_args(sys
.argv
[2:], cmd
)
4599 verbose
= cmd
.verbose
4601 if cmd
.gitdir
is None:
4602 cmd
.gitdir
= os
.path
.abspath(".git")
4603 if not isValidGitDir(cmd
.gitdir
):
4604 # "rev-parse --git-dir" without arguments will try $PWD/.git
4605 cmd
.gitdir
= read_pipe(["git", "rev-parse", "--git-dir"]).strip()
4606 if os
.path
.exists(cmd
.gitdir
):
4607 cdup
= read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
4611 if not isValidGitDir(cmd
.gitdir
):
4612 if isValidGitDir(cmd
.gitdir
+ "/.git"):
4613 cmd
.gitdir
+= "/.git"
4615 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
4617 # so git commands invoked from the P4 workspace will succeed
4618 os
.environ
["GIT_DIR"] = cmd
.gitdir
4620 if not cmd
.run(args
):
4625 if __name__
== '__main__':