/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-03-14 20:12:19 UTC
  • Revision ID: jelmer@samba.org-20110314201219-wo692nzwywu6mevh
Fix formatting, imports.

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