~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Martin Pool
  • Date: 2005-09-07 09:51:05 UTC
  • Revision ID: mbp@sourcefrog.net-20050907095105-9699d69050ff0f9d
- better error display

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
 
 
30
 
 
31
import os
 
32
import sys
 
33
import time
 
34
import tempfile
 
35
import sha
 
36
 
 
37
from binascii import hexlify
 
38
from cStringIO import StringIO
 
39
 
 
40
from bzrlib.osutils import (local_time_offset, username,
 
41
                            rand_bytes, compact_date, user_email,
 
42
                            kind_marker, is_inside_any, quotefn,
 
43
                            sha_string, sha_file, isdir, isfile)
 
44
from bzrlib.branch import gen_file_id
 
45
from bzrlib.errors import BzrError, PointlessCommit
 
46
from bzrlib.revision import Revision, RevisionReference
 
47
from bzrlib.trace import mutter, note
 
48
from bzrlib.xml5 import serializer_v5
 
49
from bzrlib.inventory import Inventory
 
50
from bzrlib.delta import compare_trees
 
51
from bzrlib.weave import Weave
 
52
from bzrlib.weavefile import read_weave, write_weave_v5
 
53
from bzrlib.atomicfile import AtomicFile
 
54
 
 
55
 
 
56
class NullCommitReporter(object):
 
57
    """I report on progress of a commit."""
 
58
    def added(self, path):
 
59
        pass
 
60
 
 
61
    def removed(self, path):
 
62
        pass
 
63
 
 
64
    def renamed(self, old_path, new_path):
 
65
        pass
 
66
 
 
67
 
 
68
class ReportCommitToLog(NullCommitReporter):
 
69
    def added(self, path):
 
70
        note('added %s', path)
 
71
 
 
72
    def removed(self, path):
 
73
        note('removed %s', path)
 
74
 
 
75
    def renamed(self, old_path, new_path):
 
76
        note('renamed %s => %s', old_path, new_path)
 
77
 
 
78
 
 
79
class Commit(object):
 
80
    """Task of committing a new revision.
 
81
 
 
82
    This is a MethodObject: it accumulates state as the commit is
 
83
    prepared, and then it is discarded.  It doesn't represent
 
84
    historical revisions, just the act of recording a new one.
 
85
 
 
86
            missing_ids
 
87
            Modified to hold a list of files that have been deleted from
 
88
            the working directory; these should be removed from the
 
89
            working inventory.
 
90
    """
 
91
    def __init__(self,
 
92
                 reporter=None):
 
93
        if reporter is not None:
 
94
            self.reporter = reporter
 
95
        else:
 
96
            self.reporter = NullCommitReporter()
 
97
 
 
98
        
 
99
    def commit(self,
 
100
               branch, message,
 
101
               timestamp=None,
 
102
               timezone=None,
 
103
               committer=None,
 
104
               specific_files=None,
 
105
               rev_id=None,
 
106
               allow_pointless=True):
 
107
        """Commit working copy as a new revision.
 
108
 
 
109
        The basic approach is to add all the file texts into the
 
110
        store, then the inventory, then make a new revision pointing
 
111
        to that inventory and store that.
 
112
 
 
113
        This is not quite safe if the working copy changes during the
 
114
        commit; for the moment that is simply not allowed.  A better
 
115
        approach is to make a temporary copy of the files before
 
116
        computing their hashes, and then add those hashes in turn to
 
117
        the inventory.  This should mean at least that there are no
 
118
        broken hash pointers.  There is no way we can get a snapshot
 
119
        of the whole directory at an instant.  This would also have to
 
120
        be robust against files disappearing, moving, etc.  So the
 
121
        whole thing is a bit hard.
 
122
 
 
123
        This raises PointlessCommit if there are no changes, no new merges,
 
124
        and allow_pointless  is false.
 
125
 
 
126
        timestamp -- if not None, seconds-since-epoch for a
 
127
             postdated/predated commit.
 
128
 
 
129
        specific_files
 
130
            If true, commit only those files.
 
131
 
 
132
        rev_id
 
133
            If set, use this as the new revision id.
 
134
            Useful for test or import commands that need to tightly
 
135
            control what revisions are assigned.  If you duplicate
 
136
            a revision id that exists elsewhere it is your own fault.
 
137
            If null (default), a time/random revision id is generated.
 
138
        """
 
