~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-28 02:24:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050328022418-9d37f56361aa18e9
doc: more on ignore patterns

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 bailout, 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
43
def find_branch_root(f=None):
49
44
    """Find the branch root enclosing f, or pwd.
50
45
 
51
 
    f may be a filename or a URL.
52
 
 
53
46
    It is not necessary that f exists.
54
47
 
55
48
    Basically we keep looking up until we find the control directory or
56
49
    run into the root."""
57
 
    if f == None:
 
50
    if f is None:
58
51
        f = os.getcwd()
59
52
    elif hasattr(os.path, 'realpath'):
60
53
        f = os.path.realpath(f)
61
54
    else:
62
55
        f = os.path.abspath(f)
63
 
    if not os.path.exists(f):
64
 
        raise BzrError('%r does not exist' % f)
65
 
        
66
56
 
67
57
    orig_f = f
68
58
 
 
59
    last_f = f
69
60
    while True:
70
61
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
71
62
            return f
72
63
        head, tail = os.path.split(f)
73
64
        if head == f:
74
65
            # reached the root, whatever that may be
75
 
            raise BzrError('%r is not in a branch' % orig_f)
 
66
            bailout('%r is not in a branch' % orig_f)
76
67
        f = head
77
68
    
78
69
 
80
71
######################################################################
81
72
# branch objects
82
73
 
83
 
class Branch(object):
 
74
class Branch:
84
75
    """Branch holding a history of revisions.
85
76
 
86
 
    base
87
 
        Base directory of the branch.
 
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.
88
89
    """
89
 
    _lockmode = None
90
 
    
91
 
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
 
90
    def __init__(self, base, init=False, find_root=True):
92
91
        """Create new branch object at a particular location.
93
92
 
94
 
        base -- Base directory for the branch.
 
93
        :param base: Base directory for the branch.
95
94
        
96
 
        init -- If True, create new control files in a previously
 
95
        :param init: If True, create new control files in a previously
97
96
             unversioned directory.  If False, the branch must already
98
97
             be versioned.
99
98
 
100
 
        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
101
100
             existing branch containing base.
102
101
 
103
102
        In the test suite, creation of new trees is tested using the
115
114
                        ['use "bzr init" to initialize a new working tree',
116
115
                         'current bzr can only operate from top-of-tree'])
117
116
        self._check_format()
118
 
        self.lock(lock_mode)
119
117
 
120
118
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
121
119
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
129
127
    __repr__ = __str__
130
128
 
131
129
 
132
 
 
133
 
    def lock(self, mode='w'):
134
 
        """Lock the on-disk branch, excluding other processes."""
135
 
        try:
136
 
            import fcntl, errno
137
 
 
138
 
            if mode == 'w':
139
 
                lm = fcntl.LOCK_EX
140
 
                om = os.O_WRONLY | os.O_CREAT
141
 
            elif mode == 'r':
142
 
                lm = fcntl.LOCK_SH
143
 
                om = os.O_RDONLY
144
 
            else:
145
 
                raise BzrError("invalid locking mode %r" % mode)
146
 
 
147
 
            try:
148
 
                lockfile = os.open(self.controlfilename('branch-lock'), om)
149
 
            except OSError, e:
150
 
                if e.errno == errno.ENOENT:
151
 
                    # might not exist on branches from <0.0.4
152
 
                    self.controlfile('branch-lock', 'w').close()
153
 
                    lockfile = os.open(self.controlfilename('branch-lock'), om)
154
 
                else:
155
 
                    raise e
156
 
            
157
 
            fcntl.lockf(lockfile, lm)
158
 
            def unlock():
159
 
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
160
 
                os.close(lockfile)
161
 
                self._lockmode = None
162
 
            self.unlock = unlock
163
 
            self._lockmode = mode
164
 
        except ImportError:
165
 
            warning("please write a locking method for platform %r" % sys.platform)
166
 
            def unlock():
167
 
                self._lockmode = None
168
 
            self.unlock = unlock
169
 
            self._lockmode = mode
170
 
 
171
 
 
172
 
    def _need_readlock(self):
173
 
        if self._lockmode not in ['r', 'w']:
174
 
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
175
 
 
176
 
    def _need_writelock(self):
177
 
        if self._lockmode not in ['w']:
178
 
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
179
 
 
180
 
 
181
130
    def abspath(self, name):
182
131
        """Return absolute filename for something in the branch"""
183
132
        return os.path.join(self.base, name)
204
153
 
205
154
 
206
155
    def controlfile(self, file_or_path, mode='r'):
207
 
        """Open a control file for this branch.
