diff options
Diffstat (limited to 'contrib')
-rwxr-xr-x | contrib/completion/git-completion.bash | 28 | ||||
-rw-r--r-- | contrib/examples/builtin-fetch--tool.c | 4 | ||||
-rwxr-xr-x | contrib/fast-import/git-p4 | 2758 | ||||
-rw-r--r-- | contrib/fast-import/git-p4.README | 12 | ||||
-rw-r--r-- | contrib/fast-import/git-p4.bat | 1 | ||||
-rw-r--r-- | contrib/subtree/.gitignore | 5 | ||||
-rw-r--r-- | contrib/subtree/COPYING | 339 | ||||
-rw-r--r-- | contrib/subtree/INSTALL | 28 | ||||
-rw-r--r-- | contrib/subtree/Makefile | 52 | ||||
-rw-r--r-- | contrib/subtree/README | 8 | ||||
-rwxr-xr-x | contrib/subtree/git-subtree.sh | 712 | ||||
-rw-r--r-- | contrib/subtree/git-subtree.txt | 366 | ||||
-rw-r--r-- | contrib/subtree/t/Makefile | 69 | ||||
-rwxr-xr-x | contrib/subtree/t/t7900-subtree.sh | 508 | ||||
-rw-r--r-- | contrib/subtree/todo | 50 |
15 files changed, 2166 insertions, 2774 deletions
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 31f714da92..2f60825b21 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -304,16 +304,16 @@ __git_ps1 () fi } -# __gitcomp_1 requires 2 arguments __gitcomp_1 () { - local c IFS=' '$'\t'$'\n' + local c IFS=$' \t\n' for c in $1; do - case "$c$2" in - --*=*) printf %s$'\n' "$c$2" ;; - *.) printf %s$'\n' "$c$2" ;; - *) printf %s$'\n' "$c$2 " ;; + c="$c$2" + case $c in + --*=*|*.) ;; + *) c="$c " ;; esac + printf '%s\n' "$c" done } @@ -676,9 +676,7 @@ __git_complete_revlist_file () *) pfx="$ref:$pfx" ;; esac - local IFS=$'\n' - COMPREPLY=($(compgen -P "$pfx" \ - -W "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \ + __gitcomp_nl "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \ | sed '/^100... blob /{ s,^.* ,, s,$, , @@ -692,7 +690,7 @@ __git_complete_revlist_file () s,$,/, } s/^.* //')" \ - -- "$cur_")) + "$pfx" "$cur_" "" ;; *...*) pfx="${cur_%...*}..." @@ -1658,7 +1656,7 @@ _git_notes () __gitcomp '--ref' ;; ,*) - case "${words[cword-1]}" in + case "$prev" in --ref) __gitcomp_nl "$(__git_refs)" ;; @@ -1684,7 +1682,7 @@ _git_notes () prune,*) ;; *) - case "${words[cword-1]}" in + case "$prev" in -m|-F) ;; *) @@ -2623,8 +2621,9 @@ _git () case "$i" in --git-dir=*) __git_dir="${i#--git-dir=}" ;; --bare) __git_dir="." ;; - --version|-p|--paginate) ;; --help) command="help"; break ;; + -c) c=$((++c)) ;; + -*) ;; *) command="$i"; break ;; esac ((c++)) @@ -2639,9 +2638,12 @@ _git () --bare --version --exec-path + --exec-path= --html-path + --info-path --work-tree= --namespace= + --no-replace-objects --help " ;; diff --git a/contrib/examples/builtin-fetch--tool.c b/contrib/examples/builtin-fetch--tool.c index 3140e405fa..0d54aa7061 100644 --- a/contrib/examples/builtin-fetch--tool.c +++ b/contrib/examples/builtin-fetch--tool.c @@ -518,7 +518,7 @@ int cmd_fetch__tool(int argc, const char **argv, const char *prefix) filename = git_path("FETCH_HEAD"); fp = fopen(filename, "a"); if (!fp) - return error("cannot open %s: %s\n", filename, strerror(errno)); + return error("cannot open %s: %s", filename, strerror(errno)); result = append_fetch_head(fp, argv[2], argv[3], argv[4], argv[5], argv[6], !!argv[7][0], @@ -536,7 +536,7 @@ int cmd_fetch__tool(int argc, const char **argv, const char *prefix) filename = git_path("FETCH_HEAD"); fp = fopen(filename, "a"); if (!fp) - return error("cannot open %s: %s\n", filename, strerror(errno)); + return error("cannot open %s: %s", filename, strerror(errno)); result = fetch_native_store(fp, argv[2], argv[3], argv[4], verbose, force); fclose(fp); diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 deleted file mode 100755 index c5362c4c11..0000000000 --- a/contrib/fast-import/git-p4 +++ /dev/null @@ -1,2758 +0,0 @@ -#!/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, subprocess, shelve -import tempfile, getopt, os.path, time, platform -import re, shutil - -verbose = False - - -def p4_build_cmd(cmd): - """Build a suitable p4 command line. - - This consolidates building and returning a p4 command line into one - location. It means that hooking into the environment, or other configuration - can be done more easily. - """ - real_cmd = ["p4"] - - user = gitConfig("git-p4.user") - if len(user) > 0: - real_cmd += ["-u",user] - - password = gitConfig("git-p4.password") - if len(password) > 0: - real_cmd += ["-P", password] - - port = gitConfig("git-p4.port") - if len(port) > 0: - real_cmd += ["-p", port] - - host = gitConfig("git-p4.host") - if len(host) > 0: - real_cmd += ["-H", host] - - client = gitConfig("git-p4.client") - if len(client) > 0: - real_cmd += ["-c", client] - - - if isinstance(cmd,basestring): - real_cmd = ' '.join(real_cmd) + ' ' + cmd - else: - real_cmd += cmd - return real_cmd - -def chdir(dir): - # P4 uses the PWD environment variable rather than getcwd(). Since we're - # not using the shell, we have to set it ourselves. This path could - # be relative, so go there first, then figure out where we ended up. - os.chdir(dir) - os.environ['PWD'] = os.getcwd() - -def die(msg): - if verbose: - raise Exception(msg) - else: - sys.stderr.write(msg + "\n") - sys.exit(1) - -def write_pipe(c, stdin): - if verbose: - sys.stderr.write('Writing pipe: %s\n' % str(c)) - - expand = isinstance(c,basestring) - p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand) - pipe = p.stdin - val = pipe.write(stdin) - pipe.close() - if p.wait(): - die('Command failed: %s' % str(c)) - - return val - -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): - if verbose: - sys.stderr.write('Reading pipe: %s\n' % str(c)) - - expand = isinstance(c,basestring) - p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) - pipe = p.stdout - val = pipe.read() - if p.wait() and not ignore_error: - die('Command failed: %s' % str(c)) - - return val - -def p4_read_pipe(c, ignore_error=False): - real_cmd = p4_build_cmd(c) - return read_pipe(real_cmd, ignore_error) - -def read_pipe_lines(c): - if verbose: - sys.stderr.write('Reading pipe: %s\n' % str(c)) - - expand = isinstance(c, basestring) - p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) - pipe = p.stdout - val = pipe.readlines() - if pipe.close() or p.wait(): - die('Command failed: %s' % str(c)) - - return val - -def p4_read_pipe_lines(c): - """Specifically invoke p4 on the command supplied. """ - real_cmd = p4_build_cmd(c) - return read_pipe_lines(real_cmd) - -def system(cmd): - expand = isinstance(cmd,basestring) - if verbose: - sys.stderr.write("executing %s\n" % str(cmd)) - subprocess.check_call(cmd, shell=expand) - -def p4_system(cmd): - """Specifically invoke p4 as the system command. """ - real_cmd = p4_build_cmd(cmd) - expand = isinstance(real_cmd, basestring) - subprocess.check_call(real_cmd, shell=expand) - -def p4_integrate(src, dest): - p4_system(["integrate", "-Dt", src, dest]) - -def p4_sync(path): - p4_system(["sync", path]) - -def p4_add(f): - p4_system(["add", f]) - -def p4_delete(f): - p4_system(["delete", f]) - -def p4_edit(f): - p4_system(["edit", f]) - -def p4_revert(f): - p4_system(["revert", f]) - -def p4_reopen(type, file): - p4_system(["reopen", "-t", type, file]) - -# -# Canonicalize the p4 type and return a tuple of the -# base type, plus any modifiers. See "p4 help filetypes" -# for a list and explanation. -# -def split_p4_type(p4type): - - p4_filetypes_historical = { - "ctempobj": "binary+Sw", - "ctext": "text+C", - "cxtext": "text+Cx", - "ktext": "text+k", - "kxtext": "text+kx", - "ltext": "text+F", - "tempobj": "binary+FSw", - "ubinary": "binary+F", - "uresource": "resource+F", - "uxbinary": "binary+Fx", - "xbinary": "binary+x", - "xltext": "text+Fx", - "xtempobj": "binary+Swx", - "xtext": "text+x", - "xunicode": "unicode+x", - "xutf16": "utf16+x", - } - if p4type in p4_filetypes_historical: - p4type = p4_filetypes_historical[p4type] - mods = "" - s = p4type.split("+") - base = s[0] - mods = "" - if len(s) > 1: - mods = s[1] - return (base, mods) - -# -# return the raw p4 type of a file (text, text+ko, etc) -# -def p4_type(file): - results = p4CmdList(["fstat", "-T", "headType", file]) - return results[0]['headType'] - -# -# Given a type base and modifier, return a regexp matching -# the keywords that can be expanded in the file -# -def p4_keywords_regexp_for_type(base, type_mods): - if base in ("text", "unicode", "binary"): - kwords = None - if "ko" in type_mods: - kwords = 'Id|Header' - elif "k" in type_mods: - kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision' - else: - return None - pattern = r""" - \$ # Starts with a dollar, followed by... - (%s) # one of the keywords, followed by... - (:[^$]+)? # possibly an old expansion, followed by... - \$ # another dollar - """ % kwords - return pattern - else: - return None - -# -# Given a file, return a regexp matching the possible -# RCS keywords that will be expanded, or None for files -# with kw expansion turned off. -# -def p4_keywords_regexp_for_file(file): - if not os.path.exists(file): - return None - else: - (type_base, type_mods) = split_p4_type(p4_type(file)) - return p4_keywords_regexp_for_type(type_base, type_mods) - -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] - - p4_reopen(p4Type, file) - -def getP4OpenedType(file): - # Returns the perforce file type for the given file. - - result = p4_read_pipe(["opened", 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', cb=None): - - if isinstance(cmd,basestring): - cmd = "-G " + cmd - expand = True - else: - cmd = ["-G"] + cmd - expand = False - - cmd = p4_build_cmd(cmd) - if verbose: - sys.stderr.write("Opening pipe: %s\n" % str(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) - if isinstance(stdin,basestring): - stdin_file.write(stdin) - else: - for i in stdin: - stdin_file.write(i + '\n') - stdin_file.flush() - stdin_file.seek(0) - - p4 = subprocess.Popen(cmd, - shell=expand, - stdin=stdin_file, - stdout=subprocess.PIPE) - - result = [] - try: - while True: - entry = marshal.load(p4.stdout) - if cb is not None: - cb(entry) - else: - 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 += "/" - depotPath = depotPath + "..." - outputList = p4CmdList(["where", depotPath]) - output = None - for entry in outputList: - if "depotFile" in entry: - if entry["depotFile"] == depotPath: - output = entry - break - elif "data" in entry: - data = entry.get("data") - space = data.find(" ") - if data[:space] == depotPath: - output = entry - break - if output == None: - return "" - 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 branchExists(ref): - rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref], - ignore_error=True) - return len(rev) > 0 - -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; - -_gitConfig = {} -def gitConfig(key, args = None): # set args to "--bool", for instance - if not _gitConfig.has_key(key): - argsFilter = "" - if args != None: - argsFilter = "%s " % args - cmd = "git config %s%s" % (argsFilter, key) - _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip() - return _gitConfig[key] - -def gitConfigList(key): - if not _gitConfig.has_key(key): - _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep) - return _gitConfig[key] - -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 - cmd = ['changes'] - for p in depotPaths: - cmd += ["%s...%s" % (p, changeRange)] - output = p4_read_pipe_lines(cmd) - - changes = {} - for line in output: - changeNum = int(line.split(" ")[1]) - changes[changeNum] = True - - changelist = changes.keys() - changelist.sort() - return changelist - -def p4PathStartsWith(path, prefix): - # This method tries to remedy a potential mixed-case issue: - # - # If UserA adds //depot/DirA/file1 - # and UserB adds //depot/dira/file2 - # - # we may or may not have a problem. If you have core.ignorecase=true, - # we treat DirA and dira as the same directory - ignorecase = gitConfig("core.ignorecase", "--bool") == "true" - if ignorecase: - return path.lower().startswith(prefix.lower()) - return path.startswith(prefix) - -def getClientSpec(): - """Look at the p4 client spec, create a View() object that contains - all the mappings, and return it.""" - - specList = p4CmdList("client -o") - if len(specList) != 1: - die('Output from "client -o" is %d lines, expecting 1' % - len(specList)) - - # dictionary of all client parameters - entry = specList[0] - - # just the keys that start with "View" - view_keys = [ k for k in entry.keys() if k.startswith("View") ] - - # hold this new View - view = View() - - # append the lines, in order, to the view - for view_num in range(len(view_keys)): - k = "View%d" % view_num - if k not in view_keys: - die("Expected view key %s missing" % k) - view.append(entry[k]) - - return view - -def getClientRoot(): - """Grab the client directory.""" - - output = p4CmdList("client -o") - if len(output) != 1: - die('Output from "client -o" is %d lines, expecting 1' % len(output)) - - entry = output[0] - if "Root" not in entry: - die('Client has no "Root"') - - return entry["Root"] - -class Command: - def __init__(self): - self.usage = "usage: %prog [options]" - self.needsGit = True - -class P4UserMap: - def __init__(self): - self.userMapFromPerforceServer = False - self.myP4UserId = None - - def p4UserId(self): - if self.myP4UserId: - return self.myP4UserId - - results = p4CmdList("user -o") - for r in results: - if r.has_key('User'): - self.myP4UserId = r['User'] - return r['User'] - die("Could not find your p4 user id") - - def p4UserIsMe(self, p4User): - # return True if the given p4 user is actually me - me = self.p4UserId() - if not p4User or p4User != me: - return False - else: - return True - - 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 = {} - self.emails = {} - - for output in p4CmdList("users"): - if not output.has_key("User"): - continue - self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">" - self.emails[output["Email"]] = output["User"] - - - s = '' - for (key, val) in self.users.items(): - s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1)) - - 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() - -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(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, P4UserMap): - def __init__(self): - Command.__init__(self) - P4UserMap.__init__(self) - self.options = [ - optparse.make_option("--verbose", dest="verbose", action="store_true"), - optparse.make_option("--origin", dest="origin"), - optparse.make_option("-M", dest="detectRenames", action="store_true"), - # preserve the user, requires relevant p4 permissions - optparse.make_option("--preserve-user", dest="preserveUser", 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.detectRenames = False - self.verbose = False - self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true" - 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:") or line.startswith("Jobs:"): - 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 patchRCSKeywords(self, file, pattern): - # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern - (handle, outFileName) = tempfile.mkstemp(dir='.') - try: - outFile = os.fdopen(handle, "w+") - inFile = open(file, "r") - regexp = re.compile(pattern, re.VERBOSE) - for line in inFile.readlines(): - line = regexp.sub(r'$\1$', line) - outFile.write(line) - inFile.close() - outFile.close() - # Forcibly overwrite the original file - os.unlink(file) - shutil.move(outFileName, file) - except: - # cleanup our temporary file - os.unlink(outFileName) - print "Failed to strip RCS keywords in %s" % file - raise - - print "Patched up RCS keywords in %s" % file - - def p4UserForCommit(self,id): - # Return the tuple (perforce user,git email) for a given git commit id - self.getUserMapFromPerforceServer() - gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id) - gitEmail = gitEmail.strip() - if not self.emails.has_key(gitEmail): - return (None,gitEmail) - else: - return (self.emails[gitEmail],gitEmail) - - def checkValidP4Users(self,commits): - # check if any git authors cannot be mapped to p4 users - for id in commits: - (user,email) = self.p4UserForCommit(id) - if not user: - msg = "Cannot find p4 user for email %s in commit %s." % (email, id) - if gitConfig('git-p4.allowMissingP4Users').lower() == "true": - print "%s" % msg - else: - die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg) - - def lastP4Changelist(self): - # Get back the last changelist number submitted in this client spec. This - # then gets used to patch up the username in the change. If the same - # client spec is being used by multiple processes then this might go - # wrong. - results = p4CmdList("client -o") # find the current client - client = None - for r in results: - if r.has_key('Client'): - client = r['Client'] - break - if not client: - die("could not get client spec") - results = p4CmdList(["changes", "-c", client, "-m", "1"]) - for r in results: - if r.has_key('change'): - return r['change'] - die("Could not get changelist number for last submit - cannot patch up user details") - - def modifyChangelistUser(self, changelist, newUser): - # fixup the user field of a changelist after it has been submitted. - changes = p4CmdList("change -o %s" % changelist) - if len(changes) != 1: - die("Bad output from p4 change modifying %s to user %s" % - (changelist, newUser)) - - c = changes[0] - if c['User'] == newUser: return # nothing to do - c['User'] = newUser - input = marshal.dumps(c) - - result = p4CmdList("change -f -i", stdin=input) - for r in result: - if r.has_key('code'): - if r['code'] == 'error': - die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data'])) - if r.has_key('data'): - print("Updated user field for changelist %s to %s" % (changelist, newUser)) - return - die("Could not modify user field of changelist %s to %s" % (changelist, newUser)) - - def canChangeChangelists(self): - # check to see if we have p4 admin or super-user permissions, either of - # which are required to modify changelists. - results = p4CmdList(["protects", self.depotPath]) - for r in results: - if r.has_key('perm'): - if r['perm'] == 'admin': - return 1 - if r['perm'] == 'super': - return 1 - return 0 - - 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 p4_read_pipe_lines(['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 p4PathStartsWith(path, self.depotPath): - continue - else: - inFilesSection = False - else: - if line.startswith("Files:"): - inFilesSection = True - - template += line - - return template - - def edit_template(self, template_file): - """Invoke the editor to let the user change the submission - message. Return true if okay to continue with the submit.""" - - # if configured to skip the editing part, just submit - if gitConfig("git-p4.skipSubmitEdit") == "true": - return True - - # look at the modification time, to check later if the user saved - # the file - mtime = os.stat(template_file).st_mtime - - # invoke the editor - if os.environ.has_key("P4EDITOR"): - editor = os.environ.get("P4EDITOR") - else: - editor = read_pipe("git var GIT_EDITOR").strip() - system(editor + " " + template_file) - - # If the file was not saved, prompt to see if this patch should - # be skipped. But skip this verification step if configured so. - if gitConfig("git-p4.skipSubmitEditCheck") == "true": - return True - - # modification time updated means user saved the file - if os.stat(template_file).st_mtime > mtime: - return True - - while True: - response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ") - if response == 'y': - return True - if response == 'n': - return False - - def applyCommit(self, id): - print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id)) - - (p4User, gitEmail) = self.p4UserForCommit(id) - - if not self.detectRenames: - # If not explicitly set check the config variable - self.detectRenames = gitConfig("git-p4.detectRenames") - - if self.detectRenames.lower() == "false" or self.detectRenames == "": - diffOpts = "" - elif self.detectRenames.lower() == "true": - diffOpts = "-M" - else: - diffOpts = "-M%s" % self.detectRenames - - detectCopies = gitConfig("git-p4.detectCopies") - if detectCopies.lower() == "true": - diffOpts += " -C" - elif detectCopies != "" and detectCopies.lower() != "false": - diffOpts += " -C%s" % detectCopies - - if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true": - diffOpts += " --find-copies-harder" - - 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": - p4_edit(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 == "C": - src, dest = diff['src'], diff['dst'] - p4_integrate(src, dest) - if diff['src_sha1'] != diff['dst_sha1']: - p4_edit(dest) - if isModeExecChanged(diff['src_mode'], diff['dst_mode']): - p4_edit(dest) - filesToChangeExecBit[dest] = diff['dst_mode'] - os.unlink(dest) - editedFiles.add(dest) - elif modifier == "R": - src, dest = diff['src'], diff['dst'] - p4_integrate(src, dest) - if diff['src_sha1'] != diff['dst_sha1']: - p4_edit(dest) - if isModeExecChanged(diff['src_mode'], diff['dst_mode']): - p4_edit(dest) - 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 -" - patch_succeeded = True - - if os.system(tryPatchCmd) != 0: - fixed_rcs_keywords = False - patch_succeeded = False - print "Unfortunately applying the change failed!" - - # Patch failed, maybe it's just RCS keyword woes. Look through - # the patch to see if that's possible. - if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true": - file = None - pattern = None - kwfiles = {} - for file in editedFiles | filesToDelete: - # did this file's delta contain RCS keywords? - pattern = p4_keywords_regexp_for_file(file) - - if pattern: - # this file is a possibility...look for RCS keywords. - regexp = re.compile(pattern, re.VERBOSE) - for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]): - if regexp.search(line): - if verbose: - print "got keyword match on %s in %s in %s" % (pattern, line, file) - kwfiles[file] = pattern - break - - for file in kwfiles: - if verbose: - print "zapping %s with %s" % (line,pattern) - self.patchRCSKeywords(file, kwfiles[file]) - fixed_rcs_keywords = True - - if fixed_rcs_keywords: - print "Retrying the patch with RCS keywords cleaned up" - if os.system(tryPatchCmd) == 0: - patch_succeeded = True - - if not patch_succeeded: - 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: - p4_revert(f) - for f in filesToAdd: - os.remove(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: - p4_add(f) - for f in filesToDelete: - p4_revert(f) - p4_delete(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 self.preserveUser: - submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User) - - if os.environ.has_key("P4DIFF"): - del(os.environ["P4DIFF"]) - diff = "" - for editedFile in editedFiles: - diff += p4_read_pipe(['diff', '-du', editedFile]) - - 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() - - if self.checkAuthorship and not self.p4UserIsMe(p4User): - submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail - submitTemplate += "######## Use git-p4 option --preserve-user to modify authorship\n" - submitTemplate += "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n" - - 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() - - if self.edit_template(fileName): - # read the edited message and submit - tmpFile = open(fileName, "rb") - message = tmpFile.read() - tmpFile.close() - submitTemplate = message[:message.index(separatorLine)] - if self.isWindows: - submitTemplate = submitTemplate.replace("\r\n", "\n") - p4_write_pipe(['submit', '-i'], submitTemplate) - - if self.preserveUser: - if p4User: - # Get last changelist number. Cannot easily get it from - # the submit command output as the output is - # unmarshalled. - changelist = self.lastP4Changelist() - self.modifyChangelistUser(changelist, p4User) - else: - # skip this patch - print "Submission cancelled, undoing p4 changes." - for f in editedFiles: - p4_revert(f) - for f in filesToAdd: - p4_revert(f) - os.remove(f) - - os.remove(fileName) - 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] - if not branchExists(self.master): - die("Branch %s does not exist" % self.master) - 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.preserveUser: - if not self.canChangeChangelists(): - die("Cannot preserve user names without p4 super-user or admin permissions") - - 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.useClientSpec = False - if gitConfig("git-p4.useclientspec", "--bool") == "true": - self.useClientSpec = True - if self.useClientSpec: - self.clientSpecDirs = getClientSpec() - - if self.useClientSpec: - # all files are relative to the client spec - self.clientPath = getClientRoot() - else: - self.clientPath = p4Where(self.depotPath) - - if self.clientPath == "": - die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath) - - print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath) - self.oldWorkingDirectory = os.getcwd() - - # ensure the clientPath exists - if not os.path.exists(self.clientPath): - os.makedirs(self.clientPath) - - chdir(self.clientPath) - print "Synchronizing p4 checkout..." - 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() - - if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"): - self.checkAuthorship = False - else: - self.checkAuthorship = True - - if self.preserveUser: - self.checkValidP4Users(commits) - - 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!" - chdir(self.oldWorkingDirectory) - - sync = P4Sync() - sync.run([]) - - rebase = P4Rebase() - rebase.rebase() - - return True - -class View(object): - """Represent a p4 view ("p4 help views"), and map files in a - repo according to the view.""" - - class Path(object): - """A depot or client path, possibly containing wildcards. - The only one supported is ... at the end, currently. - Initialize with the full path, with //depot or //client.""" - - def __init__(self, path, is_depot): - self.path = path - self.is_depot = is_depot - self.find_wildcards() - # remember the prefix bit, useful for relative mappings - m = re.match("(//[^/]+/)", self.path) - if not m: - die("Path %s does not start with //prefix/" % self.path) - prefix = m.group(1) - if not self.is_depot: - # strip //client/ on client paths - self.path = self.path[len(prefix):] - - def find_wildcards(self): - """Make sure wildcards are valid, and set up internal - variables.""" - - self.ends_triple_dot = False - # There are three wildcards allowed in p4 views - # (see "p4 help views"). This code knows how to - # handle "..." (only at the end), but cannot deal with - # "%%n" or "*". Only check the depot_side, as p4 should - # validate that the client_side matches too. - if re.search(r'%%[1-9]', self.path): - die("Can't handle %%n wildcards in view: %s" % self.path) - if self.path.find("*") >= 0: - die("Can't handle * wildcards in view: %s" % self.path) - triple_dot_index = self.path.find("...") - if triple_dot_index >= 0: - if triple_dot_index != len(self.path) - 3: - die("Can handle only single ... wildcard, at end: %s" % - self.path) - self.ends_triple_dot = True - - def ensure_compatible(self, other_path): - """Make sure the wildcards agree.""" - if self.ends_triple_dot != other_path.ends_triple_dot: - die("Both paths must end with ... if either does;\n" + - "paths: %s %s" % (self.path, other_path.path)) - - def match_wildcards(self, test_path): - """See if this test_path matches us, and fill in the value - of the wildcards if so. Returns a tuple of - (True|False, wildcards[]). For now, only the ... at end - is supported, so at most one wildcard.""" - if self.ends_triple_dot: - dotless = self.path[:-3] - if test_path.startswith(dotless): - wildcard = test_path[len(dotless):] - return (True, [ wildcard ]) - else: - if test_path == self.path: - return (True, []) - return (False, []) - - def match(self, test_path): - """Just return if it matches; don't bother with the wildcards.""" - b, _ = self.match_wildcards(test_path) - return b - - def fill_in_wildcards(self, wildcards): - """Return the relative path, with the wildcards filled in - if there are any.""" - if self.ends_triple_dot: - return self.path[:-3] + wildcards[0] - else: - return self.path - - class Mapping(object): - def __init__(self, depot_side, client_side, overlay, exclude): - # depot_side is without the trailing /... if it had one - self.depot_side = View.Path(depot_side, is_depot=True) - self.client_side = View.Path(client_side, is_depot=False) - self.overlay = overlay # started with "+" - self.exclude = exclude # started with "-" - assert not (self.overlay and self.exclude) - self.depot_side.ensure_compatible(self.client_side) - - def __str__(self): - c = " " - if self.overlay: - c = "+" - if self.exclude: - c = "-" - return "View.Mapping: %s%s -> %s" % \ - (c, self.depot_side.path, self.client_side.path) - - def map_depot_to_client(self, depot_path): - """Calculate the client path if using this mapping on the - given depot path; does not consider the effect of other - mappings in a view. Even excluded mappings are returned.""" - matches, wildcards = self.depot_side.match_wildcards(depot_path) - if not matches: - return "" - client_path = self.client_side.fill_in_wildcards(wildcards) - return client_path - - # - # View methods - # - def __init__(self): - self.mappings = [] - - def append(self, view_line): - """Parse a view line, splitting it into depot and client - sides. Append to self.mappings, preserving order.""" - - # Split the view line into exactly two words. P4 enforces - # structure on these lines that simplifies this quite a bit. - # - # Either or both words may be double-quoted. - # Single quotes do not matter. - # Double-quote marks cannot occur inside the words. - # A + or - prefix is also inside the quotes. - # There are no quotes unless they contain a space. - # The line is already white-space stripped. - # The two words are separated by a single space. - # - if view_line[0] == '"': - # First word is double quoted. Find its end. - close_quote_index = view_line.find('"', 1) - if close_quote_index <= 0: - die("No first-word closing quote found: %s" % view_line) - depot_side = view_line[1:close_quote_index] - # skip closing quote and space - rhs_index = close_quote_index + 1 + 1 - else: - space_index = view_line.find(" ") - if space_index <= 0: - die("No word-splitting space found: %s" % view_line) - depot_side = view_line[0:space_index] - rhs_index = space_index + 1 - - if view_line[rhs_index] == '"': - # Second word is double quoted. Make sure there is a - # double quote at the end too. - if not view_line.endswith('"'): - die("View line with rhs quote should end with one: %s" % - view_line) - # skip the quotes - client_side = view_line[rhs_index+1:-1] - else: - client_side = view_line[rhs_index:] - - # prefix + means overlay on previous mapping - overlay = False - if depot_side.startswith("+"): - overlay = True - depot_side = depot_side[1:] - - # prefix - means exclude this path - exclude = False - if depot_side.startswith("-"): - exclude = True - depot_side = depot_side[1:] - - m = View.Mapping(depot_side, client_side, overlay, exclude) - self.mappings.append(m) - - def map_in_client(self, depot_path): - """Return the relative location in the client where this - depot file should live. Returns "" if the file should - not be mapped in the client.""" - - paths_filled = [] - client_path = "" - - # look at later entries first - for m in self.mappings[::-1]: - - # see where will this path end up in the client - p = m.map_depot_to_client(depot_path) - - if p == "": - # Depot path does not belong in client. Must remember - # this, as previous items should not cause files to - # exist in this path either. Remember that the list is - # being walked from the end, which has higher precedence. - # Overlap mappings do not exclude previous mappings. - if not m.overlay: - paths_filled.append(m.client_side) - - else: - # This mapping matched; no need to search any further. - # But, the mapping could be rejected if the client path - # has already been claimed by an earlier mapping (i.e. - # one later in the list, which we are walking backwards). - already_mapped_in_client = False - for f in paths_filled: - # this is View.Path.match - if f.match(p): - already_mapped_in_client = True - break - if not already_mapped_in_client: - # Include this file, unless it is from a line that - # explicitly said to exclude it. - if not m.exclude: - client_path = p - - # a match, even if rejected, always stops the search - break - - return client_path - -class P4Sync(Command, P4UserMap): - delete_actions = ( "delete", "move/delete", "purge" ) - - def __init__(self): - Command.__init__(self) - P4UserMap.__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.useClientSpec_from_options = False - self.clientSpecDirs = None - self.tempBranches = [] - self.tempBranchLocation = "git-p4-tmp" - - if gitConfig("git-p4.syncFromOrigin") == "false": - self.syncWithOrigin = False - - # - # P4 wildcards are not allowed in filenames. P4 complains - # if you simply add them, but you can force it with "-f", in - # which case it translates them into %xx encoding internally. - # Search for and fix just these four characters. Do % last so - # that fixing it does not inadvertently create new %-escapes. - # - def wildcard_decode(self, path): - # Cannot have * in a filename in windows; untested as to - # what p4 would do in such a case. - if not self.isWindows: - path = path.replace("%2A", "*") - path = path.replace("%23", "#") \ - .replace("%40", "@") \ - .replace("%25", "%") - return path - - # Force a checkpoint in fast-import and wait for it to finish - def checkpoint(self): - self.gitStream.write("checkpoint\n\n") - self.gitStream.write("progress checkpoint\n\n") - out = self.gitOutput.readline() - if self.verbose: - print "checkpoint finished: " + out - - 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 p4PathStartsWith(path, p)]: - found = False - else: - found = [p for p in self.depotPaths - if p4PathStartsWith(path, 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.useClientSpec: - return self.clientSpecDirs.map_in_client(path) - - if self.keepRepoPath: - prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])] - - for p in prefixes: - if p4PathStartsWith(path, 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 p4PathStartsWith(path, 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 - - # output one file from the P4 stream - # - helper for streamP4Files - - def streamOneP4File(self, file, contents): - relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes) - relPath = self.wildcard_decode(relPath) - if verbose: - sys.stderr.write("%s\n" % relPath) - - (type_base, type_mods) = split_p4_type(file["type"]) - - git_mode = "100644" - if "x" in type_mods: - git_mode = "100755" - if type_base == "symlink": - git_mode = "120000" - # p4 print on a symlink contains "target\n"; remove the newline - data = ''.join(contents) - contents = [data[:-1]] - - if type_base == "utf16": - # p4 delivers different text in the python output to -G - # than it does when using "print -o", or normal p4 client - # operations. utf16 is converted to ascii or utf8, perhaps. - # But ascii text saved as -t utf16 is completely mangled. - # Invoke print -o to get the real contents. - text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']]) - contents = [ text ] - - if type_base == "apple": - # Apple filetype files will be streamed as a concatenation of - # its appledouble header and the contents. This is useless - # on both macs and non-macs. If using "print -q -o xx", it - # will create "xx" with the data, and "%xx" with the header. - # This is also not very useful. - # - # Ideally, someday, this script can learn how to generate - # appledouble files directly and import those to git, but - # non-mac machines can never find a use for apple filetype. - print "\nIgnoring apple filetype file %s" % file['depotFile'] - return - - # Perhaps windows wants unicode, utf16 newlines translated too; - # but this is not doing it. - if self.isWindows and type_base == "text": - mangled = [] - for data in contents: - data = data.replace("\r\n", "\n") - mangled.append(data) - contents = mangled - - # Note that we do not try to de-mangle keywords on utf16 files, - # even though in theory somebody may want that. - pattern = p4_keywords_regexp_for_type(type_base, type_mods) - if pattern: - regexp = re.compile(pattern, re.VERBOSE) - text = ''.join(contents) - text = regexp.sub(r'$\1$', text) - contents = [ text ] - - self.gitStream.write("M %s inline %s\n" % (git_mode, relPath)) - - # total length... - length = 0 - for d in contents: - length = length + len(d) - - self.gitStream.write("data %d\n" % length) - for d in contents: - self.gitStream.write(d) - self.gitStream.write("\n") - - def streamOneP4Deletion(self, file): - relPath = self.stripRepoPath(file['path'], self.branchPrefixes) - if verbose: - sys.stderr.write("delete %s\n" % relPath) - self.gitStream.write("D %s\n" % relPath) - - # handle another chunk of streaming data - def streamP4FilesCb(self, marshalled): - - if marshalled.has_key('depotFile') and self.stream_have_file_info: - # start of a new file - output the old one first - self.streamOneP4File(self.stream_file, self.stream_contents) - self.stream_file = {} - self.stream_contents = [] - self.stream_have_file_info = False - - # pick up the new file information... for the - # 'data' field we need to append to our array - for k in marshalled.keys(): - if k == 'data': - self.stream_contents.append(marshalled['data']) - else: - self.stream_file[k] = marshalled[k] - - self.stream_have_file_info = True - - # Stream directly from "p4 files" into "git fast-import" - def streamP4Files(self, files): - filesForCommit = [] - filesToRead = [] - filesToDelete = [] - - for f in files: - # if using a client spec, only add the files that have - # a path in the client - if self.clientSpecDirs: - if self.clientSpecDirs.map_in_client(f['path']) == "": - continue - - filesForCommit.append(f) - if f['action'] in self.delete_actions: - filesToDelete.append(f) - else: - filesToRead.append(f) - - # deleted files... - for f in filesToDelete: - self.streamOneP4Deletion(f) - - if len(filesToRead) > 0: - self.stream_file = {} - self.stream_contents = [] - self.stream_have_file_info = False - - # curry self argument - def streamP4FilesCbSelf(entry): - self.streamP4FilesCb(entry) - - fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead] - - p4CmdList(["-x", "-", "print"], - stdin=fileArgs, - cb=streamP4FilesCbSelf) - - # do the last chunk - if self.stream_file.has_key('depotFile'): - self.streamOneP4File(self.stream_file, self.stream_contents) - - def make_email(self, userid): - if userid in self.users: - return self.users[userid] - else: - return "%s <a@b>" % userid - - def commit(self, details, files, branch, branchPrefixes, parent = ""): - epoch = details["time"] - author = details["user"] - self.branchPrefixes = branchPrefixes - - 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 p4PathStartsWith(f['path'], p)]: - new_files.append (f) - else: - sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path']) - - 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() - committer = "%s %s %s" % (self.make_email(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) - - self.streamP4Files(new_files) - 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"] + ["%s...@%s" % (p, change) - for p in branchPrefixes]) - - if len(files) == len(labelRevisions): - - cleanedFiles = {} - for info in files: - if info["action"] in self.delete_actions: - 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"] - - # Try to use the owner of the p4 label, or failing that, - # the current p4 user id. - if owner: - email = self.make_email(owner) - else: - email = self.make_email(self.p4UserId()) - tagger = "%s %s %s" % (email, epoch, self.tz) - - self.gitStream.write("tagger %s\n" % tagger) - - description = labelDetails["Description"] - self.gitStream.write("data %d\n" % len(description)) - self.gitStream.write(description) - self.gitStream.write("\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 getLabels(self): - self.labels = {} - - l = p4CmdList(["labels"] + ["%s..." % p for p in 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"] + - ["%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() - - user = gitConfig("git-p4.branchUser") - if len(user) > 0: - command = "branches -u %s" % user - else: - command = "branches" - - for info in p4CmdList(command): - details = p4Cmd(["branch", "-o", 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 p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, 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) - - # Perforce does not strictly require branches to be defined, so we also - # check git config for a branch list. - # - # Example of branch definition in git config file: - # [git-p4] - # branchList=main:branchA - # branchList=main:branchB - # branchList=branchA:branchC - configBranches = gitConfigList("git-p4.branchList") - for branch in configBranches: - if branch: - (source, destination) = branch.split(":") - 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 searchParent(self, parent, branch, target): - parentFound = False - for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]): - blob = blob.strip() - if len(read_pipe(["git", "diff-tree", blob, target])) == 0: - parentFound = True - if self.verbose: - print "Found parent of %s in commit %s" % (branch, blob) - break - if parentFound: - return blob - else: - return None - - def importChanges(self, changes): - cnt = 1 - for change in changes: - description = p4Cmd(["describe", str(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] - - blob = None - if len(parent) > 0: - tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change)) - if self.verbose: - print "Creating temporary branch: " + tempBranch - self.commit(description, filesForCommit, tempBranch, [branchPrefix]) - self.tempBranches.append(tempBranch) - self.checkpoint() - blob = self.searchParent(parent, branch, tempBranch) - if blob: - self.commit(description, filesForCommit, branch, [branchPrefix], blob) - else: - if self.verbose: - print "Parent of %s not found. Committing into head of %s" % (branch, parent) - 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 = {} - details["user"] = "git perforce import user" - details["desc"] = ("Initial import of %s from the state at revision %s\n" - % (' '.join(self.depotPaths), revision)) - details["change"] = revision - newestRevision = 0 - - fileCnt = 0 - fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths] - - for info in p4CmdList(["files"] + fileArgs): - - if 'code' in info and info['code'] == 'error': - sys.stderr.write("p4 returned an error: %s\n" - % info['data']) - if info['data'].find("must refer to client") >= 0: - sys.stderr.write("This particular p4 error is misleading.\n") - sys.stderr.write("Perhaps the depot path was misspelled.\n"); - sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths)) - sys.exit(1) - if 'p4ExitCode' in info: - sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode']) - sys.exit(1) - - - change = int(info["change"]) - if change > newestRevision: - newestRevision = change - - if info["action"] in self.delete_actions: - # 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 - - # Use time from top-most change so that all git-p4 clones of - # the same p4 repo have the same commit SHA1s. - res = p4CmdList("describe -s %d" % newestRevision) - newestTime = None - for r in res: - if r.has_key('time'): - newestTime = int(r['time']) - if newestTime is None: - die("\"describe -s\" on newest change %d did not give a time") - details["time"] = newestTime - - 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 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)) - - # accept either the command-line option, or the configuration variable - if self.useClientSpec: - # will use this after clone to set the variable - self.useClientSpec_from_options = True - else: - if gitConfig("git-p4.useclientspec", "--bool") == "true": - self.useClientSpec = True - if self.useClientSpec: - self.clientSpecDirs = 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): - prev_list = prev.split("/") - cur_list = cur.split("/") - for i in range(0, min(len(cur_list), len(prev_list))): - if cur_list[i] <> prev_list[i]: - i = i - 1 - break - - paths.append ("/".join(cur_list[: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 = {} - - # Make sure no revision specifiers are used when --changesfile - # is specified. - bad_changesfile = False - if len(self.changesFile) > 0: - for p in self.depotPaths: - if p.find("@") >= 0 or p.find("#") >= 0: - bad_changesfile = True - break - if bad_changesfile: - die("Option --changesfile is incompatible with revision specifiers") - - 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 == []: - # pay attention to changesfile, if given, else import - # the entire p4 tree at the head revision - if len(self.changesFile) == 0: - 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: - # catch "git-p4 sync" with no new branches, in a repo that - # does not have any existing git-p4 branches - if len(args) == 0 and not self.p4BranchesInGit: - die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here."); - 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() - - # Cleanup temporary branches created during import - if self.tempBranches != []: - for branch in self.tempBranches: - read_pipe("git update-ref -d %s" % branch) - os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation)) - - 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"), - optparse.make_option("--bare", dest="cloneBare", - action="store_true", default=False), - ] - self.cloneDestination = None - self.needsGit = False - self.cloneBare = 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) - chdir(self.cloneDestination) - - init_cmd = [ "git", "init" ] - if self.cloneBare: - init_cmd.append("--bare") - subprocess.check_call(init_cmd) - - if not P4Sync.run(self, depotPaths): - return False - if self.branch != "master": - if self.importIntoRemotes: - masterbranch = "refs/remotes/p4/master" - else: - masterbranch = "refs/heads/p4/master" - if gitBranchExists(masterbranch): - system("git branch master %s" % masterbranch) - if not self.cloneBare: - system("git checkout -f") - else: - print "Could not detect main branch. No checkout/master branch created." - - # auto-set this variable if invoked with --use-client-spec - if self.useClientSpec_from_options: - system("git config --bool git-p4.useclientspec true") - - 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: - if cmd.needsGit: - 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: - 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() - sys.exit(2) - - -if __name__ == '__main__': - main() diff --git a/contrib/fast-import/git-p4.README b/contrib/fast-import/git-p4.README new file mode 100644 index 0000000000..cec5ecfa7c --- /dev/null +++ b/contrib/fast-import/git-p4.README @@ -0,0 +1,12 @@ +The git-p4 script moved to the top-level of the git source directory. + +Invoke it as any other git command, like "git p4 clone", for instance. + +Note that the top-level git-p4.py script is now the source. It is +built using make to git-p4, which will be installed. + +Windows users can copy the git-p4.py source script directly, possibly +invoking it through a batch file called "git-p4.bat" in the same folder. +It should contain just one line: + + @python "%~d0%~p0git-p4.py" %* diff --git a/contrib/fast-import/git-p4.bat b/contrib/fast-import/git-p4.bat deleted file mode 100644 index 9f97e884f5..0000000000 --- a/contrib/fast-import/git-p4.bat +++ /dev/null @@ -1 +0,0 @@ -@python "%~d0%~p0git-p4" %* diff --git a/contrib/subtree/.gitignore b/contrib/subtree/.gitignore new file mode 100644 index 0000000000..7e77c9d022 --- /dev/null +++ b/contrib/subtree/.gitignore @@ -0,0 +1,5 @@ +*~ +git-subtree.xml +git-subtree.1 +mainline +subproj diff --git a/contrib/subtree/COPYING b/contrib/subtree/COPYING new file mode 100644 index 0000000000..d511905c16 --- /dev/null +++ b/contrib/subtree/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/contrib/subtree/INSTALL b/contrib/subtree/INSTALL new file mode 100644 index 0000000000..7ab0cf4509 --- /dev/null +++ b/contrib/subtree/INSTALL @@ -0,0 +1,28 @@ +HOW TO INSTALL git-subtree +========================== + +First, build from the top source directory. + +Then, in contrib/subtree, run: + + make + make install + make install-doc + +If you used configure to do the main build the git-subtree build will +pick up those settings. If not, you will likely have to provide a +value for prefix: + + make prefix=<some dir> + make prefix=<some dir> install + make prefix=<some dir> install-doc + +To run tests first copy git-subtree to the main build area so the +newly-built git can find it: + + cp git-subtree ../.. + +Then: + + make test + diff --git a/contrib/subtree/Makefile b/contrib/subtree/Makefile new file mode 100644 index 0000000000..05cdd5c9b2 --- /dev/null +++ b/contrib/subtree/Makefile @@ -0,0 +1,52 @@ +-include ../../config.mak.autogen +-include ../../config.mak + +prefix ?= /usr/local +mandir ?= $(prefix)/share/man +libexecdir ?= $(prefix)/libexec/git-core +gitdir ?= $(shell git --exec-path) +man1dir ?= $(mandir)/man1 + +gitver ?= $(word 3,$(shell git --version)) + +# this should be set to a 'standard' bsd-type install program +INSTALL ?= install + +ASCIIDOC_CONF = ../../Documentation/asciidoc.conf +MANPAGE_NORMAL_XSL = ../../Documentation/manpage-normal.xsl + +GIT_SUBTREE_SH := git-subtree.sh +GIT_SUBTREE := git-subtree + +GIT_SUBTREE_DOC := git-subtree.1 +GIT_SUBTREE_XML := git-subtree.xml +GIT_SUBTREE_TXT := git-subtree.txt + +all: $(GIT_SUBTREE) + +$(GIT_SUBTREE): $(GIT_SUBTREE_SH) + cp $< $@ && chmod +x $@ + +doc: $(GIT_SUBTREE_DOC) + +install: $(GIT_SUBTREE) + $(INSTALL) -m 755 $(GIT_SUBTREE) $(libexecdir) + +install-doc: install-man + +install-man: $(GIT_SUBTREE_DOC) + $(INSTALL) -m 644 $^ $(man1dir) + +$(GIT_SUBTREE_DOC): $(GIT_SUBTREE_XML) + xmlto -m $(MANPAGE_NORMAL_XSL) man $^ + +$(GIT_SUBTREE_XML): $(GIT_SUBTREE_TXT) + asciidoc -b docbook -d manpage -f $(ASCIIDOC_CONF) \ + -agit_version=$(gitver) $^ + +test: + $(MAKE) -C t/ test + +clean: + rm -f *~ *.xml *.html *.1 + rm -rf subproj mainline diff --git a/contrib/subtree/README b/contrib/subtree/README new file mode 100644 index 0000000000..c686b4a69b --- /dev/null +++ b/contrib/subtree/README @@ -0,0 +1,8 @@ + +Please read git-subtree.txt for documentation. + +Please don't contact me using github mail; it's slow, ugly, and worst of +all, redundant. Email me instead at apenwarr@gmail.com and I'll be happy to +help. + +Avery diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh new file mode 100755 index 0000000000..920c664bb7 --- /dev/null +++ b/contrib/subtree/git-subtree.sh @@ -0,0 +1,712 @@ +#!/bin/bash +# +# git-subtree.sh: split/join git repositories in subdirectories of this one +# +# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com> +# +if [ $# -eq 0 ]; then + set -- -h +fi +OPTS_SPEC="\ +git subtree add --prefix=<prefix> <commit> +git subtree merge --prefix=<prefix> <commit> +git subtree pull --prefix=<prefix> <repository> <refspec...> +git subtree push --prefix=<prefix> <repository> <refspec...> +git subtree split --prefix=<prefix> <commit...> +-- +h,help show the help +q quiet +d show debug messages +P,prefix= the name of the subdir to split out +m,message= use the given message as the commit message for the merge commit + options for 'split' +annotate= add a prefix to commit message of new commits +b,branch= create a new branch from the split subtree +ignore-joins ignore prior --rejoin commits +onto= try connecting new tree to an existing one +rejoin merge the new branch back into HEAD + options for 'add', 'merge', 'pull' and 'push' +squash merge subtree changes as a single commit +" +eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" + +PATH=$PATH:$(git --exec-path) +. git-sh-setup + +require_work_tree + +quiet= +branch= +debug= +command= +onto= +rejoin= +ignore_joins= +annotate= +squash= +message= + +debug() +{ + if [ -n "$debug" ]; then + echo "$@" >&2 + fi +} + +say() +{ + if [ -z "$quiet" ]; then + echo "$@" >&2 + fi +} + +assert() +{ + if "$@"; then + : + else + die "assertion failed: " "$@" + fi +} + + +#echo "Options: $*" + +while [ $# -gt 0 ]; do + opt="$1" + shift + case "$opt" in + -q) quiet=1 ;; + -d) debug=1 ;; + --annotate) annotate="$1"; shift ;; + --no-annotate) annotate= ;; + -b) branch="$1"; shift ;; + -P) prefix="$1"; shift ;; + -m) message="$1"; shift ;; + --no-prefix) prefix= ;; + --onto) onto="$1"; shift ;; + --no-onto) onto= ;; + --rejoin) rejoin=1 ;; + --no-rejoin) rejoin= ;; + --ignore-joins) ignore_joins=1 ;; + --no-ignore-joins) ignore_joins= ;; + --squash) squash=1 ;; + --no-squash) squash= ;; + --) break ;; + *) die "Unexpected option: $opt" ;; + esac +done + +command="$1" +shift +case "$command" in + add|merge|pull) default= ;; + split|push) default="--default HEAD" ;; + *) die "Unknown command '$command'" ;; +esac + +if [ -z "$prefix" ]; then + die "You must provide the --prefix option." +fi + +case "$command" in + add) [ -e "$prefix" ] && + die "prefix '$prefix' already exists." ;; + *) [ -e "$prefix" ] || + die "'$prefix' does not exist; use 'git subtree add'" ;; +esac + +dir="$(dirname "$prefix/.")" + +if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then + revs=$(git rev-parse $default --revs-only "$@") || exit $? + dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $? + if [ -n "$dirs" ]; then + die "Error: Use --prefix instead of bare filenames." + fi +fi + +debug "command: {$command}" +debug "quiet: {$quiet}" +debug "revs: {$revs}" +debug "dir: {$dir}" +debug "opts: {$*}" +debug + +cache_setup() +{ + cachedir="$GIT_DIR/subtree-cache/$$" + rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir" + mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir" + mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree" + debug "Using cachedir: $cachedir" >&2 +} + +cache_get() +{ + for oldrev in $*; do + if [ -r "$cachedir/$oldrev" ]; then + read newrev <"$cachedir/$oldrev" + echo $newrev + fi + done +} + +cache_miss() +{ + for oldrev in $*; do + if [ ! -r "$cachedir/$oldrev" ]; then + echo $oldrev + fi + done +} + +check_parents() +{ + missed=$(cache_miss $*) + for miss in $missed; do + if [ ! -r "$cachedir/notree/$miss" ]; then + debug " incorrect order: $miss" + fi + done +} + +set_notree() +{ + echo "1" > "$cachedir/notree/$1" +} + +cache_set() +{ + oldrev="$1" + newrev="$2" + if [ "$oldrev" != "latest_old" \ + -a "$oldrev" != "latest_new" \ + -a -e "$cachedir/$oldrev" ]; then + die "cache for $oldrev already exists!" + fi + echo "$newrev" >"$cachedir/$oldrev" +} + +rev_exists() +{ + if git rev-parse "$1" >/dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +rev_is_descendant_of_branch() +{ + newrev="$1" + branch="$2" + branch_hash=$(git rev-parse $branch) + match=$(git rev-list -1 $branch_hash ^$newrev) + + if [ -z "$match" ]; then + return 0 + else + return 1 + fi +} + +# if a commit doesn't have a parent, this might not work. But we only want +# to remove the parent from the rev-list, and since it doesn't exist, it won't +# be there anyway, so do nothing in that case. +try_remove_previous() +{ + if rev_exists "$1^"; then + echo "^$1^" + fi +} + +find_latest_squash() +{ + debug "Looking for latest squash ($dir)..." + dir="$1" + sq= + main= + sub= + git log --grep="^git-subtree-dir: $dir/*\$" \ + --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD | + while read a b junk; do + debug "$a $b $junk" + debug "{{$sq/$main/$sub}}" + case "$a" in + START) sq="$b" ;; + git-subtree-mainline:) main="$b" ;; + git-subtree-split:) sub="$b" ;; + END) + if [ -n "$sub" ]; then + if [ -n "$main" ]; then + # a rejoin commit? + # Pretend its sub was a squash. + sq="$sub" + fi + debug "Squash found: $sq $sub" + echo "$sq" "$sub" + break + fi + sq= + main= + sub= + ;; + esac + done +} + +find_existing_splits() +{ + debug "Looking for prior splits..." + dir="$1" + revs="$2" + main= + sub= + git log --grep="^git-subtree-dir: $dir/*\$" \ + --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs | + while read a b junk; do + case "$a" in + START) sq="$b" ;; + git-subtree-mainline:) main="$b" ;; + git-subtree-split:) sub="$b" ;; + END) + debug " Main is: '$main'" + if [ -z "$main" -a -n "$sub" ]; then + # squash commits refer to a subtree + debug " Squash: $sq from $sub" + cache_set "$sq" "$sub" + fi + if [ -n "$main" -a -n "$sub" ]; then + debug " Prior: $main -> $sub" + cache_set $main $sub + cache_set $sub $sub + try_remove_previous "$main" + try_remove_previous "$sub" + fi + main= + sub= + ;; + esac + done +} + +copy_commit() +{ + # We're going to set some environment vars here, so + # do it in a subshell to get rid of them safely later + debug copy_commit "{$1}" "{$2}" "{$3}" + git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" | + ( + read GIT_AUTHOR_NAME + read GIT_AUTHOR_EMAIL + read GIT_AUTHOR_DATE + read GIT_COMMITTER_NAME + read GIT_COMMITTER_EMAIL + read GIT_COMMITTER_DATE + export GIT_AUTHOR_NAME \ + GIT_AUTHOR_EMAIL \ + GIT_AUTHOR_DATE \ + GIT_COMMITTER_NAME \ + GIT_COMMITTER_EMAIL \ + GIT_COMMITTER_DATE + (echo -n "$annotate"; cat ) | + git commit-tree "$2" $3 # reads the rest of stdin + ) || die "Can't copy commit $1" +} + +add_msg() +{ + dir="$1" + latest_old="$2" + latest_new="$3" + if [ -n "$message" ]; then + commit_message="$message" + else + commit_message="Add '$dir/' from commit '$latest_new'" + fi + cat <<-EOF + $commit_message + + git-subtree-dir: $dir + git-subtree-mainline: $latest_old + git-subtree-split: $latest_new + EOF +} + +add_squashed_msg() +{ + if [ -n "$message" ]; then + echo "$message" + else + echo "Merge commit '$1' as '$2'" + fi +} + +rejoin_msg() +{ + dir="$1" + latest_old="$2" + latest_new="$3" + if [ -n "$message" ]; then + commit_message="$message" + else + commit_message="Split '$dir/' into commit '$latest_new'" + fi + cat <<-EOF + $commit_message + + git-subtree-dir: $dir + git-subtree-mainline: $latest_old + git-subtree-split: $latest_new + EOF +} + +squash_msg() +{ + dir="$1" + oldsub="$2" + newsub="$3" + newsub_short=$(git rev-parse --short "$newsub") + + if [ -n "$oldsub" ]; then + oldsub_short=$(git rev-parse --short "$oldsub") + echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short" + echo + git log --pretty=tformat:'%h %s' "$oldsub..$newsub" + git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub" + else + echo "Squashed '$dir/' content from commit $newsub_short" + fi + + echo + echo "git-subtree-dir: $dir" + echo "git-subtree-split: $newsub" +} + +toptree_for_commit() +{ + commit="$1" + git log -1 --pretty=format:'%T' "$commit" -- || exit $? +} + +subtree_for_commit() +{ + commit="$1" + dir="$2" + git ls-tree "$commit" -- "$dir" | + while read mode type tree name; do + assert [ "$name" = "$dir" ] + assert [ "$type" = "tree" -o "$type" = "commit" ] + [ "$type" = "commit" ] && continue # ignore submodules + echo $tree + break + done +} + +tree_changed() +{ + tree=$1 + shift + if [ $# -ne 1 ]; then + return 0 # weird parents, consider it changed + else + ptree=$(toptree_for_commit $1) + if [ "$ptree" != "$tree" ]; then + return 0 # changed + else + return 1 # not changed + fi + fi +} + +new_squash_commit() +{ + old="$1" + oldsub="$2" + newsub="$3" + tree=$(toptree_for_commit $newsub) || exit $? + if [ -n "$old" ]; then + squash_msg "$dir" "$oldsub" "$newsub" | + git commit-tree "$tree" -p "$old" || exit $? + else + squash_msg "$dir" "" "$newsub" | + git commit-tree "$tree" || exit $? + fi +} + +copy_or_skip() +{ + rev="$1" + tree="$2" + newparents="$3" + assert [ -n "$tree" ] + + identical= + nonidentical= + p= + gotparents= + for parent in $newparents; do + ptree=$(toptree_for_commit $parent) || exit $? + [ -z "$ptree" ] && continue + if [ "$ptree" = "$tree" ]; then + # an identical parent could be used in place of this rev. + identical="$parent" + else + nonidentical="$parent" + fi + + # sometimes both old parents map to the same newparent; + # eliminate duplicates + is_new=1 + for gp in $gotparents; do + if [ "$gp" = "$parent" ]; then + is_new= + break + fi + done + if [ -n "$is_new" ]; then + gotparents="$gotparents $parent" + p="$p -p $parent" + fi + done + + if [ -n "$identical" ]; then + echo $identical + else + copy_commit $rev $tree "$p" || exit $? + fi +} + +ensure_clean() +{ + if ! git diff-index HEAD --exit-code --quiet 2>&1; then + die "Working tree has modifications. Cannot add." + fi + if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then + die "Index has modifications. Cannot add." + fi +} + +cmd_add() +{ + if [ -e "$dir" ]; then + die "'$dir' already exists. Cannot add." + fi + + ensure_clean + + if [ $# -eq 1 ]; then + "cmd_add_commit" "$@" + elif [ $# -eq 2 ]; then + "cmd_add_repository" "$@" + else + say "error: parameters were '$@'" + die "Provide either a refspec or a repository and refspec." + fi +} + +cmd_add_repository() +{ + echo "git fetch" "$@" + repository=$1 + refspec=$2 + git fetch "$@" || exit $? + revs=FETCH_HEAD + set -- $revs + cmd_add_commit "$@" +} + +cmd_add_commit() +{ + revs=$(git rev-parse $default --revs-only "$@") || exit $? + set -- $revs + rev="$1" + + debug "Adding $dir as '$rev'..." + git read-tree --prefix="$dir" $rev || exit $? + git checkout -- "$dir" || exit $? + tree=$(git write-tree) || exit $? + + headrev=$(git rev-parse HEAD) || exit $? + if [ -n "$headrev" -a "$headrev" != "$rev" ]; then + headp="-p $headrev" + else + headp= + fi + + if [ -n "$squash" ]; then + rev=$(new_squash_commit "" "" "$rev") || exit $? + commit=$(add_squashed_msg "$rev" "$dir" | + git commit-tree $tree $headp -p "$rev") || exit $? + else + commit=$(add_msg "$dir" "$headrev" "$rev" | + git commit-tree $tree $headp -p "$rev") || exit $? + fi + git reset "$commit" || exit $? + + say "Added dir '$dir'" +} + +cmd_split() +{ + debug "Splitting $dir..." + cache_setup || exit $? + + if [ -n "$onto" ]; then + debug "Reading history for --onto=$onto..." + git rev-list $onto | + while read rev; do + # the 'onto' history is already just the subdir, so + # any parent we find there can be used verbatim + debug " cache: $rev" + cache_set $rev $rev + done + fi + + if [ -n "$ignore_joins" ]; then + unrevs= + else + unrevs="$(find_existing_splits "$dir" "$revs")" + fi + + # We can't restrict rev-list to only $dir here, because some of our + # parents have the $dir contents the root, and those won't match. + # (and rev-list --follow doesn't seem to solve this) + grl='git rev-list --topo-order --reverse --parents $revs $unrevs' + revmax=$(eval "$grl" | wc -l) + revcount=0 + createcount=0 + eval "$grl" | + while read rev parents; do + revcount=$(($revcount + 1)) + say -n "$revcount/$revmax ($createcount)
" + debug "Processing commit: $rev" + exists=$(cache_get $rev) + if [ -n "$exists" ]; then + debug " prior: $exists" + continue + fi + createcount=$(($createcount + 1)) + debug " parents: $parents" + newparents=$(cache_get $parents) + debug " newparents: $newparents" + + tree=$(subtree_for_commit $rev "$dir") + debug " tree is: $tree" + + check_parents $parents + + # ugly. is there no better way to tell if this is a subtree + # vs. a mainline commit? Does it matter? + if [ -z $tree ]; then + set_notree $rev + if [ -n "$newparents" ]; then + cache_set $rev $rev + fi + continue + fi + + newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $? + debug " newrev is: $newrev" + cache_set $rev $newrev + cache_set latest_new $newrev + cache_set latest_old $rev + done || exit $? + latest_new=$(cache_get latest_new) + if [ -z "$latest_new" ]; then + die "No new revisions were found" + fi + + if [ -n "$rejoin" ]; then + debug "Merging split branch into HEAD..." + latest_old=$(cache_get latest_old) + git merge -s ours \ + -m "$(rejoin_msg $dir $latest_old $latest_new)" \ + $latest_new >&2 || exit $? + fi + if [ -n "$branch" ]; then + if rev_exists "refs/heads/$branch"; then + if ! rev_is_descendant_of_branch $latest_new $branch; then + die "Branch '$branch' is not an ancestor of commit '$latest_new'." + fi + action='Updated' + else + action='Created' + fi + git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $? + say "$action branch '$branch'" + fi + echo $latest_new + exit 0 +} + +cmd_merge() +{ + revs=$(git rev-parse $default --revs-only "$@") || exit $? + ensure_clean + + set -- $revs + if [ $# -ne 1 ]; then + die "You must provide exactly one revision. Got: '$revs'" + fi + rev="$1" + + if [ -n "$squash" ]; then + first_split="$(find_latest_squash "$dir")" + if [ -z "$first_split" ]; then + die "Can't squash-merge: '$dir' was never added." + fi + set $first_split + old=$1 + sub=$2 + if [ "$sub" = "$rev" ]; then + say "Subtree is already at commit $rev." + exit 0 + fi + new=$(new_squash_commit "$old" "$sub" "$rev") || exit $? + debug "New squash commit: $new" + rev="$new" + fi + + version=$(git version) + if [ "$version" \< "git version 1.7" ]; then + if [ -n "$message" ]; then + git merge -s subtree --message="$message" $rev + else + git merge -s subtree $rev + fi + else + if [ -n "$message" ]; then + git merge -Xsubtree="$prefix" --message="$message" $rev + else + git merge -Xsubtree="$prefix" $rev + fi + fi +} + +cmd_pull() +{ + ensure_clean + git fetch "$@" || exit $? + revs=FETCH_HEAD + set -- $revs + cmd_merge "$@" +} + +cmd_push() +{ + if [ $# -ne 2 ]; then + die "You must provide <repository> <refspec>" + fi + if [ -e "$dir" ]; then + repository=$1 + refspec=$2 + echo "git push using: " $repository $refspec + git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec + else + die "'$dir' must already exist. Try 'git subtree add'." + fi +} + +"cmd_$command" "$@" diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt new file mode 100644 index 0000000000..0c44fda011 --- /dev/null +++ b/contrib/subtree/git-subtree.txt @@ -0,0 +1,366 @@ +git-subtree(1) +============== + +NAME +---- +git-subtree - Merge subtrees together and split repository into subtrees + + +SYNOPSIS +-------- +[verse] +'git subtree' add -P <prefix> <commit> +'git subtree' pull -P <prefix> <repository> <refspec...> +'git subtree' push -P <prefix> <repository> <refspec...> +'git subtree' merge -P <prefix> <commit> +'git subtree' split -P <prefix> [OPTIONS] [<commit>] + + +DESCRIPTION +----------- +Subtrees allow subprojects to be included within a subdirectory +of the main project, optionally including the subproject's +entire history. + +For example, you could include the source code for a library +as a subdirectory of your application. + +Subtrees are not to be confused with submodules, which are meant for +the same task. Unlike submodules, subtrees do not need any special +constructions (like .gitmodule files or gitlinks) be present in +your repository, and do not force end-users of your +repository to do anything special or to understand how subtrees +work. A subtree is just a subdirectory that can be +committed to, branched, and merged along with your project in +any way you want. + +They are also not to be confused with using the subtree merge +strategy. The main difference is that, besides merging +the other project as a subdirectory, you can also extract the +entire history of a subdirectory from your project and make it +into a standalone project. Unlike the subtree merge strategy +you can alternate back and forth between these +two operations. If the standalone library gets updated, you can +automatically merge the changes into your project; if you +update the library inside your project, you can "split" the +changes back out again and merge them back into the library +project. + +For example, if a library you made for one application ends up being +useful elsewhere, you can extract its entire history and publish +that as its own git repository, without accidentally +intermingling the history of your application project. + +[TIP] +In order to keep your commit messages clean, we recommend that +people split their commits between the subtrees and the main +project as much as possible. That is, if you make a change that +affects both the library and the main application, commit it in +two pieces. That way, when you split the library commits out +later, their descriptions will still make sense. But if this +isn't important to you, it's not *necessary*. git subtree will +simply leave out the non-library-related parts of the commit +when it splits it out into the subproject later. + + +COMMANDS +-------- +add:: + Create the <prefix> subtree by importing its contents + from the given <refspec> or <repository> and remote <refspec>. + A new commit is created automatically, joining the imported + project's history with your own. With '--squash', imports + only a single commit from the subproject, rather than its + entire history. + +merge:: + Merge recent changes up to <commit> into the <prefix> + subtree. As with normal 'git merge', this doesn't + remove your own local changes; it just merges those + changes into the latest <commit>. With '--squash', + creates only one commit that contains all the changes, + rather than merging in the entire history. + + If you use '--squash', the merge direction doesn't + always have to be forward; you can use this command to + go back in time from v2.5 to v2.4, for example. If your + merge introduces a conflict, you can resolve it in the + usual ways. + +pull:: + Exactly like 'merge', but parallels 'git pull' in that + it fetches the given commit from the specified remote + repository. + +push:: + Does a 'split' (see above) using the <prefix> supplied + and then does a 'git push' to push the result to the + repository and refspec. This can be used to push your + subtree to different branches of the remote repository. + +split:: + Extract a new, synthetic project history from the + history of the <prefix> subtree. The new history + includes only the commits (including merges) that + affected <prefix>, and each of those commits now has the + contents of <prefix> at the root of the project instead + of in a subdirectory. Thus, the newly created history + is suitable for export as a separate git repository. + + After splitting successfully, a single commit id is + printed to stdout. This corresponds to the HEAD of the + newly created tree, which you can manipulate however you + want. + + Repeated splits of exactly the same history are + guaranteed to be identical (ie. to produce the same + commit ids). Because of this, if you add new commits + and then re-split, the new commits will be attached as + commits on top of the history you generated last time, + so 'git merge' and friends will work as expected. + + Note that if you use '--squash' when you merge, you + should usually not just '--rejoin' when you split. + + +OPTIONS +------- +-q:: +--quiet:: + Suppress unnecessary output messages on stderr. + +-d:: +--debug:: + Produce even more unnecessary output messages on stderr. + +-P <prefix>:: +--prefix=<prefix>:: + Specify the path in the repository to the subtree you + want to manipulate. This option is mandatory + for all commands. + +-m <message>:: +--message=<message>:: + This option is only valid for add, merge and pull (unsure). + Specify <message> as the commit message for the merge commit. + + +OPTIONS FOR add, merge, push, pull +---------------------------------- +--squash:: + This option is only valid for add, merge, push and pull + commands. + + Instead of merging the entire history from the subtree + project, produce only a single commit that contains all + the differences you want to merge, and then merge that + new commit into your project. + + Using this option helps to reduce log clutter. People + rarely want to see every change that happened between + v1.0 and v1.1 of the library they're using, since none of the + interim versions were ever included in their application. + + Using '--squash' also helps avoid problems when the same + subproject is included multiple times in the same + project, or is removed and then re-added. In such a + case, it doesn't make sense to combine the histories + anyway, since it's unclear which part of the history + belongs to which subtree. + + Furthermore, with '--squash', you can switch back and + forth between different versions of a subtree, rather + than strictly forward. 'git subtree merge --squash' + always adjusts the subtree to match the exactly + specified commit, even if getting to that commit would + require undoing some changes that were added earlier. + + Whether or not you use '--squash', changes made in your + local repository remain intact and can be later split + and send upstream to the subproject. + + +OPTIONS FOR split +----------------- +--annotate=<annotation>:: + This option is only valid for the split command. + + When generating synthetic history, add <annotation> as a + prefix to each commit message. Since we're creating new + commits with the same commit message, but possibly + different content, from the original commits, this can help + to differentiate them and avoid confusion. + + Whenever you split, you need to use the same + <annotation>, or else you don't have a guarantee that + the new re-created history will be identical to the old + one. That will prevent merging from working correctly. + git subtree tries to make it work anyway, particularly + if you use --rejoin, but it may not always be effective. + +-b <branch>:: +--branch=<branch>:: + This option is only valid for the split command. + + After generating the synthetic history, create a new + branch called <branch> that contains the new history. + This is suitable for immediate pushing upstream. + <branch> must not already exist. + +--ignore-joins:: + This option is only valid for the split command. + + If you use '--rejoin', git subtree attempts to optimize + its history reconstruction to generate only the new + commits since the last '--rejoin'. '--ignore-join' + disables this behaviour, forcing it to regenerate the + entire history. In a large project, this can take a + long time. + +--onto=<onto>:: + This option is only valid for the split command. + + If your subtree was originally imported using something + other than git subtree, its history may not match what + git subtree is expecting. In that case, you can specify + the commit id <onto> that corresponds to the first + revision of the subproject's history that was imported + into your project, and git subtree will attempt to build + its history from there. + + If you used 'git subtree add', you should never need + this option. + +--rejoin:: + This option is only valid for the split command. + + After splitting, merge the newly created synthetic + history back into your main project. That way, future + splits can search only the part of history that has + been added since the most recent --rejoin. + + If your split commits end up merged into the upstream + subproject, and then you want to get the latest upstream + version, this will allow git's merge algorithm to more + intelligently avoid conflicts (since it knows these + synthetic commits are already part of the upstream + repository). + + Unfortunately, using this option results in 'git log' + showing an extra copy of every new commit that was + created (the original, and the synthetic one). + + If you do all your merges with '--squash', don't use + '--rejoin' when you split, because you don't want the + subproject's history to be part of your project anyway. + + +EXAMPLE 1. Add command +---------------------- +Let's assume that you have a local repository that you would like +to add an external vendor library to. In this case we will add the +git-subtree repository as a subdirectory of your already existing +git-extensions repository in ~/git-extensions/: + + $ git subtree add --prefix=git-subtree --squash \ + git://github.com/apenwarr/git-subtree.git master + +'master' needs to be a valid remote ref and can be a different branch +name + +You can omit the --squash flag, but doing so will increase the number +of commits that are incldued in your local repository. + +We now have a ~/git-extensions/git-subtree directory containing code +from the master branch of git://github.com/apenwarr/git-subtree.git +in our git-extensions repository. + +EXAMPLE 2. Extract a subtree using commit, merge and pull +--------------------------------------------------------- +Let's use the repository for the git source code as an example. +First, get your own copy of the git.git repository: + + $ git clone git://git.kernel.org/pub/scm/git/git.git test-git + $ cd test-git + +gitweb (commit 1130ef3) was merged into git as of commit +0a8f4f0, after which it was no longer maintained separately. +But imagine it had been maintained separately, and we wanted to +extract git's changes to gitweb since that time, to share with +the upstream. You could do this: + + $ git subtree split --prefix=gitweb --annotate='(split) ' \ + 0a8f4f0^.. --onto=1130ef3 --rejoin \ + --branch gitweb-latest + $ gitk gitweb-latest + $ git push git@github.com:whatever/gitweb.git gitweb-latest:master + +(We use '0a8f4f0^..' because that means "all the changes from +0a8f4f0 to the current version, including 0a8f4f0 itself.") + +If gitweb had originally been merged using 'git subtree add' (or +a previous split had already been done with --rejoin specified) +then you can do all your splits without having to remember any +weird commit ids: + + $ git subtree split --prefix=gitweb --annotate='(split) ' --rejoin \ + --branch gitweb-latest2 + +And you can merge changes back in from the upstream project just +as easily: + + $ git subtree pull --prefix=gitweb \ + git@github.com:whatever/gitweb.git master + +Or, using '--squash', you can actually rewind to an earlier +version of gitweb: + + $ git subtree merge --prefix=gitweb --squash gitweb-latest~10 + +Then make some changes: + + $ date >gitweb/myfile + $ git add gitweb/myfile + $ git commit -m 'created myfile' + +And fast forward again: + + $ git subtree merge --prefix=gitweb --squash gitweb-latest + +And notice that your change is still intact: + + $ ls -l gitweb/myfile + +And you can split it out and look at your changes versus +the standard gitweb: + + git log gitweb-latest..$(git subtree split --prefix=gitweb) + +EXAMPLE 3. Extract a subtree using branch +----------------------------------------- +Suppose you have a source directory with many files and +subdirectories, and you want to extract the lib directory to its own +git project. Here's a short way to do it: + +First, make the new repository wherever you want: + + $ <go to the new location> + $ git init --bare + +Back in your original directory: + + $ git subtree split --prefix=lib --annotate="(split)" -b split + +Then push the new branch onto the new empty repository: + + $ git push <new-repo> split:master + + +AUTHOR +------ +Written by Avery Pennarun <apenwarr@gmail.com> + + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/contrib/subtree/t/Makefile b/contrib/subtree/t/Makefile new file mode 100644 index 0000000000..c864810389 --- /dev/null +++ b/contrib/subtree/t/Makefile @@ -0,0 +1,69 @@ +# Run tests +# +# Copyright (c) 2005 Junio C Hamano +# + +-include ../../../config.mak.autogen +-include ../../../config.mak + +#GIT_TEST_OPTS=--verbose --debug +SHELL_PATH ?= $(SHELL) +PERL_PATH ?= /usr/bin/perl +TAR ?= $(TAR) +RM ?= rm -f +PROVE ?= prove +DEFAULT_TEST_TARGET ?= test + +# Shell quote; +SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) + +T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) + +all: $(DEFAULT_TEST_TARGET) + +test: pre-clean $(TEST_LINT) + $(MAKE) aggregate-results-and-cleanup + +prove: pre-clean $(TEST_LINT) + @echo "*** prove ***"; GIT_CONFIG=.git/config $(PROVE) --exec '$(SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS) + $(MAKE) clean + +$(T): + @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS) + +pre-clean: + $(RM) -r test-results + +clean: + $(RM) -r 'trash directory'.* test-results + $(RM) -r valgrind/bin + $(RM) .prove + +test-lint: test-lint-duplicates test-lint-executable + +test-lint-duplicates: + @dups=`echo $(T) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \ + test -z "$$dups" || { \ + echo >&2 "duplicate test numbers:" $$dups; exit 1; } + +test-lint-executable: + @bad=`for i in $(T); do test -x "$$i" || echo $$i; done` && \ + test -z "$$bad" || { \ + echo >&2 "non-executable tests:" $$bad; exit 1; } + +aggregate-results-and-cleanup: $(T) + $(MAKE) aggregate-results + $(MAKE) clean + +aggregate-results: + for f in ../../../t/test-results/t*-*.counts; do \ + echo "$$f"; \ + done | '$(SHELL_PATH_SQ)' ../../../t/aggregate-results.sh + +valgrind: + $(MAKE) GIT_TEST_OPTS="$(GIT_TEST_OPTS) --valgrind" + +test-results: + mkdir -p test-results + +.PHONY: pre-clean $(T) aggregate-results clean valgrind diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh new file mode 100755 index 0000000000..bc2eeb0944 --- /dev/null +++ b/contrib/subtree/t/t7900-subtree.sh @@ -0,0 +1,508 @@ +#!/bin/sh +# +# Copyright (c) 2012 Avery Pennaraum +# +test_description='Basic porcelain support for subtrees + +This test verifies the basic operation of the merge, pull, add +and split subcommands of git subtree. +' + +export TEST_DIRECTORY=$(pwd)/../../../t + +. ../../../t/test-lib.sh + +create() +{ + echo "$1" >"$1" + git add "$1" +} + + +check_equal() +{ + test_debug 'echo' + test_debug "echo \"check a:\" \"{$1}\"" + test_debug "echo \" b:\" \"{$2}\"" + if [ "$1" = "$2" ]; then + return 0 + else + return 1 + fi +} + +fixnl() +{ + t="" + while read x; do + t="$t$x " + done + echo $t +} + +multiline() +{ + while read x; do + set -- $x + for d in "$@"; do + echo "$d" + done + done +} + +undo() +{ + git reset --hard HEAD~ +} + +last_commit_message() +{ + git log --pretty=format:%s -1 +} + +# 1 +test_expect_success 'init subproj' ' + test_create_repo subproj +' + +# To the subproject! +cd subproj + +# 2 +test_expect_success 'add sub1' ' + create sub1 && + git commit -m "sub1" && + git branch sub1 && + git branch -m master subproj +' + +# 3 +test_expect_success 'add sub2' ' + create sub2 && + git commit -m "sub2" && + git branch sub2 +' + +# 4 +test_expect_success 'add sub3' ' + create sub3 && + git commit -m "sub3" && + git branch sub3 +' + +# Back to mainline +cd .. + +# 5 +test_expect_success 'add main4' ' + create main4 && + git commit -m "main4" && + git branch -m master mainline && + git branch subdir +' + +# 6 +test_expect_success 'fetch subproj history' ' + git fetch ./subproj sub1 && + git branch sub1 FETCH_HEAD +' + +# 7 +test_expect_success 'no subtree exists in main tree' ' + test_must_fail git subtree merge --prefix=subdir sub1 +' + +# 8 +test_expect_success 'no pull from non-existant subtree' ' + test_must_fail git subtree pull --prefix=subdir ./subproj sub1 +' + +# 9 +test_expect_success 'check if --message works for add' ' + git subtree add --prefix=subdir --message="Added subproject" sub1 && + check_equal ''"$(last_commit_message)"'' "Added subproject" && + undo +' + +# 10 +test_expect_success 'check if --message works as -m and --prefix as -P' ' + git subtree add -P subdir -m "Added subproject using git subtree" sub1 && + check_equal ''"$(last_commit_message)"'' "Added subproject using git subtree" && + undo +' + +# 11 +test_expect_success 'check if --message works with squash too' ' + git subtree add -P subdir -m "Added subproject with squash" --squash sub1 && + check_equal ''"$(last_commit_message)"'' "Added subproject with squash" && + undo +' + +# 12 +test_expect_success 'add subproj to mainline' ' + git subtree add --prefix=subdir/ FETCH_HEAD && + check_equal ''"$(last_commit_message)"'' "Add '"'subdir/'"' from commit '"'"'''"$(git rev-parse sub1)"'''"'"'" +' + +# 13 +# this shouldn't actually do anything, since FETCH_HEAD is already a parent +test_expect_success 'merge fetched subproj' ' + git merge -m "merge -s -ours" -s ours FETCH_HEAD +' + +# 14 +test_expect_success 'add main-sub5' ' + create subdir/main-sub5 && + git commit -m "main-sub5" +' + +# 15 +test_expect_success 'add main6' ' + create main6 && + git commit -m "main6 boring" +' + +# 16 +test_expect_success 'add main-sub7' ' + create subdir/main-sub7 && + git commit -m "main-sub7" +' + +# 17 +test_expect_success 'fetch new subproj history' ' + git fetch ./subproj sub2 && + git branch sub2 FETCH_HEAD +' + +# 18 +test_expect_success 'check if --message works for merge' ' + git subtree merge --prefix=subdir -m "Merged changes from subproject" sub2 && + check_equal ''"$(last_commit_message)"'' "Merged changes from subproject" && + undo +' + +# 19 +test_expect_success 'check if --message for merge works with squash too' ' + git subtree merge --prefix subdir -m "Merged changes from subproject using squash" --squash sub2 && + check_equal ''"$(last_commit_message)"'' "Merged changes from subproject using squash" && + undo +' + +# 20 +test_expect_success 'merge new subproj history into subdir' ' + git subtree merge --prefix=subdir FETCH_HEAD && + git branch pre-split && + check_equal ''"$(last_commit_message)"'' "Merge commit '"'"'"$(git rev-parse sub2)"'"'"' into mainline" +' + +# 21 +test_expect_success 'Check that prefix argument is required for split' ' + echo "You must provide the --prefix option." > expected && + test_must_fail git subtree split > actual 2>&1 && + test_debug "echo -n expected: " && + test_debug "cat expected" && + test_debug "echo -n actual: " && + test_debug "cat actual" && + test_cmp expected actual && + rm -f expected actual +' + +# 22 +test_expect_success 'Check that the <prefix> exists for a split' ' + echo "'"'"'non-existent-directory'"'"'" does not exist\; use "'"'"'git subtree add'"'"'" > expected && + test_must_fail git subtree split --prefix=non-existent-directory > actual 2>&1 && + test_debug "echo -n expected: " && + test_debug "cat expected" && + test_debug "echo -n actual: " && + test_debug "cat actual" && + test_cmp expected actual +# rm -f expected actual +' + +# 23 +test_expect_success 'check if --message works for split+rejoin' ' + spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && + git branch spl1 "$spl1" && + check_equal ''"$(last_commit_message)"'' "Split & rejoin" && + undo +' + +# 24 +test_expect_success 'check split with --branch' ' + spl1=$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin) && + undo && + git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr1 && + check_equal ''"$(git rev-parse splitbr1)"'' "$spl1" +' + +# 25 +test_expect_success 'check split with --branch for an existing branch' ' + spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && + undo && + git branch splitbr2 sub1 && + git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr2 && + check_equal ''"$(git rev-parse splitbr2)"'' "$spl1" +' + +# 26 +test_expect_success 'check split with --branch for an incompatible branch' ' + test_must_fail git subtree split --prefix subdir --onto FETCH_HEAD --branch subdir +' + + +# 27 +test_expect_success 'check split+rejoin' ' + spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && + undo && + git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --rejoin && + check_equal ''"$(last_commit_message)"'' "Split '"'"'subdir/'"'"' into commit '"'"'"$spl1"'"'"'" +' + +# 28 +test_expect_success 'add main-sub8' ' + create subdir/main-sub8 && + git commit -m "main-sub8" +' + +# To the subproject! +cd ./subproj + +# 29 +test_expect_success 'merge split into subproj' ' + git fetch .. spl1 && + git branch spl1 FETCH_HEAD && + git merge FETCH_HEAD +' + +# 30 +test_expect_success 'add sub9' ' + create sub9 && + git commit -m "sub9" +' + +# Back to mainline +cd .. + +# 31 +test_expect_success 'split for sub8' ' + split2=''"$(git subtree split --annotate='"'*'"' --prefix subdir/ --rejoin)"'' + git branch split2 "$split2" +' + +# 32 +test_expect_success 'add main-sub10' ' + create subdir/main-sub10 && + git commit -m "main-sub10" +' + +# 33 +test_expect_success 'split for sub10' ' + spl3=''"$(git subtree split --annotate='"'*'"' --prefix subdir --rejoin)"'' && + git branch spl3 "$spl3" +' + +# To the subproject! +cd ./subproj + +# 34 +test_expect_success 'merge split into subproj' ' + git fetch .. spl3 && + git branch spl3 FETCH_HEAD && + git merge FETCH_HEAD && + git branch subproj-merge-spl3 +' + +chkm="main4 main6" +chkms="main-sub10 main-sub5 main-sub7 main-sub8" +chkms_sub=$(echo $chkms | multiline | sed 's,^,subdir/,' | fixnl) +chks="sub1 sub2 sub3 sub9" +chks_sub=$(echo $chks | multiline | sed 's,^,subdir/,' | fixnl) + +# 35 +test_expect_success 'make sure exactly the right set of files ends up in the subproj' ' + subfiles=''"$(git ls-files | fixnl)"'' && + check_equal "$subfiles" "$chkms $chks" +' + +# 36 +test_expect_success 'make sure the subproj history *only* contains commits that affect the subdir' ' + allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' && + check_equal "$allchanges" "$chkms $chks" +' + +# Back to mainline +cd .. + +# 37 +test_expect_success 'pull from subproj' ' + git fetch ./subproj subproj-merge-spl3 && + git branch subproj-merge-spl3 FETCH_HEAD && + git subtree pull --prefix=subdir ./subproj subproj-merge-spl3 +' + +# 38 +test_expect_success 'make sure exactly the right set of files ends up in the mainline' ' + mainfiles=''"$(git ls-files | fixnl)"'' && + check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub" +' + +# 39 +test_expect_success 'make sure each filename changed exactly once in the entire history' ' + # main-sub?? and /subdir/main-sub?? both change, because those are the + # changes that were split into their own history. And subdir/sub?? never + # change, since they were *only* changed in the subtree branch. + allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' && + check_equal "$allchanges" ''"$(echo $chkms $chkm $chks $chkms_sub | multiline | sort | fixnl)"'' +' + +# 40 +test_expect_success 'make sure the --rejoin commits never make it into subproj' ' + check_equal ''"$(git log --pretty=format:'"'%s'"' HEAD^2 | grep -i split)"'' "" +' + +# 41 +test_expect_success 'make sure no "git subtree" tagged commits make it into subproj' ' + # They are meaningless to subproj since one side of the merge refers to the mainline + check_equal ''"$(git log --pretty=format:'"'%s%n%b'"' HEAD^2 | grep "git-subtree.*:")"'' "" +' + +# prepare second pair of repositories +mkdir test2 +cd test2 + +# 42 +test_expect_success 'init main' ' + test_create_repo main +' + +cd main + +# 43 +test_expect_success 'add main1' ' + create main1 && + git commit -m "main1" +' + +cd .. + +# 44 +test_expect_success 'init sub' ' + test_create_repo sub +' + +cd sub + +# 45 +test_expect_success 'add sub2' ' + create sub2 && + git commit -m "sub2" +' + +cd ../main + +# check if split can find proper base without --onto + +# 46 +test_expect_success 'add sub as subdir in main' ' + git fetch ../sub master && + git branch sub2 FETCH_HEAD && + git subtree add --prefix subdir sub2 +' + +cd ../sub + +# 47 +test_expect_success 'add sub3' ' + create sub3 && + git commit -m "sub3" +' + +cd ../main + +# 48 +test_expect_success 'merge from sub' ' + git fetch ../sub master && + git branch sub3 FETCH_HEAD && + git subtree merge --prefix subdir sub3 +' + +# 49 +test_expect_success 'add main-sub4' ' + create subdir/main-sub4 && + git commit -m "main-sub4" +' + +# 50 +test_expect_success 'split for main-sub4 without --onto' ' + git subtree split --prefix subdir --branch mainsub4 +' + +# at this point, the new commit parent should be sub3 if it is not, +# something went wrong (the "newparent" of "master~" commit should +# have been sub3, but it was not, because its cache was not set to +# itself) + +# 51 +test_expect_success 'check that the commit parent is sub3' ' + check_equal ''"$(git log --pretty=format:%P -1 mainsub4)"'' ''"$(git rev-parse sub3)"'' +' + +# 52 +test_expect_success 'add main-sub5' ' + mkdir subdir2 && + create subdir2/main-sub5 && + git commit -m "main-sub5" +' + +# 53 +test_expect_success 'split for main-sub5 without --onto' ' + # also test that we still can split out an entirely new subtree + # if the parent of the first commit in the tree is not empty, + # then the new subtree has accidently been attached to something + git subtree split --prefix subdir2 --branch mainsub5 && + check_equal ''"$(git log --pretty=format:%P -1 mainsub5)"'' "" +' + +# make sure no patch changes more than one file. The original set of commits +# changed only one file each. A multi-file change would imply that we pruned +# commits too aggressively. +joincommits() +{ + commit= + all= + while read x y; do + #echo "{$x}" >&2 + if [ -z "$x" ]; then + continue + elif [ "$x" = "commit:" ]; then + if [ -n "$commit" ]; then + echo "$commit $all" + all= + fi + commit="$y" + else + all="$all $y" + fi + done + echo "$commit $all" +} + +# 54 +test_expect_success 'verify one file change per commit' ' + x= && + list=''"$(git log --pretty=format:'"'commit: %H'"' | joincommits)"'' && +# test_debug "echo HERE" && +# test_debug "echo ''"$list"''" && + (git log --pretty=format:'"'commit: %H'"' | joincommits | + ( while read commit a b; do + test_debug "echo Verifying commit "''"$commit"'' + test_debug "echo a: "''"$a"'' + test_debug "echo b: "''"$b"'' + check_equal "$b" "" + x=1 + done + check_equal "$x" 1 + )) +' + +test_done diff --git a/contrib/subtree/todo b/contrib/subtree/todo new file mode 100644 index 0000000000..7e44b0024f --- /dev/null +++ b/contrib/subtree/todo @@ -0,0 +1,50 @@ + + delete tempdir + + 'git subtree rejoin' option to do the same as --rejoin, eg. after a + rebase + + --prefix doesn't force the subtree correctly in merge/pull: + "-s subtree" should be given an explicit subtree option? + There doesn't seem to be a way to do this. We'd have to + patch git-merge-subtree. Ugh. + (but we could avoid this problem by generating squashes with + exactly the right subtree structure, rather than using + subtree merge...) + + add a 'push' subcommand to parallel 'pull' + + add a 'log' subcommand to see what's new in a subtree? + + add to-submodule and from-submodule commands + + automated tests for --squash stuff + + "add" command non-obviously requires a commitid; would be easier if + it had a "pull" sort of mode instead + + "pull" and "merge" commands should fail if you've never merged + that --prefix before + + docs should provide an example of "add" + + note that the initial split doesn't *have* to have a commitid + specified... that's just an optimization + + if you try to add (or maybe merge?) with an invalid commitid, you + get a misleading "prefix must end with /" message from + one of the other git tools that git-subtree calls. Should + detect this situation and print the *real* problem. + + "pull --squash" should do fetch-synthesize-merge, but instead just + does "pull" directly, which doesn't work at all. + + make a 'force-update' that does what 'add' does even if the subtree + already exists. That way we can help people who imported + subtrees "incorrectly" (eg. by just copying in the files) in + the past. + + guess --prefix automatically if possible based on pwd + + make a 'git subtree grafts' that automatically expands --squash'd + commits so you can see the full history if you want it. |