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.

41
git.go
View File

@@ -16,16 +16,18 @@ import (
) )
type RepoResult struct { type RepoResult struct {
Name string Name string
Path string Path string
URL string URL string
Pending bool Pending bool
UpToDate bool UpToDate bool
Offline bool Offline bool
NeedsClone bool NeedsClone bool
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