1
# Copyright (C) 2005-2010 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
__all__ = ['show_bzrdir_info']
19
from cStringIO import StringIO
31
from bzrlib.errors import (NoWorkingTree, NotBranchError,
32
NoRepositoryPresent, NotLocalUrl)
33
from bzrlib.missing import find_unmerged
36
def plural(n, base='', pl=None):
45
class LocationList(object):
47
def __init__(self, base_path):
49
self.base_path = base_path
51
def add_url(self, label, url):
52
"""Add a URL to the list, converting it to a path if possible"""
56
path = urlutils.local_path_from_url(url)
57
except errors.InvalidURL:
58
self.locs.append((label, url))
60
self.add_path(label, path)
62
def add_path(self, label, path):
63
"""Add a path, converting it to a relative path if possible"""
65
path = osutils.relpath(self.base_path, path)
66
except errors.PathNotChild:
72
path = path.rstrip('/')
73
self.locs.append((label, path))
76
max_len = max(len(l) for l, u in self.locs)
77
return [" %*s: %s\n" % (max_len, l, u) for l, u in self.locs ]
80
def gather_location_info(repository=None, branch=None, working=None,
83
if branch is not None:
84
branch_path = branch.user_url
85
master_path = branch.get_bound_location()
86
if master_path is None:
87
master_path = branch_path
92
if control is not None and control.get_branch_reference():
93
locs['checkout of branch'] = control.get_branch_reference()
94
except NotBranchError:
97
working_path = working.user_url
98
if working_path != branch_path:
99
locs['light checkout root'] = working_path
100
if master_path != branch_path:
101
if repository.is_shared():
102
locs['repository checkout root'] = branch_path
104
locs['checkout root'] = branch_path
105
if working_path != master_path:
106
locs['checkout of branch'] = master_path
107
elif repository.is_shared():
108
locs['repository branch'] = branch_path
109
elif branch_path is not None:
111
locs['branch root'] = branch_path
114
if repository is not None and repository.is_shared():
115
# lightweight checkout of branch in shared repository
116
if branch_path is not None:
117
locs['repository branch'] = branch_path
118
elif branch_path is not None:
120
locs['branch root'] = branch_path
121
elif repository is not None:
122
locs['repository'] = repository.user_url
123
elif control is not None:
124
locs['control directory'] = control.user_url
126
# Really, at least a control directory should be
127
# passed in for this method to be useful.
129
if master_path != branch_path:
130
locs['bound to branch'] = master_path
131
if repository is not None and repository.is_shared():
132
# lightweight checkout of branch in shared repository
133
locs['shared repository'] = repository.user_url
134
order = ['control directory', 'light checkout root',
135
'repository checkout root', 'checkout root',
136
'checkout of branch', 'shared repository',
137
'repository', 'repository branch', 'branch root',
139
return [(n, locs[n]) for n in order if n in locs]
142
def _show_location_info(locs, outfile):
143
"""Show known locations for working, branch and repository."""
144
outfile.write('Location:\n')
145
path_list = LocationList(osutils.getcwd())
146
for name, loc in locs:
147
path_list.add_url(name, loc)
148
outfile.writelines(path_list.get_lines())
151
def _gather_related_branches(branch):
152
locs = LocationList(osutils.getcwd())
153
locs.add_url('public branch', branch.get_public_branch())
154
locs.add_url('push branch', branch.get_push_location())
155
locs.add_url('parent branch', branch.get_parent())
156
locs.add_url('submit branch', branch.get_submit_branch())
158
locs.add_url('stacked on', branch.get_stacked_on_url())
159
except (errors.UnstackableBranchFormat, errors.UnstackableRepositoryFormat,
165
def _show_related_info(branch, outfile):
166
"""Show parent and push location of branch."""
167
locs = _gather_related_branches(branch)
168
if len(locs.locs) > 0:
170
outfile.write('Related branches:\n')
171
outfile.writelines(locs.get_lines())
174
def _show_control_dir_info(control, outfile):
175
"""Show control dir information."""
176
if control._format.colocated_branches:
178
outfile.write('Control directory:\n')
179
outfile.write(' %d branches\n' % len(control.list_branches()))
182
def _show_format_info(control=None, repository=None, branch=None,
183
working=None, outfile=None):
184
"""Show known formats for control, working, branch and repository."""
186
outfile.write('Format:\n')
188
outfile.write(' control: %s\n' %
189
control._format.get_format_description())
191
outfile.write(' working tree: %s\n' %
192
working._format.get_format_description())
194
outfile.write(' branch: %s\n' %
195
branch._format.get_format_description())
197
outfile.write(' repository: %s\n' %
198
repository._format.get_format_description())
201
def _show_locking_info(repository, branch=None, working=None, outfile=None):
202
"""Show locking status of working, branch and repository."""
203
if (repository.get_physical_lock_status() or
204
(branch and branch.get_physical_lock_status()) or
205
(working and working.get_physical_lock_status())):
207
outfile.write('Lock status:\n')
209
if working.get_physical_lock_status():
213
outfile.write(' working tree: %s\n' % status)
215
if branch.get_physical_lock_status():
219
outfile.write(' branch: %s\n' % status)
221
if repository.get_physical_lock_status():
225
outfile.write(' repository: %s\n' % status)
228
def _show_missing_revisions_branch(branch, outfile):
229
"""Show missing master revisions in branch."""
230
# Try with inaccessible branch ?
231
master = branch.get_master_branch()
233
local_extra, remote_extra = find_unmerged(branch, master)
236
outfile.write(('Branch is out of date: missing %d '
237
'revision%s.\n') % (len(remote_extra),
238
plural(len(remote_extra))))
241
def _show_missing_revisions_working(working, outfile):
242
"""Show missing revisions in working tree."""
243
branch = working.branch
244
basis = working.basis_tree()
246
branch_revno, branch_last_revision = branch.last_revision_info()
247
except errors.UnsupportedOperation:
250
tree_last_id = working.get_parent_ids()[0]
254
if branch_revno and tree_last_id != branch_last_revision:
255
tree_last_revno = branch.revision_id_to_revno(tree_last_id)
256
missing_count = branch_revno - tree_last_revno
258
outfile.write(('Working tree is out of date: missing %d '
259
'revision%s.\n') % (missing_count, plural(missing_count)))
262
def _show_working_stats(working, outfile):
263
"""Show statistics about a working tree."""
264
basis = working.basis_tree()
265
delta = working.changes_from(basis, want_unchanged=True)
268
outfile.write('In the working tree:\n')
269
outfile.write(' %8s unchanged\n' % len(delta.unchanged))
270
outfile.write(' %8d modified\n' % len(delta.modified))
271
outfile.write(' %8d added\n' % len(delta.added))
272
outfile.write(' %8d removed\n' % len(delta.removed))
273
outfile.write(' %8d renamed\n' % len(delta.renamed))
275
ignore_cnt = unknown_cnt = 0
276
for path in working.extras():
277
if working.is_ignored(path):
281
outfile.write(' %8d unknown\n' % unknown_cnt)
282
outfile.write(' %8d ignored\n' % ignore_cnt)
285
root_id = working.get_root_id()
286
for path, entry in working.iter_entries_by_dir():
287
if entry.kind == 'directory' and entry.file_id != root_id:
289
outfile.write(' %8d versioned %s\n' % (dir_cnt,
290
plural(dir_cnt, 'subdirectory', 'subdirectories')))
293
def _show_branch_stats(branch, verbose, outfile):
294
"""Show statistics about a branch."""
296
revno, head = branch.last_revision_info()
297
except errors.UnsupportedOperation:
300
outfile.write('Branch history:\n')
301
outfile.write(' %8d revision%s\n' % (revno, plural(revno)))
302
stats = branch.repository.gather_stats(head, committers=verbose)
304
committers = stats['committers']
305
outfile.write(' %8d committer%s\n' % (committers,
308
timestamp, timezone = stats['firstrev']
309
age = int((time.time() - timestamp) / 3600 / 24)
310
outfile.write(' %8d day%s old\n' % (age, plural(age)))
311
outfile.write(' first revision: %s\n' %
312
osutils.format_date(timestamp, timezone))
313
timestamp, timezone = stats['latestrev']
314
outfile.write(' latest revision: %s\n' %
315
osutils.format_date(timestamp, timezone))
319
def _show_repository_info(repository, outfile):
320
"""Show settings of a repository."""
321
if repository.make_working_trees():
323
outfile.write('Create working tree for new branches inside '
327
def _show_repository_stats(repository, stats, outfile):
328
"""Show statistics about a repository."""
330
if 'revisions' in stats:
331
revisions = stats['revisions']
332
f.write(' %8d revision%s\n' % (revisions, plural(revisions)))
334
f.write(' %8d KiB\n' % (stats['size']/1024))
335
for hook in hooks['repository']:
336
hook(repository, stats, f)
337
if f.getvalue() != "":
339
outfile.write('Repository:\n')
340
outfile.write(f.getvalue())
343
def show_bzrdir_info(a_bzrdir, verbose=False, outfile=None):
344
"""Output to stdout the 'info' for a_bzrdir."""
348
tree = a_bzrdir.open_workingtree(
349
recommend_upgrade=False)
350
except (NoWorkingTree, NotLocalUrl, NotBranchError):
353
branch = a_bzrdir.open_branch()
354
except NotBranchError:
357
repository = a_bzrdir.open_repository()
358
except NoRepositoryPresent:
362
lockable = repository
364
repository = branch.repository
368
repository = branch.repository
371
if lockable is not None:
374
show_component_info(a_bzrdir, repository, branch, tree, verbose,
377
if lockable is not None:
381
def show_component_info(control, repository, branch=None, working=None,
382
verbose=1, outfile=None):
383
"""Write info about all bzrdir components to stdout"""
390
layout = describe_layout(repository, branch, working, control)
391
format = describe_format(control, repository, branch, working)
392
outfile.write("%s (format: %s)\n" % (layout, format))
394
gather_location_info(control=control, repository=repository,
395
branch=branch, working=working),
397
if branch is not None:
398
_show_related_info(branch, outfile)
401
_show_format_info(control, repository, branch, working, outfile)
402
_show_locking_info(repository, branch, working, outfile)
403
_show_control_dir_info(control, outfile)
404
if branch is not None:
405
_show_missing_revisions_branch(branch, outfile)
406
if working is not None:
407
_show_missing_revisions_working(working, outfile)
408
_show_working_stats(working, outfile)
409
elif branch is not None:
410
_show_missing_revisions_branch(branch, outfile)
411
if branch is not None:
412
show_committers = verbose >= 2
413
stats = _show_branch_stats(branch, show_committers, outfile)
415
stats = repository.gather_stats()
416
if branch is None and working is None:
417
_show_repository_info(repository, outfile)
418
_show_repository_stats(repository, stats, outfile)
421
def describe_layout(repository=None, branch=None, tree=None, control=None):
422
"""Convert a control directory layout into a user-understandable term
424
Common outputs include "Standalone tree", "Repository branch" and
425
"Checkout". Uncommon outputs include "Unshared repository with trees"
426
and "Empty control directory"
428
if branch is None and control is not None:
430
branch_reference = control.get_branch_reference()
431
except NotBranchError:
434
if branch_reference is not None:
435
return "Dangling branch reference"
436
if repository is None:
437
return 'Empty control directory'
438
if branch is None and tree is None:
439
if repository.is_shared():
440
phrase = 'Shared repository'
442
phrase = 'Unshared repository'
443
if repository.make_working_trees():
444
phrase += ' with trees'
447
if repository.is_shared():
448
independence = "Repository "
450
independence = "Standalone "
455
if branch is None and tree is not None:
456
phrase = "branchless tree"
458
if (tree is not None and tree.user_url !=
461
phrase = "Lightweight checkout"
462
elif branch.get_bound_location() is not None:
463
if independence == 'Standalone ':
466
phrase = "Bound branch"
469
if independence != "":
470
phrase = phrase.lower()
471
return "%s%s" % (independence, phrase)
474
def describe_format(control, repository, branch, tree):
475
"""Determine the format of an existing control directory
477
Several candidates may be found. If so, the names are returned as a
478
single string, separated by ' or '.
480
If no matching candidate is found, "unnamed" is returned.
483
if (branch is not None and tree is not None and
484
branch.user_url != tree.user_url):
487
non_aliases = set(controldir.format_registry.keys())
488
non_aliases.difference_update(controldir.format_registry.aliases())
489
for key in non_aliases:
490
format = controldir.format_registry.make_bzrdir(key)
491
if isinstance(format, bzrdir.BzrDirMetaFormat1):
492
if (tree and format.workingtree_format !=
495
if (branch and format.get_branch_format() !=
498
if (repository and format.repository_format !=
501
if format.__class__ is not control._format.__class__:
503
candidates.append(key)
504
if len(candidates) == 0:
507
new_candidates = [c for c in candidates if not
508
controldir.format_registry.get_info(c).hidden]
509
if len(new_candidates) > 0:
510
# If there are any non-hidden formats that match, only return those to
511
# avoid listing hidden formats except when only a hidden format will
513
candidates = new_candidates
514
return ' or '.join(candidates)
517
class InfoHooks(_mod_hooks.Hooks):
518
"""Hooks for the info command."""
521
super(InfoHooks, self).__init__("bzrlib.info", "hooks")
522
self.add_hook('repository',
523
"Invoked when displaying the statistics for a repository. "
524
"repository is called with a statistics dictionary as returned "
525
"by the repository and a file-like object to write to.", (1, 15))