diff --git a/README.md b/README.md index 5078e6c..195815f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Overview devops-scm提供了一套统一的源代码接口,使得开发者能够方便地调用各大源代码管理平台的接口和接收源码管理平台的webhook事件。这样开发者就无需熟悉每个平台的API文档和webhook事件请求体,只需通过devops-scm提供的标准接口即可实现对源代码平台的管理。 -目前支持的提供商有-工蜂。后续会陆续对接github、gitlab、gitee、apache svn +目前支持工蜂、GitLab、Gitee、BKCode 和 SVN 等源码管理平台。 ## 代码目录说明 - devops-scm-api: api接口定义 diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 27f78e6..aeeb8ba 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,6 +1,6 @@ object Release { const val Group = "com.tencent.bk.devops.scm" - const val Version = "1.1.9" + const val Version = "1.2.0" } object Versions { diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/build.gradle.kts b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/build.gradle.kts new file mode 100644 index 0000000..23fe204 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/build.gradle.kts @@ -0,0 +1,12 @@ +dependencies { + api(project(":devops-scm-api")) + api(project(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-git-common")) + api(project(":devops-scm-sdk:devops-scm-sdk-gitlab")) + api("org.apache.commons:commons-lang3") + + testImplementation("org.junit.jupiter:junit-jupiter-api") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testImplementation("ch.qos.logback:logback-core") + testImplementation("ch.qos.logback:logback-classic") + testImplementation("org.mockito:mockito-core") +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabApiTemplate.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabApiTemplate.kt new file mode 100644 index 0000000..5919ab0 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabApiTemplate.kt @@ -0,0 +1,20 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.pojo.auth.IScmAuth +import com.tencent.devops.scm.api.pojo.repository.git.GitScmProviderRepository +import com.tencent.devops.scm.provider.common.GitScmApiTemplate +import com.tencent.devops.scm.provider.git.gitlab.auth.GitlabAuthProviderFactory +import com.tencent.devops.scm.sdk.gitlab.GitlabApi +import com.tencent.devops.scm.sdk.gitlab.GitlabApiException +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory + +object GitlabApiTemplate : GitScmApiTemplate() { + override fun getApi(repository: GitScmProviderRepository, apiFactory: GitlabApiFactory): GitlabApi { + val auth = requireNotNull(repository.auth) { "GitLab repository auth cannot be null" } + require(repository.projectIdOrPath.toString().isNotBlank()) { "GitLab project id or path cannot be blank" } + return apiFactory.fromAuthProvider(GitlabAuthProviderFactory.create(auth)) + } + + override fun getApi(auth: IScmAuth, apiFactory: GitlabApiFactory): GitlabApi = + apiFactory.fromAuthProvider(GitlabAuthProviderFactory.create(auth)) +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabCheckRunService.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabCheckRunService.kt new file mode 100644 index 0000000..2b955b0 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabCheckRunService.kt @@ -0,0 +1,43 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.CheckRunService +import com.tencent.devops.scm.api.pojo.CheckRun +import com.tencent.devops.scm.api.pojo.CheckRunInput +import com.tencent.devops.scm.api.pojo.CheckRunListOptions +import com.tencent.devops.scm.api.pojo.repository.ScmProviderRepository +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory + +class GitlabCheckRunService(private val apiFactory: GitlabApiFactory) : CheckRunService { + override fun create(repository: ScmProviderRepository, input: CheckRunInput): CheckRun { + val sha = requireNotNull(input.ref).takeIf(String::isNotBlank) + ?: throw IllegalArgumentException("check run ref cannot be blank") + val state = GitlabObjectConverter.convertCheckRunState(input.status, input.conclusion) + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertCheckRun( + api.commitStatusesApi.create( + repo.projectIdOrPath, + sha, + state, + input.name, + null, + input.detailsUrl, + input.output?.summary + ) + ) + } + } + + override fun update(repository: ScmProviderRepository, input: CheckRunInput): CheckRun = create(repository, input) + + override fun getCheckRuns(repository: ScmProviderRepository, opts: CheckRunListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.commitStatusesApi.getStatuses( + repo.projectIdOrPath, + opts.ref, + null, + null, + opts.page, + (opts.pageSize ?: 100).coerceIn(1, 100) + ).map(GitlabObjectConverter::convertCheckRun) + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabFileService.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabFileService.kt new file mode 100644 index 0000000..2361836 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabFileService.kt @@ -0,0 +1,50 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.FileService +import com.tencent.devops.scm.api.pojo.Content +import com.tencent.devops.scm.api.pojo.ContentInput +import com.tencent.devops.scm.api.pojo.Tree +import com.tencent.devops.scm.api.pojo.repository.ScmProviderRepository +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory + +class GitlabFileService(private val apiFactory: GitlabApiFactory) : FileService { + override fun find(repository: ScmProviderRepository, path: String, ref: String): Content = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertContent(api.repositoryFilesApi.getFile(repo.projectIdOrPath, path, ref)) + } + + override fun create(repository: ScmProviderRepository, path: String, input: ContentInput) { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.repositoryFilesApi.createFile( + repo.projectIdOrPath, path, input.ref, input.content, input.message + ) + } + } + + override fun update(repository: ScmProviderRepository, path: String, input: ContentInput) { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.repositoryFilesApi.updateFile( + repo.projectIdOrPath, path, input.ref, input.content, input.message + ) + } + } + + override fun listTree( + repository: ScmProviderRepository, + path: String, + ref: String, + recursive: Boolean + ): List = GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val trees = mutableListOf() + var page = 1 + do { + val current = api.repositoryFilesApi.getTree(repo.projectIdOrPath, path, ref, recursive, page++, PAGE_SIZE) + trees += current.map { GitlabObjectConverter.convertTree(it, path) } + } while (current.size == PAGE_SIZE) + trees + } + + private companion object { + const val PAGE_SIZE = 100 + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabIssueService.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabIssueService.kt new file mode 100644 index 0000000..ce66111 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabIssueService.kt @@ -0,0 +1,72 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.IssueService +import com.tencent.devops.scm.api.enums.IssueState +import com.tencent.devops.scm.api.pojo.Comment +import com.tencent.devops.scm.api.pojo.CommentInput +import com.tencent.devops.scm.api.pojo.Issue +import com.tencent.devops.scm.api.pojo.IssueInput +import com.tencent.devops.scm.api.pojo.IssueListOptions +import com.tencent.devops.scm.api.pojo.ListOptions +import com.tencent.devops.scm.api.pojo.repository.ScmProviderRepository +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabIssueParams + +class GitlabIssueService(private val apiFactory: GitlabApiFactory) : IssueService { + override fun find(repository: ScmProviderRepository, number: Int): Issue = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertIssue(api.issuesApi.getIssue(repo.projectIdOrPath, number.toLong())) + } + + override fun create(repository: ScmProviderRepository, input: IssueInput): Issue { + require(input.title.isNotBlank()) { "issue title cannot be blank" } + val params = GitlabIssueParams.builder().title(input.title).description(input.body).build() + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertIssue(api.issuesApi.createIssue(repo.projectIdOrPath, params)) + } + } + + override fun list(repository: ScmProviderRepository, opts: IssueListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.issuesApi.getIssues(repo.projectIdOrPath, state(opts.state), null, opts.page, size(opts.pageSize)) + .map(GitlabObjectConverter::convertIssue) + } + + override fun close(repository: ScmProviderRepository, number: Int) { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.issuesApi.closeIssue(repo.projectIdOrPath, number.toLong()) + } + } + + override fun findComment(repository: ScmProviderRepository, number: Int, commentId: Long): Comment = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertComment(api.notesApi.getIssueNote(repo.projectIdOrPath, number.toLong(), commentId)) + } + + override fun listComments(repository: ScmProviderRepository, number: Int, opts: ListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.notesApi.getIssueNotes(repo.projectIdOrPath, number.toLong(), opts.page, size(opts.pageSize)) + .map(GitlabObjectConverter::convertComment) + } + + override fun createComment(repository: ScmProviderRepository, number: Int, input: CommentInput): Comment { + require(input.body.isNotBlank()) { "comment body cannot be blank" } + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertComment(api.notesApi.createIssueNote(repo.projectIdOrPath, number.toLong(), input.body)) + } + } + + override fun deleteComment(repository: ScmProviderRepository, number: Int, commentId: Long) { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.notesApi.deleteIssueNote(repo.projectIdOrPath, number.toLong(), commentId) + } + } + + private fun state(value: IssueState?) = when (value) { + IssueState.OPENED, IssueState.REOPENED -> "opened" + IssueState.CLOSED -> "closed" + else -> null + } + + private fun size(value: Int?) = (value ?: 100).coerceIn(1, 100) +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectConverter.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectConverter.kt new file mode 100644 index 0000000..e928968 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectConverter.kt @@ -0,0 +1,251 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.enums.CheckRunConclusion +import com.tencent.devops.scm.api.enums.CheckRunStatus +import com.tencent.devops.scm.api.enums.ContentKind +import com.tencent.devops.scm.api.enums.Visibility +import com.tencent.devops.scm.api.pojo.Change +import com.tencent.devops.scm.api.pojo.CheckRun +import com.tencent.devops.scm.api.pojo.Comment +import com.tencent.devops.scm.api.pojo.Commit +import com.tencent.devops.scm.api.pojo.Content +import com.tencent.devops.scm.api.pojo.Hook +import com.tencent.devops.scm.api.pojo.HookEvents +import com.tencent.devops.scm.api.pojo.HookInput +import com.tencent.devops.scm.api.pojo.Issue +import com.tencent.devops.scm.api.pojo.Milestone +import com.tencent.devops.scm.api.pojo.PullRequest +import com.tencent.devops.scm.api.pojo.Reference +import com.tencent.devops.scm.api.pojo.Signature +import com.tencent.devops.scm.api.pojo.Tree +import com.tencent.devops.scm.api.pojo.User +import com.tencent.devops.scm.api.pojo.repository.git.GitScmServerRepository +import com.tencent.devops.scm.sdk.common.util.DateUtils +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabBranch +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommit +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommitStatus +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabDiff +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabIssue +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequest +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMilestone +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabNote +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProject +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProjectHook +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabRepositoryFile +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabTag +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabTreeItem +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabUser +import java.nio.charset.StandardCharsets +import java.util.Base64 +import java.util.Date + +@Suppress("TooManyFunctions") +object GitlabObjectConverter { + fun convertRepository(from: GitlabProject): GitScmServerRepository { + val fullName = from.pathWithNamespace.orEmpty().ifBlank { from.path.orEmpty() } + return GitScmServerRepository( + id = from.id ?: 0, + group = fullName.substringBeforeLast('/', ""), + name = from.name.orEmpty().ifBlank { fullName.substringAfterLast('/') }, + fullName = fullName, + defaultBranch = from.defaultBranch, + archived = from.archived, + isPrivate = from.visibility == "private", + visibility = when (from.visibility) { + "private" -> Visibility.PRIVATE + "internal" -> Visibility.INTERNAL + "public" -> Visibility.PUBLIC + else -> Visibility.UNDEFINED + }, + httpUrl = from.httpUrlToRepo.orEmpty(), + sshUrl = from.sshUrlToRepo.orEmpty(), + webUrl = from.webUrl.orEmpty() + ) + } + + fun convertBranch(from: GitlabBranch) = Reference(from.name.orEmpty(), from.commit?.id.orEmpty()) + fun convertTag(from: GitlabTag) = Reference(from.name.orEmpty(), from.commit?.id.orEmpty()) + + fun convertCommit(from: GitlabCommit) = Commit( + sha = from.id.orEmpty(), + message = from.message.orEmpty(), + link = from.webUrl.orEmpty(), + author = Signature(from.authorName.orEmpty(), from.authorEmail.orEmpty()), + committer = Signature(from.committerName.orEmpty(), from.committerEmail.orEmpty()), + commitTime = date(from.committedDate ?: from.authoredDate ?: from.createdAt) + ) + + fun convertChange(from: GitlabDiff) = Change( + added = from.isNewFile, + renamed = from.isRenamedFile, + deleted = from.isDeletedFile, + path = from.newPath.orEmpty(), + oldPath = from.oldPath.orEmpty(), + sha = "", + blobId = "" + ) + + fun convertPullRequest( + from: GitlabMergeRequest, + sourceProject: GitlabProject?, + targetProject: GitlabProject + ): PullRequest { + val target = convertRepository(targetProject) + val source = if (from.sourceProjectId == from.targetProjectId) target else sourceProject?.let(::convertRepository) + ?: emptyRepository(from.sourceProjectId ?: 0) + return PullRequest( + id = from.id ?: 0, + number = from.iid?.toInt() ?: 0, + title = from.title.orEmpty(), + body = from.description.orEmpty(), + link = from.webUrl.orEmpty(), + sha = from.sha, + sourceRepo = source, + targetRepo = target, + targetRef = Reference(from.targetBranch.orEmpty(), ""), + sourceRef = Reference(from.sourceBranch.orEmpty(), from.sha.orEmpty()), + closed = from.state == "closed", + merged = from.state == "merged", + mergeCommitSha = from.mergeCommitSha ?: from.squashCommitSha, + author = from.author?.let(::convertUser), + created = from.createdAt?.let(::date), + updated = from.updatedAt?.let(::date), + labels = from.labels, + description = from.description, + milestone = from.milestone?.let(::convertMilestone), + assignee = (from.assignees ?: from.assignee?.let(::listOf) ?: emptyList()).map(::convertUser) + ) + } + + fun convertIssue(from: GitlabIssue) = Issue( + id = from.id ?: 0, + number = from.iid?.toInt() ?: 0, + title = from.title.orEmpty(), + body = from.description, + link = from.webUrl.orEmpty(), + labels = from.labels, + closed = from.state == "closed", + author = from.author?.let(::convertUser) ?: emptyUser(), + created = date(from.createdAt), + updated = from.updatedAt?.let(::date), + milestoneId = from.milestone?.id?.toString(), + state = from.state + ) + + fun convertUser(from: GitlabUser) = User( + id = from.id ?: 0, + username = from.username.orEmpty(), + name = from.name.orEmpty(), + email = from.email, + avatar = from.avatarUrl + ) + + fun convertComment(from: GitlabNote) = Comment( + id = from.id ?: 0, + body = from.body.orEmpty(), + link = "", + author = from.author?.let(::convertUser) ?: emptyUser(), + created = date(from.createdAt), + updated = from.updatedAt?.let(::date), + type = from.noteableType.orEmpty() + ) + + fun convertContent(from: GitlabRepositoryFile): Content { + val content = if (from.encoding.equals("base64", true)) { + String(Base64.getMimeDecoder().decode(from.content.orEmpty()), StandardCharsets.UTF_8) + } else from.content.orEmpty() + return Content(from.filePath.orEmpty(), content, from.commitId.orEmpty(), from.blobId.orEmpty()) + } + + fun convertTree(from: GitlabTreeItem, rootPath: String) = Tree( + path = rootPath.trim('/').let { normalizedRootPath -> + if (normalizedRootPath.isEmpty()) { + from.name.orEmpty() + } else { + val itemPath = from.path.orEmpty() + val rootPrefix = "$normalizedRootPath/" + if (itemPath.startsWith(rootPrefix)) itemPath.removePrefix(rootPrefix) else itemPath + } + }, + sha = "", + blobId = from.id.orEmpty(), + kind = when (from.type) { + "blob" -> if (from.mode == "120000") ContentKind.SYMLINK else ContentKind.FILE + "tree" -> ContentKind.DIRECTORY + "commit" -> ContentKind.GITLINK + else -> ContentKind.UNSUPPORTED + } + ) + + fun convertHook(from: GitlabProjectHook) = Hook( + id = from.id ?: 0, + url = from.url.orEmpty(), + active = true, + name = "", + events = HookEvents( + push = from.isPushEvents, + tag = from.isTagPushEvents, + pullRequest = from.isMergeRequestsEvents, + issue = from.isIssuesEvents, + comment = from.isNoteEvents, + issueComment = from.isNoteEvents, + pullRequestComment = from.isNoteEvents + ) + ) + + fun convertFromHookInput(input: HookInput): GitlabProjectHook { + require(input.nativeEvents.isNullOrEmpty()) { "GitLab native hook events are not supported" } + return GitlabProjectHook().apply { + url = input.url + isPushEvents = input.events?.push == true + isTagPushEvents = input.events?.tag == true + isMergeRequestsEvents = input.events?.pullRequest == true + isIssuesEvents = input.events?.issue == true + isNoteEvents = input.events?.let { + it.comment == true || it.issueComment == true || it.pullRequestComment == true + } == true + isEnableSslVerification = input.skipVerify != true + } + } + + fun convertCheckRun(from: GitlabCommitStatus): CheckRun { + val (status, conclusion) = when (from.status) { + "pending" -> CheckRunStatus.QUEUED to null + "running" -> CheckRunStatus.IN_PROGRESS to null + "success" -> CheckRunStatus.COMPLETED to CheckRunConclusion.SUCCESS + "failed" -> CheckRunStatus.COMPLETED to CheckRunConclusion.FAILURE + "canceled" -> CheckRunStatus.COMPLETED to CheckRunConclusion.CANCELLED + "skipped" -> CheckRunStatus.COMPLETED to CheckRunConclusion.SKIPPED + else -> throw IllegalArgumentException("unknown GitLab commit status: ${from.status}") + } + return CheckRun(from.id ?: 0, status, from.name.orEmpty(), from.description, from.targetUrl, null, conclusion) + } + + fun convertCheckRunState(status: CheckRunStatus, conclusion: CheckRunConclusion?): String = when (status) { + CheckRunStatus.QUEUED -> "pending" + CheckRunStatus.IN_PROGRESS -> "running" + CheckRunStatus.COMPLETED -> when (requireNotNull(conclusion) { + "conclusion cannot be null when status is COMPLETED" + }) { + CheckRunConclusion.SUCCESS -> "success" + CheckRunConclusion.CANCELLED -> "canceled" + CheckRunConclusion.SKIPPED -> "skipped" + CheckRunConclusion.FAILURE, CheckRunConclusion.TIMED_OUT, CheckRunConclusion.ACTION_REQUIRED -> "failed" + CheckRunConclusion.UNKNOWN -> throw IllegalArgumentException("unknown check run conclusion") + } + CheckRunStatus.UNKNOWN -> throw IllegalArgumentException("unknown check run status") + } + + private fun convertMilestone(from: GitlabMilestone) = Milestone( + id = from.id?.toInt() ?: 0, + iid = from.iid?.toInt(), + title = from.title.orEmpty(), + state = from.state, + description = from.description, + dueDate = from.dueDate?.let(::date) + ) + + private fun emptyRepository(id: Long) = GitScmServerRepository(id, "", "", "", httpUrl = "", sshUrl = "", webUrl = "") + private fun emptyUser() = User(0, "", "") + private fun date(value: Date?) = DateUtils.convertDateToLocalDateTime(value ?: Date(0)) +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectToMapConverter.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectToMapConverter.kt new file mode 100644 index 0000000..b93cdfb --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectToMapConverter.kt @@ -0,0 +1,153 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_HOOK_MR_ID +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_HOOK_MR_COMMITTER +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_MANUAL_UNLOCK +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_AUTHOR +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_ASSIGNEE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_BASE_COMMIT +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_CREATE_TIME +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_CREATE_TIMESTAMP +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_DESCRIPTION +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_ID +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_LABELS +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_MILESTONE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_MILESTONE_DUE_DATE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_MILESTONE_ID +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_NUMBER +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_REVIEWERS +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_SOURCE_BRANCH +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_SOURCE_COMMIT +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_TARGET_BRANCH +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_TARGET_COMMIT +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_TITLE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_UPDATE_TIME +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_UPDATE_TIMESTAMP +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_NOTE_CREATED_AT +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_NOTE_UPDATED_AT +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_ACTION +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_HEAD_REF +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_MR_ID +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_MR_IID +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_MR_PROPOSER +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_MR_TITLE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_MR_URL +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_BASE_REF +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_MR_DESC +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_WEBHOOK_SOURCE_BRANCH +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_WEBHOOK_SOURCE_PROJECT_ID +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_WEBHOOK_TARGET_BRANCH +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_WEBHOOK_TARGET_PROJECT_ID +import com.tencent.devops.scm.api.pojo.PullRequest +import com.tencent.devops.scm.api.pojo.User +import com.tencent.devops.scm.api.pojo.repository.git.GitScmServerRepository +import com.tencent.devops.scm.api.constant.DateFormatConstants +import com.tencent.devops.scm.api.constant.WebhookOutputConstants.PR_DESC_MAX_LENGTH +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequest +import org.apache.commons.lang3.StringUtils +import java.text.SimpleDateFormat +import java.util.Date +import java.util.TimeZone + +object GitlabObjectToMapConverter { + fun convertPullRequest( + pullRequest: PullRequest, + action: String, + sender: User + ): MutableMap = mutableMapOf( + BK_HOOK_MR_ID to pullRequest.id, + BK_REPO_GIT_WEBHOOK_MR_ID to pullRequest.id, + BK_REPO_GIT_WEBHOOK_MR_NUMBER to pullRequest.number, + BK_REPO_GIT_WEBHOOK_MR_TITLE to pullRequest.title, + BK_REPO_GIT_WEBHOOK_MR_DESCRIPTION to pullRequest.body, + BK_REPO_GIT_WEBHOOK_MR_AUTHOR to pullRequest.author?.username.orEmpty(), + BK_REPO_GIT_WEBHOOK_MR_SOURCE_BRANCH to pullRequest.sourceRef.name, + BK_REPO_GIT_WEBHOOK_MR_TARGET_BRANCH to pullRequest.targetRef.name, + PIPELINE_GIT_MR_IID to pullRequest.number, + PIPELINE_GIT_MR_ID to pullRequest.id, + PIPELINE_GIT_MR_URL to pullRequest.link, + PIPELINE_GIT_MR_TITLE to pullRequest.title, + PIPELINE_GIT_MR_PROPOSER to sender.username, + PIPELINE_GIT_HEAD_REF to pullRequest.targetRef.name, + PIPELINE_WEBHOOK_SOURCE_PROJECT_ID to pullRequest.sourceRepo.id, + PIPELINE_WEBHOOK_TARGET_PROJECT_ID to pullRequest.targetRepo.id, + PIPELINE_WEBHOOK_SOURCE_BRANCH to pullRequest.sourceRef.name, + PIPELINE_WEBHOOK_TARGET_BRANCH to pullRequest.targetRef.name, + PIPELINE_GIT_ACTION to action, + BK_REPO_GIT_MANUAL_UNLOCK to false + ) + + fun convertNote(createdAt: String?, updatedAt: String?): MutableMap = mutableMapOf( + BK_REPO_GIT_WEBHOOK_NOTE_CREATED_AT to createdAt.orEmpty(), + BK_REPO_GIT_WEBHOOK_NOTE_UPDATED_AT to updatedAt.orEmpty(), + BK_REPO_GIT_MANUAL_UNLOCK to false + ) + + fun convertPullRequest( + mergeRequest: GitlabMergeRequest, + repository: GitScmServerRepository + ): Map = mutableMapOf().apply { + val id = mergeRequest.id?.toString().orEmpty() + val iid = mergeRequest.iid?.toString().orEmpty() + val author = mergeRequest.author?.username.orEmpty() + val description = StringUtils.substring(mergeRequest.description.orEmpty(), 0, PR_DESC_MAX_LENGTH) + + put(BK_HOOK_MR_ID, id) + put(BK_REPO_GIT_WEBHOOK_MR_ID, id) + put(PIPELINE_GIT_MR_ID, id) + put(BK_REPO_GIT_WEBHOOK_MR_NUMBER, iid) + put(PIPELINE_GIT_MR_IID, iid) + put(BK_HOOK_MR_COMMITTER, author) + put(BK_REPO_GIT_WEBHOOK_MR_AUTHOR, author) + put(PIPELINE_GIT_MR_PROPOSER, author) + put(BK_REPO_GIT_WEBHOOK_MR_TITLE, mergeRequest.title.orEmpty()) + put(PIPELINE_GIT_MR_TITLE, mergeRequest.title.orEmpty()) + put(BK_REPO_GIT_WEBHOOK_MR_DESCRIPTION, description) + put(PIPELINE_GIT_MR_DESC, description) + put(BK_REPO_GIT_WEBHOOK_MR_SOURCE_BRANCH, mergeRequest.sourceBranch.orEmpty()) + put(PIPELINE_WEBHOOK_SOURCE_BRANCH, mergeRequest.sourceBranch.orEmpty()) + put(PIPELINE_GIT_BASE_REF, mergeRequest.sourceBranch.orEmpty()) + put(BK_REPO_GIT_WEBHOOK_MR_TARGET_BRANCH, mergeRequest.targetBranch.orEmpty()) + put(PIPELINE_WEBHOOK_TARGET_BRANCH, mergeRequest.targetBranch.orEmpty()) + put(PIPELINE_GIT_HEAD_REF, mergeRequest.targetBranch.orEmpty()) + put(PIPELINE_WEBHOOK_SOURCE_PROJECT_ID, mergeRequest.sourceProjectId?.toString().orEmpty()) + put(PIPELINE_WEBHOOK_TARGET_PROJECT_ID, mergeRequest.targetProjectId?.toString().orEmpty()) + put(BK_REPO_GIT_WEBHOOK_MR_ASSIGNEE, mergeRequest.assignee?.username.orEmpty()) + put(BK_REPO_GIT_WEBHOOK_MR_REVIEWERS, mergeRequest.reviewers.orEmpty().joinToString(",") { it.username.orEmpty() }) + put(BK_REPO_GIT_WEBHOOK_MR_LABELS, mergeRequest.labels.orEmpty().joinToString(",")) + put(BK_REPO_GIT_WEBHOOK_MR_BASE_COMMIT, mergeRequest.diffRefs?.baseSha.orEmpty()) + put(BK_REPO_GIT_WEBHOOK_MR_TARGET_COMMIT, mergeRequest.diffRefs?.startSha.orEmpty()) + put(BK_REPO_GIT_WEBHOOK_MR_SOURCE_COMMIT, mergeRequest.diffRefs?.headSha ?: mergeRequest.sha.orEmpty()) + + mergeRequest.createdAt?.let { + put(BK_REPO_GIT_WEBHOOK_MR_CREATE_TIME, formatDate(it)) + put(BK_REPO_GIT_WEBHOOK_MR_CREATE_TIMESTAMP, it.time) + } + mergeRequest.updatedAt?.let { + put(BK_REPO_GIT_WEBHOOK_MR_UPDATE_TIME, formatDate(it)) + put(BK_REPO_GIT_WEBHOOK_MR_UPDATE_TIMESTAMP, it.time) + } + mergeRequest.milestone?.let { milestone -> + put(BK_REPO_GIT_WEBHOOK_MR_MILESTONE, milestone.title.orEmpty()) + put(BK_REPO_GIT_WEBHOOK_MR_MILESTONE_ID, milestone.id?.toString().orEmpty()) + milestone.dueDate?.let { put(BK_REPO_GIT_WEBHOOK_MR_MILESTONE_DUE_DATE, formatDueDate(it)) } + } + + val url = mergeRequest.webUrl.orEmpty().ifBlank { + if (repository.webUrl.isNotBlank() && mergeRequest.iid != null) { + "${repository.webUrl}/-/merge_requests/${mergeRequest.iid}" + } else { + "" + } + } + put(PIPELINE_GIT_MR_URL, url) + } + + private fun formatDate(date: Date): String = SimpleDateFormat(DateFormatConstants.ISO_8601).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format(date) + + private fun formatDueDate(date: Date): String = SimpleDateFormat("yyyy-MM-dd").apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format(date) +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabPullRequestService.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabPullRequestService.kt new file mode 100644 index 0000000..6829516 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabPullRequestService.kt @@ -0,0 +1,118 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.PullRequestService +import com.tencent.devops.scm.api.enums.PullRequestState +import com.tencent.devops.scm.api.pojo.Change +import com.tencent.devops.scm.api.pojo.Comment +import com.tencent.devops.scm.api.pojo.CommentInput +import com.tencent.devops.scm.api.pojo.Commit +import com.tencent.devops.scm.api.pojo.ListOptions +import com.tencent.devops.scm.api.pojo.PullRequest +import com.tencent.devops.scm.api.pojo.PullRequestInput +import com.tencent.devops.scm.api.pojo.PullRequestListOptions +import com.tencent.devops.scm.api.pojo.repository.ScmProviderRepository +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequest +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequestParams + +class GitlabPullRequestService(private val apiFactory: GitlabApiFactory) : PullRequestService { + override fun find(repository: ScmProviderRepository, number: Int): PullRequest = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + convert(api.mergeRequestsApi.getMergeRequest(repo.projectIdOrPath, number.toLong()), repo.projectIdOrPath, api) + } + + override fun list(repository: ScmProviderRepository, opts: PullRequestListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val target = api.projectsApi.getProject(repo.projectIdOrPath) + api.mergeRequestsApi.getMergeRequests( + repo.projectIdOrPath, + state(opts.state), + opts.sourceBranch, + opts.targetBranch, + opts.page, + size(opts.pageSize) + ).map { GitlabObjectConverter.convertPullRequest(it, null, target) } + } + + override fun create(repository: ScmProviderRepository, input: PullRequestInput): PullRequest { + require(input.title.isNotBlank()) { "merge request title cannot be blank" } + require(input.sourceBranch.isNotBlank()) { "source branch cannot be blank" } + require(input.targetBranch.isNotBlank()) { "target branch cannot be blank" } + val params = GitlabMergeRequestParams.builder() + .title(input.title).description(input.body).sourceBranch(input.sourceBranch) + .targetBranch(input.targetBranch).targetProjectId((input.targetRepo as? Number)?.toLong()).build() + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + convert(api.mergeRequestsApi.createMergeRequest(repo.projectIdOrPath, params), repo.projectIdOrPath, api) + } + } + + override fun listChanges(repository: ScmProviderRepository, number: Int, opts: ListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val mergeRequest = api.mergeRequestsApi.getMergeRequestChanges(repo.projectIdOrPath, number.toLong()) + check(mergeRequest.overflow != true) { + "GitLab merge request changes overflow; complete changes are unavailable" + } + mergeRequest.changes.orEmpty().map(GitlabObjectConverter::convertChange) + } + + override fun listCommits(repository: ScmProviderRepository, number: Int, opts: ListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.mergeRequestsApi.getMergeRequestCommits(repo.projectIdOrPath, number.toLong(), opts.page, size(opts.pageSize)) + .map(GitlabObjectConverter::convertCommit) + } + + override fun merge(repository: ScmProviderRepository, number: Int) { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.mergeRequestsApi.mergeMergeRequest(repo.projectIdOrPath, number.toLong(), null, null, null) + } + } + + override fun close(repository: ScmProviderRepository, number: Int) { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.mergeRequestsApi.closeMergeRequest(repo.projectIdOrPath, number.toLong()) + } + } + + override fun findComment(repository: ScmProviderRepository, number: Int, commentId: Long): Comment = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertComment(api.notesApi.getMergeRequestNote(repo.projectIdOrPath, number.toLong(), commentId)) + } + + override fun listComments(repository: ScmProviderRepository, number: Int, opts: ListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.notesApi.getMergeRequestNotes(repo.projectIdOrPath, number.toLong(), opts.page, size(opts.pageSize)) + .map(GitlabObjectConverter::convertComment) + } + + override fun createComment(repository: ScmProviderRepository, number: Int, input: CommentInput): Comment { + require(input.body.isNotBlank()) { "comment body cannot be blank" } + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertComment( + api.notesApi.createMergeRequestNote(repo.projectIdOrPath, number.toLong(), input.body) + ) + } + } + + override fun deleteComment(repository: ScmProviderRepository, number: Int, commentId: Long) { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.notesApi.deleteMergeRequestNote(repo.projectIdOrPath, number.toLong(), commentId) + } + } + + private fun convert(mr: GitlabMergeRequest, project: Any, api: com.tencent.devops.scm.sdk.gitlab.GitlabApi): PullRequest { + val target = api.projectsApi.getProject(mr.targetProjectId ?: project) + val source = if (mr.sourceProjectId == mr.targetProjectId) target else mr.sourceProjectId?.let { + runCatching { api.projectsApi.getProject(it) }.getOrNull() + } + return GitlabObjectConverter.convertPullRequest(mr, source, target) + } + + private fun state(value: PullRequestState?) = when (value) { + PullRequestState.OPENED, PullRequestState.REOPENED -> "opened" + PullRequestState.CLOSED -> "closed" + PullRequestState.MERGED -> "merged" + else -> null + } + + private fun size(value: Int?) = (value ?: 100).coerceIn(1, 100) +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabRefService.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabRefService.kt new file mode 100644 index 0000000..0f5fc26 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabRefService.kt @@ -0,0 +1,84 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.RefService +import com.tencent.devops.scm.api.pojo.BranchListOptions +import com.tencent.devops.scm.api.pojo.Change +import com.tencent.devops.scm.api.pojo.Commit +import com.tencent.devops.scm.api.pojo.CommitListOptions +import com.tencent.devops.scm.api.pojo.ListOptions +import com.tencent.devops.scm.api.pojo.Reference +import com.tencent.devops.scm.api.pojo.ReferenceInput +import com.tencent.devops.scm.api.pojo.TagListOptions +import com.tencent.devops.scm.api.pojo.repository.ScmProviderRepository +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory + +class GitlabRefService(private val apiFactory: GitlabApiFactory) : RefService { + override fun createBranch(repository: ScmProviderRepository, input: ReferenceInput) { + val sha = requireSha(input) + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.branchesApi.createBranch(repo.projectIdOrPath, input.name, sha) + } + } + + override fun findBranch(repository: ScmProviderRepository, name: String): Reference = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertBranch(api.branchesApi.getBranch(repo.projectIdOrPath, name)) + } + + override fun listBranches(repository: ScmProviderRepository, opts: BranchListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.branchesApi.getBranches(repo.projectIdOrPath, opts.search, opts.page, size(opts.pageSize)) + .map(GitlabObjectConverter::convertBranch) + } + + override fun createTag(repository: ScmProviderRepository, input: ReferenceInput) { + val sha = requireSha(input) + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.tagsApi.createTag(repo.projectIdOrPath, input.name, sha, null) + } + } + + override fun findTag(repository: ScmProviderRepository, name: String): Reference = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertTag(api.tagsApi.getTag(repo.projectIdOrPath, name)) + } + + override fun listTags(repository: ScmProviderRepository, opts: TagListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.tagsApi.getTags(repo.projectIdOrPath, opts.search, opts.page, size(opts.pageSize)) + .map(GitlabObjectConverter::convertTag) + } + + override fun findCommit(repository: ScmProviderRepository, ref: String): Commit = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertCommit(api.commitsApi.getCommit(repo.projectIdOrPath, ref)) + } + + override fun listCommits(repository: ScmProviderRepository, opts: CommitListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.commitsApi.getCommits( + repo.projectIdOrPath, opts.ref, opts.path, null, null, opts.page, size(opts.pageSize) + ).map(GitlabObjectConverter::convertCommit) + } + + override fun listChanges(repository: ScmProviderRepository, ref: String, opts: ListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.commitsApi.getDiff(repo.projectIdOrPath, ref, opts.page, size(opts.pageSize)) + .map(GitlabObjectConverter::convertChange) + } + + override fun compareChanges( + repository: ScmProviderRepository, + source: String, + target: String, + opts: ListOptions + ): List = GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.commitsApi.compare(repo.projectIdOrPath, source, target, false).diffs + .map(GitlabObjectConverter::convertChange) + } + + private fun size(value: Int?) = (value ?: 100).coerceIn(1, 100) + + private fun requireSha(input: ReferenceInput): String = input.sha?.takeIf(String::isNotBlank) + ?: throw IllegalArgumentException("reference sha cannot be blank") +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabRepositoryService.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabRepositoryService.kt new file mode 100644 index 0000000..ebae49f --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabRepositoryService.kt @@ -0,0 +1,87 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.RepositoryService +import com.tencent.devops.scm.api.pojo.Hook +import com.tencent.devops.scm.api.pojo.HookInput +import com.tencent.devops.scm.api.pojo.ListOptions +import com.tencent.devops.scm.api.pojo.Perm +import com.tencent.devops.scm.api.pojo.RepoListOptions +import com.tencent.devops.scm.api.pojo.auth.IScmAuth +import com.tencent.devops.scm.api.pojo.repository.ScmProviderRepository +import com.tencent.devops.scm.api.pojo.repository.ScmServerRepository +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory + +class GitlabRepositoryService(private val apiFactory: GitlabApiFactory) : RepositoryService { + override fun find(repository: ScmProviderRepository): ScmServerRepository = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertRepository(api.projectsApi.getProject(repo.projectIdOrPath)) + } + + override fun findPerms(repository: ScmProviderRepository, username: String): Perm = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val project = api.projectsApi.getProject(repo.projectIdOrPath) + val member = api.projectsApi.getMember(repo.projectIdOrPath, username) + val level = member?.accessLevel ?: 0 + val canPull = when (project.visibility) { + "public" -> true + "private" -> level >= REPORTER_ACCESS_LEVEL + else -> level >= GUEST_ACCESS_LEVEL + } + Perm(pull = canPull, push = level >= DEVELOPER_ACCESS_LEVEL, admin = level >= MAINTAINER_ACCESS_LEVEL) + } + + override fun list(auth: IScmAuth, opts: RepoListOptions): List = + GitlabApiTemplate.execute(auth, apiFactory) { api -> + api.projectsApi.getProjects(opts.repoName, opts.page, pageSize(opts.pageSize)) + .map(GitlabObjectConverter::convertRepository) + } + + override fun listHooks(repository: ScmProviderRepository, opts: ListOptions): List = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.projectHooksApi.getHooks(repo.projectIdOrPath, opts.page, pageSize(opts.pageSize)) + .map(GitlabObjectConverter::convertHook) + } + + override fun createHook(repository: ScmProviderRepository, input: HookInput): Hook { + validateHookInput(input) + val hook = GitlabObjectConverter.convertFromHookInput(input) + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertHook(api.projectHooksApi.addHook(repo.projectIdOrPath, hook, input.secret)) + } + } + + override fun updateHook(repository: ScmProviderRepository, hookId: Long, input: HookInput): Hook { + validateHookInput(input) + val hook = GitlabObjectConverter.convertFromHookInput(input) + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertHook( + api.projectHooksApi.updateHook(repo.projectIdOrPath, hookId, hook, input.secret) + ) + } + } + + override fun getHook(repository: ScmProviderRepository, hookId: Long): Hook = + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + GitlabObjectConverter.convertHook(api.projectHooksApi.getHook(repo.projectIdOrPath, hookId)) + } + + override fun deleteHook(repository: ScmProviderRepository, hookId: Long) { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + api.projectHooksApi.deleteHook(repo.projectIdOrPath, hookId) + } + } + + private fun pageSize(value: Int?) = (value ?: 100).coerceIn(1, 100) + + private fun validateHookInput(input: HookInput) { + require(input.url.isNotBlank()) { "hook url cannot be blank" } + requireNotNull(input.events) { "hook events cannot be null" } + } + + private companion object { + const val GUEST_ACCESS_LEVEL = 10 + const val REPORTER_ACCESS_LEVEL = 20 + const val DEVELOPER_ACCESS_LEVEL = 30 + const val MAINTAINER_ACCESS_LEVEL = 40 + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabScmProvider.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabScmProvider.kt new file mode 100644 index 0000000..87c218d --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabScmProvider.kt @@ -0,0 +1,41 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.CheckRunService +import com.tencent.devops.scm.api.FileService +import com.tencent.devops.scm.api.IssueService +import com.tencent.devops.scm.api.PullRequestService +import com.tencent.devops.scm.api.RefService +import com.tencent.devops.scm.api.RepositoryService +import com.tencent.devops.scm.api.TokenService +import com.tencent.devops.scm.api.UserService +import com.tencent.devops.scm.api.WebhookEnricher +import com.tencent.devops.scm.api.WebhookParser +import com.tencent.devops.scm.provider.git.command.GitScmProvider +import com.tencent.devops.scm.sdk.common.GitOauth2ClientProperties +import com.tencent.devops.scm.sdk.common.connector.ScmConnector +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory +import com.tencent.devops.scm.sdk.gitlab.GitlabOauth2Api + +class GitlabScmProvider private constructor( + private val apiFactory: GitlabApiFactory, + private val oauth2Api: GitlabOauth2Api? +) : GitScmProvider() { + constructor(apiUrl: String, connector: ScmConnector) : this(GitlabApiFactory(apiUrl, connector), null) + + constructor(apiUrl: String, connector: ScmConnector, properties: GitOauth2ClientProperties) : + this(GitlabApiFactory(apiUrl, connector), GitlabOauth2Api(properties, connector)) + + constructor(apiFactory: GitlabApiFactory) : this(apiFactory, null) + + override fun repositories(): RepositoryService = GitlabRepositoryService(apiFactory) + override fun refs(): RefService = GitlabRefService(apiFactory) + override fun issues(): IssueService = GitlabIssueService(apiFactory) + override fun users(): UserService = GitlabUserService(apiFactory) + override fun files(): FileService = GitlabFileService(apiFactory) + override fun webhookParser(): WebhookParser = GitlabWebhookParser() + override fun webhookEnricher(): WebhookEnricher = GitlabWebhookEnricher(apiFactory) + override fun pullRequests(): PullRequestService = GitlabPullRequestService(apiFactory) + override fun checkRun(): CheckRunService = GitlabCheckRunService(apiFactory) + override fun token(): TokenService = oauth2Api?.let(::GitlabTokenService) + ?: throw IllegalStateException("OAuth2 API not initialized") +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabTokenService.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabTokenService.kt new file mode 100644 index 0000000..b9b0463 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabTokenService.kt @@ -0,0 +1,26 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.TokenService +import com.tencent.devops.scm.api.pojo.Oauth2AccessToken +import com.tencent.devops.scm.sdk.gitlab.GitlabOauth2Api +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabOauth2AccessToken + +class GitlabTokenService(private val oauth2Api: GitlabOauth2Api) : TokenService { + override fun authorizationUrl(state: String): String = oauth2Api.authorizationUrl(state) + override fun callback(code: String): Oauth2AccessToken = convert(oauth2Api.callback(code)) + override fun refresh(refreshToken: String): Oauth2AccessToken = convert(oauth2Api.refresh(refreshToken)) + + private fun convert(from: GitlabOauth2AccessToken): Oauth2AccessToken { + val accessToken = from.accessToken?.takeIf(String::isNotBlank) + ?: throw IllegalArgumentException("OAuth access_token cannot be blank") + val tokenType = from.tokenType?.takeIf(String::isNotBlank) + ?: throw IllegalArgumentException("OAuth token_type cannot be blank") + return Oauth2AccessToken( + accessToken = accessToken, + tokenType = tokenType, + expiresIn = from.expiresIn ?: 0, + refreshToken = from.refreshToken.orEmpty(), + scope = from.scope + ) + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabUserService.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabUserService.kt new file mode 100644 index 0000000..5327528 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabUserService.kt @@ -0,0 +1,12 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.UserService +import com.tencent.devops.scm.api.pojo.User +import com.tencent.devops.scm.api.pojo.auth.IScmAuth +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory + +class GitlabUserService(private val apiFactory: GitlabApiFactory) : UserService { + override fun find(auth: IScmAuth): User = GitlabApiTemplate.execute(auth, apiFactory) { api -> + GitlabObjectConverter.convertUser(api.usersApi.currentUser) + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookEnricher.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookEnricher.kt new file mode 100644 index 0000000..3d5a41c --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookEnricher.kt @@ -0,0 +1,181 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.WebhookEnricher +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_SOURCE_BRANCH +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_MR_TARGET_BRANCH +import com.tencent.devops.scm.api.constant.WebhookOutputCode.CI_BRANCH +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_BASE_REF +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_COMMIT_AUTHOR +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_COMMIT_MESSAGE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_HEAD_REF +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_REF +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_SHA +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_SHA_SHORT +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_TAG_DESC +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_WEBHOOK_BRANCH +import com.tencent.devops.scm.api.enums.EventAction +import com.tencent.devops.scm.api.pojo.PullRequest +import com.tencent.devops.scm.api.pojo.repository.ScmProviderRepository +import com.tencent.devops.scm.api.pojo.repository.git.GitScmProviderRepository +import com.tencent.devops.scm.api.pojo.repository.git.GitScmServerRepository +import com.tencent.devops.scm.api.pojo.webhook.Webhook +import com.tencent.devops.scm.api.pojo.webhook.git.AbstractCommentHook +import com.tencent.devops.scm.api.pojo.webhook.git.GitPushHook +import com.tencent.devops.scm.api.pojo.webhook.git.GitTagHook +import com.tencent.devops.scm.api.pojo.webhook.git.IssueHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestCommentHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestReviewHook +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory + +@Suppress("TooManyFunctions") +class GitlabWebhookEnricher(private val apiFactory: GitlabApiFactory) : WebhookEnricher { + override fun enrich(repository: ScmProviderRepository, webhook: Webhook): Webhook { + require(repository is GitScmProviderRepository) { "GitLab requires GitScmProviderRepository" } + enrichRepository(repository, webhook.repository() as GitScmServerRepository) + return when (webhook) { + is PullRequestHook -> enrichPullRequest(repository, webhook) + is GitPushHook -> enrichPush(repository, webhook) + is IssueHook -> enrichIssue(repository, webhook) + is PullRequestReviewHook -> enrichReview(repository, webhook) + is AbstractCommentHook -> enrichComment(repository, webhook) + is GitTagHook -> enrichTag(repository, webhook) + else -> webhook + } + } + + private fun enrichRepository(repository: GitScmProviderRepository, webhookRepo: GitScmServerRepository) { + if (webhookRepo.defaultBranch != null && webhookRepo.httpUrl.isNotBlank() && webhookRepo.archived != null) { + return + } + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val project = api.projectsApi.getProject(repo.projectIdOrPath) + webhookRepo.defaultBranch = project.defaultBranch + webhookRepo.archived = project.archived + if (webhookRepo.httpUrl.isBlank()) webhookRepo.httpUrl = project.httpUrlToRepo.orEmpty() + } + } + + private fun enrichPullRequest(repository: GitScmProviderRepository, hook: PullRequestHook): PullRequestHook { + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val mr = api.mergeRequestsApi.getMergeRequestChanges( + repo.projectIdOrPath, + hook.pullRequest.number.toLong() + ) + check(mr.overflow != true) { + "GitLab merge request changes overflow; complete changes are unavailable" + } + if (hook.changes.isEmpty()) { + hook.changes = mr.changes.orEmpty().map(GitlabObjectConverter::convertChange) + } + hook.pullRequest.title = mr.title.orEmpty().ifBlank { hook.pullRequest.title } + hook.extras.putAll(GitlabObjectToMapConverter.convertPullRequest(mr, hook.repo)) + } + return hook + } + + private fun enrichPush(repository: GitScmProviderRepository, hook: GitPushHook): GitPushHook { + if (hook.action == EventAction.DELETE || hook.after == ZERO_SHA) return hook + + val needsCommit = hook.commit?.message?.isNotBlank() != true + val needsChanges = hook.action == EventAction.PUSH_FILE && hook.changes.isEmpty() && hook.before != ZERO_SHA + if (!needsCommit && !needsChanges) return hook + + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val changes = if (needsChanges) { + val compare = api.commitsApi.compare(repo.projectIdOrPath, hook.before, hook.after, true) + check(!compare.isCompareTimeout) { "GitLab commit comparison timed out; complete changes are unavailable" } + compare.diffs.orEmpty().map(GitlabObjectConverter::convertChange) + } else { + hook.changes + } + + val commit = if (needsCommit) { + GitlabObjectConverter.convertCommit(api.commitsApi.getCommit(repo.projectIdOrPath, hook.after)) + } else { + hook.commit + } + hook.copy(commit = commit, link = if (needsCommit) commit?.link.orEmpty() else hook.link, changes = changes) + } + } + + private fun enrichIssue(repository: GitScmProviderRepository, hook: IssueHook): IssueHook { + hook.extras.putAll(fillDefaultBranchVars(repository, hook, false)) + return hook + } + + private fun enrichComment(repository: GitScmProviderRepository, hook: AbstractCommentHook): AbstractCommentHook { + hook.extras.putAll(fillDefaultBranchVars(repository, hook, true)) + if (hook is PullRequestCommentHook) { + hook.pullRequest?.let { pullRequest -> + hook.extras.putAll(fillPullRequestVars(repository, pullRequest, hook.repo)) + // These legacy variables intentionally mean source and target branch for comment/review events. + hook.extras[PIPELINE_GIT_HEAD_REF] = pullRequest.sourceRef.name + hook.extras[PIPELINE_GIT_BASE_REF] = pullRequest.targetRef.name + } + } + return hook + } + + private fun enrichReview(repository: GitScmProviderRepository, hook: PullRequestReviewHook): PullRequestReviewHook { + hook.pullRequest?.let { pullRequest -> + val pullRequestVars = fillPullRequestVars(repository, pullRequest, hook.repo) + if (pullRequestVars.isNotEmpty()) { + hook.extras.putAll(pullRequestVars) + hook.extras[PIPELINE_GIT_HEAD_REF] = + hook.extras[BK_REPO_GIT_WEBHOOK_MR_SOURCE_BRANCH] ?: pullRequest.sourceRef.name + hook.extras[PIPELINE_GIT_BASE_REF] = + hook.extras[BK_REPO_GIT_WEBHOOK_MR_TARGET_BRANCH] ?: pullRequest.targetRef.name + } + } + hook.extras.putAll(fillDefaultBranchVars(repository, hook, true)) + return hook + } + + private fun enrichTag(repository: GitScmProviderRepository, hook: GitTagHook): GitTagHook { + if (hook.action == EventAction.DELETE) return hook + GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val tag = api.tagsApi.getTag(repo.projectIdOrPath, hook.ref.name) + hook.extras[PIPELINE_GIT_TAG_DESC] = tag.message.orEmpty() + } + return hook + } + + private fun fillPullRequestVars( + repository: GitScmProviderRepository, + pullRequest: PullRequest, + webhookRepo: GitScmServerRepository + ): Map { + if (pullRequest.number <= 0) return emptyMap() + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val mr = api.mergeRequestsApi.getMergeRequest(repo.projectIdOrPath, pullRequest.number.toLong()) + pullRequest.title = mr.title.orEmpty().ifBlank { pullRequest.title } + GitlabObjectToMapConverter.convertPullRequest(mr, webhookRepo) + } + } + + private fun fillDefaultBranchVars( + repository: GitScmProviderRepository, + webhook: Webhook, + includeCommitMessage: Boolean + ): Map { + val defaultBranch = (webhook.repository() as GitScmServerRepository).defaultBranch ?: return emptyMap() + return GitlabApiTemplate.execute(repository, apiFactory) { repo, api -> + val commit = api.commitsApi.getCommit(repo.projectIdOrPath, defaultBranch) + mutableMapOf( + PIPELINE_GIT_REF to defaultBranch, + CI_BRANCH to defaultBranch, + PIPELINE_WEBHOOK_BRANCH to defaultBranch, + PIPELINE_GIT_COMMIT_AUTHOR to commit.authorName.orEmpty(), + PIPELINE_GIT_SHA to commit.id.orEmpty(), + PIPELINE_GIT_SHA_SHORT to commit.shortId.orEmpty().ifBlank { commit.id.orEmpty().take(8) } + ).apply { + if (includeCommitMessage) put(PIPELINE_GIT_COMMIT_MESSAGE, commit.message.orEmpty()) + } + } + } + + companion object { + private const val ZERO_SHA = "0000000000000000000000000000000000000000" + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookParser.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookParser.kt new file mode 100644 index 0000000..4571180 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookParser.kt @@ -0,0 +1,505 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.WebhookParser +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_HOOK_MR_ID +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_MANUAL_UNLOCK +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_REVIEW_APPROVED_REVIEWERS +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_REVIEW_APPROVING_REVIEWERS +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_REVIEW_OWNER +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_REVIEW_RESTRICT_TYPE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_REVIEW_REVIEWABLE_ID +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_REVIEW_REVIEWABLE_TYPE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_REVIEW_REVIEWERS +import com.tencent.devops.scm.api.constant.WebhookOutputCode.BK_REPO_GIT_WEBHOOK_REVIEW_STATE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_ACTION +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_MR_URL +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_START_WEBHOOK_USER_ID +import com.tencent.devops.scm.api.enums.EventAction +import com.tencent.devops.scm.api.enums.ReviewState +import com.tencent.devops.scm.api.pojo.Change +import com.tencent.devops.scm.api.pojo.Comment +import com.tencent.devops.scm.api.pojo.Commit +import com.tencent.devops.scm.api.pojo.HookRequest +import com.tencent.devops.scm.api.pojo.Issue +import com.tencent.devops.scm.api.pojo.PullRequest +import com.tencent.devops.scm.api.pojo.Reference +import com.tencent.devops.scm.api.pojo.Review +import com.tencent.devops.scm.api.pojo.Signature +import com.tencent.devops.scm.api.pojo.User +import com.tencent.devops.scm.api.pojo.repository.git.GitScmServerRepository +import com.tencent.devops.scm.api.pojo.webhook.Webhook +import com.tencent.devops.scm.api.pojo.webhook.git.CommitCommentHook +import com.tencent.devops.scm.api.pojo.webhook.git.GitPushHook +import com.tencent.devops.scm.api.pojo.webhook.git.GitTagHook +import com.tencent.devops.scm.api.pojo.webhook.git.IssueCommentHook +import com.tencent.devops.scm.api.pojo.webhook.git.IssueHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestCommentHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestReviewHook +import com.tencent.devops.scm.provider.git.gitlab.enums.GitlabEventType +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabEventCommit +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabEventRepository +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabEventUser +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabIssueEvent +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabMergeRequestEvent +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabNoteEvent +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabPushEvent +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabReviewEvent +import com.tencent.devops.scm.sdk.gitlab.pojo.webhook.GitlabWebhookEvent +import com.tencent.devops.scm.sdk.gitlab.util.GitlabJsonUtil +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.time.Instant +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +class GitlabWebhookParser : WebhookParser { + override fun parse(request: HookRequest): Webhook? { + val event = decode(request.body, GitlabWebhookEvent::class.java) ?: return null + val kind = event.objectKind ?: return null + val header = request.header(EVENT_HEADER) + if (header != null && HEADER_KINDS[header] != kind) return null + return runCatching { + when (kind) { + "push" -> decode(request.body, GitlabPushEvent::class.java)?.let(::parsePush) + "tag_push" -> decode(request.body, GitlabPushEvent::class.java)?.let(::parseTag) + "merge_request" -> decode(request.body, GitlabMergeRequestEvent::class.java)?.let(::parseMergeRequest) + "issue" -> decode(request.body, GitlabIssueEvent::class.java)?.let(::parseIssue) + "note" -> decode(request.body, GitlabNoteEvent::class.java)?.let(::parseNote) + "review" -> decode(request.body, GitlabReviewEvent::class.java)?.let(::parseReview) + else -> null + } + }.getOrNull() + } + + override fun verify(request: HookRequest, secretToken: String?): Boolean { + if (secretToken.isNullOrEmpty()) return true + val supplied = request.header(TOKEN_HEADER) ?: return false + return MessageDigest.isEqual( + secretToken.toByteArray(StandardCharsets.UTF_8), + supplied.toByteArray(StandardCharsets.UTF_8) + ) + } + + private fun parsePush(src: GitlabPushEvent): GitPushHook { + val before = src.before.orEmpty() + val after = src.after.orEmpty() + val action = when { + before == ZERO_SHA -> EventAction.NEW_BRANCH + after == ZERO_SHA -> EventAction.DELETE + else -> EventAction.PUSH_FILE + } + val repo = repository(src.project, src.repository, src.projectId) + val commits = src.commits.orEmpty().map(::commit) + val current = if (action == EventAction.DELETE) null else commits.lastOrNull() + ?: src.checkoutSha?.let { Commit(it, "") } + val ref = src.ref.orEmpty().removePrefix("refs/heads/") + return GitPushHook( + action = action, + ref = ref, + repo = repo, + eventType = GitlabEventType.PUSH.name, + before = before, + after = after, + commit = current, + link = when (action) { + EventAction.NEW_BRANCH -> "${repo.webUrl}/-/tree/$ref" + EventAction.DELETE -> repo.webUrl + else -> current?.link.orEmpty() + }, + sender = pushUser(src), + commits = commits, + changes = if (action == EventAction.NEW_BRANCH) commitChanges(commits) else emptyList(), + totalCommitsCount = src.totalCommitsCount ?: commits.size, + outputCommitIndexVar = true + ) + } + + private fun commitChanges(commits: List): List { + val changes = linkedMapOf() + commits.forEach { commit -> + commit.added.forEach { path -> + changes[path] = Change(path = path, added = true, sha = "", blobId = "") + } + commit.modified.forEach { path -> + if (changes[path]?.added != true) { + changes[path] = Change(path = path, sha = "", blobId = "") + } + } + commit.removed.forEach { path -> + changes[path] = Change(path = path, deleted = true, oldPath = path, sha = "", blobId = "") + } + } + return changes.values.toList() + } + + private fun parseTag(src: GitlabPushEvent): GitTagHook { + val before = src.before.orEmpty() + val after = src.after.orEmpty() + val action = when { + before == ZERO_SHA -> EventAction.CREATE + after == ZERO_SHA -> EventAction.DELETE + else -> EventAction.UPDATE + } + val repo = repository(src.project, src.repository, src.projectId) + val name = src.ref.orEmpty().removePrefix("refs/tags/") + val sha = if (action == EventAction.DELETE) before else after + val eventCommit = src.commits.orEmpty().lastOrNull()?.let(::commit) + return GitTagHook( + ref = Reference(name, sha, if (action == EventAction.DELETE) repo.webUrl else "${repo.webUrl}/-/tags/$name"), + repo = repo, + eventType = GitlabEventType.TAG_PUSH.name, + action = action, + sender = pushUser(src), + commit = eventCommit ?: Commit(sha, ""), + createFrom = src.checkoutSha + ) + } + + private fun parseMergeRequest(src: GitlabMergeRequestEvent): PullRequestHook { + val attr = src.objectAttributes ?: throw IllegalArgumentException("object_attributes cannot be null") + val actionName = attr.action.orEmpty() + val action = when (actionName) { + "open" -> EventAction.OPEN + "reopen" -> EventAction.REOPEN + "close" -> EventAction.CLOSE + "merge" -> EventAction.MERGE + "update" -> EventAction.PUSH_UPDATE + "approved", "approval", "unapproved", "unapproval" -> EventAction.EDIT + else -> EventAction.UNKNOWN + } + val repo = repository(src.project, src.repository, attr.targetProjectId ?: src.projectId) + val sender = user(src.user) + val pr = pullRequest(attr, repo, author(attr.author, attr.authorId, sender)) + val lastCommit = attr.lastCommit?.let(::commit) ?: Commit(attr.sha.orEmpty(), "") + return PullRequestHook( + action = action, + repo = repo, + eventType = GitlabEventType.MERGE_REQUEST.name, + pullRequest = pr, + sender = sender, + commit = lastCommit, + changes = emptyList(), + extras = GitlabObjectToMapConverter.convertPullRequest(pr, actionName, sender) + ) + } + + private fun parseIssue(src: GitlabIssueEvent): IssueHook { + val attr = src.objectAttributes ?: throw IllegalArgumentException("object_attributes cannot be null") + val actionName = attr.action.orEmpty() + val sender = user(src.user) + val repo = repository(src.project, src.repository, src.projectId) + return IssueHook( + action = when (actionName) { + "open" -> EventAction.CREATE + "reopen" -> EventAction.REOPEN + "close" -> EventAction.CLOSE + "update" -> EventAction.UPDATE + else -> EventAction.UNKNOWN + }, + repo = repo, + eventType = GitlabEventType.ISSUES.name, + issue = issue(attr, author(attr.author, attr.authorId, sender), repo), + sender = sender + ) + } + + private fun parseNote(src: GitlabNoteEvent): Webhook? { + val attr = src.objectAttributes ?: throw IllegalArgumentException("object_attributes cannot be null") + val sender = user(src.user) + val repo = repository(src.project, src.repository, src.projectId) + val action = when (attr.action) { + "update" -> EventAction.UPDATE + null, "", "create" -> EventAction.CREATE + else -> EventAction.UNKNOWN + } + val comment = Comment( + id = attr.id ?: 0, + body = attr.note.orEmpty(), + link = attr.url.orEmpty(), + author = sender, + created = time(attr.createdAt) ?: EPOCH, + updated = time(attr.updatedAt), + type = attr.noteableType.orEmpty() + ) + val extras = GitlabObjectToMapConverter.convertNote(attr.createdAt, attr.updatedAt) + return when (attr.noteableType) { + "MergeRequest" -> src.mergeRequest?.let { mergeRequest -> + PullRequestCommentHook( + action, + repo, + GitlabEventType.NOTE.name, + comment, + sender, + extras, + pullRequest(mergeRequest, repo, author(mergeRequest.author, mergeRequest.authorId, sender)) + ) + } + "Issue" -> src.issue?.let { eventIssue -> + IssueCommentHook( + action, + repo, + GitlabEventType.NOTE.name, + comment, + sender, + extras, + issue(eventIssue, author(eventIssue.author, eventIssue.authorId, sender), repo) + ) + } + "Commit" -> src.commit?.let { eventCommit -> + CommitCommentHook( + action, + repo, + GitlabEventType.NOTE.name, + comment, + sender, + extras, + commit(eventCommit) + ) + } + else -> null + } + } + + private fun parseReview(src: GitlabReviewEvent): PullRequestReviewHook { + val repo = repository(null, src.repository, src.projectId) + val eventReviewer = src.reviewer + val sender = user(eventReviewer?.reviewer ?: src.author) + val sourceState = eventReviewer?.state ?: src.state + val review = Review( + id = src.id?.toLongOrNull() ?: 0, + iid = src.iid?.toIntOrNull() ?: 0, + state = reviewState(sourceState, src.event), + author = user(src.author), + link = "${repo.webUrl}/reviews/${src.iid.orEmpty()}", + closed = sourceState == "close" || sourceState == "closed", + title = "" + ) + val pullRequest = if (src.reviewableType.equals("merge_request", ignoreCase = true)) { + PullRequest( + id = src.reviewableId ?: 0, + number = 0, + title = "", + body = "", + link = "", + sourceRepo = repo, + targetRepo = repo, + sourceRef = Reference("", ""), + targetRef = Reference("", ""), + author = user(src.author) + ) + } else null + return PullRequestReviewHook( + action = EventAction.CREATE, + repo = repo, + eventType = GitlabEventType.REVIEW.name, + pullRequest = pullRequest, + review = review, + sender = sender, + extras = reviewExtras(src) + ) + } + + private fun reviewState(state: String?, event: String?): ReviewState = when (state) { + "approving" -> ReviewState.APPROVING + "approved" -> ReviewState.APPROVED + "change_required" -> ReviewState.CHANGE_REQUIRED + "change_denied" -> ReviewState.CHANGE_DENIED + "close", "closed" -> ReviewState.CLOSED + "empty" -> if (event == EventAction.CREATE.value || event == EventAction.REOPEN.value) { + ReviewState.APPROVING + } else { + ReviewState.UNKNOWN + } + else -> ReviewState.UNKNOWN + } + + private fun reviewExtras(src: GitlabReviewEvent): MutableMap { + val reviewers = src.reviewers.orEmpty().mapNotNull { it.reviewer?.username } + val approving = src.reviewers.orEmpty().filter { it.state == "approving" } + .mapNotNull { it.reviewer?.username } + val approved = src.reviewers.orEmpty().filter { it.state == "approved" } + .mapNotNull { it.reviewer?.username } + return mutableMapOf( + BK_REPO_GIT_WEBHOOK_REVIEW_STATE to src.state.orEmpty(), + BK_REPO_GIT_WEBHOOK_REVIEW_OWNER to src.author?.username.orEmpty(), + BK_REPO_GIT_WEBHOOK_REVIEW_REVIEWABLE_ID to (src.reviewableId ?: 0), + BK_REPO_GIT_WEBHOOK_REVIEW_REVIEWABLE_TYPE to src.reviewableType.orEmpty(), + BK_REPO_GIT_WEBHOOK_REVIEW_RESTRICT_TYPE to src.restrictType.orEmpty(), + BK_REPO_GIT_WEBHOOK_REVIEW_REVIEWERS to reviewers.joinToString(","), + BK_REPO_GIT_WEBHOOK_REVIEW_APPROVING_REVIEWERS to approving.joinToString(","), + BK_REPO_GIT_WEBHOOK_REVIEW_APPROVED_REVIEWERS to approved.joinToString(","), + BK_REPO_GIT_MANUAL_UNLOCK to false, + PIPELINE_GIT_ACTION to src.event.orEmpty(), + PIPELINE_START_WEBHOOK_USER_ID to src.author?.username.orEmpty() + ).apply { + if (src.reviewableType.equals("merge_request", ignoreCase = true)) { + put(BK_HOOK_MR_ID, src.reviewableId ?: 0) + put(PIPELINE_GIT_MR_URL, "${src.repository?.homepage.orEmpty()}/merge_requests/${src.iid.orEmpty()}") + } + } + } + + private fun repository( + project: GitlabEventRepository?, + legacy: GitlabEventRepository?, + projectId: Long? + ): GitScmServerRepository { + val raw = project ?: legacy + val fullName = raw?.pathWithNamespace ?: raw?.name.orEmpty() + return GitScmServerRepository( + id = raw?.id ?: projectId ?: 0, + group = fullName.substringBeforeLast('/', ""), + name = raw?.name.orEmpty().ifBlank { fullName.substringAfterLast('/') }, + fullName = fullName, + defaultBranch = raw?.defaultBranch, + httpUrl = raw?.gitHttpUrl ?: raw?.httpUrl ?: raw?.httpUrlToRepo ?: raw?.url.orEmpty(), + sshUrl = raw?.gitSshUrl ?: raw?.sshUrl ?: raw?.sshUrlToRepo.orEmpty(), + webUrl = raw?.webUrl ?: raw?.homepage.orEmpty() + ) + } + + private fun pullRequest( + attr: GitlabMergeRequestEvent.ObjectAttributes, + repo: GitScmServerRepository, + author: User + ): PullRequest { + val source = repositoryFromNested(attr.source, attr.sourceProjectId, repo) + val target = repositoryFromNested(attr.target, attr.targetProjectId, repo) + val sha = attr.lastCommit?.id ?: attr.sha.orEmpty() + val iid = attr.iid ?: 0 + return PullRequest( + id = attr.id ?: 0, + number = iid, + title = attr.title.orEmpty(), + body = attr.description.orEmpty(), + link = attr.url ?: attr.webUrl ?: "${repo.webUrl}/-/merge_requests/$iid", + sha = sha, + sourceRepo = source, + targetRepo = target, + sourceRef = Reference(attr.sourceBranch.orEmpty(), sha), + targetRef = Reference(attr.targetBranch.orEmpty(), ""), + closed = attr.state == "closed", + merged = attr.state == "merged", + mergeCommitSha = attr.mergeCommitSha ?: attr.squashCommitSha, + author = author, + created = time(attr.createdAt), + updated = time(attr.updatedAt), + labels = attr.labels.orEmpty().mapNotNull { it.title }, + description = attr.description + ) + } + + private fun repositoryFromNested( + raw: GitlabEventRepository?, + projectId: Long?, + fallback: GitScmServerRepository + ): GitScmServerRepository { + if (raw == null) return fallback.copy(id = projectId?.takeIf { it > 0 } ?: fallback.id) + return repository(raw, null, projectId) + } + + private fun issue( + attr: GitlabIssueEvent.ObjectAttributes, + author: User, + repo: GitScmServerRepository + ): Issue { + val iid = attr.iid ?: 0 + return Issue( + id = attr.id ?: 0, + number = iid, + title = attr.title.orEmpty(), + body = attr.description, + link = attr.url ?: attr.webUrl ?: "${repo.webUrl}/-/issues/$iid", + labels = attr.labels.orEmpty().mapNotNull { it.title }, + closed = attr.state == "closed", + author = author, + created = time(attr.createdAt) ?: EPOCH, + updated = time(attr.updatedAt), + milestoneId = attr.milestoneId?.toString(), + state = attr.state + ) + } + + private fun pushUser(src: GitlabPushEvent) = User( + id = src.userId ?: 0, + username = src.userUsername ?: src.userName.orEmpty(), + name = src.userName.orEmpty(), + email = src.userEmail, + avatar = src.userAvatar + ) + + private fun user(src: GitlabEventUser?) = User( + id = src?.id ?: 0, + username = src?.username.orEmpty(), + name = src?.name.orEmpty(), + email = src?.email, + avatar = src?.avatarUrl + ) + + private fun author(src: GitlabEventUser?, authorId: Long?, fallback: User): User { + if (src != null) { + return User( + id = src.id ?: fallback.id, + username = src.username ?: fallback.username, + name = src.name ?: fallback.name, + email = src.email ?: fallback.email, + avatar = src.avatarUrl ?: fallback.avatar + ) + } + return if (authorId != null && authorId > 0) fallback.copy(id = authorId) else fallback + } + + private fun commit(src: GitlabEventCommit): Commit { + val author = Signature( + src.author?.name ?: src.authorName.orEmpty(), + src.author?.email ?: src.authorEmail + ) + return Commit( + sha = src.id ?: src.sha.orEmpty(), + message = src.message.orEmpty(), + author = author, + committer = author, + commitTime = time(src.timestamp), + link = src.url ?: src.webUrl.orEmpty(), + added = src.added.orEmpty(), + modified = src.modified.orEmpty(), + removed = src.removed.orEmpty() + ) + } + + private fun time(value: String?): LocalDateTime? { + if (value.isNullOrBlank()) return null + return runCatching { LocalDateTime.ofInstant(Instant.parse(value), ZoneOffset.UTC) } + .recoverCatching { OffsetDateTime.parse(value).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime() } + .recoverCatching { LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME) } + .recoverCatching { LocalDateTime.parse(value, UTC_TIME_FORMATTER) } + .getOrNull() + } + + private fun decode(body: String, type: Class): T? = runCatching { + GitlabJsonUtil.getJsonFactory().fromJson(body, type) + }.getOrNull() + + private fun HookRequest.header(name: String) = headers?.entries?.firstOrNull { + it.key.equals(name, ignoreCase = true) + }?.value + + companion object { + private val UTC_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss 'UTC'") + private val EPOCH = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC) + private const val ZERO_SHA = "0000000000000000000000000000000000000000" + private const val EVENT_HEADER = "X-Gitlab-Event" + private const val TOKEN_HEADER = "X-Gitlab-Token" + private val HEADER_KINDS = mapOf( + "Push Hook" to "push", + "Tag Push Hook" to "tag_push", + "Merge Request Hook" to "merge_request", + "Issue Hook" to "issue", + "Note Hook" to "note", + "Review Hook" to "review" + ) + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabAuthProviderAdapter.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabAuthProviderAdapter.kt new file mode 100644 index 0000000..f50e25d --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabAuthProviderAdapter.kt @@ -0,0 +1,9 @@ +package com.tencent.devops.scm.provider.git.gitlab.auth + +import com.tencent.devops.scm.api.pojo.auth.IScmAuth +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabAuthProvider + +internal interface GitlabAuthProviderAdapter { + fun support(auth: IScmAuth): Boolean + fun get(auth: IScmAuth): GitlabAuthProvider +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabAuthProviderFactory.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabAuthProviderFactory.kt new file mode 100644 index 0000000..ece8471 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabAuthProviderFactory.kt @@ -0,0 +1,21 @@ +package com.tencent.devops.scm.provider.git.gitlab.auth + +import com.tencent.devops.scm.api.pojo.auth.AccessTokenScmAuth +import com.tencent.devops.scm.api.pojo.auth.IScmAuth +import com.tencent.devops.scm.api.pojo.auth.PersonalAccessTokenScmAuth +import com.tencent.devops.scm.api.pojo.auth.TokenSshPrivateKeyScmAuth +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabAuthProvider + +object GitlabAuthProviderFactory { + private val adapters = listOf(GitlabOauthAuthProviderAdapter(), GitlabPrivateTokenAuthProviderAdapter()) + + fun support(auth: IScmAuth): Boolean = auth is AccessTokenScmAuth || + auth is PersonalAccessTokenScmAuth || auth is TokenSshPrivateKeyScmAuth + + fun create(auth: IScmAuth): GitlabAuthProvider { + if (!support(auth)) { + throw UnsupportedOperationException("unsupported GitLab auth type: ${auth.javaClass.name}") + } + return adapters.first { it.support(auth) }.get(auth) + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabOauthAuthProviderAdapter.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabOauthAuthProviderAdapter.kt new file mode 100644 index 0000000..038667a --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabOauthAuthProviderAdapter.kt @@ -0,0 +1,15 @@ +package com.tencent.devops.scm.provider.git.gitlab.auth + +import com.tencent.devops.scm.api.pojo.auth.AccessTokenScmAuth +import com.tencent.devops.scm.api.pojo.auth.IScmAuth +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabAuthProvider +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabTokenAuthProvider + +internal class GitlabOauthAuthProviderAdapter : GitlabAuthProviderAdapter { + override fun support(auth: IScmAuth) = auth is AccessTokenScmAuth + + override fun get(auth: IScmAuth): GitlabAuthProvider { + require(auth is AccessTokenScmAuth) { "unsupported GitLab auth type: ${auth.javaClass.name}" } + return GitlabTokenAuthProvider.oauthAccessToken(auth.accessToken) + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabPrivateTokenAuthProviderAdapter.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabPrivateTokenAuthProviderAdapter.kt new file mode 100644 index 0000000..bfbfa57 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/auth/GitlabPrivateTokenAuthProviderAdapter.kt @@ -0,0 +1,18 @@ +package com.tencent.devops.scm.provider.git.gitlab.auth + +import com.tencent.devops.scm.api.pojo.auth.IScmAuth +import com.tencent.devops.scm.api.pojo.auth.PersonalAccessTokenScmAuth +import com.tencent.devops.scm.api.pojo.auth.TokenSshPrivateKeyScmAuth +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabAuthProvider +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabTokenAuthProvider + +internal class GitlabPrivateTokenAuthProviderAdapter : GitlabAuthProviderAdapter { + override fun support(auth: IScmAuth) = + auth is PersonalAccessTokenScmAuth || auth is TokenSshPrivateKeyScmAuth + + override fun get(auth: IScmAuth): GitlabAuthProvider = when (auth) { + is PersonalAccessTokenScmAuth -> GitlabTokenAuthProvider.personalAccessToken(auth.personalAccessToken) + is TokenSshPrivateKeyScmAuth -> GitlabTokenAuthProvider.privateToken(auth.token) + else -> throw UnsupportedOperationException("unsupported GitLab auth type: ${auth.javaClass.name}") + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/enums/GitlabEventType.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/enums/GitlabEventType.kt new file mode 100644 index 0000000..d22da4a --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/main/kotlin/com/tencent/devops/scm/provider/git/gitlab/enums/GitlabEventType.kt @@ -0,0 +1,10 @@ +package com.tencent.devops.scm.provider.git.gitlab.enums + +enum class GitlabEventType(val value: String) { + PUSH("push"), + TAG_PUSH("tag_push"), + MERGE_REQUEST("merge_request"), + ISSUES("issues"), + NOTE("note"), + REVIEW("review") +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabAuthProviderFactoryTest.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabAuthProviderFactoryTest.kt new file mode 100644 index 0000000..9bf7f55 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabAuthProviderFactoryTest.kt @@ -0,0 +1,33 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.pojo.auth.AccessTokenScmAuth +import com.tencent.devops.scm.api.pojo.auth.PersonalAccessTokenScmAuth +import com.tencent.devops.scm.api.pojo.auth.TokenSshPrivateKeyScmAuth +import com.tencent.devops.scm.api.pojo.auth.UserPassScmAuth +import com.tencent.devops.scm.provider.git.gitlab.auth.GitlabAuthProviderFactory +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class GitlabAuthProviderFactoryTest { + @Test + fun `supports only oauth pat and private token credentials`() { + val supported = listOf( + AccessTokenScmAuth("oauth"), + PersonalAccessTokenScmAuth("pat"), + TokenSshPrivateKeyScmAuth("private-token", "private-key", "passphrase") + ) + supported.forEach { + assertTrue(GitlabAuthProviderFactory.support(it)) + assertNotNull(GitlabAuthProviderFactory.create(it)) + } + + val unsupported = UserPassScmAuth("user", "password") + assertFalse(GitlabAuthProviderFactory.support(unsupported)) + assertThrows(UnsupportedOperationException::class.java) { + GitlabAuthProviderFactory.create(unsupported) + } + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectConverterTest.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectConverterTest.kt new file mode 100644 index 0000000..42f199c --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabObjectConverterTest.kt @@ -0,0 +1,55 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.enums.CheckRunConclusion +import com.tencent.devops.scm.api.enums.CheckRunStatus +import com.tencent.devops.scm.api.enums.Visibility +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProject +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommitStatus +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +class GitlabObjectConverterTest { + @Test + fun `converts namespaced project without deriving it from tgit urls`() { + val project = GitlabProject().apply { + id = 42 + name = "demo" + pathWithNamespace = "group/subgroup/demo" + archived = true + visibility = "internal" + httpUrlToRepo = "https://gitlab.example.com/group/subgroup/demo.git" + sshUrlToRepo = "git@gitlab.example.com:group/subgroup/demo.git" + webUrl = "https://gitlab.example.com/group/subgroup/demo" + } + val converted = GitlabObjectConverter.convertRepository(project) + assertEquals("group/subgroup", converted.group) + assertEquals("group/subgroup/demo", converted.fullName) + assertEquals(true, converted.archived) + assertEquals(Visibility.INTERNAL, converted.visibility) + } + + @Test + fun `maps all supported commit status outcomes`() { + assertEquals("pending", GitlabObjectConverter.convertCheckRunState(CheckRunStatus.QUEUED, null)) + assertEquals("running", GitlabObjectConverter.convertCheckRunState(CheckRunStatus.IN_PROGRESS, null)) + assertEquals( + "canceled", + GitlabObjectConverter.convertCheckRunState(CheckRunStatus.COMPLETED, CheckRunConclusion.CANCELLED) + ) + assertThrows(IllegalArgumentException::class.java) { + GitlabObjectConverter.convertCheckRunState(CheckRunStatus.COMPLETED, null) + } + assertThrows(IllegalArgumentException::class.java) { + GitlabObjectConverter.convertCheckRunState(CheckRunStatus.COMPLETED, CheckRunConclusion.UNKNOWN) + } + } + + @Test + fun `rejects unknown GitLab commit status`() { + val status = GitlabCommitStatus().apply { this.status = "new-status" } + assertThrows(IllegalArgumentException::class.java) { + GitlabObjectConverter.convertCheckRun(status) + } + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabServicesTest.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabServicesTest.kt new file mode 100644 index 0000000..9e3421f --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabServicesTest.kt @@ -0,0 +1,334 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.enums.CheckRunStatus +import com.tencent.devops.scm.api.exception.ScmApiException +import com.tencent.devops.scm.api.pojo.CheckRunInput +import com.tencent.devops.scm.api.pojo.ContentInput +import com.tencent.devops.scm.api.pojo.HookInput +import com.tencent.devops.scm.api.pojo.IssueInput +import com.tencent.devops.scm.api.pojo.ListOptions +import com.tencent.devops.scm.api.pojo.PullRequestInput +import com.tencent.devops.scm.api.pojo.ReferenceInput +import com.tencent.devops.scm.api.pojo.auth.PersonalAccessTokenScmAuth +import com.tencent.devops.scm.api.pojo.repository.git.GitScmProviderRepository +import com.tencent.devops.scm.sdk.gitlab.GitlabApi +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory +import com.tencent.devops.scm.sdk.gitlab.GitlabCommitStatusesApi +import com.tencent.devops.scm.sdk.gitlab.GitlabCommitsApi +import com.tencent.devops.scm.sdk.gitlab.GitlabIssuesApi +import com.tencent.devops.scm.sdk.gitlab.GitlabMergeRequestsApi +import com.tencent.devops.scm.sdk.gitlab.GitlabOauth2Api +import com.tencent.devops.scm.sdk.gitlab.GitlabProjectsApi +import com.tencent.devops.scm.sdk.gitlab.GitlabRepositoryFilesApi +import com.tencent.devops.scm.sdk.gitlab.GitlabUsersApi +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommit +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommitStatus +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabIssue +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequest +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMember +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabOauth2AccessToken +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProject +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabRepositoryFile +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabTreeItem +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabUser +import com.tencent.devops.scm.sdk.common.exception.ScmHttpRetryException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import java.util.Date + +class GitlabServicesTest { + private val auth = PersonalAccessTokenScmAuth("token") + private val repository = GitScmProviderRepository(projectIdOrPath = "group/demo", auth = auth) + + @Test + fun `repository service converts a project`() { + val (factory, api) = api() + val projects = mock(GitlabProjectsApi::class.java) + `when`(api.projectsApi).thenReturn(projects) + `when`(projects.getProject("group/demo")).thenReturn(project()) + assertEquals("group/demo", GitlabRepositoryService(factory).find(repository).fullName) + } + + @Test + fun `provider preserves retry exception type`() { + val factory = mock(GitlabApiFactory::class.java) + `when`(factory.fromAuthProvider(any())).thenThrow(ScmHttpRetryException()) + + assertThrows(ScmHttpRetryException::class.java) { + GitlabRepositoryService(factory).find(repository) + } + } + + @Test + fun `repository pull permission respects visibility and reporter level`() { + val (factory, api) = api() + val projects = mock(GitlabProjectsApi::class.java) + val project = project().apply { visibility = "private" } + val member = GitlabMember().apply { username = "dev"; accessLevel = 10 } + `when`(api.projectsApi).thenReturn(projects) + `when`(projects.getProject("group/demo")).thenReturn(project) + `when`(projects.getMember("group/demo", "dev")).thenReturn(member) + + val service = GitlabRepositoryService(factory) + assertFalse(service.findPerms(repository, "dev").pull) + member.accessLevel = 20 + assertTrue(service.findPerms(repository, "dev").pull) + project.visibility = "public" + member.accessLevel = 0 + assertTrue(service.findPerms(repository, "dev").pull) + } + + @Test + fun `ref service converts a commit`() { + val (factory, api) = api() + val commits = mock(GitlabCommitsApi::class.java) + `when`(api.commitsApi).thenReturn(commits) + `when`(commits.getCommit("group/demo", "main")).thenReturn(commit()) + assertEquals("abc", GitlabRefService(factory).findCommit(repository, "main").sha) + } + + @Test + fun `pull request service uses project iid`() { + val (factory, api) = api() + val mergeRequests = mock(GitlabMergeRequestsApi::class.java) + val projects = mock(GitlabProjectsApi::class.java) + `when`(api.mergeRequestsApi).thenReturn(mergeRequests) + `when`(api.projectsApi).thenReturn(projects) + `when`(mergeRequests.getMergeRequest("group/demo", 17)).thenReturn(mergeRequest()) + `when`(projects.getProject(42L)).thenReturn(project()) + assertEquals(17, GitlabPullRequestService(factory).find(repository, 17).number) + verify(mergeRequests).getMergeRequest("group/demo", 17) + + assertThrows(IllegalArgumentException::class.java) { + GitlabPullRequestService(factory).create(repository, PullRequestInput("", null, "source", "main")) + } + } + + @Test + fun `pull request changes reject overflow response`() { + val (factory, api) = api() + val mergeRequests = mock(GitlabMergeRequestsApi::class.java) + `when`(api.mergeRequestsApi).thenReturn(mergeRequests) + `when`(mergeRequests.getMergeRequestChanges("group/demo", 17)).thenReturn( + GitlabMergeRequest().apply { overflow = true } + ) + + val error = assertThrows(ScmApiException::class.java) { + GitlabPullRequestService(factory).listChanges(repository, 17, ListOptions()) + } + assertTrue(error.message.orEmpty().contains("overflow")) + } + + @Test + fun `issue service uses project iid`() { + val (factory, api) = api() + val issues = mock(GitlabIssuesApi::class.java) + `when`(api.issuesApi).thenReturn(issues) + `when`(issues.getIssue("group/demo", 23)).thenReturn(issue()) + assertEquals(23, GitlabIssueService(factory).find(repository, 23).number) + verify(issues).getIssue("group/demo", 23) + + assertThrows(IllegalArgumentException::class.java) { + GitlabIssueService(factory).create(repository, IssueInput("")) + } + } + + @Test + fun `file service decodes GitLab base64 content`() { + val (factory, api) = api() + val files = mock(GitlabRepositoryFilesApi::class.java) + `when`(api.repositoryFilesApi).thenReturn(files) + val file = GitlabRepositoryFile().apply { + filePath = "README.md" + encoding = "base64" + content = "aGVsbG8=" + commitId = "abc" + blobId = "blob" + } + `when`(files.getFile("group/demo", "README.md", "main")).thenReturn(file) + assertEquals("hello", GitlabFileService(factory).find(repository, "README.md", "main").content) + + GitlabFileService(factory).create(repository, "new.txt", ContentInput("main", "add", "text")) + verify(files).createFile("group/demo", "new.txt", "main", "text", "add") + } + + @Test + fun `file service lists every tree page`() { + val (factory, api) = api() + val files = mock(GitlabRepositoryFilesApi::class.java) + `when`(api.repositoryFilesApi).thenReturn(files) + val firstPage = (1..100).map { index -> + GitlabTreeItem().apply { + id = "blob-$index" + name = "file-$index" + path = "directory/file-$index" + } + } + val secondPage = listOf( + GitlabTreeItem().apply { + id = "blob-101" + name = "file-101" + path = "directory/file-101" + } + ) + `when`(files.getTree("group/demo", "", "main", true, 1, 100)).thenReturn(firstPage) + `when`(files.getTree("group/demo", "", "main", true, 2, 100)).thenReturn(secondPage) + + val trees = GitlabFileService(factory).listTree(repository, "", "main", true) + + assertEquals(101, trees.size) + assertEquals("file-1", trees.first().path) + verify(files).getTree("group/demo", "", "main", true, 2, 100) + } + + @Test + fun `file service returns paths relative to requested directory`() { + val (factory, api) = api() + val files = mock(GitlabRepositoryFilesApi::class.java) + `when`(api.repositoryFilesApi).thenReturn(files) + val fileTrees = listOf( + GitlabTreeItem().apply { + id = "build-blob" + name = "build.yml" + path = ".ci/build.yml" + type = "blob" + mode = "100644" + }, + GitlabTreeItem().apply { + id = "template-blob" + name = "base.yml" + path = ".ci/templates/base.yml" + type = "blob" + mode = "100644" + } + ) + `when`(files.getTree("group/demo", ".ci", "main", true, 1, 100)).thenReturn(fileTrees) + + val trees = GitlabFileService(factory).listTree(repository, ".ci", "main", true) + + assertEquals(listOf("build.yml", "templates/base.yml"), trees.map { it.path }) + } + + @Test + fun `ref creation rejects missing sha`() { + val factory = mock(GitlabApiFactory::class.java) + val service = GitlabRefService(factory) + listOf(null, "", " ").forEach { sha -> + assertThrows(IllegalArgumentException::class.java) { + service.createBranch(repository, ReferenceInput("branch", sha)) + } + assertThrows(IllegalArgumentException::class.java) { + service.createTag(repository, ReferenceInput("tag", sha)) + } + } + } + + @Test + fun `hook update rejects missing url or events`() { + val service = GitlabRepositoryService(mock(GitlabApiFactory::class.java)) + assertThrows(IllegalArgumentException::class.java) { + service.updateHook(repository, 1, HookInput("hook", "", events = com.tencent.devops.scm.api.pojo.HookEvents())) + } + assertThrows(IllegalArgumentException::class.java) { + service.updateHook(repository, 1, HookInput("hook", "https://example.com/hook", events = null)) + } + } + + @Test + fun `user service converts current user`() { + val (factory, api) = api() + val users = mock(GitlabUsersApi::class.java) + `when`(api.usersApi).thenReturn(users) + `when`(users.currentUser).thenReturn(GitlabUser().apply { id = 7; username = "dev"; name = "Dev" }) + assertEquals("dev", GitlabUserService(factory).find(auth).username) + } + + @Test + fun `check run service maps queued status and validates ref`() { + val (factory, api) = api() + val statuses = mock(GitlabCommitStatusesApi::class.java) + `when`(api.commitStatusesApi).thenReturn(statuses) + `when`(statuses.create("group/demo", "abc", "pending", "ci", null, null, null)).thenReturn( + GitlabCommitStatus().apply { id = 1; status = "pending"; name = "ci" } + ) + val service = GitlabCheckRunService(factory) + assertEquals(CheckRunStatus.QUEUED, service.create(repository, CheckRunInput(name = "ci", ref = "abc", status = CheckRunStatus.QUEUED)).status) + assertThrows(IllegalArgumentException::class.java) { + service.create(repository, CheckRunInput(name = "ci", ref = null, status = CheckRunStatus.QUEUED)) + } + } + + @Test + fun `token service preserves oauth fields`() { + val oauth = mock(GitlabOauth2Api::class.java) + `when`(oauth.callback("code")).thenReturn(GitlabOauth2AccessToken().apply { + accessToken = "access" + tokenType = "Bearer" + expiresIn = 7200 + refreshToken = "refresh" + scope = "api" + }) + val token = GitlabTokenService(oauth).callback("code") + assertEquals("access", token.accessToken) + assertEquals("refresh", token.refreshToken) + + `when`(oauth.callback("missing-access")).thenReturn(GitlabOauth2AccessToken().apply { tokenType = "Bearer" }) + assertThrows(IllegalArgumentException::class.java) { + GitlabTokenService(oauth).callback("missing-access") + } + `when`(oauth.refresh("missing-type")).thenReturn(GitlabOauth2AccessToken().apply { accessToken = "access" }) + assertThrows(IllegalArgumentException::class.java) { + GitlabTokenService(oauth).refresh("missing-type") + } + } + + private fun api(): Pair { + val factory = mock(GitlabApiFactory::class.java) + val api = mock(GitlabApi::class.java) + `when`(factory.fromAuthProvider(any())).thenReturn(api) + return factory to api + } + + private fun project() = GitlabProject().apply { + id = 42 + name = "demo" + pathWithNamespace = "group/demo" + httpUrlToRepo = "https://gitlab.example.com/group/demo.git" + sshUrlToRepo = "git@gitlab.example.com:group/demo.git" + webUrl = "https://gitlab.example.com/group/demo" + } + + private fun commit() = GitlabCommit().apply { + id = "abc" + message = "message" + committedDate = Date(0) + } + + private fun mergeRequest() = GitlabMergeRequest().apply { + id = 9001 + iid = 17 + title = "MR" + description = "body" + state = "opened" + sourceBranch = "feature" + targetBranch = "main" + sourceProjectId = 42 + targetProjectId = 42 + sha = "abc" + } + + private fun issue() = GitlabIssue().apply { + id = 1001 + iid = 23 + title = "Issue" + state = "opened" + createdAt = Date(0) + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookEnricherTest.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookEnricherTest.kt new file mode 100644 index 0000000..af32d32 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookEnricherTest.kt @@ -0,0 +1,230 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.pojo.HookRequest +import com.tencent.devops.scm.api.constant.WebhookOutputCode.CI_BRANCH +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_BASE_REF +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_COMMIT_MESSAGE +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_HEAD_REF +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_SHA +import com.tencent.devops.scm.api.constant.WebhookOutputCode.PIPELINE_GIT_TAG_DESC +import com.tencent.devops.scm.api.pojo.auth.PersonalAccessTokenScmAuth +import com.tencent.devops.scm.api.pojo.repository.git.GitScmProviderRepository +import com.tencent.devops.scm.api.pojo.webhook.git.GitPushHook +import com.tencent.devops.scm.api.pojo.webhook.git.GitTagHook +import com.tencent.devops.scm.api.pojo.webhook.git.IssueHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestCommentHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestReviewHook +import com.tencent.devops.scm.sdk.gitlab.GitlabApi +import com.tencent.devops.scm.sdk.gitlab.GitlabApiFactory +import com.tencent.devops.scm.sdk.gitlab.GitlabCommitsApi +import com.tencent.devops.scm.sdk.gitlab.GitlabMergeRequestsApi +import com.tencent.devops.scm.sdk.gitlab.GitlabProjectsApi +import com.tencent.devops.scm.sdk.gitlab.GitlabTagsApi +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommit +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCompareResults +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabDiff +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabDiffRefs +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequest +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMilestone +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProject +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabTag +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabUser +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoInteractions +import org.mockito.Mockito.`when` +import java.util.Date + +class GitlabWebhookEnricherTest { + private val parser = GitlabWebhookParser() + private val repository = GitScmProviderRepository( + projectIdOrPath = "group/demo", + auth = PersonalAccessTokenScmAuth("token") + ) + + @Test + fun `enriches pull request changes and complete variables`() { + val mocks = apiMocks() + val hook = parseFixture("gitlab_mr_open_event.json", "Merge Request Hook") as PullRequestHook + val mr = mergeRequest().apply { changes = listOf(diff()) } + `when`(mocks.mergeRequests.getMergeRequestChanges("group/demo", 17)).thenReturn(mr) + + val enriched = GitlabWebhookEnricher(mocks.factory).enrich(repository, hook) as PullRequestHook + + assertSame(hook, enriched) + assertEquals("src/Main.kt", enriched.changes.single().path) + assertEquals("backend,urgent", enriched.outputs()["BK_CI_REPO_GIT_WEBHOOK_MR_LABELS"]) + assertEquals("reviewer", enriched.outputs()["BK_CI_REPO_GIT_WEBHOOK_MR_REVIEWERS"]) + assertEquals("base-sha", enriched.outputs()["BK_REPO_GIT_WEBHOOK_MR_BASE_COMMIT"]) + assertEquals("head-sha", enriched.outputs()["BK_REPO_GIT_WEBHOOK_MR_SOURCE_COMMIT"]) + assertEquals(false, enriched.repo.archived) + } + + @Test + fun `enriches issue and merge request comment with default branch variables`() { + val mocks = apiMocks() + `when`(mocks.mergeRequests.getMergeRequest("group/demo", 17)).thenReturn(mergeRequest()) + val issue = parseFixture("gitlab_issue_event.json", "Issue Hook") as IssueHook + + GitlabWebhookEnricher(mocks.factory).enrich(repository, issue) + + assertEquals("main", issue.outputs()[CI_BRANCH]) + assertEquals("main-sha", issue.outputs()[PIPELINE_GIT_SHA]) + + val comment = parseFixture("gitlab_mr_note_event.json", "Note Hook") as PullRequestCommentHook + GitlabWebhookEnricher(mocks.factory).enrich(repository, comment) + + assertEquals("default message", comment.outputs()[PIPELINE_GIT_COMMIT_MESSAGE]) + assertEquals(comment.pullRequest?.sourceRef?.name, comment.outputs()[PIPELINE_GIT_HEAD_REF]) + assertEquals(comment.pullRequest?.targetRef?.name, comment.outputs()[PIPELINE_GIT_BASE_REF]) + verify(mocks.mergeRequests).getMergeRequest("group/demo", 17) + } + + @Test + fun `enriches push changes and tag description`() { + val mocks = apiMocks() + val push = parseFixture("gitlab_push_event.json", "Push Hook") as GitPushHook + `when`(mocks.commits.compare("group/demo", push.before, push.after, true)).thenReturn( + GitlabCompareResults().apply { diffs = listOf(diff()) } + ) + + val enrichedPush = GitlabWebhookEnricher(mocks.factory).enrich(repository, push) as GitPushHook + + assertEquals("src/Main.kt", enrichedPush.changes.single().path) + + val tag = parseFixture("gitlab_tag_push_event.json", "Tag Push Hook") as GitTagHook + `when`(mocks.tags.getTag("group/demo", tag.ref.name)).thenReturn( + GitlabTag().apply { message = "release notes" } + ) + GitlabWebhookEnricher(mocks.factory).enrich(repository, tag) + + assertEquals("release notes", tag.outputs()[PIPELINE_GIT_TAG_DESC]) + } + + @Test + fun `enriches review with default branch variables`() { + val mocks = apiMocks() + val review = parseBody(reviewBody(), "Review Hook") as PullRequestReviewHook + + GitlabWebhookEnricher(mocks.factory).enrich(repository, review) + + assertEquals("main", review.outputs()[CI_BRANCH]) + assertEquals("reviewer", review.outputs()["BK_CI_REPO_GIT_WEBHOOK_REVIEW_APPROVED_REVIEWERS"]) + verifyNoInteractions(mocks.mergeRequests) + } + + @Test + fun `rejects incomplete comparisons and skips deleted refs`() { + val mocks = apiMocks() + val push = parseFixture("gitlab_push_event.json", "Push Hook") as GitPushHook + `when`(mocks.commits.compare("group/demo", push.before, push.after, true)).thenReturn( + GitlabCompareResults().apply { isCompareTimeout = true } + ) + assertThrows(RuntimeException::class.java) { + GitlabWebhookEnricher(mocks.factory).enrich(repository, push) + } + + val deletedTag = parseFixture("gitlab_tag_delete_event.json", "Tag Push Hook") as GitTagHook + GitlabWebhookEnricher(mocks.factory).enrich(repository, deletedTag) + verifyNoInteractions(mocks.tags) + } + + private fun apiMocks(): ApiMocks { + val factory = mock(GitlabApiFactory::class.java) + val api = mock(GitlabApi::class.java) + val projects = mock(GitlabProjectsApi::class.java) + val commits = mock(GitlabCommitsApi::class.java) + val mergeRequests = mock(GitlabMergeRequestsApi::class.java) + val tags = mock(GitlabTagsApi::class.java) + `when`(factory.fromAuthProvider(any())).thenReturn(api) + `when`(api.projectsApi).thenReturn(projects) + `when`(api.commitsApi).thenReturn(commits) + `when`(api.mergeRequestsApi).thenReturn(mergeRequests) + `when`(api.tagsApi).thenReturn(tags) + `when`(projects.getProject("group/demo")).thenReturn(project()) + `when`(commits.getCommit("group/demo", "main")).thenReturn(defaultCommit()) + return ApiMocks(factory, projects, commits, mergeRequests, tags) + } + + private fun project() = GitlabProject().apply { + id = 42 + name = "demo" + pathWithNamespace = "group/demo" + defaultBranch = "main" + archived = false + httpUrlToRepo = "https://gitlab.example.com/group/demo.git" + webUrl = "https://gitlab.example.com/group/demo" + } + + private fun defaultCommit() = GitlabCommit().apply { + id = "main-sha" + shortId = "main-sha" + message = "default message" + authorName = "Developer" + } + + private fun mergeRequest() = GitlabMergeRequest().apply { + id = 9001 + iid = 17 + title = "MR from API" + description = "body" + sourceBranch = "feature" + targetBranch = "main" + sourceProjectId = 42 + targetProjectId = 42 + author = GitlabUser().apply { username = "author" } + assignee = GitlabUser().apply { username = "assignee" } + reviewers = listOf(GitlabUser().apply { username = "reviewer" }) + labels = listOf("backend", "urgent") + milestone = GitlabMilestone().apply { id = 3; title = "v1.2"; dueDate = Date(0) } + createdAt = Date(0) + updatedAt = Date(1000) + sha = "head-sha" + diffRefs = GitlabDiffRefs().apply { + baseSha = "base-sha" + startSha = "target-sha" + headSha = "head-sha" + } + webUrl = "https://gitlab.example.com/group/demo/-/merge_requests/17" + } + + private fun diff() = GitlabDiff().apply { + oldPath = "src/Old.kt" + newPath = "src/Main.kt" + } + + private fun parseFixture(name: String, header: String) = parser.parse( + HookRequest(mapOf("X-Gitlab-Event" to header), requireNotNull(javaClass.classLoader.getResource(name)).readText()) + ) + + private fun parseBody(body: String, header: String) = parser.parse( + HookRequest(mapOf("X-Gitlab-Event" to header), body) + ) + + private fun reviewBody() = """ + { + "object_kind":"review","id":"301","iid":"17","event":"approved","state":"approved", + "author":{"id":7,"username":"author","name":"Author"}, + "reviewer":{"id":401,"state":"approved","reviewer":{"id":8,"username":"reviewer"}}, + "reviewers":[{"id":401,"state":"approved","reviewer":{"id":8,"username":"reviewer"}}], + "project_id":42,"reviewable_id":9001,"reviewable_type":"merge_request", + "repository":{"id":42,"name":"demo","path_with_namespace":"group/demo", + "homepage":"https://gitlab.example.com/group/demo", + "git_http_url":"https://gitlab.example.com/group/demo.git"} + } + """.trimIndent() + + private data class ApiMocks( + val factory: GitlabApiFactory, + val projects: GitlabProjectsApi, + val commits: GitlabCommitsApi, + val mergeRequests: GitlabMergeRequestsApi, + val tags: GitlabTagsApi + ) +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookParserTest.kt b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookParserTest.kt new file mode 100644 index 0000000..a5ed69e --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/kotlin/com/tencent/devops/scm/provider/git/gitlab/GitlabWebhookParserTest.kt @@ -0,0 +1,270 @@ +package com.tencent.devops.scm.provider.git.gitlab + +import com.tencent.devops.scm.api.enums.EventAction +import com.tencent.devops.scm.api.pojo.HookRequest +import com.tencent.devops.scm.api.pojo.webhook.git.GitPushHook +import com.tencent.devops.scm.api.pojo.webhook.git.GitTagHook +import com.tencent.devops.scm.api.pojo.webhook.git.IssueCommentHook +import com.tencent.devops.scm.api.pojo.webhook.git.IssueHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestCommentHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestHook +import com.tencent.devops.scm.api.pojo.webhook.git.PullRequestReviewHook +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.LocalDateTime + +class GitlabWebhookParserTest { + private val parser = GitlabWebhookParser() + + @Test + fun `parses push create and delete actions`() { + val pushed = parse("gitlab_push_event.json", "Push Hook", GitPushHook::class.java) + assertEquals(EventAction.PUSH_FILE, pushed.action) + assertEquals("PUSH", pushed.eventType) + assertEquals( + EventAction.NEW_BRANCH, + parse("gitlab_branch_create_event.json", "Push Hook", GitPushHook::class.java).action + ) + val deleted = parse("gitlab_branch_delete_event.json", "Push Hook", GitPushHook::class.java) + assertEquals(EventAction.DELETE, deleted.action) + assertNull(deleted.commit) + } + + @Test + fun `parses changes from commits when creating branch`() { + val hook = parseBody(branchCreateWithCommitsBody(), "Push Hook") as GitPushHook + + assertEquals(EventAction.NEW_BRANCH, hook.action) + assertEquals(3, hook.changes.size) + assertTrue(hook.changes.single { it.path == ".ci/templates/base.yml" }.added) + assertFalse(hook.changes.single { it.path == ".ci/pipeline.yml" }.added) + assertTrue(hook.changes.single { it.path == ".ci/old.yml" }.deleted) + assertEquals(".ci/old.yml", hook.changes.single { it.path == ".ci/old.yml" }.oldPath) + } + + @Test + fun `parses tag actions`() { + val created = parse("gitlab_tag_push_event.json", "Tag Push Hook", GitTagHook::class.java) + assertEquals(EventAction.CREATE, created.action) + assertEquals("TAG_PUSH", created.eventType) + assertEquals(EventAction.DELETE, parse("gitlab_tag_delete_event.json", "Tag Push Hook", GitTagHook::class.java).action) + } + + @Test + fun `parses merge request issue and note hooks`() { + val opened = parse("gitlab_mr_open_event.json", "Merge Request Hook", PullRequestHook::class.java) + assertEquals(EventAction.OPEN, opened.action) + assertEquals("MERGE_REQUEST", opened.eventType) + val outputs = opened.outputs() + assertEquals(42L, outputs["BK_CI_HOOK_SOURCE_PROJECT_ID"]) + assertEquals(42L, outputs["BK_CI_HOOK_TARGET_PROJECT_ID"]) + assertEquals("feature/gitlab", outputs["BK_CI_HOOK_SOURCE_BRANCH"]) + assertEquals("main", outputs["BK_CI_HOOK_TARGET_BRANCH"]) + assertEquals(9001L, outputs["GIT_CI_MR_ID"]) + assertEquals("dev", outputs["GIT_CI_MR_PROPOSER"]) + assertEquals(EventAction.PUSH_UPDATE, parse("gitlab_mr_update_event.json", "Merge Request Hook", PullRequestHook::class.java).action) + assertEquals(EventAction.MERGE, parse("gitlab_mr_merge_event.json", "Merge Request Hook", PullRequestHook::class.java).action) + val issue = assertInstanceOf(IssueHook::class.java, parseRaw("gitlab_issue_event.json", "Issue Hook")) + assertEquals("ISSUES", issue.eventType) + val mrNote = assertInstanceOf( + PullRequestCommentHook::class.java, + parseRaw("gitlab_mr_note_event.json", "Note Hook") + ) + assertEquals("NOTE", mrNote.eventType) + val issueNote = assertInstanceOf( + IssueCommentHook::class.java, + parseRaw("gitlab_issue_note_event.json", "Note Hook") + ) + assertEquals("NOTE", issueNote.eventType) + } + + @Test + fun `verifies token and rejects conflicting event header`() { + val request = HookRequest(mapOf("X-Gitlab-Token" to "secret"), fixture("gitlab_push_event.json")) + assertTrue(parser.verify(request, null)) + assertTrue(parser.verify(request, "secret")) + assertFalse(parser.verify(request, "other")) + + request.headers = mapOf("X-Gitlab-Event" to "Issue Hook") + assertNull(parser.parse(request)) + } + + @Test + fun `maps merge request action variants and push updates`() { + listOf("approved", "approval", "unapproved", "unapproval").forEach { action -> + val hook = parseBody(mergeRequestBody(action), "Merge Request Hook") as PullRequestHook + assertEquals(EventAction.EDIT, hook.action) + } + val pushUpdate = parseBody(mergeRequestBody("update", "\"oldrev\":\"abc\","), "Merge Request Hook") + as PullRequestHook + assertEquals(EventAction.PUSH_UPDATE, pushUpdate.action) + } + + @Test + fun `parses review hook with CI event contract`() { + val hook = parseBody(reviewBody(), "Review Hook") as PullRequestReviewHook + + assertEquals("REVIEW", hook.eventType) + assertEquals("approved", hook.review.state?.value) + assertEquals("reviewer", hook.sender.username) + assertEquals("reviewer", hook.outputs()["BK_CI_REPO_GIT_WEBHOOK_REVIEW_APPROVED_REVIEWERS"]) + assertEquals("all", hook.outputs()["BK_CI_REPO_GIT_WEBHOOK_REVIEW_RESTRICT_TYPE"]) + } + + @Test + fun `maps note update and defaults old payload to create`() { + val updated = parseBody(noteBody("\"action\":\"update\","), "Note Hook") as IssueCommentHook + assertEquals(EventAction.UPDATE, updated.action) + val legacy = parseBody(noteBody(""), "Note Hook") as IssueCommentHook + assertEquals(EventAction.CREATE, legacy.action) + } + + @Test + fun `uses payload authors before sender`() { + val mr = parseBody( + mergeRequestBody("open", "\"author\":{\"id\":99,\"username\":\"author\",\"name\":\"Author\"},"), + "Merge Request Hook" + ) as PullRequestHook + assertEquals(99, mr.pullRequest.author?.id) + assertEquals("author", mr.pullRequest.author?.username) + + val issue = parseBody(issueBody("\"author_id\":88,"), "Issue Hook") as IssueHook + assertEquals(88, issue.issue.author.id) + assertEquals("sender", issue.issue.author.username) + } + + @Test + fun `supports GitLab UTC time and returns null for invalid optional time`() { + val hook = parseBody( + mergeRequestBody( + "open", + "\"created_at\":\"2026-07-13 08:10:00 UTC\",\"updated_at\":\"invalid\"," + ), + "Merge Request Hook" + ) as PullRequestHook + assertEquals(LocalDateTime.of(2026, 7, 13, 8, 10), hook.pullRequest.created) + assertNull(hook.pullRequest.updated) + } + + @Test + fun `builds issue link when official note payload omits it`() { + val hook = parseBody(noteBody(""), "Note Hook") as IssueCommentHook + + assertEquals("https://gitlab.example.com/group/demo/-/issues/23", hook.issue.link) + } + + @Test + fun `maps nested merge request repositories`() { + val repositories = """ + "source":{"id":41,"name":"fork","path_with_namespace":"contributor/fork", + "git_http_url":"https://gitlab.example.com/contributor/fork.git", + "git_ssh_url":"git@gitlab.example.com:contributor/fork.git", + "web_url":"https://gitlab.example.com/contributor/fork"}, + "target":{"id":42,"name":"demo","path_with_namespace":"group/demo", + "git_http_url":"https://gitlab.example.com/group/demo.git", + "web_url":"https://gitlab.example.com/group/demo"}, + """.trimIndent() + val hook = parseBody( + mergeRequestBody("open", repositories).replace("\"source_project_id\":42", "\"source_project_id\":41"), + "Merge Request Hook" + ) as PullRequestHook + + assertEquals("contributor/fork", hook.pullRequest.sourceRepo.fullName) + assertEquals("https://gitlab.example.com/contributor/fork.git", hook.pullRequest.sourceRepo.httpUrl) + assertEquals("group/demo", hook.pullRequest.targetRepo.fullName) + } + + private fun parseRaw(name: String, header: String) = parser.parse( + HookRequest(mapOf("X-Gitlab-Event" to header), fixture(name)) + ) + + private fun parseBody(body: String, header: String) = parser.parse( + HookRequest(mapOf("X-Gitlab-Event" to header), body) + ) + + private fun parse(name: String, header: String, type: Class): T = type.cast(parseRaw(name, header)) + + private fun fixture(name: String): String = requireNotNull(javaClass.classLoader.getResource(name)).readText() + + private fun mergeRequestBody(action: String, attributes: String = "") = """ + { + "object_kind":"merge_request", + "user":{"id":7,"username":"sender","name":"Sender"}, + "project":{"id":42,"name":"demo","path_with_namespace":"group/demo","web_url":"https://gitlab.example.com/group/demo"}, + "object_attributes":{ + $attributes + "id":9001,"iid":17,"action":"$action","state":"opened","title":"MR","description":"body", + "source_branch":"feature","target_branch":"main","source_project_id":42,"target_project_id":42, + "last_commit":{"id":"abc","message":"message"} + } + } + """.trimIndent() + + private fun issueBody(attributes: String) = """ + { + "object_kind":"issue", + "user":{"id":7,"username":"sender","name":"Sender"}, + "project":{"id":42,"name":"demo","path_with_namespace":"group/demo","web_url":"https://gitlab.example.com/group/demo"}, + "object_attributes":{ + $attributes + "id":1001,"iid":23,"action":"open","state":"opened","title":"Issue", + "created_at":"2026-07-13T08:10:00Z" + } + } + """.trimIndent() + + private fun noteBody(attributes: String) = """ + { + "object_kind":"note", + "user":{"id":7,"username":"sender","name":"Sender"}, + "project":{"id":42,"name":"demo","path_with_namespace":"group/demo","web_url":"https://gitlab.example.com/group/demo"}, + "object_attributes":{ + $attributes + "id":501,"note":"body","noteable_type":"Issue","created_at":"2026-07-13T08:10:00Z" + }, + "issue":{"id":1001,"iid":23,"state":"opened","title":"Issue","created_at":"2026-07-13T08:10:00Z"} + } + """.trimIndent() + + private fun reviewBody() = """ + { + "object_kind":"review", + "id":"301","iid":"17","event":"approved","state":"approved","restrict_type":"all", + "author":{"id":7,"username":"author","name":"Author"}, + "reviewer":{"id":401,"state":"approved","reviewer":{"id":8,"username":"reviewer","name":"Reviewer"}}, + "reviewers":[{"id":401,"state":"approved","reviewer":{"id":8,"username":"reviewer","name":"Reviewer"}}], + "project_id":42,"reviewable_id":9001,"reviewable_type":"merge_request", + "repository":{"id":42,"name":"demo","path_with_namespace":"group/demo", + "homepage":"https://gitlab.example.com/group/demo", + "git_http_url":"https://gitlab.example.com/group/demo.git"} + } + """.trimIndent() + + private fun branchCreateWithCommitsBody() = """ + { + "object_kind":"push", + "before":"0000000000000000000000000000000000000000", + "after":"3333333333333333333333333333333333333333", + "ref":"refs/heads/feature/new", + "checkout_sha":"3333333333333333333333333333333333333333", + "user_id":7,"user_name":"Dev User","user_username":"dev", + "project":{"id":42,"name":"demo","path_with_namespace":"group/demo", + "web_url":"https://gitlab.example.com/group/demo"}, + "commits":[ + { + "id":"2222222222222222222222222222222222222222","message":"first", + "added":[".ci/templates/base.yml"],"modified":[".ci/pipeline.yml"],"removed":[] + }, + { + "id":"3333333333333333333333333333333333333333","message":"second", + "added":[],"modified":[".ci/templates/base.yml"],"removed":[".ci/old.yml"] + } + ], + "total_commits_count":2 + } + """.trimIndent() +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_branch_create_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_branch_create_event.json new file mode 100644 index 0000000..855ed79 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_branch_create_event.json @@ -0,0 +1,10 @@ +{ + "object_kind": "push", + "before": "0000000000000000000000000000000000000000", + "after": "3333333333333333333333333333333333333333", + "ref": "refs/heads/feature/new", + "checkout_sha": "3333333333333333333333333333333333333333", + "user_id": 7, "user_name": "Dev User", "user_username": "dev", + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "commits": [], "total_commits_count": 0 +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_branch_delete_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_branch_delete_event.json new file mode 100644 index 0000000..d7b3575 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_branch_delete_event.json @@ -0,0 +1,10 @@ +{ + "object_kind": "push", + "before": "3333333333333333333333333333333333333333", + "after": "0000000000000000000000000000000000000000", + "ref": "refs/heads/feature/old", + "checkout_sha": null, + "user_id": 7, "user_name": "Dev User", "user_username": "dev", + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "commits": [], "total_commits_count": 0 +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_issue_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_issue_event.json new file mode 100644 index 0000000..cc61359 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_issue_event.json @@ -0,0 +1,6 @@ +{ + "object_kind": "issue", + "user": {"id": 7, "name": "Dev User", "username": "dev"}, + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "object_attributes": {"id": 1001, "iid": 23, "action": "open", "state": "opened", "title": "Webhook issue", "description": "Issue body", "created_at": "2026-07-13T08:10:00Z", "updated_at": "2026-07-13T08:10:00Z", "url": "https://gitlab.example.com/group/demo/-/issues/23", "labels": [{"title": "bug"}]} +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_issue_note_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_issue_note_event.json new file mode 100644 index 0000000..3f1f445 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_issue_note_event.json @@ -0,0 +1,7 @@ +{ + "object_kind": "note", + "user": {"id": 8, "name": "Maintainer", "username": "maintainer"}, + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "object_attributes": {"id": 502, "note": "Fixed in main", "noteable_type": "Issue", "created_at": "2026-07-13T12:10:00Z", "updated_at": "2026-07-13T12:10:00Z", "url": "https://gitlab.example.com/group/demo/-/issues/23#note_502"}, + "issue": {"id": 1001, "iid": 23, "state": "opened", "title": "Webhook issue", "description": "Issue body", "created_at": "2026-07-13T08:10:00Z", "updated_at": "2026-07-13T12:10:00Z", "web_url": "https://gitlab.example.com/group/demo/-/issues/23"} +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_merge_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_merge_event.json new file mode 100644 index 0000000..f6a8185 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_merge_event.json @@ -0,0 +1,6 @@ +{ + "object_kind": "merge_request", + "user": {"id": 8, "name": "Maintainer", "username": "maintainer"}, + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "object_attributes": {"id": 9001, "iid": 17, "action": "merge", "state": "merged", "title": "Add GitLab provider", "description": "Merged", "source_branch": "feature/gitlab", "target_branch": "main", "source_project_id": 42, "target_project_id": 42, "merge_commit_sha": "7777777777777777777777777777777777777777", "updated_at": "2026-07-13T10:10:00Z", "url": "https://gitlab.example.com/group/demo/-/merge_requests/17", "last_commit": {"id": "6666666666666666666666666666666666666666", "message": "Update tests", "author": {"name": "Dev User"}}} +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_note_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_note_event.json new file mode 100644 index 0000000..efbaa5f --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_note_event.json @@ -0,0 +1,7 @@ +{ + "object_kind": "note", + "user": {"id": 8, "name": "Reviewer", "username": "reviewer"}, + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "object_attributes": {"id": 501, "note": "Looks good", "noteable_type": "MergeRequest", "created_at": "2026-07-13T11:10:00Z", "updated_at": "2026-07-13T11:10:00Z", "url": "https://gitlab.example.com/group/demo/-/merge_requests/17#note_501"}, + "merge_request": {"id": 9001, "iid": 17, "state": "opened", "title": "Add GitLab provider", "description": "Provider implementation", "source_branch": "feature/gitlab", "target_branch": "main", "source_project_id": 42, "target_project_id": 42, "sha": "6666666666666666666666666666666666666666", "web_url": "https://gitlab.example.com/group/demo/-/merge_requests/17"} +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_open_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_open_event.json new file mode 100644 index 0000000..03c3465 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_open_event.json @@ -0,0 +1,13 @@ +{ + "object_kind": "merge_request", + "user": {"id": 7, "name": "Dev User", "username": "dev", "email": "dev@example.com"}, + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "object_attributes": { + "id": 9001, "iid": 17, "action": "open", "state": "opened", "title": "Add GitLab support", "description": "Provider implementation", + "source_branch": "feature/gitlab", "target_branch": "main", "source_project_id": 42, "target_project_id": 42, + "created_at": "2026-07-13T08:10:00Z", "updated_at": "2026-07-13T08:10:00Z", + "url": "https://gitlab.example.com/group/demo/-/merge_requests/17", + "last_commit": {"id": "5555555555555555555555555555555555555555", "message": "Add provider", "timestamp": "2026-07-13T08:09:00Z", "author": {"name": "Dev User", "email": "dev@example.com"}}, + "labels": [{"title": "feature"}] + } +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_update_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_update_event.json new file mode 100644 index 0000000..f6786ba --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_mr_update_event.json @@ -0,0 +1,6 @@ +{ + "object_kind": "merge_request", + "user": {"id": 7, "name": "Dev User", "username": "dev"}, + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "object_attributes": {"id": 9001, "iid": 17, "action": "update", "state": "opened", "title": "Add GitLab provider", "description": "Updated", "source_branch": "feature/gitlab", "target_branch": "main", "source_project_id": 42, "target_project_id": 42, "updated_at": "2026-07-13T09:10:00Z", "url": "https://gitlab.example.com/group/demo/-/merge_requests/17", "last_commit": {"id": "6666666666666666666666666666666666666666", "message": "Update tests", "author": {"name": "Dev User"}}} +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_push_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_push_event.json new file mode 100644 index 0000000..c003f25 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_push_event.json @@ -0,0 +1,29 @@ +{ + "object_kind": "push", + "before": "1111111111111111111111111111111111111111", + "after": "2222222222222222222222222222222222222222", + "ref": "refs/heads/main", + "checkout_sha": "2222222222222222222222222222222222222222", + "user_id": 7, + "user_name": "Dev User", + "user_username": "dev", + "user_email": "dev@example.com", + "project": { + "id": 42, + "name": "demo", + "path_with_namespace": "group/demo", + "default_branch": "main", + "web_url": "https://gitlab.example.com/group/demo", + "git_http_url": "https://gitlab.example.com/group/demo.git", + "git_ssh_url": "git@gitlab.example.com:group/demo.git" + }, + "commits": [{ + "id": "2222222222222222222222222222222222222222", + "message": "Update pipeline", + "timestamp": "2026-07-13T08:10:00Z", + "url": "https://gitlab.example.com/group/demo/-/commit/22222222", + "author": {"name": "Dev User", "email": "dev@example.com"}, + "added": ["new.txt"], "modified": ["README.md"], "removed": [] + }], + "total_commits_count": 1 +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_tag_delete_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_tag_delete_event.json new file mode 100644 index 0000000..327158e --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_tag_delete_event.json @@ -0,0 +1,10 @@ +{ + "object_kind": "tag_push", + "before": "4444444444444444444444444444444444444444", + "after": "0000000000000000000000000000000000000000", + "ref": "refs/tags/v0.9.0", + "checkout_sha": null, + "user_id": 7, "user_name": "Dev User", "user_username": "dev", + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "commits": [] +} diff --git a/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_tag_push_event.json b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_tag_push_event.json new file mode 100644 index 0000000..c9aa157 --- /dev/null +++ b/devops-scm-provider/devops-scm-provider-git/devops-scm-provider-gitlab/src/test/resources/gitlab_tag_push_event.json @@ -0,0 +1,10 @@ +{ + "object_kind": "tag_push", + "before": "0000000000000000000000000000000000000000", + "after": "4444444444444444444444444444444444444444", + "ref": "refs/tags/v1.0.0", + "checkout_sha": "4444444444444444444444444444444444444444", + "user_id": 7, "user_name": "Dev User", "user_username": "dev", + "project": {"id": 42, "name": "demo", "path_with_namespace": "group/demo", "web_url": "https://gitlab.example.com/group/demo", "git_http_url": "https://gitlab.example.com/group/demo.git", "git_ssh_url": "git@gitlab.example.com:group/demo.git"}, + "commits": [{"id": "4444444444444444444444444444444444444444", "message": "Release 1.0.0", "timestamp": "2026-07-13T08:10:00Z", "author": {"name": "Dev User", "email": "dev@example.com"}}] +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/build.gradle.kts b/devops-scm-sdk/devops-scm-sdk-gitlab/build.gradle.kts new file mode 100644 index 0000000..b71c5c3 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + api(project(":devops-scm-sdk:devops-scm-sdk-common")) + + testImplementation("org.junit.jupiter:junit-jupiter-api") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testImplementation("ch.qos.logback:logback-core") + testImplementation("ch.qos.logback:logback-classic") + testImplementation("org.mockito:mockito-core") +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/AbstractGitlabApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/AbstractGitlabApi.java new file mode 100644 index 0000000..a3aee29 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/AbstractGitlabApi.java @@ -0,0 +1,67 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.Requester; +import com.tencent.devops.scm.sdk.common.util.UrlEncoder; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProject; +import java.util.Collection; +import java.util.stream.Collectors; + +public abstract class AbstractGitlabApi implements GitlabConstants { + protected final GitlabApi gitlabApi; + + protected AbstractGitlabApi(GitlabApi gitlabApi) { + this.gitlabApi = gitlabApi; + } + + public String getProjectIdOrPath(Object value) { + if (value instanceof Number && ((Number) value).longValue() > 0) { + return value.toString(); + } + if (value instanceof String && !((String) value).trim().isEmpty()) { + return segment((String) value); + } + if (value instanceof GitlabProject) { + GitlabProject project = (GitlabProject) value; + if (project.getId() != null && project.getId() > 0) { + return project.getId().toString(); + } + if (project.getPathWithNamespace() != null && !project.getPathWithNamespace().isEmpty()) { + return segment(project.getPathWithNamespace()); + } + } + throw new GitlabApiException("Cannot determine a positive project ID or path"); + } + + protected Requester request(Object projectIdOrPath, String path) { + String project = getProjectIdOrPath(projectIdOrPath); + return gitlabApi.createRequest().withUrlPath(path.replace("%s", project)).withRepoId(project); + } + + protected String segment(String value) { + return UrlEncoder.urlEncode(value); + } + + protected int page(Integer value) { + if (value == null) { + return DEFAULT_PAGE; + } + if (value < 1) { + throw new IllegalArgumentException("page must be positive"); + } + return value; + } + + protected int perPage(Integer value) { + if (value == null) { + return DEFAULT_PER_PAGE; + } + if (value < 1 || value > DEFAULT_PER_PAGE) { + throw new IllegalArgumentException("perPage must be between 1 and 100"); + } + return value; + } + + protected String join(Collection values) { + return values == null ? null : values.stream().map(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApi.java new file mode 100644 index 0000000..2e22cc8 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApi.java @@ -0,0 +1,157 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.Requester; +import com.tencent.devops.scm.sdk.common.connector.ScmConnector; +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabAuthProvider; +import lombok.Getter; + +public class GitlabApi { + @Getter + private final GitlabApiClient client; + private volatile GitlabProjectsApi projectsApi; + private volatile GitlabProjectHooksApi projectHooksApi; + private volatile GitlabBranchesApi branchesApi; + private volatile GitlabTagsApi tagsApi; + private volatile GitlabCommitsApi commitsApi; + private volatile GitlabMergeRequestsApi mergeRequestsApi; + private volatile GitlabIssuesApi issuesApi; + private volatile GitlabNotesApi notesApi; + private volatile GitlabRepositoryFilesApi repositoryFilesApi; + private volatile GitlabUsersApi usersApi; + private volatile GitlabCommitStatusesApi commitStatusesApi; + + public GitlabApi(String apiUrl, ScmConnector connector, GitlabAuthProvider authProvider) { + this(new GitlabApiClient(apiUrl, connector, authProvider)); + } + + public GitlabApi(GitlabApiClient client) { + this.client = client; + } + + Requester createRequest() { + Requester requester = new Requester(client); + requester.setIteratorFactory(GitlabPagedIterator::create); + return requester; + } + + public GitlabProjectsApi getProjectsApi() { + if (projectsApi == null) { + synchronized (this) { + if (projectsApi == null) { + projectsApi = new GitlabProjectsApi(this); + } + } + } + return projectsApi; + } + + public GitlabProjectHooksApi getProjectHooksApi() { + if (projectHooksApi == null) { + synchronized (this) { + if (projectHooksApi == null) { + projectHooksApi = new GitlabProjectHooksApi(this); + } + } + } + return projectHooksApi; + } + + public GitlabBranchesApi getBranchesApi() { + if (branchesApi == null) { + synchronized (this) { + if (branchesApi == null) { + branchesApi = new GitlabBranchesApi(this); + } + } + } + return branchesApi; + } + + public GitlabTagsApi getTagsApi() { + if (tagsApi == null) { + synchronized (this) { + if (tagsApi == null) { + tagsApi = new GitlabTagsApi(this); + } + } + } + return tagsApi; + } + + public GitlabCommitsApi getCommitsApi() { + if (commitsApi == null) { + synchronized (this) { + if (commitsApi == null) { + commitsApi = new GitlabCommitsApi(this); + } + } + } + return commitsApi; + } + + public GitlabMergeRequestsApi getMergeRequestsApi() { + if (mergeRequestsApi == null) { + synchronized (this) { + if (mergeRequestsApi == null) { + mergeRequestsApi = new GitlabMergeRequestsApi(this); + } + } + } + return mergeRequestsApi; + } + + public GitlabIssuesApi getIssuesApi() { + if (issuesApi == null) { + synchronized (this) { + if (issuesApi == null) { + issuesApi = new GitlabIssuesApi(this); + } + } + } + return issuesApi; + } + + public GitlabNotesApi getNotesApi() { + if (notesApi == null) { + synchronized (this) { + if (notesApi == null) { + notesApi = new GitlabNotesApi(this); + } + } + } + return notesApi; + } + + public GitlabRepositoryFilesApi getRepositoryFilesApi() { + if (repositoryFilesApi == null) { + synchronized (this) { + if (repositoryFilesApi == null) { + repositoryFilesApi = new GitlabRepositoryFilesApi(this); + } + } + } + return repositoryFilesApi; + } + + public GitlabUsersApi getUsersApi() { + if (usersApi == null) { + synchronized (this) { + if (usersApi == null) { + usersApi = new GitlabUsersApi(this); + } + } + } + return usersApi; + } + + public GitlabCommitStatusesApi getCommitStatusesApi() { + if (commitStatusesApi == null) { + synchronized (this) { + if (commitStatusesApi == null) { + commitStatusesApi = new GitlabCommitStatusesApi(this); + } + } + } + return commitStatusesApi; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiClient.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiClient.java new file mode 100644 index 0000000..3562137 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiClient.java @@ -0,0 +1,67 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.ScmApiClient; +import com.tencent.devops.scm.sdk.common.ScmRequest; +import com.tencent.devops.scm.sdk.common.ScmResponse; +import com.tencent.devops.scm.sdk.common.connector.ScmConnector; +import com.tencent.devops.scm.sdk.common.connector.ScmConnectorRequest; +import com.tencent.devops.scm.sdk.common.connector.ScmConnectorResponse; +import com.tencent.devops.scm.sdk.common.exception.ScmHttpRetryException; +import com.tencent.devops.scm.sdk.common.util.ScmSdkJsonFactory; +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabAuthProvider; +import com.tencent.devops.scm.sdk.gitlab.util.GitlabJsonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class GitlabApiClient extends ScmApiClient { + private static final Logger logger = LoggerFactory.getLogger(GitlabApiClient.class); + private final GitlabAuthProvider authProvider; + + public GitlabApiClient(String apiUrl, ScmConnector connector, GitlabAuthProvider authProvider) { + super(apiUrl, connector); + if (authProvider == null) { + throw new IllegalArgumentException("authProvider cannot be null"); + } + this.authProvider = authProvider; + } + + @Override + public void beforeRequest(ScmRequest originRequest, ScmRequest.Builder builder) { + authProvider.authorization(builder); + } + + @Override + public ScmResponse sendRequest(ScmRequest request, BodyHandler handler) { + return super.sendRequest(request, response -> response.statusCode() == 204 || handler == null + ? null : handler.apply(response)); + } + + @Override + public void afterRequest(ScmConnectorResponse response, ScmRequest request) { + logger.info("GitLab API response|requestId:{}", response.header(GitlabConstants.REQUEST_ID_HEADER)); + int status = response.statusCode(); + if (status == 429 || status == 500 || status == 502 || status == 503 || status == 504) { + throw new ScmHttpRetryException(); + } + if (status >= 400) { + throw new GitlabApiException(response); + } + } + + @Override + public RuntimeException handleException(Exception error, ScmConnectorRequest request, + ScmConnectorResponse response) { + if (error instanceof ScmHttpRetryException) { + return (ScmHttpRetryException) error; + } + if (error instanceof GitlabApiException) { + return (GitlabApiException) error; + } + return new GitlabApiException(error, response); + } + + @Override + public ScmSdkJsonFactory getJsonFactory() { + return GitlabJsonUtil.getJsonFactory(); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiException.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiException.java new file mode 100644 index 0000000..77dfed0 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiException.java @@ -0,0 +1,71 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.fasterxml.jackson.databind.JsonNode; +import com.tencent.devops.scm.sdk.common.ScmResponse; +import com.tencent.devops.scm.sdk.common.connector.ScmConnectorResponse; +import com.tencent.devops.scm.sdk.common.exception.BaseScmApiException; +import com.tencent.devops.scm.sdk.gitlab.util.GitlabJsonUtil; +import lombok.Getter; + +public class GitlabApiException extends BaseScmApiException { + @Getter + private final String requestId; + private final String gitlabMessage; + + public GitlabApiException(String message) { + super(message); + this.requestId = null; + this.gitlabMessage = null; + } + + public GitlabApiException(int statusCode, String message, String requestId) { + super(statusCode, message); + this.requestId = requestId; + this.gitlabMessage = null; + } + + public GitlabApiException(Exception cause) { + super(cause); + this.requestId = null; + this.gitlabMessage = null; + } + + public GitlabApiException(Exception cause, ScmConnectorResponse response) { + super(cause, response); + this.requestId = response == null ? null : response.header(GitlabConstants.REQUEST_ID_HEADER); + this.gitlabMessage = extractErrorMessage(response); + } + + public GitlabApiException(ScmConnectorResponse response) { + super(response); + this.requestId = response.header(GitlabConstants.REQUEST_ID_HEADER); + this.gitlabMessage = extractErrorMessage(response); + } + + @Override + public String getMessage() { + return gitlabMessage == null ? super.getMessage() : gitlabMessage; + } + + private static String extractErrorMessage(ScmConnectorResponse response) { + if (response == null) { + return null; + } + try { + JsonNode body = GitlabJsonUtil.getJsonFactory() + .toJsonNode(ScmResponse.getBodyAsString(response)); + String description = nodeMessage(body.get("error_description")); + return description == null ? nodeMessage(body.get("error")) : description; + } catch (Exception ignored) { + return null; + } + } + + private static String nodeMessage(JsonNode node) { + if (node == null || node.isNull()) { + return null; + } + String value = node.isTextual() ? node.asText() : node.toString(); + return value.trim().isEmpty() ? null : value; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiFactory.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiFactory.java new file mode 100644 index 0000000..c507efd --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabApiFactory.java @@ -0,0 +1,20 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.connector.ScmConnector; +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabAuthProvider; +import lombok.Getter; + +@Getter +public class GitlabApiFactory { + private final String apiUrl; + private final ScmConnector connector; + + public GitlabApiFactory(String apiUrl, ScmConnector connector) { + this.apiUrl = apiUrl; + this.connector = connector; + } + + public GitlabApi fromAuthProvider(GitlabAuthProvider authProvider) { + return new GitlabApi(apiUrl, connector, authProvider); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabBranchesApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabBranchesApi.java new file mode 100644 index 0000000..c5de710 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabBranchesApi.java @@ -0,0 +1,33 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabBranch; +import java.util.Arrays; +import java.util.List; + +public class GitlabBranchesApi extends AbstractGitlabApi { + GitlabBranchesApi(GitlabApi api) { + super(api); + } + + public List getBranches(Object project, String search, Integer page, Integer perPage) { + GitlabBranch[] result = request(project, "projects/%s/repository/branches").method(ScmHttpMethod.GET) + .with("search", search).with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)) + .fetch(GitlabBranch[].class); + return Arrays.asList(result); + } + + public GitlabBranch getBranch(Object project, String branch) { + return request(project, "projects/%s/repository/branches/" + segment(branch)).method(ScmHttpMethod.GET) + .fetch(GitlabBranch.class); + } + + public GitlabBranch createBranch(Object project, String branch, String ref) { + return request(project, "projects/%s/repository/branches").method(ScmHttpMethod.POST) + .with("branch", branch).with("ref", ref).fetch(GitlabBranch.class); + } + + public void deleteBranch(Object project, String branch) { + request(project, "projects/%s/repository/branches/" + segment(branch)).method(ScmHttpMethod.DELETE).send(); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabCommitStatusesApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabCommitStatusesApi.java new file mode 100644 index 0000000..c50ca65 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabCommitStatusesApi.java @@ -0,0 +1,44 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.common.exception.ScmHttpRetryException; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommitStatus; +import java.util.Arrays; +import java.util.List; + +public class GitlabCommitStatusesApi extends AbstractGitlabApi { + GitlabCommitStatusesApi(GitlabApi api) { + super(api); + } + + public GitlabCommitStatus create(Object project, String sha, String state, String name, String ref, + String targetUrl, String description) { + requireSha(sha); + try { + return request(project, "projects/%s/statuses/" + segment(sha)).method(ScmHttpMethod.POST) + .with("state", state).with("name", name).with("ref", ref).with("target_url", targetUrl) + .with("description", description).fetch(GitlabCommitStatus.class); + } catch (GitlabApiException error) { + if (error.getStatusCode() == 409) { + throw new ScmHttpRetryException(); + } + throw error; + } + } + + public List getStatuses(Object project, String sha, String ref, String name, + Integer page, Integer perPage) { + requireSha(sha); + GitlabCommitStatus[] result = request(project, + "projects/%s/repository/commits/" + segment(sha) + "/statuses").method(ScmHttpMethod.GET) + .with("ref", ref).with("name", name).with(PAGE_PARAM, page(page)) + .with(PER_PAGE_PARAM, perPage(perPage)).fetch(GitlabCommitStatus[].class); + return Arrays.asList(result); + } + + private void requireSha(String sha) { + if (sha == null || sha.trim().isEmpty()) { + throw new IllegalArgumentException("sha cannot be blank"); + } + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabCommitsApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabCommitsApi.java new file mode 100644 index 0000000..af41726 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabCommitsApi.java @@ -0,0 +1,45 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommit; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCompareResults; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabDiff; +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +public class GitlabCommitsApi extends AbstractGitlabApi { + GitlabCommitsApi(GitlabApi api) { + super(api); + } + + public List getCommits(Object project, String ref, String path, Date since, Date until, + Integer page, Integer perPage) { + GitlabCommit[] result = request(project, "projects/%s/repository/commits").method(ScmHttpMethod.GET) + .with("ref_name", ref).with("path", path).with("since", iso(since)).with("until", iso(until)) + .with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)).fetch(GitlabCommit[].class); + return Arrays.asList(result); + } + + public GitlabCommit getCommit(Object project, String sha) { + return request(project, "projects/%s/repository/commits/" + segment(sha)).method(ScmHttpMethod.GET) + .fetch(GitlabCommit.class); + } + + public List getDiff(Object project, String sha, Integer page, Integer perPage) { + GitlabDiff[] result = request(project, "projects/%s/repository/commits/" + segment(sha) + "/diff") + .method(ScmHttpMethod.GET).with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)) + .fetch(GitlabDiff[].class); + return Arrays.asList(result); + } + + public GitlabCompareResults compare(Object project, String from, String to, Boolean straight) { + return request(project, "projects/%s/repository/compare").method(ScmHttpMethod.GET) + .with("from", from).with("to", to).with("straight", straight).fetch(GitlabCompareResults.class); + } + + private String iso(Date value) { + return value == null ? null : Instant.ofEpochMilli(value.getTime()).toString(); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabConstants.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabConstants.java new file mode 100644 index 0000000..540cc9a --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabConstants.java @@ -0,0 +1,18 @@ +package com.tencent.devops.scm.sdk.gitlab; + +public interface GitlabConstants { + String PAGE_PARAM = "page"; + String PER_PAGE_PARAM = "per_page"; + String PRIVATE_TOKEN_HEADER = "PRIVATE-TOKEN"; + String AUTHORIZATION_HEADER = "Authorization"; + String BEARER_PREFIX = "Bearer "; + String REQUEST_ID_HEADER = "X-Request-Id"; + String TOTAL_HEADER = "X-Total"; + String TOTAL_PAGES_HEADER = "X-Total-Pages"; + String PER_PAGE_HEADER = "X-Per-Page"; + String PAGE_HEADER = "X-Page"; + String NEXT_PAGE_HEADER = "X-Next-Page"; + String PREV_PAGE_HEADER = "X-Prev-Page"; + int DEFAULT_PAGE = 1; + int DEFAULT_PER_PAGE = 100; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabIssuesApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabIssuesApi.java new file mode 100644 index 0000000..b7d495c --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabIssuesApi.java @@ -0,0 +1,49 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.Requester; +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabIssue; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabIssueParams; +import java.util.Arrays; +import java.util.List; + +public class GitlabIssuesApi extends AbstractGitlabApi { + GitlabIssuesApi(GitlabApi api) { + super(api); + } + + public List getIssues(Object project, String state, String search, Integer page, Integer perPage) { + GitlabIssue[] result = request(project, "projects/%s/issues").method(ScmHttpMethod.GET) + .with("state", state).with("search", search).with(PAGE_PARAM, page(page)) + .with(PER_PAGE_PARAM, perPage(perPage)).fetch(GitlabIssue[].class); + return Arrays.asList(result); + } + + public GitlabIssue getIssue(Object project, long iid) { + return request(project, "projects/%s/issues/" + iid).method(ScmHttpMethod.GET).fetch(GitlabIssue.class); + } + + public GitlabIssue createIssue(Object project, GitlabIssueParams params) { + return fill(request(project, "projects/%s/issues").method(ScmHttpMethod.POST), params, true) + .fetch(GitlabIssue.class); + } + + public GitlabIssue updateIssue(Object project, long iid, GitlabIssueParams params) { + return fill(request(project, "projects/%s/issues/" + iid).method(ScmHttpMethod.PUT), params, false) + .fetch(GitlabIssue.class); + } + + public GitlabIssue closeIssue(Object project, long iid) { + return updateIssue(project, iid, GitlabIssueParams.builder().stateEvent("close").build()); + } + + private Requester fill(Requester request, GitlabIssueParams params, boolean create) { + request.with("title", params.getTitle()).with("description", params.getDescription()) + .with("assignee_id", params.getAssigneeId()).with("assignee_ids", params.getAssigneeIds()) + .with("labels", join(params.getLabels())).with("milestone_id", params.getMilestoneId()); + if (!create) { + request.with("state_event", params.getStateEvent()); + } + return request; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabMergeRequestsApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabMergeRequestsApi.java new file mode 100644 index 0000000..aa0d96d --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabMergeRequestsApi.java @@ -0,0 +1,79 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.Requester; +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabCommit; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequest; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequestParams; +import java.util.Arrays; +import java.util.List; + +public class GitlabMergeRequestsApi extends AbstractGitlabApi { + GitlabMergeRequestsApi(GitlabApi api) { + super(api); + } + + public List getMergeRequests(Object project, String state, String sourceBranch, + String targetBranch, Integer page, Integer perPage) { + GitlabMergeRequest[] result = request(project, "projects/%s/merge_requests").method(ScmHttpMethod.GET) + .with("state", state).with("source_branch", sourceBranch).with("target_branch", targetBranch) + .with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)) + .fetch(GitlabMergeRequest[].class); + return Arrays.asList(result); + } + + public GitlabMergeRequest getMergeRequest(Object project, long iid) { + return request(project, "projects/%s/merge_requests/" + iid).method(ScmHttpMethod.GET) + .fetch(GitlabMergeRequest.class); + } + + public GitlabMergeRequest createMergeRequest(Object project, GitlabMergeRequestParams params) { + return fill(request(project, "projects/%s/merge_requests").method(ScmHttpMethod.POST), params, true) + .fetch(GitlabMergeRequest.class); + } + + public GitlabMergeRequest updateMergeRequest(Object project, long iid, GitlabMergeRequestParams params) { + return fill(request(project, "projects/%s/merge_requests/" + iid).method(ScmHttpMethod.PUT), params, false) + .fetch(GitlabMergeRequest.class); + } + + public GitlabMergeRequest closeMergeRequest(Object project, long iid) { + return updateMergeRequest(project, iid, GitlabMergeRequestParams.builder().stateEvent("close").build()); + } + + public GitlabMergeRequest mergeMergeRequest(Object project, long iid, String mergeCommitMessage, + Boolean shouldRemoveSourceBranch, Boolean squash) { + return request(project, "projects/%s/merge_requests/" + iid + "/merge").method(ScmHttpMethod.PUT) + .with("merge_commit_message", mergeCommitMessage) + .with("should_remove_source_branch", shouldRemoveSourceBranch).with("squash", squash) + .fetch(GitlabMergeRequest.class); + } + + public GitlabMergeRequest getMergeRequestChanges(Object project, long iid) { + return request(project, "projects/%s/merge_requests/" + iid + "/changes").method(ScmHttpMethod.GET) + .fetch(GitlabMergeRequest.class); + } + + public List getMergeRequestCommits(Object project, long iid, Integer page, Integer perPage) { + GitlabCommit[] result = request(project, "projects/%s/merge_requests/" + iid + "/commits") + .method(ScmHttpMethod.GET).with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)) + .fetch(GitlabCommit[].class); + return Arrays.asList(result); + } + + private Requester fill(Requester request, GitlabMergeRequestParams params, boolean create) { + request.with("target_branch", params.getTargetBranch()).with("title", params.getTitle()) + .with("description", params.getDescription()).with("assignee_id", params.getAssigneeId()) + .with("assignee_ids", params.getAssigneeIds()).with("labels", join(params.getLabels())) + .with("milestone_id", params.getMilestoneId()) + .with("remove_source_branch", params.getRemoveSourceBranch()) + .with("squash", params.getSquash()); + if (create) { + request.with("source_branch", params.getSourceBranch()) + .with("target_project_id", params.getTargetProjectId()); + } else { + request.with("state_event", params.getStateEvent()); + } + return request; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabNotesApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabNotesApi.java new file mode 100644 index 0000000..f6700fe --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabNotesApi.java @@ -0,0 +1,63 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.Requester; +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabNote; +import java.util.Arrays; +import java.util.List; + +public class GitlabNotesApi extends AbstractGitlabApi { + GitlabNotesApi(GitlabApi api) { + super(api); + } + + public List getMergeRequestNotes(Object project, long iid, Integer page, Integer perPage) { + return list(project, "projects/%s/merge_requests/" + iid + "/notes", page, perPage); + } + + public GitlabNote getMergeRequestNote(Object project, long iid, long noteId) { + return get(project, "projects/%s/merge_requests/" + iid + "/notes/" + noteId); + } + + public GitlabNote createMergeRequestNote(Object project, long iid, String body) { + return create(project, "projects/%s/merge_requests/" + iid + "/notes", body); + } + + public void deleteMergeRequestNote(Object project, long iid, long noteId) { + delete(project, "projects/%s/merge_requests/" + iid + "/notes/" + noteId); + } + + public List getIssueNotes(Object project, long iid, Integer page, Integer perPage) { + return list(project, "projects/%s/issues/" + iid + "/notes", page, perPage); + } + + public GitlabNote getIssueNote(Object project, long iid, long noteId) { + return get(project, "projects/%s/issues/" + iid + "/notes/" + noteId); + } + + public GitlabNote createIssueNote(Object project, long iid, String body) { + return create(project, "projects/%s/issues/" + iid + "/notes", body); + } + + public void deleteIssueNote(Object project, long iid, long noteId) { + delete(project, "projects/%s/issues/" + iid + "/notes/" + noteId); + } + + private List list(Object project, String path, Integer page, Integer perPage) { + GitlabNote[] result = request(project, path).method(ScmHttpMethod.GET).with(PAGE_PARAM, page(page)) + .with(PER_PAGE_PARAM, perPage(perPage)).fetch(GitlabNote[].class); + return Arrays.asList(result); + } + + private GitlabNote get(Object project, String path) { + return request(project, path).method(ScmHttpMethod.GET).fetch(GitlabNote.class); + } + + private GitlabNote create(Object project, String path, String body) { + return request(project, path).method(ScmHttpMethod.POST).with("body", body).fetch(GitlabNote.class); + } + + private void delete(Object project, String path) { + request(project, path).method(ScmHttpMethod.DELETE).send(); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabOauth2Api.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabOauth2Api.java new file mode 100644 index 0000000..c49c84d --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabOauth2Api.java @@ -0,0 +1,87 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.GitOauth2ClientProperties; +import com.tencent.devops.scm.sdk.common.Requester; +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.common.ScmRequest; +import com.tencent.devops.scm.sdk.common.connector.ScmConnector; +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabAuthProvider; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabOauth2AccessToken; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +public class GitlabOauth2Api { + private static final GitlabAuthProvider ANONYMOUS = builder -> { }; + private final GitOauth2ClientProperties properties; + private final GitlabApiClient client; + + public GitlabOauth2Api(GitOauth2ClientProperties properties, ScmConnector connector) { + if (properties == null) { + throw new IllegalArgumentException("OAuth properties cannot be null"); + } + this.properties = properties; + this.client = new GitlabApiClient(normalizeWebUrl(properties.getWebUrl()), connector, ANONYMOUS); + } + + public String authorizationUrl(String state) { + return ScmRequest.newBuilder().withApiUrl(client.getApiUrl()).withUrlPath("oauth/authorize") + .with("client_id", properties.getClientId()).with("redirect_uri", properties.getRedirectUri()) + .with("response_type", "code").with("state", state).build().url().toString(); + } + + private static String normalizeWebUrl(String webUrl) { + if (webUrl == null || webUrl.trim().isEmpty()) { + throw new IllegalArgumentException("OAuth webUrl cannot be blank"); + } + int end = webUrl.length(); + while (end > 0 && webUrl.charAt(end - 1) == '/') { + end--; + } + return webUrl.substring(0, end); + } + + public GitlabOauth2AccessToken callback(String code) { + LinkedHashMap form = baseForm("authorization_code"); + form.put("code", code); + return token(form); + } + + public GitlabOauth2AccessToken refresh(String refreshToken) { + LinkedHashMap form = baseForm("refresh_token"); + form.put("refresh_token", refreshToken); + return token(form); + } + + private LinkedHashMap baseForm(String grantType) { + LinkedHashMap form = new LinkedHashMap<>(); + form.put("grant_type", grantType); + form.put("client_id", properties.getClientId()); + form.put("client_secret", properties.getClientSecret()); + form.put("redirect_uri", properties.getRedirectUri()); + return form; + } + + private GitlabOauth2AccessToken token(Map form) { + return new Requester(client).method(ScmHttpMethod.POST).withUrlPath("oauth/token") + .contentType("application/x-www-form-urlencoded").with(formBody(form)) + .fetch(GitlabOauth2AccessToken.class); + } + + private String formBody(Map form) { + StringBuilder body = new StringBuilder(); + for (Map.Entry entry : form.entrySet()) { + if (entry.getValue() == null) { + throw new IllegalArgumentException(entry.getKey() + " cannot be null"); + } + if (body.length() > 0) { + body.append('&'); + } + body.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)); + body.append('='); + body.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); + } + return body.toString(); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabPagedIterator.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabPagedIterator.java new file mode 100644 index 0000000..b1c0e61 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabPagedIterator.java @@ -0,0 +1,110 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.ScmApiClient; +import com.tencent.devops.scm.sdk.common.ScmRequest; +import com.tencent.devops.scm.sdk.common.ScmResponse; +import java.lang.reflect.Array; +import java.util.Iterator; +import java.util.NoSuchElementException; + +public final class GitlabPagedIterator implements Iterator, GitlabConstants { + private final ScmApiClient client; + private final Class type; + private ScmRequest nextRequest; + private T next; + + private GitlabPagedIterator(ScmApiClient client, ScmRequest request, Class type) { + if (!"GET".equals(request.method())) { + throw new IllegalStateException("GET is required for pagination"); + } + this.client = client; + this.type = type; + this.nextRequest = request.toBuilder().set(PAGE_PARAM, DEFAULT_PAGE).build(); + } + + static GitlabPagedIterator create(ScmApiClient client, ScmRequest request, Class type) { + return new GitlabPagedIterator<>(client, request, type); + } + + @Override + public boolean hasNext() { + fetch(); + return next != null; + } + + @Override + public T next() { + fetch(); + if (next == null) { + throw new NoSuchElementException(); + } + T value = next; + next = null; + return value; + } + + private void fetch() { + if (next != null || nextRequest == null) { + return; + } + ScmResponse response = client.sendRequest(nextRequest, + raw -> ScmResponse.parseBody(raw, type, client.getJsonFactory())); + next = response.body(); + nextRequest = nextRequest(nextRequest, response); + } + + private ScmRequest nextRequest(ScmRequest request, ScmResponse response) { + String rawNextPage = response.header(NEXT_PAGE_HEADER); + String nextPage = trim(rawNextPage); + if (nextPage != null) { + return request.toBuilder().set(PAGE_PARAM, parsePositive(NEXT_PAGE_HEADER, nextPage)).build(); + } + if (rawNextPage != null) { + return null; + } + String current = trim(response.header(PAGE_HEADER)); + String total = trim(response.header(TOTAL_PAGES_HEADER)); + if (current != null && total != null + && parsePositive(PAGE_HEADER, current) < parsePositive(TOTAL_PAGES_HEADER, total)) { + return request.toBuilder().set(PAGE_PARAM, parsePositive(PAGE_HEADER, current) + 1).build(); + } + if (current != null && total != null) { + return null; + } + int perPage = requestArg(request, PER_PAGE_PARAM, DEFAULT_PER_PAGE); + int currentPage = requestArg(request, PAGE_PARAM, DEFAULT_PAGE); + if (response.body() != null && response.body().getClass().isArray() + && Array.getLength(response.body()) >= perPage) { + return request.toBuilder().set(PAGE_PARAM, currentPage + 1).build(); + } + return null; + } + + private int requestArg(ScmRequest request, String name, int defaultValue) { + for (ScmRequest.Entry entry : request.args()) { + if (name.equals(entry.getKey())) { + return Integer.parseInt(entry.getValue().toString()); + } + } + return defaultValue; + } + + private int parsePositive(String header, String value) { + try { + int parsed = Integer.parseInt(value); + if (parsed < 1) { + throw new NumberFormatException(); + } + return parsed; + } catch (NumberFormatException error) { + throw new GitlabApiException("Invalid " + header + " response header"); + } + } + + private String trim(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + return value.trim(); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabProjectHooksApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabProjectHooksApi.java new file mode 100644 index 0000000..b4cb164 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabProjectHooksApi.java @@ -0,0 +1,45 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.Requester; +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProjectHook; +import java.util.Arrays; +import java.util.List; + +public class GitlabProjectHooksApi extends AbstractGitlabApi { + GitlabProjectHooksApi(GitlabApi api) { + super(api); + } + + public List getHooks(Object project, Integer page, Integer perPage) { + GitlabProjectHook[] result = request(project, "projects/%s/hooks").method(ScmHttpMethod.GET) + .with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)).fetch(GitlabProjectHook[].class); + return Arrays.asList(result); + } + + public GitlabProjectHook getHook(Object project, long hookId) { + return request(project, "projects/%s/hooks/" + hookId).method(ScmHttpMethod.GET) + .fetch(GitlabProjectHook.class); + } + + public GitlabProjectHook addHook(Object project, GitlabProjectHook hook, String token) { + return fill(request(project, "projects/%s/hooks").method(ScmHttpMethod.POST), hook, token) + .fetch(GitlabProjectHook.class); + } + + public GitlabProjectHook updateHook(Object project, long hookId, GitlabProjectHook hook, String token) { + return fill(request(project, "projects/%s/hooks/" + hookId).method(ScmHttpMethod.PUT), hook, token) + .fetch(GitlabProjectHook.class); + } + + public void deleteHook(Object project, long hookId) { + request(project, "projects/%s/hooks/" + hookId).method(ScmHttpMethod.DELETE).send(); + } + + private Requester fill(Requester request, GitlabProjectHook hook, String token) { + return request.with("url", hook.getUrl()).with("push_events", hook.isPushEvents()) + .with("tag_push_events", hook.isTagPushEvents()).with("issues_events", hook.isIssuesEvents()) + .with("merge_requests_events", hook.isMergeRequestsEvents()).with("note_events", hook.isNoteEvents()) + .with("enable_ssl_verification", hook.isEnableSslVerification()).with("token", token); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabProjectsApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabProjectsApi.java new file mode 100644 index 0000000..92f7d85 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabProjectsApi.java @@ -0,0 +1,49 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.PagedIterable; +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMember; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProject; +import java.util.Arrays; +import java.util.List; + +public class GitlabProjectsApi extends AbstractGitlabApi { + GitlabProjectsApi(GitlabApi api) { + super(api); + } + + public GitlabProject getProject(Object project) { + return request(project, "projects/%s").method(ScmHttpMethod.GET).fetch(GitlabProject.class); + } + + public List getProjects(String search, Integer page, Integer perPage) { + GitlabProject[] result = gitlabApi.createRequest().method(ScmHttpMethod.GET).withUrlPath("projects") + .with("membership", true).with("search", search).with(PAGE_PARAM, page(page)) + .with(PER_PAGE_PARAM, perPage(perPage)).fetch(GitlabProject[].class); + return Arrays.asList(result); + } + + public PagedIterable getProjects(int perPage) { + return gitlabApi.createRequest().method(ScmHttpMethod.GET).withUrlPath("projects") + .with("membership", true).with(PER_PAGE_PARAM, perPage(perPage)).toIterable(GitlabProject[].class); + } + + public List getAllMembers(Object project, String query, Integer page, Integer perPage) { + GitlabMember[] result = request(project, "projects/%s/members/all").method(ScmHttpMethod.GET) + .with("query", query).with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)) + .fetch(GitlabMember[].class); + return Arrays.asList(result); + } + + public GitlabMember getMember(Object project, String username) { + PagedIterable members = request(project, "projects/%s/members/all") + .method(ScmHttpMethod.GET).with("query", username).with(PER_PAGE_PARAM, DEFAULT_PER_PAGE) + .toIterable(GitlabMember[].class); + for (GitlabMember member : members) { + if (username.equals(member.getUsername())) { + return member; + } + } + return null; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabRepositoryFilesApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabRepositoryFilesApi.java new file mode 100644 index 0000000..4f82042 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabRepositoryFilesApi.java @@ -0,0 +1,68 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.Requester; +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabRepositoryFile; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabTreeItem; +import java.util.Arrays; +import java.util.List; + +public class GitlabRepositoryFilesApi extends AbstractGitlabApi { + GitlabRepositoryFilesApi(GitlabApi api) { + super(api); + } + + public GitlabRepositoryFile getFile(Object project, String filePath, String ref) { + requireRef(ref); + return request(project, "projects/%s/repository/files/" + segment(filePath)).method(ScmHttpMethod.GET) + .with("ref", ref).fetch(GitlabRepositoryFile.class); + } + + public void createFile(Object project, String filePath, String branch, String encoding, String content, + String commitMessage) { + write(request(project, "projects/%s/repository/files/" + segment(filePath)).method(ScmHttpMethod.POST), + branch, encoding, content, commitMessage).send(); + } + + public void createFile(Object project, String filePath, String branch, String content, String commitMessage) { + createFile(project, filePath, branch, null, content, commitMessage); + } + + public void updateFile(Object project, String filePath, String branch, String encoding, String content, + String commitMessage) { + write(request(project, "projects/%s/repository/files/" + segment(filePath)).method(ScmHttpMethod.PUT), + branch, encoding, content, commitMessage).send(); + } + + public void updateFile(Object project, String filePath, String branch, String content, String commitMessage) { + updateFile(project, filePath, branch, null, content, commitMessage); + } + + public void deleteFile(Object project, String filePath, String branch, String commitMessage) { + request(project, "projects/%s/repository/files/" + segment(filePath)).method(ScmHttpMethod.DELETE) + .with("branch", branch).with("commit_message", commitMessage).send(); + } + + public List getTree(Object project, String path, String ref, Boolean recursive, + Integer page, Integer perPage) { + requireRef(ref); + GitlabTreeItem[] result = request(project, "projects/%s/repository/tree").method(ScmHttpMethod.GET) + .with("path", path).with("ref", ref).with("recursive", recursive) + .with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)).fetch(GitlabTreeItem[].class); + return Arrays.asList(result); + } + + private Requester write(Requester request, String branch, String encoding, String content, String message) { + request.with("branch", branch).with("content", content).with("commit_message", message); + if (encoding != null && !"text".equalsIgnoreCase(encoding)) { + request.with("encoding", encoding); + } + return request; + } + + private void requireRef(String ref) { + if (ref == null || ref.trim().isEmpty()) { + throw new IllegalArgumentException("ref cannot be blank"); + } + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabTagsApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabTagsApi.java new file mode 100644 index 0000000..bf25705 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabTagsApi.java @@ -0,0 +1,33 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabTag; +import java.util.Arrays; +import java.util.List; + +public class GitlabTagsApi extends AbstractGitlabApi { + GitlabTagsApi(GitlabApi api) { + super(api); + } + + public List getTags(Object project, String search, Integer page, Integer perPage) { + GitlabTag[] result = request(project, "projects/%s/repository/tags").method(ScmHttpMethod.GET) + .with("search", search).with(PAGE_PARAM, page(page)).with(PER_PAGE_PARAM, perPage(perPage)) + .fetch(GitlabTag[].class); + return Arrays.asList(result); + } + + public GitlabTag getTag(Object project, String tag) { + return request(project, "projects/%s/repository/tags/" + segment(tag)).method(ScmHttpMethod.GET) + .fetch(GitlabTag.class); + } + + public GitlabTag createTag(Object project, String tag, String ref, String message) { + return request(project, "projects/%s/repository/tags").method(ScmHttpMethod.POST) + .with("tag_name", tag).with("ref", ref).with("message", message).fetch(GitlabTag.class); + } + + public void deleteTag(Object project, String tag) { + request(project, "projects/%s/repository/tags/" + segment(tag)).method(ScmHttpMethod.DELETE).send(); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabUsersApi.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabUsersApi.java new file mode 100644 index 0000000..05f2fe3 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/GitlabUsersApi.java @@ -0,0 +1,14 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.ScmHttpMethod; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabUser; + +public class GitlabUsersApi extends AbstractGitlabApi { + GitlabUsersApi(GitlabApi api) { + super(api); + } + + public GitlabUser getCurrentUser() { + return gitlabApi.createRequest().method(ScmHttpMethod.GET).withUrlPath("user").fetch(GitlabUser.class); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/auth/GitlabAuthProvider.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/auth/GitlabAuthProvider.java new file mode 100644 index 0000000..c4b8d8a --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/auth/GitlabAuthProvider.java @@ -0,0 +1,6 @@ +package com.tencent.devops.scm.sdk.gitlab.auth; + +import com.tencent.devops.scm.sdk.common.auth.HttpAuthProvider; + +public interface GitlabAuthProvider extends HttpAuthProvider { +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/auth/GitlabTokenAuthProvider.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/auth/GitlabTokenAuthProvider.java new file mode 100644 index 0000000..2833258 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/auth/GitlabTokenAuthProvider.java @@ -0,0 +1,42 @@ +package com.tencent.devops.scm.sdk.gitlab.auth; + +import com.tencent.devops.scm.sdk.common.ScmRequest; +import com.tencent.devops.scm.sdk.gitlab.GitlabConstants; + +public final class GitlabTokenAuthProvider implements GitlabAuthProvider { + private final String header; + private final String value; + + private GitlabTokenAuthProvider(String header, String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("token cannot be blank"); + } + this.header = header; + this.value = value; + } + + public static GitlabAuthProvider oauthAccessToken(String token) { + return new GitlabTokenAuthProvider( + GitlabConstants.AUTHORIZATION_HEADER, GitlabConstants.BEARER_PREFIX + requireToken(token)); + } + + public static GitlabAuthProvider personalAccessToken(String token) { + return privateToken(token); + } + + public static GitlabAuthProvider privateToken(String token) { + return new GitlabTokenAuthProvider(GitlabConstants.PRIVATE_TOKEN_HEADER, token); + } + + private static String requireToken(String token) { + if (token == null || token.trim().isEmpty()) { + throw new IllegalArgumentException("token cannot be blank"); + } + return token; + } + + @Override + public void authorization(ScmRequest.Builder builder) { + builder.setHeader(header, value); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabBranch.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabBranch.java new file mode 100644 index 0000000..f7a53ec --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabBranch.java @@ -0,0 +1,17 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class GitlabBranch { + private String name; + private boolean merged; + @JsonProperty("protected") + private boolean protectedBranch; + @JsonProperty("default") + private boolean defaultBranch; + private boolean canPush; + private String webUrl; + private GitlabCommit commit; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCommit.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCommit.java new file mode 100644 index 0000000..7222579 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCommit.java @@ -0,0 +1,22 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.Date; +import java.util.List; +import lombok.Data; + +@Data +public class GitlabCommit { + private String id; + private String shortId; + private String title; + private String message; + private String authorName; + private String authorEmail; + private String committerName; + private String committerEmail; + private Date authoredDate; + private Date committedDate; + private Date createdAt; + private List parentIds; + private String webUrl; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCommitStatus.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCommitStatus.java new file mode 100644 index 0000000..6a09ff6 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCommitStatus.java @@ -0,0 +1,19 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.Date; +import lombok.Data; + +@Data +public class GitlabCommitStatus { + private Long id; + private String sha; + private String ref; + private String status; + private String name; + private String description; + private String targetUrl; + private Date createdAt; + private Date startedAt; + private Date finishedAt; + private GitlabUser author; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCompareResults.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCompareResults.java new file mode 100644 index 0000000..c91eec2 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabCompareResults.java @@ -0,0 +1,13 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.List; +import lombok.Data; + +@Data +public class GitlabCompareResults { + private GitlabCommit commit; + private List commits; + private List diffs; + private boolean compareTimeout; + private boolean compareSameRef; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabDiff.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabDiff.java new file mode 100644 index 0000000..7e803b3 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabDiff.java @@ -0,0 +1,18 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; + +@Data +public class GitlabDiff { + private String oldPath; + private String newPath; + private String aMode; + private String bMode; + private String diff; + private boolean newFile; + private boolean renamedFile; + private boolean deletedFile; + private boolean generatedFile; + private boolean collapsed; + private boolean tooLarge; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabDiffRefs.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabDiffRefs.java new file mode 100644 index 0000000..1d64e63 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabDiffRefs.java @@ -0,0 +1,10 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; + +@Data +public class GitlabDiffRefs { + private String baseSha; + private String headSha; + private String startSha; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabIssue.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabIssue.java new file mode 100644 index 0000000..1c03aa9 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabIssue.java @@ -0,0 +1,24 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.Date; +import java.util.List; +import lombok.Data; + +@Data +public class GitlabIssue { + private Long id; + private Long iid; + private Long projectId; + private String title; + private String description; + private String state; + private GitlabUser author; + private GitlabUser assignee; + private List assignees; + private List labels; + private GitlabMilestone milestone; + private Date createdAt; + private Date updatedAt; + private Date closedAt; + private String webUrl; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabIssueParams.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabIssueParams.java new file mode 100644 index 0000000..a50ef32 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabIssueParams.java @@ -0,0 +1,17 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.List; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class GitlabIssueParams { + private String title; + private String description; + private Long assigneeId; + private List assigneeIds; + private List labels; + private Long milestoneId; + private String stateEvent; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMember.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMember.java new file mode 100644 index 0000000..6d25410 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMember.java @@ -0,0 +1,11 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GitlabMember extends GitlabUser { + private Integer accessLevel; + private String expiresAt; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMergeRequest.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMergeRequest.java new file mode 100644 index 0000000..9872c5d --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMergeRequest.java @@ -0,0 +1,36 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.Date; +import java.util.List; +import lombok.Data; + +@Data +public class GitlabMergeRequest { + private Long id; + private Long iid; + private Long projectId; + private String title; + private String description; + private String state; + private String sourceBranch; + private String targetBranch; + private Long sourceProjectId; + private Long targetProjectId; + private GitlabUser author; + private GitlabUser assignee; + private List assignees; + private List reviewers; + private List labels; + private GitlabMilestone milestone; + private Date createdAt; + private Date updatedAt; + private Date mergedAt; + private Date closedAt; + private String mergeCommitSha; + private String squashCommitSha; + private String sha; + private GitlabDiffRefs diffRefs; + private String webUrl; + private List changes; + private Boolean overflow; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMergeRequestParams.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMergeRequestParams.java new file mode 100644 index 0000000..94ef700 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMergeRequestParams.java @@ -0,0 +1,22 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.List; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class GitlabMergeRequestParams { + private String sourceBranch; + private String targetBranch; + private String title; + private String description; + private Long assigneeId; + private List assigneeIds; + private List labels; + private Long milestoneId; + private Long targetProjectId; + private Boolean removeSourceBranch; + private Boolean squash; + private String stateEvent; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMilestone.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMilestone.java new file mode 100644 index 0000000..fd1b875 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabMilestone.java @@ -0,0 +1,16 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.Date; +import lombok.Data; + +@Data +public class GitlabMilestone { + private Long id; + private Long iid; + private String title; + private String description; + private String state; + private Date createdAt; + private Date updatedAt; + private Date dueDate; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabNamespace.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabNamespace.java new file mode 100644 index 0000000..1a2be0c --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabNamespace.java @@ -0,0 +1,13 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; + +@Data +public class GitlabNamespace { + private Long id; + private String name; + private String path; + private String kind; + private String fullPath; + private String webUrl; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabNote.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabNote.java new file mode 100644 index 0000000..c602fd5 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabNote.java @@ -0,0 +1,17 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.Date; +import lombok.Data; + +@Data +public class GitlabNote { + private Long id; + private String body; + private GitlabUser author; + private Date createdAt; + private Date updatedAt; + private boolean system; + private String noteableType; + private Long noteableId; + private Long projectId; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabOauth2AccessToken.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabOauth2AccessToken.java new file mode 100644 index 0000000..5b9abe6 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabOauth2AccessToken.java @@ -0,0 +1,13 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; + +@Data +public class GitlabOauth2AccessToken { + private String accessToken; + private String tokenType; + private Long expiresIn; + private String refreshToken; + private String scope; + private Long createdAt; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabProject.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabProject.java new file mode 100644 index 0000000..8bf8625 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabProject.java @@ -0,0 +1,20 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; + +@Data +public class GitlabProject { + private Long id; + private String name; + private String path; + private String pathWithNamespace; + private String description; + private String defaultBranch; + private Boolean archived; + private String visibility; + private String webUrl; + private String httpUrlToRepo; + private String sshUrlToRepo; + private String avatarUrl; + private GitlabNamespace namespace; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabProjectHook.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabProjectHook.java new file mode 100644 index 0000000..93a6666 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabProjectHook.java @@ -0,0 +1,18 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import java.util.Date; +import lombok.Data; + +@Data +public class GitlabProjectHook { + private Long id; + private String url; + private Long projectId; + private boolean pushEvents; + private boolean tagPushEvents; + private boolean issuesEvents; + private boolean mergeRequestsEvents; + private boolean noteEvents; + private boolean enableSslVerification; + private Date createdAt; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabRepositoryFile.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabRepositoryFile.java new file mode 100644 index 0000000..74abda3 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabRepositoryFile.java @@ -0,0 +1,17 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; + +@Data +public class GitlabRepositoryFile { + private String fileName; + private String filePath; + private Long size; + private String encoding; + private String content; + private String contentSha256; + private String ref; + private String blobId; + private String commitId; + private String lastCommitId; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabTag.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabTag.java new file mode 100644 index 0000000..154b7bc --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabTag.java @@ -0,0 +1,16 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; +import lombok.Data; + +@Data +public class GitlabTag { + private String name; + private String message; + private String target; + @JsonProperty("protected") + private boolean protectedTag; + private Date createdAt; + private GitlabCommit commit; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabTreeItem.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabTreeItem.java new file mode 100644 index 0000000..d734b0e --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabTreeItem.java @@ -0,0 +1,12 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; + +@Data +public class GitlabTreeItem { + private String id; + private String name; + private String type; + private String path; + private String mode; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabUser.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabUser.java new file mode 100644 index 0000000..b5d5e95 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/GitlabUser.java @@ -0,0 +1,14 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo; + +import lombok.Data; + +@Data +public class GitlabUser { + private Long id; + private String username; + private String name; + private String state; + private String avatarUrl; + private String webUrl; + private String email; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventCommit.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventCommit.java new file mode 100644 index 0000000..d8f6e1a --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventCommit.java @@ -0,0 +1,20 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import java.util.List; +import lombok.Data; + +@Data +public class GitlabEventCommit { + private String id; + private String sha; + private String message; + private String timestamp; + private String url; + private String webUrl; + private GitlabEventUser author; + private String authorName; + private String authorEmail; + private List added; + private List modified; + private List removed; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventLabel.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventLabel.java new file mode 100644 index 0000000..563aca8 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventLabel.java @@ -0,0 +1,8 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import lombok.Data; + +@Data +public class GitlabEventLabel { + private String title; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventRepository.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventRepository.java new file mode 100644 index 0000000..8bb4da4 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventRepository.java @@ -0,0 +1,20 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import lombok.Data; + +@Data +public class GitlabEventRepository { + private Long id; + private String name; + private String pathWithNamespace; + private String defaultBranch; + private String gitHttpUrl; + private String httpUrl; + private String httpUrlToRepo; + private String gitSshUrl; + private String sshUrl; + private String sshUrlToRepo; + private String webUrl; + private String homepage; + private String url; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventUser.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventUser.java new file mode 100644 index 0000000..d09ebb7 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabEventUser.java @@ -0,0 +1,12 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import lombok.Data; + +@Data +public class GitlabEventUser { + private Long id; + private String username; + private String name; + private String email; + private String avatarUrl; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabIssueEvent.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabIssueEvent.java new file mode 100644 index 0000000..13b8d56 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabIssueEvent.java @@ -0,0 +1,33 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import java.util.List; +import lombok.Data; + +@Data +public class GitlabIssueEvent { + private String objectKind; + private String eventType; + private Long projectId; + private GitlabEventUser user; + private GitlabEventRepository project; + private GitlabEventRepository repository; + private ObjectAttributes objectAttributes; + + @Data + public static class ObjectAttributes { + private Long id; + private Integer iid; + private String action; + private String state; + private String title; + private String description; + private Long authorId; + private GitlabEventUser author; + private String createdAt; + private String updatedAt; + private String url; + private String webUrl; + private Long milestoneId; + private List labels; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabMergeRequestEvent.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabMergeRequestEvent.java new file mode 100644 index 0000000..30ad994 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabMergeRequestEvent.java @@ -0,0 +1,44 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import java.util.List; +import lombok.Data; + +@Data +public class GitlabMergeRequestEvent { + private String objectKind; + private String eventType; + private String oldrev; + private Long projectId; + private GitlabEventUser user; + private GitlabEventRepository project; + private GitlabEventRepository repository; + private ObjectAttributes objectAttributes; + + @Data + public static class ObjectAttributes { + private Long id; + private Integer iid; + private String action; + private String state; + private String title; + private String description; + private String sourceBranch; + private String targetBranch; + private Long sourceProjectId; + private Long targetProjectId; + private Long authorId; + private GitlabEventUser author; + private String createdAt; + private String updatedAt; + private String url; + private String webUrl; + private String sha; + private String oldrev; + private String mergeCommitSha; + private String squashCommitSha; + private GitlabEventRepository source; + private GitlabEventRepository target; + private GitlabEventCommit lastCommit; + private List labels; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabNoteEvent.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabNoteEvent.java new file mode 100644 index 0000000..c5b6534 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabNoteEvent.java @@ -0,0 +1,28 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import lombok.Data; + +@Data +public class GitlabNoteEvent { + private String objectKind; + private String eventType; + private Long projectId; + private GitlabEventUser user; + private GitlabEventRepository project; + private GitlabEventRepository repository; + private ObjectAttributes objectAttributes; + private GitlabEventCommit commit; + private GitlabIssueEvent.ObjectAttributes issue; + private GitlabMergeRequestEvent.ObjectAttributes mergeRequest; + + @Data + public static class ObjectAttributes { + private Long id; + private String action; + private String note; + private String noteableType; + private String createdAt; + private String updatedAt; + private String url; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabPushEvent.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabPushEvent.java new file mode 100644 index 0000000..a57868e --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabPushEvent.java @@ -0,0 +1,24 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import java.util.List; +import lombok.Data; + +@Data +public class GitlabPushEvent { + private String objectKind; + private String eventType; + private String before; + private String after; + private String ref; + private String checkoutSha; + private Long userId; + private String userName; + private String userUsername; + private String userEmail; + private String userAvatar; + private Long projectId; + private GitlabEventRepository project; + private GitlabEventRepository repository; + private List commits; + private Integer totalCommitsCount; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabReviewEvent.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabReviewEvent.java new file mode 100644 index 0000000..342d8de --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabReviewEvent.java @@ -0,0 +1,33 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import java.util.List; +import lombok.Data; + +@Data +public class GitlabReviewEvent { + private String objectKind; + private String id; + private String iid; + private String event; + private String state; + private String restrictType; + private Long projectId; + private Long reviewableId; + private String reviewableType; + private String commitId; + private GitlabEventUser author; + private Reviewer reviewer; + private List reviewers; + private GitlabEventRepository repository; + + @Data + public static class Reviewer { + private Long id; + private Long reviewId; + private Long userId; + private Long projectId; + private String type; + private String state; + private GitlabEventUser reviewer; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabWebhookEvent.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabWebhookEvent.java new file mode 100644 index 0000000..000d615 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/pojo/webhook/GitlabWebhookEvent.java @@ -0,0 +1,9 @@ +package com.tencent.devops.scm.sdk.gitlab.pojo.webhook; + +import lombok.Data; + +@Data +public class GitlabWebhookEvent { + private String objectKind; + private String eventType; +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/util/GitlabJsonUtil.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/util/GitlabJsonUtil.java new file mode 100644 index 0000000..4456ffa --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/main/java/com/tencent/devops/scm/sdk/gitlab/util/GitlabJsonUtil.java @@ -0,0 +1,51 @@ +package com.tencent.devops.scm.sdk.gitlab.util; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.tencent.devops.scm.sdk.common.util.ScmSdkJsonFactory; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Date; + +public final class GitlabJsonUtil { + private static final ScmSdkJsonFactory JSON_FACTORY = createFactory(); + + private GitlabJsonUtil() { + } + + public static ScmSdkJsonFactory getJsonFactory() { + return JSON_FACTORY; + } + + private static ScmSdkJsonFactory createFactory() { + ScmSdkJsonFactory factory = new ScmSdkJsonFactory(); + factory.getObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + SimpleModule module = new SimpleModule(); + module.addDeserializer(Date.class, new JsonDeserializer() { + @Override + public Date deserialize(JsonParser parser, DeserializationContext context) throws IOException { + String value = parser.getValueAsString(); + if (value == null || value.isEmpty()) { + return null; + } + try { + return Date.from(Instant.parse(value)); + } catch (RuntimeException ignored) { + try { + return Date.from(OffsetDateTime.parse(value).toInstant()); + } catch (RuntimeException dateTimeError) { + return Date.from(LocalDate.parse(value).atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + } + } + }); + factory.getObjectMapper().registerModule(module); + return factory; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabAuthAndOauthTest.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabAuthAndOauthTest.java new file mode 100644 index 0000000..8f6071a --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabAuthAndOauthTest.java @@ -0,0 +1,77 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.tencent.devops.scm.sdk.common.GitOauth2ClientProperties; +import com.tencent.devops.scm.sdk.common.connector.ScmConnectorRequest; +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabTokenAuthProvider; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +class GitlabAuthAndOauthTest { + @Test + void oauthAccessTokenUsesBearerHeader() { + TestConnector connector = new TestConnector().reply(200, "{\"id\":1}"); + GitlabApi api = new GitlabApi("https://gitlab.example/api/v4", connector, + GitlabTokenAuthProvider.oauthAccessToken("oauth-token")); + + api.getUsersApi().getCurrentUser(); + + assertEquals("Bearer oauth-token", connector.lastRequest().header("Authorization")); + assertNull(connector.lastRequest().header("PRIVATE-TOKEN")); + } + + @Test + void personalAndPrivateTokensUsePrivateTokenHeader() { + TestConnector connector = new TestConnector().reply(200, "{\"id\":1}").reply(200, "{\"id\":1}"); + new GitlabApi("https://gitlab.example/api/v4", connector, + GitlabTokenAuthProvider.personalAccessToken("pat")).getUsersApi().getCurrentUser(); + new GitlabApi("https://gitlab.example/api/v4", connector, + GitlabTokenAuthProvider.privateToken("provider-token")).getUsersApi().getCurrentUser(); + + assertEquals("pat", connector.requests().get(0).header("PRIVATE-TOKEN")); + assertEquals("provider-token", connector.requests().get(1).header("PRIVATE-TOKEN")); + assertNull(connector.requests().get(1).header("Authorization")); + } + + @Test + void rejectsNullAndBlankOauthTokensBeforeAddingBearerPrefix() { + assertThrows(IllegalArgumentException.class, () -> GitlabTokenAuthProvider.oauthAccessToken(null)); + assertThrows(IllegalArgumentException.class, () -> GitlabTokenAuthProvider.oauthAccessToken(" ")); + } + + @Test + void authorizationUrlNormalizesTrailingSlashes() { + GitOauth2ClientProperties properties = GitOauth2ClientProperties.builder() + .webUrl("https://gitlab.example///").clientId("client").clientSecret("secret") + .redirectUri("https://app.example/callback").build(); + + String url = new GitlabOauth2Api(properties, new TestConnector()).authorizationUrl("state"); + + assertEquals("https://gitlab.example/oauth/authorize", url.substring(0, url.indexOf('?'))); + } + + @Test + void oauthTokenExchangeUsesFormBodyWithoutSecretsInUrl() throws IOException { + TestConnector connector = new TestConnector().reply(200, + "{\"access_token\":\"result\",\"token_type\":\"Bearer\"}"); + GitOauth2ClientProperties properties = GitOauth2ClientProperties.builder() + .webUrl("https://gitlab.example").clientId("client id").clientSecret("s&cret") + .redirectUri("https://app.example/callback?a=1").build(); + + new GitlabOauth2Api(properties, connector).callback("code/value"); + + ScmConnectorRequest request = connector.lastRequest(); + String body = IOUtils.toString(request.body(), StandardCharsets.UTF_8); + assertEquals("https://gitlab.example/oauth/token", request.url().toString()); + assertEquals("application/x-www-form-urlencoded", request.contentType()); + assertEquals("grant_type=authorization_code&client_id=client+id&client_secret=s%26cret" + + "&redirect_uri=https%3A%2F%2Fapp.example%2Fcallback%3Fa%3D1&code=code%2Fvalue", body); + assertFalse(request.url().toString().contains("cret")); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabClientAndPaginationTest.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabClientAndPaginationTest.java new file mode 100644 index 0000000..ea38100 --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabClientAndPaginationTest.java @@ -0,0 +1,107 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.tencent.devops.scm.sdk.common.exception.ScmHttpRetryException; +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabTokenAuthProvider; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMember; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabProject; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class GitlabClientAndPaginationTest { + @Test + void followsNextPageHeaderAndStopsWhenItIsEmpty() { + TestConnector connector = new TestConnector() + .reply(200, "[{\"id\":1}]", TestConnector.headers( + "X-Page", "1", "X-Total-Pages", "3", "X-Next-Page", "2")) + .reply(200, "[{\"id\":2}]", TestConnector.headers( + "X-Page", "2", "X-Total-Pages", "3", "X-Next-Page", "")); + List ids = new ArrayList<>(); + + for (GitlabProject project : api(connector).getProjectsApi().getProjects(1)) { + ids.add(project.getId()); + } + + assertEquals(List.of(1L, 2L), ids); + assertEquals("1", query(connector.requests().get(0).url().getQuery(), "page")); + assertEquals("2", query(connector.requests().get(1).url().getQuery(), "page")); + } + + @Test + void convertsJsonClientErrorsAndPreservesRequestId() { + TestConnector connector = new TestConnector().reply(403, "{\"message\":\"forbidden\"}", + TestConnector.headers("Content-Type", "application/json", "X-Request-Id", "request-123")); + + GitlabApiException error = assertThrows(GitlabApiException.class, + () -> api(connector).getUsersApi().getCurrentUser()); + + assertEquals(403, error.getStatusCode()); + assertEquals("forbidden", error.getMessage()); + assertEquals("request-123", error.getRequestId()); + } + + @Test + void extractsGitlabOauthErrorMessages() { + GitlabApiException description = assertThrows(GitlabApiException.class, + () -> api(new TestConnector().reply(400, + "{\"error\":\"invalid_grant\",\"error_description\":\"Code expired\"}")) + .getUsersApi().getCurrentUser()); + GitlabApiException error = assertThrows(GitlabApiException.class, + () -> api(new TestConnector().reply(400, "{\"error\":\"invalid_grant\"}")) + .getUsersApi().getCurrentUser()); + + assertEquals("Code expired", description.getMessage()); + assertEquals("invalid_grant", error.getMessage()); + } + + @Test + void getMemberFollowsPaginationAndMatchesExactUsername() { + TestConnector connector = new TestConnector() + .reply(200, "[{\"id\":1,\"username\":\"target-user-old\"}]", + TestConnector.headers("X-Next-Page", "2")) + .reply(200, "[{\"id\":2,\"username\":\"target-user\"}]", + TestConnector.headers("X-Next-Page", "")); + + GitlabMember member = api(connector).getProjectsApi().getMember(1, "target-user"); + + assertEquals(2L, member.getId()); + assertEquals("2", query(connector.requests().get(1).url().getQuery(), "page")); + } + + @Test + void marksRateLimitAndRetryableServerStatusesForRetry() { + for (int status : new int[]{429, 500, 502, 503, 504}) { + RuntimeException error = assertThrows(RuntimeException.class, + () -> api(new TestConnector().reply(status, "{}")).getUsersApi().getCurrentUser()); + assertInstanceOf(ScmHttpRetryException.class, error); + } + } + + @Test + void acceptsNoContentResponsesWithoutDeserialization() { + TestConnector connector = new TestConnector().reply(204, "").reply(204, ""); + assertDoesNotThrow(() -> api(connector).getBranchesApi().deleteBranch(1, "obsolete")); + assertNull(api(connector).getUsersApi().getCurrentUser()); + } + + private GitlabApi api(TestConnector connector) { + return new GitlabApi("https://gitlab.example/api/v4", connector, + GitlabTokenAuthProvider.privateToken("token")); + } + + private String query(String query, String key) { + for (String item : query.split("&")) { + String[] pair = item.split("=", 2); + if (pair[0].equals(key)) { + return pair[1]; + } + } + return null; + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabEndpointTest.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabEndpointTest.java new file mode 100644 index 0000000..245864c --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/GitlabEndpointTest.java @@ -0,0 +1,128 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.tencent.devops.scm.sdk.common.exception.ScmHttpRetryException; +import com.tencent.devops.scm.sdk.gitlab.auth.GitlabTokenAuthProvider; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabIssueParams; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequest; +import com.tencent.devops.scm.sdk.gitlab.pojo.GitlabMergeRequestParams; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.ZoneId; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +class GitlabEndpointTest { + @Test + void reusesApiModules() { + GitlabApi api = api(new TestConnector()); + + assertSame(api.getProjectsApi(), api.getProjectsApi()); + assertSame(api.getProjectHooksApi(), api.getProjectHooksApi()); + assertSame(api.getBranchesApi(), api.getBranchesApi()); + assertSame(api.getTagsApi(), api.getTagsApi()); + assertSame(api.getCommitsApi(), api.getCommitsApi()); + assertSame(api.getMergeRequestsApi(), api.getMergeRequestsApi()); + assertSame(api.getIssuesApi(), api.getIssuesApi()); + assertSame(api.getNotesApi(), api.getNotesApi()); + assertSame(api.getRepositoryFilesApi(), api.getRepositoryFilesApi()); + assertSame(api.getUsersApi(), api.getUsersApi()); + assertSame(api.getCommitStatusesApi(), api.getCommitStatusesApi()); + } + + @Test + void encodesRawProjectRefAndFilePath() { + TestConnector connector = new TestConnector().reply(200, "{\"name\":\"feature/a\"}") + .reply(200, "{\"file_path\":\"dir/a b.txt\"}"); + GitlabApi api = api(connector); + + api.getBranchesApi().getBranch("group/sub/project", "feature/a"); + api.getRepositoryFilesApi().getFile("group/sub/project", "dir/a b.txt", "feature/a"); + + assertEquals("/api/v4/projects/group%2Fsub%2Fproject/repository/branches/feature%2Fa", + connector.requests().get(0).url().getPath()); + assertEquals("/api/v4/projects/group%2Fsub%2Fproject/repository/files/dir%2Fa%20b%2Etxt", + connector.requests().get(1).url().getPath()); + } + + @Test + void mergeRequestsAndIssuesUseProjectIidPaths() { + TestConnector connector = new TestConnector().reply(200, "{\"iid\":7}").reply(200, "{\"iid\":9}"); + GitlabApi api = api(connector); + + api.getMergeRequestsApi().updateMergeRequest(42, 7, + GitlabMergeRequestParams.builder().title("title").build()); + api.getIssuesApi().updateIssue(42, 9, GitlabIssueParams.builder().title("issue").build()); + + assertEquals("/api/v4/projects/42/merge_requests/7", connector.requests().get(0).url().getPath()); + assertEquals("/api/v4/projects/42/issues/9", connector.requests().get(1).url().getPath()); + } + + @Test + void rejectsPerPageAboveGitlabLimit() { + GitlabApi api = api(new TestConnector()); + assertThrows(IllegalArgumentException.class, + () -> api.getBranchesApi().getBranches(1, null, 1, 101)); + } + + @Test + void omitsTextEncodingAndSendsExplicitBase64Encoding() throws IOException { + TestConnector connector = new TestConnector().reply(200, "{}").reply(200, "{}").reply(200, "{}"); + GitlabRepositoryFilesApi filesApi = api(connector).getRepositoryFilesApi(); + + filesApi.updateFile(1, ".ci/pipeline.yml", "main", "content", "update"); + filesApi.createFile(1, "legacy.txt", "main", "text", "content", "create"); + filesApi.updateFile(1, "binary.dat", "main", "base64", "YmluYXJ5", "binary"); + + String defaultBody = IOUtils.toString(connector.requests().get(0).body(), StandardCharsets.UTF_8); + String legacyBody = IOUtils.toString(connector.requests().get(1).body(), StandardCharsets.UTF_8); + String base64Body = IOUtils.toString(connector.requests().get(2).body(), StandardCharsets.UTF_8); + assertFalse(defaultBody.contains("\"encoding\"")); + assertFalse(legacyBody.contains("\"encoding\"")); + assertTrue(base64Body.contains("\"encoding\":\"base64\"")); + } + + @Test + void mergeRequestChangesExposeOverflowAndAcceptLocalDateMilestones() { + TestConnector connector = new TestConnector().reply(200, + "{\"iid\":7,\"overflow\":true,\"changes\":[]," + + "\"reviewers\":[{\"username\":\"reviewer\"}]," + + "\"diff_refs\":{\"base_sha\":\"base\",\"start_sha\":\"target\",\"head_sha\":\"source\"}," + + "\"milestone\":{\"id\":1,\"due_date\":\"2026-07-31\"}}"); + + GitlabMergeRequest mergeRequest = api(connector).getMergeRequestsApi().getMergeRequestChanges(1, 7); + + assertEquals(Boolean.TRUE, mergeRequest.getOverflow()); + assertEquals("reviewer", mergeRequest.getReviewers().get(0).getUsername()); + assertEquals("base", mergeRequest.getDiffRefs().getBaseSha()); + assertEquals("target", mergeRequest.getDiffRefs().getStartSha()); + assertEquals("source", mergeRequest.getDiffRefs().getHeadSha()); + assertEquals(LocalDate.of(2026, 7, 31), mergeRequest.getMilestone().getDueDate().toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate()); + } + + @Test + void retriesConflictOnlyWhenCreatingCommitStatus() { + RuntimeException statusError = assertThrows(RuntimeException.class, + () -> api(new TestConnector().reply(409, "{\"message\":\"conflict\"}")) + .getCommitStatusesApi().create(1, "abc", "success", "build", "main", null, null)); + RuntimeException otherError = assertThrows(RuntimeException.class, + () -> api(new TestConnector().reply(409, "{\"message\":\"conflict\"}")) + .getUsersApi().getCurrentUser()); + + assertInstanceOf(ScmHttpRetryException.class, statusError); + assertInstanceOf(GitlabApiException.class, otherError); + } + + private GitlabApi api(TestConnector connector) { + return new GitlabApi("https://gitlab.example/api/v4", connector, + GitlabTokenAuthProvider.privateToken("token")); + } +} diff --git a/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/TestConnector.java b/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/TestConnector.java new file mode 100644 index 0000000..ed806aa --- /dev/null +++ b/devops-scm-sdk/devops-scm-sdk-gitlab/src/test/java/com/tencent/devops/scm/sdk/gitlab/TestConnector.java @@ -0,0 +1,69 @@ +package com.tencent.devops.scm.sdk.gitlab; + +import com.tencent.devops.scm.sdk.common.connector.ScmConnector; +import com.tencent.devops.scm.sdk.common.connector.ScmConnectorRequest; +import com.tencent.devops.scm.sdk.common.connector.ScmConnectorResponse; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class TestConnector implements ScmConnector { + private final Deque replies = new ArrayDeque<>(); + private final List requests = new ArrayList<>(); + + TestConnector reply(int status, String body) { + return reply(status, body, Collections.emptyMap()); + } + + TestConnector reply(int status, String body, Map> headers) { + replies.add(new Reply(status, body, headers)); + return this; + } + + ScmConnectorRequest lastRequest() { + return requests.get(requests.size() - 1); + } + + List requests() { + return requests; + } + + @Override + public ScmConnectorResponse send(ScmConnectorRequest request) { + requests.add(request); + Reply reply = replies.removeFirst(); + return new ScmConnectorResponse.ByteArrayResponse(request, reply.status, reply.headers) { + @Override + protected InputStream rawBodyStream() { + return new ByteArrayInputStream(reply.body.getBytes(StandardCharsets.UTF_8)); + } + }; + } + + static Map> headers(String... values) { + Map> headers = new LinkedHashMap<>(); + for (int i = 0; i < values.length; i += 2) { + headers.put(values[i], Collections.singletonList(values[i + 1])); + } + return headers; + } + + private static final class Reply { + private final int status; + private final String body; + private final Map> headers; + + private Reply(int status, String body, Map> headers) { + this.status = status; + this.body = body; + this.headers = headers; + } + } +} diff --git a/devops-scm-spring-boot-starter/build.gradle.kts b/devops-scm-spring-boot-starter/build.gradle.kts index c12343a..2f89a13 100644 --- a/devops-scm-spring-boot-starter/build.gradle.kts +++ b/devops-scm-spring-boot-starter/build.gradle.kts @@ -4,6 +4,7 @@ dependencies { api(project(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-git-common")) api(project(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-tgit")) + api(project(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-gitlab")) api(project(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-gitee")) api(project(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-bkcode")) api(project(":devops-scm-provider:devops-scm-provider-svn:devops-scm-provider-svn-common")) diff --git a/devops-scm-spring-boot-starter/src/main/kotlin/com/tencent/devops/scm/spring/config/ScmProviderConfiguration.kt b/devops-scm-spring-boot-starter/src/main/kotlin/com/tencent/devops/scm/spring/config/ScmProviderConfiguration.kt index fbb90e2..0790eea 100644 --- a/devops-scm-spring-boot-starter/src/main/kotlin/com/tencent/devops/scm/spring/config/ScmProviderConfiguration.kt +++ b/devops-scm-spring-boot-starter/src/main/kotlin/com/tencent/devops/scm/spring/config/ScmProviderConfiguration.kt @@ -2,6 +2,7 @@ package com.tencent.devops.scm.spring.config import com.tencent.devops.scm.spring.manager.BkCodeScmProviderFactory import com.tencent.devops.scm.spring.manager.GiteeScmProviderFactory +import com.tencent.devops.scm.spring.manager.GitlabScmProviderFactory import com.tencent.devops.scm.spring.manager.ScmConnectorFactory import com.tencent.devops.scm.spring.manager.ScmProviderFactory import com.tencent.devops.scm.spring.manager.ScmProviderManager @@ -30,6 +31,10 @@ class ScmProviderConfiguration { @ConditionalOnMissingBean(GiteeScmProviderFactory::class) fun giteeScmProviderFactory(connectorFactory: ScmConnectorFactory) = GiteeScmProviderFactory(connectorFactory) + @Bean + @ConditionalOnMissingBean(GitlabScmProviderFactory::class) + fun gitlabScmProviderFactory(connectorFactory: ScmConnectorFactory) = GitlabScmProviderFactory(connectorFactory) + @Bean @ConditionalOnMissingBean(BkCodeScmProviderFactory::class) fun bkCodeScmProviderFactory(connectorFactory: ScmConnectorFactory) = BkCodeScmProviderFactory(connectorFactory) diff --git a/devops-scm-spring-boot-starter/src/main/kotlin/com/tencent/devops/scm/spring/manager/GitlabScmProviderFactory.kt b/devops-scm-spring-boot-starter/src/main/kotlin/com/tencent/devops/scm/spring/manager/GitlabScmProviderFactory.kt new file mode 100644 index 0000000..4371c91 --- /dev/null +++ b/devops-scm-spring-boot-starter/src/main/kotlin/com/tencent/devops/scm/spring/manager/GitlabScmProviderFactory.kt @@ -0,0 +1,52 @@ +package com.tencent.devops.scm.spring.manager + +import com.tencent.devops.scm.api.ScmProvider +import com.tencent.devops.scm.api.enums.ScmProviderCodes +import com.tencent.devops.scm.provider.git.gitlab.GitlabScmProvider +import com.tencent.devops.scm.sdk.common.GitOauth2ClientProperties +import com.tencent.devops.scm.spring.properties.ScmProviderProperties + +class GitlabScmProviderFactory( + private val connectorFactory: ScmConnectorFactory +) : ScmProviderFactory { + + override fun support(properties: ScmProviderProperties): Boolean { + return ScmProviderCodes.GITLAB.name == properties.providerCode + } + + override fun build(properties: ScmProviderProperties, tokenApi: Boolean): ScmProvider { + val httpClientProperties = requireNotNull(properties.httpClientProperties) { + "httpClientProperties cannot be null" + } + val apiUrl = requireNotNull(httpClientProperties.apiUrl?.takeIf { it.isNotBlank() }) { + "httpClientProperties.apiUrl cannot be blank" + } + val connector = connectorFactory.create(httpClientProperties) + val oauthProperties = oauthProperties(properties, tokenApi) + return if (oauthProperties == null) { + GitlabScmProvider(apiUrl, connector) + } else { + GitlabScmProvider(apiUrl, connector, oauthProperties) + } + } + + private fun oauthProperties( + properties: ScmProviderProperties, + tokenApi: Boolean + ): GitOauth2ClientProperties? { + if (!tokenApi || properties.oauth2Enabled != true) return null + val oauth = requireNotNull(properties.oauth2ClientProperties) { + "oauth2ClientProperties cannot be null when OAuth2 is enabled" + } + return GitOauth2ClientProperties( + oauth.webUrl.required("oauth2ClientProperties.webUrl"), + oauth.clientId.required("oauth2ClientProperties.clientId"), + oauth.clientSecret.required("oauth2ClientProperties.clientSecret"), + oauth.redirectUri.required("oauth2ClientProperties.redirectUri") + ) + } + + private fun String?.required(name: String): String { + return requireNotNull(this?.takeIf { it.isNotBlank() }) { "$name cannot be blank" } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 88a566b..92172b5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,7 @@ include(":devops-scm-provider") include(":devops-scm-provider:devops-scm-provider-git") include(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-git-common") include(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-tgit") +include(":devops-scm-provider:devops-scm-provider-git:devops-scm-provider-gitlab") include(":devops-scm-provider:devops-scm-provider-svn") include(":devops-scm-provider:devops-scm-provider-svn:devops-scm-provider-svn-common") @@ -15,6 +16,7 @@ include(":devops-scm-provider:devops-scm-provider-svn:devops-scm-provider-tsvn") include(":devops-scm-sdk") include(":devops-scm-sdk:devops-scm-sdk-common") include(":devops-scm-sdk:devops-scm-sdk-tgit") +include(":devops-scm-sdk:devops-scm-sdk-gitlab") include(":devops-scm-sdk:devops-scm-sdk-tsvn") include(":devops-scm-spring-boot-starter")