hgext/record.py
changeset 24272 26a1c617e047
parent 24271 18792f2e38bb
child 24278 0b94b68aace9
equal deleted inserted replaced
24271:18792f2e38bb 24272:26a1c617e047
     6 # GNU General Public License version 2 or any later version.
     6 # GNU General Public License version 2 or any later version.
     7 
     7 
     8 '''commands to interactively select changes for commit/qrefresh'''
     8 '''commands to interactively select changes for commit/qrefresh'''
     9 
     9 
    10 from mercurial.i18n import _
    10 from mercurial.i18n import _
    11 from mercurial import cmdutil, commands, extensions, patch
    11 from mercurial import cmdutil, commands, extensions
    12 from mercurial import util
    12 from mercurial import util
    13 from mercurial import merge as mergemod
       
    14 import cStringIO, errno, os, shutil, tempfile
       
    15 
    13 
    16 cmdtable = {}
    14 cmdtable = {}
    17 command = cmdutil.command(cmdtable)
    15 command = cmdutil.command(cmdtable)
    18 testedwith = 'internal'
    16 testedwith = 'internal'
    19 
    17 
    48 
    46 
    49       ? - display help
    47       ? - display help
    50 
    48 
    51     This command is not available when committing a merge.'''
    49     This command is not available when committing a merge.'''
    52 
    50 
    53     dorecord(ui, repo, commands.commit, 'commit', False, *pats, **opts)
    51     cmdutil.dorecord(ui, repo, commands.commit, 'commit', False, *pats, **opts)
    54 
    52 
    55 def qrefresh(origfn, ui, repo, *pats, **opts):
    53 def qrefresh(origfn, ui, repo, *pats, **opts):
    56     if not opts['interactive']:
    54     if not opts['interactive']:
    57         return origfn(ui, repo, *pats, **opts)
    55         return origfn(ui, repo, *pats, **opts)
    58 
    56 
    64         # We can't pass *pats here since qrefresh will undo all other
    62         # We can't pass *pats here since qrefresh will undo all other
    65         # changed files in the patch that aren't in pats.
    63         # changed files in the patch that aren't in pats.
    66         mq.refresh(ui, repo, **opts)
    64         mq.refresh(ui, repo, **opts)
    67 
    65 
    68     # backup all changed files
    66     # backup all changed files
    69     dorecord(ui, repo, committomq, 'qrefresh', True, *pats, **opts)
    67     cmdutil.dorecord(ui, repo, committomq, 'qrefresh', True, *pats, **opts)
    70 
    68 
    71 # This command registration is replaced during uisetup().
    69 # This command registration is replaced during uisetup().
    72 @command('qrecord',
    70 @command('qrecord',
    73     [],
    71     [],
    74     _('hg qrecord [OPTION]... PATCH [FILE]...'),
    72     _('hg qrecord [OPTION]... PATCH [FILE]...'),
    89 
    87 
    90     def committomq(ui, repo, *pats, **opts):
    88     def committomq(ui, repo, *pats, **opts):
    91         opts['checkname'] = False
    89         opts['checkname'] = False
    92         mq.new(ui, repo, patch, *pats, **opts)
    90         mq.new(ui, repo, patch, *pats, **opts)
    93 
    91 
    94     dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts)
    92     cmdutil.dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts)
    95 
    93 
    96 def qnew(origfn, ui, repo, patch, *args, **opts):
    94 def qnew(origfn, ui, repo, patch, *args, **opts):
    97     if opts['interactive']:
    95     if opts['interactive']:
    98         return qrecord(ui, repo, patch, *args, **opts)
    96         return qrecord(ui, repo, patch, *args, **opts)
    99     return origfn(ui, repo, patch, *args, **opts)
    97     return origfn(ui, repo, patch, *args, **opts)
   100 
    98 
   101 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts):
       
   102     if not ui.interactive():
       
   103         raise util.Abort(_('running non-interactively, use %s instead') %
       
   104                          cmdsuggest)
       
   105 
       
   106     # make sure username is set before going interactive
       
   107     if not opts.get('user'):
       
   108         ui.username() # raise exception, username not provided
       
   109 
       
   110     def recordfunc(ui, repo, message, match, opts):
       
   111         """This is generic record driver.
       
   112 
       
   113         Its job is to interactively filter local changes, and
       
   114         accordingly prepare working directory into a state in which the
       
   115         job can be delegated to a non-interactive commit command such as
       
   116         'commit' or 'qrefresh'.
       
   117 
       
   118         After the actual job is done by non-interactive command, the
       
   119         working directory is restored to its original state.
       
   120 
       
   121         In the end we'll record interesting changes, and everything else
       
   122         will be left in place, so the user can continue working.
       
   123         """
       
   124 
       
   125         cmdutil.checkunfinished(repo, commit=True)
       
   126         merge = len(repo[None].parents()) > 1
       
   127         if merge:
       
   128             raise util.Abort(_('cannot partially commit a merge '
       
   129                                '(use "hg commit" instead)'))
       
   130 
       
   131         status = repo.status(match=match)
       
   132         diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
       
   133         diffopts.nodates = True
       
   134         diffopts.git = True
       
   135         originalchunks = patch.diff(repo, changes=status, opts=diffopts)
       
   136         fp = cStringIO.StringIO()
       
   137         fp.write(''.join(originalchunks))
       
   138         fp.seek(0)
       
   139 
       
   140         # 1. filter patch, so we have intending-to apply subset of it
       
   141         try:
       
   142             chunks = patch.filterpatch(ui, patch.parsepatch(fp))
       
   143         except patch.PatchError, err:
       
   144             raise util.Abort(_('error parsing patch: %s') % err)
       
   145 
       
   146         del fp
       
   147 
       
   148         contenders = set()
       
   149         for h in chunks:
       
   150             try:
       
   151                 contenders.update(set(h.files()))
       
   152             except AttributeError:
       
   153                 pass
       
   154 
       
   155         changed = status.modified + status.added + status.removed
       
   156         newfiles = [f for f in changed if f in contenders]
       
   157         if not newfiles:
       
   158             ui.status(_('no changes to record\n'))
       
   159             return 0
       
   160 
       
   161         newandmodifiedfiles = set()
       
   162         for h in chunks:
       
   163             ishunk = isinstance(h, patch.recordhunk)
       
   164             isnew = h.filename() in status.added
       
   165             if ishunk and isnew and not h in originalchunks:
       
   166                 newandmodifiedfiles.add(h.filename())
       
   167 
       
   168         modified = set(status.modified)
       
   169 
       
   170         # 2. backup changed files, so we can restore them in the end
       
   171 
       
   172         if backupall:
       
   173             tobackup = changed
       
   174         else:
       
   175             tobackup = [f for f in newfiles
       
   176                         if f in modified or f in newandmodifiedfiles]
       
   177 
       
   178         backups = {}
       
   179         if tobackup:
       
   180             backupdir = repo.join('record-backups')
       
   181             try:
       
   182                 os.mkdir(backupdir)
       
   183             except OSError, err:
       
   184                 if err.errno != errno.EEXIST:
       
   185                     raise
       
   186         try:
       
   187             # backup continues
       
   188             for f in tobackup:
       
   189                 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
       
   190                                                dir=backupdir)
       
   191                 os.close(fd)
       
   192                 ui.debug('backup %r as %r\n' % (f, tmpname))
       
   193                 util.copyfile(repo.wjoin(f), tmpname)
       
   194                 shutil.copystat(repo.wjoin(f), tmpname)
       
   195                 backups[f] = tmpname
       
   196 
       
   197             fp = cStringIO.StringIO()
       
   198             for c in chunks:
       
   199                 fname = c.filename()
       
   200                 if fname in backups or fname in newandmodifiedfiles:
       
   201                     c.write(fp)
       
   202             dopatch = fp.tell()
       
   203             fp.seek(0)
       
   204 
       
   205             [os.unlink(c) for c in newandmodifiedfiles]
       
   206 
       
   207             # 3a. apply filtered patch to clean repo  (clean)
       
   208             if backups:
       
   209                 # Equivalent to hg.revert
       
   210                 choices = lambda key: key in backups
       
   211                 mergemod.update(repo, repo.dirstate.p1(),
       
   212                         False, True, choices)
       
   213 
       
   214             # 3b. (apply)
       
   215             if dopatch:
       
   216                 try:
       
   217                     ui.debug('applying patch\n')
       
   218                     ui.debug(fp.getvalue())
       
   219                     patch.internalpatch(ui, repo, fp, 1, eolmode=None)
       
   220                 except patch.PatchError, err:
       
   221                     raise util.Abort(str(err))
       
   222             del fp
       
   223 
       
   224             # 4. We prepared working directory according to filtered
       
   225             #    patch. Now is the time to delegate the job to
       
   226             #    commit/qrefresh or the like!
       
   227 
       
   228             # Make all of the pathnames absolute.
       
   229             newfiles = [repo.wjoin(nf) for nf in newfiles]
       
   230             commitfunc(ui, repo, *newfiles, **opts)
       
   231 
       
   232             return 0
       
   233         finally:
       
   234             # 5. finally restore backed-up files
       
   235             try:
       
   236                 for realname, tmpname in backups.iteritems():
       
   237                     ui.debug('restoring %r to %r\n' % (tmpname, realname))
       
   238                     util.copyfile(tmpname, repo.wjoin(realname))
       
   239                     # Our calls to copystat() here and above are a
       
   240                     # hack to trick any editors that have f open that
       
   241                     # we haven't modified them.
       
   242                     #
       
   243                     # Also note that this racy as an editor could
       
   244                     # notice the file's mtime before we've finished
       
   245                     # writing it.
       
   246                     shutil.copystat(tmpname, repo.wjoin(realname))
       
   247                     os.unlink(tmpname)
       
   248                 if tobackup:
       
   249                     os.rmdir(backupdir)
       
   250             except OSError:
       
   251                 pass
       
   252 
       
   253     # wrap ui.write so diff output can be labeled/colorized
       
   254     def wrapwrite(orig, *args, **kw):
       
   255         label = kw.pop('label', '')
       
   256         for chunk, l in patch.difflabel(lambda: args):
       
   257             orig(chunk, label=label + l)
       
   258     oldwrite = ui.write
       
   259 
       
   260     def wrap(*args, **kwargs):
       
   261         return wrapwrite(oldwrite, *args, **kwargs)
       
   262     setattr(ui, 'write', wrap)
       
   263 
       
   264     try:
       
   265         return cmdutil.commit(ui, repo, recordfunc, pats, opts)
       
   266     finally:
       
   267         ui.write = oldwrite
       
   268 
    99 
   269 def uisetup(ui):
   100 def uisetup(ui):
   270     try:
   101     try:
   271         mq = extensions.find('mq')
   102         mq = extensions.find('mq')
   272     except KeyError:
   103     except KeyError: