PageRenderTime 367ms CodeModel.GetById 34ms RepoModel.GetById 3ms app.codeStats 0ms

/tortoisehg/util/hgshelve.py

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