3
3
# Copyright (C) 2006 Jeff Bailey
4
4
# Copyright (C) 2006 Wouter van Heyst
5
# Copyright (C) 2006-2011 Jelmer Vernooij <jelmer@samba.org>
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program; if not, write to the Free Software
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22
# setup.py can install nautilus-bzr to the right system folder, if pkg-config
25
# You can also install nautilus-bzr manually by copying it (or linking it from)
26
# ~/.local/share/nautilus-python/extensions/nautilus-bzr.py
28
from gi.repository import Gtk, GObject, Nautilus
29
from bzrlib.controldir import ControlDir
30
from bzrlib.errors import (
34
from bzrlib.ignores import tree_ignores_add_patterns
35
from bzrlib.tree import InterTree
5
# Copyright (C) 2006 Jelmer Vernooij
7
# Published under the GNU GPL
12
from bzrlib.bzrdir import BzrDir
13
from bzrlib.errors import NotBranchError
14
from bzrlib.errors import NoWorkingTree
15
from bzrlib.errors import UnsupportedProtocol
16
from bzrlib.workingtree import WorkingTree
17
from bzrlib.branch import Branch
18
from bzrlib.tree import file_status
37
20
from bzrlib.plugin import load_plugins
40
from bzrlib.plugins.gtk.i18n import _i18n
43
class BazaarExtension(Nautilus.MenuProvider, Nautilus.ColumnProvider,
44
Nautilus.InfoProvider, Nautilus.PropertyPageProvider,
45
Nautilus.LocationWidgetProvider, GObject.GObject):
46
"""Nautilus extension providing Bazaar integration."""
23
from bzrlib.plugins.gtk import cmd_visualise, cmd_gannotate
25
class BzrExtension(nautilus.MenuProvider, nautilus.ColumnProvider, nautilus.InfoProvider):
48
26
def __init__(self):
52
def _open_bzrdir(cls, vfs_file):
53
uri = vfs_file.get_uri()
54
controldir, path = ControlDir.open_containing(uri)
55
return controldir, path
58
def _open_tree(cls, vfs_file):
59
controldir, path = cls._open_bzrdir(vfs_file)
60
return controldir.open_workingtree(), path
62
def add_cb(self, menu, tree, path):
29
def add_cb(self, menu, vfs_file):
30
# We can only cope with local files
31
if vfs_file.get_uri_scheme() != 'file':
34
file = vfs_file.get_uri()
36
tree, path = WorkingTree.open_containing(file)
37
except NotBranchError:
65
def ignore_cb(self, menu, tree, path):
66
# We can only cope with local files
67
tree_ignores_add_patterns(tree, [path])
68
#FIXME: Add path to ignore file
70
def unignore_cb(self, menu, tree, path):
72
# We can only cope with local files
75
def diff_cb(self, menu, tree, path=None):
44
def ignore_cb(self, menu, vfs_file):
45
# We can only cope with local files
46
if vfs_file.get_uri_scheme() != 'file':
49
file = vfs_file.get_uri()
51
tree, path = WorkingTree.open_containing(file)
52
except NotBranchError:
59
def unignore_cb(self, menu, vfs_file):
60
# We can only cope with local files
61
if vfs_file.get_uri_scheme() != 'file':
64
file = vfs_file.get_uri()
66
tree, path = WorkingTree.open_containing(file)
67
except NotBranchError:
74
def diff_cb(self, menu, vfs_file):
75
# We can only cope with local files
76
if vfs_file.get_uri_scheme() != 'file':
79
file = vfs_file.get_uri()
81
tree, path = WorkingTree.open_containing(file)
82
except NotBranchError:
76
85
from bzrlib.plugins.gtk.diff import DiffWindow
77
86
window = DiffWindow()
78
window.set_diff(tree.branch._get_nick(local=True), tree,
79
tree.branch.basis_tree())
87
window.set_diff(tree.branch.nick, tree, tree.branch.basis_tree())
82
92
def newtree_cb(self, menu, vfs_file):
83
controldir, path = self._open_bzrdir(vfs_file)
84
controldir.create_workingtree()
86
def remove_cb(self, menu, tree, path):
93
# We can only cope with local files
94
if vfs_file.get_uri_scheme() != 'file':
97
file = vfs_file.get_uri()
99
# We only want to continue here if we get a NotBranchError
101
tree, path = WorkingTree.open_containing(file)
102
except NotBranchError:
103
BzrDir.create_standalone_workingtree(file)
105
def remove_cb(self, menu, vfs_file):
106
# We can only cope with local files
107
if vfs_file.get_uri_scheme() != 'file':
110
file = vfs_file.get_uri()
112
tree, path = WorkingTree.open_containing(file)
113
except NotBranchError:
89
def annotate_cb(self, menu, tree, path, file_id):
90
from bzrlib.plugins.gtk.annotate.gannotate import GAnnotateWindow
91
win = GAnnotateWindow()
93
win.annotate(tree, tree.branch, file_id)
118
def annotate_cb(self, menu, vfs_file):
119
# We can only cope with local files
120
if vfs_file.get_uri_scheme() != 'file':
123
file = vfs_file.get_uri()
125
vis = cmd_gannotate()
96
128
def clone_cb(self, menu, vfs_file=None):
129
# We can only cope with local files
130
if vfs_file.get_uri_scheme() != 'file':
97
133
from bzrlib.plugins.gtk.branch import BranchDialog
98
controldir, path = self._open_bzrdir(vfs_file)
100
135
dialog = BranchDialog(vfs_file.get_name())
101
136
response = dialog.run()
102
if response != Gtk.ResponseType.NONE:
137
if response != gtk.RESPONSE_NONE:
106
def commit_cb(self, menu, tree, path=None):
141
def commit_cb(self, menu, vfs_file=None):
142
# We can only cope with local files
143
if vfs_file.get_uri_scheme() != 'file':
146
file = vfs_file.get_uri()
150
tree, path = WorkingTree.open_containing(file)
152
except NotBranchError, e:
155
except NoWorkingTree, e:
158
(branch, path) = Branch.open_containing(path)
159
except NotBranchError, e:
107
162
from bzrlib.plugins.gtk.commit import CommitDialog
108
dialog = CommitDialog(tree, path)
163
dialog = CommitDialog(tree, path, not branch)
109
164
response = dialog.run()
110
if response != Gtk.ResponseType.NONE:
165
if response != gtk.RESPONSE_NONE:
114
def log_cb(self, menu, controldir, path=None):
115
from bzrlib.plugins.gtk.viz import BranchWindow
116
branch = controldir.open_branch()
117
pp = BranchWindow(branch, [branch.last_revision()], None)
121
def pull_cb(self, menu, controldir, path=None):
169
def log_cb(self, menu, vfs_file):
170
# We can only cope with local files
171
if vfs_file.get_uri_scheme() != 'file':
174
file = vfs_file.get_uri()
176
# We only want to continue here if we get a NotBranchError
178
tree, path = WorkingTree.open_containing(file)
179
except NotBranchError:
182
vis = cmd_visualise()
187
def pull_cb(self, menu, vfs_file):
188
# We can only cope with local files
189
if vfs_file.get_uri_scheme() != 'file':
192
file = vfs_file.get_uri()
194
# We only want to continue here if we get a NotBranchError
196
tree, path = WorkingTree.open_containing(file)
197
except NotBranchError:
122
200
from bzrlib.plugins.gtk.pull import PullDialog
123
dialog = PullDialog(controldir.open_workingtree(), path)
127
def push_cb(self, menu, controldir, path=None):
128
from bzrlib.plugins.gtk.push import PushDialog
129
dialog = PushDialog(branch=controldir.open_workingtree().branch)
133
def merge_cb(self, menu, tree, path=None):
201
dialog = PullDialog(tree, path)
205
def merge_cb(self, menu, vfs_file):
206
# We can only cope with local files
207
if vfs_file.get_uri_scheme() != 'file':
210
file = vfs_file.get_uri()
212
# We only want to continue here if we get a NotBranchError
214
tree, path = WorkingTree.open_containing(file)
215
except NotBranchError:
134
218
from bzrlib.plugins.gtk.merge import MergeDialog
135
219
dialog = MergeDialog(tree, path)
139
def create_tree_cb(self, menu, controldir):
140
controldir.create_workingtree()
142
223
def get_background_items(self, window, vfs_file):
225
file = vfs_file.get_uri()
144
controldir, path = self._open_bzrdir(vfs_file)
145
except NotBranchError:
227
tree, path = WorkingTree.open_containing(file)
228
except UnsupportedProtocol:
148
branch = controldir.open_branch()
149
230
except NotBranchError:
151
item = Nautilus.MenuItem(name='BzrNautilus::newtree',
152
label='Make directory versioned',
153
tip='Create new Bazaar tree in this folder',
231
item = nautilus.MenuItem('BzrNautilus::newtree',
232
'Make directory versioned',
233
'Create new Bazaar tree in this folder')
155
234
item.connect('activate', self.newtree_cb, vfs_file)
156
235
items.append(item)
158
item = Nautilus.MenuItem(name='BzrNautilus::clone',
159
label='Checkout Bazaar branch ...',
160
tip='Checkout Existing Bazaar Branch',
237
item = nautilus.MenuItem('BzrNautilus::clone',
238
'Checkout Bazaar branch',
239
'Checkout Existing Bazaar Branch')
162
240
item.connect('activate', self.clone_cb, vfs_file)
163
241
items.append(item)
168
nautilus_integration = self.check_branch_enabled(branch)
169
if not nautilus_integration:
170
item = Nautilus.MenuItem(name='BzrNautilus::enable',
171
label='Enable Bazaar Plugin for this Branch',
172
tip='Enable Bazaar plugin for nautilus',
174
item.connect('activate', self.toggle_integration, True, branch)
177
item = Nautilus.MenuItem(name='BzrNautilus::disable',
178
label='Disable Bazaar Plugin this Branch',
179
tip='Disable Bazaar plugin for nautilus',
181
item.connect('activate', self.toggle_integration, False, branch)
184
item = Nautilus.MenuItem(name='BzrNautilus::log',
186
tip='Show Bazaar history',
188
item.connect('activate', self.log_cb, controldir)
191
item = Nautilus.MenuItem(name='BzrNautilus::pull',
193
tip='Pull from another branch',
195
item.connect('activate', self.pull_cb, controldir)
199
tree = controldir.open_workingtree()
200
except NoWorkingTree:
201
item = Nautilus.MenuItem(name='BzrNautilus::create_tree',
202
label='Create working tree...',
203
tip='Create a working tree for this branch',
205
item.connect('activate', self.create_tree_cb, controldir)
208
item = Nautilus.MenuItem(name='BzrNautilus::merge',
210
tip='Merge from another branch',
212
item.connect('activate', self.merge_cb, tree, path)
215
item = Nautilus.MenuItem(name='BzrNautilus::commit',
217
tip='Commit Changes',
219
item.connect('activate', self.commit_cb, tree, path)
245
item = nautilus.MenuItem('BzrNautilus::log',
247
'Show Bazaar history')
248
item.connect('activate', self.log_cb, vfs_file)
251
item = nautilus.MenuItem('BzrNautilus::pull',
253
'Pull from another branch')
254
item.connect('activate', self.pull_cb, vfs_file)
257
item = nautilus.MenuItem('BzrNautilus::merge',
259
'Merge from another branch')
260
item.connect('activate', self.merge_cb, vfs_file)
263
item = nautilus.MenuItem('BzrNautilus::commit',
266
item.connect('activate', self.commit_cb, vfs_file)
224
def _get_file_menuitems(self, tree, intertree, path):
225
file_id = tree.path2id(path)
227
item = Nautilus.MenuItem(name='BzrNautilus::add',
229
tip='Add as versioned file',
231
item.connect('activate', self.add_cb, tree, path)
234
item = Nautilus.MenuItem(name='BzrNautilus::ignore',
236
tip='Ignore file for versioning',
238
item.connect('activate', self.ignore_cb, tree, path)
240
elif tree.is_ignored(path):
241
item = Nautilus.MenuItem(name='BzrNautilus::unignore',
243
tip='Unignore file for versioning',
245
item.connect('activate', self.unignore_cb, tree, path)
248
kind = tree.kind(file_id)
249
item = Nautilus.MenuItem(name='BzrNautilus::log',
253
item.connect('activate', self.log_cb, tree.bzrdir, path)
256
if not intertree.file_content_matches(file_id, file_id):
257
item = Nautilus.MenuItem(name='BzrNautilus::diff',
258
label='View Changes ...',
259
tip='Show differences',
261
item.connect('activate', self.diff_cb, tree, path)
264
item = Nautilus.MenuItem(name='BzrNautilus::commit',
266
tip='Commit Changes',
268
item.connect('activate', self.commit_cb, tree, path)
271
item = Nautilus.MenuItem(name='BzrNautilus::remove',
273
tip='Remove this file from versioning',
275
item.connect('activate', self.remove_cb, tree, path)
279
item = Nautilus.MenuItem(name='BzrNautilus::annotate',
280
label='Annotate ...',
281
tip='Annotate File Data',
283
item.connect('activate', self.annotate_cb, tree, path, file_id)
286
272
def get_file_items(self, window, files):
291
for vfs_file in files:
292
controldir, path = self._open_bzrdir(vfs_file)
295
tree = trees[controldir.user_url]
298
tree = controldir.open_workingtree()
299
except NoWorkingTree:
301
trees[controldir.user_url] = tree
304
nautilus_integration = self.check_branch_enabled(tree.branch)
305
if not nautilus_integration:
308
intertree = InterTree.get(tree.basis_tree(), tree)
309
items.extend(list(self._get_file_menuitems(tree, intertree, path)))
311
for tree in trees.itervalues():
276
for vfs_file in files:
277
# We can only cope with local files
278
if vfs_file.get_uri_scheme() != 'file':
281
file = vfs_file.get_uri()
283
tree, path = WorkingTree.open_containing(file)
284
except NotBranchError:
285
if not vfs_file.is_directory():
287
item = nautilus.MenuItem('BzrNautilus::newtree',
288
'Make directory versioned',
289
'Create new Bazaar tree in %s' % vfs_file.get_name())
290
item.connect('activate', self.newtree_cb, vfs_file)
292
# Refresh the list of filestatuses in the working tree
293
if path not in wtfiles.keys():
295
for rpath, file_class, kind, id, entry in tree.list_files():
296
wtfiles[rpath] = file_class
300
if wtfiles[path] == '?':
301
item = nautilus.MenuItem('BzrNautilus::add',
303
'Add as versioned file')
304
item.connect('activate', self.add_cb, vfs_file)
307
item = nautilus.MenuItem('BzrNautilus::ignore',
309
'Ignore file for versioning')
310
item.connect('activate', self.ignore_cb, vfs_file)
312
elif wtfiles[path] == 'I':
313
item = nautilus.MenuItem('BzrNautilus::unignore',
315
'Unignore file for versioning')
316
item.connect('activate', self.unignore_cb, vfs_file)
318
elif wtfiles[path] == 'V':
319
item = nautilus.MenuItem('BzrNautilus::log',
322
item.connect('activate', self.log_cb, vfs_file)
325
item = nautilus.MenuItem('BzrNautilus::diff',
328
item.connect('activate', self.diff_cb, vfs_file)
331
item = nautilus.MenuItem('BzrNautilus::remove',
333
'Remove this file from versioning')
334
item.connect('activate', self.remove_cb, vfs_file)
337
item = nautilus.MenuItem('BzrNautilus::annotate',
339
'Annotate File Data')
340
item.connect('activate', self.annotate_cb, vfs_file)
343
item = nautilus.MenuItem('BzrNautilus::commit',
346
item.connect('activate', self.commit_cb, vfs_file)
316
351
def get_columns(self):
318
Nautilus.Column(name="BzrNautilus::bzr_status",
319
attribute="bzr_status",
321
description="Version control status"),
322
Nautilus.Column(name="BzrNautilus::bzr_revision",
323
attribute="bzr_revision",
325
description="Last change revision"),
328
def _file_summary(self, tree, basis_tree, intertree, path):
352
return nautilus.Column("BzrNautilus::bzr_status",
355
"Version control status"),
357
def update_file_info(self, file):
358
if file.get_uri_scheme() != 'file':
362
tree, path = WorkingTree.open_containing(file.get_uri())
363
except NotBranchError:
332
file_id = tree.path2id(path)
334
if tree.is_ignored(path):
336
emblem = 'bzr-ignored'
338
status = 'unversioned'
339
file_revision = "N/A"
340
elif tree.has_filename(path): # Still present
341
if not intertree.file_content_matches(file_id, file_id):
342
if not basis_tree.has_id(file_id):
369
if tree.has_filename(path):
370
emblem = 'cvs-controlled'
372
id = tree.path2id(path)
374
delta = tree.changes_from(tree.branch.basis_tree())
375
if delta.touches_file_id(id):
376
emblem = 'cvs-modified'
378
for f, _, _ in delta.added:
345
file_revision = "new file"
346
elif basis_tree.path2id(file_id) != path:
347
status = 'bzr-renamed'
348
status = 'renamed from %s' % basis_tree.path2id(file_id)
350
emblem = 'bzr-modified'
353
emblem = 'bzr-controlled'
355
elif basis_tree.has_filename(path):
356
emblem = 'bzr-removed'
383
for of, f, _, _, _, _ in delta.renamed:
385
status = 'renamed from %s' % f
387
elif tree.branch.basis_tree().has_filename(path):
388
emblem = 'cvs-removed'
357
389
status = 'removed'
359
391
# FIXME: Check for ignored files
360
392
status = 'unversioned'
361
return (status, emblem, file_revision)
363
def update_file_info(self, vfs_file):
365
controldir, path = self._open_bzrdir(vfs_file)
366
except NotBranchError:
370
tree = controldir.open_workingtree()
371
except NoWorkingTree:
376
nautilus_integration = self.check_branch_enabled(tree.branch)
377
if not nautilus_integration:
380
basis_tree = tree.basis_tree()
381
intertree = InterTree.get(basis_tree, tree)
383
basis_tree.lock_read()
385
(status, emblem, file_revision) = self._file_summary(tree, basis_tree, intertree, path)
388
if emblem is not None:
389
vfs_file.add_emblem(emblem)
390
vfs_file.add_string_attribute('bzr_status', status)
391
vfs_file.add_string_attribute('bzr_revision', file_revision)
395
def check_branch_enabled(self, branch):
396
# Supports global disable, but there is currently no UI to do this
397
config = branch.get_config_stack()
398
return config.get("nautilus_integration")
400
def toggle_integration(self, menu, action, branch):
401
config = branch.get_config_stack()
402
config.set("nautilus_integration", action)
404
def get_property_pages(self, files):
406
for vfs_file in files:
408
controldir, path = self._open_bzrdir(vfs_file)
409
except NotBranchError:
413
tree = controldir.open_workingtree()
414
except NoWorkingTree:
419
file_id = tree.path2id(path)
420
pages.append(PropertyPageFile(tree, file_id, path))
421
pages.append(PropertyPageBranch(tree.branch))
426
def get_widget(self, uri, window):
427
controldir, path = ControlDir.open_containing(uri)
429
tree = controldir.open_workingtree()
430
except NoWorkingTree:
432
ret = Gtk.HBox(False, 4)
433
text = 'This is a Bazaar working tree. '
434
get_shelf_manager = getattr(tree, 'get_shelf_manager', None)
435
if get_shelf_manager is not None:
436
manager = get_shelf_manager()
437
shelves = manager.active_shelves()
438
if len(shelves) == 0:
440
elif len(shelves) == 1:
441
text += '1 shelf exists. '
443
text += '%d shelf exists. ' % len(shelves)
444
label = Gtk.Label(text)
446
ret.pack_start(label, True, True, 0)
451
class PropertyPageFile(Nautilus.PropertyPage):
453
def __init__(self, tree, file_id, path):
455
self.file_id = file_id
457
label = Gtk.Label('File Version')
460
table = self._create_table()
462
super(PropertyPageFile, self).__init__(label=label,
463
name="BzrNautilus::file_page", page=table)
465
def _create_table(self):
466
table = Gtk.Table(homogeneous=False, columns=2, rows=3)
468
table.attach(Gtk.Label(_i18n('File id:')), 0, 1, 0, 1)
469
table.attach(Gtk.Label(self.file_id), 1, 2, 0, 1)
471
table.attach(Gtk.Label(_i18n('SHA1Sum:')), 0, 1, 1, 2)
472
table.attach(Gtk.Label(self.tree.get_file_sha1(self.file_id, self.path)), 1, 1, 1, 2)
474
basis_tree = self.tree.revision_tree(self.tree.last_revision())
475
last_revision = basis_tree.get_file_revision(self.file_id)
477
table.attach(Gtk.Label(_i18n('Last Change Revision:')), 0, 1, 2, 3)
478
revno = ".".join([str(x) for x in
479
self.tree.branch.revision_id_to_dotted_revno(last_revision)])
480
table.attach(Gtk.Label(revno), 1, 1, 2, 3)
482
table.attach(Gtk.Label(_i18n('Last Change Author:')), 0, 1, 3, 4)
483
rev = self.tree.branch.repository.get_revision(last_revision)
484
table.attach(Gtk.Label("\n".join(rev.get_apparent_authors())), 1, 1, 3, 4)
490
class PropertyPageBranch(Nautilus.PropertyPage):
492
def __init__(self, branch):
494
label = Gtk.Label('Branch')
497
table = self._create_table()
499
super(PropertyPageBranch, self).__init__(label=label,
500
name="BzrNautilus::branch_page", page=table)
502
def _create_location_entry(self, get_location, set_location):
503
location = get_location()
505
if location is not None:
506
ret.set_text(location)
509
def _create_table(self):
510
table = Gtk.Table(homogeneous=False, columns=2, rows=6)
512
self._push_location_entry = self._create_location_entry(
513
self.branch.get_push_location, self.branch.set_push_location)
514
self._parent_location_entry = self._create_location_entry(
515
self.branch.get_parent, self.branch.set_parent)
516
self._bound_location_entry = self._create_location_entry(
517
self.branch.get_bound_location, self.branch.set_bound_location)
518
self._public_location_entry = self._create_location_entry(
519
self.branch.get_public_branch, self.branch.set_public_branch)
520
self._submit_location_entry = self._create_location_entry(
521
self.branch.get_submit_branch, self.branch.set_submit_branch)
523
table.attach(Gtk.Label(_i18n('Push location:')), 0, 1, 0, 1)
524
table.attach(self._push_location_entry, 1, 2, 0, 1)
526
table.attach(Gtk.Label(_i18n('Parent location:')), 0, 1, 1, 2)
527
table.attach(self._parent_location_entry, 1, 1, 1, 2)
529
table.attach(Gtk.Label(_i18n('Bound location:')), 0, 1, 2, 3)
530
table.attach(self._bound_location_entry, 1, 1, 2, 3)
532
table.attach(Gtk.Label(_i18n('Public location:')), 0, 1, 3, 4)
533
table.attach(self._public_location_entry, 1, 1, 3, 4)
535
table.attach(Gtk.Label(_i18n('Submit location:')), 0, 1, 4, 5)
536
table.attach(self._submit_location_entry, 1, 1, 4, 5)
538
self._append_revisions_only = Gtk.CheckButton(_i18n('Append revisions only'))
539
value = self.branch.get_append_revisions_only()
542
self._append_revisions_only.set_active(value)
543
table.attach(self._append_revisions_only, 0, 2, 5, 6)
394
if emblem is not None:
395
file.add_emblem(emblem)
396
file.add_string_attribute('bzr_status', status)