~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branchbuilder.py

  • Committer: Florian Dorn
  • Date: 2012-04-03 14:49:22 UTC
  • mto: This revision was merged to the branch mainline in revision 6546.
  • Revision ID: florian.dorn@boku.ac.at-20120403144922-b8y59csy8l1rzs5u
updated developer docs

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 bzrlib import (
 
20
    bzrdir,
 
21
    commit,
 
22
    errors,
 
23
    memorytree,
 
24
    revision,
 
25
    )
20
26
 
21
27
 
22
28
class BranchBuilder(object):
23
 
    """A BranchBuilder aids creating Branches with particular shapes.
24
 
    
 
29
    r"""A BranchBuilder aids creating Branches with particular shapes.
 
30
 
25
31
    The expected way to use BranchBuilder is to construct a
26
32
    BranchBuilder on the transport you want your branch on, and then call
27
33
    appropriate build_ methods on it to get the shape of history you want.
30
36
    real data.
31
37
 
32
38
    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()
 
39
 
 
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'))])
 
46
    'rev-id'
 
47
    >>> builder.build_snapshot('rev2-id', ['rev-id'],
 
48
    ...     [('modify', ('f-id', 'new-content\n'))])
 
49
    'rev2-id'
 
50
    >>> builder.finish_series()
 
51
    >>> branch = builder.get_branch()
41
52
 
42
53
    :ivar _tree: This is a private member which is not meant to be modified by
43
54
        users of this class. While a 'series' is in progress, it should hold a
46
57
        a series in progress, it should be None.
47
58
    """
48
59
 
49
 
    def __init__(self, transport, format=None):
 
60
    def __init__(self, transport=None, format=None, branch=None):
50
61
        """Construct a BranchBuilder on transport.
51
 
        
 
62
 
52
63
        :param transport: The transport the branch should be created on.
53
64
            If the path of the transport does not exist but its parent does
54
65
            it will be created.
55
66
        :param format: Either a BzrDirFormat, or the name of a format in the
56
67
            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.
57
70
        """
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)
 
71
        if branch is not None:
 
72
            if format is not None:
 
73
                raise AssertionError(
 
74
                    "branch and format kwargs are mutually exclusive")
 
75
            if transport is not None:
 
76
                raise AssertionError(
 
77
                    "branch and transport kwargs are mutually exclusive")
 
78
            self._branch = branch
 
79
        else:
 
80
            if not transport.has('.'):
 
81
                transport.mkdir('.')
 
82
            if format is None:
 
83
                format = 'default'
 
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)
66
88
        self._tree = None
67
89
 
68
 
    def build_commit(self):
69
 
        """Build a commit on the branch."""
 
90
    def build_commit(self, **commit_kwargs):
 
91
        """Build a commit on the branch.
 
92
 
 
93
        This makes a commit with no real file content for when you only want
 
94
        to look at the revision graph structure.
 
95
 
 
96
        :param commit_kwargs: Arguments to pass through to commit, such as
 
97
             timestamp.
 
98
        """
70
99
        tree = memorytree.MemoryTree.create_on_branch(self._branch)
71
100
        tree.lock_write()
72
101
        try:
73
102
            tree.add('')
74
 
            return tree.commit('commit %d' % (self._branch.revno() + 1))
 
103
            return self._do_commit(tree, **commit_kwargs)
75
104
        finally:
76
105
            tree.unlock()
77
106
 
78
 
    def _move_branch_pointer(self, new_revision_id):
 
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,
 
112
            reporter=reporter,
 
113
            **kwargs)
 
114
 
 
115
    def _move_branch_pointer(self, new_revision_id,
 
116
        allow_leftmost_as_ghost=False):
79
117
        """Point self._branch to a different revision id."""
80
118
        self._branch.lock_write()
81
119
        try:
82
120
            # We don't seem to have a simple set_last_revision(), so we
83
121
            # implement it here.
84
122
            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)
 
123
            try:
 
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:
 
130
                    raise
 
131
                new_revno = 1
89
132
        finally:
90
133
            self._branch.unlock()
91
134
        if self._tree is not None:
119
162
        self._tree = None
120
163
 
121
164
    def build_snapshot(self, revision_id, parent_ids, actions,
122
 
                       message=None):
 
165
        message=None, timestamp=None, allow_leftmost_as_ghost=False,
 
166
        committer=None, timezone=None, message_callback=None):
123
167
        """Build a commit, shaped in a specific way.
