Compare commits

...

7 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
9bf66f6d90 Nettoyage: suppression fichiers inutiles
Changements :
- Suppression icon_small.png (non utilise)
- Suppression rsrc.syso du tracking (fichier genere par build.bat)
- Suppression dossier Scripts vide
- Nettoyage .gitignore (ajout rsrc.syso, retrait entrees Python)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 12:58:54 +01:00
3f0f13147b Suppression gitchecker.exe (ancien build inutilise)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 12:57:35 +01:00
2b2eb87f45 Fix layout icone en-tete: redimensionnement 24x24 avant affichage
Changements :
- Redimensionnement de l'icone PNG a 24x24 via CatmullRom (golang.org/x/image/draw)
- Corrige le decalage de l'interface cause par l'image 912x1164

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 12:51:04 +01:00
3ffbb550ec Fix png dans le titre 2026-03-25 12:46:53 +01:00
98b5187bfc v0.7.7 - Icone PNG embarquee dans l'en-tete GUI
Changements :
- Icone icon.png embarquee dans l'exe via go:embed
- Affichage de l'icone a gauche du titre dans l'interface
- Mise a jour CLAUDE.md pour refleter la migration Go

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 12:42:33 +01:00
14 changed files with 216 additions and 65 deletions

2
.gitignore vendored
View File

@@ -1,7 +1,7 @@
log/ log/
dist/ dist/
build/ build/
__pycache__/ rsrc.syso
*.spec *.spec
*.exe.old *.exe.old
_update.bat _update.bat

105
CLAUDE.md
View File

