/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/commands.py

  • Committer: Martin Pool
  • Date: 2005-05-05 02:55:45 UTC
  • Revision ID: mbp@sourcefrog.net-20050505025545-7c45e4c5255c5fd5
- refactor command aliases into command classes
- fix edge bugs in aliases and help command
- test cases for this

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004, 2005 by Canonical Ltd
 
2
 
 
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.
 
7
 
 
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.
 
12
 
 
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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Bazaar-NG -- a free distributed version-control tool
 
18
http://bazaar-ng.org/
 
19
 
 
20
**WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION**
 
21
 
 
22
* Metadata format is not stable yet -- you may need to
 
23
  discard history in the future.
 
24
 
 
25
* Many commands unimplemented or partially implemented.
 
26
 
 
27
* Space-inefficient storage.
 
28
 
 
29
* No merge operators yet.
 
30
 
 
31
Interesting commands:
 
32
 
 
33
  bzr help [COMMAND]
 
34
      Show help screen
 
35
  bzr version
 
36
      Show software version/licence/non-warranty.
 
37
  bzr init
 
38
      Start versioning the current directory
 
39
  bzr add FILE...
 
40
      Make files versioned.
 
41
  bzr log
 
42
      Show revision history.
 
43
  bzr rename FROM TO
 
44
      Rename one file.
 
45
  bzr move FROM... DESTDIR
 
46
      Move one or more files to a different directory.
 
47
  bzr diff [FILE...]
 
48
      Show changes from last revision to working copy.
 
49
  bzr commit -m 'MESSAGE'
 
50
      Store current state as new revision.
 
51
  bzr export [-r REVNO] DESTINATION
 
52
      Export the branch state at a previous version.
 
53
  bzr status
 
54
      Show summary of pending changes.
 
55
  bzr remove FILE...
 
56
      Make a file not versioned.
 
57
  bzr info
 
58
      Show statistics about this branch.
 
59
  bzr check
 
60
      Verify history is stored safely. 
 
61
  (for more type 'bzr help commands')
 
62
"""
 
63
 
 
64
 
 
65
 
 
66
 
 
67
import sys, os, time, os.path
 
68
from sets import Set
 
69
 
 
70
import bzrlib
 
71
from bzrlib.trace import mutter, note, log_error
 
72
from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError
 
73
from bzrlib.osutils import quotefn, pumpfile, isdir, isfile
 
74
from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree
 
75
from bzrlib.revision import Revision
 
76
from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \
 
77
     format_date
 
78
 
 
79
 
 
80
def _squish_command_name(cmd):
 
81
    return 'cmd_' + cmd.replace('-', '_')
 
82
 
 
83
 
 
84
def _unsquish_command_name(cmd):
 
85
    assert cmd.startswith("cmd_")
 
86
    return cmd[4:].replace('_','-')
 
87
 
 
88
def _get_all_cmds():
 
89
    """Return canonical name and class for all registered commands."""
 
90
    for k, v in globals().iteritems():
 
91
        if k.startswith("cmd_"):
 
92
            yield _unsquish_command_name(k), v
 
93
 
 
94
def _get_cmd_class(cmd):
 
95
    """Return the canonical name and command class for a command.
 
96
    """
 
97
    cmd = str(cmd)                      # not unicode
 
98
 
 
99
    # first look up this command under the specified name
 
100
    try:
 
101
        return cmd, globals()[_squish_command_name(cmd)]
 
102
    except KeyError:
 
103
        pass
 
104
 
 
105
    # look for any command which claims this as an alias
 
106
    for cmdname, cmdclass in _get_all_cmds():
 
107
        if cmd in cmdclass.aliases:
 
108
            return cmdname, cmdclass
 
109
    else:
 
110
        raise BzrCommandError("unknown command %r" % cmd)
 
111
 
 
112
 
 
113
class Command:
 
114
    """Base class for commands.
 
115
 
 
116
    The docstring for an actual command should give a single-line
 
117
    summary, then a complete description of the command.  A grammar
 
118
    description will be inserted.
 
119
 
 
120
    takes_args
 
121
        List of argument forms, marked with whether they are optional,
 
122
        repeated, etc.
 
123
 
 
124
    takes_options
 
125
        List of options that may be given for this command.
 
126
 
 
127
    hidden
 
128
        If true, this command isn't advertised.
 
129
    """
 
130
    aliases = []
 
131
    
 
132
    takes_args = []
 
133
    takes_options = []
 
134
 
 
135
    hidden = False
 
136
    
 
137
    def __init__(self, options, arguments):
 
138
        """Construct and run the command.
 
