~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Robert Collins
  • Date: 2007-09-12 06:33:40 UTC
  • mto: (2592.5.3 pack-repository)
  • mto: This revision was merged to the branch mainline in revision 2933.
  • Revision ID: robertc@robertcollins.net-20070912063340-rebmp08maq9lmiyl
Do not create many transient knit objects, saving 4% on commit.

Show diffs side-by-side

added added

removed removed

Lines of Context:
61
61
_deprecation_warning_done = False
62
62
 
63
63
 
 
64
class CommitBuilder(object):
 
65
    """Provides an interface to build up a commit.
 
66
 
 
67
    This allows describing a tree to be committed without needing to 
 
68
    know the internals of the format of the repository.
 
69
    """
 
70
    
 
71
    # all clients should supply tree roots.
 
72
    record_root_entry = True
 
73
 
 
74
    def __init__(self, repository, parents, config, timestamp=None, 
 
75
                 timezone=None, committer=None, revprops=None, 
 
76
                 revision_id=None):
 
77
        """Initiate a CommitBuilder.
 
78
 
 
79
        :param repository: Repository to commit to.
 
80
        :param parents: Revision ids of the parents of the new revision.
 
81
        :param config: Configuration to use.
 
82
        :param timestamp: Optional timestamp recorded for commit.
 
83
        :param timezone: Optional timezone for timestamp.
 
84
        :param committer: Optional committer to set for commit.
 
85
        :param revprops: Optional dictionary of revision properties.
 
86
        :param revision_id: Optional revision id.
 
87
        """
 
88
        self._config = config
 
89
 
 
90
        if committer is None:
 
91
            self._committer = self._config.username()
 
92
        else:
 
93
            assert isinstance(committer, basestring), type(committer)
 
94
            self._committer = committer
 
95
 
 
96
        self.new_inventory = Inventory(None)
 
97
        self._new_revision_id = osutils.safe_revision_id(revision_id)
 
98
        self.parents = parents
 
99
        self.repository = repository
 
100
 
 
101
        self._revprops = {}
 
102
        if revprops is not None:
 
103
            self._revprops.update(revprops)
 
104
 
 
105
        if timestamp is None:
 
106
            timestamp = time.time()
 
107
        # Restrict resolution to 1ms
 
108
        self._timestamp = round(timestamp, 3)
 
109
 
 
110
        if timezone is None:
 
111
            self._timezone = osutils.local_time_offset()
 
112
        else:
 
113
            self._timezone = int(timezone)
 
114
 
 
115
        self._generate_revision_if_needed()
 
116
 
 
117
    def commit(self, message):
 
118
        """Make the actual commit.
 
119
 
 
120
        :return: The revision id of the recorded revision.
 
121
        """
 
122
        rev = _mod_revision.Revision(
 
123
                       timestamp=self._timestamp,
 
124
                       timezone=self._timezone,
 
125
                       committer=self._committer,
 
126
                       message=message,
 
127
                       inventory_sha1=self.inv_sha1,
 
128
                       revision_id=self._new_revision_id,
 
129
                       properties=self._revprops)
 
130
        rev.parent_ids = self.parents
 
131
        self.repository.add_revision(self._new_revision_id, rev,
 
132
            self.new_inventory, self._config)
 
133
        self.repository.commit_write_group()
 
134
        return self._new_revision_id
 
135
 
 
136
    def abort(self):
 
137
        """Abort the commit that is being built.
 
138
        """
 
139
        self.repository.abort_write_group()
 
140
 
 
141
    def revision_tree(self):
 
142
        """Return the tree that was just committed.
 
143
 
 
144
        After calling commit() this can be called to get a RevisionTree
 
145
        representing the newly committed tree. This is preferred to
 
146
        calling Repository.revision_tree() because that may require
 
147
        deserializing the inventory, while we already have a copy in
 
148
        memory.
 
149
        """
 
150
        return RevisionTree(self.repository, self.new_inventory,
 
151
                            self._new_revision_id)
 
