~launchpad-pqm/launchpad/devel

11592.1.1 by Jeroen Vermeulen
Safer bzr ids in DirectBranchCommit, more appropriate ones in translations_to_branch.
1
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
8687.15.34 by Karl Fogel
Add license header blocks to .py, .zcml, and .pt files that don't have it
2
# GNU Affero General Public License version 3 (see the file LICENSE).
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
3
4
"""Commit files straight to bzr branch."""
5
6
__metaclass__ = type
7
__all__ = [
8
    'ConcurrentUpdateError',
9
    'DirectBranchCommit',
10
    ]
11
12
13
import os.path
14
15
from bzrlib.generate_ids import gen_file_id
16
from bzrlib.revision import NULL_REVISION
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
17
from bzrlib.transform import (
18
    ROOT_PARENT,
19
    TransformPreview,
20
    )
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
21
11592.1.1 by Jeroen Vermeulen
Safer bzr ids in DirectBranchCommit, more appropriate ones in translations_to_branch.
22
from canonical.config import config
14560.2.29 by Curtis Hovey
Restored lpstorm module name because it lp engineers know that name.
23
from lp.services.database.lpstorm import IMasterObject
13084.5.2 by Aaron Bentley
Raise StaleLastMirrored as needed.
24
from lp.code.errors import StaleLastMirrored
25
from lp.codehosting.bzrutils import (
26
    get_branch_info,
27
    get_stacked_on_url,
28
    )
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
29
from lp.services.mail.sendmail import format_address_for_person
11040.1.12 by Aaron Bentley
Ensure commits work when username is unset.
30
from lp.services.osutils import override_environ
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
31
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
32
33
class ConcurrentUpdateError(Exception):
34
    """Bailout exception for concurrent updates.
35
36
    This is raised when committing to a branch would risk overwriting
37
    concurrent changes made by another party.
38
    """
39
40
41
class DirectBranchCommit:
42
    """Commit a set of files straight into a branch.
43
44
    Use this to write a set of files into a branch efficiently, without
45
    caring what was in there before.  The files may be new to the branch
46
    or they may exist there already; in the latter case they will be
47
    overwritten.
48
49
    The branch is write-locked for the entire lifetime of this object.
50
    Be sure to call unlock() when done.  This will be done for you as
51
    part of a successful commit, but unlocking more than once will do no
52
    harm.
53
54
    The trick for this was invented by Aaron Bentley.  It saves having
55
    to do a full checkout of the branch.
56
    """
57
    is_open = False
58
    is_locked = False
8963.9.1 by Jeroen Vermeulen
Fixed directory creation/lookup; used to create the same directories again every time they were needed.
59
    commit_builder = None
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
60
11486.4.2 by Aaron Bentley
Incremental diffs ignore merged changes from target/prereq branches.
61
    def __init__(self, db_branch, committer=None, no_race_check=False,
11592.1.4 by Jeroen Vermeulen
Changes for Noodles' review.
62
                 merge_parents=None, committer_id=None):
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
63
        """Create context for direct commit to branch.
64
8871.3.5 by Jeroen Vermeulen
As per mwhudson's suggestion, keep bzrserver out of DirectBranchCommit entirely.
65
        Before constructing a `DirectBranchCommit`, set up a server that
9590.1.117 by Michael Hudson
kill off get_multi_server
66
        allows write access to lp-internal:/// URLs:
8871.3.5 by Jeroen Vermeulen
As per mwhudson's suggestion, keep bzrserver out of DirectBranchCommit entirely.
67
9590.1.117 by Michael Hudson
kill off get_multi_server
68
        bzrserver = get_rw_server()
10197.5.13 by Michael Hudson
misc. fixes
69
        bzrserver.start_server()
8871.3.5 by Jeroen Vermeulen
As per mwhudson's suggestion, keep bzrserver out of DirectBranchCommit entirely.
70
        try:
71
            branchcommit = DirectBranchCommit(branch)
72
            # ...
73
        finally:
10197.5.13 by Michael Hudson
misc. fixes
74
            bzrserver.stop_server()
8871.3.5 by Jeroen Vermeulen
As per mwhudson's suggestion, keep bzrserver out of DirectBranchCommit entirely.
75
76
        Or in tests, just call `useBzrBranches` before creating a
77
        `DirectBranchCommit`.
8871.3.3 by Jeroen Vermeulen
Factored out BranchMirrorer factory in codehosting. Fixed up DirectBranchCommit to use it. Also, made test start the server before useBzrBranches.
78
8871.3.1 by Jeroen Vermeulen
Implemented changes whispered in my ear by mwhudson.
79
        :param db_branch: a Launchpad `Branch` object.
11592.1.1 by Jeroen Vermeulen
Safer bzr ids in DirectBranchCommit, more appropriate ones in translations_to_branch.
80
        :param committer: the `Person` writing to the branch.  Defaults to
81
            the branch owner.
9590.1.128 by Michael Hudson
add a "no_race_check" to directbranchcommit for use in tests
82
        :param no_race_check: don't check for other commits before committing
83
            our changes, for use in tests.
11592.1.4 by Jeroen Vermeulen
Changes for Noodles' review.
84
        :param committer_id: Optional identification (typically with email
85
            address) of the person doing the commit, for use in bzr.  If not
86
            given, the `committer`'s email address will be used instead.
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
87
        """
