/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: Jelmer Vernooij
  • Date: 2011-12-20 16:16:57 UTC
  • Revision ID: jelmer@canonical.com-20111220161657-zjn6rqjrw8ouehf8
Drop support for old bencode location.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 by Szilveszter Farkas (Phanatic) <szilveszter.farkas@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 re
 
18
 
 
19
from gi.repository import Gdk
 
20
from gi.repository import Gtk
 
21
from gi.repository import GObject
 
22
from gi.repository import Pango
 
23
 
 
24
from bzrlib import (
 
25
    bencode,
 
26
    errors,
 
27
    trace,
 
28
    )
 
29
from bzrlib.plugins.gtk.dialog import question_dialog
 
30
from bzrlib.plugins.gtk.errors import show_bzr_error
 
31
from bzrlib.plugins.gtk.i18n import _i18n
 
32
from bzrlib.plugins.gtk.commitmsgs import SavedCommitMessagesManager
 
33
 
 
34
try:
 
35
    import dbus
 
36
    import dbus.glib
 
37
    have_dbus = True
 
38
except ImportError:
 
39
    have_dbus = False
 
40
 
 
41
 
 
42
def pending_revisions(wt):
 
43
    """Return a list of pending merges or None if there are none of them.
 
44
 
 
45
    Arguably this should be a core function, and
 
46
    ``bzrlib.status.show_pending_merges`` should be built on top of it.
 
47
 
 
48
    :return: [(rev, [children])]
 
49
    """
 
50
    parents = wt.get_parent_ids()
 
51
    if len(parents) < 2:
 
52
        return None
 
53
 
 
54
    # The basic pending merge algorithm uses the same algorithm as
 
55
    # bzrlib.status.show_pending_merges
 
56
    pending = parents[1:]
 
57
    branch = wt.branch
 
58
    last_revision = parents[0]
 
59
 
 
60
    if last_revision is not None:
 
61
        graph = branch.repository.get_graph()
 
62
        ignore = set([r for r,ps in graph.iter_ancestry([last_revision])])
 
63
    else:
 
64
        ignore = set([])
 
65
 
 
66
    pm = []
 
67
    for merge in pending:
 
68
        ignore.add(merge)
 
69
        try:
 
70
            rev = branch.repository.get_revision(merge)
 
71
            children = []
 
72
            pm.append((rev, children))
 
73
 
 
74
            # This does need to be topo sorted, so we search backwards
 
75
            inner_merges = branch.repository.get_ancestry(merge)
 
76
            assert inner_merges[0] is None
 
77
            inner_merges.pop(0)
 
78
            for mmerge in reversed(inner_merges):
 
79
                if mmerge in ignore:
 
80
                    continue
 
81
                rev = branch.repository.get_revision(mmerge)
 
82
                children.append(rev)
 
83
 
 
84
                ignore.add(mmerge)
 
85
        except errors.NoSuchRevision:
 
86
            print "DEBUG: NoSuchRevision:", merge
 
87
 
 
88
    return pm
 
89
 
 
90
 
 
91
_newline_variants_re = re.compile(r'\r\n?')
 
92
def _sanitize_and_decode_message(utf8_message):
 
93
    """Turn a utf-8 message into a sanitized Unicode message."""
 
94
    fixed_newline = _newline_variants_re.sub('\n', utf8_message)
 
95
    return fixed_newline.decode('utf-8')
 
96
 
 
97
 
 
98
class CommitDialog(Gtk.Dialog):
 
99
    """Implementation of Commit."""
 
100
 
 
101
    def __init__(self, wt, selected=None, parent=None):
 
102
        super(CommitDialog, self).__init__(
 
103
            title="Commit to %s" % wt.basedir, parent=parent, flags=0)
 
104
        self.connect('delete-event', self._on_delete_window)
 
105
        self._question_dialog = question_dialog
 
106
 
 
107
        self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
 
108
 
 
109
        self._wt = wt
 
110
        # TODO: Do something with this value, it is used by Olive
 
111
        #       It used to set all changes but this one to False
 
112
        self._selected = selected
 
113
        self._enable_per_file_commits = True
 
114
        self._commit_all_changes = True
 
115
        self.committed_revision_id = None # Nothing has been committed yet
 
116
        self._saved_commit_messages_manager = SavedCommitMessagesManager(
 
117
            self._wt, self._wt.branch)
 
118
 
 
119
        self.setup_params()
 
