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
188
all_enabled = (self._selected is None)
183
189
# The first entry is always the 'whole tree'
184
all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
190
all_iter = store.append(["", "", all_enabled, 'All Files', '', ''])
185
191
initial_cursor = store.get_path(all_iter)
186
192
# should we pass specific_files?
187
193
self._wt.lock_read()
188
194
self._basis_tree.lock_read()
190
196
from diff import iter_changes_to_status
197
saved_file_messages = self._saved_commit_messages_manager.get()[1]
191
198
for (file_id, real_path, change_type, display_path
192
199
) in iter_changes_to_status(self._basis_tree, self._wt):
193
200
if self._selected and real_path != self._selected:
205
default_message = saved_file_messages[file_id]
197
208
item_iter = store.append([
199
210
real_path.encode('UTF-8'),
201
212
display_path.encode('UTF-8'),
203
'', # Initial comment
214
default_message, # Initial comment
205
216
if self._selected and enabled:
206
217
initial_cursor = store.get_path(item_iter)
298
309
self._hpane.set_position(300)
300
311
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)
312
group = Gtk.AccelGroup()
313
group.connect(Gdk.keyval_from_name('N'),
314
Gdk.ModifierType.CONTROL_MASK, 0, self._on_accel_next)
304
315
self.add_accel_group(group)
306
317
# ignore the escape key (avoid closing the window)
307
318
self.connect_object('close', self.emit_stop_by_name, 'close')
309
320
def _construct_left_pane(self):
310
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
321
self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
311
322
self._construct_file_list()
312
323
self._construct_pending_list()
314
self._check_local = gtk.CheckButton(_i18n("_Only commit locally"),
325
self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
315
326
use_underline=True)
316
self._left_pane_box.pack_end(self._check_local, False, False)
327
self._left_pane_box.pack_end(self._check_local, False, False, 0)
317
328
self._check_local.set_active(False)
319
330
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
338
349
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
340
351
def _construct_action_pane(self):
341
self._button_commit = gtk.Button(_i18n("Comm_it"), use_underline=True)
352
self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
353
self._button_cancel.connect('clicked', self._on_cancel_clicked)
354
self._button_cancel.show()
355
self.get_action_area().pack_end(
356
self._button_cancel, True, True, 0)
357
self._button_commit = Gtk.Button(_i18n("Comm_it"), use_underline=True)
342
358
self._button_commit.connect('clicked', self._on_commit_clicked)
343
self._button_commit.set_flags(gtk.CAN_DEFAULT)
359
self._button_commit.set_can_default(True)
344
360
self._button_commit.show()
345
self.action_area.pack_end(self._button_commit)
361
self.get_action_area().pack_end(
362
self._button_commit, True, True, 0)
346
363
self._button_commit.grab_default()
348
365
def _add_to_right_table(self, widget, weight, expanding=False):
363
380
self._right_pane_table_row = end_row
365
382
def _construct_file_list(self):
366
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
367
file_label = gtk.Label(_i18n('Files'))
383
self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
384
file_label = Gtk.Label(label=_i18n('Files'))
368
385
# file_label.show()
369
self._files_box.pack_start(file_label, expand=False)
386
self._files_box.pack_start(file_label, False, True, 0)
371
self._commit_all_files_radio = gtk.RadioButton(
388
self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
372
389
None, _i18n("Commit all changes"))
373
self._files_box.pack_start(self._commit_all_files_radio, expand=False)
390
self._files_box.pack_start(self._commit_all_files_radio, False, True, 0)
374
391
self._commit_all_files_radio.show()
375
392
self._commit_all_files_radio.connect('toggled',
376
393
self._toggle_commit_selection)
377
self._commit_selected_radio = gtk.RadioButton(
394
self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
378
395
self._commit_all_files_radio, _i18n("Only commit selected changes"))
379
self._files_box.pack_start(self._commit_selected_radio, expand=False)
396
self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
380
397
self._commit_selected_radio.show()
381
398
self._commit_selected_radio.connect('toggled',
382
399
self._toggle_commit_selection)
385
402
self._commit_all_files_radio.set_sensitive(False)
386
403
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()
405
scroller = Gtk.ScrolledWindow()
406
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
407
self._treeview_files = Gtk.TreeView()
391
408
self._treeview_files.show()
392
409
scroller.add(self._treeview_files)
393
scroller.set_shadow_type(gtk.SHADOW_IN)
410
scroller.set_shadow_type(Gtk.ShadowType.IN)
395
self._files_box.pack_start(scroller,
396
expand=True, fill=True)
412
self._files_box.pack_start(scroller, True, True, 0)
397
413
self._files_box.show()
398
self._left_pane_box.pack_start(self._files_box)
414
self._left_pane_box.pack_start(self._files_box, True, True, 0)
400
416
# Keep note that all strings stored in a ListStore must be UTF-8
401
417
# 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
419
liststore = Gtk.ListStore(
420
GObject.TYPE_STRING, # [0] file_id
421
GObject.TYPE_STRING, # [1] real path
422
GObject.TYPE_BOOLEAN, # [2] checkbox
423
GObject.TYPE_STRING, # [3] display path
424
GObject.TYPE_STRING, # [4] changes type
425
GObject.TYPE_STRING, # [5] commit message
411
427
self._files_store = liststore
412
428
self._treeview_files.set_model(liststore)
413
crt = gtk.CellRendererToggle()
429
crt = Gtk.CellRendererToggle()
414
430
crt.set_property('activatable', not bool(self._pending))
415
431
crt.connect("toggled", self._toggle_commit, self._files_store)
416
432
if self._pending:
417
433
name = _i18n('Commit*')
419
435
name = _i18n('Commit')
420
commit_col = gtk.TreeViewColumn(name, crt, active=2)
436
commit_col = Gtk.TreeViewColumn(name, crt, active=2)
421
437
commit_col.set_visible(False)
422
438
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))
439
self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Path'),
440
Gtk.CellRendererText(), text=3))
441
self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Type'),
442
Gtk.CellRendererText(), text=4))
427
443
self._treeview_files.connect('cursor-changed',
428
444
self._on_treeview_files_cursor_changed)
430
446
def _toggle_commit(self, cell, path, model):
431
if model[path][0] is None: # No file_id means 'All Files'
447
if model[path][0] == "": # No file_id means 'All Files'
432
448
new_val = not model[path][2]
433
449
for node in model:
434
450
node[2] = new_val
444
460
checked_col.set_visible(False)
446
462
checked_col.set_visible(True)
447
renderer = checked_col.get_cell_renderers()[0]
463
renderer = checked_col.get_cells()[0]
448
464
renderer.set_property('activatable', not all_files)
450
466
def _construct_pending_list(self):
451
467
# Pending information defaults to hidden, we put it all in 1 box, so
452
468
# that we can show/hide all of them at once
453
self._pending_box = gtk.VBox()
469
self._pending_box = Gtk.VBox()
454
470
self._pending_box.hide()
456
pending_message = gtk.Label()
472
pending_message = Gtk.Label()
457
473
pending_message.set_markup(
458
474
_i18n('<i>* Cannot select specific files when merging</i>'))
459
self._pending_box.pack_start(pending_message, expand=False, padding=5)
475
self._pending_box.pack_start(pending_message, False, True, 5)
460
476
pending_message.show()
462
pending_label = gtk.Label(_i18n('Pending Revisions'))
463
self._pending_box.pack_start(pending_label, expand=False, padding=0)
478
pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
479
self._pending_box.pack_start(pending_label, False, True, 0)
464
480
pending_label.show()
466
scroller = gtk.ScrolledWindow()
467
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
468
self._treeview_pending = gtk.TreeView()
482
scroller = Gtk.ScrolledWindow()
483
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
484
self._treeview_pending = Gtk.TreeView()
469
485
scroller.add(self._treeview_pending)
470
scroller.set_shadow_type(gtk.SHADOW_IN)
486
scroller.set_shadow_type(Gtk.ShadowType.IN)
472
self._pending_box.pack_start(scroller,
473
expand=True, fill=True, padding=5)
488
self._pending_box.pack_start(scroller, True, True, 5)
474
489
self._treeview_pending.show()
475
self._left_pane_box.pack_start(self._pending_box)
490
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
492
liststore = Gtk.ListStore(GObject.TYPE_STRING, # revision_id
493
GObject.TYPE_STRING, # date
494
GObject.TYPE_STRING, # committer
495
GObject.TYPE_STRING, # summary
482
497
self._pending_store = liststore
483
498
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))
499
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Date'),
500
Gtk.CellRendererText(), text=1))
501
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Committer'),
502
Gtk.CellRendererText(), text=2))
503
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Summary'),
504
Gtk.CellRendererText(), text=3))
491
506
def _construct_diff_view(self):
492
from diff import DiffView
507
from bzrlib.plugins.gtk.diff import DiffView
494
509
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
495
510
# decide that we really don't ever want to display it, we should
496
511
# actually remove it, and other references to it, along with the
497
512
# tests that it is set properly.
498
self._diff_label = gtk.Label(_i18n('Diff for whole tree'))
513
self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
499
514
self._diff_label.set_alignment(0, 0)
500
515
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
501
516
self._add_to_right_table(self._diff_label, 1, False)
506
521
self._diff_view.show()
508
523
def _construct_file_message(self):
509
scroller = gtk.ScrolledWindow()
510
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
524
scroller = Gtk.ScrolledWindow()
525
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
512
self._file_message_text_view = gtk.TextView()
527
self._file_message_text_view = Gtk.TextView()
513
528
scroller.add(self._file_message_text_view)
514
scroller.set_shadow_type(gtk.SHADOW_IN)
529
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)
532
self._file_message_text_view.modify_font(Pango.FontDescription("Monospace"))
533
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
519
534
self._file_message_text_view.set_accepts_tab(False)
520
535
self._file_message_text_view.show()
522
self._file_message_expander = gtk.Expander(_i18n('File commit message'))
537
self._file_message_expander = Gtk.Expander(
538
label=_i18n('File commit message'))
523
539
self._file_message_expander.set_expanded(True)
524
540
self._file_message_expander.add(scroller)
525
541
self._add_to_right_table(self._file_message_expander, 1, False)
526
542
self._file_message_expander.show()
528
544
def _construct_global_message(self):
529
self._global_message_label = gtk.Label(_i18n('Global Commit Message'))
545
self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
530
546
self._global_message_label.set_markup(
531
547
_i18n('<b>Global Commit Message</b>'))
532
548
self._global_message_label.set_alignment(0, 0)
535
551
# Can we remove the spacing between the label and the box?
536
552
self._global_message_label.show()
538
scroller = gtk.ScrolledWindow()
539
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
554
scroller = Gtk.ScrolledWindow()
555
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"))
557
self._global_message_text_view = Gtk.TextView()
558
self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
559
self._global_message_text_view.modify_font(Pango.FontDescription("Monospace"))
543
560
scroller.add(self._global_message_text_view)
544
scroller.set_shadow_type(gtk.SHADOW_IN)
561
scroller.set_shadow_type(Gtk.ShadowType.IN)
546
563
self._add_to_right_table(scroller, 2, True)
547
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
564
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
548
565
self._file_message_text_view.set_accepts_tab(False)
549
566
self._global_message_text_view.show()
575
592
# We have either made it to the end of the list, or nothing was
576
593
# selected. Either way, select All Files, and jump to the global
577
594
# commit message.
578
self._treeview_files.set_cursor((0,))
595
self._treeview_files.set_cursor(
596
Gtk.TreePath(path=0), "", False)
579
597
self._global_message_text_view.grab_focus()
581
599
# Set the cursor to this entry, and jump to the per-file commit
583
self._treeview_files.set_cursor(model.get_path(next))
601
self._treeview_files.set_cursor(model.get_path(next), None, False)
584
602
self._file_message_text_view.grab_focus()
586
604
def _save_current_file_message(self):
624
642
records = iter(self._files_store)
625
643
rec = records.next() # Skip the All Files record
626
assert rec[0] is None, "Are we skipping the wrong record?"
644
assert rec[0] == "", "Are we skipping the wrong record?"
629
647
for record in records:
630
648
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
649
file_id = osutils.safe_utf8(record[0]) # [0] file_id
650
path = osutils.safe_utf8(record[1]) # [1] real path
652
file_message = _sanitize_and_decode_message(record[5])
634
653
files.append(path.decode('UTF-8'))
635
654
if self._enable_per_file_commits and file_message:
636
655
# All of this needs to be utf-8 information
656
file_message = file_message.encode('UTF-8')
637
657
file_info.append({'path':path, 'file_id':file_id,
638
658
'message':file_message})
639
659
file_info.sort(key=lambda x:(x['path'], x['file_id']))
666
def _on_cancel_clicked(self, button):
667
""" Cancel button clicked handler. """
671
def _on_delete_window(self, source, event):
672
""" Delete window handler. """
675
def _do_cancel(self):
676
"""If requested, saves commit messages when cancelling gcommit; they are re-used by a next gcommit"""
677
mgr = SavedCommitMessagesManager()
678
self._saved_commit_messages_manager = mgr
679
mgr.insert(self._get_global_commit_message(),
680
self._get_specific_files()[1])
681
if mgr.is_not_empty(): # maybe worth saving
682
response = self._question_dialog(
683
_i18n('Commit cancelled'),
684
_i18n('Do you want to save your commit messages ?'),
686
if response == Gtk.ResponseType.NO:
687
# save nothing and destroy old comments if any
688
mgr = SavedCommitMessagesManager()
689
mgr.save(self._wt, self._wt.branch)
690
self.response(Gtk.ResponseType.CANCEL) # close window
646
693
def _on_commit_clicked(self, button):
647
694
""" Commit button clicked handler. """
648
695
self._do_commit()
702
753
specific_files=specific_files,
703
754
revprops=revprops)
704
755
self.committed_revision_id = rev_id
705
self.response(gtk.RESPONSE_OK)
756
# destroy old comments if any
757
SavedCommitMessagesManager().save(self._wt, self._wt.branch)
758
self.response(Gtk.ResponseType.OK)
707
760
def _get_global_commit_message(self):
708
761
buf = self._global_message_text_view.get_buffer()
709
762
start, end = buf.get_bounds()
710
return buf.get_text(start, end).decode('utf-8')
763
text = buf.get_text(start, end, True)
764
return _sanitize_and_decode_message(text)
712
766
def _set_global_commit_message(self, message):
713
767
"""Just a helper for the test suite."""