mercurial/cmdutil.py
changeset 24272 26a1c617e047
parent 24260 76225ab5a5da
child 24275 e1cb460a3524
equal deleted inserted replaced
24271:18792f2e38bb 24272:26a1c617e047
     5 # This software may be used and distributed according to the terms of the
     5 # This software may be used and distributed according to the terms of the
     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 from node import hex, nullid, nullrev, short
     8 from node import hex, nullid, nullrev, short
     9 from i18n import _
     9 from i18n import _
    10 import os, sys, errno, re, tempfile
    10 import os, sys, errno, re, tempfile, cStringIO, shutil
    11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
    11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
    12 import match as matchmod
    12 import match as matchmod
    13 import context, repair, graphmod, revset, phases, obsolete, pathutil
    13 import context, repair, graphmod, revset, phases, obsolete, pathutil
    14 import changelog
    14 import changelog
    15 import bookmarks
    15 import bookmarks
    16 import encoding
    16 import encoding
    17 import lock as lockmod
    17 import lock as lockmod
    18 
    18 
    19 def parsealiases(cmd):
    19 def parsealiases(cmd):
    20     return cmd.lstrip("^").split("|")
    20     return cmd.lstrip("^").split("|")
       
    21 
       
    22 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts):
       
    23     import merge as mergemod
       
    24     if not ui.interactive():
       
    25         raise util.Abort(_('running non-interactively, use %s instead') %
       
    26                          cmdsuggest)
       
    27 
       
    28     # make sure username is set before going interactive
       
    29     if not opts.get('user'):
       
    30         ui.username() # raise exception, username not provided
       
    31 
       
    32     def recordfunc(ui, repo, message, match, opts):
       
    33         """This is generic record driver.
       
    34 
       
    35         Its job is to interactively filter local changes, and
       
    36         accordingly prepare working directory into a state in which the
       
    37         job can be delegated to a non-interactive commit command such as
       
    38         'commit' or 'qrefresh'.
       
    39 
       
    40         After the actual job is done by non-interactive command, the
       
    41         working directory is restored to its original state.
       
    42 
       
    43         In the end we'll record interesting changes, and everything else
       
    44         will be left in place, so the user can continue working.
       
    45         """
       
    46 
       
    47         checkunfinished(repo, commit=True)
       
    48         merge = len(repo[None].parents()) > 1
       
    49         if merge:
       
    50             raise util.Abort(_('cannot partially commit a merge '
       
    51                                '(use "hg commit" instead)'))
       
    52 
       
    53         status = repo.status(match=match)
       
    54         diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
       
    55         diffopts.nodates = True
       
    56         diffopts.git = True
       
    57         originalchunks = patch.diff(repo, changes=status, opts=diffopts)
       
    58         fp = cStringIO.StringIO()
       
    59         fp.write(''.join(originalchunks))
       
    60         fp.seek(0)
       
    61 
       
    62         # 1. filter patch, so we have intending-to apply subset of it
       
    63         try:
       
    64             chunks = patch.filterpatch(ui, patch.parsepatch(fp))
       
    65         except patch.PatchError, err:
       
    66             raise util.Abort(_('error parsing patch: %s') % err)
       
    67 
       
    68         del fp
       
    69 
       
    70         contenders = set()
       
    71         for h in chunks:
       
    72             try:
       
    73                 contenders.update(set(h.files()))
       
    74             except AttributeError:
       
    75                 pass
       
    76 
       
    77         changed = status.modified + status.added + status.removed
       
    78         newfiles = [f for f in changed if f in contenders]
       
    79         if not newfiles:
       
    80             ui.status(_('no changes to record\n'))
       
    81             return 0
       
    82 
       
    83         newandmodifiedfiles = set()
       
    84         for h in chunks:
       
    85             ishunk = isinstance(h, patch.recordhunk)
       
    86             isnew = h.filename() in status.added
       
    87             if ishunk and isnew and not h in originalchunks:
       
    88                 newandmodifiedfiles.add(h.filename())
       
    89 
       
    90         modified = set(status.modified)
       
    91 
       
    92         # 2. backup changed files, so we can restore them in the end
       
    93 
       
    94         if backupall:
       
    95             tobackup = changed
       
    96         else:
       
    97             tobackup = [f for f in newfiles
       
    98                         if f in modified or f in newandmodifiedfiles]
       
    99 
       
   100         backups = {}
       
   101         if tobackup:
       
   102             backupdir = repo.join('record-backups')
       
   103             try:
       
   104                 os.mkdir(backupdir)
       
   105             except OSError, err:
       
   106                 if err.errno != errno.EEXIST:
       
   107                     raise
       
   108         try:
       
   109             # backup continues
       
   110             for f in tobackup:
       
   111                 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
       
   112                                                dir=backupdir)
       
   113                 os.close(fd)
       
   114                 ui.debug('backup %r as %r\n' % (f, tmpname))
       
   115                 util.copyfile(repo.wjoin(f), tmpname)
       
   116                 shutil.copystat(repo.wjoin(f), tmpname)
       
   117                 backups[f] = tmpname
       
   118 
       
   119             fp = cStringIO.StringIO()
       
   120             for c in chunks:
       
   121                 fname = c.filename()
       
   122                 if fname in backups or fname in newandmodifiedfiles:
       
   123                     c.write(fp)
       
   124             dopatch = fp.tell()
       
   125             fp.seek(0)
       
   126 
       
   127             [os.unlink(c) for c in newandmodifiedfiles]
       
   128 
       
   129             # 3a. apply filtered patch to clean repo  (clean)
       
   130             if backups:
       
   131                 # Equivalent to hg.revert
       
   132                 choices = lambda key: key in backups
       
   133                 mergemod.update(repo, repo.dirstate.p1(),
       
   134                         False, True, choices)
       
   135 
       
   136 
       
   137             # 3b. (apply)
       
   138             if dopatch:
       
   139                 try:
       
   140                     ui.debug('applying patch\n')
       
   141                     ui.debug(fp.getvalue())
       
   142                     patch.internalpatch(ui, repo, fp, 1, eolmode=None)
       
   143                 except patch.PatchError, err:
       
   144                     raise util.Abort(str(err))
       
   145             del fp
       
   146 
       
   147             # 4. We prepared working directory according to filtered
       
   148             #    patch. Now is the time to delegate the job to
       
   149             #    commit/qrefresh or the like!
       
   150 
       
   151             # Make all of the pathnames absolute.
       
   152             newfiles = [repo.wjoin(nf) for nf in newfiles]
       
   153             commitfunc(ui, repo, *newfiles, **opts)
       
   154 
       
   155             return 0
       
   156         finally:
       
   157             # 5. finally restore backed-up files
       
   158             try:
       
   159                 for realname, tmpname in backups.iteritems():
       
   160                     ui.debug('restoring %r to %r\n' % (tmpname, realname))
       
   161                     util.copyfile(tmpname, repo.wjoin(realname))
       
   162                     # Our calls to copystat() here and above are a
       
   163                     # hack to trick any editors that have f open that
       
   164                     # we haven't modified them.
       
   165                     #
       
   166                     # Also note that this racy as an editor could
       
   167                     # notice the file's mtime before we've finished
       
   168                     # writing it.
       
   169                     shutil.copystat(tmpname, repo.wjoin(realname))
       
   170                     os.unlink(tmpname)
       
   171                 if tobackup:
       
   172                     os.rmdir(backupdir)
       
   173             except OSError:
       
   174                 pass
       
   175 
       
   176     # wrap ui.write so diff output can be labeled/colorized
       
   177     def wrapwrite(orig, *args, **kw):
       
   178         label = kw.pop('label', '')
       
   179         for chunk, l in patch.difflabel(lambda: args):
       
   180             orig(chunk, label=label + l)
       
   181 
       
   182     oldwrite = ui.write
       
   183     def wrap(*args, **kwargs):
       
   184         return wrapwrite(oldwrite, *args, **kwargs)
       
   185     setattr(ui, 'write', wrap)
       
   186 
       
   187     try:
       
   188         return commit(ui, repo, recordfunc, pats, opts)
       
   189     finally:
       
   190         ui.write = oldwrite
       
   191 
    21 
   192 
    22 def findpossible(cmd, table, strict=False):
   193 def findpossible(cmd, table, strict=False):
    23     """
   194     """
    24     Return cmd -> (aliases, command table entry)
   195     Return cmd -> (aliases, command table entry)
    25     for each matching command.
   196     for each matching command.