git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [StGit PATCH 0/5] Start the refactoring
@ 2007-11-05  3:14 Karl Hasselström
  2007-11-05  3:14 ` [StGit PATCH 1/5] New StGit core infrastructure: repository operations Karl Hasselström
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Karl Hasselström @ 2007-11-05  3:14 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git

This series introduces parts of the proposed new infrastructure, and
uses it to implement "stg clean" and the new "stg coalesce".

There are undoubtedly more bugs to be found, but I'm pushing it to

  git://repo.or.cz/stgit/kha.git experimental

to make it easy for people to try it out. It does pass the test suite
after all.

---

Karl Hasselström (5):
      Add "stg coalesce"
      Let "stg clean" use the new infrastructure
      Simple test for "stg clean"
      Write metadata files used by the old infrastructure
      New StGit core infrastructure: repository operations


 stgit/commands/clean.py    |   68 ++++++------
 stgit/commands/coalesce.py |   87 ++++++++++++++++
 stgit/commands/common.py   |   10 ++
 stgit/lib/__init__.py      |   18 +++
 stgit/lib/git.py           |  245 ++++++++++++++++++++++++++++++++++++++++++++
 stgit/lib/stack.py         |  156 ++++++++++++++++++++++++++++
 stgit/lib/transaction.py   |   79 ++++++++++++++
 stgit/main.py              |    2 
 stgit/utils.py             |   24 ++++
 t/t2500-clean.sh           |   27 +++++
 t/t2600-coalesce.sh        |   31 ++++++
 11 files changed, 713 insertions(+), 34 deletions(-)
 create mode 100644 stgit/commands/coalesce.py
 create mode 100644 stgit/lib/__init__.py
 create mode 100644 stgit/lib/git.py
 create mode 100644 stgit/lib/stack.py
 create mode 100644 stgit/lib/transaction.py
 create mode 100755 t/t2500-clean.sh
 create mode 100755 t/t2600-coalesce.sh

-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle

^ permalink raw reply	[flat|nested] 6+ messages in thread

* [StGit PATCH 1/5] New StGit core infrastructure: repository operations
  2007-11-05  3:14 [StGit PATCH 0/5] Start the refactoring Karl Hasselström
@ 2007-11-05  3:14 ` Karl Hasselström
  2007-11-05  3:14 ` [StGit PATCH 2/5] Write metadata files used by the old infrastructure Karl Hasselström
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Karl Hasselström @ 2007-11-05  3:14 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git

This is the first part of the New and Improved StGit core
infrastructure. It has functions for manipulating the git repository
(commits, refs, and so on), but doesn't yet touch the index or
worktree.

Currently not used by anything.

Signed-off-by: Karl Hasselström <kha@treskal.com>

---

 stgit/lib/__init__.py    |   18 +++
 stgit/lib/git.py         |  245 ++++++++++++++++++++++++++++++++++++++++++++++
 stgit/lib/stack.py       |  121 +++++++++++++++++++++++
 stgit/lib/transaction.py |   79 +++++++++++++++
 stgit/utils.py           |   13 ++
 5 files changed, 476 insertions(+), 0 deletions(-)
 create mode 100644 stgit/lib/__init__.py
 create mode 100644 stgit/lib/git.py
 create mode 100644 stgit/lib/stack.py
 create mode 100644 stgit/lib/transaction.py


diff --git a/stgit/lib/__init__.py b/stgit/lib/__init__.py
new file mode 100644
index 0000000..45eb307
--- /dev/null
+++ b/stgit/lib/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2007, Karl Hasselström <kha@treskal.com>
+
+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
+"""
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
new file mode 100644
index 0000000..1b873e3
--- /dev/null
+++ b/stgit/lib/git.py
@@ -0,0 +1,245 @@
+import os, os.path, re
+from stgit import exception, run, utils
+
+class RepositoryException(exception.StgException):
+    pass
+
+class DetachedHeadException(RepositoryException):
+    pass
+
+class Repr(object):
+    def __repr__(self):
+        return str(self)
+
+class NoValue(object):
+    pass
+
+def make_defaults(defaults):
+    def d(val, attr):
+        if val != NoValue:
+            return val
+        elif defaults != NoValue:
+            return getattr(defaults, attr)
+        else:
+            return None
+    return d
+
+class Person(Repr):
+    """Immutable."""
+    def __init__(self, name = NoValue, email = NoValue,
+                 date = NoValue, defaults = NoValue):
+        d = make_defaults(defaults)
+        self.__name = d(name, 'name')
+        self.__email = d(email, 'email')
+        self.__date = d(date, 'date')
+    name = property(lambda self: self.__name)
+    email = property(lambda self: self.__email)
+    date = property(lambda self: self.__date)
+    def set_name(self, name):
+        return type(self)(name = name, defaults = self)
+    def set_email(self, email):
+        return type(self)(email = email, defaults = self)
+    def set_date(self, date):
+        return type(self)(date = date, defaults = self)
+    def __str__(self):
+        return '%s <%s> %s' % (self.name, self.email, self.date)
+    @classmethod
+    def parse(cls, s):
+        m = re.match(r'^([^<]*)<([^>]*)>\s+(\d+\s+[+-]\d{4})$', s)
+        assert m
+        name = m.group(1).strip()
+        email = m.group(2)
+        date = m.group(3)
+        return cls(name, email, date)
+
+class Tree(Repr):
+    """Immutable."""
+    def __init__(self, sha1):
+        self.__sha1 = sha1
+    sha1 = property(lambda self: self.__sha1)
+    def __str__(self):
+        return 'Tree<%s>' % self.sha1
+
+class Commitdata(Repr):
+    """Immutable."""
+    def __init__(self, tree = NoValue, parents = NoValue, author = NoValue,
+                 committer = NoValue, message = NoValue, defaults = NoValue):
+        d = make_defaults(defaults)
+        self.__tree = d(tree, 'tree')
+        self.__parents = d(parents, 'parents')
+        self.__author = d(author, 'author')
+        self.__committer = d(committer, 'committer')
+        self.__message = d(message, 'message')
+    tree = property(lambda self: self.__tree)
+    parents = property(lambda self: self.__parents)
+    @property
+    def parent(self):
+        assert len(self.__parents) == 1
+        return self.__parents[0]
+    author = property(lambda self: self.__author)
+    committer = property(lambda self: self.__committer)
+    message = property(lambda self: self.__message)
+    def set_tree(self, tree):
+        return type(self)(tree = tree, defaults = self)
+    def set_parents(self, parents):
+        return type(self)(parents = parents, defaults = self)
+    def add_parent(self, parent):
+        return type(self)(parents = list(self.parents or []) + [parent],
+                          defaults = self)
+    def set_parent(self, parent):
+        return self.set_parents([parent])
+    def set_author(self, author):
+        return type(self)(author = author, defaults = self)
+    def set_committer(self, committer):
+        return type(self)(committer = committer, defaults = self)
+    def set_message(self, message):
+        return type(self)(message = message, defaults = self)
+    def __str__(self):
+        if self.tree == None:
+            tree = None
+        else:
+            tree = self.tree.sha1
+        if self.parents == None:
+            parents = None
+        else:
+            parents = [p.sha1 for p in self.parents]
+        return ('Commitdata<tree: %s, parents: %s, author: %s,'
+                ' committer: %s, message: "%s">'
+                ) % (tree, parents, self.author, self.committer, self.message)
+    @classmethod
+    def parse(cls, repository, s):
+        cd = cls()
+        lines = list(s.splitlines(True))
+        for i in xrange(len(lines)):
+            line = lines[i].strip()
+            if not line:
+                return cd.set_message(''.join(lines[i+1:]))
+            key, value = line.split(None, 1)
+            if key == 'tree':
+                cd = cd.set_tree(repository.get_tree(value))
+            elif key == 'parent':
+                cd = cd.add_parent(repository.get_commit(value))
+            elif key == 'author':
+                cd = cd.set_author(Person.parse(value))
+            elif key == 'committer':
+                cd = cd.set_committer(Person.parse(value))
+            else:
+                assert False
+        assert False
+
+class Commit(Repr):
+    """Immutable."""
+    def __init__(self, repository, sha1):
+        self.__sha1 = sha1
+        self.__repository = repository
+        self.__data = None
+    sha1 = property(lambda self: self.__sha1)
+    @property
+    def data(self):
+        if self.__data == None:
+            self.__data = Commitdata.parse(
+                self.__repository,
+                self.__repository.cat_object(self.sha1))
+        return self.__data
+    def __str__(self):
+        return 'Commit<sha1: %s, data: %s>' % (self.sha1, self.__data)
+
+class Refs(object):
+    def __init__(self, repository):
+        self.__repository = repository
+        self.__refs = None
+    def __cache_refs(self):
+        self.__refs = {}
+        for line in self.__repository.run(['git', 'show-ref']).output_lines():
+            m = re.match(r'^([0-9a-f]{40})\s+(\S+)$', line)
+            sha1, ref = m.groups()
+            self.__refs[ref] = sha1
+    def get(self, ref):
+        """Throws KeyError if ref doesn't exist."""
+        if self.__refs == None:
+            self.__cache_refs()
+        return self.__repository.get_commit(self.__refs[ref])
+    def set(self, ref, commit, msg):
+        if self.__refs == None:
+            self.__cache_refs()
+        old_sha1 = self.__refs.get(ref, '0'*40)
+        new_sha1 = commit.sha1
+        if old_sha1 != new_sha1:
+            self.__repository.run(['git', 'update-ref', '-m', msg,
+                                   ref, new_sha1, old_sha1]).no_output()
+            self.__refs[ref] = new_sha1
+    def delete(self, ref):
+        if self.__refs == None:
+            self.__cache_refs()
+        self.__repository.run(['git', 'update-ref',
+                               '-d', ref, self.__refs[ref]]).no_output()
+        del self.__refs[ref]
+
+class ObjectCache(object):
+    """Cache for Python objects, for making sure that we create only one
+    Python object per git object."""
+    def __init__(self, create):
+        self.__objects = {}
+        self.__create = create
+    def __getitem__(self, name):
+        if not name in self.__objects:
+            self.__objects[name] = self.__create(name)
+        return self.__objects[name]
+    def __contains__(self, name):
+        return name in self.__objects
+    def __setitem__(self, name, val):
+        assert not name in self.__objects
+        self.__objects[name] = val
+
+class RunWithEnv(object):
+    def run(self, args, env = {}):
+        return run.Run(*args).env(utils.add_dict(self.env, env))
+
+class Repository(RunWithEnv):
+    def __init__(self, directory):
+        self.__git_dir = directory
+        self.__refs = Refs(self)
+        self.__trees = ObjectCache(lambda sha1: Tree(sha1))
+        self.__commits = ObjectCache(lambda sha1: Commit(self, sha1))
+    env = property(lambda self: { 'GIT_DIR': self.__git_dir })
+    @classmethod
+    def default(cls):
+        """Return the default repository."""
+        try:
+            return cls(run.Run('git', 'rev-parse', '--git-dir'
+                               ).output_one_line())
+        except run.RunException:
+            raise RepositoryException('Cannot find git repository')
+    directory = property(lambda self: self.__git_dir)
+    refs = property(lambda self: self.__refs)
+    def cat_object(self, sha1):
+        return self.run(['git', 'cat-file', '-p', sha1]).raw_output()
+    def get_tree(self, sha1):
+        return self.__trees[sha1]
+    def get_commit(self, sha1):
+        return self.__commits[sha1]
+    def commit(self, commitdata):
+        c = ['git', 'commit-tree', commitdata.tree.sha1]
+        for p in commitdata.parents:
+            c.append('-p')
+            c.append(p.sha1)
+        env = {}
+        for p, v1 in ((commitdata.author, 'AUTHOR'),
+                       (commitdata.committer, 'COMMITTER')):
+            if p != None:
+                for attr, v2 in (('name', 'NAME'), ('email', 'EMAIL'),
+                                 ('date', 'DATE')):
+                    if getattr(p, attr) != None:
+                        env['GIT_%s_%s' % (v1, v2)] = getattr(p, attr)
+        sha1 = self.run(c, env = env).raw_input(commitdata.message
+                                                ).output_one_line()
+        return self.get_commit(sha1)
+    @property
+    def head(self):
+        try:
+            return self.run(['git', 'symbolic-ref', '-q', 'HEAD']
+                            ).output_one_line()
+        except run.RunException:
+            raise DetachedHeadException()
+    def set_head(self, ref, msg):
+        self.run(['git', 'symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
new file mode 100644
index 0000000..b4d99ba
--- /dev/null
+++ b/stgit/lib/stack.py
@@ -0,0 +1,121 @@
+import os.path
+from stgit import utils
+from stgit.lib import git
+
+class Patch(object):
+    def __init__(self, stack, name):
+        self.__stack = stack
+        self.__name = name
+    name = property(lambda self: self.__name)
+    def __ref(self):
+        return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
+    @property
+    def commit(self):
+        return self.__stack.repository.refs.get(self.__ref())
+    def set_commit(self, commit, msg):
+        self.__stack.repository.refs.set(self.__ref(), commit, msg)
+    def delete(self):
+        self.__stack.repository.refs.delete(self.__ref())
+    def is_applied(self):
+        return self.name in self.__stack.patchorder.applied
+    def is_empty(self):
+        c = self.commit
+        return c.data.tree == c.data.parent.data.tree
+
+class PatchOrder(object):
+    """Keeps track of patch order, and which patches are applied.
+    Works with patch names, not actual patches."""
+    __list_order = [ 'applied', 'unapplied' ]
+    def __init__(self, stack):
+        self.__stack = stack
+        self.__lists = {}
+    def __read_file(self, fn):
+        return tuple(utils.read_strings(
+            os.path.join(self.__stack.directory, fn)))
+    def __write_file(self, fn, val):
+        utils.write_strings(os.path.join(self.__stack.directory, fn), val)
+    def __get_list(self, name):
+        if not name in self.__lists:
+            self.__lists[name] = self.__read_file(name)
+        return self.__lists[name]
+    def __set_list(self, name, val):
+        val = tuple(val)
+        if val != self.__lists.get(name, None):
+            self.__lists[name] = val
+            self.__write_file(name, val)
+    applied = property(lambda self: self.__get_list('applied'),
+                       lambda self, val: self.__set_list('applied', val))
+    unapplied = property(lambda self: self.__get_list('unapplied'),
+                         lambda self, val: self.__set_list('unapplied', val))
+
+class Patches(object):
+    """Creates Patch objects."""
+    def __init__(self, stack):
+        self.__stack = stack
+        def create_patch(name):
+            p = Patch(self.__stack, name)
+            p.commit # raise exception if the patch doesn't exist
+            return p
+        self.__patches = git.ObjectCache(create_patch) # name -> Patch
+    def exists(self, name):
+        try:
+            self.get(name)
+            return True
+        except KeyError:
+            return False
+    def get(self, name):
+        return self.__patches[name]
+    def new(self, name, commit, msg):
+        assert not name in self.__patches
+        p = Patch(self.__stack, name)
+        p.set_commit(commit, msg)
+        self.__patches[name] = p
+        return p
+
+class Stack(object):
+    def __init__(self, repository, name):
+        self.__repository = repository
+        self.__name = name
+        self.__patchorder = PatchOrder(self)
+        self.__patches = Patches(self)
+    name = property(lambda self: self.__name)
+    repository = property(lambda self: self.__repository)
+    patchorder = property(lambda self: self.__patchorder)
+    patches = property(lambda self: self.__patches)
+    @property
+    def directory(self):
+        return os.path.join(self.__repository.directory, 'patches', self.__name)
+    def __ref(self):
+        return 'refs/heads/%s' % self.__name
+    @property
+    def head(self):
+        return self.__repository.refs.get(self.__ref())
+    def set_head(self, commit, msg):
+        self.__repository.refs.set(self.__ref(), commit, msg)
+    @property
+    def base(self):
+        if self.patchorder.applied:
+            return self.patches.get(self.patchorder.applied[0]
+                                    ).commit.data.parent
+        else:
+            return self.head
+
+class Repository(git.Repository):
+    def __init__(self, *args, **kwargs):
+        git.Repository.__init__(self, *args, **kwargs)
+        self.__stacks = {} # name -> Stack
+    @property
+    def current_branch(self):
+        return utils.strip_leading('refs/heads/', self.head)
+    @property
+    def current_stack(self):
+        return self.get_stack(self.current_branch)
+    def get_stack(self, name):
+        if not name in self.__stacks:
+            if name == None:
+                s = None # detached HEAD
+            else:
+                # TODO: verify that the branch exists
+                s = Stack(self, name)
+            self.__stacks[name] = s
+        return self.__stacks[name]
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
new file mode 100644
index 0000000..991e64e
--- /dev/null
+++ b/stgit/lib/transaction.py
@@ -0,0 +1,79 @@
+from stgit import exception
+from stgit.out import *
+
+class TransactionException(exception.StgException):
+    pass
+
+def print_current_patch(old_applied, new_applied):
+    def now_at(pn):
+        out.info('Now at patch "%s"' % pn)
+    if not old_applied and not new_applied:
+        pass
+    elif not old_applied:
+        now_at(new_applied[-1])
+    elif not new_applied:
+        out.info('No patch applied')
+    elif old_applied[-1] == new_applied[-1]:
+        pass
+    else:
+        now_at(new_applied[-1])
+
+class StackTransaction(object):
+    def __init__(self, stack, msg):
+        self.__stack = stack
+        self.__msg = msg
+        self.__patches = {}
+        self.__applied = list(self.__stack.patchorder.applied)
+        self.__unapplied = list(self.__stack.patchorder.unapplied)
+    def __set_patches(self, val):
+        self.__patches = dict(val)
+    patches = property(lambda self: self.__patches, __set_patches)
+    def __set_applied(self, val):
+        self.__applied = list(val)
+    applied = property(lambda self: self.__applied, __set_applied)
+    def __set_unapplied(self, val):
+        self.__unapplied = list(val)
+    unapplied = property(lambda self: self.__unapplied, __set_unapplied)
+    def __check_consistency(self):
+        remaining = set(self.__applied + self.__unapplied)
+        for pn, commit in self.__patches.iteritems():
+            if commit == None:
+                assert self.__stack.patches.exists(pn)
+            else:
+                assert pn in remaining
+    def run(self):
+        self.__check_consistency()
+
+        # Get new head commit.
+        if self.__applied:
+            top_patch = self.__applied[-1]
+            try:
+                new_head = self.__patches[top_patch]
+            except KeyError:
+                new_head = self.__stack.patches.get(top_patch).commit
+        else:
+            new_head = self.__stack.base
+
+        # Set branch head.
+        if new_head == self.__stack.head:
+            pass # same commit: OK
+        elif new_head.data.tree == self.__stack.head.data.tree:
+            pass # same tree: OK
+        else:
+            # We can't handle this case yet.
+            raise TransactionException('Error: HEAD tree changed')
+        self.__stack.set_head(new_head, self.__msg)
+
+        # Write patches.
+        for pn, commit in self.__patches.iteritems():
+            if self.__stack.patches.exists(pn):
+                p = self.__stack.patches.get(pn)
+                if commit == None:
+                    p.delete()
+                else:
+                    p.set_commit(commit, self.__msg)
+            else:
+                self.__stack.patches.new(pn, commit, self.__msg)
+        print_current_patch(self.__stack.patchorder.applied, self.__applied)
+        self.__stack.patchorder.applied = self.__applied
+        self.__stack.patchorder.unapplied = self.__unapplied
diff --git a/stgit/utils.py b/stgit/utils.py
index 3a480c0..b3f6232 100644
--- a/stgit/utils.py
+++ b/stgit/utils.py
@@ -256,3 +256,16 @@ def add_sign_line(desc, sign_str, name, email):
     if not any(s in desc for s in ['\nSigned-off-by:', '\nAcked-by:']):
         desc = desc + '\n'
     return '%s\n%s\n' % (desc, sign_str)
+
+def strip_leading(prefix, s):
+    """Strip leading prefix from a string. Blow up if the prefix isn't
+    there."""
+    assert s.startswith(prefix)
+    return s[len(prefix):]
+
+def add_dict(d1, d2):
+    """Return a new dict with the contents of both d1 and d2. In case of
+    conflicting mappings, d2 takes precedence."""
+    d = dict(d1)
+    d.update(d2)
+    return d

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [StGit PATCH 2/5] Write metadata files used by the old infrastructure
  2007-11-05  3:14 [StGit PATCH 0/5] Start the refactoring Karl Hasselström
  2007-11-05  3:14 ` [StGit PATCH 1/5] New StGit core infrastructure: repository operations Karl Hasselström
