record.py
664 lines
| 23.2 KiB
| text/x-python
|
PythonLexer
/ hgext / record.py
Bryan O'Sullivan
|
r5037 | # record.py | ||
# | ||||
# Copyright 2007 Bryan O'Sullivan <bos@serpentine.com> | ||||
# | ||||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Bryan O'Sullivan
|
r5037 | |||
Dirkjan Ochtman
|
r8934 | '''commands to interactively select changes for commit/qrefresh''' | ||
Bryan O'Sullivan
|
r5037 | |||
Martin Geisler
|
r7015 | from mercurial.i18n import gettext, _ | ||
Joel Rosdahl
|
r6212 | from mercurial import cmdutil, commands, extensions, hg, mdiff, patch | ||
Bryan O'Sullivan
|
r5037 | from mercurial import util | ||
Brodie Rao
|
r13099 | import copy, cStringIO, errno, os, re, shutil, tempfile | ||
Bryan O'Sullivan
|
r5037 | |||
Idan Kamara
|
r14408 | cmdtable = {} | ||
command = cmdutil.command(cmdtable) | ||||
Bryan O'Sullivan
|
r5037 | lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)') | ||
Ingo Proetel
|
r14597 | diffopts = [ | ||
('w', 'ignore-all-space', False, | ||||
_('ignore white space when comparing lines')), | ||||
('b', 'ignore-space-change', None, | ||||
_('ignore changes in the amount of white space')), | ||||
('B', 'ignore-blank-lines', None, | ||||
_('ignore changes whose lines are all blank')), | ||||
] | ||||
Bryan O'Sullivan
|
r5037 | def scanpatch(fp): | ||
Kirill Smelkov
|
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
|
r5037 | lr = patch.linereader(fp) | ||
def scanwhile(first, p): | ||||
Kirill Smelkov
|
r5826 | """scan lr while predicate holds""" | ||
Bryan O'Sullivan
|
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 | ||||
Steve Borho
|
r13157 | if line.startswith('diff --git a/') or line.startswith('diff -r '): | ||
Bryan O'Sullivan
|
r5037 | 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
|
r5826 | """patch header | ||
Thomas Arendsen Hein
|
r6210 | |||
XXX shoudn't we move this to mercurial/patch.py ? | ||||
Kirill Smelkov
|
r5826 | """ | ||
Steve Borho
|
r13157 | diffgit_re = re.compile('diff --git a/(.*) b/(.*)$') | ||
diff_re = re.compile('diff -r .* (.*)$') | ||||
Bryan O'Sullivan
|
r5037 | 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): | ||||
Patrick Mezard
|
r13294 | return util.any(h.startswith('index ') for h in self.header) | ||
Thomas Arendsen Hein
|
r5143 | |||
Bryan O'Sullivan
|
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), | ||||
timeless
|
r11728 | sum([max(h.added, h.removed) for h in self.hunks]))) | ||
Bryan O'Sullivan
|
r5037 | break | ||
fp.write(h) | ||||
def write(self, fp): | ||||
fp.write(''.join(self.header)) | ||||
def allhunks(self): | ||||
Patrick Mezard
|
r13294 | return util.any(self.allhunks_re.match(h) for h in self.header) | ||
Bryan O'Sullivan
|
r5037 | |||
def files(self): | ||||
Steve Borho
|
r13157 | match = self.diffgit_re.match(self.header[0]) | ||
if match: | ||||
fromfile, tofile = match.groups() | ||||
if fromfile == tofile: | ||||
return [fromfile] | ||||
return [fromfile, tofile] | ||||
else: | ||||
return self.diff_re.match(self.header[0]).groups() | ||||
Bryan O'Sullivan
|
r5037 | |||
def filename(self): | ||||
return self.files()[-1] | ||||
def __repr__(self): | ||||
return '<header %s>' % (' '.join(map(repr, self.files()))) | ||||
def special(self): | ||||
Patrick Mezard
|
r13294 | return util.any(self.special_re.match(h) for h in self.header) | ||
Bryan O'Sullivan
|
r5037 | |||
def countchanges(hunk): | ||||
Kirill Smelkov
|
r5826 | """hunk -> (n+,n-)""" | ||
Bryan O'Sullivan
|
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
|
r5826 | """patch hunk | ||
Thomas Arendsen Hein
|
r6210 | |||
Kirill Smelkov
|
r5826 | XXX shouldn't we merge this with patch.hunk ? | ||
""" | ||||
Bryan O'Sullivan
|
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
|
r6949 | if self.after and self.after[-1] == '\\ No newline at end of file\n': | ||
delta -= 1 | ||||
Bryan O'Sullivan
|
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): | ||||
Patrick Mezard
|
r13293 | """patch -> [] of headers -> [] of hunks """ | ||
Bryan O'Sullivan
|
r5037 | class parser(object): | ||
Kirill Smelkov
|
r5826 | """patch parsing state machine""" | ||
Bryan O'Sullivan
|
r5037 | def __init__(self): | ||
self.fromline = 0 | ||||
self.toline = 0 | ||||
self.proc = '' | ||||
self.header = None | ||||
self.context = [] | ||||
self.before = [] | ||||
self.hunk = [] | ||||
Patrick Mezard
|
r13293 | self.headers = [] | ||
Bryan O'Sullivan
|
r5037 | |||
Renato Cunha
|
r11499 | def addrange(self, limits): | ||
fromstart, fromend, tostart, toend, proc = limits | ||||
Bryan O'Sullivan
|
r5037 | 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.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
|
r6949 | self.hunk = hunk | ||
Bryan O'Sullivan
|
r5037 | |||
def newfile(self, hdr): | ||||
self.addcontext([]) | ||||
h = header(hdr) | ||||
Patrick Mezard
|
r13293 | self.headers.append(h) | ||
Bryan O'Sullivan
|
r5037 | self.header = h | ||
def finished(self): | ||||
self.addcontext([]) | ||||
Patrick Mezard
|
r13293 | return self.headers | ||
Bryan O'Sullivan
|
r5037 | |||
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
|
r5143 | |||
Bryan O'Sullivan
|
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() | ||||
Patrick Mezard
|
r13293 | def filterpatch(ui, headers): | ||
Kirill Smelkov
|
r5826 | """Interactively filter patch chunks into applied-only chunks""" | ||
A. S. Budden
|
r16324 | def prompt(skipfile, skipall, query, chunk): | ||
Kirill Smelkov
|
r5826 | """prompt query, and process base inputs | ||
Thomas Arendsen Hein
|
r6210 | |||
Kirill Smelkov
|
r5826 | - y/n for the rest of file | ||
- y/n for the rest | ||||
- ? (help) | ||||
- q (quit) | ||||
Patrick Mezard
|
r13291 | Return True/False and possibly updated skipfile and skipall. | ||
Kirill Smelkov
|
r5826 | """ | ||
A. S. Budden
|
r16324 | newpatches = None | ||
Patrick Mezard
|
r13291 | if skipall is not None: | ||
A. S. Budden
|
r16324 | return skipall, skipfile, skipall, newpatches | ||
Patrick Mezard
|
r13291 | if skipfile is not None: | ||
A. S. Budden
|
r16324 | return skipfile, skipfile, skipall, newpatches | ||
Bryan O'Sullivan
|
r5154 | while True: | ||
A. S. Budden
|
r16324 | resps = _('[Ynesfdaq?]') | ||
Steve Borho
|
r8259 | choices = (_('&Yes, record this change'), | ||
_('&No, skip this change'), | ||||
A. S. Budden
|
r16324 | _('&Edit the change manually'), | ||
Steve Borho
|
r8259 | _('&Skip remaining changes to this file'), | ||
_('Record remaining changes to this &file'), | ||||
_('&Done, skip remaining changes and files'), | ||||
_('Record &all changes to all remaining files'), | ||||
_('&Quit, recording no changes'), | ||||
_('&?')) | ||||
timeless@mozdev.org
|
r9461 | r = ui.promptchoice("%s %s" % (query, resps), choices) | ||
Martin Geisler
|
r10694 | ui.write("\n") | ||
A. S. Budden
|
r16324 | if r == 8: # ? | ||
Martin Geisler
|
r7015 | doc = gettext(record.__doc__) | ||
Martin Geisler
|
r11236 | c = doc.find('::') + 2 | ||
Martin Geisler
|
r7015 | for l in doc[c:].splitlines(): | ||
Martin Geisler
|
r11236 | if l.startswith(' '): | ||
Matt Mackall
|
r10282 | ui.write(l.strip(), '\n') | ||
Bryan O'Sullivan
|
r5154 | continue | ||
Simon Heimberg
|
r9048 | elif r == 0: # yes | ||
Martin Geisler
|
r9837 | ret = True | ||
Simon Heimberg
|
r9048 | elif r == 1: # no | ||
Martin Geisler
|
r9837 | ret = False | ||
A. S. Budden
|
r16324 | elif r == 2: # Edit patch | ||
if chunk is None: | ||||
ui.write(_('cannot edit patch for whole file')) | ||||
ui.write("\n") | ||||
continue | ||||
if chunk.header.binary(): | ||||
ui.write(_('cannot edit patch for binary file')) | ||||
ui.write("\n") | ||||
continue | ||||
# Patch comment based on the Git one (based on comment at end of | ||||
# http://mercurial.selenic.com/wiki/RecordExtension) | ||||
phelp = '---' + _(""" | ||||
To remove '-' lines, make them ' ' lines (context). | ||||
To remove '+' lines, delete them. | ||||
Lines starting with # will be removed from the patch. | ||||
If the patch applies cleanly, the edited hunk will immediately be | ||||
added to the record list. If it does not apply cleanly, a rejects | ||||
file will be generated: you can use that when you try again. If | ||||
all lines of the hunk are removed, then the edit is aborted and | ||||
the hunk is left unchanged. | ||||
""") | ||||
(patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-", | ||||
suffix=".diff", text=True) | ||||
Matt Mackall
|
r16328 | ncpatchfp = None | ||
A. S. Budden
|
r16324 | try: | ||
# Write the initial patch | ||||
f = os.fdopen(patchfd, "w") | ||||
chunk.header.write(f) | ||||
chunk.write(f) | ||||
f.write('\n'.join(['# ' + i for i in phelp.splitlines()])) | ||||
f.close() | ||||
# Start the editor and wait for it to complete | ||||
editor = ui.geteditor() | ||||
util.system("%s \"%s\"" % (editor, patchfn), | ||||
environ={'HGUSER': ui.username()}, | ||||
onerr=util.Abort, errprefix=_("edit failed"), | ||||
out=ui.fout) | ||||
# Remove comment lines | ||||
patchfp = open(patchfn) | ||||
ncpatchfp = cStringIO.StringIO() | ||||
for line in patchfp: | ||||
if not line.startswith('#'): | ||||
ncpatchfp.write(line) | ||||
patchfp.close() | ||||
ncpatchfp.seek(0) | ||||
newpatches = parsepatch(ncpatchfp) | ||||
finally: | ||||
os.unlink(patchfn) | ||||
del ncpatchfp | ||||
# Signal that the chunk shouldn't be applied as-is, but | ||||
# provide the new patch to be used instead. | ||||
ret = False | ||||
elif r == 3: # Skip | ||||
Patrick Mezard
|
r13291 | ret = skipfile = False | ||
A. S. Budden
|
r16324 | elif r == 4: # file (Record remaining) | ||
Patrick Mezard
|
r13291 | ret = skipfile = True | ||
A. S. Budden
|
r16324 | elif r == 5: # done, skip remaining | ||
Patrick Mezard
|
r13291 | ret = skipall = False | ||
A. S. Budden
|
r16324 | elif r == 6: # all | ||
Patrick Mezard
|
r13291 | ret = skipall = True | ||
A. S. Budden
|
r16324 | elif r == 7: # quit | ||
Bryan O'Sullivan
|
r5154 | raise util.Abort(_('user quit')) | ||
A. S. Budden
|
r16324 | return ret, skipfile, skipall, newpatches | ||
Patrick Mezard
|
r13291 | |||
seen = set() | ||||
applied = {} # 'filename' -> [] of chunks | ||||
skipfile, skipall = None, None | ||||
Patrick Mezard
|
r13295 | pos, total = 1, sum(len(h.hunks) for h in headers) | ||
Patrick Mezard
|
r13293 | for h in headers: | ||
Patrick Mezard
|
r13295 | pos += len(h.hunks) | ||
Patrick Mezard
|
r13293 | skipfile = None | ||
fixoffset = 0 | ||||
hdr = ''.join(h.header) | ||||
if hdr in seen: | ||||
continue | ||||
seen.add(hdr) | ||||
if skipall is None: | ||||
h.pretty(ui) | ||||
msg = (_('examine changes to %s?') % | ||||
_(' and ').join(map(repr, h.files()))) | ||||
A. S. Budden
|
r16324 | r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None) | ||
Patrick Mezard
|
r13293 | if not r: | ||
continue | ||||
applied[h.filename()] = [h] | ||||
if h.allhunks(): | ||||
applied[h.filename()] += h.hunks | ||||
continue | ||||
for i, chunk in enumerate(h.hunks): | ||||
Patrick Mezard
|
r13291 | if skipfile is None and skipall is None: | ||
Bryan O'Sullivan
|
r5037 | chunk.pretty(ui) | ||
Martin Geisler
|
r13773 | if total == 1: | ||
msg = _('record this change to %r?') % chunk.filename() | ||||
else: | ||||
idx = pos - len(h.hunks) + i | ||||
msg = _('record change %d/%d to %r?') % (idx, total, | ||||
chunk.filename()) | ||||
A. S. Budden
|
r16324 | r, skipfile, skipall, newpatches = prompt(skipfile, | ||
skipall, msg, chunk) | ||||
Martin Geisler
|
r9837 | if r: | ||
Bryan O'Sullivan
|
r5037 | if fixoffset: | ||
chunk = copy.copy(chunk) | ||||
chunk.toline += fixoffset | ||||
applied[chunk.filename()].append(chunk) | ||||
A. S. Budden
|
r16324 | elif newpatches is not None: | ||
for newpatch in newpatches: | ||||
for newhunk in newpatch.hunks: | ||||
if fixoffset: | ||||
newhunk.toline += fixoffset | ||||
applied[newhunk.filename()].append(newhunk) | ||||
Bryan O'Sullivan
|
r5037 | else: | ||
fixoffset += chunk.removed - chunk.added | ||||
Renato Cunha
|
r11500 | return sum([h for h in applied.itervalues() | ||
if h[0].special() or len(h) > 1], []) | ||||
Bryan O'Sullivan
|
r5037 | |||
Idan Kamara
|
r14408 | @command("record", | ||
Ingo Proetel
|
r14597 | # same options as commit + white space diff options | ||
commands.table['^commit|ci'][1][:] + diffopts, | ||||
Idan Kamara
|
r14408 | _('hg record [OPTION]... [FILE]...')) | ||
Bryan O'Sullivan
|
r5037 | def record(ui, repo, *pats, **opts): | ||
Bryan O'Sullivan
|
r5154 | '''interactively select changes to commit | ||
Martin Geisler
|
r10973 | If a list of files is omitted, all changes reported by :hg:`status` | ||
Martin Geisler
|
r9272 | will be candidates for recording. | ||
Bryan O'Sullivan
|
r5154 | |||
Martin Geisler
|
r10973 | See :hg:`help dates` for a list of formats valid for -d/--date. | ||
Thomas Arendsen Hein
|
r6163 | |||
Martin Geisler
|
r9272 | 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:: | ||||
Bryan O'Sullivan
|
r5154 | |||
Martin Geisler
|
r9157 | y - record this change | ||
n - skip this change | ||||
A. S. Budden
|
r16324 | e - edit this change manually | ||
Bryan O'Sullivan
|
r5154 | |||
Martin Geisler
|
r9157 | s - skip remaining changes to this file | ||
f - record remaining changes to this file | ||||
Bryan O'Sullivan
|
r5154 | |||
Martin Geisler
|
r9157 | d - done, skip remaining changes and files | ||
a - record all changes to all remaining files | ||||
q - quit, recording no changes | ||||
Bryan O'Sullivan
|
r5154 | |||
Nicolas Dumazet
|
r11237 | ? - display help | ||
This command is not available when committing a merge.''' | ||||
Bryan O'Sullivan
|
r5037 | |||
Idan Kamara
|
r14425 | dorecord(ui, repo, commands.commit, 'commit', False, *pats, **opts) | ||
Kirill Smelkov
|
r5830 | |||
Matt Mackall
|
r15184 | def qrefresh(origfn, ui, repo, *pats, **opts): | ||
if not opts['interactive']: | ||||
return origfn(ui, repo, *pats, **opts) | ||||
Idan Kamara
|
r14426 | mq = extensions.find('mq') | ||
def committomq(ui, repo, *pats, **opts): | ||||
# At this point the working copy contains only changes that | ||||
# were accepted. All other changes were reverted. | ||||
# We can't pass *pats here since qrefresh will undo all other | ||||
# changed files in the patch that aren't in pats. | ||||
mq.refresh(ui, repo, **opts) | ||||
# backup all changed files | ||||
dorecord(ui, repo, committomq, 'qrefresh', True, *pats, **opts) | ||||
Kirill Smelkov
|
r5830 | |||
Kirill Smelkov
|
r5932 | def qrecord(ui, repo, patch, *pats, **opts): | ||
'''interactively record a new patch | ||||
Kirill Smelkov
|
r5830 | |||
Martin Geisler
|
r10973 | See :hg:`help qnew` & :hg:`help record` for more information and | ||
Martin Geisler
|
r9272 | usage. | ||
Kirill Smelkov
|
r5830 | ''' | ||
try: | ||||
mq = extensions.find('mq') | ||||
except KeyError: | ||||
raise util.Abort(_("'mq' extension not loaded")) | ||||
Idan Kamara
|
r14424 | repo.mq.checkpatchname(patch) | ||
Dan Villiom Podlaski Christiansen
|
r10323 | def committomq(ui, repo, *pats, **opts): | ||
Idan Kamara
|
r14424 | opts['checkname'] = False | ||
Kirill Smelkov
|
r5932 | mq.new(ui, repo, patch, *pats, **opts) | ||
Kirill Smelkov
|
r5830 | |||
Idan Kamara
|
r14425 | dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts) | ||
Kirill Smelkov
|
r5827 | |||
Matt Mackall
|
r15184 | def qnew(origfn, ui, repo, patch, *args, **opts): | ||
if opts['interactive']: | ||||
return qrecord(ui, repo, patch, *args, **opts) | ||||
return origfn(ui, repo, patch, *args, **opts) | ||||
Idan Kamara
|
r14425 | def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts): | ||
Matt Mackall
|
r8208 | if not ui.interactive(): | ||
Idan Kamara
|
r14407 | raise util.Abort(_('running non-interactively, use %s instead') % | ||
cmdsuggest) | ||||
Bryan O'Sullivan
|
r5037 | |||
Matt Mackall
|
r6600 | def recordfunc(ui, repo, message, match, opts): | ||
Kirill Smelkov
|
r5827 | """This is generic record driver. | ||
Kevin Bullock
|
r13195 | Its job is to interactively filter local changes, and | ||
accordingly prepare working directory into a state in which the | ||||
job can be delegated to a non-interactive commit command such as | ||||
'commit' or 'qrefresh'. | ||||
Kirill Smelkov
|
r5827 | |||
Kevin Bullock
|
r13195 | After the actual job is done by non-interactive command, the | ||
working directory is restored to its original state. | ||||
Kirill Smelkov
|
r5827 | |||
Kevin Bullock
|
r13195 | In the end we'll record interesting changes, and everything else | ||
will be left in place, so the user can continue working. | ||||
Kirill Smelkov
|
r5827 | """ | ||
Dirkjan Ochtman
|
r7754 | |||
Nicolas Dumazet
|
r11237 | merge = len(repo[None].parents()) > 1 | ||
if merge: | ||||
raise util.Abort(_('cannot partially commit a merge ' | ||||
timeless
|
r13023 | '(use "hg commit" instead)')) | ||
Nicolas Dumazet
|
r11237 | |||
Dirkjan Ochtman
|
r7754 | changes = repo.status(match=match)[:3] | ||
Ingo Proetel
|
r14597 | diffopts = mdiff.diffopts(git=True, nodates=True, | ||
ignorews=opts.get('ignore_all_space'), | ||||
ignorewsamount=opts.get('ignore_space_change'), | ||||
ignoreblanklines=opts.get('ignore_blank_lines')) | ||||
Dirkjan Ochtman
|
r7754 | chunks = patch.diff(repo, changes=changes, opts=diffopts) | ||
Bryan O'Sullivan
|
r5037 | fp = cStringIO.StringIO() | ||
Dirkjan Ochtman
|
r7308 | fp.write(''.join(chunks)) | ||
Bryan O'Sullivan
|
r5037 | fp.seek(0) | ||
Kirill Smelkov
|
r5827 | # 1. filter patch, so we have intending-to apply subset of it | ||
Bryan O'Sullivan
|
r5037 | chunks = filterpatch(ui, parsepatch(fp)) | ||
del fp | ||||
Martin Geisler
|
r8152 | contenders = set() | ||
Bryan O'Sullivan
|
r5037 | for h in chunks: | ||
Matt Mackall
|
r10282 | try: | ||
contenders.update(set(h.files())) | ||||
except AttributeError: | ||||
pass | ||||
Thomas Arendsen Hein
|
r5143 | |||
Dirkjan Ochtman
|
r7754 | changed = changes[0] + changes[1] + changes[2] | ||
newfiles = [f for f in changed if f in contenders] | ||||
Bryan O'Sullivan
|
r5037 | if not newfiles: | ||
ui.status(_('no changes to record\n')) | ||||
return 0 | ||||
Martin Geisler
|
r8152 | modified = set(changes[0]) | ||
Bryan O'Sullivan
|
r5037 | |||
Kirill Smelkov
|
r5827 | # 2. backup changed files, so we can restore them in the end | ||
Idan Kamara
|
r14425 | if backupall: | ||
tobackup = changed | ||||
else: | ||||
tobackup = [f for f in newfiles if f in modified] | ||||
Bryan O'Sullivan
|
r5037 | backups = {} | ||
Idan Kamara
|
r14425 | if tobackup: | ||
backupdir = repo.join('record-backups') | ||||
try: | ||||
os.mkdir(backupdir) | ||||
except OSError, err: | ||||
if err.errno != errno.EEXIST: | ||||
raise | ||||
Bryan O'Sullivan
|
r5037 | try: | ||
Kirill Smelkov
|
r5827 | # backup continues | ||
Idan Kamara
|
r14425 | for f in tobackup: | ||
Bryan O'Sullivan
|
r5037 | fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', | ||
dir=backupdir) | ||||
os.close(fd) | ||||
Martin Geisler
|
r9467 | ui.debug('backup %r as %r\n' % (f, tmpname)) | ||
Bryan O'Sullivan
|
r5037 | util.copyfile(repo.wjoin(f), tmpname) | ||
Brodie Rao
|
r13099 | shutil.copystat(repo.wjoin(f), tmpname) | ||
Bryan O'Sullivan
|
r5037 | 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
|
r5827 | # 3a. apply filtered patch to clean repo (clean) | ||
Bryan O'Sullivan
|
r5037 | if backups: | ||
Matt Mackall
|
r13878 | hg.revert(repo, repo.dirstate.p1(), | ||
Renato Cunha
|
r11564 | lambda key: key in backups) | ||
Bryan O'Sullivan
|
r5037 | |||
Kirill Smelkov
|
r5827 | # 3b. (apply) | ||
Bryan O'Sullivan
|
r5037 | if dopatch: | ||
Dirkjan Ochtman
|
r6950 | try: | ||
Martin Geisler
|
r9467 | ui.debug('applying patch\n') | ||
Dirkjan Ochtman
|
r6950 | ui.debug(fp.getvalue()) | ||
Patrick Mezard
|
r14370 | patch.internalpatch(ui, repo, fp, 1, eolmode=None) | ||
Dirkjan Ochtman
|
r6950 | except patch.PatchError, err: | ||
Patrick Mezard
|
r12674 | raise util.Abort(str(err)) | ||
Bryan O'Sullivan
|
r5037 | del fp | ||
Kevin Bullock
|
r13195 | # 4. We prepared working directory according to filtered | ||
# patch. Now is the time to delegate the job to | ||||
# commit/qrefresh or the like! | ||||
Kirill Smelkov
|
r5827 | |||
Kevin Bullock
|
r13195 | # it is important to first chdir to repo root -- we'll call | ||
# a highlevel command with list of pathnames relative to | ||||
# repo root | ||||
Kirill Smelkov
|
r5827 | cwd = os.getcwd() | ||
os.chdir(repo.root) | ||||
try: | ||||
Dan Villiom Podlaski Christiansen
|
r10323 | commitfunc(ui, repo, *newfiles, **opts) | ||
Kirill Smelkov
|
r5827 | finally: | ||
os.chdir(cwd) | ||||
Bryan O'Sullivan
|
r5037 | return 0 | ||
finally: | ||||
Kirill Smelkov
|
r5827 | # 5. finally restore backed-up files | ||
Bryan O'Sullivan
|
r5037 | try: | ||
for realname, tmpname in backups.iteritems(): | ||||
Martin Geisler
|
r9467 | ui.debug('restoring %r to %r\n' % (tmpname, realname)) | ||
Bryan O'Sullivan
|
r5128 | util.copyfile(tmpname, repo.wjoin(realname)) | ||
Brodie Rao
|
r13099 | # Our calls to copystat() here and above are a | ||
# hack to trick any editors that have f open that | ||||
# we haven't modified them. | ||||
# | ||||
# Also note that this racy as an editor could | ||||
# notice the file's mtime before we've finished | ||||
# writing it. | ||||
shutil.copystat(tmpname, repo.wjoin(realname)) | ||||
Bryan O'Sullivan
|
r5037 | os.unlink(tmpname) | ||
Idan Kamara
|
r14425 | if tobackup: | ||
os.rmdir(backupdir) | ||||
Bryan O'Sullivan
|
r5037 | except OSError: | ||
pass | ||||
Brodie Rao
|
r10825 | |||
# wrap ui.write so diff output can be labeled/colorized | ||||
def wrapwrite(orig, *args, **kw): | ||||
label = kw.pop('label', '') | ||||
for chunk, l in patch.difflabel(lambda: args): | ||||
orig(chunk, label=label + l) | ||||
oldwrite = ui.write | ||||
extensions.wrapfunction(ui, 'write', wrapwrite) | ||||
try: | ||||
return cmdutil.commit(ui, repo, recordfunc, pats, opts) | ||||
finally: | ||||
ui.write = oldwrite | ||||
Bryan O'Sullivan
|
r5037 | |||
Idan Kamara
|
r14408 | cmdtable["qrecord"] = \ | ||
Idan Kamara
|
r14441 | (qrecord, [], # placeholder until mq is available | ||
Idan Kamara
|
r14408 | _('hg qrecord [OPTION]... PATCH [FILE]...')) | ||
Kirill Smelkov
|
r5830 | |||
Martin Geisler
|
r9710 | def uisetup(ui): | ||
Kirill Smelkov
|
r5830 | try: | ||
mq = extensions.find('mq') | ||||
except KeyError: | ||||
return | ||||
Idan Kamara
|
r14408 | cmdtable["qrecord"] = \ | ||
Idan Kamara
|
r14427 | (qrecord, | ||
# same options as qnew, but copy them so we don't get | ||||
Ingo Proetel
|
r14597 | # -i/--interactive for qrecord and add white space diff options | ||
mq.cmdtable['^qnew'][1][:] + diffopts, | ||||
Idan Kamara
|
r14408 | _('hg qrecord [OPTION]... PATCH [FILE]...')) | ||
Idan Kamara
|
r14426 | |||
Matt Mackall
|
r15184 | _wrapcmd('qnew', mq.cmdtable, qnew, _("interactively record a new patch")) | ||
Idan Kamara
|
r14426 | _wrapcmd('qrefresh', mq.cmdtable, qrefresh, | ||
_("interactively select changes to refresh")) | ||||
def _wrapcmd(cmd, table, wrapfn, msg): | ||||
Matt Mackall
|
r15184 | entry = extensions.wrapcommand(table, cmd, wrapfn) | ||
Idan Kamara
|
r14426 | entry[1].append(('i', 'interactive', None, msg)) | ||