Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

245 lines
7.0 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bytes"
"context"
"fmt"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/hanzoai/git/modules/git/gitcmd"
"github.com/hanzoai/git/modules/proxy"
)
const prettyLogFormat = `--pretty=format:%H`
func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) {
// avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git <command> [<revision>...] -- [<file>...]'
logs, _, err := gitcmd.NewCommand("log").AddArguments(prettyLogFormat).
AddDynamicArguments(revisionRange).AddArguments("--").WithDir(repo.Path).
RunStdBytes(ctx)
if err != nil {
return nil, err
}
return repo.parsePrettyFormatLogToList(logs)
}
func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) {
var commits []*Commit
if len(logs) == 0 {
return commits, nil
}
parts := bytes.SplitSeq(logs, []byte{'\n'})
for commitID := range parts {
commit, err := repo.GetCommit(string(commitID))
if err != nil {
return nil, err
}
commits = append(commits, commit)
}
return commits, nil
}
// IsRepoURLAccessible checks if given repository URL is accessible.
func IsRepoURLAccessible(ctx context.Context, url string) bool {
_, _, err := gitcmd.NewCommand("ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(ctx)
return err == nil
}
// InitRepository initializes a new Git repository.
func InitRepository(ctx context.Context, repoPath string, bare bool, objectFormatName string) error {
err := os.MkdirAll(repoPath, os.ModePerm)
if err != nil {
return err
}
cmd := gitcmd.NewCommand("init")
if !IsValidObjectFormat(objectFormatName) {
return fmt.Errorf("invalid object format: %s", objectFormatName)
}
if DefaultFeatures().SupportHashSha256 {
cmd.AddOptionValues("--object-format", objectFormatName)
}
if bare {
cmd.AddArguments("--bare")
}
_, _, err = cmd.WithDir(repoPath).RunStdString(ctx)
return err
}
// IsEmpty Check if repository is empty.
func (repo *Repository) IsEmpty() (bool, error) {
stdout, _, err := gitcmd.NewCommand().
AddOptionFormat("--git-dir=%s", repo.Path).
AddArguments("rev-list", "-n", "1", "--all").
WithDir(repo.Path).
RunStdString(repo.Ctx)
if err != nil {
if (gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "") || gitcmd.IsErrorExitCode(err, 129) {
// git 2.11 exits with 129 if the repo is empty
return true, nil
}
return true, fmt.Errorf("check empty: %w", err)
}
return strings.TrimSpace(stdout) == "", nil
}
// CloneRepoOptions options when clone a repository
type CloneRepoOptions struct {
Timeout time.Duration
Mirror bool
Bare bool
Quiet bool
Branch string
Shared bool
NoCheckout bool
Depth int
Filter string
SkipTLSVerify bool
SingleBranch bool
Env []string
}
// Clone clones original repository to target path.
func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
toDir := path.Dir(to)
if err := os.MkdirAll(toDir, os.ModePerm); err != nil {
return err
}
cmd := gitcmd.NewCommand().AddArguments("clone")
// Never follow HTTP redirects: no clone caller needs them, and a remote redirecting to an
// otherwise-blocked address would be an SSRF vector (e.g. migrating from an attacker URL).
cmd.AddArguments("-c", "http.followRedirects=false")
if opts.SkipTLSVerify {
cmd.AddArguments("-c", "http.sslVerify=false")
}
if opts.Mirror {
cmd.AddArguments("--mirror")
}
if opts.Bare {
cmd.AddArguments("--bare")
}
if opts.Quiet {
cmd.AddArguments("--quiet")
}
if opts.Shared {
cmd.AddArguments("-s")
}
if opts.NoCheckout {
cmd.AddArguments("--no-checkout")
}
if opts.Depth > 0 {
cmd.AddArguments("--depth").AddDynamicArguments(strconv.Itoa(opts.Depth))
}
if opts.Filter != "" {
cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter)
}
if opts.SingleBranch {
cmd.AddArguments("--single-branch")
}
if len(opts.Branch) > 0 {
cmd.AddArguments("-b").AddDynamicArguments(opts.Branch)
}
cmd.AddDashesAndList(from, to)
if opts.Timeout <= 0 {
opts.Timeout = -1
}
envs := os.Environ()
if opts.Env != nil {
envs = opts.Env
} else {
u, err := url.Parse(from)
if err == nil {
envs = proxy.EnvWithProxy(u)
}
}
return cmd.
WithTimeout(opts.Timeout).
WithEnv(envs).
RunWithStderr(ctx)
}
// PushOptions options when push to remote
type PushOptions struct {
Remote string
LocalRefName string
Branch string
// Refspecs pushes an explicit set of refs, e.g. refs/heads/*:refs/heads/*.
// Unlike Mirror it never deletes a ref the remote has and we do not, so it
// carries work outward without granting authority over the remote's history.
Refspecs []string
Force bool
ForceWithLease string
Mirror bool
Env []string
Timeout time.Duration
}
// Push pushs local commits to given remote branch.
func Push(ctx context.Context, repoPath string, opts PushOptions) error {
cmd := gitcmd.NewCommand("push")
if opts.ForceWithLease != "" {
cmd.AddOptionFormat("--force-with-lease=%s", opts.ForceWithLease)
} else if opts.Force {
cmd.AddArguments("-f")
}
if opts.Mirror {
cmd.AddArguments("--mirror")
}
// A push-mirror remote is created with --mirror=push, which persists
// remote.<name>.mirror=true in the repo config. Git then applies mirror mode
// from config alone and refuses any refspec with "--mirror can't be combined
// with refspecs", so dropping the CLI flag is not enough to push additively.
// Override the config for this invocation only — the stored remote is left
// exactly as the mirror machinery wrote it. remote.<name>.push carries a
// leading + (force) for the same historical reason, so the explicit refspecs
// below replace it rather than inherit it.
if len(opts.Refspecs) > 0 && opts.Remote != "" && !opts.Mirror {
cmd.AddConfig("remote."+opts.Remote+".mirror", "false")
}
remoteBranchArgs := []string{opts.Remote}
if len(opts.Branch) > 0 {
var refspec string
if opts.LocalRefName != "" {
refspec = fmt.Sprintf("%s:%s", opts.LocalRefName, opts.Branch)
} else {
refspec = opts.Branch
}
remoteBranchArgs = append(remoteBranchArgs, refspec)
}
remoteBranchArgs = append(remoteBranchArgs, opts.Refspecs...)
cmd.AddDashesAndList(remoteBranchArgs...)
stdout, stderr, err := cmd.WithEnv(opts.Env).WithTimeout(opts.Timeout).WithDir(repoPath).RunStdString(ctx)
if err != nil {
if strings.Contains(stderr, "non-fast-forward") {
return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err}
} else if strings.Contains(stderr, "! [remote rejected]") || strings.Contains(stderr, "! [rejected]") {
err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err}
err.GenerateMessage()
return err
} else if strings.Contains(stderr, "matches more than one") {
return &ErrMoreThanOne{StdOut: stdout, StdErr: stderr, Err: err}
}
return fmt.Errorf("push failed: %w - %s\n%s", err, stderr, stdout)
}
return nil
}