139
 
 
140
        Sets self.status to the return value of run()."""
 
141
        assert isinstance(options, dict)
 
142
        assert isinstance(arguments, dict)
 
143
        cmdargs = options.copy()
 
144
        cmdargs.update(arguments)
 
145
        assert self.__doc__ != Command.__doc__, \
 
146
               ("No help message set for %r" % self)
 
147
        self.status = self.run(**cmdargs)
 
148
 
 
149
    
 
150
    def run(self):
 
151
        """Override this in sub-classes.
 
152
 
 
153
        This is invoked with the options and arguments bound to
 
154
        keyword parameters.
 
155
 
 
156
        Return 0 or None if the command was successful, or a shell
 
157
        error code if not.
 
158
        """
 
159
        return 0
 
160
 
 
161
 
 
162
 
 
163
class cmd_status(Command):
 
164
    """Display status summary.
 
165
 
 
166
    For each file there is a single line giving its file state and name.
 
167
    The name is that in the current revision unless it is deleted or
 
168
    missing, in which case the old name is shown.
 
169
    """
 
170
    takes_options = ['all']
 
171
    aliases = ['st', 'stat']
 
172
    
 
173
    def run(self, all=False):
 
174
        #import bzrlib.status
 
175
        #bzrlib.status.tree_status(Branch('.'))
 
176
        Branch('.').show_status(show_all=all)
 
177
 
 
178
 
 
179
class cmd_cat_revision(Command):
 
180
    """Write out metadata for a revision."""
 
181
 
 
182
    hidden = True
 
183
    takes_args = ['revision_id']
 
184
    
 
185
    def run(self, revision_id):
 
186
        Branch('.').get_revision(revision_id).write_xml(sys.stdout)
 
187
 
 
188
 
 
189
class cmd_revno(Command):
 
190
    """Show current revision number.
 
191
 
 
192
    This is equal to the number of revisions on this branch."""
 
193
    def run(self):
 
194
        print Branch('.').revno()
 
195
 
 
196
    
 
197
class cmd_add(Command):
 
198
    """Add specified files or directories.
 
199
 
 
200
    In non-recursive mode, all the named items are added, regardless
 
201
    of whether they were previously ignored.  A warning is given if
 
202
    any of the named files are already versioned.
 
203
 
 
204
    In recursive mode (the default), files are treated the same way
 
205
    but the behaviour for directories is different.  Directories that
 
206
    are already versioned do not give a warning.  All directories,
 
207
    whether already versioned or not, are searched for files or
 
208
    subdirectories that are neither versioned or ignored, and these
 
209
    are added.  This search proceeds recursively into versioned
 
210
    directories.
 
211
 
 
212
    Therefore simply saying 'bzr add .' will version all files that
 
213
    are currently unknown.
 
214
 
 
215
    TODO: Perhaps adding a file whose directly is not versioned should
 
216
    recursively add that parent, rather than giving an error?
 
217
    """
 
218
    takes_args = ['file+']
 
219
    takes_options = ['verbose']
 
220
    
 
221
    def run(self, file_list, verbose=False):
 
222
        bzrlib.add.smart_add(file_list, verbose)
 
223
 
 
224
 
 
225
def Relpath(Command):
 
226
    """Show path of a file relative to root"""
 
227
    takes_args = ('filename')
 
228
    
 
229
    def run(self):
 
230
        print Branch(self.args['filename']).relpath(filename)
 
231
 
 
232
 
 
233
 
 
234
class cmd_inventory(Command):
 
235
    """Show inventory of the current working copy or a revision."""
 
236
    takes_options = ['revision']
 
237
    
 
238
    def run(self, revision=None):
 
239
        b = Branch('.')
 
240
        if revision == None:
 
241
            inv = b.read_working_inventory()
 
242
        else:
 
243
            inv = b.get_revision_inventory(b.lookup_revision(revision))
 
244
 
 
245
        for path, entry in inv.iter_entries():
 
246
            print '%-50s %s' % (entry.file_id, path)
 
247
 
 
248
 
 
249
class cmd_move(Command):
 
250
    """Move files to a different directory.
 
251
 
 
252
    examples:
 
253
        bzr move *.txt doc
 
254
 
 
255
    The destination must be a versioned directory in the same branch.
 
256
    """
 
257
    takes_args = ['source$', 'dest']
 
258
    def run(self, source_list, dest):
 
259
        b = Branch('.')
 
260
 
 
261
        b.move([b.relpath(s) for s in source_list], b.relpath(dest))
 
262
 
 
263
 
 
264
class cmd_rename(Command):
 
265
    """Change the name of an entry.
 
266
 
 
267
    examples:
 
268
      bzr rename frob.c frobber.c
 
269
      bzr rename src/frob.c lib/frob.c
 
270
 
 
271
    It is an error if the destination name exists.
 
