/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 commit.py

  • Committer: Curtis Hovey
  • Date: 2011-07-31 16:50:29 UTC
  • mto: This revision was merged to the branch mainline in revision 741.
  • Revision ID: sinzui.is@verizon.net-20110731165029-9gixuqypi3lwapzm
Removed import_pygtk because gi does not impicitly call Main(). Inlined checks for gtk availablility.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
import re
18
18
 
19
 
from gi.repository import Gdk
20
19
from gi.repository import Gtk
21
20
from gi.repository import GObject
22
21
from gi.repository import Pango
23
22
 
24
23
from bzrlib import (
25
 
    bencode,
26
24
    errors,
27
25
    osutils,
28
 
    revision as _mod_revision,
29
26
    trace,
30
 
    tsort,
31
27
    )
 
28
try:
 
29
    from bzrlib import bencode
 
30
except ImportError:
 
31
    from bzrlib.util import bencode
 
32
 
32
33
from bzrlib.plugins.gtk.dialog import question_dialog
33
34
from bzrlib.plugins.gtk.errors import show_bzr_error
34
35
from bzrlib.plugins.gtk.i18n import _i18n
35
 
from bzrlib.plugins.gtk.commitmsgs import SavedCommitMessagesManager
36
36
 
37
37
try:
38
38
    import dbus
42
42
    have_dbus = False
43
43
 
44
44
 
45
 
def _get_sorted_revisions(tip_revision, revision_ids, parent_map):
46
 
    """Get an iterator which will return the revisions in merge sorted order.
47
 
 
48
 
    This will build up a list of all nodes, such that only nodes in the list
49
 
    are referenced. It then uses MergeSorter to return them in 'merge-sorted'
50
 
    order.
51
 
 
52
 
    :param revision_ids: A set of revision_ids
53
 
    :param parent_map: The parent information for each node. Revisions which
54
 
        are considered ghosts should not be present in the map.
55
 
    :return: iterator from MergeSorter.iter_topo_order()
56
 
    """
57
 
    # MergeSorter requires that all nodes be present in the graph, so get rid
58
 
    # of any references pointing outside of this graph.
59
 
    parent_graph = {}
60
 
    for revision_id in revision_ids:
61
 
        if revision_id not in parent_map: # ghost
62
 
            parent_graph[revision_id] = []
63
 
        else:
64
 
            # Only include parents which are in this sub-graph
65
 
            parent_graph[revision_id] = [p for p in parent_map[revision_id]
66
 
                                            if p in revision_ids]
67
 
    sorter = tsort.MergeSorter(parent_graph, tip_revision)
68
 
    return sorter.iter_topo_order()
69
 
 
70
 
 
71
45
def pending_revisions(wt):
72
46
    """Return a list of pending merges or None if there are none of them.
73
47
 
78
52
    """
79
53
    parents = wt.get_parent_ids()
80
54
    if len(parents) < 2:
81
 
        return
 
55
        return None
82
56
 
83
57
    # The basic pending merge algorithm uses the same algorithm as
84
58
    # bzrlib.status.show_pending_merges
86
60
    branch = wt.branch
87
61
    last_revision = parents[0]
88
62
 
89
 
    graph = branch.repository.get_graph()
90
 
    other_revisions = [last_revision]
 
63
    if last_revision is not None:
 
64
        try:
 
65
            ignore = set(branch.repository.get_ancestry(last_revision,
 
66
                                                        topo_sorted=False))
 
67
        except errors.NoSuchRevision:
 
68
            # the last revision is a ghost : assume everything is new
 
69
            # except for it
 
70
            ignore = set([None, last_revision])
 
71
    else:
 
72
        ignore = set([None])
91
73
 
92
74
    pm = []
93
75
    for merge in pending:
94
 
        try:
95
 
            merge_rev = branch.repository.get_revision(merge)
96
 
        except errors.NoSuchRevision:
97
 
            # If we are missing a revision, just print out the revision id
98
 
            trace.mutter("ghost: %r", merge)
99
 
            other_revisions.append(merge)
100
 
            continue
101
 
 
102
 
        # Find all of the revisions in the merge source, which are not in the