208
 
 
209
 
        There are two classes of file in the control directory: text
210
 
        and binary.  binary files are untranslated byte streams.  Text
211
 
        control files are stored with Unix newlines and in UTF-8, even
212
 
        if the platform or locale defaults are different.
213
 
 
214
 
        Controlfiles should almost never be opened in write mode but
215
 
        rather should be atomically copied and replaced using atomicfile.
216
 
        """
217
 
 
218
 
        fn = self.controlfilename(file_or_path)
219
 
 
220
 
        if mode == 'rb' or mode == 'wb':
221
 
            return file(fn, mode)
222
 
        elif mode == 'r' or mode == 'w':
223
 
            # open in binary mode anyhow so there's no newline translation;
224
 
            # codecs uses line buffering by default; don't want that.
225
 
            import codecs
226
 
            return codecs.open(fn, mode + 'b', 'utf-8',
227
 
                               buffering=60000)
228
 
        else:
229
 
            raise BzrError("invalid controlfile mode %r" % mode)
230
 
 
 
156
        """Open a control file for this branch"""
 
157
        return file(self.controlfilename(file_or_path), mode)
231
158
 
232
159
 
233
160
    def _make_control(self):
239
166
        for d in ('text-store', 'inventory-store', 'revision-store'):
240
167
            os.mkdir(self.controlfilename(d))
241
168
        for f in ('revision-history', 'merged-patches',
242
 
                  'pending-merged-patches', 'branch-name',
243
 
                  'branch-lock'):
 
169
                  'pending-merged-patches', 'branch-name'):
244
170
            self.controlfile(f, 'w').write('')
245
171
        mutter('created control directory in ' + self.base)
246
172
        Inventory().write_xml(self.controlfile('inventory','w'))
253
179
 
254
180
        In the future, we might need different in-memory Branch
255
181
        classes to support downlevel branches.  But not yet.
256
 
        """
257
 
        # This ignores newlines so that we can open branches created
258
 
        # on Windows from Linux and so on.  I think it might be better
259
 
        # to always make all internal files in unix format.
260
 
        fmt = self.controlfile('branch-format', 'r').read()
261
 
        fmt.replace('\r\n', '')
 
182
        """        
 
183
        # read in binary mode to detect newline wierdness.
 
184
        fmt = self.controlfile('branch-format', 'rb').read()
262
185
        if fmt != BZR_BRANCH_FORMAT:
263
186
            bailout('sorry, branch format %r not supported' % fmt,
264
187
                    ['use a different bzr version',
267
190
 
268
191
    def read_working_inventory(self):
269
192
        """Read the working inventory."""
270
 
        self._need_readlock()
271
193
        before = time.time()
272
 
        # ElementTree does its own conversion from UTF-8, so open in
273
 
        # binary.
274
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
194
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
275
195
        mutter("loaded inventory of %d items in %f"
276
196
               % (len(inv), time.time() - before))
277
197
        return inv
283
203
        That is to say, the inventory describing changes underway, that
284
204
        will be committed to the next revision.
285
205
        """
286
 
        self._need_writelock()
287
206
        ## TODO: factor out to atomicfile?  is rename safe on windows?
288
207
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
289
208
        tmpfname = self.controlfilename('inventory.tmp')
290
 
        tmpf = file(tmpfname, 'wb')
 
209
        tmpf = file(tmpfname, 'w')
291
210
        inv.write_xml(tmpf)
292
211
        tmpf.close()
293
 
        inv_fname = self.controlfilename('inventory')
294
 
        if sys.platform == 'win32':
295
 
            os.remove(inv_fname)
