~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: mbp at sourcefrog
  • Date: 2005-03-28 02:24:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050328022418-9d37f56361aa18e9
doc: more on ignore patterns

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
 
1
# Copyright (C) 2005 Canonical Ltd
3
2
 
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
29
28
from inventory import InventoryEntry, Inventory
30
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
31
30
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
32
 
     joinpath, sha_string, file_kind
 
31
     joinpath, sha_string, file_kind, local_time_offset
33
32
from store import ImmutableStore
34
33
from revision import Revision
35
34
from errors import bailout
41
40
 
42
41
 
43
42
 
 
43
def find_branch_root(f=None):
 
44
    """Find the branch root enclosing f, or pwd.
 
45
 
 
46
    It is not necessary that f exists.
 
47
 
 
48
    Basically we keep looking up until we find the control directory or
 
49
    run into the root."""
 
50
    if f is None:
 
51
        f = os.getcwd()
 
52
    elif hasattr(os.path, 'realpath'):
 
53
        f = os.path.realpath(f)
 
54
    else:
 
55
        f = os.path.abspath(f)
 
56
 
 
57
    orig_f = f
 
58
 
 
59
    last_f = f
 
60
    while True:
 
61
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
 
62
            return f
 
63
        head, tail = os.path.split(f)
 
64
        if head == f:
 
65
            # reached the root, whatever that may be
 
66
            bailout('%r is not in a branch' % orig_f)
 
67
        f = head
 
68
    
44
69
 
45
70
 
46
71
######################################################################
62
87
 
63
88
    :todo: mkdir() method.
64
89
    """
65
 
    def __init__(self, base, init=False):
 
90
    def __init__(self, base, init=False, find_root=True):
66
91
        """Create new branch object at a particular location.
67
92
 
68
93
        :param base: Base directory for the branch.
69
 
 
 
94
        
70
95
        :param init: If True, create new control files in a previously
71
96
             unversioned directory.  If False, the branch must already
72
97
             be versioned.
73
98
 
 
99
        :param find_root: If true and init is false, find the root of the
 
100
             existing branch containing base.
 
101
 
74
102
        In the test suite, creation of new trees is tested using the
75
103
        `ScratchBranch` class.
76
104
        """
77
 
        self.base = os.path.realpath(base)
78
105
        if init:
 
106
            self.base = os.path.realpath(base)
79
107
            self._make_control()
 
108
        elif find_root:
 
109
            self.base = find_branch_root(base)
80
110
        else:
 
111
            self.base = os.path.realpath(base)
81
112
            if not isdir(self.controlfilename('.')):
82
113
                bailout("not a bzr branch: %s" % quotefn(base),
83
114
                        ['use "bzr init" to initialize a new working tree',
84
115
                         'current bzr can only operate from top-of-tree'])
85
 
            self._check_format()
 
116
        self._check_format()
86
117
 
87
118
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
88
119
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
96
127
    __repr__ = __str__
97
128
 
98
129
 
99
 
    def _rel(self, name):
100
 
        """Return filename relative to branch top"""
 
130
    def abspath(self, name):
 
131
        """Return absolute filename for something in the branch"""
101
132
        return os.path.join(self.base, name)
102
 
        
 
133
 
 
134
 
 
135
    def relpath(self, path):
 
136
        """Return path relative to this branch of something inside it.
 
137
 
 
138
        Raises an error if path is not in this branch."""
 
139
        rp = os.path.realpath(path)
 
140
        # FIXME: windows
 
141
        if not rp.startswith(self.base):
 
142
            bailout("path %r is not within branch %r" % (rp, self.base))
 
143
        rp = rp[len(self.base):]
 
144
        rp = rp.lstrip(os.sep)
 
145
        return rp
 
146
 
103
147
 
104
148
    def controlfilename(self, file_or_path):
105
149
        """Return location relative to branch."""
159
203
        That is to say, the inventory describing changes underway, that
160
204
        will be committed to the next revision.
161
205
        """
162
 
        inv.write_xml(self.controlfile('inventory', 'w'))
163
 
        mutter('wrote inventory to %s' % quotefn(self.controlfilename('inventory')))
 
