* [StGit PATCH 00/10] Updated "stg reset" series
@ 2008-04-20 22:10 Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 01/10] Prevent most commands from running when there are conflicts Karl Hasselström
` (9 more replies)
0 siblings, 10 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:10 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
The patch stack log / stg reset series has matured over the weekend.
The major highlights are
1. When a command fails to push a patch due to conflicts, it will
log two entries -- one for the part of the command that
succeeded, and one just for the conflicting push. This makes it
possible to undo just the last part, and not the whole command.
2. I've tightened the checks so that conflicts will prevent most
commands from working unless they actively claim to make sense in
the face of conflicts. Basically, only commands that don't touch
the topmost patch should be able to run when there are conflicts.
3. stg reset now understands --hard, which makes it zonk any local
changes, including conflicts.
(1) and (3) means that
$ stg reset --hard <branchname>.stgit^~1
can be used instead of stg push --undo when you want to undo a
conflicting push. Now I just have to make that available as
$ stg undo
so that it actually becomes usable ...
These patches are available at
git://repo.or.cz/stgit/kha.git experimental
---
Karl Hasselström (10):
Don't write a log entry if there were no changes
Add a --hard flag to stg reset
Log conflicts separately for all commands
Log conflicts separately
New command: stg reset
Add utility function for reordering patches
Write to a stack log when stack is modified
Library functions for tree and blob manipulation
Add property with a list of all patch names
Prevent most commands from running when there are conflicts
stgit/commands/branch.py | 20 ++++-
stgit/commands/clean.py | 3 +
stgit/commands/coalesce.py | 6 +-
stgit/commands/commit.py | 13 ++--
stgit/commands/common.py | 8 ++
stgit/commands/delete.py | 14 +++-
stgit/commands/diff.py | 2 -
stgit/commands/edit.py | 3 +
stgit/commands/export.py | 2 -
stgit/commands/files.py | 2 -
stgit/commands/id.py | 2 -
stgit/commands/log.py | 2 -
stgit/commands/mail.py | 2 -
stgit/commands/patches.py | 2 -
stgit/commands/reset.py | 118 ++++++++++++++++++++++++++++++++
stgit/commands/series.py | 2 -
stgit/commands/show.py | 2 -
stgit/commands/status.py | 3 +
stgit/commands/top.py | 2 -
stgit/commands/uncommit.py | 5 +
stgit/lib/git.py | 143 +++++++++++++++++++++++++++++++++------
stgit/lib/log.py | 161 ++++++++++++++++++++++++++++++++++++++++++++
stgit/lib/stack.py | 8 ++
stgit/lib/transaction.py | 92 ++++++++++++++++++++-----
stgit/main.py | 4 +
t/t3100-reset.sh | 151 +++++++++++++++++++++++++++++++++++++++++
t/t3101-reset-hard.sh | 56 +++++++++++++++
27 files changed, 754 insertions(+), 74 deletions(-)
create mode 100644 stgit/commands/reset.py
create mode 100644 stgit/lib/log.py
create mode 100755 t/t3100-reset.sh
create mode 100755 t/t3101-reset-hard.sh
--
Karl Hasselström, kha@treskal.com
www.treskal.com/kalle
^ permalink raw reply [flat|nested] 11+ messages in thread
* [StGit PATCH 01/10] Prevent most commands from running when there are conflicts
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
@ 2008-04-20 22:10 ` Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 02/10] Add property with a list of all patch names Karl Hasselström
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:10 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
When there are conflicts, we want most commands to fail, since the
conflicts conceptually belong to the topmost patch. git read-tree
already checks this for us when we check out a new tree, but there are
operations where the top tree stays the same, e.g. stg new.
This patch inserts a conflict check when the tree to check out is the
same. By default, conflicts will prevent the checkout from succeeding,
but commands can choose to override this if the same patch stays on
top (for some definition of "same").
This change only affects the new-infrastructure commands; the others
always refuse to run when there are local changes of any kind.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/commands/clean.py | 3 ++-
stgit/commands/coalesce.py | 3 ++-
stgit/commands/commit.py | 7 ++++++-
stgit/commands/delete.py | 10 +++++++++-
stgit/commands/edit.py | 3 ++-
stgit/commands/uncommit.py | 3 ++-
stgit/lib/transaction.py | 27 ++++++++++++++++++++++-----
7 files changed, 45 insertions(+), 11 deletions(-)
diff --git a/stgit/commands/clean.py b/stgit/commands/clean.py
index 889c1dc..ed6fc6e 100644
--- a/stgit/commands/clean.py
+++ b/stgit/commands/clean.py
@@ -37,7 +37,8 @@ options = [make_option('-a', '--applied',
def _clean(stack, clean_applied, clean_unapplied):
- trans = transaction.StackTransaction(stack, 'stg clean')
+ trans = transaction.StackTransaction(stack, 'stg clean',
+ allow_conflicts = True)
def del_patch(pn):
if pn in stack.patchorder.applied:
if pn == stack.patchorder.applied[-1]:
diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
index 291a537..bf40427 100644
--- a/stgit/commands/coalesce.py
+++ b/stgit/commands/coalesce.py
@@ -79,7 +79,8 @@ def _coalesce(stack, iw, name, msg, save_template, patches):
trans.patches[name] = stack.repository.commit(new_commit_data)
trans.unapplied.insert(0, name)
- trans = transaction.StackTransaction(stack, 'stg coalesce')
+ trans = transaction.StackTransaction(stack, 'stg coalesce',
+ allow_conflicts = True)
push_new_patch = bool(set(patches) & set(trans.applied))
try:
new_commit_data = _coalesce_patches(trans, patches, msg, save_template)
diff --git a/stgit/commands/commit.py b/stgit/commands/commit.py
index bff94ce..ee95836 100644
--- a/stgit/commands/commit.py
+++ b/stgit/commands/commit.py
@@ -69,7 +69,12 @@ def func(parser, options, args):
raise common.CmdException('No patches to commit')
iw = stack.repository.default_iw
- trans = transaction.StackTransaction(stack, 'stg commit')
+ def allow_conflicts(trans):
+ # As long as the topmost patch stays where it is, it's OK to
+ # run "stg commit" with conflicts in the index.
+ return len(trans.applied) >= 1
+ trans = transaction.StackTransaction(stack, 'stg commit',
+ allow_conflicts = allow_conflicts)
try:
common_prefix = 0
for i in xrange(min(len(stack.patchorder.applied), len(patches))):
diff --git a/stgit/commands/delete.py b/stgit/commands/delete.py
index 106fbd2..14bf442 100644
--- a/stgit/commands/delete.py
+++ b/stgit/commands/delete.py
@@ -46,7 +46,15 @@ def func(parser, options, args):
+ list(stack.patchorder.unapplied))))
else:
parser.error('No patches specified')
- trans = transaction.StackTransaction(stack, 'stg delete')
+ def allow_conflicts(trans):
+ # Allow conflicts if the topmost patch stays the same.
+ if stack.patchorder.applied:
+ return (trans.applied
+ and trans.applied[-1] == stack.patchorder.applied[-1])
+ else:
+ return not trans.applied
+ trans = transaction.StackTransaction(stack, 'stg delete',
+ allow_conflicts = allow_conflicts)
try:
to_push = trans.delete_patches(lambda pn: pn in patches)
for pn in to_push:
diff --git a/stgit/commands/edit.py b/stgit/commands/edit.py
index eff9117..c354730 100644
--- a/stgit/commands/edit.py
+++ b/stgit/commands/edit.py
@@ -173,7 +173,8 @@ def func(parser, options, args):
# The patch applied, so now we have to rewrite the StGit patch
# (and any patches on top of it).
iw = stack.repository.default_iw
- trans = transaction.StackTransaction(stack, 'stg edit')
+ trans = transaction.StackTransaction(stack, 'stg edit',
+ allow_conflicts = True)
if patchname in trans.applied:
popped = trans.applied[trans.applied.index(patchname)+1:]
assert not trans.pop_patches(lambda pn: pn in popped)
diff --git a/stgit/commands/uncommit.py b/stgit/commands/uncommit.py
index 272c5db..b6765bc 100644
--- a/stgit/commands/uncommit.py
+++ b/stgit/commands/uncommit.py
@@ -131,7 +131,8 @@ def func(parser, options, args):
taken_names.add(pn)
patchnames.reverse()
- trans = transaction.StackTransaction(stack, 'stg uncommit')
+ trans = transaction.StackTransaction(stack, 'stg uncommit',
+ allow_conflicts = True)
for commit, pn in zip(commits, patchnames):
trans.patches[pn] = commit
trans.applied = list(reversed(patchnames)) + trans.applied
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
index 1ece01e..874f81b 100644
--- a/stgit/lib/transaction.py
+++ b/stgit/lib/transaction.py
@@ -34,7 +34,7 @@ class _TransPatchMap(dict):
return self.__stack.patches.get(pn).commit
class StackTransaction(object):
- def __init__(self, stack, msg):
+ def __init__(self, stack, msg, allow_conflicts = False):
self.__stack = stack
self.__msg = msg
self.__patches = _TransPatchMap(stack)
@@ -43,6 +43,10 @@ class StackTransaction(object):
self.__error = None
self.__current_tree = self.__stack.head.data.tree
self.__base = self.__stack.base
+ if isinstance(allow_conflicts, bool):
+ self.__allow_conflicts = lambda trans: allow_conflicts
+ else:
+ self.__allow_conflicts = allow_conflicts
stack = property(lambda self: self.__stack)
patches = property(lambda self: self.__patches)
def __set_applied(self, val):
@@ -63,10 +67,19 @@ class StackTransaction(object):
'This can happen if you modify a branch with git.',
'"stg repair --help" explains more about what to do next.')
self.__abort()
- if self.__current_tree != tree:
- assert iw != None
- iw.checkout(self.__current_tree, tree)
- self.__current_tree = tree
+ if self.__current_tree == tree:
+ # No tree change, but we still want to make sure that
+ # there are no unresolved conflicts. Conflicts
+ # conceptually "belong" to the topmost patch, and just
+ # carrying them along to another patch is confusing.
+ if (self.__allow_conflicts(self) or iw == None
+ or not iw.index.conflicts()):
+ return
+ out.error('Need to resolve conflicts first')
+ self.__abort()
+ assert iw != None
+ iw.checkout(self.__current_tree, tree)
+ self.__current_tree = tree
@staticmethod
def __abort():
raise TransactionException(
@@ -214,4 +227,8 @@ class StackTransaction(object):
self.applied.append(pn)
out.info('Pushed %s%s' % (pn, s))
if merge_conflict:
+ # We've just caused conflicts, so we must allow them in
+ # the final checkout.
+ self.__allow_conflicts = lambda trans: True
+
self.__halt('Merge conflict')
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 02/10] Add property with a list of all patch names
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 01/10] Prevent most commands from running when there are conflicts Karl Hasselström
@ 2008-04-20 22:10 ` Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 03/10] Library functions for tree and blob manipulation Karl Hasselström
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:10 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
This simplifies the code in a number of places.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/commands/coalesce.py | 3 +--
stgit/commands/commit.py | 6 ++----
stgit/commands/delete.py | 4 +---
stgit/commands/uncommit.py | 2 +-
stgit/lib/stack.py | 1 +
5 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
index bf40427..9a46097 100644
--- a/stgit/commands/coalesce.py
+++ b/stgit/commands/coalesce.py
@@ -115,8 +115,7 @@ def _coalesce(stack, iw, name, msg, save_template, patches):
def func(parser, options, args):
stack = directory.repository.current_stack
- patches = common.parse_patches(args, (list(stack.patchorder.applied)
- + list(stack.patchorder.unapplied)))
+ patches = common.parse_patches(args, list(stack.patchorder.all))
if len(patches) < 2:
raise common.CmdException('Need at least two patches')
return _coalesce(stack, stack.repository.default_iw, options.name,
diff --git a/stgit/commands/commit.py b/stgit/commands/commit.py
index ee95836..ab89590 100644
--- a/stgit/commands/commit.py
+++ b/stgit/commands/commit.py
@@ -45,13 +45,11 @@ options = [make_option('-n', '--number', type = 'int',
def func(parser, options, args):
"""Commit a number of patches."""
stack = directory.repository.current_stack
- args = common.parse_patches(args, (list(stack.patchorder.applied)
- + list(stack.patchorder.unapplied)))
+ args = common.parse_patches(args, list(stack.patchorder.all))
if len([x for x in [args, options.number != None, options.all] if x]) > 1:
parser.error('too many options')
if args:
- patches = [pn for pn in (stack.patchorder.applied
- + stack.patchorder.unapplied) if pn in args]
+ patches = [pn for pn in stack.patchorder.all if pn in args]
bad = set(args) - set(patches)
if bad:
raise common.CmdException('Bad patch names: %s'
diff --git a/stgit/commands/delete.py b/stgit/commands/delete.py
index 14bf442..872ed77 100644
--- a/stgit/commands/delete.py
+++ b/stgit/commands/delete.py
@@ -41,9 +41,7 @@ def func(parser, options, args):
stack = directory.repository.current_stack
iw = stack.repository.default_iw
if args:
- patches = set(common.parse_patches(
- args, (list(stack.patchorder.applied)
- + list(stack.patchorder.unapplied))))
+ patches = set(common.parse_patches(args, list(stack.patchorder.all)))
else:
parser.error('No patches specified')
def allow_conflicts(trans):
diff --git a/stgit/commands/uncommit.py b/stgit/commands/uncommit.py
index b6765bc..9ab178c 100644
--- a/stgit/commands/uncommit.py
+++ b/stgit/commands/uncommit.py
@@ -116,7 +116,7 @@ def func(parser, options, args):
next_commit = get_parent(next_commit)
patch_nr = len(commits)
- taken_names = set(stack.patchorder.applied + stack.patchorder.unapplied)
+ taken_names = set(stack.patchorder.all)
if patchnames:
for pn in patchnames:
if pn in taken_names:
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
index 3de3776..af1c994 100644
--- a/stgit/lib/stack.py
+++ b/stgit/lib/stack.py
@@ -96,6 +96,7 @@ class PatchOrder(object):
lambda self, val: self.__set_list('applied', val))
unapplied = property(lambda self: self.__get_list('unapplied'),
lambda self, val: self.__set_list('unapplied', val))
+ all = property(lambda self: self.applied + self.unapplied)
class Patches(object):
"""Creates Patch objects."""
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 03/10] Library functions for tree and blob manipulation
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 01/10] Prevent most commands from running when there are conflicts Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 02/10] Add property with a list of all patch names Karl Hasselström
@ 2008-04-20 22:10 ` Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 04/10] Write to a stack log when stack is modified Karl Hasselström
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:10 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
Wrap trees and blobs in Python objects (just like commits were already
wrapped), so that StGit code can read and write them.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/lib/git.py | 139 ++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 118 insertions(+), 21 deletions(-)
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
index f046e12..5bb1c93 100644
--- a/stgit/lib/git.py
+++ b/stgit/lib/git.py
@@ -146,13 +146,100 @@ class Person(Repr):
defaults = cls.user())
return cls.__committer
-class Tree(Repr):
+class GitObject(Repr):
+ """Base class for all git objects."""
+
+class Blobdata(Repr):
+ def __init__(self, string):
+ self.__string = str(string)
+ str = property(lambda self: self.__string)
+ def commit(self, repository):
+ sha1 = repository.run(['git', 'hash-object', '-w', '--stdin']
+ ).raw_input(self.str).output_one_line()
+ return repository.get_blob(sha1)
+
+class Blob(GitObject):
"""Immutable."""
- def __init__(self, sha1):
+ typename = 'blob'
+ default_perm = '100644'
+ def __init__(self, repository, sha1):
+ self.__repository = repository
self.__sha1 = sha1
sha1 = property(lambda self: self.__sha1)
def __str__(self):
- return 'Tree<%s>' % self.sha1
+ return 'Blob<%s>' % self.sha1
+ @property
+ def data(self):
+ return Blobdata(self.__repository.cat_object(self.sha1))
+
+class ImmutableDict(dict):
+ def error(*args, **kwargs):
+ raise TypeError('Cannot modify immutable dict')
+ __delitem__ = error
+ __setitem__ = error
+ clear = error
+ pop = error
+ popitem = error
+ setdefault = error
+ update = error
+
+class Treedata(Repr):
+ """Immutable."""
+ @staticmethod
+ def __x(po):
+ if isinstance(po, GitObject):
+ perm, object = po.default_perm, po
+ else:
+ perm, object = po
+ return perm, object
+ def __init__(self, entries):
+ self.__entries = ImmutableDict((name, self.__x(po))
+ for (name, po) in entries.iteritems())
+ entries = property(lambda self: self.__entries)
+ def set_entry(self, name, po):
+ e = dict(self.entries)
+ e[name] = self.__x(po)
+ return type(self)(e)
+ def del_entry(self, name):
+ e = dict(self.entries)
+ del e[name]
+ return type(self)(e)
+ def commit(self, repository):
+ listing = ''.join(
+ '%s %s %s\t%s\0' % (mode, obj.typename, obj.sha1, name)
+ for (name, (mode, obj)) in self.entries.iteritems())
+ sha1 = repository.run(['git', 'mktree', '-z']
+ ).raw_input(listing).output_one_line()
+ return repository.get_tree(sha1)
+ @classmethod
+ def parse(cls, repository, s):
+ entries = {}
+ for line in s.split('\0')[:-1]:
+ m = re.match(r'^([0-7]{6}) ([a-z]+) ([0-9a-f]{40})\t(.*)$', line)
+ assert m
+ perm, type, sha1, name = m.groups()
+ entries[name] = (perm, repository.get_object(type, sha1))
+ return cls(entries)
+
+class Tree(GitObject):
+ """Immutable."""
+ typename = 'tree'
+ default_perm = '040000'
+ 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 = Treedata.parse(
+ self.__repository,
+ self.__repository.run(['git', 'ls-tree', '-z', self.sha1]
+ ).raw_output())
+ return self.__data
+ def __str__(self):
+ return 'Tree<sha1: %s>' % self.sha1
class Commitdata(Repr):
"""Immutable."""
@@ -202,6 +289,22 @@ class Commitdata(Repr):
return ('Commitdata<tree: %s, parents: %s, author: %s,'
' committer: %s, message: "%s">'
) % (tree, parents, self.author, self.committer, self.message)
+ def commit(self, repository):
+ c = ['git', 'commit-tree', self.tree.sha1]
+ for p in self.parents:
+ c.append('-p')
+ c.append(p.sha1)
+ env = {}
+ for p, v1 in ((self.author, 'AUTHOR'),
+ (self.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)] = str(getattr(p, attr))
+ sha1 = repository.run(c, env = env).raw_input(self.message
+ ).output_one_line()
+ return repository.get_commit(sha1)
@classmethod
def parse(cls, repository, s):
cd = cls(parents = [])
@@ -223,8 +326,9 @@ class Commitdata(Repr):
assert False
assert False
-class Commit(Repr):
+class Commit(GitObject):
"""Immutable."""
+ typename = 'commit'
def __init__(self, repository, sha1):
self.__sha1 = sha1
self.__repository = repository
@@ -306,7 +410,8 @@ class Repository(RunWithEnv):
def __init__(self, directory):
self.__git_dir = directory
self.__refs = Refs(self)
- self.__trees = ObjectCache(lambda sha1: Tree(sha1))
+ self.__blobs = ObjectCache(lambda sha1: Blob(self, sha1))
+ self.__trees = ObjectCache(lambda sha1: Tree(self, sha1))
self.__commits = ObjectCache(lambda sha1: Commit(self, sha1))
self.__default_index = None
self.__default_worktree = None
@@ -357,26 +462,18 @@ class Repository(RunWithEnv):
).output_one_line())
except run.RunException:
raise RepositoryException('%s: No such revision' % rev)
+ def get_blob(self, sha1):
+ return self.__blobs[sha1]
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)] = str(getattr(p, attr))
- sha1 = self.run(c, env = env).raw_input(commitdata.message
- ).output_one_line()
- return self.get_commit(sha1)
+ def get_object(self, type, sha1):
+ return { Blob.typename: self.get_blob,
+ Tree.typename: self.get_tree,
+ Commit.typename: self.get_commit }[type](sha1)
+ def commit(self, objectdata):
+ return objectdata.commit(self)
@property
def head(self):
try:
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 04/10] Write to a stack log when stack is modified
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
` (2 preceding siblings ...)
2008-04-20 22:10 ` [StGit PATCH 03/10] Library functions for tree and blob manipulation Karl Hasselström
@ 2008-04-20 22:10 ` Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 05/10] Add utility function for reordering patches Karl Hasselström
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:10 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
Create a log branch (called <branchname>.stgit) for each StGit branch,
and write to it whenever the stack is modified.
Commands using the new infrastructure write to the log when they
commit a transaction. Commands using the old infrastructure get a log
entry write written for them when they exit, unless they explicitly
ask for this not to happen.
As of yet, nothing can be done with this log.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/commands/branch.py | 20 +++++--
stgit/commands/common.py | 8 ++-
stgit/commands/diff.py | 2 -
stgit/commands/export.py | 2 -
stgit/commands/files.py | 2 -
stgit/commands/id.py | 2 -
stgit/commands/log.py | 2 -
stgit/commands/mail.py | 2 -
stgit/commands/patches.py | 2 -
stgit/commands/series.py | 2 -
stgit/commands/show.py | 2 -
stgit/commands/status.py | 3 +
stgit/commands/top.py | 2 -
stgit/lib/log.py | 131 +++++++++++++++++++++++++++++++++++++++++++++
stgit/lib/transaction.py | 3 +
stgit/main.py | 2 +
16 files changed, 168 insertions(+), 19 deletions(-)
create mode 100644 stgit/lib/log.py
diff --git a/stgit/commands/branch.py b/stgit/commands/branch.py
index 50684bb..241b1ef 100644
--- a/stgit/commands/branch.py
+++ b/stgit/commands/branch.py
@@ -25,7 +25,7 @@ from stgit.commands.common import *
from stgit.utils import *
from stgit.out import *
from stgit import stack, git, basedir
-
+from stgit.lib import log
help = 'manage patch stacks'
usage = """%prog [options] branch-name [commit-id]
@@ -40,7 +40,7 @@ When displaying the branches, the names can be prefixed with
If not given any options, switch to the named branch."""
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = False)
options = [make_option('-c', '--create',
help = 'create a new development branch',
action = 'store_true'),
@@ -161,6 +161,7 @@ def func(parser, options, args):
parent_branch = parentbranch)
out.info('Branch "%s" created' % args[0])
+ log.compat_log_entry('stg branch --create')
return
elif options.clone:
@@ -181,6 +182,8 @@ def func(parser, options, args):
crt_series.clone(clone)
out.done()
+ log.copy_log(log.default_repo(), crt_series.get_name(), clone,
+ 'stg branch --clone')
return
elif options.delete:
@@ -188,6 +191,7 @@ def func(parser, options, args):
if len(args) != 1:
parser.error('incorrect number of arguments')
__delete_branch(args[0], options.force)
+ log.delete_log(log.default_repo(), args[0])
return
elif options.list:
@@ -195,13 +199,16 @@ def func(parser, options, args):
if len(args) != 0:
parser.error('incorrect number of arguments')
- branches = git.get_heads()
- branches.sort()
+ branches = set(git.get_heads())
+ for br in set(branches):
+ m = re.match(r'^(.*)\.stgit$', br)
+ if m and m.group(1) in branches:
+ branches.remove(br)
if branches:
out.info('Available branches:')
max_len = max([len(i) for i in branches])
- for i in branches:
+ for i in sorted(branches):
__print_branch(i, max_len)
else:
out.info('No branches')
@@ -238,7 +245,8 @@ def func(parser, options, args):
stack.Series(args[0]).rename(args[1])
out.info('Renamed branch "%s" to "%s"' % (args[0], args[1]))
-
+ log.rename_log(log.default_repo(), args[0], args[1],
+ 'stg branch --rename')
return
elif options.unprotect:
diff --git a/stgit/commands/common.py b/stgit/commands/common.py
index d6df813..1a45d9e 100644
--- a/stgit/commands/common.py
+++ b/stgit/commands/common.py
@@ -28,6 +28,7 @@ from stgit.run import *
from stgit import stack, git, basedir
from stgit.config import config, file_extensions
from stgit.lib import stack as libstack
+from stgit.lib import log
# Command exception class
class CmdException(StgException):
@@ -478,8 +479,9 @@ class DirectoryException(StgException):
pass
class _Directory(object):
- def __init__(self, needs_current_series = True):
+ def __init__(self, needs_current_series = True, log = True):
self.needs_current_series = needs_current_series
+ self.log = log
@readonly_constant_property
def git_dir(self):
try:
@@ -512,6 +514,9 @@ class _Directory(object):
).output_one_line()]
def cd_to_topdir(self):
os.chdir(self.__topdir_path)
+ def write_log(self, msg):
+ if self.log:
+ log.compat_log_entry(msg)
class DirectoryAnywhere(_Directory):
def setup(self):
@@ -536,6 +541,7 @@ class DirectoryHasRepositoryLib(_Directory):
"""For commands that use the new infrastructure in stgit.lib.*."""
def __init__(self):
self.needs_current_series = False
+ self.log = False # stgit.lib.transaction handles logging
def setup(self):
# This will throw an exception if we don't have a repository.
self.repository = libstack.Repository.default()
diff --git a/stgit/commands/diff.py b/stgit/commands/diff.py
index fd6be34..8966642 100644
--- a/stgit/commands/diff.py
+++ b/stgit/commands/diff.py
@@ -42,7 +42,7 @@ rev = '([patch][//[bottom | top]]) | <tree-ish> | base'
If neither bottom nor top are given but a '//' is present, the command
shows the specified patch (defaulting to the current one)."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-r', '--range',
metavar = 'rev1[..[rev2]]', dest = 'revs',
help = 'show the diff between revisions'),
diff --git a/stgit/commands/export.py b/stgit/commands/export.py
index 50f6f67..552fd44 100644
--- a/stgit/commands/export.py
+++ b/stgit/commands/export.py
@@ -49,7 +49,7 @@ file:
%(commemail)s - committer's e-mail
"""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-d', '--dir',
help = 'export patches to DIR instead of the default'),
make_option('-p', '--patch',
diff --git a/stgit/commands/files.py b/stgit/commands/files.py
index b43b12f..7844f8d 100644
--- a/stgit/commands/files.py
+++ b/stgit/commands/files.py
@@ -34,7 +34,7 @@ given patch. Note that this command doesn't show the files modified in
the working tree and not yet included in the patch by a 'refresh'
command. Use the 'diff' or 'status' commands for these files."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-s', '--stat',
help = 'show the diff stat',
action = 'store_true'),
diff --git a/stgit/commands/id.py b/stgit/commands/id.py
index 94b0229..5bb1ad2 100644
--- a/stgit/commands/id.py
+++ b/stgit/commands/id.py
@@ -33,7 +33,7 @@ the standard GIT id's like heads and tags, this command also accepts
'top' or 'bottom' are passed and <patch> is a valid patch name, 'top'
will be used by default."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-b', '--branch',
help = 'use BRANCH instead of the default one')]
diff --git a/stgit/commands/log.py b/stgit/commands/log.py
index 52d55a5..13e0baa 100644
--- a/stgit/commands/log.py
+++ b/stgit/commands/log.py
@@ -44,7 +44,7 @@ represent the changes to the entire base of the current
patch. Conflicts reset the patch content and a subsequent 'refresh'
will show the entire patch."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-b', '--branch',
help = 'use BRANCH instead of the default one'),
make_option('-p', '--patch',
diff --git a/stgit/commands/mail.py b/stgit/commands/mail.py
index b4d4e18..4027170 100644
--- a/stgit/commands/mail.py
+++ b/stgit/commands/mail.py
@@ -90,7 +90,7 @@ the following:
%(prefix)s - 'prefix ' string passed on the command line
%(shortdescr)s - the first line of the patch description"""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-a', '--all',
help = 'e-mail all the applied patches',
action = 'store_true'),
diff --git a/stgit/commands/patches.py b/stgit/commands/patches.py
index 140699d..c95c40f 100644
--- a/stgit/commands/patches.py
+++ b/stgit/commands/patches.py
@@ -33,7 +33,7 @@ it shows the patches affected by the local tree modifications. The
'--diff' option also lists the patch log and the diff for the given
files."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-d', '--diff',
help = 'show the diff for the given files',
action = 'store_true'),
diff --git a/stgit/commands/series.py b/stgit/commands/series.py
index e3467cc..8d19dd7 100644
--- a/stgit/commands/series.py
+++ b/stgit/commands/series.py
@@ -34,7 +34,7 @@ range. The applied patches are prefixed with a '+', the unapplied ones
with a '-' and the hidden ones with a '!'. The current patch is
prefixed with a '>'. Empty patches are prefixed with a '0'."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-b', '--branch',
help = 'use BRANCH instead of the default one'),
make_option('-a', '--all',
diff --git a/stgit/commands/show.py b/stgit/commands/show.py
index b77a9c8..dd2a3a3 100644
--- a/stgit/commands/show.py
+++ b/stgit/commands/show.py
@@ -30,7 +30,7 @@ Show the commit log and the diff corresponding to the given
patches. The output is similar to that generated by the 'git show'
command."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-b', '--branch',
help = 'use BRANCH instead of the default one'),
make_option('-a', '--applied',
diff --git a/stgit/commands/status.py b/stgit/commands/status.py
index 6da4516..37b79c0 100644
--- a/stgit/commands/status.py
+++ b/stgit/commands/status.py
@@ -40,7 +40,7 @@ under revision control. The files are prefixed as follows:
A 'refresh' command clears the status of the modified, new and deleted
files."""
-directory = DirectoryHasRepository(needs_current_series = False)
+directory = DirectoryHasRepository(needs_current_series = False, log = False)
options = [make_option('-m', '--modified',
help = 'show modified files only',
action = 'store_true'),
@@ -106,6 +106,7 @@ def func(parser, options, args):
directory.cd_to_topdir()
if options.reset:
+ directory.log = True
if args:
conflicts = git.get_conflicts()
git.resolved(fn for fn in args if fn in conflicts)
diff --git a/stgit/commands/top.py b/stgit/commands/top.py
index e7cb275..96d680e 100644
--- a/stgit/commands/top.py
+++ b/stgit/commands/top.py
@@ -30,7 +30,7 @@ usage = """%prog [options]
Print the name of the current (topmost) patch."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-b', '--branch',
help = 'use BRANCH instead of the default one')]
diff --git a/stgit/lib/log.py b/stgit/lib/log.py
new file mode 100644
index 0000000..fac050f
--- /dev/null
+++ b/stgit/lib/log.py
@@ -0,0 +1,131 @@
+from stgit.lib import git, stack
+from stgit import exception
+from stgit.out import out
+
+class LogException(exception.StgException):
+ pass
+
+_current_version = 1
+
+def commit_info(cd):
+ return 'Author: %s\n\n%s' % (cd.author, cd.message)
+
+def patch_tree(repository, cd):
+ return repository.commit(git.Treedata({
+ 'a': cd.parent.data.tree, 'b': cd.tree,
+ 'info': repository.commit(git.Blobdata(commit_info(cd))) }))
+
+def order_blob(repository, stack, kind):
+ return repository.commit(git.Blobdata(''.join(
+ '%s %s\n' % (stack.patches.get(pn).commit.sha1, pn)
+ for pn in getattr(stack.patchorder, kind))))
+
+def log_entry_tree(repository, stack):
+ patches = repository.commit(
+ git.Treedata(dict((pn, patch_tree(repository,
+ stack.patches.get(pn).commit.data))
+ for pn in stack.patchorder.all)))
+ return repository.commit(git.Treedata({
+ 'version': repository.commit(git.Blobdata(
+ str(_current_version))),
+ 'head': repository.commit(git.Blobdata(str(stack.head.sha1))),
+ 'patches': patches,
+ 'applied': order_blob(repository, stack, 'applied'),
+ 'unapplied': order_blob(repository, stack, 'unapplied'),
+ }))
+
+def log_ref(branch):
+ return 'refs/heads/%s.stgit' % branch
+
+def log_entry(stack, msg):
+ """Write a new log entry for the stack."""
+ ref = log_ref(stack.name)
+ try:
+ last_log = [FullLog(stack.repository, ref,
+ stack.repository.refs.get(ref))]
+ except KeyError:
+ last_log = []
+ except LogException, e:
+ out.warn(str(e), 'No log entry written.')
+ return
+ log_tree = log_entry_tree(stack.repository, stack)
+ stack_log = stack.repository.commit(
+ git.Commitdata(tree = log_tree, message = msg,
+ parents = [ll.stack_log for ll in last_log]))
+ full_log = stack.repository.commit(
+ git.Commitdata(tree = log_tree, message = msg,
+ parents = ([stack_log] + [stack.head]
+ + [ll.full_log for ll in last_log])))
+ stack.repository.refs.set(ref, full_log, msg)
+
+def compat_log_entry(msg):
+ repo = default_repo()
+ stack = repo.get_stack(repo.current_branch)
+ log_entry(stack, msg)
+
+class Log(object):
+ """Read a log entry."""
+ def __init__(self, repo, ref, commit):
+ self.commit = commit
+ mode, vblob = self.commit.data.tree.data.entries.get(
+ 'version', (None, None))
+ if not isinstance(vblob, git.Blob):
+ raise LogException('%s does not contain a valid log' % ref)
+ try:
+ version = int(vblob.data.str)
+ except ValueError:
+ raise LogException('"%s": invalid version number' % vblob.data.str)
+ if version < _current_version:
+ raise LogException(
+ '%s contains a stack log older than version %d;'
+ ' please delete it' % (ref, _current_version))
+ elif version > _current_version:
+ raise LogException(
+ 'Log contains a stack log newer than version %d;'
+ ' this version of StGit cannot read it' % _current_version)
+
+ # TODO: read the rest of the log lazily.
+
+ def pl(name):
+ patches = [x.split() for x in
+ self.commit.data.tree.data.entries[name][1]
+ .data.str.strip().split('\n') if x]
+ # TODO: handle case where we don't have the commit object.
+ return ([pn for sha1, pn in patches],
+ dict((pn, repo.get_commit(sha1)) for sha1, pn in patches))
+ self.patches = {}
+ self.applied, patches = pl('applied')
+ self.patches.update(patches)
+ self.unapplied, patches = pl('unapplied')
+ self.patches.update(patches)
+ self.head = repo.get_commit(
+ self.commit.data.tree.data.entries['head'][1].data.str)
+ if self.applied:
+ self.base = self.patches[self.applied[0]].data.parent
+ else:
+ self.base = self.head
+
+class FullLog(Log):
+ full_log = property(lambda self: self.commit)
+ stack_log = property(lambda self: self.commit.data.parents[0])
+
+def delete_log(repo, branch):
+ ref = log_ref(branch)
+ if repo.refs.exists(ref):
+ repo.refs.delete(ref)
+
+def rename_log(repo, old_branch, new_branch, msg):
+ old_ref = log_ref(old_branch)
+ new_ref = log_ref(new_branch)
+ if repo.refs.exists(old_ref):
+ repo.refs.set(new_ref, repo.refs.get(old_ref), msg)
+ repo.refs.delete(old_ref)
+
+def copy_log(repo, src_branch, dst_branch, msg):
+ src_ref = log_ref(src_branch)
+ dst_ref = log_ref(dst_branch)
+ if repo.refs.exists(src_ref):
+ repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)
+
+def default_repo():
+ return stack.Repository.default()
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
index 874f81b..4fb37ce 100644
--- a/stgit/lib/transaction.py
+++ b/stgit/lib/transaction.py
@@ -1,7 +1,7 @@
from stgit import exception, utils
from stgit.utils import any, all
from stgit.out import *
-from stgit.lib import git
+from stgit.lib import git, log
class TransactionException(exception.StgException):
pass
@@ -131,6 +131,7 @@ class StackTransaction(object):
_print_current_patch(self.__stack.patchorder.applied, self.__applied)
self.__stack.patchorder.applied = self.__applied
self.__stack.patchorder.unapplied = self.__unapplied
+ log.log_entry(self.__stack, self.__msg)
if self.__error:
return utils.STGIT_CONFLICT
diff --git a/stgit/main.py b/stgit/main.py
index aa1f8ef..bd1a187 100644
--- a/stgit/main.py
+++ b/stgit/main.py
@@ -277,6 +277,7 @@ def main():
ret = command.func(parser, options, args)
except (StgException, IOError, ParsingError, NoSectionError), err:
+ directory.write_log('%s %s' % (prog, cmd))
out.error(str(err), title = '%s %s' % (prog, cmd))
if debug_level > 0:
traceback.print_exc()
@@ -292,4 +293,5 @@ def main():
traceback.print_exc()
sys.exit(utils.STGIT_BUG_ERROR)
+ directory.write_log('%s %s' % (prog, cmd))
sys.exit(ret or utils.STGIT_SUCCESS)
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 05/10] Add utility function for reordering patches
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
` (3 preceding siblings ...)
2008-04-20 22:10 ` [StGit PATCH 04/10] Write to a stack log when stack is modified Karl Hasselström
@ 2008-04-20 22:10 ` Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 06/10] New command: stg reset Karl Hasselström
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:10 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/lib/transaction.py | 14 ++++++++++++++
1 files changed, 14 insertions(+), 0 deletions(-)
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
index 4fb37ce..4c3b448 100644
--- a/stgit/lib/transaction.py
+++ b/stgit/lib/transaction.py
@@ -1,3 +1,5 @@
+import itertools as it
+
from stgit import exception, utils
from stgit.utils import any, all
from stgit.out import *
@@ -233,3 +235,15 @@ class StackTransaction(object):
self.__allow_conflicts = lambda trans: True
self.__halt('Merge conflict')
+
+ def reorder_patches(self, applied, unapplied, iw = None):
+ """Push and pop patches to attain the given ordering."""
+ common = len(list(it.takewhile(lambda (a, b): a == b,
+ zip(self.applied, applied))))
+ to_pop = set(self.applied[common:])
+ self.pop_patches(lambda pn: pn in to_pop)
+ for pn in applied[common:]:
+ self.push_patch(pn, iw)
+ assert self.applied == applied
+ assert set(self.unapplied) == set(unapplied)
+ self.unapplied = unapplied
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 06/10] New command: stg reset
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
` (4 preceding siblings ...)
2008-04-20 22:10 ` [StGit PATCH 05/10] Add utility function for reordering patches Karl Hasselström
@ 2008-04-20 22:10 ` Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 07/10] Log conflicts separately Karl Hasselström
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:10 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
Given a commit object from the log, resets the stack (or just the
named patches) to the state given by that log entry.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/commands/reset.py | 114 +++++++++++++++++++++++++++++++++++
stgit/main.py | 2 +
t/t3100-reset.sh | 151 +++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 267 insertions(+), 0 deletions(-)
create mode 100644 stgit/commands/reset.py
create mode 100755 t/t3100-reset.sh
diff --git a/stgit/commands/reset.py b/stgit/commands/reset.py
new file mode 100644
index 0000000..e27f440
--- /dev/null
+++ b/stgit/commands/reset.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2008, 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 stgit.commands import common
+from stgit.lib import git, log, transaction
+from stgit.out import out
+
+help = 'reset the patch stack to an earlier state'
+usage = """%prog <state> [<patchnames>]
+
+Reset the patch stack to an earlier state. The state is specified with
+a commit from a stack log; for a branch foo, StGit stores the stack
+log in foo.stgit^. So to undo the last N StGit commands, you would say
+
+ stg reset foo.stgit^~N
+
+or, if you are not sure how many steps to undo, you can view the log
+with "git log" or gitk
+
+ gitk foo.stgit^
+
+and then reset to any sha1 you see in the log.
+
+If one or more patch names are given, reset only those patches, and
+leave the rest alone."""
+
+directory = common.DirectoryHasRepositoryLib()
+options = []
+
+def reset_stack(stack, iw, state, only_patches):
+ only_patches = set(only_patches)
+ def mask(s):
+ if only_patches:
+ return s & only_patches
+ else:
+ return s
+ patches_to_reset = mask(set(state.applied + state.unapplied))
+ existing_patches = set(stack.patchorder.all)
+ to_delete = mask(existing_patches - patches_to_reset)
+ trans = transaction.StackTransaction(stack, 'stg reset')
+
+ # If we have to change the stack base, we need to pop all patches
+ # first.
+ if not only_patches and trans.base != state.base:
+ trans.pop_patches(lambda pn: True)
+ out.info('Setting stack base to %s' % state.base.sha1)
+ trans.base = state.base
+
+ # In one go, do all the popping we have to in order to pop the
+ # patches we're going to delete or modify.
+ def mod(pn):
+ if only_patches and not pn in only_patches:
+ return False
+ if pn in to_delete:
+ return True
+ if stack.patches.get(pn).commit != state.patches.get(pn, None):
+ return True
+ return False
+ trans.pop_patches(mod)
+
+ # Delete and modify/create patches. We've previously popped all
+ # patches that we touch in this step.
+ trans.delete_patches(lambda pn: pn in to_delete)
+ for pn in patches_to_reset:
+ if pn in existing_patches:
+ if trans.patches[pn] == state.patches[pn]:
+ continue
+ else:
+ out.info('Resetting %s' % pn)
+ else:
+ trans.unapplied.append(pn)
+ out.info('Resurrecting %s' % pn)
+ trans.patches[pn] = state.patches[pn]
+
+ # Push/pop patches as necessary.
+ try:
+ if only_patches:
+ # Push all the patches that we've popped, if they still
+ # exist.
+ pushable = set(trans.unapplied)
+ for pn in stack.patchorder.applied:
+ if pn in pushable:
+ trans.push_patch(pn, iw)
+ else:
+ # Recreate the exact order specified by the goal state.
+ trans.reorder_patches(state.applied, state.unapplied, iw)
+ except transaction.TransactionHalted:
+ pass
+ return trans.run(iw)
+
+def func(parser, options, args):
+ stack = directory.repository.current_stack
+ if len(args) >= 1:
+ ref, patches = args[0], args[1:]
+ state = log.Log(stack.repository, ref, stack.repository.rev_parse(ref))
+ else:
+ raise common.CmdException('Wrong number of arguments')
+ return reset_stack(stack, stack.repository.default_iw, state, patches)
diff --git a/stgit/main.py b/stgit/main.py
index bd1a187..948f923 100644
--- a/stgit/main.py
+++ b/stgit/main.py
@@ -89,6 +89,7 @@ commands = Commands({
'refresh': 'refresh',
'rename': 'rename',
'repair': 'repair',
+ 'reset': 'reset',
'resolved': 'resolved',
'series': 'series',
'show': 'show',
@@ -122,6 +123,7 @@ stackcommands = (
'push',
'rebase',
'repair',
+ 'reset',
'series',
'sink',
'top',
diff --git a/t/t3100-reset.sh b/t/t3100-reset.sh
new file mode 100755
index 0000000..1805091
--- /dev/null
+++ b/t/t3100-reset.sh
@@ -0,0 +1,151 @@
+#!/bin/sh
+
+test_description='Simple test cases for "stg reset"'
+
+. ./test-lib.sh
+
+# Ignore our own output files.
+cat > .git/info/exclude <<EOF
+/expected.txt
+EOF
+
+test_expect_success 'Initialize StGit stack with three patches' '
+ stg init &&
+ echo 000 >> a &&
+ git add a &&
+ git commit -m a &&
+ echo 111 >> a &&
+ git commit -a -m p1 &&
+ echo 222 >> a &&
+ git commit -a -m p2 &&
+ echo 333 >> a &&
+ git commit -a -m p3 &&
+ stg uncommit -n 3 &&
+ stg pop
+'
+
+cat > expected.txt <<EOF
+000
+111
+EOF
+test_expect_success 'Pop one patch ...' '
+ stg pop &&
+ test "$(echo $(stg applied))" = "p1" &&
+ test "$(echo $(stg unapplied))" = "p2 p3" &&
+ test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo it' '
+ stg reset master.stgit^~1 &&
+ test "$(echo $(stg applied))" = "p1 p2" &&
+ test "$(echo $(stg unapplied))" = "p3" &&
+ test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+333
+EOF
+test_expect_success 'Push one patch ...' '
+ stg push &&
+ test "$(echo $(stg applied))" = "p1 p2 p3" &&
+ test "$(echo $(stg unapplied))" = "" &&
+ test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo it' '
+ stg reset master.stgit^~1 &&
+ test "$(echo $(stg applied))" = "p1 p2" &&
+ test "$(echo $(stg unapplied))" = "p3" &&
+ test_cmp expected.txt a
+'
+
+test_expect_success 'Commit one patch ...' '
+ stg commit &&
+ test "$(echo $(stg applied))" = "p2" &&
+ test "$(echo $(stg unapplied))" = "p3"
+'
+
+test_expect_success '... and undo it' '
+ stg reset master.stgit^~1 &&
+ test "$(echo $(stg applied))" = "p1 p2" &&
+ test "$(echo $(stg unapplied))" = "p3"
+'
+
+cat > expected.txt <<EOF
+000
+111
+EOF
+test_expect_success 'Delete two patches ...' '
+ stg delete p2 p3 &&
+ test "$(echo $(stg applied))" = "p1" &&
+ test "$(echo $(stg unapplied))" = "" &&
+ test_cmp expected.txt a
+'
+
+test_expect_success '... and undo one of the deletions ...' '
+ stg reset master.stgit^~1 p3 &&
+ test "$(echo $(stg applied))" = "p1" &&
+ test "$(echo $(stg unapplied))" = "p3" &&
+ test_cmp expected.txt a
+'
+
+test_expect_success '... then undo the first undo ...' '
+ stg reset master.stgit^~1 &&
+ test "$(echo $(stg applied))" = "p1" &&
+ test "$(echo $(stg unapplied))" = "" &&
+ test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo the other deletion' '
+ stg reset master.stgit^~3 p2 &&
+ stg push p2 &&
+ test "$(echo $(stg applied))" = "p1 p2" &&
+ test "$(echo $(stg unapplied))" = "" &&
+ test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+ggg
+EOF
+test_expect_success 'Refresh a patch ...' '
+ echo ggg >> a &&
+ stg refresh &&
+ test "$(echo $(stg applied))" = "p1 p2" &&
+ test "$(echo $(stg unapplied))" = "" &&
+ test_cmp expected.txt a
+'
+
+cat > expected.txt <<EOF
+000
+111
+222
+EOF
+test_expect_success '... and undo the refresh' '
+ stg reset master.stgit^~1 &&
+ test "$(echo $(stg applied))" = "p1 p2" &&
+ test "$(echo $(stg unapplied))" = "" &&
+ test_cmp expected.txt a
+'
+
+test_done
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 07/10] Log conflicts separately
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
` (5 preceding siblings ...)
2008-04-20 22:10 ` [StGit PATCH 06/10] New command: stg reset Karl Hasselström
@ 2008-04-20 22:11 ` Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 08/10] Log conflicts separately for all commands Karl Hasselström
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:11 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
This patch makes commands that produce a conflict log that final
conflicting push separately from the rest of the command's effects.
This makes it possible for the user to roll back just the final
conflicting push if she desires. (Rolling back the whole operation is
of course still possible, by resetting to the state yet another step
back in the log.)
This change only applies to the new-infrastructure commands.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/lib/transaction.py | 47 +++++++++++++++++++++++++++++++---------------
1 files changed, 32 insertions(+), 15 deletions(-)
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
index 4c3b448..92bcfd5 100644
--- a/stgit/lib/transaction.py
+++ b/stgit/lib/transaction.py
@@ -42,6 +42,7 @@ class StackTransaction(object):
self.__patches = _TransPatchMap(stack)
self.__applied = list(self.__stack.patchorder.applied)
self.__unapplied = list(self.__stack.patchorder.unapplied)
+ self.__conflicting_push = None
self.__error = None
self.__current_tree = self.__stack.head.data.tree
self.__base = self.__stack.base
@@ -121,19 +122,26 @@ class StackTransaction(object):
out.error(self.__error)
# 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()
+ def write(msg):
+ 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, msg)
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
- log.log_entry(self.__stack, self.__msg)
+ self.__stack.patches.new(pn, commit, msg)
+ self.__stack.patchorder.applied = self.__applied
+ self.__stack.patchorder.unapplied = self.__unapplied
+ log.log_entry(self.__stack, msg)
+ old_applied = self.__stack.patchorder.applied
+ write(self.__msg)
+ if self.__conflicting_push != None:
+ self.__patches = _TransPatchMap(self.__stack)
+ self.__conflicting_push()
+ write(self.__msg + ' (CONFLICT)')
+ _print_current_patch(old_applied, self.__applied)
if self.__error:
return utils.STGIT_CONFLICT
@@ -223,18 +231,27 @@ class StackTransaction(object):
cd = cd.set_tree(tree)
if any(getattr(cd, a) != getattr(orig_cd, a) for a in
['parent', 'tree', 'author', 'message']):
- self.patches[pn] = self.__stack.repository.commit(cd)
+ comm = self.__stack.repository.commit(cd)
else:
+ comm = None
s = ' (unmodified)'
- del self.unapplied[self.unapplied.index(pn)]
- self.applied.append(pn)
out.info('Pushed %s%s' % (pn, s))
+ def update():
+ if comm:
+ self.patches[pn] = comm
+ del self.unapplied[self.unapplied.index(pn)]
+ self.applied.append(pn)
if merge_conflict:
# We've just caused conflicts, so we must allow them in
# the final checkout.
self.__allow_conflicts = lambda trans: True
+ # Save this update so that we can run it a little later.
+ self.__conflicting_push = update
self.__halt('Merge conflict')
+ else:
+ # Update immediately.
+ update()
def reorder_patches(self, applied, unapplied, iw = None):
"""Push and pop patches to attain the given ordering."""
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 08/10] Log conflicts separately for all commands
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
` (6 preceding siblings ...)
2008-04-20 22:11 ` [StGit PATCH 07/10] Log conflicts separately Karl Hasselström
@ 2008-04-20 22:11 ` Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 09/10] Add a --hard flag to stg reset Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 10/10] Don't write a log entry if there were no changes Karl Hasselström
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:11 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
This takes care of the old-infrastructure commands as well. They'll
all be converted to the new infrastructure eventually, but until then
this patch is necessary to make all the commands behave consistently.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/lib/log.py | 29 ++++++++++++++++++++++++++++-
stgit/lib/stack.py | 7 +++++++
2 files changed, 35 insertions(+), 1 deletions(-)
diff --git a/stgit/lib/log.py b/stgit/lib/log.py
index fac050f..9904941 100644
--- a/stgit/lib/log.py
+++ b/stgit/lib/log.py
@@ -58,10 +58,37 @@ def log_entry(stack, msg):
+ [ll.full_log for ll in last_log])))
stack.repository.refs.set(ref, full_log, msg)
+class Fakestack(object):
+ """Imitates a real Stack, but with the topmost patch popped."""
+ def __init__(self, stack):
+ appl = list(stack.patchorder.applied)
+ unappl = list(stack.patchorder.unapplied)
+ class patchorder(object):
+ applied = appl[:-1]
+ unapplied = [appl[-1]] + unappl
+ all = appl + unappl
+ self.patchorder = patchorder
+ class patches(object):
+ @staticmethod
+ def get(pn):
+ if pn == appl[-1]:
+ class patch(object):
+ commit = stack.patches.get(pn).old_commit
+ return patch
+ else:
+ return stack.patches.get(pn)
+ self.patches = patches
+ self.head = stack.head.data.parent
+ self.name = stack.name
+ self.repository = stack.repository
def compat_log_entry(msg):
repo = default_repo()
stack = repo.get_stack(repo.current_branch)
- log_entry(stack, msg)
+ if repo.default_index.conflicts():
+ log_entry(Fakestack(stack), msg)
+ log_entry(stack, msg + ' (CONFLICT)')
+ else:
+ log_entry(stack, msg)
class Log(object):
"""Read a log entry."""
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
index af1c994..95b817f 100644
--- a/stgit/lib/stack.py
+++ b/stgit/lib/stack.py
@@ -17,6 +17,13 @@ class Patch(object):
def commit(self):
return self.__stack.repository.refs.get(self.__ref)
@property
+ def old_commit(self):
+ """Return the previous commit for this patch."""
+ fn = os.path.join(self.__compat_dir, 'top.old')
+ if not os.path.isfile(fn):
+ return None
+ return self.__stack.repository.get_commit(utils.read_string(fn))
+ @property
def __compat_dir(self):
return os.path.join(self.__stack.directory, 'patches', self.__name)
def __write_compat_files(self, new_commit, msg):
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 09/10] Add a --hard flag to stg reset
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
` (7 preceding siblings ...)
2008-04-20 22:11 ` [StGit PATCH 08/10] Log conflicts separately for all commands Karl Hasselström
@ 2008-04-20 22:11 ` Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 10/10] Don't write a log entry if there were no changes Karl Hasselström
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:11 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
With this flag, reset will overwrite any local changes. Useful e.g.
when undoing a push that's polluted the index+worktree with a heap of
conflicts.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/commands/reset.py | 14 +++++++-----
stgit/lib/git.py | 4 +++
stgit/lib/transaction.py | 11 +++++++--
t/t3101-reset-hard.sh | 56 ++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 77 insertions(+), 8 deletions(-)
create mode 100755 t/t3101-reset-hard.sh
diff --git a/stgit/commands/reset.py b/stgit/commands/reset.py
index e27f440..00226f0 100644
--- a/stgit/commands/reset.py
+++ b/stgit/commands/reset.py
@@ -17,12 +17,13 @@ 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.commands import common
from stgit.lib import git, log, transaction
from stgit.out import out
help = 'reset the patch stack to an earlier state'
-usage = """%prog <state> [<patchnames>]
+usage = """%prog [options] <state> [<patchnames>]
Reset the patch stack to an earlier state. The state is specified with
a commit from a stack log; for a branch foo, StGit stores the stack
@@ -41,9 +42,10 @@ If one or more patch names are given, reset only those patches, and
leave the rest alone."""
directory = common.DirectoryHasRepositoryLib()
-options = []
+options = [make_option('--hard', action = 'store_true',
+ help = 'discard changes in your index/worktree')]
-def reset_stack(stack, iw, state, only_patches):
+def reset_stack(stack, iw, state, only_patches, hard):
only_patches = set(only_patches)
def mask(s):
if only_patches:
@@ -53,7 +55,8 @@ def reset_stack(stack, iw, state, only_patches):
patches_to_reset = mask(set(state.applied + state.unapplied))
existing_patches = set(stack.patchorder.all)
to_delete = mask(existing_patches - patches_to_reset)
- trans = transaction.StackTransaction(stack, 'stg reset')
+ trans = transaction.StackTransaction(stack, 'stg reset',
+ discard_changes = hard)
# If we have to change the stack base, we need to pop all patches
# first.
@@ -111,4 +114,5 @@ def func(parser, options, args):
state = log.Log(stack.repository, ref, stack.repository.rev_parse(ref))
else:
raise common.CmdException('Wrong number of arguments')
- return reset_stack(stack, stack.repository.default_iw, state, patches)
+ return reset_stack(stack, stack.repository.default_iw, state, patches,
+ options.hard)
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
index 5bb1c93..c044b46 100644
--- a/stgit/lib/git.py
+++ b/stgit/lib/git.py
@@ -605,6 +605,10 @@ class IndexAndWorktree(RunWithEnvCwd):
env = property(lambda self: utils.add_dict(self.__index.env,
self.__worktree.env))
cwd = property(lambda self: self.__worktree.directory)
+ def checkout_hard(self, tree):
+ assert isinstance(tree, Tree)
+ self.run(['git', 'read-tree', '--reset', '-u', tree.sha1]
+ ).discard_output()
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?
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
index 92bcfd5..a008780 100644
--- a/stgit/lib/transaction.py
+++ b/stgit/lib/transaction.py
@@ -36,7 +36,8 @@ class _TransPatchMap(dict):
return self.__stack.patches.get(pn).commit
class StackTransaction(object):
- def __init__(self, stack, msg, allow_conflicts = False):
+ def __init__(self, stack, msg, discard_changes = False,
+ allow_conflicts = False):
self.__stack = stack
self.__msg = msg
self.__patches = _TransPatchMap(stack)
@@ -46,6 +47,7 @@ class StackTransaction(object):
self.__error = None
self.__current_tree = self.__stack.head.data.tree
self.__base = self.__stack.base
+ self.__discard_changes = discard_changes
if isinstance(allow_conflicts, bool):
self.__allow_conflicts = lambda trans: allow_conflicts
else:
@@ -70,7 +72,7 @@ class StackTransaction(object):
'This can happen if you modify a branch with git.',
'"stg repair --help" explains more about what to do next.')
self.__abort()
- if self.__current_tree == tree:
+ if self.__current_tree == tree and not self.__discard_changes:
# No tree change, but we still want to make sure that
# there are no unresolved conflicts. Conflicts
# conceptually "belong" to the topmost patch, and just
@@ -81,7 +83,10 @@ class StackTransaction(object):
out.error('Need to resolve conflicts first')
self.__abort()
assert iw != None
- iw.checkout(self.__current_tree, tree)
+ if self.__discard_changes:
+ iw.checkout_hard(tree)
+ else:
+ iw.checkout(self.__current_tree, tree)
self.__current_tree = tree
@staticmethod
def __abort():
diff --git a/t/t3101-reset-hard.sh b/t/t3101-reset-hard.sh
new file mode 100755
index 0000000..1e02805
--- /dev/null
+++ b/t/t3101-reset-hard.sh
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+test_description='Simple test cases for "stg reset"'
+
+. ./test-lib.sh
+
+# Ignore our own output files.
+cat > .git/info/exclude <<EOF
+/expected.txt
+/actual.txt
+EOF
+
+test_expect_success 'Initialize StGit stack with three patches' '
+ stg init &&
+ echo 000 >> a &&
+ git add a &&
+ git commit -m a &&
+ echo 111 >> a &&
+ git commit -a -m p1 &&
+ echo 222 >> a &&
+ git commit -a -m p2 &&
+ echo 333 >> a &&
+ git commit -a -m p3 &&
+ stg uncommit -n 3
+'
+
+cat > expected.txt <<EOF
+C a
+EOF
+test_expect_success 'Pop middle patch, creating a conflict' '
+ ! stg pop p2 &&
+ stg status a > actual.txt &&
+ test_cmp expected.txt actual.txt &&
+ test "$(echo $(stg applied))" = "p1 p3" &&
+ test "$(echo $(stg unapplied))" = "p2"
+'
+
+test_expect_success 'Try to reset without --hard' '
+ ! stg reset master.stgit^~1 &&
+ stg status a > actual.txt &&
+ test_cmp expected.txt actual.txt &&
+ test "$(echo $(stg applied))" = "p1 p3" &&
+ test "$(echo $(stg unapplied))" = "p2"
+'
+
+cat > expected.txt <<EOF
+EOF
+test_expect_success 'Try to reset with --hard' '
+ stg reset --hard master.stgit^~1 &&
+ stg status a > actual.txt &&
+ test_cmp expected.txt actual.txt &&
+ test "$(echo $(stg applied))" = "p1" &&
+ test "$(echo $(stg unapplied))" = "p3 p2"
+'
+
+test_done
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [StGit PATCH 10/10] Don't write a log entry if there were no changes
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
` (8 preceding siblings ...)
2008-04-20 22:11 ` [StGit PATCH 09/10] Add a --hard flag to stg reset Karl Hasselström
@ 2008-04-20 22:11 ` Karl Hasselström
9 siblings, 0 replies; 11+ messages in thread
From: Karl Hasselström @ 2008-04-20 22:11 UTC (permalink / raw)
To: Catalin Marinas; +Cc: git
Some commands end up calling log_entry() without verifying that they
did in fact change anything. (One example of this is a conflicting
push, which will log two entries, everything else and the conflicting
push, with the "everything else" part being empty if there was only
one patch to push.) So before appending to the log, make sure that the
entry we're appending isn't a no-op.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
stgit/lib/log.py | 3 +++
1 files changed, 3 insertions(+), 0 deletions(-)
diff --git a/stgit/lib/log.py b/stgit/lib/log.py
index 9904941..a18c80e 100644
--- a/stgit/lib/log.py
+++ b/stgit/lib/log.py
@@ -49,6 +49,9 @@ def log_entry(stack, msg):
out.warn(str(e), 'No log entry written.')
return
log_tree = log_entry_tree(stack.repository, stack)
+ if len(last_log) == 1 and log_tree == last_log[0].full_log.data.tree:
+ # No changes, so there's no point writing a new log entry.
+ return
stack_log = stack.repository.commit(
git.Commitdata(tree = log_tree, message = msg,
parents = [ll.stack_log for ll in last_log]))
^ permalink raw reply related [flat|nested] 11+ messages in thread
end of thread, other threads:[~2008-04-20 22:13 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2008-04-20 22:10 [StGit PATCH 00/10] Updated "stg reset" series Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 01/10] Prevent most commands from running when there are conflicts Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 02/10] Add property with a list of all patch names Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 03/10] Library functions for tree and blob manipulation Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 04/10] Write to a stack log when stack is modified Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 05/10] Add utility function for reordering patches Karl Hasselström
2008-04-20 22:10 ` [StGit PATCH 06/10] New command: stg reset Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 07/10] Log conflicts separately Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 08/10] Log conflicts separately for all commands Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 09/10] Add a --hard flag to stg reset Karl Hasselström
2008-04-20 22:11 ` [StGit PATCH 10/10] Don't write a log entry if there were no changes 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).