/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz

« back to all changes in this revision

Viewing changes to annotate/gannotate.py

  • Committer: Aaron Bentley
  • Date: 2006-12-20 20:56:41 UTC
  • mto: This revision was merged to the branch mainline in revision 126.
  • Revision ID: abentley@panoramicfeedback.com-20061220205641-d1d8esw86pl3xkla
Gannotate works with branches, not just trees

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Dan Loda <danloda@gmail.com>
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import time
 
18
 
 
19
import pygtk
 
20
pygtk.require("2.0")
 
21
import gobject
 
22
import gtk
 
23
import pango
 
24
 
 
25
from bzrlib import tsort
 
26
from bzrlib.errors import NoSuchRevision
 
27
from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
 
28
 
 
29
from colormap import AnnotateColorMap, AnnotateColorSaturation
 
30
from logview import LogView
 
31
from spanselector import SpanSelector
 
32
 
 
33
 
 
34
(
 
35
    REVISION_ID_COL,
 
36
    LINE_NUM_COL,
 
37
    COMMITTER_COL,
 
38
    REVNO_COL,
 
39
    HIGHLIGHT_COLOR_COL,
 
40
    TEXT_LINE_COL
 
41
) = range(6)
 
42
 
 
43
 
 
44
class GAnnotateWindow(gtk.Window):
 
45
    """Annotate window."""
 
46
 
 
47
    def __init__(self, all=False, plain=False):
 
48
        self.all = all
 
49
        self.plain = plain
 
50
        
 
51
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
 
52
        
 
53
        self.set_icon(self.render_icon(gtk.STOCK_FIND, gtk.ICON_SIZE_BUTTON))
 
54
        self.annotate_colormap = AnnotateColorSaturation()
 
55
 
 
56
        self._create()
 
57
 
 
58
        if self.plain:
 
59
            self.span_selector.hide()
 
60
 
 
61
    def annotate(self, tree, branch, file_id):
 
62
        self.revisions = {}
 
63
        self.annotations = []
 
64
        self.branch = branch
 
65
        self.tree = tree
 
66
        self.file_id = file_id
 
67
        self.revision_id = getattr(tree, 'get_revision_id', 
 
68
                                   lambda: CURRENT_REVISION)()
 
69
        
 
70
        # [revision id, line number, committer, revno, highlight color, line]
 
71
        self.annomodel = gtk.ListStore(gobject.TYPE_STRING,
 
72
                                       gobject.TYPE_STRING,
 
73
                                       gobject.TYPE_STRING,
 
74
                                       gobject.TYPE_STRING,
 
75
                                       gobject.TYPE_STRING,
 
76
                                       gobject.TYPE_STRING)
 
77
        
 
78
        last_seen = None
 
79
        try:
 
80
            branch.lock_read()
 
81
            branch.repository.lock_read()
 
82
            for line_no, (revision, revno, line)\
 
83
                    in enumerate(self._annotate(tree, file_id)):
 
84
                if revision.revision_id == last_seen and not self.all:
 
85
                    revno = committer = ""
 
86
                else:
 
87
                    last_seen = revision.revision_id
 
88
                    committer = revision.committer
 
89
 
 
90
                if revision.revision_id not in self.revisions:
 
91
                    self.revisions[revision.revision_id] = revision
 
92
 
 
93
                self.annomodel.append([revision.revision_id,
 
94
                                       line_no + 1,
 
95
                                       committer,
 
96
                                       revno,
 
97
                                       None,
 
98
                                       line.rstrip("\r\n")
 
99
                                      ])
 
100
                self.annotations.append(revision)
 
101
 
 
102
            if not self.plain:
 
103
                self._set_oldest_newest()
 
104
                # Recall that calling activate_default will emit "span-changed",
 
105
                # so self._span_changed_cb will take care of initial highlighting
 
106
                self.span_selector.activate_default()
 
107
        finally:
 
108
            branch.repository.unlock()
 
109
            branch.unlock()
 
110
 
 
111
        self.annoview.set_model(self.annomodel)
 
112
        self.annoview.grab_focus()
 
113
 
 
114
    def jump_to_line(self, lineno):
 
115
        if lineno > len(self.annomodel) or lineno < 1:
 
116
            row = 0
 
117
            # FIXME:should really deal with this in the gui. Perhaps a status
 
118
            # bar?
 
119
            print("gannotate: Line number %d does't exist. Defaulting to "
 
120
                  "line 1." % lineno)
 
121
            return
 
122
        else:
 
123
            row = lineno - 1
 
124
 
 
125
        self.annoview.set_cursor(row)
 
126
        self.annoview.scroll_to_cell(row, use_align=True)
 
127
 
 
128
    def _dotted_revnos(self, repository, revision_id):
 
129
        """Return a dict of revision_id -> dotted revno
 
130
        
 
131
        :param repository: The repository to get the graph from
 
132
        :param revision_id: The last revision for which this info is needed
 
133
        """
 
134
        graph = repository.get_revision_graph(revision_id)
 
135
        dotted = {}
 
