/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/branch.py

More work on roundtrip push support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
3
 
 
4
 
# This program is free software; you can redistribute it and/or modify
5
 
# it under the terms of the GNU General Public License as published by
6
 
# the Free Software Foundation; either version 2 of the License, or
7
 
# (at your option) any later version.
8
 
 
9
 
# This program is distributed in the hope that it will be useful,
10
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
# GNU General Public License for more details.
13
 
 
14
 
# You should have received a copy of the GNU General Public License
15
 
# along with this program; if not, write to the Free Software
16
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
 
 
18
 
 
19
 
from sets import Set
20
 
 
21
 
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
22
 
import traceback, socket, fnmatch, difflib, time
23
 
from binascii import hexlify
24
 
 
25
 
import bzrlib
26
 
from inventory import Inventory
27
 
from trace import mutter, note
28
 
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
29
 
from inventory import InventoryEntry, Inventory
30
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
31
 
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
32
 
     joinpath, sha_string, file_kind, local_time_offset
33
 
from store import ImmutableStore
34
 
from revision import Revision
35
 
from errors import bailout
36
 
from textui import show_status
37
 
from diff import diff_trees
38
 
 
39
 
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
40
 
## TODO: Maybe include checks for common corruption of newlines, etc?
41
 
 
42
 
 
43
 
 
44
 
 
45
 
 
46
 
######################################################################
47
 
# branch objects
48
 
 
49
 
class Branch:
50
 
    """Branch holding a history of revisions.
51
 
 
52
 
    :todo: Perhaps use different stores for different classes of object,
53
 
           so that we can keep track of how much space each one uses,
54
 
           or garbage-collect them.
55
 
 
56
 
    :todo: Add a RemoteBranch subclass.  For the basic case of read-only
57
 
           HTTP access this should be very easy by, 
58
 
           just redirecting controlfile access into HTTP requests.
59
 
           We would need a RemoteStore working similarly.
60
 
 
61
 
    :todo: Keep the on-disk branch locked while the object exists.
62
 
 
63
 
    :todo: mkdir() method.
64
 
    """
65
 
    def __init__(self, base, init=False):
66
 
        """Create new branch object at a particular location.
67
 
 
68
 
        :param base: Base directory for the branch.
69
 
 
70
 
        :param init: If True, create new control files in a previously
71
 
             unversioned directory.  If False, the branch must already
72
 
             be versioned.
73
 
 
74
 
        In the test suite, creation of new trees is tested using the
75
 
        `ScratchBranch` class.
76
 
        """
77
 
        self.base = os.path.realpath(base)
78
 
        if init:
79
 
            self._make_control()
80
 
        else:
81
 
            if not isdir(self.controlfilename('.')):
82
 
                bailout("not a bzr branch: %s" % quotefn(base),
83
 
                        ['use "bzr init" to initialize a new working tree',
84
 
                         'current bzr can only operate from top-of-tree'])
85
 
            self._check_format()
86
 
 
87
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
88
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
89
 
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
90
 
 
91
 
 
92
 
    def __str__(self):
93
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
94
 
 
95
 
 
96
 
    __repr__ = __str__
97
 
 
98
 
 
99
 
    def _rel(self, name):
100
 
        """Return filename relative to branch top"""
101
 
        return os.path.join(self.base, name)
102
 
        
103
 
 
104
 
    def controlfilename(self, file_or_path):
105
 
        """Return location relative to branch."""
106
 
        if isinstance(file_or_path, types.StringTypes):
107
 
            file_or_path = [file_or_path]
108
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
109
 
 
110
 
 
111
 
    def controlfile(self, file_or_path, mode='r'):
112
 
        """Open a control file for this branch"""
113
 
        return file(self.controlfilename(file_or_path), mode)
114
 
 
115
 
 
116
 
    def _make_control(self):
117
 
        os.mkdir(self.controlfilename([]))
118
 
        self.controlfile('README', 'w').write(
119
 
            "This is a Bazaar-NG control directory.\n"
120
 
            "Do not change any files in this directory.")
