~bzr-pqm/bzr/bzr.dev

1 by mbp at sourcefrog
import from baz patch-364
1
#! /usr/bin/python
2
3
4
# Copyright (C) 2004, 2005 by Martin Pool
5
# Copyright (C) 2005 by Canonical Ltd
6
7
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.
12
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.
17
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
21
22
"""Bazaar-NG -- a free distributed version-control tool
23
24
**WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION**
25
26
Current limitation include:
27
28
* Metadata format is not stable yet -- you may need to
29
  discard history in the future.
30
31
* No handling of subdirectories, symlinks or any non-text files.
32
33
* Insufficient error handling.
34
35
* Many commands unimplemented or partially implemented.
36
37
* Space-inefficient storage.
38
39
* No merge operators yet.
40
41
Interesting commands::
42
43
  bzr help
44
       Show summary help screen
45
  bzr version
46
       Show software version/licence/non-warranty.
47
  bzr init
48
       Start versioning the current directory
49
  bzr add FILE...
50
       Make files versioned.
51
  bzr log
52
       Show revision history.
53
  bzr diff
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.
59
  bzr status
60
       Show summary of pending changes.
61
  bzr remove FILE...
62
       Make a file not versioned.
63
"""
64
65
# not currently working:
66
#  bzr check
67
#       Run internal consistency checks.
68
#  bzr info
69
#       Show some information about this branch.
70
71
72
73
__copyright__ = "Copyright 2005 Canonical Development Ltd."
74
__author__ = "Martin Pool <mbp@canonical.com>"
75
__docformat__ = "restructuredtext en"
76
__version__ = '0.0.0'
77
78
79
import sys, os, random, time, sha, sets, types, re, shutil, tempfile
80
import traceback, socket, fnmatch, difflib
81
from os import path
82
from sets import Set
83
from pprint import pprint
84
from stat import *
85
from glob import glob
86
from ElementTree import Element, ElementTree, SubElement
87
88
import bzrlib
89
from bzrlib.store import ImmutableStore
90
from bzrlib.trace import mutter, note, log_error
91
from bzrlib.errors import bailout, BzrError
92
from bzrlib.osutils import quotefn, pumpfile, isdir, isfile
93
from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree
94
from bzrlib.revision import Revision
95
from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \
96
     format_date
97
98
BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n"
99
BZR_PATCHNAME_FORMAT = 'cset:sha1:%s'
100
101
## standard representation
102
NONE_STRING = '(none)'
103
EMPTY = 'empty'
104
105
106
## TODO: Perhaps a different version of inventory commands that
107
## returns iterators...
108
109
## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames.
110
111
## TODO: Some kind of locking on branches.  Perhaps there should be a
112
## parameter to the branch object saying whether we want a read or
113
## write lock; release it from destructor.  Perhaps don't even need a
114
## read lock to look at immutable objects?
115
116
## TODO: Perhaps make UUIDs predictable in test mode to make it easier
117
## to compare output?
118
119
## TODO: Is ElementTree really all that much better for our purposes?
120
## Perhaps using the standard MiniDOM would be enough?
121
122
123
124
125
126
127
######################################################################
128
# check status
129
130
131
def cmd_status(all=False):
132
    """Display status summary.
133
134
    For each file there is a single line giving its file state and name.
135
    The name is that in the current revision unless it is deleted or
136
    missing, in which case the old name is shown.
137
138
    :todo: Don't show unchanged files unless ``--all`` is given?
139
    """
140
    Branch('.').show_status(show_all=all)
141
142
143
144
######################################################################
145
# examining history
146
def cmd_get_revision(revision_id):
147
    Branch('.').get_revision(revision_id).write_xml(sys.stdout)
148
149
150
def cmd_get_inventory(inventory_id):
151
    """Return inventory in XML by hash"""
152
    Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout)
153
154
155
def cmd_get_revision_inventory(revision_id):
156
    """Output inventory for a revision."""
157
    b = Branch('.')
158
    b.get_revision_inventory(revision_id).write_xml(sys.stdout)
159
160
161
def cmd_get_file_text(text_id):
162
    """Get contents of a file by hash."""
163
    sf = Branch('.').text_store[text_id]
164
    pumpfile(sf, sys.stdout)
165
166
167
168
######################################################################
169
# commands
170
    
171
172
def cmd_revno():
173
    """Show number of revisions on this branch"""