120
        self.construct()
 
121
        self.fill_in_data()
 
122
 
 
123
    def setup_params(self):
 
124
        """Setup the member variables for state."""
 
125
        self._basis_tree = self._wt.basis_tree()
 
126
        self._delta = None
 
127
        self._wt.lock_read()
 
128
        try:
 
129
            self._pending = pending_revisions(self._wt)
 
130
        finally:
 
131
            self._wt.unlock()
 
132
 
 
133
        self._is_checkout = (self._wt.branch.get_bound_location() is not None)
 
134
 
 
135
    def fill_in_data(self):
 
136
        # Now that we are built, handle changes to the view based on the state
 
137
        self._fill_in_pending()
 
138
        self._fill_in_diff()
 
139
        self._fill_in_files()
 
140
        self._fill_in_checkout()
 
141
        self._fill_in_per_file_info()
 
142
 
 
143
    def _fill_in_pending(self):
 
144
        if not self._pending:
 
145
            self._pending_box.hide()
 
146
            return
 
147
 
 
148
        # TODO: We'd really prefer this to be a nested list
 
149
        for rev, children in self._pending:
 
150
            rev_info = self._rev_to_pending_info(rev)
 
151
            self._pending_store.append([
 
152
                rev_info['revision_id'],
 
153
                rev_info['date'],
 
154
                rev_info['committer'],
 
155
                rev_info['summary'],
 
156
                ])
 
157
            for child in children:
 
158
                rev_info = self._rev_to_pending_info(child)
 
159
                self._pending_store.append([
 
160
                    rev_info['revision_id'],
 
161
                    rev_info['date'],
 
162
                    rev_info['committer'],
 
163
                    rev_info['summary'],
 
164
                    ])
 
165
        self._pending_box.show()
 
166
 
 
167
    def _fill_in_files(self):
 
168
        # We should really use add a progress bar of some kind.
 
169
        # While we fill in the view, hide the store
 
170
        store = self._files_store
 
171
        self._treeview_files.set_model(None)
 
172
 
 
173
        added = _i18n('added')
 
174
        removed = _i18n('removed')
 
175
        renamed = _i18n('renamed')
 
176
        renamed_and_modified = _i18n('renamed and modified')
 
177
        modified = _i18n('modified')
 
178
        kind_changed = _i18n('kind changed')
 
179
 
 
180
        # The store holds:
 
181
        # [file_id, real path, checkbox, display path, changes type, message]
 
182
        # iter_changes returns:
 
183
        # (file_id, (path_in_source, path_in_target),
 
184
        #  changed_content, versioned, parent, name, kind,
 
185
        #  executable)
 
186
 
 
187
        all_enabled = (self._selected is None)
 
188
        # The first entry is always the 'whole tree'
 
189
        all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
 
190
        initial_cursor = store.get_path(all_iter)
 
191
        # should we pass specific_files?
 
192
        self._wt.lock_read()
 
193
        self._basis_tree.lock_read()
 
194
        try:
 
195
            from diff import iter_changes_to_status
 
196
            saved_file_messages = self._saved_commit_messages_manager.get()[1]
 
197
            for (file_id, real_path, change_type, display_path
 
198
                ) in iter_changes_to_status(self._basis_tree, self._wt):
 
199
                if self._selected and real_path != self._selected:
 
200
                    enabled = False
 
201
                else:
 
202
                    enabled = True
 
203
                try:
 
204
                    default_message = saved_file_messages[file_id]
 
205
                except KeyError:
 
206
                    default_message = ''
 
207
                item_iter = store.append([
 
208
                    file_id,
 
209
                    real_path.encode('UTF-8'),
 
210
                    enabled,
 
211
                    display_path.encode('UTF-8'),
 
212
                    change_type,
 
213
                    default_message, # Initial comment
 
214
                    ])
 
215
                if self._selected and enabled:
 
216
                    initial_cursor = store.get_path(item_iter)
 
217
        finally:
 
218
            self._basis_tree.unlock()
 
219
            self._wt.unlock()
 
220
 
 
221
        self._treeview_files.set_model(store)
 
222
        self._last_selected_file = None
 
223
        # This sets the cursor, which causes the expander to close, which
 
224
        # causes the _file_message_text_view to never get realized. So we have
 
225
        # to give it a little kick, or it warns when we try to grab the focus
 
226
        self._treeview_files.set_cursor(initial_cursor, None, False)
 
