/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 01:03:22 UTC
  • Revision ID: mbp@sourcefrog.net-20050511010322-54654b917bbce05f
- Notes on library dependencies

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(f, **args):
 
44
    if 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
        return WorkingTree(self.base, self.read_working_inventory())
 
789
 
 
790
 
 
791
    def basis_tree(self):
 
792
        """Return `Tree` object for last revision.
 
793
 
 
794
        If there are no revisions yet, return an `EmptyTree`.
 
795
 
 
796
        >>> b = ScratchBranch(files=['foo'])
 
797
        >>> b.basis_tree().has_filename('foo')
 
798
        False
 
799
        >>> b.working_tree().has_filename('foo')
 
800
        True
 
801
        >>> b.add('foo')
 
802
        >>> b.commit('add foo')
 
803
        >>> b.basis_tree().has_filename('foo')
 
804
        True
 
805
        """
 
806
        r = self.last_patch()
 
807
        if r == None:
 
808
            return EmptyTree()
 
809
        else:
 
810
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
811
 
 
812
 
 
813
 
 
814
    def rename_one(self, from_rel, to_rel):
 
815
        """Rename one file.
 
816
 
 
817
        This can change the directory or the filename or both.
 
818
        """
 
819
        self._need_writelock()
 
820
        tree = self.working_tree()
 
821
        inv = tree.inventory
 
822
        if not tree.has_filename(from_rel):
 
823
            bailout("can't rename: old working file %r does not exist" % from_rel)
 
824
        if tree.has_filename(to_rel):
 
825
            bailout("can't rename: new working file %r already exists" % to_rel)
 
826
            
 
827
        file_id = inv.path2id(from_rel)
 
828
        if file_id == None:
 
829
            bailout("can't rename: old name %r is not versioned" % from_rel)
 
830
 
 
831
        if inv.path2id(to_rel):
 
832
            bailout("can't rename: new name %r is already versioned" % to_rel)
 
833
 
 
834
        to_dir, to_tail = os.path.split(to_rel)
 
835
        to_dir_id = inv.path2id(to_dir)
 
836
        if to_dir_id == None and to_dir != '':
 
837
            bailout("can't determine destination directory id for %r" % to_dir)
 
838
 
 
839
        mutter("rename_one:")
 
840
        mutter("  file_id    {%s}" % file_id)
 
841
        mutter("  from_rel   %r" % from_rel)
 
842
        mutter("  to_rel     %r" % to_rel)
 
843
        mutter("  to_dir     %r" % to_dir)
 
844
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
845
            
 
846
        inv.rename(file_id, to_dir_id, to_tail)
 
847
 
 
848
        print "%s => %s" % (from_rel, to_rel)
 
849
        
 
850
        from_abs = self.abspath(from_rel)
 
851
        to_abs = self.abspath(to_rel)
 
852
        try:
 
853
            os.rename(from_abs, to_abs)
 
854
        except OSError, e:
 
855
            bailout("failed to rename %r to %r: %s"
 
856
                    % (from_abs, to_abs, e[1]),
 
857
                    ["rename rolled back"])
 
858
 
 
859
        self._write_inventory(inv)
 
860
            
 
861
 
 
862
 
 
863
    def move(self, from_paths, to_name):
 
864
        """Rename files.
 
865
 
 
866
        to_name must exist as a versioned directory.
 
867
 
 
868
        If to_name exists and is a directory, the files are moved into
 
869
        it, keeping their old names.  If it is a directory, 
 
870
 
 
871
        Note that to_name is only the last component of the new name;
 
872
        this doesn't change the directory.
 
873
        """
 
874
        self._need_writelock()
 
875
        ## TODO: Option to move IDs only
 
876
        assert not isinstance(from_paths, basestring)
 
877
        tree = self.working_tree()
 
878
        inv = tree.inventory
 
879
        to_abs = self.abspath(to_name)
 
880
        if not isdir(to_abs):
 
881
            bailout("destination %r is not a directory" % to_abs)
 
882
        if not tree.has_filename(to_name):
 
883
            bailout("destination %r not in working directory" % to_abs)
 
884
        to_dir_id = inv.path2id(to_name)
 
885
        if to_dir_id == None and to_name != '':
 
886
            bailout("destination %r is not a versioned directory" % to_name)
 
887
        to_dir_ie = inv[to_dir_id]
 
888
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
889
            bailout("destination %r is not a directory" % to_abs)
 
890
 
 
891
        to_idpath = Set(inv.get_idpath(to_dir_id))
 
892
 
 
893
        for f in from_paths:
 
894
            if not tree.has_filename(f):
 
895
                bailout("%r does not exist in working tree" % f)
 
896
            f_id = inv.path2id(f)
 
897
            if f_id == None:
 
898
                bailout("%r is not versioned" % f)
 
899
            name_tail = splitpath(f)[-1]
 
900
            dest_path = appendpath(to_name, name_tail)
 
901
            if tree.has_filename(dest_path):
 
902
                bailout("destination %r already exists" % dest_path)
 
903
            if f_id in to_idpath:
 
904
                bailout("can't move %r to a subdirectory of itself" % f)
 
905
 
 
906
        # OK, so there's a race here, it's possible that someone will
 
907
        # create a file in this interval and then the rename might be
 
908
        # left half-done.  But we should have caught most problems.
 
909
 
 
910
        for f in from_paths:
 
911
            name_tail = splitpath(f)[-1]
 
912
            dest_path = appendpath(to_name, name_tail)
 
913
            print "%s => %s" % (f, dest_path)
 
914
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
915
            try:
 
