- Rewrite complet en Go : exe unique sans _internal/, sans extraction temp - GUI Windows-native via github.com/lxn/walk (TableView, TextEdit, PushButton) - Meme fonctionnalites : check repos, pull, checkout, auto-update, logs - build.bat : go build -ldflags "-H windowsgui -s -w" -> 9.6 Mo, zero dependance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
135 lines
3.2 KiB
Go
135 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type RepoResult struct {
|
|
Name string
|
|
Path string
|
|
URL string
|
|
Pending bool
|
|
UpToDate bool
|
|
Offline bool
|
|
NeedsClone bool
|
|
Error string
|
|
NewCommits int
|
|
LocalChanges int
|
|
}
|
|
|
|
func runGit(args []string, cwd string, timeout time.Duration) (code int, stdout string, stderr string) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
fullArgs := append([]string{"-c", "safe.directory=*"}, args...)
|
|
cmd := newGitCmd(ctx, fullArgs, cwd)
|
|
|
|
var outBuf, errBuf strings.Builder
|
|
cmd.Stdout = &outBuf
|
|
cmd.Stderr = &errBuf
|
|
|
|
err := cmd.Run()
|
|
stdout = strings.TrimSpace(outBuf.String())
|
|
stderr = strings.TrimSpace(errBuf.String())
|
|
|
|
if err != nil {
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return 1, stdout, "Timeout"
|
|
}
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
return exitErr.ExitCode(), stdout, stderr
|
|
}
|
|
return -1, stdout, err.Error()
|
|
}
|
|
return 0, stdout, stderr
|
|
}
|
|
|
|
func absRepoPath(rel string) string {
|
|
if filepath.IsAbs(rel) {
|
|
return rel
|
|
}
|
|
return filepath.Join(exeDir(), rel)
|
|
}
|
|
|
|
func checkRepo(cfg RepoConfig) RepoResult {
|
|
res := RepoResult{Name: cfg.Name, URL: cfg.URL}
|
|
local := absRepoPath(cfg.Path)
|
|
res.Path = local
|
|
|
|
if _, err := os.Stat(filepath.Join(local, ".git")); os.IsNotExist(err) {
|
|
res.NeedsClone = true
|
|
return res
|
|
}
|
|
|
|
runGit([]string{"remote", "set-url", "origin", cfg.URL}, local, 10*time.Second)
|
|
|
|
code, _, stderr := runGit([]string{"fetch", "origin"}, local, 60*time.Second)
|
|
if code != 0 {
|
|
for _, kw := range []string{"could not resolve", "connection refused", "unable to connect", "timed out", "the remote end hung up"} {
|
|
if strings.Contains(strings.ToLower(stderr), kw) {
|
|
res.Offline = true
|
|
res.Error = "Hors ligne"
|
|
return res
|
|
}
|
|
}
|
|
res.Error = "Fetch: " + stderr
|
|
return res
|
|
}
|
|
|
|
_, branch, _ := runGit([]string{"rev-parse", "--abbrev-ref", "HEAD"}, local, 5*time.Second)
|
|
if branch == "" {
|
|
branch = "master"
|
|
}
|
|
|
|
_, countStr, _ := runGit([]string{"rev-list", "--count", "HEAD..origin/" + branch}, local, 5*time.Second)
|
|
fmt.Sscanf(countStr, "%d", &res.NewCommits)
|
|
|
|
_, status, _ := runGit([]string{"status", "--porcelain"}, local, 5*time.Second)
|
|
if status != "" {
|
|
res.LocalChanges = len(strings.Split(strings.TrimSpace(status), "\n"))
|
|
}
|
|
|
|
res.UpToDate = res.NewCommits == 0 && res.LocalChanges == 0
|
|
return res
|
|
}
|
|
|
|
func doClone(cfg RepoConfig) error {
|
|
local := absRepoPath(cfg.Path)
|
|
if err := os.MkdirAll(filepath.Dir(local), 0755); err != nil {
|
|
return err
|
|
}
|
|
code, _, stderr := runGit([]string{"clone", cfg.URL, local}, "", 300*time.Second)
|
|
if code != 0 {
|
|
return fmt.Errorf("%s", stderr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func doPull(res RepoResult) error {
|
|
_, branch, _ := runGit([]string{"rev-parse", "--abbrev-ref", "HEAD"}, res.Path, 5*time.Second)
|
|
if branch == "" {
|
|
branch = "master"
|
|
}
|
|
code, _, stderr := runGit([]string{"pull", "origin", branch}, res.Path, 120*time.Second)
|
|
if code != 0 {
|
|
return fmt.Errorf("%s", stderr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func doCheckout(res RepoResult) error {
|
|
code, _, stderr := runGit([]string{"checkout", "--", "."}, res.Path, 30*time.Second)
|
|
if code != 0 {
|
|
return fmt.Errorf("%s", stderr)
|
|
}
|
|
return nil
|
|
}
|