~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branchbuilder.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-01-13 05:14:24 UTC
  • mfrom: (3936.1.3 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20090113051424-nrk3zkfe09h46i9y
(mbp) merge 1.11 and advance to 1.12

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
19
from bzrlib import (
20
 
    bzrdir,
 
20
    bzrdir, 
21
21
    commit,
22
22
    errors,
23
23
    memorytree,
24
 
    revision,
25
24
    )
26
25
 
27
26
 
28
27
class BranchBuilder(object):
29
28
    r"""A BranchBuilder aids creating Branches with particular shapes.
30
 
 
 
29
    
31
30
    The expected way to use BranchBuilder is to construct a
32
31
    BranchBuilder on the transport you want your branch on, and then call
33
32
    appropriate build_ methods on it to get the shape of history you want.
57
56
        a series in progress, it should be None.
58
57
    """
59
58
 
60
 
    def __init__(self, transport=None, format=None, branch=None):
 
59
    def __init__(self, transport, format=None):
61
60
        """Construct a BranchBuilder on transport.
62
 
 
 
61
        
63
62
        :param transport: The transport the branch should be created on.
64
63
            If the path of the transport does not exist but its parent does
65
64
            it will be created.
66
65
        :param format: Either a BzrDirFormat, or the name of a format in the
67
66
            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.
70
67
        """
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)
 
68
        if not transport.has('.'):
 
69
            transport.mkdir('.')
 
70
        if format is None:
 
71
            format = 'default'
 
72
        if isinstance(format, str):
 
73
            format = bzrdir.format_registry.make_bzrdir(format)
 
74
        self._branch = bzrdir.BzrDir.create_branch_convenience(transport.base,
 
75
            format=format, force_new_tree=False)
88
76
        self._tree = None
89
77
 
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
 
        """
 
78
    def build_commit(self):
 
79
        """Build a commit on the branch."""
99
80
        tree = memorytree.MemoryTree.create_on_branch(self._branch)
100
81
        tree.lock_write()
101
82
        try:
102
83
            tree.add('')
103
 
            return self._do_commit(tree, **commit_kwargs)
 
84
            return self._do_commit(tree)
104
85
        finally:
105
86
            tree.unlock()
106
87
 
107
 
    def _do_commit(self, tree, message=None, message_callback=None, **kwargs):
 
88
    def _do_commit(self, tree, message=None, **kwargs):
108
89
        reporter = commit.NullCommitReporter()
109
 
        if message is None and message_callback is None:
 
90
        if message is None:
110
91
            message = u'commit %d' % (self._branch.revno() + 1,)
111
 
        return tree.commit(message, message_callback=message_callback,
 
92
        return tree.commit(message,
112
93
            reporter=reporter,
113
94
            **kwargs)
114
95
 
115
 
    def _move_branch_pointer(self, new_revision_id,
116
 
        allow_leftmost_as_ghost=False):
 
96
    def _move_branch_pointer(self, new_revision_id):
117
97
        """Point self._branch to a different revision id."""
118
98
        self._branch.lock_write()
119
99
        try:
120
100
            # We don't seem to have a simple set_last_revision(), so we
121
101
            # implement it here.
122
102
            cur_revno, cur_revision_id = self._branch.last_revision_info()
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
 
103
            g = self._branch.repository.get_graph()
 
104
            new_revno = g.find_distance_to_null(new_revision_id,
 
105
                                                [(cur_revision_id, cur_revno)])
 
106
            self._branch.set_last_revision_info(new_revno, new_revision_id)
132
107
        finally:
133
108
            self._branch.unlock()
134
109
        if self._tree is not None:
162
137
        self._tree = None
163
138
 
164
139
    def build_snapshot(self, revision_id, parent_ids, actions,
165
 
        message=None, timestamp=None, allow_leftmost_as_ghost=False,
166
 
        committer=None, timezone=None, message_callback=None):
 
140
                       message=None):
167
141
        """Build a commit, shaped in a specific way.
168
142
 
169
 
        Most of the actions are self-explanatory.  'flush' is special action to
170
 
        break a series of actions into discrete steps so that complex changes
171
 
        (such as unversioning a file-id and re-adding it with a different kind)
172
 
        can be expressed in a way that will clearly work.
173
 
 
174
143
        :param revision_id: The handle for the new commit, can be None
175
144
        :param parent_ids: A list of parent_ids to use for the commit.
176
145
            It can be None, which indicates to use the last commit.
179
148
            ('modify', ('file-id', 'new-content'))
180
149
            ('unversion', 'file-id')
181
150
            ('rename', ('orig-path', 'new-path'))
182
 
            ('flush', None)
183
151
        :param message: An optional commit message, if not supplied, a default
184
152
            commit message will be written.
185
 
        :param message_callback: A message callback to use for the commit, as
186
 
            per mutabletree.commit.
187
 
        :param timestamp: If non-None, set the timestamp of the commit to this
188
 
            value.
189
 
        :param timezone: An optional timezone for timestamp.
190
 
        :param committer: An optional username to use for commit
191
 
        :param allow_leftmost_as_ghost: True if the leftmost parent should be
192
 
            permitted to be a ghost.
193
153
        :return: The revision_id of the new commit