124
168
 
125
169
        :param revision_id: The handle for the new commit, can be None
129
173
            ('add', ('path', 'file-id', 'kind', 'content' or None))
130
174
            ('modify', ('file-id', 'new-content'))
131
175
            ('unversion', 'file-id')
132
 
            # not supported yet: ('rename', ('orig-path', 'new-path'))
 
176
            ('rename', ('orig-path', 'new-path'))
133
177
        :param message: An optional commit message, if not supplied, a default
134
178
            commit message will be written.
 
179
        :param message_callback: A message callback to use for the commit, as
 
180
            per mutabletree.commit.
 
181
        :param timestamp: If non-None, set the timestamp of the commit to this
 
182
            value.
 
183
        :param timezone: An optional timezone for timestamp.
 
184
        :param committer: An optional username to use for commit
 
185
        :param allow_leftmost_as_ghost: True if the leftmost parent should be
 
186
            permitted to be a ghost.
135
187
        :return: The revision_id of the new commit
136
188
        """
137
189
        if parent_ids is not None:
138
 
            base_id = parent_ids[0]
 
190
            if len(parent_ids) == 0:
 
191
                base_id = revision.NULL_REVISION
 
192
            else:
 
193
                base_id = parent_ids[0]
139
194
            if base_id != self._branch.last_revision():
140
 
                self._move_branch_pointer(base_id)
 
195
                self._move_branch_pointer(base_id,
 
196
                    allow_leftmost_as_ghost=allow_leftmost_as_ghost)
141
197
 
142
198
        if self._tree is not None:
143
199
            tree = self._tree
146
202
        tree.lock_write()
147
203
        try:
148
204
            if parent_ids is not None:
149
 
                tree.set_parent_ids(parent_ids)
 
205
                tree.set_parent_ids(parent_ids,
 
206
                    allow_leftmost_as_ghost=allow_leftmost_as_ghost)
150
207
            # Unfortunately, MemoryTree.add(directory) just creates an
151
208
            # inventory entry. And the only public function to create a
152
209
            # directory is MemoryTree.mkdir() which creates the directory, but
157
214
            to_add_kinds = []
158
215
            new_contents = {}
159
216
            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 = []
 
217
            to_rename = []
164
218
            for action, info in actions:
165
219
                if action == 'add':
166
220
                    path, file_id, kind, content = info
177
231
                    new_contents[file_id] = content
178
232
                elif action == 'unversion':
179
233
                    to_unversion_ids.append(info)
 
234
                elif action == 'rename':
 
235
                    from_relpath, to_relpath = info
 
236
                    to_rename.append((from_relpath, to_relpath))
180
237
                else:
181
238
                    raise ValueError('Unknown build action: "%s"' % (action,))
182
239
            if to_unversion_ids:
187
244
                    tree.add([path], [file_id], ['directory'])
188
245
                else:
189
246
                    tree.mkdir(path, file_id)
 
247
            for from_relpath, to_relpath in to_rename:
 
248
                tree.rename_one(from_relpath, to_relpath)
190
249
            tree.add(to_add_files, to_add_file_ids, to_add_kinds)
191
250
            for file_id, content in new_contents.iteritems():
192
251
                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)
 
252
            return self._do_commit(tree, message=message, rev_id=revision_id,
 
253
                timestamp=timestamp, timezone=timezone, committer=committer,
 
254
                message_callback=message_callback)
197
255
        finally:
198
256
            tree.unlock()
199
257