/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Martin Pool
  • Date: 2005-09-14 06:18:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050914061818-05a79652196cc758
- tests for deletion and removal of files in commits

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
## XXX: Can we do any better about making interrupted commits change
 
19
## nothing?
 
20
 
 
21
## XXX: If we merged two versions of a file then we still need to
 
22
## create a new version representing that merge, even if it didn't
 
23
## change from the parent.
 
24
 
 
25
## TODO: Read back the just-generated changeset, and make sure it
 
26
## applies and recreates the right state.
 
27
 
 
28
 
 
29
## This is not quite safe if the working copy changes during the
 
30
## commit; for the moment that is simply not allowed.  A better
 
31
## approach is to make a temporary copy of the files before
 
32
## computing their hashes, and then add those hashes in turn to
 
33
## the inventory.  This should mean at least that there are no
 
34
## broken hash pointers.  There is no way we can get a snapshot
 
35
## of the whole directory at an instant.  This would also have to
 
36
## be robust against files disappearing, moving, etc.  So the
 
37
## whole thing is a bit hard.
 
38
 
 
39
## The newly committed revision is going to have a shape corresponding
 
40
## to that of the working inventory.  Files that are not in the
 
41
## working tree and that were in the predecessor are reported as
 
42
## removed -- this can include files that were either removed from the
 
43
## inventory or deleted in the working tree.  If they were only
 
44
## deleted from disk, they are removed from the working inventory.
 
45
 
 
46
## We then consider the remaining entries, which will be in the new
 
47
## version.  Directory entries are simply copied across.  File entries
 
48
## must be checked to see if a new version of the file should be
 
49
## recorded.  For each parent revision inventory, we check to see what
 
50
## version of the file was present.  If the file was present in at
 
51
## least one tree, and if it was the same version in all the trees,
 
52
## then we can just refer to that version.  Otherwise, a new version
 
53
## representing the merger of the file versions must be added.
 
54
 
 
55
 
 
56
 
 
57
 
 
58
 
 
59
import os
 
60
import sys
 
61
import time
 
62
import tempfile
 
63
import sha
 
64
 
 
65
from binascii import hexlify
 
66
from cStringIO import StringIO
 
67
 
 
68
from bzrlib.osutils import (local_time_offset, username,
 
69
                            rand_bytes, compact_date, user_email,
 
70
                            kind_marker, is_inside_any, quotefn,
 
71
                            sha_string, sha_strings, sha_file, isdir, isfile)
 
72
from bzrlib.branch import gen_file_id, INVENTORY_FILEID, ANCESTRY_FILEID
 
73
from bzrlib.errors import BzrError, PointlessCommit
 
74
from bzrlib.revision import Revision, RevisionReference
 
75
from bzrlib.trace import mutter, note
 
76
from bzrlib.xml5 import serializer_v5
 
77
from bzrlib.inventory import Inventory
 
78
from bzrlib.delta import compare_trees
 
79
from bzrlib.weave import Weave
 
80
from bzrlib.weavefile import read_weave, write_weave_v5
 
81
from bzrlib.atomicfile import AtomicFile
 
82
 
 
83
 
 
84
def commit(*args, **kwargs):
 
85
    """Commit a new revision to a branch.
 
86
 
 
87
    Function-style interface for convenience of old callers.
 
88
 
 
89
    New code should use the Commit class instead.
 
90
    """
 
91
    Commit().commit(*args, **kwargs)
 
92
 
 
93
 
 
94
class NullCommitReporter(object):
 
95
    """I report on progress of a commit."""
 
96
    def added(self, path):
 
97
        pass
 
98
 
 
99
    def removed(self, path):
 
100
        pass
 
101
 
 
102
    def renamed(self, old_path, new_path):
 
103
        pass
 
104
 
 
105
 
 
106
class ReportCommitToLog(NullCommitReporter):
 
107
    def added(self, path):
 
108
        note('added %s', path)
 
109
 
 
110
    def removed(self, path):
 
111
        note('removed %s', path)
 
112
 
 
113
    def renamed(self, old_path, new_path):
 
114
        note('renamed %s => %s', old_path, new_path)
 
115
 
 
116
 
 
117
class Commit(object):
 
118
    """Task of committing a new revision.
 
119
 
 
120
    This is a MethodObject: it accumulates state as the commit is
 
121
    prepared, and then it is discarded.  It doesn't represent
 
122
    historical revisions, just the act of recording a new one.
 
123
 
 
124
            missing_ids
 
125
            Modified to hold a list of files that have been deleted from
 
126
            the working directory; these should be removed from the
 
127
            working inventory.
 
128
    """
 
129
    def __init__(self,
 
130
                 reporter=None):
 
131
        if reporter is not None:
 
132
            self.reporter = reporter
 
133
        else:
 
134
            self.reporter = NullCommitReporter()
 
135
 
 
136
        
 
137
    def commit(self,
 
138
               branch, message,
 
139
               timestamp=None,
 
140
               timezone=None,
 
141
               committer=None,
 
142
               specific_files=None,
 
143
               rev_id=None,
 
144
               allow_pointless=True):
 