152
 
 
153
    def finish_inventory(self):
 
154
        """Tell the builder that the inventory is finished."""
 
155
        if self.new_inventory.root is None:
 
156
            symbol_versioning.warn('Root entry should be supplied to'
 
157
                ' record_entry_contents, as of bzr 0.10.',
 
158
                 DeprecationWarning, stacklevel=2)
 
159
            self.new_inventory.add(InventoryDirectory(ROOT_ID, '', None))
 
160
        self.new_inventory.revision_id = self._new_revision_id
 
161
        self.inv_sha1 = self.repository.add_inventory(
 
162
            self._new_revision_id,
 
163
            self.new_inventory,
 
164
            self.parents
 
165
            )
 
166
 
 
167
    def _gen_revision_id(self):
 
168
        """Return new revision-id."""
 
169
        return generate_ids.gen_revision_id(self._config.username(),
 
170
                                            self._timestamp)
 
171
 
 
172
    def _generate_revision_if_needed(self):
 
173
        """Create a revision id if None was supplied.
 
174
        
 
175
        If the repository can not support user-specified revision ids
 
176
        they should override this function and raise CannotSetRevisionId
 
177
        if _new_revision_id is not None.
 
178
 
 
179
        :raises: CannotSetRevisionId
 
180
        """
 
181
        if self._new_revision_id is None:
 
182
            self._new_revision_id = self._gen_revision_id()
 
183
            self.random_revid = True
 
184
        else:
 
185
            self.random_revid = False
 
186
 
 
187
    def _check_root(self, ie, parent_invs, tree):
 
188
        """Helper for record_entry_contents.
 
189
 
 
190
        :param ie: An entry being added.
 
191
        :param parent_invs: The inventories of the parent revisions of the
 
192
            commit.
 
193
        :param tree: The tree that is being committed.
 
194
        """
 
195
        if ie.parent_id is not None:
 
196
            # if ie is not root, add a root automatically.
 
197
            symbol_versioning.warn('Root entry should be supplied to'
 
198
                ' record_entry_contents, as of bzr 0.10.',
 
199
                 DeprecationWarning, stacklevel=2)
 
200
            self.record_entry_contents(tree.inventory.root.copy(), parent_invs,
 
201
                                       '', tree, tree.path_content_summary(''))
 
202
        else:
 
203
            # In this revision format, root entries have no knit or weave When
 
204
            # serializing out to disk and back in root.revision is always
 
205
            # _new_revision_id
 
206
            ie.revision = self._new_revision_id
 
207
 
 
208
    def record_entry_contents(self, ie, parent_invs, path, tree,
 
209
        content_summary):
 
210
        """Record the content of ie from tree into the commit if needed.
 
211
 
 
212
        Side effect: sets ie.revision when unchanged
 
213
 
 
214
        :param ie: An inventory entry present in the commit.
 
215
        :param parent_invs: The inventories of the parent revisions of the
 
216
            commit.
 
217
        :param path: The path the entry is at in the tree.
 
218
        :param tree: The tree which contains this entry and should be used to 
 
219
            obtain content.
 
220
        :param content_summary: Summary data from the tree about the paths
 
221
            content - stat, length, exec, sha/link target. This is only
 
222
            accessed when the entry has a revision of None - that is when it is
 
223
            a candidate to commit.
 
224
        """
 
225
        if self.new_inventory.root is None:
 
226
            self._check_root(ie, parent_invs, tree)
 
227
        if ie.revision is None:
 
228
            kind = content_summary[0]
 
229
        else:
 
230
            # ie is carried over from a prior commit
 
231
            kind = ie.kind
 
232
        # XXX: repository specific check for nested tree support goes here - if
 
233
        # the repo doesn't want nested trees we skip it ?
 
234
        if (kind == 'tree-reference' and
 
235
            not self.repository._format.supports_tree_reference):
 
236
            # mismatch between commit builder logic and repository:
 
237
            # this needs the entry creation pushed down into the builder.
 
