Compare commits
25 Commits
62d6c7f89f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ba377a4e4a | |||
| 19efbe6dd7 | |||
| 9bf66f6d90 | |||
| 3f0f13147b | |||
| 2b2eb87f45 | |||
| 3ffbb550ec | |||
| 98b5187bfc | |||
| 18fe9d7186 | |||
| d8f3a29f8e | |||
| 55663e3a19 | |||
| db57cfacaf | |||
| 037f211d9d | |||
| da8fc74a68 | |||
| 30ece54758 | |||
| d03ff595ed | |||
| 8c5257e96f | |||
| af439a8e69 | |||
| 50c8ad9823 | |||
| 959298fc2d | |||
| fa17301842 | |||
| 8718b04a7d | |||
| 1a51d6b5fa | |||
| 997d82bb96 | |||
| 7d77f4d901 | |||
| 3c9a6e70eb |
@@ -12,7 +12,9 @@
|
|||||||
"Bash(python -c \"from PIL import Image; img = Image.open\\(''icon.png''\\); img.save\\(''icon.ico'', format=''ICO'', sizes=[\\(256,256\\),\\(128,128\\),\\(64,64\\),\\(32,32\\),\\(16,16\\)]\\)\")",
|
"Bash(python -c \"from PIL import Image; img = Image.open\\(''icon.png''\\); img.save\\(''icon.ico'', format=''ICO'', sizes=[\\(256,256\\),\\(128,128\\),\\(64,64\\),\\(32,32\\),\\(16,16\\)]\\)\")",
|
||||||
"Bash(pyinstaller --onefile --noconsole --name \"GitUpdateChecker\" --icon=icon.ico git_updater.py)",
|
"Bash(pyinstaller --onefile --noconsole --name \"GitUpdateChecker\" --icon=icon.ico git_updater.py)",
|
||||||
"Bash(pyinstaller --onedir --noconsole --name \"GitUpdateChecker\" --icon=icon.ico git_updater.py)",
|
"Bash(pyinstaller --onedir --noconsole --name \"GitUpdateChecker\" --icon=icon.ico git_updater.py)",
|
||||||
"Bash(\"j:/Documents/- PROJET -/Code/Lanceur-Geco/Lanceur-Geco/.gitignore\")"
|
"Bash(\"j:/Documents/- PROJET -/Code/Lanceur-Geco/Lanceur-Geco/.gitignore\")",
|
||||||
|
"Bash(pyinstaller --onefile --noconsole --runtime-tmpdir . --name \"GitUpdateChecker\" --icon=icon.ico git_updater.py)",
|
||||||
|
"Bash(python -c \"from PIL import Image; img = Image.open\\(''j:/Documents/- PROJET -/Code/Lanceur-Geco/Lanceur-Geco/icon.png''\\); print\\(f''{img.width}x{img.height}''\\)\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,8 +1,7 @@
|
|||||||
log/
|
log/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
__pycache__/
|
rsrc.syso
|
||||||
*.spec
|
*.spec
|
||||||
*.exe.old
|
*.exe.old
|
||||||
_update.bat
|
_update.bat
|
||||||
_internal/
|
|
||||||
|
|||||||
105
CLAUDE.md
105
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)
|
||||||
|
├── 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.
32
app.manifest
Normal file
32
app.manifest
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity
|
||||||
|
version="1.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
name="GitUpdateChecker"
|
||||||
|
type="win32"/>
|
||||||
|
<description>Git Update Checker</description>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df"
|
||||||
|
language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
</assembly>
|
||||||
35
build.bat
35
build.bat
@@ -1,37 +1,38 @@
|
|||||||
@echo off
|
@echo off
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo Build Git Update Checker (.exe)
|
echo Build Git Update Checker (.exe) - Go
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: Vérifier que Python est installé
|
where go >nul 2>&1
|
||||||
python --version >nul 2>&1
|
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [ERREUR] Python n'est pas installe ou pas dans le PATH.
|
echo [ERREUR] Go n'est pas installe ou pas dans le PATH.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: Installer PyInstaller si nécessaire
|
echo [*] Telechargement des dependances...
|
||||||
pip show pyinstaller >nul 2>&1
|
go mod tidy
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [*] Installation de PyInstaller...
|
echo [ERREUR] go mod tidy a echoue.
|
||||||
pip install pyinstaller
|
pause
|
||||||
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo [*] Conversion icon.png -> icon.ico...
|
echo [*] Generation du manifeste Windows (rsrc.syso)...
|
||||||
python -c "from PIL import Image; img = Image.open('icon.png'); img.save('icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(32,32),(16,16)])"
|
where rsrc >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [*] Installation de rsrc...
|
||||||
|
go install github.com/akavel/rsrc@latest
|
||||||
|
)
|
||||||
|
rsrc -manifest app.manifest -ico icon.ico -o rsrc.syso
|
||||||
|
|
||||||
echo [*] Compilation en cours...
|
echo [*] Compilation en cours...
|
||||||
echo.
|
go build -ldflags "-H windowsgui -s -w" -o GitUpdateChecker.exe .
|
||||||
|
|
||||||
pyinstaller --onefile --noconsole --name "GitUpdateChecker" --icon=icon.ico git_updater.py
|
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
if exist "dist\GitUpdateChecker.exe" (
|
if exist "GitUpdateChecker.exe" (
|
||||||
echo [*] Copie de GitUpdateChecker.exe a la racine...
|
echo [OK] GitUpdateChecker.exe cree - exe unique, aucune dependance.
|
||||||
copy /Y "dist\GitUpdateChecker.exe" "GitUpdateChecker.exe" >nul
|
|
||||||
echo [OK] Deploiement pret. Committer GitUpdateChecker.exe
|
|
||||||
) else (
|
) else (
|
||||||
echo [ERREUR] La compilation a echoue.
|
echo [ERREUR] La compilation a echoue.
|
||||||
)
|
)
|
||||||
|
|||||||
82
config.go
Normal file
82
config.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepoConfig struct {
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
Path string
|
||||||
|
Branch string // branche à suivre (défaut: "master")
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfUpdateConfig struct {
|
||||||
|
URL string
|
||||||
|
ExeName string
|
||||||
|
Branch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() ([]RepoConfig, SelfUpdateConfig, error) {
|
||||||
|
cfgPath := filepath.Join(exeDir(), "config.ini")
|
||||||
|
f, err := os.Open(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, SelfUpdateConfig{}, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var repos []RepoConfig
|
||||||
|
var su SelfUpdateConfig
|
||||||
|
section := ""
|
||||||
|
kv := map[string]string{}
|
||||||
|
|
||||||
|
flush := func() {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(section, "repo:"):
|
||||||
|
name := strings.TrimPrefix(section, "repo:")
|
||||||
|
if kv["url"] != "" && kv["path"] != "" {
|
||||||
|
repos = append(repos, RepoConfig{
|
||||||
|
Name: name,
|
||||||
|
URL: kv["url"],
|
||||||
|
Path: kv["path"],
|
||||||
|
Branch: kv["branch"], // vide = auto-détection
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case section == "self-update":
|
||||||
|
su.URL = strings.TrimRight(kv["url"], "/")
|
||||||
|
su.ExeName = kv["exe_name"]
|
||||||
|
if su.ExeName == "" {
|
||||||
|
su.ExeName = "GitUpdateChecker.exe"
|
||||||
|
}
|
||||||
|
su.Branch = kv["branch"]
|
||||||
|
if su.Branch == "" {
|
||||||
|
su.Branch = "master"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kv = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
flush()
|
||||||
|
section = strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx := strings.IndexByte(line, '='); idx > 0 {
|
||||||
|
k := strings.TrimSpace(strings.ToLower(line[:idx]))
|
||||||
|
v := strings.TrimSpace(line[idx+1:])
|
||||||
|
kv[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
|
||||||
|
return repos, su, scanner.Err()
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
[self-update]
|
[self-update]
|
||||||
url = http://192.168.1.235:3125/zogzog/Lanceur-geco
|
url = http://192.168.1.235:3125/zogzog/Lanceur-geco
|
||||||
exe_name = GitUpdateChecker.exe
|
exe_name = GitUpdateChecker.exe
|
||||||
|
branch = feature/go-rewrite
|
||||||
|
|
||||||
[repo:Scripts]
|
[repo:Scripts]
|
||||||
url = http://192.168.1.235:3125/zogzog/Scripts
|
url = http://192.168.1.235:3125/zogzog/Scripts
|
||||||
|
|||||||
473
git.go
Normal file
473
git.go
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepoResult struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
URL string
|
||||||
|
Branch string // branche configurée
|
||||||
|
Pending bool
|
||||||
|
UpToDate bool
|
||||||
|
Offline bool
|
||||||
|
NeedsClone bool
|
||||||
|
HasUpdate bool // MAJ disponible (hash local != distant)
|
||||||
|
Error string
|
||||||
|
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) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullArgs := append([]string{"-c", "safe.directory=*"}, args...)
|
||||||
|
cmd := newGitCmd(ctx, fullArgs, cwd)
|
||||||
|
|
||||||
|
var outBuf, errBuf strings.Builder
|
||||||
|
cmd.Stdout = &outBuf
|
||||||
|
cmd.Stderr = &errBuf
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
stdout = strings.TrimSpace(outBuf.String())
|
||||||
|
stderr = strings.TrimSpace(errBuf.String())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return 1, stdout, "Timeout"
|
||||||
|
}
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
return exitErr.ExitCode(), stdout, stderr
|
||||||
|
}
|
||||||
|
return -1, stdout, err.Error()
|
||||||
|
}
|
||||||
|
return 0, stdout, stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
func absRepoPath(rel string) string {
|
||||||
|
if filepath.IsAbs(rel) {
|
||||||
|
return rel
|
||||||
|
}
|
||||||
|
return filepath.Join(exeDir(), rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkRemoteOffline(stderr string) bool {
|
||||||
|
for _, kw := range []string{"could not resolve", "connection refused", "unable to connect", "timed out", "the remote end hung up", "timeout"} {
|
||||||
|
if strings.Contains(strings.ToLower(stderr), kw) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
res := RepoResult{Name: cfg.Name, URL: cfg.URL, Branch: cfg.Branch}
|
||||||
|
local := absRepoPath(cfg.Path)
|
||||||
|
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) {
|
||||||
|
// 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)
|
||||||
|
if code != 0 {
|
||||||
|
if checkRemoteOffline(stderr) {
|
||||||
|
res.Offline = true
|
||||||
|
res.Error = "Hors ligne"
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(stderr), "not found") || strings.Contains(strings.ToLower(stderr), "repository not found") {
|
||||||
|
res.Error = "Dépôt introuvable : " + cfg.URL
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
res.Error = "Erreur remote : " + stderr
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
res.NeedsClone = true
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
runGit([]string{"remote", "set-url", "origin", cfg.URL}, local, 10*time.Second)
|
||||||
|
|
||||||
|
// Hash local
|
||||||
|
_, localHash, _ := runGit([]string{"rev-parse", "HEAD"}, local, 5*time.Second)
|
||||||
|
if localHash == "" {
|
||||||
|
res.Error = "Impossible de lire le commit local"
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification rapide du remote via ls-remote (timeout court)
|
||||||
|
code, lsOut, stderr := runGit([]string{"ls-remote", "origin", "refs/heads/" + branch}, local, 15*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
if checkRemoteOffline(stderr) {
|
||||||
|
res.Offline = true
|
||||||
|
res.Error = "Hors ligne"
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
res.Error = "ls-remote: " + stderr
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraire le hash distant
|
||||||
|
remoteHash := ""
|
||||||
|
if lsOut != "" {
|
||||||
|
parts := strings.Fields(lsOut)
|
||||||
|
if len(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 == "" {
|
||||||
|
res.Error = fmt.Sprintf("Branche '%s' introuvable sur le remote", branch)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparer les hashs
|
||||||
|
if localHash != remoteHash {
|
||||||
|
res.HasUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifications locales
|
||||||
|
_, status, _ := runGit([]string{"status", "--porcelain"}, local, 5*time.Second)
|
||||||
|
if status != "" {
|
||||||
|
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.HasUpdate && res.LocalChanges == 0 && res.UntrackedFiles == 0
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func doClone(cfg RepoConfig) error {
|
||||||
|
local := absRepoPath(cfg.Path)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(local), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le dossier n'existe pas ou est vide, clone classique
|
||||||
|
entries, _ := os.ReadDir(local)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
code, _, stderr := runGit([]string{"clone", cfg.URL, local}, "", 300*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dossier non-vide sans .git : init + remote + fetch + checkout
|
||||||
|
return doCloneInPlace(cfg, local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doCloneInPlace(cfg RepoConfig, local string) error {
|
||||||
|
code, _, stderr := runGit([]string{"init"}, local, 30*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("git init: %s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, _, stderr = runGit([]string{"remote", "add", "origin", cfg.URL}, local, 10*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
// remote existe déjà, mettre à jour l'URL
|
||||||
|
runGit([]string{"remote", "set-url", "origin", cfg.URL}, local, 10*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, _, stderr = runGit([]string{"fetch", "origin"}, local, 5*time.Minute)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("fetch: %s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
branch := cfg.Branch
|
||||||
|
code, _, stderr = runGit([]string{"checkout", "origin/" + branch, "-b", branch}, local, 30*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
// Branche locale existe déjà
|
||||||
|
code, _, stderr = runGit([]string{"checkout", branch}, local, 30*time.Second)
|
||||||
|
if code == 0 {
|
||||||
|
code, _, stderr = runGit([]string{"reset", "--hard", "origin/" + branch}, local, 30*time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("checkout: %s", stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doPull(res RepoResult) error {
|
||||||
|
_, branch, _ := runGit([]string{"rev-parse", "--abbrev-ref", "HEAD"}, res.Path, 5*time.Second)
|
||||||
|
if branch == "" {
|
||||||
|
branch = "master"
|
||||||
|
}
|
||||||
|
code, _, stderr := runGit([]string{"pull", "origin", branch}, res.Path, 120*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doCheckout(res RepoResult) error {
|
||||||
|
code, _, stderr := runGit([]string{"checkout", "--", "."}, res.Path, 30*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
type ProgressInfo struct {
|
||||||
|
Phase string // ex: "Receiving objects", "Resolving deltas"
|
||||||
|
Percent float64 // 0.0 à 1.0
|
||||||
|
Current int64
|
||||||
|
Total int64
|
||||||
|
Speed string // ex: "1.2 MiB/s"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgressCallback est appelé à chaque mise à jour de la progression.
|
||||||
|
type ProgressCallback func(ProgressInfo)
|
||||||
|
|
||||||
|
// reGitProgress capture les lignes de progression git :
|
||||||
|
//
|
||||||
|
// "Receiving objects: 45% (123/456), 1.20 MiB | 500.00 KiB/s"
|
||||||
|
// "Resolving deltas: 100% (89/89), done."
|
||||||
|
var reGitProgress = regexp.MustCompile(
|
||||||
|
`(?i)([\w\s]+):\s+(\d+)%\s+\((\d+)/(\d+)\)(?:.*\|\s*(.+/s))?`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseGitProgress analyse une ligne de sortie git et renvoie un ProgressInfo.
|
||||||
|
func parseGitProgress(line string) (ProgressInfo, bool) {
|
||||||
|
m := reGitProgress.FindStringSubmatch(line)
|
||||||
|
if m == nil {
|
||||||
|
return ProgressInfo{}, false
|
||||||
|
}
|
||||||
|
pct, _ := strconv.Atoi(m[2])
|
||||||
|
cur, _ := strconv.ParseInt(m[3], 10, 64)
|
||||||
|
tot, _ := strconv.ParseInt(m[4], 10, 64)
|
||||||
|
speed := strings.TrimSpace(m[5])
|
||||||
|
return ProgressInfo{
|
||||||
|
Phase: strings.TrimSpace(m[1]),
|
||||||
|
Percent: float64(pct) / 100.0,
|
||||||
|
Current: cur,
|
||||||
|
Total: tot,
|
||||||
|
Speed: speed,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func runGitWithProgress(parent context.Context, args []string, cwd string, timeout time.Duration, cb ProgressCallback) (int, string, string) {
|
||||||
|
ctx, cancel := context.WithTimeout(parent, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fullArgs := append([]string{"-c", "safe.directory=*"}, args...)
|
||||||
|
cmd := newGitCmd(ctx, fullArgs, cwd)
|
||||||
|
|
||||||
|
var outBuf strings.Builder
|
||||||
|
cmd.Stdout = &outBuf
|
||||||
|
|
||||||
|
// Pipe stderr pour lire la progression en temps réel
|
||||||
|
stderrPipe, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return -1, "", err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return -1, "", err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lire stderr byte par byte pour détecter les \r (git écrase la ligne)
|
||||||
|
var stderrBuf strings.Builder
|
||||||
|
reader := bufio.NewReader(stderrPipe)
|
||||||
|
var lineBuf strings.Builder
|
||||||
|
|
||||||
|
for {
|
||||||
|
b, err := reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
stderrBuf.WriteString(err.Error())
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
stderrBuf.WriteByte(b)
|
||||||
|
|
||||||
|
if b == '\r' || b == '\n' {
|
||||||
|
line := lineBuf.String()
|
||||||
|
lineBuf.Reset()
|
||||||
|
if cb != nil && line != "" {
|
||||||
|
if info, ok := parseGitProgress(line); ok {
|
||||||
|
cb(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lineBuf.WriteByte(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dernière ligne sans \r\n
|
||||||
|
if lineBuf.Len() > 0 && cb != nil {
|
||||||
|
if info, ok := parseGitProgress(lineBuf.String()); ok {
|
||||||
|
cb(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
stdout := strings.TrimSpace(outBuf.String())
|
||||||
|
stderr := strings.TrimSpace(stderrBuf.String())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() == context.Canceled {
|
||||||
|
return 1, stdout, "Annulé"
|
||||||
|
}
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return 1, stdout, "Timeout"
|
||||||
|
}
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
return exitErr.ExitCode(), stdout, stderr
|
||||||
|
}
|
||||||
|
return -1, stdout, err.Error()
|
||||||
|
}
|
||||||
|
return 0, stdout, stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
// doCloneWithProgress clone un dépôt avec suivi de progression.
|
||||||
|
// 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)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(local), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le dossier n'existe pas ou est vide, clone classique avec progression
|
||||||
|
entries, _ := os.ReadDir(local)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
args := []string{"clone", "--progress"}
|
||||||
|
if branch != "" {
|
||||||
|
args = append(args, "-b", branch)
|
||||||
|
}
|
||||||
|
args = append(args, cfg.URL, local)
|
||||||
|
code, _, stderr := runGitWithProgress(
|
||||||
|
ctx, args, "", 2*time.Hour, cb,
|
||||||
|
)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dossier non-vide sans .git : init + remote + fetch avec progression + checkout
|
||||||
|
code, _, stderr := runGit([]string{"init"}, local, 30*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("git init: %s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, _, stderr = runGit([]string{"remote", "add", "origin", cfg.URL}, local, 10*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
runGit([]string{"remote", "set-url", "origin", cfg.URL}, local, 10*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, _, stderr = runGitWithProgress(
|
||||||
|
ctx, []string{"fetch", "--progress", "origin"},
|
||||||
|
local, 2*time.Hour, cb,
|
||||||
|
)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("fetch: %s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, _, stderr = runGit([]string{"checkout", "origin/" + branch, "-b", branch}, local, 30*time.Second)
|
||||||
|
if code != 0 {
|
||||||
|
code, _, stderr = runGit([]string{"checkout", branch}, local, 30*time.Second)
|
||||||
|
if code == 0 {
|
||||||
|
code, _, stderr = runGit([]string{"reset", "--hard", "origin/" + branch}, local, 30*time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("checkout: %s", stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// doPullWithProgress fait un pull avec suivi de progression.
|
||||||
|
func doPullWithProgress(ctx context.Context, res RepoResult, cb ProgressCallback) error {
|
||||||
|
_, branch, _ := runGit([]string{"rev-parse", "--abbrev-ref", "HEAD"}, res.Path, 5*time.Second)
|
||||||
|
if branch == "" {
|
||||||
|
branch = "master"
|
||||||
|
}
|
||||||
|
code, _, stderr := runGitWithProgress(
|
||||||
|
ctx, []string{"pull", "--progress", "origin", branch},
|
||||||
|
res.Path, 2*time.Hour, cb,
|
||||||
|
)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
1029
git_updater.py
1029
git_updater.py
File diff suppressed because it is too large
Load Diff
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module gitchecker
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
|
||||||
|
golang.org/x/image v0.38.0
|
||||||
|
golang.org/x/sys v0.18.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
|
||||||
|
)
|
||||||
11
go.sum
Normal file
11
go.sum
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
|
||||||
|
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/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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||||
751
gui.go
Normal file
751
gui.go
Normal file
@@ -0,0 +1,751 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lxn/walk"
|
||||||
|
. "github.com/lxn/walk/declarative"
|
||||||
|
xdraw "golang.org/x/image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed icon.png
|
||||||
|
var iconPNG []byte
|
||||||
|
|
||||||
|
// ── Modèle TableView ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type RepoItem struct {
|
||||||
|
result RepoResult
|
||||||
|
progress float64 // 0.0 à 1.0
|
||||||
|
progressText string // ex: "Réception 45% (1.2 Go/2.5 Go)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *RepoItem) statusText() string {
|
||||||
|
r := it.result
|
||||||
|
if r.Pending {
|
||||||
|
return "Vérification..."
|
||||||
|
}
|
||||||
|
if r.Error != "" {
|
||||||
|
return r.Error
|
||||||
|
}
|
||||||
|
if r.NeedsClone {
|
||||||
|
return "À cloner"
|
||||||
|
}
|
||||||
|
if r.UpToDate {
|
||||||
|
return "À jour"
|
||||||
|
}
|
||||||
|
var parts []string
|
||||||
|
if r.HasUpdate {
|
||||||
|
parts = append(parts, "MAJ disponible")
|
||||||
|
}
|
||||||
|
if r.LocalChanges > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d modif. locale(s)", r.LocalChanges))
|
||||||
|
}
|
||||||
|
if r.UntrackedFiles > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d fichier(s) en trop", r.UntrackedFiles))
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "À jour"
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// progressBarText génère une barre de progression visuelle en Unicode.
|
||||||
|
// Ex: "████████░░░░ 67% Réception objets"
|
||||||
|
func progressBarText(pct float64, width int, label string) string {
|
||||||
|
if width <= 0 {
|
||||||
|
width = 20
|
||||||
|
}
|
||||||
|
filled := int(pct * float64(width))
|
||||||
|
if filled > width {
|
||||||
|
filled = width
|
||||||
|
}
|
||||||
|
bar := strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
|
||||||
|
pctInt := int(pct * 100)
|
||||||
|
if pctInt > 100 {
|
||||||
|
pctInt = 100
|
||||||
|
}
|
||||||
|
if label != "" {
|
||||||
|
return fmt.Sprintf("%s %3d%% %s", bar, pctInt, label)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %3d%%", bar, pctInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *RepoItem) textColor() walk.Color {
|
||||||
|
r := it.result
|
||||||
|
if r.Pending {
|
||||||
|
return walk.RGB(120, 120, 120)
|
||||||
|
}
|
||||||
|
if r.Error != "" {
|
||||||
|
return walk.RGB(200, 50, 50)
|
||||||
|
}
|
||||||
|
if r.NeedsClone {
|
||||||
|
return walk.RGB(180, 120, 0)
|
||||||
|
}
|
||||||
|
if r.UpToDate {
|
||||||
|
return walk.RGB(0, 150, 0)
|
||||||
|
}
|
||||||
|
return walk.RGB(180, 120, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepoModel struct {
|
||||||
|
walk.TableModelBase
|
||||||
|
mu sync.RWMutex
|
||||||
|
items []*RepoItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRepoModel(cfgs []RepoConfig) *RepoModel {
|
||||||
|
m := &RepoModel{}
|
||||||
|
m.items = make([]*RepoItem, len(cfgs))
|
||||||
|
for i, c := range cfgs {
|
||||||
|
m.items[i] = &RepoItem{result: RepoResult{Name: c.Name, Pending: true}}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RepoModel) RowCount() int {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return len(m.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RepoModel) Value(row, col int) interface{} {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if row >= len(m.items) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
it := m.items[row]
|
||||||
|
switch col {
|
||||||
|
case 0:
|
||||||
|
return it.result.Name
|
||||||
|
case 1:
|
||||||
|
return it.statusText()
|
||||||
|
case 2:
|
||||||
|
return it.progressText
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RepoModel) StyleCell(style *walk.CellStyle) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
row := style.Row()
|
||||||
|
if row >= len(m.items) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
col := style.Col()
|
||||||
|
if col == 1 {
|
||||||
|
style.TextColor = m.items[row].textColor()
|
||||||
|
}
|
||||||
|
if col == 2 {
|
||||||
|
it := m.items[row]
|
||||||
|
if it.progress > 0 && it.progress < 1.0 {
|
||||||
|
style.TextColor = walk.RGB(0, 100, 180)
|
||||||
|
} else if it.progress >= 1.0 {
|
||||||
|
style.TextColor = walk.RGB(0, 150, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RepoModel) setResult(row int, res RepoResult) {
|
||||||
|
m.mu.Lock()
|
||||||
|
if row < len(m.items) {
|
||||||
|
m.items[row].result = res
|
||||||
|
m.items[row].progress = 0
|
||||||
|
m.items[row].progressText = ""
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
m.PublishRowChanged(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RepoModel) setProgress(row int, pct float64, text string) {
|
||||||
|
m.mu.Lock()
|
||||||
|
if row < len(m.items) {
|
||||||
|
m.items[row].progress = pct
|
||||||
|
m.items[row].progressText = text
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
m.PublishRowChanged(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RepoModel) reset(cfgs []RepoConfig) {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.items = make([]*RepoItem, len(cfgs))
|
||||||
|
for i, c := range cfgs {
|
||||||
|
m.items[i] = &RepoItem{result: RepoResult{Name: c.Name, Pending: true}}
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
m.PublishRowsReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RepoModel) getResult(row int) (RepoResult, bool) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if row < 0 || row >= len(m.items) {
|
||||||
|
return RepoResult{}, false
|
||||||
|
}
|
||||||
|
return m.items[row].result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RepoModel) hasUpdates() bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
for _, it := range m.items {
|
||||||
|
r := it.result
|
||||||
|
if !r.Pending && r.Error == "" && !r.Offline && (r.HasUpdate || r.LocalChanges > 0 || r.UntrackedFiles > 0 || r.NeedsClone) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Application ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
mw *walk.MainWindow
|
||||||
|
iconView *walk.ImageView
|
||||||
|
statusLabel *walk.Label
|
||||||
|
tv *walk.TableView
|
||||||
|
model *RepoModel
|
||||||
|
logEdit *walk.TextEdit
|
||||||
|
btnRefresh *walk.PushButton
|
||||||
|
btnUpdateAll *walk.PushButton
|
||||||
|
btnAction *walk.PushButton
|
||||||
|
btnStop *walk.PushButton
|
||||||
|
|
||||||
|
reposConfig []RepoConfig
|
||||||
|
suConfig SelfUpdateConfig
|
||||||
|
checking atomic.Bool
|
||||||
|
cancelMu sync.Mutex
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func runApp() error {
|
||||||
|
app := &App{}
|
||||||
|
var err error
|
||||||
|
app.reposConfig, app.suConfig, err = loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
logWarn("Config: " + err.Error())
|
||||||
|
}
|
||||||
|
app.model = newRepoModel(app.reposConfig)
|
||||||
|
return app.buildAndRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) buildAndRun() error {
|
||||||
|
if err := (MainWindow{
|
||||||
|
AssignTo: &a.mw,
|
||||||
|
Title: "Git Update Checker v" + VERSION,
|
||||||
|
MinSize: Size{Width: 750, Height: 400},
|
||||||
|
Size: Size{Width: 950, Height: 600},
|
||||||
|
Layout: VBox{Margins: Margins{Left: 10, Top: 10, Right: 10, Bottom: 10}},
|
||||||
|
Children: []Widget{
|
||||||
|
// En-tête
|
||||||
|
Composite{
|
||||||
|
Layout: HBox{MarginsZero: true},
|
||||||
|
Children: []Widget{
|
||||||
|
ImageView{
|
||||||
|
AssignTo: &a.iconView,
|
||||||
|
MinSize: Size{Width: 24, Height: 24},
|
||||||
|
MaxSize: Size{Width: 24, Height: 24},
|
||||||
|
Mode: ImageViewModeIdeal,
|
||||||
|
},
|
||||||
|
Label{
|
||||||
|
Text: "Git Update Checker v" + VERSION,
|
||||||
|
Font: Font{Bold: true, PointSize: 12},
|
||||||
|
},
|
||||||
|
HSpacer{},
|
||||||
|
Label{AssignTo: &a.statusLabel, Text: "Démarrage..."},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Zone principale : table + log
|
||||||
|
VSplitter{
|
||||||
|
Children: []Widget{
|
||||||
|
TableView{
|
||||||
|
AssignTo: &a.tv,
|
||||||
|
AlternatingRowBG: true,
|
||||||
|
ColumnsOrderable: false,
|
||||||
|
Columns: []TableViewColumn{
|
||||||
|
{Title: "Dépôt", Width: 150},
|
||||||
|
{Title: "Statut", Width: 200},
|
||||||
|
{Title: "Progression", Width: 350},
|
||||||
|
},
|
||||||
|
Model: a.model,
|
||||||
|
OnCurrentIndexChanged: a.onSelectionChanged,
|
||||||
|
OnItemActivated: a.doAction,
|
||||||
|
},
|
||||||
|
Composite{
|
||||||
|
Layout: VBox{MarginsZero: true},
|
||||||
|
Children: []Widget{
|
||||||
|
Composite{
|
||||||
|
Layout: HBox{MarginsZero: true},
|
||||||
|
Children: []Widget{
|
||||||
|
Label{Text: "Journal", Font: Font{Bold: true}},
|
||||||
|
HSpacer{},
|
||||||
|
PushButton{
|
||||||
|
Text: "Effacer",
|
||||||
|
MaxSize: Size{Width: 65},
|
||||||
|
OnClicked: func() { a.logEdit.SetText("") },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TextEdit{
|
||||||
|
AssignTo: &a.logEdit,
|
||||||
|
ReadOnly: true,
|
||||||
|
VScroll: true,
|
||||||
|
Font: Font{Family: "Consolas", PointSize: 9},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Boutons
|
||||||
|
Composite{
|
||||||
|
Layout: HBox{MarginsZero: true},
|
||||||
|
Children: []Widget{
|
||||||
|
PushButton{
|
||||||
|
AssignTo: &a.btnRefresh,
|
||||||
|
Text: "Rafraîchir",
|
||||||
|
OnClicked: a.startCheck,
|
||||||
|
},
|
||||||
|
PushButton{
|
||||||
|
AssignTo: &a.btnUpdateAll,
|
||||||
|
Text: "Tout mettre à jour",
|
||||||
|
Enabled: false,
|
||||||
|
OnClicked: a.updateAll,
|
||||||
|
},
|
||||||
|
PushButton{
|
||||||
|
AssignTo: &a.btnAction,
|
||||||
|
Text: "Mettre à jour",
|
||||||
|
Enabled: false,
|
||||||
|
OnClicked: a.doAction,
|
||||||
|
},
|
||||||
|
PushButton{
|
||||||
|
AssignTo: &a.btnStop,
|
||||||
|
Text: "Arrêter",
|
||||||
|
Enabled: false,
|
||||||
|
OnClicked: a.stopOperations,
|
||||||
|
},
|
||||||
|
HSpacer{},
|
||||||
|
PushButton{Text: "config.ini", OnClicked: a.openConfig},
|
||||||
|
PushButton{Text: "Logs", OnClicked: a.openLogs},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}.Create()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icône fenêtre (depuis fichier .ico externe)
|
||||||
|
if icoPath := filepath.Join(exeDir(), "icon.ico"); fileExists(icoPath) {
|
||||||
|
if icon, err := walk.NewIconFromFile(icoPath); err == nil {
|
||||||
|
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
|
||||||
|
go func() {
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
a.mw.Synchronize(a.checkSelfUpdateThenRepos)
|
||||||
|
}()
|
||||||
|
|
||||||
|
a.mw.Run()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Vérifications ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (a *App) checkSelfUpdateThenRepos() {
|
||||||
|
a.setStatus("Vérification auto-update...")
|
||||||
|
go func() {
|
||||||
|
needs, info, err := checkSelfUpdate(a.suConfig)
|
||||||
|
a.mw.Synchronize(func() {
|
||||||
|
if err != nil {
|
||||||
|
logWarn("Auto-update: " + err.Error())
|
||||||
|
}
|
||||||
|
if needs {
|
||||||
|
ans := walk.MsgBox(a.mw,
|
||||||
|
"Mise à jour disponible",
|
||||||
|
info+"\n\nTélécharger maintenant ?",
|
||||||
|
walk.MsgBoxYesNo|walk.MsgBoxIconQuestion,
|
||||||
|
)
|
||||||
|
if ans == walk.DlgCmdYes {
|
||||||
|
a.doSelfUpdate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.startCheck()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) startCheck() {
|
||||||
|
if !a.checking.CompareAndSwap(false, true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.btnRefresh.SetEnabled(false)
|
||||||
|
a.btnUpdateAll.SetEnabled(false)
|
||||||
|
a.btnAction.SetEnabled(false)
|
||||||
|
a.model.reset(a.reposConfig)
|
||||||
|
a.setStatus(fmt.Sprintf("Vérification 0/%d...", len(a.reposConfig)))
|
||||||
|
logInfo("Vérification des dépôts...")
|
||||||
|
|
||||||
|
done := atomic.Int32{}
|
||||||
|
total := int32(len(a.reposConfig))
|
||||||
|
|
||||||
|
for i, cfg := range a.reposConfig {
|
||||||
|
i, cfg := i, cfg
|
||||||
|
go func() {
|
||||||
|
res := checkRepo(cfg)
|
||||||
|
a.mw.Synchronize(func() {
|
||||||
|
a.model.setResult(i, res)
|
||||||
|
a.appendLog(logLineForResult(res))
|
||||||
|
logInfo(fmt.Sprintf("[%s] %s", res.Name, logLineForResult(res)))
|
||||||
|
|
||||||
|
n := done.Add(1)
|
||||||
|
if int32(n) == total {
|
||||||
|
a.onCheckDone()
|
||||||
|
} else {
|
||||||
|
a.setStatus(fmt.Sprintf("Vérification %d/%d...", n, total))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if total == 0 {
|
||||||
|
a.onCheckDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) onCheckDone() {
|
||||||
|
a.checking.Store(false)
|
||||||
|
a.btnRefresh.SetEnabled(true)
|
||||||
|
a.btnUpdateAll.SetEnabled(a.model.hasUpdates())
|
||||||
|
a.setStatus(fmt.Sprintf("Dernière vérification : %s", time.Now().Format("15:04:05")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func logLineForResult(r RepoResult) string {
|
||||||
|
if r.Error != "" {
|
||||||
|
return r.Error
|
||||||
|
}
|
||||||
|
if r.NeedsClone {
|
||||||
|
return "À cloner"
|
||||||
|
}
|
||||||
|
if r.UpToDate {
|
||||||
|
return "À jour"
|
||||||
|
}
|
||||||
|
var parts []string
|
||||||
|
if r.HasUpdate {
|
||||||
|
parts = append(parts, "MAJ disponible")
|
||||||
|
}
|
||||||
|
if r.LocalChanges > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d modif. locale(s)", r.LocalChanges))
|
||||||
|
}
|
||||||
|
if r.UntrackedFiles > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d fichier(s) en trop", r.UntrackedFiles))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// recheckOne re-vérifie un seul dépôt sans toucher aux autres.
|
||||||
|
func (a *App) recheckOne(idx int) {
|
||||||
|
if idx < 0 || idx >= len(a.reposConfig) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg := a.reposConfig[idx]
|
||||||
|
a.model.setResult(idx, RepoResult{Name: cfg.Name, Pending: true})
|
||||||
|
go func() {
|
||||||
|
res := checkRepo(cfg)
|
||||||
|
a.mw.Synchronize(func() {
|
||||||
|
a.model.setResult(idx, res)
|
||||||
|
a.btnUpdateAll.SetEnabled(a.model.hasUpdates())
|
||||||
|
a.onSelectionChanged()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// Le callback est appelé depuis un goroutine git et synchronise l'UI via mw.Synchronize.
|
||||||
|
func (a *App) makeProgressCB(row int) ProgressCallback {
|
||||||
|
// Limiter les mises à jour UI (max ~10/s) pour ne pas surcharger
|
||||||
|
var lastUpdate time.Time
|
||||||
|
return func(info ProgressInfo) {
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(lastUpdate) < 100*time.Millisecond && info.Percent < 1.0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastUpdate = now
|
||||||
|
|
||||||
|
label := info.Phase
|
||||||
|
if info.Speed != "" {
|
||||||
|
label += " " + info.Speed
|
||||||
|
}
|
||||||
|
text := progressBarText(info.Percent, 20, label)
|
||||||
|
a.mw.Synchronize(func() {
|
||||||
|
a.model.setProgress(row, info.Percent, text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (a *App) onSelectionChanged() {
|
||||||
|
idx := a.tv.CurrentIndex()
|
||||||
|
res, ok := a.model.getResult(idx)
|
||||||
|
if !ok || res.Pending || res.Offline || res.Error != "" {
|
||||||
|
a.btnAction.SetEnabled(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.NeedsClone {
|
||||||
|
a.btnAction.SetText("Cloner")
|
||||||
|
a.btnAction.SetEnabled(true)
|
||||||
|
} else if res.HasUpdate || res.LocalChanges > 0 || res.UntrackedFiles > 0 {
|
||||||
|
a.btnAction.SetText("Mettre à jour")
|
||||||
|
a.btnAction.SetEnabled(true)
|
||||||
|
} else {
|
||||||
|
a.btnAction.SetEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) doAction() {
|
||||||
|
idx := a.tv.CurrentIndex()
|
||||||
|
res, ok := a.model.getResult(idx)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg := a.reposConfig[idx]
|
||||||
|
|
||||||
|
// Si uniquement des fichiers en trop, proposer directement le nettoyage
|
||||||
|
if res.UntrackedFiles > 0 && !res.HasUpdate && res.LocalChanges == 0 && !res.NeedsClone {
|
||||||
|
a.proposeClean(idx, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.btnAction.SetEnabled(false)
|
||||||
|
a.btnStop.SetEnabled(true)
|
||||||
|
a.appendLog(fmt.Sprintf("[%s] Mise à jour en cours...", res.Name))
|
||||||
|
|
||||||
|
ctx := a.newCancelCtx()
|
||||||
|
cb := a.makeProgressCB(idx)
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
if res.NeedsClone {
|
||||||
|
err = doCloneWithProgress(ctx, cfg, res.Branch, cb)
|
||||||
|
} else {
|
||||||
|
if res.LocalChanges > 0 {
|
||||||
|
err = doCheckout(res)
|
||||||
|
}
|
||||||
|
if err == nil && res.HasUpdate {
|
||||||
|
err = doPullWithProgress(ctx, res, cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.mw.Synchronize(func() {
|
||||||
|
a.btnStop.SetEnabled(false)
|
||||||
|
if err != nil {
|
||||||
|
a.model.setProgress(idx, 0, "Erreur")
|
||||||
|
a.appendLog(fmt.Sprintf("[%s] Erreur: %v", res.Name, err))
|
||||||
|
logError(fmt.Sprintf("[%s] %v", res.Name, err))
|
||||||
|
} else {
|
||||||
|
a.model.setProgress(idx, 1.0, progressBarText(1.0, 20, "Terminé"))
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) updateAll() {
|
||||||
|
a.btnUpdateAll.SetEnabled(false)
|
||||||
|
a.btnRefresh.SetEnabled(false)
|
||||||
|
a.btnStop.SetEnabled(true)
|
||||||
|
pending := atomic.Int32{}
|
||||||
|
ctx := a.newCancelCtx()
|
||||||
|
|
||||||
|
for i, cfg := range a.reposConfig {
|
||||||
|
res, ok := a.model.getResult(i)
|
||||||
|
if !ok || res.Pending || res.UpToDate || res.Offline || res.Error != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pending.Add(1)
|
||||||
|
i, cfg, res := i, cfg, res
|
||||||
|
cb := a.makeProgressCB(i)
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
if res.NeedsClone {
|
||||||
|
err = doCloneWithProgress(ctx, cfg, res.Branch, cb)
|
||||||
|
} else {
|
||||||
|
if res.LocalChanges > 0 {
|
||||||
|
err = doCheckout(res)
|
||||||
|
}
|
||||||
|
if err == nil && res.HasUpdate {
|
||||||
|
err = doPullWithProgress(ctx, res, cb)
|
||||||
|
}
|
||||||
|
if err == nil && res.UntrackedFiles > 0 {
|
||||||
|
err = doClean(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.mw.Synchronize(func() {
|
||||||
|
if err != nil {
|
||||||
|
a.model.setProgress(i, 0, "Erreur")
|
||||||
|
logError(fmt.Sprintf("[%s] %v", res.Name, err))
|
||||||
|
a.appendLog(fmt.Sprintf("[%s] Erreur: %v", res.Name, err))
|
||||||
|
} else {
|
||||||
|
a.model.setProgress(i, 1.0, progressBarText(1.0, 20, "Terminé"))
|
||||||
|
logInfo(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
||||||
|
a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
||||||
|
}
|
||||||
|
if pending.Add(-1) == 0 {
|
||||||
|
a.btnStop.SetEnabled(false)
|
||||||
|
a.startCheck()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if pending.Load() == 0 {
|
||||||
|
a.btnStop.SetEnabled(false)
|
||||||
|
a.startCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auto-update programme ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (a *App) doSelfUpdate() {
|
||||||
|
a.setStatus("Téléchargement de la mise à jour...")
|
||||||
|
go func() {
|
||||||
|
err := doSelfUpdate(a.suConfig)
|
||||||
|
a.mw.Synchronize(func() {
|
||||||
|
if err != nil {
|
||||||
|
walk.MsgBox(a.mw, "Erreur", "Mise à jour échouée :\n"+err.Error(), walk.MsgBoxIconError)
|
||||||
|
logError("Auto-update: " + err.Error())
|
||||||
|
a.startCheck()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logInfo("Auto-update: mise à jour appliquée, redémarrage...")
|
||||||
|
walk.MsgBox(a.mw, "Mise à jour", "Mise à jour installée.\nLe programme va redémarrer.", walk.MsgBoxIconInformation)
|
||||||
|
exePath, _ := os.Executable()
|
||||||
|
relaunchAfterUpdate(exePath)
|
||||||
|
a.mw.Close()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Utilitaires GUI ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (a *App) setStatus(text string) {
|
||||||
|
a.statusLabel.SetText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) appendLog(line string) {
|
||||||
|
ts := time.Now().Format("15:04:05")
|
||||||
|
current := a.logEdit.Text()
|
||||||
|
if current != "" {
|
||||||
|
current += "\r\n"
|
||||||
|
}
|
||||||
|
a.logEdit.SetText(current + "[" + ts + "] " + line)
|
||||||
|
// Scroller en bas
|
||||||
|
a.logEdit.SendMessage(0x0115 /*WM_VSCROLL*/, 7 /*SB_BOTTOM*/, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) openConfig() {
|
||||||
|
p := filepath.Join(exeDir(), "config.ini")
|
||||||
|
exec.Command("notepad", p).Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) openLogs() {
|
||||||
|
p := filepath.Join(exeDir(), "log")
|
||||||
|
exec.Command("explorer", p).Start()
|
||||||
|
}
|
||||||
BIN
icon.png
BIN
icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 793 KiB |
44
logger.go
Normal file
44
logger.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logFile *os.File
|
||||||
|
|
||||||
|
func initLogger() {
|
||||||
|
dir := filepath.Join(exeDir(), "log")
|
||||||
|
os.MkdirAll(dir, 0755)
|
||||||
|
|
||||||
|
// Nettoyage logs > 30 jours
|
||||||
|
entries, _ := os.ReadDir(dir)
|
||||||
|
cutoff := time.Now().AddDate(0, 0, -30)
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() {
|
||||||
|
if info, err := e.Info(); err == nil && info.ModTime().Before(cutoff) {
|
||||||
|
os.Remove(filepath.Join(dir, e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logPath := filepath.Join(dir, time.Now().Format("2006-01-02")+".log")
|
||||||
|
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logFile = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func logMsg(level, msg string) {
|
||||||
|
line := fmt.Sprintf("[%s] %-5s %s\n", time.Now().Format("15:04:05"), level, msg)
|
||||||
|
if logFile != nil {
|
||||||
|
logFile.WriteString(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logInfo(msg string) { logMsg("INFO", msg) }
|
||||||
|
func logWarn(msg string) { logMsg("WARN", msg) }
|
||||||
|
func logError(msg string) { logMsg("ERROR", msg) }
|
||||||
36
main.go
Normal file
36
main.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/lxn/walk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const VERSION = "0.7.9"
|
||||||
|
|
||||||
|
func exeDir() string {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
return filepath.Dir(exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(p string) bool {
|
||||||
|
_, err := os.Stat(p)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
initLogger()
|
||||||
|
logInfo(fmt.Sprintf("=== Demarrage Git Update Checker v%s ===", VERSION))
|
||||||
|
|
||||||
|
if err := runApp(); err != nil {
|
||||||
|
walk.MsgBox(nil, "Erreur fatale", err.Error(), walk.MsgBoxIconError)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
platform_windows.go
Normal file
40
platform_windows.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createNoWindow = 0x08000000
|
||||||
|
|
||||||
|
// newGitCmd crée une commande git sans fenêtre console.
|
||||||
|
func newGitCmd(ctx context.Context, args []string, cwd string) *exec.Cmd {
|
||||||
|
cmd := exec.CommandContext(ctx, "git", args...)
|
||||||
|
if cwd != "" {
|
||||||
|
cmd.Dir = cwd
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr = &windows.SysProcAttr{
|
||||||
|
CreationFlags: createNoWindow,
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// relaunchAfterUpdate crée un batch qui attend 1s, relance l'exe et nettoie le .old.
|
||||||
|
func relaunchAfterUpdate(exePath string) {
|
||||||
|
oldPath := exePath + ".old"
|
||||||
|
batPath := exePath + "_update.bat"
|
||||||
|
content := fmt.Sprintf(
|
||||||
|
"@echo off\r\ntimeout /t 1 /nobreak >nul\r\nstart \"\" \"%s\"\r\ndel \"%s\" 2>nul\r\ndel \"%%~f0\"\r\n",
|
||||||
|
exePath, oldPath,
|
||||||
|
)
|
||||||
|
os.WriteFile(batPath, []byte(content), 0644)
|
||||||
|
cmd := exec.Command("cmd", "/c", batPath)
|
||||||
|
cmd.SysProcAttr = &windows.SysProcAttr{CreationFlags: createNoWindow}
|
||||||
|
cmd.Start()
|
||||||
|
}
|
||||||
95
selfupdate.go
Normal file
95
selfupdate.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseVersion(v string) [3]int {
|
||||||
|
var t [3]int
|
||||||
|
for i, p := range strings.SplitN(strings.TrimSpace(v), ".", 3) {
|
||||||
|
fmt.Sscanf(p, "%d", &t[i])
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionGreater(remote, local string) bool {
|
||||||
|
r, l := parseVersion(remote), parseVersion(local)
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if r[i] > l[i] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if r[i] < l[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSelfUpdate(cfg SelfUpdateConfig) (needsUpdate bool, info string, err error) {
|
||||||
|
if cfg.URL == "" {
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("%s/raw/branch/%s/version.txt", cfg.URL, cfg.Branch)
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
data, _ := io.ReadAll(resp.Body)
|
||||||
|
remote := strings.TrimSpace(string(data))
|
||||||
|
if !versionGreater(remote, VERSION) {
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
return true, fmt.Sprintf("Version actuelle : %s\nVersion disponible : %s", VERSION, remote), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doSelfUpdate(cfg SelfUpdateConfig) error {
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newPath := exePath + ".new"
|
||||||
|
oldPath := exePath + ".old"
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/raw/branch/%s/%s", cfg.URL, cfg.Branch, cfg.ExeName)
|
||||||
|
client := &http.Client{Timeout: 120 * time.Second}
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
f, err := os.Create(newPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n, copyErr := io.Copy(f, resp.Body)
|
||||||
|
f.Close()
|
||||||
|
if copyErr != nil {
|
||||||
|
os.Remove(newPath)
|
||||||
|
return copyErr
|
||||||
|
}
|
||||||
|
if n < 1000 {
|
||||||
|
os.Remove(newPath)
|
||||||
|
return fmt.Errorf("fichier telecharge invalide (%d octets)", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer le Mark of the Web (Zone.Identifier)
|
||||||
|
os.Remove(newPath + ":Zone.Identifier")
|
||||||
|
|
||||||
|
if err := os.Rename(exePath, oldPath); err != nil {
|
||||||
|
os.Remove(newPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Rename(newPath, exePath); err != nil {
|
||||||
|
os.Rename(oldPath, exePath) // restaurer
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
0.6.2
|
0.7.9
|
||||||
|
|||||||
Reference in New Issue
Block a user