4
# Copyright (C) 2004, 2005 by Martin Pool
 
 
5
# Copyright (C) 2005 by Canonical Ltd
 
 
8
# This program is free software; you can redistribute it and/or modify
 
 
9
# it under the terms of the GNU General Public License as published by
 
 
10
# the Free Software Foundation; either version 2 of the License, or
 
 
11
# (at your option) any later version.
 
 
13
# This program is distributed in the hope that it will be useful,
 
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
16
# GNU General Public License for more details.
 
 
18
# You should have received a copy of the GNU General Public License
 
 
19
# along with this program; if not, write to the Free Software
 
 
20
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
22
"""Bazaar-NG -- a free distributed version-control tool
 
 
24
**WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION**
 
 
26
Current limitation include:
 
 
28
* Metadata format is not stable yet -- you may need to
 
 
29
  discard history in the future.
 
 
31
* No handling of subdirectories, symlinks or any non-text files.
 
 
33
* Insufficient error handling.
 
 
35
* Many commands unimplemented or partially implemented.
 
 
37
* Space-inefficient storage.
 
 
39
* No merge operators yet.
 
 
41
Interesting commands::
 
 
44
       Show summary help screen
 
 
46
       Show software version/licence/non-warranty.
 
 
48
       Start versioning the current directory
 
 
52
       Show revision history.
 
 
54
       Show changes from last revision to working copy.
 
 
55
  bzr commit -m 'MESSAGE'
 
 
56
       Store current state as new revision.
 
 
57
  bzr export REVNO DESTINATION
 
 
58
       Export the branch state at a previous version.
 
 
60
       Show summary of pending changes.
 
 
62
       Make a file not versioned.
 
 
65
# not currently working:
 
 
67
#       Run internal consistency checks.
 
 
69
#       Show some information about this branch.
 
 
73
__copyright__ = "Copyright 2005 Canonical Development Ltd."
 
 
74
__author__ = "Martin Pool <mbp@canonical.com>"
 
 
75
__docformat__ = "restructuredtext en"
 
 
79
import sys, os, random, time, sha, sets, types, re, shutil, tempfile
 
 
80
import traceback, socket, fnmatch, difflib
 
 
83
from pprint import pprint
 
 
88
from bzrlib.store import ImmutableStore
 
 
89
from bzrlib.trace import mutter, note, log_error
 
 
90
from bzrlib.errors import bailout, BzrError
 
 
91
from bzrlib.osutils import quotefn, pumpfile, isdir, isfile
 
 
92
from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree
 
 
93
from bzrlib.revision import Revision
 
 
94
from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \
 
 
97
BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n"
 
 
98
BZR_PATCHNAME_FORMAT = 'cset:sha1:%s'
 
 
100
## standard representation
 
 
101
NONE_STRING = '(none)'
 
 
105
## TODO: Perhaps a different version of inventory commands that
 
 
106
## returns iterators...
 
 
108
## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames.
 
 
110
## TODO: Some kind of locking on branches.  Perhaps there should be a
 
 
111
## parameter to the branch object saying whether we want a read or
 
 
112
## write lock; release it from destructor.  Perhaps don't even need a
 
 
113
## read lock to look at immutable objects?
 
 
115
## TODO: Perhaps make UUIDs predictable in test mode to make it easier
 
 
116
## to compare output?
 
 
121
######################################################################
 
 
125
def cmd_status(all=False):
 
 
126
    """Display status summary.
 
 
128
    For each file there is a single line giving its file state and name.
 
 
129
    The name is that in the current revision unless it is deleted or
 
 
130
    missing, in which case the old name is shown.
 
 
132
    :todo: Don't show unchanged files unless ``--all`` is given?
 
 
134
    Branch('.').show_status(show_all=all)
 
 
138
######################################################################
 
 
140
def cmd_get_revision(revision_id):
 
 
141
    Branch('.').get_revision(revision_id).write_xml(sys.stdout)
 
 
144
def cmd_get_inventory(inventory_id):
 
 
145
    """Return inventory in XML by hash"""
 
 
146
    Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout)
 
 
149
def cmd_get_revision_inventory(revision_id):
 
 
150
    """Output inventory for a revision."""
 
 
