~ubuntu-branches/debian/sid/tortoisehg/sid

« back to all changes in this revision

Viewing changes to tortoisehg/hgtk/chunks.py

  • Committer: Bazaar Package Importer
  • Author(s): Ludovico Cavedon
  • Date: 2011-03-18 19:57:13 UTC
  • mfrom: (1.2.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20110318195713-nh62nv9y26rrdtmh
Tags: 2.0.2-1
* New upstream release.
* Update copyright file.
* Depend on PyQt4 libraries rather than on PyGtk.
* Rename manpage from hgtk to thg.
* No longer include _hgtk zsh completion file.
* Install wrapper script hgtk and add manpage link for it (LP: #732328).
* Update manpage with new options and commands.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
 
11
 
import gtk
12
 
import os
13
 
import pango
14
 
import cStringIO
15
 
 
16
 
from mercurial import cmdutil, util, patch, mdiff, error
17
 
 
18
 
from tortoisehg.util import hglib, hgshelve
19
 
from tortoisehg.util.i18n import _
20
 
 
21
 
from tortoisehg.hgtk import gtklib
22
 
 
23
 
# diffmodel row enumerations
24
 
DM_REJECTED  = 0
25
 
DM_DISP_TEXT = 1
26
 
DM_IS_HEADER = 2
27
 
DM_PATH      = 3
28
 
DM_CHUNK_ID  = 4
29
 
DM_FONT      = 5
30
 
 
31
 
 
32
 
def hunk_markup(text):
33
 
    'Format a diff hunk for display in a TreeView row with markup'
34
 
    hunk = ''
35
 
    # don't use splitlines, should split with only LF for the patch
36
 
    lines = hglib.tounicode(text).split(u'\n')
37
 
    for line in lines:
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)
47
 
        else:
48
 
            hunk += gtklib.markup(line)
49
 
    return hunk
50
 
 
51
 
 
52
 
def hunk_unmarkup(text):
53
 
    'Format a diff hunk for display in a TreeView row without markup'
54
 
    hunk = ''
55
 
    # don't use splitlines, should split with only LF for the patch
56
 
    lines = hglib.tounicode(text).split(u'\n')
57
 
    for line in lines:
58
 
        hunk += gtklib.markup(hglib.toutf(line[:512])) + '\n'
59
 
    return hunk
60
 
 
61
 
 
62
 
def check_max_diff(ctx, wfile):
63
 
    try:
64
 
        fctx = ctx.filectx(wfile)
65
 
        size = fctx.size()
66
 
        if not os.path.isfile(ctx._repo.wjoin(wfile)):
67
 
            return []
68
 
    except (EnvironmentError, error.LookupError):
69
 
        return []
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)
77
 
        return lines
78
 
    try:
79
 
        contents = fctx.data()
80
 
    except (EnvironmentError, util.Abort):
81
 
        return []
82
 
    if '\0' in contents:
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)
89
 
        return lines
90
 
    return []
91
 
 
92
 
 
93
 
class chunks(object):
94
 
 
95
 
    def __init__(self, stat):
96
 
        self.stat = stat
97
 
        self.filechunks = {}
98
 
        self.diffmodelfile = None
99
 
        self._difftree = None
100
 
 
101
 
    def difftree(self):
102
 
        if self._difftree != None:
103
 
            return self._difftree
104
 
 
105
 
        self.diffmodel = gtk.ListStore(
106
 
                bool, # DM_REJECTED
107
 
                str,  # DM_DISP_TEXT
108
 
                bool, # DM_IS_HEADER
109
 
                str,  # DM_PATH
110
 
                int,  # DM_CHUNK_ID
111
 
                pango.FontDescription # DM_FONT
112
 
            )
113
 
 
114
 
        dt = gtk.TreeView(self.diffmodel)
115
 
        self._difftree = dt
116
 
        
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)
122
 
 
123
 
        dt.connect('row-activated', self.diff_tree_row_act)
124
 
        dt.connect('copy-clipboard', self.copy_to_clipboard)
125
 
 
126
 
        cell = gtk.CellRendererText()
127
 
        diffcol = gtk.TreeViewColumn('diff', cell)