121
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
122
 
        for d in ('text-store', 'inventory-store', 'revision-store'):
123
 
            os.mkdir(self.controlfilename(d))
124
 
        for f in ('revision-history', 'merged-patches',
125
 
                  'pending-merged-patches', 'branch-name'):
126
 
            self.controlfile(f, 'w').write('')
127
 
        mutter('created control directory in ' + self.base)
128
 
        Inventory().write_xml(self.controlfile('inventory','w'))
129
 
 
130
 
 
131
 
    def _check_format(self):
132
 
        """Check this branch format is supported.
133
 
 
134
 
        The current tool only supports the current unstable format.
135
 
 
136
 
        In the future, we might need different in-memory Branch
137
 
        classes to support downlevel branches.  But not yet.
138
 
        """        
139
 
        # read in binary mode to detect newline wierdness.
140
 
        fmt = self.controlfile('branch-format', 'rb').read()
141
 
        if fmt != BZR_BRANCH_FORMAT:
142
 
            bailout('sorry, branch format %r not supported' % fmt,
143
 
                    ['use a different bzr version',
144
 
                     'or remove the .bzr directory and "bzr init" again'])
145
 
 
146
 
 
147
 
    def read_working_inventory(self):
148
 
        """Read the working inventory."""
149
 
        before = time.time()
150
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
151
 
        mutter("loaded inventory of %d items in %f"
152
 
               % (len(inv), time.time() - before))
153
 
        return inv
154
 
 
155
 
 
156
 
    def _write_inventory(self, inv):
157
 
        """Update the working inventory.
158
 
 
159
 
        That is to say, the inventory describing changes underway, that
160
 
        will be committed to the next revision.
161
 
        """
162
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
163
 
        tmpfname = self.controlfilename('inventory.tmp')
164
 
        tmpf = file(tmpfname, 'w')
165
 
        inv.write_xml(tmpf)
166
 
        tmpf.close()
167
 
        os.rename(tmpfname, self.controlfilename('inventory'))
168
 
        mutter('wrote working inventory')
169
 
 
170
 
 
171
 
    inventory = property(read_working_inventory, _write_inventory, None,
172
 
                         """Inventory for the working copy.""")
173
 
 
174
 
 
175
 
    def add(self, files, verbose=False):
176
 
        """Make files versioned.
177
 
 
178
 
        This puts the files in the Added state, so that they will be
179
 
        recorded by the next commit.
180
 
 
181
 
        :todo: Perhaps have an option to add the ids even if the files do
182
 
               not (yet) exist.
183
 
 
184
 
        :todo: Perhaps return the ids of the files?  But then again it
185
 
               is easy to retrieve them if they're needed.
186
 
 
187
 
        :todo: Option to specify file id.
188
 
 
189
 
        :todo: Adding a directory should optionally recurse down and
190
 
               add all non-ignored children.  Perhaps do that in a
191
 
               higher-level method.
192
 
 
193
 
        >>> b = ScratchBranch(files=['foo'])
194
 
        >>> 'foo' in b.unknowns()
195
 
        True
196
 
        >>> b.show_status()
197
 
        ?       foo
198
 
        >>> b.add('foo')
199
 
        >>> 'foo' in b.unknowns()
200
 
        False
201
 
        >>> bool(b.inventory.path2id('foo'))
202
 
        True
203
 
        >>> b.show_status()
204
 
        A       foo
205
 
 
206
 
        >>> b.add('foo')
207
 
        Traceback (most recent call last):
208
 
        ...
209
 
        BzrError: ('foo is already versioned', [])
210
 
 
211
 
        >>> b.add(['nothere'])
212
 
        Traceback (most recent call last):
213
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
214
 
        """
215
 
 
216
 
        # TODO: Re-adding a file that is removed in the working copy
217
 
        # should probably put it back with the previous ID.
218
 
        if isinstance(files, types.StringTypes):
219
 
            files = [files]
220
 
        
221
 
        inv = self.read_working_inventory()
222
 
        for f in files:
223
 
            if is_control_file(f):