916
                os.rename(self.abspath(f), self.abspath(dest_path))
 
917
            except OSError, e:
 
918
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
919
                        ["rename rolled back"])
 
920
 
 
921
        self._write_inventory(inv)
 
922
 
 
923
 
 
924
 
 
925
    def show_status(self, show_all=False, file_list=None):
 
926
        """Display single-line status for non-ignored working files.
 
927
 
 
928
        The list is show sorted in order by file name.
 
929
 
 
930
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
931
        >>> b.show_status()
 
932
        ?       foo
 
933
        >>> b.add('foo')
 
934
        >>> b.show_status()
 
935
        A       foo
 
936
        >>> b.commit("add foo")
 
937
        >>> b.show_status()
 
938
        >>> os.unlink(b.abspath('foo'))
 
939
        >>> b.show_status()
 
940
        D       foo
 
941
        """
 
942
        self._need_readlock()
 
943
 
 
944
        # We have to build everything into a list first so that it can
 
945
        # sorted by name, incorporating all the different sources.
 
946
 
 
947
        # FIXME: Rather than getting things in random order and then sorting,
 
948
        # just step through in order.
 
949
 
 
950
        # Interesting case: the old ID for a file has been removed,
 
951
        # but a new file has been created under that name.
 
952
 
 
953
        old = self.basis_tree()
 
954
        new = self.working_tree()
 
955
 
 
956
        items = diff_trees(old, new)
 
957
        # We want to filter out only if any file was provided in the file_list.
 
958
        if isinstance(file_list, list) and len(file_list):
 
959
            items = [item for item in items if item[3] in file_list]
 
960
 
 
961
        for fs, fid, oldname, newname, kind in items:
 
962
            if fs == 'R':
 
963
                show_status(fs, kind,
 
964
                            oldname + ' => ' + newname)
 
965
            elif fs == 'A' or fs == 'M':
 
966
                show_status(fs, kind, newname)
 
967
            elif fs == 'D':
 
968
                show_status(fs, kind, oldname)
 
969
            elif fs == '.':
 
970
                if show_all:
 
971
                    show_status(fs, kind, newname)
 
972
            elif fs == 'I':
 
973
                if show_all:
 
974
                    show_status(fs, kind, newname)
 
975
            elif fs == '?':
 
976
                show_status(fs, kind, newname)
 
977
            else:
 
978
                bailout("weird file state %r" % ((fs, fid),))
 
979
                
 
980
 
 
981
 
 
982
class ScratchBranch(Branch):
 
983
    """Special test class: a branch that cleans up after itself.
 
984
 
 
985
    >>> b = ScratchBranch()
 
986
    >>> isdir(b.base)
 
987
    True
 
988
    >>> bd = b.base
 
989
    >>> b.destroy()
 
990
    >>> isdir(bd)
 
991
    False
 
992
    """
 
993
    def __init__(self, files=[], dirs=[]):
 
994
        """Make a test branch.
 
995
 
 
996
        This creates a temporary directory and runs init-tree in it.
 
997
 
 
998
        If any files are listed, they are created in the working copy.
 
999
        """
 
1000
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
1001
        for d in dirs:
 
1002
            os.mkdir(self.abspath(d))
 
1003
            
 
1004
        for f in files:
 
1005
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1006
 
 
1007
 
 
1008
    def __del__(self):
 
1009
        self.destroy()
 
1010
 
 
1011
    def destroy(self):
 
1012
        """Destroy the test branch, removing the scratch directory."""
 
1013
        try:
 
1014
            mutter("delete ScratchBranch %s" % self.base)
 
1015
            shutil.rmtree(self.base)
 
1016
        except OSError, e:
 
1017
            # Work around for shutil.rmtree failing on Windows when
 
1018
            # readonly files are encountered
 
1019
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1020
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1021
                for name in files:
 
1022
                    os.chmod(os.path.join(root, name), 0700)
 
1023
            shutil.rmtree(self.base)
 
1024
        self.base = None
 
1025
 
 
1026
    
 
1027
 
 
1028
######################################################################
 
1029
# predicates
 
1030
 
 
1031
 
 
1032
def is_control_file(filename):
 
1033
    ## FIXME: better check
 
1034
    filename = os.path.normpath(filename)
 
1035
    while filename != '':
 
1036
        head, tail = os.path.split(filename)
 
1037
        ## mutter('check %r for control file' % ((head, tail), ))
 
1038
        if tail == bzrlib.BZRDIR:
 
1039
            return True
 
1040
        if filename == head:
 
1041
            break
 
1042
        filename = head
 
1043
    return False
 
1044
 
 
1045
 
 
1046
 
 
1047
def _gen_revision_id(when):
 
1048
    """Return new revision-id."""
 
1049
    s = '%s-%s-' % (user_email(), compact_date(when))
 
1050
    s += hexlify(rand_bytes(8))
 
1051
    return s
 
1052
 
 
1053
 
 
1054
def gen_file_id(name):
 
1055
    """Return new file id.
 
1056
 
 
1057
    This should probably generate proper UUIDs, but for the moment we
 
1058
    cope with just randomness because running uuidgen every time is
 
1059
    slow."""
 
1060
    idx = name.rfind('/')
 
1061
    if idx != -1:
 
1062
        name = name[idx+1 : ]
 
1063
    idx = name.rfind('\\')
 
1064
    if idx != -1:
 
1065
        name = name[idx+1 : ]
 
1066
 
 
1067
    name = name.lstrip('.')
 
1068
 
 
1069
    s = hexlify(rand_bytes(8))
 
1070
    return '-'.join((name, compact_date(time.time()), s))