99
class CommitDialog(gtk.Dialog):
92
_newline_variants_re = re.compile(r'\r\n?')
93
def _sanitize_and_decode_message(utf8_message):
94
"""Turn a utf-8 message into a sanitized Unicode message."""
95
fixed_newline = _newline_variants_re.sub('\n', utf8_message)
96
return osutils.safe_unicode(fixed_newline)
99
class CommitDialog(Gtk.Dialog):
100
100
"""Implementation of Commit."""
102
102
def __init__(self, wt, selected=None, parent=None):
103
gtk.Dialog.__init__(self, title="Commit - Olive",
106
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
103
super(CommitDialog, self).__init__(
104
title="Commit to %s" % wt.basedir, parent=parent, flags=0)
105
self.connect('delete-event', self._on_delete_window)
107
106
self._question_dialog = question_dialog
108
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
110
111
# TODO: Do something with this value, it is used by Olive
111
112
# It used to set all changes but this one to False
181
189
all_enabled = (self._selected is None)
182
190
# The first entry is always the 'whole tree'
183
all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
191
all_iter = store.append(["", "", all_enabled, 'All Files', '', ''])
184
192
initial_cursor = store.get_path(all_iter)
185
193
# should we pass specific_files?
186
194
self._wt.lock_read()
187
195
self._basis_tree.lock_read()
189
197
from diff import iter_changes_to_status
198
saved_file_messages = self._saved_commit_messages_manager.get()[1]
190
199
for (file_id, real_path, change_type, display_path
191
200
) in iter_changes_to_status(self._basis_tree, self._wt):
192
201
if self._selected and real_path != self._selected:
206
default_message = saved_file_messages[file_id]
196
209
item_iter = store.append([
198
211
real_path.encode('UTF-8'),
200
213
display_path.encode('UTF-8'),
202
'', # Initial comment
215
default_message, # Initial comment
204
217
if self._selected and enabled:
205
218
initial_cursor = store.get_path(item_iter)
297
310
self._hpane.set_position(300)
299
312
def _construct_accelerators(self):
300
group = gtk.AccelGroup()
301
group.connect_group(gtk.gdk.keyval_from_name('N'),
302
gtk.gdk.CONTROL_MASK, 0, self._on_accel_next)
313
group = Gtk.AccelGroup()
314
group.connect(Gdk.keyval_from_name('N'),
315
Gdk.ModifierType.CONTROL_MASK, 0, self._on_accel_next)
303
316
self.add_accel_group(group)
318
# ignore the escape key (avoid closing the window)
319
self.connect_object('close', self.emit_stop_by_name, 'close')
305
321
def _construct_left_pane(self):
306
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
322
self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
307
323
self._construct_file_list()
308
324
self._construct_pending_list()
310
self._check_local = gtk.CheckButton(_("_Only commit locally"),
326
self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
311
327
use_underline=True)
312
self._left_pane_box.pack_end(self._check_local, False, False)
328
self._left_pane_box.pack_end(self._check_local, False, False, 0)
313
329
self._check_local.set_active(False)
315
331
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
334
350
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
336
352
def _construct_action_pane(self):
337
self._button_commit = gtk.Button(_("Comm_it"), use_underline=True)
353
self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
354
self._button_cancel.connect('clicked', self._on_cancel_clicked)
355
self._button_cancel.show()
356
self.get_action_area().pack_end(
357
self._button_cancel, True, True, 0)
358
self._button_commit = Gtk.Button(_i18n("Comm_it"), use_underline=True)
338
359
self._button_commit.connect('clicked', self._on_commit_clicked)
339
self._button_commit.set_flags(gtk.CAN_DEFAULT)
360
self._button_commit.set_can_default(True)
340
361
self._button_commit.show()
341
self.action_area.pack_end(self._button_commit)
362
self.get_action_area().pack_end(
363
self._button_commit, True, True, 0)
342
364
self._button_commit.grab_default()
344
366
def _add_to_right_table(self, widget, weight, expanding=False):
359
381
self._right_pane_table_row = end_row
361
383
def _construct_file_list(self):
362
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
363
file_label = gtk.Label(_('Files'))
384
self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
385
file_label = Gtk.Label(label=_i18n('Files'))
364
386
# file_label.show()
365
self._files_box.pack_start(file_label, expand=False)
387
self._files_box.pack_start(file_label, False, True, 0)
367
self._commit_all_files_radio = gtk.RadioButton(
368
None, _("Commit all changes"))
369
self._files_box.pack_start(self._commit_all_files_radio, expand=False)
389
self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
390
None, _i18n("Commit all changes"))
391
self._files_box.pack_start(self._commit_all_files_radio, False, True, 0)
370
392
self._commit_all_files_radio.show()
371
393
self._commit_all_files_radio.connect('toggled',
372
394
self._toggle_commit_selection)
373
self._commit_selected_radio = gtk.RadioButton(
374
self._commit_all_files_radio, _("Only commit selected changes"))
375
self._files_box.pack_start(self._commit_selected_radio, expand=False)
395
self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
396
self._commit_all_files_radio, _i18n("Only commit selected changes"))
397
self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
376
398
self._commit_selected_radio.show()
377
399
self._commit_selected_radio.connect('toggled',
378
400
self._toggle_commit_selection)
379
401
if self._pending:
380
self._commit_all_files_radio.set_label(_('Commit all changes*'))
402
self._commit_all_files_radio.set_label(_i18n('Commit all changes*'))
381
403
self._commit_all_files_radio.set_sensitive(False)
382
404
self._commit_selected_radio.set_sensitive(False)
384
scroller = gtk.ScrolledWindow()
385
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
386
self._treeview_files = gtk.TreeView()
406
scroller = Gtk.ScrolledWindow()
407
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
408
self._treeview_files = Gtk.TreeView()
387
409
self._treeview_files.show()
388
410
scroller.add(self._treeview_files)
389
scroller.set_shadow_type(gtk.SHADOW_IN)
411
scroller.set_shadow_type(Gtk.ShadowType.IN)
391
self._files_box.pack_start(scroller,
392
expand=True, fill=True)
413
self._files_box.pack_start(scroller, True, True, 0)
393
414
self._files_box.show()
394
self._left_pane_box.pack_start(self._files_box)
415
self._left_pane_box.pack_start(self._files_box, True, True, 0)
396
417
# Keep note that all strings stored in a ListStore must be UTF-8
397
418
# strings. GTK does not support directly setting and restoring Unicode
399
liststore = gtk.ListStore(
400
gobject.TYPE_STRING, # [0] file_id
401
gobject.TYPE_STRING, # [1] real path
402
gobject.TYPE_BOOLEAN, # [2] checkbox
403
gobject.TYPE_STRING, # [3] display path
404
gobject.TYPE_STRING, # [4] changes type
405
gobject.TYPE_STRING, # [5] commit message
420
liststore = Gtk.ListStore(
421
GObject.TYPE_STRING, # [0] file_id
422
GObject.TYPE_STRING, # [1] real path
423
GObject.TYPE_BOOLEAN, # [2] checkbox
424
GObject.TYPE_STRING, # [3] display path
425
GObject.TYPE_STRING, # [4] changes type
426
GObject.TYPE_STRING, # [5] commit message
407
428
self._files_store = liststore
408
429
self._treeview_files.set_model(liststore)
409
crt = gtk.CellRendererToggle()
430
crt = Gtk.CellRendererToggle()
410
431
crt.set_property('activatable', not bool(self._pending))
411
432
crt.connect("toggled", self._toggle_commit, self._files_store)
412
433
if self._pending:
434
name = _i18n('Commit*')
416
commit_col = gtk.TreeViewColumn(name, crt, active=2)
436
name = _i18n('Commit')
437
commit_col = Gtk.TreeViewColumn(name, crt, active=2)
417
438
commit_col.set_visible(False)
418
439
self._treeview_files.append_column(commit_col)
419
self._treeview_files.append_column(gtk.TreeViewColumn(_('Path'),
420
gtk.CellRendererText(), text=3))
421
self._treeview_files.append_column(gtk.TreeViewColumn(_('Type'),
422
gtk.CellRendererText(), text=4))
440
self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Path'),
441
Gtk.CellRendererText(), text=3))
442
self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Type'),
443
Gtk.CellRendererText(), text=4))
423
444
self._treeview_files.connect('cursor-changed',
424
445
self._on_treeview_files_cursor_changed)
426
447
def _toggle_commit(self, cell, path, model):
427
if model[path][0] is None: # No file_id means 'All Files'
448
if model[path][0] == "": # No file_id means 'All Files'
428
449
new_val = not model[path][2]
429
450
for node in model:
430
451
node[2] = new_val
440
461
checked_col.set_visible(False)
442
463
checked_col.set_visible(True)
443
renderer = checked_col.get_cell_renderers()[0]
464
renderer = checked_col.get_cells()[0]
444
465
renderer.set_property('activatable', not all_files)
446
467
def _construct_pending_list(self):
447
468
# Pending information defaults to hidden, we put it all in 1 box, so
448
469
# that we can show/hide all of them at once
449
self._pending_box = gtk.VBox()
470
self._pending_box = Gtk.VBox()
450
471
self._pending_box.hide()
452
pending_message = gtk.Label()
473
pending_message = Gtk.Label()
453
474
pending_message.set_markup(
454
_('<i>* Cannot select specific files when merging</i>'))
455
self._pending_box.pack_start(pending_message, expand=False, padding=5)
475
_i18n('<i>* Cannot select specific files when merging</i>'))
476
self._pending_box.pack_start(pending_message, False, True, 5)
456
477
pending_message.show()
458
pending_label = gtk.Label(_('Pending Revisions'))
459
self._pending_box.pack_start(pending_label, expand=False, padding=0)
479
pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
480
self._pending_box.pack_start(pending_label, False, True, 0)
460
481
pending_label.show()
462
scroller = gtk.ScrolledWindow()
463
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
464
self._treeview_pending = gtk.TreeView()
483
scroller = Gtk.ScrolledWindow()
484
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
485
self._treeview_pending = Gtk.TreeView()
465
486
scroller.add(self._treeview_pending)
466
scroller.set_shadow_type(gtk.SHADOW_IN)
487
scroller.set_shadow_type(Gtk.ShadowType.IN)
468
self._pending_box.pack_start(scroller,
469
expand=True, fill=True, padding=5)
489
self._pending_box.pack_start(scroller, True, True, 5)
470
490
self._treeview_pending.show()
471
self._left_pane_box.pack_start(self._pending_box)
491
self._left_pane_box.pack_start(self._pending_box, True, True, 0)
473
liststore = gtk.ListStore(gobject.TYPE_STRING, # revision_id
474
gobject.TYPE_STRING, # date
475
gobject.TYPE_STRING, # committer
476
gobject.TYPE_STRING, # summary
493
liststore = Gtk.ListStore(GObject.TYPE_STRING, # revision_id
494
GObject.TYPE_STRING, # date
495
GObject.TYPE_STRING, # committer
496
GObject.TYPE_STRING, # summary
478
498
self._pending_store = liststore
479
499
self._treeview_pending.set_model(liststore)
480
self._treeview_pending.append_column(gtk.TreeViewColumn(_('Date'),
481
gtk.CellRendererText(), text=1))
482
self._treeview_pending.append_column(gtk.TreeViewColumn(_('Committer'),
483
gtk.CellRendererText(), text=2))
484
self._treeview_pending.append_column(gtk.TreeViewColumn(_('Summary'),
485
gtk.CellRendererText(), text=3))
500
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Date'),
501
Gtk.CellRendererText(), text=1))
502
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Committer'),
503
Gtk.CellRendererText(), text=2))
504
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Summary'),
505
Gtk.CellRendererText(), text=3))
487
507
def _construct_diff_view(self):
488
from diff import DiffView
508
from bzrlib.plugins.gtk.diff import DiffView
490
510
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
491
511
# decide that we really don't ever want to display it, we should
492
512
# actually remove it, and other references to it, along with the
493
513
# tests that it is set properly.
494
self._diff_label = gtk.Label(_('Diff for whole tree'))
514
self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
495
515
self._diff_label.set_alignment(0, 0)
496
516
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
497
517
self._add_to_right_table(self._diff_label, 1, False)
502
522
self._diff_view.show()
504
524
def _construct_file_message(self):
505
scroller = gtk.ScrolledWindow()
506
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
525
scroller = Gtk.ScrolledWindow()
526
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
508
self._file_message_text_view = gtk.TextView()
528
self._file_message_text_view = Gtk.TextView()
509
529
scroller.add(self._file_message_text_view)
510
scroller.set_shadow_type(gtk.SHADOW_IN)
530
scroller.set_shadow_type(Gtk.ShadowType.IN)
513
self._file_message_text_view.modify_font(pango.FontDescription("Monospace"))
514
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
533
self._file_message_text_view.modify_font(Pango.FontDescription("Monospace"))
534
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
515
535
self._file_message_text_view.set_accepts_tab(False)
516
536
self._file_message_text_view.show()
518
self._file_message_expander = gtk.Expander(_('File commit message'))
538
self._file_message_expander = Gtk.Expander(
539
label=_i18n('File commit message'))
519
540
self._file_message_expander.set_expanded(True)
520
541
self._file_message_expander.add(scroller)
521
542
self._add_to_right_table(self._file_message_expander, 1, False)
522
543
self._file_message_expander.show()
524
545
def _construct_global_message(self):
525
self._global_message_label = gtk.Label(_('Global Commit Message'))
526
self._global_message_label.set_markup(_('<b>Global Commit Message</b>'))
546
self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
547
self._global_message_label.set_markup(
548
_i18n('<b>Global Commit Message</b>'))
527
549
self._global_message_label.set_alignment(0, 0)
528
550
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
529
551
self._add_to_right_table(self._global_message_label, 1, False)
530
552
# Can we remove the spacing between the label and the box?
531
553
self._global_message_label.show()
533
scroller = gtk.ScrolledWindow()
534
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
555
scroller = Gtk.ScrolledWindow()
556
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
536
self._global_message_text_view = gtk.TextView()
537
self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
558
self._global_message_text_view = Gtk.TextView()
559
self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
560
self._global_message_text_view.modify_font(Pango.FontDescription("Monospace"))
538
561
scroller.add(self._global_message_text_view)
539
scroller.set_shadow_type(gtk.SHADOW_IN)
562
scroller.set_shadow_type(Gtk.ShadowType.IN)
541
564
self._add_to_right_table(scroller, 2, True)
542
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
565
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
543
566
self._file_message_text_view.set_accepts_tab(False)
544
567
self._global_message_text_view.show()
546
569
def _on_treeview_files_cursor_changed(self, treeview):
547
570
treeselection = treeview.get_selection()
571
if treeselection is None:
572
# The treeview was probably destroyed as the dialog closes.
548
574
(model, selection) = treeselection.get_selected()
550
576
if selection is not None:
551
577
path, display_path = model.get(selection, 1, 3)
552
self._diff_label.set_text(_('Diff for ') + display_path)
578
self._diff_label.set_text(_i18n('Diff for ') + display_path)
554
580
self._diff_view.show_diff(None)
556
self._diff_view.show_diff([path.decode('UTF-8')])
582
self._diff_view.show_diff([osutils.safe_unicode(path)])
557
583
self._update_per_file_info(selection)
559
585
def _on_accel_next(self, accel_group, window, keyval, modifier):
619
646
records = iter(self._files_store)
620
647
rec = records.next() # Skip the All Files record
621
assert rec[0] is None, "Are we skipping the wrong record?"
648
assert rec[0] == "", "Are we skipping the wrong record?"
624
651
for record in records:
625
652
if self._commit_all_changes or record[2]:# [2] checkbox
626
file_id = record[0] # [0] file_id
627
path = record[1] # [1] real path
628
file_message = record[5] # [5] commit message
653
file_id = osutils.safe_utf8(record[0]) # [0] file_id
654
path = osutils.safe_utf8(record[1]) # [1] real path
656
file_message = _sanitize_and_decode_message(record[5])
629
657
files.append(path.decode('UTF-8'))
630
658
if self._enable_per_file_commits and file_message:
631
659
# All of this needs to be utf-8 information
660
file_message = file_message.encode('UTF-8')
632
661
file_info.append({'path':path, 'file_id':file_id,
633
662
'message':file_message})
634
663
file_info.sort(key=lambda x:(x['path'], x['file_id']))
670
def _on_cancel_clicked(self, button):
671
""" Cancel button clicked handler. """
675
def _on_delete_window(self, source, event):
676
""" Delete window handler. """
679
def _do_cancel(self):
680
"""If requested, saves commit messages when cancelling gcommit; they are re-used by a next gcommit"""
681
mgr = SavedCommitMessagesManager()
682
self._saved_commit_messages_manager = mgr
683
mgr.insert(self._get_global_commit_message(),
684
self._get_specific_files()[1])
685
if mgr.is_not_empty(): # maybe worth saving
686
response = self._question_dialog(
687
_i18n('Commit cancelled'),
688
_i18n('Do you want to save your commit messages ?'),
690
if response == Gtk.ResponseType.NO:
691
# save nothing and destroy old comments if any
692
mgr = SavedCommitMessagesManager()
693
mgr.save(self._wt, self._wt.branch)
694
self.response(Gtk.ResponseType.CANCEL) # close window
641
697
def _on_commit_clicked(self, button):
642
698
""" Commit button clicked handler. """
643
699
self._do_commit()