PageRenderTime 68ms CodeModel.GetById 12ms app.highlight 49ms RepoModel.GetById 2ms app.codeStats 0ms

/tortoisehg/util/hgshelve.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 657 lines | 623 code | 16 blank | 18 comment | 28 complexity | 8500dff89f05673c36d361b3d0cbaeba MD5 | raw file
  1# hgshelve.py - TortoiseHg dialog to initialize a repo
  2#
  3# Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
  4# Copyright 2007 TK Soh <teekaysoh@gmailcom>
  5# Copyright 2009 Steve Borho <steve@borho.org>
  6#
  7# This software may be used and distributed according to the terms of the
  8# GNU General Public License version 2, incorporated herein by reference.
  9
 10'''interactive change selection to set aside that may be restored later'''
 11
 12import copy
 13import cStringIO
 14import errno
 15import operator
 16import os
 17import re
 18import tempfile
 19
 20from mercurial import cmdutil, commands, cmdutil, hg, mdiff, patch, revlog
 21from mercurial import util, fancyopts
 22
 23from tortoisehg.util.i18n import _
 24from tortoisehg.util import hglib
 25
 26lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
 27
 28def internalpatch(patchobj, ui, strip, cwd, files={}):
 29    """use builtin patch to apply <patchobj> to the working directory.
 30    returns whether patch was applied with fuzz factor.
 31
 32    Adapted from patch.internalpatch() to support reverse patching.
 33    """
 34    try:
 35        fp = file(patchobj, 'rb')
 36    except TypeError:
 37        fp = patchobj
 38    if cwd:
 39        curdir = os.getcwd()
 40        os.chdir(cwd)
 41    eolmode = ui.config('patch', 'eol', 'strict')
 42    try:
 43        eol = {'strict': None,
 44               'auto': None,
 45               'crlf': '\r\n',
 46               'lf': '\n'}[eolmode.lower()]
 47    except KeyError:
 48        raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
 49    try:
 50        if hasattr(patch, 'eolmodes'): # hg-1.5 hack
 51            ret = patch.applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
 52        else:
 53            ret = patch.applydiff(ui, fp, files, strip=strip, eol=eol)
 54    finally:
 55        if cwd:
 56            os.chdir(curdir)
 57    if ret < 0:
 58        raise patch.PatchError
 59    return ret > 0
 60
 61def scanpatch(fp):
 62    lr = patch.linereader(fp)
 63
 64    def scanwhile(first, p):
 65        lines = [first]
 66        while True:
 67            line = lr.readline()
 68            if not line:
 69                break
 70            if p(line):
 71                lines.append(line)
 72            else:
 73                lr.push(line)
 74                break
 75        return lines
 76
 77    while True:
 78        line = lr.readline()
 79        if not line:
 80            break
 81        if line.startswith('diff --git a/'):
 82            def notheader(line):
 83                s = line.split(None, 1)
 84                return not s or s[0] not in ('---', 'diff')
 85            header = scanwhile(line, notheader)
 86            fromfile = lr.readline()
 87            if fromfile.startswith('---'):
 88                tofile = lr.readline()
 89                header += [fromfile, tofile]
 90            else:
 91                lr.push(fromfile)
 92            yield 'file', header
 93        elif line[0] == ' ':
 94            yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
 95        elif line[0] in '-+':
 96            yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
 97        else:
 98            m = lines_re.match(line)
 99            if m:
100                yield 'range', m.groups()
101            else:
102                raise patch.PatchError(_('unknown patch content: %r') % line)
103
104class header(object):
105    diff_re = re.compile('diff --git a/(.*) b/(.*)$')
106    allhunks_re = re.compile('(?:index|new file|deleted file) ')
107    pretty_re = re.compile('(?:new file|deleted file) ')
108    special_re = re.compile('(?:index|new file|deleted|copy|rename) ')
109
110    def __init__(self, header):
111        self.header = header
112        self.hunks = []
113
114    def binary(self):
115        for h in self.header:
116            if h.startswith('index '):
117                return True
118
119    def selpretty(self, selected):
120        str = ''
121        for h in self.header:
122            if h.startswith('index '):
123                str += _('this modifies a binary file (all or nothing)\n')
124                break
125            if self.pretty_re.match(h):
126                str += hglib.toutf(h)
127                if self.binary():
128                    str += _('this is a binary file\n')
129                break
130            if h.startswith('---'):
131                hunks = len(self.hunks)
132                shunks, lines, slines = 0, 0, 0
133                for i, h in enumerate(self.hunks):
134                    lines += h.added + h.removed
135                    if selected(i):
136                        shunks += 1
137                        slines += h.added + h.removed
138                str += '<span foreground="blue">'
139                str += _('total: %d hunks (%d changed lines); '
140                        'selected: %d hunks (%d changed lines)') % (hunks,
141                                lines, shunks, slines)
142                str += '</span>'
143                break
144            str += hglib.toutf(h)
145        return str
146
147    def pretty(self, fp):
148        for h in self.header:
149            if h.startswith('index '):
150                fp.write(_('this modifies a binary file (all or nothing)\n'))
151                break
152            if self.pretty_re.match(h):
153                fp.write(h)
154                if self.binary():
155                    fp.write(_('this is a binary file\n'))
156                break
157            if h.startswith('---'):
158                fp.write(_('%d hunks, %d lines changed\n') %
159                         (len(self.hunks),
160                          sum([h.added + h.removed for h in self.hunks])))
161                break
162            fp.write(h)
163
164    def write(self, fp):
165        fp.write(''.join(self.header))
166
167    def allhunks(self):
168        for h in self.header:
169            if self.allhunks_re.match(h):
170                return True
171
172    def files(self):
173        fromfile, tofile = self.diff_re.match(self.header[0]).groups()
174        if fromfile == tofile:
175            return [fromfile]
176        return [fromfile, tofile]
177
178    def filename(self):
179        return self.files()[-1]
180
181    def __repr__(self):
182        return '<header %s>' % (' '.join(map(repr, self.files())))
183
184    def special(self):
185        for h in self.header:
186            if self.special_re.match(h):
187                return True
188
189    def __cmp__(self, other):
190        return cmp(repr(self), repr(other))
191
192def countchanges(hunk):
193    add = len([h for h in hunk if h[0] == '+'])
194    rem = len([h for h in hunk if h[0] == '-'])
195    return add, rem
196
197class hunk(object):
198    maxcontext = 3
199
200    def __init__(self, header, fromline, toline, proc, before, hunk, after):
201        def trimcontext(number, lines):
202            delta = len(lines) - self.maxcontext
203            if False and delta > 0:
204                return number + delta, lines[:self.maxcontext]
205            return number, lines
206
207        self.header = header
208        self.fromline, self.before = trimcontext(fromline, before)
209        self.toline, self.after = trimcontext(toline, after)
210        self.proc = proc
211        self.hunk = hunk
212        self.added, self.removed = countchanges(self.hunk)
213
214    def write(self, fp):
215        delta = len(self.before) + len(self.after)
216        if self.after and self.after[-1] == '\\ No newline at end of file\n':
217            delta -= 1
218        fromlen = delta + self.removed
219        tolen = delta + self.added
220        fp.write('@@ -%d,%d +%d,%d @@%s\n' %
221                 (self.fromline, fromlen, self.toline, tolen,
222                  self.proc and (' ' + self.proc)))
223        fp.write(''.join(self.before + self.hunk + self.after))
224
225    pretty = write
226
227    def filename(self):
228        return self.header.filename()
229
230    def __repr__(self):
231        return '<hunk %r@%d>' % (self.filename(), self.fromline)
232
233    def __cmp__(self, other):
234        return cmp(repr(self), repr(other))
235
236def parsepatch(fp):
237    class parser(object):
238        def __init__(self):
239            self.fromline = 0
240            self.toline = 0
241            self.proc = ''
242            self.header = None
243            self.context = []
244            self.before = []
245            self.hunk = []
246            self.stream = []
247
248        def addrange(self, (fromstart, fromend, tostart, toend, proc)):
249            self.fromline = int(fromstart)
250            self.toline = int(tostart)
251            self.proc = proc
252
253        def addcontext(self, context):
254            if self.hunk:
255                h = hunk(self.header, self.fromline, self.toline, self.proc,
256                         self.before, self.hunk, context)
257                self.header.hunks.append(h)
258                self.stream.append(h)
259                self.fromline += len(self.before) + h.removed
260                self.toline += len(self.before) + h.added
261                self.before = []
262                self.hunk = []
263                self.proc = ''
264            self.context = context
265
266        def addhunk(self, hunk):
267            if self.context:
268                self.before = self.context
269                self.context = []
270            self.hunk = hunk
271
272        def newfile(self, hdr):
273            self.addcontext([])
274            h = header(hdr)
275            self.stream.append(h)
276            self.header = h
277
278        def finished(self):
279            self.addcontext([])
280            return self.stream
281
282        transitions = {
283            'file': {'context': addcontext,
284                     'file': newfile,
285                     'hunk': addhunk,
286                     'range': addrange},
287            'context': {'file': newfile,
288                        'hunk': addhunk,
289                        'range': addrange},
290            'hunk': {'context': addcontext,
291                     'file': newfile,
292                     'range': addrange},
293            'range': {'context': addcontext,
294                      'hunk': addhunk},
295            }
296
297    p = parser()
298
299    state = 'context'
300    for newstate, data in scanpatch(fp):
301        try:
302            p.transitions[state][newstate](p, data)
303        except KeyError:
304            raise patch.PatchError(_('unhandled transition: %s -> %s') %
305                                   (state, newstate))
306        state = newstate
307    return p.finished()
308
309def filterpatch(ui, chunks):
310    chunks = list(chunks)
311    chunks.reverse()
312    seen = {}
313    def consumefile():
314        consumed = []
315        while chunks:
316            if isinstance(chunks[-1], header):
317                break
318            else:
319                consumed.append(chunks.pop())
320        return consumed
321    resp_all = [None]
322    resp_file = [None]
323    applied = {}
324    def prompt(query):
325        if resp_all[0] is not None:
326            return resp_all[0]
327        if resp_file[0] is not None:
328            return resp_file[0]
329        while True:
330            r = (ui.prompt(query + _(' [Ynsfdaq?] '), '(?i)[Ynsfdaq?]?$')
331                 or 'y').lower()
332            if r == '?':
333                c = shelve.__doc__.find('y - shelve this change')
334                for l in shelve.__doc__[c:].splitlines():
335                    if l: ui.write(_(l.strip()), '\n')
336                continue
337            elif r == 's':
338                r = resp_file[0] = 'n'
339            elif r == 'f':
340                r = resp_file[0] = 'y'
341            elif r == 'd':
342                r = resp_all[0] = 'n'
343            elif r == 'a':
344                r = resp_all[0] = 'y'
345            elif r == 'q':
346                raise util.Abort(_('user quit'))
347            return r
348    while chunks:
349        chunk = chunks.pop()
350        if isinstance(chunk, header):
351            resp_file = [None]
352            fixoffset = 0
353            hdr = ''.join(chunk.header)
354            if hdr in seen:
355                consumefile()
356                continue
357            seen[hdr] = True
358            if resp_all[0] is None:
359                chunk.pretty(ui)
360            r = prompt(_('shelve changes to %s?') %
361                       _(' and ').join(map(repr, chunk.files())))
362            if r == 'y':
363                applied[chunk.filename()] = [chunk]
364                if chunk.allhunks():
365                    applied[chunk.filename()] += consumefile()
366            else:
367                consumefile()
368        else:
369            if resp_file[0] is None and resp_all[0] is None:
370                chunk.pretty(ui)
371            r = prompt(_('shelve this change to %r?') %
372                       chunk.filename())
373            if r == 'y':
374                if fixoffset:
375                    chunk = copy.copy(chunk)
376                    chunk.toline += fixoffset
377                applied[chunk.filename()].append(chunk)
378            else:
379                fixoffset += chunk.removed - chunk.added
380    return reduce(operator.add, [h for h in applied.itervalues()
381                                 if h[0].special() or len(h) > 1], [])
382
383def refilterpatch(allchunk, selected):
384    ''' return unshelved chunks of files to be shelved '''
385    l = []
386    fil = []
387    for c in allchunk:
388        if isinstance(c, header):
389            if len(l) > 1 and l[0] in selected:
390                fil += l
391            l = [c]
392        elif c not in selected:
393            l.append(c)
394    if len(l) > 1 and l[0] in selected:
395        fil += l
396    return fil
397
398def makebackup(ui, repo, dir, files):
399    try:
400        os.mkdir(dir)
401    except OSError, err:
402        if err.errno != errno.EEXIST:
403            raise
404
405    backups = {}
406    for f in files:
407        fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
408                                       dir=dir)
409        os.close(fd)
410        ui.debug(_('backup %r as %r\n') % (f, tmpname))
411        try:
412            util.copyfile(repo.wjoin(f), tmpname)
413        except:
414            ui.warn(_('file copy of %s failed\n') % f)
415            raise
416        backups[f] = tmpname
417
418    return backups
419
420
421def delete_backup(ui, repo, backupdir):
422    """remove the shelve backup files and directory"""
423
424    backupdir = os.path.normpath(repo.join(backupdir))
425
426    # Do a sanity check to ensure that unrelated files aren't destroyed.
427    # All shelve file and directory paths must start with "shelve" under
428    # the .hg directory.
429    if backupdir.startswith(repo.join('shelve')):
430        try:
431            backups = os.listdir(backupdir)
432            for filename in backups:
433                ui.debug(_('removing backup file : %r\n') % filename)
434                os.unlink(os.path.join(backupdir, filename))
435            os.rmdir(backupdir)
436        except OSError:
437            ui.warn(_('delete of shelve backup failed'))
438            pass
439    else:
440        ui.warn(_('bad shelve backup directory name'))
441
442
443def get_shelve_filename(repo):
444    return repo.join('shelve')
445
446def shelve(ui, repo, *pats, **opts):
447    '''interactively select changes to set aside
448
449    If a list of files is omitted, all changes reported by "hg status"
450    will be candidates for shelveing.
451
452    You will be prompted for whether to shelve changes to each
453    modified file, and for files with multiple changes, for each
454    change to use.  For each query, the following responses are
455    possible:
456
457    y - shelve this change
458    n - skip this change
459
460    s - skip remaining changes to this file
461    f - shelve remaining changes to this file
462
463    d - done, skip remaining changes and files
464    a - shelve all changes to all remaining files
465    q - quit, shelveing no changes
466
467    ? - display help'''
468
469    if not ui.interactive():
470        raise util.Abort(_('shelve can only be run interactively'))
471
472    forced = opts['force'] or opts['append']
473    if os.path.exists(repo.join('shelve')) and not forced:
474        raise util.Abort(_('shelve data already exists'))
475
476    def shelvefunc(ui, repo, message, match, opts):
477        # If an MQ patch is applied, consider all qdiff changes
478        if hasattr(repo, 'mq') and repo.mq.applied and repo['.'] == repo['qtip']:
479            qtip = repo['.']
480            basenode = qtip.parents()[0].node()
481        else:
482            basenode = repo.dirstate.parents()[0]
483
484        changes = repo.status(node1=basenode, match=match)[:5]
485        modified, added, removed = changes[:3]
486        files = modified + added + removed
487        diffopts = mdiff.diffopts(git=True, nodates=True)
488        patch_diff = ''.join(patch.diff(repo, basenode, match=match,
489                             changes=changes, opts=diffopts))
490
491        fp = cStringIO.StringIO(patch_diff)
492        ac = parsepatch(fp)
493        fp.close()
494        chunks = filterpatch(ui, ac)
495        rc = refilterpatch(ac, chunks)
496
497        contenders = {}
498        for h in chunks:
499            try: contenders.update(dict.fromkeys(h.files()))
500            except AttributeError: pass
501
502        newfiles = [f for f in files if f in contenders]
503
504        if not newfiles:
505            ui.status(_('no changes to shelve\n'))
506            return 0
507
508        modified = dict.fromkeys(changes[0])
509
510        backupdir = repo.join('shelve-backups')
511
512        try:
513            bkfiles = [f for f in newfiles if f in modified]
514            backups = makebackup(ui, repo, backupdir, bkfiles)
515
516            # patch to shelve
517            sp = cStringIO.StringIO()
518            for c in chunks:
519                if c.filename() in backups:
520                    c.write(sp)
521            doshelve = sp.tell()
522            sp.seek(0)
523
524            # patch to apply to shelved files
525            fp = cStringIO.StringIO()
526            for c in rc:
527                if c.filename() in backups:
528                    c.write(fp)
529            dopatch = fp.tell()
530            fp.seek(0)
531
532            try:
533                # 3a. apply filtered patch to clean repo (clean)
534                if backups:
535                    hg.revert(repo, basenode, backups.has_key)
536
537                # 3b. apply filtered patch to clean repo (apply)
538                if dopatch:
539                    ui.debug(_('applying patch\n'))
540                    ui.debug(fp.getvalue())
541                    patch.internalpatch(fp, ui, 1, repo.root, eolmode=None)
542                del fp
543
544                # 3c. apply filtered patch to clean repo (shelve)
545                if doshelve:
546                    ui.debug(_('saving patch to shelve\n'))
547                    if opts['append']:
548                        f = repo.opener('shelve', "a")
549                    else:
550                        f = repo.opener('shelve', "w")
551                    f.write(sp.getvalue())
552                    del f
553                del sp
554            except:
555                try:
556                    for realname, tmpname in backups.iteritems():
557                        ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
558                        util.copyfile(tmpname, repo.wjoin(realname))
559                    ui.debug(_('removing shelve file\n'))
560                    os.unlink(repo.join('shelve'))
561                except (IOError, OSError), e:
562                    ui.warn(_('abort: backup restore failed, %s\n') % str(e))
563
564            return 0
565        finally:
566            delete_backup(ui, repo, backupdir)
567
568    fancyopts.fancyopts([], commands.commitopts, opts)
569    return cmdutil.commit(ui, repo, shelvefunc, pats, opts)
570
571
572def unshelve(ui, repo, *pats, **opts):
573    '''restore shelved changes'''
574
575    try:
576        fp = cStringIO.StringIO()
577        fp.write(repo.opener('shelve').read())
578    except:
579        ui.warn(_('nothing to unshelve\n'))
580    else:
581        if opts['inspect']:
582            ui.status(fp.getvalue())
583        else:
584            files = []
585            fp.seek(0)
586            for chunk in parsepatch(fp):
587                if isinstance(chunk, header):
588                    files += chunk.files()
589            backupdir = repo.join('shelve-backups')
590            try:
591                backups = makebackup(ui, repo, backupdir, set(files))
592            except:
593                ui.warn(_('unshelve backup aborted\n'))
594                delete_backup(ui, repo, backupdir)
595                raise
596
597            ui.debug(_('applying shelved patch\n'))
598            patchdone = 0
599            try:
600                try:
601                    fp.seek(0)
602                    pfiles = {}
603                    internalpatch(fp, ui, 1, repo.root, files=pfiles)
604                    hglib.updatedir(ui, repo, pfiles)
605                    patchdone = 1
606                except:
607                    if opts['force']:
608                        patchdone = 1
609                    else:
610                        ui.status(_('restoring backup files\n'))
611                        for realname, tmpname in backups.iteritems():
612                            ui.debug(_('restoring %r to %r\n') %
613                                     (tmpname, realname))
614                            util.copyfile(tmpname, repo.wjoin(realname))
615            finally:
616                delete_backup(ui, repo, backupdir)
617
618            if patchdone:
619                ui.debug(_('removing shelved patches\n'))
620                os.unlink(repo.join('shelve'))
621                ui.status(_('unshelve completed\n'))
622            else:
623                raise patch.PatchError
624
625
626def abandon(ui, repo):
627    '''abandon shelved changes'''
628    try:
629        if os.path.exists(repo.join('shelve')):
630            ui.debug(_('abandoning shelved file\n'))
631            os.unlink(repo.join('shelve'))
632            ui.status(_('shelved file abandoned\n'))
633        else:
634            ui.warn(_('nothing to abandon\n'))
635    except IOError:
636        ui.warn(_('abandon failed\n'))
637
638
639cmdtable = {
640    "shelve":
641        (shelve,
642         [('A', 'addremove', None,
643           _('mark new/missing files as added/removed before shelving')),
644          ('f', 'force', None,
645           _('overwrite existing shelve data')),
646          ('a', 'append', None,
647           _('append to existing shelve data')),
648         ] + commands.walkopts,
649         _('hg shelve [OPTION]... [FILE]...')),
650    "unshelve":
651        (unshelve,
652         [('i', 'inspect', None, _('inspect shelved changes only')),
653          ('f', 'force', None,
654           _('proceed even if patches do not unshelve cleanly')),
655         ],
656         _('hg unshelve [OPTION]... [FILE]...')),
657}