/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
485 by Martin Pool
- move commit code into its own module
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
1189 by Martin Pool
- BROKEN: partial support for commit into weave
18
## XXX: Everything up to here can simply be orphaned if we abort
19
## the commit; it will leave junk files behind but that doesn't
20
## matter.
21
22
## TODO: Read back the just-generated changeset, and make sure it
23
## applies and recreates the right state.
24
25
26
import os
27
import sys
1188 by Martin Pool
- clean up imports in commit code
28
import time
29
import tempfile
30
from binascii import hexlify
31
32
from bzrlib.osutils import (local_time_offset, username,
33
                            rand_bytes, compact_date, user_email,
34
                            kind_marker, is_inside_any, quotefn,
1189 by Martin Pool
- BROKEN: partial support for commit into weave
35
                            sha_string, sha_file, isdir, isfile)
1188 by Martin Pool
- clean up imports in commit code
36
from bzrlib.branch import gen_file_id
37
from bzrlib.errors import BzrError, PointlessCommit
38
from bzrlib.revision import Revision, RevisionReference
39
from bzrlib.trace import mutter, note
1189 by Martin Pool
- BROKEN: partial support for commit into weave
40
from bzrlib.xml5 import serializer_v5
1188 by Martin Pool
- clean up imports in commit code
41
from bzrlib.inventory import Inventory
1189 by Martin Pool
- BROKEN: partial support for commit into weave
42
from bzrlib.delta import compare_trees
43
from bzrlib.weave import Weave
44
from bzrlib.weavefile import read_weave, write_weave_v5
45
from bzrlib.atomicfile import AtomicFile
46
47
48
class NullCommitReporter(object):
49
    """I report on progress of a commit."""
50
    def added(self, path):
51
        pass
52
53
    def removed(self, path):
54
        pass
55
56
    def renamed(self, old_path, new_path):
57
        pass
58
59
60
class ReportCommitToLog(NullCommitReporter):
61
    def added(self, path):
62
        note('added %s', path)
63
64
    def removed(self, path):
65
        note('removed %s', path)
66
67
    def renamed(self, old_path, new_path):
68
        note('renamed %s => %s', old_path, new_path)
69
70
71
class Commit(object):
72
    """Task of committing a new revision.
73
74
    This is a MethodObject: it accumulates state as the commit is
75
    prepared, and then it is discarded.  It doesn't represent
76
    historical revisions, just the act of recording a new one.
77
78
            missing_ids
79
            Modified to hold a list of files that have been deleted from
80
            the working directory; these should be removed from the
81
            working inventory.
485 by Martin Pool
- move commit code into its own module
82
    """
1189 by Martin Pool
- BROKEN: partial support for commit into weave
83
    def __init__(self,
84
                 reporter=None):
85
        if reporter is not None:
86
            self.reporter = reporter
87
        else:
88
            self.reporter = NullCommitReporter()
89
90
        
91
    def commit(self,
92
               branch, message,
93
               timestamp=None,
94
               timezone=None,
95
               committer=None,
96
               specific_files=None,
97
               rev_id=None,
98
               allow_pointless=True):
99
        """Commit working copy as a new revision.
100
101
        The basic approach is to add all the file texts into the
102
        store, then the inventory, then make a new revision pointing
103
        to that inventory and store that.
104
105
        This is not quite safe if the working copy changes during the
106
        commit; for the moment that is simply not allowed.  A better
107
        approach is to make a temporary copy of the files before
108
        computing their hashes, and then add those hashes in turn to
109
        the inventory.  This should mean at least that there are no
110
        broken hash pointers.  There is no way we can get a snapshot
111
        of the whole directory at an instant.  This would also have to
112
        be robust against files disappearing, moving, etc.  So the
113
        whole thing is a bit hard.
114
115
        This raises PointlessCommit if there are no changes, no new merges,
116
        and allow_pointless  is false.
117
118
        timestamp -- if not None, seconds-since-epoch for a
119
             postdated/predated commit.
120
121
        specific_files
122
            If true, commit only those files.
123
124
        rev_id
125
            If set, use this as the new revision id.
126
            Useful for test or import commands that need to tightly
127
            control what revisions are assigned.  If you duplicate
128
            a revision id that exists elsewhere it is your own fault.
129
            If null (default), a time/random revision id is generated.
130
        """
131
132
        self.branch = branch
133
        self.branch.lock_write()
134
        self.rev_id = rev_id
135
        self.specific_files = specific_files
136
137
        if timestamp is None:
138
            self.timestamp = time.time()
139
        else:
140
            self.timestamp = long(timestamp)
141
            
142
        if committer is None:
143
            self.committer = username(self.branch)
144
        else:
145
            assert isinstance(committer, basestring), type(committer)
146
            self.committer = committer
147
148
        if timezone is None:
149
            self.timezone = local_time_offset()
