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