Merge branch 'ld/git-p4-updates'

"git p4" updates.

* ld/git-p4-updates:
  git-p4: auto-size the block
  git-p4: narrow the scope of exceptions caught when parsing an int
  git-p4: raise exceptions from p4CmdList based on error from p4 server
  git-p4: better error reporting when p4 fails
  git-p4: add option to disable syncing of p4/master with p4
  git-p4: disable-rebase: allow setting this via configuration
  git-p4: add options --commit and --disable-rebase
This commit is contained in:
Junio C Hamano 2018-06-18 10:18:41 -07:00
Родитель d676cc512a 3deed5e078
Коммит e638899470
5 изменённых файлов: 307 добавлений и 24 удалений

Просмотреть файл

@ -149,6 +149,12 @@ To specify a branch other than the current one, use:
$ git p4 submit topicbranch
------------
To specify a single commit or a range of commits, use:
------------
$ git p4 submit --commit <sha1>
$ git p4 submit --commit <sha1..sha1>
------------
The upstream reference is generally 'refs/remotes/p4/master', but can
be overridden using the `--origin=` command-line option.
@ -355,6 +361,19 @@ These options can be used to modify 'git p4 submit' behavior.
p4/master. See the "Sync options" section above for more
information.
--commit <sha1>|<sha1..sha1>::
Submit only the specified commit or range of commits, instead of the full
list of changes that are in the current Git branch.
--disable-rebase::
Disable the automatic rebase after all commits have been successfully
submitted. Can also be set with git-p4.disableRebase.
--disable-p4sync::
Disable the automatic sync of p4/master from Perforce after commits have
been submitted. Implies --disable-rebase. Can also be set with
git-p4.disableP4Sync. Sync with origin/master still goes ahead if possible.
Rebase options
~~~~~~~~~~~~~~
These options can be used to modify 'git p4 rebase' behavior.
@ -676,6 +695,12 @@ git-p4.conflict::
Specify submit behavior when a conflict with p4 is found, as per
--conflict. The default behavior is 'ask'.
git-p4.disableRebase::
Do not rebase the tree against p4/master following a submit.
git-p4.disableP4Sync::
Do not sync p4/master with Perforce following a submit. Implies git-p4.disableRebase.
IMPLEMENTATION DETAILS
----------------------
* Changesets from p4 are imported using Git fast-import.

180
git-p4.py
Просмотреть файл

