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