/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-11 06:07:43 UTC
  • Revision ID: mbp@sourcefrog.net-20050511060743-82c5a87967ad4537
- remove diff_trees from public api

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