227
 
 
228
        def _realize_file_message_tree_view(*args):
 
229
            self._file_message_text_view.realize()
 
230
        self.connect_after('realize', _realize_file_message_tree_view)
 
231
 
 
232
    def _fill_in_diff(self):
 
233
        self._diff_view.set_trees(self._wt, self._basis_tree)
 
234
 
 
235
    def _fill_in_checkout(self):
 
236
        if not self._is_checkout:
 
237
            self._check_local.hide()
 
238
            return
 
239
        if have_dbus:
 
240
            bus = dbus.SystemBus()
 
241
            try:
 
242
                proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
 
243
                                           '/org/freedesktop/NetworkManager')
 
244
            except dbus.DBusException:
 
245
                trace.mutter("networkmanager not available.")
 
246
                self._check_local.show()
 
247
                return
 
248
            
 
249
            dbus_iface = dbus.Interface(proxy_obj,
 
250
                                        'org.freedesktop.NetworkManager')
 
251
            try:
 
252
                # 3 is the enum value for STATE_CONNECTED
 
253
                self._check_local.set_active(dbus_iface.state() != 3)
 
254
            except dbus.DBusException, e:
 
255
                # Silently drop errors. While DBus may be
 
256
                # available, NetworkManager doesn't necessarily have to be
 
257
                trace.mutter("unable to get networkmanager state: %r" % e)
 
258
        self._check_local.show()
 
259
 
 
260
    def _fill_in_per_file_info(self):
 
261
        config = self._wt.branch.get_config()
 
262
        enable_per_file_commits = config.get_user_option('per_file_commits')
 
263
        if (enable_per_file_commits is None
 
264
            or enable_per_file_commits.lower()
 
265
                not in ('y', 'yes', 'on', 'enable', '1', 't', 'true')):
 
266
            self._enable_per_file_commits = False
 
267
        else:
 
268
            self._enable_per_file_commits = True
 
269
        if not self._enable_per_file_commits:
 
270
            self._file_message_expander.hide()
 
271
            self._global_message_label.set_markup(_i18n('<b>Commit Message</b>'))
 
272
 
 
273
    def _compute_delta(self):
 
274
        self._delta = self._wt.changes_from(self._basis_tree)
 
275
 
 
276
    def construct(self):
 
277
        """Build up the dialog widgets."""
 
278
        # The primary pane which splits it into left and right (adjustable)
 
279
        # sections.
 
280
        self._hpane = Gtk.HPaned()
 
281
 
 
282
        self._construct_left_pane()
 
283
        self._construct_right_pane()
 
284
        self._construct_action_pane()
 
285
 
 
286
        self.get_content_area().pack_start(self._hpane, True, True, 0)
 
287
        self._hpane.show()
 
288
        self.set_focus(self._global_message_text_view)
 
289
 
 
290
        self._construct_accelerators()
 
291
        self._set_sizes()
 
292
 
 
293
    def _set_sizes(self):
 
294
        # This seems like a reasonable default, we might like it to
 
295
        # be a bit wider, so that by default we can fit an 80-line diff in the
 
296
        # diff window.
 
297
        # Alternatively, we should be saving the last position/size rather than
 
298
        # setting it to a fixed value every time we start up.
 
299
        screen = self.get_screen()
 
300
        monitor = 0 # We would like it to be the monitor we are going to
 
301
                    # display on, but I don't know how to figure that out
 
302
                    # Only really useful for freaks like me that run dual
 
303
                    # monitor, with different sizes on the monitors
 
304
        monitor_rect = screen.get_monitor_geometry(monitor)
 
305
        width = int(monitor_rect.width * 0.66)
 
306
        height = int(monitor_rect.height * 0.66)
 
307
        self.set_default_size(width, height)
 
308
        self._hpane.set_position(300)
 
309
 
 
310
    def _construct_accelerators(self):
 
311
        group = Gtk.AccelGroup()
 
312
        group.connect(Gdk.keyval_from_name('N'),
 
313
                      Gdk.ModifierType.CONTROL_MASK, 0, self._on_accel_next)
 
314
        self.add_accel_group(group)
 
315
 
 
316
        # ignore the escape key (avoid closing the window)
 
317
        self.connect_object('close', self.emit_stop_by_name, 'close')
 
318
 
 
319
    def _construct_left_pane(self):
 
320
        self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
 
321
        self._construct_file_list()
 