174
    print Branch('.').revno()
175
    
176
177
def cmd_add(file_list, verbose=False):
178
    """Add specified files.
179
    
180
    Fails if the files are already added.
181
    """
182
    Branch('.').add(file_list, verbose=verbose)
183
184
185
def cmd_inventory(revision=None):
186
    """Show inventory of the current working copy."""
187
    ## TODO: Also optionally show a previous inventory
188
    ## TODO: Format options
189
    b = Branch('.')
190
    if revision == None:
191
        inv = b.read_working_inventory()
192
    else:
193
        inv = b.get_revision_inventory(b.lookup_revision(revision))
194
        
195
    for path, entry in inv.iter_entries():
196
        print '%-50s %s' % (entry.file_id, path)
197
198
199
200
def cmd_info():
201
    b = Branch('.')
202
    print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n')
203
    print 'revision number:', b.revno()
204
    print 'number of versioned files:', len(b.read_working_inventory())
205
206
207
def cmd_remove(file_list, verbose=False):
208
    Branch('.').remove(file_list, verbose=verbose)
209
210
211
212
def cmd_file_id(filename):
213
    i = Branch('.').read_working_inventory().path2id(filename)
214
    if i is None:
215
        bailout("%s is not a versioned file" % filename)
216
    else:
217
        print i
218
219
220
def cmd_find_filename(fileid):
221
    n = find_filename(fileid)
222
    if n is None:
223
        bailout("%s is not a live file id" % fileid)
224
    else:
225
        print n
226
227
228
def cmd_revision_history():
229
    for patchid in Branch('.').revision_history():
230
        print patchid
231
232
233
234
def cmd_init():
235
    # TODO: Check we're not already in a working directory?  At the
236
    # moment you'll get an ugly error.
237
    
238
    # TODO: What if we're in a subdirectory of a branch?  Would like
239
    # to allow that, but then the parent may need to understand that
240
    # the children have disappeared, or should they be versioned in
241
    # both?
242
243
    # TODO: Take an argument/option for branch name.
244
    Branch('.', init=True)
245
246
247
def cmd_diff(revision=None):
248
    """Show diff from basis to working copy.
249
250
    :todo: Take one or two revision arguments, look up those trees,
251
           and diff them.
252
253
    :todo: Allow diff across branches.
254
255
    :todo: Mangle filenames in diff to be more relevant.
256
257
    :todo: Shouldn't be in the cmd function.
258
    """
259
260
    b = Branch('.')
261
262
    if revision == None:
263
        old_tree = b.basis_tree()
264
    else:
265
        old_tree = b.revision_tree(b.lookup_revision(revision))
266
        
267
    new_tree = b.working_tree()
268
    old_inv = old_tree.inventory
269
    new_inv = new_tree.inventory
270
271
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
272
    old_label = ''
273
    new_label = ''
274
275
    DEVNULL = '/dev/null'
276
    # Windows users, don't panic about this filename -- it is a
277
    # special signal to GNU patch that the file should be created or
278
    # deleted respectively.
279
280
    # TODO: Generation of pseudo-diffs for added/deleted files could
281
    # be usefully made into a much faster special case.
282
283
    # TODO: Better to return them in sorted order I think.
284
    
285
    for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree):
286
        d = None
287
288
        # Don't show this by default; maybe do it if an option is passed
289
        # idlabel = '      {%s}' % fid
290
        idlabel = ''
291
292
        # FIXME: Something about the diff format makes patch unhappy
293
        # with newly-added files.
294
295
        def diffit(*a, **kw):
296
            sys.stdout.writelines(difflib.unified_diff(*a, **kw))
297
            print
298
        
299
        if file_state in ['.', '?', 'I']:
300
            continue
301
        elif file_state == 'A':
302
            print '*** added %s %r' % (kind, new_name)
303
            if kind == 'file':
304
                diffit([],
305
                       new_tree.get_file(fid).readlines(),
306
                       fromfile=DEVNULL,
307
                       tofile=new_label + new_name + idlabel)
308
        elif file_state == 'D':
309
            assert isinstance(old_name, types.StringTypes)
310
            print '*** deleted %s %r' % (kind, old_name)
311
            if kind == 'file':
312
                diffit(old_tree.get_file(fid).readlines(), [],
313
                       fromfile=old_label + old_name + idlabel,
314
                       tofile=DEVNULL)