88
        self.db_branch = db_branch
89
9375.3.2 by Jeroen Vermeulen
Small fixups.
90
        self.last_scanned_id = self.db_branch.last_scanned_id
9375.3.1 by Jeroen Vermeulen
Extra logging & committing.
91
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
92
        if committer is None:
93
            committer = db_branch.owner
94
        self.committer = committer
11592.1.4 by Jeroen Vermeulen
Changes for Noodles' review.
95
        self.committer_id = committer_id
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
96
9590.1.128 by Michael Hudson
add a "no_race_check" to directbranchcommit for use in tests
97
        self.no_race_check = no_race_check
98
8963.9.2 by Jeroen Vermeulen
Review changes.
99
        # Directories we create on the branch, and their ids.
8963.9.1 by Jeroen Vermeulen
Fixed directory creation/lookup; used to create the same directories again every time they were needed.
100
        self.path_ids = {}
101
9590.1.102 by Michael Hudson
undo some preliminary rosetta changes
102
        self.bzrbranch = self.db_branch.getBzrBranch()
9365.3.1 by Jeroen Vermeulen
Document design choice.
103
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
104
        self.bzrbranch.lock_write()
105
        self.is_locked = True
106
        try:
107
            self.revision_tree = self.bzrbranch.basis_tree()
108
            self.transform_preview = TransformPreview(self.revision_tree)
8963.9.2 by Jeroen Vermeulen
Review changes.
109
            assert self.transform_preview.find_conflicts() == [], (
110
                "TransformPreview is not in a consistent state.")
13084.5.2 by Aaron Bentley
Raise StaleLastMirrored as needed.
111
            if not no_race_check:
112
                last_revision = self.bzrbranch.last_revision()
13084.5.8 by Aaron Bentley
Improve last_mirrored handling.
113
                if not self._matchingLastMirrored(last_revision):
13084.5.2 by Aaron Bentley
Raise StaleLastMirrored as needed.
114
                    raise StaleLastMirrored(
115
                        db_branch, get_branch_info(self.bzrbranch))
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
116
117
            self.is_open = True
118
        except:
119
            self.unlock()
120
            raise
121
122
        self.files = set()
11486.4.2 by Aaron Bentley
Incremental diffs ignore merged changes from target/prereq branches.
123
        self.merge_parents = merge_parents
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
124
13084.5.8 by Aaron Bentley
Improve last_mirrored handling.
125
    def _matchingLastMirrored(self, revision_id):
126
        if (self.db_branch.last_mirrored_id is None
127
            and revision_id == NULL_REVISION):
128
            return True
129
        return revision_id == self.db_branch.last_mirrored_id
130
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
131
    def _getDir(self, path):
132
        """Get trans_id for directory "path."  Create if necessary."""
8963.9.1 by Jeroen Vermeulen
Fixed directory creation/lookup; used to create the same directories again every time they were needed.
133
        path_id = self.path_ids.get(path)
134
        if path_id:
135
            # This is a path we've just created in the branch.
136
            return path_id
137
138
        if self.revision_tree.path2id(path):
139
            # This is a path that was already in the branch.
8657.7.3 by Jeroen Vermeulen
Last-minute fix: got id instead of trans id for existing parent directories.
140
            return self.transform_preview.trans_id_tree_path(path)
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
141
8963.9.1 by Jeroen Vermeulen
Fixed directory creation/lookup; used to create the same directories again every time they were needed.
142
        # Look up (or create) parent directory.
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
143
        parent_dir, dirname = os.path.split(path)
144
        if dirname:
145
            parent_id = self._getDir(parent_dir)
146
        else:
7675.310.3 by Aaron Bentley
Clean up DirectBranchCommit.
147
            parent_id = ROOT_PARENT
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
148
8963.9.1 by Jeroen Vermeulen
Fixed directory creation/lookup; used to create the same directories again every time they were needed.
149
        # Create new directory.
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
150
        dirfile_id = gen_file_id(path)
8963.9.1 by Jeroen Vermeulen
Fixed directory creation/lookup; used to create the same directories again every time they were needed.
151
        path_id = self.transform_preview.new_directory(
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
152
            dirname, parent_id, dirfile_id)
8963.9.1 by Jeroen Vermeulen
Fixed directory creation/lookup; used to create the same directories again every time they were needed.
153
        self.path_ids[path] = path_id
154
        return path_id
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
155
156
    def writeFile(self, path, contents):
157
        """Write file to branch; may be an update or a new file.
158
159
        If you write a file multiple times, the first one is used and
160
        the rest ignored.
161
        """
162
        assert self.is_open, "Writing file to closed DirectBranchCommit."
