/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

Fix formatting, remove catch-all for exceptions when opening local repositories.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 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
 
 
18
 
from sets import Set
19
 
 
20
 
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
 
import traceback, socket, fnmatch, difflib, time
22
 
from binascii import hexlify
23
 
 
24
 
import bzrlib
25
 
from inventory import Inventory
26
 
from trace import mutter, note
27
 
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
28
 
from inventory import InventoryEntry, Inventory
29
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
 
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
 
     joinpath, sha_string, file_kind, local_time_offset, appendpath
32
 
from store import ImmutableStore
33
 
from revision import Revision
34
 
from errors import bailout, BzrError
35
 
from textui import show_status
36
 
from diff import diff_trees
37
 
 
38
 
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
 
## TODO: Maybe include checks for common corruption of newlines, etc?
40
 
 
41
 
 
42
 
 
43
 
def find_branch_root(f=None):
44
 
    """Find the branch root enclosing f, or pwd.
45
 
 
46
 
    It is not necessary that f exists.
47
 
 
48
 
    Basically we keep looking up until we find the control directory or
49
 
    run into the root."""
50
 
    if f == None:
51
 
        f = os.getcwd()
52
 
    elif hasattr(os.path, 'realpath'):
53
 
        f = os.path.realpath(f)
54
 
    else:
55
 
        f = os.path.abspath(f)
56
 
 
57
 
    orig_f = f
58
 
 
59
 
    while True:
60
 
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
61
 
            return f
62
 
        head, tail = os.path.split(f)
63
 
        if head == f:
64
 
            # reached the root, whatever that may be
65
 
            raise BzrError('%r is not in a branch' % orig_f)
66
 
        f = head
67
 
    
68
 
 
69
 
 
70
 
######################################################################
71
 
# branch objects
72
 
 
73
 
class Branch:
74
 
    """Branch holding a history of revisions.
75
 
 
76
 
    base
77
 
        Base directory of the branch.
78
 
    """
79
 
    def __init__(self, base, init=False, find_root=True):
80
 
        """Create new branch object at a particular location.
81
 
 
82
 
        base -- Base directory for the branch.
83
 
        
84
 
        init -- If True, create new control files in a previously
85
 
             unversioned directory.  If False, the branch must already
86
 
             be versioned.
87
 
 
88
 
        find_root -- If true and init is false, find the root of the
89
 
             existing branch containing base.
90
 
 
91
 
        In the test suite, creation of new trees is tested using the
92
 
        `ScratchBranch` class.
93
 
        """
94
 
        if init:
95
 
            self.base = os.path.realpath(base)
96
 
            self._make_control()
97
 
        elif find_root:
98
 
            self.base = find_branch_root(base)
99
 
        else:
100
 
            self.base = os.path.realpath(base)
101
 
            if not isdir(self.controlfilename('.')):
102
 
                bailout("not a bzr branch: %s" % quotefn(base),
103
 
                        ['use "bzr init" to initialize a new working tree',
104
 
                         'current bzr can only operate from top-of-tree'])
105
 
        self._check_format()
106
 
 
107
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
108
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
109
 
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
110
 
 
111
 
 
112
 
    def __str__(self):
113
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
114
 
 
115
 
 
116
 
    __repr__ = __str__
117
 
 
118
 
 
119
 
    def abspath(self, name):
120
 
        """Return absolute filename for something in the branch"""
121
 
        return os.path.join(self.base, name)
122
 
 
123
 
 
124
 
    def relpath(self, path):
125
 
        """Return path relative to this branch of something inside it.
126
 
 
127
 
        Raises an error if path is not in this branch."""
128
 
        rp = os.path.realpath(path)
129
 
        # FIXME: windows
130
 
        if not rp.startswith(self.base):
131
 
            bailout("path %r is not within branch %r" % (rp, self.base))
132
 
        rp = rp[len(self.base):]
133
 
        rp = rp.lstrip(os.sep)
134
 
        return rp
135
 
 
136
 
 
137
 
    def controlfilename(self, file_or_path):
138
 
        """Return location relative to branch."""
139
 
        if isinstance(file_or_path, types.StringTypes):
140
 
            file_or_path = [file_or_path]
141
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
142
 
 
143
 
 
144
 
    def controlfile(self, file_or_path, mode='r'):
145
 
        """Open a control file for this branch.
146
 
 
147
 
        There are two classes of file in the control directory: text
148
 
        and binary.  binary files are untranslated byte streams.  Text
149
 
        control files are stored with Unix newlines and in UTF-8, even
150
 
        if the platform or locale defaults are different.
151
 
        """
152
 
 
153
 
        fn = self.controlfilename(file_or_path)
