Commits

bbarry  committed 37434f5

added interactive shelving via hg shelve --interactive

  • Participants
  • Parent commits 4249952

Comments (0)

Files changed (4)

 from mercurial.node import bin, hex, short
 from mercurial.repo import RepoError
 from mercurial import commands, cmdutil, hg, patch, revlog, util
-from mercurial import extensions, repair
-import os, sys, re, errno
+from mercurial import extensions, repair, fancyopts
+import copy, cStringIO, os, sys, re, errno, tempfile
 
 normname = util.normpath
 
         repo.ui.write(_('user: %s\ndate: %s\nmessage: %s\n') %
                       (user, date, message))
 
+def refilterpatch(allchunk, selected):
+    """return chunks not in selected"""
+    try:
+        record = extensions.find('record')
+    except KeyError:
+        raise util.Abort(_("'record' extension not loaded"))
+    l = []
+    fil = []
+    for c in allchunk:
+        if isinstance(c, record.header):
+            if len(l) > 1 and l[0] in selected:
+                fil += l
+            l = [c]
+        elif c not in selected:
+            l.append(c)
+    if len(l) > 1 and l[0] in selected:
+        fil += l
+    return fil
+
+def makebackup(ui, repo, dir, files):
+    """make a backup for the files pointed to in the files parameter"""
+    try:
+        os.mkdir(dir)
+    except OSError, err:
+        if err.errno != errno.EEXIST:
+            raise
+
+    backups = {}
+    for f in files:
+        fd, tmpname = tempfile.mkstemp(prefix = f.replace('/', '_')+'.',
+                                       dir = dir)
+        os.close(fd)
+        ui.debug('backup %r as %r\n' % (f, tmpname))
+        util.copyfile(repo.wjoin(f), tmpname)
+        backups[f] = tmpname
+
+    return backups
+
+def interactiveshelve(ui, repo, name, pats, opts):
+    """interactively select changes to set aside"""
+    if not ui.interactive:
+        raise util.Abort(_('shelve --interactive can only be run interactively'))
+    try:
+        record = extensions.find('record')
+    except KeyError:
+        raise util.Abort(_("'record' extension not loaded"))
+
+    def shelvefunc(ui, repo, message, match, opts):
+        files = []
+        if match.files():
+            changes = None
+        else:
+            changes = repo.status(match = match)[:3]
+            modified, added, removed = changes
+            files = modified + added + removed
+            match = cmdutil.matchfiles(repo, files)
+        diffopts = repo.attic.diffopts( {'git':True, 'nodates':True})
+        chunks = patch.diff(repo, repo.dirstate.parents()[0], match = match,
+                            changes = changes, opts = diffopts)
+        fp = cStringIO.StringIO()
+        fp.write(''.join(chunks))
+        fp.seek(0)
+
+        # 1. filter patch, so we have intending-to apply subset of it
+        ac = record.parsepatch(fp)
+        chunks = record.filterpatch(ui, ac)
+        # and a not-intending-to apply subset of it
+        rc = refilterpatch(ac, chunks)
+        del fp
+        
+        contenders = {}
+        for h in chunks:
+            try: contenders.update(dict.fromkeys(h.files()))
+            except AttributeError: pass
+
+        newfiles = [f for f in files if f in contenders]
+
+        if not newfiles:
+            ui.status(_('no changes to shelve\n'))
+            return 0
+
+        modified = dict.fromkeys(changes[0])
+        backups = {}
+        backupdir = repo.join('shelve-backups')
+
+        try:
+            bkfiles = [f for f in newfiles if f in modified]
+            backups = makebackup(ui, repo, backupdir, bkfiles)
+            
+            # patch to shelve
+            sp = cStringIO.StringIO()
+            for c in chunks:
+                if c.filename() in backups:
+                    c.write(sp)
+            doshelve = sp.tell()
+            sp.seek(0)
+
+            # patch to apply to shelved files
+            fp = cStringIO.StringIO()
+            for c in rc:
+                if c.filename() in backups:
+                    c.write(fp)
+            dopatch = fp.tell()
+            fp.seek(0)
+
+            try:
+                # 3a. apply filtered patch to clean repo (clean)
+                if backups:
+                    hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
+
+                # 3b. apply filtered patch to clean repo (apply)
+                if dopatch:
+                    ui.debug('applying patch\n')
+                    ui.debug(fp.getvalue())
+                    patch.internalpatch(fp, ui, 1, repo.root)
+                del fp
+
+                # 3c. apply filtered patch to clean repo (shelve)
+                if doshelve:
+                    ui.debug("saving patch to %s\n" % (name))
+                    s = repo.attic
+                    f = s.opener(name, 'w')
+                    f.write(sp.getvalue())
+                    del f
+                    s.currentpatch = name
+                    s.persiststate()
+                del sp
+            except:
+                try:
+                    for realname, tmpname in backups.iteritems():
+                        ui.debug('restoring %r to %r\n' % (tmpname, realname))
+                        util.copyfile(tmpname, repo.wjoin(realname))
+                except OSError:
+                    pass
+
+            return 0
+        finally:
+            try:
+                for realname, tmpname in backups.iteritems():
+                    ui.debug('removing backup for %r : %r\n' % (realname, tmpname))
+                    os.unlink(tmpname)
+                os.rmdir(backupdir)
+            except OSError:
+                pass
+    fancyopts.fancyopts([], commands.commitopts, opts)
+    return cmdutil.commit(ui, repo, shelvefunc, pats, opts)
+
 def shelve(ui, repo, name = None, *pats, **opts):
     '''saves a patch to the attic from the current changes
     and removes them from the working copy'''
         currentinfo(ui, repo)
     else:
         s = repo.attic