238
            raise NotImplementedError
 
239
        # transitional assert only, will remove before release.
 
240
        assert ie.kind == kind
 
241
        self.new_inventory.add(ie)
 
242
 
 
243
        # ie.revision is always None if the InventoryEntry is considered
 
244
        # for committing. ie.snapshot will record the correct revision 
 
245
        # which may be the sole parent if it is untouched.
 
246
        if ie.revision is not None:
 
247
            return
 
248
 
 
249
        # XXX: Friction: parent_candidates should return a list not a dict
 
250
        #      so that we don't have to walk the inventories again.
 
251
        parent_candiate_entries = ie.parent_candidates(parent_invs)
 
252
        head_set = self.repository.get_graph().heads(parent_candiate_entries.keys())
 
253
        heads = []
 
254
        for inv in parent_invs:
 
255
            if ie.file_id in inv:
 
256
                old_rev = inv[ie.file_id].revision
 
257
                if old_rev in head_set:
 
258
                    heads.append(inv[ie.file_id].revision)
 
259
                    head_set.remove(inv[ie.file_id].revision)
 
260
 
 
261
        store = False
 
262
        # now we check to see if we need to write a new record to the
 
263
        # file-graph.
 
264
        # We write a new entry unless there is one head to the ancestors, and
 
265
        # the kind-derived content is unchanged.
 
266
 
 
267
        # Cheapest check first: no ancestors, or more the one head in the
 
268
        # ancestors, we write a new node.
 
269
        if len(heads) != 1:
 
270
            store = True
 
271
        if not store:
 
272
            # There is a single head, look it up for comparison
 
273
            parent_entry = parent_candiate_entries[heads[0]]
 
274
            # if the non-content specific data has changed, we'll be writing a
 
275
            # node:
 
276
            if (parent_entry.parent_id != ie.parent_id or
 
277
                parent_entry.name != ie.name):
 
278
                store = True
 
279
        # now we need to do content specific checks:
 
280
        if not store:
 
281
            # if the kind changed the content obviously has
 
282
            if kind != parent_entry.kind:
 
283
                store = True
 
284
        if kind == 'file':
 
285
            if not store:
 
286
                if (# if the file length changed we have to store:
 
287
                    parent_entry.text_size != content_summary[1] or
 
288
                    # if the exec bit has changed we have to store:
 
289
                    parent_entry.executable != content_summary[2]):
 
290
                    store = True
 
291
                elif parent_entry.text_sha1 == content_summary[3]:
 
292
                    # all meta and content is unchanged (using a hash cache
 
293
                    # hit to check the sha)
 
294
                    ie.revision = parent_entry.revision
 
295
                    ie.text_size = parent_entry.text_size
 
296
                    ie.text_sha1 = parent_entry.text_sha1
 
297
                    ie.executable = parent_entry.executable
 
298
                    return
 
299
                else:
 
300
                    # Either there is only a hash change(no hash cache entry,
 
301
                    # or same size content change), or there is no change on
 
302
                    # this file at all.
 
303
                    # There is a race condition when inserting content into the
 
304
                    # knit though that can result in different content being
 
305
                    # inserted so even though we may have had a hash cache hit
 
306
                    # here we still tell the store the hash we would *not*
 
307
                    # store a new text on, which means that it can avoid for us
 
308
                    # without a race condition and without double-shaing the
 
309
                    # lines.
 
310
                    nostore_sha = parent_entry.text_sha1
 
311
            if store:
 
312
                nostore_sha = None
 
313
            try:
 
314
                ie.executable = content_summary[2]
 
315
                lines = tree.get_file(ie.file_id, path).readlines()
 
316
                ie.text_sha1, ie.text_size = self._add_text_to_weave(
 
317
                    ie.file_id, lines, heads, nostore_sha)
 
318
            except errors.ExistingContent:
 
319
                # we are not going to store a new file graph node as it turns
 
320
                # out to be unchanged.
 
321
                ie.revision = parent_entry.revision
 
