]>
git.sthu.org Git - shutils.git/blob - hgext/hgshelve.py
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 # Copyright 2007 TK Soh <teekaysoh@gmailcom>
6 # This software may be used and distributed according to the terms of
7 # the GNU General Public License, incorporated herein by reference.
9 '''interactive change selection to set aside that may be restored later'''
11 from mercurial
.i18n
import _
12 from mercurial
import cmdutil
, commands
, cmdutil
, hg
, mdiff
, patch
, revlog
13 from mercurial
import util
, fancyopts
, extensions
14 import copy
, cStringIO
, errno
, operator
, os
, re
, shutil
, tempfile
16 lines_re
= re
.compile(r
'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
19 lr
= patch
.linereader(fp
)
21 def scanwhile(first
, p
):
38 if line
.startswith('diff --git a/'):
40 s
= line
.split(None, 1)
41 return not s
or s
[0] not in ('---', 'diff')
42 header
= scanwhile(line
, notheader
)
43 fromfile
= lr
.readline()
44 if fromfile
.startswith('---'):
45 tofile
= lr
.readline()
46 header
+= [fromfile
, tofile
]
51 yield 'context', scanwhile(line
, lambda l
: l
[0] in ' \\')
53 yield 'hunk', scanwhile(line
, lambda l
: l
[0] in '-+\\')
55 m
= lines_re
.match(line
)
57 yield 'range', m
.groups()
59 raise patch
.PatchError('unknown patch content: %r' % line
)
62 diff_re
= re
.compile('diff --git a/(.*) b/(.*)$')
63 allhunks_re
= re
.compile('(?:index|new file|deleted file) ')
64 pretty_re
= re
.compile('(?:new file|deleted file) ')
65 special_re
= re
.compile('(?:index|new|deleted|copy|rename) ')
67 def __init__(self
, header
):
73 if h
.startswith('index '):
78 if h
.startswith('index '):
79 fp
.write(_('this modifies a binary file (all or nothing)\n'))
81 if self
.pretty_re
.match(h
):
84 fp
.write(_('this is a binary file\n'))
86 if h
.startswith('---'):
87 fp
.write(_('%d hunks, %d lines changed\n') %
89 sum([h
.added
+ h
.removed
for h
in self
.hunks
])))
94 fp
.write(''.join(self
.header
))
98 if self
.allhunks_re
.match(h
):
102 fromfile
, tofile
= self
.diff_re
.match(self
.header
[0]).groups()
103 if fromfile
== tofile
:
105 return [fromfile
, tofile
]
108 return self
.files()[-1]
111 return '<header %s>' % (' '.join(map(repr, self
.files())))
114 for h
in self
.header
:
115 if self
.special_re
.match(h
):
118 def countchanges(hunk
):
119 add
= len([h
for h
in hunk
if h
[0] == '+'])
120 rem
= len([h
for h
in hunk
if h
[0] == '-'])
126 def __init__(self
, header
, fromline
, toline
, proc
, before
, hunk
, after
):
127 def trimcontext(number
, lines
):
128 delta
= len(lines
) - self
.maxcontext
129 if False and delta
> 0:
130 return number
+ delta
, lines
[:self
.maxcontext
]
134 self
.fromline
, self
.before
= trimcontext(fromline
, before
)
135 self
.toline
, self
.after
= trimcontext(toline
, after
)
138 self
.added
, self
.removed
= countchanges(self
.hunk
)
140 def __cmp__(self
, rhs
):
141 # since the hunk().toline needs to be adjusted when hunks are
142 # removed/added, we can't take it into account when we cmp
143 attrs
= ['header', 'fromline', 'proc', 'hunk', 'added', 'removed']
145 selfattr
= getattr(self
, attr
, None)
146 rhsattr
= getattr(rhs
, attr
, None)
148 if selfattr
is None or rhsattr
is None:
149 raise util
.Abort(_('non-existant attribute %s') % attr
)
151 rv
= cmp(selfattr
, rhsattr
)
158 delta
= len(self
.before
) + len(self
.after
)
159 if self
.after
and self
.after
[-1] == '\\ No newline at end of file\n':
161 fromlen
= delta
+ self
.removed
162 tolen
= delta
+ self
.added
163 fp
.write('@@ -%d,%d +%d,%d @@%s\n' %
164 (self
.fromline
, fromlen
, self
.toline
, tolen
,
165 self
.proc
and (' ' + self
.proc
)))
166 fp
.write(''.join(self
.before
+ self
.hunk
+ self
.after
))
171 return self
.header
.filename()
174 return '<hunk %r@%d>' % (self
.filename(), self
.fromline
)
177 class parser(object):
188 def addrange(self
, (fromstart
, fromend
, tostart
, toend
, proc
)):
189 self
.fromline
= int(fromstart
)
190 self
.toline
= int(tostart
)
193 def addcontext(self
, context
):
195 h
= hunk(self
.header
, self
.fromline
, self
.toline
, self
.proc
,
196 self
.before
, self
.hunk
, context
)
197 self
.header
.hunks
.append(h
)
198 self
.stream
.append(h
)
199 self
.fromline
+= len(self
.before
) + h
.removed
200 self
.toline
+= len(self
.before
) + h
.added
204 self
.context
= context
206 def addhunk(self
, hunk
):
208 self
.before
= self
.context
212 def newfile(self
, hdr
):
215 self
.stream
.append(h
)
223 'file': {'context': addcontext
,
227 'context': {'file': newfile
,
230 'hunk': {'context': addcontext
,
233 'range': {'context': addcontext
,
240 for newstate
, data
in scanpatch(fp
):
242 p
.transitions
[state
][newstate
](p
, data
)
244 raise patch
.PatchError('unhandled transition: %s -> %s' %
249 def filterpatch(ui
, chunks
, shouldprompt
=True):
250 chunks
= list(chunks
)
256 if isinstance(chunks
[-1], header
):
259 consumed
.append(chunks
.pop())
264 """ If we're not to prompt (i.e. they specified the --all flag)
265 we pre-emptively set the 'all' flag """
266 if shouldprompt
== False:
272 if resp_all
[0] is not None:
274 if resp_file
[0] is not None:
277 resps
= _('[Ynsfdaq?]')
278 choices
= (_('&Yes, shelve this change'),
279 _('&No, skip this change'),
280 _('&Skip remaining changes to this file'),
281 _('Shelve remaining changes to this &file'),
282 _('&Done, skip remaining changes and files'),
283 _('Shelve &all changes to all remaining files'),
284 _('&Quit, shelving no changes'),
286 r
= ui
.promptchoice("%s %s " % (query
, resps
), choices
)
288 c
= shelve
.__doc
__.find('y - shelve this change')
289 for l
in shelve
.__doc
__[c
:].splitlines():
290 if l
: ui
.write(_(l
.strip()) + '\n')
297 ret
= resp_file
[0] = 'n'
298 elif r
== 3: # file (shelve remaining)
299 ret
= resp_file
[0] = 'y'
300 elif r
== 4: # done, skip remaining
301 ret
= resp_all
[0] = 'n'
303 ret
= resp_all
[0] = 'y'
305 raise util
.Abort(_('user quit'))
309 if isinstance(chunk
, header
):
312 hdr
= ''.join(chunk
.header
)
317 if resp_all
[0] is None:
319 if shouldprompt
== True:
320 r
= prompt(_('shelve changes to %s?') %
321 _(' and ').join(map(repr, chunk
.files())))
326 applied
[chunk
.filename()] = [chunk
]
328 applied
[chunk
.filename()] += consumefile()
332 if resp_file
[0] is None and resp_all
[0] is None:
334 r
= prompt(_('shelve this change to %r?') %
338 chunk
= copy
.copy(chunk
)
339 chunk
.toline
+= fixoffset
340 applied
[chunk
.filename()].append(chunk
)
342 fixoffset
+= chunk
.removed
- chunk
.added
343 return reduce(operator
.add
, [h
for h
in applied
.itervalues()
344 if h
[0].special() or len(h
) > 1], [])
346 def refilterpatch(allchunk
, selected
):
347 ''' return unshelved chunks of files to be shelved '''
351 if isinstance(c
, header
):
352 if len(l
) > 1 and l
[0] in selected
:
355 elif c
not in selected
:
357 if len(l
) > 1 and l
[0] in selected
:
361 def makebackup(ui
, repo
, dir, files
):
365 if err
.errno
!= errno
.EEXIST
:
370 fd
, tmpname
= tempfile
.mkstemp(prefix
=f
.replace('/', '_')+'.',
373 ui
.debug('backup %r as %r\n' % (f
, tmpname
))
374 util
.copyfile(repo
.wjoin(f
), tmpname
)
379 def getshelfpath(repo
, name
):
381 shelfpath
= "shelves/" + name
383 # Check if a shelf from an older version exists
384 if os
.path
.isfile(repo
.join('shelve')):
387 shelfpath
= "shelves/default"
391 def shelve(ui
, repo
, *pats
, **opts
):
392 '''interactively select changes to set aside
394 If a list of files is omitted, all changes reported by "hg status"
395 will be candidates for shelving.
397 You will be prompted for whether to shelve changes to each
398 modified file, and for files with multiple changes, for each
401 The shelve command works with the Color extension to display
404 On each prompt, the following responses are possible::
406 y - shelve this change
409 s - skip remaining changes to this file
410 f - shelve remaining changes to this file
412 d - done, skip remaining changes and files
413 a - shelve all changes to all remaining files
414 q - quit, shelving no changes
418 if not ui
.interactive
:
419 raise util
.Abort(_('shelve can only be run interactively'))
421 # List all the active shelves by name and return '
426 forced
= opts
['force'] or opts
['append']
428 # Shelf name and path
429 shelfname
= opts
.get('name')
430 shelfpath
= getshelfpath(repo
, shelfname
)
432 if os
.path
.exists(repo
.join(shelfpath
)) and not forced
:
433 raise util
.Abort(_('shelve data already exists'))
435 def shelvefunc(ui
, repo
, message
, match
, opts
):
436 changes
= repo
.status(match
=match
)[:5]
437 modified
, added
, removed
= changes
[:3]
438 files
= modified
+ added
+ removed
439 diffopts
= mdiff
.diffopts(git
=True, nodates
=True)
440 patch_diff
= ''.join(patch
.diff(repo
, repo
.dirstate
.parents()[0],
441 match
=match
, changes
=changes
, opts
=diffopts
))
443 fp
= cStringIO
.StringIO(patch_diff
)
447 chunks
= filterpatch(ui
, ac
, not opts
['all'])
448 rc
= refilterpatch(ac
, chunks
)
452 try: contenders
.update(dict.fromkeys(h
.files()))
453 except AttributeError: pass
455 newfiles
= [f
for f
in files
if f
in contenders
]
458 ui
.status(_('no changes to shelve\n'))
461 modified
= dict.fromkeys(changes
[0])
463 backupdir
= repo
.join('shelve-backups')
466 bkfiles
= [f
for f
in newfiles
if f
in modified
]
467 backups
= makebackup(ui
, repo
, backupdir
, bkfiles
)
470 sp
= cStringIO
.StringIO()
472 if c
.filename() in backups
:
477 # patch to apply to shelved files
478 fp
= cStringIO
.StringIO()
480 if c
.filename() in backups
:
486 # 3a. apply filtered patch to clean repo (clean)
488 hg
.revert(repo
, repo
.dirstate
.parents()[0], backups
.has_key
)
490 # 3b. apply filtered patch to clean repo (apply)
492 ui
.debug('applying patch\n')
493 ui
.debug(fp
.getvalue())
494 patch
.internalpatch(ui
, repo
, fp
, 1)
497 # 3c. apply filtered patch to clean repo (shelve)
499 ui
.debug("saving patch to shelve\n")
501 f
= repo
.opener(shelfpath
, "a")
503 f
= repo
.opener(shelfpath
, "w")
504 f
.write(sp
.getvalue())
509 for realname
, tmpname
in backups
.iteritems():
510 ui
.debug('restoring %r to %r\n' % (tmpname
, realname
))
511 util
.copyfile(tmpname
, repo
.wjoin(realname
))
512 ui
.debug('removing shelve file\n')
513 os
.unlink(repo
.join(shelfpath
))
520 for realname
, tmpname
in backups
.iteritems():
521 ui
.debug('removing backup for %r : %r\n' % (realname
, tmpname
))
526 fancyopts
.fancyopts([], commands
.commitopts
, opts
)
528 # wrap ui.write so diff output can be labeled/colorized
529 def wrapwrite(orig
, *args
, **kw
):
530 label
= kw
.pop('label', '')
531 if label
: label
+= ' '
532 for chunk
, l
in patch
.difflabel(lambda: args
):
533 orig(chunk
, label
=label
+ l
)
535 extensions
.wrapfunction(ui
, 'write', wrapwrite
)
537 return cmdutil
.commit(ui
, repo
, shelvefunc
, pats
, opts
)
541 def listshelves(ui
, repo
):
542 # Check for shelve file at old location first
543 if os
.path
.isfile(repo
.join('shelve')):
544 ui
.status('default\n')
546 # Now go through all the files in the shelves folder and list them out
547 dirname
= repo
.join('shelves')
548 if os
.path
.isdir(dirname
):
549 for filename
in os
.listdir(repo
.join('shelves')):
550 ui
.status(filename
+ '\n')
552 def unshelve(ui
, repo
, **opts
):
553 '''restore shelved changes'''
555 # Shelf name and path
556 shelfname
= opts
.get('name')
557 shelfpath
= getshelfpath(repo
, shelfname
)
559 # List all the active shelves by name and return '
565 patch_diff
= repo
.opener(shelfpath
).read()
566 fp
= cStringIO
.StringIO(patch_diff
)
568 # wrap ui.write so diff output can be labeled/colorized
569 def wrapwrite(orig
, *args
, **kw
):
570 label
= kw
.pop('label', '')
571 if label
: label
+= ' '
572 for chunk
, l
in patch
.difflabel(lambda: args
):
573 orig(chunk
, label
=label
+ l
)
575 extensions
.wrapfunction(ui
, 'write', wrapwrite
)
577 ui
.status(fp
.getvalue())
584 if isinstance(chunk
, header
):
585 files
+= chunk
.files()
586 backupdir
= repo
.join('shelve-backups')
587 backups
= makebackup(ui
, repo
, backupdir
, set(files
))
589 ui
.debug('applying shelved patch\n')
594 patch
.internalpatch(ui
, repo
, fp
, 1)
600 ui
.status('restoring backup files\n')
601 for realname
, tmpname
in backups
.iteritems():
602 ui
.debug('restoring %r to %r\n' %
604 util
.copyfile(tmpname
, repo
.wjoin(realname
))
607 ui
.debug('removing backup files\n')
608 shutil
.rmtree(backupdir
, True)
613 ui
.debug("removing shelved patches\n")
614 os
.unlink(repo
.join(shelfpath
))
615 ui
.status("unshelve completed\n")
617 ui
.warn('nothing to unshelve\n')
622 [('A', 'addremove', None,
623 _('mark new/missing files as added/removed before shelving')),
625 _('overwrite existing shelve data')),
626 ('a', 'append', None,
627 _('append to existing shelve data')),
629 _('shelve all changes')),
631 _('shelve changes to specified shelf name')),
632 ('l', 'list', None, _('list active shelves')),
633 ] + commands
.walkopts
,
634 _('hg shelve [OPTION]... [FILE]...')),
637 [('i', 'inspect', None, _('inspect shelved changes only')),
639 _('proceed even if patches do not unshelve cleanly')),
641 _('unshelve changes from specified shelf name')),
642 ('l', 'list', None, _('list active shelves')),
644 _('hg unshelve [OPTION]...')),