315
        elif file_state in ['M', 'R']:
316
            if file_state == 'M':
317
                assert kind == 'file'
318
                assert old_name == new_name
319
                print '*** modified %s %r' % (kind, new_name)
320
            elif file_state == 'R':
321
                print '*** renamed %s %r => %r' % (kind, old_name, new_name)
322
323
            if kind == 'file':
324
                diffit(old_tree.get_file(fid).readlines(),
325
                       new_tree.get_file(fid).readlines(),
326
                       fromfile=old_label + old_name + idlabel,
327
                       tofile=new_label + new_name)
328
        else:
329
            bailout("can't represent state %s {%s}" % (file_state, fid))
330
331
332
333
def cmd_log():
334
    """Show log of this branch.
335
336
    :todo: Options for utc; to show ids; to limit range; etc.
337
    """
338
    Branch('.').write_log()
339
340
341
def cmd_ls(revision=None, verbose=False):
342
    """List files in a tree.
343
344
    :todo: Take a revision or remote path and list that tree instead.
345
    """
346
    b = Branch('.')
347
    if revision == None:
348
        tree = b.working_tree()
349
    else:
350
        tree = b.revision_tree(b.lookup_revision(revision))
351
        
352
    for fp, fc, kind, fid in tree.list_files():
353
        if verbose:
354
            if kind == 'directory':
355
                kindch = '/'
356
            elif kind == 'file':
357
                kindch = ''
358
            else:
359
                kindch = '???'
360
                
361
            print '%-8s %s%s' % (fc, fp, kindch)
362
        else:
363
            print fp
364
    
365
    
366
367
def cmd_unknowns():
368
    """List unknown files"""
369
    for f in Branch('.').unknowns():
370
        print quotefn(f)
371
372
373
def cmd_lookup_revision(revno):
374
    try:
375
        revno = int(revno)
376
    except ValueError:
377
        bailout("usage: lookup-revision REVNO",
378
                ["REVNO is a non-negative revision number for this branch"])
379
380
    print Branch('.').lookup_revision(revno) or NONE_STRING
381
382
383
384
def cmd_export(revno, dest):
385
    """Export past revision to destination directory."""
386
    b = Branch('.')
387
    rh = b.lookup_revision(int(revno))
388
    t = b.revision_tree(rh)
389
    t.export(dest)
390
391
392
393
######################################################################
394
# internal/test commands
395
396
397
def cmd_uuid():
398
    """Print a newly-generated UUID."""
399
    print uuid()
400
401
402
403
def cmd_commit(message, verbose=False):
404
    Branch('.').commit(message, verbose=verbose)
405
406
407
def cmd_check():
408
    """Check consistency of the branch."""
409
    check()
410
411
412
def cmd_is(pred, *rest):
413
    """Test whether PREDICATE is true."""
414
    try:
415
        cmd_handler = globals()['assert_' + pred.replace('-', '_')]
416
    except KeyError:
417
        bailout("unknown predicate: %s" % quotefn(pred))
418
        
419
    try:
420
        cmd_handler(*rest)
421
    except BzrCheckError:
422
        # by default we don't print the message so that this can
423
        # be used from shell scripts without producing noise
424
        sys.exit(1)
425
426
427
def cmd_username():
428
    print bzrlib.osutils.username()
429
430
431
def cmd_user_email():
432
    print bzrlib.osutils.user_email()
433
434
435
def cmd_gen_revision_id():
436
    import time
437
    print bzrlib.branch._gen_revision_id(time.time())
438
439
440
def cmd_doctest():
441
    """Run internal doctest suite"""
442
    ## -v, if present, is seen by doctest; the argument is just here
443
    ## so our parser doesn't complain
444
445
    ## TODO: --verbose option
446
    
447
    import bzr, doctest, bzrlib.store
448
    bzrlib.trace.verbose = False
449
    doctest.testmod(bzr)
450
    doctest.testmod(bzrlib.store)
451
    doctest.testmod(bzrlib.inventory)
452
    doctest.testmod(bzrlib.branch)
453
    doctest.testmod(bzrlib.osutils)
454
    doctest.testmod(bzrlib.tree)
455
456
    # more strenuous tests;
457
    import bzrlib.tests
458
    doctest.testmod(bzrlib.tests)
