Merge ~racb/git-ubuntu:decouple-gitubunturepository into git-ubuntu:master

Proposed by Robie Basak
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)
Reviewer Review Type Date Requested Status
Nish Aravamudan Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+337564@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal

PASSED: Continuous integration, rev:a71ff92849ae06203ce7c2c5d7bdce355f042560
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/279/
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://jenkins.ubuntu.com/server/job/git-ubuntu-ci/279/rebuild

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal

PASSED: Continuous integration, rev:4d1d2bdb1d514bcf5bf0ac3e736bf88ff372dbca
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/281/
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://jenkins.ubuntu.com/server/job/git-ubuntu-ci/281/rebuild

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:ebf9415338956776bfc27cd84d1ab4bc40e1d2bb
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/283/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    FAILED: Integration Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/283/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:ebf9415338956776bfc27cd84d1ab4bc40e1d2bb
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/285/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    FAILED: Integration Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/285/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:ebf9415338956776bfc27cd84d1ab4bc40e1d2bb
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/287/
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://jenkins.ubuntu.com/server/job/git-ubuntu-ci/287/rebuild

review: Approve (continuous-integration)
Revision history for this message
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._derive_env())

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:b7d356fd39f76a2498866c639fbdbcbf168b7188
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/288/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    FAILED: Integration Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/288/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:b7d356fd39f76a2498866c639fbdbcbf168b7188
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/289/
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://jenkins.ubuntu.com/server/job/git-ubuntu-ci/289/rebuild

review: Approve (continuous-integration)
Revision history for this message
Nish Aravamudan (nacc) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py
2index 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
290diff --git a/gitubuntu/importer.py b/gitubuntu/importer.py
291index 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
329diff --git a/gitubuntu/queue.py b/gitubuntu/queue.py
330index 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
342diff --git a/gitubuntu/source_builder_test.py b/gitubuntu/source_builder_test.py
343index 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
364diff --git a/gitubuntu/test_git_repository.py b/gitubuntu/test_git_repository.py
365index 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

Subscribers

People subscribed via source and target branches