demo_gitualizer_pull_push.mp4
Gitualizer is a desktop Git client built around drag and drop.
Instead of starting with a Git command, you start with the graph:
- drag a branch onto another branch to merge, rebase, fast-forward, or push;
- drag a commit onto a branch to cherry-pick or revert it;
- drag a branch or commit to the trash to see the available removal options;
- drag a rectangle around commits to move or remove them as a group;
- drag files between the working tree and staging area;
- drag working-tree files onto the stash panel to save them;
- drag a stash onto a branch or the working tree to apply it, or onto the trash to delete it.
When creating a stash, Gitualizer asks for its name. Stashes belong to the repository, so a stash created on one branch can be applied to another branch.
Gitualizer does not run a graph operation immediately. It first creates a plan, shows the exact Git commands and their expected effects, and asks for confirmation. Destructive, remote, and history-rewriting operations are marked clearly.
The graph has three layouts under View:
- Commits shows the commit history and reference labels;
- Branches provides a branch-focused overview;
- Local vs Remote places local and remote-tracking branches side by side and shows their ahead/behind relationship.
Gitualizer requires Python 3.9 or newer.
Create a virtual environment and install the project:
python3 -m venv venv_gitualizer
source venv_gitualizer/bin/activate
python -m pip install --upgrade pip
python -m pip install -e '.[dev]'Open a repository:
python -m gitualizer /path/to/repositoryThe installed command works too:
gitualizer /path/to/repositoryThe path is optional. You can choose a repository from the application:
gitualizerRun the tests with:
pytestThe buttons next to the repository path have separate responsibilities:
- Refresh rereads local repository state. It is green, matching local branch labels.
- Fetch contacts configured remotes and updates remote-tracking references
such as
origin/main. It is purple, matching remote-tracking labels.
Auto Refresh rereads local state every 2.5 seconds. Auto Fetch Remotes runs every 60 seconds. Both are enabled by default and can be changed under Preferences. A refresh never contacts a remote; a fetch does not move local branches.
The first fetch after opening a repository is interactive. Gitualizer displays an alert asking the user to check the terminal that launched the application. Git, SSH, or the configured credential helper owns the prompt. Gitualizer does not read, store, log, or replay private-key passwords, HTTPS passwords, or tokens.
After successful authentication, Git and SSH are configured to keep a short-lived, five-minute session for background fetches:
- SSH uses OpenSSH connection persistence;
- HTTPS uses Git's in-memory credential cache.
Background fetch is non-interactive while that session is available. If authentication expires, Gitualizer marks authentication as required and can open the terminal authentication flow again. Prompts are rate-limited to avoid continuous interruptions.
The clickable authentication badge in the status bar reports whether access is not checked, authorized, required, or unavailable. After a successful remote command it also shows the elapsed time since the last authorization. Clicking the badge explains the boundary between Gitualizer and the external authentication tools.
The collapsible bottom area is split into an operation preview and a complete command history.
The history records every subprocess Gitualizer directly launches, including
automatic repository reads such as status, log, and for-each-ref, as well
as fetches and confirmed modifying commands. A command appears when it starts
and is updated when it finishes. Entries contain a compact HH:MM:SS timestamp,
the command, working directory, execution mode, exit code, stdout, and stderr.
Show stdout and execution details is off by default. When it is off, the history shows only the timestamp and bold, colored command. Errors are always shown in red. When enabled, stdout and execution details appear in gray.
Interactive commands inherit their streams from the terminal, so their input and output are intentionally not captured. Their command line and exit status are still included in the history.
The main flow is:
Git CLI
-> RepositoryReader
-> RepositoryState
-> Qt widgets
-> Qt signal
-> MainWindow handler
-> OperationPlanner
-> CommandPlan and confirmation
-> CommandExecutor
-> GitRunner
-> Git CLI
-> command events -> Complete Command History
PySide uses the Qt signal/slot system. It is an observer-style, publish/subscribe mechanism.
A widget publishes a signal when something happens. For example, the graph
emits commitDroppedOnReference. The widget does not know what Git command will
be used and does not call the planner directly.
MainWindow connects that signal to a handler. The handler asks
OperationPlanner to create a command plan, then shows the preview and
confirmation dialog.
This keeps the parts separate:
- widgets detect user interaction;
- signals describe what the user did;
MainWindowcoordinates the response;- the planner decides which Git commands represent the action;
- the executor runs only an approved plan.
It is similar to the Observer pattern, but the project uses Qt's built-in signal/slot implementation rather than maintaining its own observer list.
src/gitualizer/
app/main.py Application entry point
git/runner.py Git subprocess, authentication, and command events
git/repository.py Reads and normalizes repository data
model/repository_state.py Repository dataclasses
operations/command_plan.py Planned commands and execution results
operations/planner.py Converts user intent into Git plans
operations/executor.py Executes confirmed plans
ui/main_window.py Coordination, menus, dialogs, status, and history
ui/graph_widget.py Painted graph and graph interactions
ui/file_status_widget.py Working tree and staging interactions
ui/stash_widget.py Draggable stash list
ui/drag_mime.py Shared drag-and-drop MIME types
GitRunner runs Git with argument lists instead of shell command strings. It
also emits start and finish events for the complete command history and
configures short-lived Git/SSH authentication sessions for remote commands.
RepositoryReader uses it to read HEAD, commits, reflogs, branches, tags,
remotes, stashes, file changes, and operations in progress.
The reader converts Git output into the frozen dataclasses in
repository_state.py. UI code should use these objects instead of parsing Git
output itself.
MainWindow owns the current RepositoryState and updates the panels after a
refresh.
CommitGraphWidget is a custom-painted PySide QWidget. It draws nodes, edges,
references, selections, previews, and drop targets. It performs hit testing and
emits signals, but it does not run Git commands.
FileStatusWidget handles working-tree and staging-area drag and drop. It also
emits model objects through signals.
StashWidget lists repository stashes. Files can be dropped onto it to create a
named stash. A stash can be dragged onto a branch or the working tree to apply
it. Dropping it onto the graph trash runs git stash drop, which deletes the
stash without applying its files.
References, Stashes, and Remotes share the right side through tabs. Stashes is the initial tab, and View → Open Stashes selects it. The bottom area contains the command preview and live command history, and is collapsible when more graph or panel space is needed.
Cross-component dragging uses the MIME names in ui/drag_mime.py. The source
widget writes a small payload to QMimeData; the destination widget decodes it
and emits a Qt signal. MainWindow then creates the operation plan. This lets
widgets interact without calling each other or running Git directly.
OperationPlanner returns a CommandPlan. A plan contains:
- Git commands represented as argument lists;
- a plain-language explanation;
- expected effects and preview steps;
- warnings and destructive/history-rewrite flags;
- a fingerprint of the repository state.
Before execution, MainWindow reads the repository again. If its fingerprint
changed after the preview was created, execution is refused. This prevents a
stale plan from running against a different repository state.
CommandExecutor runs the confirmed steps, stops on failure, and returns the
results to the UI. User-initiated remote commands inherit terminal input and
output so authentication remains owned by Git/SSH. Non-interactive background
fetches use the short-lived session created by an interactive command.
Stash plans follow the same path as graph plans. Creating a stash uses selected working-tree paths, applying keeps the stash available, and deleting a stash is an explicit destructive plan.
Keep new features in the layer that owns the behavior.
- Add a Qt
SignaltoCommitGraphWidget. - Detect the gesture in its mouse, hitbox, or drag/drop handling.
- Emit model objects such as
CommitorReference. - Connect the signal in
MainWindow.__init__. - Let the handler request a plan and use the existing preview workflow.
Do not execute Git from the graph widget.
- Add a method to
OperationPlanner. - Validate whether the operation is available.
- Return a complete
CommandPlanwithCommandStep(["git", ...], ...)entries. - Mark destructive, remote, or history-rewriting effects.
- Add planner tests.
- Connect the plan to a UI signal or menu action.
Most operations do not require changes to CommandExecutor.
Commands that can contact a remote are classified centrally by the executor. Keep new remote operations on this execution path so they receive the standard terminal authentication alert and are included in command history.
- Add or extend a dataclass in
model/repository_state.py. - Read the value in
RepositoryReader. - Add a repository-reader test.
- Pass the normalized value to the relevant widget.
Do not parse raw Git output inside UI code.
Create a widget under ui/, feed it from RepositoryState, and update it from
MainWindow.refresh(). Use signals to report user actions back to the window.
For drag-and-drop between panels, add a shared type in ui/drag_mime.py, encode
only model identifiers or model data, and let the receiving widget emit a
signal. Keep planning and execution in MainWindow and operations/.
Repository-reader tests create temporary real Git repositories. Planner tests use model objects and check the generated command arguments and safety metadata.
Prefer testing at the lowest responsible layer:
- Git parsing in repository-reader tests;
- command behavior in planner tests;
- authentication and command events in runner/executor tests;
- pure formatting and UI helpers in focused UI tests.