145
        """Commit working copy as a new revision.
 
146
 
 
147
        The basic approach is to add all the file texts into the
 
148
        store, then the inventory, then make a new revision pointing
 
149
        to that inventory and store that.
 
150
 
 
151
        This raises PointlessCommit if there are no changes, no new merges,
 
152
        and allow_pointless  is false.
 
153
 
 
154
        timestamp -- if not None, seconds-since-epoch for a
 
155
             postdated/predated commit.
 
156
 
 
157
        specific_files
 
158
            If true, commit only those files.
 
159
 
 
160
        rev_id
 
161
            If set, use this as the new revision id.
 
162
            Useful for test or import commands that need to tightly
 
163
            control what revisions are assigned.  If you duplicate
 
164
            a revision id that exists elsewhere it is your own fault.
 
165
            If null (default), a time/random revision id is generated.
 
166
        """
 
167
 
 
168
        self.branch = branch
 
169
        self.rev_id = rev_id
 
170
        self.specific_files = specific_files
 
171
        self.allow_pointless = allow_pointless
 
172
 
 
173
        if timestamp is None:
 
174
            self.timestamp = time.time()
 
175
        else:
 
176
            self.timestamp = long(timestamp)
 
177
            
 
178
        if committer is None:
 
179
            self.committer = username(self.branch)
 
180
        else:
 
181
            assert isinstance(committer, basestring), type(committer)
 
182
            self.committer = committer
 
183
 
 
184
        if timezone is None:
 
185
            self.timezone = local_time_offset()
 
186
        else:
 
187
            self.timezone = int(timezone)
 
188
 
 
189
        assert isinstance(message, basestring), type(message)
 
190
        self.message = message
 
191
 
 
192
        self.branch.lock_write()
 
193
        try:
 
194
            # First walk over the working inventory; and both update that
 
195
            # and also build a new revision inventory.  The revision
 
196
            # inventory needs to hold the text-id, sha1 and size of the
 
197
            # actual file versions committed in the revision.  (These are
 
198
            # not present in the working inventory.)  We also need to
 
199
            # detect missing/deleted files, and remove them from the
 
200
            # working inventory.
 
201
 
 
202
            self.work_tree = self.branch.working_tree()
 
203
            self.work_inv = self.work_tree.inventory
 
204
            self.basis_tree = self.branch.basis_tree()
 
205
            self.basis_inv = self.basis_tree.inventory
 
206
 
 
207
            self._gather_parents()
 
208
 
 
209
            if self.rev_id is None:
 
210
                self.rev_id = _gen_revision_id(self.branch, time.time())
 
211
 
 
212
            self._remove_deletions()
 
213
 
 
214
            # TODO: update hashcache
 
215
            self.delta = compare_trees(self.basis_tree, self.work_tree,
 
216
                                       specific_files=self.specific_files)
 
217
 
 
218
            if not (self.delta.has_changed()
 
219
                    or self.allow_pointless
 
220
                    or len(self.parents) != 1):
 
221
                raise PointlessCommit()
 
222
 
 
223
            self.new_inv = self.basis_inv.copy()
 
224
 
 
225
            ## FIXME: Don't write to stdout!
 
226
            self.delta.show(sys.stdout)
 
227
 
 
228
            self._remove_deleted()
 
229
            self._store_files()
 
230
 
 
231
            self.branch._write_inventory(self.work_inv)
 
232
            self._record_inventory()
 
233
            self._record_ancestry()
 
234
 
 
235
            self._make_revision()
 
236
            note('committted r%d {%s}', (self.branch.revno() + 1),
 
237
                 self.rev_id)
 
238
            self.branch.append_revision(self.rev_id)
 
239
            self.branch.set_pending_merges([])
 
240
        finally:
 
241
            self.branch.unlock()
 
242
 
 
243
 
 
244
 
 
245
    def _remove_deletions(self):
 
246
        """Remove deleted files from the working inventory."""
 
247
        pass
 
248
 
 
249
 
 
250
 
 
251
    def _record_inventory(self):
 
252
        """Store the inventory for the new revision."""
 
253
        inv_tmp = StringIO()
 
254
        serializer_v5.write_inventory(self.new_inv, inv_tmp)
 
255
        inv_tmp.seek(0)
 
256
        self.inv_sha1 = sha_string(inv_tmp.getvalue())
 
257
        inv_lines = inv_tmp.readlines()
 
258
        self.branch.weave_store.add_text(INVENTORY_FILEID, self.rev_id,
 
259
                                         inv_lines, self.parents)
 
260
 
 
261
 
 
262
    def _record_ancestry(self):
 
263
        """Append merged revision ancestry to the ancestry file."""
 
264
        if len(self.parents) > 1:
 
265
            raise NotImplementedError("sorry, can't commit merges yet")
 
266
        w = self.branch.weave_store.get_weave_or_empty(ANCESTRY_FILEID)
 
267
        if self.parents:
 
