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
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
22
## TODO: Read back the just-generated changeset, and make sure it
23
## applies and recreates the right state.
30
from binascii import hexlify
32
from bzrlib.osutils import (local_time_offset, username,
33
rand_bytes, compact_date, user_email,
34
kind_marker, is_inside_any, quotefn,
35
sha_string, sha_file, isdir, isfile)
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
40
from bzrlib.xml5 import serializer_v5
41
from bzrlib.inventory import Inventory
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
48
class NullCommitReporter(object):
49
"""I report on progress of a commit."""
50
def added(self, path):
53
def removed(self, path):
56
def renamed(self, old_path, new_path):
60
class ReportCommitToLog(NullCommitReporter):
61
def added(self, path):
62
note('added %s', path)
64
def removed(self, path):
65
note('removed %s', path)
67
def renamed(self, old_path, new_path):
68
note('renamed %s => %s', old_path, new_path)
72
"""Task of committing a new revision.
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.
79
Modified to hold a list of files that have been deleted from
80
the working directory; these should be removed from the
85
if reporter is not None:
86
self.reporter = reporter
88
self.reporter = NullCommitReporter()
98
allow_pointless=True):
99
"""Commit working copy as a new revision.
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.
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.
115
This raises PointlessCommit if there are no changes, no new merges,
116
and allow_pointless is false.
118
timestamp -- if not None, seconds-since-epoch for a
119
postdated/predated commit.
122
If true, commit only those files.
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.
133
self.branch.lock_write()
135
self.specific_files = specific_files
137
if timestamp is None:
138
self.timestamp = time.time()
140
self.timestamp = long(timestamp)
142
if committer is None:
143
self.committer = username(self.branch)
145
assert isinstance(committer, basestring), type(committer)
146
self.committer = committer
149
self.timezone = local_time_offset()
151
self.timezone = int(timezone)
153
assert isinstance(message, basestring), type(message)
154
self.message = message
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
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
170
self.pending_merges = self.branch.pending_merges()
172
if self.rev_id is None:
173
self.rev_id = _gen_revision_id(self.branch, time.time())
175
self.delta = compare_trees(self.basis_tree, self.work_tree,
176
specific_files=self.specific_files)
178
if not (self.delta.has_changed()
179
or self.allow_pointless
180
or self.pending_merges):
181
raise PointlessCommit()
183
self.new_inv = self.basis_inv.copy()
185
self.delta.show(sys.stdout)
187
self._remove_deleted()
190
self.branch._write_inventory(self.work_inv)
191
self._record_inventory()
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([])
201
def _record_inventory(self):
202
inv_tmp = tempfile.TemporaryFile()
203
serializer_v5.write_inventory(self.new_inv, inv_tmp)
205
self.inv_sha1 = sha_file(inv_tmp)
207
self.branch.inventory_store.add(inv_tmp, self.rev_id)
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)
219
self.rev.parents = []
220
precursor_id = self.branch.last_patch()
222
self.rev.parents.append(RevisionReference(precursor_id))
223
for merge_rev in self.pending_merges:
224
rev.parents.append(RevisionReference(merge_rev))
226
rev_tmp = tempfile.TemporaryFile()
227
serializer_v5.write_revision(self.rev, rev_tmp)
229
self.branch.revision_store.add(rev_tmp, self.rev_id)
230
mutter('new revision_id is {%s}', self.rev_id)
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):
242
def _store_texts(self):
243
"""Store new texts of modified/added files."""
244
for path, id, kind in self.delta.modified:
247
self._store_file_text(path, id)
249
for path, id, kind in self.delta.added:
252
self._store_file_text(path, id)
254
for old_path, new_path, id, kind, text_modified in self.delta.renamed:
257
if not text_modified:
259
self._store_file_text(path, id)
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'))
273
w.add(self.rev_id, [], new_lines)
274
af = AtomicFile(weave_fn)
276
write_weave_v5(w, af)
283
"""Build inventory preparatory to commit.
285
This adds any changed files into the text store, and sets their
286
test-id, sha and size in the returned inventory appropriately.
289
self.any_changes = False
290
self.new_inv = Inventory(self.work_inv.root.file_id)
291
self.missing_ids = []
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).
297
p = self.branch.abspath(path)
298
file_id = entry.file_id
299
mutter('commit prep file %s, id %r ' % (p, file_id))
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())
308
# omit this from committed inventory
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)
317
# this is present in the new inventory; may be new, modified or
319
old_ie = self.basis_inv.has_id(file_id) and self.basis_inv[file_id]
322
self.new_inv.add(entry)
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))
330
if entry.kind == 'directory':
332
raise BzrError("%s is entered as directory but not a directory"
334
elif entry.kind == 'file':
336
raise BzrError("%s is entered as file but is not a file" % quotefn(p))
338
new_sha1 = self.work_tree.get_file_sha1(file_id)
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}' %
349
content = file(p, 'rb').read()
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)
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)
360
marked = path + kind_marker(entry.kind)
362
self.reporter.added(marked)
363
self.any_changes = True
364
elif old_ie == entry:
366
elif (old_ie.name == entry.name
367
and old_ie.parent_id == entry.parent_id):
368
self.reporter.modified(marked)
369
self.any_changes = True
371
old_path = old_inv.id2path(file_id) + kind_marker(entry.kind)
372
self.reporter.renamed(old_path, marked)
373
self.any_changes = True
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))