322
                ie.text_size = parent_entry.text_size
 
323
                ie.text_sha1 = parent_entry.text_sha1
 
324
                ie.executable = parent_entry.executable
 
325
                return
 
326
        elif kind == 'directory':
 
327
            if not store:
 
328
                # all data is meta here, nothing specific to directory, so
 
329
                # carry over:
 
330
                ie.revision = parent_entry.revision
 
331
                return
 
332
            lines = []
 
333
            self._add_text_to_weave(ie.file_id, lines, heads, None)
 
334
        elif kind == 'symlink':
 
335
            current_link_target = content_summary[3]
 
336
            if not store:
 
337
                # symmlink target is not generic metadata, check if it has
 
338
                # changed.
 
339
                if current_link_target != parent_entry.symlink_target:
 
340
                    store = True
 
341
            if not store:
 
342
                # unchanged, carry over.
 
343
                ie.revision = parent_entry.revision
 
344
                ie.symlink_target = parent_entry.symlink_target
 
345
                return
 
346
            ie.symlink_target = current_link_target
 
347
            lines = []
 
348
            self._add_text_to_weave(ie.file_id, lines, heads, None)
 
349
        elif kind == 'tree-reference':
 
350
            if not store:
 
351
                if content_summary[3] != parent_entry.reference_revision:
 
352
                    store = True
 
353
            if not store:
 
354
                # unchanged, carry over.
 
355
                ie.reference_revision = parent_entry.reference_revision
 
356
                ie.revision = parent_entry.revision
 
357
                return
 
358
            ie.reference_revision = content_summary[3]
 
359
            lines = []
 
360
            self._add_text_to_weave(ie.file_id, lines, heads, None)
 
361
        else:
 
362
            raise NotImplementedError('unknown kind')
 
363
        ie.revision = self._new_revision_id
 
364
 
 
365
    def _add_text_to_weave(self, file_id, new_lines, parents, nostore_sha):
 
366
        versionedfile = self.repository.weave_store.get_weave_or_empty(
 
367
            file_id, self.repository.get_transaction())
 
368
        # Don't change this to add_lines - add_lines_with_ghosts is cheaper
 
369
        # than add_lines, and allows committing when a parent is ghosted for
 
370
        # some reason.
 
371
        try:
 
372
            return versionedfile.add_lines_with_ghosts(
 
373
                self._new_revision_id, parents, new_lines,
 
374
                nostore_sha=nostore_sha, random_id=self.random_revid)[0:2]
 
375
        finally:
 
376
            versionedfile.clear_cache()
 
377
 
 
378
 
 
379
class RootCommitBuilder(CommitBuilder):
 
380
    """This commitbuilder actually records the root id"""
 
381
    
 
382
    def _check_root(self, ie, parent_invs, tree):
 
383
        """Helper for record_entry_contents.
 
384
 
 
385
        :param ie: An entry being added.
 
386
        :param parent_invs: The inventories of the parent revisions of the
 
387
            commit.
 
388
        :param tree: The tree that is being committed.
 
389
        """
 
390
        # ie must be root for this builder
 
391
        assert ie.parent_id is None
 
392
 
 
393
 
64
394
######################################################################
65
395
# Repositories
66
396
 
76
406
    remote) disk.
77
407
    """
78
408
 
 
409
    # What class to use for a CommitBuilder. Often its simpler to change this
 
410
    # in a Repository class subclass rather than to override
 
411
    # get_commit_builder.
 
412
    _commit_builder_class = CommitBuilder
 
413
    # The search regex used by xml based repositories to determine what things
 
414
    # where changed in a single commit.
79
415
    _file_ids_altered_regex = lazy_regex.lazy_compile(
80
416
        r'file_id="(?P<file_id>[^"]+)"'
81
417
        r'.* revision="(?P<revision_id>[^"]+)"'
476
812
        :param revision_id: Optional revision id.
477
813
        """
478
814
        revision_id = osutils.safe_revision_id(revision_id)
