Skip to content
This repository was archived by the owner on Nov 12, 2024. It is now read-only.

Commit 38cf9aa

Browse files
authored
Merge pull request #19 from cqse/project_subpath_option
Project Subpath Option
2 parents 6ef2efb + 51f0560 commit 38cf9aa

2 files changed

Lines changed: 99 additions & 21 deletions

File tree

teamscale_precommit_client/precommit_client.py

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
# Filename of the precommit configuration. The client expects this config file at the root of the repository.
2121
PRECOMMIT_CONFIG_FILENAME = '.teamscale-precommit.config'
22+
DEFAULT_PROJECT_SUBPATH = ''
2223
DEFAULT_PATH_PREFIX = ''
2324

2425

@@ -27,17 +28,20 @@ class PrecommitClient:
2728
# Number of seconds the client waits until fetching precommit results from the server.
2829
PRECOMMIT_WAITING_TIME_IN_SECONDS = 2
2930

30-
def __init__(self, teamscale_config, repository_path, path_prefix=DEFAULT_PATH_PREFIX, analyzed_file=None,
31-
verify=True,
32-
omit_links_to_findings=False, exclude_findings_in_changed_code=False, fetch_existing_findings=False,
33-
fetch_all_findings=False, fetch_existing_findings_in_changes=False, fail_on_red_findings=False,
34-
log_to_stderr=False):
31+
def __init__(self, teamscale_config, repository_path, path_prefix=DEFAULT_PATH_PREFIX, project_subpath='',
32+
analyzed_file=None,
33+
verify=True, omit_links_to_findings=False, exclude_findings_in_changed_code=False,
34+
fetch_existing_findings=False, fetch_all_findings=False, fetch_existing_findings_in_changes=False,
35+
fail_on_red_findings=False, log_to_stderr=False):
3536
"""Constructor"""
3637
self.teamscale_client = TeamscaleClient(teamscale_config.url, teamscale_config.username,
3738
teamscale_config.access_token, teamscale_config.project_id, verify)
3839
self.repository_path = repository_path
40+
3941
# calling os.path.join ensures a tailing '/'
4042
self.path_prefix = os.path.join(path_prefix, '')
43+
self.project_subpath = os.path.join(project_subpath, '')
44+
4145
self.analyzed_file = analyzed_file
4246
self.omit_links_to_findings = omit_links_to_findings
4347
self.exclude_findings_in_changed_code = exclude_findings_in_changed_code
@@ -110,21 +114,36 @@ def _upload_precommit_data(self):
110114
self.teamscale_client.branch = self.current_branch
111115

112116
print("Uploading changes on branch '%s' in '%s'..." % (self.current_branch, self.repository_path))
113-
changed_files_with_path_prefix = self._apply_path_prefix_to_changed_files()
114-
deleted_files_with_path_prefix = self._apply_path_prefix_to_deleted_files()
117+
118+
changed_files_in_project = self._filter_changed_files_in_project_subpath(self.changed_files)
119+
deleted_files_in_project = self._filter_deleted_files_in_project_subpath(self.deleted_files)
120+
121+
changed_files_with_path_prefix = self._apply_path_prefix_to_changed_files(changed_files_in_project)
122+
deleted_files_with_path_prefix = self._apply_path_prefix_to_deleted_files(deleted_files_in_project)
123+
115124
precommit_data = PreCommitUploadData(uniformPathToContentMap=changed_files_with_path_prefix,
116125
deletedUniformPaths=deleted_files_with_path_prefix)
117126
self.teamscale_client.upload_files_for_precommit_analysis(
118127
datetime.datetime.fromtimestamp(self.parent_commit_timestamp), precommit_data)
119128

120-
def _apply_path_prefix_to_changed_files(self):
129+
def _filter_changed_files_in_project_subpath(self, uniform_path_content_map):
130+
project_files_map = {}
131+
for key in uniform_path_content_map.keys():
132+
if key.startswith(self.project_subpath):
133+
project_files_map[key] = uniform_path_content_map[key]
134+
return project_files_map
135+
136+
def _filter_deleted_files_in_project_subpath(self, deleted_files):
137+
return list(filter(lambda path: path.startswith(self.project_subpath), deleted_files))
138+
139+
def _apply_path_prefix_to_changed_files(self, changed_files):
121140
map_with_prefixes = {}
122-
for key in self.changed_files.keys():
123-
map_with_prefixes[self.path_prefix + key] = self.changed_files[key]
141+
for key in changed_files.keys():
142+
map_with_prefixes[self.path_prefix + key] = changed_files[key]
124143
return map_with_prefixes
125144

