v0.7.5 - Miroir depot git et detection fichiers non suivis

Changements :
- Detection des fichiers non suivis (untracked) dans chaque depot
- Affichage "X fichier(s) en trop" dans le statut
- Popup de confirmation listant les fichiers avant suppression (git clean -fd)
- Suppression auto des fichiers en trop via "Tout mettre a jour"
- Verification du depot distant via git ls-remote avant de proposer le clone
- Affichage "Depot introuvable" si l'URL pointe vers un repo inexistant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 10:34:40 +01:00
parent db57cfacaf
commit 55663e3a19
5 changed files with 94 additions and 16 deletions

Binary file not shown.

21
git.go
View File

@@ -26,6 +26,8 @@ type RepoResult struct {
Error string Error string
NewCommits int NewCommits int
LocalChanges 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) { 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) _, status, _ := runGit([]string{"status", "--porcelain"}, local, 5*time.Second)
if status != "" { 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 return res
} }
@@ -154,6 +163,14 @@ func doCheckout(res RepoResult) error {
return nil 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 ─────────────────────────────────────────────────────────── // ── Progression Git ───────────────────────────────────────────────────────────
// ProgressInfo contient l'état de progression d'une opération git. // ProgressInfo contient l'état de progression d'une opération git.

65
gui.go
View File

@@ -46,6 +46,12 @@ func (it *RepoItem) statusText() string {
} }
msg += fmt.Sprintf("%d modif. locale(s)", r.LocalChanges) 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 == "" { if msg == "" {
return "À jour" return "À jour"
} }
@@ -195,7 +201,7 @@ func (m *RepoModel) hasUpdates() bool {
defer m.mu.RUnlock() defer m.mu.RUnlock()
for _, it := range m.items { for _, it := range m.items {
r := it.result 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 return true
} }
} }
@@ -430,6 +436,12 @@ func logLineForResult(r RepoResult) string {
} }
msg += fmt.Sprintf("%d modif. locale(s)", r.LocalChanges) 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 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 ─────────────────────────────────────────────────────────────── // ── Progression ───────────────────────────────────────────────────────────────
// makeProgressCB crée un callback de progression pour la ligne row du tableau. // makeProgressCB crée un callback de progression pour la ligne row du tableau.
@@ -487,7 +534,7 @@ func (a *App) onSelectionChanged() {
if res.NeedsClone { if res.NeedsClone {
a.btnAction.SetText("Cloner") a.btnAction.SetText("Cloner")
a.btnAction.SetEnabled(true) 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.SetText("Mettre à jour")
a.btnAction.SetEnabled(true) a.btnAction.SetEnabled(true)
} else { } else {
@@ -503,6 +550,12 @@ func (a *App) doAction() {
} }
cfg := a.reposConfig[idx] 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.btnAction.SetEnabled(false)
a.appendLog(fmt.Sprintf("[%s] Mise à jour en cours...", res.Name)) 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)) a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
logInfo(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 // Re-vérifier uniquement ce dépôt, pas tous
a.recheckOne(idx) a.recheckOne(idx)
}) })
@@ -559,6 +617,9 @@ func (a *App) updateAll() {
if err == nil && res.NewCommits > 0 { if err == nil && res.NewCommits > 0 {
err = doPullWithProgress(res, cb) err = doPullWithProgress(res, cb)
} }
if err == nil && res.UntrackedFiles > 0 {
err = doClean(res)
}
} }
a.mw.Synchronize(func() { a.mw.Synchronize(func() {
if err != nil { if err != nil {

View File

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

View File

@@ -1 +1 @@
0.7.4 0.7.5