128
 
        diffcol.set_resizable(True)
129
 
        diffcol.add_attribute(cell, 'markup', DM_DISP_TEXT)
130
 
 
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)
136
 
 
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)
146
 
        
147
 
        return dt
148
 
 
149
 
    def __getitem__(self, wfile):
150
 
        return self.filechunks[wfile]
151
 
 
152
 
    def __contains__(self, wfile):
153
 
        return wfile in self.filechunks
154
 
 
155
 
    def clear_filechunks(self):
156
 
        self.filechunks = {}
157
 
 
158
 
    def clear(self):
159
 
        self.diffmodel.clear()
160
 
        self.diffmodelfile = None
161
 
 
162
 
    def del_file(self, wfile):
163
 
        if wfile in self.filechunks:
164
 
            del self.filechunks[wfile]
165
 
 
166
 
    def update_chunk_state(self, wfile, selected):
167
 
        if wfile not in self.filechunks:
168
 
            return
169
 
        chunks = self.filechunks[wfile]
170
 
        for chunk in chunks:
171
 
            chunk.active = selected
172
 
        if wfile != self.diffmodelfile:
173
 
            return
174
 
        for n, chunk in enumerate(chunks):
175
 
            if n == 0:
176
 
                continue
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)
180
 
 
181
 
    def update_diff_hunk(self, row):
182
 
        'Update the contents of a diff row based on its chunk state'
183
 
        wfile = row[DM_PATH]
184
 
        chunks = self.filechunks[wfile]
185
 
        chunk = chunks[row[DM_CHUNK_ID]]
186
 
        buf = cStringIO.StringIO()
187
 
        chunk.pretty(buf)
188
 
        buf.seek(0)
189
 
        if chunk.active:
190
 
            row[DM_REJECTED] = False
191
 
            row[DM_FONT] = self.stat.difffont
192
 
            row[DM_DISP_TEXT] = hunk_markup(buf.read())
193
 
        else:
194
 
            row[DM_REJECTED] = True
195
 
            row[DM_FONT] = self.rejfont
196
 
            row[DM_DISP_TEXT] = hunk_unmarkup(buf.read())
197
 
 
198
 
    def update_diff_header(self, dmodel, wfile, selected):
199
 
        try:
200
 
            chunks = self.filechunks[wfile]
201
 
        except IndexError:
202
 
            return
203
 
        lasthunk = len(chunks)-1
204
 
        sel = lambda x: x >= lasthunk or not dmodel[x+1][DM_REJECTED]
205
 
        newtext = chunks[0].selpretty(sel)
206
 
        if not selected:
207
 
            newtext = "<span foreground='" + gtklib.STATUS_REJECT_FOREGROUND + \
208
 
                "'>" + newtext + "</span>"
209
 
        dmodel[0][DM_DISP_TEXT] = newtext
210
 
 
211
 
    def get_chunks(self, wfile): # new
212
 
        if wfile in self.filechunks:
213
 
            chunks = self.filechunks[wfile]
214
 
        else:
215
 
            chunks = self.read_file_chunks(wfile)
216
 
            if chunks:
217
 
                for c in chunks:
218
 
                    c.active = True
219
 
                self.filechunks[wfile] = chunks
220
 
        return chunks
221
 
 
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)
228
 
 
229
 
    def len(self):
230
 
        return len(self.diffmodel)
231
 
 
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)
235
 
        if not chunks:
236
 
            if wfile in self.filechunks:
237
 
                del self.filechunks[wfile]
238
 
            return 0
239
 
 
240
 
        rows = []
241
 
        for n, chunk in enumerate(chunks):
242
 
            if isinstance(chunk, hgshelve.header):
243
 
                # header chunk is always active
244
 
                chunk.active = True
245
 
                rows.append([False, '', True, wfile, n, self.headerfont])
246
 
                if chunk.special():
247
 
                    chunks = chunks[:1]
248
 
                    break
249
 
            else:
250
 
                # chunks take file's selection state by default
251
 
                chunk.active = checked
252
 
                rows.append([False, '', False, wfile, n, self.stat.difffont])
253
 
 
254
 
        # recover old chunk selection/rejection states, match fromline
