drone/app/gitspace/scm/scm.go
Dhruv Dhruv 6dbe85fbfd fix: [CDE-413]: Setting branch URL in all gitspace API response (#2881)
* 

fix: [CDE-413]: Adding spacePath to method.
* 

fix: [CDE-413]: Adding method to get branch url in the scm interface and using it to set the branch url in the gitspace config api responses. Removing the SCM interface as it has only 1 impl. Minor refactoring.
* 

fix: [CDE-413]: Adding method to get branch url in the scm interface and using it to set the branch url in the gitspace config api responses. Removing the SCM interface as it has only 1 impl. Minor refactoring.
2024-10-29 04:46:03 +00:00

177 lines
5.5 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"
)
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,
}
defaultBranch, err := detectDefaultGitBranch(ctx, codeRepositoryRequest.URL)
if err == nil {
branch := "main"
if defaultBranch != "" {
branch = defaultBranch
}
return &CodeRepositoryResponse{
URL: codeRepositoryRequest.URL,
Branch: branch,
CodeRepoIsPrivate: false,
}, nil
}
repoType := enum.CodeRepoTypeUnknown
if codeRepositoryRequest.RepoType != "" {
repoType = codeRepositoryRequest.RepoType
}
scmProvider, err := s.scmProviderFactory.GetSCMProvider(repoType)
if err != nil {
return nil, fmt.Errorf("failed to resolve scm provider: %w", err)
}
codeRepo := types.CodeRepo{URL: codeRepositoryRequest.URL}
gitspaceUser := types.GitspaceUser{Identifier: codeRepositoryRequest.UserIdentifier}
gitspaceConfig := types.GitspaceConfig{
CodeRepo: codeRepo,
SpacePath: codeRepositoryRequest.SpacePath,
GitspaceUser: gitspaceUser,
}
resolvedCreds, err := scmProvider.ResolveCredentials(ctx, gitspaceConfig)
if err != nil {
return nil, fmt.Errorf("failed to resolve repo credentials and url: %w", err)
}
defaultBranch, err = detectDefaultGitBranch(ctx, resolvedCreds.CloneURL)
if err == nil {
branch := "main"
if defaultBranch != "" {
branch = defaultBranch
}
codeRepositoryResponse = &CodeRepositoryResponse{
URL: codeRepositoryRequest.URL,
Branch: branch,
CodeRepoIsPrivate: true,
}
}
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) {
filePath := devcontainerDefaultPath
if gitspaceConfig.CodeRepo.Type == "" {
gitspaceConfig.CodeRepo.Type = enum.CodeRepoTypeUnknown
}
scmProvider, err := s.scmProviderFactory.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)
}
var resolvedDetails = &ResolvedDetails{
ResolvedCredentials: resolvedCredentials,
}
catFileOutputBytes, err := scmProvider.GetFileContent(ctx, gitspaceConfig, filePath, resolvedCredentials)
if err != nil {
return nil, fmt.Errorf("failed to read devcontainer file : %w", err)
}
if len(catFileOutputBytes) == 0 {
resolvedDetails.DevcontainerConfig = &types.DevcontainerConfig{}
} else {
sanitizedJSON := removeComments(catFileOutputBytes)
var config *types.DevcontainerConfig
if err = json.Unmarshal(sanitizedJSON, &config); err != nil {
return nil, fmt.Errorf("failed to parse devcontainer json: %w", err)
}
resolvedDetails.DevcontainerConfig = config
}
return resolvedDetails, nil
}
func removeComments(input []byte) []byte {
blockCommentRegex := regexp.MustCompile(`(?s)/\*.*?\*/`)
input = blockCommentRegex.ReplaceAll(input, nil)
lineCommentRegex := regexp.MustCompile(`//.*`)
return lineCommentRegex.ReplaceAll(input, 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)
}