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 __future__ import absolute_import
19
from bzrlib import bzrdir, errors, memorytree
30
22
class BranchBuilder(object):
31
r"""A BranchBuilder aids creating Branches with particular shapes.
23
"""A BranchBuilder aids creating Branches with particular shapes.
33
25
The expected way to use BranchBuilder is to construct a
34
26
BranchBuilder on the transport you want your branch on, and then call
35
27
appropriate build_ methods on it to get the shape of history you want.
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()
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()
55
42
:ivar _tree: This is a private member which is not meant to be modified by
56
43
users of this class. While a 'series' is in progress, it should hold a
59
46
a series in progress, it should be None.
62
def __init__(self, transport=None, format=None, branch=None):
49
def __init__(self, transport, format=None):
63
50
"""Construct a BranchBuilder on transport.
65
52
:param transport: The transport the branch should be created on.
66
53
If the path of the transport does not exist but its parent does
67
54
it will be created.
68
55
:param format: Either a BzrDirFormat, or the name of a format in the
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.
56
bzrdir format registry for the branch to be built.
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)
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)
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)
68
def build_commit(self):
69
"""Build a commit on the branch."""
110
70
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)
117
return self._do_commit(tree, **commit_kwargs)
74
return tree.commit('commit %d' % (self._branch.revno() + 1))
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):
78
def _move_branch_pointer(self, new_revision_id):
131
79
"""Point self._branch to a different revision id."""
132
80
self._branch.lock_write()
134
82
# We don't seem to have a simple set_last_revision(), so we
135
83
# implement it here.
136
84
cur_revno, cur_revision_id = self._branch.last_revision_info()
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:
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)
147
90
self._branch.unlock()
148
91
if self._tree is not None:
176
119
self._tree = None
178
121
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):
181
123
"""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.
188
125
:param revision_id: The handle for the new commit, can be None
189
126
:param parent_ids: A list of parent_ids to use for the commit.
190
127
It can be None, which indicates to use the last commit.
193
130
('modify', ('file-id', 'new-content'))
194
131
('unversion', 'file-id')
195
132
('rename', ('orig-path', 'new-path'))
197
133
:param message: An optional commit message, if not supplied, a default
198
134
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.
207
135
:return: The revision_id of the new commit
209
137
if parent_ids is not None:
210
if len(parent_ids) == 0:
211
base_id = revision.NULL_REVISION
213
base_id = parent_ids[0]
138
base_id = parent_ids[0]
214
139
if base_id != self._branch.last_revision():
215
self._move_branch_pointer(base_id,
216
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
140
self._move_branch_pointer(base_id)
218
142
if self._tree is not None:
219
143
tree = self._tree
222
146
tree.lock_write()
224
148
if parent_ids is not None:
225
tree.set_parent_ids(parent_ids,
226
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
149
tree.set_parent_ids(parent_ids)
227
150
# Unfortunately, MemoryTree.add(directory) just creates an
228
151
# inventory entry. And the only public function to create a
229
152
# directory is MemoryTree.mkdir() which creates the directory, but
230
153
# also always adds it. So we have to use a multi-pass setup.
231
pending = _PendingActions()
154
to_add_directories = []
159
to_unversion_ids = []
232
161
for action, info in actions:
233
162
if action == 'add':
234
163
path, file_id, kind, content = info
235
164
if kind == 'directory':
236
pending.to_add_directories.append((path, file_id))
165
to_add_directories.append((path, file_id))
238
pending.to_add_files.append(path)
239
pending.to_add_file_ids.append(file_id)
240
pending.to_add_kinds.append(kind)
167
to_add_files.append(path)
168
to_add_file_ids.append(file_id)
169
to_add_kinds.append(kind)
241
170
if content is not None:
242
pending.new_contents[file_id] = content
171
new_contents[file_id] = content
243
172
elif action == 'modify':
244
173
file_id, content = info
245
pending.new_contents[file_id] = content
174
new_contents[file_id] = content
246
175
elif action == 'unversion':
247
pending.to_unversion_ids.add(info)
176
to_unversion_ids.append(info)
248
177
elif action == 'rename':
249
178
from_relpath, to_relpath = info
250
pending.to_rename.append((from_relpath, to_relpath))
251
elif action == 'flush':
252
self._flush_pending(tree, pending)
253
pending = _PendingActions()
179
to_rename.append((from_relpath, to_relpath))
255
181
raise ValueError('Unknown build action: "%s"' % (action,))
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)
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)
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)
283
202
def get_branch(self):
284
203
"""Return the branch created by the builder."""
285
204
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()