mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 09:17:30 +03:00
200 lines
7.9 KiB
Python
200 lines
7.9 KiB
Python
# crecord.py
|
|
#
|
|
# Copyright 2008 Mark Edgington <edgimar@gmail.com>
|
|
#
|
|
# This software may be used and distributed according to the terms of
|
|
# the GNU General Public License, incorporated herein by reference.
|
|
#
|
|
# Much of this extension is based on Bryan O'Sullivan's record extension.
|
|
|
|
'''text-gui based change selection during commit or qrefresh'''
|
|
from mercurial.i18n import _
|
|
from mercurial import cmdutil, hg, mdiff, patch
|
|
from mercurial import util
|
|
import cStringIO
|
|
import errno
|
|
import os
|
|
import tempfile
|
|
|
|
import crpatch
|
|
import chunk_selector
|
|
|
|
def dorecord(ui, repo, commitfunc, *pats, **opts):
|
|
try:
|
|
if not ui.interactive():
|
|
raise util.Abort(_('running non-interactively, use commit instead'))
|
|
except TypeError: # backwards compatibility with hg 1.1
|
|
if not ui.interactive:
|
|
raise util.Abort(_('running non-interactively, use commit instead'))
|
|
|
|
def recordfunc(ui, repo, message, match, opts):
|
|
"""This is generic record driver.
|
|
|
|
Its job is to interactively filter local changes, and accordingly
|
|
prepare working dir into a state, where the job can be delegated to
|
|
non-interactive commit command such as 'commit' or 'qrefresh'.
|
|
|
|
After the actual job is done by non-interactive command, working dir
|
|
state is restored to original.
|
|
|
|
In the end we'll record interesting changes, and everything else will be
|
|
left in place, so the user can continue his work.
|
|
"""
|
|
|
|
merge = len(repo[None].parents()) > 1
|
|
if merge:
|
|
raise util.Abort(_('cannot partially commit a merge '
|
|
'(use hg commit instead)'))
|
|
|
|
# status gives back
|
|
# modified, added, removed, deleted, unknown, ignored, clean
|
|
# we take only the first 3 of these
|
|
changes = repo.status(match=match)[:3]
|
|
modified, added, removed = changes
|
|
try:
|
|
# Mercurial >= 3.3 allow disabling format-changing diffopts
|
|
diffopts = patch.difffeatureopts(ui, opts=opts, section='crecord',
|
|
whitespace=True)
|
|
except AttributeError:
|
|
diffopts = patch.diffopts(ui, opts=opts, section='crecord')
|
|
diffopts.nodates = True
|
|
diffopts.git = True
|
|
chunks = patch.diff(repo, 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
|
|
chunks = crpatch.filterpatch(opts,
|
|
crpatch.parsepatch(changes, fp),
|
|
chunk_selector.chunkselector, ui)
|
|
del fp
|
|
|
|
contenders = set()
|
|
for h in chunks:
|
|
try:
|
|
contenders.update(set(h.files()))
|
|
except AttributeError:
|
|
pass
|
|
|
|
changed = changes[0] + changes[1] + changes[2]
|
|
newfiles = [f for f in changed if f in contenders]
|
|
|
|
if not newfiles:
|
|
ui.status(_('no changes to record\n'))
|
|
return 0
|
|
|
|
|
|
# 2. backup changed files, so we can restore them in the end
|
|
backups = {}
|
|
newly_added_backups = {}
|
|
backupdir = repo.join('record-backups')
|
|
try:
|
|
os.mkdir(backupdir)
|
|
except OSError, err:
|
|
if err.errno != errno.EEXIST:
|
|
raise
|
|
try:
|
|
# backup continues
|
|
for f in newfiles:
|
|
if f not in (modified + added):
|
|
continue
|
|
fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
|
|
dir=backupdir)
|
|
os.close(fd)
|
|
ui.debug('backup %r as %r\n' % (f, tmpname))
|
|
util.copyfile(repo.wjoin(f), tmpname)
|
|
if f in modified:
|
|
backups[f] = tmpname
|
|
elif f in added:
|
|
newly_added_backups[f] = tmpname
|
|
|
|
fp = cStringIO.StringIO()
|
|
all_backups = {}
|
|
all_backups.update(backups)
|
|
all_backups.update(newly_added_backups)
|
|
for c in chunks:
|
|
if c.filename() in all_backups:
|
|
c.write(fp)
|
|
dopatch = fp.tell()
|
|
fp.seek(0)
|
|
|
|
# 2.5 optionally review / modify patch in text editor
|
|
if opts['crecord_reviewpatch']:
|
|
patchtext = fp.read()
|
|
reviewedpatch = ui.edit(patchtext, "")
|
|
fp.truncate(0)
|
|
fp.write(reviewedpatch)
|
|
fp.seek(0)
|
|
|
|
# 3a. apply filtered patch to clean repo (clean)
|
|
if backups:
|
|
hg.revert(repo, repo.dirstate.parents()[0],
|
|
lambda key: key in backups)
|
|
# remove newly added files from 'clean' repo (so patch can apply)
|
|
for f in newly_added_backups:
|
|
os.unlink(f)
|
|
|
|
# 3b. (apply)
|
|
if dopatch:
|
|
try:
|
|
ui.debug('applying patch\n')
|
|
ui.debug(fp.getvalue())
|
|
if hasattr(patch, 'workingbackend'): # detect 1.9
|
|
patch.internalpatch(ui, repo, fp, strip=1, eolmode=None)
|
|
else:
|
|
pfiles = {}
|
|
try:
|
|
patch.internalpatch(ui, repo, fp, 1, eolmode=None)
|
|
except (TypeError, AttributeError): # pre 17cea10c343e
|
|
try:
|
|
patch.internalpatch(ui, repo, fp, 1, repo.root,
|
|
eolmode=None)
|
|
except (TypeError, AttributeError): # pre 00a881581400
|
|
try:
|
|
patch.internalpatch(fp, ui, 1, repo.root,
|
|
files=pfiles, eolmode=None)
|
|
except TypeError: # backwards compatible with hg 1.1
|
|
patch.internalpatch(fp, ui, 1,
|
|
repo.root, files=pfiles)
|
|
try:
|
|
cmdutil.updatedir(ui, repo, pfiles)
|
|
except AttributeError:
|
|
try:
|
|
patch.updatedir(ui, repo, pfiles)
|
|
except AttributeError:
|
|
# from 00a881581400 onwards
|
|
pass
|
|
except patch.PatchError, err:
|
|
s = str(err)
|
|
if s:
|
|
raise util.Abort(s)
|
|
else:
|
|
raise util.Abort(_('patch failed to apply'))
|
|
del fp
|
|
|
|
# 4. We prepared working directory according to filtered patch.
|
|
# Now is the time to delegate the job to commit/qrefresh or the like!
|
|
|
|
# it is important to first chdir to repo root -- we'll call a
|
|
# highlevel command with list of pathnames relative to repo root
|
|
newfiles = [repo.wjoin(n) for n in newfiles]
|
|
commitfunc(ui, repo, *newfiles, **opts)
|
|
|
|
return 0
|
|
finally:
|
|
# 5. finally restore backed-up files
|
|
try:
|
|
for realname, tmpname in backups.iteritems():
|
|
ui.debug('restoring %r to %r\n' % (tmpname, realname))
|
|
util.copyfile(tmpname, repo.wjoin(realname))
|
|
os.unlink(tmpname)
|
|
for realname, tmpname in newly_added_backups.iteritems():
|
|
ui.debug('restoring %r to %r\n' % (tmpname, realname))
|
|
util.copyfile(tmpname, repo.wjoin(realname))
|
|
os.unlink(tmpname)
|
|
os.rmdir(backupdir)
|
|
except OSError:
|
|
pass
|
|
return cmdutil.commit(ui, repo, recordfunc, pats, opts)
|