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