PageRenderTime 38ms CodeModel.GetById 9ms app.highlight 23ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgtk/chunks.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 393 lines | 341 code | 36 blank | 16 comment | 49 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
 11import gtk
 12import os
 13import pango
 14import cStringIO
 15
 16from mercurial import cmdutil, util, patch, mdiff, error
 17
 18from tortoisehg.util import hglib, hgshelve
 19from tortoisehg.util.i18n import _
 20
 21from tortoisehg.hgtk import gtklib
 22
 23# diffmodel row enumerations
 24DM_REJECTED  = 0
 25DM_DISP_TEXT = 1
 26DM_IS_HEADER = 2
 27DM_PATH      = 3
 28DM_CHUNK_ID  = 4
 29DM_FONT      = 5
 30
 31
 32def 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
 52def 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
 62def 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
 93class 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)