@ 2007-11-05  3:14 ` Karl Hasselström
  2007-11-05  3:14 ` [StGit PATCH 3/5] Simple test for "stg clean" Karl Hasselström
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Karl Hasselström @ 2007-11-05  3:14 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git

The new infrastructure doesn't use them, but they're needed to support
the old infrastructure during the transition when both of them are in
use.

Signed-off-by: Karl Hasselström <kha@treskal.com>

---

 stgit/lib/stack.py |   35 +++++++++++++++++++++++++++++++++++
 1 files changed, 35 insertions(+), 0 deletions(-)


diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
index b4d99ba..ce514d6 100644
--- a/stgit/lib/stack.py
+++ b/stgit/lib/stack.py
@@ -9,10 +9,45 @@ class Patch(object):
     name = property(lambda self: self.__name)
     def __ref(self):
         return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
+    def __log_ref(self):
+        return self.__ref() + '.log'
     @property
     def commit(self):
         return self.__stack.repository.refs.get(self.__ref())
+    def __write_compat_files(self, new_commit, msg):
+        """Write files used by the old infrastructure."""
+        fdir = os.path.join(self.__stack.directory, 'patches', self.__name)
+        def write(name, val, multiline = False):
+            fn = os.path.join(fdir, name)
+            if val:
+                utils.write_string(fn, val, multiline)
+            elif os.path.isfile(fn):
+                os.remove(fn)
+        def write_patchlog():
+            try:
+                old_log = [self.__stack.repository.refs.get(self.__log_ref())]
+            except KeyError:
+                old_log = []
+            cd = git.Commitdata(tree = new_commit.data.tree, parents = old_log,
+                                message = '%s\t%s' % (msg, new_commit.sha1))
+            c = self.__stack.repository.commit(cd)
+            self.__stack.repository.refs.set(self.__log_ref(), c, msg)
+            return c
+        d = new_commit.data
+        write('authname', d.author.name)
+        write('authemail', d.author.email)
+        write('authdate', d.author.date)
+        write('commname', d.committer.name)
+        write('commemail', d.committer.email)
+        write('description', d.message)
+        write('log', write_patchlog().sha1)
+        try:
+            old_commit_sha1 = self.commit
+        except KeyError:
+            old_commit_sha1 = None
+        write('top.old', old_commit_sha1)
     def set_commit(self, commit, msg):
