~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: mbp at sourcefrog
  • Date: 2005-03-23 06:25:55 UTC
  • Revision ID: mbp@sourcefrog.net-20050323062555-5489339018d0c043
- import a subset of elementtree for easier installation

Show diffs side-by-side

added added

removed removed

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