{"thread":{"id":"14141","subject":"[StGit PATCH 1/2] Convert \"stg refresh\" to the new infrastructure","startedAt":"2008-06-25T04:30:15Z","lastAt":"2008-06-29T13:46:54Z","messageCount":6,"participants":["Karl Hasselström","Catalin Marinas"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"81058","messageId":"20080625042337.6044.53357.stgit@yoghurt","threadId":"14141","inReplyTo":null,"subject":"[StGit PATCH 1/2] Convert \"stg refresh\" to the new infrastructure","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-25T04:30:15Z","receivedAt":"2008-06-25T04:30:15Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"And in the process, make it more powerful: it will now first create a\ntemp patch containing the updates, and then try to merge it into the\npatch to be updated. If that patch is applied, this is done by\npopping, pushing, and coalescing; if it is unapplied, it is done with\nan in-index merge.\n\nThe temp patch creation and merging is logged in two separate stages,\nso that the user can undo them separately.\n\nAlso, whenever path limiting is used, we will now use a temporary\nindex in order to avoid including all staged updates (since they may\ntouch stuff outside the path limiters).\n\nSupport for the --force, --undo, and --annotate flags were dropped.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\nI've been working on this for some time -- it turned out to be bigger\nthan I'd thought. What do you think (especially about the double\nlogging that enables undoing)?\n\nThis was the last step before I was planning to remove the --undo\nflags from everywhere. But I think I'll look over the log format first\n(unless you want to do it -- in case we should get all this stuff\ncommitted to a public non-rebasing development branch, since rebasing\ngets really confusing when more than one person is working on things).\n\n stgit/commands/refresh.py |  316 +++++++++++++++++++++++++++++----------------\n stgit/lib/git.py          |   62 ++++++++-\n stgit/lib/stack.py        |    9 +\n stgit/lib/transaction.py  |   11 +-\n t/t3100-reset.sh          |    2 \n 5 files changed, 275 insertions(+), 125 deletions(-)\n\n\ndiff --git a/stgit/commands/refresh.py b/stgit/commands/refresh.py\nindex 4695c62..7afc55e 100644\n--- a/stgit/commands/refresh.py\n+++ b/stgit/commands/refresh.py\n@@ -1,6 +1,8 @@\n+# -*- coding: utf-8 -*-\n \n __copyright__ = \"\"\"\n Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>\n+Copyright (C) 2008, Karl Hasselström <kha@treskal.com>\n \n This program is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License version 2 as\n@@ -16,125 +18,209 @@ along with this program; if not, write to the Free Software\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n-import sys, os\n-from optparse import OptionParser, make_option\n-\n-from stgit.commands.common import *\n-from stgit.utils import *\n-from stgit.out import *\n-from stgit import stack, git\n-from stgit.config import config\n-\n+from optparse import make_option\n+from stgit.commands import common\n+from stgit.lib import git, transaction\n+from stgit import utils\n \n help = 'generate a new commit for the current patch'\n usage = \"\"\"%prog [options] [<files or dirs>]\n \n-Include the latest tree changes in the current patch. This command\n-generates a new GIT commit object with the patch details, the previous\n-one no longer being visible. The '--force' option is useful\n-when a commit object was created with a different tool\n-but the changes need to be included in the current patch.\"\"\"\n-\n-directory = DirectoryHasRepository()\n-options = [make_option('-f', '--force',\n-                       help = 'force the refresh even if HEAD and '\\\n-                       'top differ',\n-                       action = 'store_true'),\n-           make_option('--update',\n-                       help = 'only update the current patch files',\n-                       action = 'store_true'),\n-           make_option('--index',\n-                       help = 'use the current contents of the index instead of looking at the working directory',\n-                       action = 'store_true'),\n-           make_option('--undo',\n-                       help = 'revert the commit generated by the last refresh',\n-                       action = 'store_true'),\n-           make_option('-a', '--annotate', metavar = 'NOTE',\n-                       help = 'annotate the patch log entry'),\n+Include the latest work tree and index changes in the current patch.\n+This command generates a new git commit object for the patch; the old\n+commit is no longer visible.\n+\n+You may optionally list one or more files or directories relative to\n+the current working directory; if you do, only matching files will be\n+updated.\n+\n+Behind the scenes, stg refresh first creates a new temporary patch\n+with your updates, and then merges that patch into the patch you asked\n+to have refreshed. If you asked to refresh a patch other than the\n+topmost patch, there can be conflicts; in that case, the temporary\n+patch will be left for you to take care of, for example with stg\n+coalesce.\n+\n+The creation of the temporary patch is recorded in a separate entry in\n+the patch stack log; this means that one undo step will undo the merge\n+between the other patch and the temp patch, and two undo steps will\n+additionally get rid of the temp patch.\"\"\"\n+\n+directory = common.DirectoryHasRepositoryLib()\n+options = [make_option('-u', '--update', action = 'store_true',\n+                       help = 'only update the current patch files'),\n+           make_option('-i', '--index', action = 'store_true',\n+                       help = ('only include changes from the index, not'\n+                               ' from the work tree')),\n            make_option('-p', '--patch',\n-                       help = 'refresh (applied) PATCH instead of the top one')\n-           ]\n-\n-def func(parser, options, args):\n-    \"\"\"Generate a new commit for the current or given patch.\n-    \"\"\"\n-    args = git.ls_files(args)\n-    directory.cd_to_topdir()\n-\n-    autoresolved = config.get('stgit.autoresolved')\n-    if autoresolved != 'yes':\n-        check_conflicts()\n-\n-    if options.patch:\n-        if args or options.update:\n-            raise CmdException, \\\n-                  'Only full refresh is available with the --patch option'\n-        patch = options.patch\n-        if not crt_series.patch_applied(patch):\n-            raise CmdException, 'Patches \"%s\" not applied' % patch\n+                       help = 'refresh PATCH instead of the topmost patch')]\n+\n+def get_patch(stack, given_patch):\n+    \"\"\"Get the name of the patch we are to refresh.\"\"\"\n+    if given_patch:\n+        patch_name = given_patch\n+        if not stack.patches.exists(patch_name):\n+            raise common.CmdException('%s: no such patch' % patch_name)\n+        return patch_name\n     else:\n-        patch = crt_series.get_current()\n-        if not patch:\n-            raise CmdException, 'No patches applied'\n-\n-    if options.index:\n-        if args or options.update:\n-            raise CmdException, \\\n-                  'Only full refresh is available with the --index option'\n-        if options.patch:\n-            raise CmdException, \\\n-                  '--patch is not compatible with the --index option'\n-\n-    if not options.force:\n-        check_head_top_equal(crt_series)\n-\n-    if options.undo:\n-        out.start('Undoing the refresh of \"%s\"' % patch)\n-        crt_series.undo_refresh()\n-        out.done()\n-        return\n-\n-    if not options.index:\n-        files = [path for (stat, path) in git.tree_status(files = args, verbose = True)]\n-\n-    if options.index or files or not crt_series.head_top_equal():\n-        if options.patch:\n-            applied = crt_series.get_applied()\n-            between = applied[:applied.index(patch):-1]\n-            pop_patches(crt_series, between, keep = True)\n-        elif options.update:\n-            rev1 = git_id(crt_series, '//bottom')\n-            rev2 = git_id(crt_series, '//top')\n-            patch_files = git.barefiles(rev1, rev2).split('\\n')\n-            files = [f for f in files if f in patch_files]\n-            if not files:\n-                out.info('No modified files for updating patch \"%s\"' % patch)\n-                return\n-\n-        out.start('Refreshing patch \"%s\"' % patch)\n-\n-        if autoresolved == 'yes':\n-            resolved_all()\n-\n-        if options.index:\n-            crt_series.refresh_patch(cache_update = False,\n-                                     backup = True, notes = options.annotate)\n-        else:\n-            crt_series.refresh_patch(files = files,\n-                                     backup = True, notes = options.annotate)\n-\n-        if crt_series.empty_patch(patch):\n-            out.done('empty patch')\n-        else:\n-            out.done()\n-\n-        if options.patch:\n-            between.reverse()\n-            push_patches(crt_series, between)\n-    elif options.annotate:\n-        # only annotate the top log entry as there is no need to\n-        # refresh the patch and generate a full commit\n-        crt_series.log_patch(crt_series.get_patch(patch), None,\n-                             notes = options.annotate)\n+        if not stack.patchorder.applied:\n+            raise common.CmdException(\n+                'Cannot refresh top patch, because no patches are applied')\n+        return stack.patchorder.applied[-1]\n+\n+def list_files(stack, patch_name, args, index, update):\n+    \"\"\"Figure out which files to update.\"\"\"\n+    if index:\n+        # --index: Don't update the index.\n+        return set()\n+    paths = stack.repository.default_iw.changed_files(\n+        stack.head.data.tree, args or [])\n+    if update:\n+        # --update: Restrict update to the paths that were already\n+        # part of the patch.\n+        paths &= stack.patches.get(patch_name).files()\n+    return paths\n+\n+def write_tree(stack, paths, temp_index):\n+    \"\"\"Possibly update the index, and then write its tree.\n+    @return: The written tree.\n+    @rtype: L{Tree<stgit.git.Tree>}\"\"\"\n+    def go(index):\n+        if paths:\n+            iw = git.IndexAndWorktree(index, stack.repository.default_worktree)\n+            iw.update_index(paths)\n+        return index.write_tree()\n+    if temp_index:\n+        index = stack.repository.temp_index()\n+        try:\n+            index.read_tree(stack.head)\n+            return go(index)\n+        finally:\n+            index.delete()\n+            stack.repository.default_iw.update_index(paths)\n     else:\n-        out.info('Patch \"%s\" is already up to date' % patch)\n+        return go(stack.repository.default_index)\n+\n+def make_temp_patch(stack, patch_name, paths, temp_index):\n+    \"\"\"Commit index to temp patch, in a complete transaction. If any path\n+    limiting is in effect, use a temp index.\"\"\"\n+    tree = write_tree(stack, paths, temp_index)\n+    commit = stack.repository.commit(git.CommitData(\n+            tree = tree, parents = [stack.head],\n+            message = 'Refresh of %s' % patch_name))\n+    temp_name = utils.make_patch_name('refresh-temp', stack.patches.exists)\n+    trans = transaction.StackTransaction(stack,\n+                                         'refresh (create temporary patch)')\n+    trans.patches[temp_name] = commit\n+    trans.applied.append(temp_name)\n+    return trans.run(stack.repository.default_iw,\n+                     print_current_patch = False), temp_name\n+\n+def absorb_applied(trans, iw, patch_name, temp_name):\n+    \"\"\"Absorb the temp patch (C{temp_name}) into the given patch\n+    (C{patch_name}), which must be applied.\n+\n+    @return: C{True} if we managed to absorb the temp patch, C{False}\n+             if we had to leave it for the user to deal with.\"\"\"\n+    temp_absorbed = False\n+    try:\n+        # Pop any patch on top of the patch we're refreshing.\n+        to_pop = trans.applied[trans.applied.index(patch_name)+1:]\n+        if len(to_pop) > 1:\n+            popped = trans.pop_patches(lambda pn: pn in to_pop)\n+            assert not popped # no other patches were popped\n+            trans.push_patch(temp_name, iw)\n+        assert to_pop.pop() == temp_name\n+\n+        # Absorb the temp patch.\n+        temp_cd = trans.patches[temp_name].data\n+        assert trans.patches[patch_name] == temp_cd.parent\n+        trans.patches[patch_name] = trans.stack.repository.commit(\n+            trans.patches[patch_name].data.set_tree(temp_cd.tree))\n+        popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True)\n+        assert not popped # the temp patch was topmost\n+        temp_absorbed = True\n+\n+        # Push back any patch we were forced to pop earlier.\n+        for pn in to_pop:\n+            trans.push_patch(pn, iw)\n+    except transaction.TransactionHalted:\n+        pass\n+    return temp_absorbed\n+\n+def absorb_unapplied(trans, iw, patch_name, temp_name):\n+    \"\"\"Absorb the temp patch (C{temp_name}) into the given patch\n+    (C{patch_name}), which must be unapplied.\n+\n+    @param iw: Not used.\n+    @return: C{True} if we managed to absorb the temp patch, C{False}\n+             if we had to leave it for the user to deal with.\"\"\"\n+\n+    # Pop the temp patch.\n+    popped = trans.pop_patches(lambda pn: pn == temp_name)\n+    assert not popped # the temp patch was topmost\n+\n+    # Try to create the new tree of the refreshed patch. (This is the\n+    # same operation as pushing the temp patch onto the patch we're\n+    # trying to refresh -- but we don't have a worktree to spill\n+    # conflicts to, so if the simple merge doesn't succeed, we have to\n+    # give up.)\n+    patch_cd = trans.patches[patch_name].data\n+    temp_cd = trans.patches[temp_name].data\n+    new_tree = trans.stack.repository.simple_merge(\n+        base = temp_cd.parent.data.tree,\n+        ours = patch_cd.tree, theirs = temp_cd.tree)\n+    if new_tree:\n+        # It worked. Refresh the patch with the new tree, and delete\n+        # the temp patch.\n+        trans.patches[patch_name] = trans.stack.repository.commit(\n+            patch_cd.set_tree(new_tree))\n+        popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True)\n+        assert not popped # the temp patch was not applied\n+        return True\n+    else:\n+        # Nope, we couldn't create the new tree, so we'll just have to\n+        # leave the temp patch for the user.\n+        return False\n+\n+def absorb(stack, patch_name, temp_name):\n+    \"\"\"Absorb the temp patch into the target patch.\"\"\"\n+    trans = transaction.StackTransaction(stack, 'refresh')\n+    iw = stack.repository.default_iw\n+    f = { True: absorb_applied, False: absorb_unapplied\n+          }[patch_name in trans.applied]\n+    if f(trans, iw, patch_name, temp_name):\n+        def info_msg(): pass\n+    else:\n+        def info_msg():\n+            out.warn('The new changes did not apply cleanly to %s.'\n+                     % patch_name, 'They were saved in %s.' % temp_name)\n+    r = trans.run(iw)\n+    info_msg()\n+    return r\n+\n+def func(parser, options, args):\n+    \"\"\"Generate a new commit for the current or given patch.\"\"\"\n+\n+    # Catch illegal argument combinations.\n+    path_limiting = bool(args or options.update)\n+    if options.index and path_limiting:\n+        raise common.CmdException(\n+            'Only full refresh is available with the --index option')\n+\n+    stack = directory.repository.current_stack\n+    patch_name = get_patch(stack, options.patch)\n+    paths = list_files(stack, patch_name, args, options.index, options.update)\n+\n+    # Make sure there are no conflicts in the files we want to\n+    # refresh.\n+    if stack.repository.default_index.conflicts() & paths:\n+        raise common.CmdException(\n+            'Cannot refresh -- resolve conflicts first')\n+\n+    # Commit index to temp patch, and absorb it into the target patch.\n+    retval, temp_name = make_temp_patch(\n+        stack, patch_name, paths, temp_index = path_limiting)\n+    if retval != utils.STGIT_SUCCESS:\n+        return retval\n+    return absorb(stack, patch_name, temp_name)\ndiff --git a/stgit/lib/git.py b/stgit/lib/git.py\nindex 4c2605b..2aecf10 100644\n--- a/stgit/lib/git.py\n+++ b/stgit/lib/git.py\n@@ -511,6 +511,14 @@ class RunWithEnvCwd(RunWithEnv):\n         @type env: dict\n         @param env: Extra environment\"\"\"\n         return RunWithEnv.run(self, args, env).cwd(self.cwd)\n+    def run_in_cwd(self, args):\n+        \"\"\"Run the given command with an environment given by self.env and\n+        self.env_in_cwd, without changing the current working\n+        directory.\n+\n+        @type args: list of strings\n+        @param args: Command and argument vector\"\"\"\n+        return RunWithEnv.run(self, args, self.env_in_cwd)\n \n class Repository(RunWithEnv):\n     \"\"\"Represents a git repository.\"\"\"\n@@ -655,6 +663,32 @@ class Repository(RunWithEnv):\n         assert isinstance(t2, Tree)\n         return self.run(['git', 'diff-tree', '-p'] + list(diff_opts)\n                         + [t1.sha1, t2.sha1]).raw_output()\n+    def diff_tree_files(self, t1, t2):\n+        \"\"\"Given two L{Tree}s C{t1} and C{t2}, iterate over all files for\n+        which they differ. For each file, yield a tuple with the old\n+        file mode, the new file mode, the old blob, the new blob, the\n+        status, the old filename, and the new filename. Except in case\n+        of a copy or a rename, the old and new filenames are\n+        identical.\"\"\"\n+        assert isinstance(t1, Tree)\n+        assert isinstance(t2, Tree)\n+        i = iter(self.run(['git', 'diff-tree', '-r', '-z'] + [t1.sha1, t2.sha1]\n+                          ).raw_output().split('\\0'))\n+        try:\n+            while True:\n+                x = i.next()\n+                if not x:\n+                    continue\n+                omode, nmode, osha1, nsha1, status = x[1:].split(' ')\n+                fn1 = i.next()\n+                if status[0] in ['C', 'R']:\n+                    fn2 = i.next()\n+                else:\n+                    fn2 = fn1\n+                yield (omode, nmode, self.get_blob(osha1),\n+                       self.get_blob(nsha1), status, fn1, fn2)\n+        except StopIteration:\n+            pass\n \n class MergeException(exception.StgException):\n     \"\"\"Exception raised when a merge fails for some reason.\"\"\"\n@@ -678,6 +712,9 @@ class Index(RunWithEnv):\n     def read_tree(self, tree):\n         self.run(['git', 'read-tree', tree.sha1]).no_output()\n     def write_tree(self):\n+        \"\"\"Write the index contents to the repository.\n+        @return: The resulting L{Tree}\n+        @rtype: L{Tree}\"\"\"\n         try:\n             return self.__repository.get_tree(\n                 self.run(['git', 'write-tree']).discard_stderr(\n@@ -719,6 +756,7 @@ class Worktree(object):\n     def __init__(self, directory):\n         self.__directory = directory\n     env = property(lambda self: { 'GIT_WORK_TREE': '.' })\n+    env_in_cwd = property(lambda self: { 'GIT_WORK_TREE': self.directory })\n     directory = property(lambda self: self.__directory)\n \n class CheckoutException(exception.StgException):\n@@ -735,6 +773,7 @@ class IndexAndWorktree(RunWithEnvCwd):\n     index = property(lambda self: self.__index)\n     env = property(lambda self: utils.add_dict(self.__index.env,\n                                                self.__worktree.env))\n+    env_in_cwd = property(lambda self: self.__worktree.env_in_cwd)\n     cwd = property(lambda self: self.__worktree.directory)\n     def checkout_hard(self, tree):\n         assert isinstance(tree, Tree)\n@@ -768,11 +807,24 @@ class IndexAndWorktree(RunWithEnvCwd):\n                 raise MergeConflictException()\n             else:\n                 raise MergeException('Index/worktree dirty')\n-    def changed_files(self):\n-        return self.run(['git', 'diff-files', '--name-only']).output_lines()\n-    def update_index(self, files):\n-        self.run(['git', 'update-index', '--remove', '-z', '--stdin']\n-                 ).input_nulterm(files).discard_output()\n+    def changed_files(self, tree, pathlimits = []):\n+        \"\"\"Return the set of files in the worktree that have changed with\n+        respect to C{tree}. The listing is optionally restricted to\n+        those files that match any of the path limiters given.\n+\n+        The path limiters are relative to the current working\n+        directory; the returned file names are relative to the\n+        repository root.\"\"\"\n+        assert isinstance(tree, Tree)\n+        return set(self.run_in_cwd(\n+                ['git', 'diff-index', tree.sha1, '--name-only', '-z', '--']\n+                + list(pathlimits)).raw_output().split('\\0')[:-1])\n+    def update_index(self, paths):\n+        \"\"\"Update the index with files from the worktree. C{paths} is an\n+        iterable of paths relative to the root of the repository.\"\"\"\n+        cmd = ['git', 'update-index', '--remove']\n+        self.run(cmd + ['-z', '--stdin']\n+                 ).input_nulterm(paths).discard_output()\n \n class Branch(object):\n     \"\"\"Represents a Git branch.\"\"\"\ndiff --git a/stgit/lib/stack.py b/stgit/lib/stack.py\nindex 2d686e6..3cf66e7 100644\n--- a/stgit/lib/stack.py\n+++ b/stgit/lib/stack.py\n@@ -86,6 +86,15 @@ class Patch(object):\n         return self.name in self.__stack.patchorder.applied\n     def is_empty(self):\n         return self.commit.data.is_nochange()\n+    def files(self):\n+        \"\"\"Return the set of files this patch touches.\"\"\"\n+        fs = set()\n+        for (_, _, _, _, _, oldname, newname\n+             ) in self.__stack.repository.diff_tree_files(\n+            self.commit.data.tree, self.commit.data.parent.data.tree):\n+            fs.add(oldname)\n+            fs.add(newname)\n+        return fs\n \n class PatchOrder(object):\n     \"\"\"Keeps track of patch order, and which patches are applied.\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex 0c8d9a5..ac0594d 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -163,7 +163,8 @@ class StackTransaction(object):\n         if iw:\n             self.__checkout(self.__stack.head.data.tree, iw,\n                             allow_bad_head = True)\n-    def run(self, iw = None, allow_bad_head = False):\n+    def run(self, iw = None, allow_bad_head = False,\n+            print_current_patch = True):\n         \"\"\"Execute the transaction. Will either succeed, or fail (with an\n         exception) and do nothing.\"\"\"\n         self.__check_consistency()\n@@ -203,7 +204,8 @@ class StackTransaction(object):\n             self.__patches = _TransPatchMap(self.__stack)\n             self.__conflicting_push()\n             write(self.__msg + ' (CONFLICT)')\n-        _print_current_patch(old_applied, self.__applied)\n+        if print_current_patch:\n+            _print_current_patch(old_applied, self.__applied)\n \n         if self.__error:\n             return utils.STGIT_CONFLICT\n@@ -239,7 +241,7 @@ class StackTransaction(object):\n         self.__print_popped(popped)\n         return popped1\n \n-    def delete_patches(self, p):\n+    def delete_patches(self, p, quiet = False):\n         \"\"\"Delete all patches pn for which p(pn) is true. Return the list of\n         other patches that had to be popped to accomplish this. Always\n         succeeds.\"\"\"\n@@ -257,7 +259,8 @@ class StackTransaction(object):\n             if p(pn):\n                 s = ['', ' (empty)'][self.patches[pn].data.is_nochange()]\n                 self.patches[pn] = None\n-                out.info('Deleted %s%s' % (pn, s))\n+                if not quiet:\n+                    out.info('Deleted %s%s' % (pn, s))\n         return popped\n \n     def push_patch(self, pn, iw = None):\ndiff --git a/t/t3100-reset.sh b/t/t3100-reset.sh\nindex 1805091..a721d6d 100755\n--- a/t/t3100-reset.sh\n+++ b/t/t3100-reset.sh\n@@ -142,7 +142,7 @@ cat > expected.txt <<EOF\n 222\n EOF\n test_expect_success '... and undo the refresh' '\n-    stg reset master.stgit^~1 &&\n+    stg reset master.stgit^~2 &&\n     test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n     test \"$(echo $(stg unapplied))\" = \"\" &&\n     test_cmp expected.txt a\n"},{"id":"81059","messageId":"20080625043026.6044.72652.stgit@yoghurt","threadId":"14141","inReplyTo":"20080625042337.6044.53357.stgit@yoghurt","subject":"[StGit PATCH 2/2] New refresh tests","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-25T04:30:33Z","receivedAt":"2008-06-25T04:30:33Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Test stg refresh more extensively -- including some things it only\nrecently learned to do.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n t/t2300-refresh-subdir.sh |   29 ++++++++++++++++++++++++++++-\n 1 files changed, 28 insertions(+), 1 deletions(-)\n\n\ndiff --git a/t/t2300-refresh-subdir.sh b/t/t2300-refresh-subdir.sh\nindex 92c1cc8..d731a11 100755\n--- a/t/t2300-refresh-subdir.sh\n+++ b/t/t2300-refresh-subdir.sh\n@@ -4,7 +4,7 @@ test_description='Test the refresh command from a subdirectory'\n stg init\n \n test_expect_success 'Refresh from a subdirectory' '\n-    stg new foo -m foo &&\n+    stg new p0 -m p0 &&\n     echo foo >> foo.txt &&\n     mkdir bar &&\n     echo bar >> bar/bar.txt &&\n@@ -45,4 +45,31 @@ test_expect_success 'Refresh subdirectories recursively' '\n     [ \"$(stg status)\" = \"\" ]\n '\n \n+test_expect_success 'refresh -u' '\n+    echo baz >> bar/baz.txt &&\n+    stg new p1 -m p1 &&\n+    git add bar/baz.txt &&\n+    stg refresh --index &&\n+    echo xyzzy >> foo.txt &&\n+    echo xyzzy >> bar/bar.txt &&\n+    echo xyzzy >> bar/baz.txt &&\n+    stg refresh -u &&\n+    test \"$(echo $(stg status))\" = \"M bar/bar.txt M foo.txt\"\n+'\n+\n+test_expect_success 'refresh -u -p <subdir>' '\n+    echo xyzzy >> bar/baz.txt &&\n+    stg refresh -p p0 -u bar &&\n+    test \"$(echo $(stg status))\" = \"M bar/baz.txt M foo.txt\"\n+'\n+\n+test_expect_success 'refresh an unapplied patch' '\n+    stg refresh -u &&\n+    stg goto p0 &&\n+    test \"$(stg status)\" = \"M foo.txt\" &&\n+    stg refresh -p p1 &&\n+    test \"$(stg status)\" = \"\" &&\n+    test \"$(echo $(stg files p1))\" = \"A bar/baz.txt M foo.txt\"\n+'\n+\n test_done\n"},{"id":"81628","messageId":"b0943d9e0806290242q1bc8aa67qb3523221b3db70e2@mail.gmail.com","threadId":"14141","inReplyTo":"20080625042337.6044.53357.stgit@yoghurt","subject":"Re: [StGit PATCH 1/2] Convert \"stg refresh\" to the new infrastructure","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-06-29T09:42:32Z","receivedAt":"2008-06-29T09:42:32Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/6/25 Karl Hasselström <kha@treskal.com>:\n> And in the process, make it more powerful: it will now first create a\n> temp patch containing the updates, and then try to merge it into the\n> patch to be updated. If that patch is applied, this is done by\n> popping, pushing, and coalescing; if it is unapplied, it is done with\n> an in-index merge.\n\nDoes it make sense to refresh an unapplied patch? Maybe adding a new\nfile to the patch but I don't really see a need for this.\n\n> Also, whenever path limiting is used, we will now use a temporary\n> index in order to avoid including all staged updates (since they may\n> touch stuff outside the path limiters).\n\nI haven't checked but what is the behaviour in subdirectors? It\ncurrently refreshes everythink unless \".\" is specified so that it will\nonly refresh the current subdirectory.\n\nThe patch looks fine otherwise.\n\n-- \nCatalin\n"},{"id":"81629","messageId":"20080629102147.GA5098@diana.vm.bytemark.co.uk","threadId":"14141","inReplyTo":"b0943d9e0806290242q1bc8aa67qb3523221b3db70e2@mail.gmail.com","subject":"Re: [StGit PATCH 1/2] Convert \"stg refresh\" to the new infrastructure","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-29T10:21:47Z","receivedAt":"2008-06-29T10:21:47Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-29 10:42:32 +0100, Catalin Marinas wrote:\n\n> 2008/6/25 Karl Hasselström <kha@treskal.com>:\n>\n> > And in the process, make it more powerful: it will now first\n> > create a temp patch containing the updates, and then try to merge\n> > it into the patch to be updated. If that patch is applied, this is\n> > done by popping, pushing, and coalescing; if it is unapplied, it\n> > is done with an in-index merge.\n>\n> Does it make sense to refresh an unapplied patch? Maybe adding a new\n> file to the patch but I don't really see a need for this.\n\nA change in a different part of the same file should work as well, I\nbelieve.\n\nBut no, I don't have a strong sense that this is super useful. It was\njust easy to allow, so I allowed it.\n\n> > Also, whenever path limiting is used, we will now use a temporary\n> > index in order to avoid including all staged updates (since they\n> > may touch stuff outside the path limiters).\n>\n> I haven't checked but what is the behaviour in subdirectors? It\n> currently refreshes everythink unless \".\" is specified so that it\n> will only refresh the current subdirectory.\n\nThat's the new behavior as well. Path limiters are taken to be\nrealtive to the current working directory, and without limiters we\nrefresh everything.\n\n> The patch looks fine otherwise.\n\nThanks for the review.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"81631","messageId":"b0943d9e0806290407h5eecd27bg510dd09e0188abca@mail.gmail.com","threadId":"14141","inReplyTo":"20080629102147.GA5098@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 1/2] Convert \"stg refresh\" to the new infrastructure","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-06-29T11:07:42Z","receivedAt":"2008-06-29T11:07:42Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/6/29 Karl Hasselström <kha@treskal.com>:\n> On 2008-06-29 10:42:32 +0100, Catalin Marinas wrote:\n>\n>> 2008/6/25 Karl Hasselström <kha@treskal.com>:\n>>\n>> > And in the process, make it more powerful: it will now first\n>> > create a temp patch containing the updates, and then try to merge\n>> > it into the patch to be updated. If that patch is applied, this is\n>> > done by popping, pushing, and coalescing; if it is unapplied, it\n>> > is done with an in-index merge.\n>>\n>> Does it make sense to refresh an unapplied patch? Maybe adding a new\n>> file to the patch but I don't really see a need for this.\n>\n> A change in a different part of the same file should work as well, I\n> believe.\n>\n> But no, I don't have a strong sense that this is super useful. It was\n> just easy to allow, so I allowed it.\n\nIt seems harmless, unless someone finds some unusual behaviour. What\nis the conflict behaviour? Is the refresh aborted? For unapplied\npatches, it is more complicated to let the user solve the conflict.\n\n-- \nCatalin\n"},{"id":"81639","messageId":"20080629134654.GB5098@diana.vm.bytemark.co.uk","threadId":"14141","inReplyTo":"b0943d9e0806290407h5eecd27bg510dd09e0188abca@mail.gmail.com","subject":"Re: [StGit PATCH 1/2] Convert \"stg refresh\" to the new infrastructure","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-29T13:46:54Z","receivedAt":"2008-06-29T13:46:54Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-29 12:07:42 +0100, Catalin Marinas wrote:\n\n> 2008/6/29 Karl Hasselström <kha@treskal.com>:\n>\n> > On 2008-06-29 10:42:32 +0100, Catalin Marinas wrote:\n> >\n> > > Does it make sense to refresh an unapplied patch? Maybe adding a\n> > > new file to the patch but I don't really see a need for this.\n> >\n> > But no, I don't have a strong sense that this is super useful. It\n> > was just easy to allow, so I allowed it.\n>\n> It seems harmless, unless someone finds some unusual behaviour. What\n> is the conflict behaviour? Is the refresh aborted? For unapplied\n> patches, it is more complicated to let the user solve the conflict.\n\nFirst, the refresh is stored in a temp patch. Then, that patch is\npopped. Then we try to coalesce the temp patch and the target patch;\nif this fails due to conflicts that we can't represent because we have\nno index and worktree to work with for unapplied patches, we just\nleave the temp patch in the stack for the user to deal with. (E.g. by\ntrying to apply the two patches and sort out the conflicts, by keeping\nthe temp patch separate, or by coalescing it with a different patch.)\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"}]}