459
460
461
######################################################################
462
# help
463
464
465
def cmd_help():
466
    # TODO: Specific help for particular commands
467
    print __doc__
468
469
470
def cmd_version():
471
    print "bzr (bazaar-ng) %s" % __version__
472
    print __copyright__
473
    print "http://bazaar-ng.org/"
474
    print
475
    print \
476
"""bzr comes with ABSOLUTELY NO WARRANTY.  bzr is free software, and
477
you may use, modify and redistribute it under the terms of the GNU 
478
General Public License version 2 or later."""
479
480
481
def cmd_rocks():
482
    """Statement of optimism."""
483
    print "it sure does!"
484
485
486
487
######################################################################
488
# main routine
489
490
491
# list of all available options; the rhs can be either None for an
492
# option that takes no argument, or a constructor function that checks
493
# the type.
494
OPTIONS = {
495
    'all':                    None,
496
    'help':                   None,
497
    'message':                unicode,
498
    'revision':               int,
499
    'show-ids':               None,
500
    'verbose':                None,
501
    'version':                None,
502
    }
503
504
SHORT_OPTIONS = {
505
    'm':                      'message',
506
    'r':                      'revision',
507
    'v':                      'verbose',
508
}
509
510
# List of options that apply to particular commands; commands not
511
# listed take none.
512
cmd_options = {
513
    'add':                    ['verbose'],
514
    'commit':                 ['message', 'verbose'],
515
    'diff':                   ['revision'],
516
    'inventory':              ['revision'],
517
    'ls':                     ['revision', 'verbose'],
518
    'status':                 ['all'],
519
    'log':                    ['show-ids'],
520
    'remove':                 ['verbose'],
521
    }
522
523
524
cmd_args = {
525
    'init':                   [],
526
    'add':                    ['file+'],
527
    'commit':                 [],
528
    'diff':                   [],
529
    'file-id':                ['filename'],
530
    'get-file-text':          ['text_id'],
531
    'get-inventory':          ['inventory_id'],
532
    'get-revision':           ['revision_id'],
533
    'get-revision-inventory': ['revision_id'],
534
    'log':                    [],
535
    'lookup-revision':        ['revno'],
536
    'export':                 ['revno', 'dest'],
537
    'remove':                 ['file+'],
538
    'status':                 [],
539
    }
540
541
542
def parse_args(argv):
543
    """Parse command line.
544
    
545
    Arguments and options are parsed at this level before being passed
546
    down to specific command handlers.  This routine knows, from a
547
    lookup table, something about the available options, what optargs
548
    they take, and which commands will accept them.
549
550
    >>> parse_args('bzr --help'.split())
551
    ([], {'help': True})
552
    >>> parse_args('bzr --version'.split())
553
    ([], {'version': True})
554
    >>> parse_args('bzr status --all'.split())
555
    (['status'], {'all': True})
556
    """
557
    args = []
558
    opts = {}
559
560
    # TODO: Maybe handle '--' to end options?
561
562
    it = iter(argv[1:])
563
    while it:
564
        a = it.next()
565
        if a[0] == '-':
566
            if a[1] == '-':
567
                mutter("  got option %r" % a)
568
                optname = a[2:]
569
                if optname not in OPTIONS:
570
                    bailout('unknown long option %r' % a)
571
            else:
572
                shortopt = a[1:]
573
                if shortopt not in SHORT_OPTIONS:
574
                    bailout('unknown short option %r' % a)
575
                optname = SHORT_OPTIONS[shortopt]
576
            
577
            if optname in opts:
578
                # XXX: Do we ever want to support this, e.g. for -r?
579
                bailout('repeated option %r' % a)
580
            optargfn = OPTIONS[optname]
581
            if optargfn:
582
                if not it:
583
                    bailout('option %r needs an argument' % a)
584
                opts[optname] = optargfn(it.next())
585
                mutter("    option argument %r" % opts[optname])
586
            else:
587
                # takes no option argument
588
                opts[optname] = True
589
        elif a[:1] == '-':
590
            bailout('unknown short option %r' % a)
591
        else:
592
            args.append(a)
593
594
    return args, opts
