1
# chunks.py - status/commit chunk handling for TortoiseHg
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>
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.
16
from mercurial import cmdutil, util, patch, mdiff, error
18
from tortoisehg.util import hglib, hgshelve
19
from tortoisehg.util.i18n import _
21
from tortoisehg.hgtk import gtklib
23
# diffmodel row enumerations
32
def hunk_markup(text):
33
'Format a diff hunk for display in a TreeView row with markup'
35
# don't use splitlines, should split with only LF for the patch
36
lines = hglib.tounicode(text).split(u'\n')
38
line = hglib.toutf(line[:512]) + '\n'
39
if line.startswith('---') or line.startswith('+++'):
40
hunk += gtklib.markup(line, color=gtklib.DBLUE)
41
elif line.startswith('-'):
42
hunk += gtklib.markup(line, color=gtklib.DRED)
43
elif line.startswith('+'):
44
hunk += gtklib.markup(line, color=gtklib.DGREEN)
45
elif line.startswith('@@'):
46
hunk = gtklib.markup(line, color=gtklib.DORANGE)
48
hunk += gtklib.markup(line)
52
def hunk_unmarkup(text):
53
'Format a diff hunk for display in a TreeView row without markup'
55
# don't use splitlines, should split with only LF for the patch
56
lines = hglib.tounicode(text).split(u'\n')
58
hunk += gtklib.markup(hglib.toutf(line[:512])) + '\n'
62
def check_max_diff(ctx, wfile):
64
fctx = ctx.filectx(wfile)
66
if not os.path.isfile(ctx._repo.wjoin(wfile)):
68
except (EnvironmentError, error.LookupError):
70
if size > hglib.getmaxdiffsize(ctx._repo.ui):
71
# Fake patch that displays size warning
72
lines = ['diff --git a/%s b/%s\n' % (wfile, wfile)]
73
lines.append(_('File is larger than the specified max size.\n'))
74
lines.append(_('Hunk selection is disabled for this file.\n'))
75
lines.append('--- a/%s\n' % wfile)
76
lines.append('+++ b/%s\n' % wfile)
79
contents = fctx.data()
80
except (EnvironmentError, util.Abort):
83
# Fake patch that displays binary file warning
84
lines = ['diff --git a/%s b/%s\n' % (wfile, wfile)]
85
lines.append(_('File is binary.\n'))
86
lines.append(_('Hunk selection is disabled for this file.\n'))
87
lines.append('--- a/%s\n' % wfile)
88
lines.append('+++ b/%s\n' % wfile)
95
def __init__(self, stat):
98
self.diffmodelfile = None
102
if self._difftree != None:
103
return self._difftree
105
self.diffmodel = gtk.ListStore(
111
pango.FontDescription # DM_FONT
114
dt = gtk.TreeView(self.diffmodel)
117
dt.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
118
dt.set_headers_visible(False)
119
dt.set_enable_search(False)
120
if getattr(dt, 'enable-grid-lines', None) is not None:
121
dt.set_property('enable-grid-lines', True)
123
dt.connect('row-activated', self.diff_tree_row_act)
124
dt.connect('copy-clipboard', self.copy_to_clipboard)
126
cell = gtk.CellRendererText()
127
diffcol = gtk.TreeViewColumn('diff', cell)
128
diffcol.set_resizable(True)
129
diffcol.add_attribute(cell, 'markup', DM_DISP_TEXT)
131
# differentiate header chunks
132
cell.set_property('cell-background', gtklib.STATUS_HEADER)
133
diffcol.add_attribute(cell, 'cell_background_set', DM_IS_HEADER)
134
self.headerfont = self.stat.difffont.copy()
135
self.headerfont.set_weight(pango.WEIGHT_HEAVY)
137
# differentiate rejected hunks
138
self.rejfont = self.stat.difffont.copy()
139
self.rejfont.set_weight(pango.WEIGHT_LIGHT)
140
diffcol.add_attribute(cell, 'font-desc', DM_FONT)
141
cell.set_property('background', gtklib.STATUS_REJECT_BACKGROUND)
142
cell.set_property('foreground', gtklib.STATUS_REJECT_FOREGROUND)
143
diffcol.add_attribute(cell, 'background-set', DM_REJECTED)
144
diffcol.add_attribute(cell, 'foreground-set', DM_REJECTED)
145
dt.append_column(diffcol)
149
def __getitem__(self, wfile):
150
return self.filechunks[wfile]
152
def __contains__(self, wfile):
153
return wfile in self.filechunks
155
def clear_filechunks(self):
159
self.diffmodel.clear()
160
self.diffmodelfile = None
162
def del_file(self, wfile):
163
if wfile in self.filechunks:
164
del self.filechunks[wfile]
166
def update_chunk_state(self, wfile, selected):
167
if wfile not in self.filechunks:
169
chunks = self.filechunks[wfile]
171
chunk.active = selected
172
if wfile != self.diffmodelfile:
174
for n, chunk in enumerate(chunks):
177
self.diffmodel[n][DM_REJECTED] = not selected
178
self.update_diff_hunk(self.diffmodel[n])
179
self.update_diff_header(self.diffmodel, wfile, selected)
181
def update_diff_hunk(self, row):
182
'Update the contents of a diff row based on its chunk state'
184
chunks = self.filechunks[wfile]
185
chunk = chunks[row[DM_CHUNK_ID]]
186
buf = cStringIO.StringIO()
190
row[DM_REJECTED] = False
191
row[DM_FONT] = self.stat.difffont
192
row[DM_DISP_TEXT] = hunk_markup(buf.read())
194
row[DM_REJECTED] = True
195
row[DM_FONT] = self.rejfont
196
row[DM_DISP_TEXT] = hunk_unmarkup(buf.read())
198
def update_diff_header(self, dmodel, wfile, selected):
200
chunks = self.filechunks[wfile]
203
lasthunk = len(chunks)-1
204
sel = lambda x: x >= lasthunk or not dmodel[x+1][DM_REJECTED]
205
newtext = chunks[0].selpretty(sel)
207
newtext = "<span foreground='" + gtklib.STATUS_REJECT_FOREGROUND + \
208
"'>" + newtext + "</span>"
209
dmodel[0][DM_DISP_TEXT] = newtext
211
def get_chunks(self, wfile): # new
212
if wfile in self.filechunks:
213
chunks = self.filechunks[wfile]
215
chunks = self.read_file_chunks(wfile)
219
self.filechunks[wfile] = chunks
222
def update_hunk_model(self, wfile, checked):
223
# Read this file's diffs into hunk selection model
224
self.diffmodel.clear()
225
self.diffmodelfile = wfile
226
if not self.stat.is_merge():
227
self.append_diff_hunks(wfile, checked)
230
return len(self.diffmodel)
232
def append_diff_hunks(self, wfile, checked):
233
'Append diff hunks of one file to the diffmodel'
234
chunks = self.read_file_chunks(wfile)
236
if wfile in self.filechunks:
237
del self.filechunks[wfile]
241
for n, chunk in enumerate(chunks):
242
if isinstance(chunk, hgshelve.header):
243
# header chunk is always active
245
rows.append([False, '', True, wfile, n, self.headerfont])
250
# chunks take file's selection state by default
251
chunk.active = checked
252
rows.append([False, '', False, wfile, n, self.stat.difffont])
254
# recover old chunk selection/rejection states, match fromline
255
if wfile in self.filechunks:
256
ochunks = self.filechunks[wfile]
258
for oc in ochunks[1:]:
259
for n in xrange(next, len(chunks)):
261
if oc.fromline == nc.fromline:
262
nc.active = oc.active
265
elif nc.fromline > oc.fromline:
268
self.filechunks[wfile] = chunks
270
# Set row status based on chunk state
271
rej, nonrej = False, False
272
for n, row in enumerate(rows):
273
if not row[DM_IS_HEADER]:
278
row[DM_REJECTED] = not chunks[n].active
279
self.update_diff_hunk(row)
280
self.diffmodel.append(row)
286
self.update_diff_header(self.diffmodel, wfile, newvalue)
290
def diff_tree_row_act(self, dtree, path, column):
291
'Row in diff tree (hunk) activated/toggled'
292
dmodel = dtree.get_model()
295
checked = self.stat.get_checked(wfile)
297
chunks = self.filechunks[wfile]
300
chunkrows = xrange(1, len(chunks))
301
if row[DM_IS_HEADER]:
302
for n, chunk in enumerate(chunks[1:]):
303
chunk.active = not checked
304
self.update_diff_hunk(dmodel[n+1])
305
newvalue = not checked
308
chunk = chunks[row[DM_CHUNK_ID]]
309
chunk.active = not chunk.active
310
self.update_diff_hunk(row)
311
rej = [ n for n in chunkrows if dmodel[n][DM_REJECTED] ]
312
nonrej = [ n for n in chunkrows if not dmodel[n][DM_REJECTED] ]
313
newvalue = nonrej and True or False
314
partial = rej and nonrej and True or False
315
self.update_diff_header(dmodel, wfile, newvalue)
317
self.stat.update_check_state(wfile, partial, newvalue)
319
def get_wfile(self, dtree, path):
320
dmodel = dtree.get_model()
325
def save(self, files, patchfilename):
326
buf = cStringIO.StringIO()
327
dmodel = self.diffmodel
329
if wfile in self.filechunks:
330
chunks = self.filechunks[wfile]
332
chunks = self.read_file_chunks(wfile)
335
for i, chunk in enumerate(chunks):
343
fp = open(patchfilename, 'wb')
350
def copy_to_clipboard(self, treeview):
351
'Write highlighted hunks to the clipboard'
352
if not treeview.is_focus():
353
w = self.stat.get_focus()
354
w.emit('copy-clipboard')
357
model, tpaths = treeview.get_selection().get_selected_rows()
359
wfile, cid = model[row][DM_PATH], model[row][DM_CHUNK_ID]
360
if wfile not in saves:
363
saves[wfile].append(cid)
364
fp = cStringIO.StringIO()
365
for wfile in saves.keys():
368
for cid in saves[wfile]:
370
chunks[cid].write(fp)
372
self.stat.clipboard.set_text(fp.read())
374
def read_file_chunks(self, wfile):
375
'Get diffs of working file, parse into (c)hunks'
376
difftext = cStringIO.StringIO()
377
pfile = util.pconvert(wfile)
378
lines = check_max_diff(self.stat.get_ctx(), pfile)
380
difftext.writelines(lines)
383
matcher = cmdutil.matchfiles(self.stat.repo, [pfile])
384
diffopts = mdiff.diffopts(git=True, nodates=True)
386
node1, node2 = self.stat.nodes()
387
for s in patch.diff(self.stat.repo, node1, node2,
388
match=matcher, opts=diffopts):
389
difftext.writelines(s.splitlines(True))
390
except (IOError, error.RepoError, error.LookupError, util.Abort), e:
391
self.stat.stbar.set_text(str(e))
393
return hgshelve.parsepatch(difftext)