152
    b.get_revision_inventory(revision_id).write_xml(sys.stdout)
 
 
155
def cmd_get_file_text(text_id):
 
 
156
    """Get contents of a file by hash."""
 
 
157
    sf = Branch('.').text_store[text_id]
 
 
158
    pumpfile(sf, sys.stdout)
 
 
162
######################################################################
 
 
167
    """Show number of revisions on this branch"""
 
 
168
    print Branch('.').revno()
 
 
171
def cmd_add(file_list, verbose=False):
 
 
172
    """Add specified files.
 
 
174
    Fails if the files are already added.
 
 
176
    Branch('.').add(file_list, verbose=verbose)
 
 
179
def cmd_inventory(revision=None):
 
 
180
    """Show inventory of the current working copy."""
 
 
181
    ## TODO: Also optionally show a previous inventory
 
 
182
    ## TODO: Format options
 
 
185
        inv = b.read_working_inventory()
 
 
187
        inv = b.get_revision_inventory(b.lookup_revision(revision))
 
 
189
    for path, entry in inv.iter_entries():
 
 
190
        print '%-50s %s' % (entry.file_id, path)
 
 
196
    print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n')
 
 
198
    def plural(n, base='', pl=None):
 
 
206
    count_version_dirs = 0
 
 
208
    count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0}
 
 
209
    for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()):
 
 
211
        count_status[fs] += 1
 
 
212
        if fs not in ['I', '?'] and st_tup[4] == 'directory':
 
 
213
            count_version_dirs += 1
 
 
216
    print 'in the working tree:'
 
 
217
    for name, fs in (('unchanged', '.'),
 
 
218
                     ('modified', 'M'), ('added', 'A'), ('removed', 'D'),
 
 
219
                     ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'),
 
 
221
        print '  %5d %s' % (count_status[fs], name)
 
 
222
    print '  %5d versioned subdirector%s' % (count_version_dirs,
 
 
223
                                             plural(count_version_dirs, 'y', 'ies'))
 
 
226
    print 'branch history:'
 
 
227
    history = b.revision_history()
 
 
229
    print '  %5d revision%s' % (revno, plural(revno))
 
 
232
        committers.add(b.get_revision(rev).committer)
 
 
233
    print '  %5d committer%s' % (len(committers), plural(len(committers)))
 
 
235
        firstrev = b.get_revision(history[0])
 
 
236
        age = int((time.time() - firstrev.timestamp) / 3600 / 24)
 
 
237
        print '  %5d day%s old' % (age, plural(age))
 
 
238
        print '  first revision: %s' % format_date(firstrev.timestamp,
 
 
241
        lastrev = b.get_revision(history[-1])
 
 
242
        print '  latest revision: %s' % format_date(lastrev.timestamp,
 
 
248
def cmd_remove(file_list, verbose=False):
 
 
249
    Branch('.').remove(file_list, verbose=verbose)
 
 
253
def cmd_file_id(filename):
 
 
254
    i = Branch('.').read_working_inventory().path2id(filename)
 
 
256
        bailout("%s is not a versioned file" % filename)
 
 
261
def cmd_find_filename(fileid):
 
 
262
    n = find_filename(fileid)
 
 
264
        bailout("%s is not a live file id" % fileid)
 
 
269
def cmd_revision_history():
 
 
270
    for patchid in Branch('.').revision_history():
 
 
276
    # TODO: Check we're not already in a working directory?  At the
 
 
277
    # moment you'll get an ugly error.
 
 
279
    # TODO: What if we're in a subdirectory of a branch?  Would like
 
 
280
    # to allow that, but then the parent may need to understand that
 
 
281
    # the children have disappeared, or should they be versioned in
 
 
284
    # TODO: Take an argument/option for branch name.
 
 
285
    Branch('.', init=True)
 
 
288
def cmd_diff(revision=None):
 
 
289
    """Show diff from basis to working copy.
 
 
291
    :todo: Take one or two revision arguments, look up those trees,
 
 
294
    :todo: Allow diff across branches.
 
 
296
    :todo: Mangle filenames in diff to be more relevant.
 
 
298
    :todo: Shouldn't be in the cmd function.
 
 
