/tortoisehg/hgtk/chunks.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 393 lines · 325 code · 47 blank · 21 comment · 88 complexity · 68c578c4f7a041b7256f278cda8edd4a MD5 · raw file

  1. # chunks.py - status/commit chunk handling for TortoiseHg
  2. #
  3. # Copyright 2007 Brad Schick, brad at gmail . com
  4. # Copyright 2007 TK Soh <teekaysoh@gmail.com>
  5. # Copyright 2008 Steve Borho <steve@borho.org>
  6. # Copyright 2008 Emmanuel Rosa <goaway1000@gmail.com>
  7. #
  8. # This software may be used and distributed according to the terms of the
  9. # GNU General Public License version 2, incorporated herein by reference.
  10. import gtk
  11. import os
  12. import pango
  13. import cStringIO
  14. from mercurial import cmdutil, util, patch, mdiff, error
  15. from tortoisehg.util import hglib, hgshelve
  16. from tortoisehg.util.i18n import _
  17. from tortoisehg.hgtk import gtklib
  18. # diffmodel row enumerations
  19. DM_REJECTED = 0
  20. DM_DISP_TEXT = 1
  21. DM_IS_HEADER = 2
  22. DM_PATH = 3
  23. DM_CHUNK_ID = 4
  24. DM_FONT = 5
  25. def hunk_markup(text):
  26. 'Format a diff hunk for display in a TreeView row with markup'
  27. hunk = ''
  28. # don't use splitlines, should split with only LF for the patch
  29. lines = hglib.tounicode(text).split(u'\n')
  30. for line in lines:
  31. line = hglib.toutf(line[:512]) + '\n'
  32. if line.startswith('---') or line.startswith('+++'):
  33. hunk += gtklib.markup(line, color=gtklib.DBLUE)
  34. elif line.startswith('-'):
  35. hunk += gtklib.markup(line, color=gtklib.DRED)
  36. elif line.startswith('+'):
  37. hunk += gtklib.markup(line, color=gtklib.DGREEN)
  38. elif line.startswith('@@'):
  39. hunk = gtklib.markup(line, color=gtklib.DORANGE)
  40. else:
  41. hunk += gtklib.markup(line)
  42. return hunk
  43. def hunk_unmarkup(text):
  44. 'Format a diff hunk for display in a TreeView row without markup'
  45. hunk = ''
  46. # don't use splitlines, should split with only LF for the patch
  47. lines = hglib.tounicode(text).split(u'\n')
  48. for line in lines:
  49. hunk += gtklib.markup(hglib.toutf(line[:512])) + '\n'
  50. return hunk
  51. def check_max_diff(ctx, wfile):
  52. try:
  53. fctx = ctx.filectx(wfile)
  54. size = fctx.size()
  55. if not os.path.isfile(ctx._repo.wjoin(wfile)):
  56. return []
  57. except (EnvironmentError, error.LookupError):
  58. return []
  59. if size > hglib.getmaxdiffsize(ctx._repo.ui):
  60. # Fake patch that displays size warning
  61. lines = ['diff --git a/%s b/%s\n' % (wfile, wfile)]
  62. lines.append(_('File is larger than the specified max size.\n'))
  63. lines.append(_('Hunk selection is disabled for this file.\n'))
  64. lines.append('--- a/%s\n' % wfile)
  65. lines.append('+++ b/%s\n' % wfile)
  66. return lines
  67. try:
  68. contents = fctx.data()
  69. except (EnvironmentError, util.Abort):
  70. return []
  71. if '\0' in contents:
  72. # Fake patch that displays binary file warning
  73. lines = ['diff --git a/%s b/%s\n' % (wfile, wfile)]
  74. lines.append(_('File is binary.\n'))
  75. lines.append(_('Hunk selection is disabled for this file.\n'))
  76. lines.append('--- a/%s\n' % wfile)
  77. lines.append('+++ b/%s\n' % wfile)
  78. return lines
  79. return []
  80. class chunks(object):
  81. def __init__(self, stat):
  82. self.stat = stat
  83. self.filechunks = {}
  84. self.diffmodelfile = None
  85. self._difftree = None
  86. def difftree(self):
  87. if self._difftree != None:
  88. return self._difftree
  89. self.diffmodel = gtk.ListStore(
  90. bool, # DM_REJECTED
  91. str, # DM_DISP_TEXT
  92. bool, # DM_IS_HEADER
  93. str, # DM_PATH
  94. int, # DM_CHUNK_ID
  95. pango.FontDescription # DM_FONT
  96. )
  97. dt = gtk.TreeView(self.diffmodel)
  98. self._difftree = dt
  99. dt.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
  100. dt.set_headers_visible(False)
  101. dt.set_enable_search(False)
  102. if getattr(dt, 'enable-grid-lines', None) is not None:
  103. dt.set_property('enable-grid-lines', True)
  104. dt.connect('row-activated', self.diff_tree_row_act)
  105. dt.connect('copy-clipboard', self.copy_to_clipboard)
  106. cell = gtk.CellRendererText()
  107. diffcol = gtk.TreeViewColumn('diff', cell)
  108. diffcol.set_resizable(True)
  109. diffcol.add_attribute(cell, 'markup', DM_DISP_TEXT)
  110. # differentiate header chunks
  111. cell.set_property('cell-background', gtklib.STATUS_HEADER)
  112. diffcol.add_attribute(cell, 'cell_background_set', DM_IS_HEADER)
  113. self.headerfont = self.stat.difffont.copy()
  114. self.headerfont.set_weight(pango.WEIGHT_HEAVY)
  115. # differentiate rejected hunks
  116. self.rejfont = self.stat.difffont.copy()
  117. self.rejfont.set_weight(pango.WEIGHT_LIGHT)
  118. diffcol.add_attribute(cell, 'font-desc', DM_FONT)
  119. cell.set_property('background', gtklib.STATUS_REJECT_BACKGROUND)
  120. cell.set_property('foreground', gtklib.STATUS_REJECT_FOREGROUND)
  121. diffcol.add_attribute(cell, 'background-set', DM_REJECTED)
  122. diffcol.add_attribute(cell, 'foreground-set', DM_REJECTED)
  123. dt.append_column(diffcol)
  124. return dt
  125. def __getitem__(self, wfile):
  126. return self.filechunks[wfile]
  127. def __contains__(self, wfile):
  128. return wfile in self.filechunks
  129. def clear_filechunks(self):
  130. self.filechunks = {}
  131. def clear(self):
  132. self.diffmodel.clear()
  133. self.diffmodelfile = None
  134. def del_file(self, wfile):
  135. if wfile in self.filechunks:
  136. del self.filechunks[wfile]
  137. def update_chunk_state(self, wfile, selected):
  138. if wfile not in self.filechunks:
  139. return
  140. chunks = self.filechunks[wfile]
  141. for chunk in chunks:
  142. chunk.active = selected
  143. if wfile != self.diffmodelfile:
  144. return
  145. for n, chunk in enumerate(chunks):
  146. if n == 0:
  147. continue
  148. self.diffmodel[n][DM_REJECTED] = not selected
  149. self.update_diff_hunk(self.diffmodel[n])
  150. self.update_diff_header(self.diffmodel, wfile, selected)
  151. def update_diff_hunk(self, row):
  152. 'Update the contents of a diff row based on its chunk state'
  153. wfile = row[DM_PATH]
  154. chunks = self.filechunks[wfile]
  155. chunk = chunks[row[DM_CHUNK_ID]]
  156. buf = cStringIO.StringIO()
  157. chunk.pretty(buf)
  158. buf.seek(0)
  159. if chunk.active:
  160. row[DM_REJECTED] = False
  161. row[DM_FONT] = self.stat.difffont
  162. row[DM_DISP_TEXT] = hunk_markup(buf.read())
  163. else:
  164. row[DM_REJECTED] = True
  165. row[DM_FONT] = self.rejfont
  166. row[DM_DISP_TEXT] = hunk_unmarkup(buf.read())
  167. def update_diff_header(self, dmodel, wfile, selected):
  168. try:
  169. chunks = self.filechunks[wfile]
  170. except IndexError:
  171. return
  172. lasthunk = len(chunks)-1
  173. sel = lambda x: x >= lasthunk or not dmodel[x+1][DM_REJECTED]
  174. newtext = chunks[0].selpretty(sel)
  175. if not selected:
  176. newtext = "<span foreground='" + gtklib.STATUS_REJECT_FOREGROUND + \
  177. "'>" + newtext + "</span>"
  178. dmodel[0][DM_DISP_TEXT] = newtext
  179. def get_chunks(self, wfile): # new
  180. if wfile in self.filechunks:
  181. chunks = self.filechunks[wfile]
  182. else:
  183. chunks = self.read_file_chunks(wfile)
  184. if chunks:
  185. for c in chunks:
  186. c.active = True
  187. self.filechunks[wfile] = chunks
  188. return chunks
  189. def update_hunk_model(self, wfile, checked):
  190. # Read this file's diffs into hunk selection model
  191. self.diffmodel.clear()
  192. self.diffmodelfile = wfile
  193. if not self.stat.is_merge():
  194. self.append_diff_hunks(wfile, checked)
  195. def len(self):
  196. return len(self.diffmodel)
  197. def append_diff_hunks(self, wfile, checked):
  198. 'Append diff hunks of one file to the diffmodel'
  199. chunks = self.read_file_chunks(wfile)
  200. if not chunks:
  201. if wfile in self.filechunks:
  202. del self.filechunks[wfile]
  203. return 0
  204. rows = []
  205. for n, chunk in enumerate(chunks):
  206. if isinstance(chunk, hgshelve.header):
  207. # header chunk is always active
  208. chunk.active = True
  209. rows.append([False, '', True, wfile, n, self.headerfont])
  210. if chunk.special():
  211. chunks = chunks[:1]
  212. break
  213. else:
  214. # chunks take file's selection state by default
  215. chunk.active = checked
  216. rows.append([False, '', False, wfile, n, self.stat.difffont])
  217. # recover old chunk selection/rejection states, match fromline
  218. if wfile in self.filechunks:
  219. ochunks = self.filechunks[wfile]
  220. next = 1
  221. for oc in ochunks[1:]:
  222. for n in xrange(next, len(chunks)):
  223. nc = chunks[n]
  224. if oc.fromline == nc.fromline:
  225. nc.active = oc.active
  226. next = n+1
  227. break
  228. elif nc.fromline > oc.fromline:
  229. break
  230. self.filechunks[wfile] = chunks
  231. # Set row status based on chunk state
  232. rej, nonrej = False, False
  233. for n, row in enumerate(rows):
  234. if not row[DM_IS_HEADER]:
  235. if chunks[n].active:
  236. nonrej = True
  237. else:
  238. rej = True
  239. row[DM_REJECTED] = not chunks[n].active
  240. self.update_diff_hunk(row)
  241. self.diffmodel.append(row)
  242. if len(rows) == 1:
  243. newvalue = checked
  244. else:
  245. newvalue = nonrej
  246. self.update_diff_header(self.diffmodel, wfile, newvalue)
  247. return len(rows)
  248. def diff_tree_row_act(self, dtree, path, column):
  249. 'Row in diff tree (hunk) activated/toggled'
  250. dmodel = dtree.get_model()
  251. row = dmodel[path]
  252. wfile = row[DM_PATH]
  253. checked = self.stat.get_checked(wfile)
  254. try:
  255. chunks = self.filechunks[wfile]
  256. except IndexError:
  257. pass
  258. chunkrows = xrange(1, len(chunks))
  259. if row[DM_IS_HEADER]:
  260. for n, chunk in enumerate(chunks[1:]):
  261. chunk.active = not checked
  262. self.update_diff_hunk(dmodel[n+1])
  263. newvalue = not checked
  264. partial = False
  265. else:
  266. chunk = chunks[row[DM_CHUNK_ID]]
  267. chunk.active = not chunk.active
  268. self.update_diff_hunk(row)
  269. rej = [ n for n in chunkrows if dmodel[n][DM_REJECTED] ]
  270. nonrej = [ n for n in chunkrows if not dmodel[n][DM_REJECTED] ]
  271. newvalue = nonrej and True or False
  272. partial = rej and nonrej and True or False
  273. self.update_diff_header(dmodel, wfile, newvalue)
  274. self.stat.update_check_state(wfile, partial, newvalue)
  275. def get_wfile(self, dtree, path):
  276. dmodel = dtree.get_model()
  277. row = dmodel[path]
  278. wfile = row[DM_PATH]
  279. return wfile
  280. def save(self, files, patchfilename):
  281. buf = cStringIO.StringIO()
  282. dmodel = self.diffmodel
  283. for wfile in files:
  284. if wfile in self.filechunks:
  285. chunks = self.filechunks[wfile]
  286. else:
  287. chunks = self.read_file_chunks(wfile)
  288. for c in chunks:
  289. c.active = True
  290. for i, chunk in enumerate(chunks):
  291. if i == 0:
  292. chunk.write(buf)
  293. elif chunk.active:
  294. chunk.write(buf)
  295. buf.seek(0)
  296. try:
  297. try:
  298. fp = open(patchfilename, 'wb')
  299. fp.write(buf.read())
  300. except OSError:
  301. pass
  302. finally:
  303. fp.close()
  304. def copy_to_clipboard(self, treeview):
  305. 'Write highlighted hunks to the clipboard'
  306. if not treeview.is_focus():
  307. w = self.stat.get_focus()
  308. w.emit('copy-clipboard')
  309. return False
  310. saves = {}
  311. model, tpaths = treeview.get_selection().get_selected_rows()
  312. for row, in tpaths:
  313. wfile, cid = model[row][DM_PATH], model[row][DM_CHUNK_ID]
  314. if wfile not in saves:
  315. saves[wfile] = [cid]
  316. else:
  317. saves[wfile].append(cid)
  318. fp = cStringIO.StringIO()
  319. for wfile in saves.keys():
  320. chunks = self[wfile]
  321. chunks[0].write(fp)
  322. for cid in saves[wfile]:
  323. if cid != 0:
  324. chunks[cid].write(fp)
  325. fp.seek(0)
  326. self.stat.clipboard.set_text(fp.read())
  327. def read_file_chunks(self, wfile):
  328. 'Get diffs of working file, parse into (c)hunks'
  329. difftext = cStringIO.StringIO()
  330. pfile = util.pconvert(wfile)
  331. lines = check_max_diff(self.stat.get_ctx(), pfile)
  332. if lines:
  333. difftext.writelines(lines)
  334. difftext.seek(0)
  335. else:
  336. matcher = cmdutil.matchfiles(self.stat.repo, [pfile])
  337. diffopts = mdiff.diffopts(git=True, nodates=True)
  338. try:
  339. node1, node2 = self.stat.nodes()
  340. for s in patch.diff(self.stat.repo, node1, node2,
  341. match=matcher, opts=diffopts):
  342. difftext.writelines(s.splitlines(True))
  343. except (IOError, error.RepoError, error.LookupError, util.Abort), e:
  344. self.stat.stbar.set_text(str(e))
  345. difftext.seek(0)
  346. return hgshelve.parsepatch(difftext)