296
 
        os.rename(tmpfname, inv_fname)
 
212
        os.rename(tmpfname, self.controlfilename('inventory'))
297
213
        mutter('wrote working inventory')
298
214
 
299
215
 
301
217
                         """Inventory for the working copy.""")
302
218
 
303
219
 
304
 
    def add(self, files, verbose=False, ids=None):
 
220
    def add(self, files, verbose=False):
305
221
        """Make files versioned.
306
222
 
307
 
        Note that the command line normally calls smart_add instead.
308
 
 
309
223
        This puts the files in the Added state, so that they will be
310
224
        recorded by the next commit.
311
225
 
312
 
        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
313
227
               not (yet) exist.
314
228
 
315
 
        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
316
230
               is easy to retrieve them if they're needed.
317
231
 
318
 
        TODO: Option to specify file id.
 
232
        :todo: Option to specify file id.
319
233
 
320
 
        TODO: Adding a directory should optionally recurse down and
 
234
        :todo: Adding a directory should optionally recurse down and
321
235
               add all non-ignored children.  Perhaps do that in a
322
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', [])
323
259
        """
324
 
        self._need_writelock()
325
260
 
326
261
        # TODO: Re-adding a file that is removed in the working copy
327
262
        # should probably put it back with the previous ID.
328
263
        if isinstance(files, types.StringTypes):
329
 
            assert(ids is None or isinstance(ids, types.StringTypes))
330
264
            files = [files]
331
 
            if ids is not None:
332
 
                ids = [ids]
333
 
 
334
 
        if ids is None:
335
 
            ids = [None] * len(files)
336
 
        else:
337
 
            assert(len(ids) == len(files))
338
265
        
339
266
        inv = self.read_working_inventory()
340
 
        for f,file_id in zip(files, ids):
 
267
        for f in files:
341
268
            if is_control_file(f):
342
269
                bailout("cannot add control file %s" % quotefn(f))
343
270
 
357
284
            if kind != 'file' and kind != 'directory':
358
285
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
359
286
 
360
 
            if file_id is None:
361
 
                file_id = gen_file_id(f)
 
287
            file_id = gen_file_id(f)
362
288
            inv.add_path(f, kind=kind, file_id=file_id)
363
289
 
364
290
            if verbose:
369
295
        self._write_inventory(inv)
370
296
 
371
297
 
372
 
    def print_file(self, file, revno):
373
 
        """Print `file` to stdout."""
374
 
        self._need_readlock()
375
 
        tree = self.revision_tree(self.lookup_revision(revno))
376
 
        # use inventory as it was in that revision
377
 
        file_id = tree.inventory.path2id(file)
378
 
        if not file_id:
379
 
            bailout("%r is not present in revision %d" % (file, revno))
380
 
        tree.print_file(file_id)
381
 
        
382
298
 
383
299
    def remove(self, files, verbose=False):
384
300
        """Mark nominated files for removal from the inventory.
385
301
 
386
302
        This does not remove their text.  This does not run on 
387
303
 
388
 
        TODO: Refuse to remove modified files unless --force is given?
389
 
 
390
 
        TODO: Do something useful with directories.
391
 
 
392
 
        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
393
331
        removing may be useful and the user can just use use rm, and
394
332
        is the opposite of add.  Removing it is consistent with most
395
333
        other tools.  Maybe an option.
396
334
        """
397
335
        ## TODO: Normalize names
398
336
        ## TODO: Remove nested loops; better scalability
399
 
        self._need_writelock()
400
337
 
401
338
        if isinstance(files, types.StringTypes):
402
339
            files = [files]
421
358
 
422
359
        self._write_inventory(inv)
423
360
 
424
 
    def set_inventory(self, new_inventory_list):
425
 
        inv = Inventory()
426
 
        for path, file_id, parent, kind in new_inventory_list:
427
 
            name = os.path.basename(path)
428
 
            if name == "":
429
 
                continue
430
 
            inv.add(InventoryEntry(file_id, name, kind, parent))
431
 
        self._write_inventory(inv)
432
 
 
433
361
 