322
        self._construct_pending_list()
 
323
 
 
324
        self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
 
325
                                            use_underline=True)
 
326
        self._left_pane_box.pack_end(self._check_local, False, False, 0)
 
327
        self._check_local.set_active(False)
 
328
 
 
329
        self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
 
330
        self._left_pane_box.show()
 
331
 
 
332
    def _construct_right_pane(self):
 
333
        # TODO: I really want to make it so the diff view gets more space than
 
334
        # the global commit message, and the per-file commit message gets even
 
335
        # less. When I did it with wxGlade, I set it to 4 for diff, 2 for
 
336
        # commit, and 1 for file commit, and it looked good. But I don't seem
 
337
        # to have a way to do that with the gtk boxes... :( (Which is extra
 
338
        # weird since wx uses gtk on Linux...)
 
339
        self._right_pane_table = Gtk.Table(rows=10, columns=1, homogeneous=False)
 
340
        self._right_pane_table.set_row_spacings(5)
 
341
        self._right_pane_table.set_col_spacings(5)
 
342
        self._right_pane_table_row = 0
 
343
        self._construct_diff_view()
 
344
        self._construct_file_message()
 
345
        self._construct_global_message()
 
346
 
 
347
        self._right_pane_table.show()
 
348
        self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
 
349
 
 
350
    def _construct_action_pane(self):
 
351
        self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
 
352
        self._button_cancel.connect('clicked', self._on_cancel_clicked)
 
353
        self._button_cancel.show()
 
354
        self.get_action_area().pack_end(
 
355
            self._button_cancel, True, True, 0)
 
356
        self._button_commit = Gtk.Button(_i18n("Comm_it"), use_underline=True)
 
357
        self._button_commit.connect('clicked', self._on_commit_clicked)
 
358
        self._button_commit.set_can_default(True)
 
359
        self._button_commit.show()
 
360
        self.get_action_area().pack_end(
 
361
            self._button_commit, True, True, 0)
 
362
        self._button_commit.grab_default()
 
363
 
 
364
    def _add_to_right_table(self, widget, weight, expanding=False):
 
365
        """Add another widget to the table
 
366
 
 
367
        :param widget: The object to add
 
368
        :param weight: How many rows does this widget get to request
 
369
        :param expanding: Should expand|fill|shrink be set?
 
370
        """
 
371
        end_row = self._right_pane_table_row + weight
 
372
        options = 0
 
373
        expand_opts = Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK
 
374
        if expanding:
 
375
            options = expand_opts
 
376
        self._right_pane_table.attach(widget, 0, 1,
 
377
                                      self._right_pane_table_row, end_row,
 
378
                                      xoptions=expand_opts, yoptions=options)
 
379
        self._right_pane_table_row = end_row
 
380
 
 
381
    def _construct_file_list(self):
 
382
        self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
 
383
        file_label = Gtk.Label(label=_i18n('Files'))
 
384
        # file_label.show()
 
385
        self._files_box.pack_start(file_label, False, True, 0)
 
386
 
 
387
        self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
 
388
            None, _i18n("Commit all changes"))
 
389
        self._files_box.pack_start(self._commit_all_files_radio, False, True, 0)
 
390
        self._commit_all_files_radio.show()
 
391
        self._commit_all_files_radio.connect('toggled',
 
392
            self._toggle_commit_selection)
 
393
        self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
 
394
            self._commit_all_files_radio, _i18n("Only commit selected changes"))
 
395
        self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
 
396
        self._commit_selected_radio.show()
 
397
        self._commit_selected_radio.connect('toggled',
 
398
            self._toggle_commit_selection)
 
399
        if self._pending:
 
400
            self._commit_all_files_radio.set_label(_i18n('Commit all changes*'))
 
401
            self._commit_all_files_radio.set_sensitive(False)
 
402
            self._commit_selected_radio.set_sensitive(False)
 
403
 
 
404
        scroller = Gtk.ScrolledWindow()
 
405
        scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 
406
        self._treeview_files = Gtk.TreeView()
 
407
        self._treeview_files.show()
 
408
        scroller.add(self._treeview_files)
 
409
        scroller.set_shadow_type(Gtk.ShadowType.IN)
 
410
        scroller.show()
 
411
        self._files_box.pack_start(scroller, True, True, 0)
 
412
        self._files_box.show()
 
413
        self._left_pane_box.pack_start(self._files_box, True, True, 0)
 