206
        ## TODO: factor out to atomicfile?  is rename safe on windows?
 
207
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
 
208
        tmpfname = self.controlfilename('inventory.tmp')
 
209
        tmpf = file(tmpfname, 'w')
 
210
        inv.write_xml(tmpf)
 
211
        tmpf.close()
 
212
        os.rename(tmpfname, self.controlfilename('inventory'))
 
213
        mutter('wrote working inventory')
164
214
 
165
215
 
166
216
    inventory = property(read_working_inventory, _write_inventory, None,
223
273
            if len(fp) == 0:
224
274
                bailout("cannot add top-level %r" % f)
225
275
                
226
 
            fullpath = os.path.normpath(self._rel(f))
227
 
 
228
 
            if isfile(fullpath):
229
 
                kind = 'file'
230
 
            elif isdir(fullpath):
231
 
                kind = 'directory'
232
 
            else:
233
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
234
 
 
235
 
            if len(fp) > 1:
236
 
                parent_name = joinpath(fp[:-1])
237
 
                mutter("lookup parent %r" % parent_name)
238
 
                parent_id = inv.path2id(parent_name)
239
 
                if parent_id == None:
240
 
                    bailout("cannot add: parent %r is not versioned"
241
 
                            % joinpath(fp[:-1]))
242
 
            else:
243
 
                parent_id = None
244
 
 
245
 
            file_id = _gen_file_id(fp[-1])
246
 
            inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id))
 
276
            fullpath = os.path.normpath(self.abspath(f))
 
277
 
 
278
            try:
 
279
                kind = file_kind(fullpath)
 
280
            except OSError:
 
281
                # maybe something better?
 
282
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
 
283
            
 
284
            if kind != 'file' and kind != 'directory':
 
285
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
 
286
 
 
287
            file_id = gen_file_id(f)
 
288
            inv.add_path(f, kind=kind, file_id=file_id)
 
289
 
247
290
            if verbose:
248
291
                show_status('A', kind, quotefn(f))
249
292
                
250
 
            mutter("add file %s file_id:{%s} kind=%r parent_id={%s}"
251
 
                   % (f, file_id, kind, parent_id))
 
293
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
294
            
252
295
        self._write_inventory(inv)
253
296
 
254
297
 
295
338
        if isinstance(files, types.StringTypes):
296
339
            files = [files]
297
340
        
298
 
        inv = self.read_working_inventory()
 
341
        tree = self.working_tree()
 
342
        inv = tree.inventory
299
343
 
300
344
        # do this before any modifications
301
345
        for f in files:
304
348
                bailout("cannot remove unversioned file %s" % quotefn(f))
305
349
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
306
350
            if verbose:
307
 
                show_status('D', inv[fid].kind, quotefn(f))
 
351
                # having remove it, it must be either ignored or unknown
 
352
                if tree.is_ignored(f):
 
353
                    new_status = 'I'
 
354
                else:
 
355
                    new_status = '?'
 
356
                show_status(new_status, inv[fid].kind, quotefn(f))
308
357
            del inv[fid]
309
358
 
310
359
        self._write_inventory(inv)
329
378
        return self.working_tree().unknowns()
330
379
 
331
380
 