154
 
 
155
 
        if mode == 'rb' or mode == 'wb':
156
 
            return file(fn, mode)
157
 
        elif mode == 'r' or mode == 'w':
158
 
            # open in binary mode anyhow so there's no newline translation;
159
 
            # codecs uses line buffering by default; don't want that.
160
 
            import codecs
161
 
            return codecs.open(fn, mode + 'b', 'utf-8',
162
 
                               buffering=60000)
163
 
        else:
164
 
            raise BzrError("invalid controlfile mode %r" % mode)
165
 
 
166
 
 
167
 
 
168
 
    def _make_control(self):
169
 
        os.mkdir(self.controlfilename([]))
170
 
        self.controlfile('README', 'w').write(
171
 
            "This is a Bazaar-NG control directory.\n"
172
 
            "Do not change any files in this directory.")
173
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
174
 
        for d in ('text-store', 'inventory-store', 'revision-store'):
175
 
            os.mkdir(self.controlfilename(d))
176
 
        for f in ('revision-history', 'merged-patches',
177
 
                  'pending-merged-patches', 'branch-name'):
178
 
            self.controlfile(f, 'w').write('')
179
 
        mutter('created control directory in ' + self.base)
180
 
        Inventory().write_xml(self.controlfile('inventory','w'))
181
 
 
182
 
 
183
 
    def _check_format(self):
184
 
        """Check this branch format is supported.
185
 
 
186
 
        The current tool only supports the current unstable format.
187
 
 
188
 
        In the future, we might need different in-memory Branch
189
 
        classes to support downlevel branches.  But not yet.
190
 
        """
191
 
        # This ignores newlines so that we can open branches created
192
 
        # on Windows from Linux and so on.  I think it might be better
193
 
        # to always make all internal files in unix format.
194
 
        fmt = self.controlfile('branch-format', 'r').read()
195
 
        fmt.replace('\r\n', '')
196
 
        if fmt != BZR_BRANCH_FORMAT:
197
 
            bailout('sorry, branch format %r not supported' % fmt,
198
 
                    ['use a different bzr version',
199
 
                     'or remove the .bzr directory and "bzr init" again'])
200
 
 
201
 
 
202
 
    def read_working_inventory(self):
203
 
        """Read the working inventory."""
204
 
        before = time.time()
205
 
        # ElementTree does its own conversion from UTF-8, so open in
206
 
        # binary.
207
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
208
 
        mutter("loaded inventory of %d items in %f"
209
 
               % (len(inv), time.time() - before))
210
 
        return inv
211
 
 
212
 
 
213
 
    def _write_inventory(self, inv):
214
 
        """Update the working inventory.
215
 
 
216
 
        That is to say, the inventory describing changes underway, that
217
 
        will be committed to the next revision.
218
 
        """
219
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
220
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
221
 
        tmpfname = self.controlfilename('inventory.tmp')
222
 
        tmpf = file(tmpfname, 'wb')
223
 
        inv.write_xml(tmpf)
224
 
        tmpf.close()
225
 
        inv_fname = self.controlfilename('inventory')
226
 
        if sys.platform == 'win32':
227
 
            os.remove(inv_fname)
228
 
        os.rename(tmpfname, inv_fname)
229
 
        mutter('wrote working inventory')
230
 
 
231
 
 
232
 
    inventory = property(read_working_inventory, _write_inventory, None,
233
 
                         """Inventory for the working copy.""")
234
 
 
235
 
 
236
 
    def add(self, files, verbose=False):
237
 
        """Make files versioned.
238
 
 
239
 
        Note that the command line normally calls smart_add instead.
240
 
 
241
 
        This puts the files in the Added state, so that they will be
242
 
        recorded by the next commit.
243
 
 
244
 
        TODO: Perhaps have an option to add the ids even if the files do
245
 
               not (yet) exist.
246
 
 
247
 
        TODO: Perhaps return the ids of the files?  But then again it
248
 
               is easy to retrieve them if they're needed.
249
 
 
250
 
        TODO: Option to specify file id.
251
 
 
252
 
        TODO: Adding a directory should optionally recurse down and
253
 
               add all non-ignored children.  Perhaps do that in a
254
 
               higher-level method.
255
 
 
256
 
        >>> b = ScratchBranch(files=['foo'])
257
 
        >>> 'foo' in b.unknowns()
258
 
        True
259
 
        >>> b.show_status()
260
 
        ?       foo
261
 
        >>> b.add('foo')
262
 
        >>> 'foo' in b.unknowns()
263
 
        False
264
 
        >>> bool(b.inventory.path2id('foo'))
265
 
        True
266
 
        >>> b.show_status()
267
 
        A       foo
268
 
 
269
 
        >>> b.add('foo')
270
 
        Traceback (most recent call last):
271
 
        ...
272
 
        BzrError: ('foo is already versioned', [])
273
 
 
274
 
        >>> b.add(['nothere'])
275
 
        Traceback (most recent call last):
276
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
277
 
        """