224
 
                bailout("cannot add control file %s" % quotefn(f))
225
 
 
226
 
            fp = splitpath(f)
227
 
 
228
 
            if len(fp) == 0:
229
 
                bailout("cannot add top-level %r" % f)
230
 
                
231
 
            fullpath = os.path.normpath(self._rel(f))
232
 
 
233
 
            if isfile(fullpath):
234
 
                kind = 'file'
235
 
            elif isdir(fullpath):
236
 
                kind = 'directory'
237
 
            else:
238
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
239
 
 
240
 
            if len(fp) > 1:
241
 
                parent_name = joinpath(fp[:-1])
242
 
                mutter("lookup parent %r" % parent_name)
243
 
                parent_id = inv.path2id(parent_name)
244
 
                if parent_id == None:
245
 
                    bailout("cannot add: parent %r is not versioned"
246
 
                            % joinpath(fp[:-1]))
247
 
            else:
248
 
                parent_id = None
249
 
 
250
 
            file_id = _gen_file_id(fp[-1])
251
 
            inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id))
252
 
            if verbose:
253
 
                show_status('A', kind, quotefn(f))
254
 
                
255
 
            mutter("add file %s file_id:{%s} kind=%r parent_id={%s}"
256
 
                   % (f, file_id, kind, parent_id))
257
 
        self._write_inventory(inv)
258
 
 
259
 
 
260
 
 
261
 
    def remove(self, files, verbose=False):
262
 
        """Mark nominated files for removal from the inventory.
263
 
 
264
 
        This does not remove their text.  This does not run on 
265
 
 
266
 
        :todo: Refuse to remove modified files unless --force is given?
267
 
 
268
 
        >>> b = ScratchBranch(files=['foo'])
269
 
        >>> b.add('foo')
270
 
        >>> b.inventory.has_filename('foo')
271
 
        True
272
 
        >>> b.remove('foo')
273
 
        >>> b.working_tree().has_filename('foo')
274
 
        True
275
 
        >>> b.inventory.has_filename('foo')
276
 
        False
277
 
        
278
 
        >>> b = ScratchBranch(files=['foo'])
279
 
        >>> b.add('foo')
280
 
        >>> b.commit('one')
281
 
        >>> b.remove('foo')
282
 
        >>> b.commit('two')
283
 
        >>> b.inventory.has_filename('foo') 
284
 
        False
285
 
        >>> b.basis_tree().has_filename('foo') 
286
 
        False
287
 
        >>> b.working_tree().has_filename('foo') 
288
 
        True
289
 
 
290
 
        :todo: Do something useful with directories.
291
 
 
292
 
        :todo: Should this remove the text or not?  Tough call; not
293
 
        removing may be useful and the user can just use use rm, and
294
 
        is the opposite of add.  Removing it is consistent with most
295
 
        other tools.  Maybe an option.
296
 
        """
297
 
        ## TODO: Normalize names
298
 
        ## TODO: Remove nested loops; better scalability
299
 
 
300
 
        if isinstance(files, types.StringTypes):
301
 
            files = [files]
302
 
        
303
 
        inv = self.read_working_inventory()
304
 
 
305
 
        # do this before any modifications
306
 
        for f in files:
307
 
            fid = inv.path2id(f)
308
 
            if not fid:
309
 
                bailout("cannot remove unversioned file %s" % quotefn(f))
310
 
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
311
 
            if verbose:
312
 
                show_status('D', inv[fid].kind, quotefn(f))
313
 
            del inv[fid]
314
 
 
315
 
        self._write_inventory(inv)
316
 
 
317
 
 
318
 
    def unknowns(self):
319
 
        """Return all unknown files.
320
 
 
321
 
        These are files in the working directory that are not versioned or
322
 
        control files or ignored.
323
 
        
324
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
325
 
        >>> list(b.unknowns())
326
 
        ['foo']
327
 
        >>> b.add('foo')
328
 
        >>> list(b.unknowns())
329
 
        []
330
 
        >>> b.remove('foo')
331
 
        >>> list(b.unknowns())
332
 
        ['foo']
333
 
        """
