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
99
111
return fixed_newline.decode('utf-8')
102
class CommitDialog(Gtk.Dialog):
114
class CommitDialog(gtk.Dialog):
103
115
"""Implementation of Commit."""
105
117
def __init__(self, wt, selected=None, parent=None):
106
super(CommitDialog, self).__init__(
107
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,)
108
120
self.connect('delete-event', self._on_delete_window)
109
121
self._question_dialog = question_dialog
111
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
123
self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NORMAL)
114
126
# TODO: Do something with this value, it is used by Olive
312
319
self._hpane.set_position(300)
314
321
def _construct_accelerators(self):
315
group = Gtk.AccelGroup()
316
group.connect(Gdk.keyval_from_name('N'),
317
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)
318
325
self.add_accel_group(group)
320
327
# ignore the escape key (avoid closing the window)
321
328
self.connect_object('close', self.emit_stop_by_name, 'close')
323
330
def _construct_left_pane(self):
324
self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
331
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
325
332
self._construct_file_list()
326
333
self._construct_pending_list()
328
self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
335
self._check_local = gtk.CheckButton(_i18n("_Only commit locally"),
329
336
use_underline=True)
330
self._left_pane_box.pack_end(self._check_local, False, False, 0)
337
self._left_pane_box.pack_end(self._check_local, False, False)
331
338
self._check_local.set_active(False)
333
340
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
352
359
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
354
361
def _construct_action_pane(self):
355
self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
362
self._button_cancel = gtk.Button(stock=gtk.STOCK_CANCEL)
356
363
self._button_cancel.connect('clicked', self._on_cancel_clicked)
357
364
self._button_cancel.show()
358
self.get_action_area().pack_end(
359
self._button_cancel, True, True, 0)
360
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)
361
367
self._button_commit.connect('clicked', self._on_commit_clicked)
362
self._button_commit.set_can_default(True)
368
self._button_commit.set_flags(gtk.CAN_DEFAULT)
363
369
self._button_commit.show()
364
self.get_action_area().pack_end(
365
self._button_commit, True, True, 0)
370
self.action_area.pack_end(self._button_commit)
366
371
self._button_commit.grab_default()
368
373
def _add_to_right_table(self, widget, weight, expanding=False):
383
388
self._right_pane_table_row = end_row
385
390
def _construct_file_list(self):
386
self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
387
file_label = Gtk.Label(label=_i18n('Files'))
391
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
392
file_label = gtk.Label(_i18n('Files'))
388
393
# file_label.show()
389
self._files_box.pack_start(file_label, False, True, 0)
394
self._files_box.pack_start(file_label, expand=False)
391
self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
396
self._commit_all_files_radio = gtk.RadioButton(
392
397
None, _i18n("Commit all changes"))
393
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)
394
399
self._commit_all_files_radio.show()
395
400
self._commit_all_files_radio.connect('toggled',
396
401
self._toggle_commit_selection)
397
self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
402
self._commit_selected_radio = gtk.RadioButton(
398
403
self._commit_all_files_radio, _i18n("Only commit selected changes"))
399
self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
404
self._files_box.pack_start(self._commit_selected_radio, expand=False)
400
405
self._commit_selected_radio.show()
401
406
self._commit_selected_radio.connect('toggled',
402
407
self._toggle_commit_selection)
405
410
self._commit_all_files_radio.set_sensitive(False)
406
411
self._commit_selected_radio.set_sensitive(False)
408
scroller = Gtk.ScrolledWindow()
409
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
410
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()
411
416
self._treeview_files.show()
412
417
scroller.add(self._treeview_files)
413
scroller.set_shadow_type(Gtk.ShadowType.IN)
418
scroller.set_shadow_type(gtk.SHADOW_IN)
415
self._files_box.pack_start(scroller, True, True, 0)
420
self._files_box.pack_start(scroller,
421
expand=True, fill=True)
416
422
self._files_box.show()
417
self._left_pane_box.pack_start(self._files_box, True, True, 0)
423
self._left_pane_box.pack_start(self._files_box)
419
425
# Keep note that all strings stored in a ListStore must be UTF-8
420
426
# strings. GTK does not support directly setting and restoring Unicode
422
liststore = Gtk.ListStore(
423
GObject.TYPE_STRING, # [0] file_id
424
GObject.TYPE_STRING, # [1] real path
425
GObject.TYPE_BOOLEAN, # [2] checkbox
426
GObject.TYPE_STRING, # [3] display path
427
GObject.TYPE_STRING, # [4] changes type
428
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
430
436
self._files_store = liststore
431
437
self._treeview_files.set_model(liststore)
432
crt = Gtk.CellRendererToggle()
438
crt = gtk.CellRendererToggle()
433
439
crt.set_property('activatable', not bool(self._pending))
434
440
crt.connect("toggled", self._toggle_commit, self._files_store)
435
441
if self._pending:
436
442
name = _i18n('Commit*')
438
444
name = _i18n('Commit')
439
commit_col = Gtk.TreeViewColumn(name, crt, active=2)
445
commit_col = gtk.TreeViewColumn(name, crt, active=2)
440
446
commit_col.set_visible(False)
441
447
self._treeview_files.append_column(commit_col)
442
self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Path'),
443
Gtk.CellRendererText(), text=3))
444
self._treeview_files.append_column(Gtk.TreeViewColumn(_i18n('Type'),
445
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))
446
452
self._treeview_files.connect('cursor-changed',
447
453
self._on_treeview_files_cursor_changed)
463
469
checked_col.set_visible(False)
465
471
checked_col.set_visible(True)
466
renderer = checked_col.get_cells()[0]
472
renderer = checked_col.get_cell_renderers()[0]
467
473
renderer.set_property('activatable', not all_files)
469
475
def _construct_pending_list(self):
470
476
# Pending information defaults to hidden, we put it all in 1 box, so
471
477
# that we can show/hide all of them at once
472
self._pending_box = Gtk.VBox()
478
self._pending_box = gtk.VBox()
473
479
self._pending_box.hide()
475
pending_message = Gtk.Label()
481
pending_message = gtk.Label()
476
482
pending_message.set_markup(
477
483
_i18n('<i>* Cannot select specific files when merging</i>'))
478
self._pending_box.pack_start(pending_message, False, True, 5)
484
self._pending_box.pack_start(pending_message, expand=False, padding=5)
479
485
pending_message.show()
481
pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
482
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)
483
489
pending_label.show()
485
scroller = Gtk.ScrolledWindow()
486
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
487
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()
488
494
scroller.add(self._treeview_pending)
489
scroller.set_shadow_type(Gtk.ShadowType.IN)
495
scroller.set_shadow_type(gtk.SHADOW_IN)
491
self._pending_box.pack_start(scroller, True, True, 5)
497
self._pending_box.pack_start(scroller,
498
expand=True, fill=True, padding=5)
492
499
self._treeview_pending.show()
493
self._left_pane_box.pack_start(self._pending_box, True, True, 0)
500
self._left_pane_box.pack_start(self._pending_box)
495
liststore = Gtk.ListStore(GObject.TYPE_STRING, # revision_id
496
GObject.TYPE_STRING, # date
497
GObject.TYPE_STRING, # committer
498
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
500
507
self._pending_store = liststore
501
508
self._treeview_pending.set_model(liststore)
502
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Date'),
503
Gtk.CellRendererText(), text=1))
504
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Committer'),
505
Gtk.CellRendererText(), text=2))
506
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Summary'),
507
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))
509
516
def _construct_diff_view(self):
510
from bzrlib.plugins.gtk.diff import DiffView
517
from diff import DiffView
512
519
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
513
520
# decide that we really don't ever want to display it, we should
514
521
# actually remove it, and other references to it, along with the
515
522
# tests that it is set properly.
516
self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
523
self._diff_label = gtk.Label(_i18n('Diff for whole tree'))
517
524
self._diff_label.set_alignment(0, 0)
518
525
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
519
526
self._add_to_right_table(self._diff_label, 1, False)
524
531
self._diff_view.show()
526
533
def _construct_file_message(self):
527
scroller = Gtk.ScrolledWindow()
528
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
534
scroller = gtk.ScrolledWindow()
535
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
530
self._file_message_text_view = Gtk.TextView()
537
self._file_message_text_view = gtk.TextView()
531
538
scroller.add(self._file_message_text_view)
532
scroller.set_shadow_type(Gtk.ShadowType.IN)
539
scroller.set_shadow_type(gtk.SHADOW_IN)
535
self._file_message_text_view.modify_font(Pango.FontDescription("Monospace"))
536
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)
537
544
self._file_message_text_view.set_accepts_tab(False)
538
545
self._file_message_text_view.show()
540
self._file_message_expander = Gtk.Expander(
541
label=_i18n('File commit message'))
547
self._file_message_expander = gtk.Expander(_i18n('File commit message'))
542
548
self._file_message_expander.set_expanded(True)
543
549
self._file_message_expander.add(scroller)
544
550
self._add_to_right_table(self._file_message_expander, 1, False)
545
551
self._file_message_expander.show()
547
553
def _construct_global_message(self):
548
self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
554
self._global_message_label = gtk.Label(_i18n('Global Commit Message'))
549
555
self._global_message_label.set_markup(
550
556
_i18n('<b>Global Commit Message</b>'))
551
557
self._global_message_label.set_alignment(0, 0)
554
560
# Can we remove the spacing between the label and the box?
555
561
self._global_message_label.show()
557
scroller = Gtk.ScrolledWindow()
558
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
563
scroller = gtk.ScrolledWindow()
564
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
560
self._global_message_text_view = Gtk.TextView()
566
self._global_message_text_view = gtk.TextView()
561
567
self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
562
self._global_message_text_view.modify_font(Pango.FontDescription("Monospace"))
568
self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
563
569
scroller.add(self._global_message_text_view)
564
scroller.set_shadow_type(Gtk.ShadowType.IN)
570
scroller.set_shadow_type(gtk.SHADOW_IN)
566
572
self._add_to_right_table(scroller, 2, True)
567
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
573
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
568
574
self._file_message_text_view.set_accepts_tab(False)
569
575
self._global_message_text_view.show()
595
601
# We have either made it to the end of the list, or nothing was
596
602
# selected. Either way, select All Files, and jump to the global
597
603
# commit message.
598
self._treeview_files.set_cursor(
599
Gtk.TreePath(path=0), None, False)
604
self._treeview_files.set_cursor((0,))
600
605
self._global_message_text_view.grab_focus()
602
607
# Set the cursor to this entry, and jump to the per-file commit
604
self._treeview_files.set_cursor(model.get_path(next), None, False)
609
self._treeview_files.set_cursor(model.get_path(next))
605
610
self._file_message_text_view.grab_focus()
607
612
def _save_current_file_message(self):
792
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)