150
        else:
151
            self.timezone = int(timezone)
152
153
        assert isinstance(message, basestring), type(message)
154
        self.message = message
155
156
        try:
157
            # First walk over the working inventory; and both update that
158
            # and also build a new revision inventory.  The revision
159
            # inventory needs to hold the text-id, sha1 and size of the
160
            # actual file versions committed in the revision.  (These are
161
            # not present in the working inventory.)  We also need to
162
            # detect missing/deleted files, and remove them from the
163
            # working inventory.
164
165
            self.work_tree = self.branch.working_tree()
166
            self.work_inv = self.work_tree.inventory
167
            self.basis_tree = self.branch.basis_tree()
168
            self.basis_inv = self.basis_tree.inventory
169
170
            self.pending_merges = self.branch.pending_merges()
171
172
            if self.rev_id is None:
173
                self.rev_id = _gen_revision_id(self.branch, time.time())
174
175
            self.delta = compare_trees(self.basis_tree, self.work_tree,
176
                                       specific_files=self.specific_files)
177
178
            if not (self.delta.has_changed()
179
                    or self.allow_pointless
180
                    or self.pending_merges):
181
                raise PointlessCommit()
182
183
            self.new_inv = self.basis_inv.copy()
184
185
            self.delta.show(sys.stdout)
186
187
            self._remove_deleted()
188
            self._store_texts()
189
190
            self.branch._write_inventory(self.work_inv)
191
            self._record_inventory()
192
193
            self._make_revision()
194
            note('committted r%d', (self.branch.revno() + 1))
195
            self.branch.append_revision(rev_id)
196
            self.branch.set_pending_merges([])
197
        finally:
198
            self.branch.unlock()
199
200
201
    def _record_inventory(self):
580 by Martin Pool
- Use explicit lock methods on a branch, rather than doing it
202
        inv_tmp = tempfile.TemporaryFile()
1189 by Martin Pool
- BROKEN: partial support for commit into weave
203
        serializer_v5.write_inventory(self.new_inv, inv_tmp)
204
        inv_tmp.seek(0)
205
        self.inv_sha1 = sha_file(inv_tmp)
206
        inv_tmp.seek(0)
207
        self.branch.inventory_store.add(inv_tmp, self.rev_id)
208
209
210
    def _make_revision(self):
211
        """Record a new revision object for this commit."""
212
        self.rev = Revision(timestamp=self.timestamp,
213
                            timezone=self.timezone,
214
                            committer=self.committer,
215
                            message=self.message,
216
                            inventory_sha1=self.inv_sha1,
217
                            revision_id=self.rev_id)
218
219
        self.rev.parents = []
220
        precursor_id = self.branch.last_patch()
717 by Martin Pool
- correctly set parent list when committing first
221
        if precursor_id:
1189 by Martin Pool
- BROKEN: partial support for commit into weave
222
            self.rev.parents.append(RevisionReference(precursor_id))
223
        for merge_rev in self.pending_merges:
224
            rev.parents.append(RevisionReference(merge_rev))
580 by Martin Pool
- Use explicit lock methods on a branch, rather than doing it
225
226
        rev_tmp = tempfile.TemporaryFile()
1189 by Martin Pool
- BROKEN: partial support for commit into weave
227
        serializer_v5.write_revision(self.rev, rev_tmp)
580 by Martin Pool
- Use explicit lock methods on a branch, rather than doing it
228
        rev_tmp.seek(0)
1189 by Martin Pool
- BROKEN: partial support for commit into weave
229
        self.branch.revision_store.add(rev_tmp, self.rev_id)
230
        mutter('new revision_id is {%s}', self.rev_id)
231
232
233
    def _remove_deleted(self):
234
        """Remove deleted files from the working and stored inventories."""
235
        for path, id, kind in self.delta.removed:
236
            if self.work_inv.has_id(id):
237
                del self.work_inv[id]
238
            if self.new_inv.has_id(id):
239
                del self.new_inv[id]
240
241
242
    def _store_texts(self):
243
        """Store new texts of modified/added files."""
244
        for path, id, kind in self.delta.modified:
245
            if kind != 'file':
246
                continue
247
            self._store_file_text(path, id)
248
249
        for path, id, kind in self.delta.added:
250
            if kind != 'file':
251
                continue
252
            self._store_file_text(path, id)
253
254
        for old_path, new_path, id, kind, text_modified in self.delta.renamed:
255
            if kind != 'file':
256
                continue
257
            if not text_modified:
258
                continue
259
            self._store_file_text(path, id)
260
261
262
    def _store_file_text(self, path, id):
263
        """Store updated text for one modified or added file."""
264
        # TODO: Add or update the inventory entry for this file;
265
        # put in the new text version
266
        note('store new text for {%s} in revision {%s}', id, self.rev_id)
