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