Skip to content

Commit 7765aae

Browse files
jonahdcCTY-git
andauthored
Add new step for SonarQube results (#1104)
* add ScanSonar step * add tests * use optional * add azure devops for pr creation * fix ScanSonar to get code contexts as well * fix tests * fix again * save * update init * bump version * bump and record outputs * update * Update patched.py * bump to 0.0.84 * fix error message --------- Co-authored-by: TIANYOU CHEN <[email protected]>
1 parent 16d47d3 commit 7765aae

File tree

23 files changed

+1201
-474
lines changed

23 files changed

+1201
-474
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
path: dist/
7474

7575
- name: Sign the dists with Sigstore
76-
uses: sigstore/gh-action-sigstore-python@v3
76+
uses: sigstore/gh-action-sigstore-python@v3.0.0
7777
with:
7878
inputs: >-
7979
./dist/*.tar.gz

patchwork/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,10 @@ def cli(
238238
if not disable_telemetry:
239239
patched.send_public_telemetry(patchflow_name, inputs)
240240

241-
with patched.patched_telemetry(patchflow_name, {}):
241+
with patched.patched_telemetry(patchflow_name, {}) as output_dict:
242242
patchflow_instance = patchflow_class(inputs)
243-
patchflow_instance.run()
243+
patchflow_output = patchflow_instance.run()
244+
output_dict.update(patchflow_output)
244245
except Exception as e:
245246
logger.debug(traceback.format_exc())
246247
logger.error(f"Error running patchflow {patchflow}: {e}")

patchwork/common/client/patched.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ class PatchedClient(click.ParamType):
7676
ALLOWED_TELEMETRY_KEYS = {
7777
"model",
7878
}
79+
ALLOWED_TELEMETRY_OUTPUT_KEYS = {
80+
"pr_url",
81+
"issue_url",
82+
}
7983

8084
def __init__(self, access_token: str, url: str = DEFAULT_PATCH_URL):
8185
self.access_token = access_token
@@ -140,6 +144,15 @@ def __handle_telemetry_inputs(self, inputs: dict[str, Any]) -> dict:
140144

141145
return inputs_copy
142146

147+
def __handle_telemetry_outputs(self, outputs: dict[str, Any]) -> dict:
148+
diff_keys = set(outputs.keys()).difference(self.ALLOWED_TELEMETRY_OUTPUT_KEYS)
149+
150+
outputs_copy = outputs.copy()
151+
for key in diff_keys:
152+
del outputs_copy[key]
153+
154+
return outputs_copy
155+
143156
async def _public_telemetry(self, patchflow: str, inputs: dict[str, Any]):
144157
user_config = get_user_config()
145158
requests.post(
@@ -169,38 +182,42 @@ def send_public_telemetry(self, patchflow: str, inputs: dict):
169182

170183
@contextlib.contextmanager
171184
def patched_telemetry(self, patchflow: str, inputs: dict):
185+
outputs = dict()
186+
172187
if not self.access_token:
173-
yield
188+
yield outputs
174189
return
175190

176191
try:
177192
is_valid_client = self.test_token()
178193
except Exception as e:
179194
logger.error(f"Access Token test failed: {e}")
180-
yield
195+
yield outputs
181196
return
182197

183198
if not is_valid_client:
184-
yield
199+
yield outputs
185200
return
186201

187202
try:
188203
repo = Repo(Path.cwd(), search_parent_directories=True)
189204
patchflow_run_id = self.record_patchflow_run(patchflow, repo, self.__handle_telemetry_inputs(inputs))
190205
except Exception as e:
191206
logger.error(f"Failed to record patchflow run: {e}")
192-
yield
207+
yield outputs
193208
return
194209

195210
if patchflow_run_id is None:
196-
yield
211+
yield outputs
197212
return
198213

199214
try:
200-
yield
215+
yield outputs
201216
finally:
202217
try:
203-
self.finish_record_patchflow_run(patchflow_run_id, patchflow, repo)
218+
self.finish_record_patchflow_run(
219+
patchflow_run_id, patchflow, repo, self.__handle_telemetry_outputs(outputs)
220+
)
204221
except Exception as e:
205222
logger.error(f"Failed to finish patchflow run: {e}")
206223

@@ -222,16 +239,17 @@ def record_patchflow_run(self, patchflow: str, repo: Repo, inputs: dict) -> int
222239
return None
223240

224241
logger.debug(f"Patchflow run recorded for {patchflow}")
225-
return response.json()["id"]
242+
return response.json().get("id")
226243

227-
def finish_record_patchflow_run(self, id: int, patchflow: str, repo: Repo) -> None:
244+
def finish_record_patchflow_run(self, id: int, patchflow: str, repo: Repo, outputs: dict) -> None:
228245
response = self._post(
229246
url=self.url + "/v1/patchwork/",
230247
headers={"Authorization": f"Bearer {self.access_token}"},
231248
json={
232249
"id": id,
233250
"url": repo.remotes.origin.url,
234251
"patchflow": patchflow,
252+
"outputs": outputs
235253
},
236254
)
237255

patchwork/common/client/scm.py

Lines changed: 192 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@
66
import time
77
from enum import Enum
88
from itertools import chain
9+
from pathlib import Path
10+
from urllib.parse import urlparse
911

12+
import git
1013
import gitlab.const
1114
from attrs import define
15+
from azure.devops.connection import Connection
16+
from azure.devops.released.client_factory import ClientFactory
17+
from azure.devops.released.core.core_client import CoreClient
18+
from azure.devops.released.git.git_client import GitClient
19+
from azure.devops.v7_1.git.models import GitPullRequest, GitPullRequestSearchCriteria, TeamProjectReference, GitRepository
1220
from github import Auth, Consts, Github, GithubException, PullRequest
1321
from github.GithubException import UnknownObjectException
1422
from gitlab import Gitlab, GitlabAuthenticationError, GitlabError
1523
from gitlab.v4.objects import ProjectMergeRequest
1624
from giturlparse import GitUrlParsed, parse
25+
from msrest.authentication import BasicAuthentication
1726
from typing_extensions import Protocol, TypedDict
1827

1928
from patchwork.logger import logger
@@ -53,12 +62,13 @@ class PullRequestTexts(TypedDict):
5362

5463

5564
class PullRequestState(Enum):
56-
OPEN = (["open"], ["opened"])
57-
CLOSED = (["closed"], ["closed", "merged"])
65+
OPEN = (["open"], ["opened"], ["active"])
66+
CLOSED = (["closed"], ["closed", "merged"], ["completed", "abandoned", "notSet"])
5867

59-
def __init__(self, github_state: list[str], gitlab_state: list[str]):
68+
def __init__(self, github_state: list[str], gitlab_state: list[str], azure_devops_state: list[str]):
6069
self.github_state: list[str] = github_state
6170
self.gitlab_state: list[str] = gitlab_state
71+
self.azure_devops_state: list[str] = azure_devops_state
6272

6373

6474
_COMMENT_MARKER = "<!-- PatchWork comment marker -->"
@@ -112,6 +122,11 @@ def _apply_pr_template(pr: "PullRequestProtocol", body: str) -> str:
112122
# chunk_link_format = file_link_format + "_{start_line}_{end_line}"
113123
chunk_link_format = file_link_format + ""
114124
anchor_hash = hashlib.sha1
125+
elif isinstance(pr, AzureDevopsPullRequest):
126+
backup_link_format = "{url}?_a=files"
127+
file_link_format = backup_link_format + "&path=/{path}"
128+
chunk_link_format = file_link_format + ""
129+
anchor_hash = hashlib.md5
115130
else:
116131
return pr.url()
117132

@@ -135,7 +150,7 @@ def _apply_pr_template(pr: "PullRequestProtocol", body: str) -> str:
135150
format_to_use = chunk_link_format
136151

137152
replacement_value = format_to_use.format(
138-
url=pr.url(), diff_anchor=diff_anchor, start_line=start, end_line=end
153+
url=pr.url(), path=path, diff_anchor=diff_anchor, start_line=start, end_line=end
139154
)
140155
template = template[:start_idx] + replacement_value + template[end_idx + 2 :]
141156
start_idx, end_idx = PullRequestProtocol._get_template_indexes(template)
@@ -352,6 +367,37 @@ def texts(self) -> PullRequestTexts:
352367
diffs={file.filename: file.patch for file in self._pr.get_files() if file.patch is not None},
353368
)
354369

370+
class AzureDevopsPullRequest(PullRequestProtocol):
371+
def __init__(self, pr: GitPullRequest, git_client: GitClient, pr_base_url: str):
372+
self._pr: GitPullRequest = pr
373+
self.git_client: GitClient = git_client
374+
self.pr_base_url = pr_base_url
375+
376+
@property
377+
def id(self) -> int:
378+
return self._pr.pull_request_id
379+
380+
def url(self) -> str:
381+
final_pr_url = self.pr_base_url
382+
if not final_pr_url.endswith("/"):
383+
final_pr_url += "/"
384+
return final_pr_url + str(self.id)
385+
386+
def set_pr_description(self, body: str) -> None:
387+
final_body = PullRequestProtocol._apply_pr_template(self, body)
388+
body = GitPullRequest(description=final_body)
389+
self.git_client.update_pull_request(body, repository_id=self._pr.repository.id, pull_request_id=self._pr.pull_request_id, project=self._pr.repository.project.id)
390+
391+
def create_comment(
392+
self, body: str, path: str | None = None, start_line: int | None = None, end_line: int | None = None
393+
) -> str | None:
394+
...
395+
396+
def reset_comments(self) -> None:
397+
...
398+
399+
def texts(self) -> PullRequestTexts:
400+
...
355401

356402
class GithubClient(ScmPlatformClientProtocol):
357403
DEFAULT_URL = Consts.DEFAULT_BASE_URL
@@ -608,3 +654,145 @@ def create_issue_comment(
608654

609655
obj = self.gitlab.projects.get(slug).issues.create({"title": title, "description": issue_text})
610656
return obj["web_url"]
657+
658+
659+
class AzureDevopsClient(ScmPlatformClientProtocol):
660+
DEFAULT_URL = "https://dev.azure.com/"
661+
662+
def __init__(self, access_token: str, url: str = DEFAULT_URL, remote: str = "origin"):
663+
self.credentials = BasicAuthentication('', access_token)
664+
self.__url = url
665+
self.__remote = remote
666+
git_repo = git.Repo(Path.cwd(), search_parent_directories=True)
667+
original_remote_url = git_repo.remotes[remote].url
668+
parsed_repo: GitUrlParsed = parse(original_remote_url)
669+
self.__org_name = parsed_repo.owner
670+
self.__project_name = parsed_repo.groups_path.replace("/_git", "")
671+
self.__repo_name = parsed_repo.repo
672+
673+
def __pr_resource_html_url(self):
674+
url = self.__url
675+
if not url.endswith("/"):
676+
url += "/"
677+
return f"{url}{self.__org_name}/{self.__project_name}/_git/{self.__repo_name}/pullrequest/"
678+
679+
680+
@functools.cached_property
681+
def clients(self) -> ClientFactory:
682+
url = self.__url
683+
if not url.endswith("/"):
684+
url += "/"
685+
686+
conn = Connection(base_url=f"{url}{self.__org_name}", creds=self.credentials)
687+
return conn.clients
688+
689+
@functools.cached_property
690+
def git_client(self) -> GitClient:
691+
return self.clients.get_git_client()
692+
693+
@functools.cached_property
694+
def core_client(self) -> CoreClient:
695+
return self.clients.get_core_client()
696+
697+
@functools.cached_property
698+
def project(self) -> TeamProjectReference:
699+
projs = self.core_client.get_projects()
700+
proj = next((proj for proj in projs if proj.name == self.__project_name), None)
701+
if proj is None:
702+
raise ValueError(f"Unable to determine project name from remote {self.__remote} url. Parsed project name: {self.__project_name}")
703+
return proj
704+
705+
@functools.cached_property
706+
def repo(self) -> GitRepository:
707+
repos = self.git_client.get_repositories(project=self.project.id)
708+
git_repo = next((r for r in repos if r.name == self.__repo_name), None)
709+
if git_repo is None:
710+
raise ValueError(f"Unable to determine repository name from remote {self.__remote} url. Parsed repository name: {self.__repo_name}")
711+
return git_repo
712+
713+
def set_url(self, url: str) -> None:
714+
self.__url = url
715+
716+
def test(self) -> bool:
717+
response = self.core_client.get_projects()
718+
return next(iter(response), None) is not None
719+
720+
def get_slug_and_id_from_url(self, url: str) -> tuple[str, int] | None:
721+
...
722+
723+
def find_issue_by_url(self, url: str) -> IssueText | None:
724+
...
725+
726+
def find_issue_by_id(self, slug: str, issue_id: int) -> IssueText | None:
727+
...
728+
729+
def get_pr_by_url(self, url: str) -> PullRequestProtocol | None:
730+
...
731+
732+
def find_pr_by_id(self, slug: str, pr_id: int) -> PullRequestProtocol | None:
733+
...
734+
735+
def find_prs(
736+
self,
737+
slug: str,
738+
state: PullRequestState | None = None,
739+
original_branch: str | None = None,
740+
feature_branch: str | None = None,
741+
limit: int | None = None,
742+
) -> list[PullRequestProtocol]:
743+
kwargs_list = dict(status=[None], target_ref_name=[None], source_ref_name=[None])
744+
745+
if state is not None:
746+
kwargs_list["status"] = state.gitlab_state # type: ignore
747+
if original_branch is not None:
748+
kwargs_list["target_ref_name"] = [f"refs/heads/{original_branch}"] # type: ignore
749+
if feature_branch is not None:
750+
kwargs_list["source_ref_name"] = [f"refs/heads/{feature_branch}"] # type: ignore
751+
752+
page_list = []
753+
keys = kwargs_list.keys()
754+
for instance in itertools.product(*kwargs_list.values()):
755+
kwargs = dict(((key, value) for key, value in zip(keys, instance) if value is not None))
756+
git_pr_search = GitPullRequestSearchCriteria(
757+
repository_id=self.repo.id,
758+
**kwargs,
759+
)
760+
pr_instances = self.git_client.get_pull_requests(
761+
project=self.project.id,
762+
repository_id=self.repo.id,
763+
search_criteria=git_pr_search
764+
)
765+
page_list.append(pr_instances)
766+
767+
rv_list = []
768+
for mr in itertools.islice(itertools.chain(*page_list), limit):
769+
rv_list.append(AzureDevopsPullRequest(mr, self.git_client, self.__pr_resource_html_url()))
770+
771+
return rv_list
772+
773+
def create_pr(
774+
self,
775+
slug: str,
776+
title: str,
777+
body: str,
778+
original_branch: str,
779+
feature_branch: str,
780+
) -> PullRequestProtocol:
781+
# before creating a PR, check if one already exists
782+
pr_body = GitPullRequest(
783+
source_ref_name=f"refs/heads/{feature_branch}",
784+
target_ref_name=f"refs/heads/{original_branch}",
785+
title=title,
786+
description=body,
787+
# should be web tag definition
788+
# labels="patchwork",
789+
)
790+
pr_instance = self.git_client.create_pull_request(pr_body, repository_id=self.repo.id, project=self.project.id)
791+
mr = AzureDevopsPullRequest(pr_instance, self.git_client, self.__pr_resource_html_url()) # type: ignore
792+
return mr
793+
794+
def create_issue_comment(
795+
self, slug: str, issue_text: str, title: str | None = None, issue_id: int | None = None
796+
) -> str:
797+
...
798+

0 commit comments

Comments
 (0)