You've already forked agentic-coding-workflow
git 逻辑
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -44,11 +45,13 @@ func credentialEnv(tmpDir string, cred *Credential) (env []string, cleanup func(
|
||||
}
|
||||
}
|
||||
|
||||
// patEnv 创建 askpass 脚本(Linux .sh / Windows .bat),脚本读 GIT_PAT_TOKEN 输出。
|
||||
// git 在询问 username/password 时会调 GIT_ASKPASS 两次,第一次问 username,第二次问 password;
|
||||
// 我们的脚本忽略提示参数 $1,无论谁问都先返回 username 再返回 token。
|
||||
// patEnv 创建 askpass 脚本(Linux .sh / Windows .bat),并把 username 与 token
|
||||
// 各写入一个 0600 临时文件,脚本按提示前缀(git 固定以 "Username for"/"Password for"
|
||||
// 开头)路由:命中 Username 输出用户名文件,否则输出 token 文件。
|
||||
//
|
||||
// Windows note: cmd.exe 的 .bat 没有 stderr 隔离风险(脚本只 echo 一行)。
|
||||
// 用文件 + type/cat 输出内容而非 echo env,规避 token 含 & | < > " 等特殊字符时的
|
||||
// 命令注入/认证失败,也避免 token 经 env 泄漏给所有子进程。git 会自行裁掉输出末尾
|
||||
// 的 CR/LF。
|
||||
func patEnv(tmpDir string, cred *Credential) ([]string, func(), error) {
|
||||
username := cred.Username
|
||||
if username == "" {
|
||||
@@ -77,30 +80,66 @@ func patEnv(tmpDir string, cred *Credential) ([]string, func(), error) {
|
||||
_ = os.Remove(f.Name())
|
||||
return nil, func() {}, fmt.Errorf("chmod askpass: %w", err)
|
||||
}
|
||||
|
||||
userFile, err := writeSecretFile(tmpDir, "askpass-user-*", []byte(username))
|
||||
if err != nil {
|
||||
_ = os.Remove(f.Name())
|
||||
return nil, func() {}, fmt.Errorf("write username file: %w", err)
|
||||
}
|
||||
tokenFile, err := writeSecretFile(tmpDir, "askpass-token-*", cred.Secret)
|
||||
if err != nil {
|
||||
_ = os.Remove(f.Name())
|
||||
_ = os.Remove(userFile)
|
||||
return nil, func() {}, fmt.Errorf("write token file: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"GIT_ASKPASS=" + f.Name(),
|
||||
"GIT_PAT_USERNAME=" + username,
|
||||
"GIT_PAT_TOKEN=" + string(cred.Secret),
|
||||
"GIT_PAT_USERNAME_FILE=" + userFile,
|
||||
"GIT_PAT_TOKEN_FILE=" + tokenFile,
|
||||
// GIT_TERMINAL_PROMPT=0 防止 askpass 失效时 git 退回交互
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
}
|
||||
cleanup := func() { _ = os.Remove(f.Name()) }
|
||||
cleanup := func() {
|
||||
_ = os.Remove(f.Name())
|
||||
_ = os.Remove(userFile)
|
||||
_ = os.Remove(tokenFile)
|
||||
}
|
||||
return env, cleanup, nil
|
||||
}
|
||||
|
||||
// writeSecretFile 把 data 写入 tmpDir 下的 0600 临时文件,返回路径。
|
||||
func writeSecretFile(tmpDir, pattern string, data []byte) (string, error) {
|
||||
f, err := os.CreateTemp(tmpDir, pattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := f.Write(data); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
_ = f.Close()
|
||||
if err := os.Chmod(f.Name(), 0o600); err != nil {
|
||||
_ = os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
func patScriptShBody() string {
|
||||
return `#!/bin/sh
|
||||
case "$1" in
|
||||
*Username*) printf '%s' "$GIT_PAT_USERNAME" ;;
|
||||
*) printf '%s' "$GIT_PAT_TOKEN" ;;
|
||||
Username*) cat "$GIT_PAT_USERNAME_FILE" ;;
|
||||
*) cat "$GIT_PAT_TOKEN_FILE" ;;
|
||||
esac
|
||||
`
|
||||
}
|
||||
|
||||
func patScriptBatBody() string {
|
||||
return "@echo off\r\n" +
|
||||
"echo %1 | findstr /i Username >NUL\r\n" +
|
||||
"if %ERRORLEVEL%==0 (echo %GIT_PAT_USERNAME%) else (echo %GIT_PAT_TOKEN%)\r\n"
|
||||
"echo %1 | findstr /i /c:\"Username for\" >NUL\r\n" +
|
||||
"if %ERRORLEVEL%==0 (type \"%GIT_PAT_USERNAME_FILE%\") else (type \"%GIT_PAT_TOKEN_FILE%\")\r\n"
|
||||
}
|
||||
|
||||
// sshKeyEnv 写临时密钥 + 空 known_hosts,构造 GIT_SSH_COMMAND。
|
||||
@@ -112,7 +151,12 @@ func sshKeyEnv(tmpDir string, cred *Credential) ([]string, func(), error) {
|
||||
if err != nil {
|
||||
return nil, func() {}, fmt.Errorf("create ssh key tmp: %w", err)
|
||||
}
|
||||
if _, err := keyFile.Write(cred.Secret); err != nil {
|
||||
// 规整换行:\r\n 与单独 \r 统一为 \n,并确保末尾有且仅有一个 \n。
|
||||
// OpenSSH 新格式 / ED25519 密钥缺尾换行会报 invalid format。不改 cred.Secret 本身。
|
||||
key := bytes.ReplaceAll(cred.Secret, []byte("\r\n"), []byte("\n"))
|
||||
key = bytes.ReplaceAll(key, []byte("\r"), []byte("\n"))
|
||||
key = append(bytes.TrimRight(key, "\n"), '\n')
|
||||
if _, err := keyFile.Write(key); err != nil {
|
||||
_ = keyFile.Close()
|
||||
_ = os.Remove(keyFile.Name())
|
||||
return nil, func() {}, fmt.Errorf("write ssh key: %w", err)
|
||||
@@ -136,8 +180,10 @@ func sshKeyEnv(tmpDir string, cred *Credential) ([]string, func(), error) {
|
||||
}
|
||||
_ = khFile.Close()
|
||||
|
||||
// BatchMode=yes:带 passphrase 私钥或任何交互需求立即失败并给出明确 stderr,
|
||||
// 而非挂起。
|
||||
cmd := fmt.Sprintf(
|
||||
"ssh -i %q -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%q",
|
||||
"ssh -i %q -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%q",
|
||||
keyFile.Name(), khFile.Name(),
|
||||
)
|
||||
env := []string{"GIT_SSH_COMMAND=" + cmd}
|
||||
|
||||
Reference in New Issue
Block a user