Merge ~racb/git-ubuntu:decouple-gitubunturepository into git-ubuntu:master
- Git
- lp:~racb/git-ubuntu
- decouple-gitubunturepository
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | bb9fecb4c49280eb3c2178772a856e0abe29e2c1 |
Proposed branch: | ~racb/git-ubuntu:decouple-gitubunturepository |
Merge into: | git-ubuntu:master |
Diff against target: |
423 lines (+220/-52) 5 files modified
gitubuntu/git_repository.py (+179/-36) gitubuntu/importer.py (+8/-4) gitubuntu/queue.py (+1/-1) gitubuntu/source_builder_test.py (+2/-2) gitubuntu/test_git_repository.py (+30/-9) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Nish Aravamudan | Approve | ||
Server Team CI bot | continuous-integration | Approve | |
Review via email:
|
This proposal supersedes a proposal from 2018-02-07.
Commit message
Make Jenkins happy
Description of the change
This MP focuses on refactoring and pulling out methods in GitUbuntuRepository for operations that don't really need a GitUbuntuRepository instance. This allows for easier unit testing.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:4d1d2bdb1d5
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Style Check
SUCCESS: Unit Tests
SUCCESS: Integration Tests
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:ebf94153389
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Style Check
SUCCESS: Unit Tests
FAILED: Integration Tests
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:ebf94153389
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Style Check
SUCCESS: Unit Tests
FAILED: Integration Tests
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:ebf94153389
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Style Check
SUCCESS: Unit Tests
SUCCESS: Integration Tests
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Robie Basak (racb) wrote : | # |
Just noticed a potential bug in the first commit that needs checking. @lru_cache will mean that every caller gets the same dictionary. Perhaps I should wrap the env property getter with a dict copy to make sure that one caller cannot accidentally modify the result given to another caller.
In fact I think I'd like to definitely do this for robustness. Please review assuming that this change is made:
- return self._derive_env()
+ return dict(self.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:b7d356fd39f
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Style Check
SUCCESS: Unit Tests
FAILED: Integration Tests
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:b7d356fd39f
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Style Check
SUCCESS: Unit Tests
SUCCESS: Integration Tests
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Nish Aravamudan (nacc) : | # |
Preview Diff
1 | diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py |
2 | index f3bd406..d574783 100644 |
3 | --- a/gitubuntu/git_repository.py |
4 | +++ b/gitubuntu/git_repository.py |
5 | @@ -109,6 +109,99 @@ def follow_symlinks_to_blob(repo, treeish_object, path): |
6 | ) |
7 | |
8 | |
9 | +def _derive_git_cli_env( |
10 | + pygit2_repo, |
11 | + initial_env=None, |
12 | + update_env=None, |
13 | + work_tree_path=None, |
14 | + index_path=None, |
15 | +): |
16 | + """Calculate the environment to be used in a call to the git CLI |
17 | + |
18 | + :param pygit2.Repository pygit2_repo: the repository for which to calculate |
19 | + the environment |
20 | + :param dict initial_env: the environment to start with |
21 | + :param dict update_env: additional environment setings with which to |
22 | + override the result |
23 | + :param str work_tree_path: in the case of an alternate work tree being |
24 | + used, specify this here and GIT_WORK_TREE will be set to it instead of |
25 | + the default being taken from the work tree used by pygit2_repo |
26 | + :param str index_path: if an alternate index is being used, specify it here |
27 | + and GIT_INDEX_FILE will be set accordingly. |
28 | + :rtype: dict |
29 | + :returns: a dictionary representing the environment with which to call the |
30 | + git CLI |
31 | + |
32 | + This function encapsulates the setting of the GIT_DIR, GIT_WORK_TREE and |
33 | + GIT_INDEX_FILE environment variables as necessary. The provided |
34 | + pygit2.Repository instance is used to determine these values. initial_env, |
35 | + if provided, specifies the initial environment to use instead of defaulting |
36 | + to the process' current environment. update_env allows extra environment |
37 | + variables to be added as well as the override of any variables set by this |
38 | + function, including GIT_DIR, GIT_WORK_TREE and GIT_INDEX_FILE. |
39 | + """ |
40 | + if initial_env is None: |
41 | + env = os.environ.copy() |
42 | + else: |
43 | + env = initial_env.copy() |
44 | + |
45 | + env['GIT_DIR'] = pygit2_repo.path |
46 | + |
47 | + if work_tree_path is None: |
48 | + env['GIT_WORK_TREE'] = pygit2_repo.workdir |
49 | + else: |
50 | + env['GIT_WORK_TREE'] = work_tree_path |
51 | + |
52 | + if index_path is not None: |
53 | + env['GIT_INDEX_FILE'] = index_path |
54 | + |
55 | + if update_env: |
56 | + env.update(update_env) |
57 | + |
58 | + return env |
59 | + |
60 | + |
61 | +def git_run( |
62 | + pygit2_repo, |
63 | + args, |
64 | + initial_env=None, |
65 | + update_env=None, |
66 | + work_tree_path=None, |
67 | + index_path=None, |
68 | + **kwargs |
69 | +): |
70 | + """Run the git CLI with the provided arguments |
71 | + |
72 | + :param pygit2.Repository: the repository on which to act |
73 | + :param list(str) args: arguments to the git CLI |
74 | + :param dict initial_env: the environment to use |
75 | + :param dict update_env: additional environment variables and overrides |
76 | + :param dict **kwargs: further arguments to pass through to |
77 | + gitubuntu.run.run() |
78 | + :raises subprocess.CalledProcessError: if git exits non-zero |
79 | + :rtype: (str, str) |
80 | + :returns: stdout and stderr strings containing the subprocess output |
81 | + |
82 | + If initial_env is not set, it defaults to the current process' environment. |
83 | + |
84 | + The GIT_DIR, GIT_WORK_TREE and GIT_INDEX_FILE environment variables are set |
85 | + automatically as necessary based on the repository's existing location and |
86 | + settings. |
87 | + |
88 | + If update_env is set, then the environment to be used is updated with env |
89 | + before the call to git is made. This can override GIT_DIR, |
90 | + GIT_WORK_TREE, GIT_INDEX_FILE and anything else. |
91 | + """ |
92 | + env = _derive_git_cli_env( |
93 | + pygit2_repo=pygit2_repo, |
94 | + initial_env=initial_env, |
95 | + update_env=update_env, |
96 | + work_tree_path=work_tree_path, |
97 | + index_path=index_path, |
98 | + ) |
99 | + return run(['git'] + list(args), env=env, **kwargs) |
100 | + |
101 | + |
102 | class RenameableDir: |
103 | """An on-disk directory that can be renamed and traversed recursively. |
104 | |
105 | @@ -779,10 +872,24 @@ class GitUbuntuRepository: |
106 | self._local_dir = local_dir |
107 | |
108 | self.raw_repo = pygit2.init_repository(self._local_dir) |
109 | + # We rely on raw_repo.workdir to be identical to self._local_dir to |
110 | + # avoid changing previous behaviour in the setting of GIT_WORK_TREE, so |
111 | + # assert that it is so. This may not be the case if the git repository |
112 | + # has a different workdir stored in its configuration or if the git |
113 | + # repository is a bare repository. We didn't handle these cases before |
114 | + # anyway, so with this assertion we can fail noisily and early. |
115 | + assert ( |
116 | + os.path.normpath(self.raw_repo.workdir) == |
117 | + os.path.normpath(self._local_dir) |
118 | + ) |
119 | |
120 | - self._env = os.environ.copy() |
121 | - self._env['GIT_DIR'] = self.raw_repo.path |
122 | - self._env['GIT_WORK_TREE'] = self._local_dir |
123 | + # Since previous behaviour of this class depended on the state of the |
124 | + # environment at the time it was constructed, save this for later use |
125 | + # (for example in deriving the environment to use for calls to the git |
126 | + # CLI). This permits the behaviour to remain identical for now. |
127 | + # Eventually we can break previous behaviour and eliminate the need for |
128 | + # this. |
129 | + self._initial_env = os.environ.copy() |
130 | |
131 | self.set_git_attributes() |
132 | |
133 | @@ -1224,7 +1331,24 @@ class GitUbuntuRepository: |
134 | |
135 | @property |
136 | def env(self): |
137 | - return self._env |
138 | + # Return a copy of the cached _derive_env method result so that the |
139 | + # caller cannot inadvertently modify our cached answer. Unfortunately |
140 | + # this leaks the lru_cache-ness of the _derive_env method to this |
141 | + # property getter, but this seems better than nothing. |
142 | + return dict(self._derive_env()) |
143 | + |
144 | + @lru_cache() |
145 | + def _derive_env(self): |
146 | + """Determine what the git CLI environment should be |
147 | + |
148 | + This depends on the initial environment saved from the constructor and |
149 | + the paths associated with self.raw_repo, neither of which should change |
150 | + in the lifetime of this class instance. |
151 | + """ |
152 | + return _derive_git_cli_env( |
153 | + self.raw_repo, |
154 | + initial_env=self._initial_env |
155 | + ) |
156 | |
157 | @property |
158 | def local_dir(self): |
159 | @@ -1308,12 +1432,33 @@ class GitUbuntuRepository: |
160 | return stdout.strip() |
161 | |
162 | def git_run(self, args, env=None, **kwargs): |
163 | - # Explicitly take a copy of self._env so the following update doesn't |
164 | - # accidentally update the main instance _env dictionary in-place |
165 | - combined_env = dict(self._env) |
166 | - if env is not None: |
167 | - combined_env.update(env) |
168 | - return run(['git'] + args, env=combined_env, **kwargs) |
169 | + """Run the git CLI with the provided arguments |
170 | + |
171 | + :param list(str) args: arguments to the git CLI |
172 | + :param dict env: additional environment variables to use |
173 | + :param dict **kwargs: further arguments to pass through to |
174 | + gitubuntu.run.run() |
175 | + :raises subprocess.CalledProcessError: if git exits non-zero |
176 | + :rtype: (str, str) |
177 | + :returns: stdout and stderr strings containing the subprocess output |
178 | + |
179 | + The environment used is based on the Python process' environment at the |
180 | + time this class instance was constructed. |
181 | + |
182 | + The GIT_DIR and GIT_WORK_TREE environment variables are set |
183 | + automatically based on the repository's existing location and settings. |
184 | + |
185 | + If env is set, then the environment to be used is updated with env |
186 | + before the call to git is made. This can override GIT_DIR, |
187 | + GIT_WORK_TREE, and anything else. |
188 | + """ |
189 | + return git_run( |
190 | + pygit2_repo=self.raw_repo, |
191 | + args=args, |
192 | + initial_env=self._initial_env, |
193 | + update_env=env, |
194 | + **kwargs, |
195 | + ) |
196 | |
197 | def garbage_collect(self): |
198 | self.git_run(['gc']) |
199 | @@ -1626,9 +1771,9 @@ class GitUbuntuRepository: |
200 | def clean_repository_state(self): |
201 | """Cleanup working tree""" |
202 | runq(['git', 'checkout', '--orphan', 'master'], |
203 | - check=False, env=self._env) |
204 | - runq(['git', 'reset', '--hard'], env=self._env) |
205 | - runq(['git', 'clean', '-f', '-d'], env=self._env) |
206 | + check=False, env=self.env) |
207 | + runq(['git', 'reset', '--hard'], env=self.env) |
208 | + runq(['git', 'clean', '-f', '-d'], env=self.env) |
209 | |
210 | def get_all_changelog_versions_from_treeish(self, treeish): |
211 | changelog = self.get_changelog_from_treeish(treeish) |
212 | @@ -1795,9 +1940,13 @@ class GitUbuntuRepository: |
213 | else: |
214 | return tree_builder.write() # create replacement tree object |
215 | |
216 | - def dir_to_tree(self, path, escape=False): |
217 | + @classmethod |
218 | + def dir_to_tree(cls, pygit2_repo, path, escape=False): |
219 | """Create a git tree object from the given filesystem path |
220 | |
221 | + :param pygit2.Repository pygit2_repo: the repository on which to |
222 | + operate. If you have a GitUbuntuRepository instance, you can use |
223 | + its raw_repo property. |
224 | :param path: path to filesystem directory to be the root of the tree |
225 | :param escape: if True, escape using escape_dot_git() first. This |
226 | mutates the provided filesystem tree. |
227 | @@ -1821,30 +1970,24 @@ class GitUbuntuRepository: |
228 | # we can use safely. |
229 | with tempfile.TemporaryDirectory() as index_dir: |
230 | index_path = os.path.join(index_dir, 'index') |
231 | - index_env = {'GIT_INDEX_FILE': index_path} |
232 | - self.git_run( |
233 | - ['--work-tree', path, 'add', '-f', '-A'], |
234 | - env=index_env, |
235 | - ) |
236 | - self.git_run( |
237 | - ['--work-tree', path, 'reset', 'HEAD', '--', '.git',], |
238 | - env=index_env, |
239 | - ) |
240 | - self.git_run( |
241 | - ['--work-tree', path, 'reset', 'HEAD', '--', '.pc',], |
242 | - env=index_env, |
243 | - ) |
244 | - tree_hash_str, _ = self.git_run( |
245 | - ['--work-tree', path, 'write-tree'], |
246 | - env=index_env, |
247 | - ) |
248 | + def indexed_git_run(*args): |
249 | + return git_run( |
250 | + pygit2_repo=pygit2_repo, |
251 | + args=args, |
252 | + work_tree_path=path, |
253 | + index_path=index_path, |
254 | + ) |
255 | + indexed_git_run('add', '-f', '-A') |
256 | + indexed_git_run('reset', 'HEAD', '--', '.git') |
257 | + indexed_git_run('reset', 'HEAD', '--', '.pc') |
258 | + tree_hash_str, _ = indexed_git_run('write-tree') |
259 | tree_hash_str = tree_hash_str.strip() |
260 | - tree = self.raw_repo.get(tree_hash_str) |
261 | + tree = pygit2_repo.get(tree_hash_str) |
262 | |
263 | # Add any empty directories that git did not import. Workaround for LP: |
264 | # #1687057. |
265 | - replacement_oid = self._add_missing_tree_dirs( |
266 | - repo=self.raw_repo, |
267 | + replacement_oid = cls._add_missing_tree_dirs( |
268 | + repo=pygit2_repo, |
269 | top_path=path, |
270 | top_tree_object=tree, |
271 | ) |
272 | @@ -2194,7 +2337,7 @@ with other relevant fields (see below for details). |
273 | with open(fixup_patch_path, 'rb') as f: |
274 | run(['patch', '-Rp1',], input=f.read()) |
275 | |
276 | - return self.dir_to_tree('.') |
277 | + return self.dir_to_tree(self.raw_repo, '.') |
278 | else: |
279 | return commit_tree_hash |
280 | |
281 | @@ -2291,7 +2434,7 @@ with other relevant fields (see below for details). |
282 | ], |
283 | env=self.env, |
284 | ) |
285 | - return self.dir_to_tree('.') |
286 | + return self.dir_to_tree(self.raw_repo, '.') |
287 | |
288 | # otherwise, return @commit_hash's tree hash |
289 | return commit_tree_hash |
290 | diff --git a/gitubuntu/importer.py b/gitubuntu/importer.py |
291 | index c998cf2..9f9c1ee 100644 |
292 | --- a/gitubuntu/importer.py |
293 | +++ b/gitubuntu/importer.py |
294 | @@ -90,10 +90,10 @@ class ParentOverrideError(GitUbuntuImportError): |
295 | pass |
296 | |
297 | |
298 | -def dsc_to_tree_hash(repo, dsc_path): |
299 | +def dsc_to_tree_hash(pygit2_repo, dsc_path): |
300 | '''Convert a dsc file into a git tree in the given repo |
301 | |
302 | - repo: GitUbuntuRepository object |
303 | + pygit2_repo: pygit2.Repository object |
304 | dsc_path: string path to dsc file |
305 | |
306 | Returns: tree hash as a hex string |
307 | @@ -122,7 +122,11 @@ def dsc_to_tree_hash(repo, dsc_path): |
308 | "dpkg-source -x" |
309 | ) |
310 | |
311 | - return repo.dir_to_tree(extracted_dir, escape=True) |
312 | + return GitUbuntuRepository.dir_to_tree( |
313 | + pygit2_repo, |
314 | + extracted_dir, |
315 | + escape=True, |
316 | + ) |
317 | |
318 | |
319 | def cleanup(no_clean, local_dir): |
320 | @@ -730,7 +734,7 @@ def import_patches_unapplied_tree(repo, dsc_pathname): |
321 | dsc_pathname - A string path to a DSC file |
322 | """ |
323 | |
324 | - import_tree_hash = dsc_to_tree_hash(repo, dsc_pathname) |
325 | + import_tree_hash = dsc_to_tree_hash(repo.raw_repo, dsc_pathname) |
326 | repo.clean_repository_state() |
327 | |
328 | return import_tree_hash |
329 | diff --git a/gitubuntu/queue.py b/gitubuntu/queue.py |
330 | index 8c01b1f..8ea4690 100644 |
331 | --- a/gitubuntu/queue.py |
332 | +++ b/gitubuntu/queue.py |
333 | @@ -196,7 +196,7 @@ def _commit_upload(repo, upload, parent_commit): |
334 | |
335 | # Import the source package into a git tree object |
336 | tree_hash = gitubuntu.importer.dsc_to_tree_hash( |
337 | - repo, |
338 | + repo.raw_repo, |
339 | os.path.join(tmpdir, download_key['dsc']), |
340 | ) |
341 | |
342 | diff --git a/gitubuntu/source_builder_test.py b/gitubuntu/source_builder_test.py |
343 | index 0960836..b5eb896 100644 |
344 | --- a/gitubuntu/source_builder_test.py |
345 | +++ b/gitubuntu/source_builder_test.py |
346 | @@ -16,7 +16,7 @@ def dsc_path_to_tree(repo, dsc_path): |
347 | :param str dsc_path: path to dsc file |
348 | :rtype: pygit2.Tree |
349 | """ |
350 | - tree_hash = importer.dsc_to_tree_hash(repo, dsc_path) |
351 | + tree_hash = importer.dsc_to_tree_hash(repo.raw_repo, dsc_path) |
352 | return repo.raw_repo.get(tree_hash) |
353 | |
354 | |
355 | @@ -27,7 +27,7 @@ def test_source_is_created(): |
356 | |
357 | def test_source_create_with_version(repo): |
358 | with target.Source(target.SourceSpec(version=3)) as f: |
359 | - tree_hash = importer.dsc_to_tree_hash(repo, f) |
360 | + tree_hash = importer.dsc_to_tree_hash(repo.raw_repo, f) |
361 | changelog = repo.get_changelog_from_treeish(tree_hash) |
362 | assert changelog.version == '3' |
363 | |
364 | diff --git a/gitubuntu/test_git_repository.py b/gitubuntu/test_git_repository.py |
365 | index dcb7d0e..4e29f9a 100644 |
366 | --- a/gitubuntu/test_git_repository.py |
367 | +++ b/gitubuntu/test_git_repository.py |
368 | @@ -342,21 +342,31 @@ def test_escape_dot_git_ordering(direction): |
369 | assert all(x is y for x, y in zip(top._rename_record, expected_order)) |
370 | |
371 | |
372 | -def test_empty_dir_to_tree(repo, tmpdir): |
373 | - tree_hash = repo.dir_to_tree(str(tmpdir)) |
374 | - assert tree_hash == str(Tree({}).write(repo.raw_repo)) |
375 | +def test_empty_dir_to_tree(pygit2_repo, tmpdir): |
376 | + tree_hash = target.GitUbuntuRepository.dir_to_tree( |
377 | + pygit2_repo, |
378 | + str(tmpdir), |
379 | + ) |
380 | + assert tree_hash == str(Tree({}).write(pygit2_repo)) |
381 | |
382 | |
383 | -def test_onefile_dir_to_tree(repo, tmpdir): |
384 | +def test_onefile_dir_to_tree(pygit2_repo, tmpdir): |
385 | tmpdir.join('foo').write('bar') |
386 | - tree_hash = repo.dir_to_tree(str(tmpdir)) |
387 | - assert tree_hash == str(Tree({'foo': Blob(b'bar')}).write(repo.raw_repo)) |
388 | + tree_hash = target.GitUbuntuRepository.dir_to_tree( |
389 | + pygit2_repo, |
390 | + str(tmpdir), |
391 | + ) |
392 | + assert tree_hash == str(Tree({'foo': Blob(b'bar')}).write(pygit2_repo)) |
393 | |
394 | |
395 | -def test_git_escape_dir_to_tree(repo, tmpdir): |
396 | +def test_git_escape_dir_to_tree(pygit2_repo, tmpdir): |
397 | tmpdir.mkdir('.git') |
398 | - tree_hash = repo.dir_to_tree(str(tmpdir), escape=True) |
399 | - assert tree_hash == str(Tree({'..git': Tree({})}).write(repo.raw_repo)) |
400 | + tree_hash = target.GitUbuntuRepository.dir_to_tree( |
401 | + pygit2_repo, |
402 | + str(tmpdir), |
403 | + escape=True, |
404 | + ) |
405 | + assert tree_hash == str(Tree({'..git': Tree({})}).write(pygit2_repo)) |
406 | |
407 | |
408 | @pytest.mark.parametrize('tree_data,expected_path', [ |
409 | @@ -450,3 +460,14 @@ def test_repo_quilt_env_from_treeish_str(repo): |
410 | } |
411 | for k, v in expected_inside.items(): |
412 | assert env[k] == v |
413 | + |
414 | + |
415 | +def test_repo_derive_env_change(repo): |
416 | + # Changing the dictionary of a GitUbuntuRepository instance env attribute |
417 | + # must not have any effect on the env itself. While this may stretch a |
418 | + # little further than a normal instance property, it's worth enforcing this |
419 | + # as this particular attribute is at particular risk due to how it tends to |
420 | + # be used. |
421 | + e1 = repo.env |
422 | + e1[unittest.mock.sentinel.k] = unittest.mock.sentinel.v |
423 | + assert unittest.mock.sentinel.k not in repo.env |
PASSED: Continuous integration, rev:a71ff92849a e06203ce7c2c5d7 bdce355f042560 /jenkins. ubuntu. com/server/ job/git- ubuntu- ci/279/
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Style Check
SUCCESS: Unit Tests
SUCCESS: Integration Tests
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/git- ubuntu- ci/279/ rebuild
https:/