272
 
 
273
    See also the 'move' command, which moves files into a different
 
274
    directory without changing their name.
 
275
 
 
276
    TODO: Some way to rename multiple files without invoking bzr for each
 
277
    one?"""
 
278
    takes_args = ['from_name', 'to_name']
 
279
    
 
280
    def run(self, from_name, to_name):
 
281
        b = Branch('.')
 
282
        b.rename_one(b.relpath(from_name), b.relpath(to_name))
 
283
 
 
284
 
 
285
 
 
286
class cmd_renames(Command):
 
287
    """Show list of renamed files.
 
288
 
 
289
    TODO: Option to show renames between two historical versions.
 
290
 
 
291
    TODO: Only show renames under dir, rather than in the whole branch.
 
292
    """
 
293
    takes_args = ['dir?']
 
294
 
 
295
    def run(self, dir='.'):
 
296
        b = Branch(dir)
 
297
        old_inv = b.basis_tree().inventory
 
298
        new_inv = b.read_working_inventory()
 
299
 
 
300
        renames = list(bzrlib.tree.find_renames(old_inv, new_inv))
 
301
        renames.sort()
 
302
        for old_name, new_name in renames:
 
303
            print "%s => %s" % (old_name, new_name)        
 
304
 
 
305
 
 
306
class cmd_info(Command):
 
307
    """Show statistical information for this branch"""
 
308
    def run(self):
 
309
        import info
 
310
        info.show_info(Branch('.'))        
 
311
 
 
312
 
 
313
class cmd_remove(Command):
 
314
    """Make a file unversioned.
 
315
 
 
316
    This makes bzr stop tracking changes to a versioned file.  It does
 
317
    not delete the working copy.
 
318
    """
 
319
    takes_args = ['file+']
 
320
    takes_options = ['verbose']
 
321
    
 
322
    def run(self, file_list, verbose=False):
 
323
        b = Branch(file_list[0])
 
324
        b.remove([b.relpath(f) for f in file_list], verbose=verbose)
 
325
 
 
326
 
 
327
class cmd_file_id(Command):
 
328
    """Print file_id of a particular file or directory.
 
329
 
 
330
    The file_id is assigned when the file is first added and remains the
 
331
    same through all revisions where the file exists, even when it is
 
332
    moved or renamed.
 
333
    """
 
334
    hidden = True
 
335
    takes_args = ['filename']
 
336
    def run(self, filename):
 
337
        b = Branch(filename)
 
338
        i = b.inventory.path2id(b.relpath(filename))
 
339
        if i == None:
 
340
            bailout("%r is not a versioned file" % filename)
 
341
        else:
 
342
            print i
 
343
 
 
344
 
 
345
class cmd_file_path(Command):
 
346
    """Print path of file_ids to a file or directory.
 
347
 
 
348
    This prints one line for each directory down to the target,
 
349
    starting at the branch root."""
 
350
    hidden = True
 
351
    takes_args = ['filename']
 
352
    def run(self, filename):
 
353
        b = Branch(filename)
 
354
        inv = b.inventory
 
355
        fid = inv.path2id(b.relpath(filename))
 
356
        if fid == None:
 
357
            bailout("%r is not a versioned file" % filename)
 
358
        for fip in inv.get_idpath(fid):
 
359
            print fip
 
360
 
 
361
 
 
362
class cmd_revision_history(Command):
 
363
    """Display list of revision ids on this branch."""
 
364
    def run(self):
 
365
        for patchid in Branch('.').revision_history():
 
366
            print patchid
 
367
 
 
368
 
 
369
class cmd_directories(Command):
 
370
    """Display list of versioned directories in this branch."""
 
371
    def run(self):
 
372
        for name, ie in Branch('.').read_working_inventory().directories():
 
373
            if name == '':
 
374
                print '.'
 
375
            else:
 
376
                print name
 
377
 
 
378
 
 
379
class cmd_init(Command):
 
380
    """Make a directory into a versioned branch.
 
381
 
 
382
    Use this to create an empty branch, or before importing an
 
383
    existing project.
 
384
 
 
385
    Recipe for importing a tree of files:
 
386
        cd ~/project
 
387
        bzr init
 
388
        bzr add -v .
 
389
        bzr status
 
390
        bzr commit -m 'imported project'
 
391
    """
 
392
    def run(self):
 
393
        Branch('.', init=True)
 
394
 
 
395
 
 
396
class cmd_diff(Command):
 
397
    """Show differences in working tree.
 
398
    
 
399
    If files are listed, only the changes in those files are listed.
 
400
    Otherwise, all changes for the tree are listed.
 
401
 
 
402
    TODO: Given two revision arguments, show the difference between them.
 
