~bzr-pqm/bzr/bzr.dev

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