278
 
 
279
 
        # TODO: Re-adding a file that is removed in the working copy
280
 
        # should probably put it back with the previous ID.
281
 
        if isinstance(files, types.StringTypes):
282
 
            files = [files]
283
 
        
284
 
        inv = self.read_working_inventory()
285
 
        for f in files:
286
 
            if is_control_file(f):
287
 
                bailout("cannot add control file %s" % quotefn(f))
288
 
 
289
 
            fp = splitpath(f)
290
 
 
291
 
            if len(fp) == 0:
292
 
                bailout("cannot add top-level %r" % f)
293
 
                
294
 
            fullpath = os.path.normpath(self.abspath(f))
295
 
 
296
 
            try:
297
 
                kind = file_kind(fullpath)
298
 
            except OSError:
299
 
                # maybe something better?
300
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
301
 
            
302
 
            if kind != 'file' and kind != 'directory':
303
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
304
 
 
305
 
            file_id = gen_file_id(f)
306
 
            inv.add_path(f, kind=kind, file_id=file_id)
307
 
 
308
 
            if verbose:
309
 
                show_status('A', kind, quotefn(f))
310
 
                
311
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
312
 
            
313
 
        self._write_inventory(inv)
314
 
 
315
 
 
316
 
    def print_file(self, file, revno):
317
 
        """Print `file` to stdout."""
318
 
        tree = self.revision_tree(self.lookup_revision(revno))
319
 
        # use inventory as it was in that revision
320
 
        file_id = tree.inventory.path2id(file)
321
 
        if not file_id:
322
 
            bailout("%r is not present in revision %d" % (file, revno))
323
 
        tree.print_file(file_id)
324
 
        
325
 
 
326
 
    def remove(self, files, verbose=False):
327
 
        """Mark nominated files for removal from the inventory.
328
 
 
329
 
        This does not remove their text.  This does not run on 
330
 
 
331
 
        TODO: Refuse to remove modified files unless --force is given?
332
 
 
333
 
        >>> b = ScratchBranch(files=['foo'])
334
 
        >>> b.add('foo')
335
 
        >>> b.inventory.has_filename('foo')
336
 
        True
337
 
        >>> b.remove('foo')
338
 
        >>> b.working_tree().has_filename('foo')
339
 
        True
340
 
        >>> b.inventory.has_filename('foo')
341
 
        False
342
 
        
343
 
        >>> b = ScratchBranch(files=['foo'])
344
 
        >>> b.add('foo')
345
 
        >>> b.commit('one')
346
 
        >>> b.remove('foo')
347
 
        >>> b.commit('two')
348
 
        >>> b.inventory.has_filename('foo') 
349
 
        False
350
 
        >>> b.basis_tree().has_filename('foo') 
351
 
        False
352
 
        >>> b.working_tree().has_filename('foo') 
353
 
        True
354
 
 
355
 
        TODO: Do something useful with directories.
356
 
 
357
 
        TODO: Should this remove the text or not?  Tough call; not
358
 
        removing may be useful and the user can just use use rm, and
359
 
        is the opposite of add.  Removing it is consistent with most
360
 
        other tools.  Maybe an option.
361
 
        """
362
 
        ## TODO: Normalize names
363
 
        ## TODO: Remove nested loops; better scalability
364
 
 
365
 
        if isinstance(files, types.StringTypes):
366
 
            files = [files]
367
 
        
368
 
        tree = self.working_tree()
369
 
        inv = tree.inventory
370
 
 
371
 
        # do this before any modifications
372
 
        for f in files:
373
 
            fid = inv.path2id(f)
374
 
            if not fid:
375
 
                bailout("cannot remove unversioned file %s" % quotefn(f))
376
 
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
377
 
            if verbose:
378
 
                # having remove it, it must be either ignored or unknown
379
 
                if tree.is_ignored(f):
380
 
                    new_status = 'I'
381
 
                else:
382
 
                    new_status = '?'
383
 
                show_status(new_status, inv[fid].kind, quotefn(f))
384
 
            del inv[fid]
385
 
 
386
 
        self._write_inventory(inv)
387
 
 
388
 
 
389
 
    def unknowns(self):