334
 
        return self.working_tree().unknowns()
335
 
 
336
 
 
337
 
    def commit(self, message, timestamp=None, timezone=None,
338
 
               committer=None,
339
 
               verbose=False):
340
 
        """Commit working copy as a new revision.
341
 
        
342
 
        The basic approach is to add all the file texts into the
343
 
        store, then the inventory, then make a new revision pointing
344
 
        to that inventory and store that.
345
 
        
346
 
        This is not quite safe if the working copy changes during the
347
 
        commit; for the moment that is simply not allowed.  A better
348
 
        approach is to make a temporary copy of the files before
349
 
        computing their hashes, and then add those hashes in turn to
350
 
        the inventory.  This should mean at least that there are no
351
 
        broken hash pointers.  There is no way we can get a snapshot
352
 
        of the whole directory at an instant.  This would also have to
353
 
        be robust against files disappearing, moving, etc.  So the
354
 
        whole thing is a bit hard.
355
 
 
356
 
        :param timestamp: if not None, seconds-since-epoch for a
357
 
             postdated/predated commit.
358
 
        """
359
 
 
360
 
        ## TODO: Show branch names
361
 
 
362
 
        # TODO: Don't commit if there are no changes, unless forced?
363
 
 
364
 
        # First walk over the working inventory; and both update that
365
 
        # and also build a new revision inventory.  The revision
366
 
        # inventory needs to hold the text-id, sha1 and size of the
367
 
        # actual file versions committed in the revision.  (These are
368
 
        # not present in the working inventory.)  We also need to
369
 
        # detect missing/deleted files, and remove them from the
370
 
        # working inventory.
371
 
 
372
 
        work_inv = self.read_working_inventory()
373
 
        inv = Inventory()
374
 
        basis = self.basis_tree()
375
 
        basis_inv = basis.inventory
376
 
        missing_ids = []
377
 
        for path, entry in work_inv.iter_entries():
378
 
            ## TODO: Cope with files that have gone missing.
379
 
 
380
 
            ## TODO: Check that the file kind has not changed from the previous
381
 
            ## revision of this file (if any).
382
 
 
383
 
            entry = entry.copy()
384
 
 
385
 
            p = self._rel(path)
386
 
            file_id = entry.file_id
387
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
388
 
 
389
 
            if not os.path.exists(p):
390
 
                mutter("    file is missing, removing from inventory")
391
 
                if verbose:
392
 
                    show_status('D', entry.kind, quotefn(path))
393
 
                missing_ids.append(file_id)
394
 
                continue
395
 
 
396
 
            # TODO: Handle files that have been deleted
397
 
 
398
 
            # TODO: Maybe a special case for empty files?  Seems a
399
 
            # waste to store them many times.
400
 
 
401
 
            inv.add(entry)
402
 
 
403
 
            if basis_inv.has_id(file_id):
404
 
                old_kind = basis_inv[file_id].kind
405
 
                if old_kind != entry.kind:
406
 
                    bailout("entry %r changed kind from %r to %r"
407
 
                            % (file_id, old_kind, entry.kind))
408
 
 
409
 
            if entry.kind == 'directory':
410
 
                if not isdir(p):
411
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
412
 
            elif entry.kind == 'file':
413
 
                if not isfile(p):
414
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
415
 
 
416
 
                content = file(p, 'rb').read()
417
 
 
418
 
                entry.text_sha1 = sha_string(content)
419
 
                entry.text_size = len(content)