403
 
 
404
    TODO: Allow diff across branches.
 
405
 
 
406
    TODO: Option to use external diff command; could be GNU diff, wdiff,
 
407
          or a graphical diff.
 
408
 
 
409
    TODO: Python difflib is not exactly the same as unidiff; should
 
410
          either fix it up or prefer to use an external diff.
 
411
 
 
412
    TODO: If a directory is given, diff everything under that.
 
413
 
 
414
    TODO: Selected-file diff is inefficient and doesn't show you
 
415
          deleted files.
 
416
 
 
417
    TODO: This probably handles non-Unix newlines poorly.
 
418
    """
 
419
    
 
420
    takes_args = ['file*']
 
421
    takes_options = ['revision']
 
422
    aliases = ['di']
 
423
 
 
424
    def run(self, revision=None, file_list=None):
 
425
        from bzrlib.diff import show_diff
 
426
    
 
427
        show_diff(Branch('.'), revision, file_list)
 
428
 
 
429
 
 
430
class cmd_deleted(Command):
 
431
    """List files deleted in the working tree.
 
432
 
 
433
    TODO: Show files deleted since a previous revision, or between two revisions.
 
434
    """
 
435
    def run(self, show_ids=False):
 
436
        b = Branch('.')
 
437
        old = b.basis_tree()
 
438
        new = b.working_tree()
 
439
 
 
440
        ## TODO: Much more efficient way to do this: read in new
 
441
        ## directories with readdir, rather than stating each one.  Same
 
442
        ## level of effort but possibly much less IO.  (Or possibly not,
 
443
        ## if the directories are very large...)
 
444
 
 
445
        for path, ie in old.inventory.iter_entries():
 
446
            if not new.has_id(ie.file_id):
 
447
                if show_ids:
 
448
                    print '%-50s %s' % (path, ie.file_id)
 
449
                else:
 
450
                    print path
 
451
 
 
452
class cmd_root(Command):
 
453
    """Show the tree root directory.
 
454
 
 
455
    The root is the nearest enclosing directory with a .bzr control
 
456
    directory."""
 
457
    takes_args = ['filename?']
 
458
    def run(self, filename=None):
 
459
        """Print the branch root."""
 
460
        print bzrlib.branch.find_branch_root(filename)
 
461
 
 
462
 
 
463
 
 
464
class cmd_log(Command):
 
465
    """Show log of this branch.
 
466
 
 
467
    TODO: Options to show ids; to limit range; etc.
 
468
    """
 
469
    takes_options = ['timezone', 'verbose']
 
470
    def run(self, timezone='original', verbose=False):
 
471
        Branch('.').write_log(show_timezone=timezone, verbose=verbose)
 
472
 
 
473
 
 
474
class cmd_ls(Command):
 
475
    """List files in a tree.
 
476
 
 
477
    TODO: Take a revision or remote path and list that tree instead.
 
478
    """
 
479
    hidden = True
 
480
    def run(self, revision=None, verbose=False):
 
481
        b = Branch('.')
 
482
        if revision == None:
 
483
            tree = b.working_tree()
 
484
        else:
 
485
            tree = b.revision_tree(b.lookup_revision(revision))
 
486
 
 
487
        for fp, fc, kind, fid in tree.list_files():
 
488
            if verbose:
 
489
                if kind == 'directory':
 
490
                    kindch = '/'
 
491
                elif kind == 'file':
 
492
                    kindch = ''
 
493
                else:
 
494
                    kindch = '???'
 
495
 
 
496
                print '%-8s %s%s' % (fc, fp, kindch)
 
497
            else:
 
498
                print fp
 
499
 
 
500
 
 
501
 
 
502
class cmd_unknowns(Command):
 
503
    """List unknown files"""
 
504
    def run(self):
 
505
        for f in Branch('.').unknowns():
 
506
            print quotefn(f)
 
507
 
 
508
 
 
509
 
 
510
class cmd_ignore(Command):
 
511
    """Ignore a command or pattern"""
 
512
    takes_args = ['name_pattern']
 
513
    
 
514
    def run(self, name_pattern):
 
515
        b = Branch('.')
 
516
 
 
517
        # XXX: This will fail if it's a hardlink; should use an AtomicFile class.
 
518
        f = open(b.abspath('.bzrignore'), 'at')
 
519
        f.write(name_pattern + '\n')
 
520
        f.close()
 
521
 
 
522
        inv = b.working_tree().inventory
 
523
        if inv.path2id('.bzrignore'):
 
524
            mutter('.bzrignore is already versioned')
 
525
        else:
 
526
            mutter('need to make new .bzrignore file versioned')
 
527
            b.add(['.bzrignore'])
 
528
 
 
529
 
 
530
 
 
531
class cmd_ignored(Command):
 
532
    """List ignored files and the patterns that matched them."""
 
533
    def run(self):
 
534
        tree = Branch('.').working_tree()
 
535
        for path, file_class, kind, file_id in tree.list_files():
 
536
            if file_class != 'I':
 
537
                continue
 
538
            ## XXX: Slightly inefficient since this was already calculated
 
539
            pat = tree.is_ignored(path)
 
540
            print '%-50s %s' % (path, pat)
 
541
 
 
542
 
 
543
class cmd_lookup_revision(Command):
 
544
    """Lookup the revision-id from a revision-number
 