163
164
        if path in self.files:
165
            # We already have this file.  Ignore second write.
166
            return
167
168
        file_id = self.revision_tree.path2id(path)
169
        if file_id is None:
170
            parent_path, name = os.path.split(path)
171
            parent_id = self._getDir(parent_path)
172
            file_id = gen_file_id(name)
173
            self.transform_preview.new_file(
174
                name, parent_id, [contents], file_id)
175
        else:
176
            trans_id = self.transform_preview.trans_id_tree_path(path)
177
            # Delete old contents.  It doesn't actually matter whether
178
            # we do this before creating the new contents.
179
            self.transform_preview.delete_contents(trans_id)
180
            self.transform_preview.create_file([contents], trans_id)
181
182
        self.files.add(path)
183
184
    def _checkForRace(self):
185
        """Check if bzrbranch has any changes waiting to be scanned.
186
187
        If it does, raise `ConcurrentUpdateError`.
188
        """
189
        assert self.is_locked, "Getting revision on un-locked branch."
9590.1.128 by Michael Hudson
add a "no_race_check" to directbranchcommit for use in tests
190
        if self.no_race_check:
191
            return
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
192
        last_revision = self.bzrbranch.last_revision()
9375.3.1 by Jeroen Vermeulen
Extra logging & committing.
193
        if last_revision != self.last_scanned_id:
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
194
            raise ConcurrentUpdateError(
195
                "Branch has been changed.  Not committing.")
196
11592.1.1 by Jeroen Vermeulen
Safer bzr ids in DirectBranchCommit, more appropriate ones in translations_to_branch.
197
    def getBzrCommitterID(self):
198
        """Find the committer id to pass to bzr."""
11592.1.4 by Jeroen Vermeulen
Changes for Noodles' review.
199
        if self.committer_id is not None:
200
            return self.committer_id
11592.1.1 by Jeroen Vermeulen
Safer bzr ids in DirectBranchCommit, more appropriate ones in translations_to_branch.
201
        elif self.committer.preferredemail is not None:
202
            return format_address_for_person(self.committer)
203
        else:
204
            return '"%s" <%s>' % (
205
                self.committer.displayname,
206
                config.canonical.noreply_from_address)
207
9375.3.1 by Jeroen Vermeulen
Extra logging & committing.
208
    def commit(self, commit_message, txn=None):
209
        """Commit to branch.
210
211
        :param commit_message: Message for branch's commit log.
212
        :param txn: Transaction to commit.  Can be helpful in avoiding
213
            long idle times in database transactions.  May be committed
214
            more than once.
215
        """
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
216
        assert self.is_open, "Committing closed DirectBranchCommit."
217
        assert self.is_locked, "Not locked at commit time."
218
219
        try:
220
            self._checkForRace()
221
9375.3.1 by Jeroen Vermeulen
Extra logging & committing.
222
            if txn:
223
                txn.commit()
224
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
225
            rev_id = self.revision_tree.get_revision_id()
226
            if rev_id == NULL_REVISION:
7675.310.3 by Aaron Bentley
Clean up DirectBranchCommit.
227
                if list(self.transform_preview.iter_changes()) == []:
228
                    return
11592.1.1 by Jeroen Vermeulen
Safer bzr ids in DirectBranchCommit, more appropriate ones in translations_to_branch.
229
            committer_id = self.getBzrCommitterID()
11040.1.12 by Aaron Bentley
Ensure commits work when username is unset.
230
            # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
231
            # required to generate the revision-id.
232
            with override_environ(BZR_EMAIL=committer_id):
233
                new_rev_id = self.transform_preview.commit(
11486.4.2 by Aaron Bentley
Incremental diffs ignore merged changes from target/prereq branches.
234
                    self.bzrbranch, commit_message, self.merge_parents,
235
                    committer=committer_id)
10850.3.1 by Jeroen Vermeulen
Porting production-devel fix to devel.
236
            IMasterObject(self.db_branch).branchChanged(
237
                get_stacked_on_url(self.bzrbranch), new_rev_id,
238
                self.db_branch.control_format, self.db_branch.branch_format,
239
                self.db_branch.repository_format)
8871.3.9 by Jeroen Vermeulen
requestMirror() after committing. Requires write privs on Branch.
240
9375.3.1 by Jeroen Vermeulen
Extra logging & committing.
241
            if txn:
242
                txn.commit()
243
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
244
        finally:
245
            self.unlock()
246
            self.is_open = False
9222.3.6 by Aaron Bentley
Merge preserving changes.
247
        return new_rev_id
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
248
249
    def unlock(self):
250
        """Release commit lock, if held."""
251
        if self.is_locked:
8963.9.1 by Jeroen Vermeulen
Fixed directory creation/lookup; used to create the same directories again every time they were needed.
252
            self.transform_preview.finalize()
8657.7.1 by Jeroen Vermeulen
DirectBranchCommit, for commit-translations-to-branch work.
253
            self.bzrbranch.unlock()
254
            self.is_locked = False