Benjamin Schubert pushed to branch bschubert/fix-atomic-move-git-repo at BuildStream / buildstream
Commits:
-
cc2e6ae5
by Valentin David at 2018-11-08T12:41:36Z
-
e578a89f
by Valentin David at 2018-11-08T13:11:10Z
-
c0a8bb66
by Angelos Evripiotis at 2018-11-08T15:49:16Z
-
f7231e90
by Angelos Evripiotis at 2018-11-08T15:49:16Z
-
f7643440
by Angelos Evripiotis at 2018-11-08T15:49:16Z
-
dd5e7b04
by Angelos Evripiotis at 2018-11-08T16:17:04Z
-
7cb37389
by Benjamin Schubert at 2018-11-09T09:58:07Z
10 changed files:
- CONTRIBUTING.rst
- buildstream/plugins/sources/git.py
- buildstream/scriptelement.py
- tests/frontend/buildtrack.py
- + tests/integration/project/elements/script/corruption-2.bst
- + tests/integration/project/elements/script/marked-tmpdir.bst
- + tests/integration/project/elements/script/no-tmpdir.bst
- + tests/integration/project/elements/script/tmpdir.bst
- tests/integration/script.py
- tests/sources/git.py
Changes:
... | ... | @@ -97,7 +97,13 @@ a new merge request. You can also `create a merge request for an existing branch |
97 | 97 |
You may open merge requests for the branches you create before you are ready
|
98 | 98 |
to have them reviewed and considered for inclusion if you like. Until your merge
|
99 | 99 |
request is ready for review, the merge request title must be prefixed with the
|
100 |
-``WIP:`` identifier.
|
|
100 |
+``WIP:`` identifier. GitLab `treats this specially
|
|
101 |
+<https://docs.gitlab.com/ee/user/project/merge_requests/work_in_progress_merge_requests.html>`_,
|
|
102 |
+which helps reviewers.
|
|
103 |
+ |
|
104 |
+Consider marking a merge request as WIP again if you are taking a while to
|
|
105 |
+address a review point. This signals that the next action is on you, and it
|
|
106 |
+won't appear in a reviewer's search for non-WIP merge requests to review.
|
|
101 | 107 |
|
102 | 108 |
|
103 | 109 |
Organized commits
|
... | ... | @@ -122,6 +128,12 @@ If a commit in your branch modifies behavior such that a test must also |
122 | 128 |
be changed to match the new behavior, then the tests should be updated
|
123 | 129 |
with the same commit, so that every commit passes its own tests.
|
124 | 130 |
|
131 |
+These principles apply whenever a branch is non-WIP. So for example, don't push
|
|
132 |
+'fixup!' commits when addressing review comments, instead amend the commits
|
|
133 |
+directly before pushing. GitLab has `good support
|
|
134 |
+<https://docs.gitlab.com/ee/user/project/merge_requests/versions.html>`_ for
|
|
135 |
+diffing between pushes, so 'fixup!' commits are not necessary for reviewers.
|
|
136 |
+ |
|
125 | 137 |
|
126 | 138 |
Commit messages
|
127 | 139 |
~~~~~~~~~~~~~~~
|
... | ... | @@ -144,6 +156,16 @@ number must be referenced in the commit message. |
144 | 156 |
|
145 | 157 |
Fixes #123
|
146 | 158 |
|
159 |
+Note that the 'why' of a change is as important as the 'what'.
|
|
160 |
+ |
|
161 |
+When reviewing this, folks can suggest better alternatives when they know the
|
|
162 |
+'why'. Perhaps there are other ways to avoid an error when things are not
|
|
163 |
+frobnicated.
|
|
164 |
+ |
|
165 |
+When folks modify this code, there may be uncertainty around whether the foos
|
|
166 |
+should always be frobnicated. The comments, the commit message, and issue #123
|
|
167 |
+should shed some light on that.
|
|
168 |
+ |
|
147 | 169 |
In the case that you have a commit which necessarily modifies multiple
|
148 | 170 |
components, then the summary line should still mention generally what
|
149 | 171 |
changed (if possible), followed by a colon and a brief summary.
|
... | ... | @@ -141,21 +141,24 @@ class GitMirror(SourceFetcher): |
141 | 141 |
fail="Failed to clone git repository {}".format(url),
|
142 | 142 |
fail_temporarily=True)
|
143 | 143 |
|
144 |
- # Attempt atomic rename into destination, this will fail if
|
|
145 |
- # another process beat us to the punch
|
|
146 |
- try:
|
|
147 |
- os.rename(tmpdir, self.mirror)
|
|
148 |
- except OSError as e:
|
|
149 |
- |
|
150 |
- # When renaming and the destination repo already exists, os.rename()
|
|
151 |
- # will fail with ENOTEMPTY, since an empty directory will be silently
|
|
152 |
- # replaced
|
|
153 |
- if e.errno == errno.ENOTEMPTY:
|
|
154 |
- self.source.status("{}: Discarding duplicate clone of {}"
|
|
155 |
- .format(self.source, url))
|
|
156 |
- else:
|
|
157 |
- raise SourceError("{}: Failed to move cloned git repository {} from '{}' to '{}': {}"
|
|
158 |
- .format(self.source, url, tmpdir, self.mirror, e)) from e
|
|
144 |
+ self._atomic_move_mirror(tmpdir, url)
|
|
145 |
+ |
|
146 |
+ def _atomic_move_mirror(self, tmpdir, url):
|
|
147 |
+ # Attempt atomic rename into destination, this will fail if
|
|
148 |
+ # another process beat us to the punch
|
|
149 |
+ try:
|
|
150 |
+ os.rename(tmpdir, self.mirror)
|
|
151 |
+ except OSError as e:
|
|
152 |
+ # When renaming and the destination repo already exists, os.rename()
|
|
153 |
+ # will fail with either ENOTEMPTY or EEXIST, depending on the underlying
|
|
154 |
+ # implementation.
|
|
155 |
+ # An empty directory would always be replaced.
|
|
156 |
+ if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
|
|
157 |
+ self.source.status("{}: Discarding duplicate clone of {}"
|
|
158 |
+ .format(self.source, url))
|
|
159 |
+ else:
|
|
160 |
+ raise SourceError("{}: Failed to move cloned git repository {} from '{}' to '{}': {}"
|
|
161 |
+ .format(self.source, url, tmpdir, self.mirror, e)) from e
|
|
159 | 162 |
|
160 | 163 |
def _fetch(self, alias_override=None):
|
161 | 164 |
url = self.source.translate_url(self.url,
|
... | ... | @@ -202,7 +202,7 @@ class ScriptElement(Element): |
202 | 202 |
sandbox.set_environment(self.get_environment())
|
203 | 203 |
|
204 | 204 |
# Tell the sandbox to mount the install root
|
205 |
- directories = {'/': False}
|
|
205 |
+ directories = {self.__install_root: False}
|
|
206 | 206 |
|
207 | 207 |
# Mark the artifact directories in the layout
|
208 | 208 |
for item in self.__layout:
|
... | ... | @@ -211,7 +211,10 @@ class ScriptElement(Element): |
211 | 211 |
directories[destination] = item['element'] or was_artifact
|
212 | 212 |
|
213 | 213 |
for directory, artifact in directories.items():
|
214 |
- sandbox.mark_directory(directory, artifact=artifact)
|
|
214 |
+ # Root does not need to be marked as it is always mounted
|
|
215 |
+ # with artifact (unless explicitly marked non-artifact)
|
|
216 |
+ if directory != '/':
|
|
217 |
+ sandbox.mark_directory(directory, artifact=artifact)
|
|
215 | 218 |
|
216 | 219 |
def stage(self, sandbox):
|
217 | 220 |
|
... | ... | @@ -115,6 +115,7 @@ def test_build_track(cli, datafiles, tmpdir, ref_storage, |
115 | 115 |
args += ['0.bst']
|
116 | 116 |
|
117 | 117 |
result = cli.run(project=project, silent=True, args=args)
|
118 |
+ result.assert_success()
|
|
118 | 119 |
tracked_elements = result.get_tracked_elements()
|
119 | 120 |
|
120 | 121 |
assert set(tracked_elements) == set(tracked)
|
1 |
+kind: script
|
|
2 |
+ |
|
3 |
+depends:
|
|
4 |
+- filename: base.bst
|
|
5 |
+ type: build
|
|
6 |
+- filename: script/corruption-image.bst
|
|
7 |
+ type: build
|
|
8 |
+ |
|
9 |
+config:
|
|
10 |
+ commands:
|
|
11 |
+ - echo smashed >>/canary
|
1 |
+kind: compose
|
|
2 |
+ |
|
3 |
+depends:
|
|
4 |
+- filename: base.bst
|
|
5 |
+ type: build
|
|
6 |
+ |
|
7 |
+public:
|
|
8 |
+ bst:
|
|
9 |
+ split-rules:
|
|
10 |
+ remove:
|
|
11 |
+ - "/tmp/**"
|
|
12 |
+ - "/tmp"
|
1 |
+kind: filter
|
|
2 |
+ |
|
3 |
+depends:
|
|
4 |
+- filename: script/marked-tmpdir.bst
|
|
5 |
+ type: build
|
|
6 |
+ |
|
7 |
+config:
|
|
8 |
+ exclude:
|
|
9 |
+ - remove
|
|
10 |
+ include-orphans: True
|
|
11 |
+ |
|
12 |
+ |
1 |
+kind: script
|
|
2 |
+ |
|
3 |
+depends:
|
|
4 |
+- filename: script/no-tmpdir.bst
|
|
5 |
+ type: build
|
|
6 |
+ |
|
7 |
+config:
|
|
8 |
+ commands:
|
|
9 |
+ - |
|
|
10 |
+ mkdir -p /tmp/blah
|
... | ... | @@ -184,3 +184,41 @@ def test_regression_cache_corruption(cli, tmpdir, datafiles): |
184 | 184 |
|
185 | 185 |
with open(os.path.join(checkout_after, 'canary')) as f:
|
186 | 186 |
assert f.read() == 'alive\n'
|
187 |
+ |
|
188 |
+ |
|
189 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
190 |
+def test_regression_tmpdir(cli, tmpdir, datafiles):
|
|
191 |
+ project = str(datafiles)
|
|
192 |
+ element_name = 'script/tmpdir.bst'
|
|
193 |
+ |
|
194 |
+ res = cli.run(project=project, args=['build', element_name])
|
|
195 |
+ assert res.exit_code == 0
|
|
196 |
+ |
|
197 |
+ |
|
198 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
199 |
+def test_regression_cache_corruption_2(cli, tmpdir, datafiles):
|
|
200 |
+ project = str(datafiles)
|
|
201 |
+ checkout_original = os.path.join(cli.directory, 'checkout-original')
|
|
202 |
+ checkout_after = os.path.join(cli.directory, 'checkout-after')
|
|
203 |
+ element_name = 'script/corruption-2.bst'
|
|
204 |
+ canary_element_name = 'script/corruption-image.bst'
|
|
205 |
+ |
|
206 |
+ res = cli.run(project=project, args=['build', canary_element_name])
|
|
207 |
+ assert res.exit_code == 0
|
|
208 |
+ |
|
209 |
+ res = cli.run(project=project, args=['checkout', canary_element_name,
|
|
210 |
+ checkout_original])
|
|
211 |
+ assert res.exit_code == 0
|
|
212 |
+ |
|
213 |
+ with open(os.path.join(checkout_original, 'canary')) as f:
|
|
214 |
+ assert f.read() == 'alive\n'
|
|
215 |
+ |
|
216 |
+ res = cli.run(project=project, args=['build', element_name])
|
|
217 |
+ assert res.exit_code == 0
|
|
218 |
+ |
|
219 |
+ res = cli.run(project=project, args=['checkout', canary_element_name,
|
|
220 |
+ checkout_after])
|
|
221 |
+ assert res.exit_code == 0
|
|
222 |
+ |
|
223 |
+ with open(os.path.join(checkout_after, 'canary')) as f:
|
|
224 |
+ assert f.read() == 'alive\n'
|
... | ... | @@ -21,11 +21,14 @@ |
21 | 21 |
#
|
22 | 22 |
|
23 | 23 |
import os
|
24 |
+from unittest import mock
|
|
24 | 25 |
import pytest
|
26 |
+import py.path
|
|
25 | 27 |
|
26 | 28 |
from buildstream._exceptions import ErrorDomain
|
27 |
-from buildstream import _yaml
|
|
29 |
+from buildstream import _yaml, SourceError
|
|
28 | 30 |
from buildstream.plugin import CoreWarnings
|
31 |
+from buildstream.plugins.sources.git import GitMirror
|
|
29 | 32 |
|
30 | 33 |
from tests.testutils import cli, create_repo
|
31 | 34 |
from tests.testutils.site import HAVE_GIT
|
... | ... | @@ -36,6 +39,64 @@ DATA_DIR = os.path.join( |
36 | 39 |
)
|
37 | 40 |
|
38 | 41 |
|
42 |
+@pytest.mark.parametrize(
|
|
43 |
+ ["type_", "setup"],
|
|
44 |
+ [
|
|
45 |
+ ("inexistent-dir", lambda path: None),
|
|
46 |
+ ("empty-dir", lambda path: path.mkdir()),
|
|
47 |
+ ("non-empty-dir", lambda path: path.join("bad").write("hi", ensure=True)),
|
|
48 |
+ ("file", lambda path: path.write("no")),
|
|
49 |
+ ],
|
|
50 |
+)
|
|
51 |
+def test_git_mirror_atomic_move(tmpdir, type_, setup):
|
|
52 |
+ # Create required elements
|
|
53 |
+ class DummySource:
|
|
54 |
+ def get_mirror_directory(self):
|
|
55 |
+ return str(tmpdir.join("source").mkdir())
|
|
56 |
+ |
|
57 |
+ status = mock.MagicMock()
|
|
58 |
+ |
|
59 |
+ url = "file://{}/url".format(tmpdir)
|
|
60 |
+ |
|
61 |
+ # Create fake download directory
|
|
62 |
+ download_dir = tmpdir.join("download_dir")
|
|
63 |
+ download_dir.join("oracle").write("hello", ensure=True)
|
|
64 |
+ |
|
65 |
+ # Initiate mirror
|
|
66 |
+ mirror = GitMirror(DummySource(), None, url, None)
|
|
67 |
+ |
|
68 |
+ # Setup destination directory
|
|
69 |
+ setup(py.path.local(mirror.mirror))
|
|
70 |
+ |
|
71 |
+ # Copy directory content
|
|
72 |
+ if type_ == "file":
|
|
73 |
+ with pytest.raises(SourceError):
|
|
74 |
+ mirror._atomic_move_mirror(str(download_dir), mirror.url)
|
|
75 |
+ else:
|
|
76 |
+ mirror._atomic_move_mirror(str(download_dir), mirror.url)
|
|
77 |
+ |
|
78 |
+ # Validate
|
|
79 |
+ if type_ == "non-empty-dir":
|
|
80 |
+ # With a non empty directory, we should not have overriden
|
|
81 |
+ # and notified a status
|
|
82 |
+ assert DummySource.status.called
|
|
83 |
+ |
|
84 |
+ expected_oracle = os.path.join(mirror.mirror, "bad")
|
|
85 |
+ expected_content = "hi"
|
|
86 |
+ elif type_ == "file":
|
|
87 |
+ expected_oracle = mirror.mirror
|
|
88 |
+ expected_content = "no"
|
|
89 |
+ else:
|
|
90 |
+ # Otherwise we should have a new directory and the data
|
|
91 |
+ expected_oracle = os.path.join(mirror.mirror, "oracle")
|
|
92 |
+ expected_content = "hello"
|
|
93 |
+ |
|
94 |
+ assert os.path.exists(expected_oracle)
|
|
95 |
+ |
|
96 |
+ with open(expected_oracle) as fp:
|
|
97 |
+ assert fp.read() == expected_content
|
|
98 |
+ |
|
99 |
+ |
|
39 | 100 |
@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
|
40 | 101 |
@pytest.mark.datafiles(os.path.join(DATA_DIR, 'template'))
|
41 | 102 |
def test_fetch_bad_ref(cli, tmpdir, datafiles):
|