545
 
 
546
    example:
 
547
        bzr lookup-revision 33
 
548
        """
 
549
    hidden = True
 
550
    takes_args = ['revno']
 
551
    
 
552
    def run(self, revno):
 
553
        try:
 
554
            revno = int(revno)
 
555
        except ValueError:
 
556
            raise BzrCommandError("not a valid revision-number: %r" % revno)
 
557
 
 
558
        print Branch('.').lookup_revision(revno)
 
559
 
 
560
 
 
561
class cmd_export(Command):
 
562
    """Export past revision to destination directory.
 
563
 
 
564
    If no revision is specified this exports the last committed revision."""
 
565
    takes_args = ['dest']
 
566
    takes_options = ['revision']
 
567
    def run(self, dest, revno=None):
 
568
        b = Branch('.')
 
569
        if revno == None:
 
570
            rh = b.revision_history[-1]
 
571
        else:
 
572
            rh = b.lookup_revision(int(revno))
 
573
        t = b.revision_tree(rh)
 
574
        t.export(dest)
 
575
 
 
576
 
 
577
class cmd_cat(Command):
 
578
    """Write a file's text from a previous revision."""
 
579
 
 
580
    takes_options = ['revision']
 
581
    takes_args = ['filename']
 
582
 
 
583
    def run(self, filename, revision=None):
 
584
        if revision == None:
 
585
            raise BzrCommandError("bzr cat requires a revision number")
 
586
        b = Branch('.')
 
587
        b.print_file(b.relpath(filename), int(revision))
 
588
 
 
589
 
 
590
class cmd_local_time_offset(Command):
 
591
    """Show the offset in seconds from GMT to local time."""
 
592
    hidden = True    
 
593
    def run(self):
 
594
        print bzrlib.osutils.local_time_offset()
 
595
 
 
596
 
 
597
 
 
598
class cmd_commit(Command):
 
599
    """Commit changes into a new revision.
 
600
 
 
601
    TODO: Commit only selected files.
 
602
 
 
603
    TODO: Run hooks on tree to-be-committed, and after commit.
 
604
 
 
605
    TODO: Strict commit that fails if there are unknown or deleted files.
 
606
    """
 
607
    takes_options = ['message', 'verbose']
 
608
    aliases = ['ci', 'checkin']
 
609
 
 
610
    def run(self, message=None, verbose=False):
 
611
        if not message:
 
612
            raise BzrCommandError("please specify a commit message")
 
613
        Branch('.').commit(message, verbose=verbose)
 
614
 
 
615
 
 
616
class cmd_check(Command):
 
617
    """Validate consistency of branch history.
 
618
 
 
619
    This command checks various invariants about the branch storage to
 
620
    detect data corruption or bzr bugs.
 
621
    """
 
622
    takes_args = ['dir?']
 
623
    def run(self, dir='.'):
 
624
        import bzrlib.check
 
625
        bzrlib.check.check(Branch(dir, find_root=False))
 
626
 
 
627
 
 
628
 
 
629
class cmd_whoami(Command):
 
630
    """Show bzr user id."""
 
631
    takes_options = ['email']
 
632
    
 
633
    def run(self, email=False):
 
634
        if email:
 
635
            print bzrlib.osutils.user_email()
 
636
        else:
 
637
            print bzrlib.osutils.username()
 
638
 
 
639
 
 
640
class cmd_selftest(Command):
 
641
    """Run internal test suite"""
 
642
    hidden = True
 
643
    def run(self):
 
644
        failures, tests = 0, 0
 
645
 
 
646
        import doctest, bzrlib.store, bzrlib.tests
 
647
        bzrlib.trace.verbose = False
 
648
 
 
649
        for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \
 
650
            bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add:
 
651
            mf, mt = doctest.testmod(m)
 
652
            failures += mf
 
653
            tests += mt
 
654
            print '%-40s %3d tests' % (m.__name__, mt),
 
655
            if mf:
 
656
                print '%3d FAILED!' % mf
 
657
            else:
 
658
                print
 
659
 
 
660
        print '%-40s %3d tests' % ('total', tests),
 
