diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ba650..ad10d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,82 @@ All notable user-facing changes to Gitty are documented in this file. -The project uses calendar versions. Displayed release names use `YYYY.MM.DD`; -package metadata uses the equivalent numeric form without leading zeroes where -required by the package manager. +The project uses calendar-style versions in the form `YYYY.M.PATCH`. + +## [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 @@ -88,3 +161,6 @@ required by the package manager. [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.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 diff --git a/package-lock.json b/package-lock.json index 2fb8c11..4f445a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gitty", - "version": "2026.8.2", + "version": "2026.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitty", - "version": "2026.8.2", + "version": "2026.8.3", "dependencies": { "@lucide/svelte": "^1.21.0", "@tailwindcss/vite": "^4.3.1", diff --git a/package.json b/package.json index f6e13f7..59b4370 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitty", - "version": "2026.8.2", + "version": "2026.8.3", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index f1ff97c..57af1a1 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -66,6 +66,7 @@ pub struct GitBranch { pub name: String, pub current: bool, pub remote: bool, + pub upstream: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] @@ -842,7 +843,7 @@ fn branches_for_repo(repo: &Path) -> Result, String> { repo, [ "for-each-ref", - "--format=%(refname)\t%(HEAD)", + "--format=%(refname)\t%(HEAD)\t%(upstream:short)", "refs/heads", "refs/remotes", ], @@ -851,9 +852,10 @@ fn branches_for_repo(repo: &Path) -> Result, String> { let mut branches = Vec::new(); for line in text.lines() { - let Some((ref_name, head_marker)) = line.split_once('\t') else { - continue; - }; + let mut parts = line.splitn(3, '\t'); + let ref_name = parts.next().unwrap_or_default(); + let head_marker = parts.next().unwrap_or_default(); + let configured_upstream = parts.next().unwrap_or_default().trim(); let (name, remote) = if let Some(name) = ref_name.strip_prefix("refs/heads/") { (name, false) @@ -870,6 +872,11 @@ fn branches_for_repo(repo: &Path) -> Result, String> { name: name.to_string(), current: head_marker.trim() == "*", remote, + upstream: if remote || configured_upstream.is_empty() { + None + } else { + Some(configured_upstream.to_string()) + }, }); } @@ -6240,6 +6247,56 @@ mod tests { run_git_test(repo, ["commit", "-q", "-m", "init"]); } + #[test] + fn branches_report_configured_upstream_and_local_only_state() { + let repo = init_temp_repo("branch_upstream_state"); + commit_initial_file(&repo.path); + run_git_test(&repo.path, ["branch", "feature/local-only"]); + run_git_test(&repo.path, ["branch", "feature/tracked"]); + run_git_test(&repo.path, ["remote", "add", "origin", "."]); + run_git_test( + &repo.path, + [ + "update-ref", + "refs/remotes/origin/feature/published", + "HEAD", + ], + ); + run_git_test( + &repo.path, + ["config", "branch.feature/tracked.remote", "origin"], + ); + run_git_test( + &repo.path, + [ + "config", + "branch.feature/tracked.merge", + "refs/heads/feature/published", + ], + ); + + let branches = branches_for_repo(&repo.path).expect("branches should load"); + let local_only = branches + .iter() + .find(|branch| branch.name == "feature/local-only") + .expect("local-only branch should exist"); + let tracked = branches + .iter() + .find(|branch| branch.name == "feature/tracked") + .expect("tracked branch should exist"); + let remote = branches + .iter() + .find(|branch| branch.name == "origin/feature/published") + .expect("remote branch should exist"); + + assert_eq!(local_only.upstream, None); + assert_eq!( + tracked.upstream.as_deref(), + Some("origin/feature/published") + ); + assert_eq!(remote.upstream, None); + } + #[test] fn commit_notes_can_be_created_updated_and_deleted_without_changing_commit() { let repo = init_temp_repo("commit_notes_crud"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 745251c..41a9a07 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Gitty", - "version": "2026.8.2", + "version": "2026.8.3", "identifier": "com.gitty", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.svelte b/src/App.svelte index be05b94..0984204 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -4,7 +4,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater"; - import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte"; + import { AlertCircle, Cherry, CloudOff, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte"; import { beginFrontendShutdown } from "./lib/telemetry"; import TitleBar from "./lib/TitleBar.svelte"; @@ -506,7 +506,11 @@ $: canCompare = hasRepository && compareFrom.length > 0 && compareTo.length > 0 && compareFrom !== compareTo && !isBusy; $: localBranches = branches.filter((b) => !b.remote); $: localBranchNames = localBranches.map((b) => b.name); + $: localBranchUpstreams = Object.fromEntries( + localBranches.flatMap((branch) => branch.upstream ? [[branch.name, branch.upstream]] : []), + ) as Record; $: remoteBranches = branches.filter((b) => b.remote); + $: currentBranchIsLocalOnly = Boolean(status?.current_branch) && !status?.upstream; $: repoSearchTerm = repoSearch.trim().toLowerCase(); $: openRepoRows = repoTabs.filter((repo) => repoMatchesSearch(repo, repoSearchTerm)); $: recentRepoRows = recentRepoPaths @@ -4445,6 +4449,7 @@ {operation} ahead={status?.ahead ?? 0} behind={status?.behind ?? 0} + localOnly={currentBranchIsLocalOnly} language={appLanguage} editorName={editorToolName} terminalName={terminalToolName} @@ -4896,9 +4901,22 @@ {/if}
- {#if status?.upstream}{status.upstream}{/if} - {status?.ahead ?? 0} ahead - {status?.behind ?? 0} behind + {#if currentBranchIsLocalOnly} + + + {:else} + {#if status?.upstream}{status.upstream}{/if} + {status?.ahead ?? 0} ahead + {status?.behind ?? 0} behind + {/if}
@@ -4991,6 +5009,7 @@ {commits} {selectedCommitHash} {localBranchNames} + {localBranchUpstreams} remoteBranchNames={remoteBranches.map((branch) => branch.name)} activeBranch={status?.current_branch ?? ""} activeUpstream={status?.upstream ?? ""} @@ -5027,8 +5046,14 @@ {#if workspaceActive} - ↑ {status?.ahead ?? 0} - ↓ {status?.behind ?? 0} + {#if currentBranchIsLocalOnly} + + + {:else} + ↑ {status?.ahead ?? 0} + ↓ {status?.behind ?? 0} + {/if} Auto {/if} {#if appVersion}Gitty v{appVersion}{/if} diff --git a/src/app.css b/src/app.css index 737ba1f..6216f74 100644 --- a/src/app.css +++ b/src/app.css @@ -736,6 +736,29 @@ } .repo-action-count.behind { color: #7aacff; } .repo-action-count.ahead { color: #e0a040; } + .repo-action.sync-primary.publish-local { + color: #f0bd6b; + background: linear-gradient(180deg, rgba(224,160,64,.1), rgba(224,160,64,.045)); + } + .repo-action.sync-primary.publish-local:hover:not(:disabled) { + color: #ffd48c; + background: rgba(224,160,64,.14); + } + .repo-action-local-marker { + display: inline-flex; + align-items: center; + gap: 3px; + height: 16px; + padding: 0 4px; + border: 1px dashed rgba(240,189,107,.5); + border-radius: 4px; + color: #f0bd6b; + background: rgba(224,160,64,.08); + font-family: var(--font-mono); + font-size: 7.5px; + font-weight: 900; + letter-spacing: .04em; + } .repo-toolbar-divider { width: 1px; height: 30px; @@ -1670,6 +1693,27 @@ .sync-stats strong:first-of-type { color: #e0a040; background: rgba(224,160,64,0.13); } .sync-stats strong:last-of-type { color: #7aacff; background: rgba(122,172,255,0.13); } .sync-stats span { color: var(--color-ink-dim); background: rgba(94,110,156,0.13); } + .sync-stats .sync-local-only { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 7px; + border: 1px dashed rgba(224,160,64,.38); + color: #f0bd6b; + background: rgba(224,160,64,.09); + } + .sync-stats .sync-local-only strong { + padding: 0; + color: #f0bd6b; + background: transparent; + font-size: 10.5px; + font-weight: 850; + } + .sync-stats .sync-local-only small { + color: var(--color-ink-faint); + font-size: 9px; + font-weight: 650; + } .top-section { display: grid; @@ -2513,6 +2557,12 @@ min-width: 0; min-height: 20px; } + .branch-ref-cluster { + display: inline-flex; + align-items: center; + min-width: 0; + margin-left: -10px; + } .compact-ref-chip { display: inline-flex; align-items: center; @@ -2536,7 +2586,7 @@ flex: 0 1 auto; max-width: 22px; height: 20px; - margin-left: -10px; + margin-left: 0; padding: 0 5px; border-color: color-mix(in srgb, var(--ref-lane-color, #69a7ff) 34%, transparent); border-left-width: 2px; @@ -2552,6 +2602,29 @@ background 140ms ease, box-shadow 140ms ease; } + .branch-ref-cluster.local-only .compact-ref-chip.branch { + border-radius: 0 !important; + } + .compact-ref-local-marker { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 3px; + height: 20px; + margin-left: -1px; + padding: 0 5px 0 4px; + border: 1px dashed rgba(240,189,107,.58); + border-left-style: solid; + border-radius: 0 5px 5px 0; + color: #f0bd6b; + background: linear-gradient(90deg, rgba(224,160,64,.13), rgba(224,160,64,.06)); + font-family: var(--font-mono); + font-size: 7.5px; + font-weight: 900; + letter-spacing: .05em; + line-height: 1; + box-shadow: inset 1px 0 0 rgba(240,189,107,.2); + } .compact-ref-branch-icon { flex: 0 0 auto; color: var(--ref-lane-color, #69a7ff); @@ -2713,6 +2786,12 @@ font-size: 8px; font-weight: 800; } + .commit-ref-detail-item small.local-only { + display: inline-flex; + align-items: center; + gap: 3px; + color: #d99532; + } .commit-files { display: grid; @@ -5905,6 +5984,17 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s .workspace-health.clean > span { background: #2da44e; } .workspace-statusbar .ahead { color: #d9891b; } .workspace-statusbar .behind { color: var(--color-primary); } +.workspace-statusbar .workspace-local-only { + padding: 2px 6px; + border: 1px dashed rgba(224,160,64,.42); + border-radius: 4px; + color: #f0bd6b; + background: rgba(224,160,64,.08); + font-family: var(--font-mono); + font-size: 9px; + font-weight: 850; + letter-spacing: .02em; +} .workspace-auto i { width: 7px; height: 7px; border-radius: 50%; background: var(--color-ink-faint); } .workspace-auto.active i { background: #2da44e; } .app-version { @@ -6140,6 +6230,13 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s linear-gradient(90deg, color-mix(in srgb, var(--ref-lane-color, #315fd6) 14%, #f7faff), #f7faff); } +:root[data-theme="light"] .compact-ref-local-marker { + border-color: rgba(154,82,0,.46); + color: #8b5207; + background: linear-gradient(90deg, rgba(217,137,27,.14), rgba(217,137,27,.06)); + box-shadow: inset 1px 0 0 rgba(154,82,0,.14); +} + :root[data-theme="light"] .compact-ref-chip.current { border-color: color-mix(in srgb, var(--ref-lane-color, #315fd6) 58%, rgba(49,95,214,.2)); color: #18345f; @@ -6709,6 +6806,19 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s color: #0755c8; font-weight: 800; } +:root[data-theme="light"] .repo-action.sync-primary.publish-local, +:root[data-theme="light"] .repo-action-local-marker, +:root[data-theme="light"] .workspace-statusbar .workspace-local-only { + color: #8b5207; + border-color: rgba(154,82,0,.4); + background: rgba(217,137,27,.09); +} +:root[data-theme="light"] .sync-stats .sync-local-only { + border-color: rgba(154,82,0,.36); + color: #8b5207; + background: rgba(217,137,27,.09); +} +:root[data-theme="light"] .sync-stats .sync-local-only strong { color: #8b5207; } /* AI pre-commit review --------------------------------------------------- */ .commit-review-button { diff --git a/src/lib/RepoToolbar.svelte b/src/lib/RepoToolbar.svelte index 96e4d86..2971b3d 100644 --- a/src/lib/RepoToolbar.svelte +++ b/src/lib/RepoToolbar.svelte @@ -3,6 +3,7 @@ ChevronDown, Code2, CloudDownload, + CloudOff, Download, FolderOpen, GitCompare, @@ -21,6 +22,7 @@ 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"; @@ -45,6 +47,12 @@ 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; @@ -103,10 +111,13 @@