Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
247c118bdf | ||
|
|
c59cb412dd | ||
|
|
1b83429c06 | ||
|
|
3e2885f64d | ||
|
|
eac0f059fa | ||
|
|
aaa4228b82 | ||
|
|
e5c26ea502 | ||
|
|
38b4f98233 | ||
|
|
d592894f28 | ||
|
|
9ee2f71dc3 | ||
|
|
283779610a | ||
|
|
d33f576212 | ||
|
|
a3fd8f1ce8 | ||
|
|
de06e7a15d | ||
|
|
0105e0d46e | ||
|
|
0732f9ca6d | ||
|
|
fcaaa770d6 | ||
|
|
379350b9ee |
@@ -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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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__":
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"bundle": {
|
||||||
|
"targets": ["appimage"]
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-4
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user