Skip to content

Commit 2ce53e2

Browse files
committed
Added initial gitlab integration
Signed-off-by: Michael Engel <mengel@redhat.com>
1 parent 85e61fd commit 2ce53e2

File tree

8 files changed

+355
-64
lines changed

8 files changed

+355
-64
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ fedmsg
55
fedora-messaging
66
PyGithub
77
pypandoc_binary
8+
python-gitlab
89
urllib3
910
jinja2
1011
flask

sync2jira/api/gitlab_client.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from gitlab import Gitlab
2+
3+
4+
class GitlabClient:
5+
6+
def __init__(self, url, token, project):
7+
self.url = url
8+
self.token = token
9+
self.project = project
10+
self._client = Gitlab(url=url, private_token=token)
11+
self._project = self._client.get(self.project)
12+
13+
def fetch_issue(self, iid):
14+
return self._project.issues.get(iid)
15+
16+
def fetch_notes_for_issue(self, iid):
17+
issue = self.fetch_issue(iid)
18+
return GitlabClient.map_notes_to_intermediary(issue.notes.list(all=True))
19+
20+
def fetch_mr(self, iid):
21+
return self._project.mergerequests.get(iid)
22+
23+
def fetch_notes_for_mr(self, iid):
24+
mr = self.fetch_mr(iid)
25+
return GitlabClient.map_notes_to_intermediary(mr.notes.list(all=True))
26+
27+
@staticmethod
28+
def map_notes_to_intermediary(notes):
29+
return [
30+
{
31+
"author": note.author.username,
32+
"name": note.author.name,
33+
"body": note.body,
34+
"id": note.id,
35+
"date_created": note.created_at,
36+
"changed": note.updated_at,
37+
}
38+
for note in notes
39+
]

sync2jira/handler/base.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import logging
2+
3+
# Local Modules
4+
import sync2jira.handler.github as gh
5+
import sync2jira.handler.gitlab as gl
6+
7+
log = logging.getLogger("sync2jira")
8+
9+
10+
def get_handler_for(suffix, topic, idx):
11+
"""
12+
Function to check if a handler for given suffix is configured
13+
:param String suffix: Incoming suffix
14+
:param String topic: Topic of incoming message
15+
:param String idx: Id of incoming message
16+
:returns: Handler function if configured for suffix. Otherwise None.
17+
"""
18+
if suffix.startswith("github"):
19+
return gh.get_handler_for(suffix, topic, idx)
20+
elif suffix.startswith("gitlab"):
21+
return gl.get_handler_for(suffix, topic, idx)
22+
log.info("Unsupported datasource %r", suffix)
23+
return None

sync2jira/handler/github.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
log = logging.getLogger("sync2jira")
1111

1212

13-
def handle_issue_msg(body, suffix, config):
13+
def handle_issue_msg(body, headers, suffix, config):
1414
"""
1515
Function to handle incoming github issue message
1616
:param Dict body: Incoming message body
17+
:param Dict headers: Incoming message headers
1718
:param String suffix: Incoming suffix
1819
:param Dict config: Config dict
1920
"""
@@ -44,10 +45,11 @@ def handle_issue_msg(body, suffix, config):
4445
log.info("Not handling Issue update -- not configured")
4546

4647

47-
def handle_pr_msg(body, suffix, config):
48+
def handle_pr_msg(body, headers, suffix, config):
4849
"""
4950
Function to handle incoming github PR message
5051
:param Dict body: Incoming message body
52+
:param Dict headers: Incoming message headers
5153
:param String suffix: Incoming suffix
5254
:param Dict config: Config dict
5355
"""
@@ -104,5 +106,5 @@ def get_handler_for(suffix, topic, idx):
104106
return issue_handlers.get(suffix)
105107
elif suffix in pr_handlers:
106108
return pr_handlers.get(suffix)
107-
log.info("No handler for %r %r %r", suffix, topic, idx)
109+
log.info("No github handler for %r %r %r", suffix, topic, idx)
108110
return None

