Skip to content

Implementation Details

Loky edited this page Oct 14, 2026 · 7 revisions

Implementation Details

How each feature works internally. For usage, see the feature pages (Commit and Sync, Branches, Stash Manager, Diff Viewer and Gutter, Git LFS, Issues Tracker). Structure: Architecture.

  • Failsafe Startup: verifies git is available before initializing; disables cleanly with a clear error if not. That git --version is the only git call that blocks the main thread (it reads no repository, remote or credential helper; OS.execute() has no timeout). Everything else, including the LFS version shown in the startup log, comes from async commands.

  • Async Execution: all process execution lives in GitRunner. Local commands run with OS.execute() (argument array, no shell) on a worker thread; writes, push and pull take the write lock (Commit shows "Working..." meanwhile), reads and the other network commands always run. Network commands run as a monitored process killed after network_timeout_sec. The editor never waits on git.

  • Non-ASCII Support: accented and non-Latin file names (Lumière.mp3) and text (diffs, commit history, stash names, tag messages). GitPath.unquote() decodes git's quoted paths, so staging, the Size Guard, LFS lists and the commit file list work with any file name. GitRunner._fix_encoding() restores the UTF-8 text that Windows decodes with the ANSI code page. Tested on Windows-1252 only; Linux, macOS and other Windows code pages are untested. (read: Troubleshooting).

  • Commit & Sync: Staged/Unstaged file trees parsed from git status --porcelain=v2, with single and bulk stage/unstage. Status color & icon. Warning icon for conflicted file and large-file guard. Push targets -u origin HEAD. Amend toggle (CheckButton) runs commit --amend (keeps message with --no-edit if left blank); blocked behind a confirm dialog if the tracked ahead-count shows the commit is already pushed. Before every push, GitSyncOrchestrator.plan_push(has_upstream, ahead, behind) decides from the router's ahead/behind counts (no git call): NORMAL, FORCE_CONFIRM (diverged: dialog with the counts, then --force-with-lease) or BLOCKED_BEHIND (only behind: refused, "Pull first", because a forced push would rewind origin). GitotUi.push_tooltip() uses the same inputs, so the tooltip never disagrees with the action. While a write runs, GitRunner.write_busy_changed -> GitEngine.write_busy_changed disables the commit / stage buttons ("Working..."). Cancelling either confirm dialog logs a message and changes nothing.

  • Stash Manager (Shelf): Foldable list (name, branch, relative date, exact date on tooltip) built from git stash list (\x1f-delimited format, parsed by GitStashParser). Header title shows Shelf (count/cap). Stash / Pop / Drop buttons. An optional name field feeds stash push -u -m; left blank, git's own WIP on <branch>: ... subject is used. The cap is the max_stashes setting (1-50, default 10): Stash is disabled when the shelf is full, existing entries are never deleted, and the list shows every entry, including ones made from a terminal. The selection is kept across refreshes by stash SHA (the top entry is selected by default, so Pop acts on the latest). Drop asks for confirmation and re-resolves the SHA to its current stash@{n}, since the stack can shift while the dialog is open; git's output (which contains the dropped SHA) is logged, so a mistake can be undone with git stash store <sha>. Before a pop (async: list first, then pop), the touched files are listed with git stash show --name-only --include-untracked --no-renames and only those are refreshed (update_file(), open scene reload, stale-script toast). A failed pop (conflict) refreshes nothing and logs the raw output.

  • Local Branch Management: Foldable Tree (name, sync, updated) built from git branch -a (GitBranchParser.FORMAT, \x1f-delimited, parsed by GitBranchParser). Sync uses %(upstream:trackshort) (↑ ahead, ↓ behind, ↑↓ diverged, ✓ in sync), which is locale-independent. A button row (defined in the scene) under the title: Fetch, +, Delete, Delete on origin. The current branch's sync status (symbol + counts from rev-list, never git's localized text) is a label in the title bar; row tooltips explain each state in plain words. Double-click switches. + toggles an inline name row (Enter or Create button), guarded by a confirmation unless confirm_create_branch is off. Delete runs git branch -d on the selected local, non-current branch, always after a confirmation. There is no Force, so git refuses unmerged branches. If an origin/ copy exists, the dialog and the log say it stays on origin. Delete on origin runs only git push origin --delete refs/heads/<name> (name whitelist ^[A-Za-z0-9][A-Za-z0-9._/-]*$, own confirmation dialog, old tip hash logged to restore with git push origin <sha>:refs/heads/<name>); server refusals (default / protected branch) are shown raw. On switch/create, only files git actually changed are refreshed. EditorFileSystem.update_file() for cache bookkeeping, plus EditorInterface.reload_scene_from_path() for any of those files currently open in a tab, avoiding the full-project scan() noise. Godot has no API to reload or close an open script tab, so an editor toast flags any open script that changed on disk (close/reopen manually), and a separate toast warns when changed-file detection itself is unreliable (first switch after clone, or rapid successive switches). (read: Troubleshooting).

  • Remote Branch Management: git fetch origin via the fold's title-bar button, refreshing the branch list on success. Remote-tracking branches with no local counterpart appear in the list, and double-clicking one runs switch -c <name> --track origin/<name> in one atomic op.

  • Tags Versioning: Tag is push with commit. Settings to auto use Version (from project settings) for the tag's name (with auto v prefix) and the commit message for the tag's message.

  • Repo Status Panel: Show owner/repo · branch (local/remote) · ↑ahead ↓behind. Reads GitotRepoState, so it updates live. owner/repo is a [url] link in the RichTextLabel: the click opens GitotRepoState.repo_url (built by GitRemote.web_url(), "" = plain text), never an address read from the text. Detached HEAD shown in red in place of a branch name. Ahead/behind only logs to console on actual change, not on redundant refreshes (e.g. focus-in polling) and not for the first read at startup.

  • Commit History: git log parsed via \x1f- delimited format string. Count-filtered dropdown (10/20/30, default reflects last selection). Relative date shown in-list, exact short date (YYYY-MM-DD HH-MM) on date hover tooltip. Auto-refreshes alongside status on the same trigger set (commit/push/pull/switch) and on manual Refresh Status fallback button. Selecting a row (click or arrow keys) opens that commit history in the Diff Viewer Panel.

  • Color Diff Gutter: Modified and added lines are marked directly in the script editor's gutter, computed from git diff -U0 against HEAD. Updates automatically on save (where Godot's save signal fires reliably), on Godot editor focus, and a manual fallback refresh diff gutter button. Clicking a marked line gutter opens the bottom-dock diff viewer, jumped to that line. On plugin exit teardown() hides the gutter and clears its markers in the open editors (it is not removed: CodeEdit.remove_gutter() raised an index error in 4.7).

  • Diff Viewer Panel: Separate git diff -U3 against HEAD, decoupled from the gutter's own -U0 diff (no shared parser state, no regression risk to the gutter). Renders, as a read-only CodeEdit: @@ -old,count +new,count @@ header per foldable hunk, basic syntax highlighting, dedicated +/- sign gutter, real source line numbers in their own gutter, color-coded background per line. File-path label at the top confirms which file is shown. Stale-result guard discards a diff response if the active script tab changed before it arrived. (Manual fallback refresh button). Commit history mode: selecting a commit history row lists the commit's changed files (git show --name-status --format= --no-renames, status-colored A/M/D); the selected file's diff (git show -U3 ... -- <path>) is fetched on demand, so large commits stay fast. A latest-wins gate keeps one request in flight and drops outdated results (fast arrow-key navigation always ends on the highlighted commit), and a single file diff is truncated at 200,000 characters for display. Clicking the commit header copy its hash (brief color flash confirms the copy). Clicking a gutter marker in a script returns the panel to working-tree mode. (read: Troubleshooting).

  • Restore File: In commit-history mode, restores the selected file to its content at that commit (git restore --source=<hash> -- <path>). A confirmation dialog warns that uncommitted changes to the file will be lost. Guarded against the no-file-changes merge-commit case (button no-ops if there's nothing to restore). If the file is open in the script editor, its compiled class reloads automatically, but the tab's visible text only refreshes on the next editor focus change. Tooltip and console log both call this out (read: Troubleshooting).

  • Git LFS: GitLfs (core/, facade: commands in, typed signals out) over the static GitLfsParser. State is detected with a two-step async chain, git lfs version then git config --get filter.lfs.process, giving NO_BINARY / NOT_INITIALIZED / READY. The panel is a split: left dashboard + right files list. READY shows both. Any other state shows only the dashboard as a 4-step setup guide with the one button that fixes it (Get Git LFS link or Install LFS, git lfs install, global).

    • Dashboard: state, Refresh, Download (git lfs pull), Prune, Info (shows / hides the notes at the bottom, remembered in lfs_note_visible), stats, patterns. Stats come from GitLfsParser.summarize() over the files list: counts and sizes on this computer / on GitHub, to upload, to download, missing, largest file. There is no LFS quota API, so on GitHub is an estimate (origin's tree as of the last fetch, current files only; GitHub also counts old versions).
    • Files: git lfs ls-files --json (this checkout, then @{u} for origin; merged by oid in GitLfsParser.merge_scopes()) into a 3-column Tree (file, size, status). Status uses the same words as the branch list: Local + Remote, Local (not pushed yet), Remote (only a pointer here), Missing.
    • Patterns: read from the root .gitattributes (not git lfs track, which installs hooks as a side effect). Only the root file is read, so nested, global and .git/info rules are not seen. Track / Untrack run git lfs track|untrack; the Patterns menu tracks a preset in one click (Textures, Art source, 3D, Audio, Video, Fonts, Godot binary). Godot text formats (gd gdshader gdshaderinc gdextension tscn tres import uid godot cfg) are refused, they must stay diffable and mergeable in Git. So are catch-all patterns (*, **, *.*, /): they would put every file in LFS.
    • Download: git lfs pull (fetch + checkout) is the repair path for missing or pointer files; it never overwrites modified files. Runs as a network command (network_timeout_sec), then rescans the filesystem. Prune: git lfs prune behind a confirmation; git-lfs never removes objects that are not pushed.
    • Size Guard: a file is exempt only if LFS is READY and an LFS rule without / matches its file name (String.match(), case-sensitive). Any doubt keeps the guard on.
    • LFS state is detected asynchronously at startup (two quick git calls), so the Size Guard exemption and the Track-with-LFS button work immediately. The same first call supplies GitLfs.version, logged once.
  • Large-file Guard: Size configurable in settings, 0 to disable. Exempts LFS-tracked files (see Git LFS above).

  • GitHub Issues Tracker Board: (opt-out in settings possible). Split container (columns: number/title/date/type/priority/labels, header-click sort) on the left, IssueDetail on the right (metadata header, colored label pills, Markdown body in a read-only CodeEdit, Open in Browser button). Parser normalizes raw REST JSON (native issue Type, org "Priority" issue field, null-safe body/labels). Filter: Type/Priority/Label built from the fetched data, client-side AND filter (no extra requests). Paginated fetch (50 issues/page, capped at 8 pages = 400 issues), chained automatically and stopping at the first short page. Branch creation from an issue: auto-generated name (<type-prefix><number>-<title-slug>, e.g. feature/12-fix-login, editable), base-branch picker (defaults to current branch), confirmation dialog if the working tree is dirty. If a token expires or is revoked, Gitot detects this automatically (HTTP 401) and re-prompts for a new one. The header counts open / Bug / High priority issues from the parsed entries (GithubIssueParser.count_where()), no extra request.

  • Gitot Settings Panel: settings in user://gitot_settings.cfg, every key a GitotSettings.KEY_* constant: large_file_mb, network_timeout_sec, max_stashes, status_letters, confirm_push, confirm_create_branch, confirm_amend, auto_refresh_on_focus, github_issues_enabled, lfs_note_visible, echo_to_output (see Settings).

  • Status Signs: every Staged/Unstaged row and the FileSystem dock tooltip show a file's git status from one table, GitotStatusTree.STATUS_VISUALS (per GitStatusParser.FileStatus: letter, editor icon, color, tooltip text). GitStatusParser.parse() reads the porcelain v2 XY pair: X is the staged side, Y the unstaged side. ? is UNTRACKED and u is CONFLICT (both unstaged only). Letters: U untracked, A added, M modified (also type change), D deleted, C conflict. U means untracked here (git's own U is "unmerged"). GitStatusParser.ARGS uses --no-renames, so a rename is a D plus an A.

    • Lists: each Tree has one column, so hover and selection cover the whole row. _apply_status_visuals() sets the cell. Letter mode writes M path. Icon mode keeps the plain path and a tinted icon (GitotUi.get_icon() + set_icon_modulate()). The cell color is the status color, and a tree cell has only one, so the letter and the path share it. The path is stored in the item metadata and read through _path_of(). The cell text is display-only, so Stage, Unstage, Open file and Track with LFS never parse it. A Size Guard row replaces the status icon with its warning icon (icon mode).
    • Setting: status_letters (default off = icons). Toggling it emits status_display_changed. The dock answers with refresh_files(), so the lists repaint from one git status (row selection is cleared).
    • FileSystem tooltip: GitotFileTooltip (EditorResourceTooltipPlugin) is registered in gitot.gd._enter_tree() with FileSystemDock.add_resource_tooltip_plugin() and removed in _exit_tree(). It is fed by the same parse as the lists (GitotResultRouter.status_parsed -> GitotDock.status_parsed -> update()), which rebuilds a path -> {staged, unstaged} dictionary. A hover is one lookup: no git call, no cost per hover. _make_tooltip_for_path() appends one colored Git (staged|unstaged): <letter> label per side to Godot's own tooltip (base), never replacing it, so a file that is both staged and modified shows two lines. Unchanged files keep the default tooltip. Always on, no setting.
  • Project root: Gitot builds res:// paths from git's repo-relative paths, so the Godot project folder must be the repository root. At startup git rev-parse --show-prefix is checked and a non-empty prefix logs a warning.

  • Gitot Logger: Output routed through GitotLogger (Styled with Godot's own console font), so they can be filter out of Godot output log with the "standard output message" filter. Labels and colors come from one STYLES table, and [ is escaped in every message (a path like Level[1].tscn is not read as BBCode). The console's Info menu runs git reflog -n 20 (GitEngine.REFLOG_COUNT) or git count-objects -vH and prints the raw output; Copy log copies the console as plain text, Clear log empties it (and its history).

  • Messages: a result line names its subject (branch, owner/repo, tag, file), read from GitotRepoState (HEAD / origin until known). Every failure goes through GitotLogger.fail(title, raw, hint): error line, git's raw message, then a [Gitot Hint] line (Level.HINT, GitotLogger.h()) with causes git is known to produce. Level.EXTREME ([Gitot Fatal], also printerr) is reserved for "Gitot cannot work" (git missing, dock without its engine). Per-call data travels in the command_completed context (PUSH {force}, PUSH_TAG {tag}).

  • Results: command_completed broadcasts every result to GitotResultRouter (one dispatch point: no command is pre-handled elsewhere). A caller that needs only its own answer passes on_done(exit_code, output, ...bound args): it runs after the broadcast and is skipped if its target was freed. Latest-wins consumers (GitotCommitLogDiff) keep one request in flight and bind the (hash, path) they asked for. Reads that repaint the UI (LATEST_WINS_COMMANDS: status, log, branches, stash list, ahead/behind) are stamped: a slower, older answer is not broadcast, so it cannot overwrite a newer one (its on_done still runs).

  • Refresh cost: the editor-focus refresh (auto_refresh_on_focus) is skipped when the last full refresh is under 3 s old (FOCUS_REFRESH_COOLDOWN_MSEC); Refresh and post-command refreshes never wait. The Staged / Unstaged trees draw at most 500 rows each (MAX_ROWS) plus a Show all row; Stage All reads the parsed entries, not the rows. The status is parsed once (router) and shared via status_parsed; branches via branches_parsed.

  • Shell safety: run_network() builds a shell string, so every argument must match GitRunner.SHELL_SAFE_PATTERN ([A-Za-z0-9._/@+:-]), otherwise nothing runs. Tag names are checked up front (create_tag) so a tag that could not be pushed is never created. The project root and the log file sit inside double quotes, where spaces are harmless, so they are checked for the few characters that still act there (% on Windows; $, backtick and \ on Linux / macOS; " on both) and refused the same way. The exit status is a marker the shell appends to the log; only a marker that ends the log counts.

Clone this wiki locally