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
25
from bzrlib import bencode
27
from bzrlib.util import bencode
29
30
from bzrlib import (
37
from bzrlib import bencode
39
from bzrlib.util import bencode
41
from bzrlib.plugins.gtk import _i18n
33
42
from bzrlib.plugins.gtk.dialog import question_dialog
34
43
from bzrlib.plugins.gtk.errors import show_bzr_error
35
from bzrlib.plugins.gtk.i18n import _i18n
36
from bzrlib.plugins.gtk.commitmsgs import SavedCommitMessagesManager
104
111
return fixed_newline.decode('utf-8')
107
class CommitDialog(Gtk.Dialog):
114
class CommitDialog(gtk.Dialog):
108
115
"""Implementation of Commit."""
110
117
def __init__(self, wt, selected=None, parent=None):
111
super(CommitDialog, self).__init__(
112
title="Commit to %s" % wt.basedir, parent=parent, flags=0)
118
gtk.Dialog.__init__(self, title="Commit to %s" % wt.basedir,
119
parent=parent, flags=0,)
113
120
self.connect('delete-event', self._on_delete_window)
114
121
self._question_dialog = question_dialog
116
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
123
self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NORMAL)
119
126
# TODO: Do something with this value, it is used by Olive
313
319
self._hpane.set_position(300)
315
321
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)
322
group = gtk.AccelGroup()
323
group.connect_group(gtk.gdk.keyval_from_name('N'),
324
gtk.gdk.CONTROL_MASK, 0, self._on_accel_next)
319
325
self.add_accel_group(group)
321
327
# ignore the escape key (avoid closing the window)
322
328
self.connect_object('close', self.emit_stop_by_name, 'close')
324
330
def _construct_left_pane(self):
325
self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
331
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
326
332
self._construct_file_list()
327
333
self._construct_pending_list()
329
self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
335
self._check_local = gtk.CheckButton(_i18n("_Only commit locally"),
330
336
use_underline=True)
331
self._left_pane_box.pack_end(self._check_local, False, False, 0)
337
self._left_pane_box.pack_end(self._check_local, False, False)
332
338
self._check_local.set_active(False)
334
340
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
353
359
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
355
361
def _construct_action_pane(self):
356
self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
362
self._button_cancel = gtk.Button(stock=gtk.STOCK_CANCEL)
357
363
self._button_cancel.connect('clicked', self._on_cancel_clicked)
358
364
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)
365
self.action_area.pack_end(self._button_cancel)
366
self._button_commit = gtk.Button(_i18n("Comm_it"), use_underline=True)
362
367
self._button_commit.connect('clicked', self._on_commit_clicked)
363
self._button_commit.set_can_default(True)
368
self._button_commit.set_flags(gtk.CAN_DEFAULT)
364
369
self._button_commit.show()
365
self.get_action_area().pack_end(
366
self._button_commit, True, True, 0)
370
self.action_area.pack_end(self._button_commit)
367
371
self._button_commit.grab_default()
369
373
def _add_to_right_table(self, widget, weight, expanding=False):
384
388
self._right_pane_table_row = end_row
386
390
def _construct_file_list(self):
387
self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
388
file_label = Gtk.Label(label=_i18n('Files'))
391
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
392
file_label = gtk.Label(_i18n('Files'))
389
393
# file_label.show()
390
self._files_box.pack_start(file_label, False, True, 0)
394
self._files_box.pack_start(file_label, expand=False)
392
self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
396
self._commit_all_files_radio = gtk.RadioButton(
393
397
None, _i18n("Commit all changes"))
394
self._files_box.pack_start(self._commit_all_files_radio, False, True, 0)
398
self._files_box.pack_start(self._commit_all_files_radio, expand=False)
395
399
self._commit_all_files_radio.show()
396
400
self._commit_all_files_radio.connect('toggled',
397
401
self._toggle_commit_selection)
398
self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
402
self._commit_selected_radio = gtk.RadioButton(
399
403
self._commit_all_files_radio, _i18n("Only commit selected changes"))
400
self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
404
self._files_box.pack_start(self._commit_selected_radio, expand=False)
401
405
self._commit_selected_radio.show()
402
406
self._commit_selected_radio.connect('toggled',
403
407
self._toggle_commit_selection)
406
410
self._commit_all_files_radio.set_sensitive(False)
407
411
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()
413
scroller = gtk.ScrolledWindow()
414
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
415
self._treeview_files = gtk.TreeView()
412
416
self._treeview_files.show()
413
417
scroller.add(self._treeview_files)
414
scroller.set_shadow_type(Gtk.ShadowType.IN)
418
scroller.set_shadow_type(gtk.SHADOW_IN)
416
self._files_box.pack_start(scroller, True, True, 0)
420
self._files_box.pack_start(scroller,
421
expand=True, fill=True)
417
422
self._files_box.show()
418
self._left_pane_box.pack_start(self._files_box, True, True, 0)
423
self._left_pane_box.pack_start(self._files_box)
420
425
# Keep note that all strings stored in a ListStore must be UTF-8
421
426
# 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
428
liststore = gtk.ListStore(
429
gobject.TYPE_STRING, # [0] file_id
430
gobject.TYPE_STRING, # [1] real path
431
gobject.TYPE_BOOLEAN, # [2] checkbox
432
gobject.TYPE_STRING, # [3] display path
433
gobject.TYPE_STRING, # [4] changes type
434
gobject.TYPE_STRING, # [5] commit message
431
436
self._files_store = liststore
432
437
self._treeview_files.set_model(liststore)
433
crt = Gtk.CellRendererToggle()
438
crt = gtk.CellRendererToggle()
434
439
crt.set_property('activatable', not bool(self._pending))
435
440
crt.connect("toggled", self._toggle_commit, self._files_store)
436
441
if self._pending:
437
442
name = _i18n('Commit*')
439
444
name = _i18n('Commit')
440
commit_col = Gtk.TreeViewColumn(name, crt, active=2)
445
commit_col = gtk.TreeViewColumn(name, crt, active=2)
441
446
commit_col.set_visible(False)
442
447
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))
448
self._treeview_files.append_column(gtk.TreeViewColumn(_i18n('Path'),
449
gtk.CellRendererText(), text=3))
450
self._treeview_files.append_column(gtk.TreeViewColumn(_i18n('Type'),
451
gtk.CellRendererText(), text=4))
447
452
self._treeview_files.connect('cursor-changed',
448
453
self._on_treeview_files_cursor_changed)
464
469
checked_col.set_visible(False)
466
471
checked_col.set_visible(True)
467
renderer = checked_col.get_cells()[0]
472
renderer = checked_col.get_cell_renderers()[0]
468
473
renderer.set_property('activatable', not all_files)
470
475
def _construct_pending_list(self):
471
476
# Pending information defaults to hidden, we put it all in 1 box, so
472
477
# that we can show/hide all of them at once
473
self._pending_box = Gtk.VBox()
478
self._pending_box = gtk.VBox()
474
479
self._pending_box.hide()
476
pending_message = Gtk.Label()
481
pending_message = gtk.Label()
477
482
pending_message.set_markup(
478
483
_i18n('<i>* Cannot select specific files when merging</i>'))
479
self._pending_box.pack_start(pending_message, False, True, 5)
484
self._pending_box.pack_start(pending_message, expand=False, padding=5)
480
485
pending_message.show()
482
pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
483
self._pending_box.pack_start(pending_label, False, True, 0)
487
pending_label = gtk.Label(_i18n('Pending Revisions'))
488
self._pending_box.pack_start(pending_label, expand=False, padding=0)
484
489
pending_label.show()
486
scroller = Gtk.ScrolledWindow()
487
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
488
self._treeview_pending = Gtk.TreeView()
491
scroller = gtk.ScrolledWindow()
492
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
493
self._treeview_pending = gtk.TreeView()
489
494
scroller.add(self._treeview_pending)
490
scroller.set_shadow_type(Gtk.ShadowType.IN)
495
scroller.set_shadow_type(gtk.SHADOW_IN)
492
self._pending_box.pack_start(scroller, True, True, 5)
497
self._pending_box.pack_start(scroller,
498
expand=True, fill=True, padding=5)
493
499
self._treeview_pending.show()
494
self._left_pane_box.pack_start(self._pending_box, True, True, 0)
500
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
502
liststore = gtk.ListStore(gobject.TYPE_STRING, # revision_id
503
gobject.TYPE_STRING, # date
504
gobject.TYPE_STRING, # committer
505
gobject.TYPE_STRING, # summary
501
507
self._pending_store = liststore
502
508
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))
509
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Date'),
510
gtk.CellRendererText(), text=1))
511
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Committer'),
512
gtk.CellRendererText(), text=2))
513
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Summary'),
514
gtk.CellRendererText(), text=3))
510
516
def _construct_diff_view(self):
511
from bzrlib.plugins.gtk.diff import DiffView
517
from diff import DiffView
513
519
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
514
520
# decide that we really don't ever want to display it, we should
515
521
# actually remove it, and other references to it, along with the
516
522
# tests that it is set properly.
517
self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
523
self._diff_label = gtk.Label(_i18n('Diff for whole tree'))
518
524
self._diff_label.set_alignment(0, 0)
519
525
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
520
526
self._add_to_right_table(self._diff_label, 1, False)
525
531
self._diff_view.show()
527
533
def _construct_file_message(self):
528
scroller = Gtk.ScrolledWindow()
529
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
534
scroller = gtk.ScrolledWindow()
535
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
531
self._file_message_text_view = Gtk.TextView()
537
self._file_message_text_view = gtk.TextView()
532
538
scroller.add(self._file_message_text_view)
533
scroller.set_shadow_type(Gtk.ShadowType.IN)
539
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)
542
self._file_message_text_view.modify_font(pango.FontDescription("Monospace"))
543
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
538
544
self._file_message_text_view.set_accepts_tab(False)
539
545
self._file_message_text_view.show()
541
self._file_message_expander = Gtk.Expander(
542
label=_i18n('File commit message'))
547
self._file_message_expander = gtk.Expander(_i18n('File commit message'))
543
548
self._file_message_expander.set_expanded(True)
544
549
self._file_message_expander.add(scroller)
545
550
self._add_to_right_table(self._file_message_expander, 1, False)
546
551
self._file_message_expander.show()
548
553
def _construct_global_message(self):
549
self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
554
self._global_message_label = gtk.Label(_i18n('Global Commit Message'))
550
555
self._global_message_label.set_markup(
551
556
_i18n('<b>Global Commit Message</b>'))
552
557
self._global_message_label.set_alignment(0, 0)
555
560
# Can we remove the spacing between the label and the box?
556
561
self._global_message_label.show()
558
scroller = Gtk.ScrolledWindow()
559
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
563
scroller = gtk.ScrolledWindow()
564
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
561
self._global_message_text_view = Gtk.TextView()
566
self._global_message_text_view = gtk.TextView()
562
567
self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
563
self._global_message_text_view.modify_font(Pango.FontDescription("Monospace"))
568
self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
564
569
scroller.add(self._global_message_text_view)
565
scroller.set_shadow_type(Gtk.ShadowType.IN)
570
scroller.set_shadow_type(gtk.SHADOW_IN)
567
572
self._add_to_right_table(scroller, 2, True)
568
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
573
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
569
574
self._file_message_text_view.set_accepts_tab(False)
570
575
self._global_message_text_view.show()
596
601
# We have either made it to the end of the list, or nothing was
597
602
# selected. Either way, select All Files, and jump to the global
598
603
# commit message.
599
self._treeview_files.set_cursor(
600
Gtk.TreePath(path=0), None, False)
604
self._treeview_files.set_cursor((0,))
601
605
self._global_message_text_view.grab_focus()
603
607
# 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)
609
self._treeview_files.set_cursor(model.get_path(next))
606
610
self._file_message_text_view.grab_focus()
608
612
def _save_current_file_message(self):
793
797
rev_dict['revision_id'] = rev.revision_id
801
class SavedCommitMessagesManager:
802
"""Save glogal and per-file commit messages.
804
Saves global commit message and utf-8 file_id->message dictionary
805
of per-file commit messages on disk. Re-reads them later for re-using.
808
def __init__(self, tree=None, branch=None):
809
"""If branch is None, builds empty messages, otherwise reads them
810
from branch's disk storage. 'tree' argument is for the future."""
812
self.global_message = u''
813
self.file_messages = {}
815
config = branch.get_config()._get_branch_data_config()
816
self.global_message = config.get_user_option(
817
'gtk_global_commit_message')
818
if self.global_message is None:
819
self.global_message = u''
820
file_messages = config.get_user_option('gtk_file_commit_messages')
821
if file_messages: # unicode and B-encoded:
822
self.file_messages = bencode.bdecode(
823
file_messages.encode('UTF-8'))
825
self.file_messages = {}
828
return self.global_message, self.file_messages
830
def is_not_empty(self):
831
return bool(self.global_message or self.file_messages)
833
def insert(self, global_message, file_info):
834
"""Formats per-file commit messages (list of dictionaries, one per file)
835
into one utf-8 file_id->message dictionary and merges this with
836
previously existing dictionary. Merges global commit message too."""
839
file_message = fi['message']
841
file_messages[fi['file_id']] = file_message # utf-8 strings
842
for k,v in file_messages.iteritems():
844
self.file_messages[k] = v + '\n******\n' + self.file_messages[k]
846
self.file_messages[k] = v
847
if self.global_message:
848
self.global_message = global_message + '\n******\n' \
849
+ self.global_message
851
self.global_message = global_message
853
def save(self, tree, branch):
854
# We store in branch's config, which can be a problem if two gcommit
855
# are done in two checkouts of one single branch (comments overwrite
856
# each other). Ideally should be in working tree. But uncommit does
857
# not always have a working tree, though it always has a branch.
858
# 'tree' argument is for the future
859
config = branch.get_config()
860
# should it be named "gtk_" or some more neutral name ("gui_" ?) to
861
# be compatible with qbzr in the future?
862
config.set_user_option('gtk_global_commit_message', self.global_message)
863
# bencode() does not know unicode objects but set_user_option()
865
config.set_user_option(
866
'gtk_file_commit_messages',
867
bencode.bencode(self.file_messages).decode('UTF-8'))
870
def save_commit_messages(local, master, old_revno, old_revid,
871
new_revno, new_revid):
875
mgr = SavedCommitMessagesManager(None, b)
876
revid_iterator = b.repository.iter_reverse_revision_history(old_revid)
877
cur_revno = old_revno
878
new_revision_id = old_revid
879
graph = b.repository.get_graph()
880
for rev_id in revid_iterator:
881
if cur_revno == new_revno:
884
rev = b.repository.get_revision(rev_id)
885
file_info = rev.properties.get('file-info', None)
886
if file_info is None:
889
file_info = bencode.bdecode(file_info.encode('UTF-8'))
890
global_message = osutils.safe_unicode(rev.message)
891
# Concatenate comment of the uncommitted revision
892
mgr.insert(global_message, file_info)
894
parents = graph.get_parent_map([rev_id]).get(rev_id, None)