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