/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

  • Committer: mbp at sourcefrog
  • Date: 2005-03-22 07:28:45 UTC
  • Revision ID: mbp@sourcefrog.net-20050322072845-5859efd4dab29169
use abspath() for the function that makes an absolute
path to something in a branch or tree

Show diffs side-by-side

added added

removed removed

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