390
 
        """Return all unknown files.
391
 
 
392
 
        These are files in the working directory that are not versioned or
393
 
        control files or ignored.
394
 
        
395
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
396
 
        >>> list(b.unknowns())
397
 
        ['foo']
398
 
        >>> b.add('foo')
399
 
        >>> list(b.unknowns())
400
 
        []
401
 
        >>> b.remove('foo')
402
 
        >>> list(b.unknowns())
403
 
        ['foo']
404
 
        """
405
 
        return self.working_tree().unknowns()
406
 
 
407
 
 
408
 
    def commit(self, message, timestamp=None, timezone=None,
409
 
               committer=None,
410
 
               verbose=False):
411
 
        """Commit working copy as a new revision.
412
 
        
413
 
        The basic approach is to add all the file texts into the
414
 
        store, then the inventory, then make a new revision pointing
415
 
        to that inventory and store that.
416
 
        
417
 
        This is not quite safe if the working copy changes during the
418
 
        commit; for the moment that is simply not allowed.  A better
419
 
        approach is to make a temporary copy of the files before
420
 
        computing their hashes, and then add those hashes in turn to
421
 
        the inventory.  This should mean at least that there are no
422
 
        broken hash pointers.  There is no way we can get a snapshot
423
 
        of the whole directory at an instant.  This would also have to
424
 
        be robust against files disappearing, moving, etc.  So the
425
 
        whole thing is a bit hard.
426
 
 
427
 
        timestamp -- if not None, seconds-since-epoch for a
428
 
             postdated/predated commit.
429
 
        """
430
 
 
431
 
        ## TODO: Show branch names
432
 
 
433
 
        # TODO: Don't commit if there are no changes, unless forced?
434
 
 
435
 
        # First walk over the working inventory; and both update that
436
 
        # and also build a new revision inventory.  The revision
437
 
        # inventory needs to hold the text-id, sha1 and size of the
438
 
        # actual file versions committed in the revision.  (These are
439
 
        # not present in the working inventory.)  We also need to
440
 
        # detect missing/deleted files, and remove them from the
441
 
        # working inventory.
442
 
 
443
 
        work_inv = self.read_working_inventory()
444
 
        inv = Inventory()
445
 
        basis = self.basis_tree()
446
 
        basis_inv = basis.inventory
447
 
        missing_ids = []
448
 
        for path, entry in work_inv.iter_entries():
449
 
            ## TODO: Cope with files that have gone missing.
450
 
 
451
 
            ## TODO: Check that the file kind has not changed from the previous
452
 
            ## revision of this file (if any).
453
 
 
454
 
            entry = entry.copy()
455
 
 
456
 
            p = self.abspath(path)
457
 
            file_id = entry.file_id
458
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
459
 
 
460
 
            if not os.path.exists(p):
461
 
                mutter("    file is missing, removing from inventory")
462
 
                if verbose:
463
 
                    show_status('D', entry.kind, quotefn(path))
464
 
                missing_ids.append(file_id)
465
 
                continue
466
 
 
467
 
            # TODO: Handle files that have been deleted
468
 
 
469
 
            # TODO: Maybe a special case for empty files?  Seems a
470
 
            # waste to store them many times.
471
 
 
472
 
            inv.add(entry)
473
 
 
474
 
            if basis_inv.has_id(file_id):
475
 
                old_kind = basis_inv[file_id].kind
476
 
                if old_kind != entry.kind:
477
 
                    bailout("entry %r changed kind from %r to %r"
478
 
                            % (file_id, old_kind, entry.kind))
479
 
 
480
 
            if entry.kind == 'directory':
481
 
                if not isdir(p):
482
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
483
 
            elif entry.kind == 'file':
484
 
                if not isfile(p):
485
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
486
 
 
487
 
                content = file(p, 'rb').read()
488
 
 
489
 
                entry.text_sha1 = sha_string(content)
490
 
                entry.text_size = len(content)
