From 4d0ba818236453fae51c9efb64950f23557cc428 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Karl=20Hasselstr=C3=B6m?= Date: Wed, 25 Oct 2006 20:24:55 +0100 Subject: [PATCH] New stg command: assimilate MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Introduce an "assimilate" command, with no options. It takes any GIT commits committed on top of your StGIT patch stack and converts them into StGIT patches. Also change the error message when an StGIT command can't do its job because there are GIT commits on top of the stack. Instead of recommending "refresh -f", which is a destructive operation, recommend "assimilate", which is not. NOTE: "assimilate" currently refuses to work its magic if it encounters a merge commit. This is reasonable, since merge commits can't (yet) be represented as StGIT patches. However, it would be possible (and well-defined) to replace the merge commit with a regular commit on the branch with the same end result (tree object), discarding all the parents that aren't on our branch. But this would take a substantial amount of code, and is of dubious value, so for now "assimilate" just cries bloody murder if it finds a merge. Signed-off-by: Karl Hasselström --- stgit/commands/assimilate.py | 99 ++++++++++++++++++++++++++++++++++++++++++++ stgit/commands/common.py | 8 ++-- stgit/git.py | 3 ++ stgit/main.py | 3 ++ stgit/stack.py | 28 +++++++++---- 5 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 stgit/commands/assimilate.py diff --git a/stgit/commands/assimilate.py b/stgit/commands/assimilate.py new file mode 100644 index 0000000..0821024 --- /dev/null +++ b/stgit/commands/assimilate.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +__copyright__ = """ +Copyright (C) 2006, Karl Hasselström + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys, os +from optparse import OptionParser, make_option + +from stgit.commands.common import * +from stgit.utils import * +from stgit import stack, git + +help = 'StGIT-ify any GIT commits made on top of your StGIT stack' +usage = """%prog [options] + +If you have made GIT commits on top of your stack of StGIT patches, +many StGIT commands will refuse to work. This command converts any +such commits to StGIT patches, preserving their contents. + +Only GIT commits with exactly one parent can be assimilated; in other +words, if you have committed a merge on top of your stack, this +command cannot help you.""" + +options = [] + +def func(parser, options, args): + """Assimilate a number of patches. + """ + + def nothing_to_do(): + print 'No commits to assimilate' + + top_patch = crt_series.get_current_patch() + if not top_patch: + return nothing_to_do() + + victims = [] + victim = git.get_commit(git.get_head()) + while victim.get_id_hash() != top_patch.get_top(): + victims.append(victim) + parents = victim.get_parents() + if not parents: + raise CmdException, 'Commit %s has no parents, aborting' % victim + elif len(parents) > 1: + raise CmdException, 'Commit %s is a merge, aborting' % victim + victim = git.get_commit(parents[0]) + + if not victims: + return nothing_to_do() + + if crt_series.get_protected(): + raise CmdException( + 'This branch is protected. Modification is not permitted') + + patch2name = {} + name2patch = {} + + def name_taken(name): + return name in name2patch or crt_series.patch_exists(name) + + for victim in victims: + patchname = make_patch_name(victim.get_log()) + if not patchname: + patchname = 'patch' + if name_taken(patchname): + suffix = 0 + while name_taken('%s-%d' % (patchname, suffix)): + suffix += 1 + patchname = '%s-%d' % (patchname, suffix) + patch2name[victim] = patchname + name2patch[patchname] = victim + + victims.reverse() + for victim in victims: + print ('Creating patch "%s" from commit %s' + % (patch2name[victim], victim)) + aname, amail, adate = name_email_date(victim.get_author()) + cname, cmail, cdate = name_email_date(victim.get_committer()) + crt_series.new_patch( + patch2name[victim], + can_edit = False, before_existing = False, refresh = False, + top = victim.get_id_hash(), bottom = victim.get_parent(), + message = victim.get_log(), + author_name = aname, author_email = amail, author_date = adate, + committer_name = cname, committer_email = cmail) diff --git a/stgit/commands/common.py b/stgit/commands/common.py index bf8481e..1ea6025 100644 --- a/stgit/commands/common.py +++ b/stgit/commands/common.py @@ -113,10 +113,10 @@ def check_local_changes(): def check_head_top_equal(): if not crt_series.head_top_equal(): - raise CmdException, \ - 'HEAD and top are not the same. You probably committed\n' \ - ' changes to the tree outside of StGIT. If you know what you\n' \ - ' are doing, use the "refresh -f" command' + raise CmdException( + 'HEAD and top are not the same. You probably committed\n' + ' changes to the tree outside of StGIT. To bring them\n' + ' into StGIT, use the "assimilate" command') def check_conflicts(): if os.path.exists(os.path.join(basedir.get(), 'conflicts')): diff --git a/stgit/git.py b/stgit/git.py index 43bdc7e..42b0d12 100644 --- a/stgit/git.py +++ b/stgit/git.py @@ -79,6 +79,9 @@ class Commit: def get_log(self): return self.__log + def __str__(self): + return self.get_id_hash() + # dictionary of Commit objects, used to avoid multiple calls to git __commits = dict() diff --git a/stgit/main.py b/stgit/main.py index 183f9d7..9fa0afc 100644 --- a/stgit/main.py +++ b/stgit/main.py @@ -30,6 +30,7 @@ from stgit.commands.common import * # The commands import stgit.commands.add import stgit.commands.applied +import stgit.commands.assimilate import stgit.commands.branch import stgit.commands.delete import stgit.commands.diff @@ -70,6 +71,7 @@ import stgit.commands.uncommit commands = { 'add': stgit.commands.add, 'applied': stgit.commands.applied, + 'assimilate': stgit.commands.assimilate, 'branch': stgit.commands.branch, 'delete': stgit.commands.delete, 'diff': stgit.commands.diff, @@ -113,6 +115,7 @@ repocommands = ( ) stackcommands = ( 'applied', + 'assimilate', 'clean', 'commit', 'float', diff --git a/stgit/stack.py b/stgit/stack.py index 93a3d4e..e50f189 100644 --- a/stgit/stack.py +++ b/stgit/stack.py @@ -350,9 +350,17 @@ class Series: """ return Patch(name, self.__patch_dir, self.__refs_dir) + def get_current_patch(self): + """Return a Patch object representing the topmost patch, or + None if there is no such patch.""" + crt = self.get_current() + if not crt: + return None + return Patch(crt, self.__patch_dir, self.__refs_dir) + def get_current(self): - """Return a Patch object representing the topmost patch - """ + """Return the name of the topmost patch, or None if there is + no such patch.""" if os.path.isfile(self.__current_file): name = read_string(self.__current_file) else: @@ -414,6 +422,11 @@ class Series: """ return name in self.get_unapplied() + def patch_exists(self, name): + """Return true if there is a patch with the given name, false + otherwise.""" + return self.__patch_applied(name) or self.__patch_applied(name) + def __begin_stack_check(self): """Save the current HEAD into .git/refs/heads/base if the stack is empty @@ -433,12 +446,11 @@ class Series: def head_top_equal(self): """Return true if the head and the top are the same """ - crt = self.get_current() + crt = self.get_current_patch() if not crt: # we don't care, no patches applied return True - return git.get_head() == Patch(crt, self.__patch_dir, - self.__refs_dir).get_top() + return git.get_head() == crt.get_top() def is_initialised(self): """Checks if series is already initialised @@ -688,7 +700,7 @@ class Series: top = None, bottom = None, author_name = None, author_email = None, author_date = None, committer_name = None, committer_email = None, - before_existing = False): + before_existing = False, refresh = True): """Creates a new patch """ if self.__patch_applied(name) or self.__patch_unapplied(name): @@ -741,8 +753,8 @@ class Series: else: append_string(self.__applied_file, patch.get_name()) self.__set_current(name) - - self.refresh_patch(cache_update = False, log = 'new') + if refresh: + self.refresh_patch(cache_update = False, log = 'new') def delete_patch(self, name): """Deletes a patch -- 2.11.0