420
 
 
421
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
422
 
                if (old_ie
423
 
                    and (old_ie.text_size == entry.text_size)
424
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
425
 
                    ## assert content == basis.get_file(file_id).read()
426
 
                    entry.text_id = basis_inv[file_id].text_id
427
 
                    mutter('    unchanged from previous text_id {%s}' %
428
 
                           entry.text_id)
429
 
                    
430
 
                else:
431
 
                    entry.text_id = _gen_file_id(entry.name)
432
 
                    self.text_store.add(content, entry.text_id)
433
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
434
 
                    if verbose:
435
 
                        if not old_ie:
436
 
                            state = 'A'
437
 
                        elif (old_ie.name == entry.name
438
 
                              and old_ie.parent_id == entry.parent_id):
439
 
                            state = 'R'
440
 
                        else:
441
 
                            state = 'M'
442
 
 
443
 
                        show_status(state, entry.kind, quotefn(path))
444
 
 
445
 
        for file_id in missing_ids:
446
 
            # have to do this later so we don't mess up the iterator.
447
 
            # since parents may be removed before their children we
448
 
            # have to test.
449
 
 
450
 
            # FIXME: There's probably a better way to do this; perhaps
451
 
            # the workingtree should know how to filter itself.
452
 
            if work_inv.has_id(file_id):
453
 
                del work_inv[file_id]
454
 
 
455
 
 
456
 
        inv_id = rev_id = _gen_revision_id(time.time())
457
 
        
458
 
        inv_tmp = tempfile.TemporaryFile()
459
 
        inv.write_xml(inv_tmp)
460
 
        inv_tmp.seek(0)
461
 
        self.inventory_store.add(inv_tmp, inv_id)
462
 
        mutter('new inventory_id is {%s}' % inv_id)
463
 
 
464
 
        self._write_inventory(work_inv)
465
 
 
466
 
        if timestamp == None:
467
 
            timestamp = time.time()
468
 
 
469
 
        if committer == None:
470
 
            committer = username()
471
 
 
472
 
        if timezone == None:
473
 
            timezone = local_time_offset()
474
 
 
475
 
        mutter("building commit log message")
476
 
        rev = Revision(timestamp=timestamp,
477
 
                       timezone=timezone,
478
 
                       committer=committer,
479
 
                       precursor = self.last_patch(),
480
 
                       message = message,
481
 
                       inventory_id=inv_id,
482
 
                       revision_id=rev_id)
483
 
 
484
 
        rev_tmp = tempfile.TemporaryFile()
485
 
        rev.write_xml(rev_tmp)
486
 
        rev_tmp.seek(0)
487
 
        self.revision_store.add(rev_tmp, rev_id)
488
 
        mutter("new revision_id is {%s}" % rev_id)
489
 
        
490
 
        ## XXX: Everything up to here can simply be orphaned if we abort
491
 
        ## the commit; it will leave junk files behind but that doesn't
492
 
        ## matter.
493
 
 
494
 
        ## TODO: Read back the just-generated changeset, and make sure it
495
 
        ## applies and recreates the right state.
496
 
 
497
 
        ## TODO: Also calculate and store the inventory SHA1
498
 
        mutter("committing patch r%d" % (self.revno() + 1))
499
 
 
500
 
        mutter("append to revision-history")
501
 
        self.controlfile('revision-history', 'at').write(rev_id + '\n')
502
 
 
503
 
        mutter("done!")
504
 
 
505
 
 
506
 
    def get_revision(self, revision_id):
507
 
        """Return the Revision object for a named revision"""
508
 
        r = Revision.read_xml(self.revision_store[revision_id])
509
 
        assert r.revision_id == revision_id
510
 
        return r
511
 
 
512
 
 
513
 
    def get_inventory(self, inventory_id):
514
 
        """Get Inventory object by hash.
515
 
 
516
 
        :todo: Perhaps for this and similar methods, take a revision
517
 
               parameter which can be either an integer revno or a
518
 
               string hash."""
519
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
520
 
        return i
521
 
 
522
 
 
523
 
    def get_revision_inventory(self, revision_id):
524
 
        """Return inventory of a past revision."""
525
 
        if revision_id == None:
526
 
            return Inventory()
527
 
        else:
528
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
529
 
 
530
 
 
531
 
    def revision_history(self):
532
 
        """Return sequence of revision hashes on to this branch.
533
 
 
534
 
        >>> ScratchBranch().revision_history()
535
 
        []
536
 
        """
537
 
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
538
 
 
539
 
 
540
 
    def revno(self):
541
 
        """Return current revision number for this branch.
542
 
 
543
 
        That is equivalent to the number of revisions committed to
544
 
        this branch.
545
 
 
546
 
        >>> b = ScratchBranch()
547
 
        >>> b.revno()
548
 
        0
549
 
        >>> b.commit('no foo')
550
 
        >>> b.revno()
551
 
        1
552
 
        """
553
 
        return len(self.revision_history())
554
 
 
555
 
 
556
 
    def last_patch(self):
557
 
        """Return last patch hash, or None if no history.
558
 
 
559
 
        >>> ScratchBranch().last_patch() == None
560
 
        True
561
 
        """
562
 
        ph = self.revision_history()
563
 
        if ph:
564
 
            return ph[-1]
565
 
 
566
 
 
567
 
    def lookup_revision(self, revno):
568
 
        """Return revision hash for revision number."""
569
 
        if revno == 0:
570
 
            return None
571
 
 
572
 
        try:
573
 
            # list is 0-based; revisions are 1-based
574
 
            return self.revision_history()[revno-1]
575
 
        except IndexError:
576
 
            bailout("no such revision %s" % revno)
577
 
 
578
 
 
579
 
    def revision_tree(self, revision_id):
580
 
        """Return Tree for a revision on this branch.
581
 
 
582
 
        `revision_id` may be None for the null revision, in which case
583
 
        an `EmptyTree` is returned."""
584
 
 
585
 
        if revision_id == None:
586
 
            return EmptyTree()
587
 
        else:
588
 
            inv = self.get_revision_inventory(revision_id)
589
 
            return RevisionTree(self.text_store, inv)
590
 
 
591
 
 
592
 
    def working_tree(self):
593
 
        """Return a `Tree` for the working copy."""
594
 
        return WorkingTree(self.base, self.read_working_inventory())
595
 
 
596
 
 
597
 
    def basis_tree(self):
598
 
        """Return `Tree` object for last revision.
599
 
 
600
 
        If there are no revisions yet, return an `EmptyTree`.
601
 
 
602
 
        >>> b = ScratchBranch(files=['foo'])
603
 
        >>> b.basis_tree().has_filename('foo')
604
 
        False
605
 
        >>> b.working_tree().has_filename('foo')
606
 
        True
607
 
        >>> b.add('foo')
608
 
        >>> b.commit('add foo')
609
 
        >>> b.basis_tree().has_filename('foo')
610
 
        True
611
 
        """
612
 
        r = self.last_patch()
613
 
        if r == None:
614
 
            return EmptyTree()
615
 
        else:
616
 
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
617
 
 
618
 
 
619
 
 
620
 
    def write_log(self, show_timezone='original'):
621
 
        """Write out human-readable log of commits to this branch
622
 
 
623
 
        :param utc: If true, show dates in universal time, not local time."""
624
 
        ## TODO: Option to choose either original, utc or local timezone
625
 
        revno = 1
626
 
        precursor = None
627
 
        for p in self.revision_history():
628
 
            print '-' * 40
629
 
            print 'revno:', revno
630
 
            ## TODO: Show hash if --id is given.
631
 
            ##print 'revision-hash:', p
632
 
            rev = self.get_revision(p)
633
 
            print 'committer:', rev.committer
634
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
635
 
                                                 show_timezone))
