Compare commits

...
18 Commits
Author SHA1 Message Date
Christoph 247c118bdf Merge pull request 'fix(styles): show graph hover branches only on gutter hover' (#19) from opt/app_layout into main
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 16m35s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 26m22s
Reviewed-on: #19
2026-07-06 09:45:35 +00:00
Christoph c59cb412dd Update version to 2026.7.13 2026-07-06 11:39:13 +02:00
Christoph 1b83429c06 Merge pull request 'Opt/app layout' (#18) from opt/app_layout into main
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
Reviewed-on: #18
2026-07-06 09:36:07 +00:00
Christoph Brandau 3e2885f64d fix(styles): show graph hover branches only on gutter hover
Hover behavior for graph branch highlights was adjusted to limit the
opacity/transform changes to the gutter hover state. This prevents the
highlight from triggering when hovering over the graph rows, reducing
unintended visual effects.
2026-07-06 11:35:46 +02:00
Christoph Brandau eac0f059fa feat(history): improve commit hover branch labels and titles
The history panel now generates more informative hover text for commits.
It prefers direct local branch refs when available, otherwise it derives
a concise list from containing branch labels and summarizes overflow.

- Add helpers to compute hover branch labels and title text
- Update the graph row hover rendering to use the new helpers
2026-07-06 11:31:41 +02:00
Christoph Brandau aaa4228b82 feat(history): add commit action context menu in HistoryPanel
The commit history UI now provides a compact actions menu per commit,
with positioning constrained to the history panel. The menu supports
keyboard dismissal via Escape and closes on outside clicks to keep the
interaction predictable.

- Replace per-commit action buttons with an ellipsis menu
- Add context menu state and panel-relative positioning logic
- Style the new history context menu and menu button
2026-07-06 11:16:52 +02:00
Christoph e5c26ea502 Update version to 2026.7.0 2026-07-06 08:41:32 +02:00
Christoph Brandau 38b4f98233 feat(ci): improve Tauri build pipeline and artifact collection
The CI workflow now installs the required Rust toolchain and uv on
Ubuntu, and passes a no-strip flag into the Tauri build to control
binary stripping behavior per platform. The CI helper script was
updated to collect only expected bundle and signature files, reducing
noise and improving upload correctness. Versioning was aligned across
the Node package and Tauri config for the new release.

- Add platform-specific no_strip handling in the build workflow
- Install Rust toolchain/uv for Ubuntu builds
- Filter collected artifacts by expected extensions and signatures
2026-07-06 08:23:49 +02:00
Christoph Brandau d592894f28 fix(git): split long path args to avoid Windows CreateProcess limit
Git operations that stage or restore many files could exceed the Windows
command line length limit, causing os error 206 failures. This change
splits file paths into size-bounded chunks and concatenates the output
from multiple git invocations to keep commands within safe limits.

- Chunk file path arguments for Windows compatibility
- Add a local dev command to list pkg-config packages
2026-07-06 08:22:41 +02:00
Christoph 9ee2f71dc3 Update .gitea/workflows/app_builder.yaml
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 17m5s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 29m36s
2026-07-06 06:17:03 +00:00
Christoph 283779610a Merge pull request 'Build/arch image' (#17) from build/arch_image into main
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Failing after 16m49s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Has been cancelled
Reviewed-on: #17
2026-07-06 05:40:19 +00:00
Christoph Brandau d33f576212 ci(app): install Rust toolchain on Ubuntu 22.04 builds
The workflow now sets up a Rust toolchain only for the Ubuntu
22.04 runner. This ensures Rust-based components can compile
reliably without affecting other platforms.

- Add conditional Rust toolchain install for ubuntu-22.04
2026-07-06 07:33:16 +02:00
Christoph a3fd8f1ce8 Update version to 0.100.0
publish / publish-tauri (appimage, ubuntu-22.04, linux-x86_64) (release) Failing after 1m4s
publish / publish-tauri (nsis, windows-latest, windows-x86_64) (release) Failing after 13m46s
2026-07-06 07:15:04 +02:00
Christoph de06e7a15d feat(ci): control symbol stripping via NO_STRIP matrix var
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Failing after 1m7s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Has been cancelled
The workflow now passes a per-platform NO_STRIP setting into the
Tauri build environment. This allows builds to retain symbols when
needed, improving debugging and crash analysis without changing the
default behavior for other targets.

- Add matrix no_strip values for Windows and Ubuntu
- Export NO_STRIP into the build environment for Tauri
2026-07-05 23:28:23 +02:00
Christoph 0105e0d46e feat(cicd): collect only expected artifact bundle files
Artifact collection previously grabbed every file under each bundle
directory, which could include unrelated or intermediate outputs. This
updates the logic to only include files with the expected extensions
for each artifact type, improving accuracy and reducing noise.

- Use a per-bundle suffix map to filter collected artifacts
- Switch to non-recursive directory iteration for bundle roots
2026-07-05 23:20:28 +02:00
Christoph 0732f9ca6d Merge pull request 'feat(ci): build Linux AppImage and update updater metadata' (#16) from build/arch_image into main
publish / publish-tauri (nsis, windows-latest, windows-x86_64) (release) Has been cancelled
publish / publish-tauri (appimage, ubuntu-22.04, linux-x86_64) (release) Failing after 2m18s
Reviewed-on: #16
2026-07-05 21:12:29 +00:00
Christoph fcaaa770d6 feat(ci): build Linux AppImage and update updater metadata
The CI workflow now builds a single bundle per run, adds Linux
AppImage output, and updates the Tauri version using a Node script.
The publishing tool is extended to recognize Linux artifacts,
select a primary artifact based on the target platform, and merge
platform entries into an existing latest.json when present.

- Limit matrix parallelism and pass bundle targets to Tauri
- Update updater platform handling and latest.json merging
- Add Linux Tauri config for AppImage bundling
2026-07-05 23:10:52 +02:00
Christoph 379350b9ee Update version to 2026.7.12 2026-07-05 11:05:49 +02:00
10 changed files with 340 additions and 73 deletions
+2 -1
View File
@@ -78,7 +78,8 @@
"Bash(rustfmt --edition 2024 --check src/badge.rs src/main.rs)", "Bash(rustfmt --edition 2024 --check src/badge.rs src/main.rs)",
"Bash(rustfmt --edition 2024 --check src/git.rs)", "Bash(rustfmt --edition 2024 --check src/git.rs)",
"Bash(rustfmt --edition 2024 --check src/badge.rs)", "Bash(rustfmt --edition 2024 --check src/badge.rs)",
"Bash(rustfmt --edition 2024 --check src/git.rs src/main.rs)" "Bash(rustfmt --edition 2024 --check src/git.rs src/main.rs)",
"Bash(pkg-config --list-all)"
] ]
} }
} }
+25 -14
View File
@@ -32,16 +32,21 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 1
matrix: matrix:
include: include:
# - platform: 'macos-latest' # - platform: 'macos-latest'
# args: '--target aarch64-apple-darwin' # args: '--target aarch64-apple-darwin'
# - platform: 'macos-latest' # - platform: 'macos-latest'
# args: '--target x86_64-apple-darwin' # args: '--target x86_64-apple-darwin'
# - platform: 'ubuntu-22.04'
# args: ''
- platform: "windows-latest" - platform: "windows-latest"
args: "" 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: ${{ matrix.platform }}
@@ -55,13 +60,21 @@ jobs:
if: matrix.platform == 'ubuntu-22.04' if: matrix.platform == 'ubuntu-22.04'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 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 - name: setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/* 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 - name: install frontend dependencies
run: npm ci run: npm ci
@@ -69,18 +82,13 @@ jobs:
run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version
- name: Update tauri.conf.json Version - name: Update tauri.conf.json Version
shell: powershell env:
RELEASE_VERSION: ${{ github.ref_name }}
run: | run: |
$path = ".\src-tauri\tauri.conf.json" 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');"
$version = '${{ github.ref_name }}'
$content = Get-Content -Raw -Encoding UTF8 $path
$regex = [regex]'"version"\s*:\s*"[^"]*"'
$replacement = '"version": "' + $version + '"'
$updated = $regex.Replace($content, $replacement, 1)
$encoding = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $updated, $encoding)
- name: Commit and Push Changes - name: Commit and Push Changes
if: matrix.platform == 'windows-latest'
shell: powershell shell: powershell
run: | run: |
git config user.name '${{ vars.USERNAME_GIT }}' git config user.name '${{ vars.USERNAME_GIT }}'
@@ -131,12 +139,15 @@ jobs:
env: env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}}
run: npm run tauri build NO_STRIP: ${{ matrix.no_strip }}
run: npm run tauri -- build --bundles ${{ matrix.bundles }}
# ---------------------- # ----------------------
# Upload to MinIO (via mc) and create latest.json # Upload to MinIO (via mc) and create latest.json
# ---------------------- # ----------------------
- name: Upload artifacts to MinIO with cicd_tool - name: Upload artifacts to MinIO with cicd_tool
working-directory: GitLite/cicd_tool working-directory: GitLite/cicd_tool
env:
PUBLISH_PLATFORM: ${{ matrix.updater_platform }}
run: | run: |
uv sync uv sync
uv run main.py uv run main.py
+121 -28
View File
@@ -4,7 +4,7 @@ import json
import os import os
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Iterable, List, Optional from typing import Any, Iterable, List, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from minio import Minio from minio import Minio
@@ -115,23 +115,57 @@ def _ensure_bucket(client: Minio, bucket_name: str) -> None:
def _collect_artifacts() -> List[Path]: def _collect_artifacts() -> List[Path]:
artifacts: List[Path] = [] artifacts: List[Path] = []
for directory in ( bundle_dirs = {
ARTIFACT_ROOT / "msi", ARTIFACT_ROOT / "msi": (".msi", ".sig"),
ARTIFACT_ROOT / "nsis", ARTIFACT_ROOT / "nsis": (".exe", ".sig"),
ARTIFACT_ROOT / "app", ARTIFACT_ROOT / "appimage": (".appimage", ".sig"),
): ARTIFACT_ROOT / "deb": (".deb", ".sig"),
ARTIFACT_ROOT / "rpm": (".rpm", ".sig"),
ARTIFACT_ROOT / "app": (".dmg", ".zip", ".sig"),
}
for directory, suffixes in bundle_dirs.items():
if directory.exists(): if directory.exists():
artifacts.extend(p for p in directory.rglob("*") if p.is_file()) artifacts.extend(
p
for p in directory.iterdir()
if p.is_file() and p.suffix.lower() in suffixes
)
return artifacts return artifacts
def _select_primary_artifact(artifacts: Iterable[Path]) -> Optional[Path]: def _select_primary_artifact(
priority = (".exe", ".msi", ".zip", ".dmg") artifacts: Iterable[Path], platform: Optional[str] = None
) -> Optional[Path]:
artifact_list = list(artifacts)
priorities = {
"windows-x86_64": (".exe", ".msi"),
"linux-x86_64": (".appimage", ".deb", ".rpm"),
"darwin-x86_64": (".dmg", ".zip"),
"darwin-aarch64": (".dmg", ".zip"),
}
priority = priorities.get(platform or "", ())
priority += (".exe", ".msi", ".appimage", ".deb", ".rpm", ".zip", ".dmg")
for ext in priority: for ext in priority:
for artifact in artifacts: for artifact in artifact_list:
if artifact.suffix.lower() == ext: if artifact.suffix.lower() == ext:
return artifact return artifact
return next(iter(artifacts), None) return next(
(artifact for artifact in artifact_list if artifact.suffix != ".sig"), None
)
def _infer_platform(artifact: Path) -> str:
suffix = artifact.suffix.lower()
if suffix in {".exe", ".msi"}:
return "windows-x86_64"
if suffix in {".appimage", ".deb", ".rpm"}:
return "linux-x86_64"
if suffix in {".dmg", ".zip"}:
return "darwin-x86_64"
raise ConfigurationError(
f"Cannot infer updater platform from artifact: {artifact.name}. "
"Set PUBLISH_PLATFORM explicitly."
)
def _read_signature(artifact: Path) -> str: def _read_signature(artifact: Path) -> str:
@@ -145,16 +179,55 @@ def _read_signature(artifact: Path) -> str:
raise FileNotFoundError(f"Signature not found for {artifact.name}") raise FileNotFoundError(f"Signature not found for {artifact.name}")
def _build_latest_json( def _build_artifact_url(
version: str, version: str,
primary_artifact: Path, primary_artifact: Path,
signature: str,
artifact_base_url: str, artifact_base_url: str,
app_dir: str, app_dir: str,
) -> str: ) -> str:
base_url = artifact_base_url.rstrip("/") base_url = artifact_base_url.rstrip("/")
path_parts = [app_dir.strip("/"), version, primary_artifact.name] path_parts = [app_dir.strip("/"), version, primary_artifact.name]
artifact_url = "/".join([base_url, *filter(None, path_parts)]) return "/".join([base_url, *filter(None, path_parts)])
def _read_existing_latest_json(
client: Minio, bucket_name: str
) -> Optional[dict[str, Any]]:
try:
response = client.get_object(bucket_name, "latest.json")
except S3Error as exc:
if exc.code in {"NoSuchKey", "NoSuchObject"}:
return None
raise
try:
payload = json.loads(response.read().decode("utf-8"))
except json.JSONDecodeError as exc:
raise ConfigurationError("Existing latest.json is invalid JSON") from exc
finally:
response.close()
response.release_conn()
if not isinstance(payload, dict):
raise ConfigurationError("Existing latest.json must be a JSON object")
return payload
def _build_latest_payload(
version: str,
platform: str,
primary_artifact: Path,
signature: str,
artifact_base_url: str,
app_dir: str,
existing_payload: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
artifact_url = _build_artifact_url(
version=version,
primary_artifact=primary_artifact,
artifact_base_url=artifact_base_url,
app_dir=app_dir,
)
pub_date = ( pub_date = (
datetime.now(timezone.utc) datetime.now(timezone.utc)
@@ -163,18 +236,27 @@ def _build_latest_json(
.replace("+00:00", "Z") .replace("+00:00", "Z")
) )
latest_payload = { platforms: dict[str, Any] = {}
"version": version, existing_notes = None
"pub_date": pub_date, if existing_payload and existing_payload.get("version") == version:
"notes": os.environ.get("RELEASE_NOTES", "Automated release"), existing_platforms = existing_payload.get("platforms")
"platforms": { if isinstance(existing_platforms, dict):
"windows-x86_64": { platforms.update(existing_platforms)
existing_notes = existing_payload.get("notes")
platforms[platform] = {
"signature": signature, "signature": signature,
"url": artifact_url, "url": artifact_url,
} }
},
return {
"version": version,
"pub_date": pub_date,
"notes": os.environ.get("RELEASE_NOTES")
or existing_notes
or "Automated release",
"platforms": platforms,
} }
return json.dumps(latest_payload, separators=(",", ":"))
def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None: def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
@@ -194,28 +276,36 @@ def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
"No release artifacts found in src-tauri/target/release/bundle." "No release artifacts found in src-tauri/target/release/bundle."
) )
primary = _select_primary_artifact(artifacts) publish_platform = os.environ.get("PUBLISH_PLATFORM") or os.environ.get(
"UPDATER_PLATFORM"
)
primary = _select_primary_artifact(artifacts, publish_platform)
if not primary: if not primary:
raise FileNotFoundError("Unable to determine primary artifact to publish.") raise FileNotFoundError("Unable to determine primary artifact to publish.")
platform = publish_platform or _infer_platform(primary)
signature = _read_signature(primary) signature = _read_signature(primary)
latest_json = _build_latest_json( client = _create_minio_client()
_ensure_bucket(client, bucket_name)
existing_latest_json = _read_existing_latest_json(client, bucket_name)
latest_payload = _build_latest_payload(
version=resolved_version, version=resolved_version,
platform=platform,
primary_artifact=primary, primary_artifact=primary,
signature=signature, signature=signature,
artifact_base_url=artifact_base_url, artifact_base_url=artifact_base_url,
app_dir=bucket_prefix, 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 = PROJECT_DIR / "dist"
local_dist_dir.mkdir(parents=True, exist_ok=True) local_dist_dir.mkdir(parents=True, exist_ok=True)
latest_json_path = local_dist_dir / "latest.json" latest_json_path = local_dist_dir / "latest.json"
latest_json_path.write_text(latest_json + "\n", encoding="utf-8") latest_json_path.write_text(latest_json + "\n", encoding="utf-8")
client = _create_minio_client()
_ensure_bucket(client, bucket_name)
for artifact in artifacts: for artifact in artifacts:
object_segments = [part for part in (resolved_version, artifact.name) if part] object_segments = [part for part in (resolved_version, artifact.name) if part]
object_name = "/".join(object_segments) object_name = "/".join(object_segments)
@@ -231,7 +321,10 @@ def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
length=len(latest_bytes), length=len(latest_bytes),
content_type="application/json", content_type="application/json",
) )
print(f"Uploaded latest.json -> s3://{bucket_name}/{latest_object_name}") print(
f"Uploaded latest.json for {platform} -> "
f"s3://{bucket_name}/{latest_object_name}"
)
if __name__ == "__main__": if __name__ == "__main__":
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "git-lite", "name": "git-lite",
"version": "2026.7.10", "version": "2026.7.13",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "git-lite", "name": "git-lite",
"version": "2026.7.10", "version": "2026.7.13",
"dependencies": { "dependencies": {
"@lucide/svelte": "^1.21.0", "@lucide/svelte": "^1.21.0",
"@tailwindcss/vite": "^4.3.1", "@tailwindcss/vite": "^4.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "git-lite", "name": "git-lite",
"version": "2026.7.11", "version": "2026.7.13",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+34 -3
View File
@@ -3415,16 +3415,47 @@ fn is_auth_error(details: &str) -> bool {
|| d.contains("authentication required") || d.contains("authentication required")
} }
// Windows' CreateProcess rejects command lines longer than ~32K chars with
// "os error 206" (filename or extension too long). Staging/restoring a large
// number of files can easily exceed that, so split the paths across multiple
// invocations and concatenate their output.
const MAX_PATH_ARGS_CHARS: usize = 8_000;
fn run_git_with_paths( fn run_git_with_paths(
repo: &Path, repo: &Path,
base_args: &[&str], base_args: &[&str],
files: &[String], files: &[String],
) -> Result<Vec<u8>, String> { ) -> Result<Vec<u8>, String> {
let mut args = Vec::with_capacity(base_args.len() + files.len() + 1); if files.is_empty() {
let args: Vec<OsString> = base_args.iter().map(OsString::from).collect();
return run_git(repo, args);
}
let mut combined = Vec::new();
let mut start = 0;
while start < files.len() {
let mut end = start;
let mut chunk_chars = 0usize;
while end < files.len() {
let len = files[end].len() + 1;
if end > start && chunk_chars + len > MAX_PATH_ARGS_CHARS {
break;
}
chunk_chars += len;
end += 1;
}
let chunk = &files[start..end];
let mut args = Vec::with_capacity(base_args.len() + chunk.len() + 1);
args.extend(base_args.iter().map(OsString::from)); args.extend(base_args.iter().map(OsString::from));
args.push(OsString::from("--")); args.push(OsString::from("--"));
args.extend(files.iter().map(OsString::from)); args.extend(chunk.iter().map(OsString::from));
run_git(repo, args) combined.extend(run_git(repo, args)?);
start = end;
}
Ok(combined)
} }
fn run_git_with_paths_cancellable( fn run_git_with_paths_cancellable(
+9 -4
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "GitLite", "productName": "GitLite",
"version": "2026.7.11", "version": "2026.7.13",
"identifier": "com.git-lite", "identifier": "com.git-lite",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
@@ -28,8 +28,13 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["nsis"], "targets": [
"icon": ["icons/icon.png", "icons/icon.ico"], "nsis"
],
"icon": [
"icons/icon.png",
"icons/icon.ico"
],
"createUpdaterArtifacts": true, "createUpdaterArtifacts": true,
"windows": { "windows": {
"nsis": { "nsis": {
@@ -40,7 +45,7 @@
"plugins": { "plugins": {
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdDNEJGRTZERTREOTc5QjkKUldTNWVkbmtiZjVMZk02YzdpenVLOVltREJpRkFrc2F4dUwzaTV5M3pacFRVR0tmd25xSGtjK1MK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdDNEJGRTZERTREOTc5QjkKUldTNWVkbmtiZjVMZk02YzdpenVLOVltREJpRkFrc2F4dUwzaTV5M3pacFRVR0tmd25xSGtjK1MK",
"endpoints" : [ "endpoints": [
"https://cdn.cbsk-tech.de/gitlite/latest.json" "https://cdn.cbsk-tech.de/gitlite/latest.json"
] ]
} }
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://schema.tauri.app/config/2",
"bundle": {
"targets": ["appimage"]
}
}
+26 -4
View File
@@ -1639,6 +1639,7 @@
.branch-actions .btn-sm { min-height: 24px; padding: 0 6px; font-size: 11px; } .branch-actions .btn-sm { min-height: 24px; padding: 0 6px; font-size: 11px; }
.branch-context-menu, .branch-context-menu,
.history-context-menu,
.explorer-context-menu { .explorer-context-menu {
z-index: 120; z-index: 120;
display: grid; display: grid;
@@ -1651,10 +1652,12 @@
box-shadow: 0 18px 50px rgba(0,0,0,0.5); box-shadow: 0 18px 50px rgba(0,0,0,0.5);
} }
.branch-context-menu { position: absolute; } .branch-context-menu,
.history-context-menu { position: absolute; }
.explorer-context-menu { position: fixed; } .explorer-context-menu { position: fixed; }
.branch-context-menu button, .branch-context-menu button,
.history-context-menu button,
.explorer-context-menu button { .explorer-context-menu button {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1673,13 +1676,15 @@
} }
.branch-context-menu button:hover:not(:disabled), .branch-context-menu button:hover:not(:disabled),
.history-context-menu button:hover:not(:disabled),
.explorer-context-menu button:hover:not(:disabled) { .explorer-context-menu button:hover:not(:disabled) {
border-color: var(--color-border-subtle); border-color: var(--color-border-subtle);
background: rgba(255,255,255,0.06); background: rgba(255,255,255,0.06);
color: var(--color-ink); color: var(--color-ink);
} }
.branch-context-menu .menu-separator { .branch-context-menu .menu-separator,
.history-context-menu .menu-separator {
height: 1px; height: 1px;
margin: 4px 3px; margin: 4px 3px;
background: var(--color-border-subtle); background: var(--color-border-subtle);
@@ -1696,6 +1701,7 @@
} }
.branch-context-menu button:disabled, .branch-context-menu button:disabled,
.history-context-menu button:disabled,
.explorer-context-menu button:disabled { .explorer-context-menu button:disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.48; opacity: 0.48;
@@ -1817,6 +1823,7 @@
/* --- Commit history --- */ /* --- Commit history --- */
.history-panel { position: relative; }
.history-list { min-width: 0; padding: 6px; overflow: auto; } .history-list { min-width: 0; padding: 6px; overflow: auto; }
.commit-row { display: grid; min-width: 0; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); transition: border-color 120ms; } .commit-row { display: grid; min-width: 0; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); transition: border-color 120ms; }
@@ -2017,6 +2024,22 @@
} }
.commit-action-buttons { display: flex; flex: 0 0 auto; align-items: center; gap: 5px; } .commit-action-buttons { display: flex; flex: 0 0 auto; align-items: center; gap: 5px; }
.commit-action-buttons button { flex: 0 0 auto; white-space: nowrap; } .commit-action-buttons button { flex: 0 0 auto; white-space: nowrap; }
.commit-menu-button {
width: 28px;
min-width: 28px;
min-height: 26px;
padding: 0;
border-color: rgba(94,110,156,0.16);
border-radius: 7px;
background: rgba(255,255,255,0.035);
color: var(--color-ink-dim);
}
.commit-menu-button:hover:not(:disabled),
.commit-menu-button[aria-expanded="true"] {
border-color: rgba(65,209,255,0.28);
background: rgba(65,209,255,0.08);
color: var(--color-ink);
}
.file-history-head { align-items: flex-start; } .file-history-head { align-items: flex-start; }
.file-history-heading { min-width: 0; flex: 1 1 auto; overflow: hidden; } .file-history-heading { min-width: 0; flex: 1 1 auto; overflow: hidden; }
.section-head .file-history-name { .section-head .file-history-name {
@@ -2200,8 +2223,7 @@
transform: translateY(-50%) translateX(-4px); transform: translateY(-50%) translateX(-4px);
transition: opacity 120ms ease, transform 120ms ease; transition: opacity 120ms ease, transform 120ms ease;
} }
.graph-gutter:hover .graph-hover-branches, .graph-gutter:hover .graph-hover-branches {
.graph-row:hover .graph-hover-branches {
opacity: 1; opacity: 1;
transform: translateY(-50%) translateX(0); transform: translateY(-50%) translateX(0);
} }
+111 -13
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { ChevronDown, ChevronRight, GitBranch, GitMerge, RotateCcw, X } from "@lucide/svelte"; import { ChevronDown, ChevronRight, EllipsisVertical, GitBranch, GitMerge, RotateCcw, X } from "@lucide/svelte";
import type { FileStatusKind, GitCommit, GitCommitFile } from "../types"; import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";
interface GraphSegment { interface GraphSegment {
@@ -60,6 +60,10 @@
let branchDialogOpen = $state(false); let branchDialogOpen = $state(false);
let userAdjustedBranchFilter = $state(false); let userAdjustedBranchFilter = $state(false);
let lastDefaultFilterKey = $state(""); let lastDefaultFilterKey = $state("");
let panelElement = $state<HTMLElement | null>(null);
let contextCommit = $state<GitCommit | null>(null);
let contextMenuX = $state(0);
let contextMenuY = $state(0);
function laneColor(col: number): string { function laneColor(col: number): string {
return GRAPH_COLORS[((col % GRAPH_COLORS.length) + GRAPH_COLORS.length) % GRAPH_COLORS.length]; return GRAPH_COLORS[((col % GRAPH_COLORS.length) + GRAPH_COLORS.length) % GRAPH_COLORS.length];
@@ -203,6 +207,24 @@
return labels.filter(branchIsVisible); return labels.filter(branchIsVisible);
} }
function commitHoverBranchLabels(commit: GitCommit, row: GraphRow | undefined): string[] {
const directBranches = localBranchRefs(commit);
if (directBranches.length > 0) return directBranches;
const containingBranches = row?.branchLabels ?? [];
if (containingBranches.length <= 3) return containingBranches;
return [...containingBranches.slice(0, 3), `+${containingBranches.length - 3} more`];
}
function commitHoverTitle(commit: GitCommit, row: GraphRow | undefined): string {
const directBranches = localBranchRefs(commit);
if (directBranches.length > 0) return `Branches: ${directBranches.join(", ")}`;
const containingBranches = row?.branchLabels ?? [];
if (containingBranches.length === 0) return commit.short_hash;
return `Branches containing this commit: ${containingBranches.join(", ")}`;
}
function segmentIsVisible(segment: GraphSegment): boolean { function segmentIsVisible(segment: GraphSegment): boolean {
return segment.branches.length === 0 || segment.branches.some(branchIsVisible); return segment.branches.length === 0 || segment.branches.some(branchIsVisible);
} }
@@ -316,6 +338,54 @@
} }
} }
function openCommitActionMenu(event: MouseEvent, commit: GitCommit) {
event.preventDefault();
event.stopPropagation();
if (isBusy) return;
if (contextCommit?.hash === commit.hash) {
closeCommitContextMenu();
return;
}
const panelRect = panelElement?.getBoundingClientRect();
const buttonRect = event.currentTarget instanceof HTMLElement
? event.currentTarget.getBoundingClientRect()
: null;
const rawX = panelRect && buttonRect ? buttonRect.right - panelRect.left - 184 : event.offsetX;
const rawY = panelRect && buttonRect ? buttonRect.bottom - panelRect.top + 4 : event.offsetY;
const maxX = Math.max(8, (panelRect?.width ?? window.innerWidth) - 192);
const maxY = Math.max(8, (panelRect?.height ?? window.innerHeight) - 96);
contextCommit = commit;
contextMenuX = Math.max(8, Math.min(rawX, maxX));
contextMenuY = Math.max(8, Math.min(rawY, maxY));
}
function closeCommitContextMenu() {
contextCommit = null;
}
async function createBranchFromContextCommit() {
const commit = contextCommit;
if (!commit || isBusy) return;
closeCommitContextMenu();
await onCreateBranchFromCommit(commit);
}
async function restoreContextCommit() {
const commit = contextCommit;
if (!commit || isBusy) return;
closeCommitContextMenu();
await onRestoreCommit(commit);
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key !== "Escape") return;
closeCommitContextMenu();
handleBranchDialogKeydown(event);
}
function localBranchRefs(commit: GitCommit): string[] { function localBranchRefs(commit: GitCommit): string[] {
const seen = new Set<string>(); const seen = new Set<string>();
const labels: string[] = []; const labels: string[] = [];
@@ -393,9 +463,9 @@
let graphWidth = $derived(Math.max(Math.max(graph.columns, 1) * GRAPH_LANE + 18, 42)); let graphWidth = $derived(Math.max(Math.max(graph.columns, 1) * GRAPH_LANE + 18, 42));
</script> </script>
<svelte:window onkeydown={handleBranchDialogKeydown} /> <svelte:window onclick={closeCommitContextMenu} onkeydown={handleWindowKeydown} on:contextmenu|capture={closeCommitContextMenu} />
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history"> <section bind:this={panelElement} class="panel history-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
<div class="section-head"> <div class="section-head">
<div> <div>
<span class="eyebrow">History</span> <span class="eyebrow">History</span>
@@ -431,9 +501,14 @@
{#each visibleCommitEntries as entry, rowIndex (entry.commit.hash)} {#each visibleCommitEntries as entry, rowIndex (entry.commit.hash)}
{@const item = entry.commit} {@const item = entry.commit}
{@const row = graphRows[rowIndex]} {@const row = graphRows[rowIndex]}
{@const hoverBranchRefs = visibleBranchLabels(row?.branchLabels ?? [])} {@const hoverBranchRefs = commitHoverBranchLabels(item, row)}
{@const otherRefs = visibleRefs(item)} {@const otherRefs = visibleRefs(item)}
<article class="commit-row graph-row" class:merge-row={item.parents.length > 1} class:root-row={item.parents.length === 0} class:tip-row={item.refs.length > 0}> <article
class="commit-row graph-row"
class:merge-row={item.parents.length > 1}
class:root-row={item.parents.length === 0}
class:tip-row={item.refs.length > 0}
>
<div class="graph-gutter" style={`width:${graphWidth}px`} aria-hidden="true"> <div class="graph-gutter" style={`width:${graphWidth}px`} aria-hidden="true">
{#if row} {#if row}
<svg class="graph-svg" viewBox={`0 0 ${graphWidth} 100`} preserveAspectRatio="none"> <svg class="graph-svg" viewBox={`0 0 ${graphWidth} 100`} preserveAspectRatio="none">
@@ -461,7 +536,7 @@
class:merge={item.parents.length > 1} class:merge={item.parents.length > 1}
class:tip={item.refs.length > 0} class:tip={item.refs.length > 0}
class:hidden-branch={!rowGraphIsVisible(row)} class:hidden-branch={!rowGraphIsVisible(row)}
title={hoverBranchRefs.length > 0 ? `Contained in: ${hoverBranchRefs.join(", ")}` : item.short_hash} title={commitHoverTitle(item, row)}
style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`} style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`}
></span> ></span>
{#if hoverBranchRefs.length > 0} {#if hoverBranchRefs.length > 0}
@@ -545,13 +620,17 @@
<div class="commit-actions"> <div class="commit-actions">
<time class="commit-time" datetime={item.date}>{formatCommitDate(item.date)}</time> <time class="commit-time" datetime={item.date}>{formatCommitDate(item.date)}</time>
<div class="commit-action-buttons"> <div class="commit-action-buttons">
<button class="btn-sm" type="button" onclick={() => onCreateBranchFromCommit(item)} disabled={isBusy} title="Create a new branch from this commit"> <button
<GitBranch size={15} aria-hidden="true" /> class="commit-menu-button"
Branch type="button"
</button> onclick={(event) => openCommitActionMenu(event, item)}
<button class="btn-sm" type="button" onclick={() => onRestoreCommit(item)} disabled={isBusy} title="Bring this commit's files into your working tree as unstaged changes (no history is changed)"> disabled={isBusy}
<RotateCcw size={15} aria-hidden="true" /> title="Commit actions"
Restore aria-label={`Actions for ${item.short_hash}`}
aria-haspopup="menu"
aria-expanded={contextCommit?.hash === item.hash}
>
<EllipsisVertical size={15} aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
@@ -560,6 +639,25 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{#if contextCommit}
<div
class="history-context-menu"
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
role="menu"
tabindex="-1"
aria-label={`Actions for ${contextCommit.short_hash}`}
>
<button type="button" role="menuitem" onclick={createBranchFromContextCommit} disabled={isBusy}>
<GitBranch size={14} aria-hidden="true" />
Branch
</button>
<button type="button" role="menuitem" onclick={restoreContextCommit} disabled={isBusy}>
<RotateCcw size={14} aria-hidden="true" />
Restore
</button>
</div>
{/if}
</section> </section>
{#if branchDialogOpen} {#if branchDialogOpen}