491
 
 
492
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
493
 
                if (old_ie
494
 
                    and (old_ie.text_size == entry.text_size)
495
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
496
 
                    ## assert content == basis.get_file(file_id).read()
497
 
                    entry.text_id = basis_inv[file_id].text_id
498
 
                    mutter('    unchanged from previous text_id {%s}' %
499
 
                           entry.text_id)
500
 
                    
501
 
                else:
502
 
                    entry.text_id = gen_file_id(entry.name)
503
 
                    self.text_store.add(content, entry.text_id)
504
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
505
 
                    if verbose:
506
 
                        if not old_ie:
507
 
                            state = 'A'
508
 
                        elif (old_ie.name == entry.name
509
 
                              and old_ie.parent_id == entry.parent_id):
510
 
                            state = 'M'
511
 
                        else:
512
 
                            state = 'R'
513
 
 
514
 
                        show_status(state, entry.kind, quotefn(path))
515
 
 
516
 
        for file_id in missing_ids:
517
 
            # have to do this later so we don't mess up the iterator.
518
 
            # since parents may be removed before their children we
519
 
            # have to test.
520
 
 
521
 
            # FIXME: There's probably a better way to do this; perhaps
522
 
            # the workingtree should know how to filter itself.
523
 
            if work_inv.has_id(file_id):
524
 
                del work_inv[file_id]
525
 
 
526
 
 
527
 
        inv_id = rev_id = _gen_revision_id(time.time())
528
 
        
529
 
        inv_tmp = tempfile.TemporaryFile()
530
 
        inv.write_xml(inv_tmp)
531
 
        inv_tmp.seek(0)
532
 
        self.inventory_store.add(inv_tmp, inv_id)
533
 
        mutter('new inventory_id is {%s}' % inv_id)
534
 
 
535
 
        self._write_inventory(work_inv)
536
 
 
537
 
        if timestamp == None:
538
 
            timestamp = time.time()
539
 
 
540
 
        if committer == None:
541
 
            committer = username()
542
 
 
543
 
        if timezone == None:
544
 
            timezone = local_time_offset()
545
 
 
546
 
        mutter("building commit log message")
547
 
        rev = Revision(timestamp=timestamp,
548
 
                       timezone=timezone,
549
 
                       committer=committer,
550
 
                       precursor = self.last_patch(),
551
 
                       message = message,
552
 
                       inventory_id=inv_id,
553
 
                       revision_id=rev_id)
554
 
 
555
 
        rev_tmp = tempfile.TemporaryFile()
556
 
        rev.write_xml(rev_tmp)
557
 
        rev_tmp.seek(0)
558
 
        self.revision_store.add(rev_tmp, rev_id)
559
 
        mutter("new revision_id is {%s}" % rev_id)
560
 
        
561
 
        ## XXX: Everything up to here can simply be orphaned if we abort
562
 
        ## the commit; it will leave junk files behind but that doesn't
563
 
        ## matter.
564
 
 
565
 
        ## TODO: Read back the just-generated changeset, and make sure it
566
 
        ## applies and recreates the right state.
567
 
 
568
 
        ## TODO: Also calculate and store the inventory SHA1
569
 
        mutter("committing patch r%d" % (self.revno() + 1))
570
 
 
571
 
 
572
 
        self.append_revision(rev_id)
573
 
        
574
 
        if verbose:
575
 
            note("commited r%d" % self.revno())
576
 
 
577
 
 
578
 
    def append_revision(self, revision_id):
579
 
        mutter("add {%s} to revision-history" % revision_id)
580
 
        rev_history = self.revision_history()
581
 
 
582
 
        tmprhname = self.controlfilename('revision-history.tmp')
583
 
        rhname = self.controlfilename('revision-history')
584
 
        
585
 
        f = file(tmprhname, 'wt')
586
 
        rev_history.append(revision_id)
587
 
        f.write('\n'.join(rev_history))
588
 
        f.write('\n')
589
 
        f.close()
590
 
 
591
 
        if sys.platform == 'win32':
592
 
            os.remove(rhname)
593
 
        os.rename(tmprhname, rhname)
594
 
        
595
 
 
596
 
 
597
 
    def get_revision(self, revision_id):
598
 
        """Return the Revision object for a named revision"""
599
 
        r = Revision.read_xml(self.revision_store[revision_id])
600
 
        assert r.revision_id == revision_id
601
 
        return r
602
 
 
603
 
 
604
 
    def get_inventory(self, inventory_id):
605
 
        """Get Inventory object by hash.
606
 
 
607
 
        TODO: Perhaps for this and similar methods, take a revision
608
 
               parameter which can be either an integer revno or a
609
 
               string hash."""
610
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
611
 
        return i
612
 
 
613
 
 
614
 
    def get_revision_inventory(self, revision_id):
615
 
        """Return inventory of a past revision."""
616
 
        if revision_id == None:
617
 
            return Inventory()
618
 
        else:
619
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
620
 
 
621
 
 
622
 
    def revision_history(self):
623
 
        """Return sequence of revision hashes on to this branch.
624
 
 
625
 
        >>> ScratchBranch().revision_history()
626
 
        []
627
 
        """
628
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
629
 
 
630
 
 
631
 
    def revno(self):
632
 
        """Return current revision number for this branch.
633
 
 
634
 
        That is equivalent to the number of revisions committed to
635
 
        this branch.
636
 
 
637
 
        >>> b = ScratchBranch()
638
 
        >>> b.revno()
639
 
        0
640
 
        >>> b.commit('no foo')
641
 
        >>> b.revno()
642
 
        1
643
 
        """
644
 
        return len(self.revision_history())
645
 
 
646
 
 
647
 
    def last_patch(self):
648
 
        """Return last patch hash, or None if no history.
649
 
 
650
 
        >>> ScratchBranch().last_patch() == None
651
 
        True
652
 
        """
653
 
        ph = self.revision_history()
654
 
        if ph:
655
 
            return ph[-1]
656
 
        else:
657
 
            return None
658
 
        
659
 
 
660
 
    def lookup_revision(self, revno):
661
 
        """Return revision hash for revision number."""
662
 
        if revno == 0:
663
 
            return None
664
 
 
665
 
        try:
666
 
            # list is 0-based; revisions are 1-based
667
 
            return self.revision_history()[revno-1]
668
 
        except IndexError:
669
 
            raise BzrError("no such revision %s" % revno)
670
 
 
671
 
 
672
 
    def revision_tree(self, revision_id):
673
 
        """Return Tree for a revision on this branch.
674
 
 
675
 
        `revision_id` may be None for the null revision, in which case
676
 
        an `EmptyTree` is returned."""
677
 
 
678
 
        if revision_id == None:
679
 
            return EmptyTree()
680
 
        else:
681
 
            inv = self.get_revision_inventory(revision_id)
682
 
            return RevisionTree(self.text_store, inv)
683
 
 
684
 
 
685
 
    def working_tree(self):
686
 
        """Return a `Tree` for the working copy."""
687
 
        return WorkingTree(self.base, self.read_working_inventory())
688
 
 
689
 
 
690
 
    def basis_tree(self):
691
 
        """Return `Tree` object for last revision.
692
 
 
693
 
        If there are no revisions yet, return an `EmptyTree`.
694
 
 
695
 
        >>> b = ScratchBranch(files=['foo'])
696
 
        >>> b.basis_tree().has_filename('foo')
697
 
        False
698
 
        >>> b.working_tree().has_filename('foo')
699
 
        True
700
 
        >>> b.add('foo')
701
 
        >>> b.commit('add foo')
702
 
        >>> b.basis_tree().has_filename('foo')
703
 
        True
704
 
        """
705
 
        r = self.last_patch()
706
 
        if r == None:
707
 
            return EmptyTree()
708
 
        else:
709
 
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
710
 
 
711
 
 
712
 
 
713
 
    def write_log(self, show_timezone='original', verbose=False):
714
 
        """Write out human-readable log of commits to this branch
715
 
 
716
 
        utc -- If true, show dates in universal time, not local time."""
717
 
        ## TODO: Option to choose either original, utc or local timezone
718
 
        revno = 1
719
 
        precursor = None
720
 
        for p in self.revision_history():
721
 
            print '-' * 40
722
 
            print 'revno:', revno
723
 
            ## TODO: Show hash if --id is given.
724
 
            ##print 'revision-hash:', p
725
 
            rev = self.get_revision(p)
726
 
            print 'committer:', rev.committer
727
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
728
 
                                                 show_timezone))
