62
62
_deprecation_warning_done = False
65
class CommitBuilder(object):
66
"""Provides an interface to build up a commit.
68
This allows describing a tree to be committed without needing to
69
know the internals of the format of the repository.
72
# all clients should supply tree roots.
73
record_root_entry = True
74
# the default CommitBuilder does not manage trees whose root is versioned.
75
_versioned_root = False
77
def __init__(self, repository, parents, config, timestamp=None,
78
timezone=None, committer=None, revprops=None,
80
"""Initiate a CommitBuilder.
82
:param repository: Repository to commit to.
83
:param parents: Revision ids of the parents of the new revision.
84
:param config: Configuration to use.
85
:param timestamp: Optional timestamp recorded for commit.
86
:param timezone: Optional timezone for timestamp.
87
:param committer: Optional committer to set for commit.
88
:param revprops: Optional dictionary of revision properties.
89
:param revision_id: Optional revision id.
94
self._committer = self._config.username()
96
assert isinstance(committer, basestring), type(committer)
97
self._committer = committer
99
self.new_inventory = Inventory(None)
100
self._new_revision_id = osutils.safe_revision_id(revision_id)
101
self.parents = parents
102
self.repository = repository
105
if revprops is not None:
106
self._revprops.update(revprops)
108
if timestamp is None:
109
timestamp = time.time()
110
# Restrict resolution to 1ms
111
self._timestamp = round(timestamp, 3)
114
self._timezone = osutils.local_time_offset()
116
self._timezone = int(timezone)
118
self._generate_revision_if_needed()
119
self._repo_graph = repository.get_graph()
121
def commit(self, message):
122
"""Make the actual commit.
124
:return: The revision id of the recorded revision.
126
rev = _mod_revision.Revision(
127
timestamp=self._timestamp,
128
timezone=self._timezone,
129
committer=self._committer,
131
inventory_sha1=self.inv_sha1,
132
revision_id=self._new_revision_id,
133
properties=self._revprops)
134
rev.parent_ids = self.parents
135
self.repository.add_revision(self._new_revision_id, rev,
136
self.new_inventory, self._config)
137
self.repository.commit_write_group()
138
return self._new_revision_id
141
"""Abort the commit that is being built.
143
self.repository.abort_write_group()
145
def revision_tree(self):
146
"""Return the tree that was just committed.
148
After calling commit() this can be called to get a RevisionTree
149
representing the newly committed tree. This is preferred to
150
calling Repository.revision_tree() because that may require
151
deserializing the inventory, while we already have a copy in
154
return RevisionTree(self.repository, self.new_inventory,
155
self._new_revision_id)
157
def finish_inventory(self):
158
"""Tell the builder that the inventory is finished."""
159
if self.new_inventory.root is None:
160
symbol_versioning.warn('Root entry should be supplied to'
161
' record_entry_contents, as of bzr 0.10.',
162
DeprecationWarning, stacklevel=2)
163
self.new_inventory.add(InventoryDirectory(ROOT_ID, '', None))
164
self.new_inventory.revision_id = self._new_revision_id
165
self.inv_sha1 = self.repository.add_inventory(
166
self._new_revision_id,
171
def _gen_revision_id(self):
172
"""Return new revision-id."""
173
return generate_ids.gen_revision_id(self._config.username(),
176
def _generate_revision_if_needed(self):
177
"""Create a revision id if None was supplied.
179
If the repository can not support user-specified revision ids
180
they should override this function and raise CannotSetRevisionId
181
if _new_revision_id is not None.
183
:raises: CannotSetRevisionId
185
if self._new_revision_id is None:
186
self._new_revision_id = self._gen_revision_id()
187
self.random_revid = True
189
self.random_revid = False
191
def _check_root(self, ie, parent_invs, tree):
192
"""Helper for record_entry_contents.
194
:param ie: An entry being added.
195
:param parent_invs: The inventories of the parent revisions of the
197
:param tree: The tree that is being committed.
199
# In this revision format, root entries have no knit or weave When
200
# serializing out to disk and back in root.revision is always
202
ie.revision = self._new_revision_id
204
def _get_delta(self, ie, basis_inv, path):
205
"""Get a delta against the basis inventory for ie."""
206
if ie.file_id not in basis_inv:
208
return (None, path, ie.file_id, ie)
209
elif ie != basis_inv[ie.file_id]:
211
# TODO: avoid tis id2path call.
212
return (basis_inv.id2path(ie.file_id), path, ie.file_id, ie)
217
def record_entry_contents(self, ie, parent_invs, path, tree,
219
"""Record the content of ie from tree into the commit if needed.
221
Side effect: sets ie.revision when unchanged
223
:param ie: An inventory entry present in the commit.
224
:param parent_invs: The inventories of the parent revisions of the
226
:param path: The path the entry is at in the tree.
227
:param tree: The tree which contains this entry and should be used to
229
:param content_summary: Summary data from the tree about the paths
230
content - stat, length, exec, sha/link target. This is only
231
accessed when the entry has a revision of None - that is when it is
232
a candidate to commit.
233
:return: A tuple (change_delta, version_recorded). change_delta is
234
an inventory_delta change for this entry against the basis tree of
235
the commit, or None if no change occured against the basis tree.
236
version_recorded is True if a new version of the entry has been
237
recorded. For instance, committing a merge where a file was only
238
changed on the other side will return (delta, False).
240
if self.new_inventory.root is None:
241
if ie.parent_id is not None:
242
raise errors.RootMissing()
243
self._check_root(ie, parent_invs, tree)
244
if ie.revision is None:
245
kind = content_summary[0]
247
# ie is carried over from a prior commit
249
# XXX: repository specific check for nested tree support goes here - if
250
# the repo doesn't want nested trees we skip it ?
251
if (kind == 'tree-reference' and
252
not self.repository._format.supports_tree_reference):
253
# mismatch between commit builder logic and repository:
254
# this needs the entry creation pushed down into the builder.
255
raise NotImplementedError('Missing repository subtree support.')
256
# transitional assert only, will remove before release.
257
assert ie.kind == kind
258
self.new_inventory.add(ie)
260
# TODO: slow, take it out of the inner loop.
262
basis_inv = parent_invs[0]
264
basis_inv = Inventory(root_id=None)
266
# ie.revision is always None if the InventoryEntry is considered
267
# for committing. We may record the previous parents revision if the
268
# content is actually unchanged against a sole head.
269
if ie.revision is not None:
270
if self._versioned_root or path != '':
271
# not considered for commit
274
# repositories that do not version the root set the root's
275
# revision to the new commit even when no change occurs, and
276
# this masks when a change may have occurred against the basis,
277
# so calculate if one happened.
278
if ie.file_id not in basis_inv:
280
delta = (None, path, ie.file_id, ie)
282
basis_id = basis_inv[ie.file_id]
283
if basis_id.name != '':
285
delta = (basis_inv.id2path(ie.file_id), path,
290
# not considered for commit, OR, for non-rich-root
291
return delta, ie.revision == self._new_revision_id and (path != '' or
292
self._versioned_root)
294
# XXX: Friction: parent_candidates should return a list not a dict
295
# so that we don't have to walk the inventories again.
296
parent_candiate_entries = ie.parent_candidates(parent_invs)
297
head_set = self._repo_graph.heads(parent_candiate_entries.keys())
299
for inv in parent_invs:
300
if ie.file_id in inv:
301
old_rev = inv[ie.file_id].revision
302
if old_rev in head_set:
303
heads.append(inv[ie.file_id].revision)
304
head_set.remove(inv[ie.file_id].revision)
307
# now we check to see if we need to write a new record to the
309
# We write a new entry unless there is one head to the ancestors, and
310
# the kind-derived content is unchanged.
312
# Cheapest check first: no ancestors, or more the one head in the
313
# ancestors, we write a new node.
317
# There is a single head, look it up for comparison
318
parent_entry = parent_candiate_entries[heads[0]]
319
# if the non-content specific data has changed, we'll be writing a
321
if (parent_entry.parent_id != ie.parent_id or
322
parent_entry.name != ie.name):
324
# now we need to do content specific checks:
326
# if the kind changed the content obviously has
327
if kind != parent_entry.kind:
331
if (# if the file length changed we have to store:
332
parent_entry.text_size != content_summary[1] or
333
# if the exec bit has changed we have to store:
334
parent_entry.executable != content_summary[2]):
336
elif parent_entry.text_sha1 == content_summary[3]:
337
# all meta and content is unchanged (using a hash cache
338
# hit to check the sha)
339
ie.revision = parent_entry.revision
340
ie.text_size = parent_entry.text_size
341
ie.text_sha1 = parent_entry.text_sha1
342
ie.executable = parent_entry.executable
343
return self._get_delta(ie, basis_inv, path), False
345
# Either there is only a hash change(no hash cache entry,
346
# or same size content change), or there is no change on
348
# Provide the parent's hash to the store layer, so that the
349
# content is unchanged we will not store a new node.
350
nostore_sha = parent_entry.text_sha1
352
# We want to record a new node regardless of the presence or
353
# absence of a content change in the file.
355
ie.executable = content_summary[2]
356
lines = tree.get_file(ie.file_id, path).readlines()
358
ie.text_sha1, ie.text_size = self._add_text_to_weave(
359
ie.file_id, lines, heads, nostore_sha)
360
except errors.ExistingContent:
361
# Turns out that the file content was unchanged, and we were
362
# only going to store a new node if it was changed. Carry over
364
ie.revision = parent_entry.revision
365
ie.text_size = parent_entry.text_size
366
ie.text_sha1 = parent_entry.text_sha1
367
ie.executable = parent_entry.executable
368
return self._get_delta(ie, basis_inv, path), False
369
elif kind == 'directory':
371
# all data is meta here, nothing specific to directory, so
373
ie.revision = parent_entry.revision
374
return self._get_delta(ie, basis_inv, path), False
376
self._add_text_to_weave(ie.file_id, lines, heads, None)
377
elif kind == 'symlink':
378
current_link_target = content_summary[3]
380
# symlink target is not generic metadata, check if it has
382
if current_link_target != parent_entry.symlink_target:
385
# unchanged, carry over.
386
ie.revision = parent_entry.revision
387
ie.symlink_target = parent_entry.symlink_target
388
return self._get_delta(ie, basis_inv, path), False
389
ie.symlink_target = current_link_target
391
self._add_text_to_weave(ie.file_id, lines, heads, None)
392
elif kind == 'tree-reference':
394
if content_summary[3] != parent_entry.reference_revision:
397
# unchanged, carry over.
398
ie.reference_revision = parent_entry.reference_revision
399
ie.revision = parent_entry.revision
400
return self._get_delta(ie, basis_inv, path), False
401
ie.reference_revision = content_summary[3]
403
self._add_text_to_weave(ie.file_id, lines, heads, None)
405
raise NotImplementedError('unknown kind')
406
ie.revision = self._new_revision_id
407
return self._get_delta(ie, basis_inv, path), True
409
def _add_text_to_weave(self, file_id, new_lines, parents, nostore_sha):
410
versionedfile = self.repository.weave_store.get_weave_or_empty(
411
file_id, self.repository.get_transaction())
412
# Don't change this to add_lines - add_lines_with_ghosts is cheaper
413
# than add_lines, and allows committing when a parent is ghosted for
415
# Note: as we read the content directly from the tree, we know its not
416
# been turned into unicode or badly split - but a broken tree
417
# implementation could give us bad output from readlines() so this is
418
# not a guarantee of safety. What would be better is always checking
419
# the content during test suite execution. RBC 20070912
421
return versionedfile.add_lines_with_ghosts(
422
self._new_revision_id, parents, new_lines,
423
nostore_sha=nostore_sha, random_id=self.random_revid,
424
check_content=False)[0:2]
426
versionedfile.clear_cache()
429
class RootCommitBuilder(CommitBuilder):
430
"""This commitbuilder actually records the root id"""
432
# the root entry gets versioned properly by this builder.
433
_versioned_root = True
435
def _check_root(self, ie, parent_invs, tree):
436
"""Helper for record_entry_contents.
438
:param ie: An entry being added.
439
:param parent_invs: The inventories of the parent revisions of the
441
:param tree: The tree that is being committed.
65
445
######################################################################
2130
2488
self.pb.update(message, self.count, self.total)
2133
class CommitBuilder(object):
2134
"""Provides an interface to build up a commit.
2136
This allows describing a tree to be committed without needing to
2137
know the internals of the format of the repository.
2140
record_root_entry = False
2141
def __init__(self, repository, parents, config, timestamp=None,
2142
timezone=None, committer=None, revprops=None,
2144
"""Initiate a CommitBuilder.
2146
:param repository: Repository to commit to.
2147
:param parents: Revision ids of the parents of the new revision.
2148
:param config: Configuration to use.
2149
:param timestamp: Optional timestamp recorded for commit.
2150
:param timezone: Optional timezone for timestamp.
2151
:param committer: Optional committer to set for commit.
2152
:param revprops: Optional dictionary of revision properties.
2153
:param revision_id: Optional revision id.
2155
self._config = config
2157
if committer is None:
2158
self._committer = self._config.username()
2160
assert isinstance(committer, basestring), type(committer)
2161
self._committer = committer
2163
self.new_inventory = Inventory(None)
2164
self._new_revision_id = osutils.safe_revision_id(revision_id)
2165
self.parents = parents
2166
self.repository = repository
2169
if revprops is not None:
2170
self._revprops.update(revprops)
2172
if timestamp is None:
2173
timestamp = time.time()
2174
# Restrict resolution to 1ms
2175
self._timestamp = round(timestamp, 3)
2177
if timezone is None:
2178
self._timezone = osutils.local_time_offset()
2180
self._timezone = int(timezone)
2182
self._generate_revision_if_needed()
2184
def commit(self, message):
2185
"""Make the actual commit.
2187
:return: The revision id of the recorded revision.
2189
rev = _mod_revision.Revision(
2190
timestamp=self._timestamp,
2191
timezone=self._timezone,
2192
committer=self._committer,
2194
inventory_sha1=self.inv_sha1,
2195
revision_id=self._new_revision_id,
2196
properties=self._revprops)
2197
rev.parent_ids = self.parents
2198
self.repository.add_revision(self._new_revision_id, rev,
2199
self.new_inventory, self._config)
2200
self.repository.commit_write_group()
2201
return self._new_revision_id
2204
"""Abort the commit that is being built.
2206
self.repository.abort_write_group()
2208
def revision_tree(self):
2209
"""Return the tree that was just committed.
2211
After calling commit() this can be called to get a RevisionTree
2212
representing the newly committed tree. This is preferred to
2213
calling Repository.revision_tree() because that may require
2214
deserializing the inventory, while we already have a copy in
2217
return RevisionTree(self.repository, self.new_inventory,
2218
self._new_revision_id)
2220
def finish_inventory(self):
2221
"""Tell the builder that the inventory is finished."""
2222
if self.new_inventory.root is None:
2223
symbol_versioning.warn('Root entry should be supplied to'
2224
' record_entry_contents, as of bzr 0.10.',
2225
DeprecationWarning, stacklevel=2)
2226
self.new_inventory.add(InventoryDirectory(ROOT_ID, '', None))
2227
self.new_inventory.revision_id = self._new_revision_id
2228
self.inv_sha1 = self.repository.add_inventory(
2229
self._new_revision_id,
2234
def _gen_revision_id(self):
2235
"""Return new revision-id."""
2236
return generate_ids.gen_revision_id(self._config.username(),
2239
def _generate_revision_if_needed(self):
2240
"""Create a revision id if None was supplied.
2242
If the repository can not support user-specified revision ids
2243
they should override this function and raise CannotSetRevisionId
2244
if _new_revision_id is not None.
2246
:raises: CannotSetRevisionId
2248
if self._new_revision_id is None:
2249
self._new_revision_id = self._gen_revision_id()
2251
def record_entry_contents(self, ie, parent_invs, path, tree):
2252
"""Record the content of ie from tree into the commit if needed.
2254
Side effect: sets ie.revision when unchanged
2256
:param ie: An inventory entry present in the commit.
2257
:param parent_invs: The inventories of the parent revisions of the
2259
:param path: The path the entry is at in the tree.
2260
:param tree: The tree which contains this entry and should be used to
2263
if self.new_inventory.root is None and ie.parent_id is not None:
2264
symbol_versioning.warn('Root entry should be supplied to'
2265
' record_entry_contents, as of bzr 0.10.',
2266
DeprecationWarning, stacklevel=2)
2267
self.record_entry_contents(tree.inventory.root.copy(), parent_invs,
2269
self.new_inventory.add(ie)
2271
# ie.revision is always None if the InventoryEntry is considered
2272
# for committing. ie.snapshot will record the correct revision
2273
# which may be the sole parent if it is untouched.
2274
if ie.revision is not None:
2277
# In this revision format, root entries have no knit or weave
2278
if ie is self.new_inventory.root:
2279
# When serializing out to disk and back in
2280
# root.revision is always _new_revision_id
2281
ie.revision = self._new_revision_id
2283
previous_entries = ie.find_previous_heads(
2285
self.repository.weave_store,
2286
self.repository.get_transaction())
2287
# we are creating a new revision for ie in the history store
2289
ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
2291
def modified_directory(self, file_id, file_parents):
2292
"""Record the presence of a symbolic link.
2294
:param file_id: The file_id of the link to record.
2295
:param file_parents: The per-file parent revision ids.
2297
self._add_text_to_weave(file_id, [], file_parents.keys())
2299
def modified_reference(self, file_id, file_parents):
2300
"""Record the modification of a reference.
2302
:param file_id: The file_id of the link to record.
2303
:param file_parents: The per-file parent revision ids.
2305
self._add_text_to_weave(file_id, [], file_parents.keys())
2307
def modified_file_text(self, file_id, file_parents,
2308
get_content_byte_lines, text_sha1=None,
2310
"""Record the text of file file_id
2312
:param file_id: The file_id of the file to record the text of.
2313
:param file_parents: The per-file parent revision ids.
2314
:param get_content_byte_lines: A callable which will return the byte
2316
:param text_sha1: Optional SHA1 of the file contents.
2317
:param text_size: Optional size of the file contents.
2319
# mutter('storing text of file {%s} in revision {%s} into %r',
2320
# file_id, self._new_revision_id, self.repository.weave_store)
2321
# special case to avoid diffing on renames or
2323
if (len(file_parents) == 1
2324
and text_sha1 == file_parents.values()[0].text_sha1
2325
and text_size == file_parents.values()[0].text_size):
2326
previous_ie = file_parents.values()[0]
2327
versionedfile = self.repository.weave_store.get_weave(file_id,
2328
self.repository.get_transaction())
2329
versionedfile.clone_text(self._new_revision_id,
2330
previous_ie.revision, file_parents.keys())
2331
return text_sha1, text_size
2333
new_lines = get_content_byte_lines()
2334
# TODO: Rather than invoking sha_strings here, _add_text_to_weave
2335
# should return the SHA1 and size
2336
self._add_text_to_weave(file_id, new_lines, file_parents.keys())
2337
return osutils.sha_strings(new_lines), \
2338
sum(map(len, new_lines))
2340
def modified_link(self, file_id, file_parents, link_target):
2341
"""Record the presence of a symbolic link.
2343
:param file_id: The file_id of the link to record.
2344
:param file_parents: The per-file parent revision ids.
2345
:param link_target: Target location of this link.
2347
self._add_text_to_weave(file_id, [], file_parents.keys())
2349
def _add_text_to_weave(self, file_id, new_lines, parents):
2350
versionedfile = self.repository.weave_store.get_weave_or_empty(
2351
file_id, self.repository.get_transaction())
2352
versionedfile.add_lines(self._new_revision_id, parents, new_lines)
2353
versionedfile.clear_cache()
2356
class _CommitBuilder(CommitBuilder):
2357
"""Temporary class so old CommitBuilders are detected properly
2359
Note: CommitBuilder works whether or not root entry is recorded.
2362
record_root_entry = True
2365
class RootCommitBuilder(CommitBuilder):
2366
"""This commitbuilder actually records the root id"""
2368
record_root_entry = True
2370
def record_entry_contents(self, ie, parent_invs, path, tree):
2371
"""Record the content of ie from tree into the commit if needed.
2373
Side effect: sets ie.revision when unchanged
2375
:param ie: An inventory entry present in the commit.
2376
:param parent_invs: The inventories of the parent revisions of the
2378
:param path: The path the entry is at in the tree.
2379
:param tree: The tree which contains this entry and should be used to
2382
assert self.new_inventory.root is not None or ie.parent_id is None
2383
self.new_inventory.add(ie)
2385
# ie.revision is always None if the InventoryEntry is considered
2386
# for committing. ie.snapshot will record the correct revision
2387
# which may be the sole parent if it is untouched.
2388
if ie.revision is not None:
2391
previous_entries = ie.find_previous_heads(
2393
self.repository.weave_store,
2394
self.repository.get_transaction())
2395
# we are creating a new revision for ie in the history store
2397
ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
2400
2491
_unescape_map = {