304
        old_tree = b.basis_tree()
 
 
306
        old_tree = b.revision_tree(b.lookup_revision(revision))
 
 
308
    new_tree = b.working_tree()
 
 
309
    old_inv = old_tree.inventory
 
 
310
    new_inv = new_tree.inventory
 
 
312
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
 
 
316
    DEVNULL = '/dev/null'
 
 
317
    # Windows users, don't panic about this filename -- it is a
 
 
318
    # special signal to GNU patch that the file should be created or
 
 
319
    # deleted respectively.
 
 
321
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
 
322
    # be usefully made into a much faster special case.
 
 
324
    # TODO: Better to return them in sorted order I think.
 
 
326
    for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree):
 
 
329
        # Don't show this by default; maybe do it if an option is passed
 
 
330
        # idlabel = '      {%s}' % fid
 
 
333
        # FIXME: Something about the diff format makes patch unhappy
 
 
334
        # with newly-added files.
 
 
336
        def diffit(*a, **kw):
 
 
337
            sys.stdout.writelines(difflib.unified_diff(*a, **kw))
 
 
340
        if file_state in ['.', '?', 'I']:
 
 
342
        elif file_state == 'A':
 
 
343
            print '*** added %s %r' % (kind, new_name)
 
 
346
                       new_tree.get_file(fid).readlines(),
 
 
348
                       tofile=new_label + new_name + idlabel)
 
 
349
        elif file_state == 'D':
 
 
350
            assert isinstance(old_name, types.StringTypes)
 
 
351
            print '*** deleted %s %r' % (kind, old_name)
 
 
