; PBIDE-GitTool.pb — Outil externe Git pour l’IDE 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 d’installation 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 #GCheckPush = 17 #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 d’affichage #GShowClean = 39 ; Afficher les fichiers suivis à jour (propres) #GShowIgnored = 40 ; Afficher aussi les fichiers ignorés (.gitignore) ; Fenêtre avancée (branches) #WAdv = 200 #GAdvLabel = 210 #GAdvCombo = 211 #GAdvSwitch = 212 #GAdvRestore = 213 #GAdvClose = 214 ; 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 ; --- Nouveaux gadgets --- #GDiff = 32 #GMakeIgnore = 33 #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 d’un fichier vers un commit --- #WRestore = 500 #GRestList = 501 #GRestOK = 502 #GRestCancel = 503 #GRestInfo = 504 ; ====== 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 ; ====== 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 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.s PorcelainToLabel(code$) ; (remplacer par la version complète ci-dessous) ; ====== 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 l’existence d’un 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 ; 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 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 ; 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 ' ; - 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 -- ""' 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 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 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) Procedure.i FillStatusList(List rows.FileRow()) ClearGadgetItems(#GListStatus) Protected idx.i = 0 Protected label$, file$ ForEach rows() ; 1) Texte de la 1re colonne (état lisible) label$ = PorcelainToLabel(rows()\stat) AddGadgetItem(#GListStatus, -1, label$) ; 2) Texte de la 2e colonne (chemin) file$ = rows()\file SetGadgetItemText(#GListStatus, idx, file$, 1) ; 3) Case à cocher selon include If rows()\include SetGadgetItemState(#GListStatus, idx, #PB_ListIcon_Checked) Else SetGadgetItemState(#GListStatus, idx, 0) EndIf ; 4) Debug (utile pour vérifier ce qui est réellement affiché) If #EnableDebug Debug "[Fill] idx=" + Str(idx) + " file='" + file$ + "' readback='" + GetGadgetItemText(#GListStatus, idx, 1) + "'" 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 (ou git checkout).") ButtonGadget(#GAdvRestore, 190, 60, 170, 30, "Restaurer depuis la branche") GadgetToolTip(#GAdvRestore, "git restore --source -- . (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$ = "OK" ProcedureReturn "À jour (suivi)" EndIf If code$ = "??" ProcedureReturn "Nouveau (non suivi)" EndIf If code$ = "!!" ProcedureReturn "Ignoré (.gitignore)" EndIf ; Colonne X = indexé (staged) 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 ; Colonne Y = non indexé (worktree) 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 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$ 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 d’un commit précis ; - Essaye 'git restore --source -- ' ; - Fallback 'git checkout -- ' (pour Git anciens) ; Restaure un fichier à l’état d’un commit précis ; - utilise GitPath() pour la robustesse ; - d’abord '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 d’un 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 l’historique 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 qu’il 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 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$) ; 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 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$ ; 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 ; 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 l’existence 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()) ; --- 1) status --porcelain --ignored → map fichier -> code (??, !!, " M", etc.) 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) ; --- parsing robuste du chemin (tout ce qui suit les espaces/tabs après col.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) ; renommage "old -> new" : garder la destination pos = FindString(file$, "->", 1) If pos > 0 : file$ = Trim(Mid(file$, pos + 2)) : EndIf ; normalisation simple If Left(file$, 2) = "./" : file$ = Mid(file$, 3) : EndIf file$ = ReplaceString(file$, "/", #PathSep$) ; ------------------------------------------------------------------------------- AddMapElement(statusMap(), file$) statusMap() = code$ Next ; --- 2) ls-files → tous les 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) Ajouter les 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 exists = 0 And isDelete = 0 rows()\stat = "NF" ; Introuvable (FS) rows()\include = 0 Else rows()\stat = code$ rows()\include = Bool(code$ <> "OK" And code$ <> "!!") EndIf Next ; --- 4) Ajouter non suivis (!!/??) qui ne sont pas dans 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 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 d’un 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 OpenGUI(initialDir$, prefsPath$) Protected repoDir$ = DetectRepoRoot(initialDir$) Protected remote$ = "", branch$ = "" LoadPrefs(prefsPath$, remote$, branch$) If OpenWindow(#GWindow, 0, 0, 900, 660, "PBIDE-GitTool — Git (mode simplifié)", #PB_Window_SystemMenu | #PB_Window_ScreenCentered) TextGadget(#GLabelRepo, 10, 12, 60, 22, "Dépôt :") StringGadget(#GStringRepo, 80, 10, 720, 24, repoDir$) ButtonGadget(#GButtonBrowse, 810, 10, 80, 24, "Parcourir…") ; Options d’affichage CheckBoxGadget(#GShowClean, 10, 40, 180, 20, "Afficher suivis à jour") SetGadgetState(#GShowClean, #True) ; par défaut : on veut tout voir CheckBoxGadget(#GShowIgnored, 200, 40, 200, 20, "Inclure ignorés (.gitignore)") SetGadgetState(#GShowIgnored, #False) ; Liste avec cases à cocher ListIconGadget(#GListStatus, 10, 66, 880, 300, "État", 180, #PB_ListIcon_CheckBoxes | #PB_ListIcon_FullRowSelect) AddGadgetColumn(#GListStatus, 1, "Fichier", 680) ; Ligne actions liste ButtonGadget(#GRefresh, 10, 372, 90, 26, "Rafraîchir") ButtonGadget(#GInit, 110, 372, 90, 26, "Init repo") ButtonGadget(#GInclude, 210, 372, 90, 26, "Inclure") ButtonGadget(#GExclude, 310, 372, 90, 26, "Exclure") ButtonGadget(#GIncludeAll, 410, 372, 110, 26, "Tout inclure") ButtonGadget(#GExcludeAll, 530, 372, 110, 26, "Tout exclure") ButtonGadget(#GDiff, 650, 372, 80, 26, "Diff…") ButtonGadget(#GRestoreFile,740, 372, 150, 26, "Restaurer fichier…") ; Zone commit / push TextGadget(#GLabelMsg, 10, 408, 100, 22, "Message :") StringGadget(#GStringMsg, 110, 406, 620, 24, "") CheckBoxGadget(#GCheckPush, 10, 438, 140, 22, "Pousser après") SetGadgetState(#GCheckPush, #True) TextGadget(#GLabelRemote, 160, 438, 60, 22, "Remote :") StringGadget(#GStringRemote, 220, 436, 120, 24, remote$) TextGadget(#GLabelBranch, 350, 438, 60, 22, "Branche :") ComboBoxGadget(#GComboBranch, 410, 436, 170, 24) ButtonGadget(#GSavePrefs, 590, 436, 220, 24, "Sauver défauts") ButtonGadget(#GAdvanced, 820, 436, 70, 24, "Avancé…") ButtonGadget(#GCommit, 10, 470, 120, 30, "Add + Commit") ButtonGadget(#GPush, 140, 470, 120, 30, "Push") ButtonGadget(#GPull, 270, 470, 120, 30, "Pull") ButtonGadget(#GConfig, 400, 470, 170, 30, "Configurer identité…") ButtonGadget(#GMakeIgnore, 580, 470, 230, 30, "Créer .gitignore (recommandé)") ; Panneau Guide EditorGadget(#GGuide, 10, 510, 880, 140) DisableGadget(#GGuide, 1) ; Infobulles clés GadgetToolTip(#GShowClean, "Afficher aussi les fichiers déjà suivis et sans changement (pratique pour restaurer).") GadgetToolTip(#GShowIgnored, "Inclure les fichiers ignorés par .gitignore (lecture seule pour info).") GadgetToolTip(#GListStatus, "Cochez pour inclure un fichier au commit. 'Diff…' pour voir le détail. 'Restaurer fichier…' pour revenir à un commit.") ; Branches NewList branchItems.s() ListBranches(repoDir$, branchItems()) ClearGadgetItems(#GComboBranch) ForEach branchItems() : AddGadgetItem(#GComboBranch, -1, branchItems()) : Next If branch$ <> "" : SetGadgetText(#GComboBranch, branch$) : EndIf ; Fichiers (liste complète) NewList rows.FileRow() BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) ; Boucle Repeat Protected ev.i = WaitWindowEvent() Select ev Case #PB_Event_Gadget Select EventGadget() Case #GButtonBrowse Protected newDir$ = PathRequester("Choisir le répertoire du dépôt", repoDir$) If newDir$ <> "" repoDir$ = newDir$ SetGadgetText(#GStringRepo, repoDir$) ClearList(branchItems()) ListBranches(repoDir$, branchItems()) ClearGadgetItems(#GComboBranch) ForEach branchItems() : AddGadgetItem(#GComboBranch, -1, branchItems()) : Next ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) EndIf Case #GShowClean, #GShowIgnored ; Refiltrer l’affichage à la volée ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) Case #GRefresh ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) Case #GInit DoInitRepo(repoDir$) ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) Case #GInclude Protected idx.i = GetGadgetState(#GListStatus) If idx >= 0 SetGadgetItemState(#GListStatus, idx, #PB_ListIcon_Checked) ToggleIncludeAt(idx, rows()) EndIf UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) Case #GExclude idx = GetGadgetState(#GListStatus) If idx >= 0 SetGadgetItemState(#GListStatus, idx, 0) ToggleIncludeAt(idx, rows()) EndIf UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) Case #GIncludeAll Protected c.i = CountGadgetItems(#GListStatus) For idx = 0 To c - 1 : SetGadgetItemState(#GListStatus, idx, #PB_ListIcon_Checked) : Next ForEach rows() : rows()\include = 1 : Next UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) Case #GExcludeAll c = CountGadgetItems(#GListStatus) For idx = 0 To c - 1 : SetGadgetItemState(#GListStatus, idx, 0) : Next ForEach rows() : rows()\include = 0 : Next UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) Case #GDiff OpenDiffWindow(repoDir$, rows()) Case #GRestoreFile If OpenRestoreFileWindow(repoDir$, rows()) ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) EndIf Case #GAdvanced OpenAdvancedWindow(repoDir$) ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) Case #GSavePrefs remote$ = GetGadgetText(#GStringRemote) branch$ = GetGadgetText(#GComboBranch) If SavePrefs(prefsPath$, remote$, branch$) MessageRequester("Préférences", "Valeurs par défaut enregistrées.", #PB_MessageRequester_Info) Else MessageRequester("Préférences", "Échec d'enregistrement.", #PB_MessageRequester_Error) EndIf UpdateGuide(repoDir$, rows(), branch$, remote$) Case #GConfig If ConfigIdentityWizard(repoDir$) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) EndIf Case #GMakeIgnore If MakeDefaultGitignore(repoDir$) ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) EndIf Case #GCommit remote$ = GetGadgetText(#GStringRemote) branch$ = GetGadgetText(#GComboBranch) Protected msg$ = GetGadgetText(#GStringMsg) If Trim(msg$) = "" MessageRequester("Commit", "Merci de saisir un message.", #PB_MessageRequester_Warning) Else NewList files.s() CollectIncludedFiles(rows(), files()) If ListSize(files()) > 0 DoCommitSelected(repoDir$, msg$, GetGadgetState(#GCheckPush), remote$, branch$, files()) Else DoCommit(repoDir$, msg$, GetGadgetState(#GCheckPush), remote$, branch$) EndIf ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) EndIf Case #GPush remote$ = GetGadgetText(#GStringRemote) branch$ = GetGadgetText(#GComboBranch) DoPush(repoDir$, remote$, branch$) Case #GPull remote$ = GetGadgetText(#GStringRemote) branch$ = GetGadgetText(#GComboBranch) DoPull(repoDir$, remote$, branch$) Case #GListStatus idx = GetGadgetState(#GListStatus) If idx >= 0 : ToggleIncludeAt(idx, rows()) : EndIf UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) EndSelect 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 d’outils 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 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 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.") 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 ; ====== Entrée ====== ; On ne coupe pas si Git manque, pour permettre l’assistant d’installation If EnsureGitAvailable() = 0 ; Averti, mais continue. EndIf ; 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" ; 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 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 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 = 1413 ; FirstLine = 1375 ; Folding = -------- ; EnableXP ; DPIAware ; Executable = ..\PBIDE-GitTool.exe ; CompileSourceDirectory