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

org.octopusden.octopus.infrastructure.gitea.client.GiteaClient.kt Maven / Gradle / Ivy

package org.octopusden.octopus.infrastructure.gitea.client

import feign.Headers
import feign.Param
import feign.QueryMap
import feign.RequestLine
import java.util.Date
import java.util.LinkedList
import java.util.concurrent.atomic.AtomicReference
import org.octopusden.octopus.infrastructure.gitea.client.dto.BaseGiteaEntity
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaBranch
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaCommit
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaCreateOrganization
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaCreatePullRequest
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaCreateRepository
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaCreateTag
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaEditRepoOption
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaEntityList
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaOrganization
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaPullRequest
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaPullRequestReview
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaRepository
import org.octopusden.octopus.infrastructure.gitea.client.dto.GiteaTag
import org.octopusden.octopus.infrastructure.gitea.client.exception.NotFoundException
import org.slf4j.Logger
import org.slf4j.LoggerFactory

private val _log: Logger = LoggerFactory.getLogger(GiteaClient::class.java)

const val ORG_PATH = "api/v1/orgs"
const val REPO_PATH = "api/v1/repos"
const val ENTITY_LIMIT = 50

interface GiteaClient {
    @RequestLine("GET $ORG_PATH")
    fun getOrganizations(@QueryMap requestParams: Map): GiteaEntityList

    @RequestLine("POST $ORG_PATH")
    @Headers("Content-Type: application/json")
    fun createOrganization(dto: GiteaCreateOrganization)

    @RequestLine("GET $ORG_PATH/{organization}")
    @Throws(NotFoundException::class)
    fun getOrganization(@Param("organization") organization: String): GiteaOrganization

    @RequestLine("GET $ORG_PATH/{organization}/repos")
    fun getRepositories(
        @Param("organization") organization: String,
        @QueryMap requestParams: Map
    ): GiteaEntityList

    @RequestLine("GET $REPO_PATH/{organization}/{repository}")
    @Throws(NotFoundException::class)
    fun getRepository(
        @Param("organization") organization: String,
        @Param("repository") repository: String
    ): GiteaRepository

    @RequestLine("POST $ORG_PATH/{organization}/repos")
    @Headers("Content-Type: application/json")
    fun createRepository(@Param("organization") organization: String, dto: GiteaCreateRepository)

    @RequestLine("DELETE $REPO_PATH/{organization}/{repository}")
    fun deleteRepository(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
    )

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/commits")
    fun getCommits(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @QueryMap requestParams: Map
    ): GiteaEntityList

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/git/commits/{sha}", decodeSlash = false)
    @Throws(NotFoundException::class)
    fun getCommit(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @Param("sha") sha: String,
        @QueryMap requestParams: Map
    ): GiteaCommit

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/tags")
    fun getTags(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @QueryMap requestParams: Map,
    ): GiteaEntityList

    @RequestLine("POST $REPO_PATH/{organization}/{repository}/tags")
    @Headers("Content-Type: application/json")
    fun createTag(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        dto: GiteaCreateTag
    ): GiteaTag

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/tags/{tag}", decodeSlash = false)
    @Throws(NotFoundException::class)
    fun getTag(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @Param("tag") tag: String
    ): GiteaTag

    @RequestLine("DELETE $REPO_PATH/{organization}/{repository}/tags/{tag}", decodeSlash = false)
    @Throws(NotFoundException::class)
    fun deleteTag(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @Param("tag") tag: String
    )

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/branches")
    fun getBranches(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @QueryMap requestParams: Map,
    ): GiteaEntityList?

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/branches/{branch}")
    @Throws(NotFoundException::class)
    fun getBranch(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @Param("branch") branch: String
    ): GiteaBranch

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/pulls")
    fun getPullRequests(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @QueryMap requestParams: Map
    ): GiteaEntityList

    @RequestLine("POST $REPO_PATH/{organization}/{repository}/pulls")
    @Headers("Content-Type: application/json")
    fun createPullRequest(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        dto: GiteaCreatePullRequest
    ): GiteaPullRequest

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/pulls/{number}")
    fun getPullRequest(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @Param("number") number: Long
    ): GiteaPullRequest

    @RequestLine("GET $REPO_PATH/{organization}/{repository}/pulls/{number}/reviews")
    fun getPullRequestReviews(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        @Param("number") number: Long,
        @QueryMap requestParams: Map
    ): GiteaEntityList

