mirror of https://github.com/git/git.git
Git Source Code Mirror - This is a publish-only repository and all pull requests are ignored. Please follow Documentation/SubmittingPatches procedure for any of your improvements.
https://git-scm.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4621 lines
168 KiB
4621 lines
168 KiB
#!/usr/bin/env python |
|
# |
|
# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. |
|
# |
|
# Author: Simon Hausmann <simon@lst.de> |
|
# Copyright: 2007 Simon Hausmann <simon@lst.de> |
|
# 2007 Trolltech ASA |
|
# License: MIT <http://www.opensource.org/licenses/mit-license.php> |
|
# |
|
# pylint: disable=bad-whitespace |
|
# pylint: disable=broad-except |
|
# pylint: disable=consider-iterating-dictionary |
|
# pylint: disable=disable |
|
# pylint: disable=fixme |
|
# pylint: disable=invalid-name |
|
# pylint: disable=line-too-long |
|
# pylint: disable=missing-docstring |
|
# pylint: disable=no-self-use |
|
# pylint: disable=superfluous-parens |
|
# pylint: disable=too-few-public-methods |
|
# pylint: disable=too-many-arguments |
|
# pylint: disable=too-many-branches |
|
# pylint: disable=too-many-instance-attributes |
|
# pylint: disable=too-many-lines |
|
# pylint: disable=too-many-locals |
|
# pylint: disable=too-many-nested-blocks |
|
# pylint: disable=too-many-statements |
|
# pylint: disable=ungrouped-imports |
|
# pylint: disable=unused-import |
|
# pylint: disable=wrong-import-order |
|
# pylint: disable=wrong-import-position |
|
# |
|
|
|
import struct |
|
import sys |
|
if sys.version_info.major < 3 and sys.version_info.minor < 7: |
|
sys.stderr.write("git-p4: requires Python 2.7 or later.\n") |
|
sys.exit(1) |
|
|
|
import ctypes |
|
import errno |
|
import functools |
|
import glob |
|
import marshal |
|
import optparse |
|
import os |
|
import platform |
|
import re |
|
import shutil |
|
import stat |
|
import subprocess |
|
import tempfile |
|
import time |
|
import zipfile |
|
import zlib |
|
|
|
# On python2.7 where raw_input() and input() are both availble, |
|
# we want raw_input's semantics, but aliased to input for python3 |
|
# compatibility |
|
# support basestring in python3 |
|
try: |
|
if raw_input and input: |
|
input = raw_input |
|
except: |
|
pass |
|
|
|
verbose = False |
|
|
|
# Only labels/tags matching this will be imported/exported |
|
defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' |
|
|
|
# The block size is reduced automatically if required |
|
defaultBlockSize = 1 << 20 |
|
|
|
defaultMetadataDecodingStrategy = 'passthrough' if sys.version_info.major == 2 else 'fallback' |
|
defaultFallbackMetadataEncoding = 'cp1252' |
|
|
|
p4_access_checked = False |
|
|
|
re_ko_keywords = re.compile(br'\$(Id|Header)(:[^$\n]+)?\$') |
|
re_k_keywords = re.compile(br'\$(Id|Header|Author|Date|DateTime|Change|File|Revision)(:[^$\n]+)?\$') |
|
|
|
|
|
def format_size_human_readable(num): |
|
"""Returns a number of units (typically bytes) formatted as a |
|
human-readable string. |
|
""" |
|
if num < 1024: |
|
return '{:d} B'.format(num) |
|
for unit in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: |
|
num /= 1024.0 |
|
if num < 1024.0: |
|
return "{:3.1f} {}B".format(num, unit) |
|
return "{:.1f} YiB".format(num) |
|
|
|
|
|
def p4_build_cmd(cmd): |
|
"""Build a suitable p4 command line. |
|
|
|
This consolidates building and returning a p4 command line into one |
|
location. It means that hooking into the environment, or other |
|
configuration can be done more easily. |
|
""" |
|
real_cmd = ["p4"] |
|
|
|
user = gitConfig("git-p4.user") |
|
if len(user) > 0: |
|
real_cmd += ["-u", user] |
|
|
|
password = gitConfig("git-p4.password") |
|
if len(password) > 0: |
|
real_cmd += ["-P", password] |
|
|
|
port = gitConfig("git-p4.port") |
|
if len(port) > 0: |
|
real_cmd += ["-p", port] |
|
|
|
host = gitConfig("git-p4.host") |
|
if len(host) > 0: |
|
real_cmd += ["-H", host] |
|
|
|
client = gitConfig("git-p4.client") |
|
if len(client) > 0: |
|
real_cmd += ["-c", client] |
|
|
|
retries = gitConfigInt("git-p4.retries") |
|
if retries is None: |
|
# Perform 3 retries by default |
|
retries = 3 |
|
if retries > 0: |
|
# Provide a way to not pass this option by setting git-p4.retries to 0 |
|
real_cmd += ["-r", str(retries)] |
|
|
|
real_cmd += cmd |
|
|
|
# now check that we can actually talk to the server |
|
global p4_access_checked |
|
if not p4_access_checked: |
|
p4_access_checked = True # suppress access checks in p4_check_access itself |
|
p4_check_access() |
|
|
|
return real_cmd |
|
|
|
|
|
def git_dir(path): |
|
"""Return TRUE if the given path is a git directory (/path/to/dir/.git). |
|
This won't automatically add ".git" to a directory. |
|
""" |
|
d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip() |
|
if not d or len(d) == 0: |
|
return None |
|
else: |
|
return d |
|
|
|
|
|
def chdir(path, is_client_path=False): |
|
"""Do chdir to the given path, and set the PWD environment variable for use |
|
by P4. It does not look at getcwd() output. Since we're not using the |
|
shell, it is necessary to set the PWD environment variable explicitly. |
|
|
|
Normally, expand the path to force it to be absolute. This addresses |
|
the use of relative path names inside P4 settings, e.g. |
|
P4CONFIG=.p4config. P4 does not simply open the filename as given; it |
|
looks for .p4config using PWD. |
|
|
|
If is_client_path, the path was handed to us directly by p4, and may be |
|
a symbolic link. Do not call os.getcwd() in this case, because it will |
|
cause p4 to think that PWD is not inside the client path. |
|
""" |
|
|
|
os.chdir(path) |
|
if not is_client_path: |
|
path = os.getcwd() |
|
os.environ['PWD'] = path |
|
|
|
|
|
def calcDiskFree(): |
|
"""Return free space in bytes on the disk of the given dirname.""" |
|
if platform.system() == 'Windows': |
|
free_bytes = ctypes.c_ulonglong(0) |
|
ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes)) |
|
return free_bytes.value |
|
else: |
|
st = os.statvfs(os.getcwd()) |
|
return st.f_bavail * st.f_frsize |
|
|
|
|
|
def die(msg): |
|
"""Terminate execution. Make sure that any running child processes have |
|
been wait()ed for before calling this. |
|
""" |
|
if verbose: |
|
raise Exception(msg) |
|
else: |
|
sys.stderr.write(msg + "\n") |
|
sys.exit(1) |
|
|
|
|
|
def prompt(prompt_text): |
|
"""Prompt the user to choose one of the choices. |
|
|
|
Choices are identified in the prompt_text by square brackets around a |
|
single letter option. |
|
""" |
|
choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text)) |
|
while True: |
|
sys.stderr.flush() |
|
sys.stdout.write(prompt_text) |
|
sys.stdout.flush() |
|
response = sys.stdin.readline().strip().lower() |
|
if not response: |
|
continue |
|
response = response[0] |
|
if response in choices: |
|
return response |
|
|
|
|
|
# We need different encoding/decoding strategies for text data being passed |
|
# around in pipes depending on python version |
|
if bytes is not str: |
|
# For python3, always encode and decode as appropriate |
|
def decode_text_stream(s): |
|
return s.decode() if isinstance(s, bytes) else s |
|
|
|
def encode_text_stream(s): |
|
return s.encode() if isinstance(s, str) else s |
|
else: |
|
# For python2.7, pass read strings as-is, but also allow writing unicode |
|
def decode_text_stream(s): |
|
return s |
|
|
|
def encode_text_stream(s): |
|
return s.encode('utf_8') if isinstance(s, unicode) else s |
|
|
|
|
|
class MetadataDecodingException(Exception): |
|
def __init__(self, input_string): |
|
self.input_string = input_string |
|
|
|
def __str__(self): |
|
return """Decoding perforce metadata failed! |
|
The failing string was: |
|
--- |
|
{} |
|
--- |
|
Consider setting the git-p4.metadataDecodingStrategy config option to |
|
'fallback', to allow metadata to be decoded using a fallback encoding, |
|
defaulting to cp1252.""".format(self.input_string) |
|
|
|
|
|
encoding_fallback_warning_issued = False |
|
encoding_escape_warning_issued = False |
|
def metadata_stream_to_writable_bytes(s): |
|
encodingStrategy = gitConfig('git-p4.metadataDecodingStrategy') or defaultMetadataDecodingStrategy |
|
fallbackEncoding = gitConfig('git-p4.metadataFallbackEncoding') or defaultFallbackMetadataEncoding |
|
if not isinstance(s, bytes): |
|
return s.encode('utf_8') |
|
if encodingStrategy == 'passthrough': |
|
return s |
|
try: |
|
s.decode('utf_8') |
|
return s |
|
except UnicodeDecodeError: |
|
if encodingStrategy == 'fallback' and fallbackEncoding: |
|
global encoding_fallback_warning_issued |
|
global encoding_escape_warning_issued |
|
try: |
|
if not encoding_fallback_warning_issued: |
|
print("\nCould not decode value as utf-8; using configured fallback encoding %s: %s" % (fallbackEncoding, s)) |
|
print("\n(this warning is only displayed once during an import)") |
|
encoding_fallback_warning_issued = True |
|
return s.decode(fallbackEncoding).encode('utf_8') |
|
except Exception as exc: |
|
if not encoding_escape_warning_issued: |
|
print("\nCould not decode value with configured fallback encoding %s; escaping bytes over 127: %s" % (fallbackEncoding, s)) |
|
print("\n(this warning is only displayed once during an import)") |
|
encoding_escape_warning_issued = True |
|
escaped_bytes = b'' |
|
# bytes and strings work very differently in python2 vs python3... |
|
if str is bytes: |
|
for byte in s: |
|
byte_number = struct.unpack('>B', byte)[0] |
|
if byte_number > 127: |
|
escaped_bytes += b'%' |
|
escaped_bytes += hex(byte_number)[2:].upper() |
|
else: |
|
escaped_bytes += byte |
|
else: |
|
for byte_number in s: |
|
if byte_number > 127: |
|
escaped_bytes += b'%' |
|
escaped_bytes += hex(byte_number).upper().encode()[2:] |
|
else: |
|
escaped_bytes += bytes([byte_number]) |
|
return escaped_bytes |
|
|
|
raise MetadataDecodingException(s) |
|
|
|
|
|
def decode_path(path): |
|
"""Decode a given string (bytes or otherwise) using configured path |
|
encoding options. |
|
""" |
|
|
|
encoding = gitConfig('git-p4.pathEncoding') or 'utf_8' |
|
if bytes is not str: |
|
return path.decode(encoding, errors='replace') if isinstance(path, bytes) else path |
|
else: |
|
try: |
|
path.decode('ascii') |
|
except: |
|
path = path.decode(encoding, errors='replace') |
|
if verbose: |
|
print('Path with non-ASCII characters detected. Used {} to decode: {}'.format(encoding, path)) |
|
return path |
|
|
|
|
|
def run_git_hook(cmd, param=[]): |
|
"""Execute a hook if the hook exists.""" |
|
args = ['git', 'hook', 'run', '--ignore-missing', cmd] |
|
if param: |
|
args.append("--") |
|
for p in param: |
|
args.append(p) |
|
return subprocess.call(args) == 0 |
|
|
|
|
|
def write_pipe(c, stdin, *k, **kw): |
|
if verbose: |
|
sys.stderr.write('Writing pipe: {}\n'.format(' '.join(c))) |
|
|
|
p = subprocess.Popen(c, stdin=subprocess.PIPE, *k, **kw) |
|
pipe = p.stdin |
|
val = pipe.write(stdin) |
|
pipe.close() |
|
if p.wait(): |
|
die('Command failed: {}'.format(' '.join(c))) |
|
|
|
return val |
|
|
|
|
|
def p4_write_pipe(c, stdin, *k, **kw): |
|
real_cmd = p4_build_cmd(c) |
|
if bytes is not str and isinstance(stdin, str): |
|
stdin = encode_text_stream(stdin) |
|
return write_pipe(real_cmd, stdin, *k, **kw) |
|
|
|
|
|
def read_pipe_full(c, *k, **kw): |
|
"""Read output from command. Returns a tuple of the return status, stdout |
|
text and stderr text. |
|
""" |
|
if verbose: |
|
sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c))) |
|
|
|
p = subprocess.Popen( |
|
c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, *k, **kw) |
|
out, err = p.communicate() |
|
return (p.returncode, out, decode_text_stream(err)) |
|
|
|
|
|
def read_pipe(c, ignore_error=False, raw=False, *k, **kw): |
|
"""Read output from command. Returns the output text on success. On |
|
failure, terminates execution, unless ignore_error is True, when it |
|
returns an empty string. |
|
|
|
If raw is True, do not attempt to decode output text. |
|
""" |
|
retcode, out, err = read_pipe_full(c, *k, **kw) |
|
if retcode != 0: |
|
if ignore_error: |
|
out = "" |
|
else: |
|
die('Command failed: {}\nError: {}'.format(' '.join(c), err)) |
|
if not raw: |
|
out = decode_text_stream(out) |
|
return out |
|
|
|
|
|
def read_pipe_text(c, *k, **kw): |
|
"""Read output from a command with trailing whitespace stripped. On error, |
|
returns None. |
|
""" |
|
retcode, out, err = read_pipe_full(c, *k, **kw) |
|
if retcode != 0: |
|
return None |
|
else: |
|
return decode_text_stream(out).rstrip() |
|
|
|
|
|
def p4_read_pipe(c, ignore_error=False, raw=False, *k, **kw): |
|
real_cmd = p4_build_cmd(c) |
|
return read_pipe(real_cmd, ignore_error, raw=raw, *k, **kw) |
|
|
|
|
|
def read_pipe_lines(c, raw=False, *k, **kw): |
|
if verbose: |
|
sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c))) |
|
|
|
p = subprocess.Popen(c, stdout=subprocess.PIPE, *k, **kw) |
|
pipe = p.stdout |
|
lines = pipe.readlines() |
|
if not raw: |
|
lines = [decode_text_stream(line) for line in lines] |
|
if pipe.close() or p.wait(): |
|
die('Command failed: {}'.format(' '.join(c))) |
|
return lines |
|
|
|
|
|
def p4_read_pipe_lines(c, *k, **kw): |
|
"""Specifically invoke p4 on the command supplied.""" |
|
real_cmd = p4_build_cmd(c) |
|
return read_pipe_lines(real_cmd, *k, **kw) |
|
|
|
|
|
def p4_has_command(cmd): |
|
"""Ask p4 for help on this command. If it returns an error, the command |
|
does not exist in this version of p4. |
|
""" |
|
real_cmd = p4_build_cmd(["help", cmd]) |
|
p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE, |
|
stderr=subprocess.PIPE) |
|
p.communicate() |
|
return p.returncode == 0 |
|
|
|
|
|
def p4_has_move_command(): |
|
"""See if the move command exists, that it supports -k, and that it has not |
|
been administratively disabled. The arguments must be correct, but the |
|
filenames do not have to exist. Use ones with wildcards so even if they |
|
exist, it will fail. |
|
""" |
|
|
|
if not p4_has_command("move"): |
|
return False |
|
cmd = p4_build_cmd(["move", "-k", "@from", "@to"]) |
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
out, err = p.communicate() |
|
err = decode_text_stream(err) |
|
# return code will be 1 in either case |
|
if err.find("Invalid option") >= 0: |
|
return False |
|
if err.find("disabled") >= 0: |
|
return False |
|
# assume it failed because @... was invalid changelist |
|
return True |
|
|
|
|
|
def system(cmd, ignore_error=False, *k, **kw): |
|
if verbose: |
|
sys.stderr.write("executing {}\n".format( |
|
' '.join(cmd) if isinstance(cmd, list) else cmd)) |
|
retcode = subprocess.call(cmd, *k, **kw) |
|
if retcode and not ignore_error: |
|
raise subprocess.CalledProcessError(retcode, cmd) |
|
|
|
return retcode |
|
|
|
|
|
def p4_system(cmd, *k, **kw): |
|
"""Specifically invoke p4 as the system command.""" |
|
real_cmd = p4_build_cmd(cmd) |
|
retcode = subprocess.call(real_cmd, *k, **kw) |
|
if retcode: |
|
raise subprocess.CalledProcessError(retcode, real_cmd) |
|
|
|
|
|
def die_bad_access(s): |
|
die("failure accessing depot: {0}".format(s.rstrip())) |
|
|
|
|
|
def p4_check_access(min_expiration=1): |
|
"""Check if we can access Perforce - account still logged in.""" |
|
|
|
results = p4CmdList(["login", "-s"]) |
|
|
|
if len(results) == 0: |
|
# should never get here: always get either some results, or a p4ExitCode |
|
assert("could not parse response from perforce") |
|
|
|
result = results[0] |
|
|
|
if 'p4ExitCode' in result: |
|
# p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path |
|
die_bad_access("could not run p4") |
|
|
|
code = result.get("code") |
|
if not code: |
|
# we get here if we couldn't connect and there was nothing to unmarshal |
|
die_bad_access("could not connect") |
|
|
|
elif code == "stat": |
|
expiry = result.get("TicketExpiration") |
|
if expiry: |
|
expiry = int(expiry) |
|
if expiry > min_expiration: |
|
# ok to carry on |
|
return |
|
else: |
|
die_bad_access("perforce ticket expires in {0} seconds".format(expiry)) |
|
|
|
else: |
|
# account without a timeout - all ok |
|
return |
|
|
|
elif code == "error": |
|
data = result.get("data") |
|
if data: |
|
die_bad_access("p4 error: {0}".format(data)) |
|
else: |
|
die_bad_access("unknown error") |
|
elif code == "info": |
|
return |
|
else: |
|
die_bad_access("unknown error code {0}".format(code)) |
|
|
|
|
|
_p4_version_string = None |
|
|
|
|
|
def p4_version_string(): |
|
"""Read the version string, showing just the last line, which hopefully is |
|
the interesting version bit. |
|
|
|
$ p4 -V |
|
Perforce - The Fast Software Configuration Management System. |
|
Copyright 1995-2011 Perforce Software. All rights reserved. |
|
Rev. P4/NTX86/2011.1/393975 (2011/12/16). |
|
""" |
|
global _p4_version_string |
|
if not _p4_version_string: |
|
a = p4_read_pipe_lines(["-V"]) |
|
_p4_version_string = a[-1].rstrip() |
|
return _p4_version_string |
|
|
|
|
|
def p4_integrate(src, dest): |
|
p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)]) |
|
|
|
|
|
def p4_sync(f, *options): |
|
p4_system(["sync"] + list(options) + [wildcard_encode(f)]) |
|
|
|
|
|
def p4_add(f): |
|
"""Forcibly add file names with wildcards.""" |
|
if wildcard_present(f): |
|
p4_system(["add", "-f", f]) |
|
else: |
|
p4_system(["add", f]) |
|
|
|
|
|
def p4_delete(f): |
|
p4_system(["delete", wildcard_encode(f)]) |
|
|
|
|
|
def p4_edit(f, *options): |
|
p4_system(["edit"] + list(options) + [wildcard_encode(f)]) |
|
|
|
|
|
def p4_revert(f): |
|
p4_system(["revert", wildcard_encode(f)]) |
|
|
|
|
|
def p4_reopen(type, f): |
|
p4_system(["reopen", "-t", type, wildcard_encode(f)]) |
|
|
|
|
|
def p4_reopen_in_change(changelist, files): |
|
cmd = ["reopen", "-c", str(changelist)] + files |
|
p4_system(cmd) |
|
|
|
|
|
def p4_move(src, dest): |
|
p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)]) |
|
|
|
|
|
def p4_last_change(): |
|
results = p4CmdList(["changes", "-m", "1"], skip_info=True) |
|
return int(results[0]['change']) |
|
|
|
|
|
def p4_describe(change, shelved=False): |
|
"""Make sure it returns a valid result by checking for the presence of |
|
field "time". |
|
|
|
Return a dict of the results. |
|
""" |
|
|
|
cmd = ["describe", "-s"] |
|
if shelved: |
|
cmd += ["-S"] |
|
cmd += [str(change)] |
|
|
|
ds = p4CmdList(cmd, skip_info=True) |
|
if len(ds) != 1: |
|
die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds))) |
|
|
|
d = ds[0] |
|
|
|
if "p4ExitCode" in d: |
|
die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"], |
|
str(d))) |
|
if "code" in d: |
|
if d["code"] == "error": |
|
die("p4 describe -s %d returned error code: %s" % (change, str(d))) |
|
|
|
if "time" not in d: |
|
die("p4 describe -s %d returned no \"time\": %s" % (change, str(d))) |
|
|
|
return d |
|
|
|
|
|
def split_p4_type(p4type): |
|
"""Canonicalize the p4 type and return a tuple of the base type, plus any |
|
modifiers. See "p4 help filetypes" for a list and explanation. |
|
""" |
|
|
|
p4_filetypes_historical = { |
|
"ctempobj": "binary+Sw", |
|
"ctext": "text+C", |
|
"cxtext": "text+Cx", |
|
"ktext": "text+k", |
|
"kxtext": "text+kx", |
|
"ltext": "text+F", |
|
"tempobj": "binary+FSw", |
|
"ubinary": "binary+F", |
|
"uresource": "resource+F", |
|
"uxbinary": "binary+Fx", |
|
"xbinary": "binary+x", |
|
"xltext": "text+Fx", |
|
"xtempobj": "binary+Swx", |
|
"xtext": "text+x", |
|
"xunicode": "unicode+x", |
|
"xutf16": "utf16+x", |
|
} |
|
if p4type in p4_filetypes_historical: |
|
p4type = p4_filetypes_historical[p4type] |
|
mods = "" |
|
s = p4type.split("+") |
|
base = s[0] |
|
mods = "" |
|
if len(s) > 1: |
|
mods = s[1] |
|
return (base, mods) |
|
|
|
|
|
def p4_type(f): |
|
"""Return the raw p4 type of a file (text, text+ko, etc).""" |
|
|
|
results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)]) |
|
return results[0]['headType'] |
|
|
|
|
|
def p4_keywords_regexp_for_type(base, type_mods): |
|
"""Given a type base and modifier, return a regexp matching the keywords |
|
that can be expanded in the file. |
|
""" |
|
|
|
if base in ("text", "unicode", "binary"): |
|
if "ko" in type_mods: |
|
return re_ko_keywords |
|
elif "k" in type_mods: |
|
return re_k_keywords |
|
else: |
|
return None |
|
else: |
|
return None |
|
|
|
|
|
def p4_keywords_regexp_for_file(file): |
|
"""Given a file, return a regexp matching the possible RCS keywords that |
|
will be expanded, or None for files with kw expansion turned off. |
|
""" |
|
|
|
if not os.path.exists(file): |
|
return None |
|
else: |
|
type_base, type_mods = split_p4_type(p4_type(file)) |
|
return p4_keywords_regexp_for_type(type_base, type_mods) |
|
|
|
|
|
def setP4ExecBit(file, mode): |
|
"""Reopens an already open file and changes the execute bit to match the |
|
execute bit setting in the passed in mode. |
|
""" |
|
|
|
p4Type = "+x" |
|
|
|
if not isModeExec(mode): |
|
p4Type = getP4OpenedType(file) |
|
p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type) |
|
p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type) |
|
if p4Type[-1] == "+": |
|
p4Type = p4Type[0:-1] |
|
|
|
p4_reopen(p4Type, file) |
|
|
|
|
|
def getP4OpenedType(file): |
|
"""Returns the perforce file type for the given file.""" |
|
|
|
result = p4_read_pipe(["opened", wildcard_encode(file)]) |
|
match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) |
|
if match: |
|
return match.group(1) |
|
else: |
|
die("Could not determine file type for %s (result: '%s')" % (file, result)) |
|
|
|
|
|
def getP4Labels(depotPaths): |
|
"""Return the set of all p4 labels.""" |
|
|
|
labels = set() |
|
if not isinstance(depotPaths, list): |
|
depotPaths = [depotPaths] |
|
|
|
for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]): |
|
label = l['label'] |
|
labels.add(label) |
|
|
|
return labels |
|
|
|
|
|
def getGitTags(): |
|
"""Return the set of all git tags.""" |
|
|
|
gitTags = set() |
|
for line in read_pipe_lines(["git", "tag"]): |
|
tag = line.strip() |
|
gitTags.add(tag) |
|
return gitTags |
|
|
|
|
|
_diff_tree_pattern = None |
|
|
|
|
|
def parseDiffTreeEntry(entry): |
|
"""Parses a single diff tree entry into its component elements. |
|
|
|
See git-diff-tree(1) manpage for details about the format of the diff |
|
output. This method returns a dictionary with the following elements: |
|
|
|
src_mode - The mode of the source file |
|
dst_mode - The mode of the destination file |
|
src_sha1 - The sha1 for the source file |
|
dst_sha1 - The sha1 fr the destination file |
|
status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) |
|
status_score - The score for the status (applicable for 'C' and 'R' |
|
statuses). This is None if there is no score. |
|
src - The path for the source file. |
|
dst - The path for the destination file. This is only present for |
|
copy or renames. If it is not present, this is None. |
|
|
|
If the pattern is not matched, None is returned. |
|
""" |
|
|
|
global _diff_tree_pattern |
|
if not _diff_tree_pattern: |
|
_diff_tree_pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') |
|
|
|
match = _diff_tree_pattern.match(entry) |
|
if match: |
|
return { |
|
'src_mode': match.group(1), |
|
'dst_mode': match.group(2), |
|
'src_sha1': match.group(3), |
|
'dst_sha1': match.group(4), |
|
'status': match.group(5), |
|
'status_score': match.group(6), |
|
'src': match.group(7), |
|
'dst': match.group(10) |
|
} |
|
return None |
|
|
|
|
|
def isModeExec(mode): |
|
"""Returns True if the given git mode represents an executable file, |
|
otherwise False. |
|
""" |
|
return mode[-3:] == "755" |
|
|
|
|
|
class P4Exception(Exception): |
|
"""Base class for exceptions from the p4 client.""" |
|
|
|
def __init__(self, exit_code): |
|
self.p4ExitCode = exit_code |
|
|
|
|
|
class P4ServerException(P4Exception): |
|
"""Base class for exceptions where we get some kind of marshalled up result |
|
from the server. |
|
""" |
|
|
|
def __init__(self, exit_code, p4_result): |
|
super(P4ServerException, self).__init__(exit_code) |
|
self.p4_result = p4_result |
|
self.code = p4_result[0]['code'] |
|
self.data = p4_result[0]['data'] |
|
|
|
|
|
class P4RequestSizeException(P4ServerException): |
|
"""One of the maxresults or maxscanrows errors.""" |
|
|
|
def __init__(self, exit_code, p4_result, limit): |
|
super(P4RequestSizeException, self).__init__(exit_code, p4_result) |
|
self.limit = limit |
|
|
|
|
|
class P4CommandException(P4Exception): |
|
"""Something went wrong calling p4 which means we have to give up.""" |
|
|
|
def __init__(self, msg): |
|
self.msg = msg |
|
|
|
def __str__(self): |
|
return self.msg |
|
|
|
|
|
def isModeExecChanged(src_mode, dst_mode): |
|
return isModeExec(src_mode) != isModeExec(dst_mode) |
|
|
|
|
|
def p4KeysContainingNonUtf8Chars(): |
|
"""Returns all keys which may contain non UTF-8 encoded strings |
|
for which a fallback strategy has to be applied. |
|
""" |
|
return ['desc', 'client', 'FullName'] |
|
|
|
|
|
def p4KeysContainingBinaryData(): |
|
"""Returns all keys which may contain arbitrary binary data |
|
""" |
|
return ['data'] |
|
|
|
|
|
def p4KeyContainsFilePaths(key): |
|
"""Returns True if the key contains file paths. These are handled by decode_path(). |
|
Otherwise False. |
|
""" |
|
return key.startswith('depotFile') or key in ['path', 'clientFile'] |
|
|
|
|
|
def p4KeyWhichCanBeDirectlyDecoded(key): |
|
"""Returns True if the key can be directly decoded as UTF-8 string |
|
Otherwise False. |
|
|
|
Keys which can not be encoded directly: |
|
- `data` which may contain arbitrary binary data |
|
- `desc` or `client` or `FullName` which may contain non-UTF8 encoded text |
|
- `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text, handled by decode_path() |
|
""" |
|
if key in p4KeysContainingNonUtf8Chars() or \ |
|
key in p4KeysContainingBinaryData() or \ |
|
p4KeyContainsFilePaths(key): |
|
return False |
|
return True |
|
|
|
|
|
def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False, |
|
errors_as_exceptions=False, *k, **kw): |
|
|
|
cmd = p4_build_cmd(["-G"] + cmd) |
|
if verbose: |
|
sys.stderr.write("Opening pipe: {}\n".format(' '.join(cmd))) |
|
|
|
# Use a temporary file to avoid deadlocks without |
|
# subprocess.communicate(), which would put another copy |
|
# of stdout into memory. |
|
stdin_file = None |
|
if stdin is not None: |
|
stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) |
|
if not isinstance(stdin, list): |
|
stdin_file.write(stdin) |
|
else: |
|
for i in stdin: |
|
stdin_file.write(encode_text_stream(i)) |
|
stdin_file.write(b'\n') |
|
stdin_file.flush() |
|
stdin_file.seek(0) |
|
|
|
p4 = subprocess.Popen( |
|
cmd, stdin=stdin_file, stdout=subprocess.PIPE, *k, **kw) |
|
|
|
result = [] |
|
try: |
|
while True: |
|
entry = marshal.load(p4.stdout) |
|
|
|
if bytes is not str: |
|
# Decode unmarshalled dict to use str keys and values. Special cases are handled below. |
|
decoded_entry = {} |
|
for key, value in entry.items(): |
|
key = key.decode() |
|
if isinstance(value, bytes) and p4KeyWhichCanBeDirectlyDecoded(key): |
|
value = value.decode() |
|
decoded_entry[key] = value |
|
# Parse out data if it's an error response |
|
if decoded_entry.get('code') == 'error' and 'data' in decoded_entry: |
|
decoded_entry['data'] = decoded_entry['data'].decode() |
|
entry = decoded_entry |
|
if skip_info: |
|
if 'code' in entry and entry['code'] == 'info': |
|
continue |
|
for key in p4KeysContainingNonUtf8Chars(): |
|
if key in entry: |
|
entry[key] = metadata_stream_to_writable_bytes(entry[key]) |
|
if cb is not None: |
|
cb(entry) |
|
else: |
|
result.append(entry) |
|
except EOFError: |
|
pass |
|
exitCode = p4.wait() |
|
if exitCode != 0: |
|
if errors_as_exceptions: |
|
if len(result) > 0: |
|
data = result[0].get('data') |
|
if data: |
|
m = re.search('Too many rows scanned \(over (\d+)\)', data) |
|
if not m: |
|
m = re.search('Request too large \(over (\d+)\)', data) |
|
|
|
if m: |
|
limit = int(m.group(1)) |
|
raise P4RequestSizeException(exitCode, result, limit) |
|
|
|
raise P4ServerException(exitCode, result) |
|
else: |
|
raise P4Exception(exitCode) |
|
else: |
|
entry = {} |
|
entry["p4ExitCode"] = exitCode |
|
result.append(entry) |
|
|
|
return result |
|
|
|
|
|
def p4Cmd(cmd, *k, **kw): |
|
list = p4CmdList(cmd, *k, **kw) |
|
result = {} |
|
for entry in list: |
|
result.update(entry) |
|
return result |
|
|
|
|
|
def p4Where(depotPath): |
|
if not depotPath.endswith("/"): |
|
depotPath += "/" |
|
depotPathLong = depotPath + "..." |
|
outputList = p4CmdList(["where", depotPathLong]) |
|
output = None |
|
for entry in outputList: |
|
if "depotFile" in entry: |
|
# Search for the base client side depot path, as long as it starts with the branch's P4 path. |
|
# The base path always ends with "/...". |
|
entry_path = decode_path(entry['depotFile']) |
|
if entry_path.find(depotPath) == 0 and entry_path[-4:] == "/...": |
|
output = entry |
|
break |
|
elif "data" in entry: |
|
data = entry.get("data") |
|
space = data.find(" ") |
|
if data[:space] == depotPath: |
|
output = entry |
|
break |
|
if output is None: |
|
return "" |
|
if output["code"] == "error": |
|
return "" |
|
clientPath = "" |
|
if "path" in output: |
|
clientPath = decode_path(output['path']) |
|
elif "data" in output: |
|
data = output.get("data") |
|
lastSpace = data.rfind(b" ") |
|
clientPath = decode_path(data[lastSpace + 1:]) |
|
|
|
if clientPath.endswith("..."): |
|
clientPath = clientPath[:-3] |
|
return clientPath |
|
|
|
|
|
def currentGitBranch(): |
|
return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"]) |
|
|
|
|
|
def isValidGitDir(path): |
|
return git_dir(path) is not None |
|
|
|
|
|
def parseRevision(ref): |
|
return read_pipe(["git", "rev-parse", ref]).strip() |
|
|
|
|
|
def branchExists(ref): |
|
rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref], |
|
ignore_error=True) |
|
return len(rev) > 0 |
|
|
|
|
|
def extractLogMessageFromGitCommit(commit): |
|
logMessage = "" |
|
|
|
# fixme: title is first line of commit, not 1st paragraph. |
|
foundTitle = False |
|
for log in read_pipe_lines(["git", "cat-file", "commit", commit]): |
|
if not foundTitle: |
|
if len(log) == 1: |
|
foundTitle = True |
|
continue |
|
|
|
logMessage += log |
|
return logMessage |
|
|
|
|
|
def extractSettingsGitLog(log): |
|
values = {} |
|
for line in log.split("\n"): |
|
line = line.strip() |
|
m = re.search(r"^ *\[git-p4: (.*)\]$", line) |
|
if not m: |
|
continue |
|
|
|
assignments = m.group(1).split(':') |
|
for a in assignments: |
|
vals = a.split('=') |
|
key = vals[0].strip() |
|
val = ('='.join(vals[1:])).strip() |
|
if val.endswith('\"') and val.startswith('"'): |
|
val = val[1:-1] |
|
|
|
values[key] = val |
|
|
|
paths = values.get("depot-paths") |
|
if not paths: |
|
paths = values.get("depot-path") |
|
if paths: |
|
values['depot-paths'] = paths.split(',') |
|
return values |
|
|
|
|
|
def gitBranchExists(branch): |
|
proc = subprocess.Popen(["git", "rev-parse", branch], |
|
stderr=subprocess.PIPE, stdout=subprocess.PIPE) |
|
return proc.wait() == 0 |
|
|
|
|
|
def gitUpdateRef(ref, newvalue): |
|
subprocess.check_call(["git", "update-ref", ref, newvalue]) |
|
|
|
|
|
def gitDeleteRef(ref): |
|
subprocess.check_call(["git", "update-ref", "-d", ref]) |
|
|
|
|
|
_gitConfig = {} |
|
|
|
|
|
def gitConfig(key, typeSpecifier=None): |
|
if key not in _gitConfig: |
|
cmd = ["git", "config"] |
|
if typeSpecifier: |
|
cmd += [typeSpecifier] |
|
cmd += [key] |
|
s = read_pipe(cmd, ignore_error=True) |
|
_gitConfig[key] = s.strip() |
|
return _gitConfig[key] |
|
|
|
|
|
def gitConfigBool(key): |
|
"""Return a bool, using git config --bool. It is True only if the |
|
variable is set to true, and False if set to false or not present |
|
in the config. |
|
""" |
|
|
|
if key not in _gitConfig: |
|
_gitConfig[key] = gitConfig(key, '--bool') == "true" |
|
return _gitConfig[key] |
|
|
|
|
|
def gitConfigInt(key): |
|
if key not in _gitConfig: |
|
cmd = ["git", "config", "--int", key] |
|
s = read_pipe(cmd, ignore_error=True) |
|
v = s.strip() |
|
try: |
|
_gitConfig[key] = int(gitConfig(key, '--int')) |
|
except ValueError: |
|
_gitConfig[key] = None |
|
return _gitConfig[key] |
|
|
|
|
|
def gitConfigList(key): |
|
if key not in _gitConfig: |
|
s = read_pipe(["git", "config", "--get-all", key], ignore_error=True) |
|
_gitConfig[key] = s.strip().splitlines() |
|
if _gitConfig[key] == ['']: |
|
_gitConfig[key] = [] |
|
return _gitConfig[key] |
|
|
|
def fullP4Ref(incomingRef, importIntoRemotes=True): |
|
"""Standardize a given provided p4 ref value to a full git ref: |
|
refs/foo/bar/branch -> use it exactly |
|
p4/branch -> prepend refs/remotes/ or refs/heads/ |
|
branch -> prepend refs/remotes/p4/ or refs/heads/p4/""" |
|
if incomingRef.startswith("refs/"): |
|
return incomingRef |
|
if importIntoRemotes: |
|
prepend = "refs/remotes/" |
|
else: |
|
prepend = "refs/heads/" |
|
if not incomingRef.startswith("p4/"): |
|
prepend += "p4/" |
|
return prepend + incomingRef |
|
|
|
def shortP4Ref(incomingRef, importIntoRemotes=True): |
|
"""Standardize to a "short ref" if possible: |
|
refs/foo/bar/branch -> ignore |
|
refs/remotes/p4/branch or refs/heads/p4/branch -> shorten |
|
p4/branch -> shorten""" |
|
if importIntoRemotes: |
|
longprefix = "refs/remotes/p4/" |
|
else: |
|
longprefix = "refs/heads/p4/" |
|
if incomingRef.startswith(longprefix): |
|
return incomingRef[len(longprefix):] |
|
if incomingRef.startswith("p4/"): |
|
return incomingRef[3:] |
|
return incomingRef |
|
|
|
def p4BranchesInGit(branchesAreInRemotes=True): |
|
"""Find all the branches whose names start with "p4/", looking |
|
in remotes or heads as specified by the argument. Return |
|
a dictionary of { branch: revision } for each one found. |
|
The branch names are the short names, without any |
|
"p4/" prefix. |
|
""" |
|
|
|
branches = {} |
|
|
|
cmdline = ["git", "rev-parse", "--symbolic"] |
|
if branchesAreInRemotes: |
|
cmdline.append("--remotes") |
|
else: |
|
cmdline.append("--branches") |
|
|
|
for line in read_pipe_lines(cmdline): |
|
line = line.strip() |
|
|
|
# only import to p4/ |
|
if not line.startswith('p4/'): |
|
continue |
|
# special symbolic ref to p4/master |
|
if line == "p4/HEAD": |
|
continue |
|
|
|
# strip off p4/ prefix |
|
branch = line[len("p4/"):] |
|
|
|
branches[branch] = parseRevision(line) |
|
|
|
return branches |
|
|
|
|
|
def branch_exists(branch): |
|
"""Make sure that the given ref name really exists.""" |
|
|
|
cmd = ["git", "rev-parse", "--symbolic", "--verify", branch] |
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
out, _ = p.communicate() |
|
out = decode_text_stream(out) |
|
if p.returncode: |
|
return False |
|
# expect exactly one line of output: the branch name |
|
return out.rstrip() == branch |
|
|
|
|
|
def findUpstreamBranchPoint(head="HEAD"): |
|
branches = p4BranchesInGit() |
|
# map from depot-path to branch name |
|
branchByDepotPath = {} |
|
for branch in branches.keys(): |
|
tip = branches[branch] |
|
log = extractLogMessageFromGitCommit(tip) |
|
settings = extractSettingsGitLog(log) |
|
if "depot-paths" in settings: |
|
git_branch = "remotes/p4/" + branch |
|
paths = ",".join(settings["depot-paths"]) |
|
branchByDepotPath[paths] = git_branch |
|
if "change" in settings: |
|
paths = paths + ";" + settings["change"] |
|
branchByDepotPath[paths] = git_branch |
|
|
|
settings = None |
|
parent = 0 |
|
while parent < 65535: |
|
commit = head + "~%s" % parent |
|
log = extractLogMessageFromGitCommit(commit) |
|
settings = extractSettingsGitLog(log) |
|
if "depot-paths" in settings: |
|
paths = ",".join(settings["depot-paths"]) |
|
if "change" in settings: |
|
expaths = paths + ";" + settings["change"] |
|
if expaths in branchByDepotPath: |
|
return [branchByDepotPath[expaths], settings] |
|
if paths in branchByDepotPath: |
|
return [branchByDepotPath[paths], settings] |
|
|
|
parent = parent + 1 |
|
|
|
return ["", settings] |
|
|
|
|
|
def createOrUpdateBranchesFromOrigin(localRefPrefix="refs/remotes/p4/", silent=True): |
|
if not silent: |
|
print("Creating/updating branch(es) in %s based on origin branch(es)" |
|
% localRefPrefix) |
|
|
|
originPrefix = "origin/p4/" |
|
|
|
for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]): |
|
line = line.strip() |
|
if (not line.startswith(originPrefix)) or line.endswith("HEAD"): |
|
continue |
|
|
|
headName = line[len(originPrefix):] |
|
remoteHead = localRefPrefix + headName |
|
originHead = line |
|
|
|
original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) |
|
if 'depot-paths' not in original or 'change' not in original: |
|
continue |
|
|
|
update = False |
|
if not gitBranchExists(remoteHead): |
|
if verbose: |
|
print("creating %s" % remoteHead) |
|
update = True |
|
else: |
|
settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) |
|
if 'change' in settings: |
|
if settings['depot-paths'] == original['depot-paths']: |
|
originP4Change = int(original['change']) |
|
p4Change = int(settings['change']) |
|
if originP4Change > p4Change: |
|
print("%s (%s) is newer than %s (%s). " |
|
"Updating p4 branch from origin." |
|
% (originHead, originP4Change, |
|
remoteHead, p4Change)) |
|
update = True |
|
else: |
|
print("Ignoring: %s was imported from %s while " |
|
"%s was imported from %s" |
|
% (originHead, ','.join(original['depot-paths']), |
|
remoteHead, ','.join(settings['depot-paths']))) |
|
|
|
if update: |
|
system(["git", "update-ref", remoteHead, originHead]) |
|
|
|
|
|
def originP4BranchesExist(): |
|
return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master") |
|
|
|
|
|
def p4ParseNumericChangeRange(parts): |
|
changeStart = int(parts[0][1:]) |
|
if parts[1] == '#head': |
|
changeEnd = p4_last_change() |
|
else: |
|
changeEnd = int(parts[1]) |
|
|
|
return (changeStart, changeEnd) |
|
|
|
|
|
def chooseBlockSize(blockSize): |
|
if blockSize: |
|
return blockSize |
|
else: |
|
return defaultBlockSize |
|
|
|
|
|
def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): |
|
assert depotPaths |
|
|
|
# Parse the change range into start and end. Try to find integer |
|
# revision ranges as these can be broken up into blocks to avoid |
|
# hitting server-side limits (maxrows, maxscanresults). But if |
|
# that doesn't work, fall back to using the raw revision specifier |
|
# strings, without using block mode. |
|
|
|
if changeRange is None or changeRange == '': |
|
changeStart = 1 |
|
changeEnd = p4_last_change() |
|
block_size = chooseBlockSize(requestedBlockSize) |
|
else: |
|
parts = changeRange.split(',') |
|
assert len(parts) == 2 |
|
try: |
|
changeStart, changeEnd = p4ParseNumericChangeRange(parts) |
|
block_size = chooseBlockSize(requestedBlockSize) |
|
except ValueError: |
|
changeStart = parts[0][1:] |
|
changeEnd = parts[1] |
|
if requestedBlockSize: |
|
die("cannot use --changes-block-size with non-numeric revisions") |
|
block_size = None |
|
|
|
changes = set() |
|
|
|
# Retrieve changes a block at a time, to prevent running |
|
# into a MaxResults/MaxScanRows error from the server. If |
|
# we _do_ hit one of those errors, turn down the block size |
|
|
|
while True: |
|
cmd = ['changes'] |
|
|
|
if block_size: |
|
end = min(changeEnd, changeStart + block_size) |
|
revisionRange = "%d,%d" % (changeStart, end) |
|
else: |
|
revisionRange = "%s,%s" % (changeStart, changeEnd) |
|
|
|
for p in depotPaths: |
|
cmd += ["%s...@%s" % (p, revisionRange)] |
|
|
|
# fetch the changes |
|
try: |
|
result = p4CmdList(cmd, errors_as_exceptions=True) |
|
except P4RequestSizeException as e: |
|
if not block_size: |
|
block_size = e.limit |
|
elif block_size > e.limit: |
|
block_size = e.limit |
|
else: |
|
block_size = max(2, block_size // 2) |
|
|
|
if verbose: |
|
print("block size error, retrying with block size {0}".format(block_size)) |
|
continue |
|
except P4Exception as e: |
|
die('Error retrieving changes description ({0})'.format(e.p4ExitCode)) |
|
|
|
# Insert changes in chronological order |
|
for entry in reversed(result): |
|
if 'change' not in entry: |
|
continue |
|
changes.add(int(entry['change'])) |
|
|
|
if not block_size: |
|
break |
|
|
|
if end >= changeEnd: |
|
break |
|
|
|
changeStart = end + 1 |
|
|
|
changes = sorted(changes) |
|
return changes |
|
|
|
|
|
def p4PathStartsWith(path, prefix): |
|
"""This method tries to remedy a potential mixed-case issue: |
|
|
|
If UserA adds //depot/DirA/file1 |
|
and UserB adds //depot/dira/file2 |
|
|
|
we may or may not have a problem. If you have core.ignorecase=true, |
|
we treat DirA and dira as the same directory. |
|
""" |
|
if gitConfigBool("core.ignorecase"): |
|
return path.lower().startswith(prefix.lower()) |
|
return path.startswith(prefix) |
|
|
|
|
|
def getClientSpec(): |
|
"""Look at the p4 client spec, create a View() object that contains |
|
all the mappings, and return it. |
|
""" |
|
|
|
specList = p4CmdList(["client", "-o"]) |
|
if len(specList) != 1: |
|
die('Output from "client -o" is %d lines, expecting 1' % |
|
len(specList)) |
|
|
|
# dictionary of all client parameters |
|
entry = specList[0] |
|
|
|
# the //client/ name |
|
client_name = entry["Client"] |
|
|
|
# just the keys that start with "View" |
|
view_keys = [k for k in entry.keys() if k.startswith("View")] |
|
|
|
# hold this new View |
|
view = View(client_name) |
|
|
|
# append the lines, in order, to the view |
|
for view_num in range(len(view_keys)): |
|
k = "View%d" % view_num |
|
if k not in view_keys: |
|
die("Expected view key %s missing" % k) |
|
view.append(entry[k]) |
|
|
|
return view |
|
|
|
|
|
def getClientRoot(): |
|
"""Grab the client directory.""" |
|
|
|
output = p4CmdList(["client", "-o"]) |
|
if len(output) != 1: |
|
die('Output from "client -o" is %d lines, expecting 1' % len(output)) |
|
|
|
entry = output[0] |
|
if "Root" not in entry: |
|
die('Client has no "Root"') |
|
|
|
return entry["Root"] |
|
|
|
|
|
def wildcard_decode(path): |
|
"""Decode P4 wildcards into %xx encoding |
|
|
|
P4 wildcards are not allowed in filenames. P4 complains if you simply |
|
add them, but you can force it with "-f", in which case it translates |
|
them into %xx encoding internally. |
|
""" |
|
|
|
# Search for and fix just these four characters. Do % last so |
|
# that fixing it does not inadvertently create new %-escapes. |
|
# Cannot have * in a filename in windows; untested as to |
|
# what p4 would do in such a case. |
|
if not platform.system() == "Windows": |
|
path = path.replace("%2A", "*") |
|
path = path.replace("%23", "#") \ |
|
.replace("%40", "@") \ |
|
.replace("%25", "%") |
|
return path |
|
|
|
|
|
def wildcard_encode(path): |
|
"""Encode %xx coded wildcards into P4 coding.""" |
|
|
|
# do % first to avoid double-encoding the %s introduced here |
|
path = path.replace("%", "%25") \ |
|
.replace("*", "%2A") \ |
|
.replace("#", "%23") \ |
|
.replace("@", "%40") |
|
return path |
|
|
|
|
|
def wildcard_present(path): |
|
m = re.search("[*#@%]", path) |
|
return m is not None |
|
|
|
|
|
class LargeFileSystem(object): |
|
"""Base class for large file system support.""" |
|
|
|
def __init__(self, writeToGitStream): |
|
self.largeFiles = set() |
|
self.writeToGitStream = writeToGitStream |
|
|
|
def generatePointer(self, cloneDestination, contentFile): |
|
"""Return the content of a pointer file that is stored in Git instead |
|
of the actual content. |
|
""" |
|
assert False, "Method 'generatePointer' required in " + self.__class__.__name__ |
|
|
|
def pushFile(self, localLargeFile): |
|
"""Push the actual content which is not stored in the Git repository to |
|
a server. |
|
""" |
|
assert False, "Method 'pushFile' required in " + self.__class__.__name__ |
|
|
|
def hasLargeFileExtension(self, relPath): |
|
return functools.reduce( |
|
lambda a, b: a or b, |
|
[relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')], |
|
False |
|
) |
|
|
|
def generateTempFile(self, contents): |
|
contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False) |
|
for d in contents: |
|
contentFile.write(d) |
|
contentFile.close() |
|
return contentFile.name |
|
|
|
def exceedsLargeFileThreshold(self, relPath, contents): |
|
if gitConfigInt('git-p4.largeFileThreshold'): |
|
contentsSize = sum(len(d) for d in contents) |
|
if contentsSize > gitConfigInt('git-p4.largeFileThreshold'): |
|
return True |
|
if gitConfigInt('git-p4.largeFileCompressedThreshold'): |
|
contentsSize = sum(len(d) for d in contents) |
|
if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'): |
|
return False |
|
contentTempFile = self.generateTempFile(contents) |
|
compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True) |
|
with zipfile.ZipFile(compressedContentFile, mode='w') as zf: |
|
zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED) |
|
compressedContentsSize = zf.infolist()[0].compress_size |
|
os.remove(contentTempFile) |
|
if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'): |
|
return True |
|
return False |
|
|
|
def addLargeFile(self, relPath): |
|
self.largeFiles.add(relPath) |
|
|
|
def removeLargeFile(self, relPath): |
|
self.largeFiles.remove(relPath) |
|
|
|
def isLargeFile(self, relPath): |
|
return relPath in self.largeFiles |
|
|
|
def processContent(self, git_mode, relPath, contents): |
|
"""Processes the content of git fast import. This method decides if a |
|
file is stored in the large file system and handles all necessary |
|
steps. |
|
""" |
|
if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath): |
|
contentTempFile = self.generateTempFile(contents) |
|
pointer_git_mode, contents, localLargeFile = self.generatePointer(contentTempFile) |
|
if pointer_git_mode: |
|
git_mode = pointer_git_mode |
|
if localLargeFile: |
|
# Move temp file to final location in large file system |
|
largeFileDir = os.path.dirname(localLargeFile) |
|
if not os.path.isdir(largeFileDir): |
|
os.makedirs(largeFileDir) |
|
shutil.move(contentTempFile, localLargeFile) |
|
self.addLargeFile(relPath) |
|
if gitConfigBool('git-p4.largeFilePush'): |
|
self.pushFile(localLargeFile) |
|
if verbose: |
|
sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile)) |
|
return (git_mode, contents) |
|
|
|
|
|
class MockLFS(LargeFileSystem): |
|
"""Mock large file system for testing.""" |
|
|
|
def generatePointer(self, contentFile): |
|
"""The pointer content is the original content prefixed with "pointer-". |
|
The local filename of the large file storage is derived from the |
|
file content. |
|
""" |
|
with open(contentFile, 'r') as f: |
|
content = next(f) |
|
gitMode = '100644' |
|
pointerContents = 'pointer-' + content |
|
localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1]) |
|
return (gitMode, pointerContents, localLargeFile) |
|
|
|
def pushFile(self, localLargeFile): |
|
"""The remote filename of the large file storage is the same as the |
|
local one but in a different directory. |
|
""" |
|
remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote') |
|
if not os.path.exists(remotePath): |
|
os.makedirs(remotePath) |
|
shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile))) |
|
|
|
|
|
class GitLFS(LargeFileSystem): |
|
"""Git LFS as backend for the git-p4 large file system. |
|
See https://git-lfs.github.com/ for details. |
|
""" |
|
|
|
def __init__(self, *args): |
|
LargeFileSystem.__init__(self, *args) |
|
self.baseGitAttributes = [] |
|
|
|
def generatePointer(self, contentFile): |
|
"""Generate a Git LFS pointer for the content. Return LFS Pointer file |
|
mode and content which is stored in the Git repository instead of |
|
the actual content. Return also the new location of the actual |
|
content. |
|
""" |
|
if os.path.getsize(contentFile) == 0: |
|
return (None, '', None) |
|
|
|
pointerProcess = subprocess.Popen( |
|
['git', 'lfs', 'pointer', '--file=' + contentFile], |
|
stdout=subprocess.PIPE |
|
) |
|
pointerFile = decode_text_stream(pointerProcess.stdout.read()) |
|
if pointerProcess.wait(): |
|
os.remove(contentFile) |
|
die('git-lfs pointer command failed. Did you install the extension?') |
|
|
|
# Git LFS removed the preamble in the output of the 'pointer' command |
|
# starting from version 1.2.0. Check for the preamble here to support |
|
# earlier versions. |
|
# c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43 |
|
if pointerFile.startswith('Git LFS pointer for'): |
|
pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile) |
|
|
|
oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1) |
|
# if someone use external lfs.storage ( not in local repo git ) |
|
lfs_path = gitConfig('lfs.storage') |
|
if not lfs_path: |
|
lfs_path = 'lfs' |
|
if not os.path.isabs(lfs_path): |
|
lfs_path = os.path.join(os.getcwd(), '.git', lfs_path) |
|
localLargeFile = os.path.join( |
|
lfs_path, |
|
'objects', oid[:2], oid[2:4], |
|
oid, |
|
) |
|
# LFS Spec states that pointer files should not have the executable bit set. |
|
gitMode = '100644' |
|
return (gitMode, pointerFile, localLargeFile) |
|
|
|
def pushFile(self, localLargeFile): |
|
uploadProcess = subprocess.Popen( |
|
['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)] |
|
) |
|
if uploadProcess.wait(): |
|
die('git-lfs push command failed. Did you define a remote?') |
|
|
|
def generateGitAttributes(self): |
|
return ( |
|
self.baseGitAttributes + |
|
[ |
|
'\n', |
|
'#\n', |
|
'# Git LFS (see https://git-lfs.github.com/)\n', |
|
'#\n', |
|
] + |
|
['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n' |
|
for f in sorted(gitConfigList('git-p4.largeFileExtensions')) |
|
] + |
|
['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n' |
|
for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f) |
|
] |
|
) |
|
|
|
def addLargeFile(self, relPath): |
|
LargeFileSystem.addLargeFile(self, relPath) |
|
self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes()) |
|
|
|
def removeLargeFile(self, relPath): |
|
LargeFileSystem.removeLargeFile(self, relPath) |
|
self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes()) |
|
|
|
def processContent(self, git_mode, relPath, contents): |
|
if relPath == '.gitattributes': |
|
self.baseGitAttributes = contents |
|
return (git_mode, self.generateGitAttributes()) |
|
else: |
|
return LargeFileSystem.processContent(self, git_mode, relPath, contents) |
|
|
|
|
|
class Command: |
|
delete_actions = ("delete", "move/delete", "purge") |
|
add_actions = ("add", "branch", "move/add") |
|
|
|
def __init__(self): |
|
self.usage = "usage: %prog [options]" |
|
self.needsGit = True |
|
self.verbose = False |
|
|
|
# This is required for the "append" update_shelve action |
|
def ensure_value(self, attr, value): |
|
if not hasattr(self, attr) or getattr(self, attr) is None: |
|
setattr(self, attr, value) |
|
return getattr(self, attr) |
|
|
|
|
|
class P4UserMap: |
|
def __init__(self): |
|
self.userMapFromPerforceServer = False |
|
self.myP4UserId = None |
|
|
|
def p4UserId(self): |
|
if self.myP4UserId: |
|
return self.myP4UserId |
|
|
|
results = p4CmdList(["user", "-o"]) |
|
for r in results: |
|
if 'User' in r: |
|
self.myP4UserId = r['User'] |
|
return r['User'] |
|
die("Could not find your p4 user id") |
|
|
|
def p4UserIsMe(self, p4User): |
|
"""Return True if the given p4 user is actually me.""" |
|
me = self.p4UserId() |
|
if not p4User or p4User != me: |
|
return False |
|
else: |
|
return True |
|
|
|
def getUserCacheFilename(self): |
|
home = os.environ.get("HOME", os.environ.get("USERPROFILE")) |
|
return home + "/.gitp4-usercache.txt" |
|
|
|
def getUserMapFromPerforceServer(self): |
|
if self.userMapFromPerforceServer: |
|
return |
|
self.users = {} |
|
self.emails = {} |
|
|
|
for output in p4CmdList(["users"]): |
|
if "User" not in output: |
|
continue |
|
# "FullName" is bytes. "Email" on the other hand might be bytes |
|
# or unicode string depending on whether we are running under |
|
# python2 or python3. To support |
|
# git-p4.metadataDecodingStrategy=fallback, self.users dict values |
|
# are always bytes, ready to be written to git. |
|
emailbytes = metadata_stream_to_writable_bytes(output["Email"]) |
|
self.users[output["User"]] = output["FullName"] + b" <" + emailbytes + b">" |
|
self.emails[output["Email"]] = output["User"] |
|
|
|
mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE) |
|
for mapUserConfig in gitConfigList("git-p4.mapUser"): |
|
mapUser = mapUserConfigRegex.findall(mapUserConfig) |
|
if mapUser and len(mapUser[0]) == 3: |
|
user = mapUser[0][0] |
|
fullname = mapUser[0][1] |
|
email = mapUser[0][2] |
|
fulluser = fullname + " <" + email + ">" |
|
self.users[user] = metadata_stream_to_writable_bytes(fulluser) |
|
self.emails[email] = user |
|
|
|
s = b'' |
|
for (key, val) in self.users.items(): |
|
keybytes = metadata_stream_to_writable_bytes(key) |
|
s += b"%s\t%s\n" % (keybytes.expandtabs(1), val.expandtabs(1)) |
|
|
|
open(self.getUserCacheFilename(), 'wb').write(s) |
|
self.userMapFromPerforceServer = True |
|
|
|
def loadUserMapFromCache(self): |
|
self.users = {} |
|
self.userMapFromPerforceServer = False |
|
try: |
|
cache = open(self.getUserCacheFilename(), 'rb') |
|
lines = cache.readlines() |
|
cache.close() |
|
for line in lines: |
|
entry = line.strip().split(b"\t") |
|
self.users[entry[0].decode('utf_8')] = entry[1] |
|
except IOError: |
|
self.getUserMapFromPerforceServer() |
|
|
|
|
|
class P4Submit(Command, P4UserMap): |
|
|
|
conflict_behavior_choices = ("ask", "skip", "quit") |
|
|
|
def __init__(self): |
|
Command.__init__(self) |
|
P4UserMap.__init__(self) |
|
self.options = [ |
|
optparse.make_option("--origin", dest="origin"), |
|
optparse.make_option("-M", dest="detectRenames", action="store_true"), |
|
# preserve the user, requires relevant p4 permissions |
|
optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), |
|
optparse.make_option("--export-labels", dest="exportLabels", action="store_true"), |
|
optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"), |
|
optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"), |
|
optparse.make_option("--conflict", dest="conflict_behavior", |
|
choices=self.conflict_behavior_choices), |
|
optparse.make_option("--branch", dest="branch"), |
|
optparse.make_option("--shelve", dest="shelve", action="store_true", |
|
help="Shelve instead of submit. Shelved files are reverted, " |
|
"restoring the workspace to the state before the shelve"), |
|
optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int", |
|
metavar="CHANGELIST", |
|
help="update an existing shelved changelist, implies --shelve, " |
|
"repeat in-order for multiple shelved changelists"), |
|
optparse.make_option("--commit", dest="commit", metavar="COMMIT", |
|
help="submit only the specified commit(s), one commit or xxx..xxx"), |
|
optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true", |
|
help="Disable rebase after submit is completed. Can be useful if you " |
|
"work from a local git branch that is not master"), |
|
optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true", |
|
help="Skip Perforce sync of p4/master after submit or shelve"), |
|
optparse.make_option("--no-verify", dest="no_verify", action="store_true", |
|
help="Bypass p4-pre-submit and p4-changelist hooks"), |
|
] |
|
self.description = """Submit changes from git to the perforce depot.\n |
|
The `p4-pre-submit` hook is executed if it exists and is executable. It |
|
can be bypassed with the `--no-verify` command line option. The hook takes |
|
no parameters and nothing from standard input. Exiting with a non-zero status |
|
from this script prevents `git-p4 submit` from launching. |
|
|
|
One usage scenario is to run unit tests in the hook. |
|
|
|
The `p4-prepare-changelist` hook is executed right after preparing the default |
|
changelist message and before the editor is started. It takes one parameter, |
|
the name of the file that contains the changelist text. Exiting with a non-zero |
|
status from the script will abort the process. |
|
|
|
The purpose of the hook is to edit the message file in place, and it is not |
|
supressed by the `--no-verify` option. This hook is called even if |
|
`--prepare-p4-only` is set. |
|
|
|
The `p4-changelist` hook is executed after the changelist message has been |
|
edited by the user. It can be bypassed with the `--no-verify` option. It |
|
takes a single parameter, the name of the file that holds the proposed |
|
changelist text. Exiting with a non-zero status causes the command to abort. |
|
|
|
The hook is allowed to edit the changelist file and can be used to normalize |
|
the text into some project standard format. It can also be used to refuse the |
|
Submit after inspect the message file. |
|
|
|
The `p4-post-changelist` hook is invoked after the submit has successfully |
|
occurred in P4. It takes no parameters and is meant primarily for notification |
|
and cannot affect the outcome of the git p4 submit action. |
|
""" |
|
|
|
self.usage += " [name of git branch to submit into perforce depot]" |
|
self.origin = "" |
|
self.detectRenames = False |
|
self.preserveUser = gitConfigBool("git-p4.preserveUser") |
|
self.dry_run = False |
|
self.shelve = False |
|
self.update_shelve = list() |
|
self.commit = "" |
|
self.disable_rebase = gitConfigBool("git-p4.disableRebase") |
|
self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync") |
|
self.prepare_p4_only = False |
|
self.conflict_behavior = None |
|
self.isWindows = (platform.system() == "Windows") |
|
self.exportLabels = False |
|
self.p4HasMoveCommand = p4_has_move_command() |
|
self.branch = None |
|
self.no_verify = False |
|
|
|
if gitConfig('git-p4.largeFileSystem'): |
|
die("Large file system not supported for git-p4 submit command. Please remove it from config.") |
|
|
|
def check(self): |
|
if len(p4CmdList(["opened", "..."])) > 0: |
|
die("You have files opened with perforce! Close them before starting the sync.") |
|
|
|
def separate_jobs_from_description(self, message): |
|
"""Extract and return a possible Jobs field in the commit message. It |
|
goes into a separate section in the p4 change specification. |
|
|
|
A jobs line starts with "Jobs:" and looks like a new field in a |
|
form. Values are white-space separated on the same line or on |
|
following lines that start with a tab. |
|
|
|
This does not parse and extract the full git commit message like a |
|
p4 form. It just sees the Jobs: line as a marker to pass everything |
|
from then on directly into the p4 form, but outside the description |
|
section. |
|
|
|
Return a tuple (stripped log message, jobs string). |
|
""" |
|
|
|
m = re.search(r'^Jobs:', message, re.MULTILINE) |
|
if m is None: |
|
return (message, None) |
|
|
|
jobtext = message[m.start():] |
|
stripped_message = message[:m.start()].rstrip() |
|
return (stripped_message, jobtext) |
|
|
|
def prepareLogMessage(self, template, message, jobs): |
|
"""Edits the template returned from "p4 change -o" to insert the |
|
message in the Description field, and the jobs text in the Jobs |
|
field. |
|
""" |
|
result = "" |
|
|
|
inDescriptionSection = False |
|
|
|
for line in template.split("\n"): |
|
if line.startswith("#"): |
|
result += line + "\n" |
|
continue |
|
|
|
if inDescriptionSection: |
|
if line.startswith("Files:") or line.startswith("Jobs:"): |
|
inDescriptionSection = False |
|
# insert Jobs section |
|
if jobs: |
|
result += jobs + "\n" |
|
else: |
|
continue |
|
else: |
|
if line.startswith("Description:"): |
|
inDescriptionSection = True |
|
line += "\n" |
|
for messageLine in message.split("\n"): |
|
line += "\t" + messageLine + "\n" |
|
|
|
result += line + "\n" |
|
|
|
return result |
|
|
|
def patchRCSKeywords(self, file, regexp): |
|
"""Attempt to zap the RCS keywords in a p4 controlled file matching the |
|
given regex. |
|
""" |
|
handle, outFileName = tempfile.mkstemp(dir='.') |
|
try: |
|
with os.fdopen(handle, "wb") as outFile, open(file, "rb") as inFile: |
|
for line in inFile.readlines(): |
|
outFile.write(regexp.sub(br'$\1$', line)) |
|
# Forcibly overwrite the original file |
|
os.unlink(file) |
|
shutil.move(outFileName, file) |
|
except: |
|
# cleanup our temporary file |
|
os.unlink(outFileName) |
|
print("Failed to strip RCS keywords in %s" % file) |
|
raise |
|
|
|
print("Patched up RCS keywords in %s" % file) |
|
|
|
def p4UserForCommit(self, id): |
|
"""Return the tuple (perforce user,git email) for a given git commit |
|
id. |
|
""" |
|
self.getUserMapFromPerforceServer() |
|
gitEmail = read_pipe(["git", "log", "--max-count=1", |
|
"--format=%ae", id]) |
|
gitEmail = gitEmail.strip() |
|
if gitEmail not in self.emails: |
|
return (None, gitEmail) |
|
else: |
|
return (self.emails[gitEmail], gitEmail) |
|
|
|
def checkValidP4Users(self, commits): |
|
"""Check if any git authors cannot be mapped to p4 users.""" |
|
for id in commits: |
|
user, email = self.p4UserForCommit(id) |
|
if not user: |
|
msg = "Cannot find p4 user for email %s in commit %s." % (email, id) |
|
if gitConfigBool("git-p4.allowMissingP4Users"): |
|
print("%s" % msg) |
|
else: |
|
die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg) |
|
|
|
def lastP4Changelist(self): |
|
"""Get back the last changelist number submitted in this client spec. |
|
|
|
This then gets used to patch up the username in the change. If the |
|
same client spec is being used by multiple processes then this might |
|
go wrong. |
|
""" |
|
results = p4CmdList(["client", "-o"]) # find the current client |
|
client = None |
|
for r in results: |
|
if 'Client' in r: |
|
client = r['Client'] |
|
break |
|
if not client: |
|
die("could not get client spec") |
|
results = p4CmdList(["changes", "-c", client, "-m", "1"]) |
|
for r in results: |
|
if 'change' in r: |
|
return r['change'] |
|
die("Could not get changelist number for last submit - cannot patch up user details") |
|
|
|
def modifyChangelistUser(self, changelist, newUser): |
|
"""Fixup the user field of a changelist after it has been submitted.""" |
|
changes = p4CmdList(["change", "-o", changelist]) |
|
if len(changes) != 1: |
|
die("Bad output from p4 change modifying %s to user %s" % |
|
(changelist, newUser)) |
|
|
|
c = changes[0] |
|
if c['User'] == newUser: |
|
# Nothing to do |
|
return |
|
c['User'] = newUser |
|
# p4 does not understand format version 3 and above |
|
input = marshal.dumps(c, 2) |
|
|
|
result = p4CmdList(["change", "-f", "-i"], stdin=input) |
|
for r in result: |
|
if 'code' in r: |
|
if r['code'] == 'error': |
|
die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data'])) |
|
if 'data' in r: |
|
print("Updated user field for changelist %s to %s" % (changelist, newUser)) |
|
return |
|
die("Could not modify user field of changelist %s to %s" % (changelist, newUser)) |
|
|
|
def canChangeChangelists(self): |
|
"""Check to see if we have p4 admin or super-user permissions, either |
|
of which are required to modify changelists. |
|
""" |
|
results = p4CmdList(["protects", self.depotPath]) |
|
for r in results: |
|
if 'perm' in r: |
|
if r['perm'] == 'admin': |
|
return 1 |
|
if r['perm'] == 'super': |
|
return 1 |
|
return 0 |
|
|
|
def prepareSubmitTemplate(self, changelist=None): |
|
"""Run "p4 change -o" to grab a change specification template. |
|
|
|
This does not use "p4 -G", as it is nice to keep the submission |
|
template in original order, since a human might edit it. |
|
|
|
Remove lines in the Files section that show changes to files |
|
outside the depot path we're committing into. |
|
""" |
|
|
|
upstream, settings = findUpstreamBranchPoint() |
|
|
|
template = """\ |
|
# A Perforce Change Specification. |
|
# |
|
# Change: The change number. 'new' on a new changelist. |
|
# Date: The date this specification was last modified. |
|
# Client: The client on which the changelist was created. Read-only. |
|
# User: The user who created the changelist. |
|
# Status: Either 'pending' or 'submitted'. Read-only. |
|
# Type: Either 'public' or 'restricted'. Default is 'public'. |
|
# Description: Comments about the changelist. Required. |
|
# Jobs: What opened jobs are to be closed by this changelist. |
|
# You may delete jobs from this list. (New changelists only.) |
|
# Files: What opened files from the default changelist are to be added |
|
# to this changelist. You may delete files from this list. |
|
# (New changelists only.) |
|
""" |
|
files_list = [] |
|
inFilesSection = False |
|
change_entry = None |
|
args = ['change', '-o'] |
|
if changelist: |
|
args.append(str(changelist)) |
|
for entry in p4CmdList(args): |
|
if 'code' not in entry: |
|
continue |
|
if entry['code'] == 'stat': |
|
change_entry = entry |
|
break |
|
if not change_entry: |
|
die('Failed to decode output of p4 change -o') |
|
for key, value in change_entry.items(): |
|
if key.startswith('File'): |
|
if 'depot-paths' in settings: |
|
if not [p for p in settings['depot-paths'] |
|
if p4PathStartsWith(value, p)]: |
|
continue |
|
else: |
|
if not p4PathStartsWith(value, self.depotPath): |
|
continue |
|
files_list.append(value) |
|
continue |
|
# Output in the order expected by prepareLogMessage |
|
for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']: |
|
if key not in change_entry: |
|
continue |
|
template += '\n' |
|
template += key + ':' |
|
if key == 'Description': |
|
template += '\n' |
|
for field_line in change_entry[key].splitlines(): |
|
template += '\t'+field_line+'\n' |
|
if len(files_list) > 0: |
|
template += '\n' |
|
template += 'Files:\n' |
|
for path in files_list: |
|
template += '\t'+path+'\n' |
|
return template |
|
|
|
def edit_template(self, template_file): |
|
"""Invoke the editor to let the user change the submission message. |
|
|
|
Return true if okay to continue with the submit. |
|
""" |
|
|
|
# if configured to skip the editing part, just submit |
|
if gitConfigBool("git-p4.skipSubmitEdit"): |
|
return True |
|
|
|
# look at the modification time, to check later if the user saved |
|
# the file |
|
mtime = os.stat(template_file).st_mtime |
|
|
|
# invoke the editor |
|
if "P4EDITOR" in os.environ and (os.environ. |