139
 
 
140
        self.branch = branch
 
141
        self.branch.lock_write()
 
142
        self.rev_id = rev_id
 
143
        self.specific_files = specific_files
 
144
        self.allow_pointless = allow_pointless
 
145
 
 
146
        if timestamp is None:
 
147
            self.timestamp = time.time()
 
148
        else:
 
149
            self.timestamp = long(timestamp)
 
150
            
 
151
        if committer is None:
 
152
            self.committer = username(self.branch)
 
153
        else:
 
154
            assert isinstance(committer, basestring), type(committer)
 
155
            self.committer = committer
 
156
 
 
157
        if timezone is None:
 
158
            self.timezone = local_time_offset()
 
159
        else:
 
160
            self.timezone = int(timezone)
 
161
 
 
162
        assert isinstance(message, basestring), type(message)
 
163
        self.message = message
 
164
 
 
165
        try:
 
166
            # First walk over the working inventory; and both update that
 
167
            # and also build a new revision inventory.  The revision
 
168
            # inventory needs to hold the text-id, sha1 and size of the
 
169
            # actual file versions committed in the revision.  (These are
 
170
            # not present in the working inventory.)  We also need to
 
171
            # detect missing/deleted files, and remove them from the
 
172
            # working inventory.
 
173
 
 
174
            self.work_tree = self.branch.working_tree()
 
175
            self.work_inv = self.work_tree.inventory
 
176
            self.basis_tree = self.branch.basis_tree()
 
177
            self.basis_inv = self.basis_tree.inventory
 
178
 
 
179
            self.pending_merges = self.branch.pending_merges()
 
180
 
 
181
            if self.rev_id is None:
 
182
                self.rev_id = _gen_revision_id(self.branch, time.time())
 
183
 
 
184
            # todo: update hashcache
 
185
            self.delta = compare_trees(self.basis_tree, self.work_tree,
 
186
                                       specific_files=self.specific_files)
 
187
 
 
188
            if not (self.delta.has_changed()
 
189
                    or self.allow_pointless
 
190
                    or self.pending_merges):
 
191
                raise PointlessCommit()
 
192
 
 
193
            self.new_inv = self.basis_inv.copy()
 
194
 
 
195
            self.delta.show(sys.stdout)
 
196
 
 
197
            self._remove_deleted()
 
198
            self._store_files()
 
199
 
 
200
            self.branch._write_inventory(self.work_inv)
 
201
            self._record_inventory()
 
202
 
 
203
            self._make_revision()
 
204
            note('committted r%d {%s}', (self.branch.revno() + 1),
 
205
                 self.rev_id)
 
206
            self.branch.append_revision(self.rev_id)
 
207
            self.branch.set_pending_merges([])
 
208
        finally:
 
209
            self.branch.unlock()
 
210
 
 
211
 
 
212
    def _record_inventory(self):
 
213
        inv_tmp = StringIO()
 
214
        serializer_v5.write_inventory(self.new_inv, inv_tmp)
 
215
        self.inv_sha1 = sha_string(inv_tmp.getvalue())
 
216
        inv_tmp.seek(0)
 
217
        self.branch.inventory_store.add(inv_tmp, self.rev_id)
 
218
 
 
219
 
 
220
    def _make_revision(self):
 
221
        """Record a new revision object for this commit."""
 
222
        self.rev = Revision(timestamp=self.timestamp,
 
223
                            timezone=self.timezone,
 
224
                            committer=self.committer,
 
225
                            message=self.message,
 
226
                            inventory_sha1=self.inv_sha1,
 
227
                            revision_id=self.rev_id)
 
228
 
 
229
        self.rev.parents = []
 