479
 
        result = CommitBuilder(self, parents, config, timestamp, timezone,
480
 
                              committer, revprops, revision_id)
 
815
        result = self.__class__._commit_builder_class(self, parents, config,
 
816
            timestamp, timezone, committer, revprops, revision_id)
481
817
        self.start_write_group()
482
818
        return result
483
819
 
2254
2590
        self.pb.update(message, self.count, self.total)
2255
2591
 
2256
2592
 
2257
 
class CommitBuilder(object):
2258
 
    """Provides an interface to build up a commit.
2259
 
 
2260
 
    This allows describing a tree to be committed without needing to 
2261
 
    know the internals of the format of the repository.
2262
 
    """
2263
 
    
2264
 
    # all clients should supply tree roots.
2265
 
    record_root_entry = True
2266
 
 
2267
 
    def __init__(self, repository, parents, config, timestamp=None, 
2268
 
                 timezone=None, committer=None, revprops=None, 
2269
 
                 revision_id=None):
2270
 
        """Initiate a CommitBuilder.
2271
 
 
2272
 
        :param repository: Repository to commit to.
2273
 
        :param parents: Revision ids of the parents of the new revision.
2274
 
        :param config: Configuration to use.
2275
 
        :param timestamp: Optional timestamp recorded for commit.
2276
 
        :param timezone: Optional timezone for timestamp.
2277
 
        :param committer: Optional committer to set for commit.
2278
 
        :param revprops: Optional dictionary of revision properties.
2279
 
        :param revision_id: Optional revision id.
2280
 
        """
2281
 
        self._config = config
2282
 
 
2283
 
        if committer is None:
2284
 
            self._committer = self._config.username()
2285
 
        else:
2286
 
            assert isinstance(committer, basestring), type(committer)
2287
 
            self._committer = committer
2288
 
 
2289
 
        self.new_inventory = Inventory(None)
2290
 
        self._new_revision_id = osutils.safe_revision_id(revision_id)
2291
 
        self.parents = parents
2292
 
        self.repository = repository
2293
 
 
2294
 
        self._revprops = {}
2295
 
        if revprops is not None:
2296
 
            self._revprops.update(revprops)
2297
 
 
2298
 
        if timestamp is None:
2299
 
            timestamp = time.time()
2300
 
        # Restrict resolution to 1ms
2301
 
        self._timestamp = round(timestamp, 3)
2302
 
 
2303
 
        if timezone is None:
2304
 
            self._timezone = osutils.local_time_offset()
2305
 
        else:
2306
 
            self._timezone = int(timezone)
2307
 
 
2308
 
        self._generate_revision_if_needed()
2309
 
 
2310
 
    def commit(self, message):
2311
 
        """Make the actual commit.
2312
 
 
2313
 
        :return: The revision id of the recorded revision.
2314
 
        """
2315
 
        rev = _mod_revision.Revision(
2316
 
                       timestamp=self._timestamp,
2317
 
                       timezone=self._timezone,
2318
 
                       committer=self._committer,
2319
 
                       message=message,
2320
 
                       inventory_sha1=self.inv_sha1,
2321
 
                       revision_id=self._new_revision_id,
2322
 
                       properties=self._revprops)
2323
 
        rev.parent_ids = self.parents
2324
 
        self.repository.add_revision(self._new_revision_id, rev,
2325
 
            self.new_inventory, self._config)
2326
 
        self.repository.commit_write_group()
2327
 
        return self._new_revision_id
2328
 
 
2329
 
    def abort(self):
2330
 
        """Abort the commit that is being built.
2331
 
        """
2332
 
        self.repository.abort_write_group()
2333
 
 
2334
 
    def revision_tree(self):
2335
 
        """Return the tree that was just committed.
2336
 
 
2337
 
        After calling commit() this can be called to get a RevisionTree
2338
 
        representing the newly committed tree. This is preferred to
2339
 
        calling Repository.revision_tree() because that may require
2340
 
        deserializing the inventory, while we already have a copy in
2341
 
        memory.
2342
 
        """
