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
from __future__ import absolute_import
19
__all__ = ['show_bzrdir_info']
21
from io import StringIO
26
branch as _mod_branch,
36
from .errors import (NoWorkingTree, NotBranchError,
37
NoRepositoryPresent, NotLocalUrl)
38
from .missing import find_unmerged
41
def plural(n, base=u'', pl=None):
50
class LocationList(object):
52
def __init__(self, base_path):
54
self.base_path = base_path
56
def add_url(self, label, url):
57
"""Add a URL to the list, converting it to a path if possible"""
61
path = urlutils.local_path_from_url(url)
62
except urlutils.InvalidURL:
63
self.locs.append((label, url))
65
self.add_path(label, path)
67
def add_path(self, label, path):
68
"""Add a path, converting it to a relative path if possible"""
70
path = osutils.relpath(self.base_path, path)
71
except errors.PathNotChild:
77
path = path.rstrip('/')
78
self.locs.append((label, path))
81
max_len = max(len(l) for l, u in self.locs)
82
return [" %*s: %s\n" % (max_len, l, u) for l, u in self.locs]
85
def gather_location_info(repository=None, branch=None, working=None,
88
if branch is not None:
89
branch_path = branch.user_url
90
master_path = branch.get_bound_location()
91
if master_path is None:
92
master_path = branch_path
97
if control is not None and control.get_branch_reference():
98
locs['checkout of branch'] = control.get_branch_reference()
99
except NotBranchError:
102
working_path = working.user_url
103
if working_path != branch_path:
104
locs['light checkout root'] = working_path
105
if master_path != branch_path:
106
if repository.is_shared():
107
locs['repository checkout root'] = branch_path
109
locs['checkout root'] = branch_path
110
if working_path != master_path:
111
locs['checkout of branch'] = master_path
112
elif repository.is_shared():
113
locs['repository branch'] = branch_path
114
elif branch_path is not None:
116
locs['branch root'] = branch_path
119
if repository is not None and repository.is_shared():
120
# lightweight checkout of branch in shared repository
121
if branch_path is not None:
122
locs['repository branch'] = branch_path
123
elif branch_path is not None:
125
locs['branch root'] = branch_path
126
elif repository is not None:
127
locs['repository'] = repository.user_url
128
elif control is not None:
129
locs['control directory'] = control.user_url
131
# Really, at least a control directory should be
132
# passed in for this method to be useful.
134
if master_path != branch_path:
135
locs['bound to branch'] = master_path
136
if repository is not None and repository.is_shared():
137
# lightweight checkout of branch in shared repository
138
locs['shared repository'] = repository.user_url
139
order = ['control directory', 'light checkout root',
140
'repository checkout root', 'checkout root',
141
'checkout of branch', 'shared repository',
142
'repository', 'repository branch', 'branch root',
144
return [(n, locs[n]) for n in order if n in locs]
147
def _show_location_info(locs, outfile):
148
"""Show known locations for working, branch and repository."""
149
outfile.write('Location:\n')
150
path_list = LocationList(osutils.getcwd())
151
for name, loc in locs:
152
path_list.add_url(name, loc)
153
outfile.writelines(path_list.get_lines())
156
def _gather_related_branches(branch):
157
locs = LocationList(osutils.getcwd())
158
locs.add_url('public branch', branch.get_public_branch())
159
locs.add_url('push branch', branch.get_push_location())
160
locs.add_url('parent branch', branch.get_parent())
161
locs.add_url('submit branch', branch.get_submit_branch())
163
locs.add_url('stacked on', branch.get_stacked_on_url())
164
except (_mod_branch.UnstackableBranchFormat, errors.UnstackableRepositoryFormat,
170
def _show_related_info(branch, outfile):
171
"""Show parent and push location of branch."""
172
locs = _gather_related_branches(branch)
173
if len(locs.locs) > 0:
175
outfile.write('Related branches:\n')
176
outfile.writelines(locs.get_lines())
179
def _show_control_dir_info(control, outfile):
180
"""Show control dir information."""
181
if control._format.colocated_branches:
183
outfile.write('Control directory:\n')
184
outfile.write(' %d branches\n' % len(control.list_branches()))
187
def _show_format_info(control=None, repository=None, branch=None,
188
working=None, outfile=None):
189
"""Show known formats for control, working, branch and repository."""
191
outfile.write('Format:\n')
193
outfile.write(' control: %s\n' %
194
control._format.get_format_description())
196
outfile.write(' working tree: %s\n' %
197
working._format.get_format_description())
199
outfile.write(' branch: %s\n' %
200
branch._format.get_format_description())
202
outfile.write(' repository: %s\n' %
203
repository._format.get_format_description())
206
def _show_locking_info(repository=None, branch=None, working=None,
208
"""Show locking status of working, branch and repository."""
209
if (repository and repository.get_physical_lock_status() or
210
(branch and branch.get_physical_lock_status()) or
211
(working and working.get_physical_lock_status())):
213
outfile.write('Lock status:\n')
215
if working.get_physical_lock_status():
219
outfile.write(' working tree: %s\n' % status)
221
if branch.get_physical_lock_status():
225
outfile.write(' branch: %s\n' % status)
227
if repository.get_physical_lock_status():
231
outfile.write(' repository: %s\n' % status)
234
def _show_missing_revisions_branch(branch, outfile):
235
"""Show missing master revisions in branch."""
236
# Try with inaccessible branch ?
237
master = branch.get_master_branch()
239
local_extra, remote_extra = find_unmerged(branch, master)
242
outfile.write(('Branch is out of date: missing %d '
243
'revision%s.\n') % (len(remote_extra),
244
plural(len(remote_extra))))
247
def _show_missing_revisions_working(working, outfile):
248
"""Show missing revisions in working tree."""
249
branch = working.branch
251
branch_revno, branch_last_revision = branch.last_revision_info()
252
except errors.UnsupportedOperation:
255
tree_last_id = working.get_parent_ids()[0]
259
if branch_revno and tree_last_id != branch_last_revision:
260
tree_last_revno = branch.revision_id_to_revno(tree_last_id)
261
missing_count = branch_revno - tree_last_revno
263
outfile.write(('Working tree is out of date: missing %d '
264
'revision%s.\n') % (missing_count, plural(missing_count)))
267
def _show_working_stats(working, outfile):
268
"""Show statistics about a working tree."""
269
basis = working.basis_tree()
270
delta = working.changes_from(basis, want_unchanged=True)
273
outfile.write('In the working tree:\n')
274
outfile.write(' %8s unchanged\n' % len(delta.unchanged))
275
outfile.write(' %8d modified\n' % len(delta.modified))
276
outfile.write(' %8d added\n' % len(delta.added))
277
outfile.write(' %8d removed\n' % len(delta.removed))
278
outfile.write(' %8d renamed\n' % len(delta.renamed))
280
ignore_cnt = unknown_cnt = 0
281
for path in working.extras():
282
if working.is_ignored(path):
286
outfile.write(' %8d unknown\n' % unknown_cnt)
287
outfile.write(' %8d ignored\n' % ignore_cnt)
290
for path, entry in working.iter_entries_by_dir():
291
if entry.kind == 'directory' and path != '':
293
outfile.write(' %8d versioned %s\n' % (dir_cnt,
294
plural(dir_cnt, 'subdirectory', 'subdirectories')))
297
def _show_branch_stats(branch, verbose, outfile):
298
"""Show statistics about a branch."""
300
revno, head = branch.last_revision_info()
301
except errors.UnsupportedOperation:
304
outfile.write('Branch history:\n')
305
outfile.write(' %8d revision%s\n' % (revno, plural(revno)))
306
stats = branch.repository.gather_stats(head, committers=verbose)
308
committers = stats['committers']
309
outfile.write(' %8d committer%s\n' % (committers,
312
timestamp, timezone = stats['firstrev']
313
age = int((time.time() - timestamp) / 3600 / 24)
314
outfile.write(' %8d day%s old\n' % (age, plural(age)))
315
outfile.write(' first revision: %s\n' %
316
osutils.format_date(timestamp, timezone))
317
timestamp, timezone = stats['latestrev']
318
outfile.write(' latest revision: %s\n' %
319
osutils.format_date(timestamp, timezone))
323
def _show_repository_info(repository, outfile):
324
"""Show settings of a repository."""
325
if repository.make_working_trees():
327
outfile.write('Create working tree for new branches inside '
331
def _show_repository_stats(repository, stats, outfile):
332
"""Show statistics about a repository."""
334
if 'revisions' in stats:
335
revisions = stats['revisions']
336
f.write(' %8d revision%s\n' % (revisions, plural(revisions)))
338
f.write(' %8d KiB\n' % (stats['size'] / 1024))
339
for hook in hooks['repository']:
340
hook(repository, stats, f)
341
if f.getvalue() != "":
343
outfile.write('Repository:\n')
344
outfile.write(f.getvalue())
347
def show_bzrdir_info(a_controldir, verbose=False, outfile=None):
348
"""Output to stdout the 'info' for a_controldir."""
352
tree = a_controldir.open_workingtree(
353
recommend_upgrade=False)
354
except (NoWorkingTree, NotLocalUrl, NotBranchError):
357
branch = a_controldir.open_branch(name="")
358
except NotBranchError:
361
repository = a_controldir.open_repository()
362
except NoRepositoryPresent:
366
lockable = repository
368
repository = branch.repository
372
repository = branch.repository
375
if lockable is not None:
378
show_component_info(a_controldir, repository, branch, tree, verbose,
381
if lockable is not None:
385
def show_component_info(control, repository, branch=None, working=None,
386
verbose=1, outfile=None):
387
"""Write info about all bzrdir components to stdout"""
394
layout = describe_layout(repository, branch, working, control)
395
format = describe_format(control, repository, branch, working)
396
outfile.write("%s (format: %s)\n" % (layout, format))
398
gather_location_info(control=control, repository=repository,
399
branch=branch, working=working),
401
if branch is not None:
402
_show_related_info(branch, outfile)
405
_show_format_info(control, repository, branch, working, outfile)
406
_show_locking_info(repository, branch, working, outfile)
407
_show_control_dir_info(control, outfile)
408
if branch is not None:
409
_show_missing_revisions_branch(branch, outfile)
410
if working is not None:
411
_show_missing_revisions_working(working, outfile)
412
_show_working_stats(working, outfile)
413
elif branch is not None:
414
_show_missing_revisions_branch(branch, outfile)
415
if branch is not None:
416
show_committers = verbose >= 2
417
stats = _show_branch_stats(branch, show_committers, outfile)
418
elif repository is not None:
419
stats = repository.gather_stats()
420
if branch is None and working is None and repository is not None:
421
_show_repository_info(repository, outfile)
422
if repository is not None:
423
_show_repository_stats(repository, stats, outfile)
426
def describe_layout(repository=None, branch=None, tree=None, control=None):
427
"""Convert a control directory layout into a user-understandable term
429
Common outputs include "Standalone tree", "Repository branch" and
430
"Checkout". Uncommon outputs include "Unshared repository with trees"
431
and "Empty control directory"
433
if branch is None and control is not None:
435
branch_reference = control.get_branch_reference()
436
except NotBranchError:
439
if branch_reference is not None:
440
return "Dangling branch reference"
441
if repository is None:
442
return 'Empty control directory'
443
if branch is None and tree is None:
444
if repository.is_shared():
445
phrase = 'Shared repository'
447
phrase = 'Unshared repository'
449
if repository.make_working_trees():
450
extra.append('trees')
451
if len(control.get_branches()) > 0:
452
extra.append('colocated branches')
454
phrase += ' with ' + " and ".join(extra)
457
if repository.is_shared():
458
independence = "Repository "
460
independence = "Standalone "
465
if branch is None and tree is not None:
466
phrase = "branchless tree"
468
if (tree is not None and tree.controldir.control_url !=
469
branch.controldir.control_url):
471
phrase = "Lightweight checkout"
472
elif branch.get_bound_location() is not None:
473
if independence == 'Standalone ':
476
phrase = "Bound branch"
479
if independence != "":
480
phrase = phrase.lower()
481
return "%s%s" % (independence, phrase)
484
def describe_format(control, repository, branch, tree):
485
"""Determine the format of an existing control directory
487
Several candidates may be found. If so, the names are returned as a
488
single string, separated by ' or '.
490
If no matching candidate is found, "unnamed" is returned.
493
if (branch is not None and tree is not None and
494
branch.user_url != tree.user_url):
497
non_aliases = set(controldir.format_registry.keys())
498
non_aliases.difference_update(controldir.format_registry.aliases())
499
for key in non_aliases:
500
format = controldir.format_registry.make_controldir(key)
501
if isinstance(format, bzrdir.BzrDirMetaFormat1):
502
if (tree and format.workingtree_format !=
505
if (branch and format.get_branch_format() !=
508
if (repository and format.repository_format !=
511
if format.__class__ is not control._format.__class__:
513
candidates.append(key)
514
if len(candidates) == 0:
517
new_candidates = [c for c in candidates if not
518
controldir.format_registry.get_info(c).hidden]
519
if len(new_candidates) > 0:
520
# If there are any non-hidden formats that match, only return those to
521
# avoid listing hidden formats except when only a hidden format will
523
candidates = new_candidates
524
return ' or '.join(candidates)
527
class InfoHooks(_mod_hooks.Hooks):
528
"""Hooks for the info command."""
531
super(InfoHooks, self).__init__("breezy.info", "hooks")
534
"Invoked when displaying the statistics for a repository. "
535
"repository is called with a statistics dictionary as returned "
536
"by the repository and a file-like object to write to.", (1, 15))