+        self.__write_compat_files(commit, msg)
         self.__stack.repository.refs.set(self.__ref(), commit, msg)
     def delete(self):
         self.__stack.repository.refs.delete(self.__ref())

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [StGit PATCH 3/5] Simple test for "stg clean"
  2007-11-05  3:14 [StGit PATCH 0/5] Start the refactoring Karl Hasselström
  2007-11-05  3:14 ` [StGit PATCH 1/5] New StGit core infrastructure: repository operations Karl Hasselström
  2007-11-05  3:14 ` [StGit PATCH 2/5] Write metadata files used by the old infrastructure Karl Hasselström
@ 2007-11-05  3:14 ` Karl Hasselström
  2007-11-05  3:14 ` [StGit PATCH 4/5] Let "stg clean" use the new infrastructure Karl Hasselström
  2007-11-05  3:15 ` [StGit PATCH 5/5] Add "stg coalesce" Karl Hasselström
  4 siblings, 0 replies; 6+ messages in thread
From: Karl Hasselström @ 2007-11-05  3:14 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git

Signed-off-by: Karl Hasselström <kha@treskal.com>

---

 t/t2500-clean.sh |   27 +++++++++++++++++++++++++++
 1 files changed, 27 insertions(+), 0 deletions(-)
 create mode 100755 t/t2500-clean.sh


diff --git a/t/t2500-clean.sh b/t/t2500-clean.sh
new file mode 100755
index 0000000..3364c18
--- /dev/null
+++ b/t/t2500-clean.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+test_description='Run "stg clean"'
+
+. ./test-lib.sh
+
+test_expect_success 'Initialize StGit stack' '
+    stg init &&
+    stg new e0 -m e0 &&
+    stg new p0 -m p0 &&
+    echo foo > foo.txt &&
+    git add foo.txt &&
+    stg refresh &&
+    stg new e1 -m e1 &&
+    stg new e2 -m e2 &&
+    stg pop
+'
+
+test_expect_success 'Clean empty patches' '
+    [ "$(echo $(stg applied))" = "e0 p0 e1" ] &&
+    [ "$(echo $(stg unapplied))" = "e2" ] &&
+    stg clean &&
+    [ "$(echo $(stg applied))" = "p0" ] &&
+    [ "$(echo $(stg unapplied))" = "" ]
+'
+
+test_done

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [StGit PATCH 4/5] Let "stg clean" use the new infrastructure
  2007-11-05  3:14 [StGit PATCH 0/5] Start the refactoring Karl Hasselström
                   ` (2 preceding siblings ...)
  2007-11-05  3:14 ` [StGit PATCH 3/5] Simple test for "stg clean" Karl Hasselström
@ 2007-11-05  3:14 ` Karl Hasselström
  2007-11-05  3:15 ` [StGit PATCH 5/5] Add "stg coalesce" Karl Hasselström
  4 siblings, 0 replies; 6+ messages in thread
From: Karl Hasselström @ 2007-11-05  3:14 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git

TODO:

  * detect uninitialized stack

  * upgrade stack

Signed-off-by: Karl Hasselström <kha@treskal.com>

---

 stgit/commands/clean.py  |   68 ++++++++++++++++++++++++----------------------
 stgit/commands/common.py |   10 ++++++-
 2 files changed, 44 insertions(+), 34 deletions(-)


diff --git a/stgit/commands/clean.py b/stgit/commands/clean.py
index 4484ecd..ad0b920 100644
--- a/stgit/commands/clean.py
+++ b/stgit/commands/clean.py
@@ -15,14 +15,10 @@ 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 optparse import make_option
 from stgit.out import *
-from stgit import stack, git
-
+from stgit.commands import common
+from stgit.lib import transaction
 
 help = 'delete the empty patches in the series'
 usage = """%prog [options]
@@ -31,7 +27,7 @@ Delete the empty patches in the whole series or only those applied or
 unapplied. A patch is considered empty if the two commit objects
 representing its boundaries refer to the same tree object."""
 
-directory = DirectoryHasRepository()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-a', '--applied',
                        help = 'delete the empty applied patches',
                        action = 'store_true'),
@@ -40,18 +36,35 @@ options = [make_option('-a', '--applied',
                        action = 'store_true')]
 
 
-def __delete_empty(patches, applied):
-    """Delete the empty patches
-    """
-    for p in patches:
-        if crt_series.empty_patch(p):
-            out.start('Deleting patch "%s"' % p)
-            if applied and crt_series.patch_applied(p):
-                crt_series.pop_patch(p)
-            crt_series.delete_patch(p)
-            out.done()
-        elif applied and crt_series.patch_unapplied(p):
-            crt_series.push_patch(p)
+def _clean(stack, clean_applied, clean_unapplied):
+    def deleting(pn):
+        out.info('Deleting empty patch %s' % pn)
+    trans = transaction.StackTransaction(stack, 'clean')
+    if clean_unapplied:
+        trans.unapplied = []
+        for pn in stack.patchorder.unapplied:
+            p = stack.patches.get(pn)
+            if p.is_empty():
+                trans.patches[pn] = None
+                deleting(pn)
+            else:
+                trans.unapplied.append[pn]
+    if clean_applied:
+        trans.applied = []
+        parent = stack.base
+        for pn in stack.patchorder.applied:
+            p = stack.patches.get(pn)
+            if p.is_empty():
+                trans.patches[pn] = None
+                deleting(pn)
+            else:
+                if parent != p.commit.data.parent:
+                    parent = trans.patches[pn] = stack.repository.commit(
+                        p.commit.data.set_parent(parent))
+                else:
+                    parent = p.commit
+                trans.applied.append(pn)
+    trans.run()
 
 def func(parser, options, args):
     """Delete the empty patches in the series
@@ -59,19 +72,8 @@ def func(parser, options, args):
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    check_local_changes()
-    check_conflicts()
-    check_head_top_equal(crt_series)
-
     if not (options.applied or options.unapplied):
         options.applied = options.unapplied = True
 
-    if options.applied:
-        applied = crt_series.get_applied()
-        __delete_empty(applied, True)
-
-    if options.unapplied:
-        unapplied = crt_series.get_unapplied()
-        __delete_empty(unapplied, False)
-
-    print_crt_patch(crt_series)
+    _clean(directory.repository.current_stack,
+           options.applied, options.unapplied)
diff --git a/stgit/commands/common.py b/stgit/commands/common.py
index ceeebbc..0a017a8 100644
--- a/stgit/commands/common.py
+++ b/stgit/commands/common.py
@@ -27,7 +27,7 @@ from stgit.out import *
 from stgit.run import *
 from stgit import stack, git, basedir
 from stgit.config import config, file_extensions
-
+from stgit.lib import stack as libstack
 
 # Command exception class
 class CmdException(StgException):
@@ -537,3 +537,11 @@ class DirectoryGotoToplevel(DirectoryInWorktree):
     def setup(self):
         DirectoryInWorktree.setup(self)
         self.cd_to_topdir()
+
+class DirectoryHasRepositoryLib(_Directory):
+    """For commands that use the new infrastructure in stgit.lib.*."""
+    def __init__(self):
+        self.needs_current_series = False
+    def setup(self):
+        # This will throw an exception if we don't have a repository.
+        self.repository = libstack.Repository.default()

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [StGit PATCH 5/5] Add "stg coalesce"
  2007-11-05  3:14 [StGit PATCH 0/5] Start the refactoring Karl Hasselström
                   ` (3 preceding siblings ...)
  2007-11-05  3:14 ` [StGit PATCH 4/5] Let "stg clean" use the new infrastructure Karl Hasselström
@ 2007-11-05  3:15 ` Karl Hasselström
  4 siblings, 0 replies; 6+ messages in thread