636
 
 
637
 
            ## opportunistic consistency check, same as check_patch_chaining
638
 
            if rev.precursor != precursor:
639
 
                bailout("mismatched precursor!")
640
 
 
641
 
            print 'message:'
642
 
            if not rev.message:
643
 
                print '  (no message)'
644
 
            else:
645
 
                for l in rev.message.split('\n'):
646
 
                    print '  ' + l
647
 
 
648
 
            revno += 1
649
 
            precursor = p
650
 
 
651
 
 
652
 
 
653
 
    def show_status(branch, show_all=False):
654
 
        """Display single-line status for non-ignored working files.
655
 
 
656
 
        The list is show sorted in order by file name.
657
 
 
658
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
659
 
        >>> b.show_status()
660
 
        ?       foo
661
 
        >>> b.add('foo')
662
 
        >>> b.show_status()
663
 
        A       foo
664
 
        >>> b.commit("add foo")
665
 
        >>> b.show_status()
666
 
        >>> os.unlink(b._rel('foo'))
667
 
        >>> b.show_status()
668
 
        D       foo
669
 
        
670
 
 
671
 
        :todo: Get state for single files.
672
 
 
673
 
        :todo: Perhaps show a slash at the end of directory names.        
674
 
 
675
 
        """
676
 
 
677
 
        # We have to build everything into a list first so that it can
