Skip to content

Commit e638899

Browse files
committed
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
2 parents d676cc5 + 3deed5e commit e638899

File tree

5 files changed

+307
-24
lines changed

5 files changed

+307
-24
lines changed

Documentation/git-p4.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ To specify a branch other than the current one, use:
149149
$ git p4 submit topicbranch
150150
------------
151151

152+
To specify a single commit or a range of commits, use:
153+
------------
154+
$ git p4 submit --commit <sha1>
155+
$ git p4 submit --commit <sha1..sha1>
156+
------------
157+
152158
The upstream reference is generally 'refs/remotes/p4/master', but can
153159
be overridden using the `--origin=` command-line option.
154160

@@ -355,6 +361,19 @@ These options can be used to modify 'git p4 submit' behavior.
355361
p4/master. See the "Sync options" section above for more
356362
information.
357363

364+
--commit <sha1>|<sha1..sha1>::
365+
Submit only the specified commit or range of commits, instead of the full
366+
list of changes that are in the current Git branch.
367+
368+
--disable-rebase::
369+
Disable the automatic rebase after all commits have been successfully
370+
submitted. Can also be set with git-p4.disableRebase.
371+
372+
--disable-p4sync::
373+
Disable the automatic sync of p4/master from Perforce after commits have
374+
been submitted. Implies --disable-rebase. Can also be set with
375+
git-p4.disableP4Sync. Sync with origin/master still goes ahead if possible.
376+
358377
Rebase options
359378
~~~~~~~~~~~~~~
360379
These options can be used to modify 'git p4 rebase' behavior.
@@ -676,6 +695,12 @@ git-p4.conflict::
676695
Specify submit behavior when a conflict with p4 is found, as per
677696
--conflict. The default behavior is 'ask'.
678697

698+
git-p4.disableRebase::
699+
Do not rebase the tree against p4/master following a submit.
700+
701+
git-p4.disableP4Sync::
702+
Do not sync p4/master with Perforce following a submit. Implies git-p4.disableRebase.
703+
679704
IMPLEMENTATION DETAILS
680705
----------------------
681706
* Changesets from p4 are imported using Git fast-import.

git-p4.py

Lines changed: 156 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ def __str__(self):
4747
# Only labels/tags matching this will be imported/exported
4848
defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
4949

50-
# Grab changes in blocks of this many revisions, unless otherwise requested
51-
defaultBlockSize = 512
50+
# The block size is reduced automatically if required
51+
defaultBlockSize = 1<<20
52+
53+
p4_access_checked = False
5254

5355
def p4_build_cmd(cmd):
5456
"""Build a suitable p4 command line.
@@ -91,6 +93,13 @@ def p4_build_cmd(cmd):
9193
real_cmd = ' '.join(real_cmd) + ' ' + cmd
9294
else:
9395
real_cmd += cmd
96+
97+
# now check that we can actually talk to the server
98+
global p4_access_checked
99+
if not p4_access_checked:
100+
p4_access_checked = True # suppress access checks in p4_check_access itself
101+
p4_check_access()
102+
94103
return real_cmd
95104

96105
def git_dir(path):
@@ -264,6 +273,52 @@ def p4_system(cmd):
264273
if retcode:
265274
raise CalledProcessError(retcode, real_cmd)
266275

276+
def die_bad_access(s):
277+
die("failure accessing depot: {0}".format(s.rstrip()))
278+
279+
def p4_check_access(min_expiration=1):
280+
""" Check if we can access Perforce - account still logged in
281+
"""
282+
results = p4CmdList(["login", "-s"])
283+
284+
if len(results) == 0:
285+
# should never get here: always get either some results, or a p4ExitCode
286+
assert("could not parse response from perforce")
287+
288+
result = results[0]
289+
290+
if 'p4ExitCode' in result:
291+
# p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
292+
die_bad_access("could not run p4")
293+
294+
code = result.get("code")
295+
if not code:
296+
# we get here if we couldn't connect and there was nothing to unmarshal
297+
die_bad_access("could not connect")
298+
299+
elif code == "stat":
300+
expiry = result.get("TicketExpiration")
301+
if expiry:
302+
expiry = int(expiry)
303+
if expiry > min_expiration:
304+
# ok to carry on
305+
return
306+
else:
307+
die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
308+
309+
else:
310+
# account without a timeout - all ok
311+
return
312+
313+
elif code == "error":
314+
data = result.get("data")
315+
if data:
316+
die_bad_access("p4 error: {0}".format(data))
317+
else:
318+
die_bad_access("unknown error")
319+
else:
320+
die_bad_access("unknown error code {0}".format(code))
321+
267322
_p4_version_string = None
268323
def p4_version_string():
269324
"""Read the version string, showing just the last line, which
@@ -511,10 +566,30 @@ def isModeExec(mode):
511566
# otherwise False.
512567
return mode[-3:] == "755"
513568