729
 
 
730
 
            ## opportunistic consistency check, same as check_patch_chaining
731
 
            if rev.precursor != precursor:
732
 
                bailout("mismatched precursor!")
733
 
 
734
 
            print 'message:'
735
 
            if not rev.message:
736
 
                print '  (no message)'
737
 
            else:
738
 
                for l in rev.message.split('\n'):
739
 
                    print '  ' + l
740
 
 
741
 
            if verbose == True and precursor != None:
742
 
                print 'changed files:'
743
 
                tree = self.revision_tree(p)
744
 
                prevtree = self.revision_tree(precursor)
745
 
                
746
 
                for file_state, fid, old_name, new_name, kind in \
747
 
                                        diff_trees(prevtree, tree, ):
748
 
                    if file_state == 'A' or file_state == 'M':
749
 
                        show_status(file_state, kind, new_name)
750
 
                    elif file_state == 'D':
751
 
                        show_status(file_state, kind, old_name)
752
 
                    elif file_state == 'R':
753
 
                        show_status(file_state, kind,
754
 
                            old_name + ' => ' + new_name)
755
 
                
756
 
            revno += 1
757
 
            precursor = p
758
 
 
759
 
 
760
 
    def rename_one(self, from_rel, to_rel):
761
 
        """Rename one file.
762
 
 
763
 
        This can change the directory or the filename or both.
764
 
         """
765
 
        tree = self.working_tree()
766
 
        inv = tree.inventory