268
            lines = w.get(w.lookup(self.parents[0]))
 
269
        else:
 
270
            lines = []
 
271
        lines.append(self.rev_id + '\n')
 
272
        parent_idxs = map(w.lookup, self.parents)
 
273
        w.add(self.rev_id, parent_idxs, lines)
 
274
        self.branch.weave_store.put_weave(ANCESTRY_FILEID, w)
 
275
 
 
276
 
 
277
    def _gather_parents(self):
 
278
        pending_merges = self.branch.pending_merges()
 
279
        if pending_merges:
 
280
            raise NotImplementedError("sorry, can't commit merges to the weave format yet")
 
281
        self.parents = []
 
282
        precursor_id = self.branch.last_revision()
 
283
        if precursor_id:
 
284
            self.parents.append(precursor_id)
 
285
        self.parents += pending_merges
 
286
 
 
287
 
 
288
    def _make_revision(self):
 
289
        """Record a new revision object for this commit."""
 
290
        self.rev = Revision(timestamp=self.timestamp,
 
291
                            timezone=self.timezone,
 
292
                            committer=self.committer,
 
293
                            message=self.message,
 
294
                            inventory_sha1=self.inv_sha1,
 
295
                            revision_id=self.rev_id)
 
296
        self.rev.parents = map(RevisionReference, self.parents)
 
297
        rev_tmp = tempfile.TemporaryFile()
 
298
        serializer_v5.write_revision(self.rev, rev_tmp)
 
299
        rev_tmp.seek(0)
 
300
        self.branch.revision_store.add(rev_tmp, self.rev_id)
 
301
        mutter('new revision_id is {%s}', self.rev_id)
 
302
 
 
303
 
 
304
    def _remove_deleted(self):
 
305
        """Remove deleted files from the working and stored inventories."""
 
306
        for path, id, kind in self.delta.removed:
 
307
            if self.work_inv.has_id(id):
 
308
                del self.work_inv[id]
 
309
            if self.new_inv.has_id(id):
 
310
                del self.new_inv[id]
 
311
 
 
312
 
 
313
 
 
314
    def _store_files(self):
 
315
        """Store new texts of modified/added files."""
 
316
        # We must make sure that directories are added before anything
 
317
        # inside them is added.  the files within the delta report are
 
318
        # sorted by path so we know the directory will come before its
 
319
        # contents. 
 
320
        for path, file_id, kind in self.delta.added:
 
321
            if kind != 'file':
 
322
                ie = self.work_inv[file_id].copy()
 
323
                self.new_inv.add(ie)
 
324
            else:
 
325
                self._store_file_text(file_id)
 
326
 
 
327
        for path, file_id, kind in self.delta.modified:
 
328
            if kind != 'file':
 
329
                continue
 
330
            self._store_file_text(file_id)
 
331
 
 
332
        for old_path, new_path, file_id, kind, text_modified in self.delta.renamed:
 
333
            if kind != 'file':
 
334
                continue
 
335
            if not text_modified:
 
336
                continue
 
337
            self._store_file_text(file_id)
 
338
 
 
339
 
 
340
    def _store_file_text(self, file_id):
 
341
        """Store updated text for one modified or added file."""
 
342
        note('store new text for {%s} in revision {%s}',
 
343
             file_id, self.rev_id)
 
344
        new_lines = self.work_tree.get_file(file_id).readlines()
 
345
        if file_id in self.new_inv:     # was in basis inventory
 
346
            ie = self.new_inv[file_id]
 
347
            assert ie.file_id == file_id
 
348
            assert file_id in self.basis_inv
 
349
            assert self.basis_inv[file_id].kind == 'file'
 
350
            old_version = self.basis_inv[file_id].text_version
 
351
            file_parents = [old_version]
 
352
        else:                           # new in this revision
 
353
            ie = self.work_inv[file_id].copy()
 
354
            self.new_inv.add(ie)
 
355
            assert file_id not in self.basis_inv
 
356
            file_parents = []
 
357
        assert ie.kind == 'file'
 
358
        self._add_text_to_weave(file_id, new_lines, file_parents)
 
359
        # make a new inventory entry for this file, using whatever
 
360
        # it had in the working copy, plus details on the new text
 
361
        ie.text_sha1 = sha_strings(new_lines)
 
362
        ie.text_size = sum(map(len, new_lines))
 
363
        ie.text_version = self.rev_id
 
364
        ie.entry_version = self.rev_id
 
365
 
 
366
 
 
367
    def _add_text_to_weave(self, file_id, new_lines, parents):
 
368
        if file_id.startswith('__'):
 
369
            raise ValueError('illegal file-id %r for text file' % file_id)
 
370
        self.branch.weave_store.add_text(file_id, self.rev_id, new_lines, parents)
 
371
 
 
372
 
 
373
def _gen_revision_id(branch, when):
 
374
    """Return new revision-id."""
 
375
    s = '%s-%s-' % (user_email(branch), compact_date(when))
 
376
    s += hexlify(rand_bytes(8))
 
377
    return s
 
378