drone/app/gitspace/scm/scm.go

196 lines
5.9 KiB
Go

// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scm
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"github.com/harness/gitness/git/command"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/tidwall/jsonc"
)
var (
ErrNoDefaultBranch = errors.New("no default branch")
)
const devcontainerDefaultPath = ".devcontainer/devcontainer.json"
type SCM struct {
scmProviderFactory Factory
}
func NewSCM(factory Factory) *SCM {
return &SCM{scmProviderFactory: factory}
}
// CheckValidCodeRepo checks if the current URL is a valid and accessible code repo,
// input can be connector info, user token etc.
func (s *SCM) CheckValidCodeRepo(
ctx context.Context,
codeRepositoryRequest CodeRepositoryRequest,
) (*CodeRepositoryResponse, error) {
codeRepositoryResponse := &CodeRepositoryResponse{
URL: codeRepositoryRequest.URL,
CodeRepoIsPrivate: true,
}
branch, err := s.detectBranch(ctx, codeRepositoryRequest.URL)
if err == nil {
codeRepositoryResponse.Branch = branch
codeRepositoryResponse.CodeRepoIsPrivate = false
return codeRepositoryResponse, nil
}
scmProvider, err := s.getSCMProvider(codeRepositoryRequest.RepoType)
if err != nil {
return nil, fmt.Errorf("failed to resolve SCM provider: %w", err)
}
resolvedCreds, err := s.resolveRepoCredentials(ctx, scmProvider, codeRepositoryRequest)
if err != nil {
return nil, fmt.Errorf("failed to resolve repo credentials and URL: %w", err)
}
if branch, err = s.detectBranch(ctx, resolvedCreds.CloneURL.Value()); err == nil {
codeRepositoryResponse.Branch = branch
}
return codeRepositoryResponse, nil
}
// GetSCMRepoDetails fetches repository name, credentials & devcontainer config file from the given repo and branch.
func (s *SCM) GetSCMRepoDetails(
ctx context.Context,
gitspaceConfig types.GitspaceConfig,
) (*ResolvedDetails, error) {
scmProvider, err := s.getSCMProvider(gitspaceConfig.CodeRepo.Type)
if err != nil {
return nil, fmt.Errorf("failed to resolve SCM provider: %w", err)
}
resolvedCredentials, err := scmProvider.ResolveCredentials(ctx, gitspaceConfig)
if err != nil {
return nil, fmt.Errorf("failed to resolve repo credentials and url: %w", err)
}
devcontainerConfig, err := s.getDevcontainerConfig(ctx, scmProvider, gitspaceConfig, resolvedCredentials)
if err != nil {
return nil, fmt.Errorf("failed to read or parse devcontainer config: %w", err)
}
var resolvedDetails = &ResolvedDetails{
ResolvedCredentials: *resolvedCredentials,
DevcontainerConfig: devcontainerConfig,
}
return resolvedDetails, nil
}
func detectDefaultGitBranch(ctx context.Context, gitRepoDir string) (string, error) {
cmd := command.New("ls-remote",
command.WithFlag("--symref"),
command.WithFlag("-q"),
command.WithArg(gitRepoDir),
command.WithArg("HEAD"),
)
output := &bytes.Buffer{}
if err := cmd.Run(ctx, command.WithStdout(output)); err != nil {
return "", fmt.Errorf("failed to ls remote repo")
}
var lsRemoteHeadRegexp = regexp.MustCompile(`ref: refs/heads/([^\s]+)\s+HEAD`)
match := lsRemoteHeadRegexp.FindStringSubmatch(strings.TrimSpace(output.String()))
if match == nil {
return "", ErrNoDefaultBranch
}
return match[1], nil
}
func (s *SCM) GetBranchURL(
spacePath string,
repoType enum.GitspaceCodeRepoType,
repoURL string,
branch string,
) (string, error) {
scmProvider, err := s.scmProviderFactory.GetSCMProvider(repoType)
if err != nil {
return "", fmt.Errorf("failed to resolve scm provider while generating branch url: %w", err)
}
return scmProvider.GetBranchURL(spacePath, repoURL, branch)
}
// detectBranch tries to detect the default Git branch for a given URL.
func (s *SCM) detectBranch(ctx context.Context, repoURL string) (string, error) {
defaultBranch, err := detectDefaultGitBranch(ctx, repoURL)
if err != nil {
return "", err
}
if defaultBranch == "" {
return "main", nil
}
return defaultBranch, nil
}
func (s *SCM) getSCMProvider(repoType enum.GitspaceCodeRepoType) (Provider, error) {
if repoType == "" {
repoType = enum.CodeRepoTypeUnknown
}
return s.scmProviderFactory.GetSCMProvider(repoType)
}
func (s *SCM) resolveRepoCredentials(
ctx context.Context,
scmProvider Provider,
codeRepositoryRequest CodeRepositoryRequest,
) (*ResolvedCredentials, error) {
codeRepo := types.CodeRepo{URL: codeRepositoryRequest.URL}
gitspaceUser := types.GitspaceUser{Identifier: codeRepositoryRequest.UserIdentifier}
gitspaceConfig := types.GitspaceConfig{
CodeRepo: codeRepo,
SpacePath: codeRepositoryRequest.SpacePath,
GitspaceUser: gitspaceUser,
}
return scmProvider.ResolveCredentials(ctx, gitspaceConfig)
}
func (s *SCM) getDevcontainerConfig(
ctx context.Context,
scmProvider Provider,
gitspaceConfig types.GitspaceConfig,
resolvedCredentials *ResolvedCredentials,
) (types.DevcontainerConfig, error) {
config := types.DevcontainerConfig{}
filePath := devcontainerDefaultPath
catFileOutputBytes, err := scmProvider.GetFileContent(ctx, gitspaceConfig, filePath, resolvedCredentials)
if err != nil {
return config, fmt.Errorf("failed to read devcontainer file: %w", err)
}
if len(catFileOutputBytes) == 0 {
return config, nil // Return an empty config if the file is empty
}
if err = json.Unmarshal(jsonc.ToJSON(catFileOutputBytes), &config); err != nil {
return config, fmt.Errorf("failed to parse devcontainer JSON: %w", err)
}
return config, nil
}