28
import bzrlib.errors as errors
29
from bzrlib import osutils
30
from bzrlib import errors, osutils
31
from bzrlib.trace import mutter
32
from bzrlib.util import bencode
31
34
from dialog import error_dialog, question_dialog
32
35
from errors import show_bzr_error
33
from guifiles import GLADEFILENAME
37
""" Display Commit dialog and perform the needed actions. """
38
def __init__(self, wt, wtpath, notbranch):
39
""" Initialize the Commit dialog.
40
:param wt: bzr working tree object
41
:param wtpath: path to working tree root
42
:param notbranch: flag that path is not a brach
45
self.glade = gtk.glade.XML(GLADEFILENAME, 'window_commit', 'olive-gtk')
49
self.notbranch = notbranch
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_expander = self.glade.get_widget('expander_commit_select')
56
self.file_view = self.glade.get_widget('treeview_commit_select')
57
self.pending_expander = self.glade.get_widget('expander_commit_pending')
58
self.pending_label = self.glade.get_widget('label_commit_pending')
59
self.pending_view = self.glade.get_widget('treeview_commit_pending')
61
if wt is None or notbranch:
65
self.old_tree = self.wt.branch.repository.revision_tree(self.wt.branch.last_revision())
66
self.delta = self.wt.changes_from(self.old_tree)
69
self.pending = self._pending_merges(self.wt)
71
# Dictionary for signal_autoconnect
72
dic = { "on_button_commit_commit_clicked": self.commit,
73
"on_button_commit_cancel_clicked": self.close }
75
# Connect the signals to the handlers
76
self.glade.signal_autoconnect(dic)
78
# Create the file list
79
self._create_file_view()
80
# Create the pending merges
81
self._create_pending_merges()
84
""" Display the Push dialog.
85
@return: True if dialog is shown.
87
if self.wt is None and not self.notbranch:
88
error_dialog(_('Directory does not have a working tree'),
89
_('Operation aborted.'))
93
error_dialog(_('Directory is not a branch'),
94
_('You can perform this action only in a branch.'))
98
if self.wt.branch.get_bound_location() is not None:
99
# we have a checkout, so the local commit checkbox must appear
100
self.checkbutton_local.show()
103
# There are pending merges, file selection not supported
104
self.file_expander.set_expanded(False)
105
self.file_view.set_sensitive(False)
108
self.pending_expander.hide()
110
self.textview.modify_font(pango.FontDescription("Monospace"))
114
def _create_file_view(self):
115
self.file_store = gtk.ListStore(gobject.TYPE_BOOLEAN, # [0] checkbox
116
gobject.TYPE_STRING, # [1] path to display
117
gobject.TYPE_STRING, # [2] changes type
118
gobject.TYPE_STRING) # [3] real path
119
self.file_view.set_model(self.file_store)
120
crt = gtk.CellRendererToggle()
121
crt.set_property("activatable", True)
122
crt.connect("toggled", self._toggle_commit, self.file_store)
123
self.file_view.append_column(gtk.TreeViewColumn(_('Commit'),
125
self.file_view.append_column(gtk.TreeViewColumn(_('Path'),
126
gtk.CellRendererText(), text=1))
127
self.file_view.append_column(gtk.TreeViewColumn(_('Type'),
128
gtk.CellRendererText(), text=2))
130
for path, id, kind in self.delta.added:
131
marker = osutils.kind_marker(kind)
132
self.file_store.append([ True, path+marker, _('added'), path ])
134
for path, id, kind in self.delta.removed:
135
marker = osutils.kind_marker(kind)
136
self.file_store.append([ True, path+marker, _('removed'), path ])
138
for oldpath, newpath, id, kind, text_modified, meta_modified in self.delta.renamed:
139
marker = osutils.kind_marker(kind)
140
if text_modified or meta_modified:
141
changes = _('renamed and modified')
143
changes = _('renamed')
144
self.file_store.append([ True,
145
oldpath+marker + ' => ' + newpath+marker,
150
for path, id, kind, text_modified, meta_modified in self.delta.modified:
151
marker = osutils.kind_marker(kind)
152
self.file_store.append([ True, path+marker, _('modified'), path ])
154
def _create_pending_merges(self):
156
# hide unused pending merge part
157
scrolled_window = self.glade.get_widget('scrolledwindow_commit_pending')
158
parent = scrolled_window.get_parent()
159
parent.remove(scrolled_window)
160
parent = self.pending_label.get_parent()
161
parent.remove(self.pending_label)
164
liststore = gtk.ListStore(gobject.TYPE_STRING,
167
self.pending_view.set_model(liststore)
169
self.pending_view.append_column(gtk.TreeViewColumn(_('Date'),
170
gtk.CellRendererText(), text=0))
171
self.pending_view.append_column(gtk.TreeViewColumn(_('Committer'),
172
gtk.CellRendererText(), text=1))
173
self.pending_view.append_column(gtk.TreeViewColumn(_('Summary'),
174
gtk.CellRendererText(), text=2))
176
for item in self.pending:
177
liststore.append([ item['date'],
181
def _get_specific_files(self):
183
it = self.file_store.get_iter_first()
185
if self.file_store.get_value(it, 0):
186
# get real path from hidden column 3
187
ret.append(self.file_store.get_value(it, 3))
188
it = self.file_store.iter_next(it)
192
def _toggle_commit(self, cell, path, model):
193
model[path][0] = not model[path][0]
196
def _pending_merges(self, wt):
197
""" Return a list of pending merges or None if there are none of them. """
198
parents = wt.get_parent_ids()
203
from bzrlib.osutils import format_date
205
pending = parents[1:]
207
last_revision = parents[0]
209
if last_revision is not None:
211
ignore = set(branch.repository.get_ancestry(last_revision))
212
except errors.NoSuchRevision:
213
# the last revision is a ghost : assume everything is new
215
ignore = set([None, last_revision])
220
for merge in pending:
223
m_revision = branch.repository.get_revision(merge)
226
rev['committer'] = re.sub('<.*@.*>', '', m_revision.committer).strip(' ')
227
rev['summary'] = m_revision.get_summary()
228
rev['date'] = format_date(m_revision.timestamp,
229
m_revision.timezone or 0,
230
'original', date_fmt="%Y-%m-%d",
235
inner_merges = branch.repository.get_ancestry(merge)
236
assert inner_merges[0] is None
238
inner_merges.reverse()
239
for mmerge in inner_merges:
242
mm_revision = branch.repository.get_revision(mmerge)
245
rev['committer'] = re.sub('<.*@.*>', '', mm_revision.committer).strip(' ')
246
rev['summary'] = mm_revision.get_summary()
247
rev['date'] = format_date(mm_revision.timestamp,
248
mm_revision.timezone or 0,
249
'original', date_fmt="%Y-%m-%d",
255
except errors.NoSuchRevision:
256
print "DEBUG: NoSuchRevision:", merge
260
def commit(self, widget):
261
textbuffer = self.textview.get_buffer()
262
start, end = textbuffer.get_bounds()
263
message = textbuffer.get_text(start, end).decode('utf-8')
265
checkbutton_strict = self.glade.get_widget('checkbutton_commit_strict')
266
checkbutton_force = self.glade.get_widget('checkbutton_commit_force')
269
specific_files = self._get_specific_files()
271
specific_files = None
274
response = question_dialog('Commit with an empty message ?',
275
'You can describe your commit intent'
277
if response == gtk.RESPONSE_NO:
278
# Kindly give focus to message area
279
self.textview.grab_focus()
283
self.wt.commit(message,
284
allow_pointless=checkbutton_force.get_active(),
285
strict=checkbutton_strict.get_active(),
286
local=self.checkbutton_local.get_active(),
287
specific_files=specific_files)
288
except errors.NotBranchError:
289
error_dialog(_('Directory is not a branch'),
290
_('You can perform this action only in a branch.'))
292
except errors.LocalRequiresBoundBranch:
293
error_dialog(_('Directory is not a checkout'),
294
_('You can perform local commit only on checkouts.'))
296
except errors.PointlessCommit:
297
error_dialog(_('No changes to commit'),
298
_('Try force commit if you want to commit anyway.'))
300
except errors.ConflictsInTree:
301
error_dialog(_('Conflicts in tree'),
302
_('You need to resolve the conflicts before committing.'))
304
except errors.StrictCommitFailed:
305
error_dialog(_('Strict commit failed'),
306
_('There are unknown files in the working tree.\nPlease add or delete them.'))
308
except errors.BoundBranchOutOfDate, errmsg:
309
error_dialog(_('Bound branch is out of date'),
312
except errors.BzrError, msg:
313
error_dialog(_('Unknown bzr error'), str(msg))
315
except Exception, msg:
316
error_dialog(_('Unknown error'), str(msg))
321
def close(self, widget=None):
322
self.window.destroy()
324
class CommitDialogNew(gtk.Dialog):
325
""" New implementation of the Commit dialog. """
326
def __init__(self, wt, wtpath, notbranch, selected=None, parent=None):
327
""" Initialize the Commit Dialog. """
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
class CommitDialog(gtk.Dialog):
100
"""Implementation of Commit."""
102
def __init__(self, wt, selected=None, parent=None):
328
103
gtk.Dialog.__init__(self, title="Commit - Olive",
331
106
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
336
self.notbranch = notbranch
337
self.selected = selected
340
self.old_tree = self.wt.branch.repository.revision_tree(self.wt.branch.last_revision())
341
self.delta = self.wt.changes_from(self.old_tree)
344
self.pending = self._pending_merges(self.wt)
346
# Do some preliminary checks
347
self._is_checkout = False
348
self._is_pending = False
349
if self.wt is None and not self.notbranch:
350
error_dialog(_('Directory does not have a working tree'),
351
_('Operation aborted.'))
356
error_dialog(_('Directory is not a branch'),
357
_('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
361
if self.wt.branch.get_bound_location() is not None:
362
# we have a checkout, so the local commit checkbox must appear
363
self._is_checkout = True
366
# There are pending merges, file selection not supported
367
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):
370
337
self._button_commit = gtk.Button(_("Comm_it"), use_underline=True)
371
if self._is_checkout:
372
self._check_local = gtk.CheckButton(_("_Local only commit (works in checkouts)"),
374
self._check_strict = gtk.CheckButton(_("_Strict commit (fails if unknown files are present)"),
376
self._expander_files = gtk.Expander(_("Please select the file(s) to commit"))
377
self._vpaned_main = gtk.VPaned()
378
self._scrolledwindow_files = gtk.ScrolledWindow()
379
self._scrolledwindow_message = gtk.ScrolledWindow()
380
self._treeview_files = gtk.TreeView()
381
self._vbox_message = gtk.VBox()
382
self._label_message = gtk.Label(_("Please specify a commit message:"))
383
self._textview_message = gtk.TextView()
386
self._expander_merges = gtk.Expander(_("Pending merges"))
387
self._vpaned_list = gtk.VPaned()
388
self._scrolledwindow_merges = gtk.ScrolledWindow()
389
self._treeview_merges = gtk.TreeView()
392
338
self._button_commit.connect('clicked', self._on_commit_clicked)
393
self._treeview_files.connect('row_activated', self._on_treeview_files_row_activated)
396
self._scrolledwindow_files.set_policy(gtk.POLICY_AUTOMATIC,
397
gtk.POLICY_AUTOMATIC)
398
self._scrolledwindow_message.set_policy(gtk.POLICY_AUTOMATIC,
399
gtk.POLICY_AUTOMATIC)
400
self._textview_message.modify_font(pango.FontDescription("Monospace"))
401
self.set_default_size(500, 500)
402
self._vpaned_main.set_position(200)
405
self._scrolledwindow_merges.set_policy(gtk.POLICY_AUTOMATIC,
406
gtk.POLICY_AUTOMATIC)
407
self._treeview_files.set_sensitive(False)
409
# Construct the dialog
339
self._button_commit.set_flags(gtk.CAN_DEFAULT)
340
self._button_commit.show()
410
341
self.action_area.pack_end(self._button_commit)
412
self._scrolledwindow_files.add(self._treeview_files)
413
self._scrolledwindow_message.add(self._textview_message)
415
self._expander_files.add(self._scrolledwindow_files)
417
self._vbox_message.pack_start(self._label_message, False, False)
418
self._vbox_message.pack_start(self._scrolledwindow_message, True, True)
421
self._expander_merges.add(self._scrolledwindow_merges)
422
self._scrolledwindow_merges.add(self._treeview_merges)
423
self._vpaned_list.add1(self._expander_files)
424
self._vpaned_list.add2(self._expander_merges)
425
self._vpaned_main.add1(self._vpaned_list)
427
self._vpaned_main.add1(self._expander_files)
429
self._vpaned_main.add2(self._vbox_message)
431
self.vbox.pack_start(self._vpaned_main, True, True)
432
if self._is_checkout:
433
self.vbox.pack_start(self._check_local, False, False)
434
self.vbox.pack_start(self._check_strict, False, False)
436
# Create the file list
437
self._create_file_view()
438
# Create the pending merges
439
self._create_pending_merges()
441
# Expand the corresponding expander
443
self._expander_merges.set_expanded(True)
445
self._expander_files.set_expanded(True)
450
def _on_treeview_files_row_activated(self, treeview, path, view_column):
451
# 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):
452
547
treeselection = treeview.get_selection()
453
(model, iter) = treeselection.get_selected()
456
from olive import DiffWindow
458
_selected = model.get_value(iter, 1)
461
parent_tree = self.wt.branch.repository.revision_tree(self.wt.branch.last_revision())
462
diff.set_diff(self.wt.branch.nick, self.wt, parent_tree)
464
diff.set_file(_selected)
465
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
470
641
def _on_commit_clicked(self, button):
471
642
""" Commit button clicked handler. """
472
textbuffer = self._textview_message.get_buffer()
473
start, end = textbuffer.get_bounds()
474
message = textbuffer.get_text(start, end).decode('utf-8')
477
specific_files = self._get_specific_files()
479
specific_files = None
645
def _do_commit(self):
646
message = self._get_global_commit_message()
481
648
if message == '':
482
response = question_dialog(_('Commit with an empty message?'),
483
_('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.'))
484
652
if response == gtk.RESPONSE_NO:
485
653
# Kindly give focus to message area
486
self._textview_message.grab_focus()
489
if self._is_checkout:
490
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')
495
self.wt.commit(message,
681
rev_id = self._wt.commit(message,
496
682
allow_pointless=False,
497
strict=self._check_strict.get_active(),
499
specific_files=specific_files)
685
specific_files=specific_files,
500
687
except errors.PointlessCommit:
501
response = question_dialog(_('Commit with no changes?'),
502
_('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?'))
503
692
if response == gtk.RESPONSE_YES:
504
self.wt.commit(message,
693
rev_id = self._wt.commit(message,
505
694
allow_pointless=True,
506
strict=self._check_strict.get_active(),
508
specific_files=specific_files)
697
specific_files=specific_files,
699
self.committed_revision_id = rev_id
509
700
self.response(gtk.RESPONSE_OK)
511
def _pending_merges(self, wt):
512
""" Return a list of pending merges or None if there are none of them. """
513
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."""
518
722
from bzrlib.osutils import format_date
520
pending = parents[1:]
522
last_revision = parents[0]
524
if last_revision is not None:
526
ignore = set(branch.repository.get_ancestry(last_revision))
527
except errors.NoSuchRevision:
528
# the last revision is a ghost : assume everything is new
530
ignore = set([None, last_revision])
535
for merge in pending:
538
m_revision = branch.repository.get_revision(merge)
541
rev['committer'] = re.sub('<.*@.*>', '', m_revision.committer).strip(' ')
542
rev['summary'] = m_revision.get_summary()
543
rev['date'] = format_date(m_revision.timestamp,
544
m_revision.timezone or 0,
545
'original', date_fmt="%Y-%m-%d",
550
inner_merges = branch.repository.get_ancestry(merge)
551
assert inner_merges[0] is None
553
inner_merges.reverse()
554
for mmerge in inner_merges:
557
mm_revision = branch.repository.get_revision(mmerge)
560
rev['committer'] = re.sub('<.*@.*>', '', mm_revision.committer).strip(' ')
561
rev['summary'] = mm_revision.get_summary()
562
rev['date'] = format_date(mm_revision.timestamp,
563
mm_revision.timezone or 0,
564
'original', date_fmt="%Y-%m-%d",
570
except errors.NoSuchRevision:
571
print "DEBUG: NoSuchRevision:", merge
575
def _create_file_view(self):
576
self._file_store = gtk.ListStore(gobject.TYPE_BOOLEAN, # [0] checkbox
577
gobject.TYPE_STRING, # [1] path to display
578
gobject.TYPE_STRING, # [2] changes type
579
gobject.TYPE_STRING) # [3] real path
580
self._treeview_files.set_model(self._file_store)
581
crt = gtk.CellRendererToggle()
582
crt.set_property("activatable", True)
583
crt.connect("toggled", self._toggle_commit, self._file_store)
584
self._treeview_files.append_column(gtk.TreeViewColumn(_('Commit'),
586
self._treeview_files.append_column(gtk.TreeViewColumn(_('Path'),
587
gtk.CellRendererText(), text=1))
588
self._treeview_files.append_column(gtk.TreeViewColumn(_('Type'),
589
gtk.CellRendererText(), text=2))
591
for path, id, kind in self.delta.added:
592
marker = osutils.kind_marker(kind)
593
self._file_store.append([ True, path+marker, _('added'), path ])
595
for path, id, kind in self.delta.removed:
596
marker = osutils.kind_marker(kind)
597
self._file_store.append([ True, path+marker, _('removed'), path ])
599
for oldpath, newpath, id, kind, text_modified, meta_modified in self.delta.renamed:
600
marker = osutils.kind_marker(kind)
601
if text_modified or meta_modified:
602
changes = _('renamed and modified')
604
changes = _('renamed')
605
self._file_store.append([ True,
606
oldpath+marker + ' => ' + newpath+marker,
611
for path, id, kind, text_modified, meta_modified in self.delta.modified:
612
marker = osutils.kind_marker(kind)
613
self._file_store.append([ True, path+marker, _('modified'), path ])
615
def _create_pending_merges(self):
619
liststore = gtk.ListStore(gobject.TYPE_STRING,
622
self._treeview_merges.set_model(liststore)
624
self._treeview_merges.append_column(gtk.TreeViewColumn(_('Date'),
625
gtk.CellRendererText(), text=0))
626
self._treeview_merges.append_column(gtk.TreeViewColumn(_('Committer'),
627
gtk.CellRendererText(), text=1))
628
self._treeview_merges.append_column(gtk.TreeViewColumn(_('Summary'),
629
gtk.CellRendererText(), text=2))
631
for item in self.pending:
632
liststore.append([ item['date'],
636
def _get_specific_files(self):
638
it = self._file_store.get_iter_first()
640
if self._file_store.get_value(it, 0):
641
# get real path from hidden column 3
642
ret.append(self._file_store.get_value(it, 3))
643
it = self._file_store.iter_next(it)
647
def _toggle_commit(self, cell, path, model):
648
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