767
 
        if not tree.has_filename(from_rel):
768
 
            bailout("can't rename: old working file %r does not exist" % from_rel)
769
 
        if tree.has_filename(to_rel):
770
 
            bailout("can't rename: new working file %r already exists" % to_rel)
771
 
            
772
 
        file_id = inv.path2id(from_rel)
773
 
        if file_id == None:
774
 
            bailout("can't rename: old name %r is not versioned" % from_rel)
775
 
 
776
 
        if inv.path2id(to_rel):
777
 
            bailout("can't rename: new name %r is already versioned" % to_rel)
778
 
 
779
 
        to_dir, to_tail = os.path.split(to_rel)
780
 
        to_dir_id = inv.path2id(to_dir)
781
 
        if to_dir_id == None and to_dir != '':
782
 
            bailout("can't determine destination directory id for %r" % to_dir)
783
 
 
784
 
        mutter("rename_one:")
785
 
        mutter("  file_id    {%s}" % file_id)
786
 
        mutter("  from_rel   %r" % from_rel)
787
 
        mutter("  to_rel     %r" % to_rel)
788
 
        mutter("  to_dir     %r" % to_dir)
789
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
790
 
            
791
 
        inv.rename(file_id, to_dir_id, to_tail)
792
 
 
793
 
        print "%s => %s" % (from_rel, to_rel)
794
 
        
795
 
        from_abs = self.abspath(from_rel)
796
 
        to_abs = self.abspath(to_rel)
797
 
        try:
798
 
            os.rename(from_abs, to_abs)
799
 
        except OSError, e:
800
 
            bailout("failed to rename %r to %r: %s"
801
 
                    % (from_abs, to_abs, e[1]),
802
 
                    ["rename rolled back"])
803
 
 
804
 
        self._write_inventory(inv)
805
 
            
806
 
 
807
 
 
808
 
    def move(self, from_paths, to_name):
809
 
        """Rename files.
810
 
 
811
 
        to_name must exist as a versioned directory.
812
 
 
813
 
        If to_name exists and is a directory, the files are moved into
814
 
        it, keeping their old names.  If it is a directory, 
815
 
 
816
 
        Note that to_name is only the last component of the new name;
817
 
        this doesn't change the directory.
818
 
        """
819
 
        ## TODO: Option to move IDs only
820
 
        assert not isinstance(from_paths, basestring)
821
 
        tree = self.working_tree()
822
 
        inv = tree.inventory
823
 
        to_abs = self.abspath(to_name)
824
 
        if not isdir(to_abs):
825
 
            bailout("destination %r is not a directory" % to_abs)
826
 
        if not tree.has_filename(to_name):
827
 
            bailout("destination %r not in working directory" % to_abs)
828
 
        to_dir_id = inv.path2id(to_name)
829
 
        if to_dir_id == None and to_name != '':
830
 
            bailout("destination %r is not a versioned directory" % to_name)
831
 
        to_dir_ie = inv[to_dir_id]
832
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
833
 
            bailout("destination %r is not a directory" % to_abs)
834
 
 
835
 
        to_idpath = Set(inv.get_idpath(to_dir_id))
836
 
 
837
 
        for f in from_paths:
838
 
            if not tree.has_filename(f):
839
 
                bailout("%r does not exist in working tree" % f)
840
 
            f_id = inv.path2id(f)
841
 
            if f_id == None:
842
 
                bailout("%r is not versioned" % f)
843
 
            name_tail = splitpath(f)[-1]
844
 
            dest_path = appendpath(to_name, name_tail)
845
 
            if tree.has_filename(dest_path):
846
 
                bailout("destination %r already exists" % dest_path)
847
 
            if f_id in to_idpath:
848
 
                bailout("can't move %r to a subdirectory of itself" % f)
849
 
 
850
 
        # OK, so there's a race here, it's possible that someone will
851
 
        # create a file in this interval and then the rename might be
852
 
        # left half-done.  But we should have caught most problems.
853
 
 
854
 
        for f in from_paths:
855
 
            name_tail = splitpath(f)[-1]
856
 
            dest_path = appendpath(to_name, name_tail)
857
 
            print "%s => %s" % (f, dest_path)
858
 
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
859
 
            try:
860
 
                os.rename(self.abspath(f), self.abspath(dest_path))
861
 
            except OSError, e:
862
 
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
863
 
                        ["rename rolled back"])
864
 
 
865
 
        self._write_inventory(inv)
866
 
 
867
 
 
868
 
 
869
 
    def show_status(self, show_all=False):
