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
 
30
 
from bzrlib.errors import (NoWorkingTree, NotBranchError,
 
31
 
                           NoRepositoryPresent, NotLocalUrl)
 
32
 
from bzrlib.missing import find_unmerged
 
35
 
def plural(n, base='', pl=None):
 
44
 
class LocationList(object):
 
46
 
    def __init__(self, base_path):
 
48
 
        self.base_path = base_path
 
50
 
    def add_url(self, label, url):
 
51
 
        """Add a URL to the list, converting it to a path if possible"""
 
55
 
            path = urlutils.local_path_from_url(url)
 
56
 
        except errors.InvalidURL:
 
57
 
            self.locs.append((label, url))
 
59
 
            self.add_path(label, path)
 
61
 
    def add_path(self, label, path):
 
62
 
        """Add a path, converting it to a relative path if possible"""
 
64
 
            path = osutils.relpath(self.base_path, path)
 
65
 
        except errors.PathNotChild:
 
71
 
            path = path.rstrip('/')
 
72
 
        self.locs.append((label, path))
 
75
 
        max_len = max(len(l) for l, u in self.locs)
 
76
 
        return ["  %*s: %s\n" % (max_len, l, u) for l, u in self.locs ]
 
79
 
def gather_location_info(repository, branch=None, working=None):
 
81
 
    repository_path = repository.user_url
 
82
 
    if branch is not None:
 
83
 
        branch_path = branch.user_url
 
84
 
        master_path = branch.get_bound_location()
 
85
 
        if master_path is None:
 
86
 
            master_path = branch_path
 
91
 
        working_path = working.user_url
 
92
 
        if working_path != branch_path:
 
93
 
            locs['light checkout root'] = working_path
 
94
 
        if master_path != branch_path:
 
95
 
            if repository.is_shared():
 
96
 
                locs['repository checkout root'] = branch_path
 
98
 
                locs['checkout root'] = branch_path
 
99
 
        if working_path != master_path:
 
100
 
            locs['checkout of branch'] = master_path
 
101
 
        elif repository.is_shared():
 
102
 
            locs['repository branch'] = branch_path
 
103
 
        elif branch_path is not None:
 
105
 
            locs['branch root'] = branch_path
 
108
 
        if repository.is_shared():
 
109
 
            # lightweight checkout of branch in shared repository
 
110
 
            if branch_path is not None:
 
111
 
                locs['repository branch'] = branch_path
 
112
 
        elif branch_path is not None:
 
114
 
            locs['branch root'] = branch_path
 
115
 
            if master_path != branch_path:
 
116
 
                locs['bound to branch'] = master_path
 
118
 
            locs['repository'] = repository_path
 
119
 
    if repository.is_shared():
 
120
 
        # lightweight checkout of branch in shared repository
 
121
 
        locs['shared repository'] = repository_path
 
