git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [StGit PATCH 00/10] Infrastructure rewrite series
@ 2007-11-25 20:50 Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 01/10] New StGit core infrastructure: repository operations Karl Hasselström
                   ` (9 more replies)
  0 siblings, 10 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:50 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

I've expanded on the infrastructure rewrite series. As of now, clean,
applied, unapplied, goto, uncommit, and the new command coalesce have
been converted.

The main new development is support for index and worktree operations,
so that it can do conflicting merges. This made it possible to convert
"stg goto", but the main practical attraction is the vastly improved
"stg coalesce", which can now take an arbitrary list of patches and
reorder them so that they can be joined to one big patch. It will do
so entirely automatically if the merges resolve automatically, and if
they don't it will pretend to have done a series of pops, pushes, and
deletes, and leave the user to manually resolve the first conflicting
push.

The implementation of coalesce -- particularly the ability to fail
gracefully on conflicts at any intermediate step -- is helped a lot by
some new cool transaction stuff.

Available from

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

---

Karl Hasselström (10):
      Convert "stg uncommit" to the new infrastructure
      Let "stg goto" use the new infrastructure
      Let "stg clean" use the new transaction primitives
      Teach the new infrastructure about the index and worktree
      Let "stg applied" and "stg unapplied" use the new infrastructure
      Add "stg coalesce"
      Let "stg clean" use the new infrastructure
      Upgrade older stacks to newest version
      Write metadata files used by the old infrastructure
      New StGit core infrastructure: repository operations


 contrib/stgit-completion.bash |    2 
 setup.py                      |    2 
 stgit/commands/applied.py     |   27 +--
 stgit/commands/clean.py       |   49 ++---
 stgit/commands/coalesce.py    |  109 ++++++++++++
 stgit/commands/common.py      |   10 +
 stgit/commands/goto.py        |   52 ++----
 stgit/commands/unapplied.py   |   23 +-
 stgit/commands/uncommit.py    |   79 ++++----
 stgit/lib/__init__.py         |   18 ++
 stgit/lib/git.py              |  383 +++++++++++++++++++++++++++++++++++++++++
 stgit/lib/stack.py            |  168 ++++++++++++++++++
 stgit/lib/stackupgrade.py     |   96 ++++++++++
 stgit/lib/transaction.py      |  194 +++++++++++++++++++++
 stgit/main.py                 |    2 
 stgit/stack.py                |  100 +----------
 stgit/utils.py                |   24 +++
 t/t2600-coalesce.sh           |   31 +++
 18 files changed, 1136 insertions(+), 233 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/stackupgrade.py
 create mode 100644 stgit/lib/transaction.py
 create mode 100755 t/t2600-coalesce.sh

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

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

* [StGit PATCH 01/10] New StGit core infrastructure: repository operations
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 02/10] Write metadata files used by the old infrastructure Karl Hasselström
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

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>

---

 setup.py                 |    2 
 stgit/lib/__init__.py    |   18 +++
 stgit/lib/git.py         |  253 ++++++++++++++++++++++++++++++++++++++++++++++
 stgit/lib/stack.py       |  120 ++++++++++++++++++++++
 stgit/lib/transaction.py |   79 ++++++++++++++
 stgit/utils.py           |   13 ++
 6 files changed, 484 insertions(+), 1 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/setup.py b/setup.py
index cf5b1da..cad8f7e 100755
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ setup(name = 'stgit',
       description = 'Stacked GIT',
       long_description = 'Push/pop utility on top of GIT',
       scripts = ['stg'],
-      packages = ['stgit', 'stgit.commands'],
+      packages = ['stgit', 'stgit.commands', 'stgit.lib'],
       data_files = [('share/stgit/templates', glob.glob('templates/*.tmpl')),
                     ('share/stgit/examples', glob.glob('examples/*.tmpl')),
                     ('share/stgit/examples', ['examples/gitconfig']),
diff --git a/stgit/__init__.py b/stgit/lib/__init__.py
similarity index 88%
copy from stgit/__init__.py
copy to stgit/lib/__init__.py
index 4b03e3a..45eb307 100644
--- a/stgit/__init__.py
+++ b/stgit/lib/__init__.py
@@ -1,5 +1,7 @@
+# -*- coding: utf-8 -*-
+
 __copyright__ = """
-Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+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
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
new file mode 100644
index 0000000..120ea35
--- /dev/null
+++ b/stgit/lib/git.py
@@ -0,0 +1,253 @@
+import os, os.path, re
+from stgit import exception, run, utils
+
+class RepositoryException(exception.StgException):
+    pass
+
+class DetachedHeadException(RepositoryException):
+    def __init__(self):
+        RepositoryException.__init__(self, 'Not on any branch')
+
+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 rev_parse(self, rev):
+        try:
+            return self.get_commit(self.run(
+                    ['git', 'rev-parse', '%s^{commit}' % rev]
+                    ).output_one_line())
+        except run.RunException:
+            raise RepositoryException('%s: No such revision' % rev)
+    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..d5bd488
--- /dev/null
+++ b/stgit/lib/stack.py
@@ -0,0 +1,120 @@
+import os.path
+from stgit import exception, 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
+        try:
+            self.head
+        except KeyError:
+            raise exception.StgException('%s: no such branch' % 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:
+            self.__stacks[name] = Stack(self, name)
+        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] 14+ messages in thread

* [StGit PATCH 02/10] Write metadata files used by the old infrastructure
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 01/10] New StGit core infrastructure: repository operations Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 03/10] Upgrade older stacks to newest version Karl Hasselström
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

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 |   52 +++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 49 insertions(+), 3 deletions(-)


diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
index d5bd488..5a34592 100644
--- a/stgit/lib/stack.py
+++ b/stgit/lib/stack.py
@@ -7,15 +7,61 @@ class Patch(object):
         self.__stack = stack
         self.__name = name
     name = property(lambda self: self.__name)
+    @property
     def __ref(self):
         return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
     @property
+    def __log_ref(self):
+        return self.__ref + '.log'
+    @property
     def commit(self):
-        return self.__stack.repository.refs.get(self.__ref())
+        return self.__stack.repository.refs.get(self.__ref)
+    @property
+    def __compat_dir(self):
+        return os.path.join(self.__stack.directory, 'patches', self.__name)
+    def __write_compat_files(self, new_commit, msg):
+        """Write files used by the old infrastructure."""
+        def write(name, val, multiline = False):
+            fn = os.path.join(self.__compat_dir, 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 __delete_compat_files(self):
+        if os.path.isdir(self.__compat_dir):
+            for f in os.listdir(self.__compat_dir):
+                os.remove(os.path.join(self.__compat_dir, f))
+            os.rmdir(self.__compat_dir)
+        self.__stack.repository.refs.delete(self.__log_ref)
     def set_commit(self, commit, msg):
-        self.__stack.repository.refs.set(self.__ref(), 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())
+        self.__delete_compat_files()
+        self.__stack.repository.refs.delete(self.__ref)
     def is_applied(self):
         return self.name in self.__stack.patchorder.applied
     def is_empty(self):

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

* [StGit PATCH 03/10] Upgrade older stacks to newest version
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 01/10] New StGit core infrastructure: repository operations Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 02/10] Write metadata files used by the old infrastructure Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 04/10] Let "stg clean" use the new infrastructure Karl Hasselström
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

This is of course needed by the new infrastructure as well. So break
it out into its own file, where it can be used by both new and old
infrastructure. This has the added benefit of making it easy to see
that the upgrade code doesn't depend on anything it shouldn't.

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

---

 stgit/lib/git.py          |    7 +++
 stgit/lib/stack.py        |    3 +
 stgit/lib/stackupgrade.py |   96 +++++++++++++++++++++++++++++++++++++++++++
 stgit/stack.py            |  100 +++------------------------------------------
 4 files changed, 112 insertions(+), 94 deletions(-)
 create mode 100644 stgit/lib/stackupgrade.py


diff --git a/stgit/lib/git.py b/stgit/lib/git.py
index 120ea35..c4011f9 100644
--- a/stgit/lib/git.py
+++ b/stgit/lib/git.py
@@ -160,6 +160,13 @@ class Refs(object):
         if self.__refs == None:
             self.__cache_refs()
         return self.__repository.get_commit(self.__refs[ref])
+    def exists(self, ref):
+        try:
+            self.get(ref)
+        except KeyError:
+            return False
+        else:
+            return True
     def set(self, ref, commit, msg):
         if self.__refs == None:
             self.__cache_refs()
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
index 5a34592..8fc8b08 100644
--- a/stgit/lib/stack.py
+++ b/stgit/lib/stack.py
@@ -1,6 +1,6 @@
 import os.path
 from stgit import exception, utils
-from stgit.lib import git
+from stgit.lib import git, stackupgrade
 
 class Patch(object):
     def __init__(self, stack, name):
@@ -128,6 +128,7 @@ class Stack(object):
             raise exception.StgException('%s: no such branch' % name)
         self.__patchorder = PatchOrder(self)
         self.__patches = Patches(self)
+        stackupgrade.update_to_current_format_version(repository, name)
     name = property(lambda self: self.__name)
     repository = property(lambda self: self.__repository)
     patchorder = property(lambda self: self.__patchorder)
diff --git a/stgit/lib/stackupgrade.py b/stgit/lib/stackupgrade.py
new file mode 100644
index 0000000..00bfdf0
--- /dev/null
+++ b/stgit/lib/stackupgrade.py
@@ -0,0 +1,96 @@
+import os.path
+from stgit import utils
+from stgit.out import out
+from stgit.config import config
+
+# The current StGit metadata format version.
+FORMAT_VERSION = 2
+
+def format_version_key(branch):
+    return 'branch.%s.stgit.stackformatversion' % branch
+
+def update_to_current_format_version(repository, branch):
+    """Update a potentially older StGit directory structure to the latest
+    version. Note: This function should depend as little as possible
+    on external functions that may change during a format version
+    bump, since it must remain able to process older formats."""
+
+    branch_dir = os.path.join(repository.directory, 'patches', branch)
+    key = format_version_key(branch)
+    old_key = 'branch.%s.stgitformatversion' % branch
+    def get_format_version():
+        """Return the integer format version number, or None if the
+        branch doesn't have any StGit metadata at all, of any version."""
+        fv = config.get(key)
+        ofv = config.get(old_key)
+        if fv:
+            # Great, there's an explicitly recorded format version
+            # number, which means that the branch is initialized and
+            # of that exact version.
+            return int(fv)
+        elif ofv:
+            # Old name for the version info: upgrade it.
+            config.set(key, ofv)
+            config.unset(old_key)
+            return int(ofv)
+        elif os.path.isdir(os.path.join(branch_dir, 'patches')):
+            # There's a .git/patches/<branch>/patches dirctory, which
+            # means this is an initialized version 1 branch.
+            return 1
+        elif os.path.isdir(branch_dir):
+            # There's a .git/patches/<branch> directory, which means
+            # this is an initialized version 0 branch.
+            return 0
+        else:
+            # The branch doesn't seem to be initialized at all.
+            return None
+    def set_format_version(v):
+        out.info('Upgraded branch %s to format version %d' % (branch, v))
+        config.set(key, '%d' % v)
+    def mkdir(d):
+        if not os.path.isdir(d):
+            os.makedirs(d)
+    def rm(f):
+        if os.path.exists(f):
+            os.remove(f)
+    def rm_ref(ref):
+        if repository.refs.exists(ref):
+            repository.refs.delete(ref)
+
+    # Update 0 -> 1.
+    if get_format_version() == 0:
+        mkdir(os.path.join(branch_dir, 'trash'))
+        patch_dir = os.path.join(branch_dir, 'patches')
+        mkdir(patch_dir)
+        refs_base = 'refs/patches/%s' % branch
+        for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
+                      + file(os.path.join(branch_dir, 'applied')).readlines()):
+            patch = patch.strip()
+            os.rename(os.path.join(branch_dir, patch),
+                      os.path.join(patch_dir, patch))
+            topfield = os.path.join(patch_dir, patch, 'top')
+            if os.path.isfile(topfield):
+                top = utils.read_string(topfield, False)
+            else:
+                top = None
+            if top:
+                repository.refs.set(refs_base + '/' + patch,
+                                    repository.get_commit(top), 'StGit upgrade')
+        set_format_version(1)
+
+    # Update 1 -> 2.
+    if get_format_version() == 1:
+        desc_file = os.path.join(branch_dir, 'description')
+        if os.path.isfile(desc_file):
+            desc = utils.read_string(desc_file)
+            if desc:
+                config.set('branch.%s.description' % branch, desc)
+            rm(desc_file)
+        rm(os.path.join(branch_dir, 'current'))
+        rm_ref('refs/bases/%s' % branch)
+        set_format_version(2)
+
+    # Make sure we're at the latest version.
+    if not get_format_version() in [None, FORMAT_VERSION]:
+        raise StackException('Branch %s is at format version %d, expected %d'
+                             % (branch, get_format_version(), FORMAT_VERSION))
diff --git a/stgit/stack.py b/stgit/stack.py
index f93d842..29e92c9 100644
--- a/stgit/stack.py
+++ b/stgit/stack.py
@@ -28,7 +28,7 @@ from stgit.run import *
 from stgit import git, basedir, templates
 from stgit.config import config
 from shutil import copyfile
-
+from stgit.lib import git as libgit, stackupgrade
 
 # stack exception class
 class StackException(StgException):
@@ -279,9 +279,6 @@ class Patch(StgitObject):
         self._set_field('log', value)
         self.__update_log_ref(value)
 
-# The current StGIT metadata format version.
-FORMAT_VERSION = 2
-
 class PatchSet(StgitObject):
     def __init__(self, name = None):
         try:
@@ -349,7 +346,8 @@ class PatchSet(StgitObject):
     def is_initialised(self):
         """Checks if series is already initialised
         """
-        return bool(config.get(self.format_version_key()))
+        return config.get(stackupgrade.format_version_key(self.get_name())
+                          ) != None
 
 
 def shortlog(patches):
@@ -368,7 +366,8 @@ class Series(PatchSet):
 
         # Update the branch to the latest format version if it is
         # initialized, but don't touch it if it isn't.
-        self.update_to_current_format_version()
+        stackupgrade.update_to_current_format_version(
+            libgit.Repository.default(), self.get_name())
 
         self.__refs_base = 'refs/patches/%s' % self.get_name()
 
@@ -382,92 +381,6 @@ class Series(PatchSet):
         # trash directory
         self.__trash_dir = os.path.join(self._dir(), 'trash')
 
-    def format_version_key(self):
-        return 'branch.%s.stgit.stackformatversion' % self.get_name()
-
-    def update_to_current_format_version(self):
-        """Update a potentially older StGIT directory structure to the
-        latest version. Note: This function should depend as little as
-        possible on external functions that may change during a format
-        version bump, since it must remain able to process older formats."""
-
-        branch_dir = os.path.join(self._basedir(), 'patches', self.get_name())
-        def get_format_version():
-            """Return the integer format version number, or None if the
-            branch doesn't have any StGIT metadata at all, of any version."""
-            fv = config.get(self.format_version_key())
-            ofv = config.get('branch.%s.stgitformatversion' % self.get_name())
-            if fv:
-                # Great, there's an explicitly recorded format version
-                # number, which means that the branch is initialized and
-                # of that exact version.
-                return int(fv)
-            elif ofv:
-                # Old name for the version info, upgrade it
-                config.set(self.format_version_key(), ofv)
-                config.unset('branch.%s.stgitformatversion' % self.get_name())
-                return int(ofv)
-            elif os.path.isdir(os.path.join(branch_dir, 'patches')):
-                # There's a .git/patches/<branch>/patches dirctory, which
-                # means this is an initialized version 1 branch.
-                return 1
-            elif os.path.isdir(branch_dir):
-                # There's a .git/patches/<branch> directory, which means
-                # this is an initialized version 0 branch.
-                return 0
-            else:
-                # The branch doesn't seem to be initialized at all.
-                return None
-        def set_format_version(v):
-            out.info('Upgraded branch %s to format version %d' % (self.get_name(), v))
-            config.set(self.format_version_key(), '%d' % v)
-        def mkdir(d):
-            if not os.path.isdir(d):
-                os.makedirs(d)
-        def rm(f):
-            if os.path.exists(f):
-                os.remove(f)
-        def rm_ref(ref):
-            if git.ref_exists(ref):
-                git.delete_ref(ref)
-
-        # Update 0 -> 1.
-        if get_format_version() == 0:
-            mkdir(os.path.join(branch_dir, 'trash'))
-            patch_dir = os.path.join(branch_dir, 'patches')
-            mkdir(patch_dir)
-            refs_base = 'refs/patches/%s' % self.get_name()
-            for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
-                          + file(os.path.join(branch_dir, 'applied')).readlines()):
-                patch = patch.strip()
-                os.rename(os.path.join(branch_dir, patch),
-                          os.path.join(patch_dir, patch))
-                topfield = os.path.join(patch_dir, patch, 'top')
-                if os.path.isfile(topfield):
-                    top = read_string(topfield, False)
-                else:
-                    top = None
-                if top:
-                    git.set_ref(refs_base + '/' + patch, top)
-            set_format_version(1)
-
-        # Update 1 -> 2.
-        if get_format_version() == 1:
-            desc_file = os.path.join(branch_dir, 'description')
-            if os.path.isfile(desc_file):
-                desc = read_string(desc_file)
-                if desc:
-                    config.set('branch.%s.description' % self.get_name(), desc)
-                rm(desc_file)
-            rm(os.path.join(branch_dir, 'current'))
-            rm_ref('refs/bases/%s' % self.get_name())
-            set_format_version(2)
-
-        # Make sure we're at the latest version.
-        if not get_format_version() in [None, FORMAT_VERSION]:
-            raise StackException('Branch %s is at format version %d, expected %d'
-                                 % (self.get_name(), get_format_version(), FORMAT_VERSION))
-
     def __patch_name_valid(self, name):
         """Raise an exception if the patch name is not valid.
         """
@@ -620,7 +533,8 @@ class Series(PatchSet):
         self.create_empty_field('applied')
         self.create_empty_field('unapplied')
 
-        config.set(self.format_version_key(), str(FORMAT_VERSION))
+        config.set(stackupgrade.format_version_key(self.get_name()),
+                   str(stackupgrade.FORMAT_VERSION))
 
     def rename(self, to_name):
         """Renames a series

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

* [StGit PATCH 04/10] Let "stg clean" use the new infrastructure
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
                   ` (2 preceding siblings ...)
  2007-11-25 20:51 ` [StGit PATCH 03/10] Upgrade older stacks to newest version Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 05/10] Add "stg coalesce" Karl Hasselström
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

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 c703418..bbea253 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 = DirectoryGotoToplevel()
+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 36202dd..6271572 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] 14+ messages in thread

* [StGit PATCH 05/10] Add "stg coalesce"
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
                   ` (3 preceding siblings ...)
  2007-11-25 20:51 ` [StGit PATCH 04/10] Let "stg clean" use the new infrastructure Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 06/10] Let "stg applied" and "stg unapplied" use the new infrastructure Karl Hasselström
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

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>

---

 contrib/stgit-completion.bash |    2 +
 stgit/commands/coalesce.py    |   84 +++++++++++++++++++++++++++++++++++++++++
 stgit/main.py                 |    2 +
 stgit/utils.py                |   11 +++++
 t/t2600-coalesce.sh           |   31 +++++++++++++++
 5 files changed, 130 insertions(+), 0 deletions(-)
 create mode 100644 stgit/commands/coalesce.py
 create mode 100755 t/t2600-coalesce.sh


diff --git a/contrib/stgit-completion.bash b/contrib/stgit-completion.bash
index b3b23d4..b02eb64 100644
--- a/contrib/stgit-completion.bash
+++ b/contrib/stgit-completion.bash
@@ -18,6 +18,7 @@ _stg_commands="
     diff
     clean
     clone
+    coalesce
     commit
     cp
     edit
@@ -238,6 +239,7 @@ _stg ()
         # repository commands
         id)     _stg_patches $command _all_patches ;;
         # stack commands
+        coalesce) _stg_patches $command _applied_patches ;;
         float)  _stg_patches $command _all_patches ;;
         goto)   _stg_patches $command _all_other_patches ;;
         hide)   _stg_patches $command _unapplied_patches ;;
diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
new file mode 100644
index 0000000..c4c1cf8
--- /dev/null
+++ b/stgit/commands/coalesce.py
@@ -0,0 +1,84 @@
+# -*- 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, 'stg coalesce')
+    for pn in applied[i0:i1+1]:
+        trans.patches[pn] = None
+    parent = trans.patches[name] = stack.repository.commit(cd)
+    trans.applied = applied[:i0]
+    trans.applied.append(name)
+    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)
+    patches = set(common.parse_patches(args, list(stack.patchorder.applied)))
+    if len(patches) < 2:
+        raise common.CmdException('Need at least two patches')
+    _coalesce(stack, options.name, options.message, patches)
diff --git a/stgit/main.py b/stgit/main.py
index deaac91..384803b 100644
--- a/stgit/main.py
+++ b/stgit/main.py
@@ -64,6 +64,7 @@ commands = Commands({
     'diff':             'diff',
     'clean':            'clean',
     'clone':            'clone',
+    'coalesce':         'coalesce',
     'commit':           'commit',
     'edit':             'edit',
     'export':           'export',
@@ -108,6 +109,7 @@ stackcommands = (
     'applied',
     '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] 14+ messages in thread

* [StGit PATCH 06/10] Let "stg applied" and "stg unapplied" use the new infrastructure
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
                   ` (4 preceding siblings ...)
  2007-11-25 20:51 ` [StGit PATCH 05/10] Add "stg coalesce" Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree Karl Hasselström
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

This is a trivial change since these commands are so simple, but
because these are the commands used by t4000-upgrade, we now test that
the new infrastructure can upgrade old stacks.

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

---

 stgit/commands/applied.py   |   27 +++++++++++++--------------
 stgit/commands/unapplied.py |   23 +++++++++++------------
 2 files changed, 24 insertions(+), 26 deletions(-)


diff --git a/stgit/commands/applied.py b/stgit/commands/applied.py
index 45d0926..522425b 100644
--- a/stgit/commands/applied.py
+++ b/stgit/commands/applied.py
@@ -16,25 +16,21 @@ 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
 
 
 help = 'print the applied patches'
 usage = """%prog [options]
 
-List the patches from the series which were already pushed onto the
-stack.  They are listed in the order in which they were pushed, the
+List the patches from the series which have already been pushed onto
+the stack. They are listed in the order in which they were pushed, the
 last one being the current (topmost) patch."""
 
-directory = DirectoryHasRepository()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
+                       help = 'use BRANCH instead of the default branch'),
            make_option('-c', '--count',
                        help = 'print the number of applied patches',
                        action = 'store_true')]
@@ -46,10 +42,13 @@ def func(parser, options, args):
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    applied = crt_series.get_applied()
+    if options.branch:
+        s = directory.repository.get_stack(options.branch)
+    else:
+        s = directory.repository.current_stack
 
     if options.count:
-        out.stdout(len(applied))
+        out.stdout(len(s.patchorder.applied))
     else:
-        for p in applied:
-            out.stdout(p)
+        for pn in s.patchorder.applied:
+            out.stdout(pn)
diff --git a/stgit/commands/unapplied.py b/stgit/commands/unapplied.py
index d5bb43e..7702207 100644
--- a/stgit/commands/unapplied.py
+++ b/stgit/commands/unapplied.py
@@ -16,13 +16,9 @@ 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
 
 
 help = 'print the unapplied patches'
@@ -31,9 +27,9 @@ usage = """%prog [options]
 List the patches from the series which are not pushed onto the stack.
 They are listed in the reverse order in which they were popped."""
 
-directory = DirectoryHasRepository()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-b', '--branch',
-                       help = 'use BRANCH instead of the default one'),
+                       help = 'use BRANCH instead of the default branch'),
            make_option('-c', '--count',
                        help = 'print the number of unapplied patches',
                        action = 'store_true')]
@@ -45,10 +41,13 @@ def func(parser, options, args):
     if len(args) != 0:
         parser.error('incorrect number of arguments')
 
-    unapplied = crt_series.get_unapplied()
+    if options.branch:
+        s = directory.repository.get_stack(options.branch)
+    else:
+        s = directory.repository.current_stack
 
     if options.count:
-        out.stdout(len(unapplied))
+        out.stdout(len(s.patchorder.unapplied))
     else:
-        for p in unapplied:
-            out.stdout(p)
+        for pn in s.patchorder.unapplied:
+            out.stdout(pn)

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

* [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
                   ` (5 preceding siblings ...)
  2007-11-25 20:51 ` [StGit PATCH 06/10] Let "stg applied" and "stg unapplied" use the new infrastructure Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-26  8:31   ` Karl Hasselström
  2007-11-26  8:56   ` David Kågedal
  2007-11-25 20:51 ` [StGit PATCH 08/10] Let "stg clean" use the new transaction primitives Karl Hasselström
                   ` (2 subsequent siblings)
  9 siblings, 2 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

And use the new powers to make "stg coalesce" able to handle arbitrary
patches, not just consecutive applied patches.

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

---

 stgit/commands/clean.py    |    4 +
 stgit/commands/coalesce.py |   93 ++++++++++++++++---------
 stgit/lib/git.py           |  123 ++++++++++++++++++++++++++++++++++
 stgit/lib/stack.py         |    7 +-
 stgit/lib/transaction.py   |  161 ++++++++++++++++++++++++++++++++++++++------
 5 files changed, 326 insertions(+), 62 deletions(-)


diff --git a/stgit/commands/clean.py b/stgit/commands/clean.py
index bbea253..e2d1678 100644
--- a/stgit/commands/clean.py
+++ b/stgit/commands/clean.py
@@ -44,7 +44,7 @@ def _clean(stack, clean_applied, clean_unapplied):
         trans.unapplied = []
         for pn in stack.patchorder.unapplied:
             p = stack.patches.get(pn)
-            if p.is_empty():
+            if p.commit.data.is_empty():
                 trans.patches[pn] = None
                 deleting(pn)
             else:
@@ -54,7 +54,7 @@ def _clean(stack, clean_applied, clean_unapplied):
         parent = stack.base
         for pn in stack.patchorder.applied:
             p = stack.patches.get(pn)
-            if p.is_empty():
+            if p.commit.data.is_empty():
                 trans.patches[pn] = None
                 deleting(pn)
             else:
diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
index c4c1cf8..2b121a9 100644
--- a/stgit/commands/coalesce.py
+++ b/stgit/commands/coalesce.py
@@ -35,50 +35,75 @@ 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
+def _coalesce_patches(trans, patches, msg):
+    cd = trans.patches[patches[0]].data
+    cd = git.Commitdata(tree = cd.tree, parents = cd.parents)
+    for pn in patches[1:]:
+        c = trans.patches[pn]
+        tree = trans.stack.repository.simple_merge(
+            base = c.data.parent.data.tree,
+            ours = cd.tree, theirs = c.data.tree)
+        if not tree:
+            return None
+        cd = cd.set_tree(tree)
+    if msg == None:
+        msg = '\n\n'.join('%s\n\n%s' % (pn.ljust(70, '-'),
+                                        trans.patches[pn].data.message)
+                          for pn in patches)
+        msg = utils.edit_string(msg, '.stgit-coalesce.txt').strip()
+    cd = cd.set_message(msg)
 
-    # 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')
+    return cd
 
-    # Make a commit for the coalesced patch.
+def _coalesce(stack, iw, name, msg, patches):
+
+    # If a name was supplied on the command line, make sure it's OK.
     def bad_name(pn):
         return pn not in patches and stack.patches.exists(pn)
+    def get_name(cd):
+        return name or utils.make_patch_name(cd.message, bad_name)
     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.
+    def make_coalesced_patch(trans, new_commit_data):
+        name = get_name(new_commit_data)
+        trans.patches[name] = stack.repository.commit(new_commit_data)
+        trans.unapplied.insert(0, name)
+
     trans = transaction.StackTransaction(stack, 'stg coalesce')
-    for pn in applied[i0:i1+1]:
-        trans.patches[pn] = None
-    parent = trans.patches[name] = stack.repository.commit(cd)
-    trans.applied = applied[:i0]
-    trans.applied.append(name)
-    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()
+    push_new_patch = bool(set(patches) & set(trans.applied))
+    new_commit_data = _coalesce_patches(trans, patches, msg)
+    try:
+        if new_commit_data:
+            # We were able to construct the coalesced commit
+            # automatically. So just delete its constituent patches.
+            to_push = trans.delete_patches(lambda pn: pn in patches)
+            make_coalesced_patch(trans, new_commit_data)
+        else:
+            # Automatic construction failed. So push the patches
+            # consecutively, so that a second construction attempt is
+            # guaranteed to work.
+            to_push = trans.pop_patches(lambda pn: pn in patches)
+            for pn in patches:
+                trans.push_patch(pn, iw)
+            new_commit_data = _coalesce_patches(trans, patches, msg)
+            make_coalesced_patch(trans, new_commit_data)
+            assert not trans.delete_patches(lambda pn: pn in patches)
+
+        # Push the new patch if necessary, and any unrelated patches we've
+        # had to pop out of the way.
+        if push_new_patch:
+            trans.push_patch(get_name(new_commit_data), iw)
+        for pn in to_push:
+            trans.push_patch(pn, iw)
+    except transaction.TransactionHalted:
+        pass
+    trans.run(iw)
 
 def func(parser, options, args):
     stack = directory.repository.current_stack
-    applied = set(stack.patchorder.applied)
-    patches = set(common.parse_patches(args, list(stack.patchorder.applied)))
+    patches = common.parse_patches(args, list(stack.patchorder.applied))
     if len(patches) < 2:
         raise common.CmdException('Need at least two patches')
-    _coalesce(stack, options.name, options.message, patches)
+    _coalesce(stack, stack.repository.default_iw(),
+              options.name, options.message, patches)
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
index c4011f9..ab4a376 100644
--- a/stgit/lib/git.py
+++ b/stgit/lib/git.py
@@ -95,6 +95,8 @@ class Commitdata(Repr):
         return type(self)(committer = committer, defaults = self)
     def set_message(self, message):
         return type(self)(message = message, defaults = self)
+    def is_empty(self):
+        return self.tree == self.parent.data.tree
     def __str__(self):
         if self.tree == None:
             tree = None
@@ -218,6 +220,21 @@ class Repository(RunWithEnv):
                                ).output_one_line())
         except run.RunException:
             raise RepositoryException('Cannot find git repository')
+    def default_index(self):
+        return Index(self, (os.environ.get('GIT_INDEX_FILE', None)
+                            or os.path.join(self.__git_dir, 'index')))
+    def temp_index(self):
+        return Index(self, self.__git_dir)
+    def default_worktree(self):
+        path = os.environ.get('GIT_WORK_TREE', None)
+        if not path:
+            o = run.Run('git', 'rev-parse', '--show-cdup').output_lines()
+            o = o or ['.']
+            assert len(o) == 1
+            path = o[0]
+        return Worktree(path)
+    def default_iw(self):
+        return IndexAndWorktree(self.default_index(), self.default_worktree())
     directory = property(lambda self: self.__git_dir)
     refs = property(lambda self: self.__refs)
     def cat_object(self, sha1):
@@ -258,3 +275,109 @@ class Repository(RunWithEnv):
             raise DetachedHeadException()
     def set_head(self, ref, msg):
         self.run(['git', 'symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()
+    def simple_merge(self, base, ours, theirs):
+        """Given three trees, tries to do an in-index merge in a temporary
+        index with a temporary index. Returns the result tree, or None if
+        the merge failed (due to conflicts)."""
+        assert isinstance(base, Tree)
+        assert isinstance(ours, Tree)
+        assert isinstance(theirs, Tree)
+
+        # Take care of the really trivial cases.
+        if base == ours:
+            return theirs
+        if base == theirs:
+            return ours
+        if ours == theirs:
+            return ours
+
+        index = self.temp_index()
+        try:
+            index.merge(base, ours, theirs)
+            try:
+                return index.write_tree()
+            except MergeException:
+                return None
+        finally:
+            index.delete()
+
+class MergeException(exception.StgException):
+    pass
+
+class Index(RunWithEnv):
+    def __init__(self, repository, filename):
+        self.__repository = repository
+        if os.path.isdir(filename):
+            # Create a temp index in the given directory.
+            self.__filename = os.path.join(
+                filename, 'index.temp-%d-%x' % (os.getpid(), id(self)))
+            self.delete()
+        else:
+            self.__filename = filename
+    env = property(lambda self: utils.add_dict(
+            self.__repository.env, { 'GIT_INDEX_FILE': self.__filename }))
+    def read_tree(self, tree):
+        self.run(['git', 'read-tree', tree.sha1]).no_output()
+    def write_tree(self):
+        try:
+            return self.__repository.get_tree(
+                self.run(['git', 'write-tree']).discard_stderr(
+                    ).output_one_line())
+        except run.RunException:
+            raise MergeException('Conflicting merge')
+    def is_clean(self):
+        try:
+            self.run(['git', 'update-index', '--refresh']).discard_output()
+        except run.RunException:
+            return False
+        else:
+            return True
+    def merge(self, base, ours, theirs):
+        """In-index merge, no worktree involved."""
+        self.run(['git', 'read-tree', '-m', '-i', '--aggressive',
+                  base.sha1, ours.sha1, theirs.sha1]).no_output()
+    def delete(self):
+        if os.path.isfile(self.__filename):
+            os.remove(self.__filename)
+
+class Worktree(object):
+    def __init__(self, directory):
+        self.__directory = directory
+    env = property(lambda self: { 'GIT_WORK_TREE': self.__directory })
+
+class CheckoutException(exception.StgException):
+    pass
+
+class IndexAndWorktree(RunWithEnv):
+    def __init__(self, index, worktree):
+        self.__index = index
+        self.__worktree = worktree
+    index = property(lambda self: self.__index)
+    env = property(lambda self: utils.add_dict(self.__index.env,
+                                               self.__worktree.env))
+    def checkout(self, old_tree, new_tree):
+        # TODO: Optionally do a 3-way instead of doing nothing when we
+        # have a problem. Or maybe we should stash changes in a patch?
+        assert isinstance(old_tree, Tree)
+        assert isinstance(new_tree, Tree)
+        try:
+            self.run(['git', 'read-tree', '-u', '-m',
+                      '--exclude-per-directory=.gitignore',
+                      old_tree.sha1, new_tree.sha1]
+                     ).discard_output()
+        except run.RunException:
+            raise CheckoutException('Index/workdir dirty')
+    def merge(self, base, ours, theirs):
+        assert isinstance(base, Tree)
+        assert isinstance(ours, Tree)
+        assert isinstance(theirs, Tree)
+        try:
+            self.run(['git', 'merge-recursive', base.sha1, '--', ours.sha1,
+                      theirs.sha1]).discard_output()
+        except run.RunException, e:
+            raise MergeException('Index/worktree dirty')
+    def changed_files(self):
+        return self.run(['git', 'diff-files', '--name-only']).output_lines()
+    def update_index(self, files):
+        self.run(['git', 'update-index', '--remove', '-z', '--stdin']
+                 ).input_nulterm(files).discard_output()
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
index 8fc8b08..b4512b1 100644
--- a/stgit/lib/stack.py
+++ b/stgit/lib/stack.py
@@ -64,9 +64,6 @@ class Patch(object):
         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.
@@ -150,6 +147,10 @@ class Stack(object):
                                     ).commit.data.parent
         else:
             return self.head
+    def head_top_equal(self):
+        if not self.patchorder.applied:
+            return True
+        return self.head == self.patches.get(self.patchorder.applied[-1]).commit
 
 class Repository(git.Repository):
     def __init__(self, *args, **kwargs):
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
index 991e64e..c9d355d 100644
--- a/stgit/lib/transaction.py
+++ b/stgit/lib/transaction.py
@@ -1,10 +1,14 @@
 from stgit import exception
 from stgit.out import *
+from stgit.lib import git
 
 class TransactionException(exception.StgException):
     pass
 
-def print_current_patch(old_applied, new_applied):
+class TransactionHalted(TransactionException):
+    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:
@@ -18,22 +22,47 @@ def print_current_patch(old_applied, new_applied):
     else:
         now_at(new_applied[-1])
 
+class _TransPatchMap(dict):
+    def __init__(self, stack):
+        dict.__init__(self)
+        self.__stack = stack
+    def __getitem__(self, pn):
+        try:
+            return dict.__getitem__(self, pn)
+        except KeyError:
+            return self.__stack.patches.get(pn).commit
+
 class StackTransaction(object):
     def __init__(self, stack, msg):
         self.__stack = stack
         self.__msg = msg
-        self.__patches = {}
+        self.__patches = _TransPatchMap(stack)
         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)
+        self.__error = None
+        self.__current_tree = self.__stack.head.data.tree
+    stack = property(lambda self: self.__stack)
+    patches = property(lambda self: self.__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 __checkout(self, tree, iw):
+        if not self.__stack.head_top_equal():
+            out.error('HEAD and top are not the same.',
+                      'This can happen if you modify a branch with git.',
+                      'The "repair" command can fix this situation.')
+            self.__abort()
+        if self.__current_tree != tree:
+            assert iw != None
+            iw.checkout(self.__current_tree, tree)
+            self.__current_tree = tree
+    @staticmethod
+    def __abort():
+        raise TransactionException(
+            'Command aborted (all changes rolled back)')
     def __check_consistency(self):
         remaining = set(self.__applied + self.__unapplied)
         for pn, commit in self.__patches.iteritems():
@@ -41,29 +70,29 @@ class StackTransaction(object):
                 assert self.__stack.patches.exists(pn)
             else:
                 assert pn in remaining
-    def run(self):
-        self.__check_consistency()
-
-        # Get new head commit.
+    @property
+    def __head(self):
         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
+            return self.__patches[self.__applied[-1]]
         else:
-            new_head = self.__stack.base
+            return self.__stack.base
+    def run(self, iw = None):
+        self.__check_consistency()
+        new_head = self.__head
 
         # 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')
+        try:
+            self.__checkout(new_head.data.tree, iw)
+        except git.CheckoutException:
+            # We have to abort the transaction. The only state we need
+            # to restore is index+worktree.
+            self.__checkout(self.__stack.head.data.tree, iw)
+            self.__abort()
         self.__stack.set_head(new_head, self.__msg)
 
+        if self.__error:
+            out.error(self.__error)
+
         # Write patches.
         for pn, commit in self.__patches.iteritems():
             if self.__stack.patches.exists(pn):
@@ -74,6 +103,92 @@ class StackTransaction(object):
                     p.set_commit(commit, self.__msg)
             else:
                 self.__stack.patches.new(pn, commit, self.__msg)
-        print_current_patch(self.__stack.patchorder.applied, self.__applied)
+        _print_current_patch(self.__stack.patchorder.applied, self.__applied)
         self.__stack.patchorder.applied = self.__applied
         self.__stack.patchorder.unapplied = self.__unapplied
+
+    def __halt(self, msg):
+        self.__error = msg
+        raise TransactionHalted(msg)
+
+    @staticmethod
+    def __print_popped(popped):
+        if len(popped) == 0:
+            pass
+        elif len(popped) == 1:
+            out.info('Popped %s' % popped[0])
+        else:
+            out.info('Popped %s -- %s' % (popped[-1], popped[0]))
+
+    def pop_patches(self, p):
+        """Pop all patches pn for which p(pn) is true. Return the list of
+        other patches that had to be popped to accomplish this."""
+        popped = []
+        for i in xrange(len(self.applied)):
+            if p(self.applied[i]):
+                popped = self.applied[i:]
+                del self.applied[i:]
+                break
+        popped1 = [pn for pn in popped if not p(pn)]
+        popped2 = [pn for pn in popped if p(pn)]
+        self.unapplied = popped1 + popped2 + self.unapplied
+        self.__print_popped(popped)
+        return popped1
+
+    def delete_patches(self, p):
+        """Delete all patches pn for which p(pn) is true. Return the list of
+        other patches that had to be popped to accomplish this."""
+        popped = []
+        all_patches = self.applied + self.unapplied
+        for i in xrange(len(self.applied)):
+            if p(self.applied[i]):
+                popped = self.applied[i:]
+                del self.applied[i:]
+                break
+        popped = [pn for pn in popped if not p(pn)]
+        self.unapplied = popped + [pn for pn in self.unapplied if not p(pn)]
+        self.__print_popped(popped)
+        for pn in all_patches:
+            if p(pn):
+                s = ['', ' (empty)'][self.patches[pn].data.is_empty()]
+                self.patches[pn] = None
+                out.info('Deleted %s%s' % (pn, s))
+        return popped
+
+    def push_patch(self, pn, iw = None):
+        """Attempt to push the named patch. If this results in conflicts,
+        halts the transaction. If index+worktree are given, spill any
+        conflicts to them."""
+        i = self.unapplied.index(pn)
+        cd = self.patches[pn].data
+        s = ['', ' (empty)'][cd.is_empty()]
+        oldparent = cd.parent
+        cd = cd.set_parent(self.__head)
+        base = oldparent.data.tree
+        ours = cd.parent.data.tree
+        theirs = cd.tree
+        tree = self.__stack.repository.simple_merge(base, ours, theirs)
+        merge_conflict = False
+        if not tree:
+            if iw == None:
+                self.__halt('%s does not apply cleanly' % pn)
+            try:
+                self.__checkout(ours, iw)
+            except git.CheckoutException:
+                self.__halt('Index/worktree dirty')
+            try:
+                iw.merge(base, ours, theirs)
+                tree = iw.index.write_tree()
+                self.__current_tree = tree
+                s = ' (modified)'
+            except git.MergeException:
+                tree = ours
+                merge_conflict = True
+                s = ' (conflict)'
+        cd = cd.set_tree(tree)
+        self.patches[pn] = self.__stack.repository.commit(cd)
+        del self.unapplied[i]
+        self.applied.append(pn)
+        out.info('Pushed %s%s' % (pn, s))
+        if merge_conflict:
+            self.__halt('Merge conflict')

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

* [StGit PATCH 08/10] Let "stg clean" use the new transaction primitives
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
                   ` (6 preceding siblings ...)
  2007-11-25 20:51 ` [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 09/10] Let "stg goto" use the new infrastructure Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 10/10] Convert "stg uncommit" to " Karl Hasselström
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

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

---

 stgit/commands/clean.py |   33 +++++++--------------------------
 1 files changed, 7 insertions(+), 26 deletions(-)


diff --git a/stgit/commands/clean.py b/stgit/commands/clean.py
index e2d1678..cfcc004 100644
--- a/stgit/commands/clean.py
+++ b/stgit/commands/clean.py
@@ -37,33 +37,14 @@ options = [make_option('-a', '--applied',
 
 
 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.commit.data.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.commit.data.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)
+    def del_patch(pn):
+        if pn in stack.patchorder.applied:
+            return clean_applied and trans.patches[pn].data.is_empty()
+        elif pn in stack.patchorder.unapplied:
+            return clean_unapplied and trans.patches[pn].data.is_empty()
+    for pn in trans.delete_patches(del_patch):
+        trans.push_patch(pn)
     trans.run()
 
 def func(parser, options, args):

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

* [StGit PATCH 09/10] Let "stg goto" use the new infrastructure
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
                   ` (7 preceding siblings ...)
  2007-11-25 20:51 ` [StGit PATCH 08/10] Let "stg clean" use the new transaction primitives Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  2007-11-25 20:51 ` [StGit PATCH 10/10] Convert "stg uncommit" to " Karl Hasselström
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

In the process, it loses the --keep option, since the new
infrastructure always keeps local changes (and aborts cleanly if they
are in the way).

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

---

 stgit/commands/clean.py |    2 +-
 stgit/commands/goto.py  |   52 ++++++++++++++++-------------------------------
 2 files changed, 19 insertions(+), 35 deletions(-)


diff --git a/stgit/commands/clean.py b/stgit/commands/clean.py
index cfcc004..55ab858 100644
--- a/stgit/commands/clean.py
+++ b/stgit/commands/clean.py
@@ -37,7 +37,7 @@ options = [make_option('-a', '--applied',
 
 
 def _clean(stack, clean_applied, clean_unapplied):
-    trans = transaction.StackTransaction(stack, 'clean')
+    trans = transaction.StackTransaction(stack, 'stg clean')
     def del_patch(pn):
         if pn in stack.patchorder.applied:
             return clean_applied and trans.patches[pn].data.is_empty()
diff --git a/stgit/commands/goto.py b/stgit/commands/goto.py
index 84b840b..3ea69dd 100644
--- a/stgit/commands/goto.py
+++ b/stgit/commands/goto.py
@@ -15,13 +15,9 @@ 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
-
+from stgit.commands import common
+from stgit.lib import transaction
 
 help = 'push or pop patches to the given one'
 usage = """%prog [options] <name>
@@ -30,38 +26,26 @@ Push/pop patches to/from the stack until the one given on the command
 line becomes current. There is no '--undo' option for 'goto'. Use the
 'push --undo' command for this."""
 
-directory = DirectoryGotoToplevel()
-options = [make_option('-k', '--keep',
-                       help = 'keep the local changes when popping patches',
-                       action = 'store_true')]
-
+directory = common.DirectoryHasRepositoryLib()
+options = []
 
 def func(parser, options, args):
-    """Pushes the given patch or all onto the series
-    """
     if len(args) != 1:
         parser.error('incorrect number of arguments')
-
-    check_conflicts()
-    check_head_top_equal(crt_series)
-
-    if not options.keep:
-        check_local_changes()
-
-    applied = crt_series.get_applied()
-    unapplied = crt_series.get_unapplied()
     patch = args[0]
 
-    if patch in applied:
-        applied.reverse()
-        patches = applied[:applied.index(patch)]
-        pop_patches(crt_series, patches, options.keep)
-    elif patch in unapplied:
-        if options.keep:
-            raise CmdException, 'Cannot use --keep with patch pushing'
-        patches = unapplied[:unapplied.index(patch)+1]
-        push_patches(crt_series, patches)
+    stack = directory.repository.current_stack
+    iw = stack.repository.default_iw()
+    trans = transaction.StackTransaction(stack, 'stg goto')
+    if patch in trans.applied:
+        to_pop = set(trans.applied[trans.applied.index(patch)+1:])
+        assert not trans.pop_patches(lambda pn: pn in to_pop)
+    elif patch in trans.unapplied:
+        try:
+            for pn in trans.unapplied[:trans.unapplied.index(patch)+1]:
+                trans.push_patch(pn, iw)
+        except transaction.TransactionHalted:
+            pass
     else:
-        raise CmdException, 'Patch "%s" does not exist' % patch
-
-    print_crt_patch(crt_series)
+        raise CmdException('Patch "%s" does not exist' % patch)
+    trans.run(iw)

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

* [StGit PATCH 10/10] Convert "stg uncommit" to the new infrastructure
  2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
                   ` (8 preceding siblings ...)
  2007-11-25 20:51 ` [StGit PATCH 09/10] Let "stg goto" use the new infrastructure Karl Hasselström
@ 2007-11-25 20:51 ` Karl Hasselström
  9 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-25 20:51 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

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

---

 stgit/commands/uncommit.py |   79 +++++++++++++++++++-------------------------
 1 files changed, 34 insertions(+), 45 deletions(-)


diff --git a/stgit/commands/uncommit.py b/stgit/commands/uncommit.py
index ba3448f..8422952 100644
--- a/stgit/commands/uncommit.py
+++ b/stgit/commands/uncommit.py
@@ -17,13 +17,11 @@ 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.commands import common
+from stgit.lib import transaction
 from stgit.out import *
-from stgit import stack, git
+from stgit import utils
 
 help = 'turn regular GIT commits into StGIT patches'
 usage = """%prog [<patchnames>] | -n NUM [<prefix>]] | -t <committish> [-x]
@@ -48,7 +46,7 @@ given commit should be uncommitted.
 Only commits with exactly one parent can be uncommitted; in other
 words, you can't uncommit a merge."""
 
-directory = DirectoryGotoToplevel()
+directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-n', '--number', type = 'int',
                        help = 'uncommit the specified number of commits'),
            make_option('-t', '--to',
@@ -60,19 +58,18 @@ options = [make_option('-n', '--number', type = 'int',
 def func(parser, options, args):
     """Uncommit a number of patches.
     """
+    stack = directory.repository.current_stack
     if options.to:
         if options.number:
             parser.error('cannot give both --to and --number')
         if len(args) != 0:
             parser.error('cannot specify patch name with --to')
         patch_nr = patchnames = None
-        to_commit = git_id(crt_series, options.to)
+        to_commit = stack.repository.rev_parse(options.to)
     elif options.number:
         if options.number <= 0:
             parser.error('invalid value passed to --number')
-
         patch_nr = options.number
-
         if len(args) == 0:
             patchnames = None
         elif len(args) == 1:
@@ -88,53 +85,45 @@ def func(parser, options, args):
         patchnames = args
         patch_nr = len(patchnames)
 
-    if crt_series.get_protected():
-        raise CmdException, \
-              'This branch is protected. Uncommit is not permitted'
-
-    def get_commit(commit_id):
-        commit = git.Commit(commit_id)
-        try:
-            parent, = commit.get_parents()
-        except ValueError:
-            raise CmdException('Commit %s does not have exactly one parent'
-                               % commit_id)
-        return (commit, commit_id, parent)
-
     commits = []
-    next_commit = crt_series.get_base()
+    next_commit = stack.base
     if patch_nr:
         out.start('Uncommitting %d patches' % patch_nr)
         for i in xrange(patch_nr):
-            commit, commit_id, parent = get_commit(next_commit)
-            commits.append((commit, commit_id, parent))
-            next_commit = parent
+            commits.append(next_commit)
+            next_commit = next_commit.data.parent
     else:
         if options.exclusive:
             out.start('Uncommitting to %s (exclusive)' % to_commit)
         else:
             out.start('Uncommitting to %s' % to_commit)
         while True:
-            commit, commit_id, parent = get_commit(next_commit)
-            if commit_id == to_commit:
+            if next_commit == to_commit:
                 if not options.exclusive:
-                    commits.append((commit, commit_id, parent))
+                    commits.append(next_commit)
                 break
-            commits.append((commit, commit_id, parent))
-            next_commit = parent
+            commits.append(next_commit)
+            next_commit = next_commit.data.parent
         patch_nr = len(commits)
 
-    for (commit, commit_id, parent), patchname in \
-        zip(commits, patchnames or [None for i in xrange(len(commits))]):
-        author_name, author_email, author_date = \
-                     name_email_date(commit.get_author())
-        crt_series.new_patch(patchname,
-                             can_edit = False, before_existing = True,
-                             commit = False,
-                             top = commit_id, bottom = parent,
-                             message = commit.get_log(),
-                             author_name = author_name,
-                             author_email = author_email,
-                             author_date = author_date)
-
+    taken_names = set(stack.patchorder.applied + stack.patchorder.unapplied)
+    if patchnames:
+        for pn in patchnames:
+            if pn in taken_names:
+                raise common.CmdException('Patch name "%s" already taken' % pn)
+            taken_names.add(pn)
+    else:
+        patchnames = []
+        for c in reversed(commits):
+            pn = utils.make_patch_name(c.data.message,
+                                       lambda pn: pn in taken_names)
+            patchnames.append(pn)
+            taken_names.add(pn)
+        patchnames.reverse()
+
+    trans = transaction.StackTransaction(stack, 'stg uncommit')
+    for commit, pn in zip(commits, patchnames):
+        trans.patches[pn] = commit
+    trans.applied = list(reversed(patchnames)) + trans.applied
+    trans.run()
     out.done()

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

* Re: [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree
  2007-11-25 20:51 ` [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree Karl Hasselström
@ 2007-11-26  8:31   ` Karl Hasselström
  2007-11-26  8:56   ` David Kågedal
  1 sibling, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-26  8:31 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: git, David Kågedal

On 2007-11-25 21:51:40 +0100, Karl Hasselström wrote:

> And use the new powers to make "stg coalesce" able to handle
> arbitrary patches, not just consecutive applied patches.

Obviously, this should come with an updated help text for "stg
coalesce". Will amend the patch.

diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
index 2b121a9..6c0ae6d 100644
--- a/stgit/commands/coalesce.py
+++ b/stgit/commands/coalesce.py
@@ -27,8 +27,11 @@ 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."""
+their changes.
+
+If there are conflicts when reordering the patches to match the order
+you specify, you will have to resolve them manually just as if you had
+done a sequence of pushes and pops yourself."""
 
 directory = common.DirectoryHasRepositoryLib()
 options = [make_option('-n', '--name', help = 'name of coalesced patch'),

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

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

* Re: [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree
  2007-11-25 20:51 ` [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree Karl Hasselström
  2007-11-26  8:31   ` Karl Hasselström
@ 2007-11-26  8:56   ` David Kågedal
  2007-11-26 10:44     ` Karl Hasselström
  1 sibling, 1 reply; 14+ messages in thread
From: David Kågedal @ 2007-11-26  8:56 UTC (permalink / raw)
  To: Catalin Marinas, Karl Hasselström; +Cc: git

Karl Hasselström <kha@treskal.com> writes:

> --- a/stgit/lib/git.py
> +++ b/stgit/lib/git.py
> @@ -95,6 +95,8 @@ class Commitdata(Repr):
>          return type(self)(committer = committer, defaults = self)
>      def set_message(self, message):
>          return type(self)(message = message, defaults = self)
> +    def is_empty(self):
> +        return self.tree == self.parent.data.tree
>      def __str__(self):
>          if self.tree == None:
>              tree = None

But a Commitdata describes a Git commit, right? And not an StGit
patch. So what does it mean to say that a commit is empty? I could
just as well mean that the tree is the null tree.

I think I would have called this "is_nochange" or something.

-- 
David Kågedal

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

* Re: [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree
  2007-11-26  8:56   ` David Kågedal
@ 2007-11-26 10:44     ` Karl Hasselström
  0 siblings, 0 replies; 14+ messages in thread
From: Karl Hasselström @ 2007-11-26 10:44 UTC (permalink / raw)
  To: David Kågedal; +Cc: Catalin Marinas, git

On 2007-11-26 09:56:42 +0100, David Kågedal wrote:

> Karl Hasselström <kha@treskal.com> writes:
>
> > --- a/stgit/lib/git.py
> > +++ b/stgit/lib/git.py
> > @@ -95,6 +95,8 @@ class Commitdata(Repr):
> >          return type(self)(committer = committer, defaults = self)
> >      def set_message(self, message):
> >          return type(self)(message = message, defaults = self)
> > +    def is_empty(self):
> > +        return self.tree == self.parent.data.tree
> >      def __str__(self):
> >          if self.tree == None:
> >              tree = None
>
> But a Commitdata describes a Git commit, right? And not an StGit
> patch.

Yes.

> So what does it mean to say that a commit is empty? I could just as
> well mean that the tree is the null tree. I think I would have
> called this "is_nochange" or something.

Good point.

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

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

end of thread, other threads:[~2007-11-26 10:45 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2007-11-25 20:50 [StGit PATCH 00/10] Infrastructure rewrite series Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 01/10] New StGit core infrastructure: repository operations Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 02/10] Write metadata files used by the old infrastructure Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 03/10] Upgrade older stacks to newest version Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 04/10] Let "stg clean" use the new infrastructure Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 05/10] Add "stg coalesce" Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 06/10] Let "stg applied" and "stg unapplied" use the new infrastructure Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 07/10] Teach the new infrastructure about the index and worktree Karl Hasselström
2007-11-26  8:31   ` Karl Hasselström
2007-11-26  8:56   ` David Kågedal
2007-11-26 10:44     ` Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 08/10] Let "stg clean" use the new transaction primitives Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 09/10] Let "stg goto" use the new infrastructure Karl Hasselström
2007-11-25 20:51 ` [StGit PATCH 10/10] Convert "stg uncommit" to " 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).