-        makepatch(ui, repo, name, pats, opts)
-        if opts['refresh']:
-            if name:
-                s.applied = name
-                s.persiststate()
-            repo.ui.status(_('Patch %s refreshed\n') % (s.applied))
+        if opts['interactive']:
+            interactiveshelve(ui, repo, name, pats, opts)
+            repo.ui.status(_('Patch %s shelved\n' % (s.currentpatch)))
         else:
-            s.cleanup(repo)
-            repo.ui.status(_('Patch %s shelved\n' % (s.currentpatch)))
+            makepatch(ui, repo, name, pats, opts)
+            if opts['refresh']:
+                if name:
+                    s.applied = name
+                    s.persiststate()
+                repo.ui.status(_('Patch %s refreshed\n') % (s.applied))
+            else:
+                s.cleanup(repo)
+                repo.ui.status(_('Patch %s shelved\n' % (s.currentpatch)))
 
 def unshelve(ui, repo, name = None, **opts):
     """activates a patch from the attic"""
         raise util.Abort(_('similarity must be a number'))
     if sim < 0 or sim > 100:
         raise util.Abort(_('similarity must be between 0 and 100'))
-    s.apply(repo, name, sim, opts)
+    s.apply(repo, name, sim, **opts)
     s.persiststate()
     repo.ui.status(_('Patch %s unshelved\n') % (s.applied))
     if opts['delete']:
             ('g', 'git', None, _('use git extended diff format')),
             ('r', 'refresh', None, 
                 _('refresh the current patch without stowing it away')),
+            ('i', 'interactive', None, 
+                _('use the \'record\' extension ' + 
+                  'to create a patch interactively')),
             ('c', 'current', None, 
                 _('show information about the current shelf being worked on'))
             ] + commands.walkopts + commands.commitopts + headeropts,

File tests/ignore-manual_test-shelve-interactive.bat

