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
306
# ignore the escape key (avoid closing the window)
319
307
self.connect_object('close', self.emit_stop_by_name, 'close')
321
309
def _construct_left_pane(self):
322
self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
310
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
323
311
self._construct_file_list()
324
312
self._construct_pending_list()
326
self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
314
self._check_local = gtk.CheckButton(_i18n("_Only commit locally"),
327
315
use_underline=True)
328
self._left_pane_box.pack_end(self._check_local, False, False, 0)
316
self._left_pane_box.pack_end(self._check_local, False, False)
329
317
self._check_local.set_active(False)
331
319
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
350
338
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
352
340
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)
341
self._button_commit = gtk.Button(_i18n("Comm_it"), use_underline=True)
359
342
self._button_commit.connect('clicked', self._on_commit_clicked)
360
self._button_commit.set_can_default(True)
343
self._button_commit.set_flags(gtk.CAN_DEFAULT)
361
344
self._button_commit.show()
362
self.get_action_area().pack_end(
363
self._button_commit, True, True, 0)
345
self.action_area.pack_end(self._button_commit)
364
346
self._button_commit.grab_default()
366
348
def _add_to_right_table(self, widget, weight, expanding=False):
381
363
self._right_pane_table_row = end_row
383
365
def _construct_file_list(self):
384
self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
385
file_label = Gtk.Label(label=_i18n('Files'))
366
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
367
file_label = gtk.Label(_i18n('Files'))
386
368
# file_label.show()
387
self._files_box.pack_start(file_label, False, True, 0)
369
self._files_box.pack_start(file_label, expand=False)
389
self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
371
self._commit_all_files_radio = gtk.RadioButton(
390
372
None, _i18n("Commit all changes"))
391
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)
392
374
self._commit_all_files_radio.show()
393
375
self._commit_all_files_radio.connect('toggled',
394
376
self._toggle_commit_selection)
395
self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
377
self._commit_selected_radio = gtk.RadioButton(
396
378
self._commit_all_files_radio, _i18n("Only commit selected changes"))
397
self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
379
self._files_box.pack_start(self._commit_selected_radio, expand=False)
398
380
self._commit_selected_radio.show()
399
381
self._commit_selected_radio.connect('toggled',
400
382
self._toggle_commit_selection)
403
385
self._commit_all_files_radio.set_sensitive(False)
404
386
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()
388
scroller = gtk.ScrolledWindow()
389
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
390
self._treeview_files = gtk.TreeView()
409
391
self._treeview_files.show()
410
392
scroller.add(self._treeview_files)
411
scroller.set_shadow_type(Gtk.ShadowType.IN)
393
scroller.set_shadow_type(gtk.SHADOW_IN)
413
self._files_box.pack_start(scroller, True, True, 0)
395
self._files_box.pack_start(scroller,
396
expand=True, fill=True)
414
397
self._files_box.show()
415
self._left_pane_box.pack_start(self._files_box, True, True, 0)
398
self._left_pane_box.pack_start(self._files_box)
417
400
# Keep note that all strings stored in a ListStore must be UTF-8
418
401
# 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
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
428
411
self._files_store = liststore
429
412
self._treeview_files.set_model(liststore)
430
crt = Gtk.CellRendererToggle()
413
crt = gtk.CellRendererToggle()
431
414
crt.set_property('activatable', not bool(self._pending))
432
415
crt.connect("toggled", self._toggle_commit, self._files_store)
433
416
if self._pending:
434
417
name = _i18n('Commit*')
436
419
name = _i18n('Commit')
437
commit_col = Gtk.TreeViewColumn(name, crt, active=2)
420
commit_col = gtk.TreeViewColumn(name, crt, active=2)
438
421
commit_col.set_visible(False)
439
422
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))
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))
444
427
self._treeview_files.connect('cursor-changed',
445
428
self._on_treeview_files_cursor_changed)
447
430
def _toggle_commit(self, cell, path, model):
448
if model[path][0] == "": # No file_id means 'All Files'
431
if model[path][0] is None: # No file_id means 'All Files'
449
432
new_val = not model[path][2]
450
433
for node in model:
451
434
node[2] = new_val
461
444
checked_col.set_visible(False)
463
446
checked_col.set_visible(True)
464
renderer = checked_col.get_cells()[0]
447
renderer = checked_col.get_cell_renderers()[0]
465
448
renderer.set_property('activatable', not all_files)
467
450
def _construct_pending_list(self):
468
451
# Pending information defaults to hidden, we put it all in 1 box, so
469
452
# that we can show/hide all of them at once
470
self._pending_box = Gtk.VBox()
453
self._pending_box = gtk.VBox()
471
454
self._pending_box.hide()
473
pending_message = Gtk.Label()
456
pending_message = gtk.Label()
474
457
pending_message.set_markup(
475
458
_i18n('<i>* Cannot select specific files when merging</i>'))
476
self._pending_box.pack_start(pending_message, False, True, 5)
459
self._pending_box.pack_start(pending_message, expand=False, padding=5)
477
460
pending_message.show()
479
pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
480
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)
481
464
pending_label.show()
483
scroller = Gtk.ScrolledWindow()
484
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
485
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()
486
469
scroller.add(self._treeview_pending)
487
scroller.set_shadow_type(Gtk.ShadowType.IN)
470
scroller.set_shadow_type(gtk.SHADOW_IN)
489
self._pending_box.pack_start(scroller, True, True, 5)
472
self._pending_box.pack_start(scroller,
473
expand=True, fill=True, padding=5)
490
474
self._treeview_pending.show()
491
self._left_pane_box.pack_start(self._pending_box, True, True, 0)
475
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
477
liststore = gtk.ListStore(gobject.TYPE_STRING, # revision_id
478
gobject.TYPE_STRING, # date
479
gobject.TYPE_STRING, # committer
480
gobject.TYPE_STRING, # summary
498
482
self._pending_store = liststore
499
483
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))
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))
507
491
def _construct_diff_view(self):
508
from bzrlib.plugins.gtk.diff import DiffView
492
from diff import DiffView
510
494
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
511
495
# decide that we really don't ever want to display it, we should
512
496
# actually remove it, and other references to it, along with the
513
497
# tests that it is set properly.
514
self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
498
self._diff_label = gtk.Label(_i18n('Diff for whole tree'))
515
499
self._diff_label.set_alignment(0, 0)
516
500
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
517
501
self._add_to_right_table(self._diff_label, 1, False)
522
506
self._diff_view.show()
524
508
def _construct_file_message(self):
525
scroller = Gtk.ScrolledWindow()
526
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
509
scroller = gtk.ScrolledWindow()
510
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
528
self._file_message_text_view = Gtk.TextView()
512
self._file_message_text_view = gtk.TextView()
529
513
scroller.add(self._file_message_text_view)
530
scroller.set_shadow_type(Gtk.ShadowType.IN)
514
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)
517
self._file_message_text_view.modify_font(pango.FontDescription("Monospace"))
518
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
535
519
self._file_message_text_view.set_accepts_tab(False)
536
520
self._file_message_text_view.show()
538
self._file_message_expander = Gtk.Expander(
539
label=_i18n('File commit message'))
522
self._file_message_expander = gtk.Expander(_i18n('File commit message'))
540
523
self._file_message_expander.set_expanded(True)
541
524
self._file_message_expander.add(scroller)
542
525
self._add_to_right_table(self._file_message_expander, 1, False)
543
526
self._file_message_expander.show()
545
528
def _construct_global_message(self):
546
self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
529
self._global_message_label = gtk.Label(_i18n('Global Commit Message'))
547
530
self._global_message_label.set_markup(
548
531
_i18n('<b>Global Commit Message</b>'))
549
532
self._global_message_label.set_alignment(0, 0)
552
535
# Can we remove the spacing between the label and the box?
553
536
self._global_message_label.show()
555
scroller = Gtk.ScrolledWindow()
556
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
538
scroller = gtk.ScrolledWindow()
539
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"))
541
self._global_message_text_view = gtk.TextView()
542
self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
561
543
scroller.add(self._global_message_text_view)
562
scroller.set_shadow_type(Gtk.ShadowType.IN)
544
scroller.set_shadow_type(gtk.SHADOW_IN)
564
546
self._add_to_right_table(scroller, 2, True)
565
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
547
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
566
548
self._file_message_text_view.set_accepts_tab(False)
567
549
self._global_message_text_view.show()
569
551
def _on_treeview_files_cursor_changed(self, treeview):
570
552
treeselection = treeview.get_selection()
571
if treeselection is None:
572
# The treeview was probably destroyed as the dialog closes.
574
553
(model, selection) = treeselection.get_selected()
576
555
if selection is not None:
577
556
path, display_path = model.get(selection, 1, 3)
578
557
self._diff_label.set_text(_i18n('Diff for ') + display_path)
580
559
self._diff_view.show_diff(None)
582
self._diff_view.show_diff([osutils.safe_unicode(path)])
561
self._diff_view.show_diff([path.decode('UTF-8')])
583
562
self._update_per_file_info(selection)
585
564
def _on_accel_next(self, accel_group, window, keyval, modifier):
596
575
# We have either made it to the end of the list, or nothing was
597
576
# selected. Either way, select All Files, and jump to the global
598
577
# commit message.
599
self._treeview_files.set_cursor(
600
Gtk.TreePath(path=0), "", False)
578
self._treeview_files.set_cursor((0,))
601
579
self._global_message_text_view.grab_focus()
603
581
# 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)
583
self._treeview_files.set_cursor(model.get_path(next))
606
584
self._file_message_text_view.grab_focus()
608
586
def _save_current_file_message(self):
646
624
records = iter(self._files_store)
647
625
rec = records.next() # Skip the All Files record
648
assert rec[0] == "", "Are we skipping the wrong record?"
626
assert rec[0] is None, "Are we skipping the wrong record?"
651
629
for record in records:
652
630
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])
631
file_id = record[0] # [0] file_id
632
path = record[1] # [1] real path
633
file_message = record[5] # [5] commit message
657
634
files.append(path.decode('UTF-8'))
658
635
if self._enable_per_file_commits and file_message:
659
636
# All of this needs to be utf-8 information
660
file_message = file_message.encode('UTF-8')
661
637
file_info.append({'path':path, 'file_id':file_id,
662
638
'message':file_message})
663
639
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
646
def _on_commit_clicked(self, button):
698
647
""" Commit button clicked handler. """
699
648
self._do_commit()
757
702
specific_files=specific_files,
758
703
revprops=revprops)
759
704
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)
705
self.response(gtk.RESPONSE_OK)
764
707
def _get_global_commit_message(self):
765
708
buf = self._global_message_text_view.get_buffer()
766
709
start, end = buf.get_bounds()
767
text = buf.get_text(start, end, True)
768
return _sanitize_and_decode_message(text)
710
return buf.get_text(start, end).decode('utf-8')
770
712
def _set_global_commit_message(self, message):
771
713
"""Just a helper for the test suite."""