126-
def _apply_path_prefix_to_deleted_files(self):
127-
return list(map(lambda path: self.path_prefix + path, self.deleted_files))
145+
def _apply_path_prefix_to_deleted_files(self, deleted_files):
146+
return list(map(lambda path: self.path_prefix + path, deleted_files))
128147

129148
def _wait_and_get_precommit_result(self):
130149
"""Gets the current precommit results. Waits synchronously until server is ready. """
@@ -143,9 +162,18 @@ def _print_findings(self, message, findings, branch):
143162

144163
self._print('', log_to_stderr)
145164
self._print(message, log_to_stderr)
146-
for formatted_finding in self._format_findings(findings, branch):
165+
166+
findings_without_path_prefix = list(
167+
map(lambda finding: self._copy_finding_without_path_prefix(finding), findings))
168+
169+
findings_in_project = self._remove_findings_outside_project_subpath(findings_without_path_prefix)
170+
171+
for formatted_finding in self._format_findings(findings_in_project, branch):
147172
self._print(formatted_finding, log_to_stderr)
148173

174+
def _remove_findings_outside_project_subpath(self, findings):
175+
return list(filter(lambda finding: finding.uniformPath.startswith(self.project_subpath), findings))
176+
149177
@staticmethod
150178
def _print(message, print_to_err=False):
151179
if print_to_err:
@@ -199,9 +227,7 @@ def _format_findings(self, findings, branch):
199227
if len(findings) == 0:
200228
return ['> No findings.']
201229

202-
findings_without_path_prefix = list(
203-
map(lambda finding: self._copy_finding_without_path_prefix(finding), findings))
204-
sorted_findings = sorted(findings_without_path_prefix)
230+
sorted_findings = sorted(findings)
205231
return [self._format_message(finding) for finding in sorted_findings]
206232

