diff --git a/common/constants.py b/common/constants.py index 26526f868..776e27447 100644 --- a/common/constants.py +++ b/common/constants.py @@ -130,7 +130,7 @@ class FileSource(StrEnum): GOOGLE_CLOUD_STORAGE = "google_cloud_storage" AIRTABLE = "airtable" ASANA = "asana" - + GITLAB = "gitlab" class PipelineTaskType(StrEnum): PARSE = "Parse" diff --git a/common/data_source/config.py b/common/data_source/config.py index 676696d65..8f9553365 100644 --- a/common/data_source/config.py +++ b/common/data_source/config.py @@ -55,6 +55,8 @@ class DocumentSource(str, Enum): BOX = "box" AIRTABLE = "airtable" ASANA = "asana" + GITHUB = "github" + GITLAB = "gitlab" class FileOrigin(str, Enum): """File origins""" diff --git a/common/data_source/gitlab_connector.py b/common/data_source/gitlab_connector.py new file mode 100644 index 000000000..0d2c0dab7 --- /dev/null +++ b/common/data_source/gitlab_connector.py @@ -0,0 +1,340 @@ +import fnmatch +import itertools +from collections import deque +from collections.abc import Iterable +from collections.abc import Iterator +from datetime import datetime +from datetime import timezone +from typing import Any +from typing import TypeVar +import gitlab +from gitlab.v4.objects import Project + +from common.data_source.config import DocumentSource, INDEX_BATCH_SIZE +from common.data_source.exceptions import ConnectorMissingCredentialError +from common.data_source.exceptions import ConnectorValidationError +from common.data_source.exceptions import CredentialExpiredError +from common.data_source.exceptions import InsufficientPermissionsError +from common.data_source.exceptions import UnexpectedValidationError +from common.data_source.interfaces import GenerateDocumentsOutput +from common.data_source.interfaces import LoadConnector +from common.data_source.interfaces import PollConnector +from common.data_source.interfaces import SecondsSinceUnixEpoch +from common.data_source.models import BasicExpertInfo +from common.data_source.models import Document +from common.data_source.utils import get_file_ext + +T = TypeVar("T") + + + +# List of directories/Files to exclude +exclude_patterns = [ + "logs", + ".github/", + ".gitlab/", + ".pre-commit-config.yaml", +] + + +def _batch_gitlab_objects(git_objs: Iterable[T], batch_size: int) -> Iterator[list[T]]: + it = iter(git_objs) + while True: + batch = list(itertools.islice(it, batch_size)) + if not batch: + break + yield batch + + +def get_author(author: Any) -> BasicExpertInfo: + return BasicExpertInfo( + display_name=author.get("name"), + ) + + +def _convert_merge_request_to_document(mr: Any) -> Document: + mr_text = mr.description or "" + doc = Document( + id=mr.web_url, + blob=mr_text, + source=DocumentSource.GITLAB, + semantic_identifier=mr.title, + extension=".md", + # updated_at is UTC time but is timezone unaware, explicitly add UTC + # as there is logic in indexing to prevent wrong timestamped docs + # due to local time discrepancies with UTC + doc_updated_at=mr.updated_at.replace(tzinfo=timezone.utc), + size_bytes=len(mr_text.encode("utf-8")), + primary_owners=[get_author(mr.author)], + metadata={"state": mr.state, "type": "MergeRequest", "web_url": mr.web_url}, + ) + return doc + + +def _convert_issue_to_document(issue: Any) -> Document: + issue_text = issue.description or "" + doc = Document( + id=issue.web_url, + blob=issue_text, + source=DocumentSource.GITLAB, + semantic_identifier=issue.title, + extension=".md", + # updated_at is UTC time but is timezone unaware, explicitly add UTC + # as there is logic in indexing to prevent wrong timestamped docs + # due to local time discrepancies with UTC + doc_updated_at=issue.updated_at.replace(tzinfo=timezone.utc), + size_bytes=len(issue_text.encode("utf-8")), + primary_owners=[get_author(issue.author)], + metadata={ + "state": issue.state, + "type": issue.type if issue.type else "Issue", + "web_url": issue.web_url, + }, + ) + return doc + + +def _convert_code_to_document( + project: Project, file: Any, url: str, projectName: str, projectOwner: str +) -> Document: + + # Dynamically get the default branch from the project object + default_branch = project.default_branch + + # Fetch the file content using the correct branch + file_content_obj = project.files.get( + file_path=file["path"], ref=default_branch # Use the default branch + ) + # BoxConnector uses raw bytes for blob. Keep the same here. + file_content_bytes = file_content_obj.decode() + file_url = f"{url}/{projectOwner}/{projectName}/-/blob/{default_branch}/{file['path']}" + + # Try to use the last commit timestamp for incremental sync. + # Falls back to "now" if the commit lookup fails. + last_commit_at = None + try: + # Query commit history for this file on the default branch. + commits = project.commits.list( + ref_name=default_branch, + path=file["path"], + per_page=1, + ) + if commits: + # committed_date is ISO string like "2024-01-01T00:00:00.000+00:00" + committed_date = commits[0].committed_date + if isinstance(committed_date, str): + last_commit_at = datetime.strptime( + committed_date, "%Y-%m-%dT%H:%M:%S.%f%z" + ).astimezone(timezone.utc) + elif isinstance(committed_date, datetime): + last_commit_at = committed_date.astimezone(timezone.utc) + except Exception: + last_commit_at = None + + # Create and return a Document object + doc = Document( + # Use a stable ID so reruns don't create duplicates. + id=file_url, + blob=file_content_bytes, + source=DocumentSource.GITLAB, + semantic_identifier=file.get("name"), + extension=get_file_ext(file.get("name")), + doc_updated_at=last_commit_at or datetime.now(tz=timezone.utc), + size_bytes=len(file_content_bytes) if file_content_bytes is not None else 0, + primary_owners=[], # Add owners if needed + metadata={ + "type": "CodeFile", + "path": file.get("path"), + "ref": default_branch, + "project": f"{projectOwner}/{projectName}", + "web_url": file_url, + }, + ) + return doc + + +def _should_exclude(path: str) -> bool: + """Check if a path matches any of the exclude patterns.""" + return any(fnmatch.fnmatch(path, pattern) for pattern in exclude_patterns) + + +class GitlabConnector(LoadConnector, PollConnector): + def __init__( + self, + project_owner: str, + project_name: str, + batch_size: int = INDEX_BATCH_SIZE, + state_filter: str = "all", + include_mrs: bool = True, + include_issues: bool = True, + include_code_files: bool = False, + ) -> None: + self.project_owner = project_owner + self.project_name = project_name + self.batch_size = batch_size + self.state_filter = state_filter + self.include_mrs = include_mrs + self.include_issues = include_issues + self.include_code_files = include_code_files + self.gitlab_client: gitlab.Gitlab | None = None + + def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: + self.gitlab_client = gitlab.Gitlab( + credentials["gitlab_url"], private_token=credentials["gitlab_access_token"] + ) + return None + + def validate_connector_settings(self) -> None: + if self.gitlab_client is None: + raise ConnectorMissingCredentialError("GitLab") + + try: + self.gitlab_client.auth() + self.gitlab_client.projects.get( + f"{self.project_owner}/{self.project_name}", + lazy=True, + ) + + except gitlab.exceptions.GitlabAuthenticationError as e: + raise CredentialExpiredError( + "Invalid or expired GitLab credentials." + ) from e + + except gitlab.exceptions.GitlabAuthorizationError as e: + raise InsufficientPermissionsError( + "Insufficient permissions to access GitLab resources." + ) from e + + except gitlab.exceptions.GitlabGetError as e: + raise ConnectorValidationError( + "GitLab project not found or not accessible." + ) from e + + except Exception as e: + raise UnexpectedValidationError( + f"Unexpected error while validating GitLab settings: {e}" + ) from e + + def _fetch_from_gitlab( + self, start: datetime | None = None, end: datetime | None = None + ) -> GenerateDocumentsOutput: + if self.gitlab_client is None: + raise ConnectorMissingCredentialError("Gitlab") + project: Project = self.gitlab_client.projects.get( + f"{self.project_owner}/{self.project_name}" + ) + + start_utc = start.astimezone(timezone.utc) if start else None + end_utc = end.astimezone(timezone.utc) if end else None + + # Fetch code files + if self.include_code_files: + # Fetching using BFS as project.report_tree with recursion causing slow load + queue = deque([""]) # Start with the root directory + while queue: + current_path = queue.popleft() + files = project.repository_tree(path=current_path, all=True) + for file_batch in _batch_gitlab_objects(files, self.batch_size): + code_doc_batch: list[Document] = [] + for file in file_batch: + if _should_exclude(file["path"]): + continue + + if file["type"] == "blob": + + doc = _convert_code_to_document( + project, + file, + self.gitlab_client.url, + self.project_name, + self.project_owner, + ) + + # Apply incremental window filtering for code files too. + if start_utc is not None and doc.doc_updated_at <= start_utc: + continue + if end_utc is not None and doc.doc_updated_at > end_utc: + continue + + code_doc_batch.append(doc) + elif file["type"] == "tree": + queue.append(file["path"]) + + if code_doc_batch: + yield code_doc_batch + + if self.include_mrs: + merge_requests = project.mergerequests.list( + state=self.state_filter, + order_by="updated_at", + sort="desc", + iterator=True, + ) + + for mr_batch in _batch_gitlab_objects(merge_requests, self.batch_size): + mr_doc_batch: list[Document] = [] + for mr in mr_batch: + mr.updated_at = datetime.strptime( + mr.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if start_utc is not None and mr.updated_at <= start_utc: + yield mr_doc_batch + return + if end_utc is not None and mr.updated_at > end_utc: + continue + mr_doc_batch.append(_convert_merge_request_to_document(mr)) + yield mr_doc_batch + + if self.include_issues: + issues = project.issues.list(state=self.state_filter, iterator=True) + + for issue_batch in _batch_gitlab_objects(issues, self.batch_size): + issue_doc_batch: list[Document] = [] + for issue in issue_batch: + issue.updated_at = datetime.strptime( + issue.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + # Avoid re-syncing the last-seen item. + if start_utc is not None and issue.updated_at <= start_utc: + yield issue_doc_batch + return + if end_utc is not None and issue.updated_at > end_utc: + continue + issue_doc_batch.append(_convert_issue_to_document(issue)) + yield issue_doc_batch + + def load_from_state(self) -> GenerateDocumentsOutput: + return self._fetch_from_gitlab() + + def poll_source( + self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch + ) -> GenerateDocumentsOutput: + start_datetime = datetime.fromtimestamp(start, tz=timezone.utc) + end_datetime = datetime.fromtimestamp(end, tz=timezone.utc) + return self._fetch_from_gitlab(start_datetime, end_datetime) + + +if __name__ == "__main__": + import os + + connector = GitlabConnector( + # gitlab_url="https://gitlab.com/api/v4", + project_owner=os.environ["PROJECT_OWNER"], + project_name=os.environ["PROJECT_NAME"], + batch_size=INDEX_BATCH_SIZE, + state_filter="all", + include_mrs=True, + include_issues=True, + include_code_files=True, + ) + + connector.load_credentials( + { + "gitlab_access_token": os.environ["GITLAB_ACCESS_TOKEN"], + "gitlab_url": os.environ["GITLAB_URL"], + } + ) + document_batches = connector.load_from_state() + for f in document_batches: + print("Batch:", f) + print("Finished loading from state.") \ No newline at end of file diff --git a/common/data_source/interfaces.py b/common/data_source/interfaces.py index cd180967f..679414dc6 100644 --- a/common/data_source/interfaces.py +++ b/common/data_source/interfaces.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from enum import IntFlag, auto from types import TracebackType from typing import Any, Dict, Generator, TypeVar, Generic, Callable, TypeAlias - +from collections.abc import Iterator from anthropic import BaseModel from common.data_source.models import ( @@ -16,6 +16,7 @@ from common.data_source.models import ( SecondsSinceUnixEpoch, GenerateSlimDocumentOutput ) +GenerateDocumentsOutput = Iterator[list[Document]] class LoadConnector(ABC): """Load connector interface""" diff --git a/pyproject.toml b/pyproject.toml index e5b5efaa6..08d6b1de9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,6 +151,7 @@ dependencies = [ "pyairtable>=3.3.0", "pygithub>=2.8.1", "asana>=5.2.2", + "python-gitlab>=7.0.0", ] [dependency-groups] diff --git a/rag/svr/sync_data_source.py b/rag/svr/sync_data_source.py index f3e30c8cc..f2fb1d672 100644 --- a/rag/svr/sync_data_source.py +++ b/rag/svr/sync_data_source.py @@ -38,12 +38,24 @@ from api.db.services.connector_service import ConnectorService, SyncLogsService from api.db.services.knowledgebase_service import KnowledgebaseService from common import settings from common.config_utils import show_configs -from common.data_source import BlobStorageConnector, NotionConnector, DiscordConnector, GoogleDriveConnector, MoodleConnector, JiraConnector, DropboxConnector, WebDAVConnector, AirtableConnector, AsanaConnector +from common.data_source import ( + BlobStorageConnector, + NotionConnector, + DiscordConnector, + GoogleDriveConnector, + MoodleConnector, + JiraConnector, + DropboxConnector, + WebDAVConnector, + AirtableConnector, + AsanaConnector, +) from common.constants import FileSource, TaskStatus from common.data_source.config import INDEX_BATCH_SIZE from common.data_source.confluence_connector import ConfluenceConnector from common.data_source.gmail_connector import GmailConnector from common.data_source.box_connector import BoxConnector +from common.data_source.gitlab_connector import GitlabConnector from common.data_source.interfaces import CheckpointOutputWrapper from common.log_utils import init_root_logger from common.signal_utils import start_tracemalloc_and_snapshot, stop_tracemalloc @@ -843,6 +855,47 @@ class Asana(SyncBase): return document_generator + +class Gitlab(SyncBase): + SOURCE_NAME: str = FileSource.GITLAB + + async def _generate(self, task: dict): + """ + Sync files from GitLab attachments. + """ + + self.connector = GitlabConnector( + project_owner= self.conf.get("project_owner"), + project_name= self.conf.get("project_name"), + include_mrs = self.conf.get("include_mrs", False), + include_issues = self.conf.get("include_issues", False), + include_code_files= self.conf.get("include_code_files", False), + ) + + self.connector.load_credentials( + { + "gitlab_access_token": self.conf.get("credentials", {}).get("gitlab_access_token"), + "gitlab_url": self.conf.get("credentials", {}).get("gitlab_url"), + } + ) + + if task["reindex"] == "1" or not task["poll_range_start"]: + document_generator = self.connector.load_from_state() + begin_info = "totally" + else: + poll_start = task["poll_range_start"] + if poll_start is None: + document_generator = self.connector.load_from_state() + begin_info = "totally" + else: + document_generator = self.connector.poll_source( + poll_start.timestamp(), + datetime.now(timezone.utc).timestamp() + ) + begin_info = "from {}".format(poll_start) + logging.info("Connect to Gitlab: ({}) {}".format(self.conf["project_name"], begin_info)) + return document_generator + func_factory = { FileSource.S3: S3, FileSource.R2: R2, @@ -862,7 +915,8 @@ func_factory = { FileSource.WEBDAV: WebDAV, FileSource.BOX: BOX, FileSource.AIRTABLE: Airtable, - FileSource.ASANA: Asana + FileSource.GITLAB: Gitlab, + FileSource.ASANA: Asana, } diff --git a/uv.lock b/uv.lock index 173246531..9472ee3d5 100644 --- a/uv.lock +++ b/uv.lock @@ -5924,6 +5924,19 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, ] +[[package]] +name = "python-gitlab" +version = "7.0.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/c4/0b613303b4f0fcda69b3d2e03d0a1fb1b6b079a7c7832e03a8d92461e9fe/python_gitlab-7.0.0.tar.gz", hash = "sha256:e4d934430f64efc09e6208b782c61cc0a3389527765e03ffbef17f4323dce441", size = 400568, upload-time = "2025-10-29T15:06:02.069Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/9e/811edc46a15f8deb828cba7ef8aab3451dc11ca72d033f3df72a5af865d9/python_gitlab-7.0.0-py3-none-any.whl", hash = "sha256:712a6c8c5e79e7e66f6dabb25d8fe7831a6b238d4a5132f8231df6b3b890ceff", size = 144415, upload-time = "2025-10-29T15:06:00.232Z" }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -6232,6 +6245,7 @@ dependencies = [ { name = "pypdf2" }, { name = "python-calamine" }, { name = "python-docx" }, + { name = "python-gitlab" }, { name = "python-pptx" }, { name = "pywencai" }, { name = "qianfan" }, @@ -6363,6 +6377,7 @@ requires-dist = [ { name = "pypdf2", specifier = ">=3.0.1,<4.0.0" }, { name = "python-calamine", specifier = ">=0.4.0" }, { name = "python-docx", specifier = ">=1.1.2,<2.0.0" }, + { name = "python-gitlab", specifier = ">=7.0.0" }, { name = "python-pptx", specifier = ">=1.0.2,<2.0.0" }, { name = "pywencai", specifier = ">=0.13.1,<1.0.0" }, { name = "qianfan", specifier = "==0.4.6" }, diff --git a/web/src/assets/svg/data-source/gitlab.svg b/web/src/assets/svg/data-source/gitlab.svg new file mode 100644 index 000000000..222fe006e --- /dev/null +++ b/web/src/assets/svg/data-source/gitlab.svg @@ -0,0 +1,2 @@ + +file_type_gitlab \ No newline at end of file diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index e54e5fc90..7f2250ff0 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -929,6 +929,8 @@ Beispiel: Virtual Hosted Style`, 'Verbinden Sie Ihr Gmail über OAuth, um E-Mails zu synchronisieren.', webdavDescription: 'Verbinden Sie sich mit WebDAV-Servern, um Dateien zu synchronisieren.', + gitlabDescription: + 'Verbinden Sie GitLab, um Repositories, Issues, Merge Requests und zugehörige Dokumentation zu synchronisieren.', webdavRemotePathTip: 'Optional: Geben Sie einen Ordnerpfad auf dem WebDAV-Server an (z.B. /Dokumente). Lassen Sie das Feld leer, um vom Stammverzeichnis aus zu synchronisieren.', google_driveTokenTip: diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 4cd15d5c6..7a478b418 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -933,6 +933,8 @@ Example: Virtual Hosted Style`, boxDescription: 'Connect your Box drive to sync files and folders.', airtableDescription: 'Connect to Airtable and synchronize files from a specified table within a designated workspace.', + gitlabDescription: + 'Connect GitLab to sync repositories, issues, merge requests, and related documentation.', asanaDescription: 'Connect to Asana and synchronize files from a specified workspace.', dropboxAccessTokenTip: diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index ea736a466..d887e842c 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -749,6 +749,8 @@ export default { 'Подключите ваш диск Box для синхронизации файлов и папок.', airtableDescription: 'Подключите Airtable и синхронизируйте файлы из указанной таблицы в заданном рабочем пространстве.', + gitlabDescription: + 'Подключите GitLab для синхронизации репозиториев, задач, merge requests и связанной документации.', asanaDescription: 'Подключите Asana и синхронизируйте файлы из рабочего пространства.', google_driveDescription: diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index cae1b91c5..e72449f4d 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -547,6 +547,8 @@ export default { avatar: '头像', avatarTip: '這會在你的個人主頁展示', profileDescription: '在此更新您的照片和個人詳細信息。', + gitlabDescription: + '連接 GitLab,同步儲存庫、Issue、合併請求(MR)及相關文件內容。', bedrockCredentialsHint: '提示:Access Key / Secret Key 可留空,以啟用 AWS IAM 自動驗證。', awsAuthModeAccessKeySecret: 'Access Key 和 Secret', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index c6a6237d7..7ed550003 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -862,6 +862,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 dropboxDescription: '连接 Dropbox,同步指定账号下的文件与文件夹。', boxDescription: '连接你的 Box 云盘以同步文件和文件夹。', airtableDescription: '连接 Airtable,同步指定工作区下指定表格中的文件。', + gitlabDescription: + '连接 GitLab,同步仓库、Issue、合并请求(MR)及相关文档内容。', asanaDescription: '连接 Asana,同步工作区中的文件。', r2Description: '连接你的 Cloudflare R2 存储桶以导入和同步文件。', dropboxAccessTokenTip: diff --git a/web/src/pages/user-setting/data-source/constant/index.tsx b/web/src/pages/user-setting/data-source/constant/index.tsx index b7ff9380c..33b3c1d47 100644 --- a/web/src/pages/user-setting/data-source/constant/index.tsx +++ b/web/src/pages/user-setting/data-source/constant/index.tsx @@ -25,6 +25,7 @@ export enum DataSourceKey { OCI_STORAGE = 'oci_storage', GOOGLE_CLOUD_STORAGE = 'google_cloud_storage', AIRTABLE = 'airtable', + GITLAB = 'gitlab', ASANA = 'asana', // SHAREPOINT = 'sharepoint', // SLACK = 'slack', @@ -110,6 +111,11 @@ export const generateDataSourceInfo = (t: TFunction) => { description: t(`setting.${DataSourceKey.AIRTABLE}Description`), icon: , }, + [DataSourceKey.GITLAB]: { + name: 'GitLab', + description: t(`setting.${DataSourceKey.GITLAB}Description`), + icon: , + }, [DataSourceKey.ASANA]: { name: 'Asana', description: t(`setting.${DataSourceKey.ASANA}Description`), @@ -658,6 +664,54 @@ export const DataSourceFormFields = { required: true, }, ], + [DataSourceKey.GITLAB]: [ + { + label: 'Project Owner', + name: 'config.project_owner', + type: FormFieldType.Text, + required: true, + }, + { + label: 'Project Name', + name: 'config.project_name', + type: FormFieldType.Text, + required: true, + }, + { + label: 'GitLab Personal Access Token', + name: 'config.credentials.gitlab_access_token', + type: FormFieldType.Password, + required: true, + }, + { + label: 'GitLab URL', + name: 'config.gitlab_url', + type: FormFieldType.Text, + required: true, + placeholder: 'https://gitlab.com', + }, + { + label: 'include Merge Requests', + name: 'config.include_mrs', + type: FormFieldType.Checkbox, + required: false, + defaultValue: true, + }, + { + label: 'include Issues', + name: 'config.include_issues', + type: FormFieldType.Checkbox, + required: false, + defaultValue: true, + }, + { + label: 'include Code Files', + name: 'config.include_code_files', + type: FormFieldType.Checkbox, + required: false, + defaultValue: true, + }, + ], [DataSourceKey.ASANA]: [ { label: 'API Token', @@ -883,6 +937,21 @@ export const DataSourceFormDefaultValues = { }, }, }, + [DataSourceKey.GITLAB]: { + name: '', + source: DataSourceKey.GITLAB, + config: { + project_owner: '', + project_name: '', + gitlab_url: 'https://gitlab.com', + include_mrs: true, + include_issues: true, + include_code_files: true, + credentials: { + gitlab_access_token: '', + }, + }, + }, [DataSourceKey.ASANA]: { name: '', source: DataSourceKey.ASANA,