434
362
    def unknowns(self):
435
363
        """Return all unknown files.
450
378
        return self.working_tree().unknowns()
451
379
 
452
380
 
453
 
    def append_revision(self, revision_id):
454
 
        mutter("add {%s} to revision-history" % revision_id)
455
 
        rev_history = self.revision_history()
456
 
 
457
 
        tmprhname = self.controlfilename('revision-history.tmp')
458
 
        rhname = self.controlfilename('revision-history')
459
 
        
460
 
        f = file(tmprhname, 'wt')
461
 
        rev_history.append(revision_id)
462
 
        f.write('\n'.join(rev_history))
463
 
        f.write('\n')
 
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 = 'M'
 
484
                        else:
 
485
                            state = 'R'
 
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
        f = self.controlfile('revision-history', 'at')
 
546
        f.write(rev_id + '\n')
464
547
        f.close()
465
548
 
466
 
        if sys.platform == 'win32':
467
 
            os.remove(rhname)
468
 
        os.rename(tmprhname, rhname)
469
 
        
 
549
        if verbose:
 
550
            note("commited r%d" % self.revno())
470
551
 
471
552
 
472
553
    def get_revision(self, revision_id):
473
554
        """Return the Revision object for a named revision"""
474
 
        self._need_readlock()
475
555
        r = Revision.read_xml(self.revision_store[revision_id])
476
556
        assert r.revision_id == revision_id
477
557
        return r
480
560
    def get_inventory(self, inventory_id):
481
561
        """Get Inventory object by hash.
482
562
 
483
 
        TODO: Perhaps for this and similar methods, take a revision
 
563
        :todo: Perhaps for this and similar methods, take a revision
484
564
               parameter which can be either an integer revno or a
485
565
               string hash."""
486
 
        self._need_readlock()
487
566
        i = Inventory.read_xml(self.inventory_store[inventory_id])
488
567
        return i
489
568
 
490
569
 
491
570
    def get_revision_inventory(self, revision_id):
492
571
        """Return inventory of a past revision."""
493
 
        self._need_readlock()
494
572
        if revision_id == None:
495
573
            return Inventory()
496
574
        else:
503
581
        >>> ScratchBranch().revision_history()
504
582
        []
505
583
        """
506
 
        self._need_readlock()
507
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
508
 
 
509
 
 
510
 
    def enum_history(self, direction):
511
 
        """Return (revno, revision_id) for history of branch.
512
 
 
513
 
        direction
514
 
            'forward' is from earliest to latest
515
 
            'reverse' is from latest to earliest
516
 
        """
517
 
        rh = self.revision_history()
518
 
        if direction == 'forward':
519
 
            i = 1
520
 
            for rid in rh:
521
 
                yield i, rid
522
 
                i += 1
523
 
        elif direction == 'reverse':
524
 
            i = len(rh)
525
 
            while i > 0:
526
 
                yield i, rh[i-1]
527
 
                i -= 1
528
 
        else:
529
 
            raise ValueError('invalid history direction', direction)
 
584
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
530
585
 
531
586
 
532
587
    def revno(self):
534
589
 
535
590
        That is equivalent to the number of revisions committed to
536
591
        this branch.
 
592
 
 
593
        >>> b = ScratchBranch()
 
594
        >>> b.revno()
 
595
        0
 
596
        >>> b.commit('no foo')
 
597
        >>> b.revno()
 
598
        1
537
599
        """
538
600
        return len(self.revision_history())
539
601
 
540
602
 
541
603
    def last_patch(self):
542
604
        """Return last patch hash, or None if no history.
 
605
 
 
606
        >>> ScratchBranch().last_patch() == None
 
607
        True
543
608
        """
544
609
        ph = self.revision_history()
545
610
        if ph:
546
611
            return ph[-1]
547
 
        else:
548
 
            return None
549
 
 
550
 
 
551
 
    def commit(self, *args, **kw):
552
 
        """Deprecated"""
553
 
        from bzrlib.commit import commit
554
 
        commit(self, *args, **kw)
555
 
        
 
612
 
556
613
 
557
614
    def lookup_revision(self, revno):
558
615
        """Return revision hash for revision number."""
563
620
            # list is 0-based; revisions are 1-based
564
621
            return self.revision_history()[revno-1]
565
622
        except IndexError:
566
 
            raise BzrError("no such revision %s" % revno)
 
623
            bailout("no such revision %s" % revno)
567
624
 
568
625
 
569
626
    def revision_tree(self, revision_id):
571
628
 
572
629
        `revision_id` may be None for the null revision, in which case
573
630
        an `EmptyTree` is returned."""
574
 
        # TODO: refactor this to use an existing revision object
575
 
        # so we don't need to read it in twice.
576
 
        self._need_readlock()
 
631
 
577
632
        if revision_id == None:
578
633
            return EmptyTree()
579
634
        else:
583
638
 
584
639
    def working_tree(self):
585
640
        """Return a `Tree` for the working copy."""
586
 
        from workingtree import WorkingTree
587
641
        return WorkingTree(self.base, self.read_working_inventory())
588
642
 
589
643
 
591
645
        """Return `Tree` object for last revision.
592
646
 
593
647
        If there are no revisions yet, return an `EmptyTree`.
 
648
 
 
649
        >>> b = ScratchBranch(files=['foo'])
 
650
        >>> b.basis_tree().has_filename('foo')
 
651
        False
 
652
        >>> b.working_tree().has_filename('foo')
 
653
        True
 
654
        >>> b.add('foo')
 
655
        >>> b.commit('add foo')
 
656
        >>> b.basis_tree().has_filename('foo')
 
657
        True
594
658
        """
595
659
        r = self.last_patch()
596
660
        if r == None:
600
664
 
601
665
 
602
666
 
603
 
    def rename_one(self, from_rel, to_rel):
604
 
        """Rename one file.
605
 
 
606
 
        This can change the directory or the filename or both.
607
 
        """
608
 
        self._need_writelock()
609
 
        tree = self.working_tree()
610
 
        inv = tree.inventory
611
 
        if not tree.has_filename(from_rel):
612
 
            bailout("can't rename: old working file %r does not exist" % from_rel)
613
 
        if tree.has_filename(to_rel):
614
 
            bailout("can't rename: new working file %r already exists" % to_rel)
615
 
            
616
 
        file_id = inv.path2id(from_rel)
617
 
        if file_id == None:
618
 
            bailout("can't rename: old name %r is not versioned" % from_rel)
619
 
 
620
 
        if inv.path2id(to_rel):
621
 
            bailout("can't rename: new name %r is already versioned" % to_rel)
622
 
 
623
 
        to_dir, to_tail = os.path.split(to_rel)
624
 
        to_dir_id = inv.path2id(to_dir)
625
 
        if to_dir_id == None and to_dir != '':
626
 
            bailout("can't determine destination directory id for %r" % to_dir)
627
 
 
628
 
        mutter("rename_one:")
629
 
        mutter("  file_id    {%s}" % file_id)
630
 
        mutter("  from_rel   %r" % from_rel)
631
 
        mutter("  to_rel     %r" % to_rel)
632
 
        mutter("  to_dir     %r" % to_dir)
633
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
634
 
            
635
 
        inv.rename(file_id, to_dir_id, to_tail)
636
 
 
637
 
        print "%s => %s" % (from_rel, to_rel)
 
667
    def write_log(self, show_timezone='original'):
 
668
        """Write out human-readable log of commits to this branch
 
669
 
 
670
        :param utc: If true, show dates in universal time, not local time."""
 
671
        ## TODO: Option to choose either original, utc or local timezone
 
672
        revno = 1
 
673
        precursor = None
 
674
        for p in self.revision_history():
 
675
            print '-' * 40
 
676
            print 'revno:', revno
 
677
            ## TODO: Show hash if --id is given.
 
678
            ##print 'revision-hash:', p
 
679
            rev = self.get_revision(p)
 
680
            print 'committer:', rev.committer
 
681
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
 
682
                                                 show_timezone))
 
683
 
 
684
            ## opportunistic consistency check, same as check_patch_chaining
 
685
            if rev.precursor != precursor:
 
686
                bailout("mismatched precursor!")
 
687
 
 
688
            print 'message:'
 
689
            if not rev.message:
 
690
                print '  (no message)'
 
691
            else:
 
692
                for l in rev.message.split('\n'):
 
693
                    print '  ' + l
 
694
 
 
695
            revno += 1
 
696
            precursor = p
 
697
 
 
698
 
 
699
 
 
700
    def show_status(branch, show_all=False):
 
701
        """Display single-line status for non-ignored working files.
 
702
 
 
703
        The list is show sorted in order by file name.
 
704
 
 
705
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
706
        >>> b.show_status()
 
707
        ?       foo
 
708
        >>> b.add('foo')
 
709
        >>> b.show_status()
 
710
        A       foo
 
711
        >>> b.commit("add foo")
 
712
        >>> b.show_status()
 
713
        >>> os.unlink(b.abspath('foo'))
 
714
        >>> b.show_status()
 
715
        D       foo
638
716
        
639
 
        from_abs = self.abspath(from_rel)
640
 
        to_abs = self.abspath(to_rel)
641
 
        try:
642
 
            os.rename(from_abs, to_abs)
643
 
        except OSError, e:
644
 
            bailout("failed to rename %r to %r: %s"
645
 
                    % (from_abs, to_abs, e[1]),
646
 
                    ["rename rolled back"])
647
 
 
648
 
        self._write_inventory(inv)
649
 
            
650
 
 
651
 
 
652
 
    def move(self, from_paths, to_name):
653
 
        """Rename files.
654
 
 
655
 
        to_name must exist as a versioned directory.
656
 
 
657
 
        If to_name exists and is a directory, the files are moved into
658
 
        it, keeping their old names.  If it is a directory, 
659
 
 
660
 
        Note that to_name is only the last component of the new name;
661
 
        this doesn't change the directory.
 
717
 
 
718
        :todo: Get state for single files.
 
719
 
 
720
        :todo: Perhaps show a slash at the end of directory names.        
 
721
 
662
722
        """
663
 
        self._need_writelock()
664
 
        ## TODO: Option to move IDs only
665
 
        assert not isinstance(from_paths, basestring)
666
 
        tree = self.working_tree()
667
 
        inv = tree.inventory
668
 
        to_abs = self.abspath(to_name)
669
 
        if not isdir(to_abs):
670
 
            bailout("destination %r is not a directory" % to_abs)
671
 
        if not tree.has_filename(to_name):
672
 
            bailout("destination %r not in working directory" % to_abs)
673
 
        to_dir_id = inv.path2id(to_name)
674
 
        if to_dir_id == None and to_name != '':
675
 
            bailout("destination %r is not a versioned directory" % to_name)
676
 
        to_dir_ie = inv[to_dir_id]
677
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
678
 
            bailout("destination %r is not a directory" % to_abs)
679
 
 
680
 
        to_idpath = inv.get_idpath(to_dir_id)
681
 
 
682
 
        for f in from_paths:
683
 
            if not tree.has_filename(f):
684
 
                bailout("%r does not exist in working tree" % f)
685
 
            f_id = inv.path2id(f)
686
 
            if f_id == None:
687
 
                bailout("%r is not versioned" % f)
688
 
            name_tail = splitpath(f)[-1]
689
 
            dest_path = appendpath(to_name, name_tail)
690
 
            if tree.has_filename(dest_path):
691
 
                bailout("destination %r already exists" % dest_path)
692
 
            if f_id in to_idpath:
693
 
                bailout("can't move %r to a subdirectory of itself" % f)
694
 
 
695
 
        # OK, so there's a race here, it's possible that someone will
696
 
        # create a file in this interval and then the rename might be
697
 
        # left half-done.  But we should have caught most problems.
698
 
 
699
 
        for f in from_paths:
700
 
            name_tail = splitpath(f)[-1]
701
 
            dest_path = appendpath(to_name, name_tail)
702
 
            print "%s => %s" % (f, dest_path)
703
 
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
704
 
            try:
705
 
                os.rename(self.abspath(f), self.abspath(dest_path))
706
 
            except OSError, e:
707
 
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
708
 
                        ["rename rolled back"])
709
 
 
710
 
        self._write_inventory(inv)
711
 
 
712
 
 
 
723
 
 
724
        # We have to build everything into a list first so that it can
 
725
        # sorted by name, incorporating all the different sources.
 
726
 
 
727
        # FIXME: Rather than getting things in random order and then sorting,
 
728
        # just step through in order.
 
729
 
 
730
        # Interesting case: the old ID for a file has been removed,
 
731
        # but a new file has been created under that name.
 
732
 
 
733
        old = branch.basis_tree()
 
734
        old_inv = old.inventory
 
735
        new = branch.working_tree()
 
736
        new_inv = new.inventory
 
737
 
 
738
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
 
739
            if fs == 'R':
 
740
                show_status(fs, kind,
 
741
                            oldname + ' => ' + newname)
 
742
            elif fs == 'A' or fs == 'M':
 
743
                show_status(fs, kind, newname)
 
744
            elif fs == 'D':
 
745
                show_status(fs, kind, oldname)
 
746
            elif fs == '.':
 
747
                if show_all:
 
748
                    show_status(fs, kind, newname)
 
749
            elif fs == 'I':
 
750
                if show_all:
 
751
                    show_status(fs, kind, newname)
 
752
            elif fs == '?':
 
753
                show_status(fs, kind, newname)
 
754
            else:
 
755
                bailout("wierd file state %r" % ((fs, fid),))
 
756
                
713
757
 
714
758
 
715
759
class ScratchBranch(Branch):
719
763
    >>> isdir(b.base)
720
764
    True
721
765
    >>> bd = b.base
722
 
    >>> b.destroy()
 
766
    >>> del b
723
767
    >>> isdir(bd)
724
768
    False
725
769
    """
