1
# Copyright (C) 2005 Canonical Ltd
 
 
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.
 
 
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.
 
 
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
 
 
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
 
 
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, \
 
 
30
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
 
 
31
     joinpath, sha_string, file_kind, local_time_offset, appendpath
 
 
32
from store import ImmutableStore
 
 
33
from revision import Revision
 
 
34
from errors import bailout, BzrError
 
 
35
from textui import show_status
 
 
36
from diff import diff_trees
 
 
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 
 
39
## TODO: Maybe include checks for common corruption of newlines, etc?
 
 
43
def find_branch_root(f=None):
 
 
44
    """Find the branch root enclosing f, or pwd.
 
 
46
    It is not necessary that f exists.
 
 
48
    Basically we keep looking up until we find the control directory or
 
 
52
    elif hasattr(os.path, 'realpath'):
 
 
53
        f = os.path.realpath(f)
 
 
55
        f = os.path.abspath(f)
 
 
60
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
 
 
62
        head, tail = os.path.split(f)
 
 
64
            # reached the root, whatever that may be
 
 
65
            raise BzrError('%r is not in a branch' % orig_f)
 
 
70
######################################################################
 
 
74
    """Branch holding a history of revisions.
 
 
77
        Base directory of the branch.
 
 
79
    def __init__(self, base, init=False, find_root=True):
 
 
80
        """Create new branch object at a particular location.
 
 
82
        base -- Base directory for the branch.
 
 
84
        init -- If True, create new control files in a previously
 
 
85
             unversioned directory.  If False, the branch must already
 
 
88
        find_root -- If true and init is false, find the root of the
 
 
89
             existing branch containing base.
 
 
91
        In the test suite, creation of new trees is tested using the
 
 
92
        `ScratchBranch` class.
 
 
95
            self.base = os.path.realpath(base)
 
 
98
            self.base = find_branch_root(base)
 
 
100
            self.base = os.path.realpath(base)
 
 
101
            if not isdir(self.controlfilename('.')):
 
 
102
                bailout("not a bzr branch: %s" % quotefn(base),
 
 
103
                        ['use "bzr init" to initialize a new working tree',
 
 
104
                         'current bzr can only operate from top-of-tree'])
 
 
107
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
 
108
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
 
109
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
 
 
113
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
 
119
    def abspath(self, name):
 
 
120
        """Return absolute filename for something in the branch"""
 
 
121
        return os.path.join(self.base, name)
 
 
124
    def relpath(self, path):
 
 
125
        """Return path relative to this branch of something inside it.
 
 
127
        Raises an error if path is not in this branch."""
 
 
128
        rp = os.path.realpath(path)
 
 
130
        if not rp.startswith(self.base):
 
 
131
            bailout("path %r is not within branch %r" % (rp, self.base))
 
 
132
        rp = rp[len(self.base):]
 
 
133
        rp = rp.lstrip(os.sep)
 
 
137
    def controlfilename(self, file_or_path):
 
 
138
        """Return location relative to branch."""
 
 
139
        if isinstance(file_or_path, types.StringTypes):
 
 
140
            file_or_path = [file_or_path]
 
 
141
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
 
144
    def controlfile(self, file_or_path, mode='r'):
 
 
145
        """Open a control file for this branch.
 
 
147
        There are two classes of file in the control directory: text
 
 
148
        and binary.  binary files are untranslated byte streams.  Text
 
 
149
        control files are stored with Unix newlines and in UTF-8, even
 
 
150
        if the platform or locale defaults are different.
 
 
153
        fn = self.controlfilename(file_or_path)
 
 
155
        if mode == 'rb' or mode == 'wb':
 
 
156
            return file(fn, mode)
 
 
157
        elif mode == 'r' or mode == 'w':
 
 
158
            # open in binary mode anyhow so there's no newline translation;
 
 
159
            # codecs uses line buffering by default; don't want that.
 
 
