Files
PBIDE-GitTool/PBIDE-GitTool.pb
2025-08-15 13:25:56 +02:00

2328 lines
78 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

; PBIDE-GitTool.pb — Outil externe Git pour lIDE PureBasic
; - Init, Status, Add+Commit (sélectif), Push/Pull
; - UI : liste avec cases à cocher + actions Inclure/Exclure/Tout
; - Fenêtre "Avancé…" : switch/restore de branche
; - Assistant dinstallation IDE si lancé sans paramètre
; Contraintes respectées : pas d'underscore dans nos identifiants,
; Protected uniquement dans les procédures, pas de IIf, déclarations = implémentations,
; comparaisons uniquement dans If/While/Until/For, etc.
EnableExplicit
#EnableDebug = #True
; ====== Séparateur de chemin (portable) ======
CompilerIf #PB_Compiler_OS = #PB_OS_Windows
#PathSep$ = "\"
; Chemin Git forcé pour Windows :
#GitExe$ = "C:\Program Files\Git\cmd\git.exe"
CompilerElse
#PathSep$ = "/"
; Sur macOS/Linux on garde 'git' dans le PATH :
#GitExe$ = "git"
CompilerEndIf
; ====== IDs UI (placer ce bloc AVANT toutes les procédures) ======
; 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
; Options daffichage
#GShowClean = 39 ; Afficher les fichiers suivis à jour (propres)
#GShowIgnored = 40 ; Afficher aussi les fichiers ignorés (.gitignore)
#GAddBranch = 42
#GReloadBranches= 43
#GShowPermanent = 44
#GExcludeForever = 45
#GReincludeForever= 46
; Fenêtre avancée (branches)
#WAdv = 200
#GAdvLabel = 210
#GAdvCombo = 211
#GAdvSwitch = 212
#GAdvRestore = 213
#GAdvClose = 214
; Assistant dinstallation
#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
; --- Nouveaux gadgets ---
#GDiff = 32
#GConfig = 34
#GGuide = 35
; --- Fenêtre Diff ---
#WDiff = 300
#GDiffText = 301
#GDiffClose = 302
; Fenêtre de sortie Git
#WOut = 400
#GOutText = 401
#GOutCopy = 402
#GOutClose = 403
; --- Bouton "Restaurer fichier…" dans la fenêtre principale ---
#GRestoreFile = 36
; --- Fenêtre de restauration dun fichier vers un commit ---
#WRestore = 500
#GRestList = 501
#GRestOK = 502
#GRestCancel = 503
#GRestInfo = 504
; Mémoriser / restaurer la sélection autour d'un refresh de la liste
Macro RememberSel()
keepIdx.i = GetGadgetState(#GListStatus)
keepFile$ = ""
If keepIdx >= 0
keepFile$ = GetGadgetItemText(#GListStatus, keepIdx, 1)
EndIf
EndMacro
Macro RestoreSel()
RestoreSelection(keepIdx, keepFile$)
EndMacro
; ====== Structures ======
Structure GitCall
args.s
workdir.s
output.s
errors.s
exitcode.i
EndStructure
Structure FileRow
stat.s ; 2 lettres porcelain (" M", "A ", "??", etc.)
file.s ; chemin relatif
include.i ; 1=coché (inclus), 0=exclu
EndStructure
Structure RepoPrefs
remote.s
branch.s
EndStructure
; ====== Déclarations ======
Declare.s TrimNewlines(text$)
Declare.i RunGit(*call.GitCall)
Declare.s DetectRepoRoot(startDir$)
Declare.i LoadPrefs(prefsPath$, remote$, branch$)
Declare.i SavePrefs(prefsPath$, remote$, branch$)
Declare.i SaveRepoPrefs(repoDir$, remote$, branch$)
Declare.i LoadRepoPrefs(repoDir$, *prefs.RepoPrefs)
Declare.i OpenGUI(initialDir$, prefsPath$)
Declare.i CreateOrTrackBranch(repoDir$, remote$, name$)
Declare.i EnsureGitAvailable()
Declare.i OpenGUI(initialDir$, prefsPath$)
Declare.s DirFromArgOrFallback()
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$)
Declare.i ListBranches(repoDir$, List branchesList.s())
Declare.i LoadStatusRows(repoDir$, 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 OpenAdvancedWindow(repoDir$)
Declare.i SwitchToBranch(repoDir$, branch$)
Declare.i RestoreFromBranch(repoDir$, branch$)
Declare.i InstallPBGitInIDE(ideExe$, toolsPrefs$, themeZip$)
Declare.i OpenInstallWizard()
Declare.s PorcelainToLabel(code$)
Declare.i OpenDiffWindow(repoDir$, List rows.FileRow())
Declare.i ConfigIdentityWizard(repoDir$)
Declare.i MakeDefaultGitignore(repoDir$)
Declare.i UpdateGuide(repoDir$, List rows.FileRow(), branch$, remote$)
Declare.s GetLocalConfig(repoDir$, key$)
Declare.i IsGitRepo(dir$)
Declare.i UpdateGuide(repoDir$, List rows.FileRow(), branch$, remote$)
Declare.i OpenRestoreFileWindow(repoDir$, List rows.FileRow())
Declare.i RestoreFileFromCommit(repoDir$, file$, commit$)
Declare.i BuildFullFileList(repoDir$, showClean.i, showIgnored.i, List rows.FileRow())
Declare.s FileFromRowsByIndex(index.i, List rows.FileRow())
Declare.i OpenRestoreFileWindow(repoDir$, List rows.FileRow())
Declare.s NormalizeGitFilePath(repoDir$, raw$)
Declare.i RepoFileExists(repoDir$, rel$)
Declare.i BuildFullFileList(repoDir$, showClean.i, showIgnored.i, List rows.FileRow())
Declare.i RestoreSelection(oldIndex.i, oldFile$)
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$)
; ====== Utils ======
; Convertit le chemin pour Git (Windows ok) :
; - remplace "\" par "/"
; - retire un éventuel préfixe "./"
Procedure.s GitPath(file$)
Protected p$ = file$
p$ = ReplaceString(p$, "\", "/")
If Left(p$, 2) = "./"
p$ = Mid(p$, 3)
EndIf
ProcedureReturn p$
EndProcedure
; Normalise un chemin renvoyé par Git :
; - enlève espaces/quotes superflus
; - garde la destination en cas de renommage "old -> new"
; - remplace les "/" par le séparateur OS
; - retire un éventuel "./" de tête
Procedure.s NormalizeGitFilePath(repoDir$, raw$)
Protected p$ = Trim(raw$)
; retirer quotes simples/doubles é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
; cas "old -> new" : on garde "new"
Protected pos.i = FindString(p$, "->", 1)
If pos > 0
p$ = Trim(Mid(p$, pos + 2))
EndIf
; retirer "./"
If Left(p$, 2) = "./"
p$ = Mid(p$, 3)
EndIf
; slashes vers séparateur OS
p$ = ReplaceString(p$, "/", #PathSep$)
ProcedureReturn p$
EndProcedure
; Vérifie lexistence dun fichier RELATIF au repo (fichier ou dossier)
; 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
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
; 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
; 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
; 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
; 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
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
; 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
; 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
While ProgramRunning(prg)
While AvailableProgramOutput(prg)
line$ = ReadProgramString(prg)
If line$ <> "" : out$ + line$ + #LF$ : EndIf
lineError$ = ReadProgramError(prg) ; renvoie "" si rien à lire (non bloquant)
If lineError$ <> "" : err$ + lineError$ + #LF$ : EndIf
Wend
Delay(5)
Wend
While AvailableProgramOutput(prg)
line$ = ReadProgramString(prg)
If line$ <> "" : out$ + line$ + #LF$ : EndIf
Wend
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
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
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
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
; 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
; 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
; Restaure la sélection après un FillStatusList()/rebuild
; 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
If oldIndex >= 0 And oldIndex < count
If GetGadgetItemText(#GListStatus, oldIndex, 1) = oldFile$
SetGadgetState(#GListStatus, oldIndex)
ProcedureReturn 1
EndIf
EndIf
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
; 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
; Ajoute notre fichier prefs interne aux exclusions locales si absent
; Ajoute .pbide-gittool.prefs dans .git/info/exclude si absent
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$)
; réécrit lexistant tel quel
If txt$ <> ""
WriteString(0, txt$)
If Right(txt$, 1) <> #LF$ : WriteString(0, #LF$) : EndIf
EndIf
; 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
; 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
; 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
; Ajoute file$ dans .git/info/exclude (si suivi: rm --cached d'abord)
; Ajoute file$ à .git/info/exclude (et le retire de lindex 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) Si suivi → le retirer de lindex
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) 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) É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
; Retire file$ de .git/info/exclude et le ré-inclut (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
; 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
; on 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 lajout même sil y a des règles dignore 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
; 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
; 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 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
; 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
; Création dune 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
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é à lemplacement prévu : " + #GitExe$, #PB_MessageRequester_Warning)
ProcedureReturn 0
EndProcedure
; 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
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
dir$ = GetPathPart(ProgramFilename())
If #EnableDebug : Debug "[Dir] fallback exe dir: " + dir$ : EndIf
ProcedureReturn dir$
EndProcedure
; ====== Opérations Git de base ======
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
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
Procedure.i DoCommit(repoDir$, message$, doPush.i, remote$, branch$)
Protected gc.GitCall, code.i
gc\workdir = repoDir$
gc\args = "add -A"
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
; Push amélioré :
; - 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$
; Remote vide → suppose 'origin'
If Trim(remoteName$) = ""
remoteName$ = "origin"
EndIf
; 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
; 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
; 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
; 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
; Autre erreur → fenêtre complète
ShowGitOutput("Git push — erreur", gc\output, gc\errors)
ProcedureReturn 0
EndProcedure
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
; ------------------------------------------------------------------
; 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
; 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
; 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
; Fenêtre daffichage
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
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
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 → lignes et gestion des coches ======
; Charge le status dans une liste de lignes (stat, file, include=0)
; Parsing robuste : on prend tout ce qui suit le premier séparateur (espace/tab)
; après les 2 lettres de statut ; 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
; 2 premières colonnes = statut (XY)
code$ = Left(line$, 2)
; 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)
; Renommage "old -> new" : garder la destination
pos = FindString(file$, "->", 1)
If pos > 0
file$ = Trim(Mid(file$, pos + 2))
EndIf
; (Optionnel) normalisation simple
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
; Remplit #GListStatus sans #LF$ (évite la perte du 1er caractère en col. 2)
; Remplit la liste (colonne 0 = état lisible, colonne 1 = chemin)
; Ajoute " — Exclu" si lélément a des changements mais nest pas coché
Procedure.i FillStatusList(List rows.FileRow())
ClearGadgetItems(#GListStatus)
Protected idx.i = 0
Protected label$, file$
ForEach rows()
label$ = PorcelainToLabel(rows()\stat)
; Si ce nest 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
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
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
; 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
; ====== Fenêtre Avancé… (branches) ======
#WAdv = 200
#GAdvLabel = 210
#GAdvCombo = 211
#GAdvSwitch = 212
#GAdvRestore = 213
#GAdvClose = 214
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
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
gc\args = "checkout " + Chr(34) + branch$ + Chr(34) ; fallback versions anciennes
If RunGit(@gc) = 0 : ProcedureReturn 1 : EndIf
MessageRequester("Branche", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error)
ProcedureReturn 0
EndProcedure
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
Procedure.s PorcelainToLabel(code$)
Protected x$ = Left(code$, 1)
Protected y$ = Right(code$, 1)
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
If x$ = "M" : ProcedureReturn "Modifié (indexé)" : EndIf
If x$ = "A" : ProcedureReturn "Ajouté (indexé)" : EndIf
If x$ = "D" : ProcedureReturn "Supprimé (indexé)" : EndIf
If x$ = "R" : ProcedureReturn "Renommé (indexé)" : EndIf
If x$ = "C" : ProcedureReturn "Copié (indexé)" : EndIf
If y$ = "M" : ProcedureReturn "Modifié (non indexé)" : EndIf
If y$ = "A" : ProcedureReturn "Ajouté (non indexé)" : EndIf
If y$ = "D" : ProcedureReturn "Supprimé (non indexé)" : EndIf
ProcedureReturn "Changement"
EndProcedure
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
Procedure.i ConfigIdentityWizard(repoDir$)
Protected curName$ = GetLocalConfig(repoDir$, "user.name")
Protected curMail$ = GetLocalConfig(repoDir$, "user.email")
Protected name$ = InputRequester("Identité Git", "Nom dauteur (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$
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
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
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
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
; Vérifie si 'dir$' est un dépôt Git
; - 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 : dossier .git présent ?
If FileSize(dotGitDir$) = -2
isRepo = 1
EndIf
EndIf
If #EnableDebug
Debug "[IsGitRepo] " + dir$ + " -> " + Str(isRepo)
EndIf
ProcedureReturn isRepo
EndProcedure
; Restaure un fichier à létat dun commit précis
; - Essaye 'git restore --source <commit> -- <file>'
; - Fallback 'git checkout <commit> -- <file>' (pour Git anciens)
; Restaure un fichier à létat dun commit précis
; - utilise GitPath() pour la robustesse
; - dabord 'git restore', fallback 'git checkout'
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 (compat versions 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
; Ouvre une fenêtre listant les commits du fichier sélectionné,
; puis restaure le fichier vers le commit choisi.
; Ouvre une fenêtre listant les commits du fichier sélectionné,
; puis restaure le fichier vers le commit choisi.
; Améliorations :
; - lit le chemin depuis la 2e colonne du gadget (source de vérité)
; - normalise le chemin pour Git via GitPath()
; - utilise --follow et fallback --all pour couvrir les renommages / autres branches
; Fenêtre de sélection de commit puis restauration dun fichier
; Robuste :
; - lit le nom depuis le gadget ET depuis rows()
; - corrige le cas où la 1re lettre saute (ex: "PBIDE..." → "BIDE...")
; - normalise le chemin (GitPath), suit les renommages (--follow) + fallback --all
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
; 1) Deux sources : gadget (colonne 2) et liste interne rows()
Protected fromGadget$ = GetGadgetItemText(#GListStatus, idx, 1)
Protected fromRows$ = FileFromRowsByIndex(idx, rows())
; 2) Choisir la version la plus fiable
Protected target$ = fromGadget$
If fromRows$ <> ""
; Si la version rows() est plus longue OU si fromRows$ commence par un caractère
; qui, en retirant le 1er, donne exactement fromGadget$, on privilégie rows().
If Len(fromRows$) > Len(fromGadget$)
target$ = fromRows$
ElseIf Len(fromRows$) > 1 And Mid(fromRows$, 2) = fromGadget$
target$ = fromRows$
EndIf
EndIf
; Dernier filet de sécurité
If target$ = "" : target$ = fromRows$ : EndIf
If target$ = "" : target$ = fromGadget$ : EndIf
If #EnableDebug
Debug "[Restore select] gadget='" + fromGadget$ + "' rows='" + fromRows$ + "' chosen='" + target$ + "'"
EndIf
If target$ = ""
MessageRequester("Restaurer", "Aucun fichier sélectionné.", #PB_MessageRequester_Info)
ProcedureReturn 0
EndIf
; Normaliser le chemin pour Git (slashs, ./)
Protected fileArg$ = GitPath(target$)
Protected gc.GitCall, out$, line$, n.i, i.i, q$ = Chr(34)
; 3) Historique avec suivi des renommages
gc\workdir = repoDir$
gc\args = "log --follow --date=short --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
; 4) Fallback : toutes les refs (utile si lhistorique du fichier est sur une autre branche)
If Trim(out$) = ""
gc\args = "log --all --follow --date=short --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 quil a déjà été committé.",
#PB_MessageRequester_Info)
ProcedureReturn 0
EndIf
; Liste des commits → UI
NewList hashes.s()
If OpenWindow(#WRestore, 0, 0, 760, 420, "Restaurer : " + target$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
TextGadget(#GRestInfo, 10, 10, 740, 22, "Choisissez le commit vers lequel restaurer le fichier.")
ListIconGadget(#GRestList, 10, 40, 740, 330, "Commit", 100, #PB_ListIcon_FullRowSelect)
AddGadgetColumn(#GRestList, 1, "Date", 90)
AddGadgetColumn(#GRestList, 2, "Message", 520)
ButtonGadget(#GRestOK, 540, 380, 100, 28, "Restaurer")
ButtonGadget(#GRestCancel, 650, 380, 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$)
Protected d$ = StringField(line$, 2, #TAB$)
Protected s$ = StringField(line$, 3, #TAB$)
AddGadgetItem(#GRestList, -1, h$ + #LF$ + d$ + #LF$ + s$)
AddElement(hashes()) : hashes() = h$
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
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
; Met à jour le panneau "Guide" avec un pas-à-pas adapté
; - Inclut létape “Init repo” si le dossier nest 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$)
; 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
; ---- 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
; ---- 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 lidentité 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 linstant." + #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 dun fichier sélectionné." + #LF$ + #LF$
; 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 lhistorique 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
; Affiche stdout + stderr dans une fenêtre (évite le texte tronqué)
; Construit la liste rows() avec tous les fichiers (selon options), SANS correction de nom
; - showClean=1 → inclut les fichiers suivis "propres" (OK)
; - showIgnored=1 → inclut aussi les fichiers ignorés (!!)
; - Vérifie lexistence côté disque ; si absent et pas un delete, marque "NF" (Introuvable)
; - rows()\include = 1 pour les éléments potentiellement à committer (≠ OK/!!)
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())
; 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 → 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) 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$
rows()\include = Bool(code$ <> "OK" And code$ <> "!!")
EndIf
Next
; 4) 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$
rows()\include = 1
EndIf
EndIf
Next
ProcedureReturn ListSize(rows())
EndProcedure
; 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
; Fenêtre de sélection de commit puis restauration dun fichier
; Robuste :
; - lit le nom depuis le gadget ET depuis rows()
; - corrige le cas où la 1re lettre saute (ex: "PBIDE..." → "BIDE...")
; - normalise le chemin (GitPath), suit les renommages (--follow) + fallback --all
; ------------------------------------------------------------------
; Ouvre l'interface principale (avec auto-save remote/branch par dépôt)
; ------------------------------------------------------------------
; ------------------------------------------------------------------
; Ouvre l'interface principale (boutons Inclure/Exclure retirés,
; bouton "Restaurer…" présent, auto-save remote/branch par dépôt)
; ------------------------------------------------------------------
; ------------------------------------------------------------------
; Ouvre l'interface principale (Push manuel, pas de bouton .gitignore,
; guide scrollable en lecture seule, options daffichage clarifiées)
; ------------------------------------------------------------------
; ------------------------------------------------------------------
; Ouvre l'interface principale (Push manuel, pas de bouton .gitignore,
; guide scrollable en lecture seule, options daffichage clarifiées)
; ------------------------------------------------------------------
; ------------------------------------------------------------------
; Ouvre l'interface principale (Push manuel, guide scrollable,
; prefs dépôt correctement rechargées & auto-sauvegardées)
; ------------------------------------------------------------------
Procedure.i OpenGUI(initialDir$, prefsPath$)
Protected repoDir$ = DetectRepoRoot(initialDir$)
; Défauts globaux
Protected defRemote$ = ""
Protected defBranch$ = ""
LoadPrefs(prefsPath$, defRemote$, defBranch$)
; Prefs du dépôt (via structure pour bien remonter les valeurs)
Protected rp.RepoPrefs
rp\remote = defRemote$
rp\branch = defBranch$
LoadRepoPrefs(repoDir$, @rp)
; Sélection persistante pour RememberSel/RestoreSel
Protected keepIdx.i
Protected keepFile$
; Ignorer automatiquement notre prefs locale
EnsureToolFilesExcluded(repoDir$)
If OpenWindow(#GWindow, 0, 0, 920, 720, "PBIDE-GitTool — Git (mode simplifié)", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
; --- En-tête dépôt ---
TextGadget(#GLabelRepo, 10, 12, 60, 22, "Dépôt :")
StringGadget(#GStringRepo, 80, 10, 740, 24, repoDir$)
ButtonGadget(#GButtonBrowse, 830, 10, 80, 24, "Parcourir…")
; --- Options d'affichage ---
CheckBoxGadget(#GShowClean, 10, 40, 220, 20, "Afficher suivis à jour")
SetGadgetState(#GShowClean, #True)
CheckBoxGadget(#GShowIgnored, 240, 40, 240, 20, "Afficher ignorés (.gitignore)")
SetGadgetState(#GShowIgnored, #False)
CheckBoxGadget(#GShowPermanent, 490, 40, 240, 20, "Afficher exclus permanents")
SetGadgetState(#GShowPermanent, #True)
GadgetToolTip(#GShowIgnored, "Montre les fichiers ignorés par règles (.gitignore, exclude global) — statut '!!'.")
GadgetToolTip(#GShowPermanent, "Montre les fichiers exclus via loutil (.git/info/exclude) — statut 'EX'.")
; --- Liste des fichiers ---
ListIconGadget(#GListStatus, 10, 66, 900, 320, "État", 240, #PB_ListIcon_CheckBoxes | #PB_ListIcon_FullRowSelect)
AddGadgetColumn(#GListStatus, 1, "Fichier", 640)
; --- Bande dactions sur la liste ---
ButtonGadget(#GRefresh, 10, 392, 100, 26, "Rafraîchir")
ButtonGadget(#GInit, 120, 392, 100, 26, "Init repo")
ButtonGadget(#GExcludeForever, 230, 392, 150, 26, "Exclure (permanent)")
ButtonGadget(#GReincludeForever, 390, 392, 170, 26, "Ré-inclure (permanent)")
ButtonGadget(#GDiff, 570, 392, 90, 26, "Diff…")
ButtonGadget(#GRestoreFile, 670, 392, 130, 26, "Restaurer…")
; --- Zone message / remote / branche ---
TextGadget(#GLabelMsg, 10, 428, 100, 22, "Message :")
StringGadget(#GStringMsg, 110, 426, 620, 24, "")
TextGadget(#GLabelRemote, 10, 456, 60, 22, "Remote :")
StringGadget(#GStringRemote, 70, 454, 210, 24, rp\remote)
TextGadget(#GLabelBranch, 290, 456, 60, 22, "Branche :")
ComboBoxGadget(#GComboBranch, 350, 454, 210, 24)
ButtonGadget(#GSavePrefs, 570, 454, 90, 24, "Défauts")
ButtonGadget(#GAddBranch, 670, 454, 110, 24, "Ajouter br.…")
ButtonGadget(#GReloadBranches, 790, 454, 120, 24, "Actualiser br.")
; --- Actions principales ---
ButtonGadget(#GCommit, 10, 490, 160, 30, "Add + Commit (cochés)")
ButtonGadget(#GPush, 180, 490, 120, 30, "Push")
ButtonGadget(#GPull, 310, 490, 120, 30, "Pull")
ButtonGadget(#GAdvanced, 440, 490, 120, 30, "Avancé…")
ButtonGadget(#GConfig, 570, 490, 170, 30, "Configurer identité…")
GadgetToolTip(#GCommit, "Valide SEULEMENT les lignes cochées.")
GadgetToolTip(#GPush, "Pousse les commits locaux vers le dépôt distant (manuel).")
GadgetToolTip(#GRestoreFile, "Restaurer le fichier sélectionné à un commit précis.")
GadgetToolTip(#GDiff, "Afficher les différences du fichier sélectionné.")
; --- Guide (scrollable en lecture seule) ---
EditorGadget(#GGuide, 10, 530, 900, 170)
SetGadgetAttribute(#GGuide, #PB_Editor_ReadOnly, 1)
; --- 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
; --- Fichiers : construction & 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
FillStatusList(rows())
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
; =================== Boucle événements ===================
Repeat
Protected ev.i = WaitWindowEvent()
Select ev
Case #PB_Event_Gadget
Select EventGadget()
; ---- 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$)
; recharger prefs dépôt (rp repart des défauts globaux si rien)
rp\remote = GetGadgetText(#GStringRemote)
rp\branch = GetGadgetText(#GComboBranch)
rp\remote = defRemote$
rp\branch = defBranch$
LoadRepoPrefs(repoDir$, @rp)
SetGadgetText(#GStringRemote, rp\remote)
ClearList(branchItems())
ListBranches(repoDir$, branchItems())
ClearGadgetItems(#GComboBranch)
ForEach branchItems() : AddGadgetItem(#GComboBranch, -1, branchItems()) : Next
If rp\branch <> "" : SetGadgetText(#GComboBranch, rp\branch) : EndIf
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
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
EndIf
; ---- 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
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
; ---- 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
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
; ---- 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
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
FillStatusList(rows())
RestoreSel()
EndIf
EndIf
EndIf
; ---- 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
FillStatusList(rows())
RestoreSel()
UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote))
EndIf
; ---- 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
; auto-save dépôt après création/suivi
SaveRepoPrefs(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch))
EndIf
EndIf
; ---- 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 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 ; 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
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
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))
; ---- Clic sur la liste (cocher/décocher = inclure/exclure) ----
Case #GListStatus
Protected idx2.i = GetGadgetState(#GListStatus)
If idx2 >= 0
RememberSel()
ToggleIncludeAt(idx2, rows())
FillStatusList(rows())
RestoreSel()
EndIf
EndSelect
Case #PB_Event_CloseWindow
; Dernière sauvegarde des prefs dépôt à la fermeture
SaveRepoPrefs(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch))
EndSelect
Until ev = #PB_Event_CloseWindow
ProcedureReturn 1
EndIf
ProcedureReturn 0
EndProcedure
; ====== Installation IDE ======
; Fenêtre assistant
#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
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 doutils et le thème.", #PB_MessageRequester_Error)
ProcedureReturn 0
EndIf
ideHome$ = GetPathPart(ideExe$)
themesDir$ = ideHome$ + "Themes" + #PathSep$
destZip$ = themesDir$ + GetFilePart(themeZip$)
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
copyok = CopyFile(themeZip$, destZip$)
If copyok = 0
MessageRequester("Installation", "Échec de copie du thème vers : " + destZip$, #PB_MessageRequester_Error)
ok = 0
EndIf
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 limport 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
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 dinstallation 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.")
GadgetToolTip(#GStringIde, "Chemin de PureBasic.exe (IDE).")
GadgetToolTip(#GButtonIde, "Parcourir pour sélectionner lexécutable de lIDE PureBasic.")
GadgetToolTip(#GStringTools, "Fichier doutils externes à importer (/A).")
GadgetToolTip(#GButtonTools, "Parcourir le fichier 'git_tools.prefs'.")
GadgetToolTip(#GStringTheme, "Archive ZIP du thème dicônes pour la Toolbar.")
GadgetToolTip(#GButtonTheme, "Parcourir le fichier 'PBGitTheme.zip'.")
GadgetToolTip(#GInstallGo, "Lancer linstallation (copie du thème + import des outils).")
GadgetToolTip(#GInstallCancel, "Fermer lassistant 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
; ====== Entrée ======
; On ne coupe pas si Git manque, pour permettre lassistant dinstallation
If EnsureGitAvailable() = 0
; Averti, mais continue.
EndIf
; Préférences locales de loutil
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"
; 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
; Si aucun paramètre : proposer linstallation IDE
If argCount = 0
If MessageRequester("PBIDE-GitTool", "Aucun paramètre détecté." + #LF$ + "Souhaitez-vous installer lintégration IDE (outils + thème) maintenant ?", #PB_MessageRequester_YesNo) = #PB_MessageRequester_Yes
OpenInstallWizard()
End
EndIf
EndIf
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
Define dir$ = repoArg$
If dir$ = "" : dir$ = DirFromArgOrFallback() : EndIf
OpenGUI(dir$, prefsPath$)
Else
; 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$)
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
; IDE Options = PureBasic 6.21 (Windows - x64)
; CursorPosition = 2121
; FirstLine = 2083
; Folding = ----------
; EnableXP
; DPIAware
; Executable = ..\PBIDE-GitTool.exe
; CompileSourceDirectory