414
 
 
415
        # Keep note that all strings stored in a ListStore must be UTF-8
 
416
        # strings. GTK does not support directly setting and restoring Unicode
 
417
        # objects.
 
418
        liststore = Gtk.ListStore(
 
419
            GObject.TYPE_STRING,  # [0] file_id
 
420
            GObject.TYPE_STRING,  # [1] real path
 
421
            GObject.TYPE_BOOLEAN, # [2] checkbox
 
422
            GObject.TYPE_STRING,  # [3] display path
 
423
            GObject.TYPE_STRING,  # [4] changes type
 
424
            GObject.TYPE_STRING,  # [5] commit message
 
425
            )
 
426
        self._files_store = liststore
 
427
        self._treeview_files.set_model(liststore)
 
428
        crt = Gtk.CellRendererToggle()
 
429
        crt.set_property('activatable', not bool(self._pending))
 
430
        crt.connect("toggled", self._toggle_commit, self._files_store)
 
431
        if self._pending:
 
432
            name = _i18n('Commit*')
 
433
        else:
 
434
            name = _i18n('Commit')
 
435
        commit_col = Gtk.TreeViewColumn(name, crt, active=2)
 
436
        commit_col.set_visible(False)
 
437
        self._treeview_files.append_column(commit_col)
 
438
        self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Path'),
 
439
                                           Gtk.CellRendererText(), text=3))
 
440
        self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Type'),
 
441
                                           Gtk.CellRendererText(), text=4))
 
442
        self._treeview_files.connect('cursor-changed',
 
443
                                     self._on_treeview_files_cursor_changed)
 
444
 
 
445
    def _toggle_commit(self, cell, path, model):
 
446
        if model[path][0] is None: # No file_id means 'All Files'
 
447
            new_val = not model[path][2]
 
448
            for node in model:
 
449
                node[2] = new_val
 
450
        else:
 
451
            model[path][2] = not model[path][2]
 
452
 
 
453
    def _toggle_commit_selection(self, button):
 
454
        all_files = self._commit_all_files_radio.get_active()
 
455
        if self._commit_all_changes != all_files:
 
456
            checked_col = self._treeview_files.get_column(0)
 
457
            self._commit_all_changes = all_files
 
458
            if all_files:
 
459
                checked_col.set_visible(False)
 
460
            else:
 
461
                checked_col.set_visible(True)
 
462
            renderer = checked_col.get_cells()[0]
 
463
            renderer.set_property('activatable', not all_files)
 
464
 
 
465
    def _construct_pending_list(self):
 
466
        # Pending information defaults to hidden, we put it all in 1 box, so
 
467
        # that we can show/hide all of them at once
 
468
        self._pending_box = Gtk.VBox()
 
469
        self._pending_box.hide()
 
470
 
 
471
        pending_message = Gtk.Label()
 
472
        pending_message.set_markup(
 
473
            _i18n('<i>* Cannot select specific files when merging</i>'))
 
474
        self._pending_box.pack_start(pending_message, False, True, 5)
 
475
        pending_message.show()
 
476
 
 
477
        pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
 
478
        self._pending_box.pack_start(pending_label, False, True, 0)
 
479
        pending_label.show()
 
480
 
 
481
        scroller = Gtk.ScrolledWindow()
 
482
        scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 
483
        self._treeview_pending = Gtk.TreeView()
 
484
        scroller.add(self._treeview_pending)
 
485
        scroller.set_shadow_type(Gtk.ShadowType.IN)
 
486
        scroller.show()
 
487
        self._pending_box.pack_start(scroller, True, True, 5)
 
488
        self._treeview_pending.show()
 
489
        self._left_pane_box.pack_start(self._pending_box, True, True, 0)
 
490
 
 
491
        liststore = Gtk.ListStore(GObject.TYPE_STRING, # revision_id
 
492
                                  GObject.TYPE_STRING, # date
 
493
                                  GObject.TYPE_STRING, # committer
 
494
                                  GObject.TYPE_STRING, # summary
 
495
                                 )
 
496
        self._pending_store = liststore
 
497
        self._treeview_pending.set_model(liststore)
 
498
        self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Date'),
 
499
                                             Gtk.CellRendererText(), text=1))
 
500
        self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Committer'),
 
501
                                             Gtk.CellRendererText(), text=2))
 
502
        self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Summary'),
 
503
                                             Gtk.CellRendererText(), text=3))
 
