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