230
        precursor_id = self.branch.last_patch()
 
231
        if precursor_id:
 
232
            self.rev.parents.append(RevisionReference(precursor_id))
 
233
        for merge_rev in self.pending_merges:
 
234
            rev.parents.append(RevisionReference(merge_rev))
 
235
 
 
236
        rev_tmp = tempfile.TemporaryFile()
 
237
        serializer_v5.write_revision(self.rev, rev_tmp)
 
238
        rev_tmp.seek(0)
 
239
        self.branch.revision_store.add(rev_tmp, self.rev_id)
 
240
        mutter('new revision_id is {%s}', self.rev_id)
 
241
 
 
242
 
 
243
    def _remove_deleted(self):
 
244
        """Remove deleted files from the working and stored inventories."""
 
245
        for path, id, kind in self.delta.removed:
 
246
            if self.work_inv.has_id(id):
 
247
                del self.work_inv[id]
 
248
            if self.new_inv.has_id(id):
 
249
                del self.new_inv[id]
 
250
 
 
251
 
 
252
 
 
253
    def _store_files(self):
 
254
        """Store new texts of modified/added files."""
 
255
        for path, id, kind in self.delta.modified:
 
256
            if kind != 'file':
 
257
                continue
 
258
            self._store_file_text(id)
 
259
 
 
260
        for path, id, kind in self.delta.added:
 
261
            if kind != 'file':
 
262
                continue
 
263
            self._store_file_text(id)
 
264
 
 
265
        for old_path, new_path, id, kind, text_modified in self.delta.renamed:
 
266
            if kind != 'file':
 
267
                continue
 
268
            if not text_modified:
 
269
                continue
 
270
            self._store_file_text(id)
 
271
 
 
272
 
 
273
    def _store_file_text(self, file_id):
 
274
        """Store updated text for one modified or added file."""
 
275
        note('store new text for {%s} in revision {%s}', id, self.rev_id)
 
276
        new_lines = self.work_tree.get_file(file_id).readlines()
 
277
        self._add_text_to_weave(file_id, new_lines)
 
278
        # update or add an entry
 
279
        if file_id in self.new_inv:
 
280
            ie = self.new_inv[file_id]
 
281
            assert ie.file_id == file_id
 
282
        else:
 
283
            ie = self.work_inv[file_id].copy()
 
284
            self.new_inv.add(ie)
 
285
        assert ie.kind == 'file'
 
286
        # make a new inventory entry for this file, using whatever
 
287
        # it had in the working copy, plus details on the new text
 
288
        ie.text_sha1 = _sha_strings(new_lines)
 
289
        ie.text_size = sum(map(len, new_lines))
 
290
        ie.text_version = self.rev_id
 
291
        ie.entry_version = self.rev_id
 
292
 
 
293
 
 
294
    def _add_text_to_weave(self, file_id, new_lines):
 
295
        weave_fn = self.branch.controlfilename(['weaves', file_id+'.weave'])
 
296
        if os.path.exists(weave_fn):
 
297
            w = read_weave(file(weave_fn, 'rb'))
 
298
        else:
 
299
            w = Weave()
 
300
        # XXX: Should set the appropriate parents by looking for this file_id
 
301
        # in all revision parents
 
302
        w.add(self.rev_id, [], new_lines)
 
303
        af = AtomicFile(weave_fn)
 
304
        try:
 
305
            write_weave_v5(w, af)
 
306
            af.commit()
 
307
        finally:
 
308
            af.close()
 
309
 
 
310
 
 
311
def _gen_revision_id(branch, when):
 
312
    """Return new revision-id."""
 
313
    s = '%s-%s-' % (user_email(branch), compact_date(when))
 
314
    s += hexlify(rand_bytes(8))
 
315
    return s
 
316
 
 
317
 
 
318
def _sha_strings(strings):
 
319
    """Return the sha-1 of concatenation of strings"""
 
320
    s = sha.new()
 
321
    map(s.update, strings)
 
322
    return s.hexdigest()