870
 
        """Display single-line status for non-ignored working files.
871
 
 
872
 
        The list is show sorted in order by file name.
873
 
 
874
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
875
 
        >>> b.show_status()
876
 
        ?       foo
877
 
        >>> b.add('foo')
878
 
        >>> b.show_status()
879
 
        A       foo
880
 
        >>> b.commit("add foo")
881
 
        >>> b.show_status()
882
 
        >>> os.unlink(b.abspath('foo'))
883
 
        >>> b.show_status()
884
 
        D       foo
885
 
        
886
 
        TODO: Get state for single files.
887
 
        """
888
 
 
889
 
        # We have to build everything into a list first so that it can
890
 
        # sorted by name, incorporating all the different sources.
891
 
 
892
 
        # FIXME: Rather than getting things in random order and then sorting,
893
 
        # just step through in order.
894
 
 
895
 
        # Interesting case: the old ID for a file has been removed,
896
 
        # but a new file has been created under that name.
897
 
 
898
 
        old = self.basis_tree()
899
 
        new = self.working_tree()
900
 
 
901
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
902
 
            if fs == 'R':
903
 
                show_status(fs, kind,
904
 
                            oldname + ' => ' + newname)
905
 
            elif fs == 'A' or fs == 'M':
906
 
                show_status(fs, kind, newname)
907
 
            elif fs == 'D':
908
 
                show_status(fs, kind, oldname)
909
 
            elif fs == '.':
910
 
                if show_all:
911
 
                    show_status(fs, kind, newname)
912
 
            elif fs == 'I':
913
 
                if show_all:
914
 
                    show_status(fs, kind, newname)
915
 
            elif fs == '?':
916
 
                show_status(fs, kind, newname)
917
 
            else:
918
 
                bailout("weird file state %r" % ((fs, fid),))
919
 
                
920
 
 
921
 
 
922
 
class ScratchBranch(Branch):
923
 
    """Special test class: a branch that cleans up after itself.
924
 
 
925
 
    >>> b = ScratchBranch()
926
 
    >>> isdir(b.base)
927
 
    True
928
 
    >>> bd = b.base
929
 
    >>> del b
930
 
    >>> isdir(bd)
931
 
    False
932
 
    """
933
 
    def __init__(self, files=[], dirs=[]):
934
 
        """Make a test branch.
935
 
 
936
 
        This creates a temporary directory and runs init-tree in it.
937
 
 
938
 
        If any files are listed, they are created in the working copy.
939
 
        """
940
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
941
 
        for d in dirs:
942
 
            os.mkdir(self.abspath(d))
943
 
            
944
 
        for f in files:
945
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
946
 
 
947
 
 
948
 
    def __del__(self):
949
 
        """Destroy the test branch, removing the scratch directory."""
950
 
        try:
951
 
            shutil.rmtree(self.base)
952
 
        except OSError:
953
 
            # Work around for shutil.rmtree failing on Windows when
954
 
            # readonly files are encountered
955
 
            for root, dirs, files in os.walk(self.base, topdown=False):
956
 
                for name in files:
957
 
                    os.chmod(os.path.join(root, name), 0700)
958
 
            shutil.rmtree(self.base)
959
 
 
960
 
    
961
 
 
962
 
######################################################################
963
 
# predicates
964
 
 
965
 
 
966
 
def is_control_file(filename):
967
 
    ## FIXME: better check
968
 
    filename = os.path.normpath(filename)
969
 
    while filename != '':
970
 
        head, tail = os.path.split(filename)
971
 
        ## mutter('check %r for control file' % ((head, tail), ))
972
 
        if tail == bzrlib.BZRDIR:
973
 
            return True
974
 
        if filename == head:
975
 
            break
976
 
        filename = head
977
 
    return False
978
 
 
979
 
 
980
 
 
981
 
def _gen_revision_id(when):
982
 
    """Return new revision-id."""
983
 
    s = '%s-%s-' % (user_email(), compact_date(when))
984
 
    s += hexlify(rand_bytes(8))
985
 
    return s
986
 
 
987
 
 
988
 
def gen_file_id(name):
989
 
    """Return new file id.
990
 
 
991
 
    This should probably generate proper UUIDs, but for the moment we
992
 
    cope with just randomness because running uuidgen every time is
993
 
    slow."""
994
 
    idx = name.rfind('/')
995
 
    if idx != -1:
996
 
        name = name[idx+1 : ]
997
 
    idx = name.rfind('\\')
998
 
    if idx != -1:
999
 
        name = name[idx+1 : ]
1000
 
 
1001
 
    name = name.lstrip('.')
1002
 
 
1003
 
    s = hexlify(rand_bytes(8))
1004
 
    return '-'.join((name, compact_date(time.time()), s))