29
import bzrlib.errors as errors
30
from bzrlib import osutils
30
from bzrlib import errors, osutils
31
from bzrlib.trace import mutter
32
from bzrlib.util import bencode
32
34
from dialog import error_dialog, question_dialog
33
35
from errors import show_bzr_error
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
35
99
class CommitDialog(gtk.Dialog):
36
""" New implementation of the Commit dialog. """
37
def __init__(self, wt, wtpath, notbranch, selected=None, parent=None):
38
""" Initialize the Commit Dialog. """
100
"""Implementation of Commit."""
102
def __init__(self, wt, selected=None, parent=None):
39
103
gtk.Dialog.__init__(self, title="Commit - Olive",
42
106
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
47
self.notbranch = notbranch
48
self.selected = selected
51
self.old_tree = self.wt.branch.repository.revision_tree(self.wt.branch.last_revision())
52
self.delta = self.wt.changes_from(self.old_tree)
55
self.pending = self._pending_merges(self.wt)
57
# Do some preliminary checks
58
self._is_checkout = False
59
self._is_pending = False
60
if self.wt is None and not self.notbranch:
61
error_dialog(_('Directory does not have a working tree'),
62
_('Operation aborted.'))
67
error_dialog(_('Directory is not a branch'),
68
_('You can perform this action only in a branch.'))
107
self._question_dialog = question_dialog
110
# TODO: Do something with this value, it is used by Olive
111
# It used to set all changes but this one to False
112
self._selected = selected
113
self._enable_per_file_commits = True
114
self._commit_all_changes = True
115
self.committed_revision_id = None # Nothing has been committed yet
121
def setup_params(self):
122
"""Setup the member variables for state."""
123
self._basis_tree = self._wt.basis_tree()
125
self._pending = pending_revisions(self._wt)
127
self._is_checkout = (self._wt.branch.get_bound_location() is not None)
129
def fill_in_data(self):
130
# Now that we are built, handle changes to the view based on the state
131
self._fill_in_pending()
133
self._fill_in_files()
134
self._fill_in_checkout()
135
self._fill_in_per_file_info()
137
def _fill_in_pending(self):
138
if not self._pending:
139
self._pending_box.hide()
142
# TODO: We'd really prefer this to be a nested list
143
for rev, children in self._pending:
144
rev_info = self._rev_to_pending_info(rev)
145
self._pending_store.append([
146
rev_info['revision_id'],
148
rev_info['committer'],
151
for child in children:
152
rev_info = self._rev_to_pending_info(child)
153
self._pending_store.append([
154
rev_info['revision_id'],
156
rev_info['committer'],
159
self._pending_box.show()
161
def _fill_in_files(self):
162
# We should really use add a progress bar of some kind.
163
# While we fill in the view, hide the store
164
store = self._files_store
165
self._treeview_files.set_model(None)
168
removed = _('removed')
169
renamed = _('renamed')
170
renamed_and_modified = _('renamed and modified')
171
modified = _('modified')
172
kind_changed = _('kind changed')
175
# [file_id, real path, checkbox, display path, changes type, message]
176
# iter_changes returns:
177
# (file_id, (path_in_source, path_in_target),
178
# changed_content, versioned, parent, name, kind,
181
all_enabled = (self._selected is None)
182
# The first entry is always the 'whole tree'
183
all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
184
initial_cursor = store.get_path(all_iter)
185
# should we pass specific_files?
187
self._basis_tree.lock_read()
189
from diff import iter_changes_to_status
190
for (file_id, real_path, change_type, display_path
191
) in iter_changes_to_status(self._basis_tree, self._wt):
192
if self._selected and real_path != self._selected:
196
item_iter = store.append([
198
real_path.encode('UTF-8'),
200
display_path.encode('UTF-8'),
202
'', # Initial comment
204
if self._selected and enabled:
205
initial_cursor = store.get_path(item_iter)
207
self._basis_tree.unlock()
210
self._treeview_files.set_model(store)
211
self._last_selected_file = None
212
# This sets the cursor, which causes the expander to close, which
213
# causes the _file_message_text_view to never get realized. So we have
214
# to give it a little kick, or it warns when we try to grab the focus
215
self._treeview_files.set_cursor(initial_cursor)
217
def _realize_file_message_tree_view(*args):
218
self._file_message_text_view.realize()
219
self.connect_after('realize', _realize_file_message_tree_view)
221
def _fill_in_diff(self):
222
self._diff_view.set_trees(self._wt, self._basis_tree)
224
def _fill_in_checkout(self):
225
if not self._is_checkout:
226
self._check_local.hide()
229
bus = dbus.SystemBus()
231
proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
232
'/org/freedesktop/NetworkManager')
233
except dbus.DBusException:
234
mutter("networkmanager not available.")
235
self._check_local.show()
238
dbus_iface = dbus.Interface(proxy_obj,
239
'org.freedesktop.NetworkManager')
241
# 3 is the enum value for STATE_CONNECTED
242
self._check_local.set_active(dbus_iface.state() != 3)
243
except dbus.DBusException, e:
244
# Silently drop errors. While DBus may be
245
# available, NetworkManager doesn't necessarily have to be
246
mutter("unable to get networkmanager state: %r" % e)
247
self._check_local.show()
249
def _fill_in_per_file_info(self):
250
config = self._wt.branch.get_config()
251
enable_per_file_commits = config.get_user_option('per_file_commits')
252
if (enable_per_file_commits is None
253
or enable_per_file_commits.lower()
254
not in ('y', 'yes', 'on', 'enable', '1', 't', 'true')):
255
self._enable_per_file_commits = False
72
if self.wt.branch.get_bound_location() is not None:
73
# we have a checkout, so the local commit checkbox must appear
74
self._is_checkout = True
77
# There are pending merges, file selection not supported
78
self._is_pending = True
257
self._enable_per_file_commits = True
258
if not self._enable_per_file_commits:
259
self._file_message_expander.hide()
260
self._global_message_label.set_markup(_('<b>Commit Message</b>'))
262
def _compute_delta(self):
263
self._delta = self._wt.changes_from(self._basis_tree)
266
"""Build up the dialog widgets."""
267
# The primary pane which splits it into left and right (adjustable)
269
self._hpane = gtk.HPaned()
271
self._construct_left_pane()
272
self._construct_right_pane()
273
self._construct_action_pane()
275
self.vbox.pack_start(self._hpane)
277
self.set_focus(self._global_message_text_view)
279
self._construct_accelerators()
282
def _set_sizes(self):
283
# This seems like a reasonable default, we might like it to
284
# be a bit wider, so that by default we can fit an 80-line diff in the
286
# Alternatively, we should be saving the last position/size rather than
287
# setting it to a fixed value every time we start up.
288
screen = self.get_screen()
289
monitor = 0 # We would like it to be the monitor we are going to
290
# display on, but I don't know how to figure that out
291
# Only really useful for freaks like me that run dual
292
# monitor, with different sizes on the monitors
293
monitor_rect = screen.get_monitor_geometry(monitor)
294
width = int(monitor_rect.width * 0.66)
295
height = int(monitor_rect.height * 0.66)
296
self.set_default_size(width, height)
297
self._hpane.set_position(300)
299
def _construct_accelerators(self):
300
group = gtk.AccelGroup()
301
group.connect_group(gtk.gdk.keyval_from_name('N'),
302
gtk.gdk.CONTROL_MASK, 0, self._on_accel_next)
303
self.add_accel_group(group)
305
def _construct_left_pane(self):
306
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
307
self._construct_file_list()
308
self._construct_pending_list()
310
self._check_local = gtk.CheckButton(_("_Only commit locally"),
312
self._left_pane_box.pack_end(self._check_local, False, False)
313
self._check_local.set_active(False)
315
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
316
self._left_pane_box.show()
318
def _construct_right_pane(self):
319
# TODO: I really want to make it so the diff view gets more space than
320
# the global commit message, and the per-file commit message gets even
321
# less. When I did it with wxGlade, I set it to 4 for diff, 2 for
322
# commit, and 1 for file commit, and it looked good. But I don't seem
323
# to have a way to do that with the gtk boxes... :( (Which is extra
324
# weird since wx uses gtk on Linux...)
325
self._right_pane_table = gtk.Table(rows=10, columns=1, homogeneous=False)
326
self._right_pane_table.set_row_spacings(5)
327
self._right_pane_table.set_col_spacings(5)
328
self._right_pane_table_row = 0
329
self._construct_diff_view()
330
self._construct_file_message()
331
self._construct_global_message()
333
self._right_pane_table.show()
334
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
336
def _construct_action_pane(self):
81
337
self._button_commit = gtk.Button(_("Comm_it"), use_underline=True)
83
self._check_local = gtk.CheckButton(_("_Only commit locally"),
85
self._check_strict = gtk.CheckButton(_("_Allow unknown files"),
87
self._expander_files = gtk.Expander(_("File(s) to commit"))
88
self._vpaned_main = gtk.VPaned()
89
self._scrolledwindow_files = gtk.ScrolledWindow()
90
self._scrolledwindow_message = gtk.ScrolledWindow()
91
self._treeview_files = gtk.TreeView()
92
self._vbox_message = gtk.VBox()
93
self._label_message = gtk.Label(_("Commit message:"))
94
self._textview_message = gtk.TextView()
97
self._expander_merges = gtk.Expander(_("Pending merges"))
98
self._vpaned_list = gtk.VPaned()
99
self._scrolledwindow_merges = gtk.ScrolledWindow()
100
self._treeview_merges = gtk.TreeView()
103
338
self._button_commit.connect('clicked', self._on_commit_clicked)
104
self._treeview_files.connect('row_activated', self._on_treeview_files_row_activated)
107
self._scrolledwindow_files.set_policy(gtk.POLICY_AUTOMATIC,
108
gtk.POLICY_AUTOMATIC)
109
self._scrolledwindow_message.set_policy(gtk.POLICY_AUTOMATIC,
110
gtk.POLICY_AUTOMATIC)
111
self._textview_message.modify_font(pango.FontDescription("Monospace"))
112
self.set_default_size(500, 500)
113
self._vpaned_main.set_position(200)
116
self._scrolledwindow_merges.set_policy(gtk.POLICY_AUTOMATIC,
117
gtk.POLICY_AUTOMATIC)
118
self._treeview_files.set_sensitive(False)
120
# Construct the dialog
339
self._button_commit.set_flags(gtk.CAN_DEFAULT)
340
self._button_commit.show()
121
341
self.action_area.pack_end(self._button_commit)
123
self._scrolledwindow_files.add(self._treeview_files)
124
self._scrolledwindow_message.add(self._textview_message)
126
self._expander_files.add(self._scrolledwindow_files)
128
self._vbox_message.pack_start(self._label_message, False, False)
129
self._vbox_message.pack_start(self._scrolledwindow_message, True, True)
132
self._expander_merges.add(self._scrolledwindow_merges)
133
self._scrolledwindow_merges.add(self._treeview_merges)
134
self._vpaned_list.add1(self._expander_files)
135
self._vpaned_list.add2(self._expander_merges)
136
self._vpaned_main.add1(self._vpaned_list)
138
self._vpaned_main.add1(self._expander_files)
140
self._vpaned_main.add2(self._vbox_message)
142
self.vbox.pack_start(self._vpaned_main, True, True)
143
if self._is_checkout:
144
self.vbox.pack_start(self._check_local, False, False)
145
self.vbox.pack_start(self._check_strict, False, False)
147
# Create the file list
148
self._create_file_view()
149
# Create the pending merges
150
self._create_pending_merges()
152
# Expand the corresponding expander
154
self._expander_merges.set_expanded(True)
156
self._expander_files.set_expanded(True)
161
def _on_treeview_files_row_activated(self, treeview, path, view_column):
162
# FIXME: the diff window freezes for some reason
342
self._button_commit.grab_default()
344
def _add_to_right_table(self, widget, weight, expanding=False):
345
"""Add another widget to the table
347
:param widget: The object to add
348
:param weight: How many rows does this widget get to request
349
:param expanding: Should expand|fill|shrink be set?
351
end_row = self._right_pane_table_row + weight
353
expand_opts = gtk.EXPAND | gtk.FILL | gtk.SHRINK
355
options = expand_opts
356
self._right_pane_table.attach(widget, 0, 1,
357
self._right_pane_table_row, end_row,
358
xoptions=expand_opts, yoptions=options)
359
self._right_pane_table_row = end_row
361
def _construct_file_list(self):
362
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
363
file_label = gtk.Label(_('Files'))
365
self._files_box.pack_start(file_label, expand=False)
367
self._commit_all_files_radio = gtk.RadioButton(
368
None, _("Commit all changes"))
369
self._files_box.pack_start(self._commit_all_files_radio, expand=False)
370
self._commit_all_files_radio.show()
371
self._commit_all_files_radio.connect('toggled',
372
self._toggle_commit_selection)
373
self._commit_selected_radio = gtk.RadioButton(
374
self._commit_all_files_radio, _("Only commit selected changes"))
375
self._files_box.pack_start(self._commit_selected_radio, expand=False)
376
self._commit_selected_radio.show()
377
self._commit_selected_radio.connect('toggled',
378
self._toggle_commit_selection)
380
self._commit_all_files_radio.set_label(_('Commit all changes*'))
381
self._commit_all_files_radio.set_sensitive(False)
382
self._commit_selected_radio.set_sensitive(False)
384
scroller = gtk.ScrolledWindow()
385
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
386
self._treeview_files = gtk.TreeView()
387
self._treeview_files.show()
388
scroller.add(self._treeview_files)
389
scroller.set_shadow_type(gtk.SHADOW_IN)
391
self._files_box.pack_start(scroller,
392
expand=True, fill=True)
393
self._files_box.show()
394
self._left_pane_box.pack_start(self._files_box)
396
# Keep note that all strings stored in a ListStore must be UTF-8
397
# strings. GTK does not support directly setting and restoring Unicode
399
liststore = gtk.ListStore(
400
gobject.TYPE_STRING, # [0] file_id
401
gobject.TYPE_STRING, # [1] real path
402
gobject.TYPE_BOOLEAN, # [2] checkbox
403
gobject.TYPE_STRING, # [3] display path
404
gobject.TYPE_STRING, # [4] changes type
405
gobject.TYPE_STRING, # [5] commit message
407
self._files_store = liststore
408
self._treeview_files.set_model(liststore)
409
crt = gtk.CellRendererToggle()
410
crt.set_property('activatable', not bool(self._pending))
411
crt.connect("toggled", self._toggle_commit, self._files_store)
416
commit_col = gtk.TreeViewColumn(name, crt, active=2)
417
commit_col.set_visible(False)
418
self._treeview_files.append_column(commit_col)
419
self._treeview_files.append_column(gtk.TreeViewColumn(_('Path'),
420
gtk.CellRendererText(), text=3))
421
self._treeview_files.append_column(gtk.TreeViewColumn(_('Type'),
422
gtk.CellRendererText(), text=4))
423
self._treeview_files.connect('cursor-changed',
424
self._on_treeview_files_cursor_changed)
426
def _toggle_commit(self, cell, path, model):
427
if model[path][0] is None: # No file_id means 'All Files'
428
new_val = not model[path][2]
432
model[path][2] = not model[path][2]
434
def _toggle_commit_selection(self, button):
435
all_files = self._commit_all_files_radio.get_active()
436
if self._commit_all_changes != all_files:
437
checked_col = self._treeview_files.get_column(0)
438
self._commit_all_changes = all_files
440
checked_col.set_visible(False)
442
checked_col.set_visible(True)
443
renderer = checked_col.get_cell_renderers()[0]
444
renderer.set_property('activatable', not all_files)
446
def _construct_pending_list(self):
447
# Pending information defaults to hidden, we put it all in 1 box, so
448
# that we can show/hide all of them at once
449
self._pending_box = gtk.VBox()
450
self._pending_box.hide()
452
pending_message = gtk.Label()
453
pending_message.set_markup(
454
_('<i>* Cannot select specific files when merging</i>'))
455
self._pending_box.pack_start(pending_message, expand=False, padding=5)
456
pending_message.show()
458
pending_label = gtk.Label(_('Pending Revisions'))
459
self._pending_box.pack_start(pending_label, expand=False, padding=0)
462
scroller = gtk.ScrolledWindow()
463
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
464
self._treeview_pending = gtk.TreeView()
465
scroller.add(self._treeview_pending)
466
scroller.set_shadow_type(gtk.SHADOW_IN)
468
self._pending_box.pack_start(scroller,
469
expand=True, fill=True, padding=5)
470
self._treeview_pending.show()
471
self._left_pane_box.pack_start(self._pending_box)
473
liststore = gtk.ListStore(gobject.TYPE_STRING, # revision_id
474
gobject.TYPE_STRING, # date
475
gobject.TYPE_STRING, # committer
476
gobject.TYPE_STRING, # summary
478
self._pending_store = liststore
479
self._treeview_pending.set_model(liststore)
480
self._treeview_pending.append_column(gtk.TreeViewColumn(_('Date'),
481
gtk.CellRendererText(), text=1))
482
self._treeview_pending.append_column(gtk.TreeViewColumn(_('Committer'),
483
gtk.CellRendererText(), text=2))
484
self._treeview_pending.append_column(gtk.TreeViewColumn(_('Summary'),
485
gtk.CellRendererText(), text=3))
487
def _construct_diff_view(self):
488
from diff import DiffView
490
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
491
# decide that we really don't ever want to display it, we should
492
# actually remove it, and other references to it, along with the
493
# tests that it is set properly.
494
self._diff_label = gtk.Label(_('Diff for whole tree'))
495
self._diff_label.set_alignment(0, 0)
496
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
497
self._add_to_right_table(self._diff_label, 1, False)
498
# self._diff_label.show()
500
self._diff_view = DiffView()
501
self._add_to_right_table(self._diff_view, 4, True)
502
self._diff_view.show()
504
def _construct_file_message(self):
505
scroller = gtk.ScrolledWindow()
506
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
508
self._file_message_text_view = gtk.TextView()
509
scroller.add(self._file_message_text_view)
510
scroller.set_shadow_type(gtk.SHADOW_IN)
513
self._file_message_text_view.modify_font(pango.FontDescription("Monospace"))
514
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
515
self._file_message_text_view.set_accepts_tab(False)
516
self._file_message_text_view.show()
518
self._file_message_expander = gtk.Expander(_('File commit message'))
519
self._file_message_expander.set_expanded(True)
520
self._file_message_expander.add(scroller)
521
self._add_to_right_table(self._file_message_expander, 1, False)
522
self._file_message_expander.show()
524
def _construct_global_message(self):
525
self._global_message_label = gtk.Label(_('Global Commit Message'))
526
self._global_message_label.set_markup(_('<b>Global Commit Message</b>'))
527
self._global_message_label.set_alignment(0, 0)
528
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
529
self._add_to_right_table(self._global_message_label, 1, False)
530
# Can we remove the spacing between the label and the box?
531
self._global_message_label.show()
533
scroller = gtk.ScrolledWindow()
534
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
536
self._global_message_text_view = gtk.TextView()
537
self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
538
scroller.add(self._global_message_text_view)
539
scroller.set_shadow_type(gtk.SHADOW_IN)
541
self._add_to_right_table(scroller, 2, True)
542
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
543
self._file_message_text_view.set_accepts_tab(False)
544
self._global_message_text_view.show()
546
def _on_treeview_files_cursor_changed(self, treeview):
163
547
treeselection = treeview.get_selection()
164
(model, iter) = treeselection.get_selected()
167
from diff import DiffWindow
169
_selected = model.get_value(iter, 1)
172
parent_tree = self.wt.branch.repository.revision_tree(self.wt.branch.last_revision())
173
diff.set_diff(self.wt.branch.nick, self.wt, parent_tree)
175
diff.set_file(_selected)
176
except errors.NoSuchFile:
548
(model, selection) = treeselection.get_selected()
550
if selection is not None:
551
path, display_path = model.get(selection, 1, 3)
552
self._diff_label.set_text(_('Diff for ') + display_path)
554
self._diff_view.show_diff(None)
556
self._diff_view.show_diff([path.decode('UTF-8')])
557
self._update_per_file_info(selection)
559
def _on_accel_next(self, accel_group, window, keyval, modifier):
560
# We don't really care about any of the parameters, because we know
561
# where this message came from
562
tree_selection = self._treeview_files.get_selection()
563
(model, selection) = tree_selection.get_selected()
564
if selection is None:
567
next = model.iter_next(selection)
570
# We have either made it to the end of the list, or nothing was
571
# selected. Either way, select All Files, and jump to the global
573
self._treeview_files.set_cursor((0,))
574
self._global_message_text_view.grab_focus()
576
# Set the cursor to this entry, and jump to the per-file commit
578
self._treeview_files.set_cursor(model.get_path(next))
579
self._file_message_text_view.grab_focus()
581
def _save_current_file_message(self):
582
if self._last_selected_file is None:
583
return # Nothing to save
584
text_buffer = self._file_message_text_view.get_buffer()
585
cur_text = text_buffer.get_text(text_buffer.get_start_iter(),
586
text_buffer.get_end_iter())
587
last_selected = self._files_store.get_iter(self._last_selected_file)
588
self._files_store.set_value(last_selected, 5, cur_text)
590
def _update_per_file_info(self, selection):
591
# The node is changing, so cache the current message
592
if not self._enable_per_file_commits:
595
self._save_current_file_message()
596
text_buffer = self._file_message_text_view.get_buffer()
597
file_id, display_path, message = self._files_store.get(selection, 0, 3, 5)
598
if file_id is None: # Whole tree
599
self._file_message_expander.set_label(_('File commit message'))
600
self._file_message_expander.set_expanded(False)
601
self._file_message_expander.set_sensitive(False)
602
text_buffer.set_text('')
603
self._last_selected_file = None
605
self._file_message_expander.set_label(_('Commit message for ')
607
self._file_message_expander.set_expanded(True)
608
self._file_message_expander.set_sensitive(True)
609
text_buffer.set_text(message)
610
self._last_selected_file = self._files_store.get_path(selection)
612
def _get_specific_files(self):
613
"""Return the list of selected paths, and file info.
615
:return: ([unicode paths], [{utf-8 file info}]
617
self._save_current_file_message()
619
records = iter(self._files_store)
620
rec = records.next() # Skip the All Files record
621
assert rec[0] is None, "Are we skipping the wrong record?"
624
for record in records:
625
if self._commit_all_changes or record[2]:# [2] checkbox
626
file_id = record[0] # [0] file_id
627
path = record[1] # [1] real path
628
file_message = record[5] # [5] commit message
629
files.append(path.decode('UTF-8'))
630
if self._enable_per_file_commits and file_message:
631
# All of this needs to be utf-8 information
632
file_info.append({'path':path, 'file_id':file_id,
633
'message':file_message})
634
file_info.sort(key=lambda x:(x['path'], x['file_id']))
635
if self._enable_per_file_commits:
636
return files, file_info
181
641
def _on_commit_clicked(self, button):
182
642
""" Commit button clicked handler. """
183
textbuffer = self._textview_message.get_buffer()
184
start, end = textbuffer.get_bounds()
185
message = textbuffer.get_text(start, end).decode('utf-8')
188
specific_files = self._get_specific_files()
190
specific_files = None
645
def _do_commit(self):
646
message = self._get_global_commit_message()
192
648
if message == '':
193
response = question_dialog(_('Commit with an empty message?'),
194
_('You can describe your commit intent in the message.'))
649
response = self._question_dialog(
650
_('Commit with an empty message?'),
651
_('You can describe your commit intent in the message.'))
195
652
if response == gtk.RESPONSE_NO:
196
653
# Kindly give focus to message area
197
self._textview_message.grab_focus()
200
if self._is_checkout:
201
local = self._check_local.get_active()
654
self._global_message_text_view.grab_focus()
657
specific_files, file_info = self._get_specific_files()
659
specific_files = None
661
local = self._check_local.get_active()
663
# All we care about is if there is a single unknown, so if this loop is
664
# entered, then there are unknown files.
665
# TODO: jam 20071002 It seems like this should cancel the dialog
666
# entirely, since there isn't a way for them to add the unknown
667
# files at this point.
668
for path in self._wt.unknowns():
669
response = self._question_dialog(
670
_("Commit with unknowns?"),
671
_("Unknown files exist in the working tree. Commit anyway?"))
672
if response == gtk.RESPONSE_NO:
679
revprops['file-info'] = bencode.bencode(file_info).decode('UTF-8')
206
self.wt.commit(message,
681
rev_id = self._wt.commit(message,
207
682
allow_pointless=False,
208
strict=self._check_strict.get_active(),
210
specific_files=specific_files)
685
specific_files=specific_files,
211
687
except errors.PointlessCommit:
212
response = question_dialog(_('Commit with no changes?'),
213
_('There are no changes in the working tree.'))
688
response = self._question_dialog(
689
_('Commit with no changes?'),
690
_('There are no changes in the working tree.'
691
' Do you want to commit anyway?'))
214
692
if response == gtk.RESPONSE_YES:
215
self.wt.commit(message,
693
rev_id = self._wt.commit(message,
216
694
allow_pointless=True,
217
strict=self._check_strict.get_active(),
219
specific_files=specific_files)
697
specific_files=specific_files,
699
self.committed_revision_id = rev_id
220
700
self.response(gtk.RESPONSE_OK)
222
def _pending_merges(self, wt):
223
""" Return a list of pending merges or None if there are none of them. """
224
parents = wt.get_parent_ids()
702
def _get_global_commit_message(self):
703
buf = self._global_message_text_view.get_buffer()
704
start, end = buf.get_bounds()
705
return buf.get_text(start, end).decode('utf-8')
707
def _set_global_commit_message(self, message):
708
"""Just a helper for the test suite."""
709
if isinstance(message, unicode):
710
message = message.encode('UTF-8')
711
self._global_message_text_view.get_buffer().set_text(message)
713
def _set_file_commit_message(self, message):
714
"""Helper for the test suite."""
715
if isinstance(message, unicode):
716
message = message.encode('UTF-8')
717
self._file_message_text_view.get_buffer().set_text(message)
720
def _rev_to_pending_info(rev):
721
"""Get the information from a pending merge."""
229
722
from bzrlib.osutils import format_date
231
pending = parents[1:]
233
last_revision = parents[0]
235
if last_revision is not None:
237
ignore = set(branch.repository.get_ancestry(last_revision))
238
except errors.NoSuchRevision:
239
# the last revision is a ghost : assume everything is new
241
ignore = set([None, last_revision])
246
for merge in pending:
249
m_revision = branch.repository.get_revision(merge)
252
rev['committer'] = re.sub('<.*@.*>', '', m_revision.committer).strip(' ')
253
rev['summary'] = m_revision.get_summary()
254
rev['date'] = format_date(m_revision.timestamp,
255
m_revision.timezone or 0,
256
'original', date_fmt="%Y-%m-%d",
261
inner_merges = branch.repository.get_ancestry(merge)
262
assert inner_merges[0] is None
264
inner_merges.reverse()
265
for mmerge in inner_merges:
268
mm_revision = branch.repository.get_revision(mmerge)
271
rev['committer'] = re.sub('<.*@.*>', '', mm_revision.committer).strip(' ')
272
rev['summary'] = mm_revision.get_summary()
273
rev['date'] = format_date(mm_revision.timestamp,
274
mm_revision.timezone or 0,
275
'original', date_fmt="%Y-%m-%d",
281
except errors.NoSuchRevision:
282
print "DEBUG: NoSuchRevision:", merge
286
def _create_file_view(self):
287
self._file_store = gtk.ListStore(gobject.TYPE_BOOLEAN, # [0] checkbox
288
gobject.TYPE_STRING, # [1] path to display
289
gobject.TYPE_STRING, # [2] changes type
290
gobject.TYPE_STRING) # [3] real path
291
self._treeview_files.set_model(self._file_store)
292
crt = gtk.CellRendererToggle()
293
crt.set_property("activatable", True)
294
crt.connect("toggled", self._toggle_commit, self._file_store)
295
self._treeview_files.append_column(gtk.TreeViewColumn(_('Commit'),
297
self._treeview_files.append_column(gtk.TreeViewColumn(_('Path'),
298
gtk.CellRendererText(), text=1))
299
self._treeview_files.append_column(gtk.TreeViewColumn(_('Type'),
300
gtk.CellRendererText(), text=2))
302
for path, id, kind in self.delta.added:
303
marker = osutils.kind_marker(kind)
304
if self.selected is not None:
305
if path == os.path.join(self.wtpath, self.selected):
306
self._file_store.append([ True, path+marker, _('added'), path ])
308
self._file_store.append([ False, path+marker, _('added'), path ])
310
self._file_store.append([ True, path+marker, _('added'), path ])
312
for path, id, kind in self.delta.removed:
313
marker = osutils.kind_marker(kind)
314
if self.selected is not None:
315
if path == os.path.join(self.wtpath, self.selected):
316
self._file_store.append([ True, path+marker, _('removed'), path ])
318
self._file_store.append([ False, path+marker, _('removed'), path ])
320
self._file_store.append([ True, path+marker, _('removed'), path ])
322
for oldpath, newpath, id, kind, text_modified, meta_modified in self.delta.renamed:
323
marker = osutils.kind_marker(kind)
324
if text_modified or meta_modified:
325
changes = _('renamed and modified')
327
changes = _('renamed')
328
if self.selected is not None:
329
if newpath == os.path.join(self.wtpath, self.selected):
330
self._file_store.append([ True,
331
oldpath+marker + ' => ' + newpath+marker,
336
self._file_store.append([ False,
337
oldpath+marker + ' => ' + newpath+marker,
342
self._file_store.append([ True,
343
oldpath+marker + ' => ' + newpath+marker,
348
for path, id, kind, text_modified, meta_modified in self.delta.modified:
349
marker = osutils.kind_marker(kind)
350
if self.selected is not None:
351
if path == os.path.join(self.wtpath, self.selected):
352
self._file_store.append([ True, path+marker, _('modified'), path ])
354
self._file_store.append([ False, path+marker, _('modified'), path ])
356
self._file_store.append([ True, path+marker, _('modified'), path ])
358
def _create_pending_merges(self):
362
liststore = gtk.ListStore(gobject.TYPE_STRING,
365
self._treeview_merges.set_model(liststore)
367
self._treeview_merges.append_column(gtk.TreeViewColumn(_('Date'),
368
gtk.CellRendererText(), text=0))
369
self._treeview_merges.append_column(gtk.TreeViewColumn(_('Committer'),
370
gtk.CellRendererText(), text=1))
371
self._treeview_merges.append_column(gtk.TreeViewColumn(_('Summary'),
372
gtk.CellRendererText(), text=2))
374
for item in self.pending:
375
liststore.append([ item['date'],
379
def _get_specific_files(self):
381
it = self._file_store.get_iter_first()
383
if self._file_store.get_value(it, 0):
384
# get real path from hidden column 3
385
ret.append(self._file_store.get_value(it, 3))
386
it = self._file_store.iter_next(it)
390
def _toggle_commit(self, cell, path, model):
391
model[path][0] = not model[path][0]
724
rev_dict['committer'] = re.sub('<.*@.*>', '', rev.committer).strip(' ')
725
rev_dict['summary'] = rev.get_summary()
726
rev_dict['date'] = format_date(rev.timestamp,
728
'original', date_fmt="%Y-%m-%d",
730
rev_dict['revision_id'] = rev.revision_id