136
        for n, revision_id, d, revno, e in tsort.merge_sort(graph, 
 
137
            revision_id, generate_revno=True):
 
138
            dotted[revision_id] = '.'.join(str(num) for num in revno)
 
139
        return dotted
 
140
 
 
141
    def _annotate(self, tree, file_id):
 
142
        current_revision = FakeRevision(CURRENT_REVISION)
 
143
        current_revision.committer = self.branch.get_config().username()
 
144
        current_revision.timestamp = time.time()
 
145
        current_revision.message = '[Not yet committed]'
 
146
        current_revno = '%d?' % (self.branch.revno() + 1)
 
147
        repository = self.branch.repository
 
148
        if self.revision_id == CURRENT_REVISION:
 
149
            revision_id = self.branch.last_revision()
 
150
        else:
 
151
            revision_id = self.revision_id
 
152
        dotted = self._dotted_revnos(repository, revision_id)
 
153
        revision_cache = RevisionCache(repository)
 
154
        for origin, text in tree.annotate_iter(file_id):
 
155
            rev_id = origin
 
156
            try:
 
157
                revision = revision_cache.get_revision(rev_id)
 
158
                revno = dotted.get(rev_id, 'merge')
 
159
                if len(revno) > 15:
 
160
                    revno = 'merge'
 
161
            except NoSuchRevision:
 
162
                committer = "?"
 
163
                if rev_id == CURRENT_REVISION:
 
164
                    revision = current_revision
 
165
                    revno = current_revno
 
166
                else:
 
167
                    revision = FakeRevision(rev_id)
 
168
                    revno = "?"
 
169
 
 
170
            yield revision, revno, text
 
171
 
 
172
    def _set_oldest_newest(self):
 
173
        rev_dates = map(lambda i: self.revisions[i].timestamp, self.revisions)
 
174
        if len(rev_dates) == 0:
 
175
            return
 
176
        oldest = min(rev_dates)
 
177
        newest = max(rev_dates)
 
178
 
 
179
        span = self._span_from_seconds(time.time() - oldest)
 
180
        self.span_selector.set_to_oldest_span(span)
 
181
        
 
182
        span = self._span_from_seconds(newest - oldest)
 
183
        self.span_selector.set_newest_to_oldest_span(span)
 
184
 
 
185
    def _span_from_seconds(self, seconds):
 
186
        return (seconds / (24 * 60 * 60))
 
187
    
 
188
    def _span_changed_cb(self, w, span):
 
189
        self.annotate_colormap.set_span(span)
 
190
        now = time.time()
 
191
        self.annomodel.foreach(self._highlight_annotation, now)
 
192
 
 
193
    def _highlight_annotation(self, model, path, iter, now):
 
194
        revision_id, = model.get(iter, REVISION_ID_COL)
 
195
        revision = self.revisions[revision_id]
 
196
        model.set(iter, HIGHLIGHT_COLOR_COL,
 
197
                  self.annotate_colormap.get_color(revision, now))
 
198
 
 
199
    def _show_log(self, w):
 
200
        (path, col) = self.annoview.get_cursor()
 
201
        if path is None:
 
202
            return
 
203
        rev_id = self.annomodel[path][REVISION_ID_COL]
 
204
        self.logview.set_revision(self.revisions[rev_id])
 
205
 
 
206
    def _create(self):
 
207
        self.logview = self._create_log_view()
 
208
        self.annoview = self._create_annotate_view()
 
209
        self.span_selector = self._create_span_selector()
 
210
 
 
211
        vbox = gtk.VBox(False, 12)
 
212
        vbox.set_border_width(12)
 
213
        vbox.show()
 
214
 
 
215
        sw = gtk.ScrolledWindow()
 
216
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
217
        sw.set_shadow_type(gtk.SHADOW_IN)
 
218
        sw.add(self.annoview)
 
219
        self.annoview.gwindow = self
 
220
        sw.show()
 
221
        
 
222
        self.pane = pane = gtk.VPaned()
 
223
        pane.add1(sw)
 
224
        pane.add2(self.logview)
 
225
        pane.show()
 
226
        vbox.pack_start(pane, expand=True, fill=True)
 
227
        
 
228
        hbox = gtk.HBox(True, 6)
 
229
        hbox.pack_start(self.span_selector, expand=False, fill=True)
 
230
        hbox.pack_start(self._create_button_box(), expand=False, fill=True)
 
231
        hbox.show()
 
232
        vbox.pack_start(hbox, expand=False, fill=True)
 
233
 
 
234
        self.add(vbox)
 
235
 
 
236
    def row_diff(self, tv, path, tvc):
 
237
        row = path[0]
 
238
        revision = self.annotations[row]
 
239
        repository = self.branch.repository
 
240
        if revision.revision_id == CURRENT_REVISION:
 
241
            tree1 = self.tree
 
242
            tree2 = self.tree.basis_tree()
 
243
        else:
 
244
            tree1 = repository.revision_tree(revision.revision_id)
 
245
            if len(revision.parent_ids) > 0:
 