595
596
597
598
def _match_args(cmd, args):
599
    """Check non-option arguments match required pattern.
600
601
    >>> _match_args('status', ['asdasdsadasd'])
602
    Traceback (most recent call last):
603
    ...
604
    BzrError: ("extra arguments to command status: ['asdasdsadasd']", [])
605
    >>> _match_args('add', ['asdasdsadasd'])
606
    {'file_list': ['asdasdsadasd']}
607
    >>> _match_args('add', 'abc def gj'.split())
608
    {'file_list': ['abc', 'def', 'gj']}
609
    """
610
    # match argument pattern
611
    argform = cmd_args.get(cmd, [])
612
    argdict = {}
613
    # TODO: Need a way to express 'cp SRC... DEST', where it matches
614
    # all but one.
615
    for ap in argform:
616
        argname = ap[:-1]
617
        if ap[-1] == '?':
618
            assert 0
619
        elif ap[-1] == '*':
620
            assert 0
621
        elif ap[-1] == '+':
622
            if not args:
623
                bailout("command %r needs one or more %s"
624
                        % (cmd, argname.upper()))
625
            else:
626
                argdict[argname + '_list'] = args[:]
627
                args = []
628
        else:
629
            # just a plain arg
630
            argname = ap
631
            if not args:
632
                bailout("command %r requires argument %s"
633
                        % (cmd, argname.upper()))
634
            else:
635
                argdict[argname] = args.pop(0)
636
            
637
    if args:
638
        bailout("extra arguments to command %s: %r"
639
                % (cmd, args))
640
641
    return argdict
642
643
644
645
def run_bzr(argv):
646
    """Execute a command.
647
648
    This is similar to main(), but without all the trappings for
649
    logging and error handling.
650
    """
651
    try:
652
        args, opts = parse_args(argv)
653
        if 'help' in opts:
654
            # TODO: pass down other arguments in case they asked for
655
            # help on a command name?
656
            cmd_help()
657
            return 0
658
        elif 'version' in opts:
659
            cmd_version()
660
            return 0
661
        cmd = args.pop(0)
662
    except IndexError:
663
        log_error('usage: bzr COMMAND\n')
664
        log_error('  try "bzr help"\n')
665
        return 1
666
            
667
    try:
668
        cmd_handler = globals()['cmd_' + cmd.replace('-', '_')]
669
    except KeyError:
670
        bailout("unknown command " + `cmd`)
671
672
    # TODO: special --profile option to turn on the Python profiler
673
674
    # check options are reasonable
675
    allowed = cmd_options.get(cmd, [])
676
    for oname in opts:
677
        if oname not in allowed:
678
            bailout("option %r is not allowed for command %r"
679
                    % (oname, cmd))
680
681
    cmdargs = _match_args(cmd, args)
682
    cmdargs.update(opts)
683
684
    ret = cmd_handler(**cmdargs) or 0
685
686
687
688
def main(argv):
689
    ## TODO: Handle command-line options; probably know what options are valid for
690
    ## each command
691
692
    ## TODO: If the arguments are wrong, give a usage message rather
693
    ## than just a backtrace.
694
695
    try:
696
        t = bzrlib.trace._tracefile
697
        t.write('-' * 60 + '\n')
698
        t.write('bzr invoked at %s\n' % format_date(time.time()))
699
        t.write('  by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname()))
700
        t.write('  arguments: %r\n' % argv)
701
702
        starttime = os.times()[4]
703
704
        import platform
705
        t.write('  platform: %s\n' % platform.platform())
706
        t.write('  python: %s\n' % platform.python_version())
707
708
        ret = run_bzr(argv)
709
        
710
        times = os.times()
711
        mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum"
712
               % times[:4])
713
        mutter("    %.3f elapsed" % (times[4] - starttime))
714
715
        return ret
716
    except BzrError, e:
717
        log_error('bzr: error: ' + e.args[0] + '\n')
718
        if len(e.args) > 1:
719
            for h in e.args[1]:
720
                log_error('  ' + h + '\n')
721
        return 1
722
    except Exception, e:
723
        log_error('bzr: exception: %s\n' % e)
724
        log_error('    see .bzr.log for details\n')
725
        traceback.print_exc(None, bzrlib.trace._tracefile)
726
        traceback.print_exc(None, sys.stderr)
727
        return 1
728
729
    # TODO: Maybe nicer handling of IOError?
730
731
732
733
if __name__ == '__main__':
734
    sys.exit(main(sys.argv))
735
    ##import profile
736
    ##profile.run('main(sys.argv)')
737