diff options
Diffstat (limited to 'git-p4.py')
-rwxr-xr-x | git-p4.py | 220 |
1 files changed, 156 insertions, 64 deletions
@@ -25,6 +25,7 @@ import stat import zipfile import zlib import ctypes +import errno try: from subprocess import CalledProcessError @@ -78,6 +79,13 @@ def p4_build_cmd(cmd): 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)] if isinstance(cmd,basestring): real_cmd = ' '.join(real_cmd) + ' ' + cmd @@ -85,6 +93,16 @@ def p4_build_cmd(cmd): real_cmd += cmd 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. @@ -142,17 +160,42 @@ def p4_write_pipe(c, stdin): real_cmd = p4_build_cmd(c) return write_pipe(real_cmd, stdin) -def read_pipe(c, ignore_error=False): +def read_pipe_full(c): + """ Read output from command. Returns a tuple + of the return status, stdout text and stderr + text. + """ if verbose: sys.stderr.write('Reading pipe: %s\n' % str(c)) expand = isinstance(c,basestring) p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand) (out, err) = p.communicate() - if p.returncode != 0 and not ignore_error: - die('Command failed: %s\nError: %s' % (str(c), err)) + return (p.returncode, out, err) + +def read_pipe(c, ignore_error=False): + """ 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. + """ + (retcode, out, err) = read_pipe_full(c) + if retcode != 0: + if ignore_error: + out = "" + else: + die('Command failed: %s\nError: %s' % (str(c), err)) return out +def read_pipe_text(c): + """ Read output from a command with trailing whitespace stripped. + On error, returns None. + """ + (retcode, out, err) = read_pipe_full(c) + if retcode != 0: + return None + else: + return out.rstrip() + def p4_read_pipe(c, ignore_error=False): real_cmd = p4_build_cmd(c) return read_pipe(real_cmd, ignore_error) @@ -262,6 +305,10 @@ def p4_revert(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)]) @@ -555,18 +602,10 @@ def p4Where(depotPath): return clientPath def currentGitBranch(): - retcode = system(["git", "symbolic-ref", "-q", "HEAD"], ignore_error=True) - if retcode != 0: - # on a detached head - return None - else: - return read_pipe(["git", "name-rev", "HEAD"]).split(" ")[1].strip() + return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"]) def isValidGitDir(path): - if (os.path.exists(path + "/HEAD") - and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")): - return True; - return False + return git_dir(path) != None def parseRevision(ref): return read_pipe("git rev-parse %s" % ref).strip() @@ -655,7 +694,7 @@ def gitConfigInt(key): def gitConfigList(key): if not _gitConfig.has_key(key): s = read_pipe(["git", "config", "--get-all", key], ignore_error=True) - _gitConfig[key] = s.strip().split(os.linesep) + _gitConfig[key] = s.strip().splitlines() if _gitConfig[key] == ['']: _gitConfig[key] = [] return _gitConfig[key] @@ -822,7 +861,7 @@ def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): die("cannot use --changes-block-size with non-numeric revisions") block_size = None - changes = [] + changes = set() # Retrieve changes a block at a time, to prevent running # into a MaxResults/MaxScanRows error from the server. @@ -841,7 +880,7 @@ def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): # Insert changes in chronological order for line in reversed(p4_read_pipe_lines(cmd)): - changes.append(int(line.split(" ")[1])) + changes.add(int(line.split(" ")[1])) if not block_size: break @@ -1005,18 +1044,20 @@ class LargeFileSystem(object): steps.""" if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath): contentTempFile = self.generateTempFile(contents) - (git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile) - - # 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)) + (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): @@ -1056,6 +1097,9 @@ class GitLFS(LargeFileSystem): 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 @@ -1098,10 +1142,10 @@ class GitLFS(LargeFileSystem): '# Git LFS (see https://git-lfs.github.com/)\n', '#\n', ] + - ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\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 -text\n' + ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n' for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f) ] ) @@ -1289,6 +1333,12 @@ class P4Submit(Command, P4UserMap): 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="store", type="int", + metavar="CHANGELIST", + help="update an existing shelved changelist, implies --shelve") ] self.description = "Submit changes from git to the perforce depot." self.usage += " [name of git branch to submit into perforce depot]" @@ -1296,6 +1346,8 @@ class P4Submit(Command, P4UserMap): self.detectRenames = False self.preserveUser = gitConfigBool("git-p4.preserveUser") self.dry_run = False + self.shelve = False + self.update_shelve = None self.prepare_p4_only = False self.conflict_behavior = None self.isWindows = (platform.system() == "Windows") @@ -1464,7 +1516,7 @@ class P4Submit(Command, P4UserMap): return 1 return 0 - def prepareSubmitTemplate(self): + 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. @@ -1476,7 +1528,11 @@ class P4Submit(Command, P4UserMap): template = "" inFilesSection = False - for line in p4_read_pipe_lines(['change', '-o']): + args = ['change', '-o'] + if changelist: + args.append(str(changelist)) + + for line in p4_read_pipe_lines(args): if line.endswith("\r\n"): line = line[:-2] + "\n" if inFilesSection: @@ -1538,7 +1594,7 @@ class P4Submit(Command, P4UserMap): if response == 'n': return False - def get_diff_description(self, editedFiles, filesToAdd): + def get_diff_description(self, editedFiles, filesToAdd, symlinks): # diff if os.environ.has_key("P4DIFF"): del(os.environ["P4DIFF"]) @@ -1553,10 +1609,17 @@ class P4Submit(Command, P4UserMap): newdiff += "==== new file ====\n" newdiff += "--- /dev/null\n" newdiff += "+++ %s\n" % newFile - f = open(newFile, "r") - for line in f.readlines(): - newdiff += "+" + line - f.close() + + is_link = os.path.islink(newFile) + expect_link = newFile in symlinks + + if is_link and expect_link: + newdiff += "+%s\n" % os.readlink(newFile) + else: + f = open(newFile, "r") + for line in f.readlines(): + newdiff += "+" + line + f.close() return (diff + newdiff).replace('\r\n', '\n') @@ -1574,12 +1637,16 @@ class P4Submit(Command, P4UserMap): filesToDelete = set() editedFiles = set() pureRenameCopy = set() + symlinks = set() filesToChangeExecBit = {} + all_files = list() for line in diff: diff = parseDiffTreeEntry(line) modifier = diff['status'] path = diff['src'] + all_files.append(path) + if modifier == "M": p4_edit(path) if isModeExecChanged(diff['src_mode'], diff['dst_mode']): @@ -1590,6 +1657,11 @@ class P4Submit(Command, P4UserMap): filesToChangeExecBit[path] = diff['dst_mode'] if path in filesToDelete: filesToDelete.remove(path) + + dst_mode = int(diff['dst_mode'], 8) + if dst_mode == 0120000: + symlinks.add(path) + elif modifier == "D": filesToDelete.add(path) if path in filesToAdd: @@ -1705,6 +1777,10 @@ class P4Submit(Command, P4UserMap): mode = filesToChangeExecBit[f] setP4ExecBit(f, mode) + if self.update_shelve: + print("all_files = %s" % str(all_files)) + p4_reopen_in_change(self.update_shelve, all_files) + # # Build p4 change description, starting with the contents # of the git commit message. @@ -1713,7 +1789,7 @@ class P4Submit(Command, P4UserMap): logMessage = logMessage.strip() (logMessage, jobs) = self.separate_jobs_from_description(logMessage) - template = self.prepareSubmitTemplate() + template = self.prepareSubmitTemplate(self.update_shelve) submitTemplate = self.prepareLogMessage(template, logMessage, jobs) if self.preserveUser: @@ -1727,7 +1803,7 @@ class P4Submit(Command, P4UserMap): separatorLine = "######## everything below this line is just the diff #######\n" if not self.prepare_p4_only: submitTemplate += separatorLine - submitTemplate += self.get_diff_description(editedFiles, filesToAdd) + submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks) (handle, fileName) = tempfile.mkstemp() tmpFile = os.fdopen(handle, "w+b") @@ -1785,7 +1861,17 @@ class P4Submit(Command, P4UserMap): if self.isWindows: message = message.replace("\r\n", "\n") submitTemplate = message[:message.index(separatorLine)] - p4_write_pipe(['submit', '-i'], submitTemplate) + + if self.update_shelve: + p4_write_pipe(['shelve', '-r', '-i'], submitTemplate) + elif self.shelve: + p4_write_pipe(['shelve', '-i'], submitTemplate) + else: + p4_write_pipe(['submit', '-i'], submitTemplate) + # The rename/copy happened by applying a patch that created a + # new file. This leaves it writable, which confuses p4. + for f in pureRenameCopy: + p4_sync(f, "-f") if self.preserveUser: if p4User: @@ -1795,23 +1881,20 @@ class P4Submit(Command, P4UserMap): changelist = self.lastP4Changelist() self.modifyChangelistUser(changelist, p4User) - # The rename/copy happened by applying a patch that created a - # new file. This leaves it writable, which confuses p4. - for f in pureRenameCopy: - p4_sync(f, "-f") submitted = True finally: # skip this patch - if not submitted: - print "Submission cancelled, undoing p4 changes." - for f in editedFiles: + if not submitted or self.shelve: + if self.shelve: + print ("Reverting shelved files.") + else: + print ("Submission cancelled, undoing p4 changes.") + for f in editedFiles | filesToDelete: p4_revert(f) for f in filesToAdd: p4_revert(f) os.remove(f) - for f in filesToDelete: - p4_revert(f) os.remove(fileName) return submitted @@ -1907,6 +1990,9 @@ class P4Submit(Command, P4UserMap): if len(self.origin) == 0: self.origin = upstream + if self.update_shelve: + self.shelve = True + if self.preserveUser: if not self.canChangeChangelists(): die("Cannot preserve user names without p4 super-user or admin permissions") @@ -1934,7 +2020,7 @@ class P4Submit(Command, P4UserMap): if self.useClientSpec: self.clientSpecDirs = getClientSpec() - # Check for the existance of P4 branches + # Check for the existence of P4 branches branchesDetected = (len(p4BranchesInGit().keys()) > 1) if self.useClientSpec and not branchesDetected: @@ -2067,13 +2153,13 @@ class P4Submit(Command, P4UserMap): break chdir(self.oldWorkingDirectory) - + shelved_applied = "shelved" if self.shelve else "applied" if self.dry_run: pass elif self.prepare_p4_only: pass elif len(commits) == len(applied): - print "All commits applied!" + print ("All commits {0}!".format(shelved_applied)) sync = P4Sync() if self.branch: @@ -2085,9 +2171,9 @@ class P4Submit(Command, P4UserMap): else: if len(applied) == 0: - print "No commits applied." + print ("No commits {0}.".format(shelved_applied)) else: - print "Applied only the commits marked with '*':" + print ("{0} only the commits marked with '*':".format(shelved_applied.capitalize())) for c in commits: if c in applied: star = "*" @@ -2418,11 +2504,24 @@ class P4Sync(Command, P4UserMap): self.gitStream.write(d) self.gitStream.write('\n') + def encodeWithUTF8(self, path): + try: + path.decode('ascii') + except: + encoding = 'utf8' + if gitConfig('git-p4.pathEncoding'): + encoding = gitConfig('git-p4.pathEncoding') + path = path.decode(encoding, 'replace').encode('utf8', 'replace') + if self.verbose: + print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path) + return path + # output one file from the P4 stream # - helper for streamP4Files def streamOneP4File(self, file, contents): relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes) + relPath = self.encodeWithUTF8(relPath) if verbose: size = int(self.stream_file['fileSize']) sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024)) @@ -2495,16 +2594,6 @@ class P4Sync(Command, P4UserMap): text = regexp.sub(r'$\1$', text) contents = [ text ] - try: - relPath.decode('ascii') - except: - encoding = 'utf8' - if gitConfig('git-p4.pathEncoding'): - encoding = gitConfig('git-p4.pathEncoding') - relPath = relPath.decode(encoding, 'replace').encode('utf8', 'replace') - if self.verbose: - print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, relPath) - if self.largeFileSystem: (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents) @@ -2512,6 +2601,7 @@ class P4Sync(Command, P4UserMap): def streamOneP4Deletion(self, file): relPath = self.stripRepoPath(file['path'], self.branchPrefixes) + relPath = self.encodeWithUTF8(relPath) if verbose: sys.stdout.write("delete %s\n" % relPath) sys.stdout.flush() @@ -3682,6 +3772,7 @@ def main(): if cmd.gitdir == None: cmd.gitdir = os.path.abspath(".git") if not isValidGitDir(cmd.gitdir): + # "rev-parse --git-dir" without arguments will try $PWD/.git cmd.gitdir = read_pipe("git rev-parse --git-dir").strip() if os.path.exists(cmd.gitdir): cdup = read_pipe("git rev-parse --show-cdup").strip() @@ -3694,6 +3785,7 @@ def main(): else: die("fatal: cannot locate git repository at %s" % cmd.gitdir) + # so git commands invoked from the P4 workspace will succeed os.environ["GIT_DIR"] = cmd.gitdir if not cmd.run(args): |