@ -47,8 +47,10 @@ verbose = False
# Only labels/tags matching this will be imported/exported
defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
# Grab changes in blocks of this many revisions, unless otherwise requested
defaultBlockSize = 512
# The block size is reduced automatically if required
defaultBlockSize = 1<<20
p4_access_checked = False
def p4_build_cmd(cmd):
"""Build a suitable p4 command line.
@ -91,6 +93,13 @@ def p4_build_cmd(cmd):
real_cmd = ' '.join(real_cmd) + ' ' + cmd
else:
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):
@ -264,6 +273,52 @@ def p4_system(cmd):
if retcode:
raise 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")
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
@ -511,10 +566,30 @@ def isModeExec(mode):
# 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
def isModeExecChanged(src_mode, dst_mode):
return isModeExec(src_mode) != isModeExec(dst_mode)
def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False):
def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
errors_as_exceptions=False):
if isinstance(cmd,basestring):
cmd = "-G " + cmd
@ -561,9 +636,25 @@ def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False):
pass
exitCode = p4.wait()
if exitCode != 0:
entry = {}
entry["p4ExitCode"] = exitCode
result.append(entry)
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
@ -868,7 +959,7 @@ def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
try:
(changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
block_size = chooseBlockSize(requestedBlockSize)
except:
except ValueError:
changeStart = parts[0][1:]
changeEnd = parts[1]
if requestedBlockSize:
@ -878,7 +969,8 @@ def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
changes = set()
# Retrieve changes a block at a time, to prevent running
# into a MaxResults/MaxScanRows error from the server.
# 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']
@ -892,10 +984,24 @@ def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
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(p4CmdList(cmd)):
if entry.has_key('p4ExitCode'):
die('Error retrieving changes descriptions ({})'.format(entry['p4ExitCode']))
for entry in reversed(result):
if not entry.has_key('change'):
continue
changes.add(int(entry['change']))
@ -1363,7 +1469,14 @@ class P4Submit(Command, P4UserMap):
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")
"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"),
]
self.description = "Submit changes from git to the perforce depot."
self.usage += " [name of git branch to submit into perforce depot]"
@ -1373,6 +1486,9 @@ class P4Submit(Command, P4UserMap):
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")
@ -2114,9 +2230,18 @@ class P4Submit(Command, P4UserMap):
else:
committish = 'HEAD'
for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
commits.append(line.strip())
commits.reverse()
if self.commit != "":
if self.commit.find("..") != -1:
limits_ish = self.commit.split("..")
for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
commits.append(line.strip())
commits.reverse()
else:
commits.append(self.commit)
else:
for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
commits.append(line.strip())
commits.reverse()
if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
self.checkAuthorship = False
@ -2224,10 +2349,14 @@ class P4Submit(Command, P4UserMap):
sync = P4Sync()
if self.branch:
sync.branch = self.branch
sync.run([])
if self.disable_p4sync:
sync.sync_origin_only()
else:
sync.run([])
rebase = P4Rebase()
rebase.rebase()
if not self.disable_rebase:
rebase = P4Rebase()
rebase.rebase()
else:
if len(applied) == 0:
@ -3307,6 +3436,14 @@ class P4Sync(Command, P4UserMap):
print self.gitError.read()
sys.exit(1)
def sync_origin_only(self):
if self.syncWithOrigin:
self.hasOrigin = originP4BranchesExist()
if self.hasOrigin:
if not self.silent:
print 'Syncing with origin first, using "git fetch origin"'
system("git fetch origin")
def importHeadRevision(self, revision):
print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
@ -3385,12 +3522,7 @@ class P4Sync(Command, P4UserMap):
else:
self.refPrefix = "refs/heads/p4/"
if self.syncWithOrigin:
self.hasOrigin = originP4BranchesExist()
if self.hasOrigin:
if not self.silent:
print 'Syncing with origin first, using "git fetch origin"'
system("git fetch origin")
self.sync_origin_only()
branch_arg_given = bool(self.branch)
if len(self.branch) == 0:

Просмотреть файл

@ -155,6 +155,46 @@ test_expect_success 'allow submit from branch with same revision but different n
)
'
# make two commits, but tell it to apply only one
test_expect_success 'submit --commit one' '
test_when_finished cleanup_git &&
git p4 clone --dest="$git" //depot &&
(
cd "$git" &&
test_commit "file9" &&
test_commit "file10" &&
git config git-p4.skipSubmitEdit true &&
git p4 submit --commit HEAD
) &&
(
cd "$cli" &&
test_path_is_missing "file9.t" &&
test_path_is_file "file10.t"
)
'
# make three commits, but tell it to apply only range
test_expect_success 'submit --commit range' '
test_when_finished cleanup_git &&
git p4 clone --dest="$git" //depot &&
(
cd "$git" &&
test_commit "file11" &&
test_commit "file12" &&
test_commit "file13" &&
git config git-p4.skipSubmitEdit true &&
git p4 submit --commit HEAD~2..HEAD
) &&
(
cd "$cli" &&
test_path_is_missing "file11.t" &&
test_path_is_file "file12.t" &&
test_path_is_file "file13.t"
)
'
#
# Basic submit tests, the five handled cases
#

Просмотреть файл

@ -129,6 +129,7 @@ test_expect_success 'Create a repo with multiple depot paths' '
'
test_expect_success 'Clone repo with multiple depot paths' '
test_when_finished cleanup_git &&
(
cd "$git" &&
git p4 clone --changes-block-size=4 //depot/pathA@all //depot/pathB@all \
@ -138,6 +139,13 @@ test_expect_success 'Clone repo with multiple depot paths' '
)
'
test_expect_success 'Clone repo with self-sizing block size' '
test_when_finished cleanup_git &&
git p4 clone --changes-block-size=1000000 //depot@all --destination="$git" &&
git -C "$git" log --oneline >log &&
test_line_count \> 10 log
'
test_expect_success 'kill p4d' '
kill_p4d
'

78
t/t9833-errors.sh Executable file
Просмотреть файл

@ -0,0 +1,78 @@
#!/bin/sh
test_description='git p4 errors'
. ./lib-git-p4.sh
test_expect_success 'start p4d' '
start_p4d
'
test_expect_success 'add p4 files' '
(
cd "$cli" &&
echo file1 >file1 &&
p4 add file1 &&
p4 submit -d "file1"
)
'
# after this test, the default user requires a password
test_expect_success 'error handling' '
git p4 clone --dest="$git" //depot@all &&
(
cd "$git" &&
P4PORT=: test_must_fail git p4 submit 2>errmsg
) &&
p4 passwd -P newpassword &&
(
P4PASSWD=badpassword test_must_fail git p4 clone //depot/foo 2>errmsg &&
grep -q "failure accessing depot.*P4PASSWD" errmsg
)
'
test_expect_success 'ticket logged out' '
P4TICKETS="$cli/tickets" &&
echo "newpassword" | p4 login &&
(
cd "$git" &&
test_commit "ticket-auth-check" &&
p4 logout &&
test_must_fail git p4 submit 2>errmsg &&
grep -q "failure accessing depot" errmsg
)
'
test_expect_success 'create group with short ticket expiry' '
P4TICKETS="$cli/tickets" &&
echo "newpassword" | p4 login &&
p4_add_user short_expiry_user &&
p4 -u short_expiry_user passwd -P password &&
p4 group -i <<-EOF &&
Group: testgroup
Timeout: 3
Users: short_expiry_user
EOF
p4 users | grep short_expiry_user
'
test_expect_success 'git operation with expired ticket' '
P4TICKETS="$cli/tickets" &&
P4USER=short_expiry_user &&
echo "password" | p4 login &&
(
cd "$git" &&
git p4 sync &&
sleep 5 &&
test_must_fail git p4 sync 2>errmsg &&
grep "failure accessing depot" errmsg
)
'
test_expect_success 'kill p4d' '
kill_p4d
'
test_done