@@ -6,37 +6,53 @@ Outil Windows (.exe) avec interface graphique qui vérifie les mises à jour de
Conçu pour être placé sur une **clé USB** dont la lettre de lecteur peut changer. Conçu pour être placé sur une **clé USB** dont la lettre de lecteur peut changer.
Le programme peut **s'auto-mettre à jour** car il est lui-même dans un dépôt git. Le programme peut **s'auto-mettre à jour** car il est lui-même dans un dépôt git.
## Langage
**Go** (anciennement Python, migré en Go depuis v0.7.x).
- GUI : `github.com/lxn/walk` (contrôles natifs Windows)
- Exe unique, aucune dépendance externe à l'exécution
- Build : `go build` via `build.bat`
## Structure du projet ## Structure du projet
``` ```
Lanceur-geco/ Lanceur-geco/
├── git_updater.py # Script principal Python (GUI tkinter) ├── main.go # Point d'entrée, constante VERSION
├── version.txt # Fichier contenant le numéro de version (ex: "0.5.1") ├── config.go # Chargement config.ini (repos + self-update)
├── git.go # Opérations git (check, clone, pull, checkout, clean)
├── gui.go # Interface graphique (walk/TableView)
├── logger.go # Logging fichier (1 fichier/jour, rotation 30j)
├── selfupdate.go # Auto-mise à jour du programme
├── platform_windows.go # Code spécifique Windows (création processus)
├── version.txt # Numéro de version (utilisé par l'auto-update distant)
├── config.ini # Configuration multi-repo ├── config.ini # Configuration multi-repo
├── build.bat # Script de compilation en .exe via PyInstaller ├── build.bat # Script de compilation en .exe
├── log/ # Dossier de logs (créé automatiquement, 1 fichier par jour) ├── app.manifest # Manifeste Windows (DPI, elevation)
├── icon.ico # Icône application
├── go.mod / go.sum # Dépendances Go
├── log/ # Dossier de logs (créé automatiquement)
└── CLAUDE.md └── CLAUDE.md
``` ```
## Règles importantes ## Règles importantes
- **Tous les chemins doivent être relatifs** à l'emplacement de l'exe. Jamais de chemin absolu (pas de `C:\`, `G:\`, etc.). Utiliser `..` et des chemins relatifs pour référencer les dossiers. - **Tous les chemins doivent être relatifs** à l'emplacement de l'exe. Jamais de chemin absolu (pas de `C:\`, `G:\`, etc.). Utiliser `..` et des chemins relatifs pour référencer les dossiers.
- **Accès lecture seule** : le programme ne fait que `git fetch`, `git pull` et `git checkout`. Jamais de `git push`, `git commit`, `git add`, ou toute opération d'écriture vers le remote. - **Accès lecture seule** : le programme ne fait que `git ls-remote`, `git fetch`, `git pull`, `git checkout` et `git clean`. Jamais de `git push`, `git commit`, `git add`, ou toute opération d'écriture vers le remote.
- **Multi-repo** : le programme peut surveiller plusieurs dépôts Git configurés dans `config.ini`. - **Multi-repo** : le programme peut surveiller plusieurs dépôts Git configurés dans `config.ini`.
## Versioning ## Versioning
- La version est définie en dur dans `git_updater.py` via la constante `VERSION` (ex: `VERSION = "0.5.1"`) - La version est définie en dur dans `main.go` via la constante `VERSION` (ex: `const VERSION = "0.7.6"`)
- Le fichier `version.txt` à la racine du projet contient le même numéro de version (utilisé par le mécanisme d'auto-update distant) - Le fichier `version.txt` à la racine du projet contient le même numéro de version (utilisé par le mécanisme d'auto-update distant)
- Format : **semver simplifié** `MAJEUR.MINEUR.PATCH` (ex: `0.5.1`) - Format : **semver simplifié** `MAJEUR.MINEUR.PATCH` (ex: `0.7.6`)
- **Les deux doivent toujours être synchronisés** : quand on change la version, mettre à jour `VERSION` dans `git_updater.py` ET `version.txt` - **Les deux doivent toujours être synchronisés** : quand on change la version, mettre à jour `VERSION` dans `main.go` ET `version.txt`
### Mise à jour de la version ### Mise à jour de la version
A chaque changement de version, il faut mettre à jour **4 éléments** : A chaque changement de version, il faut mettre à jour **4 éléments** :
1. `VERSION` dans `git_updater.py` (constante en haut du fichier) 1. `VERSION` dans `main.go` (constante en haut du fichier)
2. `version.txt` à la racine du projet 2. `version.txt` à la racine du projet
3. **Recompiler l'exe** via `build.bat` et copier `dist/GitUpdateChecker.exe` à la racine du projet 3. **Recompiler l'exe** via `build.bat` (produit `GitUpdateChecker.exe` à la racine)
4. **Créer un commit** avec le message suivant : 4. **Créer un commit** avec le message suivant :
``` ```
@@ -50,18 +66,18 @@ Changements :
Exemple : Exemple :
``` ```
v0.5.2 - Detection depot hors ligne v0.7.6 - Clone dossier non-vide et verification rapide
Changements : Changements :
- Ajout verification connectivite remote avant fetch (git ls-remote) - Clone dans dossier non-vide (git init + remote add + fetch + checkout)
- Affichage "HORS LIGNE" si le serveur est inaccessible - Verification rapide via git ls-remote au lieu de git fetch
- Synchronisation auto de l'URL origin depuis config.ini - Support branche par repo dans config.ini
``` ```
### Mécanisme de comparaison ### Mécanisme de comparaison
- La fonction `_version_tuple(v)` convertit la chaîne version en tuple d'entiers (ex: `"0.5.1"` -> `(0, 5, 1)`) pour permettre la comparaison numérique - La fonction `parseVersion(v)` convertit la chaîne version en `[3]int` (ex: `"0.7.6"` -> `[0, 7, 6]`) pour permettre la comparaison numérique
- L'auto-update télécharge `version.txt` depuis le serveur Gitea via HTTP (`{repo_url}/raw/branch/master/version.txt`) et compare avec la `VERSION` locale - L'auto-update télécharge `version.txt` depuis le serveur Gitea via HTTP (`{repo_url}/raw/branch/{branch}/version.txt`) et compare avec la `VERSION` locale
- Si la version distante est supérieure, une mise à jour est proposée - Si la version distante est supérieure, une mise à jour est proposée
## Fonctionnement ## Fonctionnement
@@ -71,64 +87,77 @@ Changements :
2. Compare la version distante avec la constante `VERSION` locale (comparaison par tuple numérique) 2. Compare la version distante avec la constante `VERSION` locale (comparaison par tuple numérique)
3. Si la version distante est supérieure, propose de télécharger le nouvel exe 3. Si la version distante est supérieure, propose de télécharger le nouvel exe
4. Stratégie de remplacement : télécharge dans `.new`, renomme l'exe actuel en `.old`, place le nouveau 4. Stratégie de remplacement : télécharge dans `.new`, renomme l'exe actuel en `.old`, place le nouveau
5. Après mise à jour, demande un redémarrage 5. Après mise à jour, lance un script batch de redémarrage
### Vérification des dépôts ### Vérification des dépôts
1. Lit la liste des dépôts depuis `config.ini` (chemins relatifs à l'exe) 1. Lit la liste des dépôts depuis `config.ini` (chemins relatifs à l'exe)
2. Pour chaque dépôt : 2. Pour chaque dépôt (en parallèle via goroutines) :
- `git fetch` pour récupérer l'état distant - `git ls-remote` pour vérifier la disponibilité et comparer les hashs (rapide, timeout 15s)
- Compare commits locaux vs distants - `git status --porcelain` pour détecter les fichiers modifiés/non suivis localement
- Détecte les fichiers supprimés/modifiés localement 3. Affiche le résultat dans une interface graphique (walk/TableView)
3. Affiche le résultat dans une interface graphique (tkinter)
4. Propose pour chaque dépôt : 4. Propose pour chaque dépôt :
- `git pull` si nouveaux commits distants - `git pull` si MAJ disponible (hash distant différent)
- `git checkout -- .` si fichiers locaux manquants/modifiés - `git checkout -- .` si fichiers locaux modifiés
- `git clean -fd` si fichiers non suivis en trop
### Clone dans dossier non-vide
Si le dossier cible existe déjà mais n'a pas de `.git` (ex: repos imbriqués), le programme fait un clone "in-place" :
`git init` + `git remote add` + `git fetch` + `git checkout -b <branch>`
## Configuration (config.ini) ## Configuration (config.ini)
Supporte plusieurs sections `[repo:NomDuRepo]` : Supporte plusieurs sections `[repo:NomDuRepo]` :
```ini ```ini
[repo:Batch] [self-update]
url = http://192.168.1.235:3125/zogzog/Batch url = http://192.168.1.235:3125/zogzog/Lanceur-geco
path = ../Batch exe_name = GitUpdateChecker.exe
branch = master
[repo:Powershell] [repo:Scripts]
url = http://192.168.1.235:3125/zogzog/Powershell url = http://192.168.1.235:3125/zogzog/Scripts
path = ../Powershell path = ../SOFT/Batch/Scripts
[repo:Soft]
url = http://192.168.1.235:3125/zogzog/Soft.git
path = ../SOFT/
branch = master
``` ```
- `url` : URL du dépôt Git distant - `url` : URL du dépôt Git distant
- `path` : Chemin **relatif** vers le dossier local du dépôt (relatif à l'exe) - `path` : Chemin **relatif** vers le dossier local du dépôt (relatif à l'exe)
- `branch` : Branche à suivre (optionnel, défaut: `master`)
## Logging ## Logging
- Les logs sont écrits dans `log/` à côté de l'exe (1 fichier par jour, format `YYYY-MM-DD.log`) - Les logs sont écrits dans `log/` à côté de l'exe (1 fichier par jour, format `YYYY-MM-DD.log`)
- Les vieux logs sont nettoyés automatiquement (30 jours de rétention) - Les vieux logs sont nettoyés automatiquement (30 jours de rétention)
- Chaque action git, erreur, et résultat est loggé avec timestamp - Chaque action git, erreur, et résultat est loggé avec timestamp
- Bouton "Ouvrir les logs" dans la GUI pour accéder au dossier - Bouton "Logs" dans la GUI pour ouvrir le dossier
## Build ## Build
```bat ```bat
build.bat build.bat
``` ```
Requiert Python + pip. Installe PyInstaller automatiquement si absent. Requiert Go installé et dans le PATH. Installe `rsrc` automatiquement si absent.
Produit `dist/GitUpdateChecker.exe`. Copier `config.ini` à côté de l'exe. Produit `GitUpdateChecker.exe` à la racine (exe unique, pas de dépendances).
Flags de build : `-H windowsgui -s -w` (pas de console, symboles strippés).
## Contraintes techniques ## Contraintes techniques
- **Chemins relatifs** : Tout est relatif à l'exe, jamais de chemin absolu - **Chemins relatifs** : Tout est relatif à l'exe, jamais de chemin absolu
- **Encodage** : Force UTF-8 pour les caractères Unicode
- **Clé USB** : Fonctionne sur n'importe quelle lettre de lecteur - **Clé USB** : Fonctionne sur n'importe quelle lettre de lecteur
- **Git requis** : Git doit être installé et dans le PATH de la machine - **Git requis** : Git doit être installé et dans le PATH de la machine
- **Serveur Gitea** : Le remote origin pointe vers une instance Gitea locale (192.168.1.235:3125) - **Serveur Gitea** : Le remote origin pointe vers une instance Gitea locale (192.168.1.235:3125)
- **Lecture seule** : Aucune opération d'écriture vers le remote (pas de push/commit) - **Lecture seule** : Aucune opération d'écriture vers le remote (pas de push/commit)
- **Interface** : GUI tkinter (inclus dans Python, pas de dépendance externe) - **Interface** : GUI native Windows via walk (pas de console)
- **Logs** : Dossier `log/` à côté de l'exe, rotation automatique 30 jours - **Logs** : Dossier `log/` à côté de l'exe, rotation automatique 30 jours
- **Repos imbriqués** : Supporte les dépôts git imbriqués (ex: parent/enfant) via clone in-place
## Conventions ## Conventions
- Langage : Python 3, pas de dépendances externes (seulement stdlib + tkinter) - Langage : Go 1.22+
- Interface : GUI tkinter en français - GUI : github.com/lxn/walk (contrôles natifs Windows)
- Langue : Français pour l'interface utilisateur - Interface en français
- Pas de console (flag `-H windowsgui`)

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 {

Binary file not shown.

3
go.mod
View File

@@ -1,9 +1,10 @@
module gitchecker module gitchecker
go 1.22 go 1.25.0
require ( require (
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
golang.org/x/image v0.38.0
golang.org/x/sys v0.18.0 golang.org/x/sys v0.18.0
) )

2
go.sum
View File

@@ -2,6 +2,8 @@ github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65Om
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

81
gui.go
View File

@@ -1,7 +1,12 @@
package main package main
import ( import (
"bytes"
"context"
_ "embed"
"fmt" "fmt"
"image"
"image/png"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -12,8 +17,12 @@ import (
"github.com/lxn/walk" "github.com/lxn/walk"
. "github.com/lxn/walk/declarative" . "github.com/lxn/walk/declarative"
xdraw "golang.org/x/image/draw"
) )
//go:embed icon.png
var iconPNG []byte
// ── Modèle TableView ────────────────────────────────────────────────────────── // ── Modèle TableView ──────────────────────────────────────────────────────────
type RepoItem struct { type RepoItem struct {
@@ -206,6 +215,7 @@ func (m *RepoModel) hasUpdates() bool {
type App struct { type App struct {
mw *walk.MainWindow mw *walk.MainWindow
iconView *walk.ImageView
statusLabel *walk.Label statusLabel *walk.Label
tv *walk.TableView tv *walk.TableView
model *RepoModel model *RepoModel
@@ -213,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 {
@@ -242,6 +255,12 @@ func (a *App) buildAndRun() error {
Composite{ Composite{
Layout: HBox{MarginsZero: true}, Layout: HBox{MarginsZero: true},
Children: []Widget{ Children: []Widget{
ImageView{
AssignTo: &a.iconView,
MinSize: Size{Width: 24, Height: 24},
MaxSize: Size{Width: 24, Height: 24},
Mode: ImageViewModeIdeal,
},
Label{ Label{
Text: "Git Update Checker v" + VERSION, Text: "Git Update Checker v" + VERSION,
Font: Font{Bold: true, PointSize: 12}, Font: Font{Bold: true, PointSize: 12},
@@ -312,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},
@@ -322,13 +347,28 @@ func (a *App) buildAndRun() error {
return err return err
} }
// Icône fenêtre // Icône fenêtre (depuis fichier .ico externe)
if icoPath := filepath.Join(exeDir(), "icon.ico"); fileExists(icoPath) { if icoPath := filepath.Join(exeDir(), "icon.ico"); fileExists(icoPath) {
if icon, err := walk.NewIconFromFile(icoPath); err == nil { if icon, err := walk.NewIconFromFile(icoPath); err == nil {
a.mw.SetIcon(icon) a.mw.SetIcon(icon)
} }
} }
// Icône dans l'en-tête (depuis PNG embarqué dans l'exe, redimensionné 24x24)
if img, err := png.Decode(bytes.NewReader(iconPNG)); err != nil {
logWarn("Icône PNG: décodage échoué: " + err.Error())
} else {
const iconSize = 24
dst := image.NewRGBA(image.Rect(0, 0, iconSize, iconSize))
xdraw.CatmullRom.Scale(dst, dst.Bounds(), img, img.Bounds(), xdraw.Over, nil)
bmp, err := walk.NewBitmapFromImageForDPI(dst, 96)
if err != nil {
logWarn("Icône PNG: bitmap échoué: " + err.Error())
} else {
a.iconView.SetImage(bmp)
}
}
// Lancer la vérification au démarrage // Lancer la vérification au démarrage
go func() { go func() {
time.Sleep(150 * time.Millisecond) time.Sleep(150 * time.Millisecond)
@@ -510,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() {
@@ -545,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))
@@ -584,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)
@@ -597,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)
@@ -620,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()
} }
} }

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

BIN
rsrc.syso

Binary file not shown.

View File

@@ -1 +1 @@
0.7.6 0.7.9