mirror of https://github.com/harness/drone.git
286 lines
7.9 KiB
Go
286 lines
7.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 gitea
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/gitrpc/internal/types"
|
|
|
|
gitea "code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/util"
|
|
)
|
|
|
|
var lsRemoteHeadRegexp = regexp.MustCompile(`ref: refs/heads/([^\s]+)\s+HEAD`)
|
|
|
|
// InitRepository initializes a new Git repository.
|
|
func (g Adapter) InitRepository(ctx context.Context, repoPath string, bare bool) error {
|
|
return gitea.InitRepository(ctx, repoPath, bare)
|
|
}
|
|
|
|
// SetDefaultBranch sets the default branch of a repo.
|
|
func (g Adapter) SetDefaultBranch(ctx context.Context, repoPath string,
|
|
defaultBranch string, allowEmpty bool) error {
|
|
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
|
|
if err != nil {
|
|
return processGiteaErrorf(err, "failed to open repository")
|
|
}
|
|
defer giteaRepo.Close()
|
|
|
|
// if requested, error out if branch doesn't exist. Otherwise, blindly set it.
|
|
if !allowEmpty && !giteaRepo.IsBranchExist(defaultBranch) {
|
|
// TODO: ensure this returns not found error to caller
|
|
return fmt.Errorf("branch '%s' does not exist", defaultBranch)
|
|
}
|
|
|
|
// change default branch
|
|
err = giteaRepo.SetDefaultBranch(defaultBranch)
|
|
if err != nil {
|
|
return processGiteaErrorf(err, "failed to set new default branch")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetDefaultBranch gets the default branch of a repo.
|
|
func (g Adapter) GetDefaultBranch(ctx context.Context, repoPath string) (string, error) {
|
|
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
|
|
if err != nil {
|
|
return "", processGiteaErrorf(err, "failed to open gitea repo")
|
|
}
|
|
defer giteaRepo.Close()
|
|
|
|
// get default branch
|
|
branch, err := giteaRepo.GetDefaultBranch()
|
|
if err != nil {
|
|
return "", processGiteaErrorf(err, "failed to get default branch")
|
|
}
|
|
|
|
return branch, nil
|
|
}
|
|
|
|
// GetRemoteDefaultBranch retrieves the default branch of a remote repository.
|
|
// If the repo doesn't have a default branch, types.ErrNoDefaultBranch is returned.
|
|
func (g Adapter) GetRemoteDefaultBranch(ctx context.Context, remoteURL string) (string, error) {
|
|
args := []string{
|
|
"-c", "credential.helper=",
|
|
"ls-remote",
|
|
"--symref",
|
|
"-q",
|
|
remoteURL,
|
|
"HEAD",
|
|
}
|
|
|
|
cmd := gitea.NewCommand(ctx, args...)
|
|
stdOut, _, err := cmd.RunStdString(nil)
|
|
if err != nil {
|
|
return "", processGiteaErrorf(err, "failed to ls remote repo")
|
|
}
|
|
|
|
// git output looks as follows, and we are looking for the ref that HEAD points to
|
|
// ref: refs/heads/main HEAD
|
|
// 46963bc7f0b5e8c5f039d50ac9e6e51933c78cdf HEAD
|
|
match := lsRemoteHeadRegexp.FindStringSubmatch(stdOut)
|
|
if match == nil {
|
|
return "", types.ErrNoDefaultBranch
|
|
}
|
|
|
|
return match[1], nil
|
|
}
|
|
|
|
func (g Adapter) Clone(ctx context.Context, from, to string, opts types.CloneRepoOptions) error {
|
|
err := gitea.Clone(ctx, from, to, gitea.CloneRepoOptions{
|
|
Timeout: opts.Timeout,
|
|
Mirror: opts.Mirror,
|
|
Bare: opts.Bare,
|
|
Quiet: opts.Quiet,
|
|
Branch: opts.Branch,
|
|
Shared: opts.Shared,
|
|
NoCheckout: opts.NoCheckout,
|
|
Depth: opts.Depth,
|
|
Filter: opts.Filter,
|
|
SkipTLSVerify: opts.SkipTLSVerify,
|
|
})
|
|
if err != nil {
|
|
return processGiteaErrorf(err, "failed to clone repo")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Sync synchronizes the repository to match the provided source.
|
|
// NOTE: This is a read operation and doesn't trigger any server side hooks.
|
|
func (g Adapter) Sync(ctx context.Context, repoPath string, remoteURL string) error {
|
|
args := []string{
|
|
"-c", "advice.fetchShowForcedUpdates=false",
|
|
"-c", "credential.helper=",
|
|
"fetch",
|
|
"--quiet",
|
|
"--prune",
|
|
"--atomic",
|
|
"--force",
|
|
"--no-write-fetch-head",
|
|
"--no-show-forced-updates",
|
|
remoteURL,
|
|
"+refs/*:refs/*",
|
|
}
|
|
|
|
cmd := gitea.NewCommand(ctx, args...)
|
|
_, _, err := cmd.RunStdString(&gitea.RunOpts{
|
|
Dir: repoPath,
|
|
UseContextTimeout: true,
|
|
})
|
|
if err != nil {
|
|
return processGiteaErrorf(err, "failed to sync repo")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g Adapter) AddFiles(repoPath string, all bool, files ...string) error {
|
|
err := gitea.AddChanges(repoPath, all, files...)
|
|
if err != nil {
|
|
return processGiteaErrorf(err, "failed to add changes")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Commit commits the changes of the repository.
|
|
// NOTE: Modification of gitea implementation that supports commiter_date + author_date.
|
|
func (g Adapter) Commit(ctx context.Context, repoPath string, opts types.CommitChangesOptions) error {
|
|
// setup environment variables used by git-commit
|
|
// See https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables
|
|
env := []string{
|
|
"GIT_AUTHOR_NAME=" + opts.Author.Identity.Name,
|
|
"GIT_AUTHOR_EMAIL=" + opts.Author.Identity.Email,
|
|
"GIT_AUTHOR_DATE=" + opts.Author.When.Format(time.RFC3339),
|
|
"GIT_COMMITTER_NAME=" + opts.Committer.Identity.Name,
|
|
"GIT_COMMITTER_EMAIL=" + opts.Committer.Identity.Email,
|
|
"GIT_COMMITTER_DATE=" + opts.Committer.When.Format(time.RFC3339),
|
|
}
|
|
|
|
args := []string{
|
|
"commit",
|
|
"-m",
|
|
opts.Message,
|
|
}
|
|
|
|
_, _, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath, Env: env})
|
|
// No stderr but exit status 1 means nothing to commit (see gitea CommitChanges)
|
|
if err != nil && err.Error() != "exit status 1" {
|
|
return processGiteaErrorf(err, "failed to commit changes")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g Adapter) Push(ctx context.Context, repoPath string, opts types.PushOptions) error {
|
|
err := Push(ctx, repoPath, opts)
|
|
if err != nil {
|
|
return processGiteaErrorf(err, "failed to push changes")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Push pushs local commits to given remote branch.
|
|
// NOTE: Modification of gitea implementation that supports --force-with-lease.
|
|
// TODOD: return our own error types and move to above adapter.Push method
|
|
func Push(ctx context.Context, repoPath string, opts types.PushOptions) error {
|
|
cmd := gitea.NewCommand(ctx,
|
|
"-c", "credential.helper=",
|
|
"push",
|
|
)
|
|
if opts.Force {
|
|
cmd.AddArguments("-f")
|
|
}
|
|
if opts.ForceWithLease != "" {
|
|
cmd.AddArguments(fmt.Sprintf("--force-with-lease=%s", opts.ForceWithLease))
|
|
}
|
|
if opts.Mirror {
|
|
cmd.AddArguments("--mirror")
|
|
}
|
|
cmd.AddArguments("--", opts.Remote)
|
|
|
|
if len(opts.Branch) > 0 {
|
|
cmd.AddArguments(opts.Branch)
|
|
}
|
|
|
|
// remove credentials if there are any
|
|
logRemote := opts.Remote
|
|
if strings.Contains(logRemote, "://") && strings.Contains(logRemote, "@") {
|
|
logRemote = util.SanitizeCredentialURLs(logRemote)
|
|
}
|
|
cmd.SetDescription(
|
|
fmt.Sprintf(
|
|
"pushing %s to %s (Force: %t, ForceWithLease: %s)",
|
|
opts.Branch,
|
|
logRemote,
|
|
opts.Force,
|
|
opts.ForceWithLease,
|
|
),
|
|
)
|
|
var outbuf, errbuf strings.Builder
|
|
|
|
if opts.Timeout == 0 {
|
|
opts.Timeout = -1
|
|
}
|
|
|
|
err := cmd.Run(&gitea.RunOpts{
|
|
Env: opts.Env,
|
|
Timeout: opts.Timeout,
|
|
Dir: repoPath,
|
|
Stdout: &outbuf,
|
|
Stderr: &errbuf,
|
|
})
|
|
if err != nil {
|
|
switch {
|
|
case strings.Contains(errbuf.String(), "non-fast-forward"):
|
|
return &gitea.ErrPushOutOfDate{
|
|
StdOut: outbuf.String(),
|
|
StdErr: errbuf.String(),
|
|
Err: err,
|
|
}
|
|
case strings.Contains(errbuf.String(), "! [remote rejected]"):
|
|
err := &gitea.ErrPushRejected{
|
|
StdOut: outbuf.String(),
|
|
StdErr: errbuf.String(),
|
|
Err: err,
|
|
}
|
|
err.GenerateMessage()
|
|
return err
|
|
case strings.Contains(errbuf.String(), "matches more than one"):
|
|
err := &gitea.ErrMoreThanOne{
|
|
StdOut: outbuf.String(),
|
|
StdErr: errbuf.String(),
|
|
Err: err,
|
|
}
|
|
return err
|
|
default:
|
|
// fall through to normal error handling
|
|
}
|
|
}
|
|
|
|
if errbuf.Len() > 0 && err != nil {
|
|
return fmt.Errorf("%w - %s", err, errbuf.String())
|
|
}
|
|
|
|
return err
|
|
}
|