Compare commits

...

2 Commits

Author SHA1 Message Date
ba377a4e4a v0.7.9 - Auto-detection branche par defaut du remote
Changements :
- Detection automatique de la branche par defaut (main/master) via git ls-remote --symref HEAD
- Plus besoin de specifier branch dans config.ini si le remote utilise main
- Clone avec la bonne branche detectee (-b)
- Fallback sur master si la detection echoue

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 13:34:58 +01:00
19efbe6dd7 v0.7.8 - Bouton Arreter pour annuler les telechargements
Changements :
- Bouton Arreter dans la barre de boutons (actif pendant les operations)
- Annulation des operations git en cours via context.Context
- Detection et affichage du message Annule dans le journal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 13:20:01 +01:00
6 changed files with 112 additions and 23 deletions

Binary file not shown.

View File

@@ -38,15 +38,11 @@ func loadConfig() ([]RepoConfig, SelfUpdateConfig, error) {
case strings.HasPrefix(section, "repo:"): case strings.HasPrefix(section, "repo:"):
name := strings.TrimPrefix(section, "repo:") name := strings.TrimPrefix(section, "repo:")
if kv["url"] != "" && kv["path"] != "" { if kv["url"] != "" && kv["path"] != "" {
branch := kv["branch"]
if branch == "" {
branch = "master"
}
repos = append(repos, RepoConfig{ repos = append(repos, RepoConfig{
Name: name, Name: name,
URL: kv["url"], URL: kv["url"],
Path: kv["path"], Path: kv["path"],
Branch: branch, Branch: kv["branch"], // vide = auto-détection
}) })
} }
case section == "self-update": case section == "self-update":

72
git.go
View File

@@ -75,11 +75,39 @@ func checkRemoteOffline(stderr string) bool {
return false return false
} }
// detectDefaultBranch détecte la branche par défaut d'un remote via ls-remote --symref HEAD.
// Retourne "main", "master", etc. ou "" si indétectable.
func detectDefaultBranch(urlOrRemote string, cwd string) string {
_, out, _ := runGit([]string{"ls-remote", "--symref", urlOrRemote, "HEAD"}, cwd, 15*time.Second)
// Format attendu : "ref: refs/heads/main\tHEAD"
for _, line := range strings.Split(out, "\n") {
if strings.HasPrefix(line, "ref: refs/heads/") {
parts := strings.Fields(line)
if len(parts) >= 2 {
return strings.TrimPrefix(parts[0], "ref: refs/heads/")
}
}
}
return ""
}
func checkRepo(cfg RepoConfig) RepoResult { func checkRepo(cfg RepoConfig) RepoResult {
res := RepoResult{Name: cfg.Name, URL: cfg.URL, Branch: cfg.Branch} res := RepoResult{Name: cfg.Name, URL: cfg.URL, Branch: cfg.Branch}
local := absRepoPath(cfg.Path) local := absRepoPath(cfg.Path)
res.Path = local res.Path = local
// Détecter la branche si non spécifiée dans la config
branch := cfg.Branch
if branch == "" {
detected := detectDefaultBranch(cfg.URL, "")
if detected != "" {
branch = detected
} else {
branch = "master"
}
res.Branch = branch
}
if _, err := os.Stat(filepath.Join(local, ".git")); os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(local, ".git")); os.IsNotExist(err) {
// Vérifier que le dépôt distant existe avant de proposer le clone // Vérifier que le dépôt distant existe avant de proposer le clone
code, _, stderr := runGit([]string{"ls-remote", "--exit-code", cfg.URL}, "", 15*time.Second) code, _, stderr := runGit([]string{"ls-remote", "--exit-code", cfg.URL}, "", 15*time.Second)
@@ -110,7 +138,6 @@ func checkRepo(cfg RepoConfig) RepoResult {
} }
// Vérification rapide du remote via ls-remote (timeout court) // Vérification rapide du remote via ls-remote (timeout court)
branch := cfg.Branch
code, lsOut, stderr := runGit([]string{"ls-remote", "origin", "refs/heads/" + branch}, local, 15*time.Second) code, lsOut, stderr := runGit([]string{"ls-remote", "origin", "refs/heads/" + branch}, local, 15*time.Second)
if code != 0 { if code != 0 {
if checkRemoteOffline(stderr) { if checkRemoteOffline(stderr) {
@@ -130,10 +157,28 @@ func checkRepo(cfg RepoConfig) RepoResult {
remoteHash = parts[0] remoteHash = parts[0]
} }
} }
// Si la branche n'existe pas sur le remote, détecter la branche par défaut
if remoteHash == "" {
detected := detectDefaultBranch("origin", local)
if detected == "" || detected == branch {
res.Error = fmt.Sprintf("Branche '%s' introuvable sur le remote", branch)
return res
}
logInfo(fmt.Sprintf("[%s] Branche '%s' introuvable, utilisation de '%s'", cfg.Name, branch, detected))
branch = detected
res.Branch = detected
_, lsOut, _ = runGit([]string{"ls-remote", "origin", "refs/heads/" + branch}, local, 15*time.Second)
if lsOut != "" {
parts := strings.Fields(lsOut)
if len(parts) > 0 {
remoteHash = parts[0]
}
}
if remoteHash == "" { if remoteHash == "" {
res.Error = fmt.Sprintf("Branche '%s' introuvable sur le remote", branch) res.Error = fmt.Sprintf("Branche '%s' introuvable sur le remote", branch)
return res return res
} }
}
// Comparer les hashs // Comparer les hashs
if localHash != remoteHash { if localHash != remoteHash {
@@ -280,8 +325,8 @@ func parseGitProgress(line string) (ProgressInfo, bool) {
// runGitWithProgress exécute une commande git et capture la progression en temps réel. // runGitWithProgress exécute une commande git et capture la progression en temps réel.
// Le timeout est désactivé (0) ou très long pour les gros dépôts. // Le timeout est désactivé (0) ou très long pour les gros dépôts.
func runGitWithProgress(args []string, cwd string, timeout time.Duration, cb ProgressCallback) (int, string, string) { func runGitWithProgress(parent context.Context, args []string, cwd string, timeout time.Duration, cb ProgressCallback) (int, string, string) {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(parent, timeout)
defer cancel() defer cancel()
fullArgs := append([]string{"-c", "safe.directory=*"}, args...) fullArgs := append([]string{"-c", "safe.directory=*"}, args...)
@@ -339,6 +384,9 @@ func runGitWithProgress(args []string, cwd string, timeout time.Duration, cb Pro
stderr := strings.TrimSpace(stderrBuf.String()) stderr := strings.TrimSpace(stderrBuf.String())
if err != nil { if err != nil {
if ctx.Err() == context.Canceled {
return 1, stdout, "Annulé"
}
if ctx.Err() == context.DeadlineExceeded { if ctx.Err() == context.DeadlineExceeded {
return 1, stdout, "Timeout" return 1, stdout, "Timeout"
} }
@@ -352,7 +400,8 @@ func runGitWithProgress(args []string, cwd string, timeout time.Duration, cb Pro
} }
// doCloneWithProgress clone un dépôt avec suivi de progression. // doCloneWithProgress clone un dépôt avec suivi de progression.
func doCloneWithProgress(cfg RepoConfig, cb ProgressCallback) error { // branch est la branche à checkout (détectée ou configurée).
func doCloneWithProgress(ctx context.Context, cfg RepoConfig, branch string, cb ProgressCallback) error {
local := absRepoPath(cfg.Path) local := absRepoPath(cfg.Path)
if err := os.MkdirAll(filepath.Dir(local), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(local), 0755); err != nil {
return err return err
@@ -361,9 +410,13 @@ func doCloneWithProgress(cfg RepoConfig, cb ProgressCallback) error {
// Si le dossier n'existe pas ou est vide, clone classique avec progression // Si le dossier n'existe pas ou est vide, clone classique avec progression
entries, _ := os.ReadDir(local) entries, _ := os.ReadDir(local)
if len(entries) == 0 { if len(entries) == 0 {
args := []string{"clone", "--progress"}
if branch != "" {
args = append(args, "-b", branch)
}
args = append(args, cfg.URL, local)
code, _, stderr := runGitWithProgress( code, _, stderr := runGitWithProgress(
[]string{"clone", "--progress", cfg.URL, local}, ctx, args, "", 2*time.Hour, cb,
"", 2*time.Hour, cb,
) )
if code != 0 { if code != 0 {
return fmt.Errorf("%s", stderr) return fmt.Errorf("%s", stderr)
@@ -383,14 +436,13 @@ func doCloneWithProgress(cfg RepoConfig, cb ProgressCallback) error {
} }
code, _, stderr = runGitWithProgress( code, _, stderr = runGitWithProgress(
[]string{"fetch", "--progress", "origin"}, ctx, []string{"fetch", "--progress", "origin"},
local, 2*time.Hour, cb, local, 2*time.Hour, cb,
) )
if code != 0 { if code != 0 {
return fmt.Errorf("fetch: %s", stderr) return fmt.Errorf("fetch: %s", stderr)
} }
branch := cfg.Branch
code, _, stderr = runGit([]string{"checkout", "origin/" + branch, "-b", branch}, local, 30*time.Second) code, _, stderr = runGit([]string{"checkout", "origin/" + branch, "-b", branch}, local, 30*time.Second)
if code != 0 { if code != 0 {
code, _, stderr = runGit([]string{"checkout", branch}, local, 30*time.Second) code, _, stderr = runGit([]string{"checkout", branch}, local, 30*time.Second)
@@ -405,13 +457,13 @@ func doCloneWithProgress(cfg RepoConfig, cb ProgressCallback) error {
} }
// doPullWithProgress fait un pull avec suivi de progression. // doPullWithProgress fait un pull avec suivi de progression.
func doPullWithProgress(res RepoResult, cb ProgressCallback) error { func doPullWithProgress(ctx context.Context, res RepoResult, cb ProgressCallback) error {
_, branch, _ := runGit([]string{"rev-parse", "--abbrev-ref", "HEAD"}, res.Path, 5*time.Second) _, branch, _ := runGit([]string{"rev-parse", "--abbrev-ref", "HEAD"}, res.Path, 5*time.Second)
if branch == "" { if branch == "" {
branch = "master" branch = "master"
} }
code, _, stderr := runGitWithProgress( code, _, stderr := runGitWithProgress(
[]string{"pull", "--progress", "origin", branch}, ctx, []string{"pull", "--progress", "origin", branch},
res.Path, 2*time.Hour, cb, res.Path, 2*time.Hour, cb,
) )
if code != 0 { if code != 0 {

49
gui.go
View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"context"
_ "embed" _ "embed"
"fmt" "fmt"
"image" "image"
@@ -222,10 +223,13 @@ type App struct {
btnRefresh *walk.PushButton btnRefresh *walk.PushButton
btnUpdateAll *walk.PushButton btnUpdateAll *walk.PushButton
btnAction *walk.PushButton btnAction *walk.PushButton
btnStop *walk.PushButton
reposConfig []RepoConfig reposConfig []RepoConfig
suConfig SelfUpdateConfig suConfig SelfUpdateConfig
checking atomic.Bool checking atomic.Bool
cancelMu sync.Mutex
cancelFunc context.CancelFunc
} }
func runApp() error { func runApp() error {
@@ -327,6 +331,12 @@ func (a *App) buildAndRun() error {
Enabled: false, Enabled: false,
OnClicked: a.doAction, OnClicked: a.doAction,
}, },
PushButton{
AssignTo: &a.btnStop,
Text: "Arrêter",
Enabled: false,
OnClicked: a.stopOperations,
},
HSpacer{}, HSpacer{},
PushButton{Text: "config.ini", OnClicked: a.openConfig}, PushButton{Text: "config.ini", OnClicked: a.openConfig},
PushButton{Text: "Logs", OnClicked: a.openLogs}, PushButton{Text: "Logs", OnClicked: a.openLogs},
@@ -540,6 +550,30 @@ func (a *App) makeProgressCB(row int) ProgressCallback {
} }
} }
// ── Annulation ────────────────────────────────────────────────────────────────
// newCancelCtx crée un nouveau contexte annulable et stocke la fonction cancel.
func (a *App) newCancelCtx() context.Context {
a.cancelMu.Lock()
defer a.cancelMu.Unlock()
ctx, cancel := context.WithCancel(context.Background())
a.cancelFunc = cancel
return ctx
}
// stopOperations annule toutes les opérations git en cours.
func (a *App) stopOperations() {
a.cancelMu.Lock()
fn := a.cancelFunc
a.cancelFunc = nil
a.cancelMu.Unlock()
if fn != nil {
fn()
a.appendLog("Opérations annulées par l'utilisateur")
logInfo("Opérations annulées par l'utilisateur")
}
}
// ── Actions dépôt ───────────────────────────────────────────────────────────── // ── Actions dépôt ─────────────────────────────────────────────────────────────
func (a *App) onSelectionChanged() { func (a *App) onSelectionChanged() {
@@ -575,22 +609,25 @@ func (a *App) doAction() {
} }
a.btnAction.SetEnabled(false) a.btnAction.SetEnabled(false)
a.btnStop.SetEnabled(true)
a.appendLog(fmt.Sprintf("[%s] Mise à jour en cours...", res.Name)) a.appendLog(fmt.Sprintf("[%s] Mise à jour en cours...", res.Name))
ctx := a.newCancelCtx()
cb := a.makeProgressCB(idx) cb := a.makeProgressCB(idx)
go func() { go func() {
var err error var err error
if res.NeedsClone { if res.NeedsClone {
err = doCloneWithProgress(cfg, cb) err = doCloneWithProgress(ctx, cfg, res.Branch, cb)
} else { } else {
if res.LocalChanges > 0 { if res.LocalChanges > 0 {
err = doCheckout(res) err = doCheckout(res)
} }
if err == nil && res.HasUpdate { if err == nil && res.HasUpdate {
err = doPullWithProgress(res, cb) err = doPullWithProgress(ctx, res, cb)
} }
} }
a.mw.Synchronize(func() { a.mw.Synchronize(func() {
a.btnStop.SetEnabled(false)
if err != nil { if err != nil {
a.model.setProgress(idx, 0, "Erreur") a.model.setProgress(idx, 0, "Erreur")
a.appendLog(fmt.Sprintf("[%s] Erreur: %v", res.Name, err)) a.appendLog(fmt.Sprintf("[%s] Erreur: %v", res.Name, err))
@@ -614,7 +651,9 @@ func (a *App) doAction() {
func (a *App) updateAll() { func (a *App) updateAll() {
a.btnUpdateAll.SetEnabled(false) a.btnUpdateAll.SetEnabled(false)
a.btnRefresh.SetEnabled(false) a.btnRefresh.SetEnabled(false)
a.btnStop.SetEnabled(true)
pending := atomic.Int32{} pending := atomic.Int32{}
ctx := a.newCancelCtx()
for i, cfg := range a.reposConfig { for i, cfg := range a.reposConfig {
res, ok := a.model.getResult(i) res, ok := a.model.getResult(i)
@@ -627,13 +666,13 @@ func (a *App) updateAll() {
go func() { go func() {
var err error var err error
if res.NeedsClone { if res.NeedsClone {
err = doCloneWithProgress(cfg, cb) err = doCloneWithProgress(ctx, cfg, res.Branch, cb)
} else { } else {
if res.LocalChanges > 0 { if res.LocalChanges > 0 {
err = doCheckout(res) err = doCheckout(res)
} }
if err == nil && res.HasUpdate { if err == nil && res.HasUpdate {
err = doPullWithProgress(res, cb) err = doPullWithProgress(ctx, res, cb)
} }
if err == nil && res.UntrackedFiles > 0 { if err == nil && res.UntrackedFiles > 0 {
err = doClean(res) err = doClean(res)
@@ -650,12 +689,14 @@ func (a *App) updateAll() {
a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name)) a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
} }
if pending.Add(-1) == 0 { if pending.Add(-1) == 0 {
a.btnStop.SetEnabled(false)
a.startCheck() a.startCheck()
} }
}) })
}() }()
} }
if pending.Load() == 0 { if pending.Load() == 0 {
a.btnStop.SetEnabled(false)
a.startCheck() a.startCheck()
} }
} }

View File

@@ -9,7 +9,7 @@ import (
"github.com/lxn/walk" "github.com/lxn/walk"
) )
const VERSION = "0.7.7" const VERSION = "0.7.9"
func exeDir() string { func exeDir() string {
exe, err := os.Executable() exe, err := os.Executable()

View File

@@ -1 +1 @@
0.7.7 0.7.9