All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.octopusden.octopus.vcsfacade.service.impl.GitlabService.kt Maven / Gradle / Ivy

The newest version!
package org.octopusden.octopus.vcsfacade.service.impl

import java.time.Duration
import java.time.Instant
import java.util.Date
import java.util.Stack
import java.util.concurrent.TimeUnit
import org.gitlab4j.api.GitLabApi
import org.gitlab4j.api.GitLabApiException
import org.gitlab4j.api.models.AbstractUser
import org.gitlab4j.api.models.MergeRequest
import org.gitlab4j.api.models.Project
import org.octopusden.octopus.vcsfacade.client.common.dto.Branch
import org.octopusden.octopus.vcsfacade.client.common.dto.Commit
import org.octopusden.octopus.vcsfacade.client.common.dto.CommitWithFiles
import org.octopusden.octopus.vcsfacade.client.common.dto.CreatePullRequest
import org.octopusden.octopus.vcsfacade.client.common.dto.CreateTag
import org.octopusden.octopus.vcsfacade.client.common.dto.PullRequest
import org.octopusden.octopus.vcsfacade.client.common.dto.PullRequestReviewer
import org.octopusden.octopus.vcsfacade.client.common.dto.PullRequestStatus
import org.octopusden.octopus.vcsfacade.client.common.dto.Repository
import org.octopusden.octopus.vcsfacade.client.common.dto.Tag
import org.octopusden.octopus.vcsfacade.client.common.dto.User
import org.octopusden.octopus.vcsfacade.client.common.exception.NotFoundException
import org.octopusden.octopus.vcsfacade.config.VcsConfig
import org.octopusden.octopus.vcsfacade.dto.HashOrRefOrDate
import org.octopusden.octopus.vcsfacade.dto.HashOrRefOrDate.DateValue
import org.octopusden.octopus.vcsfacade.dto.HashOrRefOrDate.HashOrRefValue
import org.octopusden.octopus.vcsfacade.service.VcsService
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Service
import org.gitlab4j.api.models.Branch as GitlabBranch
import org.gitlab4j.api.models.Commit as GitlabCommit
import org.gitlab4j.api.models.Tag as GitlabTag

