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