246
                tree2 = repository.revision_tree(revision.parent_ids[0])
 
247
            else:
 
248
                tree2 = repository.revision_tree(NULL_REVISION)
 
249
        from bzrlib.plugins.gtk.viz.diffwin import DiffWindow
 
250
        window = DiffWindow()
 
251
        window.set_diff("Diff for row %d" % (row+1), tree1, tree2)
 
252
        window.set_file(tree1.id2path(self.file_id))
 
253
        window.show()
 
254
 
 
255
 
 
256
    def _create_annotate_view(self):
 
257
        tv = gtk.TreeView()
 
258
        tv.set_rules_hint(False)
 
259
        tv.connect("cursor-changed", self._show_log)
 
260
        tv.show()
 
261
        tv.connect("row-activated", self.row_diff)
 
262
 
 
263
        cell = gtk.CellRendererText()
 
264
        cell.set_property("xalign", 1.0)
 
265
        cell.set_property("ypad", 0)
 
266
        cell.set_property("family", "Monospace")
 
267
        cell.set_property("cell-background-gdk",
 
268
                          tv.get_style().bg[gtk.STATE_NORMAL])
 
269
        col = gtk.TreeViewColumn()
 
270
        col.set_resizable(False)
 
271
        col.pack_start(cell, expand=True)
 
272
        col.add_attribute(cell, "text", LINE_NUM_COL)
 
273
        tv.append_column(col)
 
274
 
 
275
        cell = gtk.CellRendererText()
 
276
        cell.set_property("ypad", 0)
 
277
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
278
        cell.set_property("cell-background-gdk",
 
279
                          self.get_style().bg[gtk.STATE_NORMAL])
 
280
        col = gtk.TreeViewColumn("Committer")
 
281
        col.set_resizable(True)
 
282
        col.pack_start(cell, expand=True)
 
283
        col.add_attribute(cell, "text", COMMITTER_COL)
 
284
        tv.append_column(col)
 
285
 
 
286
        cell = gtk.CellRendererText()
 
287
        cell.set_property("xalign", 1.0)
 
288
        cell.set_property("ypad", 0)
 
289
        cell.set_property("cell-background-gdk",
 
290
                          self.get_style().bg[gtk.STATE_NORMAL])
 
291
        col = gtk.TreeViewColumn("Revno")
 
292
        col.set_resizable(False)
 
293
        col.pack_start(cell, expand=True)
 
294
        col.add_attribute(cell, "markup", REVNO_COL)
 
295
        tv.append_column(col)
 
296
 
 
297
        cell = gtk.CellRendererText()
 
298
        cell.set_property("ypad", 0)
 
299
        cell.set_property("family", "Monospace")
 
300
        col = gtk.TreeViewColumn()
 
301
        col.set_resizable(False)
 
302
        col.pack_start(cell, expand=True)
 
303
#        col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
 
304
        col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
 
305
        col.add_attribute(cell, "text", TEXT_LINE_COL)
 
306
        tv.append_column(col)
 
307
 
 
308
        tv.set_search_column(LINE_NUM_COL)
 
309
        
 
310
        return tv
 
311
 
 
312
    def _create_span_selector(self):
 
313
        ss = SpanSelector()
 
314
        ss.connect("span-changed", self._span_changed_cb)
 
315
        ss.show()
 
316
 
 
317
        return ss
 
318
 
 
319
    def _create_log_view(self):
 
320
        lv = LogView()
 
321
        lv.show()
 
322
 
 
323
        return lv
 
324
 
 
325
    def _create_button_box(self):
 
326
        box = gtk.HButtonBox()
 
327
        box.set_layout(gtk.BUTTONBOX_END)
 
328
        box.show()
 
329
        
 
330
        button = gtk.Button()
 
331
        button.set_use_stock(True)
 
332
        button.set_label("gtk-close")
 
333
        button.connect("clicked", lambda w: self.destroy())
 
334
        button.show()
 
335
        
 
336
        box.pack_start(button, expand=False, fill=False)
 
337
 
 
338
        return box
 
339
 
 
340
 
 
341
class FakeRevision:
 
342
    """ A fake revision.
 
343
 
 
344
    For when a revision is referenced but not present.
 
345
    """
 
346
 
 
347
    def __init__(self, revision_id, committer='?'):
 
348
        self.revision_id = revision_id
 
349
        self.parent_ids = []
 
350
        self.committer = committer
 
351
        self.message = "?"
 
352
        self.timestamp = 0.0
 
353
        self.timezone = 0
 
354
 
 
355
 
 
356
class RevisionCache(object):
 
357
    """A caching revision source"""
 
358
    def __init__(self, real_source):
 
359
        self.__real_source = real_source
 
360
        self.__cache = {}
 
361
 
 
362
    def get_revision(self, revision_id):
 
363
        if revision_id not in self.__cache:
 
364
            revision = self.__real_source.get_revision(revision_id)
 
365
            self.__cache[revision_id] = revision
 
366
        return self.__cache[revision_id]