/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: Martin Pool
  • Date: 2005-05-05 03:02:38 UTC
  • Revision ID: mbp@sourcefrog.net-20050505030238-313278579cfb17a0
- Show aliases in command help

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))