122
 
    order = ['light checkout root', 'repository checkout root',
 
123
 
             'checkout root', 'checkout of branch', 'shared repository',
 
124
 
             'repository', 'repository branch', 'branch root',
 
126
 
    return [(n, locs[n]) for n in order if n in locs]
 
129
 
def _show_location_info(locs, outfile):
 
130
 
    """Show known locations for working, branch and repository."""
 
131
 
    outfile.write('Location:\n')
 
132
 
    path_list = LocationList(osutils.getcwd())
 
133
 
    for name, loc in locs:
 
134
 
        path_list.add_url(name, loc)
 
135
 
    outfile.writelines(path_list.get_lines())
 
138
 
def _gather_related_branches(branch):
 
139
 
    locs = LocationList(osutils.getcwd())
 
140
 
    locs.add_url('public branch', branch.get_public_branch())
 
141
 
    locs.add_url('push branch', branch.get_push_location())
 
142
 
    locs.add_url('parent branch', branch.get_parent())
 
143
 
    locs.add_url('submit branch', branch.get_submit_branch())
 
145
 
        locs.add_url('stacked on', branch.get_stacked_on_url())
 
146
 
    except (errors.UnstackableBranchFormat, errors.UnstackableRepositoryFormat,
 
152
 
def _show_related_info(branch, outfile):
 
153
 
    """Show parent and push location of branch."""
 
154
 
    locs = _gather_related_branches(branch)
 
155
 
    if len(locs.locs) > 0:
 
157
 
        outfile.write('Related branches:\n')
 
158
 
        outfile.writelines(locs.get_lines())
 
161
 
def _show_format_info(control=None, repository=None, branch=None,
 
162
 
                      working=None, outfile=None):
 
163
 
    """Show known formats for control, working, branch and repository."""
 
165
 
    outfile.write('Format:\n')
 
167
 
        outfile.write('       control: %s\n' %
 
168
 
            control._format.get_format_description())
 
170
 
        outfile.write('  working tree: %s\n' %
 
171
 
            working._format.get_format_description())
 
173
 
        outfile.write('        branch: %s\n' %
 
174
 
            branch._format.get_format_description())
 
176
 
        outfile.write('    repository: %s\n' %
 
177
 
            repository._format.get_format_description())
 
180
 
def _show_locking_info(repository, branch=None, working=None, outfile=None):
 
181
 
    """Show locking status of working, branch and repository."""
 
182
 
    if (repository.get_physical_lock_status() or
 
183
 
        (branch and branch.get_physical_lock_status()) or
 
184
 
        (working and working.get_physical_lock_status())):
 
186
 
        outfile.write('Lock status:\n')
 
188
 
            if working.get_physical_lock_status():
 
192
 
            outfile.write('  working tree: %s\n' % status)
 
194
 
            if branch.get_physical_lock_status():
 
198
 
            outfile.write('        branch: %s\n' % status)
 
200
 
            if repository.get_physical_lock_status():
 
204
 
            outfile.write('    repository: %s\n' % status)
 
207
 
def _show_missing_revisions_branch(branch, outfile):
 
208
 
    """Show missing master revisions in branch."""
 
209
 
    # Try with inaccessible branch ?
 
210
 
    master = branch.get_master_branch()
 
212
 
        local_extra, remote_extra = find_unmerged(branch, master)
 
215
 
            outfile.write(('Branch is out of date: missing %d '
 
216
 
                'revision%s.\n') % (len(remote_extra),
 
217
 
                plural(len(remote_extra))))
 
220
 
def _show_missing_revisions_working(working, outfile):
 
221
 
    """Show missing revisions in working tree."""
 
222
 
    branch = working.branch
 
223
 
    basis = working.basis_tree()
 
224
 
    work_inv = working.inventory
 
225
 
    branch_revno, branch_last_revision = branch.last_revision_info()
 
227
 
        tree_last_id = working.get_parent_ids()[0]
 
231
 
    if branch_revno and tree_last_id != branch_last_revision:
 
232
 
        tree_last_revno = branch.revision_id_to_revno(tree_last_id)
 
233
 
        missing_count = branch_revno - tree_last_revno
 
235
 
        outfile.write(('Working tree is out of date: missing %d '
 
236
 
            'revision%s.\n') % (missing_count, plural(missing_count)))
 
239
 
def _show_working_stats(working, outfile):
 
240
 
    """Show statistics about a working tree."""
 
241
 
    basis = working.basis_tree()
 
242
 
    work_inv = working.inventory
 
243
 
    delta = working.changes_from(basis, want_unchanged=True)
 
246
 
    outfile.write('In the working tree:\n')
 
247
 
    outfile.write('  %8s unchanged\n' % len(delta.unchanged))
 
248
 
    outfile.write('  %8d modified\n' % len(delta.modified))
 
249
 
    outfile.write('  %8d added\n' % len(delta.added))
 
250
 
    outfile.write('  %8d removed\n' % len(delta.removed))
 
251
 
    outfile.write('  %8d renamed\n' % len(delta.renamed))
 
253
 
    ignore_cnt = unknown_cnt = 0
 
254
 
    for path in working.extras():
 
255
 
        if working.is_ignored(path):
 
259
 
    outfile.write('  %8d unknown\n' % unknown_cnt)
 
260
 
    outfile.write('  %8d ignored\n' % ignore_cnt)
 
263
 
    for file_id in work_inv:
 
264
 
        if (work_inv.get_file_kind(file_id) == 'directory' and
 
265
 
            not work_inv.is_root(file_id)):
 
267
 
    outfile.write('  %8d versioned %s\n' % (dir_cnt,
 
268
 
        plural(dir_cnt, 'subdirectory', 'subdirectories')))
 
271
 
def _show_branch_stats(branch, verbose, outfile):
 
272
 
    """Show statistics about a branch."""
 
273
 
    revno, head = branch.last_revision_info()
 
275
 
    outfile.write('Branch history:\n')
 
276
 
    outfile.write('  %8d revision%s\n' % (revno, plural(revno)))
 
277
 
    stats = branch.repository.gather_stats(head, committers=verbose)
 
279
 
        committers = stats['committers']
 
280
 
        outfile.write('  %8d committer%s\n' % (committers,
 
283
 
        timestamp, timezone = stats['firstrev']
 
284
 
        age = int((time.time() - timestamp) / 3600 / 24)
 
285
 
        outfile.write('  %8d day%s old\n' % (age, plural(age)))
 
286
 
        outfile.write('   first revision: %s\n' %
 
287
 
            osutils.format_date(timestamp, timezone))
 
288
 
        timestamp, timezone = stats['latestrev']
 
289
 
        outfile.write('  latest revision: %s\n' %
 
290
 
            osutils.format_date(timestamp, timezone))
 
294
 
def _show_repository_info(repository, outfile):
 
295
 
    """Show settings of a repository."""
 
296
 
    if repository.make_working_trees():
 
298
 
        outfile.write('Create working tree for new branches inside '
 
302
 
def _show_repository_stats(repository, stats, outfile):
 
303
 
    """Show statistics about a repository."""
 
305
 
    if 'revisions' in stats:
 
306
 
        revisions = stats['revisions']
 
307
 
        f.write('  %8d revision%s\n' % (revisions, plural(revisions)))
 
309
 
        f.write('  %8d KiB\n' % (stats['size']/1024))
 
310
 
    for hook in hooks['repository']:
 
311
 
        hook(repository, stats, f)
 
312
 
    if f.getvalue() != "":
 
314
 
        outfile.write('Repository:\n')
 
315
 
        outfile.write(f.getvalue())
 
318
 
def show_bzrdir_info(a_bzrdir, verbose=False, outfile=None):
 
319
 
    """Output to stdout the 'info' for a_bzrdir."""
 
323
 
        tree = a_bzrdir.open_workingtree(
 
324
 
            recommend_upgrade=False)
 
325
 
    except (NoWorkingTree, NotLocalUrl):
 
328
 
            branch = a_bzrdir.open_branch()
 
329
 
        except NotBranchError:
 
332
 
                repository = a_bzrdir.open_repository()
 
333
 
            except NoRepositoryPresent:
 
334
 
                # Return silently; cmd_info already returned NotBranchError
 
335
 
                # if no bzrdir could be opened.
 
338
 
                lockable = repository
 
340
 
            repository = branch.repository
 
344
 
        repository = branch.repository
 
349
 
        show_component_info(a_bzrdir, repository, branch, tree, verbose,
 
355
 
def show_component_info(control, repository, branch=None, working=None,
 
356
 
    verbose=1, outfile=None):
 
357
 
    """Write info about all bzrdir components to stdout"""
 
364
 
    layout = describe_layout(repository, branch, working)
 
365
 
    format = describe_format(control, repository, branch, working)
 
366
 
    outfile.write("%s (format: %s)\n" % (layout, format))
 
367
 
    _show_location_info(gather_location_info(repository, branch, working),
 
369
 
    if branch is not None:
 
370
 
        _show_related_info(branch, outfile)
 
373
 
    _show_format_info(control, repository, branch, working, outfile)
 
374
 
    _show_locking_info(repository, branch, working, outfile)
 
375
 
    if branch is not None:
 
376
 
        _show_missing_revisions_branch(branch, outfile)
 
377
 
    if working is not None:
 
378
 
        _show_missing_revisions_working(working, outfile)
 
379
 
        _show_working_stats(working, outfile)
 
380
 
    elif branch is not None:
 
381
 
        _show_missing_revisions_branch(branch, outfile)
 
382
 
    if branch is not None:
 
383
 
        show_committers = verbose >= 2
 
384
 
        stats = _show_branch_stats(branch, show_committers, outfile)
 
386
 
        stats = repository.gather_stats()
 
387
 
    if branch is None and working is None:
 
388
 
        _show_repository_info(repository, outfile)
 
389
 
    _show_repository_stats(repository, stats, outfile)
 
392
 
def describe_layout(repository=None, branch=None, tree=None):
 
393
 
    """Convert a control directory layout into a user-understandable term
 
395
 
    Common outputs include "Standalone tree", "Repository branch" and
 
396
 
    "Checkout".  Uncommon outputs include "Unshared repository with trees"
 
397
 
    and "Empty control directory"
 
399
 
    if repository is None:
 
400
 
        return 'Empty control directory'
 
401
 
    if branch is None and tree is None:
 
402
 
        if repository.is_shared():
 
403
 
            phrase = 'Shared repository'
 
405
 
            phrase = 'Unshared repository'
 
406
 
        if repository.make_working_trees():
 
407
 
            phrase += ' with trees'
 
410
 
        if repository.is_shared():
 
411
 
            independence = "Repository "
 
413
 
            independence = "Standalone "
 
418
 
        if branch is None and tree is not None:
 
419
 
            phrase = "branchless tree"
 
421
 
            if (tree is not None and tree.user_url !=
 
424
 
                phrase = "Lightweight checkout"
 
425
 
            elif branch.get_bound_location() is not None:
 
426
 
                if independence == 'Standalone ':
 
429
 
                    phrase = "Bound branch"
 
432
 
        if independence != "":
 
433
 
            phrase = phrase.lower()
 
434
 
        return "%s%s" % (independence, phrase)
 
437
 
def describe_format(control, repository, branch, tree):
 
438
 
    """Determine the format of an existing control directory
 
440
 
    Several candidates may be found.  If so, the names are returned as a
 
441
 
    single string, separated by ' or '.
 
443
 
    If no matching candidate is found, "unnamed" is returned.
 
446
 
    if (branch is not None and tree is not None and
 
447
 
        branch.user_url != tree.user_url):
 
450
 
    non_aliases = set(bzrdir.format_registry.keys())
 
451
 
    non_aliases.difference_update(bzrdir.format_registry.aliases())
 
452
 
    for key in non_aliases:
 
453
 
        format = bzrdir.format_registry.make_bzrdir(key)
 
454
 
        if isinstance(format, bzrdir.BzrDirMetaFormat1):
 
455
 
            if (tree and format.workingtree_format !=
 
458
 
            if (branch and format.get_branch_format() !=
 
461
 
            if (repository and format.repository_format !=
 
464
 
        if format.__class__ is not control._format.__class__:
 
466
 
        candidates.append(key)
 
467
 
    if len(candidates) == 0:
 
470
 
    new_candidates = [c for c in candidates if not
 
471
 
        bzrdir.format_registry.get_info(c).hidden]
 
472
 
    if len(new_candidates) > 0:
 
473
 
        # If there are any non-hidden formats that match, only return those to
 
474
 
        # avoid listing hidden formats except when only a hidden format will
 
476
 
        candidates = new_candidates
 
477
 
    return ' or '.join(candidates)
 
480
 
class InfoHooks(_mod_hooks.Hooks):
 
481
 
    """Hooks for the info command."""
 
484
 
        super(InfoHooks, self).__init__()
 
485
 
        self.create_hook(_mod_hooks.HookPoint('repository',
 
486
 
            "Invoked when displaying the statistics for a repository. "
 
487
 
            "repository is called with a statistics dictionary as returned "
 
488
 
            "by the repository and a file-like object to write to.", (1, 15),