/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: Vincent Ladeuil
  • Date: 2009-05-06 11:51:38 UTC
  • mfrom: (628.1.2 gtk)
  • Revision ID: v.ladeuil+lp@free.fr-20090506115138-7abxvg2qz4gzraup
UpgradeĀ COMPATIBLE_BZR_VERSIONS

Show diffs side-by-side

added added

removed removed

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