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 bencode
32
from bzrlib.util import bencode
34
from bzrlib.plugins.gtk.dialog import question_dialog
35
from bzrlib.plugins.gtk.errors import show_bzr_error
36
from bzrlib.plugins.gtk.i18n import _i18n
30
from bzrlib import errors, osutils
31
from bzrlib.trace import mutter
32
from bzrlib.util import bencode
34
from bzrlib.plugins.gtk import _i18n
35
from dialog import error_dialog, question_dialog
36
from errors import show_bzr_error
100
_newline_variants_re = re.compile(r'\r\n?')
101
def _sanitize_and_decode_message(utf8_message):
102
"""Turn a utf-8 message into a sanitized Unicode message."""
103
fixed_newline = _newline_variants_re.sub('\n', utf8_message)
104
return fixed_newline.decode('utf-8')
107
class CommitDialog(Gtk.Dialog):
100
class CommitDialog(gtk.Dialog):
108
101
"""Implementation of Commit."""
110
103
def __init__(self, wt, selected=None, parent=None):
111
Gtk.Dialog.__init__(self, title="Commit to %s" % wt.basedir,
112
parent=parent, flags=0,)
113
self.connect('delete-event', self._on_delete_window)
104
gtk.Dialog.__init__(self, title="Commit - Olive",
107
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
114
108
self._question_dialog = question_dialog
116
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
119
111
# TODO: Do something with this value, it is used by Olive
120
112
# It used to set all changes but this one to False
198
188
self._basis_tree.lock_read()
200
190
from diff import iter_changes_to_status
201
saved_file_messages = self._saved_commit_messages_manager.get()[1]
202
191
for (file_id, real_path, change_type, display_path
203
192
) in iter_changes_to_status(self._basis_tree, self._wt):
204
193
if self._selected and real_path != self._selected:
209
default_message = saved_file_messages[file_id]
212
197
item_iter = store.append([
214
199
real_path.encode('UTF-8'),
216
201
display_path.encode('UTF-8'),
218
default_message, # Initial comment
203
'', # Initial comment
220
205
if self._selected and enabled:
221
206
initial_cursor = store.get_path(item_iter)
313
298
self._hpane.set_position(300)
315
300
def _construct_accelerators(self):
316
group = Gtk.AccelGroup()
317
group.connect(Gdk.keyval_from_name('N'),
318
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)
319
304
self.add_accel_group(group)
321
# ignore the escape key (avoid closing the window)
322
self.connect_object('close', self.emit_stop_by_name, 'close')
324
306
def _construct_left_pane(self):
325
self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
307
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
326
308
self._construct_file_list()
327
309
self._construct_pending_list()
329
self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
311
self._check_local = gtk.CheckButton(_i18n("_Only commit locally"),
330
312
use_underline=True)
331
self._left_pane_box.pack_end(self._check_local, False, False, 0)
313
self._left_pane_box.pack_end(self._check_local, False, False)
332
314
self._check_local.set_active(False)
334
316
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
353
335
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
355
337
def _construct_action_pane(self):
356
self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
357
self._button_cancel.connect('clicked', self._on_cancel_clicked)
358
self._button_cancel.show()
359
self.get_action_area().pack_end(
360
self._button_cancel, True, True, 0)
361
self._button_commit = Gtk.Button(_i18n("Comm_it"), use_underline=True)
338
self._button_commit = gtk.Button(_i18n("Comm_it"), use_underline=True)
362
339
self._button_commit.connect('clicked', self._on_commit_clicked)
363
self._button_commit.set_can_default(True)
340
self._button_commit.set_flags(gtk.CAN_DEFAULT)
364
341
self._button_commit.show()
365
self.get_action_area().pack_end(
366
self._button_commit, True, True, 0)
342
self.action_area.pack_end(self._button_commit)
367
343
self._button_commit.grab_default()
369
345
def _add_to_right_table(self, widget, weight, expanding=False):
384
360
self._right_pane_table_row = end_row
386
362
def _construct_file_list(self):
387
self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
388
file_label = Gtk.Label(label=_i18n('Files'))
363
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
364
file_label = gtk.Label(_i18n('Files'))
389
365
# file_label.show()
390
self._files_box.pack_start(file_label, False, True, 0)
366
self._files_box.pack_start(file_label, expand=False)
392
self._commit_all_files_radio = Gtk.RadioButton(
368
self._commit_all_files_radio = gtk.RadioButton(
393
369
None, _i18n("Commit all changes"))
394
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)
395
371
self._commit_all_files_radio.show()
396
372
self._commit_all_files_radio.connect('toggled',
397
373
self._toggle_commit_selection)
398
self._commit_selected_radio = Gtk.RadioButton(
374
self._commit_selected_radio = gtk.RadioButton(
399
375
self._commit_all_files_radio, _i18n("Only commit selected changes"))
400
self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
376
self._files_box.pack_start(self._commit_selected_radio, expand=False)
401
377
self._commit_selected_radio.show()
402
378
self._commit_selected_radio.connect('toggled',
403
379
self._toggle_commit_selection)
406
382
self._commit_all_files_radio.set_sensitive(False)
407
383
self._commit_selected_radio.set_sensitive(False)
409
scroller = Gtk.ScrolledWindow()
410
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
411
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()
412
388
self._treeview_files.show()
413
389
scroller.add(self._treeview_files)
414
scroller.set_shadow_type(Gtk.ShadowType.IN)
390
scroller.set_shadow_type(gtk.SHADOW_IN)
416
self._files_box.pack_start(scroller, True, True, 0)
392
self._files_box.pack_start(scroller,
393
expand=True, fill=True)
417
394
self._files_box.show()
418
self._left_pane_box.pack_start(self._files_box, True, True, 0)
395
self._left_pane_box.pack_start(self._files_box)
420
397
# Keep note that all strings stored in a ListStore must be UTF-8
421
398
# strings. GTK does not support directly setting and restoring Unicode
423
liststore = Gtk.ListStore(
424
GObject.TYPE_STRING, # [0] file_id
425
GObject.TYPE_STRING, # [1] real path
426
GObject.TYPE_BOOLEAN, # [2] checkbox
427
GObject.TYPE_STRING, # [3] display path
428
GObject.TYPE_STRING, # [4] changes type
429
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
431
408
self._files_store = liststore
432
409
self._treeview_files.set_model(liststore)
433
crt = Gtk.CellRendererToggle()
410
crt = gtk.CellRendererToggle()
434
411
crt.set_property('activatable', not bool(self._pending))
435
412
crt.connect("toggled", self._toggle_commit, self._files_store)
436
413
if self._pending:
437
414
name = _i18n('Commit*')
439
416
name = _i18n('Commit')
440
commit_col = Gtk.TreeViewColumn(name, crt, active=2)
417
commit_col = gtk.TreeViewColumn(name, crt, active=2)
441
418
commit_col.set_visible(False)
442
419
self._treeview_files.append_column(commit_col)
443
self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Path'),
444
Gtk.CellRendererText(), text=3))
445
self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Type'),
446
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))
447
424
self._treeview_files.connect('cursor-changed',
448
425
self._on_treeview_files_cursor_changed)
470
447
def _construct_pending_list(self):
471
448
# Pending information defaults to hidden, we put it all in 1 box, so
472
449
# that we can show/hide all of them at once
473
self._pending_box = Gtk.VBox()
450
self._pending_box = gtk.VBox()
474
451
self._pending_box.hide()
476
pending_message = Gtk.Label()
453
pending_message = gtk.Label()
477
454
pending_message.set_markup(
478
455
_i18n('<i>* Cannot select specific files when merging</i>'))
479
self._pending_box.pack_start(pending_message, False, True, 5)
456
self._pending_box.pack_start(pending_message, expand=False, padding=5)
480
457
pending_message.show()
482
pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
483
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)
484
461
pending_label.show()
486
scroller = Gtk.ScrolledWindow()
487
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
488
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()
489
466
scroller.add(self._treeview_pending)
490
scroller.set_shadow_type(Gtk.ShadowType.IN)
467
scroller.set_shadow_type(gtk.SHADOW_IN)
492
self._pending_box.pack_start(scroller, True, True, 5)
469
self._pending_box.pack_start(scroller,
470
expand=True, fill=True, padding=5)
493
471
self._treeview_pending.show()
494
self._left_pane_box.pack_start(self._pending_box, True, True, 0)
472
self._left_pane_box.pack_start(self._pending_box)
496
liststore = Gtk.ListStore(GObject.TYPE_STRING, # revision_id
497
GObject.TYPE_STRING, # date
498
GObject.TYPE_STRING, # committer
499
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
501
479
self._pending_store = liststore
502
480
self._treeview_pending.set_model(liststore)
503
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Date'),
504
Gtk.CellRendererText(), text=1))
505
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Committer'),
506
Gtk.CellRendererText(), text=2))
507
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Summary'),
508
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))
510
488
def _construct_diff_view(self):
511
from bzrlib.plugins.gtk.diff import DiffView
489
from diff import DiffView
513
491
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
514
492
# decide that we really don't ever want to display it, we should
515
493
# actually remove it, and other references to it, along with the
516
494
# tests that it is set properly.
517
self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
495
self._diff_label = gtk.Label(_i18n('Diff for whole tree'))
518
496
self._diff_label.set_alignment(0, 0)
519
497
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
520
498
self._add_to_right_table(self._diff_label, 1, False)
525
503
self._diff_view.show()
527
505
def _construct_file_message(self):
528
scroller = Gtk.ScrolledWindow()
529
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
506
scroller = gtk.ScrolledWindow()
507
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
531
self._file_message_text_view = Gtk.TextView()
509
self._file_message_text_view = gtk.TextView()
532
510
scroller.add(self._file_message_text_view)
533
scroller.set_shadow_type(Gtk.ShadowType.IN)
511
scroller.set_shadow_type(gtk.SHADOW_IN)
536
self._file_message_text_view.modify_font(Pango.FontDescription("Monospace"))
537
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)
538
516
self._file_message_text_view.set_accepts_tab(False)
539
517
self._file_message_text_view.show()
541
self._file_message_expander = Gtk.Expander(
542
label=_i18n('File commit message'))
519
self._file_message_expander = gtk.Expander(_i18n('File commit message'))
543
520
self._file_message_expander.set_expanded(True)
544
521
self._file_message_expander.add(scroller)
545
522
self._add_to_right_table(self._file_message_expander, 1, False)
546
523
self._file_message_expander.show()
548
525
def _construct_global_message(self):
549
self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
526
self._global_message_label = gtk.Label(_i18n('Global Commit Message'))
550
527
self._global_message_label.set_markup(
551
528
_i18n('<b>Global Commit Message</b>'))
552
529
self._global_message_label.set_alignment(0, 0)
555
532
# Can we remove the spacing between the label and the box?
556
533
self._global_message_label.show()
558
scroller = Gtk.ScrolledWindow()
559
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
535
scroller = gtk.ScrolledWindow()
536
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
561
self._global_message_text_view = Gtk.TextView()
562
self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
563
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"))
564
540
scroller.add(self._global_message_text_view)
565
scroller.set_shadow_type(Gtk.ShadowType.IN)
541
scroller.set_shadow_type(gtk.SHADOW_IN)
567
543
self._add_to_right_table(scroller, 2, True)
568
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
544
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
569
545
self._file_message_text_view.set_accepts_tab(False)
570
546
self._global_message_text_view.show()
669
def _on_cancel_clicked(self, button):
670
""" Cancel button clicked handler. """
674
def _on_delete_window(self, source, event):
675
""" Delete window handler. """
678
def _do_cancel(self):
679
"""If requested, saves commit messages when cancelling gcommit; they are re-used by a next gcommit"""
680
mgr = SavedCommitMessagesManager()
681
self._saved_commit_messages_manager = mgr
682
mgr.insert(self._get_global_commit_message(),
683
self._get_specific_files()[1])
684
if mgr.is_not_empty(): # maybe worth saving
685
response = self._question_dialog(
686
_i18n('Commit cancelled'),
687
_i18n('Do you want to save your commit messages ?'),
689
if response == Gtk.ResponseType.NO:
690
# save nothing and destroy old comments if any
691
mgr = SavedCommitMessagesManager()
692
mgr.save(self._wt, self._wt.branch)
693
self.response(Gtk.ResponseType.CANCEL) # close window
696
643
def _on_commit_clicked(self, button):
697
644
""" Commit button clicked handler. """
698
645
self._do_commit()
756
699
specific_files=specific_files,
757
700
revprops=revprops)
758
701
self.committed_revision_id = rev_id
759
# destroy old comments if any
760
SavedCommitMessagesManager().save(self._wt, self._wt.branch)
761
self.response(Gtk.ResponseType.OK)
702
self.response(gtk.RESPONSE_OK)
763
704
def _get_global_commit_message(self):
764
705
buf = self._global_message_text_view.get_buffer()
765
706
start, end = buf.get_bounds()
766
text = buf.get_text(start, end, True)
767
return _sanitize_and_decode_message(text)
707
return buf.get_text(start, end).decode('utf-8')
769
709
def _set_global_commit_message(self, message):
770
710
"""Just a helper for the test suite."""
791
731
show_offset=False)
792
732
rev_dict['revision_id'] = rev.revision_id
796
class SavedCommitMessagesManager:
797
"""Save glogal and per-file commit messages.
799
Saves global commit message and utf-8 file_id->message dictionary
800
of per-file commit messages on disk. Re-reads them later for re-using.
803
def __init__(self, tree=None, branch=None):
804
"""If branch is None, builds empty messages, otherwise reads them
805
from branch's disk storage. 'tree' argument is for the future."""
807
self.global_message = u''
808
self.file_messages = {}
810
config = branch.get_config()
811
self.global_message = config.get_user_option(
812
'gtk_global_commit_message')
813
if self.global_message is None:
814
self.global_message = u''
815
file_messages = config.get_user_option('gtk_file_commit_messages')
816
if file_messages: # unicode and B-encoded:
817
self.file_messages = bencode.bdecode(
818
file_messages.encode('UTF-8'))
820
self.file_messages = {}
823
return self.global_message, self.file_messages
825
def is_not_empty(self):
826
return bool(self.global_message or self.file_messages)
828
def insert(self, global_message, file_info):
829
"""Formats per-file commit messages (list of dictionaries, one per file)
830
into one utf-8 file_id->message dictionary and merges this with
831
previously existing dictionary. Merges global commit message too."""
834
file_message = fi['message']
836
file_messages[fi['file_id']] = file_message # utf-8 strings
837
for k,v in file_messages.iteritems():
839
self.file_messages[k] = v + '\n******\n' + self.file_messages[k]
841
self.file_messages[k] = v
842
if self.global_message:
843
self.global_message = global_message + '\n******\n' \
844
+ self.global_message
846
self.global_message = global_message
848
def save(self, tree, branch):
849
# We store in branch's config, which can be a problem if two gcommit
850
# are done in two checkouts of one single branch (comments overwrite
851
# each other). Ideally should be in working tree. But uncommit does
852
# not always have a working tree, though it always has a branch.
853
# 'tree' argument is for the future
854
config = branch.get_config()
855
# should it be named "gtk_" or some more neutral name ("gui_" ?) to
856
# be compatible with qbzr in the future?
857
config.set_user_option('gtk_global_commit_message', self.global_message)
858
# bencode() does not know unicode objects but set_user_option()
860
config.set_user_option(
861
'gtk_file_commit_messages',
862
bencode.bencode(self.file_messages).decode('UTF-8'))
865
def save_commit_messages(local, master, old_revno, old_revid,
866
new_revno, new_revid):
870
mgr = SavedCommitMessagesManager(None, b)
871
revid_iterator = b.repository.iter_reverse_revision_history(old_revid)
872
cur_revno = old_revno
873
new_revision_id = old_revid
874
graph = b.repository.get_graph()
875
for rev_id in revid_iterator:
876
if cur_revno == new_revno:
879
rev = b.repository.get_revision(rev_id)
880
file_info = rev.properties.get('file-info', None)
881
if file_info is None:
884
file_info = bencode.bdecode(file_info.encode('UTF-8'))
885
global_message = osutils.safe_unicode(rev.message)
886
# Concatenate comment of the uncommitted revision
887
mgr.insert(global_message, file_info)
889
parents = graph.get_parent_map([rev_id]).get(rev_id, None)