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