##// END OF EJS Templates
convert: add bzr source
convert: add bzr source

File last commit:

r7015:6651de71 default
r7053:209ef5f3 default
Show More
record.py
539 lines | 17.1 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
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 from mercurial.i18n import gettext, _
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)
Dirkjan Ochtman
record: take diff lines for lack of trailing newlines into account (issue1282)...
r6949 if self.after and self.after[-1] == '\\ No newline at end of file\n':
delta -= 1
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 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 = []
Dirkjan Ochtman
record: take diff lines for lack of trailing newlines into account (issue1282)...
r6949 self.hunk = hunk
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037
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:
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 choices = _('[Ynsfdaq?]')
r = (ui.prompt("%s %s " % (query, choices), '(?i)%s?$' % choices)
or _('y')).lower()
if r == _('?'):
doc = gettext(record.__doc__)
c = doc.find(_('y - record this change'))
for l in doc[c:].splitlines():
if l: ui.write(l.strip(), '\n')
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 continue
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 elif r == _('s'):
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 r = resp_file[0] = 'n'
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 elif r == _('f'):
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 r = resp_file[0] = 'y'
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 elif r == _('d'):
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 r = resp_all[0] = 'n'
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 elif r == _('a'):
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 r = resp_all[0] = 'y'
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 elif r == _('q'):
Bryan O'Sullivan
record: improve docs, improve prompts
r5154 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())))
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 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())
Martin Geisler
i18n, record: improve use of translated docstring in prompts...
r7015 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'))
Matt Mackall
match: stop passing files through commitfunc
r6600 def recordfunc(ui, repo, 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.
"""
Matt Mackall
match: stop passing files through commitfunc
r6600 if match.files():
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 changes = None
else:
Matt Mackall
status: clean up all users for unknown files
r6760 changes = repo.status(match=match)[:3]
modified, added, removed = changes
Matt Mackall
match: stop passing files through commitfunc
r6600 match = cmdutil.matchfiles(repo, modified + added + removed)
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 diffopts = mdiff.diffopts(git=True, nodates=True)
fp = cStringIO.StringIO()
Matt Mackall
match: remove files argument from patch.diff
r6602 patch.diff(repo, repo.dirstate.parents()[0], match=match,
changes=changes, opts=diffopts, fp=fp)
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 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
Matt Mackall
match: stop passing files through commitfunc
r6600 newfiles = [f for f in match.files() if f in contenders]
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037
if not newfiles:
ui.status(_('no changes to record\n'))
return 0
if changes is None:
Matt Mackall
match: stop passing files through commitfunc
r6600 match = cmdutil.matchfiles(repo, newfiles)
Matt Mackall
status: clean up all users for unknown files
r6760 changes = repo.status(match=match)
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 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)
Martin Geisler
i18n: mark strings for translation in record extension
r6965 ui.debug(_('backup %r as %r\n') % (f, tmpname))
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 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:
Dirkjan Ochtman
record: catch PatchErrors from internalpatch and display error message...
r6950 try:
Martin Geisler
i18n: mark strings for translation in record extension
r6965 ui.debug(_('applying patch\n'))
Dirkjan Ochtman
record: catch PatchErrors from internalpatch and display error message...
r6950 ui.debug(fp.getvalue())
patch.internalpatch(fp, ui, 1, repo.root)
except patch.PatchError, err:
s = str(err)
if s:
raise util.Abort(s)
else:
raise util.Abort(_('patch failed to apply'))
Bryan O'Sullivan
Add record extension, giving darcs-like interactive hunk picking
r5037 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():
Martin Geisler
i18n: mark strings for translation in record extension
r6965 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)