diff options
Diffstat (limited to 'contrib/fast-import')
-rwxr-xr-x | contrib/fast-import/git-import.perl | 64 | ||||
-rwxr-xr-x | contrib/fast-import/git-import.sh | 38 | ||||
-rwxr-xr-x | contrib/fast-import/git-p4 | 1800 | ||||
-rw-r--r-- | contrib/fast-import/git-p4.bat | 1 | ||||
-rw-r--r-- | contrib/fast-import/git-p4.txt | 159 | ||||
-rwxr-xr-x | contrib/fast-import/import-tars.perl | 131 | ||||
-rwxr-xr-x | contrib/fast-import/import-zips.py | 72 |
7 files changed, 2265 insertions, 0 deletions
diff --git a/contrib/fast-import/git-import.perl b/contrib/fast-import/git-import.perl new file mode 100755 index 0000000000..f9fef6db28 --- /dev/null +++ b/contrib/fast-import/git-import.perl @@ -0,0 +1,64 @@ +#!/usr/bin/perl +# +# Performs an initial import of a directory. This is the equivalent +# of doing 'git init; git add .; git commit'. It's a little slower, +# but is meant to be a simple fast-import example. + +use strict; +use File::Find; + +my $USAGE = 'Usage: git-import branch import-message'; +my $branch = shift or die "$USAGE\n"; +my $message = shift or die "$USAGE\n"; + +chomp(my $username = `git config user.name`); +chomp(my $email = `git config user.email`); +die 'You need to set user name and email' + unless $username && $email; + +system('git init'); +open(my $fi, '|-', qw(git fast-import --date-format=now)) + or die "unable to spawn fast-import: $!"; + +print $fi <<EOF; +commit refs/heads/$branch +committer $username <$email> now +data <<MSGEOF +$message +MSGEOF + +EOF + +find( + sub { + if($File::Find::name eq './.git') { + $File::Find::prune = 1; + return; + } + return unless -f $_; + + my $fn = $File::Find::name; + $fn =~ s#^.\/##; + + open(my $in, '<', $_) + or die "unable to open $fn: $!"; + my @st = stat($in) + or die "unable to stat $fn: $!"; + my $len = $st[7]; + + print $fi "M 644 inline $fn\n"; + print $fi "data $len\n"; + while($len > 0) { + my $r = read($in, my $buf, $len < 4096 ? $len : 4096); + defined($r) or die "read error from $fn: $!"; + $r > 0 or die "premature EOF from $fn: $!"; + print $fi $buf; + $len -= $r; + } + print $fi "\n"; + + }, '.' +); + +close($fi); +exit $?; diff --git a/contrib/fast-import/git-import.sh b/contrib/fast-import/git-import.sh new file mode 100755 index 0000000000..0ca7718d05 --- /dev/null +++ b/contrib/fast-import/git-import.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# +# Performs an initial import of a directory. This is the equivalent +# of doing 'git init; git add .; git commit'. It's a lot slower, +# but is meant to be a simple fast-import example. + +if [ -z "$1" -o -z "$2" ]; then + echo "Usage: git-import branch import-message" + exit 1 +fi + +USERNAME="$(git config user.name)" +EMAIL="$(git config user.email)" + +if [ -z "$USERNAME" -o -z "$EMAIL" ]; then + echo "You need to set user name and email" + exit 1 +fi + +git init + +( + cat <<EOF +commit refs/heads/$1 +committer $USERNAME <$EMAIL> now +data <<MSGEOF +$2 +MSGEOF + +EOF + find * -type f|while read i;do + echo "M 100644 inline $i" + echo data $(stat -c '%s' "$i") + cat "$i" + echo + done + echo +) | git fast-import --date-format=now diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 new file mode 100755 index 0000000000..6ae0429c2d --- /dev/null +++ b/contrib/fast-import/git-p4 @@ -0,0 +1,1800 @@ +#!/usr/bin/env python +# +# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. +# +# Author: Simon Hausmann <simon@lst.de> +# Copyright: 2007 Simon Hausmann <simon@lst.de> +# 2007 Trolltech ASA +# License: MIT <http://www.opensource.org/licenses/mit-license.php> +# + +import optparse, sys, os, marshal, popen2, subprocess, shelve +import tempfile, getopt, sha, os.path, time, platform +import re + +from sets import Set; + +verbose = False + +def die(msg): + if verbose: + raise Exception(msg) + else: + sys.stderr.write(msg + "\n") + sys.exit(1) + +def write_pipe(c, str): + if verbose: + sys.stderr.write('Writing pipe: %s\n' % c) + + pipe = os.popen(c, 'w') + val = pipe.write(str) + if pipe.close(): + die('Command failed: %s' % c) + + return val + +def read_pipe(c, ignore_error=False): + if verbose: + sys.stderr.write('Reading pipe: %s\n' % c) + + pipe = os.popen(c, 'rb') + val = pipe.read() + if pipe.close() and not ignore_error: + die('Command failed: %s' % c) + + return val + + +def read_pipe_lines(c): + if verbose: + sys.stderr.write('Reading pipe: %s\n' % c) + ## todo: check return status + pipe = os.popen(c, 'rb') + val = pipe.readlines() + if pipe.close(): + die('Command failed: %s' % c) + + return val + +def system(cmd): + if verbose: + sys.stderr.write("executing %s\n" % cmd) + if os.system(cmd) != 0: + die("command failed: %s" % cmd) + +def isP4Exec(kind): + """Determine if a Perforce 'kind' should have execute permission + + 'p4 help filetypes' gives a list of the types. If it starts with 'x', + or x follows one of a few letters. Otherwise, if there is an 'x' after + a plus sign, it is also executable""" + return (re.search(r"(^[cku]?x)|\+.*x", kind) != None) + +def setP4ExecBit(file, mode): + # Reopens an already open file and changes the execute bit to match + # the execute bit setting in the passed in mode. + + p4Type = "+x" + + if not isModeExec(mode): + p4Type = getP4OpenedType(file) + p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type) + p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type) + if p4Type[-1] == "+": + p4Type = p4Type[0:-1] + + system("p4 reopen -t %s %s" % (p4Type, file)) + +def getP4OpenedType(file): + # Returns the perforce file type for the given file. + + result = read_pipe("p4 opened %s" % file) + match = re.match(".*\((.+)\)\r?$", result) + if match: + return match.group(1) + else: + die("Could not determine file type for %s (result: '%s')" % (file, result)) + +def diffTreePattern(): + # This is a simple generator for the diff tree regex pattern. This could be + # a class variable if this and parseDiffTreeEntry were a part of a class. + pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)') + while True: + yield pattern + +def parseDiffTreeEntry(entry): + """Parses a single diff tree entry into its component elements. + + See git-diff-tree(1) manpage for details about the format of the diff + output. This method returns a dictionary with the following elements: + + src_mode - The mode of the source file + dst_mode - The mode of the destination file + src_sha1 - The sha1 for the source file + dst_sha1 - The sha1 fr the destination file + status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc) + status_score - The score for the status (applicable for 'C' and 'R' + statuses). This is None if there is no score. + src - The path for the source file. + dst - The path for the destination file. This is only present for + copy or renames. If it is not present, this is None. + + If the pattern is not matched, None is returned.""" + + match = diffTreePattern().next().match(entry) + if match: + return { + 'src_mode': match.group(1), + 'dst_mode': match.group(2), + 'src_sha1': match.group(3), + 'dst_sha1': match.group(4), + 'status': match.group(5), + 'status_score': match.group(6), + 'src': match.group(7), + 'dst': match.group(10) + } + return None + +def isModeExec(mode): + # Returns True if the given git mode represents an executable file, + # otherwise False. + return mode[-3:] == "755" + +def isModeExecChanged(src_mode, dst_mode): + return isModeExec(src_mode) != isModeExec(dst_mode) + +def p4CmdList(cmd, stdin=None, stdin_mode='w+b'): + cmd = "p4 -G %s" % cmd + if verbose: + sys.stderr.write("Opening pipe: %s\n" % cmd) + + # Use a temporary file to avoid deadlocks without + # subprocess.communicate(), which would put another copy + # of stdout into memory. + stdin_file = None + if stdin is not None: + stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) + stdin_file.write(stdin) + stdin_file.flush() + stdin_file.seek(0) + + p4 = subprocess.Popen(cmd, shell=True, + stdin=stdin_file, + stdout=subprocess.PIPE) + + result = [] + try: + while True: + entry = marshal.load(p4.stdout) + result.append(entry) + except EOFError: + pass + exitCode = p4.wait() + if exitCode != 0: + entry = {} + entry["p4ExitCode"] = exitCode + result.append(entry) + + return result + +def p4Cmd(cmd): + list = p4CmdList(cmd) + result = {} + for entry in list: + result.update(entry) + return result; + +def p4Where(depotPath): + if not depotPath.endswith("/"): + depotPath += "/" + output = p4Cmd("where %s..." % depotPath) + if output["code"] == "error": + return "" + clientPath = "" + if "path" in output: + clientPath = output.get("path") + elif "data" in output: + data = output.get("data") + lastSpace = data.rfind(" ") + clientPath = data[lastSpace + 1:] + + if clientPath.endswith("..."): + clientPath = clientPath[:-3] + return clientPath + +def currentGitBranch(): + return read_pipe("git name-rev HEAD").split(" ")[1].strip() + +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 + +def parseRevision(ref): + return read_pipe("git rev-parse %s" % ref).strip() + +def extractLogMessageFromGitCommit(commit): + logMessage = "" + + ## fixme: title is first line of commit, not 1st paragraph. + foundTitle = False + for log in read_pipe_lines("git cat-file commit %s" % commit): + if not foundTitle: + if len(log) == 1: + foundTitle = True + continue + + logMessage += log + return logMessage + +def extractSettingsGitLog(log): + values = {} + for line in log.split("\n"): + line = line.strip() + m = re.search (r"^ *\[git-p4: (.*)\]$", line) + if not m: + continue + + assignments = m.group(1).split (':') + for a in assignments: + vals = a.split ('=') + key = vals[0].strip() + val = ('='.join (vals[1:])).strip() + if val.endswith ('\"') and val.startswith('"'): + val = val[1:-1] + + values[key] = val + + paths = values.get("depot-paths") + if not paths: + paths = values.get("depot-path") + if paths: + values['depot-paths'] = paths.split(',') + return values + +def gitBranchExists(branch): + proc = subprocess.Popen(["git", "rev-parse", branch], + stderr=subprocess.PIPE, stdout=subprocess.PIPE); + return proc.wait() == 0; + +def gitConfig(key): + return read_pipe("git config %s" % key, ignore_error=True).strip() + +def p4BranchesInGit(branchesAreInRemotes = True): + branches = {} + + cmdline = "git rev-parse --symbolic " + if branchesAreInRemotes: + cmdline += " --remotes" + else: + cmdline += " --branches" + + for line in read_pipe_lines(cmdline): + line = line.strip() + + ## only import to p4/ + if not line.startswith('p4/') or line == "p4/HEAD": + continue + branch = line + + # strip off p4 + branch = re.sub ("^p4/", "", line) + + branches[branch] = parseRevision(line) + return branches + +def findUpstreamBranchPoint(head = "HEAD"): + branches = p4BranchesInGit() + # map from depot-path to branch name + branchByDepotPath = {} + for branch in branches.keys(): + tip = branches[branch] + log = extractLogMessageFromGitCommit(tip) + settings = extractSettingsGitLog(log) + if settings.has_key("depot-paths"): + paths = ",".join(settings["depot-paths"]) + branchByDepotPath[paths] = "remotes/p4/" + branch + + settings = None + parent = 0 + while parent < 65535: + commit = head + "~%s" % parent + log = extractLogMessageFromGitCommit(commit) + settings = extractSettingsGitLog(log) + if settings.has_key("depot-paths"): + paths = ",".join(settings["depot-paths"]) + if branchByDepotPath.has_key(paths): + return [branchByDepotPath[paths], settings] + + parent = parent + 1 + + return ["", settings] + +def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True): + if not silent: + print ("Creating/updating branch(es) in %s based on origin branch(es)" + % localRefPrefix) + + originPrefix = "origin/p4/" + + for line in read_pipe_lines("git rev-parse --symbolic --remotes"): + line = line.strip() + if (not line.startswith(originPrefix)) or line.endswith("HEAD"): + continue + + headName = line[len(originPrefix):] + remoteHead = localRefPrefix + headName + originHead = line + + original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) + if (not original.has_key('depot-paths') + or not original.has_key('change')): + continue + + update = False + if not gitBranchExists(remoteHead): + if verbose: + print "creating %s" % remoteHead + update = True + else: + settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) + if settings.has_key('change') > 0: + if settings['depot-paths'] == original['depot-paths']: + originP4Change = int(original['change']) + p4Change = int(settings['change']) + if originP4Change > p4Change: + print ("%s (%s) is newer than %s (%s). " + "Updating p4 branch from origin." + % (originHead, originP4Change, + remoteHead, p4Change)) + update = True + else: + print ("Ignoring: %s was imported from %s while " + "%s was imported from %s" + % (originHead, ','.join(original['depot-paths']), + remoteHead, ','.join(settings['depot-paths']))) + + if update: + system("git update-ref %s %s" % (remoteHead, originHead)) + +def originP4BranchesExist(): + return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master") + +def p4ChangesForPaths(depotPaths, changeRange): + assert depotPaths + output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, changeRange) + for p in depotPaths])) + + changes = [] + for line in output: + changeNum = line.split(" ")[1] + changes.append(int(changeNum)) + + changes.sort() + return changes + +class Command: + def __init__(self): + self.usage = "usage: %prog [options]" + self.needsGit = True + +class P4Debug(Command): + def __init__(self): + Command.__init__(self) + self.options = [ + optparse.make_option("--verbose", dest="verbose", action="store_true", + default=False), + ] + self.description = "A tool to debug the output of p4 -G." + self.needsGit = False + self.verbose = False + + def run(self, args): + j = 0 + for output in p4CmdList(" ".join(args)): + print 'Element: %d' % j + j += 1 + print output + return True + +class P4RollBack(Command): + def __init__(self): + Command.__init__(self) + self.options = [ + optparse.make_option("--verbose", dest="verbose", action="store_true"), + optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") + ] + self.description = "A tool to debug the multi-branch import. Don't use :)" + self.verbose = False + self.rollbackLocalBranches = False + + def run(self, args): + if len(args) != 1: + return False + maxChange = int(args[0]) + + if "p4ExitCode" in p4Cmd("changes -m 1"): + die("Problems executing p4"); + + if self.rollbackLocalBranches: + refPrefix = "refs/heads/" + lines = read_pipe_lines("git rev-parse --symbolic --branches") + else: + refPrefix = "refs/remotes/" + lines = read_pipe_lines("git rev-parse --symbolic --remotes") + + for line in lines: + if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"): + line = line.strip() + ref = refPrefix + line + log = extractLogMessageFromGitCommit(ref) + settings = extractSettingsGitLog(log) + + depotPaths = settings['depot-paths'] + change = settings['change'] + + changed = False + + if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange) + for p in depotPaths]))) == 0: + print "Branch %s did not exist at change %s, deleting." % (ref, maxChange) + system("git update-ref -d %s `git rev-parse %s`" % (ref, ref)) + continue + + while change and int(change) > maxChange: + changed = True + if self.verbose: + print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange) + system("git update-ref %s \"%s^\"" % (ref, ref)) + log = extractLogMessageFromGitCommit(ref) + settings = extractSettingsGitLog(log) + + + depotPaths = settings['depot-paths'] + change = settings['change'] + + if changed: + print "%s rewound to %s" % (ref, change) + + return True + +class P4Submit(Command): + def __init__(self): + Command.__init__(self) + self.options = [ + optparse.make_option("--verbose", dest="verbose", action="store_true"), + optparse.make_option("--origin", dest="origin"), + optparse.make_option("-M", dest="detectRename", action="store_true"), + ] + self.description = "Submit changes from git to the perforce depot." + self.usage += " [name of git branch to submit into perforce depot]" + self.interactive = True + self.origin = "" + self.detectRename = False + self.verbose = False + self.isWindows = (platform.system() == "Windows") + + def check(self): + if len(p4CmdList("opened ...")) > 0: + die("You have files opened with perforce! Close them before starting the sync.") + + # replaces everything between 'Description:' and the next P4 submit template field with the + # commit message + def prepareLogMessage(self, template, message): + result = "" + + inDescriptionSection = False + + for line in template.split("\n"): + if line.startswith("#"): + result += line + "\n" + continue + + if inDescriptionSection: + if line.startswith("Files:"): + inDescriptionSection = False + else: + continue + else: + if line.startswith("Description:"): + inDescriptionSection = True + line += "\n" + for messageLine in message.split("\n"): + line += "\t" + messageLine + "\n" + + result += line + "\n" + + return result + + def prepareSubmitTemplate(self): + # remove lines in the Files section that show changes to files outside the depot path we're committing into + template = "" + inFilesSection = False + for line in read_pipe_lines("p4 change -o"): + if line.endswith("\r\n"): + line = line[:-2] + "\n" + if inFilesSection: + if line.startswith("\t"): + # path starts and ends with a tab + path = line[1:] + lastTab = path.rfind("\t") + if lastTab != -1: + path = path[:lastTab] + if not path.startswith(self.depotPath): + continue + else: + inFilesSection = False + else: + if line.startswith("Files:"): + inFilesSection = True + + template += line + + return template + + def applyCommit(self, id): + print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id)) + diffOpts = ("", "-M")[self.detectRename] + diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id)) + filesToAdd = set() + filesToDelete = set() + editedFiles = set() + filesToChangeExecBit = {} + for line in diff: + diff = parseDiffTreeEntry(line) + modifier = diff['status'] + path = diff['src'] + if modifier == "M": + system("p4 edit \"%s\"" % path) + if isModeExecChanged(diff['src_mode'], diff['dst_mode']): + filesToChangeExecBit[path] = diff['dst_mode'] + editedFiles.add(path) + elif modifier == "A": + filesToAdd.add(path) + filesToChangeExecBit[path] = diff['dst_mode'] + if path in filesToDelete: + filesToDelete.remove(path) + elif modifier == "D": + filesToDelete.add(path) + if path in filesToAdd: + filesToAdd.remove(path) + elif modifier == "R": + src, dest = diff['src'], diff['dst'] + system("p4 integrate -Dt \"%s\" \"%s\"" % (src, dest)) + system("p4 edit \"%s\"" % (dest)) + if isModeExecChanged(diff['src_mode'], diff['dst_mode']): + filesToChangeExecBit[dest] = diff['dst_mode'] + os.unlink(dest) + editedFiles.add(dest) + filesToDelete.add(src) + else: + die("unknown modifier %s for %s" % (modifier, path)) + + diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id) + patchcmd = diffcmd + " | git apply " + tryPatchCmd = patchcmd + "--check -" + applyPatchCmd = patchcmd + "--check --apply -" + + if os.system(tryPatchCmd) != 0: + print "Unfortunately applying the change failed!" + print "What do you want to do?" + response = "x" + while response != "s" and response != "a" and response != "w": + response = raw_input("[s]kip this patch / [a]pply the patch forcibly " + "and with .rej files / [w]rite the patch to a file (patch.txt) ") + if response == "s": + print "Skipping! Good luck with the next patches..." + for f in editedFiles: + system("p4 revert \"%s\"" % f); + for f in filesToAdd: + system("rm %s" %f) + return + elif response == "a": + os.system(applyPatchCmd) + if len(filesToAdd) > 0: + print "You may also want to call p4 add on the following files:" + print " ".join(filesToAdd) + if len(filesToDelete): + print "The following files should be scheduled for deletion with p4 delete:" + print " ".join(filesToDelete) + die("Please resolve and submit the conflict manually and " + + "continue afterwards with git-p4 submit --continue") + elif response == "w": + system(diffcmd + " > patch.txt") + print "Patch saved to patch.txt in %s !" % self.clientPath + die("Please resolve and submit the conflict manually and " + "continue afterwards with git-p4 submit --continue") + + system(applyPatchCmd) + + for f in filesToAdd: + system("p4 add \"%s\"" % f) + for f in filesToDelete: + system("p4 revert \"%s\"" % f) + system("p4 delete \"%s\"" % f) + + # Set/clear executable bits + for f in filesToChangeExecBit.keys(): + mode = filesToChangeExecBit[f] + setP4ExecBit(f, mode) + + logMessage = extractLogMessageFromGitCommit(id) + logMessage = logMessage.strip() + + template = self.prepareSubmitTemplate() + + if self.interactive: + submitTemplate = self.prepareLogMessage(template, logMessage) + if os.environ.has_key("P4DIFF"): + del(os.environ["P4DIFF"]) + diff = read_pipe("p4 diff -du ...") + + newdiff = "" + for newFile in filesToAdd: + 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() + + separatorLine = "######## everything below this line is just the diff #######\n" + + [handle, fileName] = tempfile.mkstemp() + tmpFile = os.fdopen(handle, "w+") + if self.isWindows: + submitTemplate = submitTemplate.replace("\n", "\r\n") + separatorLine = separatorLine.replace("\n", "\r\n") + newdiff = newdiff.replace("\n", "\r\n") + tmpFile.write(submitTemplate + separatorLine + diff + newdiff) + tmpFile.close() + defaultEditor = "vi" + if platform.system() == "Windows": + defaultEditor = "notepad" + if os.environ.has_key("P4EDITOR"): + editor = os.environ.get("P4EDITOR") + else: + editor = os.environ.get("EDITOR", defaultEditor); + system(editor + " " + fileName) + tmpFile = open(fileName, "rb") + message = tmpFile.read() + tmpFile.close() + os.remove(fileName) + submitTemplate = message[:message.index(separatorLine)] + if self.isWindows: + submitTemplate = submitTemplate.replace("\r\n", "\n") + + write_pipe("p4 submit -i", submitTemplate) + else: + fileName = "submit.txt" + file = open(fileName, "w+") + file.write(self.prepareLogMessage(template, logMessage)) + file.close() + print ("Perforce submit template written as %s. " + + "Please review/edit and then use p4 submit -i < %s to submit directly!" + % (fileName, fileName)) + + def run(self, args): + if len(args) == 0: + self.master = currentGitBranch() + if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master): + die("Detecting current git branch failed!") + elif len(args) == 1: + self.master = args[0] + else: + return False + + allowSubmit = gitConfig("git-p4.allowSubmit") + if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","): + die("%s is not in git-p4.allowSubmit" % self.master) + + [upstream, settings] = findUpstreamBranchPoint() + self.depotPath = settings['depot-paths'][0] + if len(self.origin) == 0: + self.origin = upstream + + if self.verbose: + print "Origin branch is " + self.origin + + if len(self.depotPath) == 0: + print "Internal error: cannot locate perforce depot path from existing branches" + sys.exit(128) + + self.clientPath = p4Where(self.depotPath) + + if len(self.clientPath) == 0: + print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath + sys.exit(128) + + print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath) + self.oldWorkingDirectory = os.getcwd() + + os.chdir(self.clientPath) + print "Syncronizing p4 checkout..." + system("p4 sync ...") + + self.check() + + commits = [] + for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)): + commits.append(line.strip()) + commits.reverse() + + while len(commits) > 0: + commit = commits[0] + commits = commits[1:] + self.applyCommit(commit) + if not self.interactive: + break + + if len(commits) == 0: + print "All changes applied!" + os.chdir(self.oldWorkingDirectory) + + sync = P4Sync() + sync.run([]) + + rebase = P4Rebase() + rebase.rebase() + + return True + +class P4Sync(Command): + def __init__(self): + Command.__init__(self) + self.options = [ + optparse.make_option("--branch", dest="branch"), + optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"), + optparse.make_option("--changesfile", dest="changesFile"), + optparse.make_option("--silent", dest="silent", action="store_true"), + optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"), + optparse.make_option("--verbose", dest="verbose", action="store_true"), + optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false", + help="Import into refs/heads/ , not refs/remotes"), + optparse.make_option("--max-changes", dest="maxChanges"), + optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true', + help="Keep entire BRANCH/DIR/SUBDIR prefix during import"), + optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true', + help="Only sync files that are included in the Perforce Client Spec") + ] + self.description = """Imports from Perforce into a git repository.\n + example: + //depot/my/project/ -- to import the current head + //depot/my/project/@all -- to import everything + //depot/my/project/@1,6 -- to import only from revision 1 to 6 + + (a ... is not needed in the path p4 specification, it's added implicitly)""" + + self.usage += " //depot/path[@revRange]" + self.silent = False + self.createdBranches = Set() + self.committedChanges = Set() + self.branch = "" + self.detectBranches = False + self.detectLabels = False + self.changesFile = "" + self.syncWithOrigin = True + self.verbose = False + self.importIntoRemotes = True + self.maxChanges = "" + self.isWindows = (platform.system() == "Windows") + self.keepRepoPath = False + self.depotPaths = None + self.p4BranchesInGit = [] + self.cloneExclude = [] + self.useClientSpec = False + self.clientSpecDirs = [] + + if gitConfig("git-p4.syncFromOrigin") == "false": + self.syncWithOrigin = False + + def extractFilesFromCommit(self, commit): + self.cloneExclude = [re.sub(r"\.\.\.$", "", path) + for path in self.cloneExclude] + files = [] + fnum = 0 + while commit.has_key("depotFile%s" % fnum): + path = commit["depotFile%s" % fnum] + + if [p for p in self.cloneExclude + if path.startswith (p)]: + found = False + else: + found = [p for p in self.depotPaths + if path.startswith (p)] + if not found: + fnum = fnum + 1 + continue + + file = {} + file["path"] = path + file["rev"] = commit["rev%s" % fnum] + file["action"] = commit["action%s" % fnum] + file["type"] = commit["type%s" % fnum] + files.append(file) + fnum = fnum + 1 + return files + + def stripRepoPath(self, path, prefixes): + if self.keepRepoPath: + prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])] + + for p in prefixes: + if path.startswith(p): + path = path[len(p):] + + return path + + def splitFilesIntoBranches(self, commit): + branches = {} + fnum = 0 + while commit.has_key("depotFile%s" % fnum): + path = commit["depotFile%s" % fnum] + found = [p for p in self.depotPaths + if path.startswith (p)] + if not found: + fnum = fnum + 1 + continue + + file = {} + file["path"] = path + file["rev"] = commit["rev%s" % fnum] + file["action"] = commit["action%s" % fnum] + file["type"] = commit["type%s" % fnum] + fnum = fnum + 1 + + relPath = self.stripRepoPath(path, self.depotPaths) + + for branch in self.knownBranches.keys(): + + # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2 + if relPath.startswith(branch + "/"): + if branch not in branches: + branches[branch] = [] + branches[branch].append(file) + break + + return branches + + ## Should move this out, doesn't use SELF. + def readP4Files(self, files): + filesForCommit = [] + filesToRead = [] + + for f in files: + includeFile = True + for val in self.clientSpecDirs: + if f['path'].startswith(val[0]): + if val[1] <= 0: + includeFile = False + break + + if includeFile: + filesForCommit.append(f) + if f['action'] != 'delete': + filesToRead.append(f) + + filedata = [] + if len(filesToRead) > 0: + filedata = p4CmdList('-x - print', + stdin='\n'.join(['%s#%s' % (f['path'], f['rev']) + for f in filesToRead]), + stdin_mode='w+') + + if "p4ExitCode" in filedata[0]: + die("Problems executing p4. Error: [%d]." + % (filedata[0]['p4ExitCode'])); + + j = 0; + contents = {} + while j < len(filedata): + stat = filedata[j] + j += 1 + text = []; + while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'): + text.append(filedata[j]['data']) + j += 1 + text = ''.join(text) + + if not stat.has_key('depotFile'): + sys.stderr.write("p4 print fails with: %s\n" % repr(stat)) + continue + + if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'): + text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text) + elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'): + text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text) + + contents[stat['depotFile']] = text + + for f in filesForCommit: + path = f['path'] + if contents.has_key(path): + f['data'] = contents[path] + + return filesForCommit + + def commit(self, details, files, branch, branchPrefixes, parent = ""): + epoch = details["time"] + author = details["user"] + + if self.verbose: + print "commit into %s" % branch + + # start with reading files; if that fails, we should not + # create a commit. + new_files = [] + for f in files: + if [p for p in branchPrefixes if f['path'].startswith(p)]: + new_files.append (f) + else: + sys.stderr.write("Ignoring file outside of prefix: %s\n" % path) + files = self.readP4Files(new_files) + + self.gitStream.write("commit %s\n" % branch) +# gitStream.write("mark :%s\n" % details["change"]) + self.committedChanges.add(int(details["change"])) + committer = "" + if author not in self.users: + self.getUserMapFromPerforceServer() + if author in self.users: + committer = "%s %s %s" % (self.users[author], epoch, self.tz) + else: + committer = "%s <a@b> %s %s" % (author, epoch, self.tz) + + self.gitStream.write("committer %s\n" % committer) + + self.gitStream.write("data <<EOT\n") + self.gitStream.write(details["desc"]) + self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" + % (','.join (branchPrefixes), details["change"])) + if len(details['options']) > 0: + self.gitStream.write(": options = %s" % details['options']) + self.gitStream.write("]\nEOT\n\n") + + if len(parent) > 0: + if self.verbose: + print "parent %s" % parent + self.gitStream.write("from %s\n" % parent) + + for file in files: + if file["type"] == "apple": + print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path'] + continue + + relPath = self.stripRepoPath(file['path'], branchPrefixes) + if file["action"] == "delete": + self.gitStream.write("D %s\n" % relPath) + else: + data = file['data'] + + mode = "644" + if isP4Exec(file["type"]): + mode = "755" + elif file["type"] == "symlink": + mode = "120000" + # p4 print on a symlink contains "target\n", so strip it off + data = data[:-1] + + if self.isWindows and file["type"].endswith("text"): + data = data.replace("\r\n", "\n") + + self.gitStream.write("M %s inline %s\n" % (mode, relPath)) + self.gitStream.write("data %s\n" % len(data)) + self.gitStream.write(data) + self.gitStream.write("\n") + + self.gitStream.write("\n") + + change = int(details["change"]) + + if self.labels.has_key(change): + label = self.labels[change] + labelDetails = label[0] + labelRevisions = label[1] + if self.verbose: + print "Change %s is labelled %s" % (change, labelDetails) + + files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change) + for p in branchPrefixes])) + + if len(files) == len(labelRevisions): + + cleanedFiles = {} + for info in files: + if info["action"] == "delete": + continue + cleanedFiles[info["depotFile"]] = info["rev"] + + if cleanedFiles == labelRevisions: + self.gitStream.write("tag tag_%s\n" % labelDetails["label"]) + self.gitStream.write("from %s\n" % branch) + + owner = labelDetails["Owner"] + tagger = "" + if author in self.users: + tagger = "%s %s %s" % (self.users[owner], epoch, self.tz) + else: + tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz) + self.gitStream.write("tagger %s\n" % tagger) + self.gitStream.write("data <<EOT\n") + self.gitStream.write(labelDetails["Description"]) + self.gitStream.write("EOT\n\n") + + else: + if not self.silent: + print ("Tag %s does not match with change %s: files do not match." + % (labelDetails["label"], change)) + + else: + if not self.silent: + print ("Tag %s does not match with change %s: file count is different." + % (labelDetails["label"], change)) + + def getUserCacheFilename(self): + home = os.environ.get("HOME", os.environ.get("USERPROFILE")) + return home + "/.gitp4-usercache.txt" + + def getUserMapFromPerforceServer(self): + if self.userMapFromPerforceServer: + return + self.users = {} + + for output in p4CmdList("users"): + if not output.has_key("User"): + continue + self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">" + + + s = '' + for (key, val) in self.users.items(): + s += "%s\t%s\n" % (key, val) + + open(self.getUserCacheFilename(), "wb").write(s) + self.userMapFromPerforceServer = True + + def loadUserMapFromCache(self): + self.users = {} + self.userMapFromPerforceServer = False + try: + cache = open(self.getUserCacheFilename(), "rb") + lines = cache.readlines() + cache.close() + for line in lines: + entry = line.strip().split("\t") + self.users[entry[0]] = entry[1] + except IOError: + self.getUserMapFromPerforceServer() + + def getLabels(self): + self.labels = {} + + l = p4CmdList("labels %s..." % ' '.join (self.depotPaths)) + if len(l) > 0 and not self.silent: + print "Finding files belonging to labels in %s" % `self.depotPaths` + + for output in l: + label = output["label"] + revisions = {} + newestChange = 0 + if self.verbose: + print "Querying files for label %s" % label + for file in p4CmdList("files " + + ' '.join (["%s...@%s" % (p, label) + for p in self.depotPaths])): + revisions[file["depotFile"]] = file["rev"] + change = int(file["change"]) + if change > newestChange: + newestChange = change + + self.labels[newestChange] = [output, revisions] + + if self.verbose: + print "Label changes: %s" % self.labels.keys() + + def guessProjectName(self): + for p in self.depotPaths: + if p.endswith("/"): + p = p[:-1] + p = p[p.strip().rfind("/") + 1:] + if not p.endswith("/"): + p += "/" + return p + + def getBranchMapping(self): + lostAndFoundBranches = set() + + for info in p4CmdList("branches"): + details = p4Cmd("branch -o %s" % info["branch"]) + viewIdx = 0 + while details.has_key("View%s" % viewIdx): + paths = details["View%s" % viewIdx].split(" ") + viewIdx = viewIdx + 1 + # require standard //depot/foo/... //depot/bar/... mapping + if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."): + continue + source = paths[0] + destination = paths[1] + ## HACK + if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]): + source = source[len(self.depotPaths[0]):-4] + destination = destination[len(self.depotPaths[0]):-4] + + if destination in self.knownBranches: + if not self.silent: + print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination) + print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination) + continue + + self.knownBranches[destination] = source + + lostAndFoundBranches.discard(destination) + + if source not in self.knownBranches: + lostAndFoundBranches.add(source) + + + for branch in lostAndFoundBranches: + self.knownBranches[branch] = branch + + def getBranchMappingFromGitBranches(self): + branches = p4BranchesInGit(self.importIntoRemotes) + for branch in branches.keys(): + if branch == "master": + branch = "main" + else: + branch = branch[len(self.projectName):] + self.knownBranches[branch] = branch + + def listExistingP4GitBranches(self): + # branches holds mapping from name to commit + branches = p4BranchesInGit(self.importIntoRemotes) + self.p4BranchesInGit = branches.keys() + for branch in branches.keys(): + self.initialParents[self.refPrefix + branch] = branches[branch] + + def updateOptionDict(self, d): + option_keys = {} + if self.keepRepoPath: + option_keys['keepRepoPath'] = 1 + + d["options"] = ' '.join(sorted(option_keys.keys())) + + def readOptions(self, d): + self.keepRepoPath = (d.has_key('options') + and ('keepRepoPath' in d['options'])) + + def gitRefForBranch(self, branch): + if branch == "main": + return self.refPrefix + "master" + + if len(branch) <= 0: + return branch + + return self.refPrefix + self.projectName + branch + + def gitCommitByP4Change(self, ref, change): + if self.verbose: + print "looking in ref " + ref + " for change %s using bisect..." % change + + earliestCommit = "" + latestCommit = parseRevision(ref) + + while True: + if self.verbose: + print "trying: earliest %s latest %s" % (earliestCommit, latestCommit) + next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip() + if len(next) == 0: + if self.verbose: + print "argh" + return "" + log = extractLogMessageFromGitCommit(next) + settings = extractSettingsGitLog(log) + currentChange = int(settings['change']) + if self.verbose: + print "current change %s" % currentChange + + if currentChange == change: + if self.verbose: + print "found %s" % next + return next + + if currentChange < change: + earliestCommit = "^%s" % next + else: + latestCommit = "%s" % next + + return "" + + def importNewBranch(self, branch, maxChange): + # make fast-import flush all changes to disk and update the refs using the checkpoint + # command so that we can try to find the branch parent in the git history + self.gitStream.write("checkpoint\n\n"); + self.gitStream.flush(); + branchPrefix = self.depotPaths[0] + branch + "/" + range = "@1,%s" % maxChange + #print "prefix" + branchPrefix + changes = p4ChangesForPaths([branchPrefix], range) + if len(changes) <= 0: + return False + firstChange = changes[0] + #print "first change in branch: %s" % firstChange + sourceBranch = self.knownBranches[branch] + sourceDepotPath = self.depotPaths[0] + sourceBranch + sourceRef = self.gitRefForBranch(sourceBranch) + #print "source " + sourceBranch + + branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"]) + #print "branch parent: %s" % branchParentChange + gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange) + if len(gitParent) > 0: + self.initialParents[self.gitRefForBranch(branch)] = gitParent + #print "parent git commit: %s" % gitParent + + self.importChanges(changes) + return True + + def importChanges(self, changes): + cnt = 1 + for change in changes: + description = p4Cmd("describe %s" % change) + self.updateOptionDict(description) + + if not self.silent: + sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes))) + sys.stdout.flush() + cnt = cnt + 1 + + try: + if self.detectBranches: + branches = self.splitFilesIntoBranches(description) + for branch in branches.keys(): + ## HACK --hwn + branchPrefix = self.depotPaths[0] + branch + "/" + + parent = "" + + filesForCommit = branches[branch] + + if self.verbose: + print "branch is %s" % branch + + self.updatedBranches.add(branch) + + if branch not in self.createdBranches: + self.createdBranches.add(branch) + parent = self.knownBranches[branch] + if parent == branch: + parent = "" + else: + fullBranch = self.projectName + branch + if fullBranch not in self.p4BranchesInGit: + if not self.silent: + print("\n Importing new branch %s" % fullBranch); + if self.importNewBranch(branch, change - 1): + parent = "" + self.p4BranchesInGit.append(fullBranch) + if not self.silent: + print("\n Resuming with change %s" % change); + + if self.verbose: + print "parent determined through known branches: %s" % parent + + branch = self.gitRefForBranch(branch) + parent = self.gitRefForBranch(parent) + + if self.verbose: + print "looking for initial parent for %s; current parent is %s" % (branch, parent) + + if len(parent) == 0 and branch in self.initialParents: + parent = self.initialParents[branch] + del self.initialParents[branch] + + self.commit(description, filesForCommit, branch, [branchPrefix], parent) + else: + files = self.extractFilesFromCommit(description) + self.commit(description, files, self.branch, self.depotPaths, + self.initialParent) + self.initialParent = "" + except IOError: + print self.gitError.read() + sys.exit(1) + + def importHeadRevision(self, revision): + print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch) + + details = { "user" : "git perforce import user", "time" : int(time.time()) } + details["desc"] = ("Initial import of %s from the state at revision %s" + % (' '.join(self.depotPaths), revision)) + details["change"] = revision + newestRevision = 0 + + fileCnt = 0 + for info in p4CmdList("files " + + ' '.join(["%s...%s" + % (p, revision) + for p in self.depotPaths])): + + if info['code'] == 'error': + sys.stderr.write("p4 returned an error: %s\n" + % info['data']) + sys.exit(1) + + + change = int(info["change"]) + if change > newestRevision: + newestRevision = change + + if info["action"] == "delete": + # don't increase the file cnt, otherwise details["depotFile123"] will have gaps! + #fileCnt = fileCnt + 1 + continue + + for prop in ["depotFile", "rev", "action", "type" ]: + details["%s%s" % (prop, fileCnt)] = info[prop] + + fileCnt = fileCnt + 1 + + details["change"] = newestRevision + self.updateOptionDict(details) + try: + self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths) + except IOError: + print "IO error with git fast-import. Is your git version recent enough?" + print self.gitError.read() + + + def getClientSpec(self): + specList = p4CmdList( "client -o" ) + temp = {} + for entry in specList: + for k,v in entry.iteritems(): + if k.startswith("View"): + if v.startswith('"'): + start = 1 + else: + start = 0 + index = v.find("...") + v = v[start:index] + if v.startswith("-"): + v = v[1:] + temp[v] = -len(v) + else: + temp[v] = len(v) + self.clientSpecDirs = temp.items() + self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) ) + + def run(self, args): + self.depotPaths = [] + self.changeRange = "" + self.initialParent = "" + self.previousDepotPaths = [] + + # map from branch depot path to parent branch + self.knownBranches = {} + self.initialParents = {} + self.hasOrigin = originP4BranchesExist() + if not self.syncWithOrigin: + self.hasOrigin = False + + if self.importIntoRemotes: + self.refPrefix = "refs/remotes/p4/" + else: + self.refPrefix = "refs/heads/p4/" + + if self.syncWithOrigin and self.hasOrigin: + if not self.silent: + print "Syncing with origin first by calling git fetch origin" + system("git fetch origin") + + if len(self.branch) == 0: + self.branch = self.refPrefix + "master" + if gitBranchExists("refs/heads/p4") and self.importIntoRemotes: + system("git update-ref %s refs/heads/p4" % self.branch) + system("git branch -D p4"); + # create it /after/ importing, when master exists + if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch): + system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch)) + + if self.useClientSpec or gitConfig("p4.useclientspec") == "true": + self.getClientSpec() + + # TODO: should always look at previous commits, + # merge with previous imports, if possible. + if args == []: + if self.hasOrigin: + createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent) + self.listExistingP4GitBranches() + + if len(self.p4BranchesInGit) > 1: + if not self.silent: + print "Importing from/into multiple branches" + self.detectBranches = True + + if self.verbose: + print "branches: %s" % self.p4BranchesInGit + + p4Change = 0 + for branch in self.p4BranchesInGit: + logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch) + + settings = extractSettingsGitLog(logMsg) + + self.readOptions(settings) + if (settings.has_key('depot-paths') + and settings.has_key ('change')): + change = int(settings['change']) + 1 + p4Change = max(p4Change, change) + + depotPaths = sorted(settings['depot-paths']) + if self.previousDepotPaths == []: + self.previousDepotPaths = depotPaths + else: + paths = [] + for (prev, cur) in zip(self.previousDepotPaths, depotPaths): + for i in range(0, min(len(cur), len(prev))): + if cur[i] <> prev[i]: + i = i - 1 + break + + paths.append (cur[:i + 1]) + + self.previousDepotPaths = paths + + if p4Change > 0: + self.depotPaths = sorted(self.previousDepotPaths) + self.changeRange = "@%s,#head" % p4Change + if not self.detectBranches: + self.initialParent = parseRevision(self.branch) + if not self.silent and not self.detectBranches: + print "Performing incremental import into %s git branch" % self.branch + + if not self.branch.startswith("refs/"): + self.branch = "refs/heads/" + self.branch + + if len(args) == 0 and self.depotPaths: + if not self.silent: + print "Depot paths: %s" % ' '.join(self.depotPaths) + else: + if self.depotPaths and self.depotPaths != args: + print ("previous import used depot path %s and now %s was specified. " + "This doesn't work!" % (' '.join (self.depotPaths), + ' '.join (args))) + sys.exit(1) + + self.depotPaths = sorted(args) + + revision = "" + self.users = {} + + newPaths = [] + for p in self.depotPaths: + if p.find("@") != -1: + atIdx = p.index("@") + self.changeRange = p[atIdx:] + if self.changeRange == "@all": + self.changeRange = "" + elif ',' not in self.changeRange: + revision = self.changeRange + self.changeRange = "" + p = p[:atIdx] + elif p.find("#") != -1: + hashIdx = p.index("#") + revision = p[hashIdx:] + p = p[:hashIdx] + elif self.previousDepotPaths == []: + revision = "#head" + + p = re.sub ("\.\.\.$", "", p) + if not p.endswith("/"): + p += "/" + + newPaths.append(p) + + self.depotPaths = newPaths + + + self.loadUserMapFromCache() + self.labels = {} + if self.detectLabels: + self.getLabels(); + + if self.detectBranches: + ## FIXME - what's a P4 projectName ? + self.projectName = self.guessProjectName() + + if self.hasOrigin: + self.getBranchMappingFromGitBranches() + else: + self.getBranchMapping() + if self.verbose: + print "p4-git branches: %s" % self.p4BranchesInGit + print "initial parents: %s" % self.initialParents + for b in self.p4BranchesInGit: + if b != "master": + + ## FIXME + b = b[len(self.projectName):] + self.createdBranches.add(b) + + self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60)) + + importProcess = subprocess.Popen(["git", "fast-import"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE); + self.gitOutput = importProcess.stdout + self.gitStream = importProcess.stdin + self.gitError = importProcess.stderr + + if revision: + self.importHeadRevision(revision) + else: + changes = [] + + if len(self.changesFile) > 0: + output = open(self.changesFile).readlines() + changeSet = Set() + for line in output: + changeSet.add(int(line)) + + for change in changeSet: + changes.append(change) + + changes.sort() + else: + if self.verbose: + print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths), + self.changeRange) + changes = p4ChangesForPaths(self.depotPaths, self.changeRange) + + if len(self.maxChanges) > 0: + changes = changes[:min(int(self.maxChanges), len(changes))] + + if len(changes) == 0: + if not self.silent: + print "No changes to import!" + return True + + if not self.silent and not self.detectBranches: + print "Import destination: %s" % self.branch + + self.updatedBranches = set() + + self.importChanges(changes) + + if not self.silent: + print "" + if len(self.updatedBranches) > 0: + sys.stdout.write("Updated branches: ") + for b in self.updatedBranches: + sys.stdout.write("%s " % b) + sys.stdout.write("\n") + + self.gitStream.close() + if importProcess.wait() != 0: + die("fast-import failed: %s" % self.gitError.read()) + self.gitOutput.close() + self.gitError.close() + + return True + +class P4Rebase(Command): + def __init__(self): + Command.__init__(self) + self.options = [ ] + self.description = ("Fetches the latest revision from perforce and " + + "rebases the current work (branch) against it") + self.verbose = False + + def run(self, args): + sync = P4Sync() + sync.run([]) + + return self.rebase() + + def rebase(self): + if os.system("git update-index --refresh") != 0: + die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up-to-date or stash away all your changes with git stash."); + if len(read_pipe("git diff-index HEAD --")) > 0: + die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash."); + + [upstream, settings] = findUpstreamBranchPoint() + if len(upstream) == 0: + die("Cannot find upstream branchpoint for rebase") + + # the branchpoint may be p4/foo~3, so strip off the parent + upstream = re.sub("~[0-9]+$", "", upstream) + + print "Rebasing the current branch onto %s" % upstream + oldHead = read_pipe("git rev-parse HEAD").strip() + system("git rebase %s" % upstream) + system("git diff-tree --stat --summary -M %s HEAD" % oldHead) + return True + +class P4Clone(P4Sync): + def __init__(self): + P4Sync.__init__(self) + self.description = "Creates a new git repository and imports from Perforce into it" + self.usage = "usage: %prog [options] //depot/path[@revRange]" + self.options += [ + optparse.make_option("--destination", dest="cloneDestination", + action='store', default=None, + help="where to leave result of the clone"), + optparse.make_option("-/", dest="cloneExclude", + action="append", type="string", + help="exclude depot path") + ] + self.cloneDestination = None + self.needsGit = False + + # This is required for the "append" cloneExclude action + def ensure_value(self, attr, value): + if not hasattr(self, attr) or getattr(self, attr) is None: + setattr(self, attr, value) + return getattr(self, attr) + + def defaultDestination(self, args): + ## TODO: use common prefix of args? + depotPath = args[0] + depotDir = re.sub("(@[^@]*)$", "", depotPath) + depotDir = re.sub("(#[^#]*)$", "", depotDir) + depotDir = re.sub(r"\.\.\.$", "", depotDir) + depotDir = re.sub(r"/$", "", depotDir) + return os.path.split(depotDir)[1] + + def run(self, args): + if len(args) < 1: + return False + + if self.keepRepoPath and not self.cloneDestination: + sys.stderr.write("Must specify destination for --keep-path\n") + sys.exit(1) + + depotPaths = args + + if not self.cloneDestination and len(depotPaths) > 1: + self.cloneDestination = depotPaths[-1] + depotPaths = depotPaths[:-1] + + self.cloneExclude = ["/"+p for p in self.cloneExclude] + for p in depotPaths: + if not p.startswith("//"): + return False + + if not self.cloneDestination: + self.cloneDestination = self.defaultDestination(args) + + print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination) + if not os.path.exists(self.cloneDestination): + os.makedirs(self.cloneDestination) + os.chdir(self.cloneDestination) + system("git init") + self.gitdir = os.getcwd() + "/.git" + if not P4Sync.run(self, depotPaths): + return False + if self.branch != "master": + if gitBranchExists("refs/remotes/p4/master"): + system("git branch master refs/remotes/p4/master") + system("git checkout -f") + else: + print "Could not detect main branch. No checkout/master branch created." + + return True + +class P4Branches(Command): + def __init__(self): + Command.__init__(self) + self.options = [ ] + self.description = ("Shows the git branches that hold imports and their " + + "corresponding perforce depot paths") + self.verbose = False + + def run(self, args): + if originP4BranchesExist(): + createOrUpdateBranchesFromOrigin() + + cmdline = "git rev-parse --symbolic " + cmdline += " --remotes" + + for line in read_pipe_lines(cmdline): + line = line.strip() + + if not line.startswith('p4/') or line == "p4/HEAD": + continue + branch = line + + log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch) + settings = extractSettingsGitLog(log) + + print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]) + return True + +class HelpFormatter(optparse.IndentedHelpFormatter): + def __init__(self): + optparse.IndentedHelpFormatter.__init__(self) + + def format_description(self, description): + if description: + return description + "\n" + else: + return "" + +def printUsage(commands): + print "usage: %s <command> [options]" % sys.argv[0] + print "" + print "valid commands: %s" % ", ".join(commands) + print "" + print "Try %s <command> --help for command specific help." % sys.argv[0] + print "" + +commands = { + "debug" : P4Debug, + "submit" : P4Submit, + "commit" : P4Submit, + "sync" : P4Sync, + "rebase" : P4Rebase, + "clone" : P4Clone, + "rollback" : P4RollBack, + "branches" : P4Branches +} + + +def main(): + if len(sys.argv[1:]) == 0: + printUsage(commands.keys()) + sys.exit(2) + + cmd = "" + cmdName = sys.argv[1] + try: + klass = commands[cmdName] + cmd = klass() + except KeyError: + print "unknown command %s" % cmdName + print "" + printUsage(commands.keys()) + sys.exit(2) + + options = cmd.options + cmd.gitdir = os.environ.get("GIT_DIR", None) + + args = sys.argv[2:] + + if len(options) > 0: + options.append(optparse.make_option("--git-dir", dest="gitdir")) + + parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName), + options, + description = cmd.description, + formatter = HelpFormatter()) + + (cmd, args) = parser.parse_args(sys.argv[2:], cmd); + global verbose + verbose = cmd.verbose + if cmd.needsGit: + if cmd.gitdir == None: + cmd.gitdir = os.path.abspath(".git") + if not isValidGitDir(cmd.gitdir): + 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() + if len(cdup) > 0: + os.chdir(cdup); + + if not isValidGitDir(cmd.gitdir): + if isValidGitDir(cmd.gitdir + "/.git"): + cmd.gitdir += "/.git" + else: + die("fatal: cannot locate git repository at %s" % cmd.gitdir) + + os.environ["GIT_DIR"] = cmd.gitdir + + if not cmd.run(args): + parser.print_help() + + +if __name__ == '__main__': + main() diff --git a/contrib/fast-import/git-p4.bat b/contrib/fast-import/git-p4.bat new file mode 100644 index 0000000000..9f97e884f5 --- /dev/null +++ b/contrib/fast-import/git-p4.bat @@ -0,0 +1 @@ +@python "%~d0%~p0git-p4" %* diff --git a/contrib/fast-import/git-p4.txt b/contrib/fast-import/git-p4.txt new file mode 100644 index 0000000000..b16a8384bc --- /dev/null +++ b/contrib/fast-import/git-p4.txt @@ -0,0 +1,159 @@ +git-p4 - Perforce <-> Git converter using git-fast-import + +Usage +===== + +git-p4 supports two main modes: Importing from Perforce to a Git repository is +done using "git-p4 sync" or "git-p4 rebase". Submitting changes from Git back +to Perforce is done using "git-p4 submit". + +Importing +========= + +You can simply start with + + git-p4 clone //depot/path/project + +or + + git-p4 clone //depot/path/project myproject + +This will create an empty git repository in a subdirectory called "project" (or +"myproject" with the second command), import the head revision from the +specified perforce path into a git "p4" branch (remotes/p4 actually), create a +master branch off it and check it out. If you want the entire history (not just +the head revision) then you can simply append a "@all" to the depot path: + + git-p4 clone //depot/project/main@all myproject + + + +If you want more control you can also use the git-p4 sync command directly: + + mkdir repo-git + cd repo-git + git init + git-p4 sync //path/in/your/perforce/depot + +This will import the current head revision of the specified depot path into a +"remotes/p4/master" branch of your git repository. You can use the +--branch=mybranch option to use a different branch. + +If you want to import the entire history of a given depot path just use + + git-p4 sync //path/in/depot@all + +To achieve optimal compression you may want to run 'git repack -a -d -f' after +a big import. This may take a while. + +Support for Perforce integrations is still work in progress. Don't bother +trying it unless you want to hack on it :) + +Incremental Imports +=================== + +After an initial import you can easily synchronize your git repository with +newer changes from the Perforce depot by just calling + + git-p4 sync + +in your git repository. By default the "remotes/p4/master" branch is updated. + +It is recommended to run 'git repack -a -d -f' from time to time when using +incremental imports to optimally combine the individual git packs that each +incremental import creates through the use of git-fast-import. + + +A useful setup may be that you have a periodically updated git repository +somewhere that contains a complete import of a Perforce project. That git +repository can be used to clone the working repository from and one would +import from Perforce directly after cloning using git-p4. If the connection to +the Perforce server is slow and the working repository hasn't been synced for a +while it may be desirable to fetch changes from the origin git repository using +the efficient git protocol. git-p4 supports this setup by calling "git fetch origin" +by default if there is an origin branch. You can disable this using + + git config git-p4.syncFromOrigin false + +Updating +======== + +A common working pattern is to fetch the latest changes from the Perforce depot +and merge them with local uncommitted changes. The recommended way is to use +git's rebase mechanism to preserve linear history. git-p4 provides a convenient + + git-p4 rebase + +command that calls git-p4 sync followed by git rebase to rebase the current +working branch. + +Submitting +========== + +git-p4 has support for submitting changes from a git repository back to the +Perforce depot. This requires a Perforce checkout separate to your git +repository. To submit all changes that are in the current git branch but not in +the "p4" branch (or "origin" if "p4" doesn't exist) simply call + + git-p4 submit + +in your git repository. If you want to submit changes in a specific branch that +is not your current git branch you can also pass that as an argument: + + git-p4 submit mytopicbranch + +You can override the reference branch with the --origin=mysourcebranch option. + +If a submit fails you may have to "p4 resolve" and submit manually. You can +continue importing the remaining changes with + + git-p4 submit --continue + +After submitting you should sync your perforce import branch ("p4" or "origin") +from Perforce using git-p4's sync command. + +If you have changes in your working directory that you haven't committed into +git yet but that you want to commit to Perforce directly ("quick fixes") then +you do not have to go through the intermediate step of creating a git commit +first but you can just call + + git-p4 submit --direct + + +Example +======= + +# Clone a repository + git-p4 clone //depot/path/project +# Enter the newly cloned directory + cd project +# Do some work... + vi foo.h +# ... and commit locally to gi + git commit foo.h +# In the meantime somebody submitted changes to the Perforce depot. Rebase your latest +# changes against the latest changes in Perforce: + git-p4 rebase +# Submit your locally committed changes back to Perforce + git-p4 submit +# ... and synchronize with Perforce + git-p4 rebase + + +Implementation Details... +========================= + +* Changesets from Perforce are imported using git fast-import. +* The import does not require anything from the Perforce client view as it just uses + "p4 print //depot/path/file#revision" to get the actual file contents. +* Every imported changeset has a special [git-p4...] line at the + end of the log message that gives information about the corresponding + Perforce change number and is also used by git-p4 itself to find out + where to continue importing when doing incremental imports. + Basically when syncing it extracts the perforce change number of the + latest commit in the "p4" branch and uses "p4 changes //depot/path/...@changenum,#head" + to find out which changes need to be imported. +* git-p4 submit uses "git rev-list" to pick the commits between the "p4" branch + and the current branch. + The commits themselves are applied using git diff/format-patch ... | git apply + diff --git a/contrib/fast-import/import-tars.perl b/contrib/fast-import/import-tars.perl new file mode 100755 index 0000000000..23aeb257b9 --- /dev/null +++ b/contrib/fast-import/import-tars.perl @@ -0,0 +1,131 @@ +#!/usr/bin/perl + +## tar archive frontend for git-fast-import +## +## For example: +## +## mkdir project; cd project; git init +## perl import-tars.perl *.tar.bz2 +## git whatchanged import-tars +## + +use strict; +die "usage: import-tars *.tar.{gz,bz2,Z}\n" unless @ARGV; + +my $branch_name = 'import-tars'; +my $branch_ref = "refs/heads/$branch_name"; +my $committer_name = 'T Ar Creator'; +my $committer_email = 'tar@example.com'; + +open(FI, '|-', 'git', 'fast-import', '--quiet') + or die "Unable to start git fast-import: $!\n"; +foreach my $tar_file (@ARGV) +{ + $tar_file =~ m,([^/]+)$,; + my $tar_name = $1; + + if ($tar_name =~ s/\.(tar\.gz|tgz)$//) { + open(I, '-|', 'gunzip', '-c', $tar_file) + or die "Unable to gunzip -c $tar_file: $!\n"; + } elsif ($tar_name =~ s/\.(tar\.bz2|tbz2)$//) { + open(I, '-|', 'bunzip2', '-c', $tar_file) + or die "Unable to bunzip2 -c $tar_file: $!\n"; + } elsif ($tar_name =~ s/\.tar\.Z$//) { + open(I, '-|', 'uncompress', '-c', $tar_file) + or die "Unable to uncompress -c $tar_file: $!\n"; + } elsif ($tar_name =~ s/\.tar$//) { + open(I, $tar_file) or die "Unable to open $tar_file: $!\n"; + } else { + die "Unrecognized compression format: $tar_file\n"; + } + + my $commit_time = 0; + my $next_mark = 1; + my $have_top_dir = 1; + my ($top_dir, %files); + + while (read(I, $_, 512) == 512) { + my ($name, $mode, $uid, $gid, $size, $mtime, + $chksum, $typeflag, $linkname, $magic, + $version, $uname, $gname, $devmajor, $devminor, + $prefix) = unpack 'Z100 Z8 Z8 Z8 Z12 Z12 + Z8 Z1 Z100 Z6 + Z2 Z32 Z32 Z8 Z8 Z*', $_; + last unless length($name); + if ($name eq '././@LongLink') { + # GNU tar extension + if (read(I, $_, 512) != 512) { + die ('Short archive'); + } + $name = unpack 'Z257', $_; + next unless $name; + + my $dummy; + if (read(I, $_, 512) != 512) { + die ('Short archive'); + } + ($dummy, $mode, $uid, $gid, $size, $mtime, + $chksum, $typeflag, $linkname, $magic, + $version, $uname, $gname, $devmajor, $devminor, + $prefix) = unpack 'Z100 Z8 Z8 Z8 Z12 Z12 + Z8 Z1 Z100 Z6 + Z2 Z32 Z32 Z8 Z8 Z*', $_; + } + next if $name =~ m{/\z}; + $mode = oct $mode; + $size = oct $size; + $mtime = oct $mtime; + next if $typeflag == 5; # directory + + print FI "blob\n", "mark :$next_mark\n", "data $size\n"; + while ($size > 0 && read(I, $_, 512) == 512) { + print FI substr($_, 0, $size); + $size -= 512; + } + print FI "\n"; + + my $path; + if ($prefix) { + $path = "$prefix/$name"; + } else { + $path = "$name"; + } + $files{$path} = [$next_mark++, $mode]; + + $commit_time = $mtime if $mtime > $commit_time; + $path =~ m,^([^/]+)/,; + $top_dir = $1 unless $top_dir; + $have_top_dir = 0 if $top_dir ne $1; + } + + print FI <<EOF; +commit $branch_ref +committer $committer_name <$committer_email> $commit_time +0000 +data <<END_OF_COMMIT_MESSAGE +Imported from $tar_file. +END_OF_COMMIT_MESSAGE + +deleteall +EOF + + foreach my $path (keys %files) + { + my ($mark, $mode) = @{$files{$path}}; + $path =~ s,^([^/]+)/,, if $have_top_dir; + printf FI "M %o :%i %s\n", $mode & 0111 ? 0755 : 0644, $mark, $path; + } + print FI "\n"; + + print FI <<EOF; +tag $tar_name +from $branch_ref +tagger $committer_name <$committer_email> $commit_time +0000 +data <<END_OF_TAG_MESSAGE +Package $tar_name +END_OF_TAG_MESSAGE + +EOF + + close I; +} +close FI; diff --git a/contrib/fast-import/import-zips.py b/contrib/fast-import/import-zips.py new file mode 100755 index 0000000000..c674fa2d1b --- /dev/null +++ b/contrib/fast-import/import-zips.py @@ -0,0 +1,72 @@ +#!/usr/bin/python + +## zip archive frontend for git-fast-import +## +## For example: +## +## mkdir project; cd project; git init +## python import-zips.py *.zip +## git log --stat import-zips + +from os import popen, path +from sys import argv, exit +from time import mktime +from zipfile import ZipFile + +if len(argv) < 2: + print 'Usage:', argv[0], '<zipfile>...' + exit(1) + +branch_ref = 'refs/heads/import-zips' +committer_name = 'Z Ip Creator' +committer_email = 'zip@example.com' + +fast_import = popen('git fast-import --quiet', 'w') +def printlines(list): + for str in list: + fast_import.write(str + "\n") + +for zipfile in argv[1:]: + commit_time = 0 + next_mark = 1 + common_prefix = None + mark = dict() + + zip = ZipFile(zipfile, 'r') + for name in zip.namelist(): + if name.endswith('/'): + continue + info = zip.getinfo(name) + + if commit_time < info.date_time: + commit_time = info.date_time + if common_prefix == None: + common_prefix = name[:name.rfind('/') + 1] + else: + while not name.startswith(common_prefix): + common_prefix = name[:name.rfind('/') + 1] + + mark[name] = ':' + str(next_mark) + next_mark += 1 + + printlines(('blob', 'mark ' + mark[name], \ + 'data ' + str(info.file_size))) + fast_import.write(zip.read(name) + "\n") + + committer = committer_name + ' <' + committer_email + '> %d +0000' % \ + mktime(commit_time + (0, 0, 0)) + + printlines(('commit ' + branch_ref, 'committer ' + committer, \ + 'data <<EOM', 'Imported from ' + zipfile + '.', 'EOM', \ + '', 'deleteall')) + + for name in mark.keys(): + fast_import.write('M 100644 ' + mark[name] + ' ' + + name[len(common_prefix):] + "\n") + + printlines(('', 'tag ' + path.basename(zipfile), \ + 'from ' + branch_ref, 'tagger ' + committer, \ + 'data <<EOM', 'Package ' + zipfile, 'EOM', '')) + +if fast_import.close(): + exit(1) |