739
783
 
740
784
 
741
785
    def __del__(self):
742
 
        self.destroy()
743
 
 
744
 
    def destroy(self):
745
786
        """Destroy the test branch, removing the scratch directory."""
746
 
        try:
747
 
            mutter("delete ScratchBranch %s" % self.base)
748
 
            shutil.rmtree(self.base)
749
 
        except OSError, e:
750
 
            # Work around for shutil.rmtree failing on Windows when
751
 
            # readonly files are encountered
752
 
            mutter("hit exception in destroying ScratchBranch: %s" % e)
753
 
            for root, dirs, files in os.walk(self.base, topdown=False):
754
 
                for name in files:
755
 
                    os.chmod(os.path.join(root, name), 0700)
756
 
            shutil.rmtree(self.base)
757
 
        self.base = None
 
787
        shutil.rmtree(self.base)
758
788
 
759
789
    
760
790
 
777
807
 
778
808
 
779
809
 
 
810
def _gen_revision_id(when):
 
811
    """Return new revision-id."""
 
812
    s = '%s-%s-' % (user_email(), compact_date(when))
 
813
    s += hexlify(rand_bytes(8))
 
814
    return s
 
815
 
 
816
 
780
817
def gen_file_id(name):
781
818
    """Return new file id.
782
819
 
783
820
    This should probably generate proper UUIDs, but for the moment we
784
821
    cope with just randomness because running uuidgen every time is
785
822
    slow."""
786
 
    import re
787
 
 
788
 
    # get last component
789
823
    idx = name.rfind('/')
790
824
    if idx != -1:
791
825
        name = name[idx+1 : ]
792
 
    idx = name.rfind('\\')
793
 
    if idx != -1:
794
 
        name = name[idx+1 : ]
795
826
 
796
 
    # make it not a hidden file
797
827
    name = name.lstrip('.')
798
828
 
799
 
    # remove any wierd characters; we don't escape them but rather
800
 
    # just pull them out
801
 
    name = re.sub(r'[^\w.]', '', name)
802
 
 
803
829
    s = hexlify(rand_bytes(8))
804
830
    return '-'.join((name, compact_date(time.time()), s))
 
831
 
 
832