569+
class P4Exception(Exception):
570+
""" Base class for exceptions from the p4 client """
571+
def __init__(self, exit_code):
572+
self.p4ExitCode = exit_code
573+
574+
class P4ServerException(P4Exception):
575+
""" Base class for exceptions where we get some kind of marshalled up result from the server """
576+
def __init__(self, exit_code, p4_result):
577+
super(P4ServerException, self).__init__(exit_code)
578+
self.p4_result = p4_result
579+
self.code = p4_result[0]['code']
580+
self.data = p4_result[0]['data']
581+
582+
class P4RequestSizeException(P4ServerException):
583+
""" One of the maxresults or maxscanrows errors """
584+
def __init__(self, exit_code, p4_result, limit):
585+
super(P4RequestSizeException, self).__init__(exit_code, p4_result)
586+
self.limit = limit
587+
514588
def isModeExecChanged(src_mode, dst_mode):
515589
return isModeExec(src_mode) != isModeExec(dst_mode)
516590

517-
def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False):
591+
def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
592+
errors_as_exceptions=False):
518593

519594
if isinstance(cmd,basestring):
520595
cmd = "-G " + cmd
@@ -561,9 +636,25 @@ def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False):
561636
pass
562637
exitCode = p4.wait()
563638
if exitCode != 0:
564-
entry = {}
565-
entry["p4ExitCode"] = exitCode
566-
result.append(entry)
639+
if errors_as_exceptions:
640+
if len(result) > 0:
641+
data = result[0].get('data')
642+
if data:
643+
m = re.search('Too many rows scanned \(over (\d+)\)', data)
644+
if not m:
645+
m = re.search('Request too large \(over (\d+)\)', data)
646+
647+
if m:
648+
limit = int(m.group(1))
649+
raise P4RequestSizeException(exitCode, result, limit)
650+
651+
raise P4ServerException(exitCode, result)
652+
else:
653+
raise P4Exception(exitCode)
654+
else:
655+
entry = {}
656+
entry["p4ExitCode"] = exitCode
657+
result.append(entry)
567658

568659
return result
569660

@@ -868,7 +959,7 @@ def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
868959
try:
869960
(changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
870961
block_size = chooseBlockSize(requestedBlockSize)
871-
except:
962+
except ValueError:
872963
changeStart = parts[0][1:]
873964
changeEnd = parts[1]
874965
if requestedBlockSize:
@@ -878,7 +969,8 @@ def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
878969
changes = set()
879970

880971
# Retrieve changes a block at a time, to prevent running
881-
# into a MaxResults/MaxScanRows error from the server.
972+
# into a MaxResults/MaxScanRows error from the server. If
973+
# we _do_ hit one of those errors, turn down the block size
882974

883975
while True:
884976
cmd = ['changes']
@@ -892,10 +984,24 @@ def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
892984
for p in depotPaths:
893985
cmd += ["%s...@%s" % (p, revisionRange)]
894986

987+
# fetch the changes
988+
try:
989+
result = p4CmdList(cmd, errors_as_exceptions=True)
990+
except P4RequestSizeException as e:
991+
if not block_size:
992+
block_size = e.limit
993+
elif block_size > e.limit:
994+
block_size = e.limit
995+
else:
996+
block_size = max(2, block_size // 2)
997+
998+
if verbose: print("block size error, retrying with block size {0}".format(block_size))
999+
continue
1000+
except P4Exception as e:
1001+
die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1002+
8951003
# Insert changes in chronological order
896-
for entry in reversed(p4CmdList(cmd)):
897-
if entry.has_key('p4ExitCode'):
898-
die('Error retrieving changes descriptions ({})'.format(entry['p4ExitCode']))
1004+
for entry in reversed(result):
8991005
if not entry.has_key('change'):
9001006
continue
9011007
changes.add(int(entry['change']))
@@ -1363,7 +1469,14 @@ def __init__(self):
13631469
optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
13641470
metavar="CHANGELIST",
13651471
help="update an existing shelved changelist, implies --shelve, "
1366-
"repeat in-order for multiple shelved changelists")
1472+
"repeat in-order for multiple shelved changelists"),
1473+
optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1474+
help="submit only the specified commit(s), one commit or xxx..xxx"),
1475+
optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1476+
help="Disable rebase after submit is completed. Can be useful if you "
1477+
"work from a local git branch that is not master"),
1478+
optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1479+
help="Skip Perforce sync of p4/master after submit or shelve"),
13671480
]
13681481
self.description = "Submit changes from git to the perforce depot."
13691482
self.usage += " [name of git branch to submit into perforce depot]"
@@ -1373,6 +1486,9 @@ def __init__(self):
13731486
self.dry_run = False
13741487
self.shelve = False
13751488
self.update_shelve = list()
1489+
self.commit = ""
1490+
self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1491+
self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
13761492
self.prepare_p4_only = False
13771493
self.conflict_behavior = None
13781494
self.isWindows = (platform.system() == "Windows")
@@ -2114,9 +2230,18 @@ def run(self, args):
21142230
else:
21152231
committish = 'HEAD'
21162232

