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
30
from bzrlib import errors, osutils
31
from bzrlib.trace import mutter
32
from bzrlib.util import bencode
19
from gi.repository import Gdk
20
from gi.repository import Gtk
21
from gi.repository import GObject
22
from gi.repository import Pango
34
from bzrlib.plugins.gtk import _i18n
35
from dialog import error_dialog, question_dialog
36
from errors import show_bzr_error
30
from bzrlib.plugins.gtk.dialog import question_dialog
31
from bzrlib.plugins.gtk.errors import show_bzr_error
32
from bzrlib.plugins.gtk.i18n import _i18n
33
from bzrlib.plugins.gtk.commitmsgs import SavedCommitMessagesManager
100
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):
101
100
"""Implementation of Commit."""
103
102
def __init__(self, wt, selected=None, parent=None):
104
gtk.Dialog.__init__(self, title="Commit - Olive",
107
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)
108
106
self._question_dialog = question_dialog
108
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
111
111
# TODO: Do something with this value, it is used by Olive
112
112
# It used to set all changes but this one to False
182
189
all_enabled = (self._selected is None)
183
190
# The first entry is always the 'whole tree'
184
all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
191
all_iter = store.append(["", "", all_enabled, 'All Files', '', ''])
185
192
initial_cursor = store.get_path(all_iter)
186
193
# should we pass specific_files?
187
194
self._wt.lock_read()
188
195
self._basis_tree.lock_read()
190
197
from diff import iter_changes_to_status
198
saved_file_messages = self._saved_commit_messages_manager.get()[1]
191
199
for (file_id, real_path, change_type, display_path
192
200
) in iter_changes_to_status(self._basis_tree, self._wt):
193
201
if self._selected and real_path != self._selected:
206
default_message = saved_file_messages[file_id]
197
209
item_iter = store.append([
199
211
real_path.encode('UTF-8'),
201
213
display_path.encode('UTF-8'),
203
'', # Initial comment
215
default_message, # Initial comment
205
217
if self._selected and enabled:
206
218
initial_cursor = store.get_path(item_iter)
298
310
self._hpane.set_position(300)
300
312
def _construct_accelerators(self):
301
group = gtk.AccelGroup()
302
group.connect_group(gtk.gdk.keyval_from_name('N'),
303
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)
304
316
self.add_accel_group(group)
306
318
# ignore the escape key (avoid closing the window)
307
319
self.connect_object('close', self.emit_stop_by_name, 'close')
309
321
def _construct_left_pane(self):
310
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
322
self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
311
323
self._construct_file_list()
312
324
self._construct_pending_list()
314
self._check_local = gtk.CheckButton(_i18n("_Only commit locally"),
326
self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
315
327
use_underline=True)
316
self._left_pane_box.pack_end(self._check_local, False, False)
328
self._left_pane_box.pack_end(self._check_local, False, False, 0)
317
329
self._check_local.set_active(False)
319
331
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
338
350
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
340
352
def _construct_action_pane(self):
341
self._button_commit = gtk.Button(_i18n("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)
342
359
self._button_commit.connect('clicked', self._on_commit_clicked)
343
self._button_commit.set_flags(gtk.CAN_DEFAULT)
360
self._button_commit.set_can_default(True)
344
361
self._button_commit.show()
345
self.action_area.pack_end(self._button_commit)
362
self.get_action_area().pack_end(
363
self._button_commit, True, True, 0)
346
364
self._button_commit.grab_default()
348
366
def _add_to_right_table(self, widget, weight, expanding=False):
363
381
self._right_pane_table_row = end_row
365
383
def _construct_file_list(self):
366
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
367
file_label = gtk.Label(_i18n('Files'))
384
self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
385
file_label = Gtk.Label(label=_i18n('Files'))
368
386
# file_label.show()
369
self._files_box.pack_start(file_label, expand=False)
387
self._files_box.pack_start(file_label, False, True, 0)
371
self._commit_all_files_radio = gtk.RadioButton(
389
self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
372
390
None, _i18n("Commit all changes"))
373
self._files_box.pack_start(self._commit_all_files_radio, expand=False)
391
self._files_box.pack_start(self._commit_all_files_radio, False, True, 0)
374
392
self._commit_all_files_radio.show()
375
393
self._commit_all_files_radio.connect('toggled',
376
394
self._toggle_commit_selection)
377
self._commit_selected_radio = gtk.RadioButton(
395
self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
378
396
self._commit_all_files_radio, _i18n("Only commit selected changes"))
379
self._files_box.pack_start(self._commit_selected_radio, expand=False)
397
self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
380
398
self._commit_selected_radio.show()
381
399
self._commit_selected_radio.connect('toggled',
382
400
self._toggle_commit_selection)
385
403
self._commit_all_files_radio.set_sensitive(False)
386
404
self._commit_selected_radio.set_sensitive(False)
388
scroller = gtk.ScrolledWindow()
389
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
390
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()
391
409
self._treeview_files.show()
392
410
scroller.add(self._treeview_files)
393
scroller.set_shadow_type(gtk.SHADOW_IN)
411
scroller.set_shadow_type(Gtk.ShadowType.IN)
395
self._files_box.pack_start(scroller,
396
expand=True, fill=True)
413
self._files_box.pack_start(scroller, True, True, 0)
397
414
self._files_box.show()
398
self._left_pane_box.pack_start(self._files_box)
415
self._left_pane_box.pack_start(self._files_box, True, True, 0)
400
417
# Keep note that all strings stored in a ListStore must be UTF-8
401
418
# strings. GTK does not support directly setting and restoring Unicode
403
liststore = gtk.ListStore(
404
gobject.TYPE_STRING, # [0] file_id
405
gobject.TYPE_STRING, # [1] real path
406
gobject.TYPE_BOOLEAN, # [2] checkbox
407
gobject.TYPE_STRING, # [3] display path
408
gobject.TYPE_STRING, # [4] changes type
409
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
411
428
self._files_store = liststore
412
429
self._treeview_files.set_model(liststore)
413
crt = gtk.CellRendererToggle()
430
crt = Gtk.CellRendererToggle()
414
431
crt.set_property('activatable', not bool(self._pending))
415
432
crt.connect("toggled", self._toggle_commit, self._files_store)
416
433
if self._pending:
417
434
name = _i18n('Commit*')
419
436
name = _i18n('Commit')
420
commit_col = gtk.TreeViewColumn(name, crt, active=2)
437
commit_col = Gtk.TreeViewColumn(name, crt, active=2)
421
438
commit_col.set_visible(False)
422
439
self._treeview_files.append_column(commit_col)
423
self._treeview_files.append_column(gtk.TreeViewColumn(_i18n('Path'),
424
gtk.CellRendererText(), text=3))
425
self._treeview_files.append_column(gtk.TreeViewColumn(_i18n('Type'),
426
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))
427
444
self._treeview_files.connect('cursor-changed',
428
445
self._on_treeview_files_cursor_changed)
430
447
def _toggle_commit(self, cell, path, model):
431
if model[path][0] is None: # No file_id means 'All Files'
448
if model[path][0] == "": # No file_id means 'All Files'
432
449
new_val = not model[path][2]
433
450
for node in model:
434
451
node[2] = new_val
444
461
checked_col.set_visible(False)
446
463
checked_col.set_visible(True)
447
renderer = checked_col.get_cell_renderers()[0]
464
renderer = checked_col.get_cells()[0]
448
465
renderer.set_property('activatable', not all_files)
450
467
def _construct_pending_list(self):
451
468
# Pending information defaults to hidden, we put it all in 1 box, so
452
469
# that we can show/hide all of them at once
453
self._pending_box = gtk.VBox()
470
self._pending_box = Gtk.VBox()
454
471
self._pending_box.hide()
456
pending_message = gtk.Label()
473
pending_message = Gtk.Label()
457
474
pending_message.set_markup(
458
475
_i18n('<i>* Cannot select specific files when merging</i>'))
459
self._pending_box.pack_start(pending_message, expand=False, padding=5)
476
self._pending_box.pack_start(pending_message, False, True, 5)
460
477
pending_message.show()
462
pending_label = gtk.Label(_i18n('Pending Revisions'))
463
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)
464
481
pending_label.show()
466
scroller = gtk.ScrolledWindow()
467
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
468
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()
469
486
scroller.add(self._treeview_pending)
470
scroller.set_shadow_type(gtk.SHADOW_IN)
487
scroller.set_shadow_type(Gtk.ShadowType.IN)
472
self._pending_box.pack_start(scroller,
473
expand=True, fill=True, padding=5)
489
self._pending_box.pack_start(scroller, True, True, 5)
474
490
self._treeview_pending.show()
475
self._left_pane_box.pack_start(self._pending_box)
491
self._left_pane_box.pack_start(self._pending_box, True, True, 0)
477
liststore = gtk.ListStore(gobject.TYPE_STRING, # revision_id
478
gobject.TYPE_STRING, # date
479
gobject.TYPE_STRING, # committer
480
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
482
498
self._pending_store = liststore
483
499
self._treeview_pending.set_model(liststore)
484
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Date'),
485
gtk.CellRendererText(), text=1))
486
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Committer'),
487
gtk.CellRendererText(), text=2))
488
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Summary'),
489
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))
491
507
def _construct_diff_view(self):
492
from diff import DiffView
508
from bzrlib.plugins.gtk.diff import DiffView
494
510
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
495
511
# decide that we really don't ever want to display it, we should
496
512
# actually remove it, and other references to it, along with the
497
513
# tests that it is set properly.
498
self._diff_label = gtk.Label(_i18n('Diff for whole tree'))
514
self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
499
515
self._diff_label.set_alignment(0, 0)
500
516
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
501
517
self._add_to_right_table(self._diff_label, 1, False)
506
522
self._diff_view.show()
508
524
def _construct_file_message(self):
509
scroller = gtk.ScrolledWindow()
510
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
525
scroller = Gtk.ScrolledWindow()
526
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
512
self._file_message_text_view = gtk.TextView()
528
self._file_message_text_view = Gtk.TextView()
513
529
scroller.add(self._file_message_text_view)
514
scroller.set_shadow_type(gtk.SHADOW_IN)
530
scroller.set_shadow_type(Gtk.ShadowType.IN)
517
self._file_message_text_view.modify_font(pango.FontDescription("Monospace"))
518
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)
519
535
self._file_message_text_view.set_accepts_tab(False)
520
536
self._file_message_text_view.show()
522
self._file_message_expander = gtk.Expander(_i18n('File commit message'))
538
self._file_message_expander = Gtk.Expander(
539
label=_i18n('File commit message'))
523
540
self._file_message_expander.set_expanded(True)
524
541
self._file_message_expander.add(scroller)
525
542
self._add_to_right_table(self._file_message_expander, 1, False)
526
543
self._file_message_expander.show()
528
545
def _construct_global_message(self):
529
self._global_message_label = gtk.Label(_i18n('Global Commit Message'))
546
self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
530
547
self._global_message_label.set_markup(
531
548
_i18n('<b>Global Commit Message</b>'))
532
549
self._global_message_label.set_alignment(0, 0)
535
552
# Can we remove the spacing between the label and the box?
536
553
self._global_message_label.show()
538
scroller = gtk.ScrolledWindow()
539
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
555
scroller = Gtk.ScrolledWindow()
556
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
541
self._global_message_text_view = gtk.TextView()
542
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"))
543
561
scroller.add(self._global_message_text_view)
544
scroller.set_shadow_type(gtk.SHADOW_IN)
562
scroller.set_shadow_type(Gtk.ShadowType.IN)
546
564
self._add_to_right_table(scroller, 2, True)
547
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
565
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
548
566
self._file_message_text_view.set_accepts_tab(False)
549
567
self._global_message_text_view.show()
551
569
def _on_treeview_files_cursor_changed(self, treeview):
552
570
treeselection = treeview.get_selection()
571
if treeselection is None:
572
# The treeview was probably destroyed as the dialog closes.
553
574
(model, selection) = treeselection.get_selected()
555
576
if selection is not None:
556
577
path, display_path = model.get(selection, 1, 3)
557
578
self._diff_label.set_text(_i18n('Diff for ') + display_path)
559
580
self._diff_view.show_diff(None)
561
self._diff_view.show_diff([path.decode('UTF-8')])
582
self._diff_view.show_diff([osutils.safe_unicode(path)])
562
583
self._update_per_file_info(selection)
564
585
def _on_accel_next(self, accel_group, window, keyval, modifier):
575
596
# We have either made it to the end of the list, or nothing was
576
597
# selected. Either way, select All Files, and jump to the global
577
598
# commit message.
578
self._treeview_files.set_cursor((0,))
599
self._treeview_files.set_cursor(
600
Gtk.TreePath(path=0), "", False)
579
601
self._global_message_text_view.grab_focus()
581
603
# Set the cursor to this entry, and jump to the per-file commit
583
self._treeview_files.set_cursor(model.get_path(next))
605
self._treeview_files.set_cursor(model.get_path(next), None, False)
584
606
self._file_message_text_view.grab_focus()
586
608
def _save_current_file_message(self):
624
646
records = iter(self._files_store)
625
647
rec = records.next() # Skip the All Files record
626
assert rec[0] is None, "Are we skipping the wrong record?"
648
assert rec[0] == "", "Are we skipping the wrong record?"
629
651
for record in records:
630
652
if self._commit_all_changes or record[2]:# [2] checkbox
631
file_id = record[0] # [0] file_id
632
path = record[1] # [1] real path
633
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])
634
657
files.append(path.decode('UTF-8'))
635
658
if self._enable_per_file_commits and file_message:
636
659
# All of this needs to be utf-8 information
660
file_message = file_message.encode('UTF-8')
637
661
file_info.append({'path':path, 'file_id':file_id,
638
662
'message':file_message})
639
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
646
697
def _on_commit_clicked(self, button):
647
698
""" Commit button clicked handler. """
648
699
self._do_commit()
702
757
specific_files=specific_files,
703
758
revprops=revprops)
704
759
self.committed_revision_id = rev_id
705
self.response(gtk.RESPONSE_OK)
760
# destroy old comments if any
761
SavedCommitMessagesManager().save(self._wt, self._wt.branch)
762
self.response(Gtk.ResponseType.OK)
707
764
def _get_global_commit_message(self):
708
765
buf = self._global_message_text_view.get_buffer()
709
766
start, end = buf.get_bounds()
710
return buf.get_text(start, end).decode('utf-8')
767
text = buf.get_text(start, end, True)
768
return _sanitize_and_decode_message(text)
712
770
def _set_global_commit_message(self, message):
713
771
"""Just a helper for the test suite."""