diff --git a/GitUpdateChecker.exe b/GitUpdateChecker.exe index 36ed7c3..9a7ef39 100644 Binary files a/GitUpdateChecker.exe and b/GitUpdateChecker.exe differ diff --git a/git.go b/git.go index 096924d..2be66ee 100644 --- a/git.go +++ b/git.go @@ -16,16 +16,18 @@ import ( ) type RepoResult struct { - Name string - Path string - URL string - Pending bool - UpToDate bool - Offline bool - NeedsClone bool - Error string - NewCommits int - LocalChanges int + Name string + Path string + URL string + Pending bool + UpToDate bool + Offline bool + NeedsClone bool + Error string + NewCommits int + LocalChanges int + UntrackedFiles int + UntrackedList []string // liste des fichiers non suivis } func runGit(args []string, cwd string, timeout time.Duration) (code int, stdout string, stderr string) { @@ -115,10 +117,17 @@ func checkRepo(cfg RepoConfig) RepoResult { _, status, _ := runGit([]string{"status", "--porcelain"}, local, 5*time.Second) if status != "" { - res.LocalChanges = len(strings.Split(strings.TrimSpace(status), "\n")) + for _, line := range strings.Split(strings.TrimSpace(status), "\n") { + if strings.HasPrefix(line, "?? ") { + res.UntrackedFiles++ + res.UntrackedList = append(res.UntrackedList, strings.TrimPrefix(line, "?? ")) + } else { + res.LocalChanges++ + } + } } - res.UpToDate = res.NewCommits == 0 && res.LocalChanges == 0 + res.UpToDate = res.NewCommits == 0 && res.LocalChanges == 0 && res.UntrackedFiles == 0 return res } @@ -154,6 +163,14 @@ func doCheckout(res RepoResult) error { return nil } +func doClean(res RepoResult) error { + code, _, stderr := runGit([]string{"clean", "-fd"}, res.Path, 60*time.Second) + if code != 0 { + return fmt.Errorf("%s", stderr) + } + return nil +} + // ── Progression Git ─────────────────────────────────────────────────────────── // ProgressInfo contient l'état de progression d'une opération git. diff --git a/gui.go b/gui.go index 1b59ce8..3cb8449 100644 --- a/gui.go +++ b/gui.go @@ -46,6 +46,12 @@ func (it *RepoItem) statusText() string { } msg += fmt.Sprintf("%d modif. locale(s)", r.LocalChanges) } + if r.UntrackedFiles > 0 { + if msg != "" { + msg += ", " + } + msg += fmt.Sprintf("%d fichier(s) en trop", r.UntrackedFiles) + } if msg == "" { return "À jour" } @@ -195,7 +201,7 @@ func (m *RepoModel) hasUpdates() bool { defer m.mu.RUnlock() for _, it := range m.items { r := it.result - if !r.Pending && r.Error == "" && !r.Offline && (r.NewCommits > 0 || r.LocalChanges > 0 || r.NeedsClone) { + if !r.Pending && r.Error == "" && !r.Offline && (r.NewCommits > 0 || r.LocalChanges > 0 || r.UntrackedFiles > 0 || r.NeedsClone) { return true } } @@ -430,6 +436,12 @@ func logLineForResult(r RepoResult) string { } msg += fmt.Sprintf("%d modif. locale(s)", r.LocalChanges) } + if r.UntrackedFiles > 0 { + if msg != "" { + msg += ", " + } + msg += fmt.Sprintf("%d fichier(s) en trop", r.UntrackedFiles) + } return msg } @@ -450,6 +462,41 @@ func (a *App) recheckOne(idx int) { }() } +// proposeClean affiche un popup listant les fichiers non suivis et propose de les supprimer. +func (a *App) proposeClean(idx int, res RepoResult) { + // Construire la liste des fichiers (max 30 affichés) + list := "" + for i, f := range res.UntrackedList { + if i >= 30 { + list += fmt.Sprintf("\n... et %d autre(s)", len(res.UntrackedList)-30) + break + } + list += "\n - " + f + } + msg := fmt.Sprintf("[%s] %d fichier(s) non suivi(s) détecté(s) :%s\n\nSupprimer ces fichiers ?", + res.Name, res.UntrackedFiles, list) + + ans := walk.MsgBox(a.mw, "Fichiers en trop", msg, walk.MsgBoxYesNo|walk.MsgBoxIconQuestion) + if ans == walk.DlgCmdYes { + a.appendLog(fmt.Sprintf("[%s] Nettoyage de %d fichier(s)...", res.Name, res.UntrackedFiles)) + go func() { + err := doClean(res) + a.mw.Synchronize(func() { + if err != nil { + a.appendLog(fmt.Sprintf("[%s] Erreur nettoyage: %v", res.Name, err)) + logError(fmt.Sprintf("[%s] clean: %v", res.Name, err)) + } else { + a.appendLog(fmt.Sprintf("[%s] %d fichier(s) supprimé(s)", res.Name, res.UntrackedFiles)) + logInfo(fmt.Sprintf("[%s] %d fichier(s) supprimé(s)", res.Name, res.UntrackedFiles)) + } + a.recheckOne(idx) + }) + }() + } else { + a.recheckOne(idx) + } +} + // ── Progression ─────────────────────────────────────────────────────────────── // makeProgressCB crée un callback de progression pour la ligne row du tableau. @@ -487,7 +534,7 @@ func (a *App) onSelectionChanged() { if res.NeedsClone { a.btnAction.SetText("Cloner") a.btnAction.SetEnabled(true) - } else if res.NewCommits > 0 || res.LocalChanges > 0 { + } else if res.NewCommits > 0 || res.LocalChanges > 0 || res.UntrackedFiles > 0 { a.btnAction.SetText("Mettre à jour") a.btnAction.SetEnabled(true) } else { @@ -503,6 +550,12 @@ func (a *App) doAction() { } cfg := a.reposConfig[idx] + // Si uniquement des fichiers en trop, proposer directement le nettoyage + if res.UntrackedFiles > 0 && res.NewCommits == 0 && res.LocalChanges == 0 && !res.NeedsClone { + a.proposeClean(idx, res) + return + } + a.btnAction.SetEnabled(false) a.appendLog(fmt.Sprintf("[%s] Mise à jour en cours...", res.Name)) @@ -529,6 +582,11 @@ func (a *App) doAction() { a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name)) logInfo(fmt.Sprintf("[%s] Mise à jour OK", res.Name)) } + // Si des fichiers en trop après la mise à jour, proposer le nettoyage + if res.UntrackedFiles > 0 { + a.proposeClean(idx, res) + return + } // Re-vérifier uniquement ce dépôt, pas tous a.recheckOne(idx) }) @@ -559,6 +617,9 @@ func (a *App) updateAll() { if err == nil && res.NewCommits > 0 { err = doPullWithProgress(res, cb) } + if err == nil && res.UntrackedFiles > 0 { + err = doClean(res) + } } a.mw.Synchronize(func() { if err != nil { diff --git a/main.go b/main.go index ba55f33..082ee79 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "github.com/lxn/walk" ) -const VERSION = "0.7.4" +const VERSION = "0.7.5" func exeDir() string { exe, err := os.Executable() diff --git a/version.txt b/version.txt index 0a1ffad..8bd6ba8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.7.4 +0.7.5