Compare commits
7 Commits
18fe9d7186
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ba377a4e4a | |||
| 19efbe6dd7 | |||
| 9bf66f6d90 | |||
| 3f0f13147b | |||
| 2b2eb87f45 | |||
| 3ffbb550ec | |||
| 98b5187bfc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,7 +1,7 @@
|
|||||||
log/
|
log/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
__pycache__/
|
rsrc.syso
|
||||||
*.spec
|
*.spec
|
||||||
*.exe.old
|
*.exe.old
|
||||||
_update.bat
|
_update.bat
|
||||||
|
|||||||
107
CLAUDE.md
107
CLAUDE.md
@@ -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)
|
||||||
├── config.ini # Configuration multi-repo
|
├── git.go # Opérations git (check, clone, pull, checkout, clean)
|
||||||
├── build.bat # Script de compilation en .exe via PyInstaller
|
├── gui.go # Interface graphique (walk/TableView)
|
||||||
├── log/ # Dossier de logs (créé automatiquement, 1 fichier par jour)
|
├── 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
|
||||||
|
├── build.bat # Script de compilation en .exe
|
||||||
|
├── 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.
@@ -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":
|
||||||
|
|||||||
76
git.go
76
git.go
@@ -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,9 +157,27 @@ 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 == "" {
|
if remoteHash == "" {
|
||||||
res.Error = fmt.Sprintf("Branche '%s' introuvable sur le remote", branch)
|
detected := detectDefaultBranch("origin", local)
|
||||||
return res
|
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 == "" {
|
||||||
|
res.Error = fmt.Sprintf("Branche '%s' introuvable sur le remote", branch)
|
||||||
|
return res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comparer les hashs
|
// Comparer les hashs
|
||||||
@@ -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 {
|
||||||
|
|||||||
BIN
gitchecker.exe
BIN
gitchecker.exe
Binary file not shown.
3
go.mod
3
go.mod
@@ -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
2
go.sum
@@ -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
81
gui.go
@@ -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
BIN
icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 793 KiB |
BIN
icon_small.png
BIN
icon_small.png
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
2
main.go
2
main.go
@@ -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()
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.7.6
|
0.7.9
|
||||||
|
|||||||
Reference in New Issue
Block a user