Compare commits

...
186 Commits
Author SHA1 Message Date
Christoph 22da397e39 fix(git): make unsetting upstream idempotent
Saving sync settings with no upstream could cause a fatal Git
error when unsetting upstream on a branch that never had tracking
information. This change guards the operation by checking for an existing
merge configuration before unsetting, making it idempotent. A test was
added to verify that clearing an unconfigured upstream is a no-op.

- Add test ensuring clearing an unconfigured upstream is a no-op
2026-08-26 00:35:30 +02:00
Christoph f160e48777 Update version to 2026.8.7 2026-08-23 23:41:30 +02:00
Christoph 4533f8aa38 style(theme): add dialog & panel CSS vars and refactor status panel styles
publish / Build and publish Ubuntu AppImage (release) Successful in 19m39s
publish / Build and publish Windows installer (release) Successful in 22m14s
publish / Build and publish AUR packages (release) Successful in 48m41s
Add CSS custom properties for dialog backdrop, shadows, and panel
highlights to centralize visual theming. Refactor the status panel
overlay to consume these tokens and use color-mix instead of hardcoded
rgba values. This improves visual consistency and lets overlays adapt
cleanly to appearance changes and theme variants.

- Centralize dialog and panel visuals with new CSS variables
- Replace fixed rgba values with tokenized colors and color-mix
- Move borders, shadows, and gradients to use the new theme tokens
2026-08-23 23:38:42 +02:00
Christoph f0bd74be4e Merge pull request 'feat(history): make branch filter groups collapsible' (#28) from new-desgin into main
publish / Build and publish Windows installer (release) Failing after 13s
publish / Build and publish AUR packages (release) Canceled after 0s
publish / Build and publish Ubuntu AppImage (release) Canceled after 54s
Reviewed-on: #28
2026-08-23 21:28:46 +00:00
Christoph 79f4ec21e4 feat(history): make branch filter groups collapsible
Add collapsible local and remote branch groups to the branch
visibility dialog, including toggle buttons and selection counts.
Introduce comprehensive dialog styling and responsive rules to match
the app UI. Opening the dialog now defaults to local open and remote
closed for faster access.

- Add CSS for branch-filter dialog layout, theming, and responsiveness
- Implement group toggles with chevrons and visible selected counts
- Default to local group open and remote group closed on dialog open
2026-08-23 23:20:13 +02:00
Christoph 44547f507a Merge pull request 'New desgin' (#27) from new-desgin into main
Reviewed-on: #27
2026-08-23 21:15:39 +00:00
Christoph 58027f5e22 feat(settings): add appearance and custom theme support
Introduce an appearance preference with modern, classic, and custom
modes and a persisted custom theme palette. Add helpers to load,
persist, and apply appearance and custom colors, and wire them into the
app settings lifecycle. Implement a comprehensive light theme and
appearance presets using CSS variables so custom palettes are applied
consistently across the UI, and include appearance in analytics when
settings are saved.

- Persist and apply appearance and custom color palette to :root
- Add muted light theme plus classic/modern presets and custom mapping
- Include appearance and customTheme in settings save and analytics payload
2026-08-23 23:14:07 +02:00
Christoph 3477070ec3 style(theme): refresh dark theme palette and control contrast
Update the dark theme tokens and UI surfaces to improve legibility
and clarify interactive boundaries across the app. Controls such as
buttons, inputs, and focus outlines were standardized and simplified
to reduce visual noise while preserving hierarchy.

- Overhaul color tokens and surface backgrounds for consistent tones
- Simplify primary button treatment and adjust focus/outline behavior
- Add targeted dark-mode rules to separate chrome (tabs, toolbars)
2026-08-23 22:20:25 +02:00
Christoph Brandau 7d3288a7ef chore(release): bump to 2026.8.6 and update changelog
publish / Build and publish Windows installer (release) Successful in 21m54s
publish / Build and publish Ubuntu AppImage (release) Successful in 20m10s
publish / Build and publish AUR packages (release) Successful in 48m1s
Bump version to 2026.8.6 across packaging, app config, and docs.
The change aligns binaries, config files, and changelog with the new release.
A new 2026.8.6 entry is added to the changelog and the UI surfaces release notes.

- Aligns version across packaging, app config, and UI
- Adds 2026.8.6 changelog entry and release notes in UI
- Prepares release by updating docs and build metadata
2026-08-18 21:43:32 +02:00
Christoph Brandau 6d806e87ea feat(git-lfs): improve activation and add HTTP/1.1 retry
The change adds a safer LFS activation flow that ensures a
root .gitattributes file is not hidden by ignore rules and
activates local LFS filters. It also merges patterns from the
repository attributes with those reported by Git LFS to avoid
duplicates and inaccuracies.

- Adds a retry path for large LFS uploads by forcing HTTP/1.1
during pushes when an HTTP 413 error is returned.
2026-08-18 21:36:13 +02:00
Christoph Brandau 7408371430 feat(startup): enable startup clone requests from CLI and IPC
Adds startup clone support with a new clone request type and parsing.
It wires a CLI and IPC pathway to forward a clone to a running app.
UI and docs were updated to reflect startup clone behavior.

- Introduce StartupCloneRequest and argument parsing.
- Wire IPC to pass clone requests and clone on startup.
- UI updated to queue clone requests and trigger clone.
2026-08-18 17:41:15 +02:00
Christoph Brandau e4697c74b6 feat(git): add ignore and untrack paths commands
The changes introduce server-side commands to manage gitignore
 rules and to untrack paths without deleting local files.
 A new GitIgnoreKind enum and helper functions normalize targets
 and build proper ignore patterns, and UI code was wired to use
 these commands.

- add_to_gitignore command and related helpers
- untrack_paths command to remove paths from the index
- UI wiring to expose ignore and untrack actions in explorer
2026-08-18 13:09:33 +02:00
Christoph Brandau a4936ba790 docs(ui): update changelog and help overlay text
publish / Build and publish Windows installer (release) Successful in 26m54s
publish / Build and publish Ubuntu AppImage (release) Successful in 20m10s
publish / Build and publish AUR packages (release) Successful in 45m9s
These changes update the docs to reflect the redesigned context menu.
HelpOverlay copy in EN/DE notes the separation of name, path, and count.
The changelog now records the context menu improvements in both languages.

- Update HelpOverlay translations to describe the redesigned context menu
- Extend changelog to mention the context menu improvements
- Align UI text with the new context menu design
2026-08-17 22:27:20 +02:00
Christoph Brandau b5d15f9190 feat(git): add stash push with optional paths and per-file scope
The stash push feature now supports limiting the stash to
selected files via an optional paths parameter.
The UI passes file paths to stash_push and adds a
per-file context menu for scoped stash operations.

- Extend stash API to accept optional paths for scoped stashes
- Implement per-file stash actions via a status panel context menu
- Update tests and docs to reflect scoped stash behavior
2026-08-17 22:24:22 +02:00
Christoph Brandau e7fbaadf5e build(release): bump to 2026.8.5 and update UI icons
This release bumps version numbers to 2026.8.5 across all
manifests and docs and includes UI refinements. The
status panel now uses lucide folder icons for tree view
and folders, replacing the previous icons and CSS.

- Replace the tree view icon with lucide FolderTree in status panel
- Use Folder and FolderOpen icons for expanded/collapsed folders
- Bump version to 2026.8.5 in all manifests and changelog
2026-08-17 22:02:46 +02:00
Christoph Brandau 8b559fa15c feat(status-panel): add tree view for changes
The body:
Introduces a tree view for the status panel in addition to the
existing list view. A new toggle allows switching between modes.

It builds a hierarchical tree of folders and files from changed
paths and renders both unstaged and staged sections with
collapsible folders and indentation.

Adds responsive styling so the view switch condenses to icons on
narrow screens and includes styles for folders, chevrons, and
folder counts.

- Build and sort a tree structure from file paths.
- Render folders and files with indentation in tree view.
- Add responsive styling and a view switch for status changes.
2026-08-17 21:42:32 +02:00
Christoph Brandau fac22ff173 feat(status-panel): overhaul status lanes and icons
The status panel now uses a grid-based layout with a flow divider
between unstaged and staged sections and clearer headers. Icons
were swapped to arrows for stage/unstage actions, and labels were
updated to show Working tree and Next commit for clarity.

- Replace action icons with arrows to indicate staging
- Redesign lanes and headers for a compact, responsive UI
- Add a divider and contextual labels for unstaged and staged sections
2026-08-17 21:30:53 +02:00
Christoph Brandau cd01df0108 feat(git-lfs): add Git LFS status and sidecar bundling
Adds Git LFS support to the backend API and related UI.
The app now exposes commands to inspect, install, track and pull.
A sidecar git-lfs binary is bundled and a prep script is added.
This prepares the correct binary for each target platform.

- Expose Git LFS status and management commands in API
- Bundle and prepare a sidecar git-lfs binary for targets
- Update packaging, docs, and README with LFS notes
2026-08-17 20:27:54 +02:00
Christoph 6f2d4dd8c9 style(ui): enforce square corners and preserve circular markers
Introduce a square UI language by resetting border-radius on
controls and surfaces to create consistent square corners app-wide.
Add explicit exceptions to preserve circular markers and keep
branch-flag shapes so avatars, dots, and graph connections remain
recognizable. Remove a few unnecessary !important modifiers to
simplify overrides.

- Reset border-radius globally for elements and pseudo-elements.
- Preserve circular avatars, graph dots, and similar markers.
- Keep curved branch flag shapes and targeted corner rules.
2026-08-16 16:42:22 +02:00
Christoph 33e85fe6be style(ui): tweak compact ref and repo-tab-close sizing
Increase the compact reference overflow to 20px and slightly raise its
mono font to improve legibility and vertical alignment. Add explicit
square sizing and an aspect-ratio for the repo tab close control so
its icon and layout remain consistent across breakpoints and overrides.
Use !important on the size rules to ensure they take precedence.

- bump .compact-ref-overflow height and font-size for alignment
- add fixed sizing and aspect-ratio for repo tab close button
2026-08-16 16:12:05 +02:00
Christoph 993179c25e feat(startup): wire startup repo path from CLI to UI
The app now supports opening a repository at startup by
reading a --repo argument and passing the path to the UI.
A new startup repository state is managed in the backend and
exposed via a tauri command and event, enabling the frontend
to auto-open the repository when ready. The UI adds a small
startup flow with retry handling to ensure the path is
consumed once available.
- Reads --repo or --repo=PATH and resolves relative paths
- Signals UI to open startup repo via open-startup-repository
- Frontend retries opening the repo until it succeeds
2026-08-16 15:54:54 +02:00
Christoph 25d7f27d7a feat(release): attach native Arch package and repackage for AUR
publish / Build and publish AUR packages (release) Successful in 46m31s
publish / Build and publish Ubuntu AppImage (release) Successful in 20m25s
publish / Build and publish Windows installer (release) Successful in 22m15s
Update release workflow to detect and attach the native Arch package.
Switch the binary AUR recipe to repackage the .pkg.tar.zst. Adjust
checksums and packaging steps so the output carries only the native
payload without bundling upstream package metadata.

- Attach the native .pkg.tar.zst to Gitea releases.
- Rework binary AUR recipe to extract usr from the native package.
- Update README to document using the native Arch package.
2026-08-16 10:48:28 +02:00
Christoph c5f058beb1 ci(app_builder): add fuse2 to Arch package list
publish / Build and publish Ubuntu AppImage (release) Successful in 20m12s
publish / Build and publish Windows installer (release) Successful in 22m8s
publish / Build and publish AUR packages (release) Successful in 49m53s
The app builder workflow now includes fuse2 among Arch packages.
This provides FUSE support in the build environment for tools and
mounts used during builds and tests. It prevents failures caused by
a missing FUSE runtime.

- Add fuse2 to ARCH_PACKAGES to satisfy FUSE-dependent tooling
2026-08-15 22:51:04 +02:00
Christoph 4181925047 ci(app-builder): fetch AppImage from Gitea release using token
publish / Build and publish Ubuntu AppImage (release) Successful in 20m20s
publish / Build and publish Windows installer (release) Successful in 20m56s
publish / Build and publish AUR packages (release) Failing after 45m53s
Replace unauthenticated artifact lookup with an authenticated Gitea
release API call that fetches release JSON and asset metadata.
Require a Gitea token (with optional fallback), use it to locate the
AppImage asset in the release JSON, extract its name and download URL,
and download the file with an Authorization header.
Fail early when no token is present and verify the downloaded asset
checksum to ensure integrity; this enables access to protected releases
and improves asset selection robustness.

- Enforce token presence and use Authorization header for API calls.
- Parse release JSON to select .appimage asset and determine filename.
- Remove dependency on unauthenticated artifact base URL lookup.
2026-08-15 21:15:09 +02:00
Christoph 927e0c3e92 feat(workflows): attach Arch package to Gitea release
publish / Build and publish Windows installer (release) Failing after 14m48s
publish / Build and publish Ubuntu AppImage (release) Successful in 19m53s
publish / Build and publish AUR packages (release) Failing after 46m10s
Add a CI step that finds the built Arch binary and attaches it
to the matching Gitea release using the repository API. The step
validates that the package was created, requires a Gitea token,
and skips upload if the asset already exists to avoid duplicates.

- Export artifact path to the environment and validate its presence
- Query the release to obtain its ID and upload the asset if missing
2026-08-15 19:48:17 +02:00
Christoph 0dac3e9f09 feat(remote): support authenticated deletion of remote branches
Add support for authenticated deletion of remote branches and folders.
The backend adds a batch delete command that queries the remote,
performs an atomic push --delete when possible with a fallback, and
prunes stale tracking refs; it accepts optional credentials. The
frontend and JS API integrate credential handling and prompt users when
authentication is required.

- New batch deletion command with optional username/password for auth.
- Uses ls-remote to scope deletions, tries --atomic then falls back.
- Frontend updates: credential dialog/action, pendingRemoteDelete state.
2026-08-15 19:38:39 +02:00
Christoph 4f1131e855 feat(branches): add bulk delete for branch folders
Add a context-menu action to delete all branches in a folder at once.
Top-level remote folders are protected and the currently checked-out branch
is preserved. The BranchPanel API now includes folder depth to prevent
accidental bulk-deletes and summarizes individual failures after processing.
UI polish and localization were applied, and the package/version metadata,
changelog, and help overlay were updated for the 2026.8.4 release.

- Bulk delete with top-level remote protection and failure summaries.
- BranchPanel now tracks folder depth and disables unsafe context menus.
- Version bump, changelog/help overlay updates, and UI/localization tweaks.
2026-08-15 19:20:06 +02:00
Christoph f24c39c573 feat(compare): localize compare dialog and refine styles
Add German labels to compare select options and refine styles.
Group names and the current marker are localized for German.
Style tweaks adjust spacing, weights, and muted colors for better
readability and visual consistency.

- Localize option groups and current marker to German.
- Refine dialog typography, spacing, and muted color tokens.
2026-08-15 19:12:40 +02:00
Christoph f487090b4b feat(branches): add folder delete action and context menu
Add ability to delete all branches in a folder from the UI. Add a
handler to batch-delete local or remote branches and refresh views.

- Batch deletion handles remote and local branches and reports failures.
- Folder context menu receives branches and disables delete if needed.
- Track folder deletion attempts and failures via telemetry.
2026-08-15 19:04:16 +02:00
Christoph c8247f2b2a style(repo-tabs): refine close button layout and focus state
Improve sizing and interaction of the repository tab close button. The
control now has a consistent 24px circular hit area. Hover and
focus-visible states use a subtle accent color for clarity. Icon size
is reduced to better center the glyph inside the control.

- Make the close button a 24px circular, grid-centered element.
- Add focus-visible and hover color with a subtle accent background.
- Reduce the close icon to 11px to improve visual balance.
2026-08-15 18:51:01 +02:00
Christoph a7558e457d feat(compare): add German localization and revamp compare UI
Pass the application language into compare dialogs and add German
translations so titles, buttons and helper text render appropriately.
Refactor dialog markup to introduce a compact header with a GitCompare
icon, show changed-file counts, and surface contextual help and warnings.
Adjust and extend CSS for spacing, layout and responsive behavior.

- Pass app language into compare components and derive isGerman flag
- Replace icons and restructure headers; add counts, help and warnings
- Update styles for sizing, spacing, responsive rules and new classes
2026-08-15 18:39:10 +02:00
Christoph 412e8d19b5 feat(ui): add SelectMenu component and replace native selects
Introduce a reusable SelectMenu component and replace native select
controls across the UI. This centralizes select behavior and styling,
enabling grouped options, placeholders, and a consistent popup
interaction. Add comprehensive CSS and light-theme tweaks, and update
rebase action styling to integrate the new control.

- Add a unified SelectMenu component and wire change handlers.
- Implement .select-menu styles, popup behavior, and theme overrides.
- Replace ad-hoc native selects in dialogs and rebase UI for consistency.
2026-08-15 18:26:32 +02:00
Christoph 32832f5db7 feat(git): support credentials for remote branch rename
Allow remote branch renames to be performed with optional credentials so
operations against protected remotes succeed when authentication is needed.
The backend accepts username/password and uses an authenticated push path
when provided, while the frontend prompts for and reuses stored credentials.

- Add optional username/password to rename RPC and use authenticated push
- Wire UI to queue rename, open credential dialog, and execute rename
- Extend credential dialog and handling to include the rename action
2026-08-15 17:23:40 +02:00
Christoph 37d2152fcd feat(branch-panel): improve context menu positioning and styling
This update enhances the context menu for branches and tags by
ensuring it fits within the viewport, preventing overflow and
improving usability. Additionally, the styling of the branch
context menu has been adjusted for better visibility and
interaction.

- Added dynamic positioning for context menus to avoid overflow
- Updated CSS for branch context menu to improve layout and usability
- Simplified context menu opening logic for better performance
2026-08-15 17:15:13 +02:00
Christoph 2bc74dc4a7 feat(HelpOverlay): add Git Notes feature documentation
This update introduces documentation for the Git Notes feature, which
allows users to attach additional context to commits without altering
the commit history. The new section includes detailed steps, commands,
and notes on how to effectively use Git Notes within the application.

- Added German and English documentation for Git Notes
- Included commands for viewing and managing commit notes
- Explained how to fetch and push Git Notes to remote repositories
2026-08-15 17:08:53 +02:00
Christoph 4db488415f Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite 2026-08-15 17:00:44 +02:00
Christoph be695d78a9 feat(publishing): add support for prebuilt AppImage package
This update enhances the publishing workflow by introducing a new
prebuilt AppImage package for Gitty, allowing users to install
the application without needing to compile from source. The
workflow now includes steps to generate and publish the binary
AUR package alongside the standard source package.

- Introduced PKGBUILD-bin for the prebuilt AppImage
- Updated workflow to handle both source and binary package publishing
- Enhanced README to guide users on installing the new package
2026-08-15 17:00:42 +02:00
Christoph Brandau 40311275b0 style(app): remove border radius on commit-body
Removes the rounded corners on the commit-body by setting border-radius to 0.
This makes the component use a flat edge and aligns with the design system.

- Remove dependency on ui radius variable for commit-body
2026-08-13 23:07:19 +02:00
Christoph Brandau fe577d78a8 feat(app): add background commit notes fetch and refresh logic
This change adds a background path for fetching commit notes per repo
and a shared cache to avoid duplicate work. When notes are fetched
and the active repo is visible, history is refreshed to reflect
notes without blocking user actions.

- Adds per-path backgroundCommitNotesFetches cache to debounce fetches
- Integrates note fetch into the background tick and refresh flow
- Handles credential errors and shutdown gracefully during fetches
2026-08-13 22:56:53 +02:00
Christoph Brandau a27a8666ee feat(git): add commit-note support and previews in history
This change adds Git notes support to the history UI.
Commits now carry a has_note flag which triggers a note indicator.
Notes can be previewed on hover and loaded on demand,
then the history can be refreshed after edits.

- Adds has_note support on commits and parses from logs.
- Renders a note indicator in history rows with a hover preview.
- Triggers history refresh after note-related actions.
2026-08-13 22:51:24 +02:00
Christoph Brandau c442b3735f feat(auth): add credential mode (credentials/token) support
Introduces a credential mode for stored credentials and wire it to
per-remote URL resolution and operation flows. The UI, storage, and
remote interactions now track and persist the mode, enabling token
based auth alongside username/password credentials.

- Remote URL resolution now considers direction (pull/push) and mode
- Credential dialog, saving, and keychain handling updated to pass and
  respect the mode
- Unique askpass scripts generated per invocation to avoid clashes
2026-08-13 22:27:00 +02:00
Christoph Brandau f621638eb3 feat(system): enable window destroy capability and stabilize shutdown
publish / Build and publish Ubuntu AppImage (release) Successful in 22m40s
publish / Build and publish Windows installer (release) Successful in 31m3s
publish / Build and publish gitty-desktop to AUR (release) Successful in 50m26s
Adds a new window capability to allow destroying the window.
It also changes shutdown handling in telemetry to avoid errors.

- Add core:window:allow-destroy to default capabilities
- Avoid shutdown errors by returning a pending promise in tracedInvoke
- Improve stability during shutdown for in-flight telemetry calls
2026-08-13 22:11:45 +02:00
Christoph 56467f481d Merge pull request 'Open code test' (#26) from openCodeTest into main
publish / Build and publish Ubuntu AppImage (release) Failing after 2m20s
publish / Build and publish Windows installer (release) Successful in 27m20s
publish / Build and publish gitty-desktop to AUR (release) Canceled after 40m8s
Reviewed-on: #26
2026-08-13 18:48:11 +00:00
Christoph Brandau 6434a9f10c refactor(credentials): remove credential expiry support and API
The credential expiry feature has been removed from both the Rust
backend and the frontend. Stored credentials now include only
username and password.

- Remove expiresAt field from StoredCredential
- Simplify API by removing expiry param from credSave
- Drop expiry UI and expiry checks across the app
2026-08-13 20:45:43 +02:00
Christoph Brandau cc0f1c076f refactor(ui): simplify branch status display and adjust layout
The changes simplify how branch status is shown in HistoryPanel.
The remoteName helper and the check icon were removed.
Status is composed inline with the label for local and remote branches.
CSS tweaks wrap content and adjust widths for better responsiveness.

- Remove remoteName helper and Check icon from HistoryPanel
- Inline status text for local/remote branches and avoid icons
- Improve graph layout: wrap refs and constrain widths on hover
2026-08-13 20:06:27 +02:00
Christoph Brandau b6b9964a93 feat(git): add upstream tracking and local-only branch UI
This release adds upstream tracking for branches and a local-only
indicator in the UI.
The Git integration now exposes upstreams and supports publish flows.
This enables publishing and remote-tracking configuration.

- Introduces upstream tracking and local-only branch UI markers
- Updates toolbar to reflect publish-local state and status indicators
- Bumps version to 2026.8.3 and updates changelog
2026-08-13 19:32:26 +02:00
Christoph Brandau f43fe00873 feat(git): rename remote branches atomically via push
Adds a new command to rename remote branches and update tracking refs.
Renaming remote refs is performed atomically using a single push.
The push creates the new remote ref and deletes the old if it succeeds.
The frontend now invokes remote-rename when needed and shows labels.

- Atomic remote rename via push with create/ref and delete
- Frontend supports remote branch renames from the branch panel
- Compare UI now shows labels for remote refs in results
2026-08-13 18:32:16 +02:00
Christoph Brandau ca14fac90c feat(ui): refine history graph visuals and layout
Refines history panel visuals and layout, including min width handling.
Removes the old branch visibility select in favor of a dialog button.
It also adds a graph connector to show primary branches.
Adds branch chips, icons, and hover effects to commits for clarity.

- History panel visuals and min width updated to improve layout.
- Removed branch visibility select; added dialog to customize branches.
- Branch chips and graph connectors enhanced with icons and hover effects.
2026-08-13 18:01:10 +02:00
Christoph Brandau 608b3131d2 feat(history-panel): add branch visibility modes and richer refs
Adds branch visibility modes to the history panel and remote branches.
A data model supports commits and refs including local and remote.
UI tweaks add compact ref chips and a new details panel.

- Implement focus/local/all/custom modes for branch visibility
- Introduce CommitBranchDecoration and CommitRefSummary types
- Wire remote branches and ahead/behind data to UI
2026-08-13 15:02:42 +02:00
Christoph Brandau a823aabbb9 feat(external-tools): force tools to open in new windows where needed
Adds a helper that forces selected tools to launch in a new window
instead of reusing the current one. This is applied to code editors,
diff/merge, and terminal launches, aligning behavior across platforms.
Presets and defaults are updated to pass new-window or equivalent flags,
and tests verify the new behavior for common tools.

- Update code editors to always use a new window when opened
- Normalize launch flags for Windows terminals and diff tools
- Add tests covering new-window behavior for common tools
2026-08-13 14:33:05 +02:00
Christoph Brandau 15d1f2bfd6 feat(external-tools): add cross-platform external tool discovery
Adds a new external tools subsystem to detect and launch
diff and editor tools across Windows, macOS, and Linux.
It exposes data models for tools, commands, and results to the UI
and serializes them for consumption by the app.

- Implement cross-platform discovery of editors and diff tools
- Expose serialized results to the UI for user selection
- Centralize per-OS known tool lists and overrides
2026-08-13 14:08:02 +02:00
Christoph Brandau 3eb554fee7 feat(ui): add command palette and commit selection sync
Introduce a global command palette for quick access to common repository
actions, branches, files, and commits. Wire it into the main shell so it
can open settings, help, and other dialogs while keeping keyboard access
consistent.

Also add commit selection state to the history view so the active commit
is highlighted and brought into view when chosen from either the palette
or the history panel.
2026-08-13 07:53:19 +02:00
Christoph Brandau 1fa57eea6f feat(git): convert Git commands to async for better performance
This update modifies several Git command functions to be asynchronous,
improving the responsiveness of the application. By utilizing async
runtime, operations that involve I/O or long-running tasks can now run
without blocking the main thread, enhancing user experience.

- Converted multiple Git command functions to async
- Introduced a helper function to handle async tasks with error management
- Improved overall performance and responsiveness of Git operations
2026-08-11 09:53:34 +02:00
Christoph Brandau 444a7acadd Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite 2026-08-10 17:30:33 +02:00
Christoph Brandau ac7cacd687 feat(telemetry): handle application shutdown process
This update introduces a structured shutdown process for the application. It ensures that background tasks are properly terminated and telemetry is notified when the frontend is shutting down, preventing any ongoing operations from continuing during this state.

- Added event listeners to manage app shutdown events
- Implemented a shutdown handler to clear timers and notify telemetry
- Updated background task checks to respect the shutdown state
2026-08-10 17:29:03 +02:00
Christoph 80dc66366d Update version to 2026.8.2 2026-08-10 14:24:42 +02:00
Christoph Brandau a6c86daf62 feat(gitea): upload release assets to Gitea
publish / Build and publish Windows installer (release) Successful in 28m43s
publish / Build and publish Ubuntu AppImage (release) Successful in 22m26s
publish / Build and publish gitty-desktop to AUR (release) Successful in 45m32s
This update introduces functionality to upload release assets to Gitea. A new helper function has been added to handle the API requests and manage the upload process, ensuring that existing assets are not duplicated. The main function has been modified to call this new upload feature after resolving the version.

- Implemented asset upload to Gitea releases
- Added checks to prevent duplicate uploads
- Enhanced error handling for missing configuration variables
2026-08-10 14:07:08 +02:00
Christoph Brandau f88b6961d1 fix(history): preserve branch colors across parent lanes
Keep branch metadata attached to each parent lane so the history graph
can render consistent colors and markers after merges and branch
transitions. Also soften the styling for behind-path segments to better
match the updated dot coloring.
2026-08-10 10:48:31 +02:00
Christoph eb65c1def7 fix(workflows): improve AUR SSH key handling and clone/push retries
publish / Build and publish Windows installer (release) Successful in 22m29s
publish / Build and publish Ubuntu AppImage (release) Successful in 37m9s
publish / Build and publish gitty-desktop to AUR (release) Failing after 52m11s
This update enhances the workflow for interacting with the AUR by
adding robust error handling for SSH key retrieval and repository
cloning/pushing. It implements retry logic with exponential backoff
for both SSH host key retrieval and git operations, improving
resilience against transient network issues.

- Added retries for SSH key retrieval with exponential backoff
- Implemented retry logic for cloning and pushing to AUR repository
- Ensured script exits gracefully with error messages on failure
2026-08-04 22:23:50 +02:00
Christoph 96e3805568 fix(dialog): improve escape key handling for dialogs
publish / Build and publish Windows installer (release) Successful in 23m4s
publish / Build and publish Ubuntu AppImage (release) Successful in 44m12s
publish / Build and publish gitty-desktop to AUR (release) Failing after 54m38s
The handling of the Escape key has been refined to ensure that it
closes the correct dialog based on the current state. Additionally,
the z-index for the compare dialog backdrop has been updated to
ensure proper layering with other dialogs.

- Enhanced Escape key functionality for better user experience
- Updated z-index for compare dialog backdrop to avoid overlap
2026-08-04 21:17:04 +02:00
Christoph 2d9a10e85c feat(workflows): implement timeout for pacman installation
publish / Build and publish Windows installer (release) Successful in 24m55s
publish / Build and publish Ubuntu AppImage (release) Successful in 33m25s
publish / Build and publish gitty-desktop to AUR (release) Canceled after 16m4s
This update introduces a timeout mechanism for the pacman installation process in the app builder workflow. If the installation does not complete within 30 minutes, it will terminate and log a timeout message, improving the robustness of the build process.

- Adds a timeout for the pacman command to prevent indefinite hangs
- Implements a heartbeat to monitor the installation progress
- Provides clear error messages for timeout and failure scenarios
2026-08-04 20:26:20 +02:00
Christoph 12d1b2657f fix(workflow): improve Arch Linux package installation process
publish / Build and publish Ubuntu AppImage (release) Canceled after 6m6s
publish / Build and publish Windows installer (release) Canceled after 6m4s
publish / Build and publish gitty-desktop to AUR (release) Canceled after 6m2s
The workflow for building and publishing to Arch Linux has been updated to
enhance the package installation process. This includes adding multiple
mirror sources to ensure reliability and implementing retry logic for
package installation attempts.

- Removed dependency on the Windows publish job
- Added backup mirrors for Arch Linux package management
- Implemented retry logic for package installation failures
2026-08-04 20:19:20 +02:00
Christoph b0b4fb69cb Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite 2026-08-04 20:11:51 +02:00
Christoph f3a3fcc079 feat(ci): update workflow for Windows and add Ubuntu publishing
The workflow has been modified to focus on building and publishing
Windows installers, with the previous matrix strategy removed.
Additionally, a new job for building and publishing Ubuntu AppImages
has been added, ensuring that the necessary dependencies and steps
are included for a successful build.

- Renamed job to reflect Windows installer publishing
- Added Ubuntu AppImage publishing workflow
- Simplified job structure by removing matrix strategy
2026-08-04 20:11:44 +02:00
Christoph 9d18f3af61 Update version to 2026.8.1 2026-08-04 19:35:08 +02:00
Christoph c4a922894f style(app): update styles for file history dialog
publish / Build and publish gitty-desktop to AUR (release) Failing after 4m40s
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Canceled after 37m41s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 37m26s
2026-08-04 18:41:19 +02:00
Christoph 41f474f1eb feat(dialog): add file history dialog component 2026-08-04 18:41:19 +02:00
Christoph 6dd70ec52a refactor(app): remove file history panel and related logic 2026-08-04 18:41:19 +02:00
Christoph 90070697dd refactor(explorer): update context menu for file history 2026-08-04 18:41:19 +02:00
Christoph 50524c2759 feat(commits): enhance commit listing with pagination and skipping
This update introduces pagination and skipping functionality for the
commit listing feature, allowing users to load commits in pages and
navigate through them more efficiently. The UI has been adjusted to
support loading more commits dynamically, improving the overall user
experience when dealing with large repositories.

- Added pagination support for commit history
- Introduced a loading mechanism for fetching more commits
- Updated UI components to reflect changes in commit loading behavior
2026-08-04 18:04:10 +02:00
Christoph 0cd97db523 This diff correctly updates the help documentation in HelpOverlay.svelte. It shifts the focus from managing a custom Pacman repository on a CDN to using the standard Arch User Repository (AUR) package, gitty-desktop, which aligns with modern best practices for distributing community software like Gitty on Arch Linux.
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 22m25s
publish / Build and publish gitty-desktop to AUR (release) Successful in 45m7s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
The changes are consistent across both German and English sections:
1.  **Summary:** Updated to reflect building from source via AUR.
2.  **Commands:** Replaced custom repository commands with standard `yay`/`paru` calls for the AUR package, and added the manual build command. The package name is standardized to `gitty-desktop`.
3.  **Steps:** Changed the workflow description from managing `/etc/pacman.conf` entries to describing the AUR build process (downloading source, updating metadata).
4.  **Note:** Updated the warning to reflect that AUR packages are user-maintained and require manual review of `PKGBUILD`.

No changes are needed; the provided diff is correct.
2026-07-31 14:19:30 +02:00
Christoph Brandau 8079ac5f64 chore(deps): update lockfile metadata
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Canceled after 4m59s
publish / Build and publish Arch package (release) Canceled after 4m58s
2026-07-28 20:59:42 +02:00
Christoph Brandau df26ecf9fb chore(release): publish 2026.07.22 2026-07-28 20:59:40 +02:00
Christoph Brandau af7b993550 feat(ai-commit): improve split dialog validation and apply flow 2026-07-28 16:11:24 +02:00
Christoph Brandau e07bc96127 fix(app): ignore stale repository open responses 2026-07-28 16:11:21 +02:00
Christoph Brandau f0e87d67d5 feat(ai): add AI-assisted commit splitting flow
Introduce a split-planning path for staged changes that asks supported AI
providers to group files into ordered Conventional Commit messages. The
plan is validated before execution so every staged file is assigned once
and unsafe states are rejected.

A new dialog lets users review and adjust the proposed groups before
creating the commits in sequence, with safeguards to preserve remaining
changes if something fails.
2026-07-28 15:01:32 +02:00
Christoph 3246dfdcfd Update version to 2026.7.21 2026-07-26 23:13:26 +02:00
Christoph Brandau bc3d3e88e0 feat(changelog): Add comprehensive changelog and help documentation
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 22m46s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 24m51s
publish / Build and publish Arch package (release) Successful in 53m27s
A new Changelog file has been introduced, documenting major user-facing changes across recent releases. Additionally, the HelpOverlay component was updated to include detailed sections on advanced Git workflows, including worktree management, line-level staging, and Arch Linux package automation instructions in both German and English.

- Added full changelog documentation
- Enhanced help with worktrees and line-staging guides
- Implemented Arch Linux package build steps
2026-07-26 22:59:07 +02:00
Christoph 1721341e62 Merge pull request 'Worktrees' (#25) from worktrees into main
Reviewed-on: #25
2026-07-26 20:20:24 +00:00
Christoph Brandau eb99165931 feat(ui): add worktree management toggle to branch panel
Introduces a dedicated section and interactive button for managing repository worktrees within the branch list view. This improves visibility into the overall state of local repositories by grouping related controls.

- Adds visual styling and structure for the new worktree group toggle in CSS.
- Replaces the old "Manage worktrees" button with a more descriptive, styled component in BranchPanel.svelte.
2026-07-26 22:16:35 +02:00
Christoph Brandau 24201c1302 feat(diff): enable line-level patch selection and actions
Introduces granular control over diff operations by enabling users to select individual lines within a hunk. This refactors the core logic across multiple components, allowing for precise staging, unstaging, or discarding of specific changes rather than operating on entire hunks. The UI now features dedicated controls and visual feedback for line-level selection.

- Refactored patch parsing to track selected line IDs and calculate range information.
- Updated the diff view CSS and component structure to display interactive line selection bars.
- Extended action dialogs to support confirming operations on selected lines.
2026-07-26 22:10:33 +02:00
Christoph Brandau 38b7f1a536 feat(git): Add comprehensive worktree management capabilities
This update introduces full support for Git worktrees, allowing users to manage multiple isolated working copies within a single repository. This includes new functionality to list, add, remove, move, lock, and repair worktrees, significantly enhancing the repository's capability to handle parallel development streams.

- Added `GitWorktree` structure definition across API contracts and Rust backend
- Implemented full CRUD operations for worktrees in Tauri commands
- Updated UI components (App.svelte, BranchPanel.svelte) to expose worktree management dialog
2026-07-26 21:47:35 +02:00
Christoph afa85b97aa chore(packaging): stabilize build process and resource usage
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 21m57s
publish / Build and publish Arch package (release) Successful in 49m42s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
Updates the PKGBUILD to improve stability and manage memory consumption during the compilation of large Rust dependencies. This involves adding specific environment variables within the build function to constrain compiler resources, alongside refining package options.

- Restrict parallel jobs and set explicit optimization levels for cargo builds
- Disable debug symbols in package options
2026-07-23 18:39:29 +02:00
Christoph 7869efeab5 ci(builder): pass build directory to makepkg command
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 28m52s
publish / Build and publish Arch package (release) Failing after 1h3m16s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
The CI workflow for building applications has been updated to correctly manage the build environment. Previously, makepkg was run without explicitly defining where its output should go, which could lead to unpredictable file placement or conflicts. This change ensures that a dedicated temporary directory is used as the build output location.

- Explicitly create and use a makepkg directory
- Pass BUILDDIR variable to makepkg command
2026-07-23 17:17:33 +02:00
Christoph 2b88e5a189 chore(ci): update build root ownership permissions
publish / Build and publish Arch package (release) Failing after 1m25s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Canceled after 7m5s
The workflow script responsible for application building is updated to adjust file ownership. Previously, ownership was restricted only to the source directory. By changing the scope to the entire build root, we ensure that all generated files and intermediate artifacts are correctly owned by the builder user before running makepkg.
2026-07-23 17:10:08 +02:00
Christoph f7793bc908 refactor(ci): Streamline package building workflow
publish / Build and publish Arch package (release) Failing after 1m23s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Canceled after 6m6s
The CI/CD pipeline has been significantly refactored to improve efficiency and reduce dependency on Docker containers during the build process. The core logic now uses temporary directories and direct execution commands, streamlining both the packaging of tarballs and the generation of the repository database. This change results in a faster and more robust build environment within the runner.

- Removed reliance on docker run for package building
- Simplified the mechanism for updating the pacman repository database
- Added necessary dependencies to base system installation steps
2026-07-23 17:03:57 +02:00
Christoph a1325d85e8 chore(ci): prepare arch container dependencies for building
publish / Build and publish Arch package (release) Failing after 2m17s
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 19m45s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
Added initial setup steps to the GitLite job within the continuous integration workflow. This ensures that the underlying Arch Linux environment is fully updated and contains all necessary system packages (like nodejs, git, and docker) before the main build process begins, improving overall build reliability.
2026-07-23 15:33:45 +02:00
Christoph e7e313b7d7 chore(config): Update git commands and CI runner platform
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 20m7s
publish / Build and publish Arch package (release) Failing after 3s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
Updated local development settings to include comprehensive Git status checks, improving developer workflow visibility. Additionally, the Arch package building job in the CI pipeline now targets archlinux runners for better environment parity during builds.

- Added multiple bash commands for detailed git logging and status retrieval
- Switched app builder workflow runner to archlinux
2026-07-23 13:57:28 +02:00
Christoph 482d7857b5 chore(ci): enhance build diagnostics and update runner platform
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 20m54s
publish / Build and publish Arch package (release) Failing after 1m25s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
This commit updates local configuration settings with additional diagnostic steps to improve visibility into the build environment during execution. Furthermore, the primary application builder workflow has been migrated from running on Arch Linux to Ubuntu 22.04. This change ensures broader compatibility for the CI/CD pipeline while maintaining core publishing functionality.

- Added extensive system and context checks in local settings
- Migrated app builder job runner platform to ubuntu-22.04
2026-07-23 13:22:12 +02:00
Christoph Brandau 8220e36629 feat(ci): automate arch package build and distribution
publish / Build and publish Arch package (release) Failing after 1s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Canceled after 0s
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Canceled after 1m59s
The continuous integration pipeline is significantly enhanced to fully automate the process of building, packaging, and publishing Gitty as an Arch Linux repository. A new dedicated workflow job handles version setting, dependency resolution, and package creation using makepkg within a Docker container environment. This ensures that every release is properly packaged and uploaded to the configured MinIO/S3 location for system-wide distribution.

- Added `publish-arch` job to CI pipeline
- Implemented repository management logic in arch_repo.py
- Updated PKGBUILD with necessary desktop dependencies
2026-07-23 00:58:32 +02:00
Christoph Brandau 27c8b40dc6 feat(ui): enhance branch delete confirmation flow
This update introduces a comprehensive and visually refined dialog component for confirming the deletion of branches. The logic now correctly distinguishes between deleting local and remote branches, providing tailored warnings and context to the user. Corresponding global styles were updated to implement the new layout and visual fidelity for this specific dialog type.

- Implements structured display for branch name and location
- Improves distinction between local and remote delete contexts
- Updates backdrop filter CSS properties for better compatibility
2026-07-23 00:33:17 +02:00
Christoph Brandau 6aed0b933e feat(ui): new background for commit cards 2026-07-22 00:06:43 +02:00
Christoph 94083a7106 Merge pull request 'Telemetry' (#24) from telemetry into main
Reviewed-on: #24
2026-07-21 21:51:09 +00:00
Christoph Brandau 647f2b2076 feat(ui): implement global error toast and state management
Introduces a persistent, styled error toast notification system for displaying critical application errors. This required refactoring the local error handling logic in App.svelte to manage complex state objects instead of simple timers, allowing for better control over visibility and interaction (e.g., pausing on hover).

- Adds global CSS styling and animations for the visible error toast component
- Updates error auto-hide mechanism to use structured state management
- Implements interactive features like pause/resume timing on mouse events
2026-07-21 23:45:05 +02:00
Christoph Brandau cae8390b54 feat(telemetry): Implement structured telemetry logging and reporting
Adds comprehensive client-side telemetry capabilities for usage, errors, and performance metrics. This includes integrating OpenTelemetry standards into both the frontend (Svelte) and backend (Tauri/Rust) layers to capture events, spans, and system resource utilization.

The implementation ensures that all collected data is privacy-filtered by design, explicitly excluding sensitive information like repository paths, credentials, source code, or email addresses from being logged.

- Updates README with detailed SigNoz telemetry guide
- Adds process metrics collection (CPU/Memory) in Rust backend
- Exposes `setTelemetryEnabled` state management to the frontend
2026-07-21 23:40:39 +02:00
Christoph Brandau 5c9f67700b Merge branch 'new_featrues' 2026-07-20 23:05:00 +02:00
Christoph 2bd60f0e5b feat(git): enhance error reporting and auth robustness
The commit enhances Git operation reliability by improving how authentication errors are detected and displayed to the user. It standardizes Git output language across different locales and introduces a mechanism to summarize complex raw Git stderr messages, ensuring users receive clear feedback when cloning or interacting with repositories that fail due to credentials.

- Standardize git command output using LC_ALL=C for consistent English messaging.
- Implement error summarization logic to extract the most relevant message from raw Git stderr.
- Update credential dialogs to display summarized and improved authentication failure details.
2026-07-19 16:41:21 +02:00
Christoph Brandau 95c8b01ed1 feat(remote): Enhance remote branch management and stability
Improved handling for deleting remote branches across the application, enhancing both user experience and backend reliability. This includes adding structured logging to all Git remote operations in Rust, refining UI components to handle remote-specific deletion flows, and providing clear status/error feedback in sync settings.

- Standardized styling for action toggles (Stash, Branch, Explorer) using consistent dimensions.
- Implemented detailed console logging for all Git remote operations on the backend.
- Refined dialogs and sync settings to provide explicit status and error messages during remote management.
2026-07-14 00:29:37 +02:00
Christoph Brandau 7800f0fb24 feat(git): expand core git repository management features
This update significantly expands the available Git functionality by adding robust support for initializing repositories, managing remotes, and improving complex workflow operations like merging and reverting commits. New API endpoints are exposed across the backend and frontend to handle remote setup, branch tracking, and conflict resolution workflows.

- Added full remote management capabilities (add, update, remove).
- Implemented advanced merge strategies and commit reversion logic.
- Introduced a dedicated UI component for synchronization settings.
2026-07-13 23:24:36 +02:00
Christoph 35310bec6d Update version to 2026.7.20 2026-07-13 00:27:15 +02:00
Christoph Brandau 7f85eb7d98 refactor(explorer): improve file history state management
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 19m26s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 32m24s
Adjusts the logic governing when file history views are refreshed or displayed. This ensures that selecting a file via the explorer correctly hides any active file history view, and prevents unnecessary refreshes of history data if the view is already collapsed or if the repository path is not yet set.

- Added explicit function to reset file history state
- Prevents automatic display of file history upon file selection
- Refines conditions for refreshing file history during repo updates
2026-07-13 00:23:27 +02:00
Christoph Brandau c2f55d96db feat(settings): implement persistent auto-refresh functionality
Adds a configurable and persistable auto-refresh setting to the application's settings dialog. This feature allows users to control whether the repository view automatically updates its status, branch information, and remote tracking in the background. The logic is integrated across core components to manage state changes and provide visual feedback to the user.

- Auto-refresh state is now persistent and managed via local storage.
- Updated settings dialog UI to include a dedicated toggle for auto-refresh.
- Removed manual auto-refresh toggling from the repository toolbar component.
2026-07-13 00:15:58 +02:00
Christoph Brandau 0dd5441754 feat(ui): enhance status panel actions and styling
This update significantly refactors the visual layout and functionality of the working tree status panel. It introduces dedicated action groups for discarding changes, staging, and unstaging files, improving user interaction and clarity.

- Added contextual action buttons (discard all/selected) to both staged and unstaged sections.
- Updated CSS structure to accommodate new status action containers.
- Improved handling of selected file counts within the panel headers.
2026-07-13 00:09:28 +02:00
Christoph Brandau 82c47c8d03 feat(ai): Add comprehensive pre-commit AI code review
Introduces a robust system for running automated, staged diff reviews against various large language models. This feature allows users to submit their changes to external AI services and receive structured feedback on potential bugs, security issues, and maintainability risks before committing.

The implementation covers the entire stack:
*   Backend logic was added to handle API communication with OpenAI, Anthropic, and custom endpoints.
*   A dedicated parser ensures that complex JSON outputs from LLMs are reliably converted into structured findings (severity, title, description).
*   New components and UI elements provide a clear visualization of the AI's assessment and actionable suggestions.

- Supports multiple major LLM providers (OpenAI, Anthropic)
- Parses structured JSON output for consistent review results
- Adds dedicated UI dialog to display AI findings and risk level
2026-07-13 00:03:11 +02:00
Christoph Brandau bad1263dcf feat(ui): enhance workspace status and diff styling
This commit updates several UI components to improve visual consistency and readability across various themes. It adds necessary class names to the Git branch display component and significantly refines the CSS for diff markers and general workspace status elements. These changes ensure that semantic colors are maintained on hover states and that key indicators remain highly visible in light mode.

- Updated styling for diff marker focus/hover states
- Improved visual feedback for line patch buttons (stage, unstage, discard)
- Added specific light theme styles for branch indicator and status counters
2026-07-12 23:46:44 +02:00
Christoph Brandau cc805e04bf style(ui): improve spacing and alignment of repository toolbar
Adjusts the CSS styling for the primary repository toolbar to enhance its visual organization. These changes introduce specific padding and margin rules, ensuring that various utility groups are properly centered and spaced vertically. This improves the overall aesthetic consistency of the component.
2026-07-12 23:38:44 +02:00
Christoph Brandau 735acd2551 refactor(ui): modernize status bar and component structure
This commit introduces a comprehensive overhaul of the application's UI, focusing on modernizing the global workspace status bar and improving overall theming consistency. Several components were refactored to simplify prop handling and improve separation of concerns, particularly within the TitleBar and RepoToolbar. The CSS includes extensive new variables and styles for better visual fidelity across light and dark themes.

- Overhauled the main application footer to display version, branch status, and sync metrics.
- Added global CSS variables and component styling for a modern look.
- Simplified repository state management by removing redundant props from TitleBar.
2026-07-12 23:32:55 +02:00
Christoph Brandau f9c0c00618 refactor(ui): extract repository tab bar into dedicated component
The complex logic and markup for rendering the repository tabs have been extracted from App.svelte into a new, reusable RepoTabs component. This refactoring significantly cleans up the main application view, improves separation of concerns, and makes the UI structure easier to maintain and extend. Corresponding CSS updates were applied to ensure the visual fidelity and responsiveness of the tab bar remain consistent across different states.

- Encapsulates all tab rendering logic in src/lib/RepoTabs.svelte
- Simplifies App.svelte by replacing large block of HTML with component usage
- Updates styling for better alignment and modern aesthetics
2026-07-12 22:56:39 +02:00
Christoph ff00925b13 chore(build): automate PKGBUILD version updates
This update enhances the build process by ensuring that the package definition file (PKGBUILD) is automatically updated with the current application version and release number. Changes are applied across both local build scripts and CI/CD workflows to maintain consistency. This prevents manual synchronization errors when building releases.

- Updates PKGBUILD version fields using package.json
- Ensures PKGBUILD is staged during final commit in CI/CD
- Improves robustness of the automated build pipeline
2026-07-11 23:27:59 +02:00
Christoph Brandau 982dbf136d feat(toolbar): overhaul repository action bar UI and status display
This refactors the entire appearance and structure of the repository toolbar, modernizing its layout and improving visual consistency across different states. The component now dynamically displays remote tracking information (ahead/behind) directly on Pull and Push buttons for immediate user feedback.

- Implemented a comprehensive CSS overhaul using grid and flexbox for better responsiveness.
- Added support for German localization within the toolbar actions.
- Enhanced visibility of sync status by displaying commit counts next to pull/push buttons.
2026-07-11 23:11:34 +02:00
Christoph Brandau 646dcc341e feat(help): Add comprehensive German documentation for Git concepts
The help overlay component has been significantly expanded with detailed sections covering advanced version control topics in German. These additions provide users with deep dives into core Git mechanics, complex workflows, and troubleshooting guides. This greatly enhances the user's ability to understand and utilize professional Git practices within the application.

- Detailed explanations of the Staging Area and Index concepts.
- Guides for advanced operations like interactive rebase and cherry-picking.
- Comprehensive sections on common errors (e.g., detached HEAD, non-fast-forward).
2026-07-11 22:52:31 +02:00
Christoph 5f3e55dcd7 chore: update package version and enhance build scripts
This commit updates the package version to 2026.7.19 and modifies
the build scripts to include additional commands for better
development workflow. The main window of the application is also set
to maximize upon launch.

- Updated package version in PKGBUILD
- Added new build commands in settings.local.json
- Maximized the main application window on startup
2026-07-11 20:02:23 +02:00
Christoph fdbff8175e style(ui): Move Buttons under the Repo selection 2026-07-11 18:04:49 +02:00
Christoph a6e7e991dd refactor(tauri): Improve builder initialization readability
The main application setup in src-tauri/src/main.rs has been refactored to enhance local variable scoping. By assigning the result of tauri::Builder::default() to a named variable, the code becomes clearer and more readable without changing runtime behavior.
2026-07-11 17:42:22 +02:00
Christoph 3491b8efb3 Merge remote-tracking branch 'origin/main' 2026-07-11 17:40:31 +02:00
Christoph 59a8f34e0e chore(config): update gitignore to exclude build artifacts
Updates the .gitignore file to ensure that generated package files and local build directories are ignored by Git. This prevents unnecessary binary data from being tracked in the repository history, keeping the project clean.

- Ignore pkg directory
- Ensure exclusion of tarball packages
2026-07-11 17:35:48 +02:00
Christoph 286b106baa feat(build): add PKGBUILD for package distribution
This introduces the necessary PKGBUILD file to define how Gitty should be built and packaged for system installation. It sets up the build process using npm and Tauri CLI, ensuring all dependencies are met before compiling the application binary. The script also handles installing required assets like icons, desktop entries, and documentation into the package structure.

- Defines build steps using tauri/npm
- Installs binaries, icons, and desktop entry files
- Sets up standard package metadata (pkgname, depends)
2026-07-11 17:34:48 +02:00
Christoph 670e7e24fe for linux build 2026-07-11 17:31:16 +02:00
Christoph Brandau 6729d61dca feat(language): implement internationalization and help overlay
This update introduces comprehensive language support (English/German) across the application, enabling localization for UI elements and settings dialogs. It also adds a dedicated Help Overlay component with detailed guides on using Gitty's core Git features.

- Adds language selection to App Settings
- Implements German translations in key areas
- Introduces global help documentation accessible via Ctrl+/
2026-07-11 15:03:01 +02:00
Christoph b181b384e7 Update version to 2026.7.19 2026-07-10 23:09:04 +02:00
Christoph Brandau c174fb3a80 Merge branch 'opt/light_mode'
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m59s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 27m53s
2026-07-10 22:59:20 +02:00
Christoph Brandau c747e02f29 feat(git): improve interactive rebase base handling and reflog display
The git module now correctly handles scenarios where the selected base branch is diverged from HEAD. This removes unnecessary ancestor checks during rebase planning, improving overall robustness of the feature. Additionally, the ReflogDialog component was updated to reliably retrieve the current HEAD hash for accurate display in the UI.

- Removed strict ancestor checking when starting interactive rebase operations.
- Updated App.svelte to accurately find and use the current HEAD entry from reflog data.
2026-07-10 22:56:48 +02:00
Christoph Brandau 791275f341 refactor(git): improve interactive rebase workflow robustness
This commit removes the dedicated feature QA page and significantly refactors the internal Git library logic for handling complex workflows, particularly interactive rebase. The changes introduce helper functions to manage temporary files and ensure proper cleanup of rebase artifacts regardless of success or failure. This improves the reliability of advanced git operations within the application.

- Added comprehensive cleanup routines for rebase helpers
- Centralized constants for rebase file names
- Removed obsolete QA feature code
2026-07-10 22:53:24 +02:00
Christoph Brandau f7b8beaad4 feat(git): add interactive rebase and reflog recovery features
This update significantly expands Git functionality by implementing support for advanced workflows, including interactive rebasing and recovering lost commits via the reflog. New logic handles preparing the necessary environment files (todo lists and reword queues) required by Git's internal editors. The frontend components are also updated to expose these new capabilities to the user interface.

- Implements full planning and execution flow for interactive rebase
- Adds functionality to list and restore commits using the reflog history
- Updates Rust backend commands to support advanced git operations
2026-07-10 22:49:08 +02:00
Christoph Brandau d0bca62362 refactor(ui): Improve title bar layout using flex and grid
The structure of the application title bar has been updated to utilize modern CSS layout techniques like Grid and Flexbox. This refactoring improves the responsiveness and organization of key elements such as repository name, branch indicator, and synchronization status. The changes ensure better alignment and handling of varying content lengths across different viewports.

- Restructures repo/branch info into a dedicated context container
- Uses flex properties for dynamic sizing of title bar sections
- Groups sync indicators into a cohesive unit
2026-07-10 22:21:53 +02:00
Christoph Brandau d3cde83518 style(css): standardize code surface and highlight colors
Introduces a comprehensive set of CSS variables defining surfaces, syntax elements, and diff coloring across both light and dark themes. This refactoring replaces numerous hardcoded color values throughout the stylesheet with these new variables, ensuring that all components consistently adhere to the defined theme palette.

- Added detailed variable definitions for code surfaces (e.g., `--code-surface`, `--code-input-bg`).
- Updated diff, hunk, and blame sections to use variable colors for better thematic consistency.
- Replaced fixed hex codes with variables across various component backgrounds and text colors.
2026-07-10 22:16:06 +02:00
Christoph 3fdd6d3467 Update version to 0.200.10 2026-07-10 20:27:29 +02:00
Christoph 900a6fa230 feat(core): improve platform compatibility and startup robustness
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m21s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Failing after 1h43m55s
This update enhances cross-platform reliability by addressing potential hangs during application startup on Linux environments using WebKitGTK. Additionally, native taskbar badge functionality is improved for macOS and Linux, ensuring accurate display of sync status. Configuration settings are also updated to include more specific internal crate reads and web search capabilities.

- Added timeout fallback to waitForAnimationFrame in Svelte
- Improved set_sync_badge logic for macOS/Linux platforms
- Updated local configuration with detailed dependency reads
2026-07-10 20:24:48 +02:00
Christoph 2d7946f6fd Update version to 2026.7.18 2026-07-10 18:38:11 +02:00
Christoph 0af57f1818 Merge pull request 'Opt/light mode' (#23) from opt/light_mode into main
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m49s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 27m6s
Reviewed-on: #23
2026-07-10 16:22:06 +00:00
Christoph Brandau 8e6811b786 style(ui): refresh status panel color palette
The StatusPanel component has received a visual update to modernize its appearance. This change adjusts the primary color scheme across multiple elements, shifting from purples and magentas towards cooler blue and cyan tones. The goal is to improve aesthetic consistency and give the component a more cohesive look within the application's overall design system.

- Updated accent colors and gradients throughout the panel.
- Refined borders and shadows for a cleaner visual depth.
- Adjusted trace and overlay element colors for better harmony.
2026-07-10 17:33:52 +02:00
Christoph Brandau de8832a919 style(ui): update global theme colors and palette
This commit implements a comprehensive visual refresh by updating the core color scheme across the application. A new, cohesive dark mode aesthetic has been applied, shifting primary accents from purple/orange tones to cooler blue/cyan gradients. This ensures better consistency and modernizes the look of various components, including buttons and loading indicators.

- Updated global CSS variables for consistent theming
- Refreshed component styles with the new color palette
- Adjusted repository loading overlay visuals
2026-07-10 13:47:50 +02:00
Christoph Brandau 25b98d7c4d feat(ui): Implement theme persistence and system preference support
Adds comprehensive theme management across the application, allowing users to save their preferred color scheme (light/dark) or automatically follow the operating system's settings. This required updating state management in App.svelte to handle theme loading and applying changes dynamically via CSS variables.

The styling layer was significantly updated with new CSS variables and rules for both dark and light modes, ensuring visual consistency across all components like dialogs, buttons, and sidebars.

- Added logic to detect system color scheme preference
- Implemented full support for persistent light/dark mode switching
- Overhauled global CSS variables for improved theme adaptability
2026-07-10 12:24:14 +02:00
Christoph Brandau 7ef1a2dac8 refactor(status): Support batch and all changes discard functionality
The file discarding mechanism has been significantly refactored to improve handling of multi-file operations. Instead of processing discards on a per-file basis, the system now supports batch actions for selected files or reverting all tracked modifications simultaneously. This update centralizes complex discard logic into dedicated functions across the component suite.

- Updated state management and types to handle arrays of file changes.
- Added UI elements and handlers for discarding all staged/unstaged changes.
- Enhanced the confirmation dialog to display multiple discarded targets in a list view.
2026-07-10 11:36:15 +02:00
Christoph Brandau 44a5b776d7 fix(refresh): improve auto-refresh robustness against repo switches
The automatic data refreshing mechanism has been updated to handle asynchronous state changes more reliably. It now incorporates checks to determine if the active repository path has changed during background operations. This prevents stale or incorrect data from being displayed if a user navigates away from or switches repositories while a refresh cycle is in progress.

- Added path validation checks after status fetching and bundle opening
- Ensured all subsequent refresh calls use the validated current repository path
2026-07-10 11:15:47 +02:00
Christoph 55b4c9fc50 Update version to 2026.7.17 2026-07-09 20:58:50 +02:00
Christoph Brandau 35d9fc969c feat(status-panel): enhance UI and add new functionality
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m10s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 30m34s
This update introduces several improvements to the StatusPanel component, enhancing the user interface and experience. A new function has been added to simplify file path handling, and the overlay now features a more dynamic design with animated elements for better visual feedback during operations.

- Added baseName function for improved path handling
- Enhanced status overlay with animations and new design elements
- Updated file staging and unstaging messages for clarity
2026-07-09 19:51:51 +02:00
Christoph Brandau 710c6d4a53 feat(git): refactor Git commands to use async/await pattern
This update modifies several Git command functions to utilize the async/await
pattern, enhancing performance and responsiveness during operations. The changes
allow for non-blocking execution of commands, improving the overall user experience.

- Converted multiple Git command functions to async
- Added error handling for async operations
- Updated related tests to support async execution
2026-07-09 19:17:02 +02:00
Christoph Brandau 0877d5a568 feat(status-panel): add loading overlay for ongoing operations
This update introduces a loading overlay in the StatusPanel component
to indicate ongoing operations. The overlay displays a spinner and
the current operation status, enhancing user experience during
long-running tasks.

- Added a loading overlay with a spinner for busy states
- Included operation status text to inform users of current actions
- Improved visual feedback for ongoing processes in the UI
2026-07-09 15:20:52 +02:00
Christoph Brandau f79f3dc1ec feat(recent-repos): add functionality to remove repositories from recent
This update introduces a new function to remove repositories from the
recent list. The user can now manage their recent repositories more
effectively, enhancing the overall user experience.

- Implemented removal of repositories from the recent list
- Updated UI to reflect changes in the recent repositories management
2026-07-09 15:14:29 +02:00
Christoph cf2773b6f4 Update version to 2026.7.16 2026-07-09 09:54:57 +02:00
Christoph Brandau 0ba7169efd feat(tauri): add single instance plugin and improve code formatting
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 19m14s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 26m5s
This update introduces the tauri-plugin-single-instance to ensure that
only one instance of the application can run at a time. Additionally,
the code has been reformatted for better readability, with consistent
line breaks and spacing.

- Added single instance plugin to prevent multiple app instances
- Improved code formatting for better readability and maintenance
- Updated dependencies in Cargo.toml for new plugin integration
2026-07-09 09:49:27 +02:00
Christoph Brandau c087171735 Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite 2026-07-09 09:21:53 +02:00
Christoph Brandau 2d3c30b553 feat(App): enhance repository fetching with reference and commit updates
This update improves the repository fetching process by ensuring that
the references and commit graph are refreshed after fetching remote
repositories. This change enhances the accuracy of the displayed
repository state and ensures that users have the most up-to-date
information.

- Added a new function to refresh refs and commit history
- Integrated the refresh function into the fetching workflow
- Improved status application logic for active repositories
2026-07-09 09:21:33 +02:00
Christoph 8409556158 Update README.md 2026-07-09 07:20:42 +00:00
Christoph db60f57544 Update README.md 2026-07-09 07:19:59 +00:00
Christoph cb5865094f Update README.md 2026-07-09 07:19:15 +00:00
Christoph c74db60daf Update README.md 2026-07-09 07:18:58 +00:00
Christoph 812027abcf Update README.md 2026-07-09 07:07:43 +00:00
Christoph Brandau c8d5553214 refactor(App): streamline repo search and row mapping functions
This update enhances the repo search functionality by passing the search term directly to the filtering functions. It also refactors the row mapping functions to accept parameters for better flexibility and clarity, improving the overall readability and maintainability of the code.

- Simplified search and mapping logic for repositories
- Improved parameter handling in helper functions
2026-07-09 09:01:42 +02:00
Christoph 8d9dedf93c Update version to 0.200.5 2026-07-09 08:41:49 +02:00
Christoph Brandau 2c9b8adb9e Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m57s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 28m30s
2026-07-09 08:38:44 +02:00
Christoph Brandau e6d9e60b8f feat(status-panel): enhance file staging and un-staging functionality
This update improves the file staging and un-staging process by allowing
multiple files to be selected and acted upon simultaneously. The user
interface has been enhanced to provide clear feedback on the number of
selected files and their respective statuses.

- Added support for staging and un-staging multiple files at once
- Introduced visual indicators for selected files in the UI
- Improved event handling for file selection and actions
2026-07-09 08:38:33 +02:00
Christoph Brandau 8627012d93 feat(ui): enhance panel resizing and state persistence
This update introduces functionality for resizing various panels in the UI, including the left sidebar, branch panel, stash panel, and file history. It also adds state persistence for panel sizes and collapsed states using local storage, improving user experience by maintaining preferences across sessions.

- Implemented resizing functionality for multiple UI panels
- Added local storage support for panel dimensions and collapsed states
- Enhanced accessibility features for panel controls
2026-07-09 08:33:09 +02:00
Christoph a21fa9e54c Update version to 0.200.4 2026-07-08 22:25:09 +02:00
Christoph Brandau adfb5f4158 Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 19m49s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 26m26s
2026-07-08 22:07:02 +02:00
Christoph Brandau 6f3f438f15 feat(nsis): add quiet uninstall string to registry
This update introduces a quiet uninstall string to the registry for
the application, allowing users to uninstall the software silently
without user interaction. This enhancement improves the user experience
for automated uninstall scenarios.

- Added QuietUninstallString for silent uninstallation
- Enhances automation capabilities for users
2026-07-08 22:06:56 +02:00
Christoph b495313a51 Update version to 0.200.3 2026-07-08 22:05:11 +02:00
Christoph Brandau eed7325719 Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Has been cancelled
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Has been cancelled
2026-07-08 21:54:51 +02:00
Christoph Brandau 51482c497f feat(installer): enhance installation process with registry and shortcuts
This update improves the installation process by adding macros for
registering the application in the Windows registry and creating
shortcuts on the desktop and start menu. These changes ensure a
better user experience by providing easy access to the application
and proper installation records.

- Register application details in the Windows registry
- Create shortcuts for quick access on the desktop and start menu
- Clean up legacy shortcuts during installation
2026-07-08 21:54:24 +02:00
Christoph 8d0a7b4c71 Update version to 0.200.2 2026-07-08 21:19:39 +02:00
Christoph Brandau a26e164d95 Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 19m59s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 26m52s
2026-07-08 21:03:39 +02:00
Christoph Brandau 784f133abd feat(ci): enhance publishing targets and update configuration
This update introduces a new mechanism for handling multiple publish
targets when uploading artifacts. It refines the configuration by
parsing legacy targets from environment variables and allows for
dynamic bucket and directory management during the publishing process.

- Added support for multiple publish targets from environment variables
- Refactored artifact upload logic to improve maintainability
- Updated tauri configuration to include new endpoint for latest.json
2026-07-08 21:02:55 +02:00
Christoph 3021c7c345 Update version to 0.200.1 2026-07-08 20:51:16 +02:00
Christoph Brandau e0a77607f5 feat(tauri): migrate from GitLite to Gitty and clean up legacy data
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 19m47s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 27m57s
This commit updates the installer and configuration to transition from
GitLite to Gitty. It includes macros for migrating legacy data and
uninstalling the previous GitLite installation, ensuring a smoother
upgrade experience for users.

- Added migration and uninstallation macros for legacy GitLite data
- Updated application identifier from com.git-lite to com.gitty
- Enhanced post-installation cleanup for legacy shortcuts
2026-07-08 20:48:22 +02:00
Christoph Brandau 2383abb496 feat(tauri): add NSIS hooks for post-install and post-uninstall
This update introduces NSIS hooks for managing shortcuts and directories
during the installation and uninstallation of the application. The hooks
ensure that the desktop and start menu shortcuts are removed properly,
along with the associated program directory.

- Implement post-install and post-uninstall hooks in NSIS
- Update configuration to reference the new hooks file
2026-07-08 19:52:59 +02:00
Christoph Brandau a7faaceb83 refactor(app): rename GitLite to Gitty throughout the application
The application has been rebranded from GitLite to Gitty, which involved
updating various references across the codebase, including titles,
descriptions, and identifiers. This change reflects the new branding
strategy and ensures consistency in the user interface and backend.

- Updated project name and identifiers in configuration files
- Changed all instances of "GitLite" to "Gitty" in HTML and Svelte files
- Adjusted descriptions and help texts to match the new branding
2026-07-08 19:51:28 +02:00
Christoph Brandau ea49380138 Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite 2026-07-08 18:26:02 +02:00
Christoph Brandau a072bba2e6 feat(splashscreen): enhance startup sequence and splashscreen handling
This update improves the splashscreen management during application startup. The splashscreen will now remain visible for a minimum duration and will only close after the main application is ready, ensuring a smoother user experience.

- Introduced a new startup sequence to manage background tasks.
- Added error handling for splashscreen closure to maintain functionality.
- Optimized the timing for displaying and hiding the splashscreen.
2026-07-08 18:25:43 +02:00
Christoph c45a43b866 Update version to 2026.7.16 2026-07-08 16:37:13 +02:00
Christoph Brandau 7e7c1b0b69 feat(assets): Add comprehensive platform icon assets
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Has been cancelled
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Has been cancelled
This update introduces a complete set of high-resolution icons for all target platforms. This ensures consistent branding and visual identity across desktop, Android, and iOS environments. The tauri configuration file has been updated to utilize these new assets.

- Added full sets of Android mipmap density icons
- Included comprehensive iOS AppIcon assets for multiple sizes
- Updated tauri.conf.json with modern icon paths
2026-07-08 16:34:11 +02:00
Christoph Brandau 218907a946 build(tauri): add custom icon to Windows installer
Adds configuration to specify a dedicated icon for the NSIS installer when building on Windows. This improves the visual branding and user experience during the installation process.

- Configures the tauri build system to use a specific icon file.
2026-07-08 15:46:42 +02:00
Christoph Brandau 0d4f9ca630 fix(RepoLoadingOverlay): adjust loading overlay position
The loading overlay's position has been updated to account for a new
layout change. This ensures that the overlay is displayed correctly
below the app title bar, improving the user experience during loading.

- Adjusted overlay position to maintain consistency with layout changes
2026-07-08 15:39:41 +02:00
Christoph Brandau 44be7afff2 fix(App): close context menu on right-click event
This change ensures that the repository tab context menu is closed
when the user right-clicks anywhere in the window. This improves
the user experience by preventing the context menu from remaining
open unintentionally.

- Added functionality to close the context menu on right-click.
- Updated event handling for context menu to use capture phase.
2026-07-08 15:39:32 +02:00
Christoph Brandau 8793d3de40 feat(repo-tabs): add context menu for managing repository tabs
This update introduces a context menu for repository tabs, allowing users
to easily close individual tabs, close all other tabs, or close tabs to the
right. Additionally, several visual elements related to loading animations
have been removed to streamline the user interface.

- Implement context menu actions for repository tab management
- Remove unnecessary loading animations from the splash screen and overlays
- Enhance user experience with improved tab management functionality
2026-07-08 15:34:30 +02:00
Christoph Brandau cc179e9eef feat(splashscreen): implement a splash screen for application startup
This commit introduces a splash screen that enhances the user experience during application startup. It includes a new HTML file for the splash screen layout and a corresponding icon. The main application logic has been updated to close the splash screen once the main application is ready.

- Added a splash screen HTML and icon for improved visual feedback
- Updated main application logic to manage splash screen visibility
- Enhanced title bar branding with an image instead of SVG
2026-07-08 15:16:51 +02:00
Christoph Brandau 61a22987ba style(FileHistoryPanel): enhance loading animation and styles
This update improves the loading animation for the File History Panel,
making it more visually appealing and responsive. The CSS has been
refactored to introduce new styles and animations, while the Svelte
component has been updated to utilize these changes effectively.

- Added new loading animations and styles for better user experience
- Refactored CSS to improve maintainability and visual design
- Updated Svelte component to integrate new loading elements
2026-07-08 14:36:09 +02:00
Christoph Brandau ba2065d3c4 feat(RepoLoadingOverlay): redesign loading overlay with new animations
The loading overlay component has been significantly redesigned to enhance the user experience. The previous SVG-based loading animation has been replaced with a more visually appealing layout that includes new animations and an icon. This update aims to improve the overall aesthetics and interactivity of the loading state.

- Introduced new loading animations and visual elements
- Added a background grid effect for a dynamic appearance
- Updated styles for better responsiveness and visual hierarchy
2026-07-08 14:33:25 +02:00
Christoph Brandau c9a93f1d0a feat(blame): add search functionality to blame dialog
This update introduces a search bar to the blame dialog, allowing users to filter blame lines based on their input. The search highlights matching text segments, improving the usability of the blame feature for navigating large files.

- Implemented a search bar for filtering blame lines
- Added visual feedback for matched search terms
- Enhanced overall user experience in the blame dialog
2026-07-08 14:20:51 +02:00
Christoph Brandau de6f497e35 feat(tracking): enhance event tracking for repository actions
This update significantly improves the event tracking system within the
application. Various user interactions, such as toggling favorite
repositories, managing branches, and handling file operations, are now
logged with detailed event tracking, providing better insights into user
behavior and application usage.

- Added tracking for repository management actions and file operations
- Enhanced tracking for branch and tag management events
- Implemented tracking for explorer interactions and conflict resolutions
2026-07-08 14:17:51 +02:00
Christoph Brandau 7589a38564 Merge branch 'main' of https://git.cbsk-tech.de/Christoph/GitLite 2026-07-08 14:08:07 +02:00
Christoph Brandau 5e46995c89 feat(repositories): add favorite repositories feature
This update introduces a favorites system for repositories, allowing users
to mark certain repositories as favorites for easier access. The UI has been
modified to include a star icon for toggling favorites, and the layout has
been adjusted to display favorite repositories separately.

- Implemented favorite repository management with local storage
- Updated UI to show favorite repositories with a star icon
- Enhanced repository listing to filter and display favorites
2026-07-08 14:07:50 +02:00
Christoph 82f738c15e Update version to 0.200.0 2026-07-08 13:13:00 +02:00
Christoph 6f1771f8b6 Merge pull request 'feat(icons): generate application icons in multiple sizes' (#22) from branding into main
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 19m47s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 30m37s
Reviewed-on: #22
2026-07-08 11:10:17 +00:00
Christoph Brandau a57e52a38b feat(icons): generate application icons in multiple sizes
This update introduces a new script to generate application icons in various sizes, including 32x32, 64x64, 128x128, and 512x512 pixels. The generated icons are now included in the Tauri configuration, ensuring that the application has the appropriate icons for different contexts.

- Added a script for generating icons from a source image
- Updated Tauri configuration to reference new icon sizes
- Included multiple icon formats for better compatibility
2026-07-08 13:09:14 +02:00
Christoph 4befe8b189 Update version to 2026.7.15 2026-07-07 17:40:11 +02:00
134 changed files with 26839 additions and 2665 deletions
+42 -1
View File
@@ -92,7 +92,48 @@
"Bash(rustfmt --check --edition 2021 src/git.rs)",
"Bash(awk *)",
"Bash(rustfmt --edition 2021 /tmp/blame_chunk.rs)",
"Read(//tmp/**)"
"Read(//tmp/**)",
"Bash(identify src-tauri/icons/GitCat.ico)",
"Bash(python3 -c \"import PIL; print\\(PIL.__version__\\)\")",
"Bash(python3 *)",
"Bash(xxd -l 16 src-tauri/icons/GitCat.ico)",
"Bash(pkg-config --exists openssl)",
"Bash(sudo apt-get install -y libssl-dev pkg-config)",
"Bash(dpkg -L libssl3t64)",
"Bash(grep *)",
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.11.5/src/**)",
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.11.4/src/**)",
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.11.5/src/window/**)",
"WebSearch",
"Bash(node -p \"require\\('./package.json'\\).version\")",
"Bash(bash -n PKGBUILD)",
"Bash(makepkg --printsrcinfo)",
"Bash(npx tauri *)",
"Bash(echo \"---exit code:$?---\")",
"Bash(git -C /mnt/data/Development/GitLite status --short)",
"Bash(makepkg -f --noconfirm)",
"Bash(tar -tf gitty-2026.7.19-1-x86_64.pkg.tar.zst)",
"Bash(cp /mnt/data/Development/GitLite/PKGBUILD .)",
"Bash(sed -i 's/^pkgrel=1/pkgrel=3/' PKGBUILD)",
"Bash(node -e \"const fs=require\\('fs'\\); const version='2026.8.1'; const pkgbuild=fs.readFileSync\\('PKGBUILD','utf8'\\).replace\\(/^pkgver=.*\\\\$/m, 'pkgver='+version\\).replace\\(/^pkgrel=.*\\\\$/m, 'pkgrel=1'\\); fs.writeFileSync\\('PKGBUILD', pkgbuild\\);\")",
"Bash(rm PKGBUILD)",
"Bash(git -C /mnt/data/Development/GitLite stash)",
"Bash(git -C /mnt/data/Development/GitLite stash pop)",
"Bash(command -v docker)",
"Read(//home/christoph/.docker/**)",
"Bash(docker info *)",
"Bash(sudo -n test -f /root/.docker/config.json)",
"Bash(systemctl list-units *)",
"Bash(find / -maxdepth 4 -iname \"act_runner*\")",
"Bash(find / -maxdepth 6 -iname \"config.yaml\" -path \"*runner*\")",
"Read(//home/**)",
"Bash(getent passwd *)",
"Bash(docker context *)",
"Bash(git -C /mnt/data/Development/GitLite log --oneline -1 -- PKGBUILD)",
"Bash(git -C /mnt/data/Development/GitLite show --stat HEAD -- PKGBUILD)",
"Bash(git -C /mnt/data/Development/GitLite log --oneline -10)",
"Bash(git -C /mnt/data/Development/GitLite status)",
"Bash(git *)"
]
}
}
+412 -38
View File
@@ -5,7 +5,8 @@ on:
types: [published]
jobs:
publish-tauri:
publish-windows:
name: Build and publish Windows installer
permissions:
contents: write
environment: production
@@ -30,25 +31,7 @@ jobs:
run:
working-directory: GitLite
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
# - platform: 'macos-latest'
# args: '--target aarch64-apple-darwin'
# - platform: 'macos-latest'
# args: '--target x86_64-apple-darwin'
- platform: "windows-latest"
bundles: "nsis"
updater_platform: "windows-x86_64"
no_strip: ""
- platform: "ubuntu-22.04"
bundles: "appimage"
updater_platform: "linux-x86_64"
no_strip: "1"
runs-on: ${{ matrix.platform }}
runs-on: windows-latest
steps:
# cicd_tool/main.py expects the repo at <workspace>/GitLite, so check out there.
@@ -56,25 +39,11 @@ jobs:
with:
path: GitLite
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y build-essential curl wget file libssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libxdo-dev libfuse2
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install uv
if: matrix.platform == 'ubuntu-22.04'
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
- name: install rust toolchain
if: matrix.platform == 'ubuntu-22.04'
uses: dtolnay/rust-toolchain@stable
- name: install frontend dependencies
run: npm ci
@@ -88,7 +57,6 @@ jobs:
node -e "const fs=require('fs'); const path='src-tauri/tauri.conf.json'; const config=JSON.parse(fs.readFileSync(path,'utf8')); config.version=process.env.RELEASE_VERSION; fs.writeFileSync(path, JSON.stringify(config,null,2)+'\n');"
- name: Commit and Push Changes
if: matrix.platform == 'windows-latest'
shell: powershell
run: |
git config user.name '${{ vars.USERNAME_GIT }}'
@@ -139,15 +107,421 @@ jobs:
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}}
NO_STRIP: ${{ matrix.no_strip }}
run: npm run tauri -- build --bundles ${{ matrix.bundles }}
NO_STRIP: ""
run: npm run tauri -- build --bundles nsis
# ----------------------
# Upload to MinIO (via mc) and create latest.json
# ----------------------
- name: Upload artifacts to MinIO with cicd_tool
working-directory: GitLite/cicd_tool
env:
PUBLISH_PLATFORM: ${{ matrix.updater_platform }}
PUBLISH_PLATFORM: windows-x86_64
run: |
uv sync
uv run main.py
publish-arch:
name: Build and publish AUR packages
needs: publish-ubuntu
permissions:
contents: write
runs-on: archlinux
environment: production
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
GITEA_API: https://git.cbsk-tech.de/api/v1
OWNER: Christoph
REPO: GitLite
GITEA_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
GITEA_FALLBACK_TOKEN: ${{ github.token }}
defaults:
run:
working-directory: GitLite
steps:
- name: Prepare Arch job container
working-directory: /
run: |
set -euo pipefail
# The runner image may contain only a single geo mirror. Seed several
# currently active HTTPS mirrors and retain the image's list as a
# fallback so one slow CDN cannot fail the complete release.
ARCH_MIRROR_BACKUP="$(mktemp)"
cp /etc/pacman.d/mirrorlist "$ARCH_MIRROR_BACKUP"
printf '%s\n' \
'Server = https://frankfurt.mirror.pkgbuild.com/$repo/os/$arch' \
'Server = https://mirror.osbeck.com/archlinux/$repo/os/$arch' \
'Server = https://mirror.ubrco.de/archlinux/$repo/os/$arch' \
'Server = https://ftp.halifax.rwth-aachen.de/archlinux/$repo/os/$arch' \
'Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch' \
> /etc/pacman.d/mirrorlist
sed -n '/^[[:space:]]*Server[[:space:]]*=/p' "$ARCH_MIRROR_BACKUP" \
>> /etc/pacman.d/mirrorlist
ARCH_PACKAGES=(
base-devel curl git git-lfs nodejs npm openssh rust
webkit2gtk-4.1 gtk3 hicolor-icon-theme
libappindicator-gtk3 librsvg xdotool
)
for ARCH_INSTALL_ATTEMPT in 1 2 3; do
echo "pacman attempt $ARCH_INSTALL_ATTEMPT of 3"
timeout --signal=TERM 30m \
pacman -Syu --needed --noconfirm --noprogressbar \
"${ARCH_PACKAGES[@]}" \
</dev/null &
ARCH_PACMAN_PID=$!
(
ARCH_WAIT_SECONDS=0
while true; do
sleep 30
((ARCH_WAIT_SECONDS += 30))
echo "pacman is still running (${ARCH_WAIT_SECONDS}s elapsed)"
done
) &
ARCH_HEARTBEAT_PID=$!
ARCH_PACMAN_STATUS=0
wait "$ARCH_PACMAN_PID" || ARCH_PACMAN_STATUS=$?
kill "$ARCH_HEARTBEAT_PID" 2>/dev/null || true
wait "$ARCH_HEARTBEAT_PID" 2>/dev/null || true
if [ "$ARCH_PACMAN_STATUS" -eq 0 ]; then
break
fi
if [ "$ARCH_PACMAN_STATUS" -eq 124 ]; then
echo "pacman attempt timed out after 30 minutes" >&2
fi
if [ "$ARCH_INSTALL_ATTEMPT" -eq 3 ]; then
echo "pacman failed after 3 attempts" >&2
exit 1
fi
sleep "$((ARCH_INSTALL_ATTEMPT * 5))"
done
- uses: actions/checkout@v5
with:
path: GitLite
- name: Set release version
env:
RELEASE_VERSION: ${{ github.ref_name }}
run: |
npm version "$RELEASE_VERSION" --no-git-tag-version --allow-same-version
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> "$GITHUB_ENV"
echo "RELEASE_TAG=$RELEASE_VERSION" >> "$GITHUB_ENV"
- name: Generate and build AUR package
run: |
if ! id -u builder >/dev/null 2>&1; then
useradd --create-home builder
fi
AUR_SOURCE_DIR="$(mktemp -d)"
cp PKGBUILD "$AUR_SOURCE_DIR/PKGBUILD"
SOURCE_ARCHIVE="$AUR_SOURCE_DIR/gitty-desktop-$PACKAGE_VERSION.tar.gz"
curl --fail --location --silent --show-error \
--output "$SOURCE_ARCHIVE" \
"https://git.cbsk-tech.de/Christoph/GitLite/archive/$RELEASE_TAG.tar.gz"
CHECKSUM="$(sha256sum "$SOURCE_ARCHIVE" | cut -d ' ' -f 1)"
sed -i \
-e "s/^pkgver=.*/pkgver=$PACKAGE_VERSION/" \
-e "s/^pkgrel=.*/pkgrel=1/" \
-e "s/^_tag=.*/_tag=$RELEASE_TAG/" \
-e "s/^sha256sums=.*/sha256sums=('$CHECKSUM')/" \
"$AUR_SOURCE_DIR/PKGBUILD"
chown -R builder:builder "$AUR_SOURCE_DIR"
runuser -u builder -- \
bash -lc "cd '$AUR_SOURCE_DIR' && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO"
ARCH_PACKAGE_PATH="$(find "$AUR_SOURCE_DIR" -maxdepth 1 -type f -name 'gitty-desktop-*.pkg.tar.zst' -print -quit)"
if [ -z "$ARCH_PACKAGE_PATH" ]; then
echo "The native Arch package was not created" >&2
exit 1
fi
echo "AUR_SOURCE_DIR=$AUR_SOURCE_DIR" >> "$GITHUB_ENV"
echo "ARCH_PACKAGE_PATH=$ARCH_PACKAGE_PATH" >> "$GITHUB_ENV"
- name: Attach native Arch package to Gitea release
run: |
set -euo pipefail
TOKEN="${GITEA_TOKEN:-${GITEA_FALLBACK_TOKEN:-}}"
if [ -z "$TOKEN" ]; then
echo "A Gitea token is required to upload the native Arch package" >&2
exit 1
fi
RELEASE_JSON="$(curl --fail --location --silent --show-error \
--header "Authorization: token $TOKEN" \
"$GITEA_API/repos/$OWNER/$REPO/releases/tags/$RELEASE_TAG")"
export RELEASE_JSON
RELEASE_ID="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); if (!r.id) process.exit(1); process.stdout.write(String(r.id))")"
PACKAGE_NAME="${ARCH_PACKAGE_PATH##*/}"
export PACKAGE_NAME
if node -e "const r=JSON.parse(process.env.RELEASE_JSON); process.exit((r.assets || []).some(a => a.name === process.env.PACKAGE_NAME) ? 0 : 1)"; then
echo "Release asset $PACKAGE_NAME already exists; skipping upload."
else
curl --fail --location --silent --show-error \
--request POST \
--header "Authorization: token $TOKEN" \
--form "attachment=@$ARCH_PACKAGE_PATH;type=application/octet-stream" \
"$GITEA_API/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets?name=$PACKAGE_NAME"
echo
echo "Attached $PACKAGE_NAME to Gitea release $RELEASE_TAG."
fi
- name: Generate and build binary AUR package
run: |
set -euo pipefail
TOKEN="${GITEA_TOKEN:-${GITEA_FALLBACK_TOKEN:-}}"
if [ -z "$TOKEN" ]; then
echo "A Gitea token is required to locate the native Arch package" >&2
exit 1
fi
RELEASE_JSON="$(curl --fail --location --silent --show-error \
--header "Authorization: token $TOKEN" \
"$GITEA_API/repos/$OWNER/$REPO/releases/tags/$RELEASE_TAG")"
export RELEASE_JSON
PACKAGE_NAME="${ARCH_PACKAGE_PATH##*/}"
export PACKAGE_NAME
PACKAGE_URL="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); const a=(r.assets || []).find(a => a.name === process.env.PACKAGE_NAME); if (!a?.browser_download_url) process.exit(1); process.stdout.write(a.browser_download_url)")"
AUR_BIN_DIR="$(mktemp -d)"
cp PKGBUILD-bin "$AUR_BIN_DIR/PKGBUILD"
PACKAGE_PATH="$AUR_BIN_DIR/$PACKAGE_NAME"
curl --fail --location --silent --show-error \
--header "Authorization: token $TOKEN" \
--output "$PACKAGE_PATH" "$PACKAGE_URL"
PACKAGE_CHECKSUM="$(sha256sum "$PACKAGE_PATH" | cut -d ' ' -f 1)"
sed -i \
-e "s/^pkgver=.*/pkgver=$PACKAGE_VERSION/" \
-e "s/^pkgrel=.*/pkgrel=1/" \
-e "s|^_package=.*|_package=\"$PACKAGE_NAME\"|" \
-e "s|^_artifact_url=.*|_artifact_url=\"$PACKAGE_URL\"|" \
-e "s/^sha256sums=.*/sha256sums=('$PACKAGE_CHECKSUM')/" \
"$AUR_BIN_DIR/PKGBUILD"
chown -R builder:builder "$AUR_BIN_DIR"
runuser -u builder -- \
bash -lc "cd '$AUR_BIN_DIR' && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO"
ARCH_BIN_PACKAGE_PATH="$(find "$AUR_BIN_DIR" -maxdepth 1 -type f -name 'gitty-desktop-bin-*.pkg.tar.zst' -print -quit)"
if [ -z "$ARCH_BIN_PACKAGE_PATH" ]; then
echo "The binary Arch package was not created" >&2
exit 1
fi
echo "AUR_BIN_DIR=$AUR_BIN_DIR" >> "$GITHUB_ENV"
echo "ARCH_BIN_PACKAGE_PATH=$ARCH_BIN_PACKAGE_PATH" >> "$GITHUB_ENV"
- name: Attach binary Arch package to Gitea release
run: |
set -euo pipefail
TOKEN="${GITEA_TOKEN:-${GITEA_FALLBACK_TOKEN:-}}"
if [ -z "$TOKEN" ]; then
echo "A Gitea token is required to upload the Arch package" >&2
exit 1
fi
RELEASE_JSON="$(curl --fail --location --silent --show-error \
--header "Authorization: token $TOKEN" \
"$GITEA_API/repos/$OWNER/$REPO/releases/tags/$RELEASE_TAG")"
export RELEASE_JSON
RELEASE_ID="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); if (!r.id) process.exit(1); process.stdout.write(String(r.id))")"
PACKAGE_NAME="${ARCH_BIN_PACKAGE_PATH##*/}"
export PACKAGE_NAME
if node -e "const r=JSON.parse(process.env.RELEASE_JSON); process.exit((r.assets || []).some(a => a.name === process.env.PACKAGE_NAME) ? 0 : 1)"; then
echo "Release asset $PACKAGE_NAME already exists; skipping upload."
else
curl --fail --location --silent --show-error \
--request POST \
--header "Authorization: token $TOKEN" \
--form "attachment=@$ARCH_BIN_PACKAGE_PATH;type=application/octet-stream" \
"$GITEA_API/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets?name=$PACKAGE_NAME"
echo
echo "Attached $PACKAGE_NAME to Gitea release $RELEASE_TAG."
fi
- name: Publish PKGBUILD to AUR
env:
AUR_GIT_NAME: ${{ vars.USERNAME_GIT }}
AUR_GIT_EMAIL: ${{ vars.EMAIL_GIT }}
run: |
set -euo pipefail
if [ -z "$AUR_SSH_PRIVATE_KEY" ]; then
echo "AUR_SSH_PRIVATE_KEY is required" >&2
exit 1
fi
install -d -m 0700 "$HOME/.ssh"
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$HOME/.ssh/aur"
chmod 0600 "$HOME/.ssh/aur"
AUR_RETRY_DELAYS=(30 60 120 240)
AUR_KNOWN_HOSTS_TEMP="$(mktemp)"
AUR_HOST_KEY_READY=0
for AUR_ATTEMPT in 1 2 3 4 5; do
echo "AUR host-key attempt $AUR_ATTEMPT of 5"
if timeout 30s ssh-keyscan -T 15 -H aur.archlinux.org > "$AUR_KNOWN_HOSTS_TEMP" \
&& [ -s "$AUR_KNOWN_HOSTS_TEMP" ]; then
AUR_HOST_KEY_READY=1
break
fi
if [ "$AUR_ATTEMPT" -lt 5 ]; then
sleep "${AUR_RETRY_DELAYS[$((AUR_ATTEMPT - 1))]}"
fi
done
if [ "$AUR_HOST_KEY_READY" -ne 1 ]; then
echo "Could not retrieve the AUR SSH host key after 5 attempts" >&2
exit 1
fi
install -m 0600 "$AUR_KNOWN_HOSTS_TEMP" "$HOME/.ssh/known_hosts"
export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/aur -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=2"
publish_aur_package() {
local package_name="$1"
local package_source_dir="$2"
local checkout_root checkout clone_candidate pushed
checkout_root="$(mktemp -d)"
checkout=""
for AUR_ATTEMPT in 1 2 3 4 5; do
clone_candidate="$checkout_root/attempt-$AUR_ATTEMPT"
echo "$package_name clone attempt $AUR_ATTEMPT of 5"
if timeout 3m git -c init.defaultBranch=master clone \
"ssh://aur@aur.archlinux.org/$package_name.git" "$clone_candidate"; then
checkout="$clone_candidate"
break
fi
if [ "$AUR_ATTEMPT" -lt 5 ]; then
echo "AUR is unavailable; retrying after backoff"
sleep "${AUR_RETRY_DELAYS[$((AUR_ATTEMPT - 1))]}"
fi
done
if [ -z "$checkout" ]; then
echo "Could not clone $package_name after 5 attempts" >&2
return 1
fi
cp "$package_source_dir/PKGBUILD" "$package_source_dir/.SRCINFO" "$checkout/"
git -C "$checkout" config user.name "${AUR_GIT_NAME:-Gitty Release Bot}"
git -C "$checkout" config user.email "${AUR_GIT_EMAIL:-aur@localhost}"
git -C "$checkout" add PKGBUILD .SRCINFO
if git -C "$checkout" diff --cached --quiet; then
echo "$package_name already matches release $PACKAGE_VERSION"
return 0
fi
git -C "$checkout" commit -m "Update to $PACKAGE_VERSION"
pushed=0
for AUR_ATTEMPT in 1 2 3 4 5; do
echo "$package_name push attempt $AUR_ATTEMPT of 5"
if timeout 3m git -C "$checkout" push origin HEAD:master; then
pushed=1
break
fi
if [ "$AUR_ATTEMPT" -lt 5 ]; then
echo "AUR push failed; retrying after backoff"
sleep "${AUR_RETRY_DELAYS[$((AUR_ATTEMPT - 1))]}"
fi
done
if [ "$pushed" -ne 1 ]; then
echo "Could not publish $package_name after 5 attempts" >&2
return 1
fi
}
publish_aur_package gitty-desktop "$AUR_SOURCE_DIR"
publish_aur_package gitty-desktop-bin "$AUR_BIN_DIR"
publish-ubuntu:
name: Build and publish Ubuntu AppImage
permissions:
contents: write
environment: production
env:
GITEA_API: https://git.cbsk-tech.de/api/v1
OWNER: Christoph
REPO: GitLite
GITEA_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
GITEA_FALLBACK_TOKEN: ${{ github.token }}
MINIO_ENDPOINT: ${{ vars.MINIO_ENDPOINT }}
S3_BUCKET: ${{ vars.S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
ARTIFACT_BASE_URL: ${{ vars.ARTIFACT_BASE_URL }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
defaults:
run:
working-directory: GitLite
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
with:
path: GitLite
- name: Install Ubuntu dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential curl wget file git-lfs libssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libxdo-dev libfuse2
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install frontend dependencies
run: npm ci
- name: Update package.json version
run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version
- name: Update tauri.conf.json version
env:
RELEASE_VERSION: ${{ github.ref_name }}
run: |
node -e "const fs=require('fs'); const path='src-tauri/tauri.conf.json'; const config=JSON.parse(fs.readFileSync(path,'utf8')); config.version=process.env.RELEASE_VERSION; fs.writeFileSync(path, JSON.stringify(config,null,2)+'\n');"
- name: Build Tauri App
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}}
NO_STRIP: "1"
run: npm run tauri -- build --bundles appimage
- name: Upload artifacts to MinIO with cicd_tool
working-directory: GitLite/cicd_tool
env:
PUBLISH_PLATFORM: linux-x86_64
run: |
uv sync
uv run main.py
+6
View File
@@ -1,9 +1,15 @@
/node_modules
/dist
/src-tauri/target
/src-tauri/binaries/git-lfs
/src-tauri/binaries/git-lfs.exe
/src-tauri/binaries/git-lfs-*-*-*
*.log
.idea
.DS_Store
~
.codex*
target
*.pkg.tar.zst
pkg
+251
View File
@@ -0,0 +1,251 @@
# Changelog
All notable user-facing changes to Gitty are documented in this file.
The project uses calendar-style versions in the form `YYYY.M.PATCH`.
## [2026.8.6] - 2026-08-18
### Changed
- Activating LFS or adding a tracking pattern now ensures that a root
`.gitattributes` file is not hidden by Git ignore rules. When necessary,
Gitty adds the scoped `!/.gitattributes` exception to `.gitignore`.
- The standard Tauri development launcher now removes an injected non-routing
`127.0.0.1:9` proxy and blocking SSH placeholder from the debug child
process, while preserving real user and company proxy settings.
### Fixed
- Root `.gitattributes` LFS patterns remain visible while the file is
untracked or ignored, instead of disappearing from the LFS dialog.
- Git LFS pushes rejected with HTTP 413 are retried once with a command-scoped
HTTP/1.1 override, which works around Azure DevOps' large HTTP/2 upload
behavior without changing repository or global Git settings.
- LFS and other generic push failures no longer trigger the unrelated
Pull/Push retry flow intended only for non-fast-forward rejections.
## [2026.8.5] - 2026-08-17
### Added
- Integrated Git LFS management now detects the available extension and the
repository setup, shows tracked patterns and checkout objects, and provides
actions for local activation, tracking and untracking patterns, downloading
objects, and pruning the local cache. Desktop installers bundle Git LFS,
while Arch packages declare it as a dependency.
- The Changes panel now offers a List/Tree switch. Tree view groups staged and
unstaged files into independently collapsible folders while retaining all
existing file actions.
- Files and folders in the Changes panel now have context-menu actions for
staging or unstaging their scope and for creating a stash containing only
the selected file or folder. New and untracked items can also be added to
the repository `.gitignore` from Changes or the File Explorer as an exact
file, a complete folder, or an extension-wide pattern. Folder rules are only
offered for folder selections. Tracked files and folders can be removed from
the Git index without deleting their working-tree contents.
- Gitty can open a repository directly at startup through the `--repo PATH` or
`--repo=PATH` command-line argument.
- The executable can clone and immediately open a repository with
`clone REMOTE TARGET`, `--clone REMOTE TARGET`, or `--clone=REMOTE TARGET`.
Relative targets use the caller's working directory, and requests are also
forwarded to an already-running Gitty instance.
### Changed
- Successful clones and pulls now detect repositories that use LFS and
download the required LFS objects automatically with the same remote and
credentials. Fresh clones also activate LFS locally before they are opened.
- Unstaged and staged changes use an equal-width side-by-side layout with
independent scrolling, directional stage/unstage actions, and a responsive
vertical fallback. The List/Tree switch is centered above both areas.
- File and folder context menus now separate the selected name from its parent
path, show the affected file count, and present stage, unstage, and stash
actions with clearer icons and descriptions.
- Controls and surfaces use a more consistent square visual language while
circular status markers, avatars, and branch shapes remain recognizable.
- The built-in German and English help now includes Git LFS setup, tracking,
troubleshooting, and everyday workflow guidance.
## [2026.8.4] - 2026-08-15
### Added
- Branch folders now have a context-menu action for deleting all contained
local or remote branches at once. The top-level remote folder such as
`origin` is protected, while its nested folders remain manageable. The
currently checked-out branch is kept,
and individual failures are reported after the remaining branches have been
processed.
### Changed
- The Compare selector is fully localized in German and uses the same visual
language as the external-tool selectors for its fields, groups, typography,
and dialog surfaces.
- Repository-tab close buttons are square and have clearer spacing, hover
behavior, and keyboard-focus feedback.
## [2026.8.3] - 2026-08-13
### Added
- Configurable external tools for editors, diff viewers, merge tools,
terminals, and file managers, including automatic cross-platform discovery
and presets for VS Code, JetBrains IDEs, Beyond Compare, and other common
applications.
- Repository and file actions for opening content in the configured external
application. Supported tools open in a separate window.
- A choice between Gitty's internal diff/merge views and the configured
external applications.
- Git Notes support for attaching editable notes to commits without rewriting
commit history, including fetch and push synchronization.
- A command palette for quickly opening repository actions, files, and commits.
- Complete branch-to-branch comparisons for local and remote branches. The
comparison dialog shows every changed file and its side-by-side diff.
- Safe remote branch renaming from the branch context menu.
### Changed
- Redesigned the settings window with tool categories, detected applications,
preset dropdowns, and clearer explanations of where each tool is used.
- Redesigned the history graph's branch presentation with compact labels,
hover details, cleaner flag connectors, and branch visibility controls.
- Reduced the minimum width of the commit history panel so the workspace can
be resized more freely.
- Local-only branches are now identified consistently in the toolbar,
repository summary, status bar, and commit graph. Their first push is labeled
Publish and configures the remote tracking branch automatically.
- Git operations now run asynchronously to keep the application responsive
during slower repository commands.
### Fixed
- Closing supported external tools no longer reports their documented
comparison result codes as application errors.
- External tools that otherwise reuse an existing process are explicitly
opened in a new window where supported.
- Remote branch renaming uses an atomic push with lease checks, preventing an
existing destination branch or a newly changed remote branch from being
overwritten.
## [2026.8.2] - 2026-08-10
### Changed
- History graph colors remain stable across parent lanes, making longer and
branching histories easier to follow.
- Release artifacts are published to the matching Gitea release automatically
without creating duplicate assets.
- Application shutdown now completes telemetry cleanup more reliably.
## [2026.8.1] - 2026-08-04
### Added
- Paginated commit history that loads older commits on demand instead of
limiting the visible repository history to the initial page.
- A dedicated file-history dialog opened from the explorer context menu.
- Windows and Ubuntu release publishing plus improved AUR packaging workflows.
### Changed
- File history moved out of the permanent workspace panel into a focused,
larger dialog.
- Dialogs close more consistently with the Escape key.
- Arch Linux installation documentation now uses the `gitty-desktop` AUR
package.
### Fixed
- AUR SSH setup, package installation timeouts, and clone/push retries are more
robust in the release workflow.
## [2026.07.22] - 2026-07-22
### Added
- AI-assisted commit splitting for staged changes:
- analyze the staged diff and propose an ordered set of logical commits;
- generate an editable Conventional Commit message for every group;
- move files between proposed commits before applying the plan;
- create all accepted commits sequentially with **Commit all**.
- Support for commit-splitting suggestions through OpenAI, Anthropic, and
custom OpenAI-compatible endpoints.
### Changed
- The commit-splitting dialog now explains why a plan cannot be applied and
clearly marks AI-generated commit messages as editable.
- Safety checks prevent applying a stale plan after the staged files change
and reject files that contain both staged and unstaged changes.
### Fixed
- **Commit all** now passes a plain state snapshot to the commit workflow
instead of failing silently when cloning a reactive UI proxy.
- Closing the active repository tab no longer allows an in-flight status or
fetch response to reopen and reactivate the closed repository.
## [2026.07.21] - 2026-07-21
### Added
- Full Git worktree management:
- create a worktree from an existing branch;
- create a new branch and worktree together;
- create a detached worktree at a commit;
- open a worktree as a repository tab;
- move, lock, unlock, repair, remove, and prune worktrees.
- A dedicated **Worktrees** entry below **Tags** in the branch panel.
- An **Open in new worktree** action in the local branch context menu.
- Line-level patch selection with Shift-click range selection.
- Stage, unstage, and discard actions for selected lines while retaining the
existing whole-file and whole-hunk workflows.
- Arch Linux package automation that builds from `PKGBUILD`, publishes the
`.pkg.tar.zst` artifact, and updates the Pacman repository database on the
CDN.
- Detailed German and English help for worktrees, line-level staging, Pacman
installation, and release changes.
### Changed
- Branch deletion now uses a clearer confirmation dialog with more explicit
branch and upstream information.
- Dialog backdrops blur the application instead of covering it with a nearly
black background.
- Remote operations and authentication errors provide more actionable
feedback.
- Build diagnostics and resource handling for the Arch package pipeline are
more robust.
### Infrastructure
- Added centralized error notifications.
- Added structured, privacy-conscious telemetry.
- Stabilized the Arch Linux container build and `makepkg` workflow.
## [2026.7.20] - 2026-07-13
### Added
- AI-assisted review of staged changes before committing.
- Optional automatic repository refresh.
- Comprehensive German Git documentation in the built-in help.
- Initial `PKGBUILD` and automated package version synchronization.
### Changed
- Redesigned the repository action toolbar and workspace status presentation.
- Improved repository tabs, diff styling, spacing, and panel structure.
- Improved file-history state handling.
[2026.07.22]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.22
[2026.07.21]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.21
[2026.7.20]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.20
[2026.8.6]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.6
[2026.8.5]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.5
[2026.8.4]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.4
[2026.8.3]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.3
[2026.8.2]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.2
[2026.8.1]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.1
+69
View File
@@ -0,0 +1,69 @@
# Maintainer: Christoph Brandau <c.brandau91@googlemail.com>
pkgname=gitty-desktop
pkgver=2026.7.22
pkgrel=1
pkgdesc="A lightweight, modern Git client built with Tauri"
arch=('x86_64')
url="https://git.cbsk-tech.de/Christoph/GitLite"
license=('MIT')
depends=('webkit2gtk-4.1' 'gtk3' 'git' 'git-lfs' 'hicolor-icon-theme' 'libappindicator-gtk3' 'librsvg' 'xdotool')
makedepends=('rust' 'nodejs' 'npm')
options=('!lto' '!debug')
_tag=2026.7.22
source=("gitty-desktop-${pkgver}.tar.gz::${url}/archive/${_tag}.tar.gz")
sha256sums=('bd6c7d9da54917cbea98ce31172934b4657cc2fe2020f49d27ea2acb51348e9c')
prepare() {
cd "$srcdir/gitlite"
# Keep application metadata aligned even when a release tag contains
# leading zeroes that npm normalizes (for example 2026.7.01 -> 2026.7.1).
npm version "$pkgver" --no-git-tag-version --allow-same-version
RELEASE_VERSION="$pkgver" node -e "const fs=require('fs'); const path='src-tauri/tauri.conf.json'; const config=JSON.parse(fs.readFileSync(path,'utf8')); config.version=process.env.RELEASE_VERSION; fs.writeFileSync(path,JSON.stringify(config,null,2)+'\n');"
}
build() {
cd "$srcdir/gitlite"
# Keep the large Rust release build within the Arch runner's memory limit.
export CARGO_BUILD_JOBS=1
export CARGO_PROFILE_RELEASE_OPT_LEVEL=2
export CARGO_PROFILE_RELEASE_LTO=false
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8
export CARGO_PROFILE_RELEASE_DEBUG=0
npm ci
npm run tauri -- build --no-bundle
}
package() {
cd "$srcdir/gitlite"
install -Dm755 "src-tauri/target/release/gitty" "$pkgdir/usr/bin/gitty-desktop"
install -Dm644 "src-tauri/icons/32x32.png" \
"$pkgdir/usr/share/icons/hicolor/32x32/apps/gitty-desktop.png"
install -Dm644 "src-tauri/icons/128x128.png" \
"$pkgdir/usr/share/icons/hicolor/128x128/apps/gitty-desktop.png"
install -Dm644 "src-tauri/icons/128x128@2x.png" \
"$pkgdir/usr/share/icons/hicolor/256x256@2/apps/gitty-desktop.png"
install -Dm644 "src-tauri/icons/icon.png" \
"$pkgdir/usr/share/icons/hicolor/512x512/apps/gitty-desktop.png"
install -d "$pkgdir/usr/share/applications"
cat > "$pkgdir/usr/share/applications/gitty-desktop.desktop" <<-EOF
[Desktop Entry]
Type=Application
Name=Gitty
Comment=$pkgdesc
Exec=gitty-desktop
Icon=gitty-desktop
Terminal=false
Categories=Development;RevisionControl;
StartupWMClass=gitty
EOF
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
}
+25
View File
@@ -0,0 +1,25 @@
# Maintainer: Christoph Brandau <c.brandau91@googlemail.com>
pkgname=gitty-desktop-bin
pkgver=2026.8.6
pkgrel=1
pkgdesc="A lightweight, modern Git client built with Tauri (prebuilt Arch package)"
arch=('x86_64')
url="https://git.cbsk-tech.de/Christoph/GitLite"
license=('MIT')
depends=('git' 'git-lfs' 'webkit2gtk-4.1' 'gtk3' 'hicolor-icon-theme' 'libappindicator-gtk3' 'librsvg' 'xdotool')
provides=('gitty-desktop')
conflicts=('gitty-desktop')
options=('!strip')
_package="gitty-desktop-${pkgver}-1-x86_64.pkg.tar.zst"
_artifact_url="https://git.cbsk-tech.de/Christoph/GitLite/releases/download/${pkgver}/${_package}"
source=("${_package}::${_artifact_url}")
noextract=("${_package}")
sha256sums=('SKIP')
package() {
# Extract only the native package payload, without carrying its package
# metadata (.PKGINFO, .BUILDINFO and .MTREE) across.
bsdtar -xf "$srcdir/$_package" -C "$pkgdir" usr
}
+139
View File
@@ -1 +1,140 @@
<div align="center">
<img src="public/splash-icon.png" alt="Gitty Logo" width="360">
# Gitty
**A lightweight, modern Git client built with Tauri.**
Fast, simple, and designed for developers who want a clean Git experience without unnecessary complexity.
</div>
---
## ✨ Features
- 🚀 Fast native performance
- 🌿 Branch management
- 📝 Commit history
- 🔄 Pull, Push & Fetch
- 🔀 Merge & Rebase
- 📦 Repository management
- 🗄️ Git LFS detection, tracking and object management
- 🎨 Modern and intuitive UI
---
## 📸 Preview
> Screenshots coming soon.
---
## 🛠️ Built With
- **Tauri**
- **Rust**
- **Svelte**
---
## 🚧 Status
Gitty is currently under active development.
Contributions, feedback and feature requests are always welcome!
---
## 📄 License
MIT License
---
## Arch Linux
Install Gitty from the AUR with an AUR helper:
```bash
yay -S gitty-desktop
```
To install the prebuilt native Arch package instead of compiling from source:
```bash
yay -S gitty-desktop-bin
```
Or build the AUR package manually:
```bash
git clone https://aur.archlinux.org/gitty-desktop.git
cd gitty-desktop
makepkg -si
```
The source recipe downloads the public Gitea release archive and builds Gitty.
The `-bin` recipe repackages the native `.pkg.tar.zst` release artifact. The
release pipeline updates both packages' versions, checksums, and `.SRCINFO`
files.
---
## Command line
Open an existing repository when Gitty starts:
```text
gitty.exe --repo "D:\Projects\ExistingRepo"
```
Clone a remote into an exact local target folder and open it immediately:
```text
gitty.exe --clone "https://example.com/team/project.git" "D:\Projects\Project"
gitty clone "git@example.com:team/project.git" "D:\Projects\Project"
```
`--clone=<REMOTE>` is also accepted. Relative target paths are resolved from
the current working directory. Clone and repository requests are forwarded to
the running window when Gitty is already open.
---
## Development
Start the complete desktop application in development mode with:
```bash
npm run tauri:dev
```
The development launcher preserves normal proxy settings. If a sandboxed
development terminal injects the non-routing `127.0.0.1:9` proxy or its
blocking SSH placeholder, those values are removed only for the Tauri child
process so Git remotes and Git LFS remain testable in the debug application.
---
## Git LFS
Gitty bundles the `git-lfs` executable in its desktop installers and checks it
at runtime before offering LFS actions. Arch packages also declare `git-lfs` as
a dependency so Git hooks and command-line workflows outside Gitty use the same
extension. The repository toolbar exposes LFS setup, tracked patterns, object
downloads, and safe cache pruning. After every successful clone or pull, Gitty
detects LFS usage and automatically downloads the required LFS objects with the
same remote and credentials, so no second pull is needed. Fresh clones also get
repository-local LFS filters and the pre-push hook before they are opened.
---
## SigNoz telemetry
After the user opts in, Gitty sends privacy-filtered product events and technical errors as OTLP/HTTP JSON logs. Every Git/Tauri command is also captured as a trace span with its command name, duration and success state, but without command arguments or results. CPU utilization and resident memory usage of the Gitty process are sampled every 30 seconds and exported as OpenTelemetry gauges. The default endpoints are `https://telemetry.cbsk-tech.de/v1/logs`, `https://telemetry.cbsk-tech.de/v1/traces`, and `https://telemetry.cbsk-tech.de/v1/metrics`.
The SigNoz reverse proxy must forward all three paths to the Collector's OTLP/HTTP receiver (normally port `4318`). For development or a separate collector hostname, set `GITTY_OTLP_LOGS_ENDPOINT`, `GITTY_OTLP_TRACES_ENDPOINT`, and `GITTY_OTLP_METRICS_ENDPOINT` to the full signal URLs before starting Gitty.
Telemetry is non-blocking, bounded, disabled until consent is stored, and never includes repository paths, remote URLs, branch names, commit messages, diffs, file names, credentials, source code, URLs, or email addresses.
+156 -32
View File
@@ -1,17 +1,21 @@
import argparse
import io
import json
import mimetypes
import os
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable, List, Optional
from typing import Any, Iterable, List, NamedTuple, Optional
from urllib.parse import urlparse
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
from minio import Minio
from minio.error import S3Error
# cicd_tool lives inside the GitLite repo, so its parent dir is the app root.
# cicd_tool lives inside the Gitty repo, so its parent dir is the app root.
PROJECT_DIR = Path(__file__).resolve().parent.parent
ARTIFACT_ROOT = PROJECT_DIR / "src-tauri" / "target" / "release" / "bundle"
@@ -20,6 +24,11 @@ class ConfigurationError(RuntimeError):
"""Raised when required configuration values are missing or invalid."""
class PublishTarget(NamedTuple):
bucket_name: str
app_dir: str
def _resolve_version(version_arg: Optional[str]) -> str:
"""Resolve the version to publish, falling back to tauri.conf.json."""
if version_arg and version_arg.lower() not in {"auto"}:
@@ -259,6 +268,130 @@ def _build_latest_payload(
}
def _parse_publish_targets(primary_bucket: str, primary_app_dir: str) -> list[PublishTarget]:
targets = [PublishTarget(primary_bucket, primary_app_dir)]
raw_legacy_targets = os.environ.get("LEGACY_UPDATE_TARGETS", "gitlite:gitlite")
for raw_target in raw_legacy_targets.split(","):
value = raw_target.strip()
if not value:
continue
if ":" in value:
bucket_name, app_dir = value.split(":", 1)
else:
bucket_name = value
app_dir = value
target = PublishTarget(bucket_name.strip().lower(), app_dir.strip("/"))
if target.bucket_name and target not in targets:
targets.append(target)
return targets
def _publish_target(
client: Minio,
target: PublishTarget,
version: str,
platform: str,
artifacts: Iterable[Path],
primary_artifact: Path,
signature: str,
artifact_base_url: str,
) -> None:
_ensure_bucket(client, target.bucket_name)
existing_latest_json = _read_existing_latest_json(client, target.bucket_name)
latest_payload = _build_latest_payload(
version=version,
platform=platform,
primary_artifact=primary_artifact,
signature=signature,
artifact_base_url=artifact_base_url,
app_dir=target.app_dir,
existing_payload=existing_latest_json,
)
latest_json = json.dumps(latest_payload, separators=(",", ":"))
for artifact in artifacts:
object_segments = [part for part in (version, artifact.name) if part]
object_name = "/".join(object_segments)
client.fput_object(target.bucket_name, object_name, str(artifact))
print(f"Uploaded {artifact.name} -> s3://{target.bucket_name}/{object_name}")
latest_bytes = latest_json.encode("utf-8")
client.put_object(
bucket_name=target.bucket_name,
object_name="latest.json",
data=io.BytesIO(latest_bytes),
length=len(latest_bytes),
content_type="application/json",
)
print(
f"Uploaded latest.json for {platform} -> "
f"s3://{target.bucket_name}/latest.json"
)
def _gitea_request(url: str, token: str, data: Optional[bytes] = None, **headers: str):
request = Request(url, data=data, headers={"Authorization": f"token {token}", **headers})
return urlopen(request, timeout=120)
def _upload_gitea_release_asset(version: str, artifact: Path) -> None:
"""Attach the installer to the Gitea release that triggered this build."""
api_url = os.environ.get("GITEA_API", "").rstrip("/")
owner = os.environ.get("OWNER", "")
repo = os.environ.get("REPO", "")
token = os.environ.get("GITEA_TOKEN") or os.environ.get("GITEA_FALLBACK_TOKEN")
if not all((api_url, owner, repo, token)):
raise ConfigurationError(
"GITEA_API, OWNER, REPO and GITEA_TOKEN are required to publish "
"the release asset."
)
release_url = (
f"{api_url}/repos/{quote(owner, safe='')}/{quote(repo, safe='')}"
f"/releases/tags/{quote(version, safe='')}"
)
with _gitea_request(release_url, token) as response:
release = json.load(response)
existing_names = {
asset.get("name") for asset in release.get("assets", []) if isinstance(asset, dict)
}
if artifact.name in existing_names:
print(f"Release asset {artifact.name} already exists; skipping upload.")
return
boundary = f"----gitty-{uuid.uuid4().hex}"
content_type = mimetypes.guess_type(artifact.name)[0] or "application/octet-stream"
body = b"".join(
(
f"--{boundary}\r\n".encode(),
(
f'Content-Disposition: form-data; name="attachment"; '
f'filename="{artifact.name}"\r\n'
).encode(),
f"Content-Type: {content_type}\r\n\r\n".encode(),
artifact.read_bytes(),
f"\r\n--{boundary}--\r\n".encode(),
)
)
upload_url = (
f"{api_url}/repos/{quote(owner, safe='')}/{quote(repo, safe='')}"
f"/releases/{release['id']}/assets?{urlencode({'name': artifact.name})}"
)
with _gitea_request(
upload_url,
token,
data=body,
**{"Content-Type": f"multipart/form-data; boundary={boundary}"},
):
pass
print(f"Attached {artifact.name} to Gitea release {version}.")
def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
resolved_version = _resolve_version(version)
artifact_base_url = os.environ.get("ARTIFACT_BASE_URL", "")
@@ -266,9 +399,10 @@ def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
raise ConfigurationError("ARTIFACT_BASE_URL is required")
# S3/MinIO bucket names must be lowercase, so normalise regardless of how the
# S3_BUCKET variable is cased (e.g. "GitLite" -> "gitlite").
bucket_name = (os.environ.get("S3_BUCKET") or "gitlite").lower()
bucket_prefix = (app_dir or os.environ.get("APP_DIR") or "gitlite").strip("/")
# S3_BUCKET variable is cased (e.g. "Gitty" -> "gitty").
bucket_name = (os.environ.get("S3_BUCKET") or "gitty").lower()
bucket_prefix = (app_dir or os.environ.get("APP_DIR") or "gitty").strip("/")
publish_targets = _parse_publish_targets(bucket_name, bucket_prefix)
artifacts = _collect_artifacts()
if not artifacts:
@@ -287,9 +421,10 @@ def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
signature = _read_signature(primary)
client = _create_minio_client()
_ensure_bucket(client, bucket_name)
existing_latest_json = _read_existing_latest_json(client, bucket_name)
local_dist_dir = PROJECT_DIR / "dist"
local_dist_dir.mkdir(parents=True, exist_ok=True)
latest_json_path = local_dist_dir / "latest.json"
latest_payload = _build_latest_payload(
version=resolved_version,
platform=platform,
@@ -297,39 +432,28 @@ def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
signature=signature,
artifact_base_url=artifact_base_url,
app_dir=bucket_prefix,
existing_payload=existing_latest_json,
)
latest_json = json.dumps(latest_payload, separators=(",", ":"))
local_dist_dir = PROJECT_DIR / "dist"
local_dist_dir.mkdir(parents=True, exist_ok=True)
latest_json_path = local_dist_dir / "latest.json"
latest_json_path.write_text(latest_json + "\n", encoding="utf-8")
for artifact in artifacts:
object_segments = [part for part in (resolved_version, artifact.name) if part]
object_name = "/".join(object_segments)
client.fput_object(bucket_name, object_name, str(artifact))
print(f"Uploaded {artifact.name} -> s3://{bucket_name}/{object_name}")
for target in publish_targets:
_publish_target(
client=client,
target=target,
version=resolved_version,
platform=platform,
artifacts=artifacts,
primary_artifact=primary,
signature=signature,
artifact_base_url=artifact_base_url,
)
latest_object_name = "latest.json"
latest_bytes = latest_json.encode("utf-8")
client.put_object(
bucket_name=bucket_name,
object_name=latest_object_name,
data=io.BytesIO(latest_bytes),
length=len(latest_bytes),
content_type="application/json",
)
print(
f"Uploaded latest.json for {platform} -> "
f"s3://{bucket_name}/{latest_object_name}"
)
_upload_gitea_release_asset(resolved_version, primary)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Upload GitLite release artifacts to MinIO."
description="Upload Gitty release artifacts to MinIO."
)
parser.add_argument(
"-v",
@@ -340,7 +464,7 @@ if __name__ == "__main__":
parser.add_argument(
"--app-dir",
default=None,
help="App directory prefix inside the bucket (defaults to gitlite).",
help="App directory prefix inside the bucket (defaults to gitty).",
)
args = parser.parse_args()
+86 -4
View File
@@ -15,6 +15,9 @@ interface GitStatus {
behind: number;
files: GitFileStatus[];
clean: boolean;
rebase_in_progress: boolean;
cherry_pick_in_progress: boolean;
merge_in_progress: boolean;
}
interface GitFileStatus {
@@ -30,6 +33,24 @@ interface GitBranch {
remote: boolean;
}
interface GitWorktree {
path: string;
head: string | null;
short_head: string | null;
branch: string | null;
bare: boolean;
detached: boolean;
locked: boolean;
lock_reason: string | null;
prunable: boolean;
prune_reason: string | null;
missing: boolean;
is_main: boolean;
is_current: boolean;
clean: boolean;
changed_files: number;
}
interface GitCommit {
hash: string;
short_hash: string;
@@ -52,23 +73,84 @@ interface GitRepositoryFile {
tracked: boolean;
status: FileStatusKind | null;
}
interface GitLfsStatus {
available: boolean;
bundled: boolean;
version: string | null;
filters_configured: boolean;
hook_installed: boolean;
repository_uses_lfs: boolean;
patterns: GitLfsPattern[];
files: GitLfsFile[];
}
interface GitLfsPattern {
pattern: string;
source: string;
lockable: boolean;
tracked: boolean;
}
interface GitLfsFile {
name: string;
size: number;
checkout: boolean;
downloaded: boolean;
oid_type: string;
oid: string;
version: string;
}
```
## Commands
The command list below includes the repository-management and synchronization API. The TypeScript wrappers in `src/lib/git.ts` are the authoritative full list.
- `open_repository(path: string): Promise<GitStatus>`
- `init_repository(path: string, initialBranch?: string): Promise<GitStatus>`
- `clone_repository(...): Promise<RepositoryBundle>`
- `get_status(path: string): Promise<GitStatus>`
- `git_lfs_status(path: string): Promise<GitLfsStatus>`
- `git_lfs_install(path: string): Promise<GitLfsStatus>`
- `git_lfs_track(path: string, pattern: string, lockable?: boolean): Promise<GitLfsStatus>`
- `git_lfs_untrack(path: string, pattern: string): Promise<GitLfsStatus>`
- `git_lfs_pull(path: string, remote?: string, username?: string, password?: string): Promise<GitLfsStatus>`
- `git_lfs_prune(path: string): Promise<GitLfsStatus>`
- `pull(...)` automatically runs `git lfs pull` after a successful Git pull when
the repository contains LFS attributes or tracked LFS objects.
- `list_branches(path: string): Promise<GitBranch[]>`
- `list_remotes(path: string): Promise<GitRemote[]>`
- `add_remote(path: string, name: string, url: string): Promise<GitRemote[]>`
- `update_remote(path: string, name: string, url: string): Promise<GitRemote[]>`
- `remove_remote(path: string, name: string): Promise<GitRemote[]>`
- `set_branch_upstream(path: string, branch: string, upstream?: string): Promise<GitStatus>`
- `delete_remote_branch(path: string, remote: string, branch: string): Promise<GitStatus>`
- `checkout_branch(path: string, branch: string): Promise<GitStatus>`
- `list_worktrees(path: string): Promise<GitWorktree[]>`
- `add_worktree(path: string, worktreePath: string, ...): Promise<GitWorktree[]>`
- `remove_worktree(path: string, worktreePath: string, force?: boolean): Promise<GitWorktree[]>`
- `move_worktree(path: string, worktreePath: string, destination: string): Promise<GitWorktree[]>`
- `lock_worktree(path: string, worktreePath: string, reason?: string): Promise<GitWorktree[]>`
- `unlock_worktree(path: string, worktreePath: string): Promise<GitWorktree[]>`
- `prune_worktrees(path: string): Promise<GitWorktree[]>`
- `repair_worktree(path: string, worktreePath: string): Promise<GitWorktree[]>`
- `stage_files(path: string, files: string[]): Promise<GitStatus>`
- `unstage_files(path: string, files: string[]): Promise<GitStatus>`
- `add_to_gitignore(path: string, target: string, kind: "file" | "extension" | "folder"): Promise<GitStatus>`; appends a repository-root `.gitignore` rule and unstages newly-added matching files.
- `untrack_paths(path: string, targets: string[]): Promise<GitStatus>`; removes files or folders from the Git index while preserving their working-tree contents.
- `restore_files(path: string, files: string[], staged: boolean): Promise<GitStatus>`
- `stash_push(path: string, message?: string, includeUntracked?: boolean, paths?: string[]): Promise<GitStatus>`; when `paths` is provided, only matching files are stashed.
- `commit(path: string, message: string): Promise<GitStatus>`
- `pull(path: string): Promise<GitStatus>`
- `push(path: string): Promise<GitStatus>`
- `list_commits(path: string, limit?: number): Promise<GitCommit[]>`
- `fetch(path: string, prune?: boolean, remote?: string): Promise<GitStatus>`
- `pull(path: string, strategy?: "merge" | "rebase" | "ff-only", remote?: string, branch?: string): Promise<GitStatus>`
- `push(path: string, forceWithLease?: boolean, remote?: string): Promise<GitStatus>`
- `list_commits(path: string, limit?: number, skip?: number): Promise<GitCommit[]>`
- `restore_to_commit(path: string, commit: string): Promise<GitStatus>`
- `restore_file_from_commit(path: string, commit: string, file: string): Promise<GitStatus>` (the `file` argument can also be a folder path)
- `merge_branch(path: string, branch: string): Promise<GitStatus>`
- `merge_branch(path: string, branch: string, strategy?: "default" | "squash" | "ff-only" | "no-ff"): Promise<GitStatus>`
- `merge_continue(path: string): Promise<GitStatus>`
- `merge_abort(path: string): Promise<GitStatus>`
- `revert_commit(path: string, commit: string): Promise<GitStatus>`
- `list_repository_files(path: string): Promise<GitRepositoryFile[]>`
- `list_file_history(path: string, file: string, limit?: number): Promise<GitCommit[]>` (the `file` argument can also be a folder path)
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri Git Lite</title>
<title>Gitty</title>
</head>
<body>
<div id="app"></div>
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "git-lite",
"version": "2026.7.14",
"name": "gitty",
"version": "2026.8.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "git-lite",
"version": "2026.7.14",
"name": "gitty",
"version": "2026.8.7",
"dependencies": {
"@lucide/svelte": "^1.21.0",
"@tailwindcss/vite": "^4.3.1",
+5 -3
View File
@@ -1,14 +1,16 @@
{
"name": "git-lite",
"version": "2026.7.14",
"name": "gitty",
"version": "2026.8.7",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "vite build",
"prepare:lfs": "node scripts/prepare-git-lfs-sidecar.mjs",
"icons": "node scripts/generate-app-icons.mjs",
"preview": "vite preview --host 127.0.0.1",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:dev": "node scripts/tauri-dev.mjs",
"tauri:build": "tauri build",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

+241
View File
@@ -0,0 +1,241 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gitty</title>
<style>
:root {
color-scheme: dark;
--ink: #f3f5ff;
--muted: #9aa3c5;
--purple: #9b70ff;
--orange: #ff6d26;
--panel: rgba(15, 18, 31, 0.9);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050712;
}
body {
display: grid;
place-items: center;
background:
radial-gradient(circle at 50% 38%, rgba(137, 92, 255, 0.22), transparent 36%),
linear-gradient(135deg, #050712 0%, #080b16 48%, #140b18 100%);
}
.grid {
position: fixed;
inset: 0;
opacity: 0.28;
background-image:
linear-gradient(rgba(151, 118, 255, 0.12) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 109, 38, 0.08) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: radial-gradient(circle at center, black 0%, transparent 70%);
animation: grid-drift 10s linear infinite;
}
.splash {
position: relative;
display: grid;
justify-items: center;
gap: 16px;
width: 100vw;
height: 100vh;
align-content: center;
padding: 32px;
border: 0;
border-radius: 0;
background:
linear-gradient(180deg, rgba(23, 26, 43, 0.92), rgba(12, 15, 27, 0.94)),
var(--panel);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
overflow: hidden;
}
.mark {
position: relative;
display: grid;
place-items: center;
width: 174px;
height: 174px;
isolation: isolate;
}
.halo {
position: absolute;
inset: 10px;
border: 1px solid rgba(151, 118, 255, 0.25);
border-radius: 36px;
transform: rotate(45deg);
animation: halo-breathe 2.4s ease-in-out infinite;
}
.halo.secondary {
inset: 25px;
border-color: rgba(255, 109, 38, 0.28);
animation-direction: reverse;
}
.traces {
position: absolute;
inset: -18px;
width: 210px;
height: 210px;
overflow: visible;
}
.trace {
fill: none;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 165;
stroke-dashoffset: 165;
filter: drop-shadow(0 0 8px rgba(151, 118, 255, 0.55));
animation: trace-draw 2.6s ease-in-out infinite;
}
.trace.main {
stroke: var(--purple);
}
.trace.branch {
stroke: var(--orange);
animation-delay: 0.28s;
}
.trace.base {
stroke: rgba(255, 255, 255, 0.42);
stroke-dasharray: 128;
stroke-dashoffset: 128;
animation-delay: 0.55s;
}
.icon {
position: relative;
z-index: 1;
width: 136px;
height: 136px;
object-fit: contain;
filter:
drop-shadow(0 18px 24px rgba(0, 0, 0, 0.55))
drop-shadow(0 0 18px rgba(151, 118, 255, 0.3));
animation: icon-float 2.4s ease-in-out infinite;
}
.copy {
position: relative;
display: grid;
gap: 4px;
justify-items: center;
text-align: center;
}
.copy strong {
color: var(--ink);
font-size: 22px;
font-weight: 900;
line-height: 1;
}
.copy span {
color: var(--muted);
font-size: 12.5px;
font-weight: 700;
}
.bar {
position: relative;
width: 230px;
height: 4px;
margin-top: 2px;
overflow: hidden;
border-radius: 999px;
background: rgba(151, 118, 255, 0.14);
}
.bar span {
position: absolute;
inset: 0;
width: 46%;
border-radius: inherit;
background: linear-gradient(90deg, transparent, var(--purple) 40%, var(--orange) 74%, transparent);
box-shadow: 0 0 16px rgba(255, 109, 38, 0.32);
animation: bar-slide 1.35s ease-in-out infinite;
}
@keyframes grid-drift {
to { transform: translate3d(42px, 42px, 0); }
}
@keyframes icon-float {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-5px) scale(1.015); }
}
@keyframes halo-breathe {
0%, 100% { opacity: 0.35; transform: rotate(45deg) scale(0.95); }
50% { opacity: 0.8; transform: rotate(45deg) scale(1.04); }
}
@keyframes trace-draw {
0% { stroke-dashoffset: 165; opacity: 0; }
36% { opacity: 1; }
64%, 100% { stroke-dashoffset: 0; opacity: 0.72; }
}
@keyframes bar-slide {
0% { transform: translateX(-120%); }
100% { transform: translateX(320%); }
}
@media (prefers-reduced-motion: reduce) {
.grid,
.halo,
.trace,
.icon,
.bar span {
animation: none;
}
.trace {
stroke-dashoffset: 0;
opacity: 0.72;
}
}
</style>
</head>
<body>
<div class="grid" aria-hidden="true"></div>
<main class="splash" role="status" aria-live="polite">
<div class="mark" aria-hidden="true">
<span class="halo"></span>
<span class="halo secondary"></span>
<svg class="traces" viewBox="0 0 220 220">
<path class="trace main" d="M28 154 C72 114, 88 108, 110 110 S156 116, 192 68" />
<path class="trace branch" d="M62 74 C100 88, 126 124, 158 166" />
<path class="trace base" d="M46 180 L174 180" />
</svg>
<img class="icon" src="/splash-icon.png" alt="" />
</div>
<div class="copy">
<strong>Gitty</strong>
<span>Starting workspace</span>
</div>
<div class="bar" aria-hidden="true"><span></span></div>
</main>
</body>
</html>
+266
View File
@@ -0,0 +1,266 @@
import { Buffer } from "node:buffer";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { deflateSync, inflateSync } from "node:zlib";
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const iconsDir = join(root, "src-tauri", "icons");
const sourcePath = join(iconsDir, "GitCat_org.png");
const pngSizes = [32, 64, 128, 512];
const icoSizes = [16, 24, 32, 48, 64, 128, 256];
function decodePng(buffer) {
if (buffer.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") {
throw new Error("Source icon must be a PNG file.");
}
let width = 0;
let height = 0;
const idatChunks = [];
for (let offset = 8; offset < buffer.length; ) {
const length = buffer.readUInt32BE(offset);
const type = buffer.subarray(offset + 4, offset + 8).toString("ascii");
const dataStart = offset + 8;
const dataEnd = dataStart + length;
const data = buffer.subarray(dataStart, dataEnd);
if (type === "IHDR") {
width = data.readUInt32BE(0);
height = data.readUInt32BE(4);
if (data[8] !== 8 || data[9] !== 6) {
throw new Error("Source icon must be an 8-bit RGBA PNG.");
}
} else if (type === "IDAT") {
idatChunks.push(data);
} else if (type === "IEND") {
break;
}
offset = dataEnd + 4;
}
const stride = width * 4;
const raw = inflateSync(Buffer.concat(idatChunks));
const rgba = Buffer.alloc(width * height * 4);
for (let y = 0; y < height; y += 1) {
const rowStart = y * (stride + 1);
const filter = raw[rowStart];
const input = raw.subarray(rowStart + 1, rowStart + 1 + stride);
const priorOffset = (y - 1) * stride;
const outputOffset = y * stride;
for (let x = 0; x < stride; x += 1) {
const left = x >= 4 ? rgba[outputOffset + x - 4] : 0;
const up = y > 0 ? rgba[priorOffset + x] : 0;
const upLeft = y > 0 && x >= 4 ? rgba[priorOffset + x - 4] : 0;
let value = input[x];
if (filter === 1) value += left;
else if (filter === 2) value += up;
else if (filter === 3) value += Math.floor((left + up) / 2);
else if (filter === 4) value += paeth(left, up, upLeft);
else if (filter !== 0) throw new Error(`Unsupported PNG filter: ${filter}`);
rgba[outputOffset + x] = value & 0xff;
}
}
return { width, height, rgba };
}
function paeth(left, up, upLeft) {
const estimate = left + up - upLeft;
const distanceLeft = Math.abs(estimate - left);
const distanceUp = Math.abs(estimate - up);
const distanceUpLeft = Math.abs(estimate - upLeft);
if (distanceLeft <= distanceUp && distanceLeft <= distanceUpLeft) return left;
if (distanceUp <= distanceUpLeft) return up;
return upLeft;
}
function visibleSquare(image, alphaThreshold = 8) {
let minX = image.width;
let minY = image.height;
let maxX = -1;
let maxY = -1;
for (let y = 0; y < image.height; y += 1) {
for (let x = 0; x < image.width; x += 1) {
const alpha = image.rgba[(y * image.width + x) * 4 + 3];
if (alpha > alphaThreshold) {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
}
if (maxX < 0 || maxY < 0) {
throw new Error("Source icon has no visible pixels.");
}
const visibleWidth = maxX - minX + 1;
const visibleHeight = maxY - minY + 1;
const side = Math.max(visibleWidth, visibleHeight);
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
const left = Math.max(0, Math.min(image.width - side, Math.round(centerX - side / 2)));
const top = Math.max(0, Math.min(image.height - side, Math.round(centerY - side / 2)));
return { left, top, side };
}
function resizePng(image, crop, size) {
const rgba = Buffer.alloc(size * size * 4);
const scale = crop.side / size;
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) {
const sourceX = crop.left + (x + 0.5) * scale - 0.5;
const sourceY = crop.top + (y + 0.5) * scale - 0.5;
const pixel = sampleBilinear(image, sourceX, sourceY);
const offset = (y * size + x) * 4;
rgba[offset] = pixel.r;
rgba[offset + 1] = pixel.g;
rgba[offset + 2] = pixel.b;
rgba[offset + 3] = pixel.a;
}
}
return encodePng(size, size, rgba);
}
function sampleBilinear(image, x, y) {
const x0 = Math.max(0, Math.min(image.width - 1, Math.floor(x)));
const y0 = Math.max(0, Math.min(image.height - 1, Math.floor(y)));
const x1 = Math.max(0, Math.min(image.width - 1, x0 + 1));
const y1 = Math.max(0, Math.min(image.height - 1, y0 + 1));
const tx = x - x0;
const ty = y - y0;
const top = mixPixels(getPixel(image, x0, y0), getPixel(image, x1, y0), tx);
const bottom = mixPixels(getPixel(image, x0, y1), getPixel(image, x1, y1), tx);
return mixPixels(top, bottom, ty);
}
function getPixel(image, x, y) {
const offset = (y * image.width + x) * 4;
return {
r: image.rgba[offset],
g: image.rgba[offset + 1],
b: image.rgba[offset + 2],
a: image.rgba[offset + 3],
};
}
function mixPixels(a, b, t) {
return {
r: Math.round(a.r + (b.r - a.r) * t),
g: Math.round(a.g + (b.g - a.g) * t),
b: Math.round(a.b + (b.b - a.b) * t),
a: Math.round(a.a + (b.a - a.a) * t),
};
}
const crcTable = new Uint32Array(256);
for (let n = 0; n < 256; n += 1) {
let c = n;
for (let k = 0; k < 8; k += 1) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
crcTable[n] = c >>> 0;
}
function crc32(buffer) {
let crc = 0xffffffff;
for (const byte of buffer) {
crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
function chunk(type, payload) {
const typeBytes = Buffer.from(type);
const out = Buffer.alloc(12 + payload.length);
out.writeUInt32BE(payload.length, 0);
typeBytes.copy(out, 4);
payload.copy(out, 8);
out.writeUInt32BE(crc32(Buffer.concat([typeBytes, payload])), out.length - 4);
return out;
}
function encodePng(width, height, rgba) {
const header = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8;
ihdr[9] = 6;
const raw = Buffer.alloc((width * 4 + 1) * height);
for (let y = 0; y < height; y += 1) {
const rowStart = y * (width * 4 + 1);
raw[rowStart] = 0;
rgba.copy(raw, rowStart + 1, y * width * 4, (y + 1) * width * 4);
}
return Buffer.concat([
header,
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw, { level: 9 })),
chunk("IEND", Buffer.alloc(0)),
]);
}
function makeIco(images) {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(images.length, 4);
const entries = Buffer.alloc(images.length * 16);
let offset = 6 + images.length * 16;
images.forEach((image, index) => {
const entry = index * 16;
entries[entry] = image.size >= 256 ? 0 : image.size;
entries[entry + 1] = image.size >= 256 ? 0 : image.size;
entries[entry + 2] = 0;
entries[entry + 3] = 0;
entries.writeUInt16LE(1, entry + 4);
entries.writeUInt16LE(32, entry + 6);
entries.writeUInt32LE(image.png.length, entry + 8);
entries.writeUInt32LE(offset, entry + 12);
offset += image.png.length;
});
return Buffer.concat([header, entries, ...images.map((image) => image.png)]);
}
mkdirSync(iconsDir, { recursive: true });
const source = decodePng(readFileSync(sourcePath));
const crop = visibleSquare(source);
const rendered = new Map(
[...new Set([...pngSizes, ...icoSizes])].map((size) => [size, resizePng(source, crop, size)]),
);
writeFileSync(join(iconsDir, "32x32.png"), rendered.get(32));
writeFileSync(join(iconsDir, "64x64.png"), rendered.get(64));
writeFileSync(join(iconsDir, "128x128.png"), rendered.get(128));
writeFileSync(join(iconsDir, "icon.png"), rendered.get(512));
writeFileSync(join(iconsDir, "icon.ico"), makeIco(icoSizes.map((size) => ({ size, png: rendered.get(size) }))));
console.log(`Read ${join("src-tauri", "icons", "GitCat_org.png")}`);
console.log(`Cropped visible icon area to ${crop.side}x${crop.side} at ${crop.left},${crop.top}`);
console.log(`Wrote ${join("src-tauri", "icons", "32x32.png")}`);
console.log(`Wrote ${join("src-tauri", "icons", "64x64.png")}`);
console.log(`Wrote ${join("src-tauri", "icons", "128x128.png")}`);
console.log(`Wrote ${join("src-tauri", "icons", "icon.png")}`);
console.log(`Wrote ${join("src-tauri", "icons", "icon.ico")} (${icoSizes.length} sizes)`);
+4
View File
@@ -1,3 +1,7 @@
import "./generate-app-icons.mjs";
process.exit(0);
import { Buffer } from "node:buffer";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
+71
View File
@@ -0,0 +1,71 @@
import { copyFileSync, existsSync, mkdirSync, chmodSync, statSync } from "node:fs";
import { basename, delimiter, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(scriptDirectory, "..");
const binariesDirectory = join(projectRoot, "src-tauri", "binaries");
const extension = process.platform === "win32" ? ".exe" : "";
function defaultTargetTriple() {
const triples = {
"win32:x64": "x86_64-pc-windows-msvc",
"win32:arm64": "aarch64-pc-windows-msvc",
"linux:x64": "x86_64-unknown-linux-gnu",
"linux:arm64": "aarch64-unknown-linux-gnu",
"darwin:x64": "x86_64-apple-darwin",
"darwin:arm64": "aarch64-apple-darwin",
};
return triples[`${process.platform}:${process.arch}`];
}
function executableCandidates() {
const configured = process.env.GIT_LFS_BINARY?.trim();
if (configured) return [configured];
const pathEntries = (process.env.PATH ?? "")
.split(delimiter)
.filter(Boolean);
const candidates = pathEntries.map((entry) => join(entry, `git-lfs${extension}`));
if (process.platform === "win32") {
for (const entry of pathEntries) {
if (basename(entry).toLowerCase() === "cmd") {
candidates.push(resolve(entry, "..", "mingw64", "bin", "git-lfs.exe"));
}
}
}
return candidates;
}
const source = executableCandidates()
.filter((candidate) => existsSync(candidate))
// Git for Windows exposes a small launcher in cmd/ and the standalone
// executable in mingw64/bin/. The standalone file is the portable sidecar.
.sort((left, right) => statSync(right).size - statSync(left).size)[0];
if (!source) {
throw new Error(
"Git LFS was not found. Install git-lfs or set GIT_LFS_BINARY before building Gitty.",
);
}
const targetTriple = (
process.env.GITTY_TARGET_TRIPLE
?? process.env.TAURI_ENV_TARGET_TRIPLE
?? process.env.TARGET
?? defaultTargetTriple()
)?.trim();
if (!targetTriple) throw new Error("Rust did not report a target triple.");
mkdirSync(binariesDirectory, { recursive: true });
const developmentTarget = join(binariesDirectory, `git-lfs${extension}`);
const bundleTarget = join(
binariesDirectory,
`git-lfs-${targetTriple}${extension}`,
);
for (const target of [developmentTarget, bundleTarget]) {
if (resolve(source) !== resolve(target)) copyFileSync(source, target);
if (process.platform !== "win32") chmodSync(target, 0o755);
}
console.log(`Prepared bundled git-lfs from ${source} for ${targetTriple}.`);
+61
View File
@@ -0,0 +1,61 @@
import { spawn } from "node:child_process";
const debugEnvironment = { ...process.env };
const clearedVariables = [];
const blockedProxy = /^https?:\/\/127\.0\.0\.1:9\/?$/i;
const proxyVariables = new Set([
"all_proxy",
"http_proxy",
"https_proxy",
"git_http_proxy",
"git_https_proxy",
]);
for (const [name, value] of Object.entries(debugEnvironment)) {
if (proxyVariables.has(name.toLowerCase()) && blockedProxy.test(value ?? "")) {
delete debugEnvironment[name];
clearedVariables.push(name);
}
}
for (const [name, value] of Object.entries(debugEnvironment)) {
if (
name.toLowerCase() === "git_ssh_command"
&& /^cmd(?:\.exe)?\s+\/c\s+exit\s+1$/i.test((value ?? "").trim())
) {
delete debugEnvironment[name];
clearedVariables.push(name);
}
}
if (process.argv.includes("--check-environment")) {
process.stdout.write(
clearedVariables.length > 0
? `Debug environment ready; cleared: ${clearedVariables.sort().join(", ")}\n`
: "Debug environment ready; no blocked proxy variables found.\n",
);
process.exit(0);
}
if (clearedVariables.length > 0) {
process.stdout.write(
`Starting Gitty without the blocked debug proxy (${clearedVariables.sort().join(", ")}).\n`,
);
}
const isWindows = process.platform === "win32";
const child = spawn(isWindows ? "tauri.cmd" : "tauri", ["dev"], {
cwd: process.cwd(),
env: debugEnvironment,
stdio: "inherit",
shell: isWindows,
});
child.on("error", (error) => {
process.stderr.write(`Could not start Tauri development mode: ${error.message}\n`);
process.exitCode = 1;
});
child.on("exit", (code) => {
process.exitCode = code ?? 1;
});
+380 -133
View File
@@ -574,13 +574,22 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
dependencies = [
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
"objc2 0.6.4",
]
[[package]]
@@ -1119,6 +1128,19 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.13.0",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.25.0"
@@ -1592,9 +1614,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.13.0",
"block2",
"block2 0.6.2",
"libc",
"objc2",
"objc2 0.6.4",
]
[[package]]
@@ -2532,17 +2554,21 @@ dependencies = [
]
[[package]]
name = "git_lite"
name = "gitty"
version = "0.1.0"
dependencies = [
"commit_ai",
"keyring",
"log",
"reqwest 0.12.28",
"serde",
"serde_json",
"sysinfo 0.38.3",
"tauri",
"tauri-build",
"tauri-plugin-aptabase",
"tauri-plugin-dialog",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tokio",
]
@@ -3420,7 +3446,7 @@ dependencies = [
"security-framework 2.11.1",
"security-framework 3.7.0",
"windows-sys 0.60.2",
"zbus",
"zbus 4.4.0",
"zeroize",
]
@@ -3882,7 +3908,7 @@ dependencies = [
"statrs",
"strum 0.27.2",
"symphonia",
"sysinfo",
"sysinfo 0.36.1",
"thiserror 2.0.18",
"tokenizers 0.21.4",
"tokio",
@@ -4012,10 +4038,10 @@ dependencies = [
"dpi",
"gtk",
"keyboard-types",
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-foundation 0.3.1",
"once_cell",
"png 0.18.1",
"serde",
@@ -4309,6 +4335,22 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "objc-sys"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
[[package]]
name = "objc2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
dependencies = [
"objc-sys",
"objc2-encode",
]
[[package]]
name = "objc2"
version = "0.6.4"
@@ -4321,92 +4363,80 @@ dependencies = [
[[package]]
name = "objc2-app-kit"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [
"bitflags 2.13.0",
"block2",
"objc2",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d"
dependencies = [
"bitflags 2.13.0",
"objc2",
"objc2-foundation",
"objc2 0.6.4",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-core-data"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d"
dependencies = [
"objc2",
"objc2-foundation",
"objc2 0.6.4",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.13.0",
"dispatch2",
"objc2",
"objc2 0.6.4",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
dependencies = [
"bitflags 2.13.0",
"dispatch2",
"objc2",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-core-image"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e"
dependencies = [
"objc2",
"objc2-foundation",
"objc2 0.6.4",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-core-location"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-text"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags 2.13.0",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2 0.6.4",
"objc2-foundation 0.3.1",
]
[[package]]
@@ -4426,22 +4456,34 @@ dependencies = [
[[package]]
name = "objc2-foundation"
version = "0.3.2"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.13.0",
"block2",
"block2 0.5.1",
"libc",
"objc2",
"objc2 0.5.2",
]
[[package]]
name = "objc2-foundation"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
"bitflags 2.13.0",
"block2 0.6.2",
"libc",
"objc2 0.6.4",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a"
dependencies = [
"libc",
"objc2-core-foundation",
@@ -4449,82 +4491,106 @@ dependencies = [
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
dependencies = [
"bitflags 2.13.0",
"objc2",
"objc2 0.6.4",
"objc2-core-foundation",
]
[[package]]
name = "objc2-metal"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.13.0",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d"
dependencies = [
"bitflags 2.13.0",
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.13.0",
"objc2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
dependencies = [
"bitflags 2.13.0",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
dependencies = [
"bitflags 2.13.0",
"block2",
"objc2",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image",
"objc2-core-location",
"objc2-core-text",
"objc2-foundation",
"objc2-quartz-core",
"objc2-foundation 0.3.1",
"objc2-quartz-core 0.3.1",
"objc2-user-notifications",
]
[[package]]
name = "objc2-user-notifications"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd"
dependencies = [
"objc2",
"objc2-foundation",
"objc2 0.6.4",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-web-kit"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad"
dependencies = [
"bitflags 2.13.0",
"block2",
"objc2",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-foundation 0.3.1",
]
[[package]]
@@ -4662,8 +4728,8 @@ dependencies = [
"android_system_properties",
"log",
"nix 0.31.3",
"objc2",
"objc2-foundation",
"objc2 0.6.4",
"objc2-foundation 0.3.1",
"objc2-ui-kit",
"serde",
"windows-sys 0.61.2",
@@ -4675,8 +4741,8 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2 0.6.4",
"objc2-foundation 0.3.1",
"objc2-osa-kit",
"serde",
"serde_json",
@@ -5538,17 +5604,17 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"block2 0.6.2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-foundation 0.3.1",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
@@ -5895,7 +5961,7 @@ dependencies = [
"rand 0.8.6",
"serde",
"sha2",
"zbus",
"zbus 4.4.0",
]
[[package]]
@@ -6356,24 +6422,24 @@ dependencies = [
[[package]]
name = "softbuffer"
version = "0.4.8"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics 0.24.0",
"foreign-types 0.5.0",
"js-sys",
"ndk",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"objc2-quartz-core",
"log",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2",
"raw-window-handle",
"redox_syscall",
"tracing",
"wasm-bindgen",
"web-sys",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -6770,7 +6836,21 @@ dependencies = [
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
"windows 0.61.3",
]
[[package]]
name = "sysinfo"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d03c61d2a49c649a15c407338afe7accafde9dac869995dccb73e5f7ef7d9034"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows 0.62.2",
]
[[package]]
@@ -6814,9 +6894,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
dependencies = [
"bitflags 2.13.0",
"block2",
"block2 0.6.2",
"core-foundation 0.10.1",
"core-graphics",
"core-graphics 0.25.0",
"crossbeam-channel",
"dbus",
"dispatch2",
@@ -6830,9 +6910,9 @@ dependencies = [
"log",
"ndk",
"ndk-sys",
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.3.1",
"objc2-ui-kit",
"once_cell",
"parking_lot",
@@ -6841,7 +6921,7 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@@ -6897,9 +6977,9 @@ dependencies = [
"log",
"mime",
"muda",
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.3.1",
"objc2-ui-kit",
"objc2-web-kit",
"percent-encoding",
@@ -6923,7 +7003,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -7052,7 +7132,7 @@ dependencies = [
"dunce",
"glob",
"log",
"objc2-foundation",
"objc2-foundation 0.3.1",
"percent-encoding",
"schemars 0.8.22",
"serde",
@@ -7066,6 +7146,21 @@ dependencies = [
"url",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.18",
"tracing",
"windows-sys 0.60.2",
"zbus 5.17.0",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
@@ -7110,7 +7205,7 @@ dependencies = [
"gtk",
"http",
"jni 0.21.1",
"objc2",
"objc2 0.6.4",
"objc2-ui-kit",
"objc2-web-kit",
"raw-window-handle",
@@ -7121,7 +7216,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -7134,7 +7229,7 @@ dependencies = [
"http",
"jni 0.21.1",
"log",
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"once_cell",
"percent-encoding",
@@ -7146,7 +7241,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
"wry",
]
@@ -7794,11 +7889,11 @@ dependencies = [
"dirs",
"libappindicator",
"muda",
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"objc2-foundation 0.3.1",
"once_cell",
"png 0.18.1",
"serde",
@@ -8372,7 +8467,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement",
"windows-interface",
@@ -8396,7 +8491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [
"thiserror 2.0.18",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
]
@@ -8459,10 +8554,10 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
dependencies = [
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-foundation 0.3.1",
"raw-window-handle",
"windows-sys 0.59.0",
"windows-version",
@@ -8474,11 +8569,23 @@ version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-collections 0.2.0",
"windows-core 0.61.2",
"windows-future",
"windows-future 0.2.1",
"windows-link 0.1.3",
"windows-numerics",
"windows-numerics 0.2.0",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
]
[[package]]
@@ -8490,6 +8597,15 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -8524,7 +8640,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-threading",
"windows-threading 0.1.0",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
]
[[package]]
@@ -8571,6 +8698,16 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
@@ -8744,6 +8881,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-version"
version = "0.1.7"
@@ -8986,7 +9132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
dependencies = [
"base64 0.22.1",
"block2",
"block2 0.6.2",
"cookie",
"crossbeam-channel",
"dirs",
@@ -9000,10 +9146,10 @@ dependencies = [
"jni 0.21.1",
"libc",
"ndk",
"objc2",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-foundation 0.3.1",
"objc2-ui-kit",
"objc2-web-kit",
"once_cell",
@@ -9017,7 +9163,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@@ -9126,9 +9272,44 @@ dependencies = [
"uds_windows",
"windows-sys 0.52.0",
"xdg-home",
"zbus_macros",
"zbus_names",
"zvariant",
"zbus_macros 4.4.0",
"zbus_names 3.0.0",
"zvariant 4.2.0",
]
[[package]]
name = "zbus"
version = "5.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid 1.23.4",
"windows-sys 0.61.2",
"winnow 1.0.3",
"zbus_macros 5.17.0",
"zbus_names 4.3.3",
"zvariant 5.13.0",
]
[[package]]
@@ -9141,7 +9322,22 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
"zvariant_utils",
"zvariant_utils 2.1.0",
]
[[package]]
name = "zbus_macros"
version = "5.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.118",
"zbus_names 4.3.3",
"zvariant 5.13.0",
"zvariant_utils 3.5.0",
]
[[package]]
@@ -9152,7 +9348,18 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
dependencies = [
"serde",
"static_assertions",
"zvariant",
"zvariant 4.2.0",
]
[[package]]
name = "zbus_names"
version = "4.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2"
dependencies = [
"serde",
"winnow 1.0.3",
"zvariant 5.13.0",
]
[[package]]
@@ -9313,7 +9520,21 @@ dependencies = [
"enumflags2",
"serde",
"static_assertions",
"zvariant_derive",
"zvariant_derive 4.2.0",
]
[[package]]
name = "zvariant"
version = "5.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 1.0.3",
"zvariant_derive 5.13.0",
"zvariant_utils 3.5.0",
]
[[package]]
@@ -9326,7 +9547,20 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
"zvariant_utils",
"zvariant_utils 2.1.0",
]
[[package]]
name = "zvariant_derive"
version = "5.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.118",
"zvariant_utils 3.5.0",
]
[[package]]
@@ -9339,3 +9573,16 @@ dependencies = [
"quote",
"syn 2.0.118",
]
[[package]]
name = "zvariant_utils"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.118",
"winnow 1.0.3",
]
+11 -1
View File
@@ -1,5 +1,5 @@
[package]
name = "git_lite"
name = "gitty"
version = "0.1.0"
description = "Rust backend for a lightweight Git desktop client"
edition = "2024"
@@ -21,11 +21,21 @@ tauri-plugin-aptabase = "1.0"
keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] }
commit_ai = { path = "crates/commit_ai" }
tokio = "1.52.3"
log = "0.4"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
sysinfo = { version = "=0.38.3", default-features = false, features = ["system"] }
[build-dependencies]
tauri-build = { version = "2", features = [] }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2"
# Self-updating only makes sense for install methods this app ships itself
# (NSIS on Windows, the .app bundle on macOS). On Linux the app is meant to be
# installed via the system package manager (e.g. the PKGBUILD), which owns
# updates instead — so the updater plugin isn't even compiled in there.
[target.'cfg(not(any(target_os = "android", target_os = "ios", target_os = "linux")))'.dependencies]
tauri-plugin-updater = "2"
# ── Build profiles ───────────────────────────────────────────────────────────
+48
View File
@@ -0,0 +1,48 @@
MIT License
Copyright (c) 2014- GitHub, Inc. and Git LFS contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Portions of the subprocess and tools directories are copied from Go and are
under the following license:
Copyright (c) 2009,2010 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Google Inc. nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+1
View File
@@ -9,6 +9,7 @@
"core:window:allow-toggle-maximize",
"core:window:allow-unminimize",
"core:window:allow-close",
"core:window:allow-destroy",
"core:window:allow-is-maximized",
"core:window:allow-start-dragging",
"dialog:allow-open",
+1 -2
View File
@@ -2,8 +2,7 @@
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
"windows"
],
"windows": [
"main"
+246 -1
View File
@@ -2,7 +2,7 @@ use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::{build_messages, looks_like_diff_echo, sanitize_message};
use crate::{build_messages, build_review_messages, looks_like_diff_echo, sanitize_message};
// Generous sizing so a detailed body with bullet points isn't cut off.
const DEFAULT_MAX_TOKENS: u32 = 1500;
@@ -139,6 +139,165 @@ pub async fn generate_custom(
openai_compatible_request(url, api_key, model, diff, notes).await
}
async fn openai_compatible_review_request(
url: String,
bearer: Option<&str>,
model: &str,
diff: &str,
) -> Result<String, String> {
let (system, user) = build_review_messages(diff)?;
let body = OpenAiRequest {
model: model.to_string(),
messages: vec![
OpenAiMessage {
role: "system",
content: system,
},
OpenAiMessage {
role: "user",
content: user,
},
],
temperature: 0.1,
};
let client = http_client()?;
let mut request = client.post(url).json(&body);
if let Some(key) = bearer.filter(|key| !key.trim().is_empty()) {
request = request.bearer_auth(key);
}
let response = request
.send()
.await
.map_err(|err| format!("Request to the AI model failed: {err}"))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|err| format!("Could not read response: {err}"))?;
if !status.is_success() {
return Err(format!("API error ({status}): {text}"));
}
let parsed: OpenAiResponse =
serde_json::from_str(&text).map_err(|err| format!("Could not process response: {err}"))?;
parsed
.choices
.into_iter()
.next()
.and_then(|choice| choice.message.content)
.map(|content| sanitize_message(&content))
.filter(|content| !content.is_empty())
.ok_or_else(|| "The model did not return a review.".to_string())
}
pub async fn review_openai(api_key: &str, model: &str, diff: &str) -> Result<String, String> {
if api_key.trim().is_empty() {
return Err("OpenAI API key is missing.".to_string());
}
openai_compatible_review_request(
"https://api.openai.com/v1/chat/completions".to_string(),
Some(api_key),
model,
diff,
)
.await
}
pub async fn review_custom(
base_url: &str,
api_key: Option<&str>,
model: &str,
diff: &str,
) -> Result<String, String> {
if base_url.trim().is_empty() {
return Err("Endpoint URL is missing.".to_string());
}
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
openai_compatible_review_request(url, api_key, model, diff).await
}
async fn openai_compatible_split_request(
url: String,
bearer: Option<&str>,
model: &str,
diff: &str,
) -> Result<String, String> {
let system = "You split staged Git changes into small, logical commits. Return only JSON in this exact shape: {\"summary\":\"...\",\"groups\":[{\"message\":\"type(scope): subject\",\"reason\":\"...\",\"files\":[\"path\"]}]}. Every staged file must appear exactly once. Use only paths from the supplied staged file list. Keep messages in English and use Conventional Commits. Do not use markdown.";
let user =
format!("Analyze these staged changes and propose an ordered commit plan:\n\n{diff}");
let body = OpenAiRequest {
model: model.to_string(),
messages: vec![
OpenAiMessage {
role: "system",
content: system.to_string(),
},
OpenAiMessage {
role: "user",
content: user,
},
],
temperature: 0.1,
};
let client = http_client()?;
let mut request = client.post(url).json(&body);
if let Some(key) = bearer.filter(|key| !key.trim().is_empty()) {
request = request.bearer_auth(key);
}
let response = request
.send()
.await
.map_err(|err| format!("Request to the AI model failed: {err}"))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|err| format!("Could not read response: {err}"))?;
if !status.is_success() {
return Err(format!("API error ({status}): {text}"));
}
let parsed: OpenAiResponse =
serde_json::from_str(&text).map_err(|err| format!("Could not process response: {err}"))?;
parsed
.choices
.into_iter()
.next()
.and_then(|choice| choice.message.content)
.map(|content| sanitize_message(&content))
.filter(|content| !content.is_empty())
.ok_or_else(|| "The model did not return a commit plan.".to_string())
}
pub async fn split_openai(api_key: &str, model: &str, diff: &str) -> Result<String, String> {
if api_key.trim().is_empty() {
return Err("OpenAI API key is missing.".to_string());
}
openai_compatible_split_request(
"https://api.openai.com/v1/chat/completions".to_string(),
Some(api_key),
model,
diff,
)
.await
}
pub async fn split_custom(
base_url: &str,
api_key: Option<&str>,
model: &str,
diff: &str,
) -> Result<String, String> {
if base_url.trim().is_empty() {
return Err("Endpoint URL is missing.".to_string());
}
openai_compatible_split_request(
format!("{}/chat/completions", base_url.trim_end_matches('/')),
api_key,
model,
diff,
)
.await
}
#[derive(Serialize)]
struct AnthropicMessage {
role: &'static str,
@@ -220,3 +379,89 @@ pub async fn generate_anthropic(
}
Ok(message)
}
pub async fn review_anthropic(api_key: &str, model: &str, diff: &str) -> Result<String, String> {
if api_key.trim().is_empty() {
return Err("Anthropic API key is missing.".to_string());
}
let (system, user) = build_review_messages(diff)?;
let body = AnthropicRequest {
model: model.to_string(),
max_tokens: 2400,
system,
messages: vec![AnthropicMessage {
role: "user",
content: user,
}],
};
let client = http_client()?;
let response = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.json(&body)
.send()
.await
.map_err(|err| format!("Request to Anthropic failed: {err}"))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|err| format!("Could not read response: {err}"))?;
if !status.is_success() {
return Err(format!("API error ({status}): {text}"));
}
let parsed: AnthropicResponse =
serde_json::from_str(&text).map_err(|err| format!("Could not process response: {err}"))?;
parsed
.content
.into_iter()
.find_map(|block| block.text)
.map(|text| sanitize_message(&text))
.filter(|text| !text.is_empty())
.ok_or_else(|| "The model did not return a review.".to_string())
}
pub async fn split_anthropic(api_key: &str, model: &str, diff: &str) -> Result<String, String> {
if api_key.trim().is_empty() {
return Err("Anthropic API key is missing.".to_string());
}
let system = "You split staged Git changes into small, logical commits. Return only JSON in this exact shape: {\"summary\":\"...\",\"groups\":[{\"message\":\"type(scope): subject\",\"reason\":\"...\",\"files\":[\"path\"]}]}. Every staged file must appear exactly once. Use only paths from the supplied staged file list. Keep messages in English and use Conventional Commits. Do not use markdown.".to_string();
let body = AnthropicRequest {
model: model.to_string(),
max_tokens: 2400,
system,
messages: vec![AnthropicMessage {
role: "user",
content: format!(
"Analyze these staged changes and propose an ordered commit plan:\n\n{diff}"
),
}],
};
let client = http_client()?;
let response = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.json(&body)
.send()
.await
.map_err(|err| format!("Request to Anthropic failed: {err}"))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|err| format!("Could not read response: {err}"))?;
if !status.is_success() {
return Err(format!("API error ({status}): {text}"));
}
let parsed: AnthropicResponse =
serde_json::from_str(&text).map_err(|err| format!("Could not process response: {err}"))?;
parsed
.content
.into_iter()
.find_map(|block| block.text)
.map(|text| sanitize_message(&text))
.filter(|text| !text.is_empty())
.ok_or_else(|| "The model did not return a commit plan.".to_string())
}
+28 -1
View File
@@ -1,6 +1,9 @@
mod cloud;
pub use cloud::{generate_anthropic, generate_custom, generate_openai};
pub use cloud::{
generate_anthropic, generate_custom, generate_openai, review_anthropic, review_custom,
review_openai, split_anthropic, split_custom, split_openai,
};
use std::{
collections::hash_map::DefaultHasher,
@@ -443,3 +446,27 @@ instead of a direct string comparison.\n\n\
user.push_str(&format!("Staged diff:\n{diff}"));
Ok((system, user))
}
pub(crate) fn build_review_messages(diff: &str) -> Result<(String, String), String> {
if diff.trim().is_empty() {
return Err("No staged changes available for review.".to_string());
}
const MAX_CHARS: usize = 36_000;
let diff = truncate_at_char_boundary(diff, MAX_CHARS);
let system = r#"You are a senior software engineer performing a focused pre-commit review.
Review only the supplied staged Git diff. Look for concrete correctness bugs, security issues,
data loss, regressions, broken edge cases, unsafe error handling, and meaningful performance or
maintainability risks. Do not report formatting preferences or speculative nitpicks.
Return ONLY valid JSON with this exact shape:
{"summary":"one concise overall assessment","risk":"low|medium|high","findings":[{"severity":"critical|warning|info","title":"short title","description":"clear evidence and impact","file":"path or null","line":123,"suggestion":"specific safe next step"}]}
Use the new-file line number from the diff when it is known; otherwise use null. Use null for file
when the issue is repository-wide. Maximum 12 findings, ordered critical then warning then info.
If no actionable issue exists, return an empty findings array and risk low. Never use markdown,
code fences, commentary outside the JSON, or claim that tests were executed."#
.to_string();
let user = format!("Staged diff to review:\n{diff}");
Ok((system, user))
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+72
View File
@@ -0,0 +1,72 @@
!macro GITTY_MIGRATE_LEGACY_DATA_DIR oldDir newDir
${IfNot} ${FileExists} "${newDir}\*.*"
${AndIf} ${FileExists} "${oldDir}\*.*"
Rename "${oldDir}" "${newDir}"
${EndIf}
!macroend
!macro GITTY_UNINSTALL_LEGACY_GITLITE rootKey
ClearErrors
ReadRegStr $R0 ${rootKey} "Software\Microsoft\Windows\CurrentVersion\Uninstall\GitLite" "UninstallString"
${If} "$R0" != ""
ReadRegStr $R1 ${rootKey} "Software\git-lite\GitLite" ""
${If} "$R1" == ""
ReadRegStr $R1 ${rootKey} "Software\Microsoft\Windows\CurrentVersion\Uninstall\GitLite" "InstallLocation"
${EndIf}
DetailPrint "Removing legacy GitLite installation"
${If} "$R1" != ""
ExecWait '$R0 /S _?=$R1' $R2
${Else}
ExecWait '$R0 /S' $R2
${EndIf}
${EndIf}
DeleteRegKey ${rootKey} "Software\Microsoft\Windows\CurrentVersion\Uninstall\GitLite"
DeleteRegKey ${rootKey} "Software\git-lite\GitLite"
DeleteRegKey /ifempty ${rootKey} "Software\git-lite"
!macroend
!macro GITTY_CLEANUP_LEGACY_SHORTCUTS
Delete "$DESKTOP\GitLite.lnk"
Delete "$SMPROGRAMS\GitLite.lnk"
RMDir /r "$SMPROGRAMS\GitLite"
!macroend
!macro GITTY_REGISTER_INSTALL
WriteRegStr HKCU "${MANUPRODUCTKEY}" "" "$INSTDIR"
WriteRegStr HKCU "${UNINSTKEY}" "MainBinaryName" "${MAINBINARYNAME}.exe"
WriteRegStr HKCU "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}"
WriteRegStr HKCU "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\""
WriteRegStr HKCU "${UNINSTKEY}" "DisplayVersion" "${VERSION}"
WriteRegStr HKCU "${UNINSTKEY}" "Publisher" "${MANUFACTURER}"
WriteRegStr HKCU "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\""
WriteRegStr HKCU "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKCU "${UNINSTKEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
WriteRegDWORD HKCU "${UNINSTKEY}" "NoModify" "1"
WriteRegDWORD HKCU "${UNINSTKEY}" "NoRepair" "1"
!macroend
!macro GITTY_CREATE_UPDATE_SHORTCUTS
CreateShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
!insertmacro SetLnkAppUserModelId "$SMPROGRAMS\${PRODUCTNAME}.lnk"
CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
!insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk"
!macroend
!macro NSIS_HOOK_PREINSTALL
!insertmacro GITTY_MIGRATE_LEGACY_DATA_DIR "$APPDATA\com.git-lite" "$APPDATA\com.gitty"
!insertmacro GITTY_MIGRATE_LEGACY_DATA_DIR "$LOCALAPPDATA\com.git-lite" "$LOCALAPPDATA\com.gitty"
!insertmacro GITTY_UNINSTALL_LEGACY_GITLITE HKCU
!insertmacro GITTY_UNINSTALL_LEGACY_GITLITE HKLM
!macroend
!macro NSIS_HOOK_POSTINSTALL
!insertmacro GITTY_CLEANUP_LEGACY_SHORTCUTS
!insertmacro GITTY_REGISTER_INSTALL
!insertmacro GITTY_CREATE_UPDATE_SHORTCUTS
!macroend
!macro NSIS_HOOK_POSTUNINSTALL
!insertmacro GITTY_CLEANUP_LEGACY_SHORTCUTS
!macroend
+15 -1
View File
@@ -177,7 +177,21 @@ pub fn set_sync_badge(
.map_err(|err| format!("Could not set taskbar badge: {err}"))?;
}
#[cfg(not(target_os = "windows"))]
// Native badge count: works on Linux (via the desktop's launcher API, e.g. Unity's
// LauncherEntry, also honored by GNOME/KDE) and macOS (dock badge). Matched against the
// app's `<product-name>.desktop` file on Linux, so it's a silent no-op on launchers that
// don't implement the protocol.
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let Some(window) = app.get_webview_window("main") else {
return Ok(());
};
window
.set_badge_count(if count > 0 { Some(count as i64) } else { None })
.map_err(|err| format!("Could not set taskbar badge: {err}"))?;
}
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
{
let _ = (app, count);
}
File diff suppressed because it is too large Load Diff
+4419 -310
View File
File diff suppressed because it is too large Load Diff
+364 -23
View File
@@ -1,50 +1,369 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod badge;
mod external_tools;
mod git;
mod telemetry;
use badge::set_sync_badge;
use git::{
SearchCancellationState, amend_commit, apply_file_patch, cancel_code_search,
cancel_file_history, checkout_branch, cherry_pick_abort, cherry_pick_commit,
cherry_pick_continue, clone_repository, commit, commit_ai_generate, commit_ai_load,
commit_ai_local_models, commit_ai_status, compare_commits, compare_file_to_head,
compare_file_to_parent, create_branch, create_tag, cred_delete, cred_load, cred_save,
delete_branch, delete_tag, diff_file_against_working_tree, fetch, get_file_blame,
get_file_patch, get_remote_url, get_status, last_commit_message, list_branches, list_commits,
list_file_history, list_repository_files, list_stashes, list_tags, merge_branch,
open_repo_in_explorer, open_repository, open_repository_bundle, open_repository_file, pull,
push, push_tag, read_conflict, rebase_abort, rebase_branch, rebase_continue, rename_branch,
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop, stash_pop,
stash_push, undo_last_commit, unstage_files,
use external_tools::{
detect_external_tools, launch_external_diff, launch_external_merge, launch_external_tool,
};
use git::{
SearchCancellationState, add_remote, add_to_gitignore, add_worktree, amend_commit,
apply_file_patch, cancel_code_search, cancel_file_history, checkout_branch, cherry_pick_abort,
cherry_pick_commit, cherry_pick_continue, clone_repository, commit, commit_ai_generate,
commit_ai_load, commit_ai_local_models, commit_ai_review, commit_ai_split, commit_ai_status,
compare_commits, compare_file_to_head, compare_file_to_parent, create_branch, create_tag,
cred_delete, cred_load, cred_save, delete_branch, delete_commit_note, delete_remote_branch,
delete_remote_branches, delete_tag, diff_file_against_working_tree, fetch, fetch_commit_notes,
get_commit_note, get_file_blame, get_file_patch, get_remote_url, get_status, git_lfs_install,
git_lfs_prune, git_lfs_pull, git_lfs_status, git_lfs_track, git_lfs_untrack, init_repository,
last_commit_message, list_branches, list_commits, list_file_history,
list_interactive_rebase_commits, list_reflog, list_remotes, list_repository_files,
list_stashes, list_tags, list_worktrees, lock_worktree, merge_abort, merge_branch,
merge_continue, move_worktree, open_repo_in_explorer, open_repository, open_repository_bundle,
open_repository_file, prune_worktrees, pull, push, push_commit_notes, push_tag, read_conflict,
rebase_abort, rebase_branch, rebase_continue, remove_remote, remove_worktree, rename_branch,
rename_remote_branch, repair_worktree, resolve_conflict, resolve_conflict_side,
restore_file_from_commit, restore_files, restore_reflog_entry, restore_to_commit,
revert_commit, run_sequence_editor_if_requested, search_code_introductions,
set_branch_upstream, set_commit_note, stage_files, start_interactive_rebase, stash_apply,
stash_drop, stash_pop, stash_push, undo_last_commit, unlock_worktree, unstage_files,
untrack_paths, update_remote,
};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tauri::{Emitter, Manager};
use telemetry::{emit_frontend_log, emit_frontend_span, set_telemetry_enabled};
struct StartupRepository(Mutex<Option<String>>);
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct StartupCloneRequest {
remote_url: String,
parent_path: String,
directory_name: String,
}
struct StartupClone(Mutex<Option<StartupCloneRequest>>);
fn resolve_startup_path(path: &str, cwd: &Path) -> Option<PathBuf> {
let path = path.trim();
if path.is_empty() {
return None;
}
let path = PathBuf::from(path);
Some(if path.is_absolute() {
path
} else {
cwd.join(path)
})
}
fn clone_request_from_args(
args: impl IntoIterator<Item = String>,
cwd: &Path,
) -> Option<StartupCloneRequest> {
let mut args = args.into_iter();
while let Some(arg) = args.next() {
let remote_url = if arg == "clone" || arg == "--clone" {
args.next()
} else {
arg.strip_prefix("--clone=").map(ToString::to_string)
};
let Some(remote_url) = remote_url.filter(|value| !value.trim().is_empty()) else {
continue;
};
let target = resolve_startup_path(&args.next()?, cwd)?;
let directory_name = target.file_name()?.to_string_lossy().trim().to_string();
let parent_path = target.parent()?.to_string_lossy().into_owned();
if directory_name.is_empty() || parent_path.trim().is_empty() {
return None;
}
return Some(StartupCloneRequest {
remote_url,
parent_path,
directory_name,
});
}
None
}
fn repository_path_from_args(args: impl IntoIterator<Item = String>, cwd: &Path) -> Option<String> {
let mut args = args.into_iter();
let mut repository = None;
while let Some(arg) = args.next() {
if arg == "clone" || arg == "--clone" || arg.starts_with("--clone=") {
return None;
}
if arg == "--repo" {
repository = args.next();
break;
}
if let Some(path) = arg.strip_prefix("--repo=") {
repository = Some(path.to_string());
break;
}
if !arg.starts_with('-') {
repository = Some(arg);
break;
}
}
repository
.and_then(|path| resolve_startup_path(&path, cwd))
.map(|path| path.to_string_lossy().into_owned())
}
#[cfg(test)]
mod startup_repository_tests {
use super::{clone_request_from_args, repository_path_from_args};
use std::path::Path;
#[test]
fn accepts_direct_relative_repository_path() {
let path =
repository_path_from_args(["projects/repo".to_string()], Path::new("/home/user"));
assert_eq!(
path.map(|value| value.replace('\\', "/")),
Some("/home/user/projects/repo".to_string())
);
}
#[test]
fn accepts_repo_option() {
let path = repository_path_from_args(
["--repo".to_string(), "/projects/repo".to_string()],
Path::new("/home/user"),
);
assert_eq!(path.as_deref(), Some("/projects/repo"));
}
#[test]
fn accepts_repo_equals_option() {
let path = repository_path_from_args(
["--repo=projects/repo".to_string()],
Path::new("/home/user"),
);
assert_eq!(
path.map(|value| value.replace('\\', "/")),
Some("/home/user/projects/repo".to_string())
);
}
#[test]
fn accepts_clone_command_with_exact_relative_target() {
let request = clone_request_from_args(
[
"clone".to_string(),
"https://example.com/team/project.git".to_string(),
"clones/local-copy".to_string(),
],
Path::new("/home/user"),
)
.expect("clone request should be parsed");
assert_eq!(request.remote_url, "https://example.com/team/project.git");
assert_eq!(request.directory_name, "local-copy");
assert_eq!(request.parent_path.replace('\\', "/"), "/home/user/clones");
}
#[test]
fn accepts_clone_option_and_clone_equals_option() {
for args in [
vec![
"--clone".to_string(),
"git@example.com:team/project.git".to_string(),
"/projects/project".to_string(),
],
vec![
"--clone=git@example.com:team/project.git".to_string(),
"/projects/project".to_string(),
],
] {
let request = clone_request_from_args(args, Path::new("/home/user"))
.expect("clone option should be parsed");
assert_eq!(request.directory_name, "project");
assert_eq!(request.parent_path.replace('\\', "/"), "/projects");
}
}
#[test]
fn repository_parser_does_not_treat_clone_values_as_repository_paths() {
let path = repository_path_from_args(
[
"--clone".to_string(),
"https://example.com/team/project.git".to_string(),
"clones/project".to_string(),
],
Path::new("/home/user"),
);
assert!(path.is_none());
}
}
#[tauri::command]
fn take_startup_repository(state: tauri::State<'_, StartupRepository>) -> Option<String> {
state.0.lock().ok()?.take()
}
#[tauri::command]
fn take_startup_clone(state: tauri::State<'_, StartupClone>) -> Option<StartupCloneRequest> {
state.0.lock().ok()?.take()
}
struct ConsoleLogger;
impl log::Log for ConsoleLogger {
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
metadata.level() <= log::Level::Info
}
fn log(&self, record: &log::Record<'_>) {
if self.enabled(record.metadata()) {
eprintln!(
"[{}] [{}] {}",
record.level(),
record.target(),
record.args()
);
}
}
fn flush(&self) {}
}
static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger;
fn init_console_logging() {
if log::set_logger(&CONSOLE_LOGGER).is_ok() {
log::set_max_level(log::LevelFilter::Info);
log::info!(target: "gitty", "Rust console logging initialized");
}
}
#[tauri::command]
fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("splashscreen") {
let _ = window.hide();
window
.destroy()
.map_err(|error| format!("failed to destroy splashscreen: {error}"))?;
}
if let Some(window) = app.get_webview_window("main") {
let _ = window.maximize();
window
.show()
.map_err(|error| format!("failed to show main window: {error}"))?;
let _ = window.set_focus();
}
Ok(())
}
#[tokio::main]
async fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_aptabase::Builder::new("A-SH-1344793789").with_options(
tauri_plugin_aptabase::InitOptions {
host: Some("https://aptabase.cbsk-tech.de".to_string()),
flush_interval:None
init_console_logging();
telemetry::init();
if let Some(result) = run_sequence_editor_if_requested() {
if let Err(error) = result {
eprintln!("{error}");
std::process::exit(1);
}
return;
}
let startup_args: Vec<String> = std::env::args().skip(1).collect();
let startup_cwd = std::env::current_dir().unwrap_or_default();
let startup_clone = clone_request_from_args(startup_args.clone(), &startup_cwd);
let startup_repository = repository_path_from_args(startup_args, &startup_cwd);
let builder = tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
let args: Vec<String> = args.into_iter().skip(1).collect();
if let Some(request) = clone_request_from_args(args.clone(), Path::new(&cwd)) {
if let Ok(mut pending) = app.state::<StartupClone>().0.lock() {
*pending = Some(request);
}
).build())
let _ = app.emit("open-startup-repository", ());
} else if let Some(path) = repository_path_from_args(args, Path::new(&cwd)) {
if let Ok(mut pending) = app.state::<StartupRepository>().0.lock() {
*pending = Some(path);
}
let _ = app.emit("open-startup-repository", ());
}
#[cfg(desktop)]
let _ = app
.get_webview_window("main")
.expect("no main window")
.set_focus();
}))
.plugin(
tauri_plugin_aptabase::Builder::new("A-SH-1344793789")
.with_options(tauri_plugin_aptabase::InitOptions {
host: Some("https://aptabase.cbsk-tech.de".to_string()),
flush_interval: None,
})
.build(),
)
.manage(StartupRepository(Mutex::new(startup_repository)))
.manage(StartupClone(Mutex::new(startup_clone)))
.manage(SearchCancellationState::default())
.manage(commit_ai::CommitAiEngine::new())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_dialog::init());
// Linux installs are expected to come from the system package manager (see the
// PKGBUILD), which owns updates itself — the self-updater is only wired up for
// the platforms whose install method this app ships (NSIS/Windows, .app/macOS).
#[cfg(not(target_os = "linux"))]
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
builder
.invoke_handler(tauri::generate_handler![
open_repository,
init_repository,
clone_repository,
open_repo_in_explorer,
open_repository_file,
detect_external_tools,
launch_external_tool,
launch_external_diff,
launch_external_merge,
get_status,
git_lfs_status,
git_lfs_install,
git_lfs_track,
git_lfs_untrack,
git_lfs_pull,
git_lfs_prune,
list_branches,
list_remotes,
add_remote,
update_remote,
remove_remote,
set_branch_upstream,
delete_remote_branch,
delete_remote_branches,
list_stashes,
checkout_branch,
create_branch,
rename_branch,
rename_remote_branch,
delete_branch,
list_worktrees,
add_worktree,
remove_worktree,
move_worktree,
lock_worktree,
unlock_worktree,
prune_worktrees,
repair_worktree,
list_tags,
create_tag,
delete_tag,
@@ -54,6 +373,8 @@ async fn main() {
cherry_pick_abort,
stage_files,
unstage_files,
add_to_gitignore,
untrack_paths,
stash_push,
stash_apply,
stash_pop,
@@ -69,16 +390,30 @@ async fn main() {
commit_ai_load,
commit_ai_local_models,
commit_ai_generate,
commit_ai_review,
commit_ai_split,
pull,
push,
fetch,
list_commits,
get_commit_note,
set_commit_note,
delete_commit_note,
fetch_commit_notes,
push_commit_notes,
restore_to_commit,
restore_file_from_commit,
merge_branch,
merge_continue,
merge_abort,
revert_commit,
rebase_branch,
rebase_continue,
rebase_abort,
list_interactive_rebase_commits,
start_interactive_rebase,
list_reflog,
restore_reflog_entry,
list_repository_files,
open_repository_bundle,
list_file_history,
@@ -97,7 +432,13 @@ async fn main() {
cred_load,
cred_save,
cred_delete,
set_sync_badge
set_sync_badge,
close_splashscreen,
set_telemetry_enabled,
emit_frontend_log,
emit_frontend_span,
take_startup_repository,
take_startup_clone
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
+363
View File
@@ -0,0 +1,363 @@
use std::{
sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
mpsc::{SyncSender, sync_channel},
},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use serde::Deserialize;
use serde_json::json;
const DEFAULT_OTLP_LOGS_ENDPOINT: &str = "https://telemetry.cbsk-tech.de/v1/logs";
const DEFAULT_OTLP_TRACES_ENDPOINT: &str = "https://telemetry.cbsk-tech.de/v1/traces";
const DEFAULT_OTLP_METRICS_ENDPOINT: &str = "https://telemetry.cbsk-tech.de/v1/metrics";
const METRICS_INTERVAL: Duration = Duration::from_secs(30);
const MAX_BODY_BYTES: usize = 2_048;
static ENABLED: AtomicBool = AtomicBool::new(false);
static SENDER: OnceLock<SyncSender<TelemetrySignal>> = OnceLock::new();
#[derive(Debug)]
enum TelemetrySignal {
Log(TelemetryRecord),
Span(FrontendSpan),
Metrics(ProcessMetrics),
}
#[derive(Debug)]
struct ProcessMetrics {
cpu_utilization: f64,
memory_bytes: u64,
}
#[derive(Debug)]
struct TelemetryRecord {
severity: &'static str,
severity_number: u8,
body: String,
event_name: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendLog {
level: String,
message: String,
event_name: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendSpan {
name: String,
trace_id: String,
span_id: String,
started_at_ms: u64,
duration_ms: f64,
success: bool,
}
pub fn init() {
let endpoint = std::env::var("GITTY_OTLP_LOGS_ENDPOINT")
.unwrap_or_else(|_| DEFAULT_OTLP_LOGS_ENDPOINT.to_string());
let traces_endpoint = std::env::var("GITTY_OTLP_TRACES_ENDPOINT")
.unwrap_or_else(|_| DEFAULT_OTLP_TRACES_ENDPOINT.to_string());
let metrics_endpoint = std::env::var("GITTY_OTLP_METRICS_ENDPOINT")
.unwrap_or_else(|_| DEFAULT_OTLP_METRICS_ENDPOINT.to_string());
let (sender, receiver) = sync_channel::<TelemetrySignal>(256);
if SENDER.set(sender.clone()).is_err() {
return;
}
start_process_metrics(sender);
std::thread::Builder::new()
.name("gitty-telemetry".into())
.spawn(move || {
let client = match reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(5))
.build()
{
Ok(client) => client,
Err(error) => {
eprintln!("[WARN] [gitty::telemetry] could not create OTLP client: {error}");
return;
}
};
let mut export_warning_shown = false;
while let Ok(signal) = receiver.recv() {
let (target_endpoint, payload) = match signal {
TelemetrySignal::Log(record) => (&endpoint, log_payload(record)),
TelemetrySignal::Span(span) => (&traces_endpoint, span_payload(span)),
TelemetrySignal::Metrics(metrics) => {
(&metrics_endpoint, metrics_payload(metrics))
}
};
export(
&client,
target_endpoint,
&payload,
&mut export_warning_shown,
);
}
})
.expect("failed to start telemetry worker");
}
fn start_process_metrics(sender: SyncSender<TelemetrySignal>) {
std::thread::Builder::new()
.name("gitty-process-metrics".into())
.spawn(move || {
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
let Ok(pid) = sysinfo::get_current_pid() else {
return;
};
let refresh_kind = ProcessRefreshKind::nothing().with_cpu().with_memory();
let cpu_count = std::thread::available_parallelism()
.map(|count| count.get())
.unwrap_or(1) as f64;
let mut system = System::new();
let mut primed = false;
loop {
std::thread::sleep(METRICS_INTERVAL);
if !ENABLED.load(Ordering::Relaxed) {
primed = false;
continue;
}
system.refresh_processes_specifics(
ProcessesToUpdate::Some(&[pid]),
true,
refresh_kind,
);
let Some(process) = system.process(pid) else {
continue;
};
if !primed {
primed = true;
continue;
}
let metrics = ProcessMetrics {
cpu_utilization: (f64::from(process.cpu_usage()) / (100.0 * cpu_count))
.clamp(0.0, 1.0),
memory_bytes: process.memory(),
};
let _ = sender.try_send(TelemetrySignal::Metrics(metrics));
}
})
.expect("failed to start process metrics worker");
}
fn resource_attributes() -> serde_json::Value {
json!([
{ "key": "service.name", "value": { "stringValue": "gitty-desktop" } },
{ "key": "service.version", "value": { "stringValue": env!("CARGO_PKG_VERSION") } },
{ "key": "deployment.environment.name", "value": { "stringValue": if cfg!(debug_assertions) { "development" } else { "production" } } },
{ "key": "os.type", "value": { "stringValue": std::env::consts::OS } },
{ "key": "host.arch", "value": { "stringValue": std::env::consts::ARCH } }
])
}
fn log_payload(record: TelemetryRecord) -> serde_json::Value {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_string();
let mut attributes = vec![json!({
"key": "telemetry.sdk.language",
"value": { "stringValue": "rust" }
})];
if let Some(event_name) = record.event_name {
attributes.push(json!({
"key": "event.name",
"value": { "stringValue": event_name }
}));
}
let payload = json!({
"resourceLogs": [{
"resource": { "attributes": resource_attributes() },
"scopeLogs": [{
"scope": { "name": "gitty.telemetry" },
"logRecords": [{
"timeUnixNano": timestamp,
"observedTimeUnixNano": timestamp,
"severityNumber": record.severity_number,
"severityText": record.severity,
"body": { "stringValue": record.body },
"attributes": attributes
}]
}]
}]
});
payload
}
fn span_payload(mut span: FrontendSpan) -> serde_json::Value {
if span.name.len() > 128 {
span.name.truncate(128);
}
let start = u128::from(span.started_at_ms) * 1_000_000;
let duration = (span.duration_ms.max(0.0) * 1_000_000.0) as u128;
let attributes = vec![
json!({ "key": "rpc.system", "value": { "stringValue": "tauri" } }),
json!({ "key": "rpc.method", "value": { "stringValue": span.name } }),
];
json!({
"resourceSpans": [{
"resource": { "attributes": resource_attributes() },
"scopeSpans": [{
"scope": { "name": "gitty.tauri", "version": env!("CARGO_PKG_VERSION") },
"spans": [{
"traceId": span.trace_id,
"spanId": span.span_id,
"name": format!("tauri.{}", span.name),
"kind": 1,
"startTimeUnixNano": start.to_string(),
"endTimeUnixNano": (start + duration).to_string(),
"attributes": attributes,
"status": {
"code": if span.success { 1 } else { 2 },
"message": if span.success { "" } else { "Tauri command failed" }
}
}]
}]
}]
})
}
fn metrics_payload(metrics: ProcessMetrics) -> serde_json::Value {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_string();
json!({
"resourceMetrics": [{
"resource": { "attributes": resource_attributes() },
"scopeMetrics": [{
"scope": { "name": "gitty.process", "version": env!("CARGO_PKG_VERSION") },
"metrics": [
{
"name": "process.cpu.utilization",
"description": "Normalized CPU utilization of the Gitty process",
"unit": "1",
"gauge": { "dataPoints": [{
"timeUnixNano": timestamp,
"asDouble": metrics.cpu_utilization,
"attributes": []
}] }
},
{
"name": "process.memory.usage",
"description": "Resident memory used by the Gitty process",
"unit": "By",
"gauge": { "dataPoints": [{
"timeUnixNano": timestamp,
"asInt": metrics.memory_bytes.to_string(),
"attributes": []
}] }
}
]
}]
}]
})
}
fn export(
client: &reqwest::blocking::Client,
endpoint: &str,
payload: &serde_json::Value,
export_warning_shown: &mut bool,
) {
match client.post(endpoint).json(payload).send() {
Ok(response) if response.status().is_success() => {
if response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.starts_with("text/html"))
&& !*export_warning_shown
{
eprintln!(
"[WARN] [gitty::telemetry] OTLP endpoint returned HTML; configure the collector endpoint with GITTY_OTLP_LOGS_ENDPOINT"
);
*export_warning_shown = true;
}
}
Ok(response) if !*export_warning_shown => {
eprintln!(
"[WARN] [gitty::telemetry] OTLP export failed with status {}",
response.status()
);
*export_warning_shown = true;
}
Err(error) if !*export_warning_shown => {
eprintln!("[WARN] [gitty::telemetry] OTLP export failed: {error}");
*export_warning_shown = true;
}
_ => {}
}
}
fn emit(record: TelemetryRecord) {
if !ENABLED.load(Ordering::Relaxed) {
return;
}
if let Some(sender) = SENDER.get() {
let _ = sender.try_send(TelemetrySignal::Log(record));
}
}
#[tauri::command]
pub fn set_telemetry_enabled(enabled: bool) {
ENABLED.store(enabled, Ordering::Relaxed);
if enabled {
emit(TelemetryRecord {
severity: "INFO",
severity_number: 9,
body: "Telemetry enabled".into(),
event_name: Some("telemetry.enabled".into()),
});
}
}
#[tauri::command]
pub fn emit_frontend_log(log: FrontendLog) {
let (severity, severity_number) = match log.level.as_str() {
"error" => ("ERROR", 17),
"warn" => ("WARN", 13),
_ => ("INFO", 9),
};
let mut body = log.message;
if body.len() > MAX_BODY_BYTES {
body.truncate(MAX_BODY_BYTES);
}
emit(TelemetryRecord {
severity,
severity_number,
body,
event_name: log.event_name,
});
}
#[tauri::command]
pub fn emit_frontend_span(span: FrontendSpan) {
if !ENABLED.load(Ordering::Relaxed) {
return;
}
if span.trace_id.len() != 32
|| span.span_id.len() != 16
|| !span.trace_id.bytes().all(|byte| byte.is_ascii_hexdigit())
|| !span.span_id.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return;
}
if let Some(sender) = SENDER.get() {
let _ = sender.try_send(TelemetrySignal::Span(span));
}
}
+33 -8
View File
@@ -1,25 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "GitLite",
"version": "2026.7.14",
"identifier": "com.git-lite",
"productName": "Gitty",
"version": "2026.8.7",
"identifier": "com.gitty",
"build": {
"beforeDevCommand": "npm run dev",
"beforeDevCommand": "npm run prepare:lfs && npm run dev",
"devUrl": "http://127.0.0.1:1420",
"beforeBuildCommand": "npm run build",
"beforeBuildCommand": "npm run prepare:lfs && npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"label": "main",
"title": "GitLite",
"title": "Gitty",
"width": 1200,
"height": 800,
"minWidth": 900,
"minHeight": 600,
"visible": false,
"decorations": false,
"shadow": true
},
{
"label": "splashscreen",
"url": "splashscreen.html",
"title": "Gitty",
"width": 460,
"height": 420,
"resizable": false,
"decorations": false,
"center": true,
"skipTaskbar": true,
"shadow": true
}
],
"security": {
@@ -28,17 +41,28 @@
},
"bundle": {
"active": true,
"externalBin": [
"binaries/git-lfs"
],
"resources": [
"binaries/git-lfs-LICENSE.txt"
],
"targets": [
"nsis"
],
"icon": [
"icons/icon.png",
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"createUpdaterArtifacts": true,
"windows": {
"nsis": {
"installMode": "currentUser"
"installMode": "currentUser",
"installerIcon": "icons/icon.ico",
"installerHooks": "nsis-hooks.nsh"
}
}
},
@@ -46,6 +70,7 @@
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdDNEJGRTZERTREOTc5QjkKUldTNWVkbmtiZjVMZk02YzdpenVLOVltREJpRkFrc2F4dUwzaTV5M3pacFRVR0tmd25xSGtjK1MK",
"endpoints": [
"https://cdn.cbsk-tech.de/gitty/latest.json",
"https://cdn.cbsk-tech.de/gitlite/latest.json"
]
}
+3429 -523
View File
File diff suppressed because it is too large Load Diff
+5096 -475
View File
File diff suppressed because it is too large Load Diff
+77
View File
@@ -0,0 +1,77 @@
<script lang="ts">
import { BookOpen, Database, Plus, X } from "@lucide/svelte";
interface RepositoryTabItem {
path: string;
name: string;
branch: string | null;
}
export let activeView: "management" | "repository" = "management";
export let repoTabs: RepositoryTabItem[] = [];
export let isBusy: boolean = false;
export let language: "en" | "de" = "en";
export let onOpenManagement: () => void = () => {};
export let isActive: (path: string) => boolean = () => false;
export let onSelect: (path: string) => void | Promise<void> = () => {};
export let onClose: (path: string, event: MouseEvent) => void | Promise<void> = () => {};
export let onContextMenu: (path: string, event: MouseEvent) => void = () => {};
export let onAdd: () => void | Promise<void> = () => {};
</script>
<header class="repo-tabbar" aria-label={language === "de" ? "Repository-Reiter" : "Repository tabs"}>
<button
class="repo-tab management"
class:active={activeView === "management"}
type="button"
onclick={onOpenManagement}
disabled={isBusy}
title={language === "de" ? "Repository-Verwaltung" : "Repository Management"}
>
<BookOpen size={14} aria-hidden="true" />
<span>{language === "de" ? "Repository-Verwaltung" : "Repository Management"}</span>
</button>
<div class="repo-tabs-scroll">
{#each repoTabs as repo (repo.path)}
<div
class="repo-tab-wrap"
class:active={isActive(repo.path)}
role="presentation"
oncontextmenu={(event) => onContextMenu(repo.path, event)}
>
<button
class="repo-tab"
type="button"
onclick={() => onSelect(repo.path)}
disabled={isBusy}
title={repo.path}
>
<Database size={15} aria-hidden="true" />
<span>{repo.name}</span>
</button>
<button
class="repo-tab-close"
type="button"
onclick={(event) => onClose(repo.path, event)}
disabled={isBusy}
aria-label={language === "de" ? `${repo.name} schließen` : `Close ${repo.name}`}
title={language === "de" ? "Repository-Tab schließen" : "Close repository tab"}
>
<X size={11} aria-hidden="true" />
</button>
</div>
{/each}
</div>
<button
class="repo-tab-add"
type="button"
onclick={onAdd}
disabled={isBusy}
title={language === "de" ? "Repository-Ordner öffnen" : "Open repository folder"}
aria-label={language === "de" ? "Repository-Ordner öffnen" : "Open repository folder"}
>
<Plus size={15} aria-hidden="true" />
</button>
</header>
+250
View File
@@ -0,0 +1,250 @@
<script lang="ts">
import {
Box,
ChevronDown,
Code2,
CloudDownload,
CloudOff,
Download,
FolderOpen,
GitCompare,
History,
ListRestart,
LoaderCircle,
RefreshCw,
Search,
Upload,
Settings2,
Terminal,
} from "@lucide/svelte";
export let hasRepository: boolean = false;
export let isBusy: boolean = false;
export let operation: string = "";
export let ahead: number = 0;
export let behind: number = 0;
export let localOnly: boolean = false;
export let language: "en" | "de" = "en";
export let editorName: string = "Editor";
export let terminalName: string = "Terminal";
export let fileManagerName: string = "Explorer";
export let onFetch: () => void = () => {};
export let onPull: () => void = () => {};
export let onPush: () => void = () => {};
export let onRefresh: () => void = () => {};
export let onSearch: () => void = () => {};
export let onCompare: () => void = () => {};
export let onInteractiveRebase: () => void = () => {};
export let onReflog: () => void = () => {};
export let onOpenInExplorer: () => void = () => {};
export let onOpenInEditor: () => void = () => {};
export let onOpenTerminal: () => void = () => {};
export let onFetchPrune: () => void = () => {};
export let onForcePush: () => void = () => {};
export let onSyncOptions: () => void = () => {};
export let onOpenLfs: () => void = () => {};
let historyOpen = false;
let syncOpen = false;
let toolbarElement: HTMLDivElement;
$: isGerman = language === "de";
$: pushLabel = localOnly ? (isGerman ? "Veröffentlichen" : "Publish") : "Push";
$: pushTitle = localOnly
? (isGerman
? "Dieser Branch existiert nur lokal. Veröffentlichen erstellt den Remote-Branch und richtet das Tracking ein."
: "This branch exists only locally. Publish creates the remote branch and configures tracking.")
: "Push";
function runHistoryAction(action: () => void) {
historyOpen = false;
action();
}
function handleWindowClick(event: MouseEvent) {
if (toolbarElement && !toolbarElement.contains(event.target as Node)) { historyOpen = false; syncOpen = false; }
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key === "Escape" && historyOpen) {
event.stopPropagation();
historyOpen = false;
}
}
</script>
<svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} />
<div bind:this={toolbarElement} class="repo-toolbar" role="toolbar" aria-label={isGerman ? "Repository-Aktionen" : "Repository actions"}>
<div class="repo-toolbar-sync">
<div class="repo-action-group repo-sync-actions">
<button
class="repo-action fetch"
onclick={onFetch}
disabled={!hasRepository || isBusy}
title="Fetch"
aria-label="Fetch"
>
{#if operation === "Fetching"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<CloudDownload size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">Fetch</span>
</button>
<button
class="repo-action sync-primary"
onclick={onPull}
disabled={!hasRepository || isBusy}
title="Pull"
aria-label={behind > 0
? `Pull, ${behind} ${isGerman ? (behind === 1 ? "Remote-Commit voraus" : "Remote-Commits voraus") : (behind === 1 ? "commit behind" : "commits behind")}`
: "Pull"}
>
{#if operation === "Pulling"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<Download size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">Pull</span>
{#if behind > 0}<span class="repo-action-count behind">{behind}</span>{/if}
</button>
<button
class="repo-action sync-primary"
class:publish-local={localOnly}
onclick={onPush}
disabled={!hasRepository || isBusy}
title={pushTitle}
aria-label={localOnly
? pushTitle
: ahead > 0
? `Push, ${ahead} ${isGerman ? (ahead === 1 ? "lokaler Commit voraus" : "lokale Commits voraus") : (ahead === 1 ? "commit ahead" : "commits ahead")}`
: "Push"}
>
{#if operation === "Pushing"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<Upload size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">{pushLabel}</span>
{#if localOnly}
<span class="repo-action-local-marker"><CloudOff size={9} aria-hidden="true" />{isGerman ? "NUR LOKAL" : "LOCAL"}</span>
{:else if ahead > 0}
<span class="repo-action-count ahead">{ahead}</span>
{/if}
</button>
<div class="repo-history-wrap">
<button class="repo-action" type="button" onclick={() => { syncOpen = !syncOpen; historyOpen = false; }} disabled={!hasRepository || isBusy} aria-label={isGerman ? "Sync-Optionen" : "Sync options"} aria-haspopup="menu">
<ChevronDown size={14} aria-hidden="true" />
</button>
{#if syncOpen}
<div class="repo-history-menu" role="menu">
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onFetchPrune(); }}><CloudDownload size={15} /><span><strong>Fetch + Prune</strong><small>{isGerman ? "Veraltete Remote-Branches entfernen" : "Remove stale remote branches"}</small></span></button>
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onForcePush(); }}><Upload size={15} /><span><strong>Force with lease</strong><small>{isGerman ? "Sicheres Pushen nach Rebase" : "Safe push after rebase"}</small></span></button>
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onSyncOptions(); }}><Settings2 size={15} /><span><strong>{isGerman ? "Remotes & Strategien" : "Remotes & strategies"}</strong><small>{isGerman ? "Upstream, Pull und Remote verwalten" : "Manage upstream, pull and remotes"}</small></span></button>
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onOpenLfs(); }}><Box size={15} /><span><strong>Git LFS</strong><small>{isGerman ? "Große Dateien und LFS-Installation verwalten" : "Manage large files and LFS installation"}</small></span></button>
</div>
{/if}
</div>
</div>
</div>
<div class="repo-toolbar-divider" aria-hidden="true"></div>
<div class="repo-action-group repo-inspect-actions">
<button
class="repo-action"
onclick={onSearch}
disabled={!hasRepository || isBusy}
title={isGerman ? "Globale Suche" : "Global search"}
aria-label={isGerman ? "Globale Suche" : "Global search"}
>
<Search size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Suchen" : "Search"}</span>
</button>
<button
class="repo-action"
onclick={onCompare}
disabled={!hasRepository || isBusy}
title={isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits"}
aria-label={isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits"}
>
<GitCompare size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Vergleichen" : "Compare"}</span>
</button>
<div class="repo-history-wrap">
<button
class="repo-action history-trigger"
class:active={historyOpen}
type="button"
onclick={() => { historyOpen = !historyOpen; }}
disabled={!hasRepository || isBusy}
aria-haspopup="menu"
aria-expanded={historyOpen}
title={isGerman ? "Verlauf und Rebase" : "History and rebase"}
>
<History size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Verlauf" : "History"}</span>
<ChevronDown class="history-chevron" size={13} aria-hidden="true" />
</button>
{#if historyOpen}
<div class="repo-history-menu" role="menu">
<button type="button" role="menuitem" onclick={() => runHistoryAction(onReflog)}>
<History size={15} aria-hidden="true" />
<span>
<strong>Reflog</strong>
<small>{isGerman ? "Lokale Referenzbewegungen" : "Local reference movements"}</small>
</span>
</button>
<button type="button" role="menuitem" onclick={() => runHistoryAction(onInteractiveRebase)}>
<ListRestart size={15} aria-hidden="true" />
<span>
<strong>{isGerman ? "Interaktiver Rebase" : "Interactive rebase"}</strong>
<small>{isGerman ? "Commits ordnen und zusammenfassen" : "Reorder and combine commits"}</small>
</span>
</button>
</div>
{/if}
</div>
</div>
<div class="repo-toolbar-spacer"></div>
<div class="repo-toolbar-divider utility" aria-hidden="true"></div>
<div class="repo-action-group repo-utility-actions">
<button class="repo-action" onclick={onOpenInEditor} disabled={!hasRepository || isBusy} title={isGerman ? `Repository in ${editorName} öffnen` : `Open repository in ${editorName}`}>
<Code2 size={15} aria-hidden="true" />
<span class="repo-action-label utility-label">{editorName}</span>
</button>
<button class="repo-action" onclick={onOpenTerminal} disabled={!hasRepository || isBusy} title={isGerman ? `${terminalName} im Repository öffnen` : `Open ${terminalName} in repository`}>
<Terminal size={15} aria-hidden="true" />
<span class="repo-action-label utility-label">{terminalName}</span>
</button>
<button
class="repo-action"
onclick={onOpenInExplorer}
disabled={!hasRepository || isBusy}
title={isGerman ? `Repository in ${fileManagerName} öffnen` : `Open repository in ${fileManagerName}`}
aria-label={isGerman ? `Repository in ${fileManagerName} öffnen` : `Open repository in ${fileManagerName}`}
>
<FolderOpen size={15} aria-hidden="true" />
<span class="repo-action-label utility-label">{fileManagerName}</span>
</button>
<button
class="repo-action"
onclick={onRefresh}
disabled={isBusy || !hasRepository}
title={isGerman ? "Manuell aktualisieren" : "Refresh now"}
aria-label={isGerman ? "Manuell aktualisieren" : "Refresh now"}
>
<RefreshCw class={operation === "Refreshing" ? "spin" : ""} size={16} aria-hidden="true" />
<span class="repo-action-label utility-label">{isGerman ? "Aktualisieren" : "Refresh"}</span>
</button>
</div>
</div>
+73 -215
View File
@@ -1,242 +1,100 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { getVersion } from "@tauri-apps/api/app";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
import { CircleHelp, Minus, Settings, X } from "@lucide/svelte";
import iconUrl from "../../src-tauri/icons/icon.png";
export let branch: string = "";
export let ahead: number = 0;
export let behind: number = 0;
export let repoName: string = "";
export let hasRepository: boolean = false;
export let isBusy: boolean = false;
export let operation: string = "";
export let autoRefreshEnabled: boolean = true;
export let autoRefreshInFlight: boolean = false;
export let onFetch: () => void = () => {};
export let onPull: () => void = () => {};
export let onPush: () => void = () => {};
export let onRefresh: () => void = () => {};
export let onSearch: () => void = () => {};
export let onCompare: () => void = () => {};
export let onOpenInExplorer: () => void = () => {};
export let onToggleAutoRefresh: () => void = () => {};
export let onOpenHelp: () => void = () => {};
export let onOpenSettings: () => void = () => {};
export let language: "en" | "de" = "en";
const win = getCurrentWindow();
let win: ReturnType<typeof getCurrentWindow> | null = null;
let isMaximized = false;
let unlisten: (() => void) | undefined;
let appVersion = "";
onMount(async () => {
isMaximized = await win.isMaximized();
unlisten = await win.onResized(async () => {
try {
win = getCurrentWindow();
isMaximized = await win.isMaximized();
});
appVersion = await getVersion();
unlisten = await win.onResized(async () => {
isMaximized = await win?.isMaximized() ?? false;
});
} catch {
win = null;
}
});
onDestroy(() => {
unlisten?.();
});
function minimizeWindow() {
void win?.minimize();
}
function toggleMaximizeWindow() {
void win?.toggleMaximize();
}
function closeWindow() {
void win?.close();
}
</script>
<header class="titlebar" data-tauri-drag-region>
<!-- Brand -->
<div class="titlebar-brand" data-tauri-drag-region>
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-tauri-drag-region
<span class="titlebar-brand-icon" data-tauri-drag-region>
<img src={iconUrl} alt="" data-tauri-drag-region />
</span>
<span data-tauri-drag-region>Gitty</span>
</div>
<div class="titlebar-drag" data-tauri-drag-region aria-hidden="true"></div>
<!-- Right: app-global actions + window controls -->
<div class="titlebar-globals">
<button
class="tb-action"
onclick={onOpenHelp}
title={language === "de" ? "Hilfe (Ctrl+/)" : "Help (Ctrl+/)"}
aria-label={language === "de" ? "Hilfe öffnen" : "Open help"}
>
<circle cx="12" cy="18" r="3" />
<circle cx="6" cy="6" r="3" />
<circle cx="18" cy="6" r="3" />
<path d="M18 9v1a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9" />
<line x1="12" y1="12" x2="12" y2="15" />
</svg>
<span data-tauri-drag-region>GitLite</span>
{#if appVersion}
<span class="tb-version" data-tauri-drag-region title="Version {appVersion}">v{appVersion}</span>
{/if}
<CircleHelp size={14} aria-hidden="true" />
</button>
<button
class="tb-action"
onclick={onOpenSettings}
title={language === "de" ? "Einstellungen" : "Settings"}
aria-label={language === "de" ? "Einstellungen" : "Settings"}
>
<Settings size={14} aria-hidden="true" />
</button>
</div>
<!-- Center: repo + branch info -->
<div class="titlebar-info" data-tauri-drag-region>
{#if hasRepository}
{#if repoName}
<span class="tb-repo" data-tauri-drag-region>{repoName}</span>
<span class="tb-sep" data-tauri-drag-region aria-hidden="true">/</span>
<div class="titlebar-controls">
<button class="tb-btn" onclick={minimizeWindow} title="Minimize" aria-label="Minimize">
<Minus size={12} aria-hidden="true" />
</button>
<button
class="tb-btn"
onclick={toggleMaximizeWindow}
title={isMaximized ? "Restore" : "Maximize"}
aria-label={isMaximized ? "Restore" : "Maximize"}
>
{#if isMaximized}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<rect x="3" y="1" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.5" />
<path d="M1 3v7a1 1 0 0 0 1 1h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
{:else}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<rect x="1" y="1" width="10" height="10" rx="1" stroke="currentColor" stroke-width="1.5" />
</svg>
{/if}
<GitBranch size={12} aria-hidden="true" />
<span class="tb-branch" data-tauri-drag-region>{branch}</span>
{#if ahead > 0}
<span class="tb-sync ahead" title="{ahead} commits ahead">{ahead}</span>
{/if}
{#if behind > 0}
<span class="tb-sync behind" title="{behind} commits behind">{behind}</span>
{/if}
{:else}
<span class="tb-no-repo" data-tauri-drag-region>No repository open</span>
{/if}
</div>
<!-- Right: actions + window controls -->
<div class="titlebar-right">
<div class="titlebar-actions" role="toolbar" aria-label="Repository actions">
<button
class="tb-action"
onclick={onOpenInExplorer}
disabled={!hasRepository || isBusy}
title="Open repository in Explorer"
aria-label="Open repository in Explorer"
>
<FolderOpen size={14} aria-hidden="true" />
<span class="tb-action-label">Explorer</span>
</button>
<button
class="tb-action"
onclick={onSearch}
disabled={!hasRepository || isBusy}
title="Global search"
aria-label="Global search"
>
<Search size={14} aria-hidden="true" />
<span class="tb-action-label">Search</span>
</button>
<button
class="tb-action"
onclick={onCompare}
disabled={!hasRepository || isBusy}
title="Compare commits"
aria-label="Compare commits"
>
<GitCompare size={14} aria-hidden="true" />
<span class="tb-action-label">Compare</span>
</button>
<button
class="tb-action"
onclick={onFetch}
disabled={!hasRepository || isBusy}
title="Fetch"
aria-label="Fetch"
>
{#if operation === "Fetching"}
<LoaderCircle class="spin" size={14} aria-hidden="true" />
{:else}
<CloudDownload size={14} aria-hidden="true" />
{/if}
<span class="tb-action-label">Fetch</span>
</button>
<button
class="tb-action"
onclick={onPull}
disabled={!hasRepository || isBusy}
title="Pull"
aria-label="Pull"
>
{#if operation === "Pulling"}
<LoaderCircle class="spin" size={14} aria-hidden="true" />
{:else}
<Download size={14} aria-hidden="true" />
{/if}
<span class="tb-action-label">Pull</span>
</button>
<button
class="tb-action"
onclick={onPush}
disabled={!hasRepository || isBusy}
title="Push"
aria-label="Push"
>
{#if operation === "Pushing"}
<LoaderCircle class="spin" size={14} aria-hidden="true" />
{:else}
<Upload size={14} aria-hidden="true" />
{/if}
<span class="tb-action-label">Push</span>
</button>
<button
class="tb-action"
onclick={onRefresh}
disabled={isBusy || !hasRepository}
title="Refresh"
aria-label="Refresh"
>
<RefreshCw
class={operation === "Refreshing" ? "spin" : ""}
size={14}
aria-hidden="true"
/>
<span class="tb-action-label">Refresh</span>
</button>
<button
class="tb-action auto-toggle"
class:active={autoRefreshEnabled}
onclick={onToggleAutoRefresh}
aria-pressed={autoRefreshEnabled}
title={autoRefreshEnabled ? "Auto-refresh on — click to disable" : "Auto-refresh off — click to enable"}
aria-label="Toggle auto-refresh"
>
<RefreshCw
class={autoRefreshInFlight ? "spin" : ""}
size={14}
aria-hidden="true"
/>
<span class="tb-action-label">Auto</span>
</button>
<button
class="tb-action"
onclick={onOpenSettings}
title="Settings"
aria-label="Settings"
>
<Settings size={14} aria-hidden="true" />
<span class="tb-action-label">Settings</span>
</button>
</div>
<div class="titlebar-divider" aria-hidden="true"></div>
<div class="titlebar-controls">
<button class="tb-btn" onclick={() => win.minimize()} title="Minimize" aria-label="Minimize">
<Minus size={12} aria-hidden="true" />
</button>
<button
class="tb-btn"
onclick={() => win.toggleMaximize()}
title={isMaximized ? "Restore" : "Maximize"}
aria-label={isMaximized ? "Restore" : "Maximize"}
>
{#if isMaximized}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<rect x="3" y="1" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.5" />
<path d="M1 3v7a1 1 0 0 0 1 1h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
{:else}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<rect x="1" y="1" width="10" height="10" rx="1" stroke="currentColor" stroke-width="1.5" />
</svg>
{/if}
</button>
<button class="tb-btn close" onclick={() => win.close()} title="Close" aria-label="Close">
<X size={12} aria-hidden="true" />
</button>
</div>
</button>
<button class="tb-btn close" onclick={closeWindow} title="Close" aria-label="Close">
<X size={12} aria-hidden="true" />
</button>
</div>
</header>
+3
View File
@@ -1,8 +1,11 @@
import { invoke } from "@tauri-apps/api/core";
import { telemetryLog } from "./telemetry";
export type AnalyticsEventProperties = Record<string, string | number>;
export function trackAnalyticsEvent(name: string, props?: AnalyticsEventProperties) {
const details = props && Object.keys(props).length > 0 ? ` ${JSON.stringify(props)}` : "";
telemetryLog("info", `${name}${details}`, `product.${name}`);
void invoke("plugin:aptabase|track_event", {
name,
props: props && Object.keys(props).length > 0 ? props : undefined,
@@ -0,0 +1,103 @@
<script lang="ts">
import { GitCommitHorizontal, LoaderCircle, Sparkles, X } from "@lucide/svelte";
import type { AiCommitPlan } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface Props {
plan: AiCommitPlan;
isApplying: boolean;
onApply: (plan: AiCommitPlan) => void;
onClose: () => void;
}
let { plan, isApplying = false, onApply, onClose }: Props = $props();
let draft = $state<AiCommitPlan>({ summary: "", groups: [] });
$effect.pre(() => {
if (draft.groups.length === 0) draft = structuredClone(plan);
});
let valid = $derived(draft.groups.length > 1 && draft.groups.every((group) => group.message.trim() && group.files.length));
let validationIssue = $derived(
draft.groups.findIndex((group) => group.files.length === 0) >= 0
? "Every commit needs at least one file."
: draft.groups.findIndex((group) => !group.message.trim()) >= 0
? "Every commit needs a message."
: "",
);
function setMessage(index: number, message: string) {
draft.groups[index].message = message;
}
function moveFile(file: string, from: number, to: number) {
if (from === to) return;
draft.groups[from].files = draft.groups[from].files.filter((path) => path !== file);
draft.groups[to].files = [...draft.groups[to].files, file];
}
function applyDraft() {
// `draft` is a deeply reactive Svelte proxy. `structuredClone(draft)`
// throws a DataCloneError before the callback runs, which made the button
// appear to do nothing. A state snapshot is a plain, cloneable object.
onApply($state.snapshot(draft));
}
</script>
<svelte:window onkeydown={(event) => { if (event.key === "Escape" && !isApplying) onClose(); }} />
<div class="dialog-backdrop" role="presentation">
<div class="dialog split-dialog" role="dialog" aria-modal="true" aria-label="AI commit split">
<header class="dialog-header">
<div><span class="eyebrow">Staged changes</span><h2>Split into logical commits</h2></div>
<button class="dialog-close" type="button" onclick={onClose} disabled={isApplying} aria-label="Close"><X size={18} /></button>
</header>
<div class="split-intro"><Sparkles size={18} /><p>{draft.summary}</p></div>
<div class="split-groups">
{#each draft.groups as group, groupIndex}
<article class:empty={group.files.length === 0}>
<header><GitCommitHorizontal size={17} /><strong>Commit {groupIndex + 1}</strong><span>{group.files.length} files</span></header>
<label>
<span>Commit message <em>AI-generated · editable</em></span>
<input value={group.message} oninput={(event) => setMessage(groupIndex, event.currentTarget.value)} disabled={isApplying} />
</label>
{#if group.reason}<p>{group.reason}</p>{/if}
<div class="split-files">
{#each group.files as file}
<div><code>{file}</code>
<SelectMenu class="split-file-target" value={String(groupIndex)} options={draft.groups.map((_, target) => ({ value: String(target), label: `Commit ${target + 1}` }))} disabled={isApplying} onChange={(value) => moveFile(file, groupIndex, Number(value))} />
</div>
{/each}
</div>
</article>
{/each}
</div>
<footer class="dialog-footer">
<p class:invalid={!!validationIssue}>{validationIssue || "Messages are generated automatically. All commits are created in this order."}</p>
<div><button class="btn-secondary" type="button" onclick={onClose} disabled={isApplying}>Cancel</button>
<button class="btn-primary" type="button" onclick={applyDraft} disabled={!valid || isApplying}>
{#if isApplying}<LoaderCircle class="spin" size={14} />{/if}Commit all ({draft.groups.length})
</button></div>
</footer>
</div>
</div>
<style>
.split-dialog{width:min(820px,calc(100vw - 32px));max-height:min(820px,calc(100vh - 32px));display:flex;flex-direction:column}
.split-intro{display:flex;gap:10px;align-items:flex-start;padding:14px 18px;border-bottom:1px solid var(--color-border-subtle);color:var(--color-ink-muted)}
.split-intro p{margin:0;line-height:1.5}
.split-groups{display:grid;gap:10px;padding:14px 18px;overflow:auto}
article{display:grid;gap:10px;padding:13px;border:1px solid var(--color-border-subtle);border-radius:9px;background:var(--color-surface-raised)}
article.empty{border-color:#d88a45}
article>header{display:flex;align-items:center;gap:8px;color:var(--color-ink)}
article>header span{margin-left:auto;color:var(--color-ink-faint);font-size:11px}
label{display:grid;gap:5px;color:var(--color-ink-faint);font-size:10px;font-weight:800;text-transform:uppercase}
label span{display:flex;align-items:center;justify-content:space-between;gap:8px}
label em{color:var(--color-accent);font-size:9px;font-style:normal;font-weight:700;text-transform:none}
input{height:34px;padding:0 10px;border:1px solid var(--color-border);border-radius:6px;background:var(--color-surface);color:var(--color-ink);font-family:var(--font-mono)}
article>p{margin:0;color:var(--color-ink-muted);font-size:12px;line-height:1.45}
.split-files{display:grid;gap:5px}
.split-files>div{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;background:var(--color-surface)}
code{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;color:var(--color-ink-muted);font-size:11px;white-space:nowrap}
:global(.split-file-target){width:110px;flex:0 0 110px}
:global(.split-file-target .select-menu-trigger){height:28px;min-height:28px;font-size:11px}
.dialog-footer p.invalid{color:#e0a040}
</style>
+94
View File
@@ -0,0 +1,94 @@
<script lang="ts">
import { AlertTriangle, CircleAlert, FileCode, Info, LoaderCircle, RotateCw, ShieldCheck, X } from "@lucide/svelte";
import type { AiReviewFinding, AiReviewResult, CommitAiProvider } from "../types";
interface Props {
result: AiReviewResult;
provider: CommitAiProvider;
isReviewing: boolean;
onRerun: () => void;
onClose: () => void;
}
let { result, provider, isReviewing = false, onRerun, onClose }: Props = $props();
function providerLabel(value: CommitAiProvider): string {
if (value === "openai") return "OpenAI";
if (value === "anthropic") return "Anthropic";
if (value === "custom") return "Custom endpoint";
return "Local AI";
}
function locationLabel(finding: AiReviewFinding): string {
if (!finding.file) return "Repository-wide";
return finding.line ? `${finding.file}:${finding.line}` : finding.file;
}
</script>
<svelte:window onkeydown={(event) => { if (event.key === "Escape" && !isReviewing) onClose(); }} />
<div class="dialog-backdrop" role="presentation">
<div class="dialog ai-review-dialog" role="dialog" aria-modal="true" aria-label="AI pre-commit review" tabindex="-1">
<header class="dialog-header ai-review-header">
<div>
<span class="eyebrow">Staged changes</span>
<h2>AI pre-commit review</h2>
</div>
<div class="dialog-header-actions">
<span class="ai-review-provider">{providerLabel(provider)}</span>
<button class="dialog-close" type="button" onclick={onClose} disabled={isReviewing} aria-label="Close review"><X size={18} aria-hidden="true" /></button>
</div>
</header>
<div class="ai-review-summary">
<div class="ai-review-summary-icon" class:clean={result.findings.length === 0}>
{#if result.findings.length === 0}<ShieldCheck size={22} aria-hidden="true" />{:else}<AlertTriangle size={22} aria-hidden="true" />{/if}
</div>
<div>
<div class="ai-review-summary-line">
<strong>{result.findings.length === 0 ? "No actionable issues found" : `${result.findings.length} review ${result.findings.length === 1 ? "finding" : "findings"}`}</strong>
<span class="ai-review-risk {result.risk}">{result.risk} risk</span>
</div>
<p>{result.summary}</p>
</div>
</div>
<div class="ai-review-findings">
{#if result.findings.length === 0}
<div class="ai-review-clean-state">
<ShieldCheck size={30} aria-hidden="true" />
<strong>The staged diff looks ready for human verification.</strong>
<span>AI reviews can miss issues. Run the relevant tests before committing.</span>
</div>
{:else}
{#each result.findings as finding, index (`${finding.file ?? "repo"}:${finding.line ?? 0}:${finding.title}:${index}`)}
<article class="ai-review-finding {finding.severity}">
<div class="ai-review-finding-icon">
{#if finding.severity === "critical"}<CircleAlert size={17} aria-hidden="true" />{:else if finding.severity === "warning"}<AlertTriangle size={17} aria-hidden="true" />{:else}<Info size={17} aria-hidden="true" />{/if}
</div>
<div class="ai-review-finding-body">
<div class="ai-review-finding-title">
<span>{finding.severity}</span>
<strong>{finding.title}</strong>
</div>
<p>{finding.description}</p>
<div class="ai-review-location"><FileCode size={13} aria-hidden="true" /><code>{locationLabel(finding)}</code></div>
{#if finding.suggestion}<div class="ai-review-suggestion"><strong>Suggested next step</strong><span>{finding.suggestion}</span></div>{/if}
</div>
</article>
{/each}
{/if}
</div>
<footer class="dialog-footer ai-review-footer">
<p>Review suggestions are advisory and never modify files automatically.</p>
<div>
<button class="btn-secondary" type="button" onclick={onRerun} disabled={isReviewing}>
{#if isReviewing}<LoaderCircle class="spin" size={14} aria-hidden="true" />{:else}<RotateCw size={14} aria-hidden="true" />{/if}
Review again
</button>
<button class="btn-primary" type="button" onclick={onClose} disabled={isReviewing}>Done</button>
</div>
</footer>
</div>
</div>
+3 -6
View File
@@ -3,6 +3,7 @@
import { AlertCircle, Bot, Check, Cpu, Eye, EyeOff, Gauge, Globe, Key, LoaderCircle, Sparkles, X, Zap } from "@lucide/svelte";
import { credDelete, credLoad, credSave } from "../git";
import type { AiSettings, CommitAiLocalProfile, CommitAiProvider, LocalModelOption } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface Props {
settings: AiSettings;
@@ -85,7 +86,7 @@
const key = CRED_KEYS[target];
const trimmed = value.trim();
if (trimmed) {
await credSave(key, "api-key", trimmed, null);
await credSave(key, "api-key", trimmed);
} else {
await credDelete(key);
}
@@ -200,11 +201,7 @@
</div>
<label class="cred-field">
<span class="cred-field-label">Model</span>
<select bind:value={localModelId}>
{#each localModels as option (option.id)}
<option value={option.id}>{option.label} {formatSize(option.approx_size_mb)}</option>
{/each}
</select>
<SelectMenu value={localModelId} options={localModels.map((option) => ({ value: option.id, label: `${option.label} - ${formatSize(option.approx_size_mb)}` }))} onChange={(value) => { localModelId = value; }} />
</label>
<div class="cred-token-hint">
<AlertCircle size={13} aria-hidden="true" />
@@ -30,10 +30,10 @@
</div>
<div class="analytics-notice-copy">
<p>
GitLite can send anonymous usage events to Aptabase so crashes, rough edges, and commonly used workflows are easier to improve.
Gitty can send anonymous usage events and technical error logs to Aptabase and the self-hosted SigNoz service so crashes, rough edges, and commonly used workflows are easier to improve.
</p>
<p>
Events do not include repository paths, remote URLs, branch names, commit messages, diffs, file names, credentials, or source code.
Telemetry does not include repository paths, remote URLs, branch names, commit messages, diffs, file names, credentials, or source code.
</p>
</div>
@@ -47,7 +47,7 @@
</div>
<footer class="dialog-footer analytics-notice-footer">
<span class="dialog-footer-info">Privacy-friendly, optional, and limited to product usage events.</span>
<span class="dialog-footer-info">Privacy-friendly, optional, and limited to product usage and technical errors.</span>
<button class="btn-primary" type="button" onclick={() => onContinue(allowAnalytics)}>
<Check size={16} aria-hidden="true" />
Continue
+639 -32
View File
@@ -1,19 +1,102 @@
<script lang="ts">
import { Check, Settings, X } from "@lucide/svelte";
import type { AnalyticsSettings } from "../types";
import { open } from "@tauri-apps/plugin-dialog";
import {
Check,
CheckCircle2,
ChevronDown,
ChevronRight,
CircleDashed,
Code2,
FolderOpen,
GitCompare,
GitMerge,
Languages,
Palette,
RefreshCw,
RotateCw,
Settings2,
ShieldCheck,
SlidersHorizontal,
Terminal,
Wrench,
X,
} from "@lucide/svelte";
import {
applyExternalToolPreset,
defaultExternalToolsSettings,
externalToolPresets,
isExternalToolPresetAvailable,
type ExternalToolKind,
type ExternalToolPreset,
} from "../externalTools";
import type {
AnalyticsSettings,
AppAppearance,
AppLanguage,
AppTheme,
CustomThemeColors,
DetectedExternalTool,
ExternalToolsSettings,
ToolOpenMode,
} from "../types";
import SelectMenu from "./SelectMenu.svelte";
type SettingsPage = "general" | "tools";
interface Props {
analytics: AnalyticsSettings;
onSave: (settings: AnalyticsSettings) => void;
theme: AppTheme;
appearance: AppAppearance;
customTheme: CustomThemeColors;
language: AppLanguage;
autoRefresh: boolean;
externalTools: ExternalToolsSettings;
detectedTools: DetectedExternalTool[];
detectionPending: boolean;
detectionUnavailable: boolean;
onRefreshDetectedTools: () => void | Promise<void>;
onSave: (settings: AnalyticsSettings, theme: AppTheme, appearance: AppAppearance, customTheme: CustomThemeColors, language: AppLanguage, autoRefresh: boolean, externalTools: ExternalToolsSettings) => void;
onClose: () => void;
}
let { analytics, onSave = () => {}, onClose = () => {} }: Props = $props();
let {
analytics,
theme = "system",
appearance = "modern",
customTheme = { background: "#cfd5dc", surface: "#e2e6ea", accent: "#2eb5d1", text: "#222328" },
language = "en",
autoRefresh = true,
externalTools,
detectedTools = [],
detectionPending = false,
detectionUnavailable = false,
onRefreshDetectedTools = () => {},
onSave = () => {},
onClose = () => {},
}: Props = $props();
const toolKinds: ExternalToolKind[] = ["editor", "diff", "merge", "terminal", "fileManager"];
let activePage = $state<SettingsPage>("tools");
let activeToolKind = $state<ExternalToolKind>("editor");
let advancedOpen = $state(false);
let analyticsEnabled = $state(true);
let selectedTheme = $state<AppTheme>("system");
let selectedAppearance = $state<AppAppearance>("modern");
let customColors = $state<CustomThemeColors>({ background: "#cfd5dc", surface: "#e2e6ea", accent: "#2eb5d1", text: "#222328" });
let selectedLanguage = $state<AppLanguage>("en");
let autoRefreshEnabled = $state(true);
let tools = $state<ExternalToolsSettings>(defaultExternalToolsSettings());
const isGerman = $derived(selectedLanguage === "de");
$effect(() => {
analyticsEnabled = analytics.enabled;
selectedTheme = theme;
selectedAppearance = appearance;
customColors = structuredClone(customTheme);
selectedLanguage = language;
autoRefreshEnabled = autoRefresh;
tools = structuredClone(externalTools);
});
function save() {
@@ -21,48 +104,572 @@
...analytics,
enabled: analyticsEnabled,
noticeSeen: true,
}, selectedTheme, selectedAppearance, $state.snapshot(customColors), selectedLanguage, autoRefreshEnabled, $state.snapshot(tools));
}
function resetCustomColors() {
customColors = selectedTheme === "dark"
? { background: "#222328", surface: "#2b2e34", accent: "#2eb5d1", text: "#f0f1f2" }
: { background: "#cfd5dc", surface: "#e2e6ea", accent: "#2eb5d1", text: "#222328" };
}
function cssColorToHex(value: string, fallback: string): string {
const color = value.trim();
if (/^#[0-9a-f]{6}$/i.test(color)) return color.toLowerCase();
if (!color || typeof document === "undefined") return fallback;
const probe = document.createElement("span");
probe.style.color = color;
if (!probe.style.color) return fallback;
probe.style.display = "none";
document.body.appendChild(probe);
const match = getComputedStyle(probe).color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
probe.remove();
if (!match) return fallback;
return `#${match.slice(1, 4).map((part) => Number(part).toString(16).padStart(2, "0")).join("")}`;
}
function currentThemeColors(): CustomThemeColors {
const styles = getComputedStyle(document.documentElement);
const fallback = selectedTheme === "dark"
? { background: "#222328", surface: "#2b2e34", accent: "#2eb5d1", text: "#f0f1f2" }
: { background: "#cfd5dc", surface: "#e2e6ea", accent: "#2eb5d1", text: "#222328" };
return {
background: cssColorToHex(styles.getPropertyValue("--app-bg"), fallback.background),
surface: cssColorToHex(styles.getPropertyValue("--color-surface"), fallback.surface),
accent: cssColorToHex(styles.getPropertyValue("--color-accent"), fallback.accent),
text: cssColorToHex(styles.getPropertyValue("--color-ink"), fallback.text),
};
}
function selectAppearance(next: AppAppearance) {
if (next === "custom" && selectedAppearance !== "custom") customColors = currentThemeColors();
selectedAppearance = next;
}
function toolLabel(kind: ExternalToolKind): string {
const labels = {
editor: "Editor",
diff: isGerman ? "Diff-Tool" : "Diff tool",
merge: isGerman ? "Merge-Tool" : "Merge tool",
terminal: "Terminal",
fileManager: isGerman ? "Dateimanager" : "File manager",
};
return labels[kind];
}
function toolDescription(kind: ExternalToolKind): string {
const descriptions = isGerman
? {
editor: "Öffnet Repositories und einzelne Dateien zum Bearbeiten.",
diff: "Vergleicht eine Arbeitsdatei mit ihrer Version aus HEAD.",
merge: "Übergibt Base, Current, Incoming und Ergebnis an einen 3-Wege-Merger.",
terminal: "Startet eine Shell direkt im Repository-Verzeichnis.",
fileManager: "Öffnet das Repository im bevorzugten Dateimanager.",
}
: {
editor: "Opens repositories and individual files for editing.",
diff: "Compares a working file with its version from HEAD.",
merge: "Passes base, current, incoming, and result to a three-way merger.",
terminal: "Starts a shell directly in the repository directory.",
fileManager: "Opens the repository in your preferred file manager.",
};
return descriptions[kind];
}
function toolUsage(kind: ExternalToolKind): string {
const usage = isGerman
? {
editor: "Oben in der Repository-Leiste oder über das Code-Symbol im Datei-Explorer.",
diff: "Datei im Explorer markieren und das Vergleichs-Symbol anklicken alternativ Rechtsklick auf die Datei.",
merge: "Bei einem Konflikt „Konflikte lösen“ öffnen und anschließend dieses Merge-Tool starten.",
terminal: "Oben in der Repository-Leiste über den Terminal-Button.",
fileManager: "Oben in der Repository-Leiste über den Ordner-Button.",
}
: {
editor: "Use the repository toolbar or the code button in the file explorer.",
diff: "Select a file in Explorer and click the compare button, or right-click the file.",
merge: "Open Resolve conflicts and start this merge tool from the conflict view.",
terminal: "Use the terminal button in the repository toolbar.",
fileManager: "Use the folder button in the repository toolbar.",
};
return usage[kind];
}
function presetAvailable(kind: ExternalToolKind, preset: ExternalToolPreset): boolean {
return isExternalToolPresetAvailable(kind, preset, detectedTools);
}
function availablePresets(kind: ExternalToolKind): ExternalToolPreset[] {
return externalToolPresets[kind].filter((preset) => presetAvailable(kind, preset));
}
function otherPresets(kind: ExternalToolKind): ExternalToolPreset[] {
return externalToolPresets[kind].filter((preset) => !presetAvailable(kind, preset));
}
function selectedPreset(kind: ExternalToolKind): ExternalToolPreset | undefined {
return externalToolPresets[kind].find((preset) => preset.id === tools[kind].preset);
}
function selectedToolName(kind: ExternalToolKind): string {
return tools[kind].preset === "custom"
? tools[kind].program.split(/[\\/]/).pop() || (isGerman ? "Eigenes Programm" : "Custom application")
: selectedPreset(kind)?.label ?? tools[kind].program;
}
function openMode(kind: "diff" | "merge"): ToolOpenMode {
return kind === "diff" ? tools.diffOpenMode : tools.mergeOpenMode;
}
function setOpenMode(kind: "diff" | "merge", mode: ToolOpenMode) {
if (kind === "diff") tools.diffOpenMode = mode;
else tools.mergeOpenMode = mode;
}
function selectionAvailable(kind: ExternalToolKind): boolean {
if (tools[kind].preset === "custom") return tools[kind].program.trim().length > 0;
const preset = selectedPreset(kind);
return preset ? presetAvailable(kind, preset) : false;
}
function selectionStatus(kind: ExternalToolKind): string {
if (tools[kind].preset === "custom") {
return tools[kind].program.trim()
? (isGerman ? "Manuell konfiguriert" : "Manually configured")
: (isGerman ? "Programmpfad fehlt" : "Application path missing");
}
return selectionAvailable(kind)
? (isGerman ? "Installiert und verfügbar" : "Installed and available")
: (isGerman ? "Nicht automatisch erkannt" : "Not automatically detected");
}
function changePreset(kind: ExternalToolKind, id: string) {
if (id === "custom") {
tools[kind] = { ...tools[kind], preset: "custom" };
advancedOpen = true;
return;
}
tools[kind] = applyExternalToolPreset(kind, id, detectedTools);
}
function selectToolKind(kind: ExternalToolKind) {
activeToolKind = kind;
advancedOpen = tools[kind].preset === "custom";
}
function updateProgram(kind: ExternalToolKind, program: string) {
tools[kind] = { ...tools[kind], preset: "custom", program };
}
function updateArgs(kind: ExternalToolKind, value: string) {
tools[kind] = {
...tools[kind],
preset: "custom",
args: value.split("\n").map((arg) => arg.trim()).filter(Boolean),
};
}
async function browseProgram(kind: ExternalToolKind) {
const selected = await open({
title: isGerman ? `${toolLabel(kind)} auswählen` : `Choose ${toolLabel(kind)}`,
multiple: false,
directory: false,
});
if (typeof selected === "string") {
updateProgram(kind, selected);
advancedOpen = true;
}
}
</script>
<div class="dialog-backdrop" role="presentation">
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label="Settings" tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">GitLite</span>
<h2 class="dialog-title">Settings</h2>
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label={isGerman ? "Einstellungen" : "Settings"} tabindex="-1">
<header class="app-settings-head">
<div class="app-settings-title">
<span class="app-settings-mark"><Settings2 size={18} aria-hidden="true" /></span>
<div>
<h2>{isGerman ? "Einstellungen" : "Settings"}</h2>
<p>{isGerman ? "Gitty an deinen Workflow anpassen" : "Make Gitty fit your workflow"}</p>
</div>
</div>
<button class="dialog-close" type="button" onclick={onClose} title="Close">
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"} aria-label={isGerman ? "Einstellungen schließen" : "Close settings"}>
<X size={18} aria-hidden="true" />
</button>
</header>
<form class="app-settings-form" onsubmit={(event) => { event.preventDefault(); save(); }}>
<section class="settings-section">
<header>
<Settings size={16} aria-hidden="true" />
<div>
<span class="eyebrow">Analytics</span>
<h3>Anonymous usage analytics</h3>
<form class="app-settings-shell" onsubmit={(event) => { event.preventDefault(); save(); }}>
<div class="app-settings-body">
<nav class="settings-nav" aria-label={isGerman ? "Einstellungsbereiche" : "Settings sections"}>
<button type="button" class:active={activePage === "general"} onclick={() => { activePage = "general"; }}>
<SlidersHorizontal size={16} aria-hidden="true" />
<span>
<strong>{isGerman ? "Allgemein" : "General"}</strong>
<small>{isGerman ? "Darstellung & Verhalten" : "Appearance & behavior"}</small>
</span>
</button>
<button type="button" class:active={activePage === "tools"} onclick={() => { activePage = "tools"; }}>
<Wrench size={16} aria-hidden="true" />
<span>
<strong>{isGerman ? "Externe Tools" : "External tools"}</strong>
<small>{isGerman ? "Editor, Diff & Terminal" : "Editor, diff & terminal"}</small>
</span>
{#if !detectionUnavailable}<em>{detectedTools.length}</em>{/if}
</button>
<div class="settings-nav-note">
<ShieldCheck size={15} aria-hidden="true" />
<p>{isGerman ? "Tool-Argumente werden direkt und ohne zusätzliche Shell übergeben." : "Tool arguments are passed directly without an extra shell."}</p>
</div>
</header>
</nav>
<label class="settings-toggle-row">
<input type="checkbox" bind:checked={analyticsEnabled} />
<span>
<strong>Allow anonymous Aptabase events</strong>
<small>No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent.</small>
</span>
</label>
</section>
<div class="settings-content">
{#if activePage === "general"}
<div class="settings-page-head">
<div>
<h3>{isGerman ? "Allgemein" : "General"}</h3>
<p>{isGerman ? "Darstellung, Sprache und Hintergrundverhalten." : "Appearance, language, and background behavior."}</p>
</div>
</div>
<div class="new-branch-actions">
<button class="btn-secondary" type="button" onclick={onClose}>Cancel</button>
<button class="btn-primary" type="submit">
<Check size={16} aria-hidden="true" />
Save
</button>
<div class="general-settings-grid">
<section class="general-setting-panel">
<header><Palette size={16} /><div><h4>{isGerman ? "Farbschema" : "Theme"}</h4><p>{isGerman ? "Passend zu deiner Umgebung." : "Match your environment."}</p></div></header>
<div class="settings-segmented" role="radiogroup" aria-label={isGerman ? "Farbschema" : "Theme"}>
<label class:active={selectedTheme === "system"}><input type="radio" bind:group={selectedTheme} value="system" /><span>System</span></label>
<label class:active={selectedTheme === "light"}><input type="radio" bind:group={selectedTheme} value="light" /><span>{isGerman ? "Hell" : "Light"}</span></label>
<label class:active={selectedTheme === "dark"}><input type="radio" bind:group={selectedTheme} value="dark" /><span>{isGerman ? "Dunkel" : "Dark"}</span></label>
</div>
</section>
<section class="general-setting-panel">
<header><SlidersHorizontal size={16} /><div><h4>{isGerman ? "Darstellungsstil" : "Design style"}</h4><p>{isGerman ? "Aktuell, klassisch oder selbst gestaltet." : "Current, classic, or designed by you."}</p></div></header>
<div class="settings-segmented" role="radiogroup" aria-label={isGerman ? "Darstellungsstil" : "Design style"}>
<label class:active={selectedAppearance === "modern"}><input type="radio" name="appearance" value="modern" checked={selectedAppearance === "modern"} onchange={() => selectAppearance("modern")} /><span>{isGerman ? "Aktuell" : "Modern"}</span></label>
<label class:active={selectedAppearance === "classic"}><input type="radio" name="appearance" value="classic" checked={selectedAppearance === "classic"} onchange={() => selectAppearance("classic")} /><span>{isGerman ? "Klassisch" : "Classic"}</span></label>
<label class:active={selectedAppearance === "custom"}><input type="radio" name="appearance" value="custom" checked={selectedAppearance === "custom"} onchange={() => selectAppearance("custom")} /><span>{isGerman ? "Eigene" : "Custom"}</span></label>
</div>
</section>
{#if selectedAppearance === "custom"}
<section class="general-setting-panel general-setting-wide custom-theme-panel">
<header>
<Palette size={16} />
<div><h4>{isGerman ? "Theme-Generator" : "Theme generator"}</h4><p>{isGerman ? "Erstelle dein eigenes Farbprofil." : "Create your own color profile."}</p></div>
<button class="theme-reset-button" type="button" onclick={resetCustomColors}><RotateCw size={13} />{isGerman ? "Zurücksetzen" : "Reset"}</button>
</header>
<div
class="theme-preview"
style={`--preview-bg:${customColors.background};--preview-surface:${customColors.surface};--preview-accent:${customColors.accent};--preview-text:${customColors.text};`}
aria-label={isGerman ? "Vorschau des eigenen Themes" : "Custom theme preview"}
>
<span class="theme-preview-sidebar"></span>
<span class="theme-preview-content"><i></i><b></b><em></em></span>
</div>
<div class="theme-color-grid">
<label><span>{isGerman ? "Hintergrund" : "Background"}</span><input type="color" bind:value={customColors.background} aria-label={isGerman ? "Hintergrundfarbe" : "Background color"} /><code>{customColors.background}</code></label>
<label><span>{isGerman ? "Fläche" : "Surface"}</span><input type="color" bind:value={customColors.surface} aria-label={isGerman ? "Flächenfarbe" : "Surface color"} /><code>{customColors.surface}</code></label>
<label><span>{isGerman ? "Akzent" : "Accent"}</span><input type="color" bind:value={customColors.accent} aria-label={isGerman ? "Akzentfarbe" : "Accent color"} /><code>{customColors.accent}</code></label>
<label><span>{isGerman ? "Schrift" : "Text"}</span><input type="color" bind:value={customColors.text} aria-label={isGerman ? "Schriftfarbe" : "Text color"} /><code>{customColors.text}</code></label>
</div>
<p class="theme-generator-note">{isGerman ? "Die Farben werden beim Speichern auf die gesamte Oberfläche angewendet." : "The colors are applied across the interface when you save."}</p>
</section>
{/if}
<section class="general-setting-panel">
<header><Languages size={16} /><div><h4>{isGerman ? "Sprache" : "Language"}</h4><p>{isGerman ? "Sprache der Oberfläche." : "Language used by the interface."}</p></div></header>
<div class="settings-segmented settings-language" role="radiogroup" aria-label={isGerman ? "App-Sprache" : "App language"}>
<label class:active={selectedLanguage === "en"}><input type="radio" bind:group={selectedLanguage} value="en" /><span>EN · English</span></label>
<label class:active={selectedLanguage === "de"}><input type="radio" bind:group={selectedLanguage} value="de" /><span>DE · Deutsch</span></label>
</div>
</section>
<section class="general-setting-panel general-setting-wide">
<header><RefreshCw size={16} /><div><h4>{isGerman ? "Repository-Aktualisierung" : "Repository refresh"}</h4><p>{isGerman ? "Arbeitsbereich und Remotes aktuell halten." : "Keep the working tree and remotes current."}</p></div></header>
<label class="settings-switch-row">
<span><strong>{isGerman ? "Automatisch aktualisieren" : "Refresh automatically"}</strong><small>{isGerman ? "Branch-Status und Änderungen regelmäßig im Hintergrund prüfen." : "Periodically check branch state and working-tree changes."}</small></span>
<input type="checkbox" bind:checked={autoRefreshEnabled} />
</label>
</section>
<section class="general-setting-panel general-setting-wide">
<header><ShieldCheck size={16} /><div><h4>{isGerman ? "Datenschutz" : "Privacy"}</h4><p>{isGerman ? "Anonyme Produkt- und Fehlerdiagnose." : "Anonymous product and error diagnostics."}</p></div></header>
<label class="settings-switch-row">
<span><strong>{isGerman ? "Anonyme Analytics erlauben" : "Allow anonymous analytics"}</strong><small>{isGerman ? "Keine Pfade, Remotes, Branches, Diffs, Zugangsdaten oder Quelltexte." : "No paths, remotes, branches, diffs, credentials, or source code."}</small></span>
<input type="checkbox" bind:checked={analyticsEnabled} />
</label>
</section>
</div>
{:else}
<div class="settings-page-head tools-page-head">
<div>
<h3>{isGerman ? "Externe Tools" : "External tools"}</h3>
<p>
{detectionUnavailable
? (isGerman ? "Automatische Erkennung ist in dieser Umgebung nicht verfügbar." : "Automatic detection is unavailable in this environment.")
: (isGerman ? `${detectedTools.length} installierte Programme erkannt.` : `${detectedTools.length} installed applications detected.`)}
</p>
</div>
<button class="tool-rescan-button" type="button" onclick={onRefreshDetectedTools} disabled={detectionPending}>
<RotateCw class={detectionPending ? "spin" : ""} size={14} aria-hidden="true" />
{detectionPending ? (isGerman ? "Erkennung läuft…" : "Detecting…") : (isGerman ? "Neu erkennen" : "Detect again")}
</button>
</div>
<div class="tool-kind-tabs" role="tablist" aria-label={isGerman ? "Tool-Kategorie" : "Tool category"}>
{#each toolKinds as kind}
<button type="button" role="tab" aria-selected={activeToolKind === kind} class:active={activeToolKind === kind} onclick={() => selectToolKind(kind)}>
{#if kind === "editor"}<Code2 size={16} />
{:else if kind === "diff"}<GitCompare size={16} />
{:else if kind === "merge"}<GitMerge size={16} />
{:else if kind === "terminal"}<Terminal size={16} />
{:else}<FolderOpen size={16} />{/if}
<span>{toolLabel(kind)}</span>
<small class:available={selectionAvailable(kind)}></small>
</button>
{/each}
</div>
<section class="tool-config-panel" aria-label={`${toolLabel(activeToolKind)} ${isGerman ? "konfigurieren" : "configuration"}`}>
<div class="tool-config-summary">
<span class="tool-config-icon">
{#if activeToolKind === "editor"}<Code2 size={22} />
{:else if activeToolKind === "diff"}<GitCompare size={22} />
{:else if activeToolKind === "merge"}<GitMerge size={22} />
{:else if activeToolKind === "terminal"}<Terminal size={22} />
{:else}<FolderOpen size={22} />{/if}
</span>
<div>
<h4>{toolLabel(activeToolKind)}</h4>
<p>{toolDescription(activeToolKind)}</p>
</div>
<span class="tool-status" class:available={selectionAvailable(activeToolKind)}>
{#if selectionAvailable(activeToolKind)}<CheckCircle2 size={13} />{:else}<CircleDashed size={13} />{/if}
{selectionStatus(activeToolKind)}
</span>
</div>
{#if activeToolKind === "diff" || activeToolKind === "merge"}
{@const openModeKind = activeToolKind as "diff" | "merge"}
<div class="tool-route" aria-label={isGerman ? "Aktuelle Standardansicht" : "Current default view"}>
<span>{isGerman ? "Standard" : "Default"}</span><ChevronRight size={15} aria-hidden="true" />
<strong>{openMode(openModeKind) === "gitty" ? (isGerman ? "Gitty · integriert" : "Gitty · built in") : selectedToolName(activeToolKind)}</strong>
</div>
<fieldset class="tool-open-mode">
<legend>{isGerman ? "Beim Öffnen verwenden" : "Use when opening"}</legend>
<div>
<button type="button" class:active={openMode(openModeKind) === "gitty"} aria-pressed={openMode(openModeKind) === "gitty"} onclick={() => setOpenMode(openModeKind, "gitty")}>
<span>Gitty</span><small>{isGerman ? "Integrierte Ansicht" : "Built-in view"}</small>
</button>
<button type="button" class:active={openMode(openModeKind) === "external"} aria-pressed={openMode(openModeKind) === "external"} onclick={() => setOpenMode(openModeKind, "external")}>
<span>{selectedToolName(activeToolKind)}</span><small>{isGerman ? "Externes Programm" : "External application"}</small>
</button>
</div>
</fieldset>
{:else}
<div class="tool-route" aria-label={isGerman ? "Aktuelle Standardzuordnung" : "Current default mapping"}>
<span>Gitty</span><ChevronRight size={15} aria-hidden="true" /><strong>{selectedToolName(activeToolKind)}</strong>
</div>
{/if}
<label class="tool-default-field">
<span>{isGerman ? `Standardprogramm für ${toolLabel(activeToolKind)}` : `Default application for ${toolLabel(activeToolKind)}`}</span>
<SelectMenu
class="tool-preset-select"
value={tools[activeToolKind].preset}
options={[
...availablePresets(activeToolKind).map((preset) => ({ value: preset.id, label: `Installed - ${preset.label}`, group: isGerman ? "Installiert" : "Installed" })),
...otherPresets(activeToolKind).map((preset) => ({ value: preset.id, label: preset.label, group: isGerman ? "Weitere unterstützte Programme" : "Other supported applications" })),
{ value: "custom", label: isGerman ? "Eigenes Programm auswählen..." : "Choose a custom application..." },
]}
ariaLabel={isGerman ? `Standardprogramm für ${toolLabel(activeToolKind)}` : `Default application for ${toolLabel(activeToolKind)}`}
onChange={(value) => changePreset(activeToolKind, value)}
/>
<small>{isGerman ? "Diese Auswahl wird gespeichert und für alle passenden Aktionen verwendet." : "This selection is saved and used for every matching action."}</small>
</label>
<div class="tool-usage-callout">
<span>{isGerman ? "So öffnest du es" : "How to open it"}</span>
<p>{toolUsage(activeToolKind)}</p>
</div>
<button class="tool-advanced-toggle" type="button" aria-expanded={advancedOpen} onclick={() => { advancedOpen = !advancedOpen; }}>
<span>{isGerman ? "Programmpfad und Argumente" : "Application path and arguments"}</span>
{#if advancedOpen}<ChevronDown size={15} />{:else}<ChevronRight size={15} />{/if}
</button>
{#if advancedOpen}
<div class="tool-advanced-panel">
<label>
<span>{isGerman ? "Programmpfad" : "Application path"}</span>
<div class="tool-program-row">
<input value={tools[activeToolKind].program} oninput={(event) => updateProgram(activeToolKind, event.currentTarget.value)} spellcheck="false" />
<button type="button" onclick={() => browseProgram(activeToolKind)} title={isGerman ? "Programm auswählen" : "Choose application"} aria-label={isGerman ? "Programm auswählen" : "Choose application"}><FolderOpen size={15} /></button>
</div>
</label>
<label>
<span>{isGerman ? "Argumente · eine Zeile pro Argument" : "Arguments · one per line"}</span>
<textarea value={tools[activeToolKind].args.join("\n")} oninput={(event) => updateArgs(activeToolKind, event.currentTarget.value)} spellcheck="false"></textarea>
</label>
<p class="tool-placeholders">
<span>{isGerman ? "Verfügbare Platzhalter" : "Available placeholders"}</span>
<code>{"{repo}"}</code><code>{"{file}"}</code><code>{"{parent}"}</code><code>{"{left}"}</code><code>{"{right}"}</code><code>{"{base}"}</code><code>{"{ours}"}</code><code>{"{theirs}"}</code><code>{"{result}"}</code>
</p>
</div>
{/if}
</section>
{/if}
</div>
</div>
<footer class="app-settings-footer">
<span>{isGerman ? "Änderungen werden erst beim Speichern übernommen." : "Changes are applied only after saving."}</span>
<div>
<button class="btn-secondary" type="button" onclick={onClose}>{isGerman ? "Abbrechen" : "Cancel"}</button>
<button class="btn-primary" type="submit"><Check size={16} aria-hidden="true" />{isGerman ? "Änderungen speichern" : "Save changes"}</button>
</div>
</footer>
</form>
</div>
</div>
<style>
.app-settings-dialog { display: grid; grid-template-rows: auto minmax(0, 1fr); width: min(920px, calc(100vw - 32px)); height: min(720px, calc(100vh - 32px)); overflow: hidden; }
.app-settings-head { display: flex; align-items: center; justify-content: space-between; min-height: 70px; padding: 14px 18px; border-bottom: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.app-settings-title { display: flex; align-items: center; gap: 12px; min-width: 0; }
.app-settings-mark { display: grid; place-items: center; width: 38px; height: 38px; border: 1px solid color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 9%, transparent); }
.app-settings-title h2, .settings-page-head h3, .tool-config-summary h4, .general-setting-panel h4 { margin: 0; color: var(--color-ink); }
.app-settings-title h2 { font-size: 18px; line-height: 1.2; }
.app-settings-title p, .settings-page-head p, .tool-config-summary p, .general-setting-panel p { margin: 0; color: var(--color-ink-dim); }
.app-settings-title p { margin-top: 3px; font-size: 11px; }
.app-settings-shell { display: grid; min-height: 0; grid-template-rows: minmax(0, 1fr) auto; }
.app-settings-body { display: grid; min-height: 0; grid-template-columns: 205px minmax(0, 1fr); }
.settings-nav { display: flex; flex-direction: column; gap: 6px; min-width: 0; padding: 14px 12px; border-right: 1px solid var(--color-border-subtle); background: color-mix(in srgb, var(--app-dialog-chrome) 72%, var(--app-dialog-bg)); }
.settings-nav > button { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 10px; width: 100%; min-height: 50px; padding: 8px 10px; border: 1px solid transparent; border-radius: 8px; color: var(--color-ink-dim); background: transparent; text-align: left; }
.settings-nav > button:hover { color: var(--color-ink); background: var(--color-surface-hover); }
.settings-nav > button.active { border-color: color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); color: var(--color-ink); background: color-mix(in srgb, var(--color-accent) 9%, var(--color-surface-raised)); box-shadow: inset 2px 0 0 var(--color-accent); }
.settings-nav button > :global(svg) { color: var(--color-ink-muted); }
.settings-nav button.active > :global(svg) { color: var(--color-accent); }
.settings-nav button span { display: grid; min-width: 0; gap: 2px; }
.settings-nav button strong { font-size: 12px; }
.settings-nav button small { overflow: hidden; color: var(--color-ink-faint); font-size: 9.5px; text-overflow: ellipsis; white-space: nowrap; }
.settings-nav button em { display: grid; place-items: center; min-width: 21px; height: 20px; padding-inline: 5px; border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 12%, transparent); font-size: 9px; font-style: normal; font-weight: 800; }
.settings-nav-note { display: flex; align-items: flex-start; gap: 8px; margin-top: auto; padding: 10px; border-top: 1px solid var(--color-border-subtle); color: var(--color-ink-faint); }
.settings-nav-note :global(svg) { flex: 0 0 auto; margin-top: 1px; color: var(--color-success); }
.settings-nav-note p { margin: 0; font-size: 9.5px; line-height: 1.45; }
.settings-content { min-width: 0; overflow: auto; padding: 18px 20px 22px; }
.settings-page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
.settings-page-head h3 { font-size: 18px; }
.settings-page-head p { margin-top: 4px; font-size: 11px; line-height: 1.45; }
.tool-rescan-button { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; min-height: 30px; padding: 0 10px; border: 1px solid var(--color-border); border-radius: 7px; color: var(--color-ink-muted); background: var(--color-surface-raised); font-size: 10px; font-weight: 750; }
.tool-rescan-button:hover:not(:disabled) { color: var(--color-ink); border-color: var(--color-border-input); background: var(--color-surface-hover); }
.tool-kind-tabs { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 6px; margin-bottom: 14px; padding: 4px; border: 1px solid var(--color-border-subtle); border-radius: 10px; background: var(--app-settings-row-bg); }
.tool-kind-tabs button { position: relative; display: flex; align-items: center; justify-content: center; gap: 7px; min-width: 0; height: 38px; padding: 0 8px; border: 1px solid transparent; border-radius: 7px; color: var(--color-ink-dim); background: transparent; font-size: 10.5px; font-weight: 750; }
.tool-kind-tabs button:hover { color: var(--color-ink); background: var(--color-surface-hover); }
.tool-kind-tabs button.active { border-color: var(--color-border-input); color: var(--color-ink); background: var(--color-surface-raised); box-shadow: 0 2px 8px rgba(0,0,0,.12); }
.tool-kind-tabs button.active :global(svg) { color: var(--color-accent); }
.tool-kind-tabs button small { position: absolute; top: 5px; right: 6px; width: 5px; height: 5px; border-radius: 50%; background: var(--color-ink-faint); }
.tool-kind-tabs button small.available { background: var(--color-success); box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-success) 15%, transparent); }
.tool-config-panel { display: grid; gap: 14px; padding: 16px; border: 1px solid var(--color-border-subtle); border-radius: 12px; background: color-mix(in srgb, var(--app-settings-row-bg) 72%, transparent); }
.tool-config-summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 11px; }
.tool-config-icon { display: grid; place-items: center; width: 42px; height: 42px; border: 1px solid color-mix(in srgb, var(--color-accent) 25%, var(--color-border)); border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 8%, transparent); }
.tool-config-summary h4 { font-size: 14px; }
.tool-config-summary p { margin-top: 3px; font-size: 10.5px; line-height: 1.4; }
.tool-status { display: inline-flex; align-items: center; gap: 5px; padding: 5px 7px; border: 1px solid var(--color-border-subtle); border-radius: 6px; color: var(--color-ink-faint); font-size: 9px; font-weight: 750; }
.tool-status.available { border-color: color-mix(in srgb, var(--color-success) 24%, var(--color-border)); color: var(--color-success); background: color-mix(in srgb, var(--color-success) 6%, transparent); }
.tool-route { display: flex; align-items: center; gap: 8px; min-height: 34px; padding: 7px 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; color: var(--color-ink-faint); background: var(--color-surface-raised); font-size: 10.5px; }
.tool-route strong { color: var(--color-ink); }
.tool-open-mode { display: grid; gap: 6px; min-width: 0; margin: 0; padding: 0; border: 0; }
.tool-open-mode legend { margin-bottom: 6px; color: var(--color-ink-muted); font-size: 10.5px; font-weight: 750; }
.tool-open-mode > div { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; padding: 4px; border: 1px solid var(--color-border-subtle); border-radius: 9px; background: var(--color-surface-raised); }
.tool-open-mode button { display: grid; justify-items: start; gap: 2px; min-width: 0; min-height: 46px; padding: 7px 10px; border: 1px solid transparent; border-radius: 7px; color: var(--color-ink-dim); background: transparent; text-align: left; }
.tool-open-mode button:hover { color: var(--color-ink); background: var(--color-surface-hover); }
.tool-open-mode button.active { border-color: color-mix(in srgb, var(--color-accent) 38%, var(--color-border)); color: var(--color-ink); background: color-mix(in srgb, var(--color-accent) 9%, var(--color-surface-hover)); box-shadow: inset 2px 0 0 var(--color-accent); }
.tool-open-mode button span { max-width: 100%; overflow: hidden; font-size: 11px; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
.tool-open-mode button small { color: var(--color-ink-faint); font-size: 9px; font-weight: 550; }
.tool-default-field, .tool-advanced-panel label { display: grid; gap: 6px; min-width: 0; color: var(--color-ink-muted); font-size: 10.5px; font-weight: 750; }
.tool-advanced-panel input, .tool-advanced-panel textarea { width: 100%; min-width: 0; border: 1px solid var(--color-border); border-radius: 7px; color: var(--color-ink); background: var(--color-surface-raised); font: inherit; }
:global(.tool-preset-select .select-menu-trigger) { height: 38px; min-height: 38px; padding: 0 11px; border-color: var(--color-border); font-size: 12px; font-weight: 700; }
.tool-default-field small { color: var(--color-ink-faint); font-size: 9.5px; font-weight: 500; }
.tool-usage-callout { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: 10px; padding: 10px 11px; border-left: 2px solid var(--color-accent); border-radius: 0 7px 7px 0; background: color-mix(in srgb, var(--color-accent) 6%, transparent); }
.tool-usage-callout span { color: var(--color-accent); font-size: 9.5px; font-weight: 850; text-transform: uppercase; letter-spacing: .04em; }
.tool-usage-callout p { margin: 0; color: var(--color-ink-muted); font-size: 10.5px; line-height: 1.45; }
.tool-advanced-toggle { display: flex; align-items: center; justify-content: space-between; min-height: 32px; padding: 0; border: 0; border-top: 1px solid var(--color-border-subtle); color: var(--color-ink-dim); background: transparent; font-size: 10.5px; font-weight: 750; }
.tool-advanced-toggle:hover { color: var(--color-ink); }
.tool-advanced-panel { display: grid; gap: 11px; padding-top: 2px; }
.tool-program-row { display: grid; grid-template-columns: minmax(0, 1fr) 34px; gap: 6px; }
.tool-advanced-panel input { height: 34px; padding: 0 9px; font-family: var(--font-mono); font-size: 10.5px; }
.tool-advanced-panel textarea { min-height: 80px; padding: 8px 9px; resize: vertical; font-family: var(--font-mono); font-size: 10.5px; line-height: 1.45; }
.tool-program-row button { display: grid; place-items: center; border: 1px solid var(--color-border); border-radius: 7px; color: var(--color-ink-muted); background: var(--color-surface-raised); }
.tool-program-row button:hover { color: var(--color-ink); border-color: var(--color-border-input); background: var(--color-surface-hover); }
.tool-placeholders { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin: 0; color: var(--color-ink-faint); font-size: 9px; }
.tool-placeholders span { margin-right: 3px; }
.tool-placeholders code { padding: 2px 4px; border-radius: 4px; color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 9%, transparent); }
.general-settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.general-setting-panel { display: grid; align-content: start; gap: 14px; min-width: 0; padding: 14px; border: 1px solid var(--color-border-subtle); border-radius: 10px; background: var(--app-settings-row-bg); }
.general-setting-panel.general-setting-wide { grid-column: 1 / -1; }
.general-setting-panel > header { display: flex; align-items: flex-start; gap: 9px; }
.general-setting-panel > header > :global(svg) { flex: 0 0 auto; margin-top: 1px; color: var(--color-accent); }
.general-setting-panel h4 { font-size: 12.5px; }
.general-setting-panel p { margin-top: 3px; font-size: 9.5px; }
.settings-segmented { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; padding: 3px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); }
.settings-segmented.settings-language { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.settings-segmented label { display: flex; align-items: center; justify-content: center; min-height: 31px; border: 1px solid transparent; border-radius: 6px; color: var(--color-ink-dim); font-size: 10.5px; font-weight: 750; }
.settings-segmented label.active { border-color: var(--color-border-input); color: var(--color-ink); background: var(--color-surface-hover); }
.settings-segmented input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.custom-theme-panel > header { align-items: center; }
.custom-theme-panel > header > div { min-width: 0; }
.theme-reset-button { display: inline-flex; align-items: center; gap: 5px; min-height: 27px; margin-left: auto; padding: 0 8px; border: 1px solid var(--color-border); color: var(--color-ink-muted); background: var(--color-surface-raised); font-size: 9.5px; font-weight: 750; }
.theme-reset-button:hover:not(:disabled) { border-color: var(--color-border-input); color: var(--color-ink); background: var(--color-surface-hover); }
.theme-preview { display: grid; grid-template-columns: 28% 1fr; min-height: 78px; overflow: hidden; border: 1px solid color-mix(in srgb, var(--preview-text) 30%, var(--preview-surface)); background: var(--preview-bg); }
.theme-preview-sidebar { border-right: 1px solid color-mix(in srgb, var(--preview-text) 24%, var(--preview-surface)); background: color-mix(in srgb, var(--preview-surface) 86%, var(--preview-bg)); }
.theme-preview-content { display: grid; grid-template-columns: 1fr auto; align-content: start; gap: 8px; margin: 10px; padding: 10px; border: 1px solid color-mix(in srgb, var(--preview-text) 22%, var(--preview-surface)); color: var(--preview-text); background: var(--preview-surface); }
.theme-preview-content i { display: block; width: 54%; height: 7px; background: var(--preview-text); opacity: .82; }
.theme-preview-content b { display: block; width: 34px; height: 18px; grid-row: 1 / 3; grid-column: 2; background: var(--preview-accent); }
.theme-preview-content em { display: block; width: 76%; height: 5px; background: var(--preview-text); opacity: .32; }
.theme-color-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
.theme-color-grid label { display: grid; grid-template-columns: minmax(0, 1fr) 32px auto; align-items: center; gap: 8px; min-width: 0; min-height: 38px; padding: 5px 7px; border: 1px solid var(--color-border-subtle); background: var(--color-surface-raised); }
.theme-color-grid label > span { color: var(--color-ink-muted); font-size: 10px; font-weight: 750; }
.theme-color-grid input[type="color"] { width: 32px; height: 25px; padding: 2px; border: 1px solid var(--color-border-input); background: transparent; cursor: pointer; }
.theme-color-grid code { color: var(--color-ink-faint); font: 9px var(--font-mono); text-transform: uppercase; }
.theme-generator-note { margin: -5px 0 0 !important; color: var(--color-ink-faint) !important; }
.settings-switch-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 14px; }
.settings-switch-row span { display: grid; gap: 3px; }
.settings-switch-row strong { color: var(--color-ink); font-size: 11px; }
.settings-switch-row small { color: var(--color-ink-dim); font-size: 9.5px; line-height: 1.4; }
.settings-switch-row input { width: 32px; height: 18px; accent-color: var(--color-accent); }
.app-settings-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 62px; padding: 11px 16px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.app-settings-footer > span { color: var(--color-ink-faint); font-size: 9.5px; }
.app-settings-footer > div { display: flex; gap: 8px; }
.app-settings-footer button { min-height: 32px; }
@media (max-width: 760px) {
.app-settings-dialog { height: min(760px, calc(100vh - 20px)); width: min(660px, calc(100vw - 20px)); }
.app-settings-body { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
.settings-nav { flex-direction: row; padding: 8px 10px; border-right: 0; border-bottom: 1px solid var(--color-border-subtle); }
.settings-nav > button { width: auto; min-width: 0; flex: 1 1 0; min-height: 42px; }
.settings-nav-note { display: none; }
.settings-content { padding: 14px; }
.tool-kind-tabs { grid-template-columns: repeat(5, minmax(42px, 1fr)); overflow-x: auto; }
.tool-kind-tabs button { height: 40px; }
.tool-kind-tabs button span { display: none; }
.tool-config-summary { grid-template-columns: auto minmax(0, 1fr); }
.tool-status { grid-column: 1 / -1; justify-self: start; }
.app-settings-footer > span { display: none; }
.app-settings-footer { justify-content: flex-end; }
}
@media (max-width: 520px) {
.app-settings-head { min-height: 58px; padding: 10px 12px; }
.app-settings-mark { width: 34px; height: 34px; }
.settings-nav button small, .settings-nav button em { display: none; }
.settings-nav > button { grid-template-columns: auto minmax(0, 1fr); }
.settings-page-head { align-items: stretch; flex-direction: column; }
.tool-rescan-button { align-self: flex-start; }
.general-settings-grid { grid-template-columns: 1fr; }
.general-setting-panel.general-setting-wide { grid-column: auto; }
.theme-color-grid { grid-template-columns: 1fr; }
.tool-config-panel { padding: 13px; }
.tool-usage-callout { grid-template-columns: 1fr; gap: 4px; }
}
</style>
+124 -24
View File
@@ -1,7 +1,12 @@
<script lang="ts">
import { FileCode, LoaderCircle, X } from "@lucide/svelte";
import { FileCode, LoaderCircle, Search, X } from "@lucide/svelte";
import type { GitBlameLine } from "../types";
const commitDateFormatter = new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
});
interface BlameGroup {
id: string;
hash: string;
@@ -14,6 +19,11 @@
lines: GitBlameLine[];
}
interface TextSegment {
text: string;
matched: boolean;
}
interface Props {
filePath: string;
lines: GitBlameLine[];
@@ -32,6 +42,8 @@
onClose = () => {},
}: Props = $props();
let blameSearch = $state("");
function groupBlameLines(source: GitBlameLine[]): BlameGroup[] {
const groups: BlameGroup[] = [];
for (const line of source) {
@@ -59,7 +71,7 @@
if (!seconds) return "";
const date = new Date(seconds * 1000);
if (Number.isNaN(date.getTime())) return "";
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
return commitDateFormatter.format(date);
}
function groupTooltip(group: BlameGroup): string {
@@ -67,7 +79,56 @@
return `${group.authorName} <${group.authorEmail}>\n${group.summary}\n${group.hash}`;
}
let groups = $derived(groupBlameLines(lines));
function blameLineSearchText(line: GitBlameLine): string {
return [
line.line_number,
line.content,
line.commit_hash,
line.short_hash,
line.author_name,
line.author_email,
formatBlameDate(line.author_time),
line.summary,
line.is_uncommitted ? "uncommitted not committed yet" : "",
].join("\n").toLowerCase();
}
function lineMatchesSearch(line: GitBlameLine, query: string): boolean {
return !query || blameLineSearchText(line).includes(query);
}
function textSegments(value: string | number): TextSegment[] {
const text = String(value ?? "");
const query = blameSearch.trim();
if (!query || !text) return [{ text, matched: false }];
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const segments: TextSegment[] = [];
let cursor = 0;
let matchIndex = lowerText.indexOf(lowerQuery);
while (matchIndex !== -1) {
if (matchIndex > cursor) {
segments.push({ text: text.slice(cursor, matchIndex), matched: false });
}
const end = matchIndex + query.length;
segments.push({ text: text.slice(matchIndex, end), matched: true });
cursor = end;
matchIndex = lowerText.indexOf(lowerQuery, cursor);
}
if (cursor < text.length) {
segments.push({ text: text.slice(cursor), matched: false });
}
return segments.length > 0 ? segments : [{ text, matched: false }];
}
let searchQuery = $derived(blameSearch.trim().toLowerCase());
let searchActive = $derived(searchQuery.length > 0);
let visibleLines = $derived(searchActive ? lines.filter((line) => lineMatchesSearch(line, searchQuery)) : lines);
let groups = $derived(groupBlameLines(visibleLines));
</script>
<div class="dialog-backdrop" role="presentation">
@@ -93,13 +154,28 @@
</div>
{:else if error}
<div class="blank-state">{error}</div>
{:else if groups.length === 0}
{:else if lines.length === 0}
<div class="blank-state">No blame information available for this file.</div>
{:else}
<div class="diff-header blame-code-header">
<FileCode size={13} aria-hidden="true" />
<span title={filePath}>{filePath}</span>
<strong>{lines.length} lines</strong>
<strong>{searchActive ? `${visibleLines.length} of ${lines.length} lines` : `${lines.length} lines`}</strong>
</div>
<div class="blame-search-bar">
<Search size={14} aria-hidden="true" />
<input
bind:value={blameSearch}
autocomplete="off"
spellcheck="false"
placeholder="Search blame"
aria-label="Search blame"
/>
{#if searchActive}
<button class="btn-sm blame-search-clear" type="button" onclick={() => { blameSearch = ""; }} aria-label="Clear blame search">
<X size={14} aria-hidden="true" />
</button>
{/if}
</div>
<div class="split-col-headers blame-column-headers">
<div class="split-col-label blame-commit-col-label">Commit</div>
@@ -107,26 +183,50 @@
</div>
<div class="split-diff blame-diff" role="table" aria-label="File blame">
<div class="split-pane blame-scroll">
<div class="blame-code-table">
{#each groups as group (group.id)}
<div class="blame-group" class:uncommitted={group.isUncommitted} title={groupTooltip(group)}>
<div class="blame-meta">
<span class="blame-hash">{group.isUncommitted ? "Uncommitted" : group.shortHash}</span>
<span class="blame-author">{group.isUncommitted ? "Not committed yet" : group.authorName}</span>
<span class="blame-summary">{group.summary || "No commit message"}</span>
{#if !group.isUncommitted}
<span class="blame-date">{formatBlameDate(group.authorTime)}</span>
{/if}
{#if groups.length === 0}
<div class="blank-state">No matches found.</div>
{:else}
<div class="blame-code-table">
{#each groups as group (group.id)}
<div class="blame-group" class:uncommitted={group.isUncommitted} title={groupTooltip(group)}>
<div class="blame-meta">
<span class="blame-hash">
{#each textSegments(group.isUncommitted ? "Uncommitted" : group.shortHash) as segment, index (`hash-${group.id}-${index}`)}
{#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
{/each}
</span>
<span class="blame-author">
{#each textSegments(group.isUncommitted ? "Not committed yet" : group.authorName) as segment, index (`author-${group.id}-${index}`)}
{#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
{/each}
</span>
<span class="blame-summary">
{#each textSegments(group.summary || "No commit message") as segment, index (`summary-${group.id}-${index}`)}
{#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
{/each}
</span>
{#if !group.isUncommitted}
<span class="blame-date">
{#each textSegments(formatBlameDate(group.authorTime)) as segment, index (`date-${group.id}-${index}`)}
{#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
{/each}
</span>
{/if}
</div>
<div class="split-pane-grid blame-lines">
{#each group.lines as line (line.line_number)}
<span class="split-num blame-line-number">{line.line_number}</span>
<code class="split-cell blame-line-code">
{#each textSegments(line.content) as segment, index (`line-${line.line_number}-${index}`)}
{#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
{/each}
</code>
{/each}
</div>
</div>
<div class="split-pane-grid blame-lines">
{#each group.lines as line (line.line_number)}
<span class="split-num blame-line-number">{line.line_number}</span>
<code class="split-cell blame-line-code">{line.content}</code>
{/each}
</div>
</div>
{/each}
</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
@@ -18,16 +18,24 @@
onClose = () => {},
}: Props = $props();
let title = $derived(force ? "Force delete branch?" : "Delete branch?");
let title = $derived(branch.remote ? "Delete remote branch?" : force ? "Force delete branch?" : "Delete branch?");
let remoteParts = $derived(branch.remote ? branch.name.split(/\/(.+)/) : []);
let branchName = $derived(branch.remote ? remoteParts[1] || branch.name : branch.name);
let branchLocation = $derived(branch.remote ? remoteParts[0] || "Remote" : "Local repository");
</script>
<div class="dialog-backdrop" role="presentation">
<div class="dialog branch-delete-dialog" role="dialog" aria-modal="true" aria-label={title}>
<header class="dialog-header">
<div>
<span class="eyebrow">{force ? "Force delete" : "Delete branch"}</span>
<p class="dialog-title">{title}</p>
<div class="dialog branch-delete-dialog" role="dialog" aria-modal="true" aria-labelledby="branch-delete-title">
<header class="dialog-header branch-delete-header">
<div class="branch-delete-heading">
<span class:force class="branch-delete-heading-icon" aria-hidden="true">
<Trash2 size={16} />
</span>
<div>
<span class="eyebrow">{branch.remote ? "Remote branch" : force ? "Force delete" : "Delete branch"}</span>
<p class="dialog-title" id="branch-delete-title">{title}</p>
</div>
</div>
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
<X size={16} aria-hidden="true" />
@@ -35,27 +43,35 @@
</header>
<div class="branch-delete-body">
<div class="discard-warning-icon" aria-hidden="true">
<div class:force class="discard-warning-icon branch-delete-warning-icon" aria-hidden="true">
<AlertTriangle size={22} />
</div>
<div class="discard-confirm-copy">
<p>
{#if force}
This branch is not fully merged. Force deleting removes the branch pointer even if some commits are only reachable from this branch.
<p class="branch-delete-lead">
{#if branch.remote}
This branch will be removed from the shared remote repository.
{:else if force}
This branch is not fully merged. Some commits may only exist here.
{:else}
Delete this local branch from the repository?
This branch will be removed from your local repository.
{/if}
</p>
<code class="discard-target" title={branch.name}>
<GitBranch size={13} aria-hidden="true" />
{branch.name}
</code>
<div class="branch-delete-target" title={branch.name}>
<span class="branch-delete-target-icon" aria-hidden="true"><GitBranch size={16} /></span>
<span class="branch-delete-target-copy">
<code>{branchName}</code>
<span>{branchLocation}</span>
</span>
<span class:remote={branch.remote} class="branch-delete-scope">{branch.remote ? "Remote" : "Local"}</span>
</div>
<p class="discard-warning-text">
{#if force}
Make sure you no longer need the unique commits on this branch.
{#if branch.remote}
This affects everyone using <strong>{remoteParts[0] || "the remote"}</strong>. Your local branch is kept.
{:else if force}
Force deletion can make unmerged commits difficult to recover.
{:else}
Git will refuse if the branch is not fully merged.
Git will stop the deletion if the branch contains unmerged commits.
{/if}
</p>
</div>
@@ -63,13 +79,13 @@
<footer class="discard-confirm-actions">
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>Cancel</button>
<button class="btn-danger" type="button" onclick={onConfirm} disabled={isBusy}>
<button class="btn-danger branch-delete-confirm" type="button" onclick={onConfirm} disabled={isBusy}>
{#if isBusy}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<Trash2 size={15} aria-hidden="true" />
{/if}
{force ? "Force delete" : "Delete"}
{branch.remote ? "Delete from remote" : force ? "Force delete" : "Delete"}
</button>
</footer>
</div>

Some files were not shown because too many files have changed in this diff Show More