678
 
        # sorted by name, incorporating all the different sources.
679
 
 
680
 
        # FIXME: Rather than getting things in random order and then sorting,
681
 
        # just step through in order.
682
 
 
683
 
        # Interesting case: the old ID for a file has been removed,
684
 
        # but a new file has been created under that name.
685
 
 
686
 
        old = branch.basis_tree()
687
 
        old_inv = old.inventory
688
 
        new = branch.working_tree()
689
 
        new_inv = new.inventory
690
 
 
691
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
692
 
            if fs == 'R':
693
 
                show_status(fs, kind,
694
 
                            oldname + ' => ' + newname)
695
 
            elif fs == 'A' or fs == 'M':
696
 
                show_status(fs, kind, newname)
697
 
            elif fs == 'D':
698
 
                show_status(fs, kind, oldname)
699
 
            elif fs == '.':
700
 
                if show_all:
701
 
                    show_status(fs, kind, newname)
702
 
            elif fs == 'I':
703
 
                if show_all:
704
 
                    show_status(fs, kind, newname)
705
 
            elif fs == '?':
706
 
                show_status(fs, kind, newname)
707
 
            else:
708
 
                bailout("wierd file state %r" % ((fs, fid),))
709
 
                
710
 
 
711
 
 
712
 
class ScratchBranch(Branch):
713
 
    """Special test class: a branch that cleans up after itself.
714
 
 
715
 
    >>> b = ScratchBranch()
716
 
    >>> isdir(b.base)
717
 
    True
718
 
    >>> bd = b.base
719
 
    >>> del b
720
 
    >>> isdir(bd)
721
 
    False
722
 
    """
723
 
    def __init__(self, files = []):
724
 
        """Make a test branch.
725
 
 
726
 
        This creates a temporary directory and runs init-tree in it.
727
 
 
728
 
        If any files are listed, they are created in the working copy.
729
 
        """
730
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
731
 
        for f in files:
732
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
733
 
 
734
 
 
735
 
    def __del__(self):
736
 
        """Destroy the test branch, removing the scratch directory."""
737
 
        shutil.rmtree(self.base)
738
 
 
739
 
    
740
 
 
741
 
######################################################################
742
 
# predicates
743
 
 
744
 
 
745
 
def is_control_file(filename):
746
 
    ## FIXME: better check
747
 
    filename = os.path.normpath(filename)
748
 
    while filename != '':
749
 
        head, tail = os.path.split(filename)
750
 
        ## mutter('check %r for control file' % ((head, tail), ))
751
 
        if tail == bzrlib.BZRDIR:
752
 
            return True
753
 
        filename = head
754
 
    return False
755
 
 
756
 
 
757
 
 
758
 
def _gen_revision_id(when):
759
 
    """Return new revision-id."""
760
 
    s = '%s-%s-' % (user_email(), compact_date(when))
761
 
    s += hexlify(rand_bytes(8))
762
 
    return s
763
 
 
764
 
 
765
 
def _gen_file_id(name):
766
 
    """Return new file id.
767
 
 
768
 
    This should probably generate proper UUIDs, but for the moment we
769
 
    cope with just randomness because running uuidgen every time is
770
 
    slow."""
771
 
    assert '/' not in name
772
 
    while name[0] == '.':
773
 
        name = name[1:]
774
 
    s = hexlify(rand_bytes(8))
775
 
    return '-'.join((name, compact_date(time.time()), s))
776
 
 
777