255
 
        if wfile in self.filechunks:
256
 
            ochunks = self.filechunks[wfile]
257
 
            next = 1
258
 
            for oc in ochunks[1:]:
259
 
                for n in xrange(next, len(chunks)):
260
 
                    nc = chunks[n]
261
 
                    if oc.fromline == nc.fromline:
262
 
                        nc.active = oc.active
263
 
                        next = n+1
264
 
                        break
265
 
                    elif nc.fromline > oc.fromline:
266
 
                        break
267
 
 
268
 
        self.filechunks[wfile] = chunks
269
 
 
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]:
274
 
                if chunks[n].active:
275
 
                    nonrej = True
276
 
                else:
277
 
                    rej = True
278
 
                row[DM_REJECTED] = not chunks[n].active
279
 
                self.update_diff_hunk(row)
280
 
            self.diffmodel.append(row)
281
 
 
282
 
        if len(rows) == 1:
283
 
            newvalue = checked
284
 
        else:
285
 
            newvalue = nonrej
286
 
        self.update_diff_header(self.diffmodel, wfile, newvalue)
287
 
        
288
 
        return len(rows)
289
 
 
290
 
    def diff_tree_row_act(self, dtree, path, column):
291
 
        'Row in diff tree (hunk) activated/toggled'
292
 
        dmodel = dtree.get_model()
293
 
        row = dmodel[path]
294
 
        wfile = row[DM_PATH]
295
 
        checked = self.stat.get_checked(wfile)
296
 
        try:
297
 
            chunks = self.filechunks[wfile]
298
 
        except IndexError:
299
 
            pass
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
306
 
            partial = False
307
 
        else:
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)
316
 
 
317
 
        self.stat.update_check_state(wfile, partial, newvalue)
318
 
 
319
 
    def get_wfile(self, dtree, path):
320
 
        dmodel = dtree.get_model()
321
 
        row = dmodel[path]
322
 
        wfile = row[DM_PATH]
323
 
        return wfile
324
 
 
325
 
    def save(self, files, patchfilename):
326
 
        buf = cStringIO.StringIO()
327
 
        dmodel = self.diffmodel
328
 
        for wfile in files:
329
 
            if wfile in self.filechunks:
330
 
                chunks = self.filechunks[wfile]
331
 
            else:
332
 
                chunks = self.read_file_chunks(wfile)
333
 
                for c in chunks:
334
 
                    c.active = True
335
 
            for i, chunk in enumerate(chunks):
336
 
                if i == 0:
337
 
                    chunk.write(buf)
338
 
                elif chunk.active:
339
 
                    chunk.write(buf)
340
 
        buf.seek(0)
341
 
        try:
342
 
            try:
343
 
                fp = open(patchfilename, 'wb')
344
 
                fp.write(buf.read())
345
 
            except OSError:
346
 
                pass
347
 
        finally:
348
 
            fp.close()
349
 
 
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')
355
 
            return False
356
 
        saves = {}
357
 
        model, tpaths = treeview.get_selection().get_selected_rows()
358
 
        for row, in tpaths:
359
 
            wfile, cid = model[row][DM_PATH], model[row][DM_CHUNK_ID]
360
 
            if wfile not in saves:
361
 
                saves[wfile] = [cid]
362
 
            else:
363
 
                saves[wfile].append(cid)
364
 
        fp = cStringIO.StringIO()
365
 
        for wfile in saves.keys():
366
 
            chunks = self[wfile]
367
 
            chunks[0].write(fp)
368
 
            for cid in saves[wfile]:
369
 
                if cid != 0:
370
 
                    chunks[cid].write(fp)
371
 
        fp.seek(0)
372
 
        self.stat.clipboard.set_text(fp.read())
373
 
 
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)
379
 
        if lines:
380
 
            difftext.writelines(lines)
381
 
            difftext.seek(0)
382
 
        else:
383
 
            matcher = cmdutil.matchfiles(self.stat.repo, [pfile])
384
 
            diffopts = mdiff.diffopts(git=True, nodates=True)
385
 
            try:
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))
392
 
            difftext.seek(0)
393
 
        return hgshelve.parsepatch(difftext)