/brz/remove-bazaar

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