Files
PBIDE-GitTool/PBIDE-GitTool.pb

2870 lines
106 KiB
Plaintext

; =============================================================================
; PBIDE-GitTool.pb — Git External Tool for PureBasic IDE
; Outil externe Git pour l'IDE PureBasic
;
; Features / Fonctionnalités :
; - Init, Status, Add+Commit (selective), Push/Pull
; - UI: List with checkboxes + Include/Exclude/All actions
; - Advanced window: branch switch/restore
; - IDE installation wizard if launched without parameters
;
; Constraints respected / Contraintes respectées :
; - No underscores in identifiers / Pas d'underscore dans nos identifiants
; - Protected only in procedures / Protected uniquement dans les procédures
; - No IIf usage / Pas de IIf
; - Declarations = implementations / Déclarations = implémentations
; - Comparisons only in If/While/Until/For / Comparaisons uniquement dans If/While/Until/For
; =============================================================================
EnableExplicit
#EnableDebug = #True
; =============================================================================
; PLATFORM CONFIGURATION / CONFIGURATION PLATEFORME
; =============================================================================
; Path separator (portable) / Séparateur de chemin (portable)
CompilerIf #PB_Compiler_OS = #PB_OS_Windows
#PathSep$ = "\"
; Forced Git path for Windows / Chemin Git forcé pour Windows
#GitExe$ = "C:\Program Files\Git\cmd\git.exe"
CompilerElse
#PathSep$ = "/"
; On macOS/Linux keep 'git' in PATH / Sur macOS/Linux on garde 'git' dans le PATH
#GitExe$ = "git"
CompilerEndIf
; ===== Language constants / Constantes de langue =====
#LangEnglish = 0
#LangFrench = 1
Global gLanguage.i = #LangFrench ; default FR → overridden at runtime / FR par défaut → surchargé au runtime
; =============================================================================
; UI IDENTIFIERS / IDENTIFIANTS UI
; =============================================================================
; Main window / Fenêtre principale
#GWindow = 1
#GLabelRepo = 10
#GStringRepo = 11
#GButtonBrowse = 12
#GListStatus = 13
#GRefresh = 14
#GLabelMsg = 15
#GStringMsg = 16
#GLabelRemote = 18
#GStringRemote = 19
#GLabelBranch = 20
#GComboBranch = 21
#GSavePrefs = 22
#GInit = 23
#GCommit = 24
#GPull = 25
#GPush = 26
#GInclude = 27
#GExclude = 28
#GIncludeAll = 29
#GExcludeAll = 30
#GAdvanced = 31
#GDiff = 32
#GConfig = 34
#GGuide = 35
#GRestoreFile = 36
#GRenameFile = 37 ; Bouton pour renommer un fichier
#GDeleteFile = 38 ; Bouton pour supprimer un fichier avec git rm
; Display options / Options d'affichage
#GShowClean = 39 ; Show tracked files that are up to date / Afficher les fichiers suivis à jour
#GShowIgnored = 40 ; Show ignored files (.gitignore) / Afficher aussi les fichiers ignorés
#GShowPermanent = 44 ; Show permanently excluded files / Afficher les fichiers exclus définitivement
#GExcludeForever = 45 ; Permanently exclude button / Bouton exclure définitivement
#GReincludeForever = 46; Re-include permanently button / Bouton ré-inclure définitivement
; Branch management / Gestion des branches
#GAddBranch = 42
#GReloadBranches = 43
; Advanced window (branches) / Fenêtre avancée (branches)
#WAdv = 200
#GAdvLabel = 210
#GAdvCombo = 211
#GAdvSwitch = 212
#GAdvRestore = 213
#GAdvClose = 214
; Installation wizard / Assistant d'installation
#WInstall = 100
#GLabelIde = 110
#GStringIde = 111
#GButtonIde = 112
#GLabelTools = 113
#GStringTools = 114
#GButtonTools = 115
#GLabelTheme = 116
#GStringTheme = 117
#GButtonTheme = 118
#GInstallGo = 119
#GInstallCancel = 120
#GInstallNote = 121
; Diff window / Fenêtre Diff
#WDiff = 300
#GDiffText = 301
#GDiffClose = 302
; Git output window / Fenêtre de sortie Git
#WOut = 400
#GOutText = 401
#GOutCopy = 402
#GOutClose = 403
; File restore window / Fenêtre de restauration d'un fichier
#WRestore = 500
#GRestList = 501
#GRestOK = 502
#GRestCancel = 503
#GRestInfo = 504
; =============================================================================
; MACROS FOR SELECTION PERSISTENCE / MACROS POUR PERSISTANCE DE LA SÉLECTION
; =============================================================================
; Remember current selection before refreshing list
; Mémoriser la sélection courante avant un refresh de la liste
Macro RememberSel()
keepIdx.i = GetGadgetState(#GListStatus)
keepFile$ = ""
If keepIdx >= 0
keepFile$ = GetGadgetItemText(#GListStatus, keepIdx, 1)
EndIf
EndMacro
; Restore selection after list refresh
; Restaurer la sélection après un refresh de la liste
Macro RestoreSel()
RestoreSelection(keepIdx, keepFile$)
EndMacro
; =============================================================================
; STRUCTURES / STRUCTURES
; =============================================================================
; Git command execution data / Données d'exécution d'une commande Git
Structure GitCall
args.s ; Command arguments / Arguments de la commande
workdir.s ; Working directory / Répertoire de travail
output.s ; Standard output / Sortie standard
errors.s ; Error output / Sortie d'erreur
exitcode.i ; Exit code / Code de sortie
EndStructure
; File status row in the list / Ligne de statut de fichier dans la liste
Structure FileRow
stat.s ; 2-letter porcelain status (" M", "A ", "??", etc.) / Statut porcelain 2 lettres
file.s ; Relative path / Chemin relatif
include.i ; 1=checked (included), 0=excluded / 1=coché (inclus), 0=exclu
EndStructure
; Repository preferences / Préférences du dépôt
Structure RepoPrefs
remote.s ; Remote name or URL / Nom ou URL du remote
branch.s ; Branch name / Nom de la branche
EndStructure
; =============================================================================
; FUNCTION DECLARATIONS / DÉCLARATIONS DE FONCTIONS
; =============================================================================
; Core utilities / Utilitaires de base
Declare.s TrimNewlines(text$)
Declare.i RunGit(*call.GitCall)
Declare.s DetectRepoRoot(startDir$)
; Preferences management / Gestion des préférences
Declare.i LoadPrefs(prefsPath$, remote$, branch$)
Declare.i SavePrefs(prefsPath$, remote$, branch$)
Declare.i SaveRepoPrefs(repoDir$, remote$, branch$)
Declare.i LoadRepoPrefs(repoDir$, *prefs.RepoPrefs)
; UI and main operations / Interface et opérations principales
Declare.i OpenGUI(initialDir$, prefsPath$)
Declare.i CreateOrTrackBranch(repoDir$, remote$, name$)
Declare.i EnsureGitAvailable()
Declare.s DirFromArgOrFallback()
; Git operations / Opérations Git
Declare.i DoInitRepo(repoDir$)
Declare.i DoStatus(repoDir$, out$)
Declare.i DoCommit(repoDir$, message$, doPush.i, remote$, branch$)
Declare.i DoPush(repoDir$, remote$, branch$)
Declare.i DoPull(repoDir$, remote$, branch$)
; Branch management / Gestion des branches
Declare.i ListBranches(repoDir$, List branchesList.s())
Declare.i SwitchToBranch(repoDir$, branch$)
Declare.i RestoreFromBranch(repoDir$, branch$)
; File list management / Gestion de la liste de fichiers
Declare.i LoadStatusRows(repoDir$, List rows.FileRow())
Declare.i BuildFullFileList(repoDir$, showClean.i, showIgnored.i, List rows.FileRow())
Declare.i FillStatusList(List rows.FileRow())
Declare.i ToggleIncludeAt(index.i, List rows.FileRow())
Declare.i CollectIncludedFiles(List rows.FileRow(), List files.s())
Declare.i DoCommitSelected(repoDir$, message$, doPush.i, remote$, branch$, List files.s())
Declare.i SortRowsByInterest(List rows.FileRow())
; Advanced features / Fonctionnalités avancées
Declare.i OpenAdvancedWindow(repoDir$)
Declare.i OpenDiffWindow(repoDir$, List rows.FileRow())
Declare.i OpenRestoreFileWindow(repoDir$, List rows.FileRow())
Declare.i RestoreFileFromCommit(repoDir$, file$, commit$)
Declare.i DoRenameFile(repoDir$, oldFile$, newFile$)
Declare.i DoDeleteFile(repoDir$, file$)
; Configuration and setup / Configuration et installation
Declare.i ConfigIdentityWizard(repoDir$)
Declare.i MakeDefaultGitignore(repoDir$)
Declare.i InstallPBGitInIDE(ideExe$, toolsPrefs$, themeZip$)
Declare.i OpenInstallWizard()
; Helper functions / Fonctions d'aide
; --- Localization / Localisation ---
Declare.i InitLanguage()
Declare.s Tr(fr$, en$)
Declare.i LocalizeGUI()
Declare.s PorcelainToLabel(code$)
Declare.s GetLocalConfig(repoDir$, key$)
Declare.i IsGitRepo(dir$)
Declare.i UpdateGuide(repoDir$, List rows.FileRow(), branch$, remote$)
Declare.s NormalizeGitFilePath(repoDir$, raw$)
Declare.i RepoFileExists(repoDir$, rel$)
Declare.i RestoreSelection(oldIndex.i, oldFile$)
Declare.s FileFromRowsByIndex(index.i, List rows.FileRow())
; Exclusion management / Gestion des exclusions
Declare.s LocalExcludePath(repoDir$)
Declare.i EnsureToolFilesExcluded(repoDir$)
Declare.i LoadLocalExcludes(repoDir$, List excl.s())
Declare.i IsPermanentlyExcluded(file$, List excl.s())
Declare.i AddPermanentExclude(repoDir$, file$)
Declare.i RemovePermanentExclude(repoDir$, file$)
; =============================================================================
; UTILITY FUNCTIONS / FONCTIONS UTILITAIRES
; =============================================================================
; Detect IDE language first (PB_TOOL_Language), then OS env vars.
; Détecte d'abord la langue de l'IDE (PB_TOOL_Language), puis des variables d'environnement OS.
Procedure.i InitLanguage()
Protected val$, lc$, got.i = -1
; 1) IDE language (PureBasic exposes PB_TOOL_Language to external tools)
val$ = GetEnvironmentVariable("PB_TOOL_Language")
lc$ = LCase(val$)
If lc$ <> ""
If Left(lc$, 2) = "fr" Or FindString(lc$, "fran", 1)
got = #LangFrench
ElseIf Left(lc$, 2) = "en" Or FindString(lc$, "engl", 1) Or FindString(lc$, "angl", 1)
got = #LangEnglish
EndIf
EndIf
; 2) OS locale fallback
If got < 0
val$ = GetEnvironmentVariable("LANGUAGE")
If val$ = "" : val$ = GetEnvironmentVariable("LANG") : EndIf
If val$ = "" : val$ = GetEnvironmentVariable("LC_ALL") : EndIf
If val$ = "" : val$ = GetEnvironmentVariable("LC_MESSAGES") : EndIf
lc$ = LCase(val$)
If lc$ <> ""
If Left(lc$, 2) = "fr"
got = #LangFrench
ElseIf Left(lc$, 2) = "en"
got = #LangEnglish
EndIf
EndIf
EndIf
If got < 0 : got = #LangFrench : EndIf ; default FR if nothing else / FR par défaut
gLanguage = got
If #EnableDebug
Debug "[Lang] PB_TOOL_Language=" + GetEnvironmentVariable("PB_TOOL_Language") + " -> gLanguage=" + Str(gLanguage)
EndIf
ProcedureReturn gLanguage
EndProcedure
; Tiny translator helper: pass both texts, it returns the one matching gLanguage.
; Petit helper de traduction : passe FR et EN, renvoie selon gLanguage.
Procedure.s Tr(fr$, en$)
If gLanguage = #LangFrench
ProcedureReturn fr$
EndIf
ProcedureReturn en$
EndProcedure
; Convert path for Git (Windows compatible)
; Convertit le chemin pour Git (compatible Windows)
; - Replace "\" with "/"
; - Remove "./" prefix if present
Procedure.s GitPath(file$)
Protected p$ = file$
p$ = ReplaceString(p$, "\", "/")
If Left(p$, 2) = "./"
p$ = Mid(p$, 3)
EndIf
ProcedureReturn p$
EndProcedure
; Normalize a path returned by Git
; Normalise un chemin renvoyé par Git
; - Remove extra spaces/quotes
; - Keep destination in case of rename "old -> new"
; - Replace "/" with OS separator
; - Remove leading "./" if present
Procedure.s NormalizeGitFilePath(repoDir$, raw$)
Protected p$ = Trim(raw$)
; Remove surrounding quotes if present / Retirer quotes éventuelles autour
If Left(p$, 1) = Chr(34) And Right(p$, 1) = Chr(34)
p$ = Mid(p$, 2, Len(p$)-2)
EndIf
If Left(p$, 1) = "'" And Right(p$, 1) = "'"
p$ = Mid(p$, 2, Len(p$)-2)
EndIf
; Handle "old -> new" case: keep "new" / Cas "old -> new" : on garde "new"
Protected pos.i = FindString(p$, "->", 1)
If pos > 0
p$ = Trim(Mid(p$, pos + 2))
EndIf
; Remove leading "./" / Retirer "./"
If Left(p$, 2) = "./"
p$ = Mid(p$, 3)
EndIf
; Convert slashes to OS separator / Slashes vers séparateur OS
p$ = ReplaceString(p$, "/", #PathSep$)
ProcedureReturn p$
EndProcedure
; Check existence of a file RELATIVE to repo (file or directory)
; Vérifie l'existence d'un fichier RELATIF au repo (fichier ou dossier)
; Returns 1 if present, 0 otherwise / Renvoie 1 si présent, 0 sinon
Procedure.i RepoFileExists(repoDir$, rel$)
Protected base$ = repoDir$
If Right(base$, 1) <> #PathSep$ : base$ + #PathSep$ : EndIf
Protected abs$ = base$ + rel$
Protected s.q = FileSize(abs$)
If s >= 0 Or s = -2
ProcedureReturn 1
EndIf
ProcedureReturn 0
EndProcedure
; Remove trailing newlines from text / Supprime les retours à la ligne en fin de texte
Procedure.s TrimNewlines(text$)
Protected s$ = text$
While Right(s$, 1) = #LF$ Or Right(s$, 1) = #CR$
s$ = Left(s$, Len(s$)-1)
Wend
ProcedureReturn s$
EndProcedure
; Detect if 'remote$' looks like a URL (http(s):// or git@…)
; Détecte si 'remote$' ressemble à une URL (http(s):// ou git@…)
Procedure.i IsUrlRemote(remote$)
If FindString(remote$, "://", 1) Or FindString(remote$, "@", 1)
ProcedureReturn 1
EndIf
ProcedureReturn 0
EndProcedure
; Read entire text file and return its content (with #LF$ between lines)
; Lit tout le fichier texte et renvoie son contenu (avec #LF$ entre les lignes)
Procedure.s ReadAllText(path$)
Protected out$ = ""
If ReadFile(0, path$)
While Eof(0) = 0
out$ + ReadString(0) + #LF$
Wend
CloseFile(0)
EndIf
ProcedureReturn out$
EndProcedure
; Get current branch name (HEAD → returns "" if detached)
; Récupère le nom de branche courante (HEAD → renvoie "" si détachée)
Procedure.s GetCurrentBranch(repoDir$)
Protected gc.GitCall
gc\workdir = repoDir$
gc\args = "rev-parse --abbrev-ref HEAD"
If RunGit(@gc) = 0
Protected b$ = TrimNewlines(gc\output)
If b$ <> "" And LCase(b$) <> "head"
ProcedureReturn b$
EndIf
EndIf
ProcedureReturn ""
EndProcedure
; Test if a named remote exists / Teste si un remote nommé existe
Procedure.i RemoteExists(repoDir$, remoteName$)
Protected gc.GitCall, out$, i.i, n.i, line$
gc\workdir = repoDir$
gc\args = "remote"
If RunGit(@gc) <> 0
ProcedureReturn 0
EndIf
out$ = gc\output
n = CountString(out$, #LF$) + 1
For i = 1 To n
line$ = Trim(StringField(out$, i, #LF$))
If line$ <> "" And LCase(line$) = LCase(remoteName$)
ProcedureReturn 1
EndIf
Next
ProcedureReturn 0
EndProcedure
; Display Git command output in a window / Affiche la sortie d'une commande Git dans une fenêtre
Procedure.i ShowGitOutput(title$, stdOut$, stdErr$)
Protected full$
full$ + "=== STDOUT ===" + #LF$ + stdOut$ + #LF$ + "=== STDERR ===" + #LF$ + stdErr$
If OpenWindow(#WOut, 0, 0, 820, 520, title$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
EditorGadget(#GOutText, 10, 10, 800, 460)
ButtonGadget(#GOutCopy, 600, 480, 100, 28, "Copier")
ButtonGadget(#GOutClose, 710, 480, 100, 28, "Fermer")
SetGadgetText(#GOutText, full$)
Repeat
Protected ev.i = WaitWindowEvent()
If ev = #PB_Event_Gadget
Select EventGadget()
Case #GOutCopy
SetClipboardText(full$)
Case #GOutClose
CloseWindow(#WOut)
Break
EndSelect
EndIf
Until ev = #PB_Event_CloseWindow
EndIf
ProcedureReturn 1
EndProcedure
; Add a remote / Ajoute un remote
Procedure.i AddRemote(repoDir$, remoteName$, remoteUrl$)
Protected gc.GitCall
gc\workdir = repoDir$
gc\args = "remote add " + remoteName$ + " " + Chr(34) + remoteUrl$ + Chr(34)
If RunGit(@gc) = 0
ProcedureReturn 1
EndIf
ShowGitOutput("Git remote add — erreur", "", gc\errors)
ProcedureReturn 0
EndProcedure
; =============================================================================
; CORE GIT OPERATIONS / OPÉRATIONS GIT DE BASE
; =============================================================================
; Execute git with stdout/stderr capture (structure passed by pointer)
; Exécute git avec capture stdout/stderr (structure passée par pointeur)
Procedure.i RunGit(*call.GitCall)
Protected prg.i, line$, lineError$, out$, err$
If #EnableDebug
Debug "[RunGit] " + #GitExe$ + " " + *call\args + " (wd=" + *call\workdir + ")"
EndIf
prg = RunProgram(#GitExe$, *call\args, *call\workdir, #PB_Program_Open | #PB_Program_Read | #PB_Program_Error | #PB_Program_Hide)
If prg = 0
*call\output = ""
*call\errors = "Impossible de lancer '" + #GitExe$+"'."
*call\exitcode = -1
ProcedureReturn *call\exitcode
EndIf
; Read output while program is running / Lire la sortie pendant l'exécution
While ProgramRunning(prg)
While AvailableProgramOutput(prg)
line$ = ReadProgramString(prg)
If line$ <> "" : out$ + line$ + #LF$ : EndIf
lineError$ = ReadProgramError(prg)
If lineError$ <> "" : err$ + lineError$ + #LF$ : EndIf
Wend
Delay(5)
Wend
; Read remaining output / Lire le reste de la sortie
While AvailableProgramOutput(prg)
line$ = ReadProgramString(prg)
If line$ <> "" : out$ + line$ + #LF$ : EndIf
Wend
; Read remaining errors / Lire le reste des erreurs
Repeat
line$ = ReadProgramError(prg)
If line$ = "" : Break : EndIf
err$ + line$ + #LF$
ForEver
*call\output = out$
*call\errors = err$
*call\exitcode = ProgramExitCode(prg)
CloseProgram(prg)
If #EnableDebug
Debug "[RunGit] code=" + Str(*call\exitcode)
If *call\output <> "" : Debug *call\output : EndIf
If *call\errors <> "" : Debug "[stderr] " + *call\errors : EndIf
EndIf
ProcedureReturn *call\exitcode
EndProcedure
; Detect Git repository root / Détecte la racine du dépôt Git
Procedure.s DetectRepoRoot(startDir$)
Protected gc.GitCall
gc\args = "rev-parse --show-toplevel"
gc\workdir = startDir$
If RunGit(@gc) = 0
ProcedureReturn TrimNewlines(gc\output)
EndIf
ProcedureReturn startDir$
EndProcedure
; =============================================================================
; PREFERENCES MANAGEMENT / GESTION DES PRÉFÉRENCES
; =============================================================================
; Load global preferences / Charge les préférences globales
Procedure.i LoadPrefs(prefsPath$, remote$, branch$)
If OpenPreferences(prefsPath$)
PreferenceGroup("git")
remote$ = ReadPreferenceString("remote", "origin")
branch$ = ReadPreferenceString("branch", "main")
ClosePreferences()
If #EnableDebug
Debug "[LoadPrefs] " + prefsPath$ + " remote=" + remote$ + " branch=" + branch$
EndIf
ProcedureReturn 1
EndIf
remote$ = "origin"
branch$ = "main"
ProcedureReturn 0
EndProcedure
; Save global preferences / Sauve les préférences globales
Procedure.i SavePrefs(prefsPath$, remote$, branch$)
If CreatePreferences(prefsPath$)
PreferenceGroup("git")
WritePreferenceString("remote", remote$)
WritePreferenceString("branch", branch$)
ClosePreferences()
If #EnableDebug
Debug "[SavePrefs] " + prefsPath$ + " remote=" + remote$ + " branch=" + branch$
EndIf
ProcedureReturn 1
EndIf
ProcedureReturn 0
EndProcedure
; Save remote/branch for THIS repository (.pbide-gittool.prefs at repo root)
; Sauvegarde remote/branch pour CE dépôt (.pbide-gittool.prefs à la racine du repo)
Procedure.i SaveRepoPrefs(repoDir$, remote$, branch$)
Protected base$ = repoDir$
If Right(base$, 1) <> #PathSep$ : base$ + #PathSep$ : EndIf
Protected p$ = base$ + ".pbide-gittool.prefs"
If CreatePreferences(p$)
PreferenceGroup("git")
WritePreferenceString("remote", remote$)
WritePreferenceString("branch", branch$)
ClosePreferences()
If #EnableDebug
Debug "[SaveRepoPrefs] " + p$ + " remote=" + remote$ + " branch=" + branch$
EndIf
ProcedureReturn 1
EndIf
If #EnableDebug : Debug "[SaveRepoPrefs] ERROR cannot write " + p$ : EndIf
ProcedureReturn 0
EndProcedure
; Load repository remote/branch into *prefs (returns 1 if found)
; Charge remote/branch du dépôt dans *prefs (retourne 1 si trouvé)
Procedure.i LoadRepoPrefs(repoDir$, *prefs.RepoPrefs)
If *prefs = 0 : ProcedureReturn 0 : EndIf
Protected base$ = repoDir$
If Right(base$, 1) <> #PathSep$ : base$ + #PathSep$ : EndIf
Protected p$ = base$ + ".pbide-gittool.prefs"
If OpenPreferences(p$)
PreferenceGroup("git")
*prefs\remote = ReadPreferenceString("remote", *prefs\remote)
*prefs\branch = ReadPreferenceString("branch", *prefs\branch)
ClosePreferences()
If #EnableDebug
Debug "[LoadRepoPrefs] " + p$ + " remote=" + *prefs\remote + " branch=" + *prefs\branch
EndIf
ProcedureReturn 1
EndIf
If #EnableDebug : Debug "[LoadRepoPrefs] none for repo " + repoDir$ : EndIf
ProcedureReturn 0
EndProcedure
; =============================================================================
; SELECTION PERSISTENCE / PERSISTANCE DE LA SÉLECTION
; =============================================================================
; Restore selection after FillStatusList()/rebuild
; Restaure la sélection après un FillStatusList()/rebuild
; Try index first, then search by filename (column 1)
; Essaie d'abord l'index, puis cherche par nom de fichier (colonne 1)
Procedure.i RestoreSelection(oldIndex.i, oldFile$)
Protected count.i = CountGadgetItems(#GListStatus)
If count = 0
ProcedureReturn 0
EndIf
; Try to restore by index if file matches / Essaie de restaurer par index si le fichier correspond
If oldIndex >= 0 And oldIndex < count
If GetGadgetItemText(#GListStatus, oldIndex, 1) = oldFile$
SetGadgetState(#GListStatus, oldIndex)
ProcedureReturn 1
EndIf
EndIf
; Search by filename / Recherche par nom de fichier
Protected i.i, name$
For i = 0 To count - 1
name$ = GetGadgetItemText(#GListStatus, i, 1)
If name$ = oldFile$
SetGadgetState(#GListStatus, i)
ProcedureReturn 1
EndIf
Next
ProcedureReturn 0
EndProcedure
; =============================================================================
; EXCLUSION MANAGEMENT / GESTION DES EXCLUSIONS
; =============================================================================
; Path to .git/info/exclude / Chemin de .git/info/exclude
Procedure.s LocalExcludePath(repoDir$)
Protected base$ = repoDir$
If Right(base$, 1) <> #PathSep$ : base$ + #PathSep$ : EndIf
ProcedureReturn base$ + ".git" + #PathSep$ + "info" + #PathSep$ + "exclude"
EndProcedure
; Add our internal prefs file to local exclusions if missing
; Ajoute notre fichier prefs interne aux exclusions locales si absent
; Add .pbide-gittool.prefs to .git/info/exclude if missing
Procedure.i EnsureToolFilesExcluded(repoDir$)
Protected excl$ = LocalExcludePath(repoDir$)
Protected line$ = ".pbide-gittool.prefs"
Protected txt$ = ReadAllText(excl$)
Protected found.i = 0
If txt$ <> ""
If FindString(#LF$ + txt$ + #LF$, #LF$ + line$ + #LF$, 1) > 0
found = 1
EndIf
EndIf
If found = 0
If CreateFile(0, excl$)
; Rewrite existing content as is / Réécrit l'existant tel quel
If txt$ <> ""
WriteString(0, txt$)
If Right(txt$, 1) <> #LF$ : WriteString(0, #LF$) : EndIf
EndIf
; Add our line / Ajoute notre ligne
WriteString(0, line$ + #LF$)
CloseFile(0)
If #EnableDebug
Debug "[EnsureExclude] added " + line$
EndIf
ProcedureReturn 1
Else
If #EnableDebug
Debug "[EnsureExclude] cannot write " + excl$
EndIf
ProcedureReturn 0
EndIf
EndIf
ProcedureReturn 1
EndProcedure
; Load .git/info/exclude (non-empty / non-commented lines)
; Charge .git/info/exclude (lignes non vides / non commentées)
Procedure.i LoadLocalExcludes(repoDir$, List excl.s())
ClearList(excl())
Protected excl$ = LocalExcludePath(repoDir$), l$
If ReadFile(0, excl$)
While Eof(0) = 0
l$ = Trim(ReadString(0))
If l$ <> "" And Left(l$, 1) <> "#"
AddElement(excl()) : excl() = ReplaceString(l$, "/", #PathSep$)
EndIf
Wend
CloseFile(0)
ProcedureReturn ListSize(excl())
EndIf
ProcedureReturn 0
EndProcedure
; Simple test: exclusion by exact equality (no wildcards)
; Test simple : exclusion par égalité exacte (sans wildcards)
Procedure.i IsPermanentlyExcluded(file$, List excl.s())
ForEach excl()
If excl() = file$
ProcedureReturn 1
EndIf
Next
ProcedureReturn 0
EndProcedure
; Add file$ to .git/info/exclude (and remove from index if tracked)
; Ajoute file$ à .git/info/exclude (et le retire de l'index si suivi)
Procedure.i AddPermanentExclude(repoDir$, file$)
Protected q$ = Chr(34)
Protected gc.GitCall
Protected excl$ = LocalExcludePath(repoDir$)
Protected txt$ = ReadAllText(excl$)
Protected found.i = 0
; 1) If tracked → remove from index / Si suivi → le retirer de l'index
gc\workdir = repoDir$
gc\args = "ls-files -- " + q$ + file$ + q$
If RunGit(@gc) = 0 And Trim(gc\output) <> ""
gc\args = "rm --cached -- " + q$ + file$ + q$
If RunGit(@gc) <> 0
MessageRequester("Exclusion permanente", "Échec du retrait de l'index : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
EndIf
; 2) If already in exclude → nothing to do / Si déjà présent dans exclude → rien à faire
If txt$ <> ""
If FindString(#LF$ + txt$ + #LF$, #LF$ + file$ + #LF$, 1) > 0
found = 1
EndIf
EndIf
If found
MessageRequester("Exclusion permanente", "Déjà présent dans .git/info/exclude.", #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
; 3) Write / Écrire
If CreateFile(0, excl$)
If txt$ <> ""
WriteString(0, txt$)
If Right(txt$, 1) <> #LF$ : WriteString(0, #LF$) : EndIf
EndIf
WriteString(0, file$ + #LF$)
CloseFile(0)
MessageRequester("Exclusion permanente", "Ajouté à .git/info/exclude.", #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
MessageRequester("Exclusion permanente", "Impossible d'écrire " + excl$, #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
; Remove file$ from .git/info/exclude and force re-inclusion (git add -f)
; Retire file$ de .git/info/exclude et force la ré-inclusion (git add -f)
Procedure.i RemovePermanentExclude(repoDir$, file$)
Protected excl$ = LocalExcludePath(repoDir$)
Protected out$ = ""
Protected l$, removed.i = 0
; Read line by line and rewrite without target line
; On relit ligne à ligne et on réécrit sans la ligne ciblée
If ReadFile(0, excl$) = 0
MessageRequester("Ré-inclure (permanent)", "Fichier d'exclusion introuvable : " + excl$, #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
While Eof(0) = 0
l$ = Trim(ReadString(0))
If l$ <> "" And Left(l$, 1) <> "#"
If l$ = file$
removed = 1
Else
out$ + l$ + #LF$
EndIf
Else
; Keep empty/commented lines as is / Conserve les lignes vides/commentées telles quelles
out$ + l$ + #LF$
EndIf
Wend
CloseFile(0)
If removed = 0
MessageRequester("Ré-inclure (permanent)", "Le fichier n'était pas dans .git/info/exclude.", #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
If CreateFile(0, excl$)
WriteString(0, out$)
CloseFile(0)
Else
MessageRequester("Ré-inclure (permanent)", "Impossible d'écrire " + excl$, #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
; Force add even with ignore rules elsewhere / Force l'ajout même s'il y a des règles d'ignore ailleurs
Protected gc.GitCall, q$ = Chr(34)
gc\workdir = repoDir$
gc\args = "add -f -- " + q$ + file$ + q$
If RunGit(@gc) <> 0
MessageRequester("Ré-inclure (permanent)", "Avertissement : git add -f a échoué : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Warning)
Else
MessageRequester("Ré-inclure (permanent)", "Le fichier a été ré-inclus.", #PB_MessageRequester_Info)
EndIf
ProcedureReturn 1
EndProcedure
; =============================================================================
; BRANCH MANAGEMENT / GESTION DES BRANCHES
; =============================================================================
; Create local branch or track remote branch "remote/branch"
; Crée une branche locale ou suit une branche distante "remote/branch"
; - name$ = "featureX" → git switch -c featureX
; - name$ = "origin/main" → git fetch origin + git switch -c main --track origin/main
Procedure.i CreateOrTrackBranch(repoDir$, remote$, name$)
Protected gc.GitCall, local$, rem$, br$, pos.i, q$ = Chr(34)
If Trim(name$) = "" : ProcedureReturn 0 : EndIf
; Detect "remote/branch" / Détection "remote/branch"
pos = FindString(name$, "/", 1)
If pos > 0
rem$ = Left(name$, pos - 1)
br$ = Mid(name$, pos + 1)
local$ = br$
If #EnableDebug : Debug "[Branch] track " + rem$ + "/" + br$ + " as " + local$ : EndIf
; Fetch first to have the ref / Fetch d'abord pour avoir la ref
gc\workdir = repoDir$
gc\args = "fetch " + rem$
If RunGit(@gc) <> 0
MessageRequester("Branche", "Échec fetch: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
; Create local following remote / Création locale en suivant la remote
gc\args = "switch -c " + q$ + local$ + q$ + " --track " + rem$ + "/" + br$
If RunGit(@gc) = 0
MessageRequester("Branche", "Branche locale '" + local$ + "' suivant " + rem$ + "/" + br$ + ".", #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
MessageRequester("Branche", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
; Create simple local branch / Création d'une branche locale simple
gc\workdir = repoDir$
gc\args = "switch -c " + q$ + name$ + q$
If RunGit(@gc) = 0
MessageRequester("Branche", "Branche '" + name$ + "' créée et sélectionnée.", #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
MessageRequester("Branche", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
; Check if Git is available / Vérifie si Git est disponible
Procedure.i EnsureGitAvailable()
Protected gc.GitCall
gc\args = "--version"
If RunGit(@gc) = 0 And FindString(LCase(gc\output), "git version", 1)
ProcedureReturn 1
EndIf
MessageRequester("PBIDE-GitTool", "Git n'est pas détecté à l'emplacement prévu : " + #GitExe$, #PB_MessageRequester_Warning)
ProcedureReturn 0
EndProcedure
; Determine relevant directory (project → env → --repo → exe)
; Détermine un répertoire pertinent (project → env → --repo → exe)
Procedure.s DirFromArgOrFallback()
Protected n.i = CountProgramParameters()
Protected gotNextProject.i = 0
Protected gotNextRepo.i = 0
Protected dir$ = "", i.i, p$
For i = 0 To n - 1
p$ = ProgramParameter(i)
If gotNextProject
dir$ = p$
gotNextProject = 0
ElseIf gotNextRepo
dir$ = p$
gotNextRepo = 0
ElseIf LCase(p$) = "--project"
gotNextProject = 1
ElseIf LCase(p$) = "--repo"
gotNextRepo = 1
ElseIf Left(p$, 1) <> "-"
If dir$ = "" : dir$ = p$ : EndIf
EndIf
Next
If dir$ <> ""
If #EnableDebug : Debug "[Dir] from args: " + dir$ : EndIf
ProcedureReturn dir$
EndIf
; Try environment variable / Essaie la variable d'environnement
Protected projFile$ = GetEnvironmentVariable("PB_TOOL_Project")
If projFile$ <> ""
Protected projDir$ = GetPathPart(projFile$)
If projDir$ <> ""
If #EnableDebug : Debug "[Dir] from PB_TOOL_Project: " + projDir$ : EndIf
ProcedureReturn projDir$
EndIf
EndIf
; Fallback to exe directory / Repli sur le répertoire de l'exe
dir$ = GetPathPart(ProgramFilename())
If #EnableDebug : Debug "[Dir] fallback exe dir: " + dir$ : EndIf
ProcedureReturn dir$
EndProcedure
; =============================================================================
; BASIC GIT OPERATIONS / OPÉRATIONS GIT DE BASE
; =============================================================================
; Initialize Git repository / Initialise un dépôt Git
Procedure.i DoInitRepo(repoDir$)
Protected gc.GitCall
gc\args = "init"
gc\workdir = repoDir$
If RunGit(@gc) = 0
MessageRequester("Git init", "Répertoire initialisé." + #LF$ + TrimNewlines(gc\output), #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
MessageRequester("Git init", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
; Get Git status / Récupère le statut Git
Procedure.i DoStatus(repoDir$, out$)
Protected gc.GitCall
gc\args = "status --porcelain"
gc\workdir = repoDir$
If RunGit(@gc) <> 0
out$ = ""
MessageRequester("Git status", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
out$ = gc\output
ProcedureReturn 1
EndProcedure
; Commit all changes / Valide tous les changements
Procedure.i DoCommit(repoDir$, message$, doPush.i, remote$, branch$)
Protected gc.GitCall, code.i
gc\workdir = repoDir$
; Add all changes / Ajouter tous les changements
gc\args = "add -A"
code = RunGit(@gc)
If code <> 0
MessageRequester("Git add", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
; Commit with message / Valider avec un message
gc\args = "commit -m " + Chr(34) + message$ + Chr(34)
code = RunGit(@gc)
If code <> 0
MessageRequester("Git commit", "Échec ou rien à valider: " + #LF$ + TrimNewlines(gc\errors) + #LF$ + TrimNewlines(gc\output), #PB_MessageRequester_Warning)
If doPush = 0 : ProcedureReturn 0 : EndIf
Else
MessageRequester("Git commit", "OK:" + #LF$ + TrimNewlines(gc\output), #PB_MessageRequester_Info)
EndIf
If doPush
ProcedureReturn DoPush(repoDir$, remote$, branch$)
EndIf
ProcedureReturn 1
EndProcedure
; Enhanced Push operation / Push amélioré :
; - Accepts 'Remote' as URL: creates/uses 'origin' automatically
; - If no upstream: retry with 'push -u <remote> <branch>'
; - Shows full output on error
; - Accepte 'Remote' comme URL : crée/emploie 'origin' automatiquement
; - Si pas d'upstream : retente avec 'push -u <remote> <branch>'
; - Affiche sorties complètes en cas d'erreur
Procedure.i DoPush(repoDir$, remote$, branch$)
Protected gc.GitCall, code.i
Protected remoteName$ = remote$
Protected remoteUrl$ = ""
Protected curBranch$
; Empty remote → assume 'origin' / Remote vide → suppose 'origin'
If Trim(remoteName$) = ""
remoteName$ = "origin"
EndIf
; If user entered URL as 'remote', create/use 'origin'
; Si l'utilisateur a saisi une URL comme 'remote', crée/emploie 'origin'
If IsUrlRemote(remoteName$)
remoteUrl$ = remoteName$
remoteName$ = "origin"
If RemoteExists(repoDir$, remoteName$) = 0
If AddRemote(repoDir$, remoteName$, remoteUrl$) = 0
ProcedureReturn 0
EndIf
EndIf
EndIf
; Branch to use: field or current branch / Branche à utiliser : champ ou branche courante
If Trim(branch$) = ""
curBranch$ = GetCurrentBranch(repoDir$)
Else
curBranch$ = branch$
EndIf
If curBranch$ = ""
MessageRequester("Git push", "Branche courante introuvable (HEAD détachée ?). Sélectionnez une branche.", #PB_MessageRequester_Warning)
ProcedureReturn 0
EndIf
; Normal push attempt / Tentative de push normal
gc\workdir = repoDir$
gc\args = "push " + remoteName$ + " " + curBranch$
code = RunGit(@gc)
If code = 0
MessageRequester("Git push", "OK:" + #LF$ + TrimNewlines(gc\output), #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
; If no upstream, retry with -u (first push) / Si pas d'upstream, retente avec -u (premier push)
If FindString(LCase(gc\errors), "no upstream branch", 1) Or FindString(LCase(gc\errors), "set the remote as upstream", 1)
gc\args = "push -u " + remoteName$ + " " + curBranch$
code = RunGit(@gc)
If code = 0
MessageRequester("Git push (premier envoi)", "Upstream configuré et push effectué." + #LF$ + TrimNewlines(gc\output), #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
EndIf
; Other error → full window / Autre erreur → fenêtre complète
ShowGitOutput("Git push — erreur", gc\output, gc\errors)
ProcedureReturn 0
EndProcedure
; Pull from remote / Tire depuis le remote
Procedure.i DoPull(repoDir$, remote$, branch$)
Protected gc.GitCall
gc\args = "pull " + remote$ + " " + branch$
gc\workdir = repoDir$
If RunGit(@gc) = 0
MessageRequester("Git pull", "OK:" + #LF$ + TrimNewlines(gc\output), #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
MessageRequester("Git pull", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
Procedure.i DoRenameFile(repoDir$, oldFile$, newFile$)
Protected gc.GitCall, q$ = Chr(34)
Protected oldPath$ = GitPath(oldFile$)
Protected newPath$ = GitPath(newFile$)
If oldPath$ = "" Or newPath$ = ""
MessageRequester("Renommer", "Les noms de fichier ne peuvent pas être vides.", #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
gc\workdir = repoDir$
gc\args = "mv " + q$ + oldPath$ + q$ + " " + q$ + newPath$ + q$
If RunGit(@gc) = 0
MessageRequester("Renommer", "Fichier renommé avec succès.", #PB_MessageRequester_Info)
ProcedureReturn 1
Else
MessageRequester("Renommer", "Échec du renommage : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
EndProcedure
; Supprime un fichier avec git rm
Procedure.i DoDeleteFile(repoDir$, file$)
Protected gc.GitCall, q$ = Chr(34)
Protected path$ = GitPath(file$)
If path$ = ""
MessageRequester("Supprimer", "Le nom du fichier ne peut pas être vide.", #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
; Vérifie que le fichier existe dans le dépôt
If RepoFileExists(repoDir$, path$) = 0
MessageRequester("Supprimer", "Le fichier n'existe pas dans le dépôt : " + path$, #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
; Confirme la suppression
If MessageRequester("Supprimer", "Voulez-vous vraiment supprimer ce fichier ?" + #LF$ + path$, #PB_MessageRequester_YesNo) = #PB_MessageRequester_No
ProcedureReturn 0
EndIf
; Exécute git rm
gc\workdir = repoDir$
gc\args = "rm -- " + q$ + path$ + q$
If RunGit(@gc) = 0
MessageRequester("Supprimer", "Fichier supprimé avec succès.", #PB_MessageRequester_Info)
ProcedureReturn 1
Else
MessageRequester("Supprimer", "Échec de la suppression : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
EndProcedure
; =============================================================================
; DIFF WINDOW / FENÊTRE DIFF
; =============================================================================
; Open window showing diff of selected file / Ouvre une fenêtre affichant le diff du fichier sélectionné
Procedure.i OpenDiffWindow(repoDir$, List rows.FileRow())
Protected idx.i = GetGadgetState(#GListStatus)
If idx < 0
MessageRequester("Diff", "Sélectionnez un fichier dans la liste.", #PB_MessageRequester_Info)
ProcedureReturn 0
EndIf
; Find file path from rows() list / Retrouver le chemin du fichier depuis la liste rows()
Protected j.i = 0, target$
ForEach rows()
If j = idx
target$ = rows()\file
Break
EndIf
j + 1
Next
If target$ = ""
MessageRequester("Diff", "Aucun fichier sélectionné.", #PB_MessageRequester_Info)
ProcedureReturn 0
EndIf
; Execute 'git diff -- "<file>"' / Exécuter 'git diff -- "<fichier>"'
Protected gc.GitCall
gc\workdir = repoDir$
gc\args = "diff -- " + Chr(34) + target$ + Chr(34)
If RunGit(@gc) <> 0
MessageRequester("Diff", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
Protected diff$ = gc\output
If Trim(diff$) = ""
diff$ = "Aucune différence détectée pour ce fichier."
EndIf
; Display window / Fenêtre d'affichage
Protected title$ = "Diff — " + target$
If OpenWindow(#WDiff, 0, 0, 800, 500, title$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
EditorGadget(#GDiffText, 10, 10, 780, 440)
ButtonGadget(#GDiffClose, 690, 460, 100, 28, "Fermer")
SetGadgetText(#GDiffText, diff$)
Repeat
Protected ev.i = WaitWindowEvent()
If ev = #PB_Event_Gadget And EventGadget() = #GDiffClose
CloseWindow(#WDiff)
Break
EndIf
Until ev = #PB_Event_CloseWindow
EndIf
ProcedureReturn 1
EndProcedure
; =============================================================================
; BRANCH LISTING / LISTAGE DES BRANCHES
; =============================================================================
; List local branches / Liste les branches locales
Procedure.i ListBranches(repoDir$, List branchesList.s())
ClearList(branchesList())
Protected gc.GitCall
gc\args = "branch --list --format='%(refname:short)'"
gc\workdir = repoDir$
If RunGit(@gc) <> 0
; Fallback defaults / Valeurs par défaut de repli
AddElement(branchesList()) : branchesList() = "main"
AddElement(branchesList()) : branchesList() = "master"
ProcedureReturn 0
EndIf
Protected out$ = ReplaceString(gc\output, "'", "")
Protected i.i, n.i = CountString(out$, #LF$) + 1
For i = 1 To n
Protected b$ = Trim(StringField(out$, i, #LF$))
If b$ <> ""
AddElement(branchesList())
branchesList() = b$
EndIf
Next i
ProcedureReturn 1
EndProcedure
; =============================================================================
; STATUS & FILE LIST MANAGEMENT / GESTION DU STATUS ET DE LA LISTE DE FICHIERS
; =============================================================================
; Load status into list of rows (stat, file, include=0)
; Charge le status dans une liste de lignes (stat, file, include=0)
; Robust parsing: take everything after first separator (space/tab) after 2 status letters
; Parsing robuste : on prend tout ce qui suit le premier séparateur après les 2 lettres de statut
; Also handles "old -> new" (rename)
; Gère aussi "old -> new" (renommage)
Procedure.i LoadStatusRows(repoDir$, List rows.FileRow())
Protected gc.GitCall, text$, line$, code$, file$
Protected i.i, n.i, start.i, ch$, pos.i
gc\args = "status --porcelain"
gc\workdir = repoDir$
If RunGit(@gc) <> 0
MessageRequester("Git status", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
text$ = gc\output
n = CountString(text$, #LF$) + 1
ClearList(rows())
For i = 1 To n
line$ = StringField(text$, i, #LF$)
line$ = Trim(line$)
If line$ = "" : Continue : EndIf
; First 2 columns = status (XY) / 2 premières colonnes = statut (XY)
code$ = Left(line$, 2)
; Find path start: after spaces/tabs following column 3
; Cherche le début du chemin : après les espaces/tabs suivant la colonne 3
start = 3
While start <= Len(line$)
ch$ = Mid(line$, start, 1)
If ch$ <> " " And ch$ <> #TAB$
Break
EndIf
start + 1
Wend
file$ = Mid(line$, start)
; Rename "old -> new": keep destination / Renommage "old -> new" : garder la destination
pos = FindString(file$, "->", 1)
If pos > 0
file$ = Trim(Mid(file$, pos + 2))
EndIf
; Optional simple normalization / Normalisation simple optionnelle
If Left(file$, 2) = "./" : file$ = Mid(file$, 3) : EndIf
file$ = ReplaceString(file$, "/", #PathSep$)
AddElement(rows())
rows()\stat = code$
rows()\file = file$
rows()\include = 0
Next i
ProcedureReturn ListSize(rows())
EndProcedure
; Fill #GListStatus without #LF$ (avoids losing 1st character in col. 2)
; Remplit la liste (colonne 0 = état lisible, colonne 1 = chemin)
; Add " — Exclu" if element has changes but isn't checked
; Ajoute " — Exclu" si l'élément a des changements mais n'est pas coché
Procedure.i FillStatusList(List rows.FileRow())
ClearGadgetItems(#GListStatus)
Protected idx.i = 0
Protected label$, file$
ForEach rows()
label$ = PorcelainToLabel(rows()\stat)
; If not "OK/ignored" file and include=0, indicate "Exclu"
; Si ce n'est pas un fichier "OK/ignoré" et que include=0, on indique "Exclu"
If rows()\stat <> "OK" And rows()\stat <> "!!"
If rows()\include = 0
label$ + " — Exclu"
EndIf
EndIf
AddGadgetItem(#GListStatus, -1, label$)
file$ = rows()\file
SetGadgetItemText(#GListStatus, idx, file$, 1)
If rows()\include
SetGadgetItemState(#GListStatus, idx, #PB_ListIcon_Checked)
Else
SetGadgetItemState(#GListStatus, idx, 0)
EndIf
idx + 1
Next
ProcedureReturn CountGadgetItems(#GListStatus)
EndProcedure
; Toggle include state at given index / Bascule l'état d'inclusion à l'index donné
Procedure.i ToggleIncludeAt(index.i, List rows.FileRow())
If index < 0 : ProcedureReturn 0 : EndIf
Protected c.i = CountGadgetItems(#GListStatus)
If index >= c : ProcedureReturn 0 : EndIf
Protected state.i = GetGadgetItemState(#GListStatus, index)
Protected want.i
If state & #PB_ListIcon_Checked : want = 1 : Else : want = 0 : EndIf
Protected j.i = 0
ForEach rows()
If j = index
rows()\include = want
Break
EndIf
j + 1
Next
ProcedureReturn 1
EndProcedure
; Collect checked files into list / Collecte les fichiers cochés dans une liste
Procedure.i CollectIncludedFiles(List rows.FileRow(), List files.s())
ClearList(files())
ForEach rows()
If rows()\include
AddElement(files())
files() = rows()\file
EndIf
Next
ProcedureReturn ListSize(files())
EndProcedure
; Selective commit if files are checked / Commit sélectif si des fichiers sont cochés
Procedure.i DoCommitSelected(repoDir$, message$, doPush.i, remote$, branch$, List files.s())
If ListSize(files()) = 0
ProcedureReturn DoCommit(repoDir$, message$, doPush, remote$, branch$)
EndIf
Protected gc.GitCall, code.i, argsAdd$, q$
gc\workdir = repoDir$
q$ = Chr(34)
argsAdd$ = "add"
ForEach files()
argsAdd$ + " " + q$ + files() + q$
Next
gc\args = argsAdd$
code = RunGit(@gc)
If code <> 0
MessageRequester("Git add", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
gc\args = "commit -m " + Chr(34) + message$ + Chr(34)
code = RunGit(@gc)
If code <> 0
MessageRequester("Git commit", "Échec ou rien à valider: " + #LF$ + TrimNewlines(gc\errors) + #LF$ + TrimNewlines(gc\output), #PB_MessageRequester_Warning)
If doPush = 0 : ProcedureReturn 0 : EndIf
Else
MessageRequester("Git commit", "OK:" + #LF$ + TrimNewlines(gc\output), #PB_MessageRequester_Info)
EndIf
If doPush
ProcedureReturn DoPush(repoDir$, remote$, branch$)
EndIf
ProcedureReturn 1
EndProcedure
; =============================================================================
; ADVANCED WINDOW (BRANCHES) / FENÊTRE AVANCÉE (BRANCHES)
; =============================================================================
; Open advanced branch operations window / Ouvre la fenêtre d'opérations avancées sur les branches
Procedure.i OpenAdvancedWindow(repoDir$)
Protected b$
NewList blist.s()
ListBranches(repoDir$, blist())
If OpenWindow(#WAdv, 0, 0, 440, 160, "Actions avancées — Branches", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
TextGadget(#GAdvLabel, 10, 14, 140, 24, "Branche :")
ComboBoxGadget(#GAdvCombo, 160, 12, 270, 24)
ForEach blist() : AddGadgetItem(#GAdvCombo, -1, blist()) : Next
GadgetToolTip(#GAdvCombo, "Sélectionnez la branche cible.")
ButtonGadget(#GAdvSwitch, 10, 60, 170, 30, "Basculer sur la branche")
GadgetToolTip(#GAdvSwitch, "git switch <branche> (ou git checkout).")
ButtonGadget(#GAdvRestore, 190, 60, 170, 30, "Restaurer depuis la branche")
GadgetToolTip(#GAdvRestore, "git restore --source <branche> -- . (remplace le contenu de travail).")
ButtonGadget(#GAdvClose, 370, 60, 60, 30, "Fermer")
Repeat
Protected ev.i = WaitWindowEvent()
If ev = #PB_Event_Gadget
Select EventGadget()
Case #GAdvSwitch
b$ = GetGadgetText(#GAdvCombo)
If b$ <> ""
If SwitchToBranch(repoDir$, b$)
MessageRequester("Branche", "Basculé sur '" + b$ + "'.", #PB_MessageRequester_Info)
EndIf
EndIf
Case #GAdvRestore
b$ = GetGadgetText(#GAdvCombo)
If b$ <> ""
If MessageRequester("Restauration", "Cette action va restaurer le contenu depuis '" + b$ + "'." + #LF$ + "Continuer ?", #PB_MessageRequester_YesNo) = #PB_MessageRequester_Yes
If RestoreFromBranch(repoDir$, b$)
MessageRequester("Restauration", "Contenu restauré depuis '" + b$ + "'.", #PB_MessageRequester_Info)
EndIf
EndIf
EndIf
Case #GAdvClose
CloseWindow(#WAdv)
ProcedureReturn 1
EndSelect
EndIf
Until ev = #PB_Event_CloseWindow
ProcedureReturn 1
EndIf
ProcedureReturn 0
EndProcedure
; Switch to specified branch / Bascule vers la branche spécifiée
Procedure.i SwitchToBranch(repoDir$, branch$)
Protected gc.GitCall
gc\workdir = repoDir$
gc\args = "switch " + Chr(34) + branch$ + Chr(34)
If RunGit(@gc) = 0 : ProcedureReturn 1 : EndIf
; Fallback for older Git versions / Fallback pour versions anciennes
gc\args = "checkout " + Chr(34) + branch$ + Chr(34)
If RunGit(@gc) = 0 : ProcedureReturn 1 : EndIf
MessageRequester("Branche", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
; Restore working directory from specified branch / Restaure le répertoire de travail depuis la branche spécifiée
Procedure.i RestoreFromBranch(repoDir$, branch$)
Protected gc.GitCall
gc\workdir = repoDir$
gc\args = "restore --source " + Chr(34) + branch$ + Chr(34) + " -- ."
If RunGit(@gc) = 0 : ProcedureReturn 1 : EndIf
MessageRequester("Restauration", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
; =============================================================================
; STATUS LABEL CONVERSION / CONVERSION DES LABELS DE STATUT
; =============================================================================
; Convert Git porcelain status codes to readable labels
; Convertit les codes de statut porcelain Git en labels lisibles
; Convert Git porcelain status codes to readable labels
; Convertit les codes de statut porcelain Git en labels lisibles
Procedure.s PorcelainToLabel(code$)
Protected x$ = Left(code$, 1)
Protected y$ = Right(code$, 1)
; Custom status codes / Codes de statut personnalisés
If code$ = "EX"
ProcedureReturn "Exclu (permanent)"
EndIf
If code$ = "NF"
ProcedureReturn "Introuvable (FS)"
EndIf
If code$ = "OK"
ProcedureReturn "À jour (suivi)"
EndIf
If code$ = "??"
ProcedureReturn "Nouveau (non suivi)"
EndIf
If code$ = "!!"
ProcedureReturn "Ignoré (.gitignore)"
EndIf
; Deleted files / Fichiers supprimés
If x$ = "D" Or y$ = "D"
ProcedureReturn "Supprimé"
EndIf
; Index status (X column) / Statut de l'index (colonne X)
If x$ = "M" : ProcedureReturn "Modifié (indexé)" : EndIf
If x$ = "A" : ProcedureReturn "Ajouté (indexé)" : EndIf
If x$ = "R" : ProcedureReturn "Renommé (indexé)" : EndIf
If x$ = "C" : ProcedureReturn "Copié (indexé)" : EndIf
; Working tree status (Y column) / Statut du répertoire de travail (colonne Y)
If y$ = "M" : ProcedureReturn "Modifié (non indexé)" : EndIf
If y$ = "A" : ProcedureReturn "Ajouté (non indexé)" : EndIf
; Default for other cases / Par défaut pour les autres cas
ProcedureReturn "Changement"
EndProcedure
; =============================================================================
; GIT CONFIGURATION / CONFIGURATION GIT
; =============================================================================
; Get local Git configuration value / Récupère une valeur de configuration Git locale
Procedure.s GetLocalConfig(repoDir$, key$)
Protected gc.GitCall
gc\workdir = repoDir$
gc\args = "config --get " + key$
If RunGit(@gc) = 0
ProcedureReturn TrimNewlines(gc\output)
EndIf
ProcedureReturn ""
EndProcedure
; Configuration wizard for Git identity / Assistant de configuration pour l'identité Git
Procedure.i ConfigIdentityWizard(repoDir$)
Protected curName$ = GetLocalConfig(repoDir$, "user.name")
Protected curMail$ = GetLocalConfig(repoDir$, "user.email")
Protected name$ = InputRequester("Identité Git", "Nom d'auteur (user.name)", curName$)
If name$ = "" : ProcedureReturn 0 : EndIf
Protected mail$ = InputRequester("Identité Git", "Email (user.email)", curMail$)
If mail$ = "" Or FindString(mail$, "@", 1) = 0
MessageRequester("Identité Git", "Email invalide.", #PB_MessageRequester_Warning)
ProcedureReturn 0
EndIf
Protected gc.GitCall
gc\workdir = repoDir$
; Set user name / Définir le nom d'utilisateur
gc\args = "config user.name " + Chr(34) + name$ + Chr(34)
If RunGit(@gc) <> 0
MessageRequester("Git config", "Échec user.name: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
; Set user email / Définir l'email d'utilisateur
gc\args = "config user.email " + Chr(34) + mail$ + Chr(34)
If RunGit(@gc) <> 0
MessageRequester("Git config", "Échec user.email: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
MessageRequester("Identité Git", "Identité locale configurée.", #PB_MessageRequester_Info)
ProcedureReturn 1
EndProcedure
; Create default .gitignore file / Crée un fichier .gitignore par défaut
Procedure.i MakeDefaultGitignore(repoDir$)
Protected base$ = repoDir$
If Right(base$, 1) <> #PathSep$ : base$ + #PathSep$ : EndIf
Protected path$ = base$ + ".gitignore"
If FileSize(path$) > 0
If MessageRequester(".gitignore", "Un .gitignore existe déjà. Le remplacer ?", #PB_MessageRequester_YesNo) = #PB_MessageRequester_No
ProcedureReturn 0
EndIf
EndIf
; Default ignore patterns for PureBasic / Patterns d'ignore par défaut pour PureBasic
Protected txt$ = ""
txt$ + "# PureBasic / build artefacts" + #LF$
txt$ + "*.exe" + #LF$
txt$ + "*.dll" + #LF$
txt$ + "*.so" + #LF$
txt$ + "*.dylib" + #LF$
txt$ + "*.o" + #LF$
txt$ + "*.obj" + #LF$
txt$ + "*.pdb" + #LF$
txt$ + "*.res" + #LF$
txt$ + "*.a" + #LF$
txt$ + "*.lib" + #LF$
txt$ + "*.d" + #LF$
txt$ + "*.map" + #LF$
txt$ + "*.dbg" + #LF$
txt$ + "*.log" + #LF$
txt$ + "*.temp" + #LF$
txt$ + "*.bak" + #LF$
txt$ + "*.cache" + #LF$
txt$ + #LF$
txt$ + "# IDE / OS" + #LF$
txt$ + ".DS_Store" + #LF$
txt$ + "Thumbs.db" + #LF$
txt$ + ".idea" + #LF$
txt$ + ".vscode" + #LF$
Protected ok.i
If CreateFile(0, path$)
WriteString(0, txt$) : CloseFile(0) : ok = 1
EndIf
If ok
MessageRequester(".gitignore", "Fichier .gitignore créé.", #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
MessageRequester(".gitignore", "Échec de création.", #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
; Check if 'dir is a Git repository / Vérifie si 'dir est un dépôt Git
; - Try via 'git rev-parse --is-inside-work-tree'
; - Fallback: presence of .git folder
; - Essaie via 'git rev-parse --is-inside-work-tree'
; - Fallback: présence du dossier .git
Procedure.i IsGitRepo(dir$)
Protected gc.GitCall
Protected isRepo.i = 0
Protected dotGitDir$ = dir$
If Right(dotGitDir$, 1) <> #PathSep$
dotGitDir$ + #PathSep$
EndIf
dotGitDir$ + ".git"
gc\workdir = dir$
gc\args = "rev-parse --is-inside-work-tree"
If RunGit(@gc) = 0
If FindString(LCase(TrimNewlines(gc\output)), "true", 1)
isRepo = 1
EndIf
Else
; Fallback: .git folder present? / Fallback : dossier .git présent ?
If FileSize(dotGitDir$) = -2
isRepo = 1
EndIf
EndIf
If #EnableDebug
Debug "[IsGitRepo] " + dir$ + " -> " + Str(isRepo)
EndIf
ProcedureReturn isRepo
EndProcedure
; =============================================================================
; FILE RESTORATION / RESTAURATION DE FICHIERS
; =============================================================================
; Restore file to specific commit state / Restaure un fichier à l'état d'un commit précis
; - Try 'git restore --source <commit> -- <file>'
; - Fallback 'git checkout <commit> -- <file>' (for older Git)
; - Essaye 'git restore --source <commit> -- <file>'
; - Fallback 'git checkout <commit> -- <file>' (pour Git anciens)
Procedure.i RestoreFileFromCommit(repoDir$, file$, commit$)
Protected gc.GitCall, q$ = Chr(34)
Protected path$ = GitPath(file$)
gc\workdir = repoDir$
gc\args = "restore --source " + q$ + commit$ + q$ + " -- " + q$ + path$ + q$
If #EnableDebug
Debug "[RestoreFile] " + gc\args + " (wd=" + repoDir$ + ")"
EndIf
If RunGit(@gc) = 0
MessageRequester("Restaurer", "Le fichier a été restauré depuis le commit " + commit$ + ".", #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
; Fallback for Git compatibility / Fallback pour compatibilité Git
gc\args = "checkout " + q$ + commit$ + q$ + " -- " + q$ + path$ + q$
If #EnableDebug
Debug "[RestoreFile fallback] " + gc\args
EndIf
If RunGit(@gc) = 0
MessageRequester("Restaurer", "Le fichier a été restauré (fallback checkout) depuis " + commit$ + ".", #PB_MessageRequester_Info)
ProcedureReturn 1
EndIf
MessageRequester("Restaurer", "Échec : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
; Open window listing commits for selected file, then restore file to chosen commit
; Ouvre une fenêtre listant les commits du fichier sélectionné, puis restaure le fichier vers le commit choisi
; Features / Fonctionnalités :
; - Read path from gadget AND from rows()
; - Normalize path (GitPath), follow renames (--follow) + fallback --all
; - Now shows date + time (ISO format with timezone)
; - Lit le nom depuis le gadget ET depuis rows()
; - Normalise le chemin (GitPath), suit les renommages (--follow) + fallback --all
; - Affiche maintenant date + heure (format ISO, avec fuseau)
Procedure.i OpenRestoreFileWindow(repoDir$, List rows.FileRow())
Protected idx.i = GetGadgetState(#GListStatus)
If idx < 0
MessageRequester("Restaurer", "Sélectionnez d'abord un fichier dans la liste.", #PB_MessageRequester_Info)
ProcedureReturn 0
EndIf
; Get filename from list (column 1 = path) / Récupérer le nom de fichier depuis la liste (colonne 1 = chemin)
Protected target$ = GetGadgetItemText(#GListStatus, idx, 1)
If target$ = ""
MessageRequester("Restaurer", "Aucun fichier sélectionné.", #PB_MessageRequester_Info)
ProcedureReturn 0
EndIf
; Normalize for Git (slashes, ./) / Normaliser pour Git (slashs, ./)
Protected fileArg$ = GitPath(target$)
Protected gc.GitCall, out$, line$, n.i, i.i, q$ = Chr(34)
; 1) History with time (--date=iso shows YYYY-MM-DD HH:MM:SS +ZZZZ)
; 1) Historique avec heure (--date=iso affiche YYYY-MM-DD HH:MM:SS +ZZZZ)
gc\workdir = repoDir$
gc\args = "log --follow --date=iso --pretty=format:%h%x09%ad%x09%s -n 200 -- " + q$ + fileArg$ + q$
If #EnableDebug : Debug "[Restore log] " + gc\args : EndIf
If RunGit(@gc) <> 0
MessageRequester("Restaurer", "Échec du log : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
out$ = gc\output
; 2) Fallback: all refs (useful if history is on another branch)
; 2) Fallback : toutes les refs (utile si l'historique est sur une autre branche)
If Trim(out$) = ""
gc\args = "log --all --follow --date=iso --pretty=format:%h%x09%ad%x09%s -n 200 -- " + q$ + fileArg$ + q$
If #EnableDebug : Debug "[Restore log fallback --all] " + gc\args : EndIf
If RunGit(@gc) = 0
out$ = gc\output
EndIf
EndIf
If Trim(out$) = ""
MessageRequester("Restaurer", "Aucun commit trouvé pour ce fichier." + #LF$ +
"Vérifiez que le fichier est (ou a été) suivi par Git et qu'il a déjà été committé.",
#PB_MessageRequester_Info)
ProcedureReturn 0
EndIf
; 3) UI: commit list with Date/Time / UI : liste des commits avec Date/Heure
NewList hashes.s()
If OpenWindow(#WRestore, 0, 0, 780, 440, "Restaurer : " + target$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
TextGadget(#GRestInfo, 10, 10, 760, 22, "Choisissez le commit vers lequel restaurer le fichier.")
ListIconGadget(#GRestList, 10, 40, 760, 340, "Commit", 110, #PB_ListIcon_FullRowSelect)
AddGadgetColumn(#GRestList, 1, "Date / Heure", 190) ; Widened for time and timezone / Élargi pour l'heure et le fuseau
AddGadgetColumn(#GRestList, 2, "Message", 440)
ButtonGadget(#GRestOK, 550, 390, 100, 28, "Restaurer")
ButtonGadget(#GRestCancel, 660, 390, 100, 28, "Annuler")
n = CountString(out$, #LF$) + 1
For i = 1 To n
line$ = StringField(out$, i, #LF$)
If Trim(line$) <> ""
Protected h$ = StringField(line$, 1, #TAB$) ; Abbreviated hash / Hash abrégé
Protected d$ = StringField(line$, 2, #TAB$) ; Date + time (+ZZZZ) / Date + heure (+ZZZZ)
Protected s$ = StringField(line$, 3, #TAB$) ; Message / Message
AddGadgetItem(#GRestList, -1, h$ + #LF$ + d$ + #LF$ + s$)
AddElement(hashes()) : hashes() = h$
If #EnableDebug : Debug "[Restore list] " + h$ + " | " + d$ + " | " + s$ : EndIf
EndIf
Next
Repeat
Protected ev.i = WaitWindowEvent()
If ev = #PB_Event_Gadget
Select EventGadget()
Case #GRestOK
idx = GetGadgetState(#GRestList)
If idx < 0
MessageRequester("Restaurer", "Sélectionnez un commit.", #PB_MessageRequester_Warning)
Else
; Get chosen hash / Récupérer le hash choisi
Protected k.i = 0, chosen$
ForEach hashes()
If k = idx : chosen$ = hashes() : Break : EndIf
k + 1
Next
If chosen$ <> ""
If RestoreFileFromCommit(repoDir$, fileArg$, chosen$)
CloseWindow(#WRestore)
ProcedureReturn 1
EndIf
EndIf
EndIf
Case #GRestCancel
CloseWindow(#WRestore)
ProcedureReturn 0
EndSelect
EndIf
Until ev = #PB_Event_CloseWindow
ProcedureReturn 0
EndIf
ProcedureReturn 0
EndProcedure
; Renomme un fichier avec git mv
; =============================================================================
; GUIDE PANEL / PANNEAU GUIDE
; =============================================================================
; Update "Guide" panel with step-by-step instructions
; Met à jour le panneau "Guide" avec un pas-à-pas adapté
; - Include "Init repo" step if folder isn't a repository yet
; - Remind difference between Commit / Push for beginners
; - Inclut l'étape "Init repo" si le dossier n'est pas encore un dépôt
; - Rappelle la différence Commit / Push pour débutants
Procedure.i UpdateGuide(repoDir$, List rows.FileRow(), branch$, remote$)
Protected tips$ = ""
Protected hasChanges.i = 0
Protected name$ = GetLocalConfig(repoDir$, "user.name")
Protected mail$ = GetLocalConfig(repoDir$, "user.email")
Protected repoReady.i = IsGitRepo(repoDir$)
; Are there lines in status? / Y a-t-il des lignes dans le status ?
ForEach rows()
hasChanges = 1
Break
Next
tips$ + "Bienvenue !" + #LF$
tips$ + "- Ce panneau vous guide pas à pas." + #LF$ + #LF$
If repoReady = 0
; ---- Repository not initialized: start with git init ----
; ---- Dépôt non initialisé : on commence par git init ----
tips$ + "Étapes de départ :" + #LF$
tips$ + "1) " + Chr(149) + " Cliquez sur le bouton 'Init repo' pour INITIALISER le dépôt dans ce dossier." + #LF$
tips$ + "2) " + Chr(149) + " (Recommandé) Créez un '.gitignore' avec le bouton dédié, pour ignorer les binaires/artefacts." + #LF$
tips$ + "3) " + Chr(149) + " Configurez votre identité (bouton 'Configurer identité…')." + #LF$
tips$ + "4) " + Chr(149) + " Cochez les fichiers à inclure, saisissez un message, puis 'Add + Commit'." + #LF$
tips$ + "5) " + Chr(149) + " Pour partager en ligne : créez un dépôt distant (GitHub/GitLab…) puis utilisez 'Push'." + #LF$ + #LF$
Else
; ---- Repository already ready ---- / ---- Dépôt déjà prêt ----
tips$ + "Étapes :" + #LF$
tips$ + "1) " + Chr(149) + " (Optionnel) Créez/complétez un '.gitignore' si nécessaire." + #LF$
tips$ + "2) " + Chr(149) + " Vérifiez l'identité Git locale." + #LF$
If name$ = "" Or mail$ = ""
tips$ + " → Identité : INCOMPLÈTE (utilisez 'Configurer identité…')." + #LF$
Else
tips$ + " → Identité : " + name$ + " <" + mail$ + ">" + #LF$
EndIf
tips$ + "3) " + Chr(149) + " Cochez les fichiers à inclure au prochain commit (bouton 'Diff…' pour voir les détails)." + #LF$
If hasChanges
tips$ + " → Des modifications sont détectées ci-dessus." + #LF$
Else
tips$ + " → Aucune modification détectée pour l'instant." + #LF$
EndIf
tips$ + "4) " + Chr(149) + " Saisissez un message, cliquez 'Add + Commit' (et cochez 'Pousser après' si vous voulez envoyer au serveur)." + #LF$
tips$ + "5) " + Chr(149) + " Pour changer/restaurer une branche : 'Avancé…'." + #LF$ + #LF$
EndIf
tips$ + "Infos :" + #LF$
tips$ + "• Remote : " + remote$ + " • Branche : " + branch$ + #LF$
tips$ + "• 'Diff…' affiche le détail d'un fichier sélectionné." + #LF$ + #LF$
; Educational reminder (commit vs push) / Rappel pédagogique (commit vs push)
tips$ + "Rappel : différence entre Commit et Push" + #LF$
tips$ + "• Commit : enregistre localement un instantané de vos fichiers (dans l'historique du dépôt sur votre machine)." + #LF$
tips$ + "• Push : envoie vos commits locaux vers un serveur distant (GitHub, GitLab, etc.)." + #LF$
tips$ + "→ On peut faire des commits hors-ligne ; le push nécessite un dépôt distant configuré (ex. 'origin') et une branche (ex. 'main')." + #LF$
SetGadgetText(#GGuide, tips$)
If #EnableDebug
Debug "[UpdateGuide] repoReady=" + Str(repoReady) + " changes=" + Str(hasChanges)
EndIf
ProcedureReturn 1
EndProcedure
; =============================================================================
; FULL FILE LIST BUILDING / CONSTRUCTION DE LA LISTE COMPLÈTE DE FICHIERS
; =============================================================================
; Build rows() list with all files (according to options), WITHOUT name correction
; Construit la liste rows() avec tous les fichiers (selon options), SANS correction de nom
; - showClean=1 → include tracked "clean" files (OK)
; - showIgnored=1 → also include ignored files (!!)
; - Check disk-side existence; if missing and not a delete, mark "NF" (Not Found)
; - rows()\include = 1 for elements potentially to commit (≠ OK/!!)
; - showClean=1 → inclut les fichiers suivis "propres" (OK)
; - showIgnored=1 → inclut aussi les fichiers ignorés (!!)
; - Vérifie l'existence côté disque ; si absent et pas un delete, marque "NF" (Introuvable)
; - rows()\include = 1 pour les éléments potentiellement à committer (≠ OK/!!)
; Build rows() list with all files (according to options), WITHOUT name correction
; Construit la liste rows() avec tous les fichiers (selon options), SANS correction de nom
; - showClean=1 → include tracked "clean" files (OK)
; - showIgnored=1 → also include ignored files (!!)
; - Check disk-side existence; if missing and not a delete, mark "NF" (Not Found)
; - rows()\include = 1 for elements potentially to commit (≠ OK/!!) BUT NOT for deleted files
; - showClean=1 → inclut les fichiers suivis "propres" (OK)
; - showIgnored=1 → inclut aussi les fichiers ignorés (!!)
; - Vérifie l'existence côté disque ; si absent et pas un delete, marque "NF" (Introuvable)
; - rows()\include = 1 pour les éléments potentiellement à committer (≠ OK/!!) MAIS PAS pour les fichiers supprimés
Procedure.i BuildFullFileList(repoDir$, showClean.i, showIgnored.i, List rows.FileRow())
Protected gc.GitCall
Protected text$, line$, code$, file$
Protected i.i, n.i, start.i, ch$, pos.i, exists.i, isDelete.i
ClearList(rows())
; Load permanent local exclusions / Charger exclusions permanentes locales
NewList excl.s()
LoadLocalExcludes(repoDir$, excl())
; 1) status --porcelain --ignored
NewMap statusMap.s()
gc\workdir = repoDir$
gc\args = "status --porcelain --ignored"
If RunGit(@gc) <> 0
MessageRequester("Git status", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
text$ = gc\output
n = CountString(text$, #LF$) + 1
For i = 1 To n
line$ = Trim(StringField(text$, i, #LF$))
If line$ = "" : Continue : EndIf
code$ = Left(line$, 2)
start = 3
While start <= Len(line$)
ch$ = Mid(line$, start, 1)
If ch$ <> " " And ch$ <> #TAB$
Break
EndIf
start + 1
Wend
file$ = Mid(line$, start)
pos = FindString(file$, "->", 1)
If pos > 0 : file$ = Trim(Mid(file$, pos + 2)) : EndIf
If Left(file$, 2) = "./" : file$ = Mid(file$, 3) : EndIf
file$ = ReplaceString(file$, "/", #PathSep$)
AddMapElement(statusMap(), file$)
statusMap() = code$
Next
; 2) ls-files → tracked / Suivis
NewMap trackedMap.i()
gc\args = "ls-files"
If RunGit(@gc) = 0
text$ = gc\output
n = CountString(text$, #LF$) + 1
For i = 1 To n
file$ = Trim(StringField(text$, i, #LF$))
If file$ = "" : Continue : EndIf
If Left(file$, 2) = "./" : file$ = Mid(file$, 3) : EndIf
file$ = ReplaceString(file$, "/", #PathSep$)
AddMapElement(trackedMap(), file$) : trackedMap() = 1
Next
EndIf
; 3) Add tracked files (clean/modified) / Ajout suivis (propres/modifiés)
ForEach trackedMap()
file$ = MapKey(trackedMap())
If FindMapElement(statusMap(), file$)
code$ = statusMap()
Else
code$ = "OK"
EndIf
If code$ = "OK" And showClean = 0 : Continue : EndIf
If code$ = "!!" And showIgnored = 0 : Continue : EndIf
exists = RepoFileExists(repoDir$, file$)
isDelete = 0
If Left(code$, 1) = "D" Or Right(code$, 1) = "D" : isDelete = 1 : EndIf
AddElement(rows())
rows()\file = file$
If IsPermanentlyExcluded(file$, excl())
rows()\stat = "EX"
rows()\include = 0
ElseIf exists = 0 And isDelete = 0
rows()\stat = "NF"
rows()\include = 0
Else
rows()\stat = code$
; Ne pas cocher par défaut les fichiers supprimés (D) et les fichiers OK/ignorés
rows()\include = Bool(code$ <> "OK" And code$ <> "!!" And isDelete = 0)
EndIf
Next
; 4) Add untracked (??/!!) outside trackedMap / Ajouter non suivis (??/!!) hors trackedMap
ForEach statusMap()
file$ = MapKey(statusMap())
code$ = statusMap()
If FindMapElement(trackedMap(), file$) = 0
If code$ = "!!" And showIgnored = 0 : Continue : EndIf
exists = RepoFileExists(repoDir$, file$)
AddElement(rows())
rows()\file = file$
If IsPermanentlyExcluded(file$, excl())
rows()\stat = "EX"
rows()\include = 0
ElseIf exists = 0 And Left(code$, 1) <> "D" And Right(code$, 1) <> "D"
rows()\stat = "NF"
rows()\include = 0
Else
rows()\stat = code$
; Ne pas cocher par défaut les fichiers supprimés
Protected isDeleteUntracked.i = 0
If Left(code$, 1) = "D" Or Right(code$, 1) = "D" : isDeleteUntracked = 1 : EndIf
rows()\include = Bool(isDeleteUntracked = 0)
EndIf
EndIf
Next
ProcedureReturn ListSize(rows())
EndProcedure
; Get filename from line 'index' from internal list / Récupère le nom de fichier de la ligne 'index' depuis la liste interne
Procedure.s FileFromRowsByIndex(index.i, List rows.FileRow())
Protected j.i = 0
ForEach rows()
If j = index
ProcedureReturn rows()\file
EndIf
j + 1
Next
ProcedureReturn ""
EndProcedure
; =============================================================================
; SORTING BY INTEREST / TRI PAR INTÉRÊT
; =============================================================================
; Sort rows() list by "interest" (see order above) then by path
; Trie la liste rows() par "intérêt" (cf. ordre ci-dessus) puis par chemin
; Uses buckets then reassembles. No underscore, no IIf.
; Utilise des seaux (buckets) puis recolle le tout. Pas de underscore, pas de IIf.
; Sort rows() list by "interest" (see order above) then by path
; Trie la liste rows() par "intérêt" (cf. ordre ci-dessus) puis par chemin
; Uses buckets then reassembles. No underscore, no IIf.
; Utilise des seaux (buckets) puis recolle le tout. Pas de underscore, pas de IIf.
Procedure.i SortRowsByInterest(List rows.FileRow())
; Local categorization / Catégorisation locale
Protected ProcedureReturnValue.i = 0
Protected cat.i
Protected x$, y$
; Buckets / Seaux
NewList b0.FileRow() ; Conflicts (U) / Conflits (U)
NewList b1.FileRow() ; Indexed changes (X) / Changements indexés (X)
NewList b2.FileRow() ; Non-indexed changes (Y) / Changements non indexés (Y)
NewList b3.FileRow() ; New (??) / Nouveaux (??)
NewList b4.FileRow() ; OK
NewList b5.FileRow() ; Ignored (!!) / Ignorés (!!)
NewList b6.FileRow() ; Permanently excluded (EX) / Exclus permanents (EX)
NewList b7.FileRow() ; Deleted (D) / Supprimés (D)
NewList b8.FileRow() ; Not found (NF) / Introuvables (NF)
; Helper: push a FileRow into a bucket / Helper: pousse un FileRow dans un seau
Macro PushTo(bucket)
AddElement(bucket())
bucket()\file = rows()\file
bucket()\stat = rows()\stat
bucket()\include = rows()\include
EndMacro
; 1) Distribution into buckets / Répartition dans les seaux
ForEach rows()
cat = -1
x$ = Left(rows()\stat, 1)
y$ = Right(rows()\stat, 1)
If FindString(rows()\stat, "U", 1) > 0
cat = 0 ; Conflicts / Conflits
ElseIf rows()\stat = "??"
cat = 3 ; New files / Nouveaux fichiers
ElseIf rows()\stat = "OK"
cat = 4 ; Up to date / À jour
ElseIf rows()\stat = "!!"
cat = 5 ; Ignored / Ignorés
ElseIf rows()\stat = "EX"
cat = 6 ; Permanently excluded / Exclus permanents
ElseIf x$ = "D" Or y$ = "D"
cat = 7 ; Deleted / Supprimés
ElseIf rows()\stat = "NF"
cat = 8 ; Not found / Introuvables
Else
; Other porcelain XY codes / Autres codes porcelain XY
If x$ <> " "
cat = 1 ; Indexed changes / Changements indexés
ElseIf y$ <> " "
cat = 2 ; Working tree changes / Changements répertoire de travail
Else
; Safety, consider as non-indexed / Par sécurité, considère comme non indexé
cat = 2
EndIf
EndIf
Select cat
Case 0 : PushTo(b0)
Case 1 : PushTo(b1)
Case 2 : PushTo(b2)
Case 3 : PushTo(b3)
Case 4 : PushTo(b4)
Case 5 : PushTo(b5)
Case 6 : PushTo(b6)
Case 7 : PushTo(b7)
Default: PushTo(b8)
EndSelect
Next
; 2) Alphabetical sort by path in each bucket / Tri alphabétique par chemin dans chaque seau
SortStructuredList(b0(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
SortStructuredList(b1(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
SortStructuredList(b2(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
SortStructuredList(b3(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
SortStructuredList(b4(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
SortStructuredList(b5(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
SortStructuredList(b6(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
SortStructuredList(b7(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
SortStructuredList(b8(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String)
; 3) Recomposition of rows() in bucket order / Recomposition de rows() dans l'ordre des seaux
ClearList(rows())
Macro AppendBucket(bucket2)
ForEach bucket2()
AddElement(rows())
rows()\file = bucket2()\file
rows()\stat = bucket2()\stat
rows()\include = bucket2()\include
ProcedureReturnValue + 1
Next
EndMacro
AppendBucket(b0) ; Conflicts / Conflits
AppendBucket(b1) ; Indexed changes / Changements indexés
AppendBucket(b2) ; Non-indexed changes / Changements non indexés
AppendBucket(b3) ; New files / Nouveaux fichiers
AppendBucket(b4) ; OK
AppendBucket(b5) ; Ignored / Ignorés
AppendBucket(b6) ; Permanently excluded / Exclus permanents
AppendBucket(b7) ; Deleted / Supprimés
AppendBucket(b8) ; Not found / Introuvables
If #EnableDebug
Debug "[SortRows] total=" + Str(ProcedureReturnValue)
EndIf
ProcedureReturn ProcedureReturnValue
EndProcedure
; Apply English labels/tooltips to main window when needed.
; Applique les libellés/bulles en anglais sur la fenêtre principale si nécessaire.
Procedure.i LocalizeGUI()
If gLanguage = #LangFrench
ProcedureReturn 1 ; Nothing to change / Rien à changer
EndIf
; Window title
SetWindowTitle(#GWindow, "PBIDE-GitTool — Git (simple mode)")
; Header
SetGadgetText(#GLabelRepo, "Repository:")
SetGadgetText(#GButtonBrowse, "Browse…")
; Options
SetGadgetText(#GShowClean, "Show tracked up-to-date")
SetGadgetText(#GShowIgnored, "Show ignored (.gitignore)")
SetGadgetText(#GShowPermanent, "Show permanently excluded")
GadgetToolTip(#GShowIgnored, "Show files ignored by rules (.gitignore, global exclude) — status '!!'.")
GadgetToolTip(#GShowPermanent, "Show files excluded via the tool (.git/info/exclude) — status 'EX'.")
; List columns
SetGadgetItemText(#GListStatus, -1, "Status", 0)
SetGadgetItemText(#GListStatus, -1, "File", 1)
; Action strip
SetGadgetText(#GRefresh, "Refresh")
SetGadgetText(#GInit, "Init repo")
SetGadgetText(#GExcludeForever, "Exclude (permanent)")
SetGadgetText(#GReincludeForever, "Re-include (permanent)")
SetGadgetText(#GDiff, "Diff…")
SetGadgetText(#GRestoreFile, "Restore…")
SetGadgetText(#GRenameFile, "Rename…")
GadgetToolTip(#GRenameFile, "Rename the selected file with git mv.")
; Msg/remote/branch
SetGadgetText(#GLabelMsg, "Message:")
SetGadgetText(#GLabelRemote, "Remote:")
SetGadgetText(#GLabelBranch, "Branch:")
SetGadgetText(#GSavePrefs, "Defaults")
SetGadgetText(#GAddBranch, "Add branch…")
SetGadgetText(#GReloadBranches, "Reload branches")
; Main actions
SetGadgetText(#GCommit, "Add + Commit (checked)")
SetGadgetText(#GAdvanced, "Advanced…")
SetGadgetText(#GConfig, "Configure identity…")
GadgetToolTip(#GCommit, "Commits ONLY checked lines.")
GadgetToolTip(#GPush, "Push local commits to the remote (manual).")
GadgetToolTip(#GRestoreFile, "Restore the selected file to a specific commit.")
GadgetToolTip(#GDiff, "Show diff for the selected file.")
ProcedureReturn 1
EndProcedure
; =============================================================================
; MAIN GUI / INTERFACE PRINCIPALE
; =============================================================================
; Open main interface / Ouvre l'interface principale
; Features / Fonctionnalités :
; - Manual push, scrollable guide
; - Repository prefs correctly reloaded & auto-saved
; - Sort by interest before display
; - Push manuel, guide scrollable
; - Prefs dépôt correctement rechargées & auto-sauvegardées
; - Tri par intérêt avant affichage
; Open main interface / Ouvre l'interface principale
Procedure.i OpenGUI(initialDir$, prefsPath$)
Protected repoDir$ = DetectRepoRoot(initialDir$)
; Global defaults / Défauts globaux
Protected defRemote$ = ""
Protected defBranch$ = ""
LoadPrefs(prefsPath$, defRemote$, defBranch$)
; Repository prefs (via structure) / Prefs du dépôt (via structure)
Protected rp.RepoPrefs
rp\remote = defRemote$
rp\branch = defBranch$
LoadRepoPrefs(repoDir$, @rp)
; Persistent selection / Sélection persistante
Protected keepIdx.i
Protected keepFile$
; Automatically ignore our local prefs / Ignorer automatiquement notre prefs locale
EnsureToolFilesExcluded(repoDir$)
If OpenWindow(#GWindow, 0, 0, 950, 800, "PBIDE-GitTool — Git (mode simplifié)", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
; --- SECTION 1: Configuration du dépôt (10-45) ---
TextGadget(#GLabelRepo, 15, 15, 60, 22, "Dépôt :")
StringGadget(#GStringRepo, 80, 13, 750, 26, repoDir$)
ButtonGadget(#GButtonBrowse, 840, 13, 95, 26, "Parcourir…")
; --- SECTION 2: Actions de base sur le dépôt (50-85) ---
ButtonGadget(#GRefresh, 15, 50, 95, 30, "Rafraîchir")
ButtonGadget(#GInit, 120, 50, 95, 30, "Init repo")
; --- SECTION 3: Options d'affichage (55-85) ---
; Regroupement logique des options sur une seule ligne avec plus d'espace
CheckBoxGadget(#GShowClean, 240, 55, 200, 22, "Afficher suivis à jour")
SetGadgetState(#GShowClean, #True)
CheckBoxGadget(#GShowIgnored, 450, 55, 220, 22, "Afficher ignorés (.gitignore)")
SetGadgetState(#GShowIgnored, #False)
GadgetToolTip(#GShowIgnored, "Montre les fichiers ignorés par règles (.gitignore, exclude global) — statut '!!'.")
CheckBoxGadget(#GShowPermanent, 680, 55, 220, 22, "Afficher exclus permanents")
SetGadgetState(#GShowPermanent, #True)
GadgetToolTip(#GShowPermanent, "Montre les fichiers exclus via l'outil (.git/info/exclude) — statut 'EX'.")
; --- SECTION 4: Liste des fichiers (90-425) ---
ListIconGadget(#GListStatus, 15, 90, 920, 335, "État", 120, #PB_ListIcon_CheckBoxes | #PB_ListIcon_FullRowSelect)
AddGadgetColumn(#GListStatus, 1, "Fichier", 780)
; --- SECTION 5: Actions sur les fichiers (435-475) ---
; Première ligne d'actions
ButtonGadget(#GDiff, 15, 435, 90, 30, "Diff…")
GadgetToolTip(#GDiff, "Afficher les différences du fichier sélectionné.")
ButtonGadget(#GRestoreFile, 115, 435, 110, 30, "Restaurer…")
GadgetToolTip(#GRestoreFile, "Restaurer le fichier sélectionné à un commit précis.")
ButtonGadget(#GRenameFile, 235, 435, 100, 30, "Renommer…")
GadgetToolTip(#GRenameFile, "Renommer le fichier sélectionné avec git mv.")
ButtonGadget(#GDeleteFile, 345, 435, 100, 30, "Supprimer")
GadgetToolTip(#GDeleteFile, "Supprimer le fichier sélectionné avec git rm.")
; Deuxième ligne d'actions (séparée pour éviter l'encombrement)
ButtonGadget(#GExcludeForever, 15, 475, 140, 30, "Exclure (permanent)")
ButtonGadget(#GReincludeForever, 165, 475, 150, 30, "Ré-inclure (permanent)")
; --- SECTION 6: Configuration commit/remote/branche (515-585) ---
; Message de commit
TextGadget(#GLabelMsg, 15, 520, 80, 22, "Message :")
StringGadget(#GStringMsg, 100, 518, 835, 26, "")
; Remote et branche sur une ligne séparée
TextGadget(#GLabelRemote, 15, 555, 60, 22, "Remote :")
StringGadget(#GStringRemote, 80, 553, 200, 26, "origin") ; Valeur par défaut
TextGadget(#GLabelBranch, 290, 555, 60, 22, "Branche :")
ComboBoxGadget(#GComboBranch, 355, 553, 180, 26)
; Boutons de gestion des branches
ButtonGadget(#GSavePrefs, 545, 553, 85, 26, "Défauts")
ButtonGadget(#GAddBranch, 640, 553, 110, 26, "Ajouter br.…")
ButtonGadget(#GReloadBranches, 760, 553, 120, 26, "Actualiser br.")
; --- SECTION 7: Actions principales Git (595-640) ---
ButtonGadget(#GCommit, 15, 595, 180, 35, "Add + Commit (cochés)")
GadgetToolTip(#GCommit, "Valide SEULEMENT les lignes cochées.")
ButtonGadget(#GPush, 205, 595, 120, 35, "Push")
GadgetToolTip(#GPush, "Pousse les commits locaux vers le dépôt distant (manuel).")
ButtonGadget(#GPull, 335, 595, 120, 35, "Pull")
ButtonGadget(#GAdvanced, 465, 595, 120, 35, "Avancé…")
ButtonGadget(#GConfig, 595, 595, 160, 35, "Configurer identité…")
; --- SECTION 8: Guide utilisateur (650-790) ---
EditorGadget(#GGuide, 15, 650, 920, 140)
SetGadgetAttribute(#GGuide, #PB_Editor_ReadOnly, 1)
; --- Local branches / Branches locales ---
NewList branchItems.s()
ListBranches(repoDir$, branchItems())
ClearGadgetItems(#GComboBranch)
ForEach branchItems()
AddGadgetItem(#GComboBranch, -1, branchItems())
Next
If rp\branch <> "" : SetGadgetText(#GComboBranch, rp\branch) : EndIf
; --- Files: build, sort, display / Fichiers : construction, tri, affichage ---
NewList rows.FileRow()
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
; =================== EVENT LOOP / BOUCLE ÉVÉNEMENTS ===================
Repeat
Protected ev.i = WaitWindowEvent()
Select ev
Case #PB_Event_Gadget
Select EventGadget()
; ---- Repository change / Changement de dépôt ----
Case #GButtonBrowse
Protected newDir$ = PathRequester("Choisir le répertoire du dépôt", repoDir$)
If newDir$ <> ""
repoDir$ = newDir$
SetGadgetText(#GStringRepo, repoDir$)
EnsureToolFilesExcluded(repoDir$)
; Reload repository preferences / Recharger les préférences du dépôt
rp\remote = defRemote$
rp\branch = defBranch$
LoadRepoPrefs(repoDir$, @rp)
SetGadgetText(#GStringRemote, rp\remote)
; Refresh branch list / Actualiser la liste des branches
ClearList(branchItems())
ListBranches(repoDir$, branchItems())
ClearGadgetItems(#GComboBranch)
ForEach branchItems() : AddGadgetItem(#GComboBranch, -1, branchItems()) : Next
If rp\branch <> "" : SetGadgetText(#GComboBranch, rp\branch) : EndIf
; Refresh file list / Actualiser la liste des fichiers
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
EndIf
; ---- Display filters / Filtres d'affichage ----
Case #GShowClean, #GShowIgnored, #GShowPermanent, #GRefresh
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
; ---- Initialize repository / Init dépôt ----
Case #GInit
DoInitRepo(repoDir$)
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
; ---- Permanent exclusion / re-inclusion / Exclusion permanente / ré-inclusion ----
Case #GExcludeForever
Protected idx.i = GetGadgetState(#GListStatus)
If idx >= 0
Protected target$ = GetGadgetItemText(#GListStatus, idx, 1)
If MessageRequester("Exclusion permanente", "Exclure définitivement de Git ?" + #LF$ + target$, #PB_MessageRequester_YesNo) = #PB_MessageRequester_Yes
If AddPermanentExclude(repoDir$, target$)
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
EndIf
EndIf
EndIf
Case #GReincludeForever
idx = GetGadgetState(#GListStatus)
If idx >= 0
target$ = GetGadgetItemText(#GListStatus, idx, 1)
If MessageRequester("Ré-inclure (permanent)", "Retirer des exclusions et ré-inclure ?" + #LF$ + target$, #PB_MessageRequester_YesNo) = #PB_MessageRequester_Yes
If RemovePermanentExclude(repoDir$, target$)
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
EndIf
EndIf
EndIf
; ---- Diff / Restore / Diff / Restaurer ----
Case #GDiff
OpenDiffWindow(repoDir$, rows())
Case #GRestoreFile
If OpenRestoreFileWindow(repoDir$, rows())
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
EndIf
Case #GRenameFile
idx.i = GetGadgetState(#GListStatus)
If idx >= 0
Protected oldFile$ = GetGadgetItemText(#GListStatus, idx, 1)
Protected newFile$ = InputRequester("Renommer", "Nouveau nom pour " + oldFile$ + " :", oldFile$)
If newFile$ <> "" And newFile$ <> oldFile$
If DoRenameFile(repoDir$, oldFile$, newFile$)
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
EndIf
EndIf
Else
MessageRequester("Renommer", "Sélectionnez d'abord un fichier dans la liste.", #PB_MessageRequester_Info)
EndIf
Case #GDeleteFile
idx.i = GetGadgetState(#GListStatus)
If idx >= 0
Protected file$ = GetGadgetItemText(#GListStatus, idx, 1)
If DoDeleteFile(repoDir$, file$)
; Rafraîchir la liste après suppression
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
EndIf
Else
MessageRequester("Supprimer", "Sélectionnez d'abord un fichier dans la liste.", #PB_MessageRequester_Info)
EndIf
; ---- Branches: update / add / Branches : maj / ajout ----
Case #GReloadBranches
ClearList(branchItems())
ListBranches(repoDir$, branchItems())
ClearGadgetItems(#GComboBranch)
ForEach branchItems() : AddGadgetItem(#GComboBranch, -1, branchItems()) : Next
Case #GAddBranch
Protected name$ = InputRequester("Ajouter une branche", "Nom (ex: featureX) ou remote/branche (ex: origin/main)", "")
If name$ <> ""
If CreateOrTrackBranch(repoDir$, GetGadgetText(#GStringRemote), name$)
ClearList(branchItems())
ListBranches(repoDir$, branchItems())
ClearGadgetItems(#GComboBranch)
ForEach branchItems() : AddGadgetItem(#GComboBranch, -1, branchItems()) : Next
If FindString(name$, "/", 1) = 0
SetGadgetText(#GComboBranch, name$)
EndIf
SaveRepoPrefs(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch))
EndIf
EndIf
; ---- Save global defaults (optional) / Sauver défauts globaux (optionnel) ----
Case #GSavePrefs
defRemote$ = GetGadgetText(#GStringRemote)
defBranch$ = GetGadgetText(#GComboBranch)
If SavePrefs(prefsPath$, defRemote$, defBranch$)
MessageRequester("Préférences", "Valeurs par défaut enregistrées (globales).", #PB_MessageRequester_Info)
Else
MessageRequester("Préférences", "Échec d'enregistrement.", #PB_MessageRequester_Error)
EndIf
; ---- Auto-save repository: remote and branch when they change ----
; ---- Auto-save dépôt : remote et branche quand ils changent ----
Case #GStringRemote
If EventType() = #PB_EventType_Change
SaveRepoPrefs(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch))
EndIf
Case #GComboBranch
If EventType() = #PB_EventType_Change
SaveRepoPrefs(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch))
EndIf
; ---- Commit / Push / Pull ----
Case #GCommit
Protected msg$ = GetGadgetText(#GStringMsg)
If Trim(msg$) = ""
MessageRequester("Commit", "Merci de saisir un message.", #PB_MessageRequester_Warning)
Else
Protected remote$ = GetGadgetText(#GStringRemote)
Protected branch$ = GetGadgetText(#GComboBranch)
Protected pushAfter.i = 0 ; Manual push / Push manuel
NewList files.s()
CollectIncludedFiles(rows(), files())
If ListSize(files()) > 0
DoCommitSelected(repoDir$, msg$, pushAfter, remote$, branch$, files())
Else
DoCommit(repoDir$, msg$, pushAfter, remote$, branch$)
EndIf
; Refresh after commit / Actualiser après commit
RememberSel()
ClearList(rows())
BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows())
If GetGadgetState(#GShowPermanent) = 0
ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next
EndIf
SortRowsByInterest(rows())
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
EndIf
Case #GPush
DoPush(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch))
Case #GPull
DoPull(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch))
; ---- Advanced window / Fenêtre avancée ----
Case #GAdvanced
OpenAdvancedWindow(repoDir$)
; ---- Identity configuration / Configuration d'identité ----
Case #GConfig
ConfigIdentityWizard(repoDir$)
; ---- List click (check/uncheck = include/exclude) / Clic sur la liste (cocher/décocher = inclure/exclure) ----
Case #GListStatus
Protected idx2.i = GetGadgetState(#GListStatus)
If idx2 >= 0
RememberSel()
ToggleIncludeAt(idx2, rows())
SortRowsByInterest(rows())
FillStatusList(rows()) ; Keep selection afterwards / Garde la sélection par la suite
RestoreSel()
EndIf
EndSelect
Case #PB_Event_CloseWindow
; Save repository preferences on exit / Sauvegarder les préférences du dépôt à la sortie
SaveRepoPrefs(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch))
EndSelect
Until ev = #PB_Event_CloseWindow
ProcedureReturn 1
EndIf
ProcedureReturn 0
EndProcedure
; =============================================================================
; IDE INSTALLATION / INSTALLATION IDE
; =============================================================================
; Install PBGit integration in IDE / Installe l'intégration PBGit dans l'IDE
Procedure.i InstallPBGitInIDE(ideExe$, toolsPrefs$, themeZip$)
Protected ok.i = 1, ideHome$, themesDir$, destZip$, args$, prg.i, copyok.i
If FileSize(ideExe$) <= 0 Or FileSize(toolsPrefs$) <= 0 Or FileSize(themeZip$) <= 0
MessageRequester("Installation", "Chemins invalides. Sélectionnez PureBasic.exe, le fichier d'outils et le thème.", #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
ideHome$ = GetPathPart(ideExe$)
themesDir$ = ideHome$ + "Themes" + #PathSep$
destZip$ = themesDir$ + GetFilePart(themeZip$)
; Create Themes directory if needed / Créer le dossier Themes si nécessaire
If FileSize(themesDir$) <> -2
If CreateDirectory(themesDir$) = 0
MessageRequester("Installation", "Impossible de créer le dossier Themes : " + themesDir$, #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
EndIf
; Copy theme / Copier le thème
copyok = CopyFile(themeZip$, destZip$)
If copyok = 0
MessageRequester("Installation", "Échec de copie du thème vers : " + destZip$, #PB_MessageRequester_Error)
ok = 0
EndIf
; Import tools via /A switch / Importer les outils via le switch /A
If ok
args$ = "/A " + Chr(34) + toolsPrefs$ + Chr(34)
prg = RunProgram(ideExe$, args$, "", #PB_Program_Hide | #PB_Program_Wait)
If prg = 0
MessageRequester("Installation", "Échec de l'import des outils (/A).", #PB_MessageRequester_Error)
ok = 0
EndIf
EndIf
If ok
MessageRequester("Installation", "Intégration importée !" + #LF$ +
"- Thème copié : " + destZip$ + #LF$ +
"- Outils importés via /A", #PB_MessageRequester_Info)
EndIf
ProcedureReturn ok
EndProcedure
; Open installation wizard / Ouvre l'assistant d'installation
Procedure.i OpenInstallWizard()
Protected exeDefault$ = ""
Protected dirExe$ = GetPathPart(ProgramFilename())
Protected prefsDefault$ = dirExe$ + "git_tools.prefs"
Protected themeDefault$ = dirExe$ + "PBGitTheme.zip"
If OpenWindow(#WInstall, 0, 0, 680, 240, "PBIDE-GitTool — Assistant d'installation IDE", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
TextGadget(#GLabelIde, 10, 16, 140, 22, "PureBasic.exe :")
StringGadget(#GStringIde, 160, 14, 420, 24, exeDefault$)
ButtonGadget(#GButtonIde, 590, 14, 80, 24, "Parcourir…")
TextGadget(#GLabelTools, 10, 56, 140, 22, "Fichier outils :")
StringGadget(#GStringTools, 160, 54, 420, 24, prefsDefault$)
ButtonGadget(#GButtonTools, 590, 54, 80, 24, "Parcourir…")
TextGadget(#GLabelTheme, 10, 96, 140, 22, "Thème icônes :")
StringGadget(#GStringTheme, 160, 94, 420, 24, themeDefault$)
ButtonGadget(#GButtonTheme, 590, 94, 80, 24, "Parcourir…")
ButtonGadget(#GInstallGo, 380, 150, 140, 30, "Installer")
ButtonGadget(#GInstallCancel, 530, 150, 140, 30, "Annuler")
TextGadget(#GInstallNote, 10, 190, 660, 40,
"Astuce : placez 'git_tools.prefs' et 'PBGitTheme.zip' à côté de PBIDE-GitTool.exe pour auto-renseignement." + #LF$ +
"Ensuite choisissez le thème (Préférences → Themes) et ajoutez les boutons 'Run tool' dans la Toolbar.")
; Tooltips / Info-bulles
GadgetToolTip(#GStringIde, "Chemin de PureBasic.exe (IDE).")
GadgetToolTip(#GButtonIde, "Parcourir pour sélectionner l'exécutable de l'IDE PureBasic.")
GadgetToolTip(#GStringTools, "Fichier d'outils externes à importer (/A).")
GadgetToolTip(#GButtonTools, "Parcourir le fichier 'git_tools.prefs'.")
GadgetToolTip(#GStringTheme, "Archive ZIP du thème d'icônes pour la Toolbar.")
GadgetToolTip(#GButtonTheme, "Parcourir le fichier 'PBGitTheme.zip'.")
GadgetToolTip(#GInstallGo, "Lancer l'installation (copie du thème + import des outils).")
GadgetToolTip(#GInstallCancel, "Fermer l'assistant sans rien modifier.")
Repeat
Protected ev.i = WaitWindowEvent()
If ev = #PB_Event_Gadget
Select EventGadget()
Case #GButtonIde
Protected pickExe$ = OpenFileRequester("Choisir PureBasic.exe", GetPathPart(GetGadgetText(#GStringIde)), "Exécutable|*.exe;*|Tous|*.*", 0)
If pickExe$ <> "" : SetGadgetText(#GStringIde, pickExe$) : EndIf
Case #GButtonTools
Protected pickPrefs$ = OpenFileRequester("Choisir le fichier d'outils", GetGadgetText(#GStringTools), "Prefs|*.prefs;*.txt|Tous|*.*", 0)
If pickPrefs$ <> "" : SetGadgetText(#GStringTools, pickPrefs$) : EndIf
Case #GButtonTheme
Protected pickZip$ = OpenFileRequester("Choisir le thème (zip)", GetGadgetText(#GStringTheme), "Zip|*.zip|Tous|*.*", 0)
If pickZip$ <> "" : SetGadgetText(#GStringTheme, pickZip$) : EndIf
Case #GInstallGo
Protected ok.i = InstallPBGitInIDE(GetGadgetText(#GStringIde), GetGadgetText(#GStringTools), GetGadgetText(#GStringTheme))
If ok : CloseWindow(#WInstall) : ProcedureReturn 1 : EndIf
Case #GInstallCancel
CloseWindow(#WInstall)
ProcedureReturn 0
EndSelect
EndIf
Until ev = #PB_Event_CloseWindow
ProcedureReturn 0
EndIf
ProcedureReturn 0
EndProcedure
; =============================================================================
; MAIN ENTRY POINT / POINT D'ENTRÉE PRINCIPAL
; =============================================================================
; Don't exit if Git is missing, to allow installation wizard
; On ne coupe pas si Git manque, pour permettre l'assistant d'installation
If EnsureGitAvailable() = 0
; Warned, but continue / Averti, mais continue
EndIf
; Local tool preferences / Préférences locales de l'outil
Define baseDoc$ = GetUserDirectory(#PB_Directory_Documents)
If Right(baseDoc$, 1) <> #PathSep$ : baseDoc$ + #PathSep$ : EndIf
Define prefsDir$ = baseDoc$ + "PBIDE-GitTool" + #PathSep$
If FileSize(prefsDir$) <> -2 : CreateDirectory(prefsDir$) : EndIf
Define prefsPath$ = prefsDir$ + "settings.prefs"
; CLI parameters / Paramètres CLI
Define argCount.i = CountProgramParameters()
Define useGui.i = 1
Define repoArg$ = "", msgArg$ = "", remoteArg$ = "", branchArg$ = ""
Define wantStatus.i = 0, wantInit.i = 0, wantCommit.i = 0, wantPush.i = 0, wantPull.i = 0
Define w.i
; If no parameters: offer IDE installation / Si aucun paramètre : proposer l'installation IDE
If argCount = 0
If MessageRequester("PBIDE-GitTool", "Aucun paramètre détecté." + #LF$ + "Souhaitez-vous installer l'intégration IDE (outils + thème) maintenant ?", #PB_MessageRequester_YesNo) = #PB_MessageRequester_Yes
OpenInstallWizard()
End
EndIf
EndIf
; Parse command line arguments / Analyser les arguments de ligne de commande
For w = 0 To argCount - 1
Define a$ = ProgramParameter(w)
Select LCase(a$)
Case "--project" : If w + 1 < argCount : repoArg$ = ProgramParameter(w + 1) : EndIf
Case "--repo" : If w + 1 < argCount : repoArg$ = ProgramParameter(w + 1) : EndIf
Case "--status" : wantStatus = 1 : useGui = 0
Case "--init" : wantInit = 1 : useGui = 0
Case "--commit" : wantCommit = 1 : useGui = 0 : If w + 1 < argCount : msgArg$ = ProgramParameter(w + 1) : EndIf
Case "--push" : wantPush = 1 : useGui = 0
Case "--pull" : wantPull = 1 : useGui = 0
Case "--remote" : If w + 1 < argCount : remoteArg$ = ProgramParameter(w + 1) : EndIf
Case "--branch" : If w + 1 < argCount : branchArg$ = ProgramParameter(w + 1) : EndIf
EndSelect
Next w
If useGui
; GUI mode / Mode interface graphique
Define dir$ = repoArg$
If dir$ = "" : dir$ = DirFromArgOrFallback() : EndIf
OpenGUI(dir$, prefsPath$)
Else
; Non-interactive mode / Mode non interactif
Define remote$ = "", branch$ = ""
LoadPrefs(prefsPath$, remote$, branch$)
If remoteArg$ <> "" : remote$ = remoteArg$ : EndIf
If branchArg$ <> "" : branch$ = branchArg$ : EndIf
Define baseDir$ = repoArg$
If baseDir$ = "" : baseDir$ = DirFromArgOrFallback() : EndIf
baseDir$ = DetectRepoRoot(baseDir$)
; Execute requested operation / Exécuter l'opération demandée
If wantStatus
Define st$, okstat.i = DoStatus(baseDir$, st$)
If okstat : PrintN(st$) : EndIf
ElseIf wantInit
DoInitRepo(baseDir$)
ElseIf wantCommit
If msgArg$ = "" : msgArg$ = "update" : EndIf
DoCommit(baseDir$, msgArg$, wantPush, remote$, branch$)
ElseIf wantPush
DoPush(baseDir$, remote$, branch$)
ElseIf wantPull
DoPull(baseDir$, remote$, branch$)
EndIf
EndIf
; =============================================================================
; END OF FILE / FIN DU FICHIER
; =============================================================================
; IDE Options = PureBasic 6.21 (Windows - x64)
; CursorPosition = 2049
; FirstLine = 2010
; Folding = -----------
; EnableXP
; DPIAware
; Executable = ..\PBIDE-GitTool.exe
; CompileSourceDirectory