504
 
 
505
    def _construct_diff_view(self):
 
506
        from bzrlib.plugins.gtk.diff import DiffView
 
507
 
 
508
        # TODO: jam 2007-10-30 The diff label is currently disabled. If we
 
509
        #       decide that we really don't ever want to display it, we should
 
510
        #       actually remove it, and other references to it, along with the
 
511
        #       tests that it is set properly.
 
512
        self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
 
513
        self._diff_label.set_alignment(0, 0)
 
514
        self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
 
515
        self._add_to_right_table(self._diff_label, 1, False)
 
516
        # self._diff_label.show()
 
517
 
 
518
        self._diff_view = DiffView()
 
519
        self._add_to_right_table(self._diff_view, 4, True)
 
520
        self._diff_view.show()
 
521
 
 
522
    def _construct_file_message(self):
 
523
        scroller = Gtk.ScrolledWindow()
 
524
        scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 
525
 
 
526
        self._file_message_text_view = Gtk.TextView()
 
527
        scroller.add(self._file_message_text_view)
 
528
        scroller.set_shadow_type(Gtk.ShadowType.IN)
 
529
        scroller.show()
 
530
 
 
531
        self._file_message_text_view.modify_font(Pango.FontDescription("Monospace"))
 
532
        self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
 
533
        self._file_message_text_view.set_accepts_tab(False)
 
534
        self._file_message_text_view.show()
 
535
 
 
536
        self._file_message_expander = Gtk.Expander(
 
537
            label=_i18n('File commit message'))
 
538
        self._file_message_expander.set_expanded(True)
 
539
        self._file_message_expander.add(scroller)
 
540
        self._add_to_right_table(self._file_message_expander, 1, False)
 
541
        self._file_message_expander.show()
 
542
 
 
543
    def _construct_global_message(self):
 
544
        self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
 
545
        self._global_message_label.set_markup(
 
546
            _i18n('<b>Global Commit Message</b>'))
 
547
        self._global_message_label.set_alignment(0, 0)
 
548
        self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
 
549
        self._add_to_right_table(self._global_message_label, 1, False)
 
550
        # Can we remove the spacing between the label and the box?
 
551
        self._global_message_label.show()
 
552
 
 
553
        scroller = Gtk.ScrolledWindow()
 
554
        scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 
555
 
 
556
        self._global_message_text_view = Gtk.TextView()
 
557
        self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
 
558
        self._global_message_text_view.modify_font(Pango.FontDescription("Monospace"))
 
559
        scroller.add(self._global_message_text_view)
 
560
        scroller.set_shadow_type(Gtk.ShadowType.IN)
 
561
        scroller.show()
 
562
        self._add_to_right_table(scroller, 2, True)
 
563
        self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
 
564
        self._file_message_text_view.set_accepts_tab(False)
 
565
        self._global_message_text_view.show()
 
566
 
 
567
    def _on_treeview_files_cursor_changed(self, treeview):
 
568
        treeselection = treeview.get_selection()
 
569
        (model, selection) = treeselection.get_selected()
 
570
 
 
571
        if selection is not None:
 
572
            path, display_path = model.get(selection, 1, 3)
 
573
            self._diff_label.set_text(_i18n('Diff for ') + display_path)
 
574
            if path is None:
 
575
                self._diff_view.show_diff(None)
 
576
            else:
 
577
                self._diff_view.show_diff([path.decode('UTF-8')])
 
578
            self._update_per_file_info(selection)
 
579
 
 
580
    def _on_accel_next(self, accel_group, window, keyval, modifier):
 
581
        # We don't really care about any of the parameters, because we know
 
582
        # where this message came from
 
583
        tree_selection = self._treeview_files.get_selection()
 
584
        (model, selection) = tree_selection.get_selected()
 
585
        if selection is None:
 
586
            next = None
 
587
        else:
 
588
            next = model.iter_next(selection)
 
589
 
 
590
        if next is None:
 
591
            # We have either made it to the end of the list, or nothing was
 
592
            # selected. Either way, select All Files, and jump to the global
 
593
            # commit message.
 
594
            self._treeview_files.set_cursor(
 
595
                Gtk.TreePath(path=0), None, False)
 
596
            self._global_message_text_view.grab_focus()
 
597
        else:
 
598
            # Set the cursor to this entry, and jump to the per-file commit
 
599
            # message
 