2343
 
        return RevisionTree(self.repository, self.new_inventory,
2344
 
                            self._new_revision_id)
2345
 
 
2346
 
    def finish_inventory(self):
2347
 
        """Tell the builder that the inventory is finished."""
2348
 
        if self.new_inventory.root is None:
2349
 
            symbol_versioning.warn('Root entry should be supplied to'
2350
 
                ' record_entry_contents, as of bzr 0.10.',
2351
 
                 DeprecationWarning, stacklevel=2)
2352
 
            self.new_inventory.add(InventoryDirectory(ROOT_ID, '', None))
2353
 
        self.new_inventory.revision_id = self._new_revision_id
2354
 
        self.inv_sha1 = self.repository.add_inventory(
2355
 
            self._new_revision_id,
2356
 
            self.new_inventory,
2357
 
            self.parents
2358
 
            )
2359
 
 
2360
 
    def _gen_revision_id(self):
2361
 
        """Return new revision-id."""
2362
 
        return generate_ids.gen_revision_id(self._config.username(),
2363
 
                                            self._timestamp)
2364
 
 
2365
 
    def _generate_revision_if_needed(self):
2366
 
        """Create a revision id if None was supplied.
2367
 
        
2368
 
        If the repository can not support user-specified revision ids
2369
 
        they should override this function and raise CannotSetRevisionId
2370
 
        if _new_revision_id is not None.
2371
 
 
2372
 
        :raises: CannotSetRevisionId
2373
 
        """
2374
 
        if self._new_revision_id is None:
2375
 
            self._new_revision_id = self._gen_revision_id()
2376
 
            self.random_revid = True
2377
 
        else:
2378
 
            self.random_revid = False
2379
 
 
2380
 
    def _check_root(self, ie, parent_invs, tree):
2381
 
        """Helper for record_entry_contents.
2382
 
 
2383
 
        :param ie: An entry being added.
2384
 
        :param parent_invs: The inventories of the parent revisions of the
2385
 
            commit.
2386
 
        :param tree: The tree that is being committed.
2387
 
        """
2388
 
        if ie.parent_id is not None:
2389
 
            # if ie is not root, add a root automatically.
2390
 
            symbol_versioning.warn('Root entry should be supplied to'
2391
 
                ' record_entry_contents, as of bzr 0.10.',
2392
 
                 DeprecationWarning, stacklevel=2)
2393
 
            self.record_entry_contents(tree.inventory.root.copy(), parent_invs,
2394
 
                                       '', tree, tree.path_content_summary(''))
2395
 
        else:
2396
 
            # In this revision format, root entries have no knit or weave When
2397
 
            # serializing out to disk and back in root.revision is always
2398
 
            # _new_revision_id
2399
 
            ie.revision = self._new_revision_id
2400
 
 
2401
 
    def record_entry_contents(self, ie, parent_invs, path, tree,
2402
 
        content_summary):
2403
 
        """Record the content of ie from tree into the commit if needed.
2404
 
 
2405
 
        Side effect: sets ie.revision when unchanged
2406
 
 
2407
 
        :param ie: An inventory entry present in the commit.
2408
 
        :param parent_invs: The inventories of the parent revisions of the
2409
 
            commit.
2410
 
        :param path: The path the entry is at in the tree.
2411
 
        :param tree: The tree which contains this entry and should be used to 
2412
 
            obtain content.
2413
 
        :param content_summary: Summary data from the tree about the paths
2414
 
            content - stat, length, exec, sha/link target. This is only
2415
 
            accessed when the entry has a revision of None - that is when it is
2416
 
            a candidate to commit.
2417
 
        """
2418
 
        if self.new_inventory.root is None:
2419
 
            self._check_root(ie, parent_invs, tree)
2420
 
        if ie.revision is None:
2421
 
            kind = content_summary[0]
2422
 
        else:
2423
 
            # ie is carried over from a prior commit
2424
 
            kind = ie.kind
2425
 
        # XXX: repository specific check for nested tree support goes here - if
