~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branchbuilder.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007, 2008 Canonical Ltd
 
1
# Copyright (C) 2007, 2008, 2009 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
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
16
16
 
17
17
"""Utility for create branches with particular contents."""
18
18
 
19
 
from bzrlib import bzrdir, errors, memorytree
 
19
from __future__ import absolute_import
 
20
 
 
21
from bzrlib import (
 
22
    controldir,
 
23
    commit,
 
24
    errors,
 
25
    memorytree,
 
26
    revision,
 
27
    )
20
28
 
21
29
 
22
30
class BranchBuilder(object):
23
 
    """A BranchBuilder aids creating Branches with particular shapes.
24
 
    
 
31
    r"""A BranchBuilder aids creating Branches with particular shapes.
 
32
 
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.
30
38
    real data.
31
39
 
32
40
    For instance:
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()
 
41
 
 
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'))])
 
48
    'rev-id'
 
49
    >>> builder.build_snapshot('rev2-id', ['rev-id'],
 
50
    ...     [('modify', ('f-id', 'new-content\n'))])
 
51
    'rev2-id'
 
52
    >>> builder.finish_series()
 
53
    >>> branch = builder.get_branch()
41
54
 
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.
47
60
    """
48
61
 
49
 
    def __init__(self, transport, format=None):
 
62
    def __init__(self, transport=None, format=None, branch=None):
50
63
        """Construct a BranchBuilder on transport.
51
 
        
 
64
 
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.
57
72
        """
58
 
        if not transport.has('.'):
59
 
            transport.mkdir('.')
60
 
        if format is None:
61
 
            format = 'default'
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:
 
75
                raise AssertionError(
 
76
                    "branch and format kwargs are mutually exclusive")
 
77
            if transport is not None:
 
78
                raise AssertionError(
 
79
                    "branch and transport kwargs are mutually exclusive")
 
80
            self._branch = branch
 
81
        else:
 
82
            if not transport.has('.'):
 
83
                transport.mkdir('.')
 
84
            if format is None:
 
85
                format = 'default'
 
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)
66
90
        self._tree = None
67
91
 
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,
 
93
                     **commit_kwargs):
 
94
        """Build a commit on the branch.
 
95
 
 
96
        This makes a commit with no real file content for when you only want
 
97
        to look at the revision graph structure.
 
98
 
 
99
        :param commit_kwargs: Arguments to pass through to commit, such as
 
100
             timestamp.
 
101
        """
 
102
        if parent_ids is not None:
 
103
            if len(parent_ids) == 0:
 
104
                base_id = revision.NULL_REVISION
 
105
            else:
 
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)
71
111
        tree.lock_write()
72
112
        try:
 
113
            if parent_ids is not None:
 
114
                tree.set_parent_ids(parent_ids,
 
115
                    allow_leftmost_as_ghost=allow_leftmost_as_ghost)
73
116
            tree.add('')
74
 
            return tree.commit('commit %d' % (self._branch.revno() + 1))
 
117
            return self._do_commit(tree, **commit_kwargs)
75
118
        finally:
76
119
            tree.unlock()
77
120
 
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,
 
126
            reporter=reporter,
 
127
            **kwargs)
 
128
 
 
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()
81
133
        try:
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)
 
137
            try:
 
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:
 
144
                    raise
 
145
                new_revno = 1
89
146
        finally:
90
147
            self._branch.unlock()
91
148
        if self._tree is not None:
119
176
        self._tree = None
120
177
 
121
178
    def build_snapshot(self, revision_id, parent_ids, actions,
122
 
                       message=None):
 
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.
124
182
 
 
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.
 
187
 
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'))
 
196
            ('flush', None)
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
 
202
            value.
 
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
136
208
        """
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
 
212
            else:
 
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)
141
217
 
142
218
        if self._tree is not None:
143
219
            tree = self._tree
146
222
        tree.lock_write()
147
223
        try:
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 = []
155
 
            to_add_files = []
156
 
            to_add_file_ids = []
157
 
            to_add_kinds = []
158
 
            new_contents = {}
159
 
            to_unversion_ids = []
160
 
            to_rename = []
 
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))
166
237
                    else:
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()
180
254
                else:
181
255
                    raise ValueError('Unknown build action: "%s"' % (action,))
182
 
            if to_unversion_ids:
183
 
                tree.unversion(to_unversion_ids)
184
 
            for path, file_id in to_add_directories:
185
 
                if path == '':
186
 
                    # Special case, because the path already exists
187
 
                    tree.add([path], [file_id], ['directory'])
188
 
                else:
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)
195
 
 
196
 
            if message is None:
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)
199
260
        finally:
200
261
            tree.unlock()
201
262
 
 
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:
 
266
            if path == '':
 
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'])
 
273
            else:
 
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)
 
282
 
202
283
    def get_branch(self):
203
284
        """Return the branch created by the builder."""
204
285
        return self._branch
 
286
 
 
287
 
 
288
class _PendingActions(object):
 
289
    """Pending actions for build_snapshot to take.
 
290
 
 
291
    This is just a simple class to hold a bunch of the intermediate state of
 
292
    build_snapshot in single object.
 
293
    """
 
294
 
 
295
    def __init__(self):
 
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()
 
302
        self.to_rename = []
 
303