661
        if failures:
 
662
            print '%3d FAILED!' % failures
 
663
        else:
 
664
            print
 
665
 
 
666
 
 
667
 
 
668
class cmd_version(Command):
 
669
    """Show version of bzr"""
 
670
    def run(self):
 
671
        show_version()
 
672
 
 
673
def show_version():
 
674
    print "bzr (bazaar-ng) %s" % bzrlib.__version__
 
675
    print bzrlib.__copyright__
 
676
    print "http://bazaar-ng.org/"
 
677
    print
 
678
    print "bzr comes with ABSOLUTELY NO WARRANTY.  bzr is free software, and"
 
679
    print "you may use, modify and redistribute it under the terms of the GNU"
 
680
    print "General Public License version 2 or later."
 
681
 
 
682
 
 
683
class cmd_rocks(Command):
 
684
    """Statement of optimism."""
 
685
    hidden = True
 
686
    def run(self):
 
687
        print "it sure does!"
 
688
 
 
689
 
 
690
class cmd_assert_fail(Command):
 
691
    """Test reporting of assertion failures"""
 
692
    hidden = True
 
693
    def run(self):
 
694
        assert False, "always fails"
 
695
 
 
696
 
 
697
class cmd_help(Command):
 
698
    """Show help on a command or other topic.
 
699
 
 
700
    For a list of all available commands, say 'bzr help commands'."""
 
701
    takes_args = ['topic?']
 
702
    aliases = ['?']
 
703
    
 
704
    def run(self, topic=None):
 
705
        help(topic)
 
706
 
 
707
 
 
708
def help(topic=None):
 
709
    if topic == None:
 
710
        print __doc__
 
711
    elif topic == 'commands':
 
712
        help_commands()
 
713
    else:
 
714
        help_on_command(topic)
 
715
 
 
716
 
 
717
def help_on_command(cmdname):
 
718
    cmdname = str(cmdname)
 
719
 
 
720
    from inspect import getdoc
 
721
    topic, cmdclass = _get_cmd_class(cmdname)
 
722
 
 
723
    doc = getdoc(cmdclass)
 
724
    if doc == None:
 
725
        raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname)
 
726
 
 
727
    if '\n' in doc:
 
728
        short, rest = doc.split('\n', 1)
 
729
    else:
 
730
        short = doc
 
731
        rest = ''
 
732
 
 
733
    print 'usage: bzr ' + topic,
 
734
    for aname in cmdclass.takes_args:
 
735
        aname = aname.upper()
 
736
        if aname[-1] in ['$', '+']:
 
737
            aname = aname[:-1] + '...'
 
738
        elif aname[-1] == '?':
 
739
            aname = '[' + aname[:-1] + ']'
 
740
        elif aname[-1] == '*':
 
741
            aname = '[' + aname[:-1] + '...]'
 
742
        print aname,
 
743
    print 
 
744
    print short
 
745
    if rest:
 
746
        print rest
 
747
 
 
748
    help_on_option(cmdclass.takes_options)
 
749
 
 
750
 
 
751
def help_on_option(options):
 
752
    if not options:
 
753
        return
 
754
    
 
755
    print
 
756
    print 'options:'
 
757
    for on in options:
 
758
        l = '    --' + on
 
759
        for shortname, longname in SHORT_OPTIONS.items():
 
760
            if longname == on:
 
761
                l += ', -' + shortname
 
762
                break
 
763
        print l
 
764
 
 
765
 
 
766
def help_commands():
 
767
    """List all commands"""
 
768
    import inspect
 
769
    
 
770
    accu = []
 
771
    for cmdname, cmdclass in _get_all_cmds():
 
772
        accu.append((cmdname, cmdclass))
 
773
    accu.sort()
 
774
    for cmdname, cmdclass in accu:
 
775
        if cmdclass.hidden:
 
776
            continue
 
777
        print cmdname
 
778
        help = inspect.getdoc(cmdclass)
 
779
        if help:
 
780
            print "    " + help.split('\n', 1)[0]
 
781
            
 
782
 
 
783
######################################################################
 
784
# main routine
 
785
 
 
786
 
 
787
# list of all available options; the rhs can be either None for an
 
788
# option that takes no argument, or a constructor function that checks
 
789
# the type.
 
790
OPTIONS = {
 
791
    'all':                    None,
 
792
    'help':                   None,
 
793
    'message':                unicode,
 
794
    'profile':                None,
 
795
    'revision':               int,
 
796
    'show-ids':               None,
 
797
    'timezone':               str,
 
798
    'verbose':                None,
 
799
    'version':                None,
 
800
    'email':                  None,
 
801
    }
 
