Maj branch
This commit is contained in:
Binary file not shown.
152
git.go
152
git.go
@@ -1,12 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -71,7 +75,7 @@ func checkRepo(cfg RepoConfig) RepoResult {
|
|||||||
|
|
||||||
runGit([]string{"remote", "set-url", "origin", cfg.URL}, local, 10*time.Second)
|
runGit([]string{"remote", "set-url", "origin", cfg.URL}, local, 10*time.Second)
|
||||||
|
|
||||||
code, _, stderr := runGit([]string{"fetch", "origin"}, local, 60*time.Second)
|
code, _, stderr := runGit([]string{"fetch", "origin"}, local, 5*time.Minute)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
for _, kw := range []string{"could not resolve", "connection refused", "unable to connect", "timed out", "the remote end hung up"} {
|
for _, kw := range []string{"could not resolve", "connection refused", "unable to connect", "timed out", "the remote end hung up"} {
|
||||||
if strings.Contains(strings.ToLower(stderr), kw) {
|
if strings.Contains(strings.ToLower(stderr), kw) {
|
||||||
@@ -132,3 +136,149 @@ func doCheckout(res RepoResult) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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(args []string, cwd string, timeout time.Duration, cb ProgressCallback) (int, string, string) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 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.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.
|
||||||
|
func doCloneWithProgress(cfg RepoConfig, cb ProgressCallback) error {
|
||||||
|
local := absRepoPath(cfg.Path)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(local), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
code, _, stderr := runGitWithProgress(
|
||||||
|
[]string{"clone", "--progress", cfg.URL, local},
|
||||||
|
"", 2*time.Hour, cb,
|
||||||
|
)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// doPullWithProgress fait un pull avec suivi de progression.
|
||||||
|
func doPullWithProgress(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(
|
||||||
|
[]string{"pull", "--progress", "origin", branch},
|
||||||
|
res.Path, 2*time.Hour, cb,
|
||||||
|
)
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
114
gui.go
114
gui.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +18,8 @@ import (
|
|||||||
|
|
||||||
type RepoItem struct {
|
type RepoItem struct {
|
||||||
result RepoResult
|
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 {
|
func (it *RepoItem) statusText() string {
|
||||||
@@ -49,6 +52,27 @@ func (it *RepoItem) statusText() string {
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func (it *RepoItem) textColor() walk.Color {
|
||||||
r := it.result
|
r := it.result
|
||||||
if r.Pending {
|
if r.Pending {
|
||||||
@@ -94,28 +118,54 @@ func (m *RepoModel) Value(row, col int) interface{} {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
it := m.items[row]
|
it := m.items[row]
|
||||||
if col == 0 {
|
switch col {
|
||||||
|
case 0:
|
||||||
return it.result.Name
|
return it.result.Name
|
||||||
}
|
case 1:
|
||||||
return it.statusText()
|
return it.statusText()
|
||||||
|
case 2:
|
||||||
|
return it.progressText
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *RepoModel) StyleCell(style *walk.CellStyle) {
|
func (m *RepoModel) StyleCell(style *walk.CellStyle) {
|
||||||
if style.Col() != 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
if style.Row() >= len(m.items) {
|
row := style.Row()
|
||||||
|
if row >= len(m.items) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
style.TextColor = m.items[style.Row()].textColor()
|
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) {
|
func (m *RepoModel) setResult(row int, res RepoResult) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
if row < len(m.items) {
|
if row < len(m.items) {
|
||||||
m.items[row].result = res
|
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.mu.Unlock()
|
||||||
m.PublishRowChanged(row)
|
m.PublishRowChanged(row)
|
||||||
@@ -184,8 +234,8 @@ func (a *App) buildAndRun() error {
|
|||||||
if err := (MainWindow{
|
if err := (MainWindow{
|
||||||
AssignTo: &a.mw,
|
AssignTo: &a.mw,
|
||||||
Title: "Git Update Checker v" + VERSION,
|
Title: "Git Update Checker v" + VERSION,
|
||||||
MinSize: Size{Width: 650, Height: 400},
|
MinSize: Size{Width: 750, Height: 400},
|
||||||
Size: Size{Width: 820, Height: 600},
|
Size: Size{Width: 950, Height: 600},
|
||||||
Layout: VBox{Margins: Margins{Left: 10, Top: 10, Right: 10, Bottom: 10}},
|
Layout: VBox{Margins: Margins{Left: 10, Top: 10, Right: 10, Bottom: 10}},
|
||||||
Children: []Widget{
|
Children: []Widget{
|
||||||
// En-tête
|
// En-tête
|
||||||
@@ -208,8 +258,9 @@ func (a *App) buildAndRun() error {
|
|||||||
AlternatingRowBG: true,
|
AlternatingRowBG: true,
|
||||||
ColumnsOrderable: false,
|
ColumnsOrderable: false,
|
||||||
Columns: []TableViewColumn{
|
Columns: []TableViewColumn{
|
||||||
{Title: "Dépôt", Width: 180},
|
{Title: "Dépôt", Width: 150},
|
||||||
{Title: "Statut", Width: 350},
|
{Title: "Statut", Width: 200},
|
||||||
|
{Title: "Progression", Width: 350},
|
||||||
},
|
},
|
||||||
Model: a.model,
|
Model: a.model,
|
||||||
OnCurrentIndexChanged: a.onSelectionChanged,
|
OnCurrentIndexChanged: a.onSelectionChanged,
|
||||||
@@ -382,6 +433,31 @@ func logLineForResult(r RepoResult) string {
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Actions dépôt ─────────────────────────────────────────────────────────────
|
// ── Actions dépôt ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func (a *App) onSelectionChanged() {
|
func (a *App) onSelectionChanged() {
|
||||||
@@ -413,23 +489,26 @@ func (a *App) doAction() {
|
|||||||
a.btnAction.SetEnabled(false)
|
a.btnAction.SetEnabled(false)
|
||||||
a.appendLog(fmt.Sprintf("[%s] Mise à jour en cours...", res.Name))
|
a.appendLog(fmt.Sprintf("[%s] Mise à jour en cours...", res.Name))
|
||||||
|
|
||||||
|
cb := a.makeProgressCB(idx)
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
var err error
|
||||||
if res.NeedsClone {
|
if res.NeedsClone {
|
||||||
err = doClone(cfg)
|
err = doCloneWithProgress(cfg, cb)
|
||||||
} else {
|
} else {
|
||||||
if res.LocalChanges > 0 {
|
if res.LocalChanges > 0 {
|
||||||
err = doCheckout(res)
|
err = doCheckout(res)
|
||||||
}
|
}
|
||||||
if err == nil && res.NewCommits > 0 {
|
if err == nil && res.NewCommits > 0 {
|
||||||
err = doPull(res)
|
err = doPullWithProgress(res, cb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.mw.Synchronize(func() {
|
a.mw.Synchronize(func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
a.model.setProgress(idx, 0, "Erreur")
|
||||||
walk.MsgBox(a.mw, "Erreur", res.Name+"\n\n"+err.Error(), walk.MsgBoxIconError)
|
walk.MsgBox(a.mw, "Erreur", res.Name+"\n\n"+err.Error(), walk.MsgBoxIconError)
|
||||||
logError(fmt.Sprintf("[%s] %v", res.Name, err))
|
logError(fmt.Sprintf("[%s] %v", res.Name, err))
|
||||||
} else {
|
} else {
|
||||||
|
a.model.setProgress(idx, 1.0, progressBarText(1.0, 20, "Terminé"))
|
||||||
a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
||||||
logInfo(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
logInfo(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
||||||
}
|
}
|
||||||
@@ -449,24 +528,27 @@ func (a *App) updateAll() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pending.Add(1)
|
pending.Add(1)
|
||||||
cfg, res := cfg, res
|
i, cfg, res := i, cfg, res
|
||||||
|
cb := a.makeProgressCB(i)
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
var err error
|
||||||
if res.NeedsClone {
|
if res.NeedsClone {
|
||||||
err = doClone(cfg)
|
err = doCloneWithProgress(cfg, cb)
|
||||||
} else {
|
} else {
|
||||||
if res.LocalChanges > 0 {
|
if res.LocalChanges > 0 {
|
||||||
err = doCheckout(res)
|
err = doCheckout(res)
|
||||||
}
|
}
|
||||||
if err == nil && res.NewCommits > 0 {
|
if err == nil && res.NewCommits > 0 {
|
||||||
err = doPull(res)
|
err = doPullWithProgress(res, cb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.mw.Synchronize(func() {
|
a.mw.Synchronize(func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
a.model.setProgress(i, 0, "Erreur")
|
||||||
logError(fmt.Sprintf("[%s] %v", res.Name, err))
|
logError(fmt.Sprintf("[%s] %v", res.Name, err))
|
||||||
a.appendLog(fmt.Sprintf("[%s] Erreur: %v", res.Name, err))
|
a.appendLog(fmt.Sprintf("[%s] Erreur: %v", res.Name, err))
|
||||||
} else {
|
} else {
|
||||||
|
a.model.setProgress(i, 1.0, progressBarText(1.0, 20, "Terminé"))
|
||||||
logInfo(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
logInfo(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
||||||
a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
a.appendLog(fmt.Sprintf("[%s] Mise à jour OK", res.Name))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user