sync2jira/handler/gitlab.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import logging
2+
3+
from sync2jira.api.gitlab_client import GitlabClient
4+
import sync2jira.downstream_issue as d_issue
5+
import sync2jira.downstream_pr as d_pr
6+
7+
# Local Modules
8+
import sync2jira.intermediary as i
9+
10+
log = logging.getLogger("sync2jira")
11+
12+
13+
def should_sync(upstream, labels, config, event_type):
14+
mapped_repos = config["sync2jira"]["map"]["gitlab"]
15+
if upstream not in mapped_repos:
16+
log.debug("%r not in Gitlab map: %r", upstream, mapped_repos.keys())
17+
return None
18+
if event_type not in mapped_repos[upstream].get("sync", []):
19+
log.debug(
20+
"%r not in Gitlab sync map: %r",
21+
event_type,
22+
mapped_repos[upstream].get("sync", []),
23+
)
24+
return None
25+
26+
_filter = config["sync2jira"].get("filters", {}).get("gitlab", {}).get(upstream, {})
27+
for key, expected in _filter.items():
28+
if key == "labels":
29+
if labels.isdisjoint(expected):
30+
log.debug("Labels %s not found on issue: %s", expected, upstream)
31+
return None
32+
33+
34+
def handle_gitlab_issue(body, headers, config, suffix):
35+
"""
36+
Handle GitLab issue from FedMsg.
37+
38+
:param Dict body: FedMsg Message body
39+
:param Dict body: FedMsg Message headers
40+
:param Dict config: Config File
41+
:param Bool is_pr: msg refers to a pull request
42+
"""
43+
upstream = body["project"]["path_with_namespace"]
44+
url = headers["x-gitlab-instance"]
45+
token = config["sync2jira"].get("github_token")
46+
labels = {label["title"] for label in body.get("labels", [])}
47+
iid = body.get("object_attributes").get("iid")
48+
49+
if should_sync(upstream, labels, config, "issue"):
50+
sync_gitlab_issue(GitlabClient(url, token, upstream), iid, upstream, config)
51+
52+
53+
def handle_gitlab_note(body, headers, config, suffix):
54+
"""
55+
Handle Gitlab note from FedMsg.
56+
57+
:param Dict body: FedMsg Message body
58+
:param Dict body: FedMsg Message headers
59+
:param Dict config: Config File
60+
:param String suffix: FedMsg suffix
61+
"""
62+
upstream = body["project"]["path_with_namespace"]
63+
url = headers["x-gitlab-instance"]
64+
token = config["sync2jira"].get("github_token")
65+
66+
if "merge_request" in body:
67+
labels = {
68+
label["title"] for label in body.get("merge_request").get("labels", [])
69+
}
70+
iid = body.get("merge_request").get("iid")
71+
72+
if should_sync(upstream, labels, config, "issue"):
73+
sync_gitlab_mr(GitlabClient(url, token, upstream), iid, upstream)
74+
if "issue" in body:
75+
labels = {label["title"] for label in body.get("issue").get("labels", [])}
76+
iid = body.get("issue").get("iid")
77+
78+
if should_sync(upstream, labels, config, "pullrequest"):
79+
sync_gitlab_issue(GitlabClient(url, token, upstream), iid, upstream)
80+
log.info("Note was not added to an issue or merge request. Skipping note event.")
81+
82+
83+
def handle_gitlab_mr(body, headers, config, suffix):
84+
"""
85+
Handle Gitlab merge request from FedMsg.
86+
87+
:param Dict body: FedMsg Message body
88+
:param Dict body: FedMsg Message headers
89+
:param Dict config: Config File
90+
:param String suffix: FedMsg suffix
91+
"""
92+
upstream = body["project"]["path_with_namespace"]
93+
url = headers["x-gitlab-instance"]
94+
token = config["sync2jira"].get("github_token")
95+
labels = {label["title"] for label in body.get("labels", [])}
96+
iid = body.get("object_attributes").get("iid")
97+
98+
if should_sync(upstream, labels, config, "pullrequest"):
99+
sync_gitlab_mr(GitlabClient(url, token, upstream), iid, upstream, config)
100+
101+
102+
def sync_gitlab_issue(client, iid, upstream, config):
103+
gitlab_issue = client.fetch_issue(iid)
104+
comments = gitlab_issue.notes.list(all=True)
105+
106+
issue = i.Issue.from_gitlab(gitlab_issue, comments, upstream, config)
107+
d_issue.sync_with_jira(issue, config)
108+
109+
110+
def sync_gitlab_mr(client, iid, upstream, config):
111+
gitlab_mr = client.fetch_mr(iid)
112+
comments = gitlab_mr.notes.list(all=True)
113+
114+
mr = i.PR.from_gitlab(gitlab_mr, comments, upstream, config)
115+
d_pr.sync_with_jira(mr, config)
116+
117+
118+
handlers = {
119+
"gitlab.issues": handle_gitlab_issue,
120+
"gitlab.issue_comment": handle_gitlab_mr,
121+
"gitlab.note": handle_gitlab_note,
122+
}
123+
124+
125+
def get_handler_for(suffix, topic, idx):
126+
"""
127+
Function to check if a handler for given suffix is configured
128+
:param String suffix: Incoming suffix
129+
:param String topic: Topic of incoming message
130+
:param String idx: Id of incoming message
131+
:returns: Handler function if configured for suffix. Otherwise None.
132+
"""
133+
if suffix in handlers:
134+
return handlers.get(suffix)
135+
log.info("No gitlab handler for %r %r %r", suffix, topic, idx)
136+
return None

sync2jira/intermediary.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def upstream_title(self):
8686

8787
@classmethod
8888
def from_github(cls, upstream, issue, config):
89-
"""Helper function to create an intermediary Issue object."""
89+
"""Helper function to create an intermediary Issue object from Github."""
9090
upstream_source = "github"
9191
comments = reformat_github_comments(issue)
9292

@@ -131,6 +131,41 @@ def from_github(cls, upstream, issue, config):
131131
issue_type=issue_type,
132132
)
133133

134+
@classmethod
135+
def from_gitlab(cls, issue, comments, upstream, config):
136+
"""Helper function to create an intermediary Issue object from Gitlab."""
137+
upstream_source = "gitlab"
138+
139+
# Reformat the state field
140+
issue_status = issue.state
141+
if issue_status == "open":
142+
issue_status = "Open"
143+
elif issue_status == "closed":
144+
issue_status = "Closed"
145+
146+
return cls(
147+
source=upstream_source,
148+
title=issue.title,
149+
url=issue.web_url,
150+
upstream=upstream,
151+
config=config,
152+
comments=reformat_gitlab_comments(comments),
153+
tags=issue.labels,
154+
fixVersion="" if not issue.milestone else issue.milestone.id,
155+
priority="",
156+
content=issue.description,
157+
reporter={"login": issue.author.username, "fullname": issue.author.name},
158+
assignee=[
159+
{"login": entry.username, "fullname": entry.name}
160+
for entry in issue.assignees
161+
],
162+
status=issue_status,
163+
id_=issue.id,
164+
storypoints="",
165+
upstream_id=issue.iid,
166+
issue_type=issue.type,
167+
)
168+
134169
def __repr__(self):
135170
return f"<Issue {self.url} >"
136171

@@ -248,6 +283,54 @@ def from_github(cls, upstream, pr, suffix, config):
248283
match=match,
249284
)
250285

286+
@classmethod
287+
def from_gitlab(cls, pr, comments, upstream, config):
288+
"""Helper function to create an intermediary PR object from gitlab"""
289+
upstream_source = "github"
290+
reformatted_comments = reformat_gitlab_comments(comments)
291+
292+
# Match to a JIRA
293+
match = matcher(pr.description, reformatted_comments)
294+
295+
# Return our PR object
296+
return cls(
297+
source=upstream_source,
298+
jira_key=match,
299+
title=pr.title,
300+
url=pr.web_url,
301+
upstream=upstream,
302+
config=config,
303+
comments=reformatted_comments,
304+
# tags=issue['labels'],
305+
# fixVersion=[issue['milestone']],
306+
priority=None,
307+
content=pr.description,
308+
reporter=pr.author.name,
309+
assignee={
310+
"login": pr.assignee.username
311+
}, # used like this in jira sync code
312+
# GitHub PRs do not have status
313+
status=None,
314+
id_=pr.iid,
315+
# upstream_id=issue['number'],
316+
suffix=pr.state,
317+
match=match,
318+
)
319+
320+
321+
def reformat_gitlab_comments(comments):
322+
return [
323+
{
324+
"author": comment.author.name,
325+
"name": comment.author.username,
326+
"body": trim_string(comment.body),
327+
"id": comment.id,
328+
"date_created": comment.created_at,
329+
"changed": comment.updated_at,
330+
}
331+
for comment in comments
332+
]
333+
251334

252335
def reformat_github_comments(issue):
253336
return [

0 commit comments

Comments
 (0)