Skip to content

Commit

Permalink
Merge branch 'ld/git-p4-updates'
Browse files Browse the repository at this point in the history
"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
  • Loading branch information
gitster committed Jun 18, 2018
2 parents d676cc5 + 3deed5e commit e638899
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 24 deletions.
25 changes: 25 additions & 0 deletions Documentation/git-p4.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
180 changes: 156 additions & 24 deletions git-p4.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ def __str__(self):
# 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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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']
Expand All @@ -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']))
Expand Down Expand Up @@ -1363,7 +1469,14 @@ def __init__(self):
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]"
Expand All @@ -1373,6 +1486,9 @@ def __init__(self):
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")
Expand Down Expand Up @@ -2114,9 +2230,18 @@ def run(self, args):
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
Expand Down Expand Up @@ -2224,10 +2349,14 @@ def run(self, args):
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:
Expand Down Expand Up @@ -3307,6 +3436,14 @@ def importChanges(self, changes, shelved=False, origin_revision=0):
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)

Expand Down Expand Up @@ -3385,12 +3522,7 @@ def run(self, args):
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:
Expand Down
40 changes: 40 additions & 0 deletions t/t9807-git-p4-submit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down
8 changes: 8 additions & 0 deletions t/t9818-git-p4-block.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand 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
'
Expand Down
Loading

0 comments on commit e638899

Please sign in to comment.