161
            return codecs.open(fn, mode + 'b', 'utf-8',
 
 
164
            raise BzrError("invalid controlfile mode %r" % mode)
 
 
168
    def _make_control(self):
 
 
169
        os.mkdir(self.controlfilename([]))
 
 
170
        self.controlfile('README', 'w').write(
 
 
171
            "This is a Bazaar-NG control directory.\n"
 
 
172
            "Do not change any files in this directory.")
 
 
173
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
 
174
        for d in ('text-store', 'inventory-store', 'revision-store'):
 
 
175
            os.mkdir(self.controlfilename(d))
 
 
176
        for f in ('revision-history', 'merged-patches',
 
 
177
                  'pending-merged-patches', 'branch-name'):
 
 
178
            self.controlfile(f, 'w').write('')
 
 
179
        mutter('created control directory in ' + self.base)
 
 
180
        Inventory().write_xml(self.controlfile('inventory','w'))
 
 
183
    def _check_format(self):
 
 
184
        """Check this branch format is supported.
 
 
186
        The current tool only supports the current unstable format.
 
 
188
        In the future, we might need different in-memory Branch
 
 
189
        classes to support downlevel branches.  But not yet.
 
 
191
        # This ignores newlines so that we can open branches created
 
 
192
        # on Windows from Linux and so on.  I think it might be better
 
 
193
        # to always make all internal files in unix format.
 
 
194
        fmt = self.controlfile('branch-format', 'r').read()
 
 
195
        fmt.replace('\r\n', '')
 
 
196
        if fmt != BZR_BRANCH_FORMAT:
 
 
197
            bailout('sorry, branch format %r not supported' % fmt,
 
 
198
                    ['use a different bzr version',
 
 
199
                     'or remove the .bzr directory and "bzr init" again'])
 
 
202
    def read_working_inventory(self):
 
 
203
        """Read the working inventory."""
 
 
205
        # ElementTree does its own conversion from UTF-8, so open in
 
 
207
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
 
208
        mutter("loaded inventory of %d items in %f"
 
 
209
               % (len(inv), time.time() - before))
 
 
213
    def _write_inventory(self, inv):
 
 
214
        """Update the working inventory.
 
 
216
        That is to say, the inventory describing changes underway, that
 
 
217
        will be committed to the next revision.
 
 
219
        ## TODO: factor out to atomicfile?  is rename safe on windows?
 
 
220
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
 
 
221
        tmpfname = self.controlfilename('inventory.tmp')
 
 
222
        tmpf = file(tmpfname, 'wb')
 
 
225
        inv_fname = self.controlfilename('inventory')
 
 
226
        if sys.platform == 'win32':
 
 
228
        os.rename(tmpfname, inv_fname)
 
 
229
        mutter('wrote working inventory')
 
 
232
    inventory = property(read_working_inventory, _write_inventory, None,
 
 
233
                         """Inventory for the working copy.""")
 
 
236
    def add(self, files, verbose=False):
 
 
237
        """Make files versioned.
 
 
239
        Note that the command line normally calls smart_add instead.
 
 
241
        This puts the files in the Added state, so that they will be
 
 
242
        recorded by the next commit.
 
 
244
        TODO: Perhaps have an option to add the ids even if the files do
 
 
247
        TODO: Perhaps return the ids of the files?  But then again it
 
 
248
               is easy to retrieve them if they're needed.
 
 
250
        TODO: Option to specify file id.
 
 
252
        TODO: Adding a directory should optionally recurse down and
 
 
253
               add all non-ignored children.  Perhaps do that in a
 
 
256
        >>> b = ScratchBranch(files=['foo'])
 
 
257
        >>> 'foo' in b.unknowns()
 
 
262
        >>> 'foo' in b.unknowns()
 
 
264
        >>> bool(b.inventory.path2id('foo'))
 
 
270
        Traceback (most recent call last):
 
 
272
        BzrError: ('foo is already versioned', [])
 
 
274
        >>> b.add(['nothere'])
 
 
275
        Traceback (most recent call last):
 
 
276
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
 
 
279
        # TODO: Re-adding a file that is removed in the working copy
 
 
280
        # should probably put it back with the previous ID.
 
 
281
        if isinstance(files, types.StringTypes):
 
 
284
        inv = self.read_working_inventory()
 
 
286
            if is_control_file(f):
 
 
287
                bailout("cannot add control file %s" % quotefn(f))
 
 
292
                bailout("cannot add top-level %r" % f)
 
 
294
            fullpath = os.path.normpath(self.abspath(f))
 
 
297
                kind = file_kind(fullpath)
 
 
299
                # maybe something better?
 
 
300
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
 
 
302
            if kind != 'file' and kind != 'directory':
 
 
303
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
 
 
305
            file_id = gen_file_id(f)
 
 
306
            inv.add_path(f, kind=kind, file_id=file_id)
 
 
309
                show_status('A', kind, quotefn(f))
 
 
311
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
 
313
        self._write_inventory(inv)
 
 
316
    def print_file(self, file, revno):
 
 
317
        """Print `file` to stdout."""
 
 
318
        tree = self.revision_tree(self.lookup_revision(revno))
 
 
319
        # use inventory as it was in that revision
 
 
320
        file_id = tree.inventory.path2id(file)
 
 
322
            bailout("%r is not present in revision %d" % (file, revno))
 
 
323
        tree.print_file(file_id)
 
 
326
    def remove(self, files, verbose=False):
 
 
327
        """Mark nominated files for removal from the inventory.
 
 
329
        This does not remove their text.  This does not run on 
 
 
331
        TODO: Refuse to remove modified files unless --force is given?
 
 
333
        >>> b = ScratchBranch(files=['foo'])
 
 
335
        >>> b.inventory.has_filename('foo')
 
 
338
        >>> b.working_tree().has_filename('foo')
 
 
340
        >>> b.inventory.has_filename('foo')
 
 
343
        >>> b = ScratchBranch(files=['foo'])
 
 
348
        >>> b.inventory.has_filename('foo') 
 
 
350
        >>> b.basis_tree().has_filename('foo') 
 
 
352
        >>> b.working_tree().has_filename('foo') 
 
 
355
        TODO: Do something useful with directories.
 
 
357
        TODO: Should this remove the text or not?  Tough call; not
 
 
358
        removing may be useful and the user can just use use rm, and
 
 
359
        is the opposite of add.  Removing it is consistent with most
 
 
360
        other tools.  Maybe an option.
 
 
362
        ## TODO: Normalize names
 
 
363
        ## TODO: Remove nested loops; better scalability
 
 
365
        if isinstance(files, types.StringTypes):
 
 
368
        tree = self.working_tree()
 
 
371
        # do this before any modifications
 
 
375
                bailout("cannot remove unversioned file %s" % quotefn(f))
 
 
376
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
 
378
                # having remove it, it must be either ignored or unknown
 
 
379
                if tree.is_ignored(f):
 
 
383
                show_status(new_status, inv[fid].kind, quotefn(f))
 
 
386
        self._write_inventory(inv)
 
 
390
        """Return all unknown files.
 
 
392
        These are files in the working directory that are not versioned or
 
 
393
        control files or ignored.
 
 
395
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
 
396
        >>> list(b.unknowns())
 
 
399
        >>> list(b.unknowns())
 
 
402
        >>> list(b.unknowns())
 
 
405
        return self.working_tree().unknowns()
 
 
408
    def commit(self, message, timestamp=None, timezone=None,
 
 
411
        """Commit working copy as a new revision.
 
 
413
        The basic approach is to add all the file texts into the
 
 
414
        store, then the inventory, then make a new revision pointing
 
 
415
        to that inventory and store that.
 
 
417
        This is not quite safe if the working copy changes during the
 
 
418
        commit; for the moment that is simply not allowed.  A better
 
 
419
        approach is to make a temporary copy of the files before
 
 
420
        computing their hashes, and then add those hashes in turn to
 
 
421
        the inventory.  This should mean at least that there are no
 
 
422
        broken hash pointers.  There is no way we can get a snapshot
 
 
423
        of the whole directory at an instant.  This would also have to
 
 
424
        be robust against files disappearing, moving, etc.  So the
 
 
425
        whole thing is a bit hard.
 
 
427
        timestamp -- if not None, seconds-since-epoch for a
 
 
428
             postdated/predated commit.
 
 
431
        ## TODO: Show branch names
 
 
433
        # TODO: Don't commit if there are no changes, unless forced?
 
 
435
        # First walk over the working inventory; and both update that
 
 
436
        # and also build a new revision inventory.  The revision
 
 
437
        # inventory needs to hold the text-id, sha1 and size of the
 
 
438
        # actual file versions committed in the revision.  (These are
 
 
439
        # not present in the working inventory.)  We also need to
 
 
440
        # detect missing/deleted files, and remove them from the
 
 
443
        work_inv = self.read_working_inventory()
 
 
445
        basis = self.basis_tree()
 
 
446
        basis_inv = basis.inventory
 
 
448
        for path, entry in work_inv.iter_entries():
 
 
449
            ## TODO: Cope with files that have gone missing.
 
 
451
            ## TODO: Check that the file kind has not changed from the previous
 
 
452
            ## revision of this file (if any).
 
 
456
            p = self.abspath(path)
 
 
457
            file_id = entry.file_id
 
 
458
            mutter('commit prep file %s, id %r ' % (p, file_id))
 
 
460
            if not os.path.exists(p):
 
 
461
                mutter("    file is missing, removing from inventory")
 
 
463
                    show_status('D', entry.kind, quotefn(path))
 
 
464
                missing_ids.append(file_id)
 
 
467
            # TODO: Handle files that have been deleted
 
 
469
            # TODO: Maybe a special case for empty files?  Seems a
 
 
470
            # waste to store them many times.
 
 
474
            if basis_inv.has_id(file_id):
 
 
475
                old_kind = basis_inv[file_id].kind
 
 
476
                if old_kind != entry.kind:
 
 
477
                    bailout("entry %r changed kind from %r to %r"
 
 
478
                            % (file_id, old_kind, entry.kind))
 
 
480
            if entry.kind == 'directory':
 
 
482
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
 
 
483
            elif entry.kind == 'file':
 
 
485
                    bailout("%s is entered as file but is not a file" % quotefn(p))
 
 
487
                content = file(p, 'rb').read()
 
 
489
                entry.text_sha1 = sha_string(content)
 
 
490
                entry.text_size = len(content)
 
 
492
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
 
 
494
                    and (old_ie.text_size == entry.text_size)
 
 
495
                    and (old_ie.text_sha1 == entry.text_sha1)):
 
 
