22
32
from bzrlib.osutils import (local_time_offset, username,
23
33
rand_bytes, compact_date, user_email,
24
34
kind_marker, is_inside_any, quotefn,
25
sha_string, isdir, isfile)
35
sha_string, sha_file, isdir, isfile)
26
36
from bzrlib.branch import gen_file_id
27
37
from bzrlib.errors import BzrError, PointlessCommit
28
38
from bzrlib.revision import Revision, RevisionReference
29
39
from bzrlib.trace import mutter, note
30
from bzrlib.xml import serializer_v4
40
from bzrlib.xml5 import serializer_v5
31
41
from bzrlib.inventory import Inventory
34
def commit(branch, message,
41
allow_pointless=True):
42
"""Commit working copy as a new revision.
44
The basic approach is to add all the file texts into the
45
store, then the inventory, then make a new revision pointing
46
to that inventory and store that.
48
This is not quite safe if the working copy changes during the
49
commit; for the moment that is simply not allowed. A better
50
approach is to make a temporary copy of the files before
51
computing their hashes, and then add those hashes in turn to
52
the inventory. This should mean at least that there are no
53
broken hash pointers. There is no way we can get a snapshot
54
of the whole directory at an instant. This would also have to
55
be robust against files disappearing, moving, etc. So the
56
whole thing is a bit hard.
58
This raises PointlessCommit if there are no changes, no new merges,
59
and allow_pointless is false.
61
timestamp -- if not None, seconds-since-epoch for a
62
postdated/predated commit.
65
If true, commit only those files.
68
If set, use this as the new revision id.
69
Useful for test or import commands that need to tightly
70
control what revisions are assigned. If you duplicate
71
a revision id that exists elsewhere it is your own fault.
72
If null (default), a time/random revision id is generated.
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
78
# First walk over the working inventory; and both update that
79
# and also build a new revision inventory. The revision
80
# inventory needs to hold the text-id, sha1 and size of the
81
# actual file versions committed in the revision. (These are
82
# not present in the working inventory.) We also need to
83
# detect missing/deleted files, and remove them from the
86
work_tree = branch.working_tree()
87
work_inv = work_tree.inventory
88
basis = branch.basis_tree()
89
basis_inv = basis.inventory
92
# note('looking for changes...')
93
# print 'looking for changes...'
94
# disabled; should be done at a higher level
97
pending_merges = branch.pending_merges()
99
missing_ids, new_inv, any_changes = \
100
_gather_commit(branch,
107
if not (any_changes or allow_pointless or pending_merges):
108
raise PointlessCommit()
110
for file_id in missing_ids:
111
# Any files that have been deleted are now removed from the
112
# working inventory. Files that were not selected for commit
113
# are left as they were in the working inventory and ommitted
114
# from the revision inventory.
116
# have to do this later so we don't mess up the iterator.
117
# since parents may be removed before their children we
120
# FIXME: There's probably a better way to do this; perhaps
121
# the workingtree should know how to filter itbranch.
122
if work_inv.has_id(file_id):
123
del work_inv[file_id]
126
rev_id = _gen_revision_id(branch, time.time())
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):
129
202
inv_tmp = tempfile.TemporaryFile()
131
serializer_v4.write_inventory(new_inv, inv_tmp)
133
branch.inventory_store.add(inv_tmp, inv_id)
134
mutter('new inventory_id is {%s}' % inv_id)
136
# We could also just sha hash the inv_tmp file
137
# however, in the case that branch.inventory_store.add()
138
# ever actually does anything special
139
inv_sha1 = branch.get_inventory_sha1(inv_id)
141
branch._write_inventory(work_inv)
143
if timestamp == None:
144
timestamp = time.time()
146
if committer == None:
147
committer = username(branch)
150
timezone = local_time_offset()
152
mutter("building commit log message")
153
rev = Revision(timestamp=timestamp,
158
inventory_sha1=inv_sha1,
162
precursor_id = branch.last_patch()
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()
164
precursor_sha1 = branch.get_revision_sha1(precursor_id)
165
rev.parents.append(RevisionReference(precursor_id, precursor_sha1))
166
for merge_rev in pending_merges:
167
rev.parents.append(RevisionReference(merge_rev))
222
self.rev.parents.append(RevisionReference(precursor_id))
223
for merge_rev in self.pending_merges:
224
rev.parents.append(RevisionReference(merge_rev))
169
226
rev_tmp = tempfile.TemporaryFile()
170
serializer_v4.write_revision(rev, rev_tmp)
227
serializer_v5.write_revision(self.rev, rev_tmp)
172
branch.revision_store.add(rev_tmp, rev_id)
173
mutter("new revision_id is {%s}" % rev_id)
175
## XXX: Everything up to here can simply be orphaned if we abort
176
## the commit; it will leave junk files behind but that doesn't
179
## TODO: Read back the just-generated changeset, and make sure it
180
## applies and recreates the right state.
182
## TODO: Also calculate and store the inventory SHA1
183
mutter("committing patch r%d" % (branch.revno() + 1))
185
branch.append_revision(rev_id)
187
branch.set_pending_merges([])
190
# disabled; should go through logging
191
# note("commited r%d" % branch.revno())
192
# print ("commited r%d" % branch.revno())
199
def _gen_revision_id(branch, when):
200
"""Return new revision-id."""
201
s = '%s-%s-' % (user_email(branch), compact_date(when))
202
s += hexlify(rand_bytes(8))
206
def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files,
208
"""Build inventory preparatory to commit.
210
Returns missing_ids, new_inv, any_changes.
212
This adds any changed files into the text store, and sets their
213
test-id, sha and size in the returned inventory appropriately.
216
Modified to hold a list of files that have been deleted from
217
the working directory; these should be removed from the
221
inv = Inventory(work_inv.root.file_id)
224
for path, entry in work_inv.iter_entries():
225
## TODO: Check that the file kind has not changed from the previous
226
## revision of this file (if any).
228
p = branch.abspath(path)
229
file_id = entry.file_id
230
mutter('commit prep file %s, id %r ' % (p, file_id))
232
if specific_files and not is_inside_any(specific_files, path):
233
mutter(' skipping file excluded from commit')
234
if basis_inv.has_id(file_id):
235
# carry over with previous state
236
inv.add(basis_inv[file_id].copy())
238
# omit this from committed inventory
242
if not work_tree.has_id(file_id):
244
print('deleted %s%s' % (path, kind_marker(entry.kind)))
246
mutter(" file is missing, removing from inventory")
247
missing_ids.append(file_id)
250
# this is present in the new inventory; may be new, modified or
252
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
258
old_kind = old_ie.kind
259
if old_kind != entry.kind:
260
raise BzrError("entry %r changed kind from %r to %r"
261
% (file_id, old_kind, entry.kind))
263
if entry.kind == 'directory':
265
raise BzrError("%s is entered as directory but not a directory"
267
elif entry.kind == 'file':
269
raise BzrError("%s is entered as file but is not a file" % quotefn(p))
271
new_sha1 = work_tree.get_file_sha1(file_id)
274
and old_ie.text_sha1 == new_sha1):
275
## assert content == basis.get_file(file_id).read()
276
entry.text_id = old_ie.text_id
277
entry.text_sha1 = new_sha1
278
entry.text_size = old_ie.text_size
279
mutter(' unchanged from previous text_id {%s}' %
282
content = file(p, 'rb').read()
284
# calculate the sha again, just in case the file contents
285
# changed since we updated the cache
286
entry.text_sha1 = sha_string(content)
287
entry.text_size = len(content)
289
entry.text_id = gen_file_id(entry.name)
290
branch.text_store.add(content, entry.text_id)
291
mutter(' stored with text_id {%s}' % entry.text_id)
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)
294
360
marked = path + kind_marker(entry.kind)
296
print 'added', marked
362
self.reporter.added(marked)
363
self.any_changes = True
298
364
elif old_ie == entry:
300
366
elif (old_ie.name == entry.name
301
367
and old_ie.parent_id == entry.parent_id):
302
print 'modified', marked
368
self.reporter.modified(marked)
369
self.any_changes = True
305
print 'renamed', marked
308
return missing_ids, inv, any_changes
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))