Compare commits

...
5 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
8 changed files with 430 additions and 47 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "gitty",
"version": "2026.8.6",
"version": "2026.8.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gitty",
"version": "2026.8.6",
"version": "2026.8.7",
"dependencies": {
"@lucide/svelte": "^1.21.0",
"@tailwindcss/vite": "^4.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gitty",
"version": "2026.8.6",
"version": "2026.8.7",
"private": true,
"type": "module",
"scripts": {
+26 -1
View File
@@ -836,7 +836,13 @@ pub fn set_branch_upstream(
)?;
}
None => {
run_git(&repo, ["branch", "--unset-upstream", branch.as_str()])?;
// Saving sync settings with "No upstream" must be idempotent. Git
// exits with a fatal error when --unset-upstream is used on a
// branch that never had tracking information, which is the normal
// state immediately after adding the first remote.
if git_config_value(&repo, &format!("branch.{branch}.merge")).is_some() {
run_git(&repo, ["branch", "--unset-upstream", branch.as_str()])?;
}
}
}
status_for_repo(&repo)
@@ -7661,6 +7667,25 @@ mod tests {
assert_eq!(remote.upstream, None);
}
#[test]
fn clearing_an_unconfigured_upstream_is_a_noop() {
let repo = init_temp_repo("unset_missing_upstream");
commit_initial_file(&repo.path);
let branch = git_output_test(&repo.path, ["branch", "--show-current"]);
run_git_test(&repo.path, ["remote", "add", "origin", "."]);
let status = set_branch_upstream(
repo.path.to_string_lossy().to_string(),
branch.clone(),
None,
)
.expect("saving an empty upstream should not fail");
assert_eq!(status.current_branch.as_deref(), Some(branch.as_str()));
assert_eq!(status.upstream, None);
assert!(git_config_value(&repo.path, &format!("branch.{branch}.merge")).is_none());
}
#[test]
fn commit_notes_can_be_created_updated_and_deleted_without_changing_commit() {
let repo = init_temp_repo("commit_notes_crud");
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Gitty",
"version": "2026.8.6",
"version": "2026.8.7",
"identifier": "com.gitty",
"build": {
"beforeDevCommand": "npm run prepare:lfs && npm run dev",
+5 -1
View File
@@ -1179,7 +1179,8 @@
root.dataset.appearance = next;
const customProperties = [
"--app-bg", "--app-button-bg", "--app-input-bg", "--app-dialog-bg", "--app-dialog-chrome",
"--app-settings-row-bg", "--color-surface", "--color-surface-alt", "--color-surface-dim",
"--app-dialog-backdrop", "--app-dialog-shadow", "--app-panel-shadow", "--app-settings-row-bg",
"--color-surface", "--color-surface-alt", "--color-surface-dim",
"--color-surface-hover", "--color-surface-raised", "--color-surface-solid", "--color-border",
"--color-border-subtle", "--color-border-input", "--color-primary", "--color-primary-dark",
"--color-accent", "--color-ink", "--color-ink-muted", "--color-ink-faint", "--color-ink-dim",
@@ -1195,6 +1196,9 @@
"--app-input-bg": `color-mix(in srgb, ${surface} 88%, white)`,
"--app-dialog-bg": surface,
"--app-dialog-chrome": `color-mix(in srgb, ${surface} 90%, ${background})`,
"--app-dialog-backdrop": `color-mix(in srgb, ${background} 72%, transparent)`,
"--app-dialog-shadow": `0 24px 68px color-mix(in srgb, ${text} 24%, transparent), 0 2px 12px color-mix(in srgb, ${text} 12%, transparent)`,
"--app-panel-shadow": `0 18px 48px color-mix(in srgb, ${text} 18%, transparent), inset 0 1px 0 color-mix(in srgb, ${surface} 88%, white)`,
"--app-settings-row-bg": `color-mix(in srgb, ${surface} 92%, ${background})`,
"--color-surface": surface,
"--color-surface-alt": `color-mix(in srgb, ${surface} 82%, ${background})`,
+315
View File
@@ -8523,3 +8523,318 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
:root[data-appearance="custom"] .graph-gutter {
background: var(--color-surface-dim);
}
/* Branch visibility dialog ---------------------------------------------
Local and remote branches share one visual and interaction language. */
.branch-filter-backdrop,
:root[data-theme="light"] .branch-filter-backdrop {
padding: 24px;
background: var(--app-dialog-backdrop);
backdrop-filter: blur(5px);
}
.branch-filter-dialog,
:root[data-theme="light"] .branch-filter-dialog {
width: min(560px, 100%);
max-height: min(680px, calc(100vh - 48px));
border-color: var(--color-border);
background: var(--app-dialog-bg);
box-shadow: var(--app-dialog-shadow);
}
.branch-filter-dialog-head {
min-height: 64px;
padding: 12px 14px;
border-bottom-color: var(--color-border);
background: var(--app-dialog-chrome);
}
.branch-filter-dialog-head h3 {
color: var(--color-ink);
font-size: 15px;
}
.branch-filter-summary {
min-height: 48px;
padding: 8px 14px;
border-bottom-color: var(--color-border);
color: var(--color-ink-muted);
background: var(--color-surface-dim);
}
.branch-filter-actions button {
min-height: 28px;
border-color: var(--color-border);
color: var(--color-ink-muted);
background: var(--color-surface-raised);
}
.branch-filter-actions button:hover:not(:disabled) {
border-color: var(--color-border-input);
color: var(--color-ink);
background: var(--color-surface-hover);
}
.branch-filter-dialog-list {
gap: 8px;
padding: 10px;
background: var(--color-surface-alt);
}
.branch-filter-group {
display: grid;
min-width: 0;
overflow: hidden;
border: 1px solid var(--color-border);
background: var(--color-surface);
}
.branch-filter-group-toggle {
display: grid;
grid-template-columns: 16px minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
width: 100%;
min-height: 36px;
padding: 0 10px;
border: 0;
color: var(--color-ink-muted);
background: var(--color-surface-dim);
text-align: left;
}
.branch-filter-group-toggle:hover:not(:disabled) {
color: var(--color-ink);
background: var(--color-surface-hover);
}
.branch-filter-group-toggle > svg {
color: var(--color-accent);
}
.branch-filter-group-toggle > span {
font-size: 10px;
font-weight: 850;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.branch-filter-group-toggle > em {
min-width: 34px;
padding: 2px 5px;
border: 1px solid var(--color-border-subtle);
color: var(--color-ink-faint);
background: var(--color-surface-raised);
font-size: 9px;
font-style: normal;
font-weight: 800;
text-align: center;
}
.branch-filter-group-list {
display: grid;
gap: 4px;
padding: 6px;
border-top: 1px solid var(--color-border-subtle);
}
.branch-filter-option,
:root[data-theme="light"] .branch-filter-option,
:root[data-theme="light"] .branch-filter-option.remote {
min-height: 34px;
border-color: color-mix(in srgb, var(--color-accent) 32%, var(--color-border));
color: var(--color-ink);
background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface-raised));
opacity: 1;
}
.branch-filter-option:hover,
:root[data-theme="light"] .branch-filter-option:hover {
border-color: color-mix(in srgb, var(--color-accent) 52%, var(--color-border));
color: var(--color-ink);
background: color-mix(in srgb, var(--color-accent) 13%, var(--color-surface-hover));
}
.branch-filter-option.muted,
:root[data-theme="light"] .branch-filter-option.muted {
border-color: var(--color-border-subtle);
color: var(--color-ink-faint);
background: var(--color-surface-raised);
opacity: 0.72;
}
.branch-filter-option input {
accent-color: var(--color-accent);
}
@media (max-width: 560px) {
.branch-filter-backdrop { padding: 8px; }
.branch-filter-summary { align-items: stretch; flex-direction: column; }
.branch-filter-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Authentication and token dialogs ------------------------------------
Keep credentials, access tokens and API-key forms on the active theme
instead of the legacy violet/black palette. */
.cred-card,
:root[data-theme="light"] .cred-card {
border-color: var(--color-border-input);
background: var(--app-dialog-bg);
box-shadow: var(--app-dialog-shadow);
}
.cred-hero,
:root[data-theme="light"] .cred-hero {
border-bottom-color: var(--color-border);
background:
radial-gradient(circle at 8% 0%, color-mix(in srgb, var(--color-accent) 14%, transparent), transparent 44%),
var(--app-dialog-chrome);
}
.cred-hero-icon,
:root[data-theme="light"] .cred-hero-icon {
border-color: color-mix(in srgb, var(--color-accent) 42%, var(--color-border));
color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface-raised));
box-shadow: 0 10px 24px color-mix(in srgb, var(--color-accent) 18%, transparent);
}
.cred-hero-label,
:root[data-theme="light"] .cred-hero-label {
color: var(--color-accent);
}
.cred-hero-title,
:root[data-theme="light"] .cred-hero-title {
color: var(--color-ink);
}
.cred-hero-copy,
:root[data-theme="light"] .cred-hero-copy {
color: var(--color-ink-muted);
}
.cred-security-note,
:root[data-theme="light"] .cred-security-note {
border-color: color-mix(in srgb, var(--color-accent) 32%, var(--color-border));
color: var(--color-ink-muted);
background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface-raised));
}
.cred-security-note svg,
:root[data-theme="light"] .cred-security-note svg {
color: var(--color-accent);
}
.cred-close,
:root[data-theme="light"] .cred-close {
border-color: var(--color-border);
color: var(--color-ink-faint);
background: var(--color-surface-raised);
}
.cred-close:hover:not(:disabled),
:root[data-theme="light"] .cred-close:hover:not(:disabled) {
border-color: var(--color-border-input);
color: var(--color-ink);
background: var(--color-surface-hover);
}
.cred-body,
:root[data-theme="light"] .cred-body {
background: var(--app-dialog-bg);
}
.cred-segment,
:root[data-theme="light"] .cred-segment {
border-color: var(--color-border);
background: var(--color-surface-alt);
}
.cred-seg-btn,
:root[data-theme="light"] .cred-seg-btn {
color: var(--color-ink-faint);
}
.cred-seg-btn:hover:not(.active),
:root[data-theme="light"] .cred-seg-btn:hover:not(.active) {
color: var(--color-ink);
background: var(--color-surface-hover);
}
.cred-seg-btn.active,
:root[data-theme="light"] .cred-seg-btn.active {
border-color: color-mix(in srgb, var(--color-accent) 46%, var(--color-border));
color: var(--color-ink);
background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface-raised));
box-shadow: inset 0 -2px 0 var(--color-accent);
}
.cred-input input,
.cred-expiry input[type="date"],
:root[data-theme="light"] .cred-input input,
:root[data-theme="light"] .cred-expiry input[type="date"] {
border-color: var(--color-border-input);
color: var(--color-ink);
background: var(--app-input-bg);
color-scheme: var(--app-color-scheme);
}
.cred-input input::placeholder,
:root[data-theme="light"] .cred-input input::placeholder {
color: var(--color-ink-faint);
opacity: 0.72;
}
.cred-token-hint,
:root[data-theme="light"] .cred-token-hint {
border-color: color-mix(in srgb, var(--color-accent) 34%, var(--color-border));
color: var(--color-ink-muted);
background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface-dim));
}
.cred-token-hint code,
:root[data-theme="light"] .cred-token-hint code {
color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface-raised));
}
.cred-footer {
border-top-color: var(--color-border-subtle);
}
.cred-save input[type="checkbox"] {
accent-color: var(--color-accent);
}
.cred-save span {
color: var(--color-ink-muted);
}
.cred-cancel {
border-color: var(--color-border);
color: var(--color-ink-muted);
background: var(--color-surface-raised);
}
.cred-submit {
border-color: color-mix(in srgb, var(--color-accent) 62%, var(--color-border));
color: var(--color-surface-solid);
background: var(--color-primary-dark);
box-shadow: none;
}
.cred-submit:hover:not(:disabled) {
border-color: var(--color-accent);
color: var(--color-surface-solid);
background: var(--color-primary);
}
@media (max-width: 560px) {
.cred-card { width: calc(100vw - 16px); max-height: calc(100vh - 16px); }
.cred-hero { padding: 16px; }
.cred-body { padding: 16px; }
.cred-security-note { align-items: flex-start; border-radius: 8px; padding-block: 7px; }
.cred-footer { align-items: stretch; flex-direction: column; }
.cred-btns { display: grid; grid-template-columns: 1fr 1fr; }
}
+58 -24
View File
@@ -117,6 +117,8 @@
let customVisibleBranches = $state<Set<string>>(new Set());
let loadedVisibilityRepository = $state("");
let branchDialogOpen = $state(false);
let localBranchGroupOpen = $state(true);
let remoteBranchGroupOpen = $state(false);
let expandedRefsCommitHash = $state("");
let panelElement = $state<HTMLElement | null>(null);
let contextCommit = $state<GitCommit | null>(null);
@@ -428,6 +430,8 @@
}
function openBranchDialog() {
localBranchGroupOpen = true;
remoteBranchGroupOpen = false;
branchDialogOpen = true;
}
@@ -1202,32 +1206,62 @@
<div class="branch-filter-dialog-list">
{#if localBranchNames.length > 0}
<span class="branch-filter-group-label">Local</span>
{#each localBranchNames as branch}
<label class:muted={!branchIsVisible(branch)} class="branch-filter-option" title={branch}>
<input
type="checkbox"
checked={branchIsVisible(branch)}
onchange={() => toggleGraphBranch(branch)}
/>
<GitBranch size={14} aria-hidden="true" />
<span>{branch}</span>
</label>
{/each}
<section class="branch-filter-group">
<button
class="branch-filter-group-toggle"
type="button"
aria-expanded={localBranchGroupOpen}
onclick={() => { localBranchGroupOpen = !localBranchGroupOpen; }}
>
{#if localBranchGroupOpen}<ChevronDown size={14} aria-hidden="true" />{:else}<ChevronRight size={14} aria-hidden="true" />{/if}
<span>Local</span>
<em>{localBranchNames.filter(branchIsVisible).length}/{localBranchNames.length}</em>
</button>
{#if localBranchGroupOpen}
<div class="branch-filter-group-list">
{#each localBranchNames as branch}
<label class:muted={!branchIsVisible(branch)} class="branch-filter-option" title={branch}>
<input
type="checkbox"
checked={branchIsVisible(branch)}
onchange={() => toggleGraphBranch(branch)}
/>
<GitBranch size={14} aria-hidden="true" />
<span>{branch}</span>
</label>
{/each}
</div>
{/if}
</section>
{/if}
{#if remoteBranchNames.length > 0}
<span class="branch-filter-group-label">Remote</span>
{#each remoteBranchNames as branch}
<label class:muted={!branchIsVisible(branch)} class="branch-filter-option remote" title={branch}>
<input
type="checkbox"
checked={branchIsVisible(branch)}
onchange={() => toggleGraphBranch(branch)}
/>
<GitBranch size={14} aria-hidden="true" />
<span>{branch}</span>
</label>
{/each}
<section class="branch-filter-group">
<button
class="branch-filter-group-toggle"
type="button"
aria-expanded={remoteBranchGroupOpen}
onclick={() => { remoteBranchGroupOpen = !remoteBranchGroupOpen; }}
>
{#if remoteBranchGroupOpen}<ChevronDown size={14} aria-hidden="true" />{:else}<ChevronRight size={14} aria-hidden="true" />{/if}
<span>Remote</span>
<em>{remoteBranchNames.filter(branchIsVisible).length}/{remoteBranchNames.length}</em>
</button>
{#if remoteBranchGroupOpen}
<div class="branch-filter-group-list">
{#each remoteBranchNames as branch}
<label class:muted={!branchIsVisible(branch)} class="branch-filter-option" title={branch}>
<input
type="checkbox"
checked={branchIsVisible(branch)}
onchange={() => toggleGraphBranch(branch)}
/>
<GitBranch size={14} aria-hidden="true" />
<span>{branch}</span>
</label>
{/each}
</div>
{/if}
</section>
{/if}
</div>
</div>
+22 -17
View File
@@ -660,8 +660,12 @@
display: grid;
place-items: center;
background:
radial-gradient(circle at 50% 38%, rgba(111, 140, 255, 0.1), transparent 55%),
rgba(7, 10, 16, 0.62);
radial-gradient(
circle at 50% 38%,
color-mix(in srgb, var(--color-accent) 13%, transparent),
transparent 55%
),
var(--app-dialog-backdrop);
backdrop-filter: blur(4px);
animation: status-panel-overlay-in 120ms ease;
}
@@ -673,13 +677,13 @@
gap: 12px;
width: min(300px, calc(100% - 32px));
padding: 26px 28px 26px;
border: 1px solid rgba(90, 111, 154, 0.28);
border: 1px solid var(--color-border-input);
border-radius: 16px;
background:
linear-gradient(180deg, rgba(23, 29, 43, 0.92), rgba(12, 17, 27, 0.94)),
var(--color-surface-raised);
var(--app-panel-highlight),
var(--app-dialog-bg);
color: var(--color-ink);
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.05);
box-shadow: var(--app-dialog-shadow);
}
.status-panel-overlay-mark {
@@ -694,7 +698,7 @@
.status-panel-overlay-halo {
position: absolute;
inset: 6px;
border: 1px solid rgba(111, 140, 255, 0.22);
border: 1px solid color-mix(in srgb, var(--color-primary) 34%, transparent);
border-radius: 22px;
transform: rotate(45deg);
}
@@ -703,7 +707,7 @@
}
.status-panel-overlay-halo.halo-two {
inset: 16px;
border-color: rgba(77, 182, 214, 0.24);
border-color: color-mix(in srgb, var(--color-accent) 38%, transparent);
animation: status-panel-overlay-halo-breathe 2.4s ease-in-out infinite reverse;
}
@@ -721,16 +725,16 @@
stroke-linecap: round;
stroke-dasharray: 165;
stroke-dashoffset: 165;
filter: drop-shadow(0 0 6px rgba(77, 182, 214, 0.32));
filter: drop-shadow(0 0 6px color-mix(in srgb, var(--color-accent) 38%, transparent));
animation: status-panel-overlay-trace-draw 2.6s ease-in-out infinite;
}
.status-panel-overlay-traces .trace-main { stroke: #6f8cff; }
.status-panel-overlay-traces .trace-main { stroke: var(--color-primary); }
.status-panel-overlay-traces .trace-branch {
stroke: #4db6d6;
stroke: var(--color-accent);
animation-delay: 0.28s;
}
.status-panel-overlay-traces .trace-cut {
stroke: rgba(177, 186, 208, 0.42);
stroke: color-mix(in srgb, var(--color-ink-muted) 52%, transparent);
stroke-dasharray: 128;
stroke-dashoffset: 128;
animation-delay: 0.55s;
@@ -743,8 +747,8 @@
height: 64px;
object-fit: contain;
filter:
drop-shadow(0 8px 12px rgba(0, 0, 0, 0.5))
drop-shadow(0 0 10px rgba(77, 182, 214, 0.2));
drop-shadow(0 8px 12px color-mix(in srgb, var(--color-ink) 24%, transparent))
drop-shadow(0 0 10px color-mix(in srgb, var(--color-accent) 24%, transparent));
animation: status-panel-overlay-icon-float 2.4s ease-in-out infinite;
}
@@ -765,15 +769,16 @@
height: 4px;
overflow: hidden;
border-radius: 999px;
background: rgba(111, 140, 255, 0.14);
border: 1px solid var(--color-border-subtle);
background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface-dim));
}
.status-panel-overlay-bar span {
position: absolute;
inset: 0;
width: 46%;
border-radius: inherit;
background: linear-gradient(90deg, transparent, #6f8cff 40%, #4db6d6 74%, transparent);
box-shadow: 0 0 12px rgba(77, 182, 214, 0.26);
background: linear-gradient(90deg, transparent, var(--color-primary) 40%, var(--color-accent) 74%, transparent);
box-shadow: 0 0 12px color-mix(in srgb, var(--color-accent) 32%, transparent);
animation: status-panel-overlay-bar-slide 1.35s ease-in-out infinite;
}