207233
def _format_message(self, finding):
@@ -271,6 +297,9 @@ def _parse_args():
271297
parser.add_argument('--log-to-stderr', dest='log_to_stderr', action='store_true',
272298
help='When this option is set, any finding will be logged to stderr instead of stdout: '
273299
'(default: False)')
300+
parser.add_argument('--project-subpath', metavar='project-subpath', type=str, default=DEFAULT_PROJECT_SUBPATH,
301+
help='Project path relative to the git repository. '
302+
'Pre-commit analysis will only be performed for files under this path.')
274303
parser.add_argument('--path-prefix', metavar='PATH_PREFIX', type=str,
275304
help='Path prefix on Teamscale as configured with "Prepend repository identifier" or '
276305
'"Path prefix transformation". Please use "/" to separate folders.',
@@ -294,7 +323,7 @@ def _configure_precommit_client(parsed_args):
294323
config_file = os.path.join(repo_path, PRECOMMIT_CONFIG_FILENAME)
295324
config = get_teamscale_client_configuration(config_file)
296325
return PrecommitClient(config, repository_path=repo_path, path_prefix=parsed_args.path_prefix,
297-
analyzed_file=path_to_file_in_repo,
326+
project_subpath=parsed_args.project_subpath, analyzed_file=path_to_file_in_repo,
298327
verify=parsed_args.verify, omit_links_to_findings=parsed_args.omit_links_to_findings,
299328
exclude_findings_in_changed_code=parsed_args.exclude_findings_in_changed_code,
300329
fetch_existing_findings=parsed_args.fetch_existing_findings,

tests/precommit_test.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import os
12
import re
23
import sys
34
from io import StringIO
45

56
import responses
67

8+
from teamscale_precommit_client.precommit_client import DEFAULT_PROJECT_SUBPATH
9+
710
# The mock package is only available from Python 3.3 onwards. Thank you, Python.
811
if sys.version_info >= (3, 3):
912
from unittest.mock import Mock
@@ -136,6 +139,50 @@ def test_get_added_and_existing_findings_in_changes_for_changes(self):
136139
self.assert_findings_ids(self.precommit_client.findings_in_changed_code, [])
137140
self.assert_findings_ids(self.precommit_client.existing_findings, [4, 5, 6, 7])
138141

142+
@responses.activate
143+
def test_only_upload_files_in_project_subpath(self):
144+
project_subpath = 'project'
145+
path_outside_project = 'another_path'
146+
147+
changed_file_in_project = os.path.join(project_subpath, ANALYZED_FILE_NAME)
148+
changed_file_outside_project = os.path.join(path_outside_project, ANALYZED_FILE_NAME)
149+
deleted_file_in_project = os.path.join(project_subpath, DELETED_FILE_NAME)
150+
deleted_file_outside_project = os.path.join(path_outside_project, DELETED_FILE_NAME)
151+
152+
self.precommit_client = self._get_precommit_client(
153+
{changed_file_in_project: '',
154+
changed_file_outside_project: ''},
155+
[deleted_file_in_project, deleted_file_outside_project],
156+
project_subpath=project_subpath)
157+
self.mock_precommit_findings_churn()
158+
159+
self.precommit_client.run()
160+
161+
precommit_request = next(call.request for call in responses.calls if call.request.method == 'PUT')
162+
163+
self.assertIn(changed_file_in_project, precommit_request.body)
164+
self.assertNotIn(changed_file_outside_project, precommit_request.body)
165+
self.assertIn(deleted_file_in_project, precommit_request.body)
166+
self.assertNotIn(deleted_file_outside_project, precommit_request.body)
167+
168+
@responses.activate
169+
def test_only_print_findings_in_project_subpath(self):
170+
project_subpath = 'project'
171+
172+
self.precommit_client = self._get_precommit_client(self._get_no_changed_files(), self._get_no_deleted_files(),
173+
project_subpath=project_subpath, fetch_all_findings=True)
174+
# The mocked finding does not have the project sub-path
175+
self.mock_existing_findings(CURRENT_BRANCH, [1])
176+
177+
captured_output = StringIO()
178+
sys.stdout = captured_output
179+
180+
self.precommit_client.run()
181+
182+
# We only mocked one finding which is not located under the project sub-path,
183+
# so we expect it to be removed in the output
184+
self.assertNotIn(ANALYZED_FILE_NAME, captured_output.getvalue())
185+
139186
@responses.activate
140187
def test_adding_path_prefix(self):
141188
path_prefix = 'prefix'
@@ -202,15 +249,17 @@ def get_project_service_mock(service_id, branch=''):
202249

203250
@staticmethod
204251
def _get_precommit_client(changed_files, deleted_files, path_prefix=DEFAULT_PATH_PREFIX,
205-
fetch_existing_findings=False,
206-
fetch_existing_findings_in_changes=False):
252+
project_subpath=DEFAULT_PROJECT_SUBPATH, fetch_existing_findings=False,
253+
fetch_existing_findings_in_changes=False, fetch_all_findings=False):
207254
"""Gets a precommit client some of whose methods are mocked out for testing."""
208255
responses.add(responses.GET, PrecommitClientTest.get_global_service_mock('service-api-info'), status=200,
209256
content_type="application/json", body='{"apiVersion": 6}')
210257
precommit_client = PrecommitClient(PrecommitClientTest._get_precommit_client_config(),
211258
repository_path=REPO_PATH, path_prefix=path_prefix,
212-
analyzed_file=ANALYZED_FILE_PATH, verify=False,
213-
omit_links_to_findings=True, fetch_existing_findings=fetch_existing_findings,
259+
project_subpath=project_subpath, analyzed_file=ANALYZED_FILE_PATH,
260+
verify=False, omit_links_to_findings=True,
261+
fetch_all_findings=fetch_all_findings,
262+
fetch_existing_findings=fetch_existing_findings,
214263
fetch_existing_findings_in_changes=fetch_existing_findings_in_changes)
215264
precommit_client._calculate_modifications = Mock()
216265
precommit_client.current_branch = CURRENT_BRANCH

0 commit comments

Comments
 (0)