    @RequestLine("PATCH $REPO_PATH/{organization}/{repository}")
    @Headers("Content-Type: application/json")
    fun updateRepositoryConfiguration(
        @Param("organization") organization: String,
        @Param("repository") repository: String,
        dto: GiteaEditRepoOption
    )
}

fun GiteaClient.getOrganizations(): Collection {
    return execute({ parameters: Map -> getOrganizations(parameters) })
}

@Suppress("unused")
fun GiteaClient.getRepositories(organization: String): Collection =
    execute({ parameters: Map -> getRepositories(organization, parameters) })

fun GiteaClient.getCommit(
    organization: String,
    repository: String,
    sha: String,
    files: Boolean = false
) = getCommit(
    organization, repository, sha, mapOf(
        "stat" to false, "verification" to false, "files" to files
    )
)

fun GiteaClient.getCommits(
    organization: String,
    repository: String,
    until: String,
    sinceDate: Date? = null,
    files: Boolean = false
) = execute({ parameters: Map ->
    getCommits(
        organization, repository, parameters + mapOf(
            "stat" to false, "verification" to false, "files" to files, "sha" to until
        )
    )
}, { commit: GiteaCommit -> sinceDate == null || commit.created > sinceDate })

@Suppress("unused")
fun GiteaClient.getCommits(
    organization: String,
    repository: String,
    until: String,
    since: String,
    files: Boolean = false
): List {
    val toSha = getCommit(organization, repository, until).sha
    val fromSha = getCommit(organization, repository, since).sha
    if (fromSha == toSha) {
        return emptyList()
    }
    val parameters = mapOf(
        "limit" to ENTITY_LIMIT, "stat" to false, "verification" to false, "files" to files, "sha" to toSha
    )
    val commits = mutableMapOf()
    var page = 0
    var sinceCommitFound = false
    var orphanedCommits = listOf()
    val excludedCommits = mutableSetOf()
    do {
        val giteaResponse = getCommits(organization, repository, parameters + mapOf("page" to ++page))
        val includedCommits = mutableListOf()
        for (commit in giteaResponse.values) {
            if (commit.sha == fromSha) {
                sinceCommitFound = true
                excludedCommits.add(commit.sha)
            }
            if (excludedCommits.contains(commit.sha)) {
                excludedCommits.addAll(commit.parents.map { it.sha })
            } else {
                includedCommits.add(commit)
            }
        }
        commits.putAll(includedCommits.associateBy { it.sha })
        orphanedCommits = (orphanedCommits + includedCommits).filter { commit ->
            commit.parents.any { parentCommit ->
                !excludedCommits.contains(parentCommit.sha) &&
                        !commits.containsKey(parentCommit.sha)
            }
        }
    } while ((giteaResponse.hasMore ?: (giteaResponse.values.isNotEmpty())) && orphanedCommits.isNotEmpty())
    _log.debug("Pages retrieved: $page")
    if (!sinceCommitFound) {
        throw NotFoundException("Cannot find commit '$fromSha' in commit graph for commit '$toSha' in '$organization:$repository'")
    }
    return commits.map { it.value }
}

fun GiteaClient.getBranchesCommitGraph(organization: String, repository: String, files: Boolean = false)
        : Sequence {
    val parameters = mapOf("limit" to ENTITY_LIMIT, "stat" to false, "verification" to false, "files" to files)
    return GiteaCommitGraphSequence(getBranches(organization, repository)) { branch, page ->
        getCommits(organization, repository, parameters + mapOf("sha" to branch, "page" to page))
    }
}

