1
# Copyright (C) 2006-2011 Canonical Ltd
2
# Copyright (C) 2020 Breezy Developers
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
from __future__ import absolute_import
23
from .. import errors, ui
24
from ..i18n import gettext
25
from ..mutabletree import MutableTree
26
from ..sixish import viewitems
27
from ..transform import (
33
TransformRenameFailed,
36
from ..bzr import inventory
37
from ..bzr.transform import TransformPreview as GitTransformPreview
40
class GitTreeTransform(TreeTransform):
41
"""Tree transform for Bazaar trees."""
43
def version_file(self, trans_id, file_id=None):
44
"""Schedule a file to become versioned."""
47
unique_add(self._new_id, trans_id, file_id)
48
unique_add(self._r_new_id, file_id, trans_id)
50
def cancel_versioning(self, trans_id):
51
"""Undo a previous versioning of a file"""
52
file_id = self._new_id[trans_id]
53
del self._new_id[trans_id]
54
del self._r_new_id[file_id]
56
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
57
"""Apply all changes to the inventory and filesystem.
59
If filesystem or inventory conflicts are present, MalformedTransform
62
If apply succeeds, finalize is not necessary.
64
:param no_conflicts: if True, the caller guarantees there are no
65
conflicts, so no check is made.
66
:param precomputed_delta: An inventory delta to use instead of
68
:param _mover: Supply an alternate FileMover, for testing
70
for hook in MutableTree.hooks['pre_transform']:
71
hook(self._tree, self)
73
self._check_malformed()
74
with ui.ui_factory.nested_progress_bar() as child_pb:
75
if precomputed_delta is None:
76
child_pb.update(gettext('Apply phase'), 0, 2)
77
changes = self._generate_transform_changes()
81
(op, np, ie) for (op, np, fid, ie) in precomputed_delta]
88
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
89
self._apply_removals(mover)
90
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
91
modified_paths = self._apply_insertions(mover)
96
mover.apply_deletions()
97
if self.final_file_id(self.root) is None:
98
changes = [e for e in changes if e[0] != '']
99
self._tree._apply_transform_delta(changes)
102
return _TransformResults(modified_paths, self.rename_count)
104
def _apply_removals(self, mover):
105
"""Perform tree operations that remove directory/inventory names.
107
That is, delete files that are to be deleted, and put any files that
108
need renaming into limbo. This must be done in strict child-to-parent
111
If inventory_delta is None, no inventory delta generation is performed.
113
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
114
with ui.ui_factory.nested_progress_bar() as child_pb:
115
for num, (path, trans_id) in enumerate(tree_paths):
116
# do not attempt to move root into a subdirectory of itself.
119
child_pb.update(gettext('removing file'), num, len(tree_paths))
120
full_path = self._tree.abspath(path)
121
if trans_id in self._removed_contents:
122
delete_path = os.path.join(self._deletiondir, trans_id)
123
mover.pre_delete(full_path, delete_path)
124
elif (trans_id in self._new_name or
125
trans_id in self._new_parent):
127
mover.rename(full_path, self._limbo_name(trans_id))
128
except TransformRenameFailed as e:
129
if e.errno != errno.ENOENT:
132
self.rename_count += 1
134
def _apply_insertions(self, mover):
135
"""Perform tree operations that insert directory/inventory names.
137
That is, create any files that need to be created, and restore from
138
limbo any files that needed renaming. This must be done in strict
139
parent-to-child order.
141
If inventory_delta is None, no inventory delta is calculated, and
142
no list of modified paths is returned.
144
new_paths = self.new_paths(filesystem_only=True)
146
with ui.ui_factory.nested_progress_bar() as child_pb:
147
for num, (path, trans_id) in enumerate(new_paths):
149
child_pb.update(gettext('adding file'),
151
full_path = self._tree.abspath(path)
152
if trans_id in self._needs_rename:
154
mover.rename(self._limbo_name(trans_id), full_path)
155
except TransformRenameFailed as e:
156
# We may be renaming a dangling inventory id
157
if e.errno != errno.ENOENT:
160
self.rename_count += 1
161
# TODO: if trans_id in self._observed_sha1s, we should
162
# re-stat the final target, since ctime will be
163
# updated by the change.
164
if (trans_id in self._new_contents
165
or self.path_changed(trans_id)):
166
if trans_id in self._new_contents:
167
modified_paths.append(full_path)
168
if trans_id in self._new_executability:
169
self._set_executability(path, trans_id)
170
if trans_id in self._observed_sha1s:
171
o_sha1, o_st_val = self._observed_sha1s[trans_id]
172
st = osutils.lstat(full_path)
173
self._observed_sha1s[trans_id] = (o_sha1, st)
174
for path, trans_id in new_paths:
175
# new_paths includes stuff like workingtree conflicts. Only the
176
# stuff in new_contents actually comes from limbo.
177
if trans_id in self._limbo_files:
178
del self._limbo_files[trans_id]
179
self._new_contents.clear()
180
return modified_paths
182
def _inventory_altered(self):
183
"""Determine which trans_ids need new Inventory entries.
185
An new entry is needed when anything that would be reflected by an
186
inventory entry changes, including file name, file_id, parent file_id,
187
file kind, and the execute bit.
189
Some care is taken to return entries with real changes, not cases
190
where the value is deleted and then restored to its original value,
191
but some actually unchanged values may be returned.
193
:returns: A list of (path, trans_id) for all items requiring an
194
inventory change. Ordered by path.
197
# Find entries whose file_ids are new (or changed).
198
new_file_id = set(t for t in self._new_id
199
if self._new_id[t] != self.tree_file_id(t))
200
for id_set in [self._new_name, self._new_parent, new_file_id,
201
self._new_executability]:
202
changed_ids.update(id_set)
203
# removing implies a kind change
204
changed_kind = set(self._removed_contents)
206
changed_kind.intersection_update(self._new_contents)
207
# Ignore entries that are already known to have changed.
208
changed_kind.difference_update(changed_ids)
209
# to keep only the truly changed ones
210
changed_kind = (t for t in changed_kind
211
if self.tree_kind(t) != self.final_kind(t))
212
# all kind changes will alter the inventory
213
changed_ids.update(changed_kind)
214
# To find entries with changed parent_ids, find parents which existed,
215
# but changed file_id.
216
# Now add all their children to the set.
217
for parent_trans_id in new_file_id:
218
changed_ids.update(self.iter_tree_children(parent_trans_id))
219
return sorted(FinalPaths(self).get_paths(changed_ids))
221
def _generate_transform_changes(self):
222
"""Generate an inventory delta for the current transform."""
224
new_paths = self._inventory_altered()
225
total_entries = len(new_paths) + len(self._removed_id)
226
with ui.ui_factory.nested_progress_bar() as child_pb:
227
for num, trans_id in enumerate(self._removed_id):
229
child_pb.update(gettext('removing file'),
231
if trans_id == self._new_root:
232
file_id = self._tree.path2id('')
234
file_id = self.tree_file_id(trans_id)
235
# File-id isn't really being deleted, just moved
236
if file_id in self._r_new_id:
238
path = self._tree_id_paths[trans_id]
239
changes.append((path, None, None))
240
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
242
for num, (path, trans_id) in enumerate(new_paths):
244
child_pb.update(gettext('adding file'),
245
num + len(self._removed_id), total_entries)
246
file_id = new_path_file_ids[trans_id]
249
kind = self.final_kind(trans_id)
251
kind = self._tree.stored_kind(self._tree.id2path(file_id))
252
parent_trans_id = self.final_parent(trans_id)
253
parent_file_id = new_path_file_ids.get(parent_trans_id)
254
if parent_file_id is None:
255
parent_file_id = self.final_file_id(parent_trans_id)
256
if trans_id in self._new_reference_revision:
257
new_entry = inventory.TreeReference(
259
self._new_name[trans_id],
260
self.final_file_id(self._new_parent[trans_id]),
261
None, self._new_reference_revision[trans_id])
263
new_entry = inventory.make_entry(kind,
264
self.final_name(trans_id),
265
parent_file_id, file_id)
267
old_path = self._tree.id2path(new_entry.file_id)
268
except errors.NoSuchId:
270
new_executability = self._new_executability.get(trans_id)
271
if new_executability is not None:
272
new_entry.executable = new_executability
274
(old_path, path, new_entry))