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 p4CmdList(cmd
, stdin
=None, stdin_mode
='w+b', cb
=None, skip_info
=False,
826 errors_as_exceptions
=False, *k
, **kw
):
828 cmd
= p4_build_cmd(["-G"] + cmd
)
830 sys
.stderr
.write("Opening pipe: {}\n".format(' '.join(cmd
)))
832 # Use a temporary file to avoid deadlocks without
833 # subprocess.communicate(), which would put another copy
834 # of stdout into memory.
836 if stdin
is not None:
837 stdin_file
= tempfile
.TemporaryFile(prefix
='p4-stdin', mode
=stdin_mode
)
838 if not isinstance(stdin
, list):
839 stdin_file
.write(stdin
)
842 stdin_file
.write(encode_text_stream(i
))
843 stdin_file
.write(b
'\n')
847 p4
= subprocess
.Popen(
848 cmd
, stdin
=stdin_file
, stdout
=subprocess
.PIPE
, *k
, **kw
)
853 entry
= marshal
.load(p4
.stdout
)
855 # Decode unmarshalled dict to use str keys and values, except for:
856 # - `data` which may contain arbitrary binary data
857 # - `desc` or `FullName` which may contain non-UTF8 encoded text handled below, eagerly converted to bytes
858 # - `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text, handled by decode_path()
860 for key
, value
in entry
.items():
862 if isinstance(value
, bytes
) and not (key
in ('data', 'desc', 'FullName', 'path', 'clientFile') or key
.startswith('depotFile')):
863 value
= value
.decode()
864 decoded_entry
[key
] = value
865 # Parse out data if it's an error response
866 if decoded_entry
.get('code') == 'error' and 'data' in decoded_entry
:
867 decoded_entry
['data'] = decoded_entry
['data'].decode()
868 entry
= decoded_entry
870 if 'code' in entry
and entry
['code'] == 'info':
873 entry
['desc'] = metadata_stream_to_writable_bytes(entry
['desc'])
874 if 'FullName' in entry
:
875 entry
['FullName'] = metadata_stream_to_writable_bytes(entry
['FullName'])
884 if errors_as_exceptions
:
886 data
= result
[0].get('data')
888 m
= re
.search('Too many rows scanned \(over (\d+)\)', data
)
890 m
= re
.search('Request too large \(over (\d+)\)', data
)
893 limit
= int(m
.group(1))
894 raise P4RequestSizeException(exitCode
, result
, limit
)
896 raise P4ServerException(exitCode
, result
)
898 raise P4Exception(exitCode
)
901 entry
["p4ExitCode"] = exitCode
907 def p4Cmd(cmd
, *k
, **kw
):
908 list = p4CmdList(cmd
, *k
, **kw
)
915 def p4Where(depotPath
):
916 if not depotPath
.endswith("/"):
918 depotPathLong
= depotPath
+ "..."
919 outputList
= p4CmdList(["where", depotPathLong
])
921 for entry
in outputList
:
922 if "depotFile" in entry
:
923 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
924 # The base path always ends with "/...".
925 entry_path
= decode_path(entry
['depotFile'])
926 if entry_path
.find(depotPath
) == 0 and entry_path
[-4:] == "/...":
929 elif "data" in entry
:
930 data
= entry
.get("data")
931 space
= data
.find(" ")
932 if data
[:space
] == depotPath
:
937 if output
["code"] == "error":
941 clientPath
= decode_path(output
['path'])
942 elif "data" in output
:
943 data
= output
.get("data")
944 lastSpace
= data
.rfind(b
" ")
945 clientPath
= decode_path(data
[lastSpace
+ 1:])
947 if clientPath
.endswith("..."):
948 clientPath
= clientPath
[:-3]
952 def currentGitBranch():
953 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
956 def isValidGitDir(path
):
957 return git_dir(path
) is not None
960 def parseRevision(ref
):
961 return read_pipe(["git", "rev-parse", ref
]).strip()
964 def branchExists(ref
):
965 rev
= read_pipe(["git", "rev-parse", "-q", "--verify", ref
],
970 def extractLogMessageFromGitCommit(commit
):
973 # fixme: title is first line of commit, not 1st paragraph.
975 for log
in read_pipe_lines(["git", "cat-file", "commit", commit
]):
985 def extractSettingsGitLog(log
):
987 for line
in log
.split("\n"):
989 m
= re
.search(r
"^ *\[git-p4: (.*)\]$", line
)
993 assignments
= m
.group(1).split(':')
994 for a
in assignments
:
996 key
= vals
[0].strip()
997 val
= ('='.join(vals
[1:])).strip()
998 if val
.endswith('\"') and val
.startswith('"'):
1003 paths
= values
.get("depot-paths")
1005 paths
= values
.get("depot-path")
1007 values
['depot-paths'] = paths
.split(',')
1011 def gitBranchExists(branch
):
1012 proc
= subprocess
.Popen(["git", "rev-parse", branch
],
1013 stderr
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
)
1014 return proc
.wait() == 0
1017 def gitUpdateRef(ref
, newvalue
):
1018 subprocess
.check_call(["git", "update-ref", ref
, newvalue
])
1021 def gitDeleteRef(ref
):
1022 subprocess
.check_call(["git", "update-ref", "-d", ref
])
1028 def gitConfig(key
, typeSpecifier
=None):
1029 if key
not in _gitConfig
:
1030 cmd
= ["git", "config"]
1032 cmd
+= [typeSpecifier
]
1034 s
= read_pipe(cmd
, ignore_error
=True)
1035 _gitConfig
[key
] = s
.strip()
1036 return _gitConfig
[key
]
1039 def gitConfigBool(key
):
1040 """Return a bool, using git config --bool. It is True only if the
1041 variable is set to true, and False if set to false or not present
1045 if key
not in _gitConfig
:
1046 _gitConfig
[key
] = gitConfig(key
, '--bool') == "true"
1047 return _gitConfig
[key
]
1050 def gitConfigInt(key
):
1051 if key
not in _gitConfig
:
1052 cmd
= ["git", "config", "--int", key
]
1053 s
= read_pipe(cmd
, ignore_error
=True)
1056 _gitConfig
[key
] = int(gitConfig(key
, '--int'))
1058 _gitConfig
[key
] = None
1059 return _gitConfig
[key
]
1062 def gitConfigList(key
):
1063 if key
not in _gitConfig
:
1064 s
= read_pipe(["git", "config", "--get-all", key
], ignore_error
=True)
1065 _gitConfig
[key
] = s
.strip().splitlines()
1066 if _gitConfig
[key
] == ['']:
1067 _gitConfig
[key
] = []
1068 return _gitConfig
[key
]
1070 def fullP4Ref(incomingRef
, importIntoRemotes
=True):
1071 """Standardize a given provided p4 ref value to a full git ref:
1072 refs/foo/bar/branch -> use it exactly
1073 p4/branch -> prepend refs/remotes/ or refs/heads/
1074 branch -> prepend refs/remotes/p4/ or refs/heads/p4/"""
1075 if incomingRef
.startswith("refs/"):
1077 if importIntoRemotes
:
1078 prepend
= "refs/remotes/"
1080 prepend
= "refs/heads/"
1081 if not incomingRef
.startswith("p4/"):
1083 return prepend
+ incomingRef
1085 def shortP4Ref(incomingRef
, importIntoRemotes
=True):
1086 """Standardize to a "short ref" if possible:
1087 refs/foo/bar/branch -> ignore
1088 refs/remotes/p4/branch or refs/heads/p4/branch -> shorten
1089 p4/branch -> shorten"""
1090 if importIntoRemotes
:
1091 longprefix
= "refs/remotes/p4/"
1093 longprefix
= "refs/heads/p4/"
1094 if incomingRef
.startswith(longprefix
):
1095 return incomingRef
[len(longprefix
):]
1096 if incomingRef
.startswith("p4/"):
1097 return incomingRef
[3:]
1100 def p4BranchesInGit(branchesAreInRemotes
=True):
1101 """Find all the branches whose names start with "p4/", looking
1102 in remotes or heads as specified by the argument. Return
1103 a dictionary of { branch: revision } for each one found.
1104 The branch names are the short names, without any
1110 cmdline
= ["git", "rev-parse", "--symbolic"]
1111 if branchesAreInRemotes
:
1112 cmdline
.append("--remotes")
1114 cmdline
.append("--branches")
1116 for line
in read_pipe_lines(cmdline
):
1119 # only import to p4/
1120 if not line
.startswith('p4/'):
1122 # special symbolic ref to p4/master
1123 if line
== "p4/HEAD":
1126 # strip off p4/ prefix
1127 branch
= line
[len("p4/"):]
1129 branches
[branch
] = parseRevision(line
)
1134 def branch_exists(branch
):
1135 """Make sure that the given ref name really exists."""
1137 cmd
= ["git", "rev-parse", "--symbolic", "--verify", branch
]
1138 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
1139 out
, _
= p
.communicate()
1140 out
= decode_text_stream(out
)
1143 # expect exactly one line of output: the branch name
1144 return out
.rstrip() == branch
1147 def findUpstreamBranchPoint(head
="HEAD"):
1148 branches
= p4BranchesInGit()
1149 # map from depot-path to branch name
1150 branchByDepotPath
= {}
1151 for branch
in branches
.keys():
1152 tip
= branches
[branch
]
1153 log
= extractLogMessageFromGitCommit(tip
)
1154 settings
= extractSettingsGitLog(log
)
1155 if "depot-paths" in settings
:
1156 git_branch
= "remotes/p4/" + branch
1157 paths
= ",".join(settings
["depot-paths"])
1158 branchByDepotPath
[paths
] = git_branch
1159 if "change" in settings
:
1160 paths
= paths
+ ";" + settings
["change"]
1161 branchByDepotPath
[paths
] = git_branch
1165 while parent
< 65535:
1166 commit
= head
+ "~%s" % parent
1167 log
= extractLogMessageFromGitCommit(commit
)
1168 settings
= extractSettingsGitLog(log
)
1169 if "depot-paths" in settings
:
1170 paths
= ",".join(settings
["depot-paths"])
1171 if "change" in settings
:
1172 expaths
= paths
+ ";" + settings
["change"]
1173 if expaths
in branchByDepotPath
:
1174 return [branchByDepotPath
[expaths
], settings
]
1175 if paths
in branchByDepotPath
:
1176 return [branchByDepotPath
[paths
], settings
]
1180 return ["", settings
]
1183 def createOrUpdateBranchesFromOrigin(localRefPrefix
="refs/remotes/p4/", silent
=True):
1185 print("Creating/updating branch(es) in %s based on origin branch(es)"
1188 originPrefix
= "origin/p4/"
1190 for line
in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
1192 if (not line
.startswith(originPrefix
)) or line
.endswith("HEAD"):
1195 headName
= line
[len(originPrefix
):]
1196 remoteHead
= localRefPrefix
+ headName
1199 original
= extractSettingsGitLog(extractLogMessageFromGitCommit(originHead
))
1200 if 'depot-paths' not in original
or 'change' not in original
:
1204 if not gitBranchExists(remoteHead
):
1206 print("creating %s" % remoteHead
)
1209 settings
= extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead
))
1210 if 'change' in settings
:
1211 if settings
['depot-paths'] == original
['depot-paths']:
1212 originP4Change
= int(original
['change'])
1213 p4Change
= int(settings
['change'])
1214 if originP4Change
> p4Change
:
1215 print("%s (%s) is newer than %s (%s). "
1216 "Updating p4 branch from origin."
1217 % (originHead
, originP4Change
,
1218 remoteHead
, p4Change
))
1221 print("Ignoring: %s was imported from %s while "
1222 "%s was imported from %s"
1223 % (originHead
, ','.join(original
['depot-paths']),
1224 remoteHead
, ','.join(settings
['depot-paths'])))
1227 system(["git", "update-ref", remoteHead
, originHead
])
1230 def originP4BranchesExist():
1231 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
1234 def p4ParseNumericChangeRange(parts
):
1235 changeStart
= int(parts
[0][1:])
1236 if parts
[1] == '#head':
1237 changeEnd
= p4_last_change()
1239 changeEnd
= int(parts
[1])
1241 return (changeStart
, changeEnd
)
1244 def chooseBlockSize(blockSize
):
1248 return defaultBlockSize
1251 def p4ChangesForPaths(depotPaths
, changeRange
, requestedBlockSize
):
1254 # Parse the change range into start and end. Try to find integer
1255 # revision ranges as these can be broken up into blocks to avoid
1256 # hitting server-side limits (maxrows, maxscanresults). But if
1257 # that doesn't work, fall back to using the raw revision specifier
1258 # strings, without using block mode.
1260 if changeRange
is None or changeRange
== '':
1262 changeEnd
= p4_last_change()
1263 block_size
= chooseBlockSize(requestedBlockSize
)
1265 parts
= changeRange
.split(',')
1266 assert len(parts
) == 2
1268 changeStart
, changeEnd
= p4ParseNumericChangeRange(parts
)
1269 block_size
= chooseBlockSize(requestedBlockSize
)
1271 changeStart
= parts
[0][1:]
1272 changeEnd
= parts
[1]
1273 if requestedBlockSize
:
1274 die("cannot use --changes-block-size with non-numeric revisions")
1279 # Retrieve changes a block at a time, to prevent running
1280 # into a MaxResults/MaxScanRows error from the server. If
1281 # we _do_ hit one of those errors, turn down the block size
1287 end
= min(changeEnd
, changeStart
+ block_size
)
1288 revisionRange
= "%d,%d" % (changeStart
, end
)
1290 revisionRange
= "%s,%s" % (changeStart
, changeEnd
)
1292 for p
in depotPaths
:
1293 cmd
+= ["%s...@%s" % (p
, revisionRange
)]
1297 result
= p4CmdList(cmd
, errors_as_exceptions
=True)
1298 except P4RequestSizeException
as e
:
1300 block_size
= e
.limit
1301 elif block_size
> e
.limit
:
1302 block_size
= e
.limit
1304 block_size
= max(2, block_size
// 2)
1307 print("block size error, retrying with block size {0}".format(block_size
))
1309 except P4Exception
as e
:
1310 die('Error retrieving changes description ({0})'.format(e
.p4ExitCode
))
1312 # Insert changes in chronological order
1313 for entry
in reversed(result
):
1314 if 'change' not in entry
:
1316 changes
.add(int(entry
['change']))
1321 if end
>= changeEnd
:
1324 changeStart
= end
+ 1
1326 changes
= sorted(changes
)
1330 def p4PathStartsWith(path
, prefix
):
1331 """This method tries to remedy a potential mixed-case issue:
1333 If UserA adds //depot/DirA/file1
1334 and UserB adds //depot/dira/file2
1336 we may or may not have a problem. If you have core.ignorecase=true,
1337 we treat DirA and dira as the same directory.
1339 if gitConfigBool("core.ignorecase"):
1340 return path
.lower().startswith(prefix
.lower())
1341 return path
.startswith(prefix
)
1344 def getClientSpec():
1345 """Look at the p4 client spec, create a View() object that contains
1346 all the mappings, and return it.
1349 specList
= p4CmdList(["client", "-o"])
1350 if len(specList
) != 1:
1351 die('Output from "client -o" is %d lines, expecting 1' %
1354 # dictionary of all client parameters
1357 # the //client/ name
1358 client_name
= entry
["Client"]
1360 # just the keys that start with "View"
1361 view_keys
= [k
for k
in entry
.keys() if k
.startswith("View")]
1363 # hold this new View
1364 view
= View(client_name
)
1366 # append the lines, in order, to the view
1367 for view_num
in range(len(view_keys
)):
1368 k
= "View%d" % view_num
1369 if k
not in view_keys
:
1370 die("Expected view key %s missing" % k
)
1371 view
.append(entry
[k
])
1376 def getClientRoot():
1377 """Grab the client directory."""
1379 output
= p4CmdList(["client", "-o"])
1380 if len(output
) != 1:
1381 die('Output from "client -o" is %d lines, expecting 1' % len(output
))
1384 if "Root" not in entry
:
1385 die('Client has no "Root"')
1387 return entry
["Root"]
1390 def wildcard_decode(path
):
1391 """Decode P4 wildcards into %xx encoding
1393 P4 wildcards are not allowed in filenames. P4 complains if you simply
1394 add them, but you can force it with "-f", in which case it translates
1395 them into %xx encoding internally.
1398 # Search for and fix just these four characters. Do % last so
1399 # that fixing it does not inadvertently create new %-escapes.
1400 # Cannot have * in a filename in windows; untested as to
1401 # what p4 would do in such a case.
1402 if not platform
.system() == "Windows":
1403 path
= path
.replace("%2A", "*")
1404 path
= path
.replace("%23", "#") \
1405 .replace("%40", "@") \
1406 .replace("%25", "%")
1410 def wildcard_encode(path
):
1411 """Encode %xx coded wildcards into P4 coding."""
1413 # do % first to avoid double-encoding the %s introduced here
1414 path
= path
.replace("%", "%25") \
1415 .replace("*", "%2A") \
1416 .replace("#", "%23") \
1417 .replace("@", "%40")
1421 def wildcard_present(path
):
1422 m
= re
.search("[*#@%]", path
)
1423 return m
is not None
1426 class LargeFileSystem(object):
1427 """Base class for large file system support."""
1429 def __init__(self
, writeToGitStream
):
1430 self
.largeFiles
= set()
1431 self
.writeToGitStream
= writeToGitStream
1433 def generatePointer(self
, cloneDestination
, contentFile
):
1434 """Return the content of a pointer file that is stored in Git instead
1435 of the actual content.
1437 assert False, "Method 'generatePointer' required in " + self
.__class
__.__name
__
1439 def pushFile(self
, localLargeFile
):
1440 """Push the actual content which is not stored in the Git repository to
1443 assert False, "Method 'pushFile' required in " + self
.__class
__.__name
__
1445 def hasLargeFileExtension(self
, relPath
):
1446 return functools
.reduce(
1447 lambda a
, b
: a
or b
,
1448 [relPath
.endswith('.' + e
) for e
in gitConfigList('git-p4.largeFileExtensions')],
1452 def generateTempFile(self
, contents
):
1453 contentFile
= tempfile
.NamedTemporaryFile(prefix
='git-p4-large-file', delete
=False)
1455 contentFile
.write(d
)
1457 return contentFile
.name
1459 def exceedsLargeFileThreshold(self
, relPath
, contents
):
1460 if gitConfigInt('git-p4.largeFileThreshold'):
1461 contentsSize
= sum(len(d
) for d
in contents
)
1462 if contentsSize
> gitConfigInt('git-p4.largeFileThreshold'):
1464 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1465 contentsSize
= sum(len(d
) for d
in contents
)
1466 if contentsSize
<= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1468 contentTempFile
= self
.generateTempFile(contents
)
1469 compressedContentFile
= tempfile
.NamedTemporaryFile(prefix
='git-p4-large-file', delete
=True)
1470 with zipfile
.ZipFile(compressedContentFile
, mode
='w') as zf
:
1471 zf
.write(contentTempFile
, compress_type
=zipfile
.ZIP_DEFLATED
)
1472 compressedContentsSize
= zf
.infolist()[0].compress_size
1473 os
.remove(contentTempFile
)
1474 if compressedContentsSize
> gitConfigInt('git-p4.largeFileCompressedThreshold'):
1478 def addLargeFile(self
, relPath
):
1479 self
.largeFiles
.add(relPath
)
1481 def removeLargeFile(self
, relPath
):
1482 self
.largeFiles
.remove(relPath
)
1484 def isLargeFile(self
, relPath
):
1485 return relPath
in self
.largeFiles
1487 def processContent(self
, git_mode
, relPath
, contents
):
1488 """Processes the content of git fast import. This method decides if a
1489 file is stored in the large file system and handles all necessary
1492 if self
.exceedsLargeFileThreshold(relPath
, contents
) or self
.hasLargeFileExtension(relPath
):
1493 contentTempFile
= self
.generateTempFile(contents
)
1494 pointer_git_mode
, contents
, localLargeFile
= self
.generatePointer(contentTempFile
)
1495 if pointer_git_mode
:
1496 git_mode
= pointer_git_mode
1498 # Move temp file to final location in large file system
1499 largeFileDir
= os
.path
.dirname(localLargeFile
)
1500 if not os
.path
.isdir(largeFileDir
):
1501 os
.makedirs(largeFileDir
)
1502 shutil
.move(contentTempFile
, localLargeFile
)
1503 self
.addLargeFile(relPath
)
1504 if gitConfigBool('git-p4.largeFilePush'):
1505 self
.pushFile(localLargeFile
)
1507 sys
.stderr
.write("%s moved to large file system (%s)\n" % (relPath
, localLargeFile
))
1508 return (git_mode
, contents
)
1511 class MockLFS(LargeFileSystem
):
1512 """Mock large file system for testing."""
1514 def generatePointer(self
, contentFile
):
1515 """The pointer content is the original content prefixed with "pointer-".
1516 The local filename of the large file storage is derived from the
1519 with
open(contentFile
, 'r') as f
:
1522 pointerContents
= 'pointer-' + content
1523 localLargeFile
= os
.path
.join(os
.getcwd(), '.git', 'mock-storage', 'local', content
[:-1])
1524 return (gitMode
, pointerContents
, localLargeFile
)
1526 def pushFile(self
, localLargeFile
):
1527 """The remote filename of the large file storage is the same as the
1528 local one but in a different directory.
1530 remotePath
= os
.path
.join(os
.path
.dirname(localLargeFile
), '..', 'remote')
1531 if not os
.path
.exists(remotePath
):
1532 os
.makedirs(remotePath
)
1533 shutil
.copyfile(localLargeFile
, os
.path
.join(remotePath
, os
.path
.basename(localLargeFile
)))
1536 class GitLFS(LargeFileSystem
):
1537 """Git LFS as backend for the git-p4 large file system.
1538 See https://git-lfs.github.com/ for details.
1541 def __init__(self
, *args
):
1542 LargeFileSystem
.__init
__(self
, *args
)
1543 self
.baseGitAttributes
= []
1545 def generatePointer(self
, contentFile
):
1546 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1547 mode and content which is stored in the Git repository instead of
1548 the actual content. Return also the new location of the actual
1551 if os
.path
.getsize(contentFile
) == 0:
1552 return (None, '', None)
1554 pointerProcess
= subprocess
.Popen(
1555 ['git', 'lfs', 'pointer', '--file=' + contentFile
],
1556 stdout
=subprocess
.PIPE
1558 pointerFile
= decode_text_stream(pointerProcess
.stdout
.read())
1559 if pointerProcess
.wait():
1560 os
.remove(contentFile
)
1561 die('git-lfs pointer command failed. Did you install the extension?')
1563 # Git LFS removed the preamble in the output of the 'pointer' command
1564 # starting from version 1.2.0. Check for the preamble here to support
1566 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1567 if pointerFile
.startswith('Git LFS pointer for'):
1568 pointerFile
= re
.sub(r
'Git LFS pointer for.*\n\n', '', pointerFile
)
1570 oid
= re
.search(r
'^oid \w+:(\w+)', pointerFile
, re
.MULTILINE
).group(1)
1571 # if someone use external lfs.storage ( not in local repo git )
1572 lfs_path
= gitConfig('lfs.storage')
1575 if not os
.path
.isabs(lfs_path
):
1576 lfs_path
= os
.path
.join(os
.getcwd(), '.git', lfs_path
)
1577 localLargeFile
= os
.path
.join(
1579 'objects', oid
[:2], oid
[2:4],
1582 # LFS Spec states that pointer files should not have the executable bit set.
1584 return (gitMode
, pointerFile
, localLargeFile
)
1586 def pushFile(self
, localLargeFile
):
1587 uploadProcess
= subprocess
.Popen(
1588 ['git', 'lfs', 'push', '--object-id', 'origin', os
.path
.basename(localLargeFile
)]
1590 if uploadProcess
.wait():
1591 die('git-lfs push command failed. Did you define a remote?')
1593 def generateGitAttributes(self
):
1595 self
.baseGitAttributes
+
1599 '# Git LFS (see https://git-lfs.github.com/)\n',
1602 ['*.' + f
.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1603 for f
in sorted(gitConfigList('git-p4.largeFileExtensions'))
1605 ['/' + f
.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1606 for f
in sorted(self
.largeFiles
) if not self
.hasLargeFileExtension(f
)
1610 def addLargeFile(self
, relPath
):
1611 LargeFileSystem
.addLargeFile(self
, relPath
)
1612 self
.writeToGitStream('100644', '.gitattributes', self
.generateGitAttributes())
1614 def removeLargeFile(self
, relPath
):
1615 LargeFileSystem
.removeLargeFile(self
, relPath
)
1616 self
.writeToGitStream('100644', '.gitattributes', self
.generateGitAttributes())
1618 def processContent(self
, git_mode
, relPath
, contents
):
1619 if relPath
== '.gitattributes':
1620 self
.baseGitAttributes
= contents
1621 return (git_mode
, self
.generateGitAttributes())
1623 return LargeFileSystem
.processContent(self
, git_mode
, relPath
, contents
)
1627 delete_actions
= ("delete", "move/delete", "purge")
1628 add_actions
= ("add", "branch", "move/add")
1631 self
.usage
= "usage: %prog [options]"
1632 self
.needsGit
= True
1633 self
.verbose
= False
1635 # This is required for the "append" update_shelve action
1636 def ensure_value(self
, attr
, value
):
1637 if not hasattr(self
, attr
) or getattr(self
, attr
) is None:
1638 setattr(self
, attr
, value
)
1639 return getattr(self
, attr
)
1644 self
.userMapFromPerforceServer
= False
1645 self
.myP4UserId
= None
1649 return self
.myP4UserId
1651 results
= p4CmdList(["user", "-o"])
1654 self
.myP4UserId
= r
['User']
1656 die("Could not find your p4 user id")
1658 def p4UserIsMe(self
, p4User
):
1659 """Return True if the given p4 user is actually me."""
1660 me
= self
.p4UserId()
1661 if not p4User
or p4User
!= me
:
1666 def getUserCacheFilename(self
):
1667 home
= os
.environ
.get("HOME", os
.environ
.get("USERPROFILE"))
1668 return home
+ "/.gitp4-usercache.txt"
1670 def getUserMapFromPerforceServer(self
):
1671 if self
.userMapFromPerforceServer
:
1676 for output
in p4CmdList(["users"]):
1677 if "User" not in output
:
1679 # "FullName" is bytes. "Email" on the other hand might be bytes
1680 # or unicode string depending on whether we are running under
1681 # python2 or python3. To support
1682 # git-p4.metadataDecodingStrategy=fallback, self.users dict values
1683 # are always bytes, ready to be written to git.
1684 emailbytes
= metadata_stream_to_writable_bytes(output
["Email"])
1685 self
.users
[output
["User"]] = output
["FullName"] + b
" <" + emailbytes
+ b
">"
1686 self
.emails
[output
["Email"]] = output
["User"]
1688 mapUserConfigRegex
= re
.compile(r
"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re
.VERBOSE
)
1689 for mapUserConfig
in gitConfigList("git-p4.mapUser"):
1690 mapUser
= mapUserConfigRegex
.findall(mapUserConfig
)
1691 if mapUser
and len(mapUser
[0]) == 3:
1692 user
= mapUser
[0][0]
1693 fullname
= mapUser
[0][1]
1694 email
= mapUser
[0][2]
1695 fulluser
= fullname
+ " <" + email
+ ">"
1696 self
.users
[user
] = metadata_stream_to_writable_bytes(fulluser
)
1697 self
.emails
[email
] = user
1700 for (key
, val
) in self
.users
.items():
1701 keybytes
= metadata_stream_to_writable_bytes(key
)
1702 s
+= b
"%s\t%s\n" % (keybytes
.expandtabs(1), val
.expandtabs(1))
1704 open(self
.getUserCacheFilename(), 'wb').write(s
)
1705 self
.userMapFromPerforceServer
= True
1707 def loadUserMapFromCache(self
):
1709 self
.userMapFromPerforceServer
= False
1711 cache
= open(self
.getUserCacheFilename(), 'rb')
1712 lines
= cache
.readlines()
1715 entry
= line
.strip().split(b
"\t")
1716 self
.users
[entry
[0].decode('utf_8')] = entry
[1]
1718 self
.getUserMapFromPerforceServer()
1721 class P4Submit(Command
, P4UserMap
):
1723 conflict_behavior_choices
= ("ask", "skip", "quit")
1726 Command
.__init
__(self
)
1727 P4UserMap
.__init
__(self
)
1729 optparse
.make_option("--origin", dest
="origin"),
1730 optparse
.make_option("-M", dest
="detectRenames", action
="store_true"),
1731 # preserve the user, requires relevant p4 permissions
1732 optparse
.make_option("--preserve-user", dest
="preserveUser", action
="store_true"),
1733 optparse
.make_option("--export-labels", dest
="exportLabels", action
="store_true"),
1734 optparse
.make_option("--dry-run", "-n", dest
="dry_run", action
="store_true"),
1735 optparse
.make_option("--prepare-p4-only", dest
="prepare_p4_only", action
="store_true"),
1736 optparse
.make_option("--conflict", dest
="conflict_behavior",
1737 choices
=self
.conflict_behavior_choices
),
1738 optparse
.make_option("--branch", dest
="branch"),
1739 optparse
.make_option("--shelve", dest
="shelve", action
="store_true",
1740 help="Shelve instead of submit. Shelved files are reverted, "
1741 "restoring the workspace to the state before the shelve"),
1742 optparse
.make_option("--update-shelve", dest
="update_shelve", action
="append", type="int",
1743 metavar
="CHANGELIST",
1744 help="update an existing shelved changelist, implies --shelve, "
1745 "repeat in-order for multiple shelved changelists"),
1746 optparse
.make_option("--commit", dest
="commit", metavar
="COMMIT",
1747 help="submit only the specified commit(s), one commit or xxx..xxx"),
1748 optparse
.make_option("--disable-rebase", dest
="disable_rebase", action
="store_true",
1749 help="Disable rebase after submit is completed. Can be useful if you "
1750 "work from a local git branch that is not master"),
1751 optparse
.make_option("--disable-p4sync", dest
="disable_p4sync", action
="store_true",
1752 help="Skip Perforce sync of p4/master after submit or shelve"),
1753 optparse
.make_option("--no-verify", dest
="no_verify", action
="store_true",
1754 help="Bypass p4-pre-submit and p4-changelist hooks"),
1756 self
.description
= """Submit changes from git to the perforce depot.\n
1757 The `p4-pre-submit` hook is executed if it exists and is executable. It
1758 can be bypassed with the `--no-verify` command line option. The hook takes
1759 no parameters and nothing from standard input. Exiting with a non-zero status
1760 from this script prevents `git-p4 submit` from launching.
1762 One usage scenario is to run unit tests in the hook.
1764 The `p4-prepare-changelist` hook is executed right after preparing the default
1765 changelist message and before the editor is started. It takes one parameter,
1766 the name of the file that contains the changelist text. Exiting with a non-zero
1767 status from the script will abort the process.
1769 The purpose of the hook is to edit the message file in place, and it is not
1770 supressed by the `--no-verify` option. This hook is called even if
1771 `--prepare-p4-only` is set.
1773 The `p4-changelist` hook is executed after the changelist message has been
1774 edited by the user. It can be bypassed with the `--no-verify` option. It
1775 takes a single parameter, the name of the file that holds the proposed
1776 changelist text. Exiting with a non-zero status causes the command to abort.
1778 The hook is allowed to edit the changelist file and can be used to normalize
1779 the text into some project standard format. It can also be used to refuse the
1780 Submit after inspect the message file.
1782 The `p4-post-changelist` hook is invoked after the submit has successfully
1783 occurred in P4. It takes no parameters and is meant primarily for notification
1784 and cannot affect the outcome of the git p4 submit action.
1787 self
.usage
+= " [name of git branch to submit into perforce depot]"
1789 self
.detectRenames
= False
1790 self
.preserveUser
= gitConfigBool("git-p4.preserveUser")
1791 self
.dry_run
= False
1793 self
.update_shelve
= list()
1795 self
.disable_rebase
= gitConfigBool("git-p4.disableRebase")
1796 self
.disable_p4sync
= gitConfigBool("git-p4.disableP4Sync")
1797 self
.prepare_p4_only
= False
1798 self
.conflict_behavior
= None
1799 self
.isWindows
= (platform
.system() == "Windows")
1800 self
.exportLabels
= False
1801 self
.p4HasMoveCommand
= p4_has_move_command()
1803 self
.no_verify
= False
1805 if gitConfig('git-p4.largeFileSystem'):
1806 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1809 if len(p4CmdList(["opened", "..."])) > 0:
1810 die("You have files opened with perforce! Close them before starting the sync.")
1812 def separate_jobs_from_description(self
, message
):
1813 """Extract and return a possible Jobs field in the commit message. It
1814 goes into a separate section in the p4 change specification.
1816 A jobs line starts with "Jobs:" and looks like a new field in a
1817 form. Values are white-space separated on the same line or on
1818 following lines that start with a tab.
1820 This does not parse and extract the full git commit message like a
1821 p4 form. It just sees the Jobs: line as a marker to pass everything
1822 from then on directly into the p4 form, but outside the description
1825 Return a tuple (stripped log message, jobs string).
1828 m
= re
.search(r
'^Jobs:', message
, re
.MULTILINE
)
1830 return (message
, None)
1832 jobtext
= message
[m
.start():]
1833 stripped_message
= message
[:m
.start()].rstrip()
1834 return (stripped_message
, jobtext
)
1836 def prepareLogMessage(self
, template
, message
, jobs
):
1837 """Edits the template returned from "p4 change -o" to insert the
1838 message in the Description field, and the jobs text in the Jobs
1843 inDescriptionSection
= False
1845 for line
in template
.split("\n"):
1846 if line
.startswith("#"):
1847 result
+= line
+ "\n"
1850 if inDescriptionSection
:
1851 if line
.startswith("Files:") or line
.startswith("Jobs:"):
1852 inDescriptionSection
= False
1853 # insert Jobs section
1855 result
+= jobs
+ "\n"
1859 if line
.startswith("Description:"):
1860 inDescriptionSection
= True
1862 for messageLine
in message
.split("\n"):
1863 line
+= "\t" + messageLine
+ "\n"
1865 result
+= line
+ "\n"
1869 def patchRCSKeywords(self
, file, regexp
):
1870 """Attempt to zap the RCS keywords in a p4 controlled file matching the
1873 handle
, outFileName
= tempfile
.mkstemp(dir='.')
1875 with os
.fdopen(handle
, "wb") as outFile
, open(file, "rb") as inFile
:
1876 for line
in inFile
.readlines():
1877 outFile
.write(regexp
.sub(br
'$\1$', line
))
1878 # Forcibly overwrite the original file
1880 shutil
.move(outFileName
, file)
1882 # cleanup our temporary file
1883 os
.unlink(outFileName
)
1884 print("Failed to strip RCS keywords in %s" % file)
1887 print("Patched up RCS keywords in %s" % file)
1889 def p4UserForCommit(self
, id):
1890 """Return the tuple (perforce user,git email) for a given git commit
1893 self
.getUserMapFromPerforceServer()
1894 gitEmail
= read_pipe(["git", "log", "--max-count=1",
1895 "--format=%ae", id])
1896 gitEmail
= gitEmail
.strip()
1897 if gitEmail
not in self
.emails
:
1898 return (None, gitEmail
)
1900 return (self
.emails
[gitEmail
], gitEmail
)
1902 def checkValidP4Users(self
, commits
):
1903 """Check if any git authors cannot be mapped to p4 users."""
1905 user
, email
= self
.p4UserForCommit(id)
1907 msg
= "Cannot find p4 user for email %s in commit %s." % (email
, id)
1908 if gitConfigBool("git-p4.allowMissingP4Users"):
1911 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg
)
1913 def lastP4Changelist(self
):
1914 """Get back the last changelist number submitted in this client spec.
1916 This then gets used to patch up the username in the change. If the
1917 same client spec is being used by multiple processes then this might
1920 results
= p4CmdList(["client", "-o"]) # find the current client
1924 client
= r
['Client']
1927 die("could not get client spec")
1928 results
= p4CmdList(["changes", "-c", client
, "-m", "1"])
1932 die("Could not get changelist number for last submit - cannot patch up user details")
1934 def modifyChangelistUser(self
, changelist
, newUser
):
1935 """Fixup the user field of a changelist after it has been submitted."""
1936 changes
= p4CmdList(["change", "-o", changelist
])
1937 if len(changes
) != 1:
1938 die("Bad output from p4 change modifying %s to user %s" %
1939 (changelist
, newUser
))
1942 if c
['User'] == newUser
:
1946 # p4 does not understand format version 3 and above
1947 input = marshal
.dumps(c
, 2)
1949 result
= p4CmdList(["change", "-f", "-i"], stdin
=input)
1952 if r
['code'] == 'error':
1953 die("Could not modify user field of changelist %s to %s:%s" % (changelist
, newUser
, r
['data']))
1955 print("Updated user field for changelist %s to %s" % (changelist
, newUser
))
1957 die("Could not modify user field of changelist %s to %s" % (changelist
, newUser
))
1959 def canChangeChangelists(self
):
1960 """Check to see if we have p4 admin or super-user permissions, either
1961 of which are required to modify changelists.
1963 results
= p4CmdList(["protects", self
.depotPath
])
1966 if r
['perm'] == 'admin':
1968 if r
['perm'] == 'super':
1972 def prepareSubmitTemplate(self
, changelist
=None):
1973 """Run "p4 change -o" to grab a change specification template.
1975 This does not use "p4 -G", as it is nice to keep the submission
1976 template in original order, since a human might edit it.
1978 Remove lines in the Files section that show changes to files
1979 outside the depot path we're committing into.
1982 upstream
, settings
= findUpstreamBranchPoint()
1985 # A Perforce Change Specification.
1987 # Change: The change number. 'new' on a new changelist.
1988 # Date: The date this specification was last modified.
1989 # Client: The client on which the changelist was created. Read-only.
1990 # User: The user who created the changelist.
1991 # Status: Either 'pending' or 'submitted'. Read-only.
1992 # Type: Either 'public' or 'restricted'. Default is 'public'.
1993 # Description: Comments about the changelist. Required.
1994 # Jobs: What opened jobs are to be closed by this changelist.
1995 # You may delete jobs from this list. (New changelists only.)
1996 # Files: What opened files from the default changelist are to be added
1997 # to this changelist. You may delete files from this list.
1998 # (New changelists only.)
2001 inFilesSection
= False
2003 args
= ['change', '-o']
2005 args
.append(str(changelist
))
2006 for entry
in p4CmdList(args
):
2007 if 'code' not in entry
:
2009 if entry
['code'] == 'stat':
2010 change_entry
= entry
2012 if not change_entry
:
2013 die('Failed to decode output of p4 change -o')
2014 for key
, value
in change_entry
.items():
2015 if key
.startswith('File'):
2016 if 'depot-paths' in settings
:
2017 if not [p
for p
in settings
['depot-paths']
2018 if p4PathStartsWith(value
, p
)]:
2021 if not p4PathStartsWith(value
, self
.depotPath
):
2023 files_list
.append(value
)
2025 # Output in the order expected by prepareLogMessage
2026 for key
in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
2027 if key
not in change_entry
:
2030 template
+= key
+ ':'
2031 if key
== 'Description':
2033 for field_line
in change_entry
[key
].splitlines():
2034 template
+= '\t'+field_line
+'\n'
2035 if len(files_list
) > 0:
2037 template
+= 'Files:\n'
2038 for path
in files_list
:
2039 template
+= '\t'+path
+'\n'
2042 def edit_template(self
, template_file
):
2043 """Invoke the editor to let the user change the submission message.
2045 Return true if okay to continue with the submit.
2048 # if configured to skip the editing part, just submit
2049 if gitConfigBool("git-p4.skipSubmitEdit"):
2052 # look at the modification time, to check later if the user saved
2054 mtime
= os
.stat(template_file
).st_mtime
2057 if "P4EDITOR" in os
.environ
and (os
.environ
.get("P4EDITOR") != ""):
2058 editor
= os
.environ
.get("P4EDITOR")
2060 editor
= read_pipe(["git", "var", "GIT_EDITOR"]).strip()
2061 system(["sh", "-c", ('%s "$@"' % editor
), editor
, template_file
])
2063 # If the file was not saved, prompt to see if this patch should
2064 # be skipped. But skip this verification step if configured so.
2065 if gitConfigBool("git-p4.skipSubmitEditCheck"):
2068 # modification time updated means user saved the file
2069 if os
.stat(template_file
).st_mtime
> mtime
:
2072 response
= prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
2078 def get_diff_description(self
, editedFiles
, filesToAdd
, symlinks
):
2080 if "P4DIFF" in os
.environ
:
2081 del(os
.environ
["P4DIFF"])
2083 for editedFile
in editedFiles
:
2084 diff
+= p4_read_pipe(['diff', '-du',
2085 wildcard_encode(editedFile
)])
2089 for newFile
in filesToAdd
:
2090 newdiff
+= "==== new file ====\n"
2091 newdiff
+= "--- /dev/null\n"
2092 newdiff
+= "+++ %s\n" % newFile
2094 is_link
= os
.path
.islink(newFile
)
2095 expect_link
= newFile
in symlinks
2097 if is_link
and expect_link
:
2098 newdiff
+= "+%s\n" % os
.readlink(newFile
)
2100 f
= open(newFile
, "r")
2102 for line
in f
.readlines():
2103 newdiff
+= "+" + line
2104 except UnicodeDecodeError:
2105 # Found non-text data and skip, since diff description
2106 # should only include text
2110 return (diff
+ newdiff
).replace('\r\n', '\n')
2112 def applyCommit(self
, id):
2113 """Apply one commit, return True if it succeeded."""
2115 print("Applying", read_pipe(["git", "show", "-s",
2116 "--format=format:%h %s", id]))
2118 p4User
, gitEmail
= self
.p4UserForCommit(id)
2120 diff
= read_pipe_lines(
2121 ["git", "diff-tree", "-r"] + self
.diffOpts
+ ["{}^".format(id), id])
2123 filesToChangeType
= set()
2124 filesToDelete
= set()
2126 pureRenameCopy
= set()
2128 filesToChangeExecBit
= {}
2132 diff
= parseDiffTreeEntry(line
)
2133 modifier
= diff
['status']
2135 all_files
.append(path
)
2139 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
2140 filesToChangeExecBit
[path
] = diff
['dst_mode']
2141 editedFiles
.add(path
)
2142 elif modifier
== "A":
2143 filesToAdd
.add(path
)
2144 filesToChangeExecBit
[path
] = diff
['dst_mode']
2145 if path
in filesToDelete
:
2146 filesToDelete
.remove(path
)
2148 dst_mode
= int(diff
['dst_mode'], 8)
2149 if dst_mode
== 0o120000:
2152 elif modifier
== "D":
2153 filesToDelete
.add(path
)
2154 if path
in filesToAdd
:
2155 filesToAdd
.remove(path
)
2156 elif modifier
== "C":
2157 src
, dest
= diff
['src'], diff
['dst']
2158 all_files
.append(dest
)
2159 p4_integrate(src
, dest
)
2160 pureRenameCopy
.add(dest
)
2161 if diff
['src_sha1'] != diff
['dst_sha1']:
2163 pureRenameCopy
.discard(dest
)
2164 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
2166 pureRenameCopy
.discard(dest
)
2167 filesToChangeExecBit
[dest
] = diff
['dst_mode']
2169 # turn off read-only attribute
2170 os
.chmod(dest
, stat
.S_IWRITE
)
2172 editedFiles
.add(dest
)
2173 elif modifier
== "R":
2174 src
, dest
= diff
['src'], diff
['dst']
2175 all_files
.append(dest
)
2176 if self
.p4HasMoveCommand
:
2177 p4_edit(src
) # src must be open before move
2178 p4_move(src
, dest
) # opens for (move/delete, move/add)
2180 p4_integrate(src
, dest
)
2181 if diff
['src_sha1'] != diff
['dst_sha1']:
2184 pureRenameCopy
.add(dest
)
2185 if isModeExecChanged(diff
['src_mode'], diff
['dst_mode']):
2186 if not self
.p4HasMoveCommand
:
2187 p4_edit(dest
) # with move: already open, writable
2188 filesToChangeExecBit
[dest
] = diff
['dst_mode']
2189 if not self
.p4HasMoveCommand
:
2191 os
.chmod(dest
, stat
.S_IWRITE
)
2193 filesToDelete
.add(src
)
2194 editedFiles
.add(dest
)
2195 elif modifier
== "T":
2196 filesToChangeType
.add(path
)
2198 die("unknown modifier %s for %s" % (modifier
, path
))
2200 diffcmd
= "git diff-tree --full-index -p \"%s\"" % (id)
2201 patchcmd
= diffcmd
+ " | git apply "
2202 tryPatchCmd
= patchcmd
+ "--check -"
2203 applyPatchCmd
= patchcmd
+ "--check --apply -"
2204 patch_succeeded
= True
2207 print("TryPatch: %s" % tryPatchCmd
)
2209 if os
.system(tryPatchCmd
) != 0:
2210 fixed_rcs_keywords
= False
2211 patch_succeeded
= False
2212 print("Unfortunately applying the change failed!")
2214 # Patch failed, maybe it's just RCS keyword woes. Look through
2215 # the patch to see if that's possible.
2216 if gitConfigBool("git-p4.attemptRCSCleanup"):
2219 for file in editedFiles | filesToDelete
:
2220 # did this file's delta contain RCS keywords?
2221 regexp
= p4_keywords_regexp_for_file(file)
2223 # this file is a possibility...look for RCS keywords.
2224 for line
in read_pipe_lines(
2225 ["git", "diff", "%s^..%s" % (id, id), file],
2227 if regexp
.search(line
):
2229 print("got keyword match on %s in %s in %s" % (regex
.pattern
, line
, file))
2230 kwfiles
[file] = regexp
2233 for file, regexp
in kwfiles
.items():
2235 print("zapping %s with %s" % (line
, regexp
.pattern
))
2236 # File is being deleted, so not open in p4. Must
2237 # disable the read-only bit on windows.
2238 if self
.isWindows
and file not in editedFiles
:
2239 os
.chmod(file, stat
.S_IWRITE
)
2240 self
.patchRCSKeywords(file, kwfiles
[file])
2241 fixed_rcs_keywords
= True
2243 if fixed_rcs_keywords
:
2244 print("Retrying the patch with RCS keywords cleaned up")
2245 if os
.system(tryPatchCmd
) == 0:
2246 patch_succeeded
= True
2247 print("Patch succeesed this time with RCS keywords cleaned")
2249 if not patch_succeeded
:
2250 for f
in editedFiles
:
2255 # Apply the patch for real, and do add/delete/+x handling.
2257 system(applyPatchCmd
, shell
=True)
2259 for f
in filesToChangeType
:
2260 p4_edit(f
, "-t", "auto")
2261 for f
in filesToAdd
:
2263 for f
in filesToDelete
:
2267 # Set/clear executable bits
2268 for f
in filesToChangeExecBit
.keys():
2269 mode
= filesToChangeExecBit
[f
]
2270 setP4ExecBit(f
, mode
)
2273 if len(self
.update_shelve
) > 0:
2274 update_shelve
= self
.update_shelve
.pop(0)
2275 p4_reopen_in_change(update_shelve
, all_files
)
2278 # Build p4 change description, starting with the contents
2279 # of the git commit message.
2281 logMessage
= extractLogMessageFromGitCommit(id)
2282 logMessage
= logMessage
.strip()
2283 logMessage
, jobs
= self
.separate_jobs_from_description(logMessage
)
2285 template
= self
.prepareSubmitTemplate(update_shelve
)
2286 submitTemplate
= self
.prepareLogMessage(template
, logMessage
, jobs
)
2288 if self
.preserveUser
:
2289 submitTemplate
+= "\n######## Actual user %s, modified after commit\n" % p4User
2291 if self
.checkAuthorship
and not self
.p4UserIsMe(p4User
):
2292 submitTemplate
+= "######## git author %s does not match your p4 account.\n" % gitEmail
2293 submitTemplate
+= "######## Use option --preserve-user to modify authorship.\n"
2294 submitTemplate
+= "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2296 separatorLine
= "######## everything below this line is just the diff #######\n"
2297 if not self
.prepare_p4_only
:
2298 submitTemplate
+= separatorLine
2299 submitTemplate
+= self
.get_diff_description(editedFiles
, filesToAdd
, symlinks
)
2301 handle
, fileName
= tempfile
.mkstemp()
2302 tmpFile
= os
.fdopen(handle
, "w+b")
2304 submitTemplate
= submitTemplate
.replace("\n", "\r\n")
2305 tmpFile
.write(encode_text_stream(submitTemplate
))
2311 # Allow the hook to edit the changelist text before presenting it
2313 if not run_git_hook("p4-prepare-changelist", [fileName
]):
2316 if self
.prepare_p4_only
:
2318 # Leave the p4 tree prepared, and the submit template around
2319 # and let the user decide what to do next
2323 print("P4 workspace prepared for submission.")
2324 print("To submit or revert, go to client workspace")
2325 print(" " + self
.clientPath
)
2327 print("To submit, use \"p4 submit\" to write a new description,")
2328 print("or \"p4 submit -i <%s\" to use the one prepared by"
2329 " \"git p4\"." % fileName
)
2330 print("You can delete the file \"%s\" when finished." % fileName
)
2332 if self
.preserveUser
and p4User
and not self
.p4UserIsMe(p4User
):
2333 print("To preserve change ownership by user %s, you must\n"
2334 "do \"p4 change -f <change>\" after submitting and\n"
2335 "edit the User field.")
2337 print("After submitting, renamed files must be re-synced.")
2338 print("Invoke \"p4 sync -f\" on each of these files:")
2339 for f
in pureRenameCopy
:
2343 print("To revert the changes, use \"p4 revert ...\", and delete")
2344 print("the submit template file \"%s\"" % fileName
)
2346 print("Since the commit adds new files, they must be deleted:")
2347 for f
in filesToAdd
:
2353 if self
.edit_template(fileName
):
2354 if not self
.no_verify
:
2355 if not run_git_hook("p4-changelist", [fileName
]):
2356 print("The p4-changelist hook failed.")
2360 # read the edited message and submit
2361 tmpFile
= open(fileName
, "rb")
2362 message
= decode_text_stream(tmpFile
.read())
2365 message
= message
.replace("\r\n", "\n")
2366 if message
.find(separatorLine
) != -1:
2367 submitTemplate
= message
[:message
.index(separatorLine
)]
2369 submitTemplate
= message
2371 if len(submitTemplate
.strip()) == 0:
2372 print("Changelist is empty, aborting this changelist.")
2377 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate
)
2379 p4_write_pipe(['shelve', '-i'], submitTemplate
)
2381 p4_write_pipe(['submit', '-i'], submitTemplate
)
2382 # The rename/copy happened by applying a patch that created a
2383 # new file. This leaves it writable, which confuses p4.
2384 for f
in pureRenameCopy
:
2387 if self
.preserveUser
:
2389 # Get last changelist number. Cannot easily get it from
2390 # the submit command output as the output is
2392 changelist
= self
.lastP4Changelist()
2393 self
.modifyChangelistUser(changelist
, p4User
)
2397 run_git_hook("p4-post-changelist")
2399 # Revert changes if we skip this patch
2400 if not submitted
or self
.shelve
:
2402 print("Reverting shelved files.")
2404 print("Submission cancelled, undoing p4 changes.")
2406 for f
in editedFiles | filesToDelete
:
2408 for f
in filesToAdd
:
2412 if not self
.prepare_p4_only
:
2416 def exportGitTags(self
, gitTags
):
2417 """Export git tags as p4 labels. Create a p4 label and then tag with
2421 validLabelRegexp
= gitConfig("git-p4.labelExportRegexp")
2422 if len(validLabelRegexp
) == 0:
2423 validLabelRegexp
= defaultLabelRegexp
2424 m
= re
.compile(validLabelRegexp
)
2426 for name
in gitTags
:
2428 if not m
.match(name
):
2430 print("tag %s does not match regexp %s" % (name
, validLabelRegexp
))
2433 # Get the p4 commit this corresponds to
2434 logMessage
= extractLogMessageFromGitCommit(name
)
2435 values
= extractSettingsGitLog(logMessage
)
2437 if 'change' not in values
:
2438 # a tag pointing to something not sent to p4; ignore
2440 print("git tag %s does not give a p4 commit" % name
)
2443 changelist
= values
['change']
2445 # Get the tag details.
2449 for l
in read_pipe_lines(["git", "cat-file", "-p", name
]):
2452 if re
.match(r
'tag\s+', l
):
2454 elif re
.match(r
'\s*$', l
):
2461 body
= ["lightweight tag imported by git p4\n"]
2463 # Create the label - use the same view as the client spec we are using
2464 clientSpec
= getClientSpec()
2466 labelTemplate
= "Label: %s\n" % name
2467 labelTemplate
+= "Description:\n"
2469 labelTemplate
+= "\t" + b
+ "\n"
2470 labelTemplate
+= "View:\n"
2471 for depot_side
in clientSpec
.mappings
:
2472 labelTemplate
+= "\t%s\n" % depot_side
2475 print("Would create p4 label %s for tag" % name
)
2476 elif self
.prepare_p4_only
:
2477 print("Not creating p4 label %s for tag due to option"
2478 " --prepare-p4-only" % name
)
2480 p4_write_pipe(["label", "-i"], labelTemplate
)
2483 p4_system(["tag", "-l", name
] +
2484 ["%s@%s" % (depot_side
, changelist
) for depot_side
in clientSpec
.mappings
])
2487 print("created p4 label for tag %s" % name
)
2489 def run(self
, args
):
2491 self
.master
= currentGitBranch()
2492 elif len(args
) == 1:
2493 self
.master
= args
[0]
2494 if not branchExists(self
.master
):
2495 die("Branch %s does not exist" % self
.master
)
2499 for i
in self
.update_shelve
:
2501 sys
.exit("invalid changelist %d" % i
)
2504 allowSubmit
= gitConfig("git-p4.allowSubmit")
2505 if len(allowSubmit
) > 0 and not self
.master
in allowSubmit
.split(","):
2506 die("%s is not in git-p4.allowSubmit" % self
.master
)
2508 upstream
, settings
= findUpstreamBranchPoint()
2509 self
.depotPath
= settings
['depot-paths'][0]
2510 if len(self
.origin
) == 0:
2511 self
.origin
= upstream
2513 if len(self
.update_shelve
) > 0:
2516 if self
.preserveUser
:
2517 if not self
.canChangeChangelists():
2518 die("Cannot preserve user names without p4 super-user or admin permissions")
2520 # if not set from the command line, try the config file
2521 if self
.conflict_behavior
is None:
2522 val
= gitConfig("git-p4.conflict")
2524 if val
not in self
.conflict_behavior_choices
:
2525 die("Invalid value '%s' for config git-p4.conflict" % val
)
2528 self
.conflict_behavior
= val
2531 print("Origin branch is " + self
.origin
)
2533 if len(self
.depotPath
) == 0:
2534 print("Internal error: cannot locate perforce depot path from existing branches")
2537 self
.useClientSpec
= False
2538 if gitConfigBool("git-p4.useclientspec"):
2539 self
.useClientSpec
= True
2540 if self
.useClientSpec
:
2541 self
.clientSpecDirs
= getClientSpec()
2543 # Check for the existence of P4 branches
2544 branchesDetected
= (len(p4BranchesInGit().keys()) > 1)
2546 if self
.useClientSpec
and not branchesDetected
:
2547 # all files are relative to the client spec
2548 self
.clientPath
= getClientRoot()
2550 self
.clientPath
= p4Where(self
.depotPath
)
2552 if self
.clientPath
== "":
2553 die("Error: Cannot locate perforce checkout of %s in client view" % self
.depotPath
)
2555 print("Perforce checkout for depot path %s located at %s" % (self
.depotPath
, self
.clientPath
))
2556 self
.oldWorkingDirectory
= os
.getcwd()
2558 # ensure the clientPath exists
2559 new_client_dir
= False
2560 if not os
.path
.exists(self
.clientPath
):
2561 new_client_dir
= True
2562 os
.makedirs(self
.clientPath
)
2564 chdir(self
.clientPath
, is_client_path
=True)
2566 print("Would synchronize p4 checkout in %s" % self
.clientPath
)
2568 print("Synchronizing p4 checkout...")
2570 # old one was destroyed, and maybe nobody told p4
2571 p4_sync("...", "-f")
2578 committish
= self
.master
2582 if self
.commit
!= "":
2583 if self
.commit
.find("..") != -1:
2584 limits_ish
= self
.commit
.split("..")
2585 for line
in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish
[0], limits_ish
[1])]):
2586 commits
.append(line
.strip())
2589 commits
.append(self
.commit
)
2591 for line
in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self
.origin
, committish
)]):
2592 commits
.append(line
.strip())
2595 if self
.preserveUser
or gitConfigBool("git-p4.skipUserNameCheck"):
2596 self
.checkAuthorship
= False
2598 self
.checkAuthorship
= True
2600 if self
.preserveUser
:
2601 self
.checkValidP4Users(commits
)
2604 # Build up a set of options to be passed to diff when
2605 # submitting each commit to p4.
2607 if self
.detectRenames
:
2608 # command-line -M arg
2609 self
.diffOpts
= ["-M"]
2611 # If not explicitly set check the config variable
2612 detectRenames
= gitConfig("git-p4.detectRenames")
2614 if detectRenames
.lower() == "false" or detectRenames
== "":
2616 elif detectRenames
.lower() == "true":
2617 self
.diffOpts
= ["-M"]
2619 self
.diffOpts
= ["-M{}".format(detectRenames
)]
2621 # no command-line arg for -C or --find-copies-harder, just
2623 detectCopies
= gitConfig("git-p4.detectCopies")
2624 if detectCopies
.lower() == "false" or detectCopies
== "":
2626 elif detectCopies
.lower() == "true":
2627 self
.diffOpts
.append("-C")
2629 self
.diffOpts
.append("-C{}".format(detectCopies
))
2631 if gitConfigBool("git-p4.detectCopiesHarder"):
2632 self
.diffOpts
.append("--find-copies-harder")
2634 num_shelves
= len(self
.update_shelve
)
2635 if num_shelves
> 0 and num_shelves
!= len(commits
):
2636 sys
.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2637 (len(commits
), num_shelves
))
2639 if not self
.no_verify
:
2641 if not run_git_hook("p4-pre-submit"):
2642 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip "
2643 "this pre-submission check by adding\nthe command line option '--no-verify', "
2644 "however,\nthis will also skip the p4-changelist hook as well.")
2646 except Exception as e
:
2647 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "
2648 "with the error '{0}'".format(e
.message
))
2652 # Apply the commits, one at a time. On failure, ask if should
2653 # continue to try the rest of the patches, or quit.
2656 print("Would apply")
2658 last
= len(commits
) - 1
2659 for i
, commit
in enumerate(commits
):
2661 print(" ", read_pipe(["git", "show", "-s",
2662 "--format=format:%h %s", commit
]))
2665 ok
= self
.applyCommit(commit
)
2667 applied
.append(commit
)
2668 if self
.prepare_p4_only
:
2670 print("Processing only the first commit due to option"
2671 " --prepare-p4-only")
2675 # prompt for what to do, or use the option/variable
2676 if self
.conflict_behavior
== "ask":
2677 print("What do you want to do?")
2678 response
= prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2679 elif self
.conflict_behavior
== "skip":
2681 elif self
.conflict_behavior
== "quit":
2684 die("Unknown conflict_behavior '%s'" %
2685 self
.conflict_behavior
)
2688 print("Skipping this commit, but applying the rest")
2693 chdir(self
.oldWorkingDirectory
)
2694 shelved_applied
= "shelved" if self
.shelve
else "applied"
2697 elif self
.prepare_p4_only
:
2699 elif len(commits
) == len(applied
):
2700 print("All commits {0}!".format(shelved_applied
))
2704 sync
.branch
= self
.branch
2705 if self
.disable_p4sync
:
2706 sync
.sync_origin_only()
2710 if not self
.disable_rebase
:
2715 if len(applied
) == 0:
2716 print("No commits {0}.".format(shelved_applied
))
2718 print("{0} only the commits marked with '*':".format(shelved_applied
.capitalize()))
2724 print(star
, read_pipe(["git", "show", "-s",
2725 "--format=format:%h %s", c
]))
2726 print("You will have to do 'git p4 sync' and rebase.")
2728 if gitConfigBool("git-p4.exportLabels"):
2729 self
.exportLabels
= True
2731 if self
.exportLabels
:
2732 p4Labels
= getP4Labels(self
.depotPath
)
2733 gitTags
= getGitTags()
2735 missingGitTags
= gitTags
- p4Labels
2736 self
.exportGitTags(missingGitTags
)
2738 # exit with error unless everything applied perfectly
2739 if len(commits
) != len(applied
):
2746 """Represent a p4 view ("p4 help views"), and map files in a repo according
2750 def __init__(self
, client_name
):
2752 self
.client_prefix
= "//%s/" % client_name
2753 # cache results of "p4 where" to lookup client file locations
2754 self
.client_spec_path_cache
= {}
2756 def append(self
, view_line
):
2757 """Parse a view line, splitting it into depot and client sides. Append
2758 to self.mappings, preserving order. This is only needed for tag
2762 # Split the view line into exactly two words. P4 enforces
2763 # structure on these lines that simplifies this quite a bit.
2765 # Either or both words may be double-quoted.
2766 # Single quotes do not matter.
2767 # Double-quote marks cannot occur inside the words.
2768 # A + or - prefix is also inside the quotes.
2769 # There are no quotes unless they contain a space.
2770 # The line is already white-space stripped.
2771 # The two words are separated by a single space.
2773 if view_line
[0] == '"':
2774 # First word is double quoted. Find its end.
2775 close_quote_index
= view_line
.find('"', 1)
2776 if close_quote_index
<= 0:
2777 die("No first-word closing quote found: %s" % view_line
)
2778 depot_side
= view_line
[1:close_quote_index
]
2779 # skip closing quote and space
2780 rhs_index
= close_quote_index
+ 1 + 1
2782 space_index
= view_line
.find(" ")
2783 if space_index
<= 0:
2784 die("No word-splitting space found: %s" % view_line
)
2785 depot_side
= view_line
[0:space_index
]
2786 rhs_index
= space_index
+ 1
2788 # prefix + means overlay on previous mapping
2789 if depot_side
.startswith("+"):
2790 depot_side
= depot_side
[1:]
2792 # prefix - means exclude this path, leave out of mappings
2794 if depot_side
.startswith("-"):
2796 depot_side
= depot_side
[1:]
2799 self
.mappings
.append(depot_side
)
2801 def convert_client_path(self
, clientFile
):
2802 # chop off //client/ part to make it relative
2803 if not decode_path(clientFile
).startswith(self
.client_prefix
):
2804 die("No prefix '%s' on clientFile '%s'" %
2805 (self
.client_prefix
, clientFile
))
2806 return clientFile
[len(self
.client_prefix
):]
2808 def update_client_spec_path_cache(self
, files
):
2809 """Caching file paths by "p4 where" batch query."""
2811 # List depot file paths exclude that already cached
2812 fileArgs
= [f
['path'] for f
in files
if decode_path(f
['path']) not in self
.client_spec_path_cache
]
2814 if len(fileArgs
) == 0:
2815 return # All files in cache
2817 where_result
= p4CmdList(["-x", "-", "where"], stdin
=fileArgs
)
2818 for res
in where_result
:
2819 if "code" in res
and res
["code"] == "error":
2820 # assume error is "... file(s) not in client view"
2822 if "clientFile" not in res
:
2823 die("No clientFile in 'p4 where' output")
2825 # it will list all of them, but only one not unmap-ped
2827 depot_path
= decode_path(res
['depotFile'])
2828 if gitConfigBool("core.ignorecase"):
2829 depot_path
= depot_path
.lower()
2830 self
.client_spec_path_cache
[depot_path
] = self
.convert_client_path(res
["clientFile"])
2832 # not found files or unmap files set to ""
2833 for depotFile
in fileArgs
:
2834 depotFile
= decode_path(depotFile
)
2835 if gitConfigBool("core.ignorecase"):
2836 depotFile
= depotFile
.lower()
2837 if depotFile
not in self
.client_spec_path_cache
:
2838 self
.client_spec_path_cache
[depotFile
] = b
''
2840 def map_in_client(self
, depot_path
):
2841 """Return the relative location in the client where this depot file
2844 Returns "" if the file should not be mapped in the client.
2847 if gitConfigBool("core.ignorecase"):
2848 depot_path
= depot_path
.lower()
2850 if depot_path
in self
.client_spec_path_cache
:
2851 return self
.client_spec_path_cache
[depot_path
]
2853 die("Error: %s is not found in client spec path" % depot_path
)
2857 def cloneExcludeCallback(option
, opt_str
, value
, parser
):
2858 # prepend "/" because the first "/" was consumed as part of the option itself.
2859 # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2860 parser
.values
.cloneExclude
+= ["/" + re
.sub(r
"\.\.\.$", "", value
)]
2863 class P4Sync(Command
, P4UserMap
):
2866 Command
.__init
__(self
)
2867 P4UserMap
.__init
__(self
)
2869 optparse
.make_option("--branch", dest
="branch"),
2870 optparse
.make_option("--detect-branches", dest
="detectBranches", action
="store_true"),
2871 optparse
.make_option("--changesfile", dest
="changesFile"),
2872 optparse
.make_option("--silent", dest
="silent", action
="store_true"),
2873 optparse
.make_option("--detect-labels", dest
="detectLabels", action
="store_true"),
2874 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
2875 optparse
.make_option("--import-local", dest
="importIntoRemotes", action
="store_false",
2876 help="Import into refs/heads/ , not refs/remotes"),
2877 optparse
.make_option("--max-changes", dest
="maxChanges",
2878 help="Maximum number of changes to import"),
2879 optparse
.make_option("--changes-block-size", dest
="changes_block_size", type="int",
2880 help="Internal block size to use when iteratively calling p4 changes"),
2881 optparse
.make_option("--keep-path", dest
="keepRepoPath", action
='store_true',
2882 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2883 optparse
.make_option("--use-client-spec", dest
="useClientSpec", action
='store_true',
2884 help="Only sync files that are included in the Perforce Client Spec"),
2885 optparse
.make_option("-/", dest
="cloneExclude",
2886 action
="callback", callback
=cloneExcludeCallback
, type="string",
2887 help="exclude depot path"),
2889 self
.description
= """Imports from Perforce into a git repository.\n
2891 //depot/my/project/ -- to import the current head
2892 //depot/my/project/@all -- to import everything
2893 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2895 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2897 self
.usage
+= " //depot/path[@revRange]"
2899 self
.createdBranches
= set()
2900 self
.committedChanges
= set()
2902 self
.detectBranches
= False
2903 self
.detectLabels
= False
2904 self
.importLabels
= False
2905 self
.changesFile
= ""
2906 self
.syncWithOrigin
= True
2907 self
.importIntoRemotes
= True
2908 self
.maxChanges
= ""
2909 self
.changes_block_size
= None
2910 self
.keepRepoPath
= False
2911 self
.depotPaths
= None
2912 self
.p4BranchesInGit
= []
2913 self
.cloneExclude
= []
2914 self
.useClientSpec
= False
2915 self
.useClientSpec_from_options
= False
2916 self
.clientSpecDirs
= None
2917 self
.tempBranches
= []
2918 self
.tempBranchLocation
= "refs/git-p4-tmp"
2919 self
.largeFileSystem
= None
2920 self
.suppress_meta_comment
= False
2922 if gitConfig('git-p4.largeFileSystem'):
2923 largeFileSystemConstructor
= globals()[gitConfig('git-p4.largeFileSystem')]
2924 self
.largeFileSystem
= largeFileSystemConstructor(
2925 lambda git_mode
, relPath
, contents
: self
.writeToGitStream(git_mode
, relPath
, contents
)
2928 if gitConfig("git-p4.syncFromOrigin") == "false":
2929 self
.syncWithOrigin
= False
2931 self
.depotPaths
= []
2932 self
.changeRange
= ""
2933 self
.previousDepotPaths
= []
2934 self
.hasOrigin
= False
2936 # map from branch depot path to parent branch
2937 self
.knownBranches
= {}
2938 self
.initialParents
= {}
2940 self
.tz
= "%+03d%02d" % (- time
.timezone
/ 3600, ((- time
.timezone
% 3600) / 60))
2943 def checkpoint(self
):
2944 """Force a checkpoint in fast-import and wait for it to finish."""
2945 self
.gitStream
.write("checkpoint\n\n")
2946 self
.gitStream
.write("progress checkpoint\n\n")
2947 self
.gitStream
.flush()
2948 out
= self
.gitOutput
.readline()
2950 print("checkpoint finished: " + out
)
2952 def isPathWanted(self
, path
):
2953 for p
in self
.cloneExclude
:
2955 if p4PathStartsWith(path
, p
):
2957 # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2958 elif path
.lower() == p
.lower():
2960 for p
in self
.depotPaths
:
2961 if p4PathStartsWith(path
, decode_path(p
)):
2965 def extractFilesFromCommit(self
, commit
, shelved
=False, shelved_cl
=0):
2968 while "depotFile%s" % fnum
in commit
:
2969 path
= commit
["depotFile%s" % fnum
]
2970 found
= self
.isPathWanted(decode_path(path
))
2977 file["rev"] = commit
["rev%s" % fnum
]
2978 file["action"] = commit
["action%s" % fnum
]
2979 file["type"] = commit
["type%s" % fnum
]
2981 file["shelved_cl"] = int(shelved_cl
)
2986 def extractJobsFromCommit(self
, commit
):
2989 while "job%s" % jnum
in commit
:
2990 job
= commit
["job%s" % jnum
]
2995 def stripRepoPath(self
, path
, prefixes
):
2996 """When streaming files, this is called to map a p4 depot path to where
2997 it should go in git. The prefixes are either self.depotPaths, or
2998 self.branchPrefixes in the case of branch detection.
3001 if self
.useClientSpec
:
3002 # branch detection moves files up a level (the branch name)
3003 # from what client spec interpretation gives
3004 path
= decode_path(self
.clientSpecDirs
.map_in_client(path
))
3005 if self
.detectBranches
:
3006 for b
in self
.knownBranches
:
3007 if p4PathStartsWith(path
, b
+ "/"):
3008 path
= path
[len(b
)+1:]
3010 elif self
.keepRepoPath
:
3011 # Preserve everything in relative path name except leading
3012 # //depot/; just look at first prefix as they all should
3013 # be in the same depot.
3014 depot
= re
.sub("^(//[^/]+/).*", r
'\1', prefixes
[0])
3015 if p4PathStartsWith(path
, depot
):
3016 path
= path
[len(depot
):]
3020 if p4PathStartsWith(path
, p
):
3021 path
= path
[len(p
):]
3024 path
= wildcard_decode(path
)
3027 def splitFilesIntoBranches(self
, commit
):
3028 """Look at each depotFile in the commit to figure out to what branch it
3032 if self
.clientSpecDirs
:
3033 files
= self
.extractFilesFromCommit(commit
)
3034 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
3038 while "depotFile%s" % fnum
in commit
:
3039 raw_path
= commit
["depotFile%s" % fnum
]
3040 path
= decode_path(raw_path
)
3041 found
= self
.isPathWanted(path
)
3047 file["path"] = raw_path
3048 file["rev"] = commit
["rev%s" % fnum
]
3049 file["action"] = commit
["action%s" % fnum
]
3050 file["type"] = commit
["type%s" % fnum
]
3053 # start with the full relative path where this file would
3055 if self
.useClientSpec
:
3056 relPath
= decode_path(self
.clientSpecDirs
.map_in_client(path
))
3058 relPath
= self
.stripRepoPath(path
, self
.depotPaths
)
3060 for branch
in self
.knownBranches
.keys():
3061 # add a trailing slash so that a commit into qt/4.2foo
3062 # doesn't end up in qt/4.2, e.g.
3063 if p4PathStartsWith(relPath
, branch
+ "/"):
3064 if branch
not in branches
:
3065 branches
[branch
] = []
3066 branches
[branch
].append(file)
3071 def writeToGitStream(self
, gitMode
, relPath
, contents
):
3072 self
.gitStream
.write(encode_text_stream(u
'M {} inline {}\n'.format(gitMode
, relPath
)))
3073 self
.gitStream
.write('data %d\n' % sum(len(d
) for d
in contents
))
3075 self
.gitStream
.write(d
)
3076 self
.gitStream
.write('\n')
3078 def encodeWithUTF8(self
, path
):
3080 path
.decode('ascii')
3083 if gitConfig('git-p4.pathEncoding'):
3084 encoding
= gitConfig('git-p4.pathEncoding')
3085 path
= path
.decode(encoding
, 'replace').encode('utf8', 'replace')
3087 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding
, path
))
3090 def streamOneP4File(self
, file, contents
):
3091 """Output one file from the P4 stream.
3093 This is a helper for streamP4Files().
3096 file_path
= file['depotFile']
3097 relPath
= self
.stripRepoPath(decode_path(file_path
), self
.branchPrefixes
)
3100 if 'fileSize' in self
.stream_file
:
3101 size
= int(self
.stream_file
['fileSize'])
3103 # Deleted files don't get a fileSize apparently
3105 sys
.stdout
.write('\r%s --> %s (%s)\n' % (
3106 file_path
, relPath
, format_size_human_readable(size
)))
3109 type_base
, type_mods
= split_p4_type(file["type"])
3112 if "x" in type_mods
:
3114 if type_base
== "symlink":
3116 # p4 print on a symlink sometimes contains "target\n";
3117 # if it does, remove the newline
3118 data
= ''.join(decode_text_stream(c
) for c
in contents
)
3120 # Some version of p4 allowed creating a symlink that pointed
3121 # to nothing. This causes p4 errors when checking out such
3122 # a change, and errors here too. Work around it by ignoring
3123 # the bad symlink; hopefully a future change fixes it.
3124 print("\nIgnoring empty symlink in %s" % file_path
)
3126 elif data
[-1] == '\n':
3127 contents
= [data
[:-1]]
3131 if type_base
== "utf16":
3132 # p4 delivers different text in the python output to -G
3133 # than it does when using "print -o", or normal p4 client
3134 # operations. utf16 is converted to ascii or utf8, perhaps.
3135 # But ascii text saved as -t utf16 is completely mangled.
3136 # Invoke print -o to get the real contents.
3138 # On windows, the newlines will always be mangled by print, so put
3139 # them back too. This is not needed to the cygwin windows version,
3140 # just the native "NT" type.
3143 text
= p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw
=True)
3144 except Exception as e
:
3145 if 'Translation of file content failed' in str(e
):
3146 type_base
= 'binary'
3150 if p4_version_string().find('/NT') >= 0:
3151 text
= text
.replace(b
'\r\n', b
'\n')
3154 if type_base
== "apple":
3155 # Apple filetype files will be streamed as a concatenation of
3156 # its appledouble header and the contents. This is useless
3157 # on both macs and non-macs. If using "print -q -o xx", it
3158 # will create "xx" with the data, and "%xx" with the header.
3159 # This is also not very useful.
3161 # Ideally, someday, this script can learn how to generate
3162 # appledouble files directly and import those to git, but
3163 # non-mac machines can never find a use for apple filetype.
3164 print("\nIgnoring apple filetype file %s" % file['depotFile'])
3167 if type_base
== "utf8":
3168 # The type utf8 explicitly means utf8 *with BOM*. These are
3169 # streamed just like regular text files, however, without
3170 # the BOM in the stream.
3171 # Therefore, to accurately import these files into git, we
3172 # need to explicitly re-add the BOM before writing.
3173 # 'contents' is a set of bytes in this case, so create the
3174 # BOM prefix as a b'' literal.
3175 contents
= [b
'\xef\xbb\xbf' + contents
[0]] + contents
[1:]
3177 # Note that we do not try to de-mangle keywords on utf16 files,
3178 # even though in theory somebody may want that.
3179 regexp
= p4_keywords_regexp_for_type(type_base
, type_mods
)
3181 contents
= [regexp
.sub(br
'$\1$', c
) for c
in contents
]
3183 if self
.largeFileSystem
:
3184 git_mode
, contents
= self
.largeFileSystem
.processContent(git_mode
, relPath
, contents
)
3186 self
.writeToGitStream(git_mode
, relPath
, contents
)
3188 def streamOneP4Deletion(self
, file):
3189 relPath
= self
.stripRepoPath(decode_path(file['path']), self
.branchPrefixes
)
3191 sys
.stdout
.write("delete %s\n" % relPath
)
3193 self
.gitStream
.write(encode_text_stream(u
'D {}\n'.format(relPath
)))
3195 if self
.largeFileSystem
and self
.largeFileSystem
.isLargeFile(relPath
):
3196 self
.largeFileSystem
.removeLargeFile(relPath
)
3198 def streamP4FilesCb(self
, marshalled
):
3199 """Handle another chunk of streaming data."""
3201 # catch p4 errors and complain
3203 if "code" in marshalled
:
3204 if marshalled
["code"] == "error":
3205 if "data" in marshalled
:
3206 err
= marshalled
["data"].rstrip()
3208 if not err
and 'fileSize' in self
.stream_file
:
3209 required_bytes
= int((4 * int(self
.stream_file
["fileSize"])) - calcDiskFree())
3210 if required_bytes
> 0:
3211 err
= 'Not enough space left on %s! Free at least %s.' % (
3212 os
.getcwd(), format_size_human_readable(required_bytes
))
3216 if self
.stream_have_file_info
:
3217 if "depotFile" in self
.stream_file
:
3218 f
= self
.stream_file
["depotFile"]
3219 # force a failure in fast-import, else an empty
3220 # commit will be made
3221 self
.gitStream
.write("\n")
3222 self
.gitStream
.write("die-now\n")
3223 self
.gitStream
.close()
3224 # ignore errors, but make sure it exits first
3225 self
.importProcess
.wait()
3227 die("Error from p4 print for %s: %s" % (f
, err
))
3229 die("Error from p4 print: %s" % err
)
3231 if 'depotFile' in marshalled
and self
.stream_have_file_info
:
3232 # start of a new file - output the old one first
3233 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
3234 self
.stream_file
= {}
3235 self
.stream_contents
= []
3236 self
.stream_have_file_info
= False
3238 # pick up the new file information... for the
3239 # 'data' field we need to append to our array
3240 for k
in marshalled
.keys():
3242 if 'streamContentSize' not in self
.stream_file
:
3243 self
.stream_file
['streamContentSize'] = 0
3244 self
.stream_file
['streamContentSize'] += len(marshalled
['data'])
3245 self
.stream_contents
.append(marshalled
['data'])
3247 self
.stream_file
[k
] = marshalled
[k
]
3250 'streamContentSize' in self
.stream_file
and
3251 'fileSize' in self
.stream_file
and
3252 'depotFile' in self
.stream_file
):
3253 size
= int(self
.stream_file
["fileSize"])
3255 progress
= 100*self
.stream_file
['streamContentSize']/size
3256 sys
.stdout
.write('\r%s %d%% (%s)' % (
3257 self
.stream_file
['depotFile'], progress
,
3258 format_size_human_readable(size
)))
3261 self
.stream_have_file_info
= True
3263 def streamP4Files(self
, files
):
3264 """Stream directly from "p4 files" into "git fast-import."""
3271 filesForCommit
.append(f
)
3272 if f
['action'] in self
.delete_actions
:
3273 filesToDelete
.append(f
)
3275 filesToRead
.append(f
)
3278 for f
in filesToDelete
:
3279 self
.streamOneP4Deletion(f
)
3281 if len(filesToRead
) > 0:
3282 self
.stream_file
= {}
3283 self
.stream_contents
= []
3284 self
.stream_have_file_info
= False
3286 # curry self argument
3287 def streamP4FilesCbSelf(entry
):
3288 self
.streamP4FilesCb(entry
)
3291 for f
in filesToRead
:
3292 if 'shelved_cl' in f
:
3293 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3295 fileArg
= f
['path'] + encode_text_stream('@={}'.format(f
['shelved_cl']))
3297 fileArg
= f
['path'] + encode_text_stream('#{}'.format(f
['rev']))
3299 fileArgs
.append(fileArg
)
3301 p4CmdList(["-x", "-", "print"],
3303 cb
=streamP4FilesCbSelf
)
3306 if 'depotFile' in self
.stream_file
:
3307 self
.streamOneP4File(self
.stream_file
, self
.stream_contents
)
3309 def make_email(self
, userid
):
3310 if userid
in self
.users
:
3311 return self
.users
[userid
]
3313 userid_bytes
= metadata_stream_to_writable_bytes(userid
)
3314 return b
"%s <a@b>" % userid_bytes
3316 def streamTag(self
, gitStream
, labelName
, labelDetails
, commit
, epoch
):
3319 Commit is either a git commit, or a fast-import mark, ":<p4commit>".
3323 print("writing tag %s for commit %s" % (labelName
, commit
))
3324 gitStream
.write("tag %s\n" % labelName
)
3325 gitStream
.write("from %s\n" % commit
)
3327 if 'Owner' in labelDetails
:
3328 owner
= labelDetails
["Owner"]
3332 # Try to use the owner of the p4 label, or failing that,
3333 # the current p4 user id.
3335 email
= self
.make_email(owner
)
3337 email
= self
.make_email(self
.p4UserId())
3339 gitStream
.write("tagger ")
3340 gitStream
.write(email
)
3341 gitStream
.write(" %s %s\n" % (epoch
, self
.tz
))
3343 print("labelDetails=", labelDetails
)
3344 if 'Description' in labelDetails
:
3345 description
= labelDetails
['Description']
3347 description
= 'Label from git p4'
3349 gitStream
.write("data %d\n" % len(description
))
3350 gitStream
.write(description
)
3351 gitStream
.write("\n")
3353 def inClientSpec(self
, path
):
3354 if not self
.clientSpecDirs
:
3356 inClientSpec
= self
.clientSpecDirs
.map_in_client(path
)
3357 if not inClientSpec
and self
.verbose
:
3358 print('Ignoring file outside of client spec: {0}'.format(path
))
3361 def hasBranchPrefix(self
, path
):
3362 if not self
.branchPrefixes
:
3364 hasPrefix
= [p
for p
in self
.branchPrefixes
3365 if p4PathStartsWith(path
, p
)]
3366 if not hasPrefix
and self
.verbose
:
3367 print('Ignoring file outside of prefix: {0}'.format(path
))
3370 def findShadowedFiles(self
, files
, change
):
3371 """Perforce allows you commit files and directories with the same name,
3372 so you could have files //depot/foo and //depot/foo/bar both checked
3373 in. A p4 sync of a repository in this state fails. Deleting one of
3374 the files recovers the repository.
3376 Git will not allow the broken state to exist and only the most
3377 recent of the conflicting names is left in the repository. When one
3378 of the conflicting files is deleted we need to re-add the other one
3379 to make sure the git repository recovers in the same way as
3383 deleted
= [f
for f
in files
if f
['action'] in self
.delete_actions
]
3386 path
= decode_path(f
['path'])
3387 to_check
.add(path
+ '/...')
3389 path
= path
.rsplit("/", 1)[0]
3390 if path
== "/" or path
in to_check
:
3393 to_check
= ['%s@%s' % (wildcard_encode(p
), change
) for p
in to_check
3394 if self
.hasBranchPrefix(p
)]
3396 stat_result
= p4CmdList(["-x", "-", "fstat", "-T",
3397 "depotFile,headAction,headRev,headType"], stdin
=to_check
)
3398 for record
in stat_result
:
3399 if record
['code'] != 'stat':
3401 if record
['headAction'] in self
.delete_actions
:
3405 'path': record
['depotFile'],
3406 'rev': record
['headRev'],
3407 'type': record
['headType']})
3409 def commit(self
, details
, files
, branch
, parent
="", allow_empty
=False):
3410 epoch
= details
["time"]
3411 author
= details
["user"]
3412 jobs
= self
.extractJobsFromCommit(details
)
3415 print('commit into {0}'.format(branch
))
3417 files
= [f
for f
in files
3418 if self
.hasBranchPrefix(decode_path(f
['path']))]
3419 self
.findShadowedFiles(files
, details
['change'])
3421 if self
.clientSpecDirs
:
3422 self
.clientSpecDirs
.update_client_spec_path_cache(files
)
3424 files
= [f
for f
in files
if self
.inClientSpec(decode_path(f
['path']))]
3426 if gitConfigBool('git-p4.keepEmptyCommits'):
3429 if not files
and not allow_empty
:
3430 print('Ignoring revision {0} as it would produce an empty commit.'
3431 .format(details
['change']))
3434 self
.gitStream
.write("commit %s\n" % branch
)
3435 self
.gitStream
.write("mark :%s\n" % details
["change"])
3436 self
.committedChanges
.add(int(details
["change"]))
3437 if author
not in self
.users
:
3438 self
.getUserMapFromPerforceServer()
3440 self
.gitStream
.write("committer ")
3441 self
.gitStream
.write(self
.make_email(author
))
3442 self
.gitStream
.write(" %s %s\n" % (epoch
, self
.tz
))
3444 self
.gitStream
.write("data <<EOT\n")
3445 self
.gitStream
.write(details
["desc"])
3447 self
.gitStream
.write("\nJobs: %s" % (' '.join(jobs
)))
3449 if not self
.suppress_meta_comment
:
3450 self
.gitStream
.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3451 (','.join(self
.branchPrefixes
), details
["change"]))
3452 if len(details
['options']) > 0:
3453 self
.gitStream
.write(": options = %s" % details
['options'])
3454 self
.gitStream
.write("]\n")
3456 self
.gitStream
.write("EOT\n\n")
3460 print("parent %s" % parent
)
3461 self
.gitStream
.write("from %s\n" % parent
)
3463 self
.streamP4Files(files
)
3464 self
.gitStream
.write("\n")
3466 change
= int(details
["change"])
3468 if change
in self
.labels
:
3469 label
= self
.labels
[change
]
3470 labelDetails
= label
[0]
3471 labelRevisions
= label
[1]
3473 print("Change %s is labelled %s" % (change
, labelDetails
))
3475 files
= p4CmdList(["files"] + ["%s...@%s" % (p
, change
)
3476 for p
in self
.branchPrefixes
])
3478 if len(files
) == len(labelRevisions
):
3482 if info
["action"] in self
.delete_actions
:
3484 cleanedFiles
[info
["depotFile"]] = info
["rev"]
3486 if cleanedFiles
== labelRevisions
:
3487 self
.streamTag(self
.gitStream
, 'tag_%s' % labelDetails
['label'], labelDetails
, branch
, epoch
)
3491 print("Tag %s does not match with change %s: files do not match."
3492 % (labelDetails
["label"], change
))
3496 print("Tag %s does not match with change %s: file count is different."
3497 % (labelDetails
["label"], change
))
3499 def getLabels(self
):
3500 """Build a dictionary of changelists and labels, for "detect-labels"
3506 l
= p4CmdList(["labels"] + ["%s..." % p
for p
in self
.depotPaths
])
3507 if len(l
) > 0 and not self
.silent
:
3508 print("Finding files belonging to labels in %s" % self
.depotPaths
)
3511 label
= output
["label"]
3515 print("Querying files for label %s" % label
)
3516 for file in p4CmdList(["files"] +
3517 ["%s...@%s" % (p
, label
)
3518 for p
in self
.depotPaths
]):
3519 revisions
[file["depotFile"]] = file["rev"]
3520 change
= int(file["change"])
3521 if change
> newestChange
:
3522 newestChange
= change
3524 self
.labels
[newestChange
] = [output
, revisions
]
3527 print("Label changes: %s" % self
.labels
.keys())
3529 def importP4Labels(self
, stream
, p4Labels
):
3530 """Import p4 labels as git tags. A direct mapping does not exist, so
3531 assume that if all the files are at the same revision then we can
3532 use that, or it's something more complicated we should just ignore.
3536 print("import p4 labels: " + ' '.join(p4Labels
))
3538 ignoredP4Labels
= gitConfigList("git-p4.ignoredP4Labels")
3539 validLabelRegexp
= gitConfig("git-p4.labelImportRegexp")
3540 if len(validLabelRegexp
) == 0:
3541 validLabelRegexp
= defaultLabelRegexp
3542 m
= re
.compile(validLabelRegexp
)
3544 for name
in p4Labels
:
3547 if not m
.match(name
):
3549 print("label %s does not match regexp %s" % (name
, validLabelRegexp
))
3552 if name
in ignoredP4Labels
:
3555 labelDetails
= p4CmdList(['label', "-o", name
])[0]
3557 # get the most recent changelist for each file in this label
3558 change
= p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p
, name
)
3559 for p
in self
.depotPaths
])
3561 if 'change' in change
:
3562 # find the corresponding git commit; take the oldest commit
3563 changelist
= int(change
['change'])
3564 if changelist
in self
.committedChanges
:
3565 gitCommit
= ":%d" % changelist
# use a fast-import mark
3568 gitCommit
= read_pipe(["git", "rev-list", "--max-count=1",
3569 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist
], ignore_error
=True)
3570 if len(gitCommit
) == 0:
3571 print("importing label %s: could not find git commit for changelist %d" % (name
, changelist
))
3574 gitCommit
= gitCommit
.strip()
3577 # Convert from p4 time format
3579 tmwhen
= time
.strptime(labelDetails
['Update'], "%Y/%m/%d %H:%M:%S")
3581 print("Could not convert label time %s" % labelDetails
['Update'])
3584 when
= int(time
.mktime(tmwhen
))
3585 self
.streamTag(stream
, name
, labelDetails
, gitCommit
, when
)
3587 print("p4 label %s mapped to git commit %s" % (name
, gitCommit
))
3590 print("Label %s has no changelists - possibly deleted?" % name
)
3593 # We can't import this label; don't try again as it will get very
3594 # expensive repeatedly fetching all the files for labels that will
3595 # never be imported. If the label is moved in the future, the
3596 # ignore will need to be removed manually.
3597 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name
])
3599 def guessProjectName(self
):
3600 for p
in self
.depotPaths
:
3603 p
= p
[p
.strip().rfind("/") + 1:]
3604 if not p
.endswith("/"):
3608 def getBranchMapping(self
):
3609 lostAndFoundBranches
= set()
3611 user
= gitConfig("git-p4.branchUser")
3613 for info
in p4CmdList(
3614 ["branches"] + (["-u", user
] if len(user
) > 0 else [])):
3615 details
= p4Cmd(["branch", "-o", info
["branch"]])
3617 while "View%s" % viewIdx
in details
:
3618 paths
= details
["View%s" % viewIdx
].split(" ")
3619 viewIdx
= viewIdx
+ 1
3620 # require standard //depot/foo/... //depot/bar/... mapping
3621 if len(paths
) != 2 or not paths
[0].endswith("/...") or not paths
[1].endswith("/..."):
3624 destination
= paths
[1]
3626 if p4PathStartsWith(source
, self
.depotPaths
[0]) and p4PathStartsWith(destination
, self
.depotPaths
[0]):
3627 source
= source
[len(self
.depotPaths
[0]):-4]
3628 destination
= destination
[len(self
.depotPaths
[0]):-4]
3630 if destination
in self
.knownBranches
:
3632 print("p4 branch %s defines a mapping from %s to %s" % (info
["branch"], source
, destination
))
3633 print("but there exists another mapping from %s to %s already!" % (self
.knownBranches
[destination
], destination
))
3636 self
.knownBranches
[destination
] = source
3638 lostAndFoundBranches
.discard(destination
)
3640 if source
not in self
.knownBranches
:
3641 lostAndFoundBranches
.add(source
)
3643 # Perforce does not strictly require branches to be defined, so we also
3644 # check git config for a branch list.
3646 # Example of branch definition in git config file:
3648 # branchList=main:branchA
3649 # branchList=main:branchB
3650 # branchList=branchA:branchC
3651 configBranches
= gitConfigList("git-p4.branchList")
3652 for branch
in configBranches
:
3654 source
, destination
= branch
.split(":")
3655 self
.knownBranches
[destination
] = source
3657 lostAndFoundBranches
.discard(destination
)
3659 if source
not in self
.knownBranches
:
3660 lostAndFoundBranches
.add(source
)
3662 for branch
in lostAndFoundBranches
:
3663 self
.knownBranches
[branch
] = branch
3665 def getBranchMappingFromGitBranches(self
):
3666 branches
= p4BranchesInGit(self
.importIntoRemotes
)
3667 for branch
in branches
.keys():
3668 if branch
== "master":
3671 branch
= branch
[len(self
.projectName
):]
3672 self
.knownBranches
[branch
] = branch
3674 def updateOptionDict(self
, d
):
3676 if self
.keepRepoPath
:
3677 option_keys
['keepRepoPath'] = 1
3679 d
["options"] = ' '.join(sorted(option_keys
.keys()))
3681 def readOptions(self
, d
):
3682 self
.keepRepoPath
= ('options' in d
3683 and ('keepRepoPath' in d
['options']))
3685 def gitRefForBranch(self
, branch
):
3686 if branch
== "main":
3687 return self
.refPrefix
+ "master"
3689 if len(branch
) <= 0:
3692 return self
.refPrefix
+ self
.projectName
+ branch
3694 def gitCommitByP4Change(self
, ref
, change
):
3696 print("looking in ref " + ref
+ " for change %s using bisect..." % change
)
3699 latestCommit
= parseRevision(ref
)
3703 print("trying: earliest %s latest %s" % (earliestCommit
, latestCommit
))
3704 next
= read_pipe(["git", "rev-list", "--bisect",
3705 latestCommit
, earliestCommit
]).strip()
3710 log
= extractLogMessageFromGitCommit(next
)
3711 settings
= extractSettingsGitLog(log
)
3712 currentChange
= int(settings
['change'])
3714 print("current change %s" % currentChange
)
3716 if currentChange
== change
:
3718 print("found %s" % next
)
3721 if currentChange
< change
:
3722 earliestCommit
= "^%s" % next
3724 if next
== latestCommit
:
3725 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref
, change
))
3726 latestCommit
= "%s^@" % next
3730 def importNewBranch(self
, branch
, maxChange
):
3731 # make fast-import flush all changes to disk and update the refs using the checkpoint
3732 # command so that we can try to find the branch parent in the git history
3733 self
.gitStream
.write("checkpoint\n\n")
3734 self
.gitStream
.flush()
3735 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
3736 range = "@1,%s" % maxChange
3737 changes
= p4ChangesForPaths([branchPrefix
], range, self
.changes_block_size
)
3738 if len(changes
) <= 0:
3740 firstChange
= changes
[0]
3741 sourceBranch
= self
.knownBranches
[branch
]
3742 sourceDepotPath
= self
.depotPaths
[0] + sourceBranch
3743 sourceRef
= self
.gitRefForBranch(sourceBranch
)
3745 branchParentChange
= int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath
, firstChange
)])["change"])
3746 gitParent
= self
.gitCommitByP4Change(sourceRef
, branchParentChange
)
3747 if len(gitParent
) > 0:
3748 self
.initialParents
[self
.gitRefForBranch(branch
)] = gitParent
3750 self
.importChanges(changes
)
3753 def searchParent(self
, parent
, branch
, target
):
3754 targetTree
= read_pipe(["git", "rev-parse",
3755 "{}^{{tree}}".format(target
)]).strip()
3756 for line
in read_pipe_lines(["git", "rev-list", "--format=%H %T",
3757 "--no-merges", parent
]):
3758 if line
.startswith("commit "):
3760 commit
, tree
= line
.strip().split(" ")
3761 if tree
== targetTree
:
3763 print("Found parent of %s in commit %s" % (branch
, commit
))
3767 def importChanges(self
, changes
, origin_revision
=0):
3769 for change
in changes
:
3770 description
= p4_describe(change
)
3771 self
.updateOptionDict(description
)
3774 sys
.stdout
.write("\rImporting revision %s (%d%%)" % (
3775 change
, (cnt
* 100) // len(changes
)))
3780 if self
.detectBranches
:
3781 branches
= self
.splitFilesIntoBranches(description
)
3782 for branch
in branches
.keys():
3784 branchPrefix
= self
.depotPaths
[0] + branch
+ "/"
3785 self
.branchPrefixes
= [branchPrefix
]
3789 filesForCommit
= branches
[branch
]
3792 print("branch is %s" % branch
)
3794 self
.updatedBranches
.add(branch
)
3796 if branch
not in self
.createdBranches
:
3797 self
.createdBranches
.add(branch
)
3798 parent
= self
.knownBranches
[branch
]
3799 if parent
== branch
:
3802 fullBranch
= self
.projectName
+ branch
3803 if fullBranch
not in self
.p4BranchesInGit
:
3805 print("\n Importing new branch %s" % fullBranch
)
3806 if self
.importNewBranch(branch
, change
- 1):
3808 self
.p4BranchesInGit
.append(fullBranch
)
3810 print("\n Resuming with change %s" % change
)
3813 print("parent determined through known branches: %s" % parent
)
3815 branch
= self
.gitRefForBranch(branch
)
3816 parent
= self
.gitRefForBranch(parent
)
3819 print("looking for initial parent for %s; current parent is %s" % (branch
, parent
))
3821 if len(parent
) == 0 and branch
in self
.initialParents
:
3822 parent
= self
.initialParents
[branch
]
3823 del self
.initialParents
[branch
]
3827 tempBranch
= "%s/%d" % (self
.tempBranchLocation
, change
)
3829 print("Creating temporary branch: " + tempBranch
)
3830 self
.commit(description
, filesForCommit
, tempBranch
)
3831 self
.tempBranches
.append(tempBranch
)
3833 blob
= self
.searchParent(parent
, branch
, tempBranch
)
3835 self
.commit(description
, filesForCommit
, branch
, blob
)
3838 print("Parent of %s not found. Committing into head of %s" % (branch
, parent
))
3839 self
.commit(description
, filesForCommit
, branch
, parent
)
3841 files
= self
.extractFilesFromCommit(description
)
3842 self
.commit(description
, files
, self
.branch
,
3844 # only needed once, to connect to the previous commit
3845 self
.initialParent
= ""
3847 print(self
.gitError
.read())
3850 def sync_origin_only(self
):
3851 if self
.syncWithOrigin
:
3852 self
.hasOrigin
= originP4BranchesExist()
3855 print('Syncing with origin first, using "git fetch origin"')
3856 system(["git", "fetch", "origin"])
3858 def importHeadRevision(self
, revision
):
3859 print("Doing initial import of %s from revision %s into %s" % (' '.join(self
.depotPaths
), revision
, self
.branch
))
3862 details
["user"] = "git perforce import user"
3863 details
["desc"] = ("Initial import of %s from the state at revision %s\n"
3864 % (' '.join(self
.depotPaths
), revision
))
3865 details
["change"] = revision
3869 fileArgs
= ["%s...%s" % (p
, revision
) for p
in self
.depotPaths
]
3871 for info
in p4CmdList(["files"] + fileArgs
):
3873 if 'code' in info
and info
['code'] == 'error':
3874 sys
.stderr
.write("p4 returned an error: %s\n"
3876 if info
['data'].find("must refer to client") >= 0:
3877 sys
.stderr
.write("This particular p4 error is misleading.\n")
3878 sys
.stderr
.write("Perhaps the depot path was misspelled.\n")
3879 sys
.stderr
.write("Depot path: %s\n" % " ".join(self
.depotPaths
))
3881 if 'p4ExitCode' in info
:
3882 sys
.stderr
.write("p4 exitcode: %s\n" % info
['p4ExitCode'])
3885 change
= int(info
["change"])
3886 if change
> newestRevision
:
3887 newestRevision
= change
3889 if info
["action"] in self
.delete_actions
:
3892 for prop
in ["depotFile", "rev", "action", "type"]:
3893 details
["%s%s" % (prop
, fileCnt
)] = info
[prop
]
3895 fileCnt
= fileCnt
+ 1
3897 details
["change"] = newestRevision
3899 # Use time from top-most change so that all git p4 clones of
3900 # the same p4 repo have the same commit SHA1s.
3901 res
= p4_describe(newestRevision
)
3902 details
["time"] = res
["time"]
3904 self
.updateOptionDict(details
)
3906 self
.commit(details
, self
.extractFilesFromCommit(details
), self
.branch
)
3907 except IOError as err
:
3908 print("IO error with git fast-import. Is your git version recent enough?")
3909 print("IO error details: {}".format(err
))
3910 print(self
.gitError
.read())
3912 def importRevisions(self
, args
, branch_arg_given
):
3915 if len(self
.changesFile
) > 0:
3916 with
open(self
.changesFile
) as f
:
3917 output
= f
.readlines()
3920 changeSet
.add(int(line
))
3922 for change
in changeSet
:
3923 changes
.append(change
)
3927 # catch "git p4 sync" with no new branches, in a repo that
3928 # does not have any existing p4 branches
3930 if not self
.p4BranchesInGit
:
3931 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3933 # The default branch is master, unless --branch is used to
3934 # specify something else. Make sure it exists, or complain
3935 # nicely about how to use --branch.
3936 if not self
.detectBranches
:
3937 if not branch_exists(self
.branch
):
3938 if branch_arg_given
:
3939 raise P4CommandException("Error: branch %s does not exist." % self
.branch
)
3941 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
3945 print("Getting p4 changes for %s...%s" % (', '.join(self
.depotPaths
),
3947 changes
= p4ChangesForPaths(self
.depotPaths
, self
.changeRange
, self
.changes_block_size
)
3949 if len(self
.maxChanges
) > 0:
3950 changes
= changes
[:min(int(self
.maxChanges
), len(changes
))]
3952 if len(changes
) == 0:
3954 print("No changes to import!")
3956 if not self
.silent
and not self
.detectBranches
:
3957 print("Import destination: %s" % self
.branch
)
3959 self
.updatedBranches
= set()
3961 if not self
.detectBranches
:
3963 # start a new branch
3964 self
.initialParent
= ""
3966 # build on a previous revision
3967 self
.initialParent
= parseRevision(self
.branch
)
3969 self
.importChanges(changes
)
3973 if len(self
.updatedBranches
) > 0:
3974 sys
.stdout
.write("Updated branches: ")
3975 for b
in self
.updatedBranches
:
3976 sys
.stdout
.write("%s " % b
)
3977 sys
.stdout
.write("\n")
3979 def openStreams(self
):
3980 self
.importProcess
= subprocess
.Popen(["git", "fast-import"],
3981 stdin
=subprocess
.PIPE
,
3982 stdout
=subprocess
.PIPE
,
3983 stderr
=subprocess
.PIPE
)
3984 self
.gitOutput
= self
.importProcess
.stdout
3985 self
.gitStream
= self
.importProcess
.stdin
3986 self
.gitError
= self
.importProcess
.stderr
3988 if bytes
is not str:
3989 # Wrap gitStream.write() so that it can be called using `str` arguments
3990 def make_encoded_write(write
):
3991 def encoded_write(s
):
3992 return write(s
.encode() if isinstance(s
, str) else s
)
3993 return encoded_write
3995 self
.gitStream
.write
= make_encoded_write(self
.gitStream
.write
)
3997 def closeStreams(self
):
3998 if self
.gitStream
is None:
4000 self
.gitStream
.close()
4001 if self
.importProcess
.wait() != 0:
4002 die("fast-import failed: %s" % self
.gitError
.read())
4003 self
.gitOutput
.close()
4004 self
.gitError
.close()
4005 self
.gitStream
= None
4007 def run(self
, args
):
4008 if self
.importIntoRemotes
:
4009 self
.refPrefix
= "refs/remotes/p4/"
4011 self
.refPrefix
= "refs/heads/p4/"
4013 self
.sync_origin_only()
4015 branch_arg_given
= bool(self
.branch
)
4016 if len(self
.branch
) == 0:
4017 self
.branch
= self
.refPrefix
+ "master"
4018 if gitBranchExists("refs/heads/p4") and self
.importIntoRemotes
:
4019 system(["git", "update-ref", self
.branch
, "refs/heads/p4"])
4020 system(["git", "branch", "-D", "p4"])
4022 # accept either the command-line option, or the configuration variable
4023 if self
.useClientSpec
:
4024 # will use this after clone to set the variable
4025 self
.useClientSpec_from_options
= True
4027 if gitConfigBool("git-p4.useclientspec"):
4028 self
.useClientSpec
= True
4029 if self
.useClientSpec
:
4030 self
.clientSpecDirs
= getClientSpec()
4032 # TODO: should always look at previous commits,
4033 # merge with previous imports, if possible.
4036 createOrUpdateBranchesFromOrigin(self
.refPrefix
, self
.silent
)
4038 # branches holds mapping from branch name to sha1
4039 branches
= p4BranchesInGit(self
.importIntoRemotes
)
4041 # restrict to just this one, disabling detect-branches
4042 if branch_arg_given
:
4043 short
= shortP4Ref(self
.branch
, self
.importIntoRemotes
)
4044 if short
in branches
:
4045 self
.p4BranchesInGit
= [short
]
4046 elif self
.branch
.startswith('refs/') and \
4047 branchExists(self
.branch
) and \
4048 '[git-p4:' in extractLogMessageFromGitCommit(self
.branch
):
4049 self
.p4BranchesInGit
= [self
.branch
]
4051 self
.p4BranchesInGit
= branches
.keys()
4053 if len(self
.p4BranchesInGit
) > 1:
4055 print("Importing from/into multiple branches")
4056 self
.detectBranches
= True
4057 for branch
in branches
.keys():
4058 self
.initialParents
[self
.refPrefix
+ branch
] = \
4062 print("branches: %s" % self
.p4BranchesInGit
)
4065 for branch
in self
.p4BranchesInGit
:
4066 logMsg
= extractLogMessageFromGitCommit(fullP4Ref(branch
,
4067 self
.importIntoRemotes
))
4069 settings
= extractSettingsGitLog(logMsg
)
4071 self
.readOptions(settings
)
4072 if 'depot-paths' in settings
and 'change' in settings
:
4073 change
= int(settings
['change']) + 1
4074 p4Change
= max(p4Change
, change
)
4076 depotPaths
= sorted(settings
['depot-paths'])
4077 if self
.previousDepotPaths
== []:
4078 self
.previousDepotPaths
= depotPaths
4081 for (prev
, cur
) in zip(self
.previousDepotPaths
, depotPaths
):
4082 prev_list
= prev
.split("/")
4083 cur_list
= cur
.split("/")
4084 for i
in range(0, min(len(cur_list
), len(prev_list
))):
4085 if cur_list
[i
] != prev_list
[i
]:
4089 paths
.append("/".join(cur_list
[:i
+ 1]))
4091 self
.previousDepotPaths
= paths
4094 self
.depotPaths
= sorted(self
.previousDepotPaths
)
4095 self
.changeRange
= "@%s,#head" % p4Change
4096 if not self
.silent
and not self
.detectBranches
:
4097 print("Performing incremental import into %s git branch" % self
.branch
)
4099 self
.branch
= fullP4Ref(self
.branch
, self
.importIntoRemotes
)
4101 if len(args
) == 0 and self
.depotPaths
:
4103 print("Depot paths: %s" % ' '.join(self
.depotPaths
))
4105 if self
.depotPaths
and self
.depotPaths
!= args
:
4106 print("previous import used depot path %s and now %s was specified. "
4107 "This doesn't work!" % (' '.join(self
.depotPaths
),
4111 self
.depotPaths
= sorted(args
)
4116 # Make sure no revision specifiers are used when --changesfile
4118 bad_changesfile
= False
4119 if len(self
.changesFile
) > 0:
4120 for p
in self
.depotPaths
:
4121 if p
.find("@") >= 0 or p
.find("#") >= 0:
4122 bad_changesfile
= True
4125 die("Option --changesfile is incompatible with revision specifiers")
4128 for p
in self
.depotPaths
:
4129 if p
.find("@") != -1:
4130 atIdx
= p
.index("@")
4131 self
.changeRange
= p
[atIdx
:]
4132 if self
.changeRange
== "@all":
4133 self
.changeRange
= ""
4134 elif ',' not in self
.changeRange
:
4135 revision
= self
.changeRange
4136 self
.changeRange
= ""
4138 elif p
.find("#") != -1:
4139 hashIdx
= p
.index("#")
4140 revision
= p
[hashIdx
:]
4142 elif self
.previousDepotPaths
== []:
4143 # pay attention to changesfile, if given, else import
4144 # the entire p4 tree at the head revision
4145 if len(self
.changesFile
) == 0:
4148 p
= re
.sub("\.\.\.$", "", p
)
4149 if not p
.endswith("/"):
4154 self
.depotPaths
= newPaths
4156 # --detect-branches may change this for each branch
4157 self
.branchPrefixes
= self
.depotPaths
4159 self
.loadUserMapFromCache()
4161 if self
.detectLabels
:
4164 if self
.detectBranches
:
4165 # FIXME - what's a P4 projectName ?
4166 self
.projectName
= self
.guessProjectName()
4169 self
.getBranchMappingFromGitBranches()
4171 self
.getBranchMapping()
4173 print("p4-git branches: %s" % self
.p4BranchesInGit
)
4174 print("initial parents: %s" % self
.initialParents
)
4175 for b
in self
.p4BranchesInGit
:
4179 b
= b
[len(self
.projectName
):]
4180 self
.createdBranches
.add(b
)
4190 self
.importHeadRevision(revision
)
4192 self
.importRevisions(args
, branch_arg_given
)
4194 if gitConfigBool("git-p4.importLabels"):
4195 self
.importLabels
= True
4197 if self
.importLabels
:
4198 p4Labels
= getP4Labels(self
.depotPaths
)
4199 gitTags
= getGitTags()
4201 missingP4Labels
= p4Labels
- gitTags
4202 self
.importP4Labels(self
.gitStream
, missingP4Labels
)
4204 except P4CommandException
as e
:
4213 # Cleanup temporary branches created during import
4214 if self
.tempBranches
!= []:
4215 for branch
in self
.tempBranches
:
4216 read_pipe(["git", "update-ref", "-d", branch
])
4217 os
.rmdir(os
.path
.join(os
.environ
.get("GIT_DIR", ".git"), self
.tempBranchLocation
))
4219 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4220 # a convenient shortcut refname "p4".
4221 if self
.importIntoRemotes
:
4222 head_ref
= self
.refPrefix
+ "HEAD"
4223 if not gitBranchExists(head_ref
) and gitBranchExists(self
.branch
):
4224 system(["git", "symbolic-ref", head_ref
, self
.branch
])
4229 class P4Rebase(Command
):
4231 Command
.__init
__(self
)
4233 optparse
.make_option("--import-labels", dest
="importLabels", action
="store_true"),
4235 self
.importLabels
= False
4236 self
.description
= ("Fetches the latest revision from perforce and "
4237 + "rebases the current work (branch) against it")
4239 def run(self
, args
):
4241 sync
.importLabels
= self
.importLabels
4244 return self
.rebase()
4247 if os
.system("git update-index --refresh") != 0:
4248 die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up to date or stash away all your changes with git stash.")
4249 if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
4250 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.")
4252 upstream
, settings
= findUpstreamBranchPoint()
4253 if len(upstream
) == 0:
4254 die("Cannot find upstream branchpoint for rebase")
4256 # the branchpoint may be p4/foo~3, so strip off the parent
4257 upstream
= re
.sub("~[0-9]+$", "", upstream
)
4259 print("Rebasing the current branch onto %s" % upstream
)
4260 oldHead
= read_pipe(["git", "rev-parse", "HEAD"]).strip()
4261 system(["git", "rebase", upstream
])
4262 system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead
,
4267 class P4Clone(P4Sync
):
4269 P4Sync
.__init
__(self
)
4270 self
.description
= "Creates a new git repository and imports from Perforce into it"
4271 self
.usage
= "usage: %prog [options] //depot/path[@revRange]"
4273 optparse
.make_option("--destination", dest
="cloneDestination",
4274 action
='store', default
=None,
4275 help="where to leave result of the clone"),
4276 optparse
.make_option("--bare", dest
="cloneBare",
4277 action
="store_true", default
=False),
4279 self
.cloneDestination
= None
4280 self
.needsGit
= False
4281 self
.cloneBare
= False
4283 def defaultDestination(self
, args
):
4284 # TODO: use common prefix of args?
4286 depotDir
= re
.sub("(@[^@]*)$", "", depotPath
)
4287 depotDir
= re
.sub("(#[^#]*)$", "", depotDir
)
4288 depotDir
= re
.sub(r
"\.\.\.$", "", depotDir
)
4289 depotDir
= re
.sub(r
"/$", "", depotDir
)
4290 return os
.path
.split(depotDir
)[1]
4292 def run(self
, args
):
4296 if self
.keepRepoPath
and not self
.cloneDestination
:
4297 sys
.stderr
.write("Must specify destination for --keep-path\n")
4302 if not self
.cloneDestination
and len(depotPaths
) > 1:
4303 self
.cloneDestination
= depotPaths
[-1]
4304 depotPaths
= depotPaths
[:-1]
4306 for p
in depotPaths
:
4307 if not p
.startswith("//"):
4308 sys
.stderr
.write('Depot paths must start with "//": %s\n' % p
)
4311 if not self
.cloneDestination
:
4312 self
.cloneDestination
= self
.defaultDestination(args
)
4314 print("Importing from %s into %s" % (', '.join(depotPaths
), self
.cloneDestination
))
4316 if not os
.path
.exists(self
.cloneDestination
):
4317 os
.makedirs(self
.cloneDestination
)
4318 chdir(self
.cloneDestination
)
4320 init_cmd
= ["git", "init"]
4322 init_cmd
.append("--bare")
4323 retcode
= subprocess
.call(init_cmd
)
4325 raise subprocess
.CalledProcessError(retcode
, init_cmd
)
4327 if not P4Sync
.run(self
, depotPaths
):
4330 # create a master branch and check out a work tree
4331 if gitBranchExists(self
.branch
):
4332 system(["git", "branch", currentGitBranch(), self
.branch
])
4333 if not self
.cloneBare
:
4334 system(["git", "checkout", "-f"])
4336 print('Not checking out any branch, use '
4337 '"git checkout -q -b master <branch>"')
4339 # auto-set this variable if invoked with --use-client-spec
4340 if self
.useClientSpec_from_options
:
4341 system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
4343 # persist any git-p4 encoding-handling config options passed in for clone:
4344 if gitConfig('git-p4.metadataDecodingStrategy'):
4345 system(["git", "config", "git-p4.metadataDecodingStrategy", gitConfig('git-p4.metadataDecodingStrategy')])
4346 if gitConfig('git-p4.metadataFallbackEncoding'):
4347 system(["git", "config", "git-p4.metadataFallbackEncoding", gitConfig('git-p4.metadataFallbackEncoding')])
4348 if gitConfig('git-p4.pathEncoding'):
4349 system(["git", "config", "git-p4.pathEncoding", gitConfig('git-p4.pathEncoding')])
4354 class P4Unshelve(Command
):
4356 Command
.__init
__(self
)
4358 self
.origin
= "HEAD"
4359 self
.description
= "Unshelve a P4 changelist into a git commit"
4360 self
.usage
= "usage: %prog [options] changelist"
4362 optparse
.make_option("--origin", dest
="origin",
4363 help="Use this base revision instead of the default (%s)" % self
.origin
),
4365 self
.verbose
= False
4366 self
.noCommit
= False
4367 self
.destbranch
= "refs/remotes/p4-unshelved"
4369 def renameBranch(self
, branch_name
):
4370 """Rename the existing branch to branch_name.N ."""
4373 for i
in range(0, 1000):
4374 backup_branch_name
= "{0}.{1}".format(branch_name
, i
)
4375 if not gitBranchExists(backup_branch_name
):
4376 # Copy ref to backup
4377 gitUpdateRef(backup_branch_name
, branch_name
)
4378 gitDeleteRef(branch_name
)
4380 print("renamed old unshelve branch to {0}".format(backup_branch_name
))
4384 sys
.exit("gave up trying to rename existing branch {0}".format(sync
.branch
))
4386 def findLastP4Revision(self
, starting_point
):
4387 """Look back from starting_point for the first commit created by git-p4
4388 to find the P4 commit we are based on, and the depot-paths.
4391 for parent
in (range(65535)):
4392 log
= extractLogMessageFromGitCommit("{0}~{1}".format(starting_point
, parent
))
4393 settings
= extractSettingsGitLog(log
)
4394 if 'change' in settings
:
4397 sys
.exit("could not find git-p4 commits in {0}".format(self
.origin
))
4399 def createShelveParent(self
, change
, branch_name
, sync
, origin
):
4400 """Create a commit matching the parent of the shelved changelist
4403 parent_description
= p4_describe(change
, shelved
=True)
4404 parent_description
['desc'] = 'parent for shelved changelist {}\n'.format(change
)
4405 files
= sync
.extractFilesFromCommit(parent_description
, shelved
=False, shelved_cl
=change
)
4409 # if it was added in the shelved changelist, it won't exist in the parent
4410 if f
['action'] in self
.add_actions
:
4413 # if it was deleted in the shelved changelist it must not be deleted
4414 # in the parent - we might even need to create it if the origin branch
4416 if f
['action'] in self
.delete_actions
:
4419 parent_files
.append(f
)
4421 sync
.commit(parent_description
, parent_files
, branch_name
,
4422 parent
=origin
, allow_empty
=True)
4423 print("created parent commit for {0} based on {1} in {2}".format(
4424 change
, self
.origin
, branch_name
))
4426 def run(self
, args
):
4430 if not gitBranchExists(self
.origin
):
4431 sys
.exit("origin branch {0} does not exist".format(self
.origin
))
4436 # only one change at a time
4439 # if the target branch already exists, rename it
4440 branch_name
= "{0}/{1}".format(self
.destbranch
, change
)
4441 if gitBranchExists(branch_name
):
4442 self
.renameBranch(branch_name
)
4443 sync
.branch
= branch_name
4445 sync
.verbose
= self
.verbose
4446 sync
.suppress_meta_comment
= True
4448 settings
= self
.findLastP4Revision(self
.origin
)
4449 sync
.depotPaths
= settings
['depot-paths']
4450 sync
.branchPrefixes
= sync
.depotPaths
4453 sync
.loadUserMapFromCache()
4456 # create a commit for the parent of the shelved changelist
4457 self
.createShelveParent(change
, branch_name
, sync
, self
.origin
)
4459 # create the commit for the shelved changelist itself
4460 description
= p4_describe(change
, True)
4461 files
= sync
.extractFilesFromCommit(description
, True, change
)
4463 sync
.commit(description
, files
, branch_name
, "")
4466 print("unshelved changelist {0} into {1}".format(change
, branch_name
))
4471 class P4Branches(Command
):
4473 Command
.__init
__(self
)
4475 self
.description
= ("Shows the git branches that hold imports and their "
4476 + "corresponding perforce depot paths")
4477 self
.verbose
= False
4479 def run(self
, args
):
4480 if originP4BranchesExist():
4481 createOrUpdateBranchesFromOrigin()
4483 for line
in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
4486 if not line
.startswith('p4/') or line
== "p4/HEAD":
4490 log
= extractLogMessageFromGitCommit("refs/remotes/%s" % branch
)
4491 settings
= extractSettingsGitLog(log
)
4493 print("%s <= %s (%s)" % (branch
, ",".join(settings
["depot-paths"]), settings
["change"]))
4497 class HelpFormatter(optparse
.IndentedHelpFormatter
):
4499 optparse
.IndentedHelpFormatter
.__init
__(self
)
4501 def format_description(self
, description
):
4503 return description
+ "\n"
4508 def printUsage(commands
):
4509 print("usage: %s <command> [options]" % sys
.argv
[0])
4511 print("valid commands: %s" % ", ".join(commands
))
4513 print("Try %s <command> --help for command specific help." % sys
.argv
[0])
4523 "branches": P4Branches
,
4524 "unshelve": P4Unshelve
,
4529 if len(sys
.argv
[1:]) == 0:
4530 printUsage(commands
.keys())
4533 cmdName
= sys
.argv
[1]
4535 klass
= commands
[cmdName
]
4538 print("unknown command %s" % cmdName
)
4540 printUsage(commands
.keys())
4543 options
= cmd
.options
4544 cmd
.gitdir
= os
.environ
.get("GIT_DIR", None)
4548 options
.append(optparse
.make_option("--verbose", "-v", dest
="verbose", action
="store_true"))
4550 options
.append(optparse
.make_option("--git-dir", dest
="gitdir"))
4552 parser
= optparse
.OptionParser(cmd
.usage
.replace("%prog", "%prog " + cmdName
),
4554 description
=cmd
.description
,
4555 formatter
=HelpFormatter())
4558 cmd
, args
= parser
.parse_args(sys
.argv
[2:], cmd
)
4564 verbose
= cmd
.verbose
4566 if cmd
.gitdir
is None:
4567 cmd
.gitdir
= os
.path
.abspath(".git")
4568 if not isValidGitDir(cmd
.gitdir
):
4569 # "rev-parse --git-dir" without arguments will try $PWD/.git
4570 cmd
.gitdir
= read_pipe(["git", "rev-parse", "--git-dir"]).strip()
4571 if os
.path
.exists(cmd
.gitdir
):
4572 cdup
= read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
4576 if not isValidGitDir(cmd
.gitdir
):
4577 if isValidGitDir(cmd
.gitdir
+ "/.git"):
4578 cmd
.gitdir
+= "/.git"
4580 die("fatal: cannot locate git repository at %s" % cmd
.gitdir
)
4582 # so git commands invoked from the P4 workspace will succeed
4583 os
.environ
["GIT_DIR"] = cmd
.gitdir
4585 if not cmd
.run(args
):
4590 if __name__
== '__main__':