2426
 
        # the repo doesn't want nested trees we skip it ?
2427
 
        if (kind == 'tree-reference' and
2428
 
            not self.repository._format.supports_tree_reference):
2429
 
            # mismatch between commit builder logic and repository:
2430
 
            # this needs the entry creation pushed down into the builder.
2431
 
            raise NotImplementedError
2432
 
        # transitional assert only, will remove before release.
2433
 
        assert ie.kind == kind
2434
 
        self.new_inventory.add(ie)
2435
 
 
2436
 
        # ie.revision is always None if the InventoryEntry is considered
2437
 
        # for committing. ie.snapshot will record the correct revision 
2438
 
        # which may be the sole parent if it is untouched.
2439
 
        if ie.revision is not None:
2440
 
            return
2441
 
 
2442
 
        # XXX: Friction: parent_candidates should return a list not a dict
2443
 
        #      so that we don't have to walk the inventories again.
2444
 
        parent_candiate_entries = ie.parent_candidates(parent_invs)
2445
 
        head_set = self.repository.get_graph().heads(parent_candiate_entries.keys())
2446
 
        heads = []
2447
 
        for inv in parent_invs:
2448
 
            if ie.file_id in inv:
2449
 
                old_rev = inv[ie.file_id].revision
2450
 
                if old_rev in head_set:
2451
 
                    heads.append(inv[ie.file_id].revision)
2452
 
                    head_set.remove(inv[ie.file_id].revision)
2453
 
 
2454
 
        store = False
2455
 
        # now we check to see if we need to write a new record to the
2456
 
        # file-graph.
2457
 
        # We write a new entry unless there is one head to the ancestors, and
2458
 
        # the kind-derived content is unchanged.
2459
 
 
2460
 
        # Cheapest check first: no ancestors, or more the one head in the
2461
 
        # ancestors, we write a new node.
2462
 
        if len(heads) != 1:
2463
 
            store = True
2464
 
        if not store:
2465
 
            # There is a single head, look it up for comparison
2466
 
            parent_entry = parent_candiate_entries[heads[0]]
2467
 
            # if the non-content specific data has changed, we'll be writing a
2468
 
            # node:
2469
 
            if (parent_entry.parent_id != ie.parent_id or
2470
 
                parent_entry.name != ie.name):
2471
 
                store = True
2472
 
        # now we need to do content specific checks:
2473
 
        if not store:
2474
 
            # if the kind changed the content obviously has
2475
 
            if kind != parent_entry.kind:
2476
 
                store = True
2477
 
        if kind == 'file':
2478
 
            if not store:
2479
 
                if (# if the file length changed we have to store:
2480
 
                    parent_entry.text_size != content_summary[1] or
2481
 
                    # if the exec bit has changed we have to store:
2482
 
                    parent_entry.executable != content_summary[2]):
2483
 
                    store = True
2484
 
                elif parent_entry.text_sha1 == content_summary[3]:
2485
 
                    # all meta and content is unchanged (using a hash cache
2486
 
                    # hit to check the sha)
2487
 
                    ie.revision = parent_entry.revision
2488
 
                    ie.text_size = parent_entry.text_size
2489
 
                    ie.text_sha1 = parent_entry.text_sha1
2490
 
                    ie.executable = parent_entry.executable
2491
 
                    return
2492
 
                else:
2493
 
                    # Either there is only a hash change(no hash cache entry,
2494
 
                    # or same size content change), or there is no change on
2495
 
                    # this file at all.
2496
 
                    # There is a race condition when inserting content into the
2497
 
                    # knit though that can result in different content being
2498
 
                    # inserted so even though we may have had a hash cache hit
2499
 
                    # here we still tell the store the hash we would *not*
2500
 
                    # store a new text on, which means that it can avoid for us
2501
 
                    # without a race condition and without double-shaing the
2502
 
                    # lines.
2503
 
                    nostore_sha = parent_entry.text_sha1
2504
 
            if store:
