1
1
mirror of https://github.com/cooperspencer/gickup synced 2026-05-04 09:40:36 +02:00
Files
gickup/sourcehut/sourcehut.go
Andreas Wachter e6999418e2 Fix linting (#372)
2026-04-05 21:39:20 +02:00

401 lines
9.5 KiB
Go

package sourcehut
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/cooperspencer/gickup/logger"
"github.com/cooperspencer/gickup/types"
graphqlclient "github.com/hasura/go-graphql-client"
"github.com/rs/zerolog"
)
var sub zerolog.Logger
const defaultSourcehutURL = "https://git.sr.ht"
func normalizeBearerToken(token string) string {
token = strings.TrimSpace(token)
if token == "" {
return token
}
lower := strings.ToLower(token)
if strings.HasPrefix(lower, "bearer ") {
return strings.TrimSpace(token[len("bearer "):])
}
if strings.HasPrefix(lower, "token ") {
return strings.TrimSpace(token[len("token "):])
}
return token
}
func normalizeURL(rawURL string) string {
if strings.TrimSpace(rawURL) == "" {
return defaultSourcehutURL
}
return strings.TrimRight(strings.TrimSpace(rawURL), "/")
}
func graphQLEndpoint(rawURL string) string {
return fmt.Sprintf("%s/query", normalizeURL(rawURL))
}
func newGraphQLClient(endpoint, token string) *graphqlclient.Client {
token = normalizeBearerToken(token)
client := graphqlclient.NewClient(endpoint, &http.Client{})
if token != "" {
client = client.WithRequestModifier(func(r *http.Request) {
r.Header.Set("Authorization", "Bearer "+token)
})
}
return client
}
func execGraphQL(endpoint, token, query string, variables map[string]interface{}, dataTarget interface{}) error {
client := newGraphQLClient(endpoint, token)
raw, err := client.ExecRaw(context.Background(), query, variables)
if err != nil {
return err
}
if dataTarget == nil {
return nil
}
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
return errors.New("sourcehut graphql returned no data")
}
return json.Unmarshal(raw, dataTarget)
}
func resolveSourcehutUsername(endpoint, token, configuredUser string) (string, error) {
if configuredUser != "" {
return strings.TrimPrefix(configuredUser, "~"), nil
}
query := `query { me { username } }`
response := queryMe{}
if err := execGraphQL(endpoint, token, query, nil, &response); err != nil {
return "", err
}
if response.Me.Username == "" {
return "", errors.New("no user associated with this token")
}
return response.Me.Username, nil
}
func getRepositoriesForUser(endpoint, token, username string) ([]repository, error) {
query := `query($username: String!, $cursor: Cursor) {
user(username: $username) {
repositories(cursor: $cursor) {
results {
id
created
updated
name
description
visibility
owner {
canonicalName
}
}
cursor
}
}
}`
allRepos := []repository{}
var cursor *string
for {
variables := map[string]interface{}{
"username": strings.TrimPrefix(username, "~"),
"cursor": cursor,
}
response := queryUser{}
if err := execGraphQL(endpoint, token, query, variables, &response); err != nil {
return nil, err
}
if response.User == nil {
return nil, fmt.Errorf("couldn't find sourcehut user %s", username)
}
allRepos = append(allRepos, response.User.Repositories.Results...)
if response.User.Repositories.Cursor == nil {
break
}
cursor = response.User.Repositories.Cursor
}
return allRepos, nil
}
func getRepositoryByName(endpoint, token, username, repoName string) (*repository, error) {
if username != "" {
query := `query($username: String!, $name: String!) {
user(username: $username) {
repository(name: $name) {
id
name
owner {
canonicalName
}
}
}
}`
response := queryUser{}
variables := map[string]interface{}{
"username": strings.TrimPrefix(username, "~"),
"name": repoName,
}
if err := execGraphQL(endpoint, token, query, variables, &response); err != nil {
return nil, err
}
if response.User == nil {
return nil, fmt.Errorf("couldn't find sourcehut user %s", username)
}
return response.User.Repository, nil
}
query := `query($name: String!) {
me {
repository(name: $name) {
id
name
owner {
canonicalName
}
}
}
}`
response := queryMe{}
variables := map[string]interface{}{"name": repoName}
if err := execGraphQL(endpoint, token, query, variables, &response); err != nil {
return nil, err
}
return response.Me.Repository, nil
}
func createRepository(endpoint, token string, repo types.Repo, visibility string) (*repository, error) {
query := `mutation($name: String!, $visibility: Visibility!, $description: String) {
createRepository(name: $name, visibility: $visibility, description: $description) {
id
name
owner {
canonicalName
}
}
}`
response := mutationCreateRepository{}
variables := map[string]interface{}{
"name": repo.Name,
"visibility": visibility,
"description": repo.Description,
}
if err := execGraphQL(endpoint, token, query, variables, &response); err != nil {
return nil, err
}
if response.CreateRepository == nil {
return nil, errors.New("sourcehut did not return a created repository")
}
return response.CreateRepository, nil
}
func mapVisibilityToGraphQLEnum(visibility string) string {
switch strings.ToLower(strings.TrimSpace(visibility)) {
case "private":
return "PRIVATE"
case "unlisted":
return "UNLISTED"
default:
return "PUBLIC"
}
}
func buildHTTPURL(baseURL, canonicalOwner, repoName string) string {
return fmt.Sprintf("%s/%s/%s", normalizeURL(baseURL), canonicalOwner, repoName)
}
func buildSSHURL(baseURL, canonicalOwner, repoName string) string {
return fmt.Sprintf("git@%s:%s/%s", types.GetHost(normalizeURL(baseURL)), canonicalOwner, repoName)
}
// Get TODO.
func Get(conf *types.Conf) ([]types.Repo, bool) {
ran := false
repos := []types.Repo{}
for _, repo := range conf.Source.Sourcehut {
repo.URL = normalizeURL(repo.URL)
sub = logger.CreateSubLogger("stage", "sourcehut", "url", repo.URL)
err := repo.Filter.ParseDuration()
if err != nil {
sub.Warn().
Msg(err.Error())
}
ran = true
endpoint := graphQLEndpoint(repo.URL)
token := repo.GetToken()
repo.User, err = resolveSourcehutUsername(endpoint, token, repo.User)
if err != nil {
sub.Error().
Msg(err.Error())
continue
}
sub.Info().
Msgf("grabbing repositories from %s", repo.User)
include := types.GetMap(repo.Include)
exclude := types.GetMap(repo.Exclude)
repositories, err := getRepositoriesForUser(endpoint, token, repo.User)
if err != nil {
sub.Error().
Msg(err.Error())
continue
}
if len(repositories) == 0 {
sub.Error().Msgf("couldn't find any repositories for user %s", repo.User)
continue
}
for _, r := range repositories {
ownerCanonicalName := r.Owner.CanonicalName
if ownerCanonicalName == "" {
ownerCanonicalName = fmt.Sprintf("~%s", strings.TrimPrefix(repo.User, "~"))
}
repoURL := buildHTTPURL(repo.URL, ownerCanonicalName, r.Name)
sshURL := buildSSHURL(repo.URL, ownerCanonicalName, r.Name)
sub.Debug().Msg(repoURL)
if repo.Filter.LastActivityDuration != 0 {
if !r.Updated.IsZero() && time.Since(r.Updated) > repo.Filter.LastActivityDuration {
continue
}
}
ownerName := strings.TrimPrefix(ownerCanonicalName, "~")
isPrivate := strings.EqualFold(r.Visibility, "PRIVATE") || strings.EqualFold(r.Visibility, "private")
if include[r.Name] {
repos = append(repos, types.Repo{
Name: r.Name,
URL: repoURL,
SSHURL: sshURL,
Token: token,
Origin: repo,
Owner: ownerName,
Hoster: types.GetHost(repo.URL),
Description: r.Description,
Private: isPrivate,
})
if repo.Wiki {
repos = append(repos, types.Repo{
Name: r.Name + "-docs",
URL: repoURL + "-docs",
SSHURL: sshURL + "-docs",
Token: token,
Origin: repo,
Owner: ownerName,
Hoster: types.GetHost(repo.URL),
Description: r.Description,
Private: isPrivate,
})
}
continue
}
if exclude[r.Name] {
continue
}
if len(include) == 0 {
repos = append(repos, types.Repo{
Name: r.Name,
URL: repoURL,
SSHURL: sshURL,
Token: token,
Origin: repo,
Owner: ownerName,
Hoster: types.GetHost(repo.URL),
Description: r.Description,
Private: isPrivate,
})
if repo.Wiki {
repos = append(repos, types.Repo{
Name: r.Name + "-docs",
URL: repoURL + "-docs",
SSHURL: sshURL + "-docs",
Token: token,
Origin: repo,
Owner: ownerName,
Hoster: types.GetHost(repo.URL),
Description: r.Description,
Private: isPrivate,
})
}
}
}
}
return repos, ran
}
func GetOrCreate(destination types.GenRepo, repo types.Repo) (string, error) {
destination.URL = normalizeURL(destination.URL)
sub = logger.CreateSubLogger("stage", "sourcehut", "url", destination.URL)
token := destination.GetToken()
endpoint := graphQLEndpoint(destination.URL)
configuredUser := strings.TrimPrefix(destination.User, "~")
remoteRepo, err := getRepositoryByName(endpoint, token, configuredUser, repo.Name)
if err != nil {
return "", err
}
if remoteRepo == nil {
remoteRepo, err = createRepository(endpoint, token, repo, mapVisibilityToGraphQLEnum(destination.Visibility.Repositories))
if err != nil {
return "", err
}
}
if remoteRepo == nil || remoteRepo.Owner.CanonicalName == "" {
return "", errors.New("sourcehut repository owner could not be determined")
}
return buildSSHURL(destination.URL, remoteRepo.Owner.CanonicalName, repo.Name), nil
}