103
 
        # last committed revision.
104
 
        merge_extra = graph.find_unique_ancestors(merge, other_revisions)
105
 
        other_revisions.append(merge)
106
 
        merge_extra.discard(_mod_revision.NULL_REVISION)
107
 
 
108
 
        # Get a handle to all of the revisions we will need
109
 
        try:
110
 
            revisions = dict((rev.revision_id, rev) for rev in
111
 
                             branch.repository.get_revisions(merge_extra))
112
 
        except errors.NoSuchRevision:
113
 
            # One of the sub nodes is a ghost, check each one
114
 
            revisions = {}
115
 
            for revision_id in merge_extra:
116
 
                try:
117
 
                    rev = branch.repository.get_revisions([revision_id])[0]
118
 
                except errors.NoSuchRevision:
119
 
                    revisions[revision_id] = None
120
 
                else:
121
 
                    revisions[revision_id] = rev
122
 
 
123
 
         # Display the revisions brought in by this merge.
124
 
        rev_id_iterator = _get_sorted_revisions(merge, merge_extra,
125
 
                            branch.repository.get_parent_map(merge_extra))
126
 
        # Skip the first node
127
 
        num, first, depth, eom = rev_id_iterator.next()
128
 
        if first != merge:
129
 
            raise AssertionError('Somehow we misunderstood how'
130
 
                ' iter_topo_order works %s != %s' % (first, merge))
131
 
        children = []
132
 
        for num, sub_merge, depth, eom in rev_id_iterator:
133
 
            rev = revisions[sub_merge]
134
 
            if rev is None:
135
 
                trace.warning("ghost: %r", sub_merge)
136
 
                continue
137
 
            children.append(rev)
138
 
        yield (merge_rev, children)
 
76
        ignore.add(merge)
 
77
        try:
 
78
            rev = branch.repository.get_revision(merge)
 
79
            children = []
 
80
            pm.append((rev, children))
 
81
 
 
82
            # This does need to be topo sorted, so we search backwards
 
83
            inner_merges = branch.repository.get_ancestry(merge)
 
84
            assert inner_merges[0] is None
 
85
            inner_merges.pop(0)
 
86
            for mmerge in reversed(inner_merges):
 
87
                if mmerge in ignore:
 
88
                    continue
 
89
                rev = branch.repository.get_revision(mmerge)
 
90
                children.append(rev)
 
91
 
 
92
                ignore.add(mmerge)
 
93
        except errors.NoSuchRevision:
 
94
            print "DEBUG: NoSuchRevision:", merge
 
95
 
 
96
    return pm
139
97
 
140
98
 
141
99
_newline_variants_re = re.compile(r'\r\n?')
142
100
def _sanitize_and_decode_message(utf8_message):
143
101
    """Turn a utf-8 message into a sanitized Unicode message."""
144
102
    fixed_newline = _newline_variants_re.sub('\n', utf8_message)
145
 
    return osutils.safe_unicode(fixed_newline)
 
103
    return fixed_newline.decode('utf-8')
146
104
 
147
105
 
148
106
class CommitDialog(Gtk.Dialog):
149
107
    """Implementation of Commit."""
150
108
 
151
109
    def __init__(self, wt, selected=None, parent=None):
152
 
        super(CommitDialog, self).__init__(
153
 
            title="Commit to %s" % wt.basedir, parent=parent, flags=0)
 
110
        GObject.GObject.__init__(self, title="Commit to %s" % wt.basedir,
 
111
                            parent=parent, flags=0,)
154
112
        self.connect('delete-event', self._on_delete_window)
155
113
        self._question_dialog = question_dialog
156
114
 
163
121
        self._enable_per_file_commits = True
164
122
        self._commit_all_changes = True
165
123
        self.committed_revision_id = None # Nothing has been committed yet
166
 
        self._last_selected_file = None
167
 
        self._saved_commit_messages_manager = SavedCommitMessagesManager(
168
 
            self._wt, self._wt.branch)
 
124
        self._saved_commit_messages_manager = SavedCommitMessagesManager(self._wt, self._wt.branch)
169
125
 
170
126
        self.setup_params()
171
127
        self.construct()
175
131
        """Setup the member variables for state."""
176
132
        self._basis_tree = self._wt.basis_tree()
177
133
        self._delta = None
178
 
        self._wt.lock_read()
179
 
        try:
180
 
            self._pending = list(pending_revisions(self._wt))
181
 
        finally:
182
 
            self._wt.unlock()
 
134
        self._pending = pending_revisions(self._wt)
183
135
 
184
136
        self._is_checkout = (self._wt.branch.get_bound_location() is not None)
185
137
 
237
189
 
238
190
        all_enabled = (self._selected is None)
239
191
        # The first entry is always the 'whole tree'
240
 
        all_iter = store.append(["", "", all_enabled, 'All Files', '', ''])
 
192
        all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
241
193
        initial_cursor = store.get_path(all_iter)
242
194
        # should we pass specific_files?
243
195
        self._wt.lock_read()
274
226
        # This sets the cursor, which causes the expander to close, which
275
227
        # causes the _file_message_text_view to never get realized. So we have
276
228
        # to give it a little kick, or it warns when we try to grab the focus
277
 
        self._treeview_files.set_cursor(initial_cursor, None, False)
 
229
        self._treeview_files.set_cursor(initial_cursor)
278
230
 
279
231
        def _realize_file_message_tree_view(*args):
280
232
            self._file_message_text_view.realize()
288
240
            self._check_local.hide()
289
241
            return
290
242
        if have_dbus:
291
 
            try:
292
 
                bus = dbus.SystemBus()
293
 
            except dbus.DBusException:
294
 
                trace.mutter("DBus system bus not available")
295
 
                self._check_local.show()
296
 
                return
 
243
            bus = dbus.SystemBus()
297
244
            try:
298
245
                proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
299
246
                                           '/org/freedesktop/NetworkManager')
301
248
                trace.mutter("networkmanager not available.")
302
249
                self._check_local.show()
303
250
                return
304
 
 
 
251
            
305
252
            dbus_iface = dbus.Interface(proxy_obj,
306
253
                                        'org.freedesktop.NetworkManager')
307
254
            try:
333
280
        """Build up the dialog widgets."""
334
281
        # The primary pane which splits it into left and right (adjustable)
335
282
        # sections.
336
 
        self._hpane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
 
283
        self._hpane = Gtk.HPaned()
337
284
 
338
285
        self._construct_left_pane()
339
286
        self._construct_right_pane()
340
287
        self._construct_action_pane()
341
288
 
342
 
        self.get_content_area().pack_start(self._hpane, True, True, 0)
 
289
        self.vbox.pack_start(self._hpane, True, True, 0)
343
290
        self._hpane.show()
344
291
        self.set_focus(self._global_message_text_view)
345
292
 
365
312
 
366
313
    def _construct_accelerators(self):
367
314
        group = Gtk.AccelGroup()
368
 
        group.connect(Gdk.keyval_from_name('N'),
369
 
                      Gdk.ModifierType.CONTROL_MASK, 0, self._on_accel_next)
 
315
        group.connect_group(Gdk.keyval_from_name('N'),
 
316
                            Gdk.EventMask.CONTROL_MASK, 0, self._on_accel_next)
370
317
        self.add_accel_group(group)
371
318
 
372
319
        # ignore the escape key (avoid closing the window)
379
326
 
380
327
        self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
381
328
                                            use_underline=True)
382
 
        self._left_pane_box.pack_end(self._check_local, False, False, 0)
 
329
        self._left_pane_box.pack_end(self._check_local, False, False)
383
330
        self._check_local.set_active(False)
384
331
 
385
332
        self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
407
354
        self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
408
355
        self._button_cancel.connect('clicked', self._on_cancel_clicked)
409
356
        self._button_cancel.show()
410
 
        self.get_action_area().pack_end(
411
 
            self._button_cancel, True, True, 0)
 
357
        self.action_area.pack_end(self._button_cancel)
412
358
        self._button_commit = Gtk.Button(_i18n("Comm_it"), use_underline=True)
413
359
        self._button_commit.connect('clicked', self._on_commit_clicked)
414
360
        self._button_commit.set_can_default(True)
415
361
        self._button_commit.show()
416
 
        self.get_action_area().pack_end(
417
 
            self._button_commit, True, True, 0)
 
362
        self.action_area.pack_end(self._button_commit)
418
363
        self._button_commit.grab_default()
419
364
 
420
365
    def _add_to_right_table(self, widget, weight, expanding=False):
440
385
        # file_label.show()
441
386
        self._files_box.pack_start(file_label, False, True, 0)
442
387
 
443
 
        self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
 
388
        self._commit_all_files_radio = Gtk.RadioButton(
444
389
            None, _i18n("Commit all changes"))
445
390
        self._files_box.pack_start(self._commit_all_files_radio, False, True, 0)
446
391
        self._commit_all_files_radio.show()
447
392
        self._commit_all_files_radio.connect('toggled',
448
393
            self._toggle_commit_selection)
449
 
        self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
 
394
        self._commit_selected_radio = Gtk.RadioButton(
450
395
            self._commit_all_files_radio, _i18n("Only commit selected changes"))
451
396
        self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
452
397
        self._commit_selected_radio.show()
464
409
        scroller.add(self._treeview_files)
465
410
        scroller.set_shadow_type(Gtk.ShadowType.IN)
466
411
        scroller.show()
467
 
        self._files_box.pack_start(scroller, True, True, 0)
 
412
        self._files_box.pack_start(scroller,
 
413
                                   expand=True, fill=True)
468
414
        self._files_box.show()
469
415
        self._left_pane_box.pack_start(self._files_box, True, True, 0)
470
416
 
499
445
                                     self._on_treeview_files_cursor_changed)
500
446
 
501
447
    def _toggle_commit(self, cell, path, model):
502
 
        if model[path][0] == "": # No file_id means 'All Files'
 
448
        if model[path][0] is None: # No file_id means 'All Files'
503
449
            new_val = not model[path][2]
504
450
            for node in model:
505
451
                node[2] = new_val
515
461
                checked_col.set_visible(False)
516
462
            else:
517
463
                checked_col.set_visible(True)
518
 
            renderer = checked_col.get_cells()[0]
 
464
            renderer = checked_col.get_cell_renderers()[0]
519
465
            renderer.set_property('activatable', not all_files)
520
466
 
521
467
    def _construct_pending_list(self):
527
473
        pending_message = Gtk.Label()
528
474
        pending_message.set_markup(
529
475
            _i18n('<i>* Cannot select specific files when merging</i>'))
530
 
        self._pending_box.pack_start(pending_message, False, True, 5)
 
476
        self._pending_box.pack_start(pending_message, expand=False, padding=5)
531
477
        pending_message.show()
532
478
 
533
479
        pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
534
 
        self._pending_box.pack_start(pending_label, False, True, 0)
 
480
        self._pending_box.pack_start(pending_label, expand=False, padding=0)
535
481
        pending_label.show()
536
482
 
537
483
        scroller = Gtk.ScrolledWindow()
540
486
        scroller.add(self._treeview_pending)
541
487
        scroller.set_shadow_type(Gtk.ShadowType.IN)
542
488
        scroller.show()
543
 
        self._pending_box.pack_start(scroller, True, True, 5)
 
489
        self._pending_box.pack_start(scroller,
 
490
                                     expand=True, fill=True, padding=5)
544
491
        self._treeview_pending.show()
545
492
        self._left_pane_box.pack_start(self._pending_box, True, True, 0)
546
493
 
589
536
        self._file_message_text_view.set_accepts_tab(False)
590
537
        self._file_message_text_view.show()
591
538
 
592
 
        self._file_message_expander = Gtk.Expander(
593
 
            label=_i18n('File commit message'))
 
539
        self._file_message_expander = Gtk.Expander(_i18n('File commit message'))
594
540
        self._file_message_expander.set_expanded(True)
595
541
        self._file_message_expander.add(scroller)
596
542
        self._add_to_right_table(self._file_message_expander, 1, False)
622
568
 
623
569
    def _on_treeview_files_cursor_changed(self, treeview):
624
570
        treeselection = treeview.get_selection()
625
 
        if treeselection is None:
626
 
            # The treeview was probably destroyed as the dialog closes.
627
 
            return
628
571
        (model, selection) = treeselection.get_selected()
629
572
 
630
573
        if selection is not None:
631
574
            path, display_path = model.get(selection, 1, 3)
632
575
            self._diff_label.set_text(_i18n('Diff for ') + display_path)
633
 
            if path == "":
 
576
            if path is None:
634
577
                self._diff_view.show_diff(None)
635
578
            else:
636
 
                self._diff_view.show_diff([osutils.safe_unicode(path)])
 
579
                self._diff_view.show_diff([path.decode('UTF-8')])
637
580
            self._update_per_file_info(selection)
638
581
 
639
582
    def _on_accel_next(self, accel_group, window, keyval, modifier):
650
593
            # We have either made it to the end of the list, or nothing was
651
594
            # selected. Either way, select All Files, and jump to the global
652
595
            # commit message.
653
 
            self._treeview_files.set_cursor(
654
 
                Gtk.TreePath(path=0), "", False)
 
596
            self._treeview_files.set_cursor((0,))
655
597
            self._global_message_text_view.grab_focus()
656
598
        else:
657
599
            # Set the cursor to this entry, and jump to the per-file commit
658
600
            # message
659
 
            self._treeview_files.set_cursor(model.get_path(next), None, False)
 
601
            self._treeview_files.set_cursor(model.get_path(next))
660
602
            self._file_message_text_view.grab_focus()
661
603
 
662
604
    def _save_current_file_message(self):
664
606
            return # Nothing to save
665
607
        text_buffer = self._file_message_text_view.get_buffer()
666
608
        cur_text = text_buffer.get_text(text_buffer.get_start_iter(),
667
 
                                        text_buffer.get_end_iter(), True)
 
609
                                        text_buffer.get_end_iter())
668
610
        last_selected = self._files_store.get_iter(self._last_selected_file)
669
611
        self._files_store.set_value(last_selected, 5, cur_text)
670
612
 
676
618
        self._save_current_file_message()
677
619
        text_buffer = self._file_message_text_view.get_buffer()
678
620
        file_id, display_path, message = self._files_store.get(selection, 0, 3, 5)
679
 
        if file_id == "": # Whole tree
 
621
        if file_id is None: # Whole tree
680
622
            self._file_message_expander.set_label(_i18n('File commit message'))
681
623
            self._file_message_expander.set_expanded(False)
682
624
            self._file_message_expander.set_sensitive(False)
699
641
        files = []
700
642
        records = iter(self._files_store)
701
643
        rec = records.next() # Skip the All Files record
702
 
        assert rec[0] == "", "Are we skipping the wrong record?"
 
644
        assert rec[0] is None, "Are we skipping the wrong record?"
703
645
 
704
646
        file_info = []
705
647
        for record in records:
706
648
            if self._commit_all_changes or record[2]:# [2] checkbox
707
 
                file_id = osutils.safe_utf8(record[0]) # [0] file_id
708
 
                path = osutils.safe_utf8(record[1])    # [1] real path
 
649
                file_id = record[0] # [0] file_id
 
650
                path = record[1]    # [1] real path
709
651
                # [5] commit message
710
652
                file_message = _sanitize_and_decode_message(record[5])
711
653
                files.append(path.decode('UTF-8'))
818
760
    def _get_global_commit_message(self):
819
761
        buf = self._global_message_text_view.get_buffer()
820
762
        start, end = buf.get_bounds()
821
 
        text = buf.get_text(start, end, True)
 
763
        text = buf.get_text(start, end)
822
764
        return _sanitize_and_decode_message(text)
823
765
 
824
766
    def _set_global_commit_message(self, message):
847
789
        rev_dict['revision_id'] = rev.revision_id
848
790
        return rev_dict
849
791
 
 
792
 
 
793
class SavedCommitMessagesManager:
 
794
    """Save glogal and per-file commit messages.
 
795
 
 
796
    Saves global commit message and utf-8 file_id->message dictionary
 
797
    of per-file commit messages on disk. Re-reads them later for re-using.
 
798
    """
 
799
 
 
800
    def __init__(self, tree=None, branch=None):
 
801
        """If branch is None, builds empty messages, otherwise reads them
 
802
        from branch's disk storage. 'tree' argument is for the future."""
 
803
        if branch is None:
 
804
            self.global_message = u''
 
805
            self.file_messages = {}
 
806
        else:
 
807
            config = branch.get_config()
 
808
            self.global_message = config.get_user_option(
 
809
                'gtk_global_commit_message')
 
810
            if self.global_message is None:
 
811
                self.global_message = u''
 
812
            file_messages = config.get_user_option('gtk_file_commit_messages')
 
813
            if file_messages: # unicode and B-encoded:
 
814
                self.file_messages = bencode.bdecode(
 
815
                    file_messages.encode('UTF-8'))
 
816
            else:
 
817
                self.file_messages = {}
 
818
 
 
819
    def get(self):
 
820
        return self.global_message, self.file_messages
 
821
 
 
822
    def is_not_empty(self):
 
823
        return bool(self.global_message or self.file_messages)
 
824
 
 
825
    def insert(self, global_message, file_info):
 
826
        """Formats per-file commit messages (list of dictionaries, one per file)
 
827
        into one utf-8 file_id->message dictionary and merges this with
 
828
        previously existing dictionary. Merges global commit message too."""
 
829
        file_messages = {}
 
830
        for fi in file_info:
 
831
            file_message = fi['message']
 
832
            if file_message:
 
833
                file_messages[fi['file_id']] = file_message # utf-8 strings
 
834
        for k,v in file_messages.iteritems():
 
835
            try:
 
836
                self.file_messages[k] = v + '\n******\n' + self.file_messages[k]
 
837
            except KeyError:
 
838
                self.file_messages[k] = v
 
839
        if self.global_message:
 
840
            self.global_message = global_message + '\n******\n' \
 
841
                + self.global_message
 
842
        else:
 
843
            self.global_message = global_message
 
844
 
 
845
    def save(self, tree, branch):
 
846
        # We store in branch's config, which can be a problem if two gcommit
 
847
        # are done in two checkouts of one single branch (comments overwrite
 
848
        # each other). Ideally should be in working tree. But uncommit does
 
849
        # not always have a working tree, though it always has a branch.
 
850
        # 'tree' argument is for the future
 
851
        config = branch.get_config()
 
852
        # should it be named "gtk_" or some more neutral name ("gui_" ?) to
 
853
        # be compatible with qbzr in the future?
 
854
        config.set_user_option('gtk_global_commit_message', self.global_message)
 
855
        # bencode() does not know unicode objects but set_user_option()
 
856
        # requires one:
 
857
        config.set_user_option(
 
858
            'gtk_file_commit_messages',
 
859
            bencode.bencode(self.file_messages).decode('UTF-8'))
 
860
 
 
861
 
 
862
def save_commit_messages(local, master, old_revno, old_revid,
 
863
                         new_revno, new_revid):
 
864
    b = local
 
865
    if b is None:
 
866
        b = master
 
867
    mgr = SavedCommitMessagesManager(None, b)
 
868
    revid_iterator = b.repository.iter_reverse_revision_history(old_revid)
 
869
    cur_revno = old_revno
 
870
    new_revision_id = old_revid
 
871
    graph = b.repository.get_graph()
 
872
    for rev_id in revid_iterator:
 
873
        if cur_revno == new_revno:
 
874
            break
 
875
        cur_revno -= 1
 
876
        rev = b.repository.get_revision(rev_id)
 
877
        file_info = rev.properties.get('file-info', None)
 
878
        if file_info is None:
 
879
            file_info = {}
 
880
        else:
 
881
            file_info = bencode.bdecode(file_info.encode('UTF-8'))
 
882
        global_message = osutils.safe_unicode(rev.message)
 
883
        # Concatenate comment of the uncommitted revision
 
884
        mgr.insert(global_message, file_info)
 
885
 
 
886
        parents = graph.get_parent_map([rev_id]).get(rev_id, None)
 
887
        if not parents:
 
888
            continue
 
889
    mgr.save(None, b)