class GiteaCommitGraphSequence(
    branches: Collection,
    pageRequest: (branchSha: String, page: Int) -> GiteaEntityList
) : Sequence {
    private val iteratorRef = AtomicReference(GiteaCommitGraphIterator(branches, pageRequest))

    override operator fun iterator() =
        iteratorRef.getAndSet(null) ?: throw IllegalStateException("This iterator can be consumed only once")

    class GiteaCommitGraphIterator(
        branches: Collection,
        private val pageRequest: (branchSha: String, page: Int) -> GiteaEntityList
    ) : Iterator {
        private val branchBuffer = LinkedList(branches)
        private val commitBuffer = LinkedList()
        private val visited = mutableSetOf()
        private val notVisitedParents = mutableSetOf()

        private var commitPage: Int = 0
        private var currentBranch = branchBuffer.poll()

        init {
            fetch()
        }

        override fun hasNext() = synchronized(this) { commitBuffer.isNotEmpty() }

        override fun next(): GiteaCommit = synchronized(this) {
            commitBuffer.pop().also { if (commitBuffer.isEmpty()) fetch() }
        }

        private fun fetch() {
            while (currentBranch != null && commitBuffer.isEmpty()) {
                val currentBranchSha = currentBranch!!.commit.id
                val giteaResponse = pageRequest(currentBranchSha, ++commitPage)
                giteaResponse.values.filter { commit -> commit.sha !in visited }.forEach { commit ->
                    commitBuffer.add(commit)
                    visited.add(commit.sha)
                    commit.parents.forEach { parent ->
                        notVisitedParents.add(parent.sha)
                    }
                }
                notVisitedParents.removeAll(visited)
                if (giteaResponse.hasMore == false || giteaResponse.values.isEmpty() || notVisitedParents.isEmpty()) {
                    _log.debug("Branch commits pages retrieved: ${currentBranch!!.name}:$commitPage")
                    currentBranch = branchBuffer.poll()
                    notVisitedParents.clear()
                    commitPage = 0
                }
            }
        }
    }
}

fun GiteaClient.getTags(
    organization: String,
    repository: String
): Collection = execute({ parameters: Map -> getTags(organization, repository, parameters) })

fun GiteaClient.getBranches(
    organization: String,
    repository: String
): Collection = execute({ parameters: Map ->
    getBranches(organization, repository, parameters) ?: GiteaEntityList(false, emptyList())
})

fun GiteaClient.createPullRequestWithDefaultReviewers(
    organization: String,
    repository: String,
    sourceBranch: String,
    targetBranch: String,
    title: String,
    description: String,
    assignee: String? = null
): GiteaPullRequest {
    val head = getBranch(organization, repository, sourceBranch).name
    val base = getBranch(organization, repository, targetBranch).name
    return createPullRequest(
        organization,
        repository,
        GiteaCreatePullRequest(title, description, head, base, assignee)
    )
}

@Suppress("unused")
fun GiteaClient.getPullRequests(
    organization: String,
    repository: String
) = execute({ parameters: Map ->
    getPullRequests(organization, repository, parameters)
})

@Suppress("unused")
fun GiteaClient.getPullRequestReviews(
    organization: String,
    repository: String,
    number: Long
) = execute({ parameters: Map ->
    getPullRequestReviews(organization, repository, number, parameters)
})

private fun  execute(
    function: (Map) -> GiteaEntityList,
    filter: (element: T) -> Boolean = { true }
): MutableList {
    var page = 1
    val entities = mutableListOf()
    val parameters = mutableMapOf()
    parameters["limit"] = ENTITY_LIMIT
    do {
        parameters["page"] = page
        val giteaResponse = function.invoke(parameters)
        val currentPartEntities = giteaResponse.values
        val inFilter: Boolean = with(currentPartEntities.all(filter)) {
            entities += if (this) {
                currentPartEntities
            } else {
                currentPartEntities.filter(filter)
            }
            this
        }
        page++
    } while ((giteaResponse.hasMore ?: (currentPartEntities.isNotEmpty())) && inFilter)
    _log.debug("Pages retrieved: $page")
    return entities
}

fun GiteaRepository.toGiteaEditRepoOption(): GiteaEditRepoOption {
    return GiteaEditRepoOption(
        allowMergeCommits = allowMergeCommits,
        allowRebase = allowRebase,
        allowRebaseExplicit = allowRebaseExplicit,
        allowRebaseUpdate = allowRebaseUpdate,
        allowSquashMerge = allowSquashMerge,
        archived = archived,
        defaultAllowMaintainerEdit = defaultAllowMaintainerEdit,
        defaultBranch = defaultBranch,
        defaultDeleteBranchAfterMerge = defaultDeleteBranchAfterMerge,
        defaultMergeStyle = defaultMergeStyle,
        description = description,
        externalTracker = externalTracker,
        externalWiki = externalWiki,
        hasIssues = hasIssues,
        hasProjects = hasProjects,
        hasPullRequests = hasPullRequests,
        hasWiki = hasWiki,
        ignoreWhitespaceConflicts = ignoreWhitespaceConflicts,
        internalTracker = internalTracker,
        mirrorInterval = mirror?.let { m ->
            if (m) {
                mirrorInterval
            } else {
                null
            }
        },
        name = name,
        private = private,
        template = template,
        website = website
    )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy