1
# Copyright (C) 2006 by Szilveszter Farkas (Phanatic) <szilveszter.farkas@gmail.com>
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from gi.repository import Gtk
20
from gi.repository import GObject
21
from gi.repository import Pango
29
from bzrlib import bencode
31
from bzrlib.util import bencode
33
from bzrlib.plugins.gtk.dialog import question_dialog
34
from bzrlib.plugins.gtk.errors import show_bzr_error
35
from bzrlib.plugins.gtk.i18n import _i18n
45
def pending_revisions(wt):
46
"""Return a list of pending merges or None if there are none of them.
48
Arguably this should be a core function, and
49
``bzrlib.status.show_pending_merges`` should be built on top of it.
51
:return: [(rev, [children])]
53
parents = wt.get_parent_ids()
57
# The basic pending merge algorithm uses the same algorithm as
58
# bzrlib.status.show_pending_merges
61
last_revision = parents[0]
63
if last_revision is not None:
65
ignore = set(branch.repository.get_ancestry(last_revision,
67
except errors.NoSuchRevision:
68
# the last revision is a ghost : assume everything is new
70
ignore = set([None, last_revision])
78
rev = branch.repository.get_revision(merge)
80
pm.append((rev, children))
82
# This does need to be topo sorted, so we search backwards
83
inner_merges = branch.repository.get_ancestry(merge)
84
assert inner_merges[0] is None
86
for mmerge in reversed(inner_merges):
89
rev = branch.repository.get_revision(mmerge)
93
except errors.NoSuchRevision:
94
print "DEBUG: NoSuchRevision:", merge
99
_newline_variants_re = re.compile(r'\r\n?')
100
def _sanitize_and_decode_message(utf8_message):
101
"""Turn a utf-8 message into a sanitized Unicode message."""
102
fixed_newline = _newline_variants_re.sub('\n', utf8_message)
103
return fixed_newline.decode('utf-8')
106
class CommitDialog(Gtk.Dialog):
107
"""Implementation of Commit."""
109
def __init__(self, wt, selected=None, parent=None):
110
GObject.GObject.__init__(self, title="Commit to %s" % wt.basedir,
111
parent=parent, flags=0,)
112
self.connect('delete-event', self._on_delete_window)
113
self._question_dialog = question_dialog
115
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
118
# TODO: Do something with this value, it is used by Olive
119
# It used to set all changes but this one to False
120
self._selected = selected
121
self._enable_per_file_commits = True
122
self._commit_all_changes = True
123
self.committed_revision_id = None # Nothing has been committed yet
124
self._saved_commit_messages_manager = SavedCommitMessagesManager(self._wt, self._wt.branch)
130
def setup_params(self):
131
"""Setup the member variables for state."""
132
self._basis_tree = self._wt.basis_tree()
134
self._pending = pending_revisions(self._wt)
136
self._is_checkout = (self._wt.branch.get_bound_location() is not None)
138
def fill_in_data(self):
139
# Now that we are built, handle changes to the view based on the state
140
self._fill_in_pending()
142
self._fill_in_files()
143
self._fill_in_checkout()
144
self._fill_in_per_file_info()
146
def _fill_in_pending(self):
147
if not self._pending:
148
self._pending_box.hide()
151
# TODO: We'd really prefer this to be a nested list
152
for rev, children in self._pending:
153
rev_info = self._rev_to_pending_info(rev)
154
self._pending_store.append([
155
rev_info['revision_id'],
157
rev_info['committer'],
160
for child in children:
161
rev_info = self._rev_to_pending_info(child)
162
self._pending_store.append([
163
rev_info['revision_id'],
165
rev_info['committer'],
168
self._pending_box.show()
170
def _fill_in_files(self):
171
# We should really use add a progress bar of some kind.
172
# While we fill in the view, hide the store
173
store = self._files_store
174
self._treeview_files.set_model(None)
176
added = _i18n('added')
177
removed = _i18n('removed')
178
renamed = _i18n('renamed')
179
renamed_and_modified = _i18n('renamed and modified')
180
modified = _i18n('modified')
181
kind_changed = _i18n('kind changed')
184
# [file_id, real path, checkbox, display path, changes type, message]
185
# iter_changes returns:
186
# (file_id, (path_in_source, path_in_target),
187
# changed_content, versioned, parent, name, kind,
190
all_enabled = (self._selected is None)
191
# The first entry is always the 'whole tree'
192
all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
193
initial_cursor = store.get_path(all_iter)
194
# should we pass specific_files?
196
self._basis_tree.lock_read()
198
from diff import iter_changes_to_status
199
saved_file_messages = self._saved_commit_messages_manager.get()[1]
200
for (file_id, real_path, change_type, display_path
201
) in iter_changes_to_status(self._basis_tree, self._wt):
202
if self._selected and real_path != self._selected:
207
default_message = saved_file_messages[file_id]
210
item_iter = store.append([
212
real_path.encode('UTF-8'),
214
display_path.encode('UTF-8'),
216
default_message, # Initial comment
218
if self._selected and enabled:
219
initial_cursor = store.get_path(item_iter)
221
self._basis_tree.unlock()
224
self._treeview_files.set_model(store)
225
self._last_selected_file = None
226
# This sets the cursor, which causes the expander to close, which
227
# causes the _file_message_text_view to never get realized. So we have
228
# to give it a little kick, or it warns when we try to grab the focus
229
self._treeview_files.set_cursor(initial_cursor)
231
def _realize_file_message_tree_view(*args):
232
self._file_message_text_view.realize()
233
self.connect_after('realize', _realize_file_message_tree_view)
235
def _fill_in_diff(self):
236
self._diff_view.set_trees(self._wt, self._basis_tree)
238
def _fill_in_checkout(self):
239
if not self._is_checkout:
240
self._check_local.hide()
243
bus = dbus.SystemBus()
245
proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
246
'/org/freedesktop/NetworkManager')
247
except dbus.DBusException:
248
trace.mutter("networkmanager not available.")
249
self._check_local.show()
252
dbus_iface = dbus.Interface(proxy_obj,
253
'org.freedesktop.NetworkManager')
255
# 3 is the enum value for STATE_CONNECTED
256
self._check_local.set_active(dbus_iface.state() != 3)
257
except dbus.DBusException, e:
258
# Silently drop errors. While DBus may be
259
# available, NetworkManager doesn't necessarily have to be
260
trace.mutter("unable to get networkmanager state: %r" % e)
261
self._check_local.show()
263
def _fill_in_per_file_info(self):
264
config = self._wt.branch.get_config()
265
enable_per_file_commits = config.get_user_option('per_file_commits')
266
if (enable_per_file_commits is None
267
or enable_per_file_commits.lower()
268
not in ('y', 'yes', 'on', 'enable', '1', 't', 'true')):
269
self._enable_per_file_commits = False
271
self._enable_per_file_commits = True
272
if not self._enable_per_file_commits:
273
self._file_message_expander.hide()
274
self._global_message_label.set_markup(_i18n('<b>Commit Message</b>'))
276
def _compute_delta(self):
277
self._delta = self._wt.changes_from(self._basis_tree)
280
"""Build up the dialog widgets."""
281
# The primary pane which splits it into left and right (adjustable)
283
self._hpane = Gtk.HPaned()
285
self._construct_left_pane()
286
self._construct_right_pane()
287
self._construct_action_pane()
289
self.vbox.pack_start(self._hpane, True, True, 0)
291
self.set_focus(self._global_message_text_view)
293
self._construct_accelerators()
296
def _set_sizes(self):
297
# This seems like a reasonable default, we might like it to
298
# be a bit wider, so that by default we can fit an 80-line diff in the
300
# Alternatively, we should be saving the last position/size rather than
301
# setting it to a fixed value every time we start up.
302
screen = self.get_screen()
303
monitor = 0 # We would like it to be the monitor we are going to
304
# display on, but I don't know how to figure that out
305
# Only really useful for freaks like me that run dual
306
# monitor, with different sizes on the monitors
307
monitor_rect = screen.get_monitor_geometry(monitor)
308
width = int(monitor_rect.width * 0.66)
309
height = int(monitor_rect.height * 0.66)
310
self.set_default_size(width, height)
311
self._hpane.set_position(300)
313
def _construct_accelerators(self):
314
group = Gtk.AccelGroup()
315
group.connect_group(Gdk.keyval_from_name('N'),
316
Gdk.EventMask.CONTROL_MASK, 0, self._on_accel_next)
317
self.add_accel_group(group)
319
# ignore the escape key (avoid closing the window)
320
self.connect_object('close', self.emit_stop_by_name, 'close')
322
def _construct_left_pane(self):
323
self._left_pane_box = Gtk.VBox(homogeneous=False, spacing=5)
324
self._construct_file_list()
325
self._construct_pending_list()
327
self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
329
self._left_pane_box.pack_end(self._check_local, False, False)
330
self._check_local.set_active(False)
332
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
333
self._left_pane_box.show()
335
def _construct_right_pane(self):
336
# TODO: I really want to make it so the diff view gets more space than
337
# the global commit message, and the per-file commit message gets even
338
# less. When I did it with wxGlade, I set it to 4 for diff, 2 for
339
# commit, and 1 for file commit, and it looked good. But I don't seem
340
# to have a way to do that with the gtk boxes... :( (Which is extra
341
# weird since wx uses gtk on Linux...)
342
self._right_pane_table = Gtk.Table(rows=10, columns=1, homogeneous=False)
343
self._right_pane_table.set_row_spacings(5)
344
self._right_pane_table.set_col_spacings(5)
345
self._right_pane_table_row = 0
346
self._construct_diff_view()
347
self._construct_file_message()
348
self._construct_global_message()
350
self._right_pane_table.show()
351
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
353
def _construct_action_pane(self):
354
self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
355
self._button_cancel.connect('clicked', self._on_cancel_clicked)
356
self._button_cancel.show()
357
self.action_area.pack_end(self._button_cancel)
358
self._button_commit = Gtk.Button(_i18n("Comm_it"), use_underline=True)
359
self._button_commit.connect('clicked', self._on_commit_clicked)
360
self._button_commit.set_can_default(True)
361
self._button_commit.show()
362
self.action_area.pack_end(self._button_commit)
363
self._button_commit.grab_default()
365
def _add_to_right_table(self, widget, weight, expanding=False):
366
"""Add another widget to the table
368
:param widget: The object to add
369
:param weight: How many rows does this widget get to request
370
:param expanding: Should expand|fill|shrink be set?
372
end_row = self._right_pane_table_row + weight
374
expand_opts = Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK
376
options = expand_opts
377
self._right_pane_table.attach(widget, 0, 1,
378
self._right_pane_table_row, end_row,
379
xoptions=expand_opts, yoptions=options)
380
self._right_pane_table_row = end_row
382
def _construct_file_list(self):
383
self._files_box = Gtk.VBox(homogeneous=False, spacing=0)
384
file_label = Gtk.Label(label=_i18n('Files'))
386
self._files_box.pack_start(file_label, False, True, 0)
388
self._commit_all_files_radio = Gtk.RadioButton(
389
None, _i18n("Commit all changes"))
390
self._files_box.pack_start(self._commit_all_files_radio, False, True, 0)
391
self._commit_all_files_radio.show()
392
self._commit_all_files_radio.connect('toggled',
393
self._toggle_commit_selection)
394
self._commit_selected_radio = Gtk.RadioButton(
395
self._commit_all_files_radio, _i18n("Only commit selected changes"))
396
self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
397
self._commit_selected_radio.show()
398
self._commit_selected_radio.connect('toggled',
399
self._toggle_commit_selection)
401
self._commit_all_files_radio.set_label(_i18n('Commit all changes*'))
402
self._commit_all_files_radio.set_sensitive(False)
403
self._commit_selected_radio.set_sensitive(False)
405
scroller = Gtk.ScrolledWindow()
406
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
407
self._treeview_files = Gtk.TreeView()
408
self._treeview_files.show()
409
scroller.add(self._treeview_files)
410
scroller.set_shadow_type(Gtk.ShadowType.IN)
412
self._files_box.pack_start(scroller,
413
expand=True, fill=True)
414
self._files_box.show()
415
self._left_pane_box.pack_start(self._files_box, True, True, 0)
417
# Keep note that all strings stored in a ListStore must be UTF-8
418
# 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
428
self._files_store = liststore
429
self._treeview_files.set_model(liststore)
430
crt = Gtk.CellRendererToggle()
431
crt.set_property('activatable', not bool(self._pending))
432
crt.connect("toggled", self._toggle_commit, self._files_store)
434
name = _i18n('Commit*')
436
name = _i18n('Commit')
437
commit_col = Gtk.TreeViewColumn(name, crt, active=2)
438
commit_col.set_visible(False)
439
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))
444
self._treeview_files.connect('cursor-changed',
445
self._on_treeview_files_cursor_changed)
447
def _toggle_commit(self, cell, path, model):
448
if model[path][0] is None: # No file_id means 'All Files'
449
new_val = not model[path][2]
453
model[path][2] = not model[path][2]
455
def _toggle_commit_selection(self, button):
456
all_files = self._commit_all_files_radio.get_active()
457
if self._commit_all_changes != all_files:
458
checked_col = self._treeview_files.get_column(0)
459
self._commit_all_changes = all_files
461
checked_col.set_visible(False)
463
checked_col.set_visible(True)
464
renderer = checked_col.get_cell_renderers()[0]
465
renderer.set_property('activatable', not all_files)
467
def _construct_pending_list(self):
468
# Pending information defaults to hidden, we put it all in 1 box, so
469
# that we can show/hide all of them at once
470
self._pending_box = Gtk.VBox()
471
self._pending_box.hide()
473
pending_message = Gtk.Label()
474
pending_message.set_markup(
475
_i18n('<i>* Cannot select specific files when merging</i>'))
476
self._pending_box.pack_start(pending_message, expand=False, padding=5)
477
pending_message.show()
479
pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
480
self._pending_box.pack_start(pending_label, expand=False, padding=0)
483
scroller = Gtk.ScrolledWindow()
484
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
485
self._treeview_pending = Gtk.TreeView()
486
scroller.add(self._treeview_pending)
487
scroller.set_shadow_type(Gtk.ShadowType.IN)
489
self._pending_box.pack_start(scroller,
490
expand=True, fill=True, padding=5)
491
self._treeview_pending.show()
492
self._left_pane_box.pack_start(self._pending_box, True, True, 0)
494
liststore = Gtk.ListStore(GObject.TYPE_STRING, # revision_id
495
GObject.TYPE_STRING, # date
496
GObject.TYPE_STRING, # committer
497
GObject.TYPE_STRING, # summary
499
self._pending_store = liststore
500
self._treeview_pending.set_model(liststore)
501
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Date'),
502
Gtk.CellRendererText(), text=1))
503
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Committer'),
504
Gtk.CellRendererText(), text=2))
505
self._treeview_pending.append_column(Gtk.TreeViewColumn(_i18n('Summary'),
506
Gtk.CellRendererText(), text=3))
508
def _construct_diff_view(self):
509
from bzrlib.plugins.gtk.diff import DiffView
511
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
512
# decide that we really don't ever want to display it, we should
513
# actually remove it, and other references to it, along with the
514
# tests that it is set properly.
515
self._diff_label = Gtk.Label(label=_i18n('Diff for whole tree'))
516
self._diff_label.set_alignment(0, 0)
517
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
518
self._add_to_right_table(self._diff_label, 1, False)
519
# self._diff_label.show()
521
self._diff_view = DiffView()
522
self._add_to_right_table(self._diff_view, 4, True)
523
self._diff_view.show()
525
def _construct_file_message(self):
526
scroller = Gtk.ScrolledWindow()
527
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
529
self._file_message_text_view = Gtk.TextView()
530
scroller.add(self._file_message_text_view)
531
scroller.set_shadow_type(Gtk.ShadowType.IN)
534
self._file_message_text_view.modify_font(Pango.FontDescription("Monospace"))
535
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
536
self._file_message_text_view.set_accepts_tab(False)
537
self._file_message_text_view.show()
539
self._file_message_expander = Gtk.Expander(_i18n('File commit message'))
540
self._file_message_expander.set_expanded(True)
541
self._file_message_expander.add(scroller)
542
self._add_to_right_table(self._file_message_expander, 1, False)
543
self._file_message_expander.show()
545
def _construct_global_message(self):
546
self._global_message_label = Gtk.Label(label=_i18n('Global Commit Message'))
547
self._global_message_label.set_markup(
548
_i18n('<b>Global Commit Message</b>'))
549
self._global_message_label.set_alignment(0, 0)
550
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
551
self._add_to_right_table(self._global_message_label, 1, False)
552
# Can we remove the spacing between the label and the box?
553
self._global_message_label.show()
555
scroller = Gtk.ScrolledWindow()
556
scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.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"))
561
scroller.add(self._global_message_text_view)
562
scroller.set_shadow_type(Gtk.ShadowType.IN)
564
self._add_to_right_table(scroller, 2, True)
565
self._file_message_text_view.set_wrap_mode(Gtk.WrapMode.WORD)
566
self._file_message_text_view.set_accepts_tab(False)
567
self._global_message_text_view.show()
569
def _on_treeview_files_cursor_changed(self, treeview):
570
treeselection = treeview.get_selection()
571
(model, selection) = treeselection.get_selected()
573
if selection is not None:
574
path, display_path = model.get(selection, 1, 3)
575
self._diff_label.set_text(_i18n('Diff for ') + display_path)
577
self._diff_view.show_diff(None)
579
self._diff_view.show_diff([path.decode('UTF-8')])
580
self._update_per_file_info(selection)
582
def _on_accel_next(self, accel_group, window, keyval, modifier):
583
# We don't really care about any of the parameters, because we know
584
# where this message came from
585
tree_selection = self._treeview_files.get_selection()
586
(model, selection) = tree_selection.get_selected()
587
if selection is None:
590
next = model.iter_next(selection)
593
# We have either made it to the end of the list, or nothing was
594
# selected. Either way, select All Files, and jump to the global
596
self._treeview_files.set_cursor((0,))
597
self._global_message_text_view.grab_focus()
599
# Set the cursor to this entry, and jump to the per-file commit
601
self._treeview_files.set_cursor(model.get_path(next))
602
self._file_message_text_view.grab_focus()
604
def _save_current_file_message(self):
605
if self._last_selected_file is None:
606
return # Nothing to save
607
text_buffer = self._file_message_text_view.get_buffer()
608
cur_text = text_buffer.get_text(text_buffer.get_start_iter(),
609
text_buffer.get_end_iter())
610
last_selected = self._files_store.get_iter(self._last_selected_file)
611
self._files_store.set_value(last_selected, 5, cur_text)
613
def _update_per_file_info(self, selection):
614
# The node is changing, so cache the current message
615
if not self._enable_per_file_commits:
618
self._save_current_file_message()
619
text_buffer = self._file_message_text_view.get_buffer()
620
file_id, display_path, message = self._files_store.get(selection, 0, 3, 5)
621
if file_id is None: # Whole tree
622
self._file_message_expander.set_label(_i18n('File commit message'))
623
self._file_message_expander.set_expanded(False)
624
self._file_message_expander.set_sensitive(False)
625
text_buffer.set_text('')
626
self._last_selected_file = None
628
self._file_message_expander.set_label(_i18n('Commit message for ')
630
self._file_message_expander.set_expanded(True)
631
self._file_message_expander.set_sensitive(True)
632
text_buffer.set_text(message)
633
self._last_selected_file = self._files_store.get_path(selection)
635
def _get_specific_files(self):
636
"""Return the list of selected paths, and file info.
638
:return: ([unicode paths], [{utf-8 file info}]
640
self._save_current_file_message()
642
records = iter(self._files_store)
643
rec = records.next() # Skip the All Files record
644
assert rec[0] is None, "Are we skipping the wrong record?"
647
for record in records:
648
if self._commit_all_changes or record[2]:# [2] checkbox
649
file_id = record[0] # [0] file_id
650
path = record[1] # [1] real path
652
file_message = _sanitize_and_decode_message(record[5])
653
files.append(path.decode('UTF-8'))
654
if self._enable_per_file_commits and file_message:
655
# All of this needs to be utf-8 information
656
file_message = file_message.encode('UTF-8')
657
file_info.append({'path':path, 'file_id':file_id,
658
'message':file_message})
659
file_info.sort(key=lambda x:(x['path'], x['file_id']))
660
if self._enable_per_file_commits:
661
return files, file_info
666
def _on_cancel_clicked(self, button):
667
""" Cancel button clicked handler. """
671
def _on_delete_window(self, source, event):
672
""" Delete window handler. """
675
def _do_cancel(self):
676
"""If requested, saves commit messages when cancelling gcommit; they are re-used by a next gcommit"""
677
mgr = SavedCommitMessagesManager()
678
self._saved_commit_messages_manager = mgr
679
mgr.insert(self._get_global_commit_message(),
680
self._get_specific_files()[1])
681
if mgr.is_not_empty(): # maybe worth saving
682
response = self._question_dialog(
683
_i18n('Commit cancelled'),
684
_i18n('Do you want to save your commit messages ?'),
686
if response == Gtk.ResponseType.NO:
687
# save nothing and destroy old comments if any
688
mgr = SavedCommitMessagesManager()
689
mgr.save(self._wt, self._wt.branch)
690
self.response(Gtk.ResponseType.CANCEL) # close window
693
def _on_commit_clicked(self, button):
694
""" Commit button clicked handler. """
697
def _do_commit(self):
698
message = self._get_global_commit_message()
701
response = self._question_dialog(
702
_i18n('Commit with an empty message?'),
703
_i18n('You can describe your commit intent in the message.'),
705
if response == Gtk.ResponseType.NO:
706
# Kindly give focus to message area
707
self._global_message_text_view.grab_focus()
710
specific_files, file_info = self._get_specific_files()
712
specific_files = None
714
local = self._check_local.get_active()
716
# All we care about is if there is a single unknown, so if this loop is
717
# entered, then there are unknown files.
718
# TODO: jam 20071002 It seems like this should cancel the dialog
719
# entirely, since there isn't a way for them to add the unknown
720
# files at this point.
721
for path in self._wt.unknowns():
722
response = self._question_dialog(
723
_i18n("Commit with unknowns?"),
724
_i18n("Unknown files exist in the working tree. Commit anyway?"),
726
# Doesn't set a parent for the dialog..
727
if response == Gtk.ResponseType.NO:
734
revprops['file-info'] = bencode.bencode(file_info).decode('UTF-8')
736
rev_id = self._wt.commit(message,
737
allow_pointless=False,
740
specific_files=specific_files,
742
except errors.PointlessCommit:
743
response = self._question_dialog(
744
_i18n('Commit with no changes?'),
745
_i18n('There are no changes in the working tree.'
746
' Do you want to commit anyway?'),
748
if response == Gtk.ResponseType.YES:
749
rev_id = self._wt.commit(message,
750
allow_pointless=True,
753
specific_files=specific_files,
755
self.committed_revision_id = rev_id
756
# destroy old comments if any
757
SavedCommitMessagesManager().save(self._wt, self._wt.branch)
758
self.response(Gtk.ResponseType.OK)
760
def _get_global_commit_message(self):
761
buf = self._global_message_text_view.get_buffer()
762
start, end = buf.get_bounds()
763
text = buf.get_text(start, end)
764
return _sanitize_and_decode_message(text)
766
def _set_global_commit_message(self, message):
767
"""Just a helper for the test suite."""
768
if isinstance(message, unicode):
769
message = message.encode('UTF-8')
770
self._global_message_text_view.get_buffer().set_text(message)
772
def _set_file_commit_message(self, message):
773
"""Helper for the test suite."""
774
if isinstance(message, unicode):
775
message = message.encode('UTF-8')
776
self._file_message_text_view.get_buffer().set_text(message)
779
def _rev_to_pending_info(rev):
780
"""Get the information from a pending merge."""
781
from bzrlib.osutils import format_date
783
rev_dict['committer'] = re.sub('<.*@.*>', '', rev.committer).strip(' ')
784
rev_dict['summary'] = rev.get_summary()
785
rev_dict['date'] = format_date(rev.timestamp,
787
'original', date_fmt="%Y-%m-%d",
789
rev_dict['revision_id'] = rev.revision_id
793
class SavedCommitMessagesManager:
794
"""Save glogal and per-file commit messages.
796
Saves global commit message and utf-8 file_id->message dictionary
797
of per-file commit messages on disk. Re-reads them later for re-using.
800
def __init__(self, tree=None, branch=None):
801
"""If branch is None, builds empty messages, otherwise reads them
802
from branch's disk storage. 'tree' argument is for the future."""
804
self.global_message = u''
805
self.file_messages = {}
807
config = branch.get_config()
808
self.global_message = config.get_user_option(
809
'gtk_global_commit_message')
810
if self.global_message is None:
811
self.global_message = u''
812
file_messages = config.get_user_option('gtk_file_commit_messages')
813
if file_messages: # unicode and B-encoded:
814
self.file_messages = bencode.bdecode(
815
file_messages.encode('UTF-8'))
817
self.file_messages = {}
820
return self.global_message, self.file_messages
822
def is_not_empty(self):
823
return bool(self.global_message or self.file_messages)
825
def insert(self, global_message, file_info):
826
"""Formats per-file commit messages (list of dictionaries, one per file)
827
into one utf-8 file_id->message dictionary and merges this with
828
previously existing dictionary. Merges global commit message too."""
831
file_message = fi['message']
833
file_messages[fi['file_id']] = file_message # utf-8 strings
834
for k,v in file_messages.iteritems():
836
self.file_messages[k] = v + '\n******\n' + self.file_messages[k]
838
self.file_messages[k] = v
839
if self.global_message:
840
self.global_message = global_message + '\n******\n' \
841
+ self.global_message
843
self.global_message = global_message
845
def save(self, tree, branch):
846
# We store in branch's config, which can be a problem if two gcommit
847
# are done in two checkouts of one single branch (comments overwrite
848
# each other). Ideally should be in working tree. But uncommit does
849
# not always have a working tree, though it always has a branch.
850
# 'tree' argument is for the future
851
config = branch.get_config()
852
# should it be named "gtk_" or some more neutral name ("gui_" ?) to
853
# be compatible with qbzr in the future?
854
config.set_user_option('gtk_global_commit_message', self.global_message)
855
# bencode() does not know unicode objects but set_user_option()
857
config.set_user_option(
858
'gtk_file_commit_messages',
859
bencode.bencode(self.file_messages).decode('UTF-8'))
862
def save_commit_messages(local, master, old_revno, old_revid,
863
new_revno, new_revid):
867
mgr = SavedCommitMessagesManager(None, b)
868
revid_iterator = b.repository.iter_reverse_revision_history(old_revid)
869
cur_revno = old_revno
870
new_revision_id = old_revid
871
graph = b.repository.get_graph()
872
for rev_id in revid_iterator:
873
if cur_revno == new_revno:
876
rev = b.repository.get_revision(rev_id)
877
file_info = rev.properties.get('file-info', None)
878
if file_info is None:
881
file_info = bencode.bdecode(file_info.encode('UTF-8'))
882
global_message = osutils.safe_unicode(rev.message)
883
# Concatenate comment of the uncommitted revision
884
mgr.insert(global_message, file_info)
886
parents = graph.get_parent_map([rev_id]).get(rev_id, None)