13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
17
"""Utility for create branches with particular contents."""
19
from bzrlib import bzrdir, errors, memorytree
28
22
class BranchBuilder(object):
29
r"""A BranchBuilder aids creating Branches with particular shapes.
23
"""A BranchBuilder aids creating Branches with particular shapes.
31
25
The expected way to use BranchBuilder is to construct a
32
26
BranchBuilder on the transport you want your branch on, and then call
33
27
appropriate build_ methods on it to get the shape of history you want.
40
>>> from bzrlib.transport.memory import MemoryTransport
41
>>> builder = BranchBuilder(MemoryTransport("memory:///"))
42
>>> builder.start_series()
43
>>> builder.build_snapshot('rev-id', None, [
44
... ('add', ('', 'root-id', 'directory', '')),
45
... ('add', ('filename', 'f-id', 'file', 'content\n'))])
47
>>> builder.build_snapshot('rev2-id', ['rev-id'],
48
... [('modify', ('f-id', 'new-content\n'))])
50
>>> builder.finish_series()
51
>>> branch = builder.get_branch()
33
builder = BranchBuilder(self.get_transport().clone('relpath'))
34
builder.start_series()
35
builder.build_snapshot('rev-id', [],
36
[('add', ('filename', 'f-id', 'file', 'content\n'))])
37
builder.build_snapshot('rev2-id', ['rev-id'],
38
[('modify', ('f-id', 'new-content\n'))])
39
builder.finish_series()
40
branch = builder.get_branch()
53
42
:ivar _tree: This is a private member which is not meant to be modified by
54
43
users of this class. While a 'series' is in progress, it should hold a
57
46
a series in progress, it should be None.
60
def __init__(self, transport=None, format=None, branch=None):
49
def __init__(self, transport, format=None):
61
50
"""Construct a BranchBuilder on transport.
63
52
:param transport: The transport the branch should be created on.
64
53
If the path of the transport does not exist but its parent does
65
54
it will be created.
66
55
:param format: Either a BzrDirFormat, or the name of a format in the
67
56
bzrdir format registry for the branch to be built.
68
:param branch: An already constructed branch to use. This param is
69
mutually exclusive with the transport and format params.
71
if branch is not None:
72
if format is not None:
74
"branch and format kwargs are mutually exclusive")
75
if transport is not None:
77
"branch and transport kwargs are mutually exclusive")
80
if not transport.has('.'):
84
if isinstance(format, str):
85
format = bzrdir.format_registry.make_bzrdir(format)
86
self._branch = bzrdir.BzrDir.create_branch_convenience(
87
transport.base, format=format, force_new_tree=False)
58
if not transport.has('.'):
62
if isinstance(format, str):
63
format = bzrdir.format_registry.make_bzrdir(format)
64
self._branch = bzrdir.BzrDir.create_branch_convenience(transport.base,
65
format=format, force_new_tree=False)
90
def build_commit(self, **commit_kwargs):
91
"""Build a commit on the branch.
93
This makes a commit with no real file content for when you only want
94
to look at the revision graph structure.
96
:param commit_kwargs: Arguments to pass through to commit, such as
68
def build_commit(self):
69
"""Build a commit on the branch."""
99
70
tree = memorytree.MemoryTree.create_on_branch(self._branch)
103
return self._do_commit(tree, **commit_kwargs)
74
return tree.commit('commit %d' % (self._branch.revno() + 1))
107
def _do_commit(self, tree, message=None, message_callback=None, **kwargs):
108
reporter = commit.NullCommitReporter()
109
if message is None and message_callback is None:
110
message = u'commit %d' % (self._branch.revno() + 1,)
111
return tree.commit(message, message_callback=message_callback,
115
def _move_branch_pointer(self, new_revision_id,
116
allow_leftmost_as_ghost=False):
78
def _move_branch_pointer(self, new_revision_id):
117
79
"""Point self._branch to a different revision id."""
118
80
self._branch.lock_write()
120
82
# We don't seem to have a simple set_last_revision(), so we
121
83
# implement it here.
122
84
cur_revno, cur_revision_id = self._branch.last_revision_info()
124
g = self._branch.repository.get_graph()
125
new_revno = g.find_distance_to_null(new_revision_id,
126
[(cur_revision_id, cur_revno)])
127
self._branch.set_last_revision_info(new_revno, new_revision_id)
128
except errors.GhostRevisionsHaveNoRevno:
129
if not allow_leftmost_as_ghost:
85
g = self._branch.repository.get_graph()
86
new_revno = g.find_distance_to_null(new_revision_id,
87
[(cur_revision_id, cur_revno)])
88
self._branch.set_last_revision_info(new_revno, new_revision_id)
133
90
self._branch.unlock()
134
91
if self._tree is not None:
162
119
self._tree = None
164
121
def build_snapshot(self, revision_id, parent_ids, actions,
165
message=None, timestamp=None, allow_leftmost_as_ghost=False,
166
committer=None, timezone=None, message_callback=None):
167
123
"""Build a commit, shaped in a specific way.
169
Most of the actions are self-explanatory. 'flush' is special action to
170
break a series of actions into discrete steps so that complex changes
171
(such as unversioning a file-id and re-adding it with a different kind)
172
can be expressed in a way that will clearly work.
174
125
:param revision_id: The handle for the new commit, can be None
175
126
:param parent_ids: A list of parent_ids to use for the commit.
176
127
It can be None, which indicates to use the last commit.
178
129
('add', ('path', 'file-id', 'kind', 'content' or None))
179
130
('modify', ('file-id', 'new-content'))
180
131
('unversion', 'file-id')
181
('rename', ('orig-path', 'new-path'))
132
# not supported yet: ('rename', ('orig-path', 'new-path'))
183
133
:param message: An optional commit message, if not supplied, a default
184
134
commit message will be written.
185
:param message_callback: A message callback to use for the commit, as
186
per mutabletree.commit.
187
:param timestamp: If non-None, set the timestamp of the commit to this
189
:param timezone: An optional timezone for timestamp.
190
:param committer: An optional username to use for commit
191
:param allow_leftmost_as_ghost: True if the leftmost parent should be
192
permitted to be a ghost.
193
135
:return: The revision_id of the new commit
195
137
if parent_ids is not None:
196
if len(parent_ids) == 0:
197
base_id = revision.NULL_REVISION
199
base_id = parent_ids[0]
138
base_id = parent_ids[0]
200
139
if base_id != self._branch.last_revision():
201
self._move_branch_pointer(base_id,
202
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
140
self._move_branch_pointer(base_id)
204
142
if self._tree is not None:
205
143
tree = self._tree
208
146
tree.lock_write()
210
148
if parent_ids is not None:
211
tree.set_parent_ids(parent_ids,
212
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
149
tree.set_parent_ids(parent_ids)
213
150
# Unfortunately, MemoryTree.add(directory) just creates an
214
151
# inventory entry. And the only public function to create a
215
152
# directory is MemoryTree.mkdir() which creates the directory, but
216
153
# also always adds it. So we have to use a multi-pass setup.
217
pending = _PendingActions()
154
to_add_directories = []
159
to_unversion_ids = []
160
# TODO: MemoryTree doesn't support rename() or
161
# apply_inventory_delta, so we'll postpone allowing renames
218
164
for action, info in actions:
219
165
if action == 'add':
220
166
path, file_id, kind, content = info
221
167
if kind == 'directory':
222
pending.to_add_directories.append((path, file_id))
168
to_add_directories.append((path, file_id))
224
pending.to_add_files.append(path)
225
pending.to_add_file_ids.append(file_id)
226
pending.to_add_kinds.append(kind)
170
to_add_files.append(path)
171
to_add_file_ids.append(file_id)
172
to_add_kinds.append(kind)
227
173
if content is not None:
228
pending.new_contents[file_id] = content
174
new_contents[file_id] = content
229
175
elif action == 'modify':
230
176
file_id, content = info
231
pending.new_contents[file_id] = content
177
new_contents[file_id] = content
232
178
elif action == 'unversion':
233
pending.to_unversion_ids.add(info)
234
elif action == 'rename':
235
from_relpath, to_relpath = info
236
pending.to_rename.append((from_relpath, to_relpath))
237
elif action == 'flush':
238
self._flush_pending(tree, pending)
239
pending = _PendingActions()
179
to_unversion_ids.append(info)
241
181
raise ValueError('Unknown build action: "%s"' % (action,))
242
self._flush_pending(tree, pending)
243
return self._do_commit(tree, message=message, rev_id=revision_id,
244
timestamp=timestamp, timezone=timezone, committer=committer,
245
message_callback=message_callback)
183
tree.unversion(to_unversion_ids)
184
for path, file_id in to_add_directories:
186
# Special case, because the path already exists
187
tree.add([path], [file_id], ['directory'])
189
tree.mkdir(path, file_id)
190
tree.add(to_add_files, to_add_file_ids, to_add_kinds)
191
for file_id, content in new_contents.iteritems():
192
tree.put_file_bytes_non_atomic(file_id, content)
195
message = u'commit %d' % (self._branch.revno() + 1,)
196
return tree.commit(message, rev_id=revision_id)
249
def _flush_pending(self, tree, pending):
250
"""Flush the pending actions in 'pending', i.e. apply them to 'tree'."""
251
for path, file_id in pending.to_add_directories:
253
old_id = tree.path2id(path)
254
if old_id is not None and old_id in pending.to_unversion_ids:
255
# We're overwriting this path, no need to unversion
256
pending.to_unversion_ids.discard(old_id)
257
# Special case, because the path already exists
258
tree.add([path], [file_id], ['directory'])
260
tree.mkdir(path, file_id)
261
for from_relpath, to_relpath in pending.to_rename:
262
tree.rename_one(from_relpath, to_relpath)
263
if pending.to_unversion_ids:
264
tree.unversion(pending.to_unversion_ids)
265
tree.add(pending.to_add_files, pending.to_add_file_ids, pending.to_add_kinds)
266
for file_id, content in pending.new_contents.iteritems():
267
tree.put_file_bytes_non_atomic(file_id, content)
269
200
def get_branch(self):
270
201
"""Return the branch created by the builder."""
271
202
return self._branch
274
class _PendingActions(object):
275
"""Pending actions for build_snapshot to take.
277
This is just a simple class to hold a bunch of the intermediate state of
278
build_snapshot in single object.
282
self.to_add_directories = []
283
self.to_add_files = []
284
self.to_add_file_ids = []
285
self.to_add_kinds = []
286
self.new_contents = {}
287
self.to_unversion_ids = set()