15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
# FIXME: "bzr commit doc/format" commits doc/format.txt!
21
def commit(branch, message,
28
allow_pointless=True):
29
"""Commit working copy as a new revision.
31
The basic approach is to add all the file texts into the
32
store, then the inventory, then make a new revision pointing
33
to that inventory and store that.
35
This is not quite safe if the working copy changes during the
36
commit; for the moment that is simply not allowed. A better
37
approach is to make a temporary copy of the files before
38
computing their hashes, and then add those hashes in turn to
39
the inventory. This should mean at least that there are no
40
broken hash pointers. There is no way we can get a snapshot
41
of the whole directory at an instant. This would also have to
42
be robust against files disappearing, moving, etc. So the
43
whole thing is a bit hard.
45
This raises PointlessCommit if there are no changes, no new merges,
46
and allow_pointless is false.
48
timestamp -- if not None, seconds-since-epoch for a
49
postdated/predated commit.
52
If true, commit only those files.
55
If set, use this as the new revision id.
56
Useful for test or import commands that need to tightly
57
control what revisions are assigned. If you duplicate
58
a revision id that exists elsewhere it is your own fault.
59
If null (default), a time/random revision id is generated.
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
64
from bzrlib.osutils import local_time_offset, username
65
from bzrlib.branch import gen_file_id
66
from bzrlib.errors import BzrError, PointlessCommit
67
from bzrlib.revision import Revision, RevisionReference
68
from bzrlib.trace import mutter, note
69
from bzrlib.xml import pack_xml
74
# First walk over the working inventory; and both update that
75
# and also build a new revision inventory. The revision
76
# inventory needs to hold the text-id, sha1 and size of the
77
# actual file versions committed in the revision. (These are
78
# not present in the working inventory.) We also need to
79
# detect missing/deleted files, and remove them from the
82
work_tree = branch.working_tree()
83
work_inv = work_tree.inventory
84
basis = branch.basis_tree()
85
basis_inv = basis.inventory
88
note('looking for changes...')
90
pending_merges = branch.pending_merges()
92
missing_ids, new_inv, any_changes = \
93
_gather_commit(branch,
100
if not (any_changes or allow_pointless or pending_merges):
101
raise PointlessCommit()
103
for file_id in missing_ids:
104
# Any files that have been deleted are now removed from the
105
# working inventory. Files that were not selected for commit
106
# are left as they were in the working inventory and ommitted
107
# from the revision inventory.
109
# have to do this later so we don't mess up the iterator.
110
# since parents may be removed before their children we
113
# FIXME: There's probably a better way to do this; perhaps
114
# the workingtree should know how to filter itbranch.
115
if work_inv.has_id(file_id):
116
del work_inv[file_id]
120
rev_id = _gen_revision_id(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):
123
202
inv_tmp = tempfile.TemporaryFile()
124
pack_xml(new_inv, inv_tmp)
126
branch.inventory_store.add(inv_tmp, inv_id)
127
mutter('new inventory_id is {%s}' % inv_id)
129
# We could also just sha hash the inv_tmp file
130
# however, in the case that branch.inventory_store.add()
131
# ever actually does anything special
132
inv_sha1 = branch.get_inventory_sha1(inv_id)
134
branch._write_inventory(work_inv)
136
if timestamp == None:
137
timestamp = time.time()
139
if committer == None:
140
committer = username()
143
timezone = local_time_offset()
145
mutter("building commit log message")
146
rev = Revision(timestamp=timestamp,
151
inventory_sha1=inv_sha1,
155
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()
157
precursor_sha1 = branch.get_revision_sha1(precursor_id)
158
rev.parents.append(RevisionReference(precursor_id, precursor_sha1))
159
for merge_rev in pending_merges:
160
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))
162
226
rev_tmp = tempfile.TemporaryFile()
163
pack_xml(rev, rev_tmp)
227
serializer_v5.write_revision(self.rev, rev_tmp)
165
branch.revision_store.add(rev_tmp, rev_id)
166
mutter("new revision_id is {%s}" % rev_id)
168
## XXX: Everything up to here can simply be orphaned if we abort
169
## the commit; it will leave junk files behind but that doesn't
172
## TODO: Read back the just-generated changeset, and make sure it
173
## applies and recreates the right state.
175
## TODO: Also calculate and store the inventory SHA1
176
mutter("committing patch r%d" % (branch.revno() + 1))
178
branch.append_revision(rev_id)
180
branch.set_pending_merges([])
183
note("commited r%d" % branch.revno())
189
def _gen_revision_id(when):
190
"""Return new revision-id."""
191
from binascii import hexlify
192
from osutils import rand_bytes, compact_date, user_email
194
s = '%s-%s-' % (user_email(), compact_date(when))
195
s += hexlify(rand_bytes(8))
199
def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files,
201
"""Build inventory preparatory to commit.
203
Returns missing_ids, new_inv, any_changes.
205
This adds any changed files into the text store, and sets their
206
test-id, sha and size in the returned inventory appropriately.
209
Modified to hold a list of files that have been deleted from
210
the working directory; these should be removed from the
213
from bzrlib.inventory import Inventory
214
from bzrlib.osutils import isdir, isfile, sha_string, quotefn, \
215
local_time_offset, username, kind_marker, is_inside_any
217
from bzrlib.branch import gen_file_id
218
from bzrlib.errors import BzrError
219
from bzrlib.revision import Revision
220
from bzrlib.trace import mutter, note
223
inv = Inventory(work_inv.root.file_id)
226
for path, entry in work_inv.iter_entries():
227
## TODO: Check that the file kind has not changed from the previous
228
## revision of this file (if any).
230
p = branch.abspath(path)
231
file_id = entry.file_id
232
mutter('commit prep file %s, id %r ' % (p, file_id))
234
if specific_files and not is_inside_any(specific_files, path):
235
mutter(' skipping file excluded from commit')
236
if basis_inv.has_id(file_id):
237
# carry over with previous state
238
inv.add(basis_inv[file_id].copy())
240
# omit this from committed inventory
244
if not work_tree.has_id(file_id):
246
print('deleted %s%s' % (path, kind_marker(entry.kind)))
248
mutter(" file is missing, removing from inventory")
249
missing_ids.append(file_id)
252
# this is present in the new inventory; may be new, modified or
254
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
260
old_kind = old_ie.kind
261
if old_kind != entry.kind:
262
raise BzrError("entry %r changed kind from %r to %r"
263
% (file_id, old_kind, entry.kind))
265
if entry.kind == 'directory':
267
raise BzrError("%s is entered as directory but not a directory"
269
elif entry.kind == 'file':
271
raise BzrError("%s is entered as file but is not a file" % quotefn(p))
273
new_sha1 = work_tree.get_file_sha1(file_id)
276
and old_ie.text_sha1 == new_sha1):
277
## assert content == basis.get_file(file_id).read()
278
entry.text_id = old_ie.text_id
279
entry.text_sha1 = new_sha1
280
entry.text_size = old_ie.text_size
281
mutter(' unchanged from previous text_id {%s}' %
284
content = file(p, 'rb').read()
286
# calculate the sha again, just in case the file contents
287
# changed since we updated the cache
288
entry.text_sha1 = sha_string(content)
289
entry.text_size = len(content)
291
entry.text_id = gen_file_id(entry.name)
292
branch.text_store.add(content, entry.text_id)
293
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)
296
360
marked = path + kind_marker(entry.kind)
298
print 'added', marked
362
self.reporter.added(marked)
363
self.any_changes = True
300
364
elif old_ie == entry:
302
366
elif (old_ie.name == entry.name
303
367
and old_ie.parent_id == entry.parent_id):
304
print 'modified', marked
368
self.reporter.modified(marked)
369
self.any_changes = True
307
print 'renamed', marked
310
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))