2505
 
                nostore_sha = None
2506
 
            try:
2507
 
                ie.executable = content_summary[2]
2508
 
                lines = tree.get_file(ie.file_id, path).readlines()
2509
 
                ie.text_sha1, ie.text_size = self._add_text_to_weave(
2510
 
                    ie.file_id, lines, heads, nostore_sha)
2511
 
            except errors.ExistingContent:
2512
 
                # we are not going to store a new file graph node as it turns
2513
 
                # out to be unchanged.
2514
 
                ie.revision = parent_entry.revision
2515
 
                ie.text_size = parent_entry.text_size
2516
 
                ie.text_sha1 = parent_entry.text_sha1
2517
 
                ie.executable = parent_entry.executable
2518
 
                return
2519
 
        elif kind == 'directory':
2520
 
            if not store:
2521
 
                # all data is meta here, nothing specific to directory, so
2522
 
                # carry over:
2523
 
                ie.revision = parent_entry.revision
2524
 
                return
2525
 
            lines = []
2526
 
            self._add_text_to_weave(ie.file_id, lines, heads, None)
2527
 
        elif kind == 'symlink':
2528
 
            current_link_target = content_summary[3]
2529
 
            if not store:
2530
 
                # symmlink target is not generic metadata, check if it has
2531
 
                # changed.
2532
 
                if current_link_target != parent_entry.symlink_target:
2533
 
                    store = True
2534
 
            if not store:
2535
 
                # unchanged, carry over.
2536
 
                ie.revision = parent_entry.revision
2537
 
                ie.symlink_target = parent_entry.symlink_target
2538
 
                return
2539
 
            ie.symlink_target = current_link_target
2540
 
            lines = []
2541
 
            self._add_text_to_weave(ie.file_id, lines, heads, None)
2542
 
        elif kind == 'tree-reference':
2543
 
            if not store:
2544
 
                if content_summary[3] != parent_entry.reference_revision:
2545
 
                    store = True
2546
 
            if not store:
2547
 
                # unchanged, carry over.
2548
 
                ie.reference_revision = parent_entry.reference_revision
2549
 
                ie.revision = parent_entry.revision
2550
 
                return
2551
 
            ie.reference_revision = content_summary[3]
2552
 
            lines = []
2553
 
            self._add_text_to_weave(ie.file_id, lines, heads, None)
2554
 
        else:
2555
 
            raise NotImplementedError('unknown kind')
2556
 
        ie.revision = self._new_revision_id
2557
 
 
2558
 
    def _add_text_to_weave(self, file_id, new_lines, parents, nostore_sha):
2559
 
        versionedfile = self.repository.weave_store.get_weave_or_empty(
2560
 
            file_id, self.repository.get_transaction())
2561
 
        # Don't change this to add_lines - add_lines_with_ghosts is cheaper
2562
 
        # than add_lines, and allows committing when a parent is ghosted for
2563
 
        # some reason.
2564
 
        try:
2565
 
            return versionedfile.add_lines_with_ghosts(
2566
 
                self._new_revision_id, parents, new_lines,
2567
 
                nostore_sha=nostore_sha, random_id=self.random_revid)[0:2]
2568
 
        finally:
2569
 
            versionedfile.clear_cache()
2570
 
 
2571
 
 
2572
 
class RootCommitBuilder(CommitBuilder):
2573
 
    """This commitbuilder actually records the root id"""
2574
 
    
2575
 
    def _check_root(self, ie, parent_invs, tree):
2576
 
        """Helper for record_entry_contents.
2577
 
 
2578
 
        :param ie: An entry being added.
2579
 
        :param parent_invs: The inventories of the parent revisions of the
2580
 
            commit.
2581
 
        :param tree: The tree that is being committed.
2582
 
        """
2583
 
        # ie must be root for this builder
2584
 
        assert ie.parent_id is None
2585
 
 
2586
 
 
2587
2593
_unescape_map = {
2588
2594
    'apos':"'",
2589
2595
    'quot':'"',