@Service
@ConditionalOnProperty(
    prefix = "vcs-facade.vcs.gitlab", name = ["enabled"], havingValue = "true", matchIfMissing = true
)
@Deprecated("Not used")
class GitlabService(
    gitlabProperties: VcsConfig.GitlabProperties
) : VcsService(gitlabProperties) {
    private val clientFunc: () -> GitLabApi = {
        val authException by lazy {
            IllegalStateException("Auth Token or username/password must be specified for Gitlab access")
        }
        gitlabProperties.token?.let { GitLabApi(httpUrl, gitlabProperties.token) } ?: getGitlabApi(
            gitlabProperties, authException
        ).also { api ->
            api.setAuthTokenSupplier { getToken(gitlabProperties, authException) }
        }
    }

    private val client by lazy { clientFunc() }

    private var tokenObtained: Instant = Instant.MIN
    private var token: String = ""

    override val sshUrlRegex = "(?:ssh://)?git@$host:((?:[^/]+/)+)([^/]+).git".toRegex()

    override fun getBranches(group: String, repository: String): Sequence {
        log.trace("=> getBranches({}, {})", group, repository)
        return retryableExecution {
            client.repositoryApi.getBranches(getProject(group, repository).id).asSequence().map {
                it.toBranch(group, repository)
            }
        }.also { log.trace("<= getBranches({}, {}): {}", group, repository, it) }
    }

    override fun getTags(group: String, repository: String): Sequence {
        log.trace("=> getTags({}, {})", group, repository)
        return retryableExecution {
            client.tagsApi.getTags(getProject(group, repository).id).asSequence().map { it.toTag(group, repository) }
        }.also { log.trace("<= getTags({}, {}): {}", group, repository, it) }
    }

    override fun createTag(group: String, repository: String, createTag: CreateTag): Tag {
        log.trace("=> createTag({}, {}, {})", group, repository, createTag)
        return retryableExecution {
            client.tagsApi.createTag(
                getProject(group, repository).id, createTag.name, createTag.hashOrRef, createTag.message, ""
            ).toTag(group, repository)
        }.also { log.trace("<= createTag({}, {}, {}): {}", group, repository, createTag, it) }
    }

    override fun getTag(group: String, repository: String, name: String): Tag {
        log.trace("=> getTag({}, {}, {})", group, repository, name)
        return retryableExecution {
            client.tagsApi.getTag(getProject(group, repository).id, name).toTag(group, repository)
        }.also { log.trace("<= getTag({}, {}, {}): {}", group, repository, name, it) }
    }

    override fun deleteTag(group: String, repository: String, name: String) {
        log.trace("=> deleteTag({}, {}, {})", group, repository, name)
        retryableExecution { client.tagsApi.deleteTag(getProject(group, repository).id, name) }
        log.trace("<= deleteTag({}, {}, {})", group, repository, name)
    }

    override fun getCommits(
        group: String,
        repository: String,
        from: HashOrRefOrDate?,
        toHashOrRef: String
    ): Sequence {
        log.trace("=> getCommits({}, {}, {}, {})", group, repository, from, toHashOrRef)
        val project = getProject(group, repository)
        val toHash = getCommitByHashOrRef(project, toHashOrRef).id
        val commits = lazy {
            retryableExecution {
                client.commitsApi.getCommits(project.id, toHash, null, null, 100).asSequence().flatten()
                    .map { it.toCommit(group, repository) }.toList()
            }
        }
        return if (from is HashOrRefValue) {
            val fromHash = getCommitByHashOrRef(project, from.value).id
            if (toHash == fromHash) {
                emptySequence()
            } else {
                filterCommitGraph(group, repository, commits.value, fromHash, null, toHash).also {
                    log.trace("<= getCommits({}, {}, {}, {}): {}", group, repository, from, toHashOrRef, it)
                }.asSequence()
            }
        } else {
            val fromDate = (from as? DateValue)?.value
            filterCommitGraph(group, repository, commits.value, null, fromDate, toHash).also {
                log.trace("<= getCommits({}, {}, {}, {}): {}", group, repository, fromDate, toHashOrRef, it)
            }.asSequence()
        }
    }

    override fun getCommitsWithFiles(
        group: String,
        repository: String,
        from: HashOrRefOrDate?,
        toHashOrRef: String
    ): Sequence {
        log.warn("There is no native implementation of getCommitsWithFiles")
        return getCommits(group, repository, from, toHashOrRef).map { CommitWithFiles(it, 0, emptyList()) }
    }

    override fun getCommit(group: String, repository: String, hashOrRef: String): Commit {
        log.trace("=> getCommit({}, {}, {})", group, repository, hashOrRef)
        return getCommitByHashOrRef(getProject(group, repository), hashOrRef).toCommit(group, repository).also {
            log.trace("<= getCommit({}, {}, {}): {}", group, repository, hashOrRef, it)
        }
    }

    override fun getCommitWithFiles(group: String, repository: String, hashOrRef: String): CommitWithFiles {
        log.warn("There is no native implementation of getCommitWithFiles")
        return CommitWithFiles(getCommit(group, repository, hashOrRef), 0, emptyList())
    }

    override fun createPullRequest(
        group: String, repository: String, createPullRequest: CreatePullRequest
    ): PullRequest {
        log.trace("=> createPullRequest({}, {}, {})", group, repository, createPullRequest)
        val project = getProject(group, repository)
        val sourceBranch = createPullRequest.sourceBranch.toShortRefName()
        val targetBranch = createPullRequest.targetBranch.toShortRefName()
        retryableExecution("Source branch 'absent' not found in '$group:$repository'") {
            client.repositoryApi.getBranch(project.id, sourceBranch)
        }
        retryableExecution("Target branch 'absent' not found in '$group:$repository'") {
            client.repositoryApi.getBranch(project.id, targetBranch)
        }
        return retryableExecution {
            client.mergeRequestApi.createMergeRequest(
                project.id, sourceBranch, targetBranch, createPullRequest.title, createPullRequest.description, 0L
            )
        }.toPullRequest(group, repository).also {
            log.trace("<= createPullRequest({}, {}, {}): {}", group, repository, createPullRequest, it)
        }
    }

    override fun getPullRequest(group: String, repository: String, index: Long): PullRequest {
        log.trace("=> getPullRequest({}, {}, {})", group, repository, index)
        return retryableExecution {
            client.mergeRequestApi.getMergeRequestApprovals(getProject(group, repository).id, index)
        }.toPullRequest(group, repository).also {
            log.trace("<= getPullRequest({}, {}, {}): {}", group, repository, index, it)
        }
    }

    override fun findCommits(group: String, repository: String, hashes: Set): Sequence {
        log.trace("=> findCommits({}, {}, {})", group, repository, hashes)
        return hashes.mapNotNull {
            try {
                getCommit(group, repository, it)
            } catch (e: NotFoundException) {
                null
            }
        }.asSequence().also {
            log.trace("<= findCommits({}, {}, {}): {}", group, repository, hashes, it)
        }
    }

    override fun findPullRequests(group: String, repository: String, indexes: Set): Sequence {
        log.trace("=> findPullRequests({}, {}, {})", group, repository, indexes)
        return indexes.mapNotNull {
            try {
                getPullRequest(group, repository, it)
            } catch (e: NotFoundException) {
                null
            }
        }.asSequence().also {
            log.trace("<= findPullRequests({}, {}, {}): {}", group, repository, indexes, it)
        }
    }

    override fun findBranches(issueKey: String): Sequence {
        log.warn("The is no native implementation of findBranches")
        return emptySequence()
    }

    override fun findCommits(issueKey: String): Sequence {
        log.warn("There is no native implementation of findCommits")
        return emptySequence()
    }

    override fun findCommitsWithFiles(issueKey: String): Sequence {
        log.warn("There is no native implementation of findCommitsWithFiles")
        return emptySequence()
    }

    override fun findPullRequests(issueKey: String): Sequence {
        log.warn("There is no native implementation of findPullRequests")
        return emptySequence()
    }

    private fun getCommitByHashOrRef(project: Project, hashOrRef: String): GitlabCommit {
        val shortRefName = hashOrRef.toShortRefName()
        val hash = retryableExecution {
            client.repositoryApi.getBranches(project, shortRefName)
                .firstOrNull { b -> b.name == shortRefName }?.commit?.id
        } ?: retryableExecution {
            client.tagsApi.getTags(project.id).firstOrNull { t -> t.name == shortRefName }?.commit?.id
        } ?: hashOrRef
        return retryableExecution("Commit '$hash' does not exist in repository '${project.namespace.path}:${project.name}'.") {
            client.commitsApi.getCommit(project.id, hash)
        }
    }

    private fun getProject(namespace: String, project: String): Project {
        retryableExecution("Group '$namespace' does not exist.") { client.groupApi.getGroup(namespace) }
        return retryableExecution("Repository '$namespace:$project' does not exist.") {
            client.projectApi.getProject(namespace, project)
        }
    }

    private fun getRepository(namespace: String, project: String) = Repository(
        "ssh://git@$host:$namespace/$project.git", "$httpUrl/$namespace/$project"
    )

    private fun GitlabBranch.toBranch(namespace: String, project: String) = Branch(
        name, commit.id, "$httpUrl/$namespace/$project/-/tree/$name?ref_type=heads", getRepository(namespace, project)
    )

    private fun GitlabTag.toTag(namespace: String, project: String) = Tag(
        name, commit.id, "$httpUrl/$namespace/$project/-/tree/$name?ref_type=tags", getRepository(namespace, project)
    )

    private fun > AbstractUser.toUser() = User(username, avatarUrl)

    private fun GitlabCommit.toCommit(namespace: String, project: String) = Commit(
        id,
        message,
        committedDate,
        author?.toUser() ?: User(authorName),
        parentIds,
        "$httpUrl/$namespace/$project/-/commit/$id",
        getRepository(namespace, project)
    )

    private fun MergeRequest.toPullRequest(namespace: String, project: String) = PullRequest(
        id,
        title,
        description,
        author.toUser(),
        sourceBranch,
        targetBranch,
        assignees.map { it.toUser() },
        reviewers.map { reviewer ->
            PullRequestReviewer(reviewer.toUser(), this.approvedBy.find { it.id == reviewer.id } != null)
        },
        when (state) {
            "merged" -> PullRequestStatus.MERGED
            "closed" -> PullRequestStatus.DECLINED
            else -> PullRequestStatus.OPEN
        },
        createdAt,
        updatedAt,
        "$httpUrl/$namespace/$project/-/merge_requests/$id",
        getRepository(namespace, project)
    )

    private fun  retryableExecution(
        message: String = "", attemptLimit: Int = 3, attemptIntervalSec: Long = 3, func: () -> T
    ): T {
        lateinit var latestException: Exception
        for (attempt in 1..attemptLimit) {
            try {
                return func()
            } catch (e: GitLabApiException) {
                if (e.httpStatus == 404 || e.httpStatus == 400 &&
                    e.message?.let { it.startsWith("Target ") && it.endsWith(" is invalid") } == true
                ) {
                    throw NotFoundException(message)
                }
                log.error("${e.message}, attempt=$attempt:$attemptLimit, retry in $attemptIntervalSec sec")
                latestException = e
                TimeUnit.SECONDS.sleep(attemptIntervalSec)
            }
        }
        throw IllegalStateException(latestException.message)
    }

    private fun getGitlabApi(
        gitlabProperties: VcsConfig.GitlabProperties, authException: IllegalStateException
    ) = GitLabApi.oauth2Login(
        gitlabProperties.host,
        gitlabProperties.username ?: throw authException,
        gitlabProperties.password ?: throw authException
    ).also { api ->
        tokenObtained = Instant.now()
        token = api.authToken
    }

    private fun getToken(
        gitlabProperties: VcsConfig.GitlabProperties, authException: IllegalStateException
    ): String {
        if (tokenObtained.isBefore(Instant.now().minus(Duration.ofMinutes(110)))) {
            log.info("Refresh auth token")
            getGitlabApi(gitlabProperties, authException)
        }
        return token
    }

    private fun filterCommitGraph(
        namespace: String,
        project: String,
        commits: List,
        fromHash: String?,
        fromDate: Date?,
        toHash: String
    ): List {
        val graph = commits.map { commit -> commit.hash to commit }.toMap()
        log.trace("Graph has {} items: {}", graph.size, graph)
        val releasedCommits = fromHash?.let { fromHashValue ->
            val exceptionFunction: (hash: String) -> NotFoundException = { commit ->
                getCommit(namespace, project, fromHashValue)
                NotFoundException("Cannot find commit '$commit' in commit graph for commit '$toHash' in '$namespace:$project'")
            }
            graph.findReleasedCommits(fromHashValue, exceptionFunction)
        } ?: emptySet()
        val rootCommit = graph[toHash]
            ?: throw NotFoundException("Commit '$toHash' does not exist in repository '$namespace:$project'.")
        // Classical dfs to find all commits that should be passed to release
        val stack = Stack().also { it.push(rootCommit) }
        val visited = mutableSetOf()
        while (stack.isNotEmpty()) {
            val currentCommit = stack.pop()
            visited += currentCommit
            currentCommit.parents.map { graph[it]!! }.filter { it !in visited && it !in releasedCommits }
                .forEach { stack.add(it) }
        }
        val filter = fromHash?.let { _ ->
            { true }
        } ?: fromDate?.let { fromDateValue -> { c: Commit -> c.date > fromDateValue } } ?: { true }
        return visited.filter(filter)
    }

    companion object {
        private val log = LoggerFactory.getLogger(GitlabService::class.java)

        private fun String.toShortRefName() = replace("^refs/heads/".toRegex(), "")

        private fun Map.findReleasedCommits(
            lastReleaseHash: String, errorFunction: (hash: String) -> Exception
        ): Set {
            val releaseCommit = get(lastReleaseHash) ?: throw errorFunction(lastReleaseHash)
            val visited = mutableSetOf()
            val stack = Stack().also { it.push(releaseCommit) }
            while (stack.isNotEmpty()) {
                val currentCommit = stack.pop()
                visited += currentCommit
                currentCommit.parents.map { get(it) }.filter { it !in visited }.forEach { stack.push(it) }
            }
            return visited
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy