28
import bzrlib.errors as errors
29
from bzrlib import osutils
31
from dialog import error_dialog
32
from guifiles import GLADEFILENAME
36
""" Display Commit dialog and perform the needed actions. """
37
def __init__(self, wt, wtpath, standalone=False):
38
""" Initialize the Commit dialog.
39
@param wt: bzr working tree object
40
@param wtpath: path to working tree root
41
@param standalone: when used in gcommit command as standalone window
42
this argument should be True
30
from bzrlib import errors, osutils
31
from bzrlib.trace import mutter
32
from bzrlib.util import bencode
34
from bzrlib.plugins.gtk import _i18n
35
from dialog import error_dialog, question_dialog
36
from errors import show_bzr_error
46
def pending_revisions(wt):
47
"""Return a list of pending merges or None if there are none of them.
49
Arguably this should be a core function, and
50
``bzrlib.status.show_pending_merges`` should be built on top of it.
52
:return: [(rev, [children])]
54
parents = wt.get_parent_ids()
58
# The basic pending merge algorithm uses the same algorithm as
59
# bzrlib.status.show_pending_merges
62
last_revision = parents[0]
64
if last_revision is not None:
66
ignore = set(branch.repository.get_ancestry(last_revision,
68
except errors.NoSuchRevision:
69
# the last revision is a ghost : assume everything is new
71
ignore = set([None, last_revision])
79
rev = branch.repository.get_revision(merge)
81
pm.append((rev, children))
83
# This does need to be topo sorted, so we search backwards
84
inner_merges = branch.repository.get_ancestry(merge)
85
assert inner_merges[0] is None
87
for mmerge in reversed(inner_merges):
90
rev = branch.repository.get_revision(mmerge)
94
except errors.NoSuchRevision:
95
print "DEBUG: NoSuchRevision:", merge
100
class CommitDialog(gtk.Dialog):
101
"""Implementation of Commit."""
103
def __init__(self, wt, selected=None, parent=None):
104
gtk.Dialog.__init__(self, title="Commit - Olive",
107
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
108
self._question_dialog = question_dialog
111
# TODO: Do something with this value, it is used by Olive
112
# It used to set all changes but this one to False
113
self._selected = selected
114
self._enable_per_file_commits = True
115
self._commit_all_changes = True
116
self.committed_revision_id = None # Nothing has been committed yet
122
def setup_params(self):
123
"""Setup the member variables for state."""
124
self._basis_tree = self._wt.basis_tree()
126
self._pending = pending_revisions(self._wt)
128
self._is_checkout = (self._wt.branch.get_bound_location() is not None)
130
def fill_in_data(self):
131
# Now that we are built, handle changes to the view based on the state
132
self._fill_in_pending()
134
self._fill_in_files()
135
self._fill_in_checkout()
136
self._fill_in_per_file_info()
138
def _fill_in_pending(self):
139
if not self._pending:
140
self._pending_box.hide()
143
# TODO: We'd really prefer this to be a nested list
144
for rev, children in self._pending:
145
rev_info = self._rev_to_pending_info(rev)
146
self._pending_store.append([
147
rev_info['revision_id'],
149
rev_info['committer'],
152
for child in children:
153
rev_info = self._rev_to_pending_info(child)
154
self._pending_store.append([
155
rev_info['revision_id'],
157
rev_info['committer'],
160
self._pending_box.show()
162
def _fill_in_files(self):
163
# We should really use add a progress bar of some kind.
164
# While we fill in the view, hide the store
165
store = self._files_store
166
self._treeview_files.set_model(None)
168
added = _i18n('added')
169
removed = _i18n('removed')
170
renamed = _i18n('renamed')
171
renamed_and_modified = _i18n('renamed and modified')
172
modified = _i18n('modified')
173
kind_changed = _i18n('kind changed')
176
# [file_id, real path, checkbox, display path, changes type, message]
177
# iter_changes returns:
178
# (file_id, (path_in_source, path_in_target),
179
# changed_content, versioned, parent, name, kind,
182
all_enabled = (self._selected is None)
183
# The first entry is always the 'whole tree'
184
all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
185
initial_cursor = store.get_path(all_iter)
186
# should we pass specific_files?
188
self._basis_tree.lock_read()
190
from diff import iter_changes_to_status
191
for (file_id, real_path, change_type, display_path
192
) in iter_changes_to_status(self._basis_tree, self._wt):
193
if self._selected and real_path != self._selected:
197
item_iter = store.append([
199
real_path.encode('UTF-8'),
201
display_path.encode('UTF-8'),
203
'', # Initial comment
205
if self._selected and enabled:
206
initial_cursor = store.get_path(item_iter)
208
self._basis_tree.unlock()
211
self._treeview_files.set_model(store)
212
self._last_selected_file = None
213
# This sets the cursor, which causes the expander to close, which
214
# causes the _file_message_text_view to never get realized. So we have
215
# to give it a little kick, or it warns when we try to grab the focus
216
self._treeview_files.set_cursor(initial_cursor)
218
def _realize_file_message_tree_view(*args):
219
self._file_message_text_view.realize()
220
self.connect_after('realize', _realize_file_message_tree_view)
222
def _fill_in_diff(self):
223
self._diff_view.set_trees(self._wt, self._basis_tree)
225
def _fill_in_checkout(self):
226
if not self._is_checkout:
227
self._check_local.hide()
230
bus = dbus.SystemBus()
232
proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
233
'/org/freedesktop/NetworkManager')
234
except dbus.DBusException:
235
mutter("networkmanager not available.")
236
self._check_local.show()
239
dbus_iface = dbus.Interface(proxy_obj,
240
'org.freedesktop.NetworkManager')
242
# 3 is the enum value for STATE_CONNECTED
243
self._check_local.set_active(dbus_iface.state() != 3)
244
except dbus.DBusException, e:
245
# Silently drop errors. While DBus may be
246
# available, NetworkManager doesn't necessarily have to be
247
mutter("unable to get networkmanager state: %r" % e)
248
self._check_local.show()
250
def _fill_in_per_file_info(self):
251
config = self._wt.branch.get_config()
252
enable_per_file_commits = config.get_user_option('per_file_commits')
253
if (enable_per_file_commits is None
254
or enable_per_file_commits.lower()
255
not in ('y', 'yes', 'on', 'enable', '1', 't', 'true')):
256
self._enable_per_file_commits = False
258
self._enable_per_file_commits = True
259
if not self._enable_per_file_commits:
260
self._file_message_expander.hide()
261
self._global_message_label.set_markup(_i18n('<b>Commit Message</b>'))
263
def _compute_delta(self):
264
self._delta = self._wt.changes_from(self._basis_tree)
267
"""Build up the dialog widgets."""
268
# The primary pane which splits it into left and right (adjustable)
270
self._hpane = gtk.HPaned()
272
self._construct_left_pane()
273
self._construct_right_pane()
274
self._construct_action_pane()
276
self.vbox.pack_start(self._hpane)
278
self.set_focus(self._global_message_text_view)
280
self._construct_accelerators()
283
def _set_sizes(self):
284
# This seems like a reasonable default, we might like it to
285
# be a bit wider, so that by default we can fit an 80-line diff in the
287
# Alternatively, we should be saving the last position/size rather than
288
# setting it to a fixed value every time we start up.
289
screen = self.get_screen()
290
monitor = 0 # We would like it to be the monitor we are going to
291
# display on, but I don't know how to figure that out
292
# Only really useful for freaks like me that run dual
293
# monitor, with different sizes on the monitors
294
monitor_rect = screen.get_monitor_geometry(monitor)
295
width = int(monitor_rect.width * 0.66)
296
height = int(monitor_rect.height * 0.66)
297
self.set_default_size(width, height)
298
self._hpane.set_position(300)
300
def _construct_accelerators(self):
301
group = gtk.AccelGroup()
302
group.connect_group(gtk.gdk.keyval_from_name('N'),
303
gtk.gdk.CONTROL_MASK, 0, self._on_accel_next)
304
self.add_accel_group(group)
306
# ignore the escape key (avoid closing the window)
307
self.connect_object('close', self.emit_stop_by_name, 'close')
309
def _construct_left_pane(self):
310
self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
311
self._construct_file_list()
312
self._construct_pending_list()
314
self._check_local = gtk.CheckButton(_i18n("_Only commit locally"),
316
self._left_pane_box.pack_end(self._check_local, False, False)
317
self._check_local.set_active(False)
319
self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
320
self._left_pane_box.show()
322
def _construct_right_pane(self):
323
# TODO: I really want to make it so the diff view gets more space than
324
# the global commit message, and the per-file commit message gets even
325
# less. When I did it with wxGlade, I set it to 4 for diff, 2 for
326
# commit, and 1 for file commit, and it looked good. But I don't seem
327
# to have a way to do that with the gtk boxes... :( (Which is extra
328
# weird since wx uses gtk on Linux...)
329
self._right_pane_table = gtk.Table(rows=10, columns=1, homogeneous=False)
330
self._right_pane_table.set_row_spacings(5)
331
self._right_pane_table.set_col_spacings(5)
332
self._right_pane_table_row = 0
333
self._construct_diff_view()
334
self._construct_file_message()
335
self._construct_global_message()
337
self._right_pane_table.show()
338
self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
340
def _construct_action_pane(self):
341
self._button_commit = gtk.Button(_i18n("Comm_it"), use_underline=True)
342
self._button_commit.connect('clicked', self._on_commit_clicked)
343
self._button_commit.set_flags(gtk.CAN_DEFAULT)
344
self._button_commit.show()
345
self.action_area.pack_end(self._button_commit)
346
self._button_commit.grab_default()
348
def _add_to_right_table(self, widget, weight, expanding=False):
349
"""Add another widget to the table
351
:param widget: The object to add
352
:param weight: How many rows does this widget get to request
353
:param expanding: Should expand|fill|shrink be set?
44
self.glade = gtk.glade.XML(GLADEFILENAME, 'window_commit', 'olive-gtk')
49
self.standalone = standalone
51
# Get some important widgets
52
self.window = self.glade.get_widget('window_commit')
53
self.checkbutton_local = self.glade.get_widget('checkbutton_commit_local')
54
self.textview = self.glade.get_widget('textview_commit')
55
self.file_view = self.glade.get_widget('treeview_commit_select')
56
self.pending_label = self.glade.get_widget('label_commit_pending')
57
self.pending_view = self.glade.get_widget('treeview_commit_pending')
59
file_id = self.wt.path2id(wtpath)
61
self.notbranch = False
67
self.old_tree = self.wt.branch.repository.revision_tree(self.wt.branch.last_revision())
68
self.delta = self.wt.changes_from(self.old_tree)
71
self.pending = self._pending_merges(self.wt)
73
# Dictionary for signal_autoconnect
74
dic = { "on_button_commit_commit_clicked": self.commit,
75
"on_button_commit_cancel_clicked": self.close }
78
dic["on_button_commit_cancel_clicked"] = self.quit
79
self.window.connect("delete_event", gtk.main_quit)
81
# Connect the signals to the handlers
82
self.glade.signal_autoconnect(dic)
84
# Create the file list
85
self._create_file_view()
86
# Create the pending merges
87
self._create_pending_merges()
90
""" Display the Push dialog. """
92
error_dialog(_('Directory is not a branch'),
93
_('You can perform this action only in a branch.'))
96
if self.wt.branch.get_bound_location() is not None:
97
# we have a checkout, so the local commit checkbox must appear
98
self.checkbutton_local.show()
101
# There are pending merges, file selection not supported
102
self.file_view.set_sensitive(False)
105
self.pending_view.set_sensitive(False)
107
self.textview.modify_font(pango.FontDescription("Monospace"))
111
def _create_file_view(self):
112
self.file_store = gtk.ListStore(gobject.TYPE_BOOLEAN, # [0] checkbox
113
gobject.TYPE_STRING, # [1] path to display
114
gobject.TYPE_STRING, # [2] changes type
115
gobject.TYPE_STRING) # [3] real path
116
self.file_view.set_model(self.file_store)
355
end_row = self._right_pane_table_row + weight
357
expand_opts = gtk.EXPAND | gtk.FILL | gtk.SHRINK
359
options = expand_opts
360
self._right_pane_table.attach(widget, 0, 1,
361
self._right_pane_table_row, end_row,
362
xoptions=expand_opts, yoptions=options)
363
self._right_pane_table_row = end_row
365
def _construct_file_list(self):
366
self._files_box = gtk.VBox(homogeneous=False, spacing=0)
367
file_label = gtk.Label(_i18n('Files'))
369
self._files_box.pack_start(file_label, expand=False)
371
self._commit_all_files_radio = gtk.RadioButton(
372
None, _i18n("Commit all changes"))
373
self._files_box.pack_start(self._commit_all_files_radio, expand=False)
374
self._commit_all_files_radio.show()
375
self._commit_all_files_radio.connect('toggled',
376
self._toggle_commit_selection)
377
self._commit_selected_radio = gtk.RadioButton(
378
self._commit_all_files_radio, _i18n("Only commit selected changes"))
379
self._files_box.pack_start(self._commit_selected_radio, expand=False)
380
self._commit_selected_radio.show()
381
self._commit_selected_radio.connect('toggled',
382
self._toggle_commit_selection)
384
self._commit_all_files_radio.set_label(_i18n('Commit all changes*'))
385
self._commit_all_files_radio.set_sensitive(False)
386
self._commit_selected_radio.set_sensitive(False)
388
scroller = gtk.ScrolledWindow()
389
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
390
self._treeview_files = gtk.TreeView()
391
self._treeview_files.show()
392
scroller.add(self._treeview_files)
393
scroller.set_shadow_type(gtk.SHADOW_IN)
395
self._files_box.pack_start(scroller,
396
expand=True, fill=True)
397
self._files_box.show()
398
self._left_pane_box.pack_start(self._files_box)
400
# Keep note that all strings stored in a ListStore must be UTF-8
401
# strings. GTK does not support directly setting and restoring Unicode
403
liststore = gtk.ListStore(
404
gobject.TYPE_STRING, # [0] file_id
405
gobject.TYPE_STRING, # [1] real path
406
gobject.TYPE_BOOLEAN, # [2] checkbox
407
gobject.TYPE_STRING, # [3] display path
408
gobject.TYPE_STRING, # [4] changes type
409
gobject.TYPE_STRING, # [5] commit message
411
self._files_store = liststore
412
self._treeview_files.set_model(liststore)
117
413
crt = gtk.CellRendererToggle()
118
crt.set_property("activatable", True)
119
crt.connect("toggled", self._toggle_commit, self.file_store)
120
self.file_view.append_column(gtk.TreeViewColumn(_('Commit'),
122
self.file_view.append_column(gtk.TreeViewColumn(_('Path'),
123
gtk.CellRendererText(), text=1))
124
self.file_view.append_column(gtk.TreeViewColumn(_('Type'),
125
gtk.CellRendererText(), text=2))
127
for path, id, kind in self.delta.added:
128
marker = osutils.kind_marker(kind)
129
self.file_store.append([ True, path+marker, _('added'), path ])
131
for path, id, kind in self.delta.removed:
132
marker = osutils.kind_marker(kind)
133
self.file_store.append([ True, path+marker, _('removed'), path ])
135
for oldpath, newpath, id, kind, text_modified, meta_modified in self.delta.renamed:
136
marker = osutils.kind_marker(kind)
137
if text_modified or meta_modified:
138
changes = _('renamed and modified')
140
changes = _('renamed')
141
self.file_store.append([ True,
142
oldpath+marker + ' => ' + newpath+marker,
147
for path, id, kind, text_modified, meta_modified in self.delta.modified:
148
marker = osutils.kind_marker(kind)
149
self.file_store.append([ True, path+marker, _('modified'), path ])
151
def _create_pending_merges(self):
152
liststore = gtk.ListStore(gobject.TYPE_STRING,
155
self.pending_view.set_model(liststore)
157
self.pending_view.append_column(gtk.TreeViewColumn(_('Date'),
158
gtk.CellRendererText(), text=0))
159
self.pending_view.append_column(gtk.TreeViewColumn(_('Committer'),
160
gtk.CellRendererText(), text=1))
161
self.pending_view.append_column(gtk.TreeViewColumn(_('Summary'),
162
gtk.CellRendererText(), text=2))
414
crt.set_property('activatable', not bool(self._pending))
415
crt.connect("toggled", self._toggle_commit, self._files_store)
417
name = _i18n('Commit*')
419
name = _i18n('Commit')
420
commit_col = gtk.TreeViewColumn(name, crt, active=2)
421
commit_col.set_visible(False)
422
self._treeview_files.append_column(commit_col)
423
self._treeview_files.append_column(gtk.TreeViewColumn(_i18n('Path'),
424
gtk.CellRendererText(), text=3))
425
self._treeview_files.append_column(gtk.TreeViewColumn(_i18n('Type'),
426
gtk.CellRendererText(), text=4))
427
self._treeview_files.connect('cursor-changed',
428
self._on_treeview_files_cursor_changed)
430
def _toggle_commit(self, cell, path, model):
431
if model[path][0] is None: # No file_id means 'All Files'
432
new_val = not model[path][2]
436
model[path][2] = not model[path][2]
438
def _toggle_commit_selection(self, button):
439
all_files = self._commit_all_files_radio.get_active()
440
if self._commit_all_changes != all_files:
441
checked_col = self._treeview_files.get_column(0)
442
self._commit_all_changes = all_files
444
checked_col.set_visible(False)
446
checked_col.set_visible(True)
447
renderer = checked_col.get_cell_renderers()[0]
448
renderer.set_property('activatable', not all_files)
450
def _construct_pending_list(self):
451
# Pending information defaults to hidden, we put it all in 1 box, so
452
# that we can show/hide all of them at once
453
self._pending_box = gtk.VBox()
454
self._pending_box.hide()
456
pending_message = gtk.Label()
457
pending_message.set_markup(
458
_i18n('<i>* Cannot select specific files when merging</i>'))
459
self._pending_box.pack_start(pending_message, expand=False, padding=5)
460
pending_message.show()
462
pending_label = gtk.Label(_i18n('Pending Revisions'))
463
self._pending_box.pack_start(pending_label, expand=False, padding=0)
466
scroller = gtk.ScrolledWindow()
467
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
468
self._treeview_pending = gtk.TreeView()
469
scroller.add(self._treeview_pending)
470
scroller.set_shadow_type(gtk.SHADOW_IN)
472
self._pending_box.pack_start(scroller,
473
expand=True, fill=True, padding=5)
474
self._treeview_pending.show()
475
self._left_pane_box.pack_start(self._pending_box)
477
liststore = gtk.ListStore(gobject.TYPE_STRING, # revision_id
478
gobject.TYPE_STRING, # date
479
gobject.TYPE_STRING, # committer
480
gobject.TYPE_STRING, # summary
482
self._pending_store = liststore
483
self._treeview_pending.set_model(liststore)
484
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Date'),
485
gtk.CellRendererText(), text=1))
486
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Committer'),
487
gtk.CellRendererText(), text=2))
488
self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Summary'),
489
gtk.CellRendererText(), text=3))
491
def _construct_diff_view(self):
492
from diff import DiffView
494
# TODO: jam 2007-10-30 The diff label is currently disabled. If we
495
# decide that we really don't ever want to display it, we should
496
# actually remove it, and other references to it, along with the
497
# tests that it is set properly.
498
self._diff_label = gtk.Label(_i18n('Diff for whole tree'))
499
self._diff_label.set_alignment(0, 0)
500
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
501
self._add_to_right_table(self._diff_label, 1, False)
502
# self._diff_label.show()
504
self._diff_view = DiffView()
505
self._add_to_right_table(self._diff_view, 4, True)
506
self._diff_view.show()
508
def _construct_file_message(self):
509
scroller = gtk.ScrolledWindow()
510
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
512
self._file_message_text_view = gtk.TextView()
513
scroller.add(self._file_message_text_view)
514
scroller.set_shadow_type(gtk.SHADOW_IN)
517
self._file_message_text_view.modify_font(pango.FontDescription("Monospace"))
518
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
519
self._file_message_text_view.set_accepts_tab(False)
520
self._file_message_text_view.show()
522
self._file_message_expander = gtk.Expander(_i18n('File commit message'))
523
self._file_message_expander.set_expanded(True)
524
self._file_message_expander.add(scroller)
525
self._add_to_right_table(self._file_message_expander, 1, False)
526
self._file_message_expander.show()
528
def _construct_global_message(self):
529
self._global_message_label = gtk.Label(_i18n('Global Commit Message'))
530
self._global_message_label.set_markup(
531
_i18n('<b>Global Commit Message</b>'))
532
self._global_message_label.set_alignment(0, 0)
533
self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
534
self._add_to_right_table(self._global_message_label, 1, False)
535
# Can we remove the spacing between the label and the box?
536
self._global_message_label.show()
538
scroller = gtk.ScrolledWindow()
539
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
541
self._global_message_text_view = gtk.TextView()
542
self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
543
scroller.add(self._global_message_text_view)
544
scroller.set_shadow_type(gtk.SHADOW_IN)
546
self._add_to_right_table(scroller, 2, True)
547
self._file_message_text_view.set_wrap_mode(gtk.WRAP_WORD)
548
self._file_message_text_view.set_accepts_tab(False)
549
self._global_message_text_view.show()
551
def _on_treeview_files_cursor_changed(self, treeview):
552
treeselection = treeview.get_selection()
553
(model, selection) = treeselection.get_selected()
555
if selection is not None:
556
path, display_path = model.get(selection, 1, 3)
557
self._diff_label.set_text(_i18n('Diff for ') + display_path)
559
self._diff_view.show_diff(None)
561
self._diff_view.show_diff([path.decode('UTF-8')])
562
self._update_per_file_info(selection)
564
def _on_accel_next(self, accel_group, window, keyval, modifier):
565
# We don't really care about any of the parameters, because we know
566
# where this message came from
567
tree_selection = self._treeview_files.get_selection()
568
(model, selection) = tree_selection.get_selected()
569
if selection is None:
572
next = model.iter_next(selection)
575
# We have either made it to the end of the list, or nothing was
576
# selected. Either way, select All Files, and jump to the global
578
self._treeview_files.set_cursor((0,))
579
self._global_message_text_view.grab_focus()
581
# Set the cursor to this entry, and jump to the per-file commit
583
self._treeview_files.set_cursor(model.get_path(next))
584
self._file_message_text_view.grab_focus()
586
def _save_current_file_message(self):
587
if self._last_selected_file is None:
588
return # Nothing to save
589
text_buffer = self._file_message_text_view.get_buffer()
590
cur_text = text_buffer.get_text(text_buffer.get_start_iter(),
591
text_buffer.get_end_iter())
592
last_selected = self._files_store.get_iter(self._last_selected_file)
593
self._files_store.set_value(last_selected, 5, cur_text)
595
def _update_per_file_info(self, selection):
596
# The node is changing, so cache the current message
597
if not self._enable_per_file_commits:
167
for item in self.pending:
168
liststore.append([ item['date'],
600
self._save_current_file_message()
601
text_buffer = self._file_message_text_view.get_buffer()
602
file_id, display_path, message = self._files_store.get(selection, 0, 3, 5)
603
if file_id is None: # Whole tree
604
self._file_message_expander.set_label(_i18n('File commit message'))
605
self._file_message_expander.set_expanded(False)
606
self._file_message_expander.set_sensitive(False)
607
text_buffer.set_text('')
608
self._last_selected_file = None
610
self._file_message_expander.set_label(_i18n('Commit message for ')
612
self._file_message_expander.set_expanded(True)
613
self._file_message_expander.set_sensitive(True)
614
text_buffer.set_text(message)
615
self._last_selected_file = self._files_store.get_path(selection)
172
617
def _get_specific_files(self):
174
it = self.file_store.get_iter_first()
176
if self.file_store.get_value(it, 0):
177
# get real path from hidden column 3
178
ret.append(self.file_store.get_value(it, 3))
179
it = self.file_store.iter_next(it)
183
def _toggle_commit(self, cell, path, model):
184
model[path][0] = not model[path][0]
187
def _pending_merges(self, wt):
188
""" Return a list of pending merges or None if there are none of them. """
189
parents = wt.get_parent_ids()
194
from bzrlib.osutils import format_date
196
pending = parents[1:]
198
last_revision = parents[0]
200
if last_revision is not None:
202
ignore = set(branch.repository.get_ancestry(last_revision))
203
except errors.NoSuchRevision:
204
# the last revision is a ghost : assume everything is new
206
ignore = set([None, last_revision])
211
for merge in pending:
214
m_revision = branch.repository.get_revision(merge)
217
rev['committer'] = re.sub('<.*@.*>', '', m_revision.committer).strip(' ')
218
rev['summary'] = m_revision.get_summary()
219
rev['date'] = format_date(m_revision.timestamp,
220
m_revision.timezone or 0,
221
'original', date_fmt="%Y-%m-%d",
226
inner_merges = branch.repository.get_ancestry(merge)
227
assert inner_merges[0] is None
229
inner_merges.reverse()
230
for mmerge in inner_merges:
233
mm_revision = branch.repository.get_revision(mmerge)
236
rev['committer'] = re.sub('<.*@.*>', '', mm_revision.committer).strip(' ')
237
rev['summary'] = mm_revision.get_summary()
238
rev['date'] = format_date(mm_revision.timestamp,
239
mm_revision.timezone or 0,
240
'original', date_fmt="%Y-%m-%d",
246
except errors.NoSuchRevision:
247
print "DEBUG: NoSuchRevision:", merge
251
def commit(self, widget):
252
textbuffer = self.textview.get_buffer()
253
start, end = textbuffer.get_bounds()
254
message = textbuffer.get_text(start, end).decode('utf-8')
256
checkbutton_strict = self.glade.get_widget('checkbutton_commit_strict')
257
checkbutton_force = self.glade.get_widget('checkbutton_commit_force')
260
specific_files = self._get_specific_files()
618
"""Return the list of selected paths, and file info.
620
:return: ([unicode paths], [{utf-8 file info}]
622
self._save_current_file_message()
624
records = iter(self._files_store)
625
rec = records.next() # Skip the All Files record
626
assert rec[0] is None, "Are we skipping the wrong record?"
629
for record in records:
630
if self._commit_all_changes or record[2]:# [2] checkbox
631
file_id = record[0] # [0] file_id
632
path = record[1] # [1] real path
633
file_message = record[5] # [5] commit message
634
files.append(path.decode('UTF-8'))
635
if self._enable_per_file_commits and file_message:
636
# All of this needs to be utf-8 information
637
file_info.append({'path':path, 'file_id':file_id,
638
'message':file_message})
639
file_info.sort(key=lambda x:(x['path'], x['file_id']))
640
if self._enable_per_file_commits:
641
return files, file_info
646
def _on_commit_clicked(self, button):
647
""" Commit button clicked handler. """
650
def _do_commit(self):
651
message = self._get_global_commit_message()
654
response = self._question_dialog(
655
_i18n('Commit with an empty message?'),
656
_i18n('You can describe your commit intent in the message.'))
657
if response == gtk.RESPONSE_NO:
658
# Kindly give focus to message area
659
self._global_message_text_view.grab_focus()
662
specific_files, file_info = self._get_specific_files()
262
664
specific_files = None
666
local = self._check_local.get_active()
668
# All we care about is if there is a single unknown, so if this loop is
669
# entered, then there are unknown files.
670
# TODO: jam 20071002 It seems like this should cancel the dialog
671
# entirely, since there isn't a way for them to add the unknown
672
# files at this point.
673
for path in self._wt.unknowns():
674
response = self._question_dialog(
675
_i18n("Commit with unknowns?"),
676
_i18n("Unknown files exist in the working tree. Commit anyway?"))
677
if response == gtk.RESPONSE_NO:
684
revprops['file-info'] = bencode.bencode(file_info).decode('UTF-8')
265
self.wt.commit(message,
266
allow_pointless=checkbutton_force.get_active(),
267
strict=checkbutton_strict.get_active(),
268
local=self.checkbutton_local.get_active(),
269
specific_files=specific_files)
270
except errors.NotBranchError:
271
error_dialog(_('Directory is not a branch'),
272
_('You can perform this action only in a branch.'))
274
except errors.LocalRequiresBoundBranch:
275
error_dialog(_('Directory is not a checkout'),
276
_('You can perform local commit only on checkouts.'))
686
rev_id = self._wt.commit(message,
687
allow_pointless=False,
690
specific_files=specific_files,
278
692
except errors.PointlessCommit:
279
error_dialog(_('No changes to commit'),
280
_('Try force commit if you want to commit anyway.'))
282
except errors.ConflictsInTree:
283
error_dialog(_('Conflicts in tree'),
284
_('You need to resolve the conflicts before committing.'))
286
except errors.StrictCommitFailed:
287
error_dialog(_('Strict commit failed'),
288
_('There are unknown files in the working tree.\nPlease add or delete them.'))
290
except errors.BoundBranchOutOfDate, errmsg:
291
error_dialog(_('Bound branch is out of date'),
294
except errors.BzrError, msg:
295
error_dialog(_('Unknown bzr error'), str(msg))
297
except Exception, msg:
298
error_dialog(_('Unknown error'), str(msg))
301
if not self.standalone:
306
def close(self, widget=None):
307
self.window.destroy()
309
def quit(self, widget=None):
693
response = self._question_dialog(
694
_i18n('Commit with no changes?'),
695
_i18n('There are no changes in the working tree.'
696
' Do you want to commit anyway?'))
697
if response == gtk.RESPONSE_YES:
698
rev_id = self._wt.commit(message,
699
allow_pointless=True,
702
specific_files=specific_files,
704
self.committed_revision_id = rev_id
705
self.response(gtk.RESPONSE_OK)
707
def _get_global_commit_message(self):
708
buf = self._global_message_text_view.get_buffer()
709
start, end = buf.get_bounds()
710
return buf.get_text(start, end).decode('utf-8')
712
def _set_global_commit_message(self, message):
713
"""Just a helper for the test suite."""
714
if isinstance(message, unicode):
715
message = message.encode('UTF-8')
716
self._global_message_text_view.get_buffer().set_text(message)
718
def _set_file_commit_message(self, message):
719
"""Helper for the test suite."""
720
if isinstance(message, unicode):
721
message = message.encode('UTF-8')
722
self._file_message_text_view.get_buffer().set_text(message)
725
def _rev_to_pending_info(rev):
726
"""Get the information from a pending merge."""
727
from bzrlib.osutils import format_date
729
rev_dict['committer'] = re.sub('<.*@.*>', '', rev.committer).strip(' ')
730
rev_dict['summary'] = rev.get_summary()
731
rev_dict['date'] = format_date(rev.timestamp,
733
'original', date_fmt="%Y-%m-%d",
735
rev_dict['revision_id'] = rev.revision_id