mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
refactor: add credential and Git packages (#1123)
- `internal/auth`: credentials in the OS keyring or a private file, OAuth login with token refresh, SSH HTTP-signature signers (key files with passphrase prompt, ssh-agent keys and certificates), and the Git credential helper protocol. - `internal/git`: cancellable Git subprocesses, repository and remote discovery (tracked remote via `branch.<name>.remote`), PR checkout by full ref, AGit push, branch cleanup. Nothing imports these packages yet. Reviewed-on: https://gitea.com/gitea/tea/pulls/1123 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
1 parent
2b218a5bae
commit
88b91faa54
28 files changed
+3044
-3
No files matched your search
@@ -0,0 +1,162 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package git contains subprocess boundaries, independent of command flags.
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Runner abstracts context-aware Git subprocess execution.
|
||||
type Runner interface {
|
||||
Output(context.Context, string, bool, ...string) (string, error)
|
||||
}
|
||||
|
||||
// CommandError carries Git's exit status without including command arguments or output.
|
||||
type CommandError struct{ ExitCode int }
|
||||
|
||||
func (*CommandError) Error() string {
|
||||
return "Git operation failed; check repository, remote, and credentials"
|
||||
}
|
||||
|
||||
// requireWorkTree fails unless run operates inside a Git working tree. action names the operation in the error.
|
||||
func requireWorkTree(run func(...string) (string, error), action string) error {
|
||||
inside, err := run("rev-parse", "--is-inside-work-tree")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inside != "true" {
|
||||
return errors.New(action + " requires a Git working tree")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// System executes installed Git with explicit stream and prompting policy.
|
||||
type System struct {
|
||||
// Env overrides the subprocess environment when supplied.
|
||||
Env []string
|
||||
In io.Reader
|
||||
ErrOut io.Writer
|
||||
}
|
||||
|
||||
func setEnv(env []string, key, value string) []string {
|
||||
prefix := key + "="
|
||||
result := make([]string, 0, len(env)+1)
|
||||
for _, entry := range env {
|
||||
if !strings.HasPrefix(entry, prefix) {
|
||||
result = append(result, entry)
|
||||
}
|
||||
}
|
||||
return append(result, prefix+value)
|
||||
}
|
||||
|
||||
func networkSubcommand(args []string) bool {
|
||||
for index := 0; index < len(args); index++ {
|
||||
switch {
|
||||
case args[index] == "-c" || args[index] == "-C":
|
||||
index++
|
||||
case strings.HasPrefix(args[index], "-"):
|
||||
default:
|
||||
switch args[index] {
|
||||
case "push", "fetch", "pull", "ls-remote", "clone":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// batchSSHEnv makes SSH fail instead of prompting. A per-operation core.sshCommand
|
||||
// is preserved, and a GIT_SSH program (such as plink) is left untouched.
|
||||
func batchSSHEnv(ctx context.Context, dir string, env, args []string) []string {
|
||||
sshCommand, sshProgram := "", ""
|
||||
for _, entry := range env {
|
||||
if value, ok := strings.CutPrefix(entry, "GIT_SSH_COMMAND="); ok {
|
||||
sshCommand = value
|
||||
} else if value, ok := strings.CutPrefix(entry, "GIT_SSH="); ok {
|
||||
sshProgram = value
|
||||
}
|
||||
}
|
||||
if sshCommand == "" {
|
||||
configArgs := []string{}
|
||||
for index := 0; index+1 < len(args) && args[index] == "-c"; index += 2 {
|
||||
configArgs = append(configArgs, args[index:index+2]...)
|
||||
}
|
||||
configArgs = append(configArgs, "config", "--get", "core.sshCommand")
|
||||
config := exec.CommandContext(ctx, "git", configArgs...)
|
||||
config.Dir = dir
|
||||
config.Env = env
|
||||
data, _ := config.Output()
|
||||
sshCommand = strings.TrimSpace(string(data))
|
||||
}
|
||||
if sshCommand == "" {
|
||||
if sshProgram != "" {
|
||||
return env
|
||||
}
|
||||
sshCommand = "ssh"
|
||||
}
|
||||
if plink(env, sshCommand) {
|
||||
return setEnv(env, "GIT_SSH_COMMAND", sshCommand+" -batch")
|
||||
}
|
||||
return setEnv(env, "GIT_SSH_COMMAND", sshCommand+" -oBatchMode=yes")
|
||||
}
|
||||
|
||||
// plink reports whether Git treats sshCommand as PuTTY's plink, which rejects OpenSSH options.
|
||||
// Like Git, it honors GIT_SSH_VARIANT and otherwise looks at the program's base name.
|
||||
func plink(env []string, sshCommand string) bool {
|
||||
variant := ""
|
||||
for _, entry := range env {
|
||||
if value, ok := strings.CutPrefix(entry, "GIT_SSH_VARIANT="); ok {
|
||||
variant = value
|
||||
}
|
||||
}
|
||||
if variant == "" {
|
||||
program, _, _ := strings.Cut(sshCommand, " ")
|
||||
if quote := sshCommand[0]; quote == '"' || quote == '\'' {
|
||||
program, _, _ = strings.Cut(sshCommand[1:], string(quote))
|
||||
}
|
||||
variant = strings.TrimSuffix(strings.ToLower(program[strings.LastIndexAny(program, `/\`)+1:]), ".exe")
|
||||
}
|
||||
return variant == "plink" || variant == "tortoiseplink"
|
||||
}
|
||||
|
||||
// Output runs Git, suppressing credential prompts when interaction is disabled.
|
||||
func (s System) Output(ctx context.Context, dir string, prompt bool, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "git", args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stderr = s.ErrOut
|
||||
env := append([]string(nil), s.Env...)
|
||||
if s.Env == nil {
|
||||
env = os.Environ()
|
||||
}
|
||||
if prompt {
|
||||
cmd.Stdin = s.In
|
||||
} else {
|
||||
env = setEnv(env, "GIT_TERMINAL_PROMPT", "0")
|
||||
env = setEnv(env, "GIT_ASKPASS", "")
|
||||
env = setEnv(env, "SSH_ASKPASS", "")
|
||||
env = setEnv(env, "SSH_ASKPASS_REQUIRE", "never")
|
||||
if networkSubcommand(args) {
|
||||
env = batchSSHEnv(ctx, dir, env, args)
|
||||
}
|
||||
}
|
||||
cmd.Env = env
|
||||
output, err := cmd.Output()
|
||||
if ctx.Err() != nil {
|
||||
return "", ctx.Err()
|
||||
}
|
||||
if err != nil {
|
||||
if failure, ok := errors.AsType[*exec.ExitError](err); ok {
|
||||
return "", &CommandError{ExitCode: failure.ExitCode()}
|
||||
}
|
||||
return "", errors.New("Git operation failed; check repository, remote, and credentials")
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
Reference in new issue
Block a user