600
            self._treeview_files.set_cursor(model.get_path(next), None, False)
 
601
            self._file_message_text_view.grab_focus()
 
602
 
 
603
    def _save_current_file_message(self):
 
604
        if self._last_selected_file is None:
 
605
            return # Nothing to save
 
606
        text_buffer = self._file_message_text_view.get_buffer()
 
607
        cur_text = text_buffer.get_text(text_buffer.get_start_iter(),
 
608
                                        text_buffer.get_end_iter(), True)
 
609
        last_selected = self._files_store.get_iter(self._last_selected_file)
 
610
        self._files_store.set_value(last_selected, 5, cur_text)
 
611
 
 
612
    def _update_per_file_info(self, selection):
 
613
        # The node is changing, so cache the current message
 
614
        if not self._enable_per_file_commits:
 
615
            return
 
616
 
 
617
        self._save_current_file_message()
 
618
        text_buffer = self._file_message_text_view.get_buffer()
 
619
        file_id, display_path, message = self._files_store.get(selection, 0, 3, 5)
 
620
        if file_id is None: # Whole tree
 
621
            self._file_message_expander.set_label(_i18n('File commit message'))
 
622
            self._file_message_expander.set_expanded(False)
 
623
            self._file_message_expander.set_sensitive(False)
 
624
            text_buffer.set_text('')
 
625
            self._last_selected_file = None
 
626
        else:
 
627
            self._file_message_expander.set_label(_i18n('Commit message for ')
 
628
                                                  + display_path)
 
629
            self._file_message_expander.set_expanded(True)
 
630
            self._file_message_expander.set_sensitive(True)
 
631
            text_buffer.set_text(message)
 
632
            self._last_selected_file = self._files_store.get_path(selection)
 
633
 
 
634
    def _get_specific_files(self):
 
635
        """Return the list of selected paths, and file info.
 
636
 
 
637
        :return: ([unicode paths], [{utf-8 file info}]
 
638
        """
 
639
        self._save_current_file_message()
 
640
        files = []
 
641
        records = iter(self._files_store)
 
642
        rec = records.next() # Skip the All Files record
 
643
        assert rec[0] is None, "Are we skipping the wrong record?"
 
644
 
 
645
        file_info = []
 
646
        for record in records:
 
647
            if self._commit_all_changes or record[2]:# [2] checkbox
 
648
                file_id = record[0] # [0] file_id
 
649
                path = record[1]    # [1] real path
 
650
                # [5] commit message
 
651
                file_message = _sanitize_and_decode_message(record[5])
 
652
                files.append(path.decode('UTF-8'))
 
653
                if self._enable_per_file_commits and file_message:
 
654
                    # All of this needs to be utf-8 information
 
655
                    file_message = file_message.encode('UTF-8')
 
656
                    file_info.append({'path':path, 'file_id':file_id,
 
657
                                     'message':file_message})
 
658
        file_info.sort(key=lambda x:(x['path'], x['file_id']))
 
659
        if self._enable_per_file_commits:
 
660
            return files, file_info
 
661
        else:
 
662
            return files, []
 
663
 
 
664
    @show_bzr_error
 
665
    def _on_cancel_clicked(self, button):
 
666
        """ Cancel button clicked handler. """
 
667
        self._do_cancel()
 
668
 
 
669
    @show_bzr_error
 
670
    def _on_delete_window(self, source, event):
 
671
        """ Delete window handler. """
 
672
        self._do_cancel()
 
673
 
 
674
    def _do_cancel(self):
 
675
        """If requested, saves commit messages when cancelling gcommit; they are re-used by a next gcommit"""
 
676
        mgr = SavedCommitMessagesManager()
 
677
        self._saved_commit_messages_manager = mgr
 
678
        mgr.insert(self._get_global_commit_message(),
 
679
                   self._get_specific_files()[1])
 
680
        if mgr.is_not_empty(): # maybe worth saving
 
681
            response = self._question_dialog(
 
682
                _i18n('Commit cancelled'),
 
683
                _i18n('Do you want to save your commit messages ?'),
 
684
                parent=self)
 
685
            if response == Gtk.ResponseType.NO:
 
686
                 # save nothing and destroy old comments if any
 
687
                mgr = SavedCommitMessagesManager()
 
688
        mgr.save(self._wt, self._wt.branch)
 
689
        self.response(Gtk.ResponseType.CANCEL) # close window
 
690
 
 
691
    @show_bzr_error
 
692
    def _on_commit_clicked(self, button):
 
693
        """ Commit button clicked handler. """
 
694
        self._do_commit()
 
695
 
 
696
    def _do_commit(self):
 
697
        message = self._get_global_commit_message()
 
698
 
 
699
        if message == '':
 
700
            response = self._question_dialog(
 
701
                _i18n('Commit with an empty message?'),
 
702
                _i18n('You can describe your commit intent in the message.'),
 
703
                parent=self)
 
704
            if response == Gtk.ResponseType.NO:
 
705
                # Kindly give focus to message area
 
706
                self._global_message_text_view.grab_focus()
 
707
                return
 
708
 
 
709
        specific_files, file_info = self._get_specific_files()
 
710
        if self._pending:
 
711
            specific_files = None
 
712
 
 
713
        local = self._check_local.get_active()
 
714
 
 
715
        # All we care about is if there is a single unknown, so if this loop is
 
716
        # entered, then there are unknown files.
 
717
        # TODO: jam 20071002 It seems like this should cancel the dialog
 
718
        #       entirely, since there isn't a way for them to add the unknown
 
719
        #       files at this point.
 
720
        for path in self._wt.unknowns():
 
721
            response = self._question_dialog(
 
722
                _i18n("Commit with unknowns?"),
 
723
                _i18n("Unknown files exist in the working tree. Commit anyway?"),
 
724
                parent=self)
 
725
                # Doesn't set a parent for the dialog..
 
726
            if response == Gtk.ResponseType.NO:
 
727
                return
 
728
            break
 
729
 
 
730
        rev_id = None
 
731
        revprops = {}
 
732
        if file_info:
 
733
            revprops['file-info'] = bencode.bencode(file_info).decode('UTF-8')
 
734
        try:
 
735
            rev_id = self._wt.commit(message,
 
736
                       allow_pointless=False,
 
737
                       strict=False,
 
738
                       local=local,
 
739
                       specific_files=specific_files,
 
740
                       revprops=revprops)
 
741
        except errors.PointlessCommit:
 
742
            response = self._question_dialog(
 
743
                _i18n('Commit with no changes?'),
 
744
                _i18n('There are no changes in the working tree.'
 
745
                      ' Do you want to commit anyway?'),
 
746
                parent=self)
 
747
            if response == Gtk.ResponseType.YES:
 
748
                rev_id = self._wt.commit(message,
 
749
                               allow_pointless=True,
 
750
                               strict=False,
 
751
                               local=local,
 
752
                               specific_files=specific_files,
 
753
                               revprops=revprops)
 
754
        self.committed_revision_id = rev_id
 
755
        # destroy old comments if any
 
756
        SavedCommitMessagesManager().save(self._wt, self._wt.branch)
 
757
        self.response(Gtk.ResponseType.OK)
 
758
 
 
759
    def _get_global_commit_message(self):
 
760
        buf = self._global_message_text_view.get_buffer()
 
761
        start, end = buf.get_bounds()
 
762
        text = buf.get_text(start, end, True)
 
763
        return _sanitize_and_decode_message(text)
 
764
 
 
765
    def _set_global_commit_message(self, message):
 
766
        """Just a helper for the test suite."""
 
767
        if isinstance(message, unicode):
 
768
            message = message.encode('UTF-8')
 
769
        self._global_message_text_view.get_buffer().set_text(message)
 
770
 
 
771
    def _set_file_commit_message(self, message):
 
772
        """Helper for the test suite."""
 
773
        if isinstance(message, unicode):
 
774
            message = message.encode('UTF-8')
 
775
        self._file_message_text_view.get_buffer().set_text(message)
 
776
 
 
777
    @staticmethod
 
778
    def _rev_to_pending_info(rev):
 
779
        """Get the information from a pending merge."""
 
780
        from bzrlib.osutils import format_date
 
781
        rev_dict = {}
 
782
        rev_dict['committer'] = re.sub('<.*@.*>', '', rev.committer).strip(' ')
 
783
        rev_dict['summary'] = rev.get_summary()
 
784
        rev_dict['date'] = format_date(rev.timestamp,
 
785
                                       rev.timezone or 0,
 
786
                                       'original', date_fmt="%Y-%m-%d",
 
787
                                       show_offset=False)
 
788
        rev_dict['revision_id'] = rev.revision_id
 
789
        return rev_dict
 
790