; 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 #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) #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 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 #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 ; 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$) Declare.i SortRowsByInterest(List rows.FileRow()) ; ====== 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 ; 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 l’existant 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 l’index si suivi) Procedure.i AddPermanentExclude(repoDir$, file$) Protected q$ = Chr(34) Protected gc.GitCall Protected excl$ = LocalExcludePath(repoDir$) Protected txt$ = ReadAllText(excl$) Protected found.i = 0 ; 1) Si suivi → le retirer de l’index gc\workdir = repoDir$ gc\args = "ls-files -- " + q$ + file$ + q$ If RunGit(@gc) = 0 And Trim(gc\output) <> "" gc\args = "rm --cached -- " + q$ + file$ + q$ If RunGit(@gc) <> 0 MessageRequester("Exclusion permanente", "Échec du retrait de l'index : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error) ProcedureReturn 0 EndIf EndIf ; 2) 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 l’ajout même s’il y a des règles d’ignore ailleurs Protected gc.GitCall, q$ = Chr(34) gc\workdir = repoDir$ gc\args = "add -f -- " + q$ + file$ + q$ If RunGit(@gc) <> 0 MessageRequester("Ré-inclure (permanent)", "Avertissement : git add -f a échoué : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Warning) Else MessageRequester("Ré-inclure (permanent)", "Le fichier a été ré-inclus.", #PB_MessageRequester_Info) EndIf ProcedureReturn 1 EndProcedure ; 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 d’une branche locale simple gc\workdir = repoDir$ gc\args = "switch -c " + q$ + name$ + q$ If RunGit(@gc) = 0 MessageRequester("Branche", "Branche '" + name$ + "' créée et sélectionnée.", #PB_MessageRequester_Info) ProcedureReturn 1 EndIf MessageRequester("Branche", "Échec: " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error) ProcedureReturn 0 EndProcedure 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) ; Remplit la liste (colonne 0 = état lisible, colonne 1 = chemin) ; Ajoute " — Exclu" si l’élément a des changements mais n’est pas coché Procedure.i FillStatusList(List rows.FileRow()) ClearGadgetItems(#GListStatus) Protected idx.i = 0 Protected label$, file$ ForEach rows() label$ = PorcelainToLabel(rows()\stat) ; Si ce n’est pas un fichier "OK/ignoré" et que include=0, on indique "Exclu" If rows()\stat <> "OK" And rows()\stat <> "!!" If rows()\include = 0 label$ + " — Exclu" EndIf EndIf AddGadgetItem(#GListStatus, -1, label$) file$ = rows()\file SetGadgetItemText(#GListStatus, idx, file$, 1) If rows()\include SetGadgetItemState(#GListStatus, idx, #PB_ListIcon_Checked) Else SetGadgetItemState(#GListStatus, idx, 0) EndIf idx + 1 Next ProcedureReturn CountGadgetItems(#GListStatus) EndProcedure 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$ = "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 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 ; Fenêtre de sélection de commit puis restauration d’un fichier ; ➜ Affiche maintenant date + heure (format ISO, avec fuseau) Procedure.i OpenRestoreFileWindow(repoDir$, List rows.FileRow()) Protected idx.i = GetGadgetState(#GListStatus) If idx < 0 MessageRequester("Restaurer", "Sélectionnez d'abord un fichier dans la liste.", #PB_MessageRequester_Info) ProcedureReturn 0 EndIf ; Récupérer le nom de fichier depuis la liste (colonne 1 = chemin) Protected target$ = GetGadgetItemText(#GListStatus, idx, 1) If target$ = "" MessageRequester("Restaurer", "Aucun fichier sélectionné.", #PB_MessageRequester_Info) ProcedureReturn 0 EndIf ; Normaliser pour Git (slashs, ./) Protected fileArg$ = GitPath(target$) Protected gc.GitCall, out$, line$, n.i, i.i, q$ = Chr(34) ; 1) Historique avec heure (—date=iso affiche YYYY-MM-DD HH:MM:SS +ZZZZ) ; Note: si tu préfères l’heure locale stricte (Git récent), tu peux essayer --date=iso-strict-local gc\workdir = repoDir$ gc\args = "log --follow --date=iso --pretty=format:%h%x09%ad%x09%s -n 200 -- " + q$ + fileArg$ + q$ If #EnableDebug : Debug "[Restore log] " + gc\args : EndIf If RunGit(@gc) <> 0 MessageRequester("Restaurer", "Échec du log : " + #LF$ + TrimNewlines(gc\errors), #PB_MessageRequester_Error) ProcedureReturn 0 EndIf out$ = gc\output ; 2) Fallback : toutes les refs (utile si l’historique est sur une autre branche) If Trim(out$) = "" gc\args = "log --all --follow --date=iso --pretty=format:%h%x09%ad%x09%s -n 200 -- " + q$ + fileArg$ + q$ If #EnableDebug : Debug "[Restore log fallback --all] " + gc\args : EndIf If RunGit(@gc) = 0 out$ = gc\output EndIf EndIf If Trim(out$) = "" MessageRequester("Restaurer", "Aucun commit trouvé pour ce fichier." + #LF$ + "Vérifiez que le fichier est (ou a été) suivi par Git et qu’il a déjà été committé.", #PB_MessageRequester_Info) ProcedureReturn 0 EndIf ; 3) UI : liste des commits avec Date/Heure NewList hashes.s() If OpenWindow(#WRestore, 0, 0, 780, 440, "Restaurer : " + target$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered) TextGadget(#GRestInfo, 10, 10, 760, 22, "Choisissez le commit vers lequel restaurer le fichier.") ListIconGadget(#GRestList, 10, 40, 760, 340, "Commit", 110, #PB_ListIcon_FullRowSelect) AddGadgetColumn(#GRestList, 1, "Date / Heure", 190) ; élargi pour l’heure et le fuseau AddGadgetColumn(#GRestList, 2, "Message", 440) ButtonGadget(#GRestOK, 550, 390, 100, 28, "Restaurer") ButtonGadget(#GRestCancel, 660, 390, 100, 28, "Annuler") n = CountString(out$, #LF$) + 1 For i = 1 To n line$ = StringField(out$, i, #LF$) If Trim(line$) <> "" Protected h$ = StringField(line$, 1, #TAB$) ; hash abrégé Protected d$ = StringField(line$, 2, #TAB$) ; date + heure (+ZZZZ) Protected s$ = StringField(line$, 3, #TAB$) ; message AddGadgetItem(#GRestList, -1, h$ + #LF$ + d$ + #LF$ + s$) AddElement(hashes()) : hashes() = h$ If #EnableDebug : Debug "[Restore list] " + h$ + " | " + d$ + " | " + s$ : EndIf EndIf Next Repeat Protected ev.i = WaitWindowEvent() If ev = #PB_Event_Gadget Select EventGadget() Case #GRestOK idx = GetGadgetState(#GRestList) If idx < 0 MessageRequester("Restaurer", "Sélectionnez un commit.", #PB_MessageRequester_Warning) Else ; Récupérer le hash choisi Protected k.i = 0, chosen$ ForEach hashes() If k = idx : chosen$ = hashes() : Break : EndIf k + 1 Next If chosen$ <> "" If RestoreFileFromCommit(repoDir$, fileArg$, chosen$) CloseWindow(#WRestore) ProcedureReturn 1 EndIf EndIf EndIf Case #GRestCancel CloseWindow(#WRestore) ProcedureReturn 0 EndSelect EndIf Until ev = #PB_Event_CloseWindow ProcedureReturn 0 EndIf ProcedureReturn 0 EndProcedure ; 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()) ; 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 ; Trie la liste rows() par "intérêt" (cf. ordre ci-dessus) puis par chemin. ; Utilise des seaux (buckets) puis recolle le tout. Pas de underscore, pas de IIf. Procedure.i SortRowsByInterest(List rows.FileRow()) ; Catégorisation locale Protected ProcedureReturnValue.i = 0 Protected cat.i Protected x$, y$ ; Seaux NewList b0.FileRow() ; conflits (U) NewList b1.FileRow() ; changements indexés (X) NewList b2.FileRow() ; changements non indexés (Y) NewList b3.FileRow() ; nouveaux (??) NewList b4.FileRow() ; OK NewList b5.FileRow() ; ignorés (!!) NewList b6.FileRow() ; exclus permanents (EX) NewList b7.FileRow() ; introuvables (NF) ; Helper: pousse un FileRow dans un seau Macro PushTo(bucket) AddElement(bucket()) bucket()\file = rows()\file bucket()\stat = rows()\stat bucket()\include = rows()\include EndMacro ; 1) Répartition dans les seaux ForEach rows() cat = -1 x$ = Left(rows()\stat, 1) y$ = Right(rows()\stat, 1) If FindString(rows()\stat, "U", 1) > 0 cat = 0 ElseIf rows()\stat = "??" cat = 3 ElseIf rows()\stat = "OK" cat = 4 ElseIf rows()\stat = "!!" cat = 5 ElseIf rows()\stat = "EX" cat = 6 ElseIf rows()\stat = "NF" cat = 7 Else ; autres codes porcelain XY If x$ <> " " cat = 1 ElseIf y$ <> " " cat = 2 Else ; par sécurité, considère comme non indexé cat = 2 EndIf EndIf Select cat Case 0 : PushTo(b0) Case 1 : PushTo(b1) Case 2 : PushTo(b2) Case 3 : PushTo(b3) Case 4 : PushTo(b4) Case 5 : PushTo(b5) Case 6 : PushTo(b6) Default: PushTo(b7) EndSelect Next ; 2) Tri alphabétique par chemin dans chaque seau SortStructuredList(b0(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String) SortStructuredList(b1(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String) SortStructuredList(b2(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String) SortStructuredList(b3(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String) SortStructuredList(b4(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String) SortStructuredList(b5(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String) SortStructuredList(b6(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String) SortStructuredList(b7(), #PB_Sort_Ascending, OffsetOf(FileRow\file), #PB_String) ; 3) Recomposition de rows() dans l'ordre des seaux ClearList(rows()) Macro AppendBucket(bucket2) ForEach bucket2() AddElement(rows()) rows()\file = bucket2()\file rows()\stat = bucket2()\stat rows()\include = bucket2()\include ProcedureReturnValue + 1 Next EndMacro AppendBucket(b0) AppendBucket(b1) AppendBucket(b2) AppendBucket(b3) AppendBucket(b4) AppendBucket(b5) AppendBucket(b6) AppendBucket(b7) If #EnableDebug Debug "[SortRows] total=" + Str(ProcedureReturnValue) EndIf ProcedureReturn ProcedureReturnValue EndProcedure ; ------------------------------------------------------------------ ; Ouvre l'interface principale (Push manuel, guide scrollable, ; prefs dépôt correctement rechargées & auto-sauvegardées) ; ------------------------------------------------------------------ ; ------------------------------------------------------------------ ; Ouvre l'interface principale (tri par intérêt avant affichage) ; ------------------------------------------------------------------ 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) Protected rp.RepoPrefs rp\remote = defRemote$ rp\branch = defBranch$ LoadRepoPrefs(repoDir$, @rp) ; Sélection persistante 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 l’outil (.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 d’actions 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, tri, affichage --- NewList rows.FileRow() BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) If GetGadgetState(#GShowPermanent) = 0 ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next EndIf SortRowsByInterest(rows()) FillStatusList(rows()) UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) ; =================== 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$) 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 SortRowsByInterest(rows()) 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 SortRowsByInterest(rows()) 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 SortRowsByInterest(rows()) 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 SortRowsByInterest(rows()) FillStatusList(rows()) RestoreSel() EndIf EndIf EndIf Case #GReincludeForever idx = GetGadgetState(#GListStatus) If idx >= 0 target$ = GetGadgetItemText(#GListStatus, idx, 1) If MessageRequester("Ré-inclure (permanent)", "Retirer des exclusions et ré-inclure ?" + #LF$ + target$, #PB_MessageRequester_YesNo) = #PB_MessageRequester_Yes If RemovePermanentExclude(repoDir$, target$) RememberSel() ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) If GetGadgetState(#GShowPermanent) = 0 ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next EndIf SortRowsByInterest(rows()) FillStatusList(rows()) RestoreSel() EndIf EndIf EndIf ; ---- Diff / Restaurer ---- Case #GDiff OpenDiffWindow(repoDir$, rows()) Case #GRestoreFile If OpenRestoreFileWindow(repoDir$, rows()) RememberSel() ClearList(rows()) BuildFullFileList(repoDir$, GetGadgetState(#GShowClean), GetGadgetState(#GShowIgnored), rows()) If GetGadgetState(#GShowPermanent) = 0 ForEach rows() : If rows()\stat = "EX" : DeleteElement(rows()) : EndIf : Next EndIf SortRowsByInterest(rows()) FillStatusList(rows()) RestoreSel() UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) EndIf ; ---- Branches : maj / ajout ---- Case #GReloadBranches ClearList(branchItems()) ListBranches(repoDir$, branchItems()) ClearGadgetItems(#GComboBranch) ForEach branchItems() : AddGadgetItem(#GComboBranch, -1, branchItems()) : Next Case #GAddBranch Protected name$ = InputRequester("Ajouter une branche", "Nom (ex: featureX) ou remote/branche (ex: origin/main)", "") If name$ <> "" If CreateOrTrackBranch(repoDir$, GetGadgetText(#GStringRemote), name$) ClearList(branchItems()) ListBranches(repoDir$, branchItems()) ClearGadgetItems(#GComboBranch) ForEach branchItems() : AddGadgetItem(#GComboBranch, -1, branchItems()) : Next If FindString(name$, "/", 1) = 0 SetGadgetText(#GComboBranch, name$) EndIf SaveRepoPrefs(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch)) EndIf EndIf ; ---- 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 SortRowsByInterest(rows()) FillStatusList(rows()) RestoreSel() UpdateGuide(repoDir$, rows(), GetGadgetText(#GComboBranch), GetGadgetText(#GStringRemote)) EndIf Case #GPush DoPush(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch)) Case #GPull DoPull(repoDir$, GetGadgetText(#GStringRemote), GetGadgetText(#GComboBranch)) ; ---- Clic sur la liste (cocher/décocher = inclure/exclure) ---- Case #GListStatus Protected idx2.i = GetGadgetState(#GListStatus) If idx2 >= 0 RememberSel() ToggleIncludeAt(idx2, rows()) SortRowsByInterest(rows()) FillStatusList(rows()) ; garde la sélection par la suite RestoreSel() EndIf EndSelect Case #PB_Event_CloseWindow 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 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 = 1835 ; FirstLine = 1824 ; Folding = ----------- ; EnableXP ; DPIAware ; Executable = ..\PBIDE-GitTool.exe ; CompileSourceDirectory