267
        new_lines = self.work_tree.get_file(id).readlines()
268
        weave_fn = self.branch.controlfilename(['weaves', id+'.weave'])
269
        if os.path.exists(weave_fn):
270
            w = read_weave(file(weave_fn, 'rb'))
271
        else:
272
            w = Weave()
273
        w.add(self.rev_id, [], new_lines)
274
        af = AtomicFile(weave_fn)
275
        try:
276
            write_weave_v5(w, af)
277
            af.commit()
278
        finally:
279
            af.close()
280
281
282
    def _gather(self):
283
        """Build inventory preparatory to commit.
284
285
        This adds any changed files into the text store, and sets their
286
        test-id, sha and size in the returned inventory appropriately.
287
288
        """
289
        self.any_changes = False
290
        self.new_inv = Inventory(self.work_inv.root.file_id)
291
        self.missing_ids = []
292
293
        for path, entry in self.work_inv.iter_entries():
294
            ## TODO: Check that the file kind has not changed from the previous
295
            ## revision of this file (if any).
296
297
            p = self.branch.abspath(path)
298
            file_id = entry.file_id
299
            mutter('commit prep file %s, id %r ' % (p, file_id))
300
301
            if (self.specific_files
302
            and not is_inside_any(self.specific_files, path)):
303
                mutter('  skipping file excluded from commit')
304
                if self.basis_inv.has_id(file_id):
305
                    # carry over with previous state
306
                    self.new_inv.add(self.basis_inv[file_id].copy())
307
                else:
308
                    # omit this from committed inventory
309
                    pass
310
                continue
311
312
            if not self.work_tree.has_id(file_id):
313
                mutter("    file is missing, removing from inventory")
314
                self.missing_ids.append(file_id)
315
                continue
316
317
            # this is present in the new inventory; may be new, modified or
318
            # unchanged.
319
            old_ie = self.basis_inv.has_id(file_id) and self.basis_inv[file_id]
320
321
            entry = entry.copy()
322
            self.new_inv.add(entry)
323
324
            if old_ie:
325
                old_kind = old_ie.kind
326
                if old_kind != entry.kind:
327
                    raise BzrError("entry %r changed kind from %r to %r"
328
                            % (file_id, old_kind, entry.kind))
329
330
            if entry.kind == 'directory':
331
                if not isdir(p):
332
                    raise BzrError("%s is entered as directory but not a directory"
333
                                   % quotefn(p))
334
            elif entry.kind == 'file':
335
                if not isfile(p):
336
                    raise BzrError("%s is entered as file but is not a file" % quotefn(p))
337
338
                new_sha1 = self.work_tree.get_file_sha1(file_id)
339
340
                if (old_ie
341
                    and old_ie.text_sha1 == new_sha1):
342
                    ## assert content == basis.get_file(file_id).read()
343
                    entry.text_id = old_ie.text_id
344
                    entry.text_sha1 = new_sha1
345
                    entry.text_size = old_ie.text_size
346
                    mutter('    unchanged from previous text_id {%s}' %
347
                           entry.text_id)
348
                else:
349
                    content = file(p, 'rb').read()
350
351
                    # calculate the sha again, just in case the file contents
352
                    # changed since we updated the cache
353
                    entry.text_sha1 = sha_string(content)
354
                    entry.text_size = len(content)
355
356
                    entry.text_id = gen_file_id(entry.name)
357
                    self.branch.text_store.add(content, entry.text_id)
358
                    mutter('    stored with text_id {%s}' % entry.text_id)
359
633 by Martin Pool
- Show added/renamed/modified messages from commit for non-file
360
            marked = path + kind_marker(entry.kind)
361
            if not old_ie:
1189 by Martin Pool
- BROKEN: partial support for commit into weave
362
                self.reporter.added(marked)
363
                self.any_changes = True
633 by Martin Pool
- Show added/renamed/modified messages from commit for non-file
364
            elif old_ie == entry:
365
                pass                    # unchanged
366
            elif (old_ie.name == entry.name
367
                  and old_ie.parent_id == entry.parent_id):
1189 by Martin Pool
- BROKEN: partial support for commit into weave
368
                self.reporter.modified(marked)
369
                self.any_changes = True
633 by Martin Pool
- Show added/renamed/modified messages from commit for non-file
370
            else:
1189 by Martin Pool
- BROKEN: partial support for commit into weave
371
                old_path = old_inv.id2path(file_id) + kind_marker(entry.kind)
372
                self.reporter.renamed(old_path, marked)
373
                self.any_changes = True
374
375
376
377
def _gen_revision_id(branch, when):
378
    """Return new revision-id."""
379
    s = '%s-%s-' % (user_email(branch), compact_date(when))
380
    s += hexlify(rand_bytes(8))
381
    return s
633 by Martin Pool
- Show added/renamed/modified messages from commit for non-file
382
383