802
 
 
803
SHORT_OPTIONS = {
 
804
    'm':                      'message',
 
805
    'r':                      'revision',
 
806
    'v':                      'verbose',
 
807
}
 
808
 
 
809
 
 
810
def parse_args(argv):
 
811
    """Parse command line.
 
812
    
 
813
    Arguments and options are parsed at this level before being passed
 
814
    down to specific command handlers.  This routine knows, from a
 
815
    lookup table, something about the available options, what optargs
 
816
    they take, and which commands will accept them.
 
817
 
 
818
    >>> parse_args('--help'.split())
 
819
    ([], {'help': True})
 
820
    >>> parse_args('--version'.split())
 
821
    ([], {'version': True})
 
822
    >>> parse_args('status --all'.split())
 
823
    (['status'], {'all': True})
 
824
    >>> parse_args('commit --message=biter'.split())
 
825
    (['commit'], {'message': u'biter'})
 
826
    """
 
827
    args = []
 
828
    opts = {}
 
829
 
 
830
    # TODO: Maybe handle '--' to end options?
 
831
 
 
832
    while argv:
 
833
        a = argv.pop(0)
 
834
        if a[0] == '-':
 
835
            # option names must not be unicode
 
836
            a = str(a)
 
837
            optarg = None
 
838
            if a[1] == '-':
 
839
                mutter("  got option %r" % a)
 
840
                if '=' in a:
 
841
                    optname, optarg = a[2:].split('=', 1)
 
842
                else:
 
843
                    optname = a[2:]
 
844
                if optname not in OPTIONS:
 
845
                    bailout('unknown long option %r' % a)
 
846
            else:
 
847
                shortopt = a[1:]
 
848
                if shortopt not in SHORT_OPTIONS:
 
849
                    bailout('unknown short option %r' % a)
 
850
                optname = SHORT_OPTIONS[shortopt]
 
851
            
 
852
            if optname in opts:
 
853
                # XXX: Do we ever want to support this, e.g. for -r?
 
854
                bailout('repeated option %r' % a)
 
855
                
 
856
            optargfn = OPTIONS[optname]
 
857
            if optargfn:
 
858
                if optarg == None:
 
859
                    if not argv:
 
860
                        bailout('option %r needs an argument' % a)
 
861
                    else:
 
862
                        optarg = argv.pop(0)
 
863
                opts[optname] = optargfn(optarg)
 
864
            else:
 
865
                if optarg != None:
 
866
                    bailout('option %r takes no argument' % optname)
 
867
                opts[optname] = True
 
868
        else:
 
869
            args.append(a)
 
870
 
 
871
    return args, opts
 
872
 
 
873
 
 
874
 
 
875
 
 
876
def _match_argform(cmd, takes_args, args):
 
877
    argdict = {}
 
878
 
 
879
    # step through args and takes_args, allowing appropriate 0-many matches
 
880
    for ap in takes_args:
 
881
        argname = ap[:-1]
 
882
        if ap[-1] == '?':
 
883
            if args:
 
884
                argdict[argname] = args.pop(0)
 
885
        elif ap[-1] == '*': # all remaining arguments
 
886
            if args:
 
887
                argdict[argname + '_list'] = args[:]
 
888
                args = []
 
889
            else:
 
890
                argdict[argname + '_list'] = None
 
891
        elif ap[-1] == '+':
 
892
            if not args:
 
893
                raise BzrCommandError("command %r needs one or more %s"
 
894
                        % (cmd, argname.upper()))
 
895
            else:
 
896
                argdict[argname + '_list'] = args[:]
 
897
                args = []
 
898
        elif ap[-1] == '$': # all but one
 
899
            if len(args) < 2:
 
900
                raise BzrCommandError("command %r needs one or more %s"
 
901
                        % (cmd, argname.upper()))
 
902
            argdict[argname + '_list'] = args[:-1]
 
903
            args[:-1] = []                
 
904
        else:
 
905
            # just a plain arg
 
906
            argname = ap
 
907
            if not args:
 
908
                raise BzrCommandError("command %r requires argument %s"
 
909
                        % (cmd, argname.upper()))
 
910
            else:
 
911
                argdict[argname] = args.pop(0)
 
912
            
 
913
    if args:
 
914
        raise BzrCommandError("extra argument to command %s: %s"
 
915
                              % (cmd, args[0]))
 
916
 
 
917
    return argdict
 
918
 
 
919
 
 
920
 
 
921
def run_bzr(argv):
 
922
    """Execute a command.
 
923
 
 
924
    This is similar to main(), but without all the trappings for
 
925
    logging and error handling.  
 
926
    """
 
927
 
 
928
    argv = [a.decode(bzrlib.user_encoding) for a in argv]
 