+@echo off
+
+rem setup hgattic in a temp test directory
+setlocal
+cd ..
+cd > tmpfile
+set /p atticdir= < tmpfile
+erase tmpfile
+cd tests
+mkdir test
+cd test
+call hg init
+cd .hg
+echo [extensions] >tmp
+echo hgattic=%atticdir%\attic.py >>tmp
+move tmp hgrc
+cd ..
+
+rem replace here with test content
+echo ### echo a ^> a.txt
+echo a > a.txt
+echo ### echo b ^> b.txt
+echo b > b.txt
+echo ### call hg addrem
+call hg addrem
+call hg ci -m "commit"
+echo ### echo a ^>^> a.txt
+echo a >> a.txt
+echo ### echo b ^>^> b.txt
+echo b >> b.txt
+echo ### call hg st
+call hg st
+echo ### call hg shelve --interactive --git a
+call hg shelve --interactive --git a
+echo ### echo hgext.record= ^>^>.hg\hgrc
+echo hgext.record= >>.hg\hgrc
+echo ### call hg shelve --interactive --git a
+echo ### answer f then s
+call hg shelve --interactive --git a
+echo ### call hg st
+call hg st
+echo ### files in attic
+for %%f in (.hg\attic\*.*) do echo %%f
+echo ### call hg unshelve (should fail)
+call hg unshelve
+echo ### call hg shelve b
+call hg shelve b
+echo ### call hg unshelve a
+call hg unshelve a
+echo ### call hg st
+call hg st
+rem end test content
+
+rem cleanup
+cd ..
+rmdir /S /Q test
+endlocal

File tests/test-unshelve.bat

 for %%f in (.hg\attic\*.*) do echo %%f
 echo ### call hg unshelve --delete b
 call hg unshelve --delete b
-echo ### call hg unshelve --delete a
+echo ### call hg unshelve --delete a (should fail)
 call hg unshelve --delete a
+echo ### call hg unshelve --delete -f a
+call hg unshelve --delete -f a
 echo ### files in attic
 for %%f in (.hg\attic\*.*) do echo %%f
 rem end test content

File tests/test-unshelve.out

- adding a.txt
-adding b.txt
+ adding a.txt
+adding b.txt
 ### hg st
 ### hg unshelve
-Patch b unshelved
+Patch b unshelved
 ### hg st
-A b.txt
+A b.txt
 ### hg unshelve a
-Patch a unshelved
+Patch a unshelved
 ### hg st
-A a.txt
+A a.txt
 ### hg unshelve b (should fail)
-abort: cannot apply a patch over an already active patch
+abort: cannot apply a patch over an already active patch
 ### hg unshelve -f b (should pass)
-Patch b unshelved
+Patch b unshelved
 ### hg st
-A a.txt
-A b.txt
+A a.txt
+A b.txt
 ### current applied patch
 b### hg shelve c (should fail)
-abort: a different patch is active
+abort: a different patch is active
 ### hg shelve -f c (should pass)
-Patch c shelved
+Patch c shelved
 ### hg st
 ### contents of c
-diff --git a/a.txt b/a.txt
-new file mode 100644
---- /dev/null
-+++ b/a.txt
-@@ -0,0 +1,1 @@
+diff --git a/a.txt b/a.txt
+new file mode 100644
+--- /dev/null
++++ b/a.txt
+@@ -0,0 +1,1 @@
 +a 
-diff --git a/b.txt b/b.txt
-new file mode 100644
---- /dev/null
-+++ b/b.txt
-@@ -0,0 +1,1 @@
+diff --git a/b.txt b/b.txt
+new file mode 100644
+--- /dev/null
++++ b/b.txt
+@@ -0,0 +1,1 @@
 +b 
 ### files in attic
 .hg\attic\.applied
 .hg\attic\b
 .hg\attic\c
 ### call hg unshelve --delete b
-Patch b unshelved
-patch removed
-### call hg unshelve --delete a
-Patch a unshelved
-patch removed
+Patch b unshelved
+patch removed
+### call hg unshelve --delete a (should fail)
+abort: local changes found
+### call hg unshelve --delete -f a
+Patch a unshelved
+patch removed
 ### files in attic
 .hg\attic\.applied
 .hg\attic\.current