194
154
        """
195
155
        if parent_ids is not None:
196
 
            if len(parent_ids) == 0:
197
 
                base_id = revision.NULL_REVISION
198
 
            else:
199
 
                base_id = parent_ids[0]
 
156
            base_id = parent_ids[0]
200
157
            if base_id != self._branch.last_revision():
201
 
                self._move_branch_pointer(base_id,
202
 
                    allow_leftmost_as_ghost=allow_leftmost_as_ghost)
 
158
                self._move_branch_pointer(base_id)
203
159
 
204
160
        if self._tree is not None:
205
161
            tree = self._tree
208
164
        tree.lock_write()
209
165
        try:
210
166
            if parent_ids is not None:
211
 
                tree.set_parent_ids(parent_ids,
212
 
                    allow_leftmost_as_ghost=allow_leftmost_as_ghost)
 
167
                tree.set_parent_ids(parent_ids)
213
168
            # Unfortunately, MemoryTree.add(directory) just creates an
214
169
            # inventory entry. And the only public function to create a
215
170
            # directory is MemoryTree.mkdir() which creates the directory, but
216
171
            # also always adds it. So we have to use a multi-pass setup.
217
 
            pending = _PendingActions()
 
172
            to_add_directories = []
 
173
            to_add_files = []
 
174
            to_add_file_ids = []
 
175
            to_add_kinds = []
 
176
            new_contents = {}
 
177
            to_unversion_ids = []
 
178
            to_rename = []
218
179
            for action, info in actions:
219
180
                if action == 'add':
220
181
                    path, file_id, kind, content = info
221
182
                    if kind == 'directory':
222
 
                        pending.to_add_directories.append((path, file_id))
 
183
                        to_add_directories.append((path, file_id))
223
184
                    else:
224
 
                        pending.to_add_files.append(path)
225
 
                        pending.to_add_file_ids.append(file_id)
226
 
                        pending.to_add_kinds.append(kind)
 
185
                        to_add_files.append(path)
 
186
                        to_add_file_ids.append(file_id)
 
187
                        to_add_kinds.append(kind)
227
188
                        if content is not None:
228
 
                            pending.new_contents[file_id] = content
 
189
                            new_contents[file_id] = content
229
190
                elif action == 'modify':
230
191
                    file_id, content = info
231
 
                    pending.new_contents[file_id] = content
 
192
                    new_contents[file_id] = content
232
193
                elif action == 'unversion':
233
 
                    pending.to_unversion_ids.add(info)
 
194
                    to_unversion_ids.append(info)
234
195
                elif action == 'rename':
235
196
                    from_relpath, to_relpath = info
236
 
                    pending.to_rename.append((from_relpath, to_relpath))
237
 
                elif action == 'flush':
238
 
                    self._flush_pending(tree, pending)
239
 
                    pending = _PendingActions()
 
197
                    to_rename.append((from_relpath, to_relpath))
240
198
                else:
241
199
                    raise ValueError('Unknown build action: "%s"' % (action,))
242
 
            self._flush_pending(tree, pending)
243
 
            return self._do_commit(tree, message=message, rev_id=revision_id,
244
 
                timestamp=timestamp, timezone=timezone, committer=committer,
245
 
                message_callback=message_callback)
 
200
            if to_unversion_ids:
 
201
                tree.unversion(to_unversion_ids)
 
202
            for path, file_id in to_add_directories:
 
203
                if path == '':
 
204
                    # Special case, because the path already exists
 
205
                    tree.add([path], [file_id], ['directory'])
 
206
                else:
 
207
                    tree.mkdir(path, file_id)
 
208
            for from_relpath, to_relpath in to_rename:
 
209
                tree.rename_one(from_relpath, to_relpath)
 
210
            tree.add(to_add_files, to_add_file_ids, to_add_kinds)
 
211
            for file_id, content in new_contents.iteritems():
 
212
                tree.put_file_bytes_non_atomic(file_id, content)
 
213
            return self._do_commit(tree, message=message, rev_id=revision_id) 
246
214
        finally:
247
215
            tree.unlock()
248
216
 
249
 
    def _flush_pending(self, tree, pending):
250
 
        """Flush the pending actions in 'pending', i.e. apply them to 'tree'."""
251
 
        for path, file_id in pending.to_add_directories:
252
 
            if path == '':
253
 
                old_id = tree.path2id(path)
254
 
                if old_id is not None and old_id in pending.to_unversion_ids:
255
 
                    # We're overwriting this path, no need to unversion
256
 
                    pending.to_unversion_ids.discard(old_id)
257
 
                # Special case, because the path already exists
258
 
                tree.add([path], [file_id], ['directory'])
259
 
            else:
260
 
                tree.mkdir(path, file_id)
261
 
        for from_relpath, to_relpath in pending.to_rename:
262
 
            tree.rename_one(from_relpath, to_relpath)
263
 
        if pending.to_unversion_ids:
264
 
            tree.unversion(pending.to_unversion_ids)
265
 
        tree.add(pending.to_add_files, pending.to_add_file_ids, pending.to_add_kinds)
266
 
        for file_id, content in pending.new_contents.iteritems():
267
 
            tree.put_file_bytes_non_atomic(file_id, content)
268
 
 
269
217
    def get_branch(self):
270
218
        """Return the branch created by the builder."""
271
219
        return self._branch
272
 
 
273
 
 
274
 
class _PendingActions(object):
275
 
    """Pending actions for build_snapshot to take.
276
 
 
277
 
    This is just a simple class to hold a bunch of the intermediate state of
278
 
    build_snapshot in single object.
279
 
    """
280
 
 
281
 
    def __init__(self):
282
 
        self.to_add_directories = []
283
 
        self.to_add_files = []
284
 
        self.to_add_file_ids = []
285
 
        self.to_add_kinds = []
286
 
        self.new_contents = {}
287
 
        self.to_unversion_ids = set()
288
 
        self.to_rename = []
289