From: Karl Hasselström @ 2007-11-05  3:15 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git

It coalesces two or more consecutive applied patches, with no need to
touch index/worktree, and no possibiliy of conflicts.

Future improvements could relax the "consecutive" and "applied"
restrictions, by building a new chain of commits just like "stg push"
will do once it's been converted to the new infrastructure.

Signed-off-by: Karl Hasselström <kha@treskal.com>

---

 stgit/commands/coalesce.py |   87 ++++++++++++++++++++++++++++++++++++++++++++
 stgit/main.py              |    2 +
 stgit/utils.py             |   11 ++++++
 t/t2600-coalesce.sh        |   31 ++++++++++++++++
 4 files changed, 131 insertions(+), 0 deletions(-)
 create mode 100644 stgit/commands/coalesce.py
 create mode 100755 t/t2600-coalesce.sh


diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
new file mode 100644
index 0000000..11b7b52
--- /dev/null
+++ b/stgit/commands/coalesce.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2007, Karl Hasselström <kha@treskal.com>
+
+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
+"""
+
+from optparse import make_option
+from stgit.out import *
+from stgit import utils
+from stgit.commands import common
+from stgit.lib import git, transaction
+
+help = 'coalesce two or more patches into one'
+usage = """%prog [options] <patches>
+
+Coalesce two or more patches, creating one big patch that contains all
+their changes. The patches must all be applied, and must be
+consecutive."""
+
+directory = common.DirectoryHasRepositoryLib()
+options = [make_option('-n', '--name', help = 'name of coalesced patch'),
+           make_option('-m', '--message',
+                       help = 'commit message of coalesced patch')]
+
+def _coalesce(stack, name, msg, patches):
+    applied = stack.patchorder.applied
+
+    # Make sure the patches are consecutive.
+    applied_ix = dict((applied[i], i) for i in xrange(len(applied)))
+    ixes = list(sorted(applied_ix[p] for p in patches))
+    i0, i1 = ixes[0], ixes[-1]
+    if i1 - i0 + 1 != len(patches):
+        raise common.CmdException('The patches must be consecutive')
+
+    # Make a commit for the coalesced patch.
+    def bad_name(pn):
+        return pn not in patches and stack.patches.exists(pn)
+    if name and bad_name(name):
+        raise common.CmdException('Patch name "%s" already taken')
+    ps = [stack.patches.get(pn) for pn in applied[i0:i1+1]]
+    if msg == None:
+        msg = '\n\n'.join('%s\n\n%s' % (p.name.ljust(70, '-'),
+                                        p.commit.data.message)
+                          for p in ps)
+        msg = utils.edit_string(msg, '.stgit-coalesce.txt').strip()
+    if not name:
+        name = utils.make_patch_name(msg, bad_name)
+    cd = git.Commitdata(tree = ps[-1].commit.data.tree,
+                        parents = ps[0].commit.data.parents, message = msg)
+
+    # Rewrite refs.
+    trans = transaction.StackTransaction(stack, 'coalesce')
+    parent = trans.patches[name] = stack.repository.commit(cd)
+    trans.applied = applied[:i0]
+    trans.applied.append(name)
+    for pn in applied[i0:i1+1]:
+        trans.patches[pn] = None
+    for pn in applied[i1+1:]:
+        p = stack.patches.get(pn)
+        parent = trans.patches[pn] = stack.repository.commit(
+            p.commit.data.set_parent(parent))
+        trans.applied.append(pn)
+    trans.run()
+
+def func(parser, options, args):
+    stack = directory.repository.current_stack
+    applied = set(stack.patchorder.applied)
+    for pn in args:
+        if not pn in applied:
+            raise common.CmdException('%s is not applied' % pn)
+    patches = set(args)
+    if len(patches) < 2:
+        raise common.CmdException('Need at least two patches')
+    _coalesce(stack, options.name, options.message, args)
diff --git a/stgit/main.py b/stgit/main.py
index 9ef6d44..ac46cde 100644
--- a/stgit/main.py
+++ b/stgit/main.py
@@ -65,6 +65,7 @@ commands = Commands({
     'diff':             'diff',
     'clean':            'clean',
     'clone':            'clone',
+    'coalesce':         'coalesce',
     'commit':           'commit',
     'edit':             'edit',
     'export':           'export',
@@ -109,6 +110,7 @@ stackcommands = (
     'assimilate',
     'branch',
     'clean',
+    'coalesce',
     'commit',
     'float',
     'goto',
diff --git a/stgit/utils.py b/stgit/utils.py
index b3f6232..688276c 100644
--- a/stgit/utils.py
+++ b/stgit/utils.py
@@ -189,6 +189,17 @@ def call_editor(filename):
         raise EditorException, 'editor failed, exit code: %d' % err
     out.done()
 
+def edit_string(s, filename):
+    f = file(filename, 'w')
+    f.write(s)
+    f.close()
+    call_editor(filename)
+    f = file(filename)
+    s = f.read()
+    f.close()
+    os.remove(filename)
+    return s
+
 def patch_name_from_msg(msg):
     """Return a string to be used as a patch name. This is generated
     from the top line of the string passed as argument."""
diff --git a/t/t2600-coalesce.sh b/t/t2600-coalesce.sh
new file mode 100755
index 0000000..f13a309
--- /dev/null
+++ b/t/t2600-coalesce.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+test_description='Run "stg coalesce"'
+
+. ./test-lib.sh
+
+test_expect_success 'Initialize StGit stack' '
+    stg init &&
+    for i in 0 1 2 3; do
+        stg new p$i -m "foo $i" &&
+        echo "foo $i" >> foo.txt &&
+        git add foo.txt &&
+        stg refresh
+    done
+'
+
+test_expect_success 'Coalesce some patches' '
+    [ "$(echo $(stg applied))" = "p0 p1 p2 p3" ] &&
+    [ "$(echo $(stg unapplied))" = "" ] &&
+    stg coalesce --name=q0 --message="wee woo" p1 p2 &&
+    [ "$(echo $(stg applied))" = "p0 q0 p3" ] &&
+    [ "$(echo $(stg unapplied))" = "" ]
+'
+
+test_expect_success 'Coalesce at stack top' '
+    stg coalesce --name=q1 --message="wee woo wham" q0 p3 &&
+    [ "$(echo $(stg applied))" = "p0 q1" ] &&
+    [ "$(echo $(stg unapplied))" = "" ]
+'
+
+test_done

^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2007-11-05  3:15 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2007-11-05  3:14 [StGit PATCH 0/5] Start the refactoring Karl Hasselström
2007-11-05  3:14 ` [StGit PATCH 1/5] New StGit core infrastructure: repository operations Karl Hasselström
2007-11-05  3:14 ` [StGit PATCH 2/5] Write metadata files used by the old infrastructure Karl Hasselström
2007-11-05  3:14 ` [StGit PATCH 3/5] Simple test for "stg clean" Karl Hasselström
2007-11-05  3:14 ` [StGit PATCH 4/5] Let "stg clean" use the new infrastructure Karl Hasselström
2007-11-05  3:15 ` [StGit PATCH 5/5] Add "stg coalesce" Karl Hasselström

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).