2117-
for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2118-
commits.append(line.strip())
2119-
commits.reverse()
2233+
if self.commit != "":
2234+
if self.commit.find("..") != -1:
2235+
limits_ish = self.commit.split("..")
2236+
for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2237+
commits.append(line.strip())
2238+
commits.reverse()
2239+
else:
2240+
commits.append(self.commit)
2241+
else:
2242+
for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2243+
commits.append(line.strip())
2244+
commits.reverse()
21202245

21212246
if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
21222247
self.checkAuthorship = False
@@ -2224,10 +2349,14 @@ def run(self, args):
22242349
sync = P4Sync()
22252350
if self.branch:
22262351
sync.branch = self.branch
2227-
sync.run([])
2352+
if self.disable_p4sync:
2353+
sync.sync_origin_only()
2354+
else:
2355+
sync.run([])
22282356

2229-
rebase = P4Rebase()
2230-
rebase.rebase()
2357+
if not self.disable_rebase:
2358+
rebase = P4Rebase()
2359+
rebase.rebase()
22312360

22322361
else:
22332362
if len(applied) == 0:
@@ -3307,6 +3436,14 @@ def importChanges(self, changes, shelved=False, origin_revision=0):
33073436
print self.gitError.read()
33083437
sys.exit(1)
33093438

3439+
def sync_origin_only(self):
3440+
if self.syncWithOrigin:
3441+
self.hasOrigin = originP4BranchesExist()
3442+
if self.hasOrigin:
3443+
if not self.silent:
3444+
print 'Syncing with origin first, using "git fetch origin"'
3445+
system("git fetch origin")
3446+
33103447
def importHeadRevision(self, revision):
33113448
print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
33123449

@@ -3385,12 +3522,7 @@ def run(self, args):
33853522
else:
33863523
self.refPrefix = "refs/heads/p4/"
33873524

3388-
if self.syncWithOrigin:
3389-
self.hasOrigin = originP4BranchesExist()
3390-
if self.hasOrigin:
3391-
if not self.silent:
3392-
print 'Syncing with origin first, using "git fetch origin"'
3393-
system("git fetch origin")
3525+
self.sync_origin_only()
33943526

33953527
branch_arg_given = bool(self.branch)
33963528
if len(self.branch) == 0:

t/t9807-git-p4-submit.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,46 @@ test_expect_success 'allow submit from branch with same revision but different n
155155
)
156156
'
157157

158+
# make two commits, but tell it to apply only one
159+
160+
test_expect_success 'submit --commit one' '
161+
test_when_finished cleanup_git &&
162+
git p4 clone --dest="$git" //depot &&
163+
(
164+
cd "$git" &&
165+
test_commit "file9" &&
166+
test_commit "file10" &&
167+
git config git-p4.skipSubmitEdit true &&
168+
git p4 submit --commit HEAD
169+
) &&
170+
(
171+
cd "$cli" &&
172+
test_path_is_missing "file9.t" &&
173+
test_path_is_file "file10.t"
174+
)
175+
'
176+
177+
# make three commits, but tell it to apply only range
178+
179+
test_expect_success 'submit --commit range' '
180+
test_when_finished cleanup_git &&
181+
git p4 clone --dest="$git" //depot &&
182+
(
183+
cd "$git" &&
184+
test_commit "file11" &&
185+
test_commit "file12" &&
186+
test_commit "file13" &&
187+
git config git-p4.skipSubmitEdit true &&
188+
git p4 submit --commit HEAD~2..HEAD
189+
) &&
190+
(
191+
cd "$cli" &&
192+
test_path_is_missing "file11.t" &&
193+
test_path_is_file "file12.t" &&
194+
test_path_is_file "file13.t"
195+
)
196+
'
197+
158198
#
159199
# Basic submit tests, the five handled cases
160200
#

t/t9818-git-p4-block.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ test_expect_success 'Create a repo with multiple depot paths' '
129129
'
130130

131131
test_expect_success 'Clone repo with multiple depot paths' '
132+
test_when_finished cleanup_git &&
132133
(
133134
cd "$git" &&
134135
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' '
138139
)
139140
'
140141

142+
test_expect_success 'Clone repo with self-sizing block size' '
143+
test_when_finished cleanup_git &&
144+
git p4 clone --changes-block-size=1000000 //depot@all --destination="$git" &&
145+
git -C "$git" log --oneline >log &&
146+
test_line_count \> 10 log
147+
'
148+
141149
test_expect_success 'kill p4d' '
142150
kill_p4d
143151
'

0 commit comments

Comments
 (0)