~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branchbuilder.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007, 2008, 2009 Canonical Ltd
 
1
# Copyright (C) 2007, 2008 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
"""Utility for create branches with particular contents."""
18
18
 
19
 
from __future__ import absolute_import
20
 
 
21
 
from bzrlib import (
22
 
    controldir,
23
 
    commit,
24
 
    errors,
25
 
    memorytree,
26
 
    revision,
27
 
    )
 
19
from bzrlib import bzrdir, errors, memorytree
28
20
 
29
21
 
30
22
class BranchBuilder(object):
31
 
    r"""A BranchBuilder aids creating Branches with particular shapes.
32
 
 
 
23
    """A BranchBuilder aids creating Branches with particular shapes.
 
24
    
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.
38
30
    real data.
39
31
 
40
32
    For instance:
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()
 
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()
54
41
 
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.
60
47
    """
61
48
 
62
 
    def __init__(self, transport=None, format=None, branch=None):
 
49
    def __init__(self, transport, format=None):
63
50
        """Construct a BranchBuilder on transport.
64
 
 
 
51
        
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.
72
57
        """
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)
 
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)
90
66
        self._tree = None
91
67
 
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)
 
68
    def build_commit(self):
 
69
        """Build a commit on the branch."""
110
70
        tree = memorytree.MemoryTree.create_on_branch(self._branch)
111
71
        tree.lock_write()
112
72
        try:
113
 
            if parent_ids is not None:
114
 
                tree.set_parent_ids(parent_ids,
115
 
                    allow_leftmost_as_ghost=allow_leftmost_as_ghost)
116
73
            tree.add('')
117
 
            return self._do_commit(tree, **commit_kwargs)
 
74
            return tree.commit('commit %d' % (self._branch.revno() + 1))
118
75
        finally:
119
76
            tree.unlock()
120
77
 
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):
 
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()
133
81
        try:
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()
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
 
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)
146
89
        finally:
147
90
            self._branch.unlock()
148
91
        if self._tree is not None:
176
119
        self._tree = None
177
120
 
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):
 
122
                       message=None):
181
123
        """Build a commit, shaped in a specific way.
182
124
 
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
 
 
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.
192
129
            ('add', ('path', 'file-id', 'kind', 'content' or None))
193
130
            ('modify', ('file-id', 'new-content'))
194
131
            ('unversion', 'file-id')
195
 
            ('rename', ('orig-path', 'new-path'))
196
 
            ('flush', None)
 
132
            # not supported yet: ('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
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.
207
135
        :return: The revision_id of the new commit
208
136
        """
209
137
        if parent_ids is not None:
210
 
            if len(parent_ids) == 0:
211
 
                base_id = revision.NULL_REVISION
212
 
            else:
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)
217
141
 
218
142
        if self._tree is not None:
219
143
            tree = self._tree
222
146
        tree.lock_write()
223
147
        try:
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 = []
 
155
            to_add_files = []
 
156
            to_add_file_ids = []
 
157
            to_add_kinds = []
 
158
            new_contents = {}
 
159
            to_unversion_ids = []
 
160
            # TODO: MemoryTree doesn't support rename() or
 
161
            #       apply_inventory_delta, so we'll postpone allowing renames
 
162
            #       for now
 
163
            # to_rename = []
232
164
            for action, info in actions:
233
165
                if action == 'add':
234
166
                    path, file_id, kind, content = info
235
167
                    if kind == 'directory':
236
 
                        pending.to_add_directories.append((path, file_id))
 
168
                        to_add_directories.append((path, file_id))
237
169
                    else:
238
 
                        pending.to_add_files.append(path)
239
 
                        pending.to_add_file_ids.append(file_id)
240
 
                        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)
241
173
                        if content is not None:
242
 
                            pending.new_contents[file_id] = content
 
174
                            new_contents[file_id] = content
243
175
                elif action == 'modify':
244
176
                    file_id, content = info
245
 
                    pending.new_contents[file_id] = content
 
177
                    new_contents[file_id] = content
246
178
                elif action == 'unversion':
247
 
                    pending.to_unversion_ids.add(info)
248
 
                elif action == 'rename':
249
 
                    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_unversion_ids.append(info)
254
180
                else:
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)
 
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
            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)
 
193
 
 
194
            if message is None:
 
195
                message = u'commit %d' % (self._branch.revno() + 1,)
 
196
            return tree.commit(message, rev_id=revision_id)
260
197
        finally:
261
198
            tree.unlock()
262
199
 
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
 
 
283
200
    def get_branch(self):
284
201
        """Return the branch created by the builder."""
285
202
        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