496
                    ## assert content == basis.get_file(file_id).read()
 
 
497
                    entry.text_id = basis_inv[file_id].text_id
 
 
498
                    mutter('    unchanged from previous text_id {%s}' %
 
 
502
                    entry.text_id = gen_file_id(entry.name)
 
 
503
                    self.text_store.add(content, entry.text_id)
 
 
504
                    mutter('    stored with text_id {%s}' % entry.text_id)
 
 
508
                        elif (old_ie.name == entry.name
 
 
509
                              and old_ie.parent_id == entry.parent_id):
 
 
514
                        show_status(state, entry.kind, quotefn(path))
 
 
516
        for file_id in missing_ids:
 
 
517
            # have to do this later so we don't mess up the iterator.
 
 
518
            # since parents may be removed before their children we
 
 
521
            # FIXME: There's probably a better way to do this; perhaps
 
 
522
            # the workingtree should know how to filter itself.
 
 
523
            if work_inv.has_id(file_id):
 
 
524
                del work_inv[file_id]
 
 
527
        inv_id = rev_id = _gen_revision_id(time.time())
 
 
529
        inv_tmp = tempfile.TemporaryFile()
 
 
530
        inv.write_xml(inv_tmp)
 
 
532
        self.inventory_store.add(inv_tmp, inv_id)
 
 
533
        mutter('new inventory_id is {%s}' % inv_id)
 
 
535
        self._write_inventory(work_inv)
 
 
537
        if timestamp == None:
 
 
538
            timestamp = time.time()
 
 
540
        if committer == None:
 
 
541
            committer = username()
 
 
544
            timezone = local_time_offset()
 
 
546
        mutter("building commit log message")
 
 
547
        rev = Revision(timestamp=timestamp,
 
 
550
                       precursor = self.last_patch(),
 
 
555
        rev_tmp = tempfile.TemporaryFile()
 
 
556
        rev.write_xml(rev_tmp)
 
 
558
        self.revision_store.add(rev_tmp, rev_id)
 
 
559
        mutter("new revision_id is {%s}" % rev_id)
 
 
561
        ## XXX: Everything up to here can simply be orphaned if we abort
 
 
562
        ## the commit; it will leave junk files behind but that doesn't
 
 
565
        ## TODO: Read back the just-generated changeset, and make sure it
 
 
566
        ## applies and recreates the right state.
 
 
568
        ## TODO: Also calculate and store the inventory SHA1
 
 
569
        mutter("committing patch r%d" % (self.revno() + 1))
 
 
572
        self.append_revision(rev_id)
 
 
575
            note("commited r%d" % self.revno())
 
 
578
    def append_revision(self, revision_id):
 
 
579
        mutter("add {%s} to revision-history" % revision_id)
 
 
580
        rev_history = self.revision_history()
 
 
582
        tmprhname = self.controlfilename('revision-history.tmp')
 
 
583
        rhname = self.controlfilename('revision-history')
 
 
585
        f = file(tmprhname, 'wt')
 
 
586
        rev_history.append(revision_id)
 
 
587
        f.write('\n'.join(rev_history))
 
 
591
        if sys.platform == 'win32':
 
 
593
        os.rename(tmprhname, rhname)
 
 
597
    def get_revision(self, revision_id):
 
 
598
        """Return the Revision object for a named revision"""
 
 
599
        r = Revision.read_xml(self.revision_store[revision_id])
 
 
600
        assert r.revision_id == revision_id
 
 
604
    def get_inventory(self, inventory_id):
 
 
605
        """Get Inventory object by hash.
 
 
607
        TODO: Perhaps for this and similar methods, take a revision
 
 
608
               parameter which can be either an integer revno or a
 
 
610
        i = Inventory.read_xml(self.inventory_store[inventory_id])
 
 
614
    def get_revision_inventory(self, revision_id):
 
 
615
        """Return inventory of a past revision."""
 
 
616
        if revision_id == None:
 
 
619
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
 
622
    def revision_history(self):
 
 
623
        """Return sequence of revision hashes on to this branch.
 
 
625
        >>> ScratchBranch().revision_history()
 
 
628
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
 
 
632
        """Return current revision number for this branch.
 
 
634
        That is equivalent to the number of revisions committed to
 
 
637
        >>> b = ScratchBranch()
 
 
640
        >>> b.commit('no foo')
 
 
644
        return len(self.revision_history())
 
 
647
    def last_patch(self):
 
 
648
        """Return last patch hash, or None if no history.
 
 
650
        >>> ScratchBranch().last_patch() == None
 
 
653
        ph = self.revision_history()
 
 
660
    def lookup_revision(self, revno):
 
 
661
        """Return revision hash for revision number."""
 
 
666
            # list is 0-based; revisions are 1-based
 
 
667
            return self.revision_history()[revno-1]
 
 
669
            raise BzrError("no such revision %s" % revno)
 
 
672
    def revision_tree(self, revision_id):
 
 
673
        """Return Tree for a revision on this branch.
 
 
675
        `revision_id` may be None for the null revision, in which case
 
 
676
        an `EmptyTree` is returned."""
 
 
678
        if revision_id == None:
 
 
681
            inv = self.get_revision_inventory(revision_id)
 
 
682
            return RevisionTree(self.text_store, inv)
 
 
685
    def working_tree(self):
 
 
686
        """Return a `Tree` for the working copy."""
 
 
687
        return WorkingTree(self.base, self.read_working_inventory())
 
 
690
    def basis_tree(self):
 
 
691
        """Return `Tree` object for last revision.
 
 
693
        If there are no revisions yet, return an `EmptyTree`.
 
 
695
        >>> b = ScratchBranch(files=['foo'])
 
 
696
        >>> b.basis_tree().has_filename('foo')
 
 
698
        >>> b.working_tree().has_filename('foo')
 
 
701
        >>> b.commit('add foo')
 
 
702
        >>> b.basis_tree().has_filename('foo')
 
 
705
        r = self.last_patch()
 
 
709
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
 
713
    def write_log(self, show_timezone='original', verbose=False):
 
 
714
        """Write out human-readable log of commits to this branch
 
 
716
        utc -- If true, show dates in universal time, not local time."""
 
 
717
        ## TODO: Option to choose either original, utc or local timezone
 
 
720
        for p in self.revision_history():
 
 
722
            print 'revno:', revno
 
 
723
            ## TODO: Show hash if --id is given.
 
 
724
            ##print 'revision-hash:', p
 
 
725
            rev = self.get_revision(p)
 
 
726
            print 'committer:', rev.committer
 
 
727
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
 
 
730
            ## opportunistic consistency check, same as check_patch_chaining
 
 
731
            if rev.precursor != precursor:
 
 
732
                bailout("mismatched precursor!")
 
 
736
                print '  (no message)'
 
 
738
                for l in rev.message.split('\n'):
 
 
741
            if verbose == True and precursor != None:
 
 
742
                print 'changed files:'
 
 
743
                tree = self.revision_tree(p)
 
 
744
                prevtree = self.revision_tree(precursor)
 
 
746
                for file_state, fid, old_name, new_name, kind in \
 
 
747
                                        diff_trees(prevtree, tree, ):
 
 
748
                    if file_state == 'A' or file_state == 'M':
 
 
749
                        show_status(file_state, kind, new_name)
 
 
750
                    elif file_state == 'D':
 
 
751
                        show_status(file_state, kind, old_name)
 
 
752
                    elif file_state == 'R':
 
 
753
                        show_status(file_state, kind,
 
 
754
                            old_name + ' => ' + new_name)
 
 
760
    def rename_one(self, from_rel, to_rel):
 
 
763
        This can change the directory or the filename or both.
 
 
765
        tree = self.working_tree()
 
 
767
        if not tree.has_filename(from_rel):
 
 
768
            bailout("can't rename: old working file %r does not exist" % from_rel)
 
 
769
        if tree.has_filename(to_rel):
 
 
770
            bailout("can't rename: new working file %r already exists" % to_rel)
 
 
772
        file_id = inv.path2id(from_rel)
 
 
774
            bailout("can't rename: old name %r is not versioned" % from_rel)
 
 
776
        if inv.path2id(to_rel):
 
 
777
            bailout("can't rename: new name %r is already versioned" % to_rel)
 
 
779
        to_dir, to_tail = os.path.split(to_rel)
 
 
780
        to_dir_id = inv.path2id(to_dir)
 
 
781
        if to_dir_id == None and to_dir != '':
 
 
782
            bailout("can't determine destination directory id for %r" % to_dir)
 
 
784
        mutter("rename_one:")
 
 
785
        mutter("  file_id    {%s}" % file_id)
 
 
786
        mutter("  from_rel   %r" % from_rel)
 
 
787
        mutter("  to_rel     %r" % to_rel)
 
 
788
        mutter("  to_dir     %r" % to_dir)
 
 
789
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
 
791
        inv.rename(file_id, to_dir_id, to_tail)
 
 
793
        print "%s => %s" % (from_rel, to_rel)
 
 
795
        from_abs = self.abspath(from_rel)
 
 
796
        to_abs = self.abspath(to_rel)
 
 
798
            os.rename(from_abs, to_abs)
 
 
800
            bailout("failed to rename %r to %r: %s"
 
 
801
                    % (from_abs, to_abs, e[1]),
 
 
802
                    ["rename rolled back"])
 
 
804
        self._write_inventory(inv)
 
 
808
    def move(self, from_paths, to_name):
 
 
811
        to_name must exist as a versioned directory.
 
 
813
        If to_name exists and is a directory, the files are moved into
 
 
814
        it, keeping their old names.  If it is a directory, 
 
 
816
        Note that to_name is only the last component of the new name;
 
 
817
        this doesn't change the directory.
 
 
819
        ## TODO: Option to move IDs only
 
 
820
        assert not isinstance(from_paths, basestring)
 
 
821
        tree = self.working_tree()
 
 
823
        to_abs = self.abspath(to_name)
 
 
824
        if not isdir(to_abs):
 
 
825
            bailout("destination %r is not a directory" % to_abs)
 
 
826
        if not tree.has_filename(to_name):
 
 
827
            bailout("destination %r not in working directory" % to_abs)
 
 
828
        to_dir_id = inv.path2id(to_name)
 
 
829
        if to_dir_id == None and to_name != '':
 
 
830
            bailout("destination %r is not a versioned directory" % to_name)
 
 
831
        to_dir_ie = inv[to_dir_id]
 
 
832
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
 
833
            bailout("destination %r is not a directory" % to_abs)
 
 
835
        to_idpath = Set(inv.get_idpath(to_dir_id))
 
 
838
            if not tree.has_filename(f):
 
 
839
                bailout("%r does not exist in working tree" % f)
 
 
840
            f_id = inv.path2id(f)
 
 
842
                bailout("%r is not versioned" % f)
 
 
843
            name_tail = splitpath(f)[-1]
 
 
844
            dest_path = appendpath(to_name, name_tail)
 
 
845
            if tree.has_filename(dest_path):
 
 
846
                bailout("destination %r already exists" % dest_path)
 
 
847
            if f_id in to_idpath:
 
 
848
                bailout("can't move %r to a subdirectory of itself" % f)
 
 
850
        # OK, so there's a race here, it's possible that someone will
 
 
851
        # create a file in this interval and then the rename might be
 
 
852
        # left half-done.  But we should have caught most problems.
 
 
855
            name_tail = splitpath(f)[-1]
 
 
856
            dest_path = appendpath(to_name, name_tail)
 
 
857
            print "%s => %s" % (f, dest_path)
 
 
858
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
 
860
                os.rename(self.abspath(f), self.abspath(dest_path))
 
 
862
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
 
863
                        ["rename rolled back"])
 
 
865
        self._write_inventory(inv)
 
 
869
    def show_status(self, show_all=False):
 
 
870
        """Display single-line status for non-ignored working files.
 
 
872
        The list is show sorted in order by file name.
 
 
874
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
 
880
        >>> b.commit("add foo")
 
 
882
        >>> os.unlink(b.abspath('foo'))
 
 
886
        TODO: Get state for single files.
 
 
889
        # We have to build everything into a list first so that it can
 
 
890
        # sorted by name, incorporating all the different sources.
 
 
892
        # FIXME: Rather than getting things in random order and then sorting,
 
 
893
        # just step through in order.
 
 
895
        # Interesting case: the old ID for a file has been removed,
 
 
896
        # but a new file has been created under that name.
 
 
898
        old = self.basis_tree()
 
 
899
        new = self.working_tree()
 
 
901
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
 
 
903
                show_status(fs, kind,
 
 
904
                            oldname + ' => ' + newname)
 
 
905
            elif fs == 'A' or fs == 'M':
 
 
906
                show_status(fs, kind, newname)
 
 
908
                show_status(fs, kind, oldname)
 
 
911
                    show_status(fs, kind, newname)
 
 
914
                    show_status(fs, kind, newname)
 
 
916
                show_status(fs, kind, newname)
 
 
918
                bailout("weird file state %r" % ((fs, fid),))
 
 
922
class ScratchBranch(Branch):
 
 
923
    """Special test class: a branch that cleans up after itself.
 
 
925
    >>> b = ScratchBranch()
 
 
933
    def __init__(self, files=[], dirs=[]):
 
 
934
        """Make a test branch.
 
 
936
        This creates a temporary directory and runs init-tree in it.
 
 
938
        If any files are listed, they are created in the working copy.
 
 
940
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
 
942
            os.mkdir(self.abspath(d))
 
 
945
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
 
949
        """Destroy the test branch, removing the scratch directory."""
 
 
951
            shutil.rmtree(self.base)
 
 
953
            # Work around for shutil.rmtree failing on Windows when
 
 
954
            # readonly files are encountered
 
 
955
            for root, dirs, files in os.walk(self.base, topdown=False):
 
 
957
                    os.chmod(os.path.join(root, name), 0700)
 
 
958
            shutil.rmtree(self.base)
 
 
962
######################################################################
 
 
966
def is_control_file(filename):
 
 
967
    ## FIXME: better check
 
 
968
    filename = os.path.normpath(filename)
 
 
969
    while filename != '':
 
 
970
        head, tail = os.path.split(filename)
 
 
971
        ## mutter('check %r for control file' % ((head, tail), ))
 
 
972
        if tail == bzrlib.BZRDIR:
 
 
981
def _gen_revision_id(when):
 
 
982
    """Return new revision-id."""
 
 
983
    s = '%s-%s-' % (user_email(), compact_date(when))
 
 
984
    s += hexlify(rand_bytes(8))
 
 
988
def gen_file_id(name):
 
 
989
    """Return new file id.
 
 
991
    This should probably generate proper UUIDs, but for the moment we
 
 
992
    cope with just randomness because running uuidgen every time is
 
 
994
    idx = name.rfind('/')
 
 
996
        name = name[idx+1 : ]
 
 
997
    idx = name.rfind('\\')
 
 
999
        name = name[idx+1 : ]
 
 
1001
    name = name.lstrip('.')
 
 
1003
    s = hexlify(rand_bytes(8))
 
 
1004
    return '-'.join((name, compact_date(time.time()), s))