summaryrefslogtreecommitdiff
path: root/git-p4.py
diff options
context:
space:
mode:
Diffstat (limited to 'git-p4.py')
-rwxr-xr-xgit-p4.py279
1 files changed, 222 insertions, 57 deletions
diff --git a/git-p4.py b/git-p4.py
index 1fe03cd339..d551efb0dd 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -34,6 +34,7 @@ import zipfile
import zlib
import ctypes
import errno
+import glob
# On python2.7 where raw_input() and input() are both availble,
# we want raw_input's semantics, but aliased to input for python3
@@ -165,7 +166,10 @@ def prompt(prompt_text):
"""
choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text))
while True:
- response = input(prompt_text).strip().lower()
+ sys.stderr.flush()
+ sys.stdout.write(prompt_text)
+ sys.stdout.flush()
+ response=sys.stdin.readline().strip().lower()
if not response:
continue
response = response[0]
@@ -202,6 +206,73 @@ def decode_path(path):
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."""
+ if verbose:
+ sys.stderr.write("Looking for hook: %s\n" % cmd)
+ sys.stderr.flush()
+
+ hooks_path = gitConfig("core.hooksPath")
+ if len(hooks_path) <= 0:
+ hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
+
+ if not isinstance(param, list):
+ param=[param]
+
+ # resolve hook file name, OS depdenent
+ hook_file = os.path.join(hooks_path, cmd)
+ if platform.system() == 'Windows':
+ if not os.path.isfile(hook_file):
+ # look for the file with an extension
+ files = glob.glob(hook_file + ".*")
+ if not files:
+ return True
+ files.sort()
+ hook_file = files.pop()
+ while hook_file.upper().endswith(".SAMPLE"):
+ # The file is a sample hook. We don't want it
+ if len(files) > 0:
+ hook_file = files.pop()
+ else:
+ return True
+
+ if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
+ return True
+
+ return run_hook_command(hook_file, param) == 0
+
+def run_hook_command(cmd, param):
+ """Executes a git hook command
+ cmd = the command line file to be executed. This can be
+ a file that is run by OS association.
+
+ param = a list of parameters to pass to the cmd command
+
+ On windows, the extension is checked to see if it should
+ be run with the Git for Windows Bash shell. If there
+ is no file extension, the file is deemed a bash shell
+ and will be handed off to sh.exe. Otherwise, Windows
+ will be called with the shell to handle the file assocation.
+
+ For non Windows operating systems, the file is called
+ as an executable.
+ """
+ cli = [cmd] + param
+ use_shell = False
+ if platform.system() == 'Windows':
+ (root,ext) = os.path.splitext(cmd)
+ if ext == "":
+ exe_path = os.environ.get("EXEPATH")
+ if exe_path is None:
+ exe_path = ""
+ else:
+ exe_path = os.path.join(exe_path, "bin")
+ cli = [os.path.join(exe_path, "SH.EXE")] + cli
+ else:
+ use_shell = True
+ return subprocess.call(cli, shell=use_shell)
+
+
def write_pipe(c, stdin):
if verbose:
sys.stderr.write('Writing pipe: %s\n' % str(c))
@@ -1567,13 +1638,39 @@ class P4Submit(Command, P4UserMap):
"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.
- The hook takes no parameters and nothing from standard input. Exiting with
- 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-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
+ occured 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 = ""
@@ -1591,6 +1688,7 @@ class P4Submit(Command, P4UserMap):
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.")
@@ -1978,6 +2076,9 @@ class P4Submit(Command, P4UserMap):
applyPatchCmd = patchcmd + "--check --apply -"
patch_succeeded = True
+ if verbose:
+ print("TryPatch: %s" % tryPatchCmd)
+
if os.system(tryPatchCmd) != 0:
fixed_rcs_keywords = False
patch_succeeded = False
@@ -2017,6 +2118,7 @@ class P4Submit(Command, P4UserMap):
print("Retrying the patch with RCS keywords cleaned up")
if os.system(tryPatchCmd) == 0:
patch_succeeded = True
+ print("Patch succeesed this time with RCS keywords cleaned")
if not patch_succeeded:
for f in editedFiles:
@@ -2077,55 +2179,73 @@ class P4Submit(Command, P4UserMap):
tmpFile.write(encode_text_stream(submitTemplate))
tmpFile.close()
- if self.prepare_p4_only:
- #
- # Leave the p4 tree prepared, and the submit template around
- # and let the user decide what to do next
- #
- print()
- print("P4 workspace prepared for submission.")
- print("To submit or revert, go to client workspace")
- print(" " + self.clientPath)
- print()
- print("To submit, use \"p4 submit\" to write a new description,")
- print("or \"p4 submit -i <%s\" to use the one prepared by" \
- " \"git p4\"." % fileName)
- print("You can delete the file \"%s\" when finished." % fileName)
-
- if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
- print("To preserve change ownership by user %s, you must\n" \
- "do \"p4 change -f <change>\" after submitting and\n" \
- "edit the User field.")
- if pureRenameCopy:
- print("After submitting, renamed files must be re-synced.")
- print("Invoke \"p4 sync -f\" on each of these files:")
- for f in pureRenameCopy:
- print(" " + f)
-
- print()
- print("To revert the changes, use \"p4 revert ...\", and delete")
- print("the submit template file \"%s\"" % fileName)
- if filesToAdd:
- print("Since the commit adds new files, they must be deleted:")
- for f in filesToAdd:
- print(" " + f)
- print()
- return True
-
- #
- # Let the user edit the change description, then submit it.
- #
submitted = False
try:
+ # Allow the hook to edit the changelist text before presenting it
+ # to the user.
+ if not run_git_hook("p4-prepare-changelist", [fileName]):
+ return False
+
+ if self.prepare_p4_only:
+ #
+ # Leave the p4 tree prepared, and the submit template around
+ # and let the user decide what to do next
+ #
+ submitted = True
+ print("")
+ print("P4 workspace prepared for submission.")
+ print("To submit or revert, go to client workspace")
+ print(" " + self.clientPath)
+ print("")
+ print("To submit, use \"p4 submit\" to write a new description,")
+ print("or \"p4 submit -i <%s\" to use the one prepared by" \
+ " \"git p4\"." % fileName)
+ print("You can delete the file \"%s\" when finished." % fileName)
+
+ if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
+ print("To preserve change ownership by user %s, you must\n" \
+ "do \"p4 change -f <change>\" after submitting and\n" \
+ "edit the User field.")
+ if pureRenameCopy:
+ print("After submitting, renamed files must be re-synced.")
+ print("Invoke \"p4 sync -f\" on each of these files:")
+ for f in pureRenameCopy:
+ print(" " + f)
+
+ print("")
+ print("To revert the changes, use \"p4 revert ...\", and delete")
+ print("the submit template file \"%s\"" % fileName)
+ if filesToAdd:
+ print("Since the commit adds new files, they must be deleted:")
+ for f in filesToAdd:
+ print(" " + f)
+ print("")
+ sys.stdout.flush()
+ return True
+
if self.edit_template(fileName):
+ if not self.no_verify:
+ if not run_git_hook("p4-changelist", [fileName]):
+ print("The p4-changelist hook failed.")
+ sys.stdout.flush()
+ return False
+
# read the edited message and submit
tmpFile = open(fileName, "rb")
message = decode_text_stream(tmpFile.read())
tmpFile.close()
if self.isWindows:
message = message.replace("\r\n", "\n")
- submitTemplate = message[:message.index(separatorLine)]
+ if message.find(separatorLine) != -1:
+ submitTemplate = message[:message.index(separatorLine)]
+ else:
+ submitTemplate = message
+
+ if len(submitTemplate.strip()) == 0:
+ print("Changelist is empty, aborting this changelist.")
+ sys.stdout.flush()
+ return False
if update_shelve:
p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
@@ -2148,20 +2268,23 @@ class P4Submit(Command, P4UserMap):
submitted = True
+ run_git_hook("p4-post-changelist")
finally:
- # skip this patch
+ # Revert changes if we skip this patch
if not submitted or self.shelve:
if self.shelve:
print ("Reverting shelved files.")
else:
print ("Submission cancelled, undoing p4 changes.")
+ sys.stdout.flush()
for f in editedFiles | filesToDelete:
p4_revert(f)
for f in filesToAdd:
p4_revert(f)
os.remove(f)
- os.remove(fileName)
+ if not self.prepare_p4_only:
+ os.remove(fileName)
return submitted
# Export git tags as p4 labels. Create a p4 label and then tag
@@ -2385,13 +2508,17 @@ class P4Submit(Command, P4UserMap):
sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
(len(commits), num_shelves))
- hooks_path = gitConfig("core.hooksPath")
- if len(hooks_path) <= 0:
- hooks_path = os.path.join(os.environ.get("GIT_DIR", ".git"), "hooks")
-
- hook_file = os.path.join(hooks_path, "p4-pre-submit")
- if os.path.isfile(hook_file) and os.access(hook_file, os.X_OK) and subprocess.call([hook_file]) != 0:
- sys.exit(1)
+ if not self.no_verify:
+ try:
+ if not run_git_hook("p4-pre-submit"):
+ print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip " \
+ "this pre-submission check by adding\nthe command line option '--no-verify', " \
+ "however,\nthis will also skip the p4-changelist hook as well.")
+ sys.exit(1)
+ except Exception as e:
+ print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "\
+ "with the error '{0}'".format(e.message) )
+ sys.exit(1)
#
# Apply the commits, one at a time. On failure, ask if should
@@ -3087,6 +3214,42 @@ class P4Sync(Command, P4UserMap):
print('Ignoring file outside of prefix: {0}'.format(path))
return hasPrefix
+ def findShadowedFiles(self, files, change):
+ # Perforce allows you commit files and directories with the same name,
+ # so you could have files //depot/foo and //depot/foo/bar both checked
+ # in. A p4 sync of a repository in this state fails. Deleting one of
+ # the files recovers the repository.
+ #
+ # Git will not allow the broken state to exist and only the most recent
+ # of the conflicting names is left in the repository. When one of the
+ # conflicting files is deleted we need to re-add the other one to make
+ # sure the git repository recovers in the same way as perforce.
+ deleted = [f for f in files if f['action'] in self.delete_actions]
+ to_check = set()
+ for f in deleted:
+ path = decode_path(f['path'])
+ to_check.add(path + '/...')
+ while True:
+ path = path.rsplit("/", 1)[0]
+ if path == "/" or path in to_check:
+ break
+ to_check.add(path)
+ to_check = ['%s@%s' % (wildcard_encode(p), change) for p in to_check
+ if self.hasBranchPrefix(p)]
+ if to_check:
+ stat_result = p4CmdList(["-x", "-", "fstat", "-T",
+ "depotFile,headAction,headRev,headType"], stdin=to_check)
+ for record in stat_result:
+ if record['code'] != 'stat':
+ continue
+ if record['headAction'] in self.delete_actions:
+ continue
+ files.append({
+ 'action': 'add',
+ 'path': record['depotFile'],
+ 'rev': record['headRev'],
+ 'type': record['headType']})
+
def commit(self, details, files, branch, parent = "", allow_empty=False):
epoch = details["time"]
author = details["user"]
@@ -3095,11 +3258,14 @@ class P4Sync(Command, P4UserMap):
if self.verbose:
print('commit into {0}'.format(branch))
+ files = [f for f in files
+ if self.hasBranchPrefix(decode_path(f['path']))]
+ self.findShadowedFiles(files, details['change'])
+
if self.clientSpecDirs:
self.clientSpecDirs.update_client_spec_path_cache(files)
- files = [f for (f, path) in ((f, decode_path(f['path'])) for f in files)
- if self.inClientSpec(path) and self.hasBranchPrefix(path)]
+ files = [f for f in files if self.inClientSpec(decode_path(f['path']))]
if gitConfigBool('git-p4.keepEmptyCommits'):
allow_empty = True
@@ -4205,7 +4371,6 @@ commands = {
"unshelve" : P4Unshelve,
}
-
def main():
if len(sys.argv[1:]) == 0:
printUsage(commands.keys())