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