929
    
 
930
    try:
 
931
        args, opts = parse_args(argv[1:])
 
932
        if 'help' in opts:
 
933
            if args:
 
934
                help(args[0])
 
935
            else:
 
936
                help()
 
937
            return 0
 
938
        elif 'version' in opts:
 
939
            show_version()
 
940
            return 0
 
941
        cmd = str(args.pop(0))
 
942
    except IndexError:
 
943
        log_error('usage: bzr COMMAND')
 
944
        log_error('  try "bzr help"')
 
945
        return 1
 
946
 
 
947
    canonical_cmd, cmd_class = _get_cmd_class(cmd)
 
948
 
 
949
    # global option
 
950
    if 'profile' in opts:
 
951
        profile = True
 
952
        del opts['profile']
 
953
    else:
 
954
        profile = False
 
955
 
 
956
    # check options are reasonable
 
957
    allowed = cmd_class.takes_options
 
958
    for oname in opts:
 
959
        if oname not in allowed:
 
960
            raise BzrCommandError("option %r is not allowed for command %r"
 
961
                                  % (oname, cmd))
 
962
 
 
963
    # mix arguments and options into one dictionary
 
964
    cmdargs = _match_argform(cmd, cmd_class.takes_args, args)
 
965
    cmdopts = {}
 
966
    for k, v in opts.items():
 
967
        cmdopts[k.replace('-', '_')] = v
 
968
 
 
969
    if profile:
 
970
        import hotshot, tempfile
 
971
        pffileno, pfname = tempfile.mkstemp()
 
972
        try:
 
973
            prof = hotshot.Profile(pfname)
 
974
            ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0
 
975
            prof.close()
 
976
 
 
977
            import hotshot.stats
 
978
            stats = hotshot.stats.load(pfname)
 
979
            #stats.strip_dirs()
 
980
            stats.sort_stats('time')
 
981
            ## XXX: Might like to write to stderr or the trace file instead but
 
982
            ## print_stats seems hardcoded to stdout
 
983
            stats.print_stats(20)
 
984
            
 
985
            return ret.status
 
986
 
 
987
        finally:
 
988
            os.close(pffileno)
 
989
            os.remove(pfname)
 
990
    else:
 
991
        cmdobj = cmd_class(cmdopts, cmdargs).status 
 
992
 
 
993
 
 
994
def _report_exception(e, summary, quiet=False):
 
995
    import traceback
 
996
    log_error('bzr: ' + summary)
 
997
    bzrlib.trace.log_exception(e)
 
998
 
 
999
    if not quiet:
 
1000
        tb = sys.exc_info()[2]
 
1001
        exinfo = traceback.extract_tb(tb)
 
1002
        if exinfo:
 
1003
            sys.stderr.write('  at %s:%d in %s()\n' % exinfo[-1][:3])
 
1004
        sys.stderr.write('  see ~/.bzr.log for debug information\n')
 
1005
 
 
1006
 
 
1007
 
 
1008
def main(argv):
 
1009
    import errno
 
1010
    
 
1011
    bzrlib.open_tracefile(argv)
 
1012
 
 
1013
    try:
 
1014
        try:
 
1015
            try:
 
1016
                return run_bzr(argv)
 
1017
            finally:
 
1018
                # do this here inside the exception wrappers to catch EPIPE
 
1019
                sys.stdout.flush()
 
1020
        except BzrError, e:
 
1021
            quiet = isinstance(e, (BzrCommandError))
 
1022
            _report_exception(e, 'error: ' + e.args[0], quiet=quiet)
 
1023
            if len(e.args) > 1:
 
1024
                for h in e.args[1]:
 
1025
                    # some explanation or hints
 
1026
                    log_error('  ' + h)
 
1027
            return 1
 
1028
        except AssertionError, e:
 
1029
            msg = 'assertion failed'
 
1030
            if str(e):
 
1031
                msg += ': ' + str(e)
 
1032
            _report_exception(e, msg)
 
1033
            return 2
 
1034
        except KeyboardInterrupt, e:
 
1035
            _report_exception(e, 'interrupted', quiet=True)
 
1036
            return 2
 
1037
        except Exception, e:
 
1038
            quiet = False
 
1039
            if isinstance(e, IOError) and e.errno == errno.EPIPE:
 
1040
                quiet = True
 
1041
                msg = 'broken pipe'
 
1042
            else:
 
1043
                msg = str(e).rstrip('\n')
 
1044
            _report_exception(e, msg, quiet)
 
1045
            return 2
 
1046
    finally:
 
1047
        bzrlib.trace.close_trace()
 
1048
 
 
1049
 
 
1050
if __name__ == '__main__':
 
1051
    sys.exit(main(sys.argv))