##// END OF EJS Templates
two fixes for the pager stuff...
two fixes for the pager stuff - open pipe in binary mode - don't start a pager if stdout is not a tty (this prevents test-ssh from hanging if you set ui.usepager=True in run-tests.py)

File last commit:

r6212:e75aab65 default
r6303:577a7da1 default
Show More
record.py
527 lines | 16.6 KiB | text/x-python | PythonLexer
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 # record.py
#
# Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
#
# This software may be used and distributed according to the terms of
# the GNU General Public License, incorporated herein by reference.
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830 '''interactive change selection during commit or qrefresh'''
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037
from mercurial.i18n import _
Joel Rosdahl
Remove unused imports
r6212 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 from mercurial import util
Joel Rosdahl
Remove unused imports
r6212 import copy, cStringIO, errno, operator, os, re, tempfile
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037
lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
def scanpatch(fp):
Kirill Smelkov
record: some docs...
r5826 """like patch.iterhunks, but yield different events
- ('file', [header_lines + fromfile + tofile])
- ('context', [context_lines])
- ('hunk', [hunk_lines])
- ('range', (-start,len, +start,len, diffp))
"""
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 lr = patch.linereader(fp)
def scanwhile(first, p):
Kirill Smelkov
record: some docs...
r5826 """scan lr while predicate holds"""
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 lines = [first]
while True:
line = lr.readline()
if not line:
break
if p(line):
lines.append(line)
else:
lr.push(line)
break
return lines
while True:
line = lr.readline()
if not line:
break
if line.startswith('diff --git a/'):
def notheader(line):
s = line.split(None, 1)
return not s or s[0] not in ('---', 'diff')
header = scanwhile(line, notheader)
fromfile = lr.readline()
if fromfile.startswith('---'):
tofile = lr.readline()
header += [fromfile, tofile]
else:
lr.push(fromfile)
yield 'file', header
elif line[0] == ' ':
yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
elif line[0] in '-+':
yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
else:
m = lines_re.match(line)
if m:
yield 'range', m.groups()
else:
raise patch.PatchError('unknown patch content: %r' % line)
class header(object):
Kirill Smelkov
record: some docs...
r5826 """patch header
Thomas Arendsen Hein
Removed trailing spaces from everything except test output
r6210
XXX shoudn't we move this to mercurial/patch.py ?
Kirill Smelkov
record: some docs...
r5826 """
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
allhunks_re = re.compile('(?:index|new file|deleted file) ')
pretty_re = re.compile('(?:new file|deleted file) ')
special_re = re.compile('(?:index|new|deleted|copy|rename) ')
def __init__(self, header):
self.header = header
self.hunks = []
def binary(self):
for h in self.header:
if h.startswith('index '):
return True
Thomas Arendsen Hein
Remove trailing spaces, fix indentation
r5143
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 def pretty(self, fp):
for h in self.header:
if h.startswith('index '):
fp.write(_('this modifies a binary file (all or nothing)\n'))
break
if self.pretty_re.match(h):
fp.write(h)
if self.binary():
fp.write(_('this is a binary file\n'))
break
if h.startswith('---'):
fp.write(_('%d hunks, %d lines changed\n') %
(len(self.hunks),
sum([h.added + h.removed for h in self.hunks])))
break
fp.write(h)
def write(self, fp):
fp.write(''.join(self.header))
def allhunks(self):
for h in self.header:
if self.allhunks_re.match(h):
return True
def files(self):
fromfile, tofile = self.diff_re.match(self.header[0]).groups()
if fromfile == tofile:
return [fromfile]
return [fromfile, tofile]
def filename(self):
return self.files()[-1]
def __repr__(self):
return '<header %s>' % (' '.join(map(repr, self.files())))
def special(self):
for h in self.header:
if self.special_re.match(h):
return True
def countchanges(hunk):
Kirill Smelkov
record: some docs...
r5826 """hunk -> (n+,n-)"""
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 add = len([h for h in hunk if h[0] == '+'])
rem = len([h for h in hunk if h[0] == '-'])
return add, rem
class hunk(object):
Kirill Smelkov
record: some docs...
r5826 """patch hunk
Thomas Arendsen Hein
Removed trailing spaces from everything except test output
r6210
Kirill Smelkov
record: some docs...
r5826 XXX shouldn't we merge this with patch.hunk ?
"""
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 maxcontext = 3
def __init__(self, header, fromline, toline, proc, before, hunk, after):
def trimcontext(number, lines):
delta = len(lines) - self.maxcontext
if False and delta > 0:
return number + delta, lines[:self.maxcontext]
return number, lines
self.header = header
self.fromline, self.before = trimcontext(fromline, before)
self.toline, self.after = trimcontext(toline, after)
self.proc = proc
self.hunk = hunk
self.added, self.removed = countchanges(self.hunk)
def write(self, fp):
delta = len(self.before) + len(self.after)
fromlen = delta + self.removed
tolen = delta + self.added
fp.write('@@ -%d,%d +%d,%d @@%s\n' %
(self.fromline, fromlen, self.toline, tolen,
self.proc and (' ' + self.proc)))
fp.write(''.join(self.before + self.hunk + self.after))
pretty = write
def filename(self):
return self.header.filename()
def __repr__(self):
return '<hunk %r@%d>' % (self.filename(), self.fromline)
def parsepatch(fp):
Kirill Smelkov
record: some docs...
r5826 """patch -> [] of hunks """
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 class parser(object):
Kirill Smelkov
record: some docs...
r5826 """patch parsing state machine"""
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 def __init__(self):
self.fromline = 0
self.toline = 0
self.proc = ''
self.header = None
self.context = []
self.before = []
self.hunk = []
self.stream = []
def addrange(self, (fromstart, fromend, tostart, toend, proc)):
self.fromline = int(fromstart)
self.toline = int(tostart)
self.proc = proc
def addcontext(self, context):
if self.hunk:
h = hunk(self.header, self.fromline, self.toline, self.proc,
self.before, self.hunk, context)
self.header.hunks.append(h)
self.stream.append(h)
self.fromline += len(self.before) + h.removed
self.toline += len(self.before) + h.added
self.before = []
self.hunk = []
self.proc = ''
self.context = context
def addhunk(self, hunk):
if self.context:
self.before = self.context
self.context = []
self.hunk = data
def newfile(self, hdr):
self.addcontext([])
h = header(hdr)
self.stream.append(h)
self.header = h
def finished(self):
self.addcontext([])
return self.stream
transitions = {
'file': {'context': addcontext,
'file': newfile,
'hunk': addhunk,
'range': addrange},
'context': {'file': newfile,
'hunk': addhunk,
'range': addrange},
'hunk': {'context': addcontext,
'file': newfile,
'range': addrange},
'range': {'context': addcontext,
'hunk': addhunk},
}
Thomas Arendsen Hein
Remove trailing spaces, fix indentation
r5143
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 p = parser()
state = 'context'
for newstate, data in scanpatch(fp):
try:
p.transitions[state][newstate](p, data)
except KeyError:
raise patch.PatchError('unhandled transition: %s -> %s' %
(state, newstate))
state = newstate
return p.finished()
def filterpatch(ui, chunks):
Kirill Smelkov
record: some docs...
r5826 """Interactively filter patch chunks into applied-only chunks"""
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 chunks = list(chunks)
chunks.reverse()
seen = {}
def consumefile():
Kirill Smelkov
record: some docs...
r5826 """fetch next portion from chunks until a 'header' is seen
NB: header == new-file mark
"""
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 consumed = []
while chunks:
if isinstance(chunks[-1], header):
break
else:
consumed.append(chunks.pop())
return consumed
Kirill Smelkov
record: some docs...
r5826
resp_all = [None] # this two are changed from inside prompt,
resp_file = [None] # so can't be usual variables
applied = {} # 'filename' -> [] of chunks
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 def prompt(query):
Kirill Smelkov
record: some docs...
r5826 """prompt query, and process base inputs
Thomas Arendsen Hein
Removed trailing spaces from everything except test output
r6210
Kirill Smelkov
record: some docs...
r5826 - y/n for the rest of file
- y/n for the rest
- ? (help)
- q (quit)
else, input is returned to the caller.
"""
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 if resp_all[0] is not None:
return resp_all[0]
if resp_file[0] is not None:
return resp_file[0]
while True:
Kirill Smelkov
prompt: kill matchflags...
r5751 r = (ui.prompt(query + _(' [Ynsfdaq?] '), '(?i)[Ynsfdaq?]?$')
or 'y').lower()
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 if r == '?':
c = record.__doc__.find('y - record this change')
for l in record.__doc__[c:].splitlines():
if l: ui.write(_(l.strip()), '\n')
continue
elif r == 's':
r = resp_file[0] = 'n'
elif r == 'f':
r = resp_file[0] = 'y'
elif r == 'd':
r = resp_all[0] = 'n'
elif r == 'a':
r = resp_all[0] = 'y'
elif r == 'q':
raise util.Abort(_('user quit'))
return r
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 while chunks:
chunk = chunks.pop()
if isinstance(chunk, header):
Kirill Smelkov
record: some docs...
r5826 # new-file mark
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 resp_file = [None]
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 fixoffset = 0
hdr = ''.join(chunk.header)
if hdr in seen:
consumefile()
continue
seen[hdr] = True
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 if resp_all[0] is None:
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 chunk.pretty(ui)
Bryan O'Sullivan
record: change wording of initial per-file prompt
r5285 r = prompt(_('examine changes to %s?') %
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 _(' and ').join(map(repr, chunk.files())))
if r == 'y':
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 applied[chunk.filename()] = [chunk]
if chunk.allhunks():
applied[chunk.filename()] += consumefile()
else:
consumefile()
else:
Kirill Smelkov
record: some docs...
r5826 # new hunk
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 if resp_file[0] is None and resp_all[0] is None:
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 chunk.pretty(ui)
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 r = prompt(_('record this change to %r?') %
chunk.filename())
if r == 'y':
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 if fixoffset:
chunk = copy.copy(chunk)
chunk.toline += fixoffset
applied[chunk.filename()].append(chunk)
else:
fixoffset += chunk.removed - chunk.added
return reduce(operator.add, [h for h in applied.itervalues()
if h[0].special() or len(h) > 1], [])
def record(ui, repo, *pats, **opts):
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 '''interactively select changes to commit
If a list of files is omitted, all changes reported by "hg status"
will be candidates for recording.
Thomas Arendsen Hein
Document log date ranges and mention 'hg help dates' for all commands (issue998)
r6163 See 'hg help dates' for a list of formats valid for -d/--date.
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 You will be prompted for whether to record changes to each
modified file, and for files with multiple changes, for each
change to use. For each query, the following responses are
possible:
y - record this change
n - skip this change
s - skip remaining changes to this file
f - record remaining changes to this file
d - done, skip remaining changes and files
a - record all changes to all remaining files
q - quit, recording no changes
? - display help'''
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830 def record_committer(ui, repo, pats, opts):
Kirill Smelkov
record: refactor record into generic record driver...
r5827 commands.commit(ui, repo, *pats, **opts)
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830 dorecord(ui, repo, record_committer, *pats, **opts)
Kirill Smelkov
qrecord: record complements commit, so qrecord should complement qnew...
r5932 def qrecord(ui, repo, patch, *pats, **opts):
'''interactively record a new patch
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830
Kirill Smelkov
qrecord: record complements commit, so qrecord should complement qnew...
r5932 see 'hg help qnew' & 'hg help record' for more information and usage
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830 '''
try:
mq = extensions.find('mq')
except KeyError:
raise util.Abort(_("'mq' extension not loaded"))
def qrecord_committer(ui, repo, pats, opts):
Kirill Smelkov
qrecord: record complements commit, so qrecord should complement qnew...
r5932 mq.new(ui, repo, patch, *pats, **opts)
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830
Kirill Smelkov
qrecord: record complements commit, so qrecord should complement qnew...
r5932 opts = opts.copy()
opts['force'] = True # always 'qnew -f'
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830 dorecord(ui, repo, qrecord_committer, *pats, **opts)
Kirill Smelkov
record: refactor record into generic record driver...
r5827
def dorecord(ui, repo, committer, *pats, **opts):
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 if not ui.interactive:
raise util.Abort(_('running non-interactively, use commit instead'))
def recordfunc(ui, repo, files, message, match, opts):
Kirill Smelkov
record: refactor record into generic record driver...
r5827 """This is generic record driver.
It's 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 intresting changes, and everything else will be
left in place, so the user can continue his work.
"""
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 if files:
changes = None
else:
changes = repo.status(files=files, match=match)[:5]
modified, added, removed = changes[:3]
files = modified + added + removed
diffopts = mdiff.diffopts(git=True, nodates=True)
fp = cStringIO.StringIO()
patch.diff(repo, repo.dirstate.parents()[0], files=files,
match=match, changes=changes, opts=diffopts, fp=fp)
fp.seek(0)
Kirill Smelkov
record: refactor record into generic record driver...
r5827 # 1. filter patch, so we have intending-to apply subset of it
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 chunks = filterpatch(ui, parsepatch(fp))
del fp
contenders = {}
for h in chunks:
try: contenders.update(dict.fromkeys(h.files()))
except AttributeError: pass
Thomas Arendsen Hein
Remove trailing spaces, fix indentation
r5143
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 newfiles = [f for f in files if f in contenders]
if not newfiles:
ui.status(_('no changes to record\n'))
return 0
if changes is None:
changes = repo.status(files=newfiles, match=match)[:5]
modified = dict.fromkeys(changes[0])
Kirill Smelkov
record: refactor record into generic record driver...
r5827 # 2. backup changed files, so we can restore them in the end
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 backups = {}
backupdir = repo.join('record-backups')
try:
os.mkdir(backupdir)
except OSError, err:
Bryan O'Sullivan
record: raise an exception correctly if we can't create a backup directory
r5129 if err.errno != errno.EEXIST:
raise
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 try:
Kirill Smelkov
record: refactor record into generic record driver...
r5827 # backup continues
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 for f in newfiles:
if f not in modified:
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)
backups[f] = tmpname
fp = cStringIO.StringIO()
for c in chunks:
if c.filename() in backups:
c.write(fp)
dopatch = fp.tell()
fp.seek(0)
Kirill Smelkov
record: refactor record into generic record driver...
r5827 # 3a. apply filtered patch to clean repo (clean)
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 if backups:
hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
Kirill Smelkov
record: refactor record into generic record driver...
r5827 # 3b. (apply)
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 if dopatch:
ui.debug('applying patch\n')
ui.debug(fp.getvalue())
patch.internalpatch(fp, ui, 1, repo.root)
del fp
Kirill Smelkov
record: refactor record into generic record driver...
r5827 # 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
cwd = os.getcwd()
os.chdir(repo.root)
try:
committer(ui, repo, newfiles, opts)
finally:
os.chdir(cwd)
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 return 0
finally:
Kirill Smelkov
record: refactor record into generic record driver...
r5827 # 5. finally restore backed-up files
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 try:
for realname, tmpname in backups.iteritems():
ui.debug('restoring %r to %r\n' % (tmpname, realname))
Bryan O'Sullivan
record: work properly if invoked in a subdirectory
r5128 util.copyfile(tmpname, repo.wjoin(realname))
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 os.unlink(tmpname)
os.rmdir(backupdir)
except OSError:
pass
return cmdutil.commit(ui, repo, recordfunc, pats, opts)
cmdtable = {
Thomas Arendsen Hein
Update style of record's cmdtable to match mercurial/commands.py
r5040 "record":
(record,
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830
# add commit options
commands.table['^commit|ci'][1],
Thomas Arendsen Hein
Update style of record's cmdtable to match mercurial/commands.py
r5040 _('hg record [OPTION]... [FILE]...')),
}
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830
def extsetup():
try:
mq = extensions.find('mq')
except KeyError:
return
qcmdtable = {
"qrecord":
(qrecord,
Kirill Smelkov
qrecord: record complements commit, so qrecord should complement qnew...
r5932 # add qnew options, except '--force'
[opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830
Kirill Smelkov
qrecord: record complements commit, so qrecord should complement qnew...
r5932 _('hg qrecord [OPTION]... PATCH [FILE]...')),
Kirill Smelkov
hg qrecord -- like record, but for mq...
r5830 }
cmdtable.update(qcmdtable)