353
                diffit(old_tree.get_file(fid).readlines(), [],
 
 
354
                       fromfile=old_label + old_name + idlabel,
 
 
356
        elif file_state in ['M', 'R']:
 
 
357
            if file_state == 'M':
 
 
358
                assert kind == 'file'
 
 
359
                assert old_name == new_name
 
 
360
                print '*** modified %s %r' % (kind, new_name)
 
 
361
            elif file_state == 'R':
 
 
362
                print '*** renamed %s %r => %r' % (kind, old_name, new_name)
 
 
365
                diffit(old_tree.get_file(fid).readlines(),
 
 
366
                       new_tree.get_file(fid).readlines(),
 
 
367
                       fromfile=old_label + old_name + idlabel,
 
 
368
                       tofile=new_label + new_name)
 
 
370
            bailout("can't represent state %s {%s}" % (file_state, fid))
 
 
374
def cmd_log(timezone='original'):
 
 
375
    """Show log of this branch.
 
 
377
    :todo: Options for utc; to show ids; to limit range; etc.
 
 
379
    Branch('.').write_log(show_timezone=timezone)
 
 
382
def cmd_ls(revision=None, verbose=False):
 
 
383
    """List files in a tree.
 
 
385
    :todo: Take a revision or remote path and list that tree instead.
 
 
389
        tree = b.working_tree()
 
 
391
        tree = b.revision_tree(b.lookup_revision(revision))
 
 
393
    for fp, fc, kind, fid in tree.list_files():
 
 
395
            if kind == 'directory':
 
 
402
            print '%-8s %s%s' % (fc, fp, kindch)
 
 
409
    """List unknown files"""
 
 
410
    for f in Branch('.').unknowns():
 
 
414
def cmd_lookup_revision(revno):
 
 
418
        bailout("usage: lookup-revision REVNO",
 
 
419
                ["REVNO is a non-negative revision number for this branch"])
 
 
421
    print Branch('.').lookup_revision(revno) or NONE_STRING
 
 
425
def cmd_export(revno, dest):
 
 
426
    """Export past revision to destination directory."""
 
 
428
    rh = b.lookup_revision(int(revno))
 
 
429
    t = b.revision_tree(rh)
 
 
434
######################################################################
 
 
435
# internal/test commands
 
 
439
    """Print a newly-generated UUID."""
 
 
444
def cmd_local_time_offset():
 
 
445
    print bzrlib.osutils.local_time_offset()
 
 
449
def cmd_commit(message, verbose=False):
 
 
450
    Branch('.').commit(message, verbose=verbose)
 
 
454
    """Check consistency of the branch."""
 
 
458
def cmd_is(pred, *rest):
 
 
459
    """Test whether PREDICATE is true."""
 
 
461
        cmd_handler = globals()['assert_' + pred.replace('-', '_')]
 
 
463
        bailout("unknown predicate: %s" % quotefn(pred))
 
 
467
    except BzrCheckError:
 
 
468
        # by default we don't print the message so that this can
 
 
469
        # be used from shell scripts without producing noise
 
 
474
    print bzrlib.osutils.username()
 
 
477
def cmd_user_email():
 
 
478
    print bzrlib.osutils.user_email()
 
 
481
def cmd_gen_revision_id():
 
 
483
    print bzrlib.branch._gen_revision_id(time.time())
 
 
487
    """Run internal doctest suite"""
 
 
488
    ## -v, if present, is seen by doctest; the argument is just here
 
 
489
    ## so our parser doesn't complain
 
 
491
    ## TODO: --verbose option
 
 
493
    import bzr, doctest, bzrlib.store
 
 
494
    bzrlib.trace.verbose = False
 
 
496
    doctest.testmod(bzrlib.store)
 
 
497
    doctest.testmod(bzrlib.inventory)
 
 
498
    doctest.testmod(bzrlib.branch)
 
 
499
    doctest.testmod(bzrlib.osutils)
 
 
500
    doctest.testmod(bzrlib.tree)
 
 
502
    # more strenuous tests;
 
 
504
    doctest.testmod(bzrlib.tests)
 
 
507
######################################################################
 
 
512
    # TODO: Specific help for particular commands
 
 
517
    print "bzr (bazaar-ng) %s" % __version__
 
 
519
    print "http://bazaar-ng.org/"
 
 
522
"""bzr comes with ABSOLUTELY NO WARRANTY.  bzr is free software, and
 
 
523
you may use, modify and redistribute it under the terms of the GNU 
 
 
524
General Public License version 2 or later."""
 
 
528
    """Statement of optimism."""
 
 
529
    print "it sure does!"
 
 
533
######################################################################
 
 
537
# list of all available options; the rhs can be either None for an
 
 
538
# option that takes no argument, or a constructor function that checks
 
 
557
# List of options that apply to particular commands; commands not
 
 
561
    'commit':                 ['message', 'verbose'],
 
 
562
    'diff':                   ['revision'],
 
 
563
    'inventory':              ['revision'],
 
 
564
    'log':                    ['show-ids', 'timezone'],
 
 
565
    'ls':                     ['revision', 'verbose'],
 
 
566
    'remove':                 ['verbose'],
 
 
576
    'file-id':                ['filename'],
 
 
577
    'get-file-text':          ['text_id'],
 
 
578
    'get-inventory':          ['inventory_id'],
 
 
579
    'get-revision':           ['revision_id'],
 
 
580
    'get-revision-inventory': ['revision_id'],
 
 
582
    'lookup-revision':        ['revno'],
 
 
583
    'export':                 ['revno', 'dest'],
 
 
589
def parse_args(argv):
 
 
590
    """Parse command line.
 
 
592
    Arguments and options are parsed at this level before being passed
 
 
593
    down to specific command handlers.  This routine knows, from a
 
 
594
    lookup table, something about the available options, what optargs
 
 
595
    they take, and which commands will accept them.
 
 
597
    >>> parse_args('bzr --help'.split())
 
 
599
    >>> parse_args('bzr --version'.split())
 
 
600
    ([], {'version': True})
 
 
601
    >>> parse_args('bzr status --all'.split())
 
 
602
    (['status'], {'all': True})
 
 
603
    >>> parse_args('bzr commit --message=biter'.split())
 
 
604
    (['commit'], {'message': u'biter'})
 
 
609
    # TODO: Maybe handle '--' to end options?
 
 
617
                mutter("  got option %r" % a)
 
 
619
                    optname, optarg = a[2:].split('=', 1)
 
 
622
                if optname not in OPTIONS:
 
 
623
                    bailout('unknown long option %r' % a)
 
 
626
                if shortopt not in SHORT_OPTIONS:
 
 
627
                    bailout('unknown short option %r' % a)
 
 
628
                optname = SHORT_OPTIONS[shortopt]
 
 
631
                # XXX: Do we ever want to support this, e.g. for -r?
 
 
632
                bailout('repeated option %r' % a)
 
 
634
            optargfn = OPTIONS[optname]
 
 
638
                        bailout('option %r needs an argument' % a)
 
 
641
                opts[optname] = optargfn(optarg)
 
 
642
                mutter("    option argument %r" % opts[optname])
 
 
645
                    bailout('option %r takes no argument' % optname)
 
 
654
def _match_args(cmd, args):
 
 
655
    """Check non-option arguments match required pattern.
 
 
657
    >>> _match_args('status', ['asdasdsadasd'])
 
 
658
    Traceback (most recent call last):
 
 
660
    BzrError: ("extra arguments to command status: ['asdasdsadasd']", [])
 
 
661
    >>> _match_args('add', ['asdasdsadasd'])
 
 
662
    {'file_list': ['asdasdsadasd']}
 
 
663
    >>> _match_args('add', 'abc def gj'.split())
 
 
664
    {'file_list': ['abc', 'def', 'gj']}
 
 
666
    # match argument pattern
 
 
667
    argform = cmd_args.get(cmd, [])
 
 
669
    # TODO: Need a way to express 'cp SRC... DEST', where it matches
 
 
679
                bailout("command %r needs one or more %s"
 
 
680
                        % (cmd, argname.upper()))
 
 
682
                argdict[argname + '_list'] = args[:]
 
 
688
                bailout("command %r requires argument %s"
 
 
689
                        % (cmd, argname.upper()))
 
 
691
                argdict[argname] = args.pop(0)
 
 
694
        bailout("extra arguments to command %s: %r"
 
 
702
    """Execute a command.
 
 
704
    This is similar to main(), but without all the trappings for
 
 
705
    logging and error handling.
 
 
708
        args, opts = parse_args(argv)
 
 
710
            # TODO: pass down other arguments in case they asked for
 
 
711
            # help on a command name?
 
 
714
        elif 'version' in opts:
 
 
719
        log_error('usage: bzr COMMAND\n')
 
 
720
        log_error('  try "bzr help"\n')
 
 
724
        cmd_handler = globals()['cmd_' + cmd.replace('-', '_')]
 
 
726
        bailout("unknown command " + `cmd`)
 
 
728
    # TODO: special --profile option to turn on the Python profiler
 
 
730
    # check options are reasonable
 
 
731
    allowed = cmd_options.get(cmd, [])
 
 
733
        if oname not in allowed:
 
 
734
            bailout("option %r is not allowed for command %r"
 
 
737
    cmdargs = _match_args(cmd, args)
 
 
740
    ret = cmd_handler(**cmdargs) or 0
 
 
745
    ## TODO: Handle command-line options; probably know what options are valid for
 
 
748
    ## TODO: If the arguments are wrong, give a usage message rather
 
 
749
    ## than just a backtrace.
 
 
752
        t = bzrlib.trace._tracefile
 
 
753
        t.write('-' * 60 + '\n')
 
 
754
        t.write('bzr invoked at %s\n' % format_date(time.time()))
 
 
755
        t.write('  by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname()))
 
 
756
        t.write('  arguments: %r\n' % argv)
 
 
758
        starttime = os.times()[4]
 
 
761
        t.write('  platform: %s\n' % platform.platform())
 
 
762
        t.write('  python: %s\n' % platform.python_version())
 
 
767
        mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum"
 
 
769
        mutter("    %.3f elapsed" % (times[4] - starttime))
 
 
773
        log_error('bzr: error: ' + e.args[0] + '\n')
 
 
776
                log_error('  ' + h + '\n')
 
 
779
        log_error('bzr: exception: %s\n' % e)
 
 
780
        log_error('    see .bzr.log for details\n')
 
 
781
        traceback.print_exc(None, bzrlib.trace._tracefile)
 
 
782
        traceback.print_exc(None, sys.stderr)
 
 
785
    # TODO: Maybe nicer handling of IOError?
 
 
789
if __name__ == '__main__':
 
 
790
    sys.exit(main(sys.argv))
 
 
792
    ##profile.run('main(sys.argv)')