332
 
    def commit(self, message, timestamp=None, committer=None,
 
381
    def commit(self, message, timestamp=None, timezone=None,
 
382
               committer=None,
333
383
               verbose=False):
334
384
        """Commit working copy as a new revision.
335
385
        
376
426
 
377
427
            entry = entry.copy()
378
428
 
379
 
            p = self._rel(path)
 
429
            p = self.abspath(path)
380
430
            file_id = entry.file_id
381
431
            mutter('commit prep file %s, id %r ' % (p, file_id))
382
432
 
422
472
                           entry.text_id)
423
473
                    
424
474
                else:
425
 
                    entry.text_id = _gen_file_id(entry.name)
 
475
                    entry.text_id = gen_file_id(entry.name)
426
476
                    self.text_store.add(content, entry.text_id)
427
477
                    mutter('    stored with text_id {%s}' % entry.text_id)
428
478
                    if verbose:
430
480
                            state = 'A'
431
481
                        elif (old_ie.name == entry.name
432
482
                              and old_ie.parent_id == entry.parent_id):
 
483
                            state = 'M'
 
484
                        else:
433
485
                            state = 'R'
434
 
                        else:
435
 
                            state = 'M'
436
486
 
437
487
                        show_status(state, entry.kind, quotefn(path))
438
488
 
463
513
        if committer == None:
464
514
            committer = username()
465
515
 
 
516
        if timezone == None:
 
517
            timezone = local_time_offset()
 
518
 
466
519
        mutter("building commit log message")
467
520
        rev = Revision(timestamp=timestamp,
 
521
                       timezone=timezone,
468
522
                       committer=committer,
469
523
                       precursor = self.last_patch(),
470
524
                       message = message,
488
542
        mutter("committing patch r%d" % (self.revno() + 1))
489
543
 
490
544
        mutter("append to revision-history")
491
 
        self.controlfile('revision-history', 'at').write(rev_id + '\n')
 
545
        f = self.controlfile('revision-history', 'at')
 
546
        f.write(rev_id + '\n')
 
547
        f.close()
492
548
 
493
 
        mutter("done!")
 
549
        if verbose:
 
550
            note("commited r%d" % self.revno())
494
551
 
495
552
 
496
553
    def get_revision(self, revision_id):
607
664
 
608
665
 
609
666
 
610
 
    def write_log(self, utc=False):
 
667
    def write_log(self, show_timezone='original'):
611
668
        """Write out human-readable log of commits to this branch
612
669
 
613
670
        :param utc: If true, show dates in universal time, not local time."""
 
671
        ## TODO: Option to choose either original, utc or local timezone
614
672
        revno = 1
615
673
        precursor = None
616
674
        for p in self.revision_history():
620
678
            ##print 'revision-hash:', p
621
679
            rev = self.get_revision(p)
622
680
            print 'committer:', rev.committer
623
 
            print 'timestamp: %s' % (format_date(rev.timestamp, utc))
 
681
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
 
682
                                                 show_timezone))
624
683
 
625
684
            ## opportunistic consistency check, same as check_patch_chaining
626
685
            if rev.precursor != precursor:
651
710
        A       foo
652
711
        >>> b.commit("add foo")
653
712
        >>> b.show_status()
 
713
        >>> os.unlink(b.abspath('foo'))
 
714
        >>> b.show_status()
 
715
        D       foo
 
716
        
654
717
 
655
718
        :todo: Get state for single files.
656
719
 
704
767
    >>> isdir(bd)
705
768
    False
706
769
    """
707
 
    def __init__(self, files = []):
 
770
    def __init__(self, files=[], dirs=[]):
708
771
        """Make a test branch.
709
772
 
710
773
        This creates a temporary directory and runs init-tree in it.
712
775
        If any files are listed, they are created in the working copy.
713
776
        """
714
777
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
778
        for d in dirs:
 
779
            os.mkdir(self.abspath(d))
 
780
            
715
781
        for f in files:
716
782
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
717
783
 
734
800
        ## mutter('check %r for control file' % ((head, tail), ))
735
801
        if tail == bzrlib.BZRDIR:
736
802
            return True
 
803
        if filename == head:
 
804
            break
737
805
        filename = head
738
806
    return False
739
807
 
746
814
    return s
747
815
 
748
816
 
749
 
def _gen_file_id(name):
 
817
def gen_file_id(name):
750
818
    """Return new file id.
751
819
 
752
820
    This should probably generate proper UUIDs, but for the moment we
753
821
    cope with just randomness because running uuidgen every time is
754
822
    slow."""
755
 
    assert '/' not in name
756
 
    while name[0] == '.':
757
 
        name = name[1:]
 
823
    idx = name.rfind('/')
 
824
    if idx != -1:
 
825
        name = name[idx+1 : ]
 
826
 
 
827
    name = name.lstrip('.')
 
828
 
758
829
    s = hexlify(rand_bytes(8))
759
830
    return '-'.join((name, compact_date(time.time()), s))
760
831