From 58bd246221c3d4f4ec682c16655e4e60ca855602 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Tue, 30 Jun 2026 13:23:30 +0200 Subject: [PATCH] add auto-updater and git workflow enhancements This change introduces a comprehensive auto-update mechanism for the application and significant improvements to the integrated Git client experience. Key features include: - **Automated Release Workflow:** A new Gitea Actions workflow (`app_builder.yaml`) and a Python `cicd_tool` are added to automatically build, sign, and upload release artifacts to S3-compatible storage (MinIO) upon a new release. This also generates the `latest.json` file required by the updater. - **Tauri Updater Integration:** The `tauri-plugin-updater` is integrated into the application, enabling it to check for and apply updates seamlessly. - **Robust Git Push Handling:** The application now intelligently handles non-fast-forward push failures by prompting the user to perform a pull/merge operation before re-attempting the push. - **Enhanced File Comparison:** A new `compare_file_to_parent` command is introduced, allowing detailed file diffs against a commit's direct parent, including the "empty tree" for initial commits. - **Explorer Panel Improvements:** "Expand All" and "Collapse All" functionality is added to the file explorer for better navigation. --- .gitea/workflows/app_builder.yaml | 134 ++++++++++ .gitignore | 1 + cicd_tool/.python-version | 1 + cicd_tool/main.py | 232 +++++++++++++++++ cicd_tool/pyproject.toml | 9 + cicd_tool/uv.lock | 161 ++++++++++++ package-lock.json | 10 + package.json | 1 + src-tauri/2 | 7 + src-tauri/Cargo.lock | 332 +++++++++++++++++++++++- src-tauri/Cargo.toml | 3 + src-tauri/capabilities/desktop.json | 14 + src-tauri/src/git.rs | 246 ++++++++++++++++-- src-tauri/src/main.rs | 13 +- src-tauri/tauri.conf.json | 16 +- src/App.svelte | 85 +++++- src/app.css | 17 ++ src/lib/components/ExplorerPanel.svelte | 29 ++- src/lib/git.ts | 9 + 19 files changed, 1283 insertions(+), 37 deletions(-) create mode 100644 .gitea/workflows/app_builder.yaml create mode 100644 cicd_tool/.python-version create mode 100644 cicd_tool/main.py create mode 100644 cicd_tool/pyproject.toml create mode 100644 cicd_tool/uv.lock create mode 100644 src-tauri/2 create mode 100644 src-tauri/capabilities/desktop.json diff --git a/.gitea/workflows/app_builder.yaml b/.gitea/workflows/app_builder.yaml new file mode 100644 index 0000000..b6931ea --- /dev/null +++ b/.gitea/workflows/app_builder.yaml @@ -0,0 +1,134 @@ +name: "publish" + +on: + release: + types: [published] + +jobs: + publish-tauri: + permissions: + contents: write + environment: production + env: + # --- Gitea (unchanged) --- + GITEA_API: https://git.cbsk-tech.de/api/v1 + OWNER: Christoph + REPO: GitLite + GITEA_TOKEN: ${{ secrets.ACTIONS_TOKEN }} + GITEA_FALLBACK_TOKEN: ${{ github.token }} + + # --- MinIO / S3-compatible upload --- + MINIO_ENDPOINT: ${{ vars.MINIO_ENDPOINT }} # e.g. https://updates.example.com + S3_BUCKET: ${{ vars.S3_BUCKET }} # e.g. updates + AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} # used by aws cli + AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }} # used by aws cli + ARTIFACT_BASE_URL: ${{ vars.ARTIFACT_BASE_URL }} # e.g. https://updates.example.com + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} # used by cicd_tool + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} # used by cicd_tool + + defaults: + run: + working-directory: GitLite + + strategy: + fail-fast: false + matrix: + include: + # - platform: 'macos-latest' + # args: '--target aarch64-apple-darwin' + # - platform: 'macos-latest' + # args: '--target x86_64-apple-darwin' + # - platform: 'ubuntu-22.04' + # args: '' + - platform: "windows-latest" + args: "" + + runs-on: ${{ matrix.platform }} + + steps: + # cicd_tool/main.py expects the repo at /GitLite, so check out there. + - uses: actions/checkout@v5 + with: + path: GitLite + + - name: install dependencies (ubuntu only) + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: install frontend dependencies + run: npm ci + + - name: Update package.json Version + run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version + + - name: Update tauri.conf.json Version + shell: pwsh + run: | + $path = ".\src-tauri\tauri.conf.json" + $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 + shell: pwsh + run: | + git config user.name '${{ vars.USERNAME_GIT }}' + git config user.email '${{ vars.EMAIL_GIT }}' + git add package.json src-tauri/tauri.conf.json + + git commit -m 'Update version to ${{ github.ref_name }}' + if ($LASTEXITCODE -ne 0) { + Write-Host 'No changes to commit.' + } + + # Get current branch; if detached, resolve to a remote branch that contains this commit + $branch = (git rev-parse --abbrev-ref HEAD).Trim() + if ($branch -eq 'HEAD' -or [string]::IsNullOrEmpty($branch)) { + # If target_commitish is a branch, use it; if it's a SHA, find a branch that contains it + $candidate = '${{ github.event.release.target_commitish }}' + if ($candidate -match '^[0-9a-f]{40}$') { + $branch = ( + git branch -r --contains $env:GITHUB_SHA | + ForEach-Object { $_.Trim() } | + Where-Object { $_ -and ($_ -notmatch '->') } | + ForEach-Object { $_ -replace '^(?:remotes/)?[^/]+/', '' } | + Select-Object -First 1 + ) + } else { + $branch = $candidate + } + } + + if ([string]::IsNullOrEmpty($branch)) { + Write-Host 'Could not determine branch for push.' + git branch -a + exit 1 + } + + git push origin "HEAD:refs/heads/$branch" + + - name: Build Tauri App + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}} + run: npm run tauri build + # ---------------------- + # Upload to MinIO (via mc) and create latest.json + # ---------------------- + - name: Upload artifacts to MinIO with cicd_tool + working-directory: GitLite/cicd_tool + run: | + pip install uv + uv sync + uv run main.py diff --git a/.gitignore b/.gitignore index ab694d8..5d15844 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.log .idea .DS_Store +~ \ No newline at end of file diff --git a/cicd_tool/.python-version b/cicd_tool/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/cicd_tool/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/cicd_tool/main.py b/cicd_tool/main.py new file mode 100644 index 0000000..12feb8f --- /dev/null +++ b/cicd_tool/main.py @@ -0,0 +1,232 @@ +import argparse +import io +import json +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable, List, Optional +from urllib.parse import urlparse + +from minio import Minio +from minio.error import S3Error + + +REPO_ROOT = Path(__file__).resolve().parent.parent +PROJECT_DIR = REPO_ROOT / "GitLite" +ARTIFACT_ROOT = PROJECT_DIR / "src-tauri" / "target" / "release" / "bundle" + + +class ConfigurationError(RuntimeError): + """Raised when required configuration values are missing or invalid.""" + + +def _resolve_version(version_arg: Optional[str]) -> str: + """Resolve the version to publish, falling back to tauri.conf.json.""" + if version_arg and version_arg.lower() not in {"auto"}: + return version_arg + + config_path = PROJECT_DIR / "src-tauri" / "tauri.conf.json" + if not config_path.exists(): + raise ConfigurationError(f"Cannot locate {config_path}") + + try: + tauri_conf = json.loads(config_path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ConfigurationError(f"Invalid JSON in {config_path}") from exc + + version = tauri_conf.get("version") + if not version: + raise ConfigurationError("Version not found in tauri.conf.json") + + return str(version) + + +def _normalise_endpoint(raw_endpoint: str) -> tuple[str, bool]: + """Return (endpoint, secure) suitable for the MinIO client.""" + if not raw_endpoint: + raise ConfigurationError("MINIO_ENDPOINT is required") + + if "://" not in raw_endpoint: + return raw_endpoint, not raw_endpoint.startswith("http://") + + parsed = urlparse(raw_endpoint) + if not parsed.netloc: + raise ConfigurationError(f"MINIO_ENDPOINT is invalid: {raw_endpoint}") + secure = parsed.scheme != "http" + return parsed.netloc, secure + + +def _create_minio_client() -> Minio: + endpoint_raw = os.environ.get("MINIO_ENDPOINT", "") + endpoint, secure = _normalise_endpoint(endpoint_raw) + + access_key = os.environ.get("S3_ACCESS_KEY") or os.environ.get("AWS_ACCESS_KEY_ID") + secret_key = os.environ.get("S3_SECRET_KEY") or os.environ.get( + "AWS_SECRET_ACCESS_KEY" + ) + if not access_key or not secret_key: + raise ConfigurationError( + "Missing S3 credentials. Provide S3_ACCESS_KEY/S3_SECRET_KEY or " + "AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY." + ) + + region = os.environ.get("S3_REGION") or os.environ.get("AWS_REGION") + + return Minio( + endpoint=endpoint, + access_key=access_key, + secret_key=secret_key, + secure=secure, + region=region, + ) + + +def _ensure_bucket(client: Minio, bucket_name: str) -> None: + if not bucket_name: + raise ConfigurationError("S3_BUCKET is required") + + exists = client.bucket_exists(bucket_name) + if not exists: + client.make_bucket(bucket_name) + + +def _collect_artifacts() -> List[Path]: + artifacts: List[Path] = [] + for directory in ( + ARTIFACT_ROOT / "msi", + ARTIFACT_ROOT / "nsis", + ARTIFACT_ROOT / "app", + ): + if directory.exists(): + artifacts.extend(p for p in directory.rglob("*") if p.is_file()) + return artifacts + + +def _select_primary_artifact(artifacts: Iterable[Path]) -> Optional[Path]: + priority = (".exe", ".msi", ".zip", ".dmg") + for ext in priority: + for artifact in artifacts: + if artifact.suffix.lower() == ext: + return artifact + return next(iter(artifacts), None) + + +def _read_signature(artifact: Path) -> str: + candidates = [ + artifact.with_suffix(artifact.suffix + ".sig"), + artifact.with_name(f"{artifact.stem}.sig"), + ] + for candidate in candidates: + if candidate.exists(): + return candidate.read_text(encoding="utf-8").strip() + raise FileNotFoundError(f"Signature not found for {artifact.name}") + + +def _build_latest_json( + version: str, + primary_artifact: Path, + signature: str, + artifact_base_url: str, + app_dir: str, +) -> str: + base_url = artifact_base_url.rstrip("/") + path_parts = [app_dir.strip("/"), version, primary_artifact.name] + artifact_url = "/".join([base_url, *filter(None, path_parts)]) + + pub_date = ( + datetime.now(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z") + ) + + latest_payload = { + "version": version, + "pub_date": pub_date, + "notes": os.environ.get("RELEASE_NOTES", "Automated release"), + "platforms": { + "windows-x86_64": { + "signature": signature, + "url": artifact_url, + } + }, + } + return json.dumps(latest_payload, separators=(",", ":")) + + +def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None: + resolved_version = _resolve_version(version) + artifact_base_url = os.environ.get("ARTIFACT_BASE_URL", "") + if not artifact_base_url: + raise ConfigurationError("ARTIFACT_BASE_URL is required") + + bucket_name = os.environ.get("S3_BUCKET", "gitlite") + bucket_prefix = (app_dir or os.environ.get("APP_DIR") or "gitlite").strip("/") + + artifacts = _collect_artifacts() + if not artifacts: + raise FileNotFoundError( + "No release artifacts found in src-tauri/target/release/bundle." + ) + + primary = _select_primary_artifact(artifacts) + if not primary: + raise FileNotFoundError("Unable to determine primary artifact to publish.") + + signature = _read_signature(primary) + + latest_json = _build_latest_json( + version=resolved_version, + primary_artifact=primary, + signature=signature, + artifact_base_url=artifact_base_url, + app_dir=bucket_prefix, + ) + + local_dist_dir = PROJECT_DIR / "dist" + local_dist_dir.mkdir(parents=True, exist_ok=True) + latest_json_path = local_dist_dir / "latest.json" + latest_json_path.write_text(latest_json + "\n", encoding="utf-8") + + client = _create_minio_client() + _ensure_bucket(client, bucket_name) + + for artifact in artifacts: + object_segments = [part for part in (resolved_version, artifact.name) if part] + object_name = "/".join(object_segments) + client.fput_object(bucket_name, object_name, str(artifact)) + print(f"Uploaded {artifact.name} -> s3://{bucket_name}/{object_name}") + + latest_object_name = "latest.json" + latest_bytes = latest_json.encode("utf-8") + client.put_object( + bucket_name=bucket_name, + object_name=latest_object_name, + data=io.BytesIO(latest_bytes), + length=len(latest_bytes), + content_type="application/json", + ) + print(f"Uploaded latest.json -> s3://{bucket_name}/{latest_object_name}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Upload GitLite release artifacts to MinIO." + ) + parser.add_argument( + "-v", + "--version", + default=None, + help="Version tag to upload (defaults to reading tauri.conf.json).", + ) + parser.add_argument( + "--app-dir", + default=None, + help="App directory prefix inside the bucket (defaults to gitlite).", + ) + args = parser.parse_args() + + try: + main(version=args.version, app_dir=args.app_dir) + except (ConfigurationError, FileNotFoundError, S3Error) as exc: + raise SystemExit(f"Upload failed: {exc}") from exc diff --git a/cicd_tool/pyproject.toml b/cicd_tool/pyproject.toml new file mode 100644 index 0000000..7d9e860 --- /dev/null +++ b/cicd_tool/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "cicd-tool" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "minio>=7.2.20", +] diff --git a/cicd_tool/uv.lock b/cicd_tool/uv.lock new file mode 100644 index 0000000..a84b415 --- /dev/null +++ b/cicd_tool/uv.lock @@ -0,0 +1,161 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cicd-tool" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "minio" }, +] + +[package.metadata] +requires-dist = [{ name = "minio", specifier = ">=7.2.20" }] + +[[package]] +name = "minio" +version = "7.2.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi" }, + { name = "certifi" }, + { name = "pycryptodome" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/package-lock.json b/package-lock.json index bbe8b67..5bf0fa0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tailwindcss/vite": "^4.3.1", "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-updater": "^2.10.1", "simple-icons": "^16.24.1", "svelte": "^5.0.0", "tailwindcss": "^4.3.1" @@ -1437,6 +1438,15 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-updater": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz", + "integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", diff --git a/package.json b/package.json index 82a52d7..b6e42f5 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@tailwindcss/vite": "^4.3.1", "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-updater": "^2.10.1", "simple-icons": "^16.24.1", "svelte": "^5.0.0", "tailwindcss": "^4.3.1" diff --git a/src-tauri/2 b/src-tauri/2 new file mode 100644 index 0000000..a8a36b6 --- /dev/null +++ b/src-tauri/2 @@ -0,0 +1,7 @@ + +added 1 package, and audited 67 packages in 5s + +13 packages are looking for funding + run `npm fund` for details + +found 0 vulnerabilities diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index beb09b5..e90ddc4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -58,6 +58,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -754,6 +763,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -1039,6 +1059,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1601,6 +1631,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1860,6 +1905,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -2066,6 +2141,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2391,6 +2472,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2454,6 +2547,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2470,6 +2569,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2900,15 +3013,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -2944,6 +3062,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2972,6 +3104,79 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2987,6 +3192,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3330,6 +3544,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.3" @@ -3537,7 +3767,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -3570,6 +3800,17 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3593,7 +3834,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3747,6 +3988,39 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.11.3" @@ -3757,7 +4031,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -3780,7 +4054,7 @@ checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -3857,6 +4131,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tauri-plugin-updater", ] [[package]] @@ -3991,6 +4266,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4304,6 +4589,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4553,6 +4844,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -5087,7 +5387,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -5134,6 +5434,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xdg-home" version = "1.3.0" @@ -5323,6 +5633,18 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5e4358d..52fbb2a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,3 +15,6 @@ keyring = { version = "3", features = ["apple-native", "windows-native", "async- [build-dependencies] tauri-build = { version = "2", features = [] } + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-updater = "2" diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json new file mode 100644 index 0000000..ba6d080 --- /dev/null +++ b/src-tauri/capabilities/desktop.json @@ -0,0 +1,14 @@ +{ + "identifier": "desktop-capability", + "platforms": [ + "macOS", + "windows", + "linux" + ], + "windows": [ + "main" + ], + "permissions": [ + "updater:default" + ] +} \ No newline at end of file diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index e764d09..1d5bf52 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -3,7 +3,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::{Command, Output, Stdio}, sync::{ atomic::{AtomicU64, Ordering}, Arc, Mutex, @@ -139,6 +139,7 @@ enum CheckoutPlan { } const FULL_FILE_DIFF_CONTEXT: &str = "--unified=1000000"; +const EMPTY_TREE_HASH: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; const SEARCH_CANCELLED_MESSAGE: &str = "Suche wurde abgebrochen."; static CANCELLABLE_GIT_OUTPUT_COUNTER: AtomicU64 = AtomicU64::new(0); @@ -336,15 +337,35 @@ pub fn pull( password: Option, ) -> Result { let repo = resolve_repo(&path)?; - match (username.as_deref(), password.as_deref()) { + let pull_args = ["pull", "--no-rebase", "--ff", "--no-edit"]; + let output = match (username.as_deref(), password.as_deref()) { (Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => { - run_git_authenticated(&repo, ["pull", "--ff-only"], u, p)?; - } - _ => { - run_git(&repo, ["pull", "--ff-only"])?; + run_git_authenticated_output(&repo, pull_args, u, p)? } + _ => Command::new("git") + .arg("-C") + .arg(&repo) + .args(pull_args) + .output() + .map_err(|err| { + format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}") + })?, + }; + + if output.status.success() { + return status_for_repo(&repo); } - status_for_repo(&repo) + + let status = status_for_repo(&repo)?; + if has_unresolved_conflicts(&status) { + return Ok(status); + } + + let details = command_output_details(&output); + if is_auth_error(&details) { + return Err(format!("AUTH_FAILED:{details}")); + } + Err(format!("Git-Befehl fehlgeschlagen: {details}")) } #[tauri::command] @@ -949,6 +970,78 @@ pub fn compare_file_to_head( }) } +#[tauri::command] +pub fn compare_file_to_parent( + path: String, + commit: String, + file: String, + old_file: Option, +) -> Result { + let repo = resolve_repo(&path)?; + let mut pathspecs = Vec::new(); + if let Some(old_file) = old_file.filter(|value| !value.trim().is_empty() && value != &file) { + pathspecs.push(old_file); + } + pathspecs.push(file); + validate_files(&pathspecs)?; + + let commit_hash = verify_commit(&repo, &commit)?; + let parents = commit_parents(&repo, &commit_hash)?; + let (from_hash, from_short) = if let Some(parent) = parents.first() { + (parent.clone(), short_hash(parent)) + } else { + (EMPTY_TREE_HASH.to_string(), "empty tree".to_string()) + }; + + let name_status = run_git_with_paths( + &repo, + &[ + "diff", + "--name-status", + "-M", + "-z", + from_hash.as_str(), + commit_hash.as_str(), + ], + &pathspecs, + )?; + let numstat = run_git_with_paths( + &repo, + &[ + "diff", + "--numstat", + "-M", + "-z", + from_hash.as_str(), + commit_hash.as_str(), + ], + &pathspecs, + )?; + let patch_output = run_git_with_paths( + &repo, + &[ + "diff", + "-M", + FULL_FILE_DIFF_CONTEXT, + from_hash.as_str(), + commit_hash.as_str(), + ], + &pathspecs, + )?; + + let files = parse_diff_files(&name_status, &numstat)?; + let patch = String::from_utf8_lossy(&patch_output).to_string(); + + Ok(GitCommitComparison { + from_short, + to_short: short_hash(&commit_hash), + from_hash, + to_hash: commit_hash, + files, + patch, + }) +} + #[tauri::command] pub fn read_conflict(path: String, file: String) -> Result { let repo = resolve_repo(&path)?; @@ -1926,6 +2019,29 @@ fn run_git_authenticated( username: &str, password: &str, ) -> Result, String> +where + I: IntoIterator, + S: AsRef, +{ + let output = run_git_authenticated_output(repo, args, username, password)?; + + if output.status.success() { + return Ok(output.stdout); + } + + let details = command_output_details(&output); + if is_auth_error(&details) { + return Err(format!("AUTH_FAILED:{details}")); + } + Err(format!("Git-Befehl fehlgeschlagen: {details}")) +} + +fn run_git_authenticated_output( + repo: &Path, + args: I, + username: &str, + password: &str, +) -> Result where I: IntoIterator, S: AsRef, @@ -1944,26 +2060,19 @@ where .map_err(|err| format!("Git konnte nicht gestartet werden: {err}")); let _ = std::fs::remove_file(&askpass); + result +} - let output = result?; - - if output.status.success() { - return Ok(output.stdout); - } - +fn command_output_details(output: &Output) -> String { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); - let details = if !stderr.trim().is_empty() { + if !stderr.trim().is_empty() { stderr.trim().to_string() } else if !stdout.trim().is_empty() { stdout.trim().to_string() } else { "Unbekannter Fehler".to_string() - }; - if is_auth_error(&details) { - return Err(format!("AUTH_FAILED:{details}")); } - Err(format!("Git-Befehl fehlgeschlagen: {details}")) } /// Heuristic: did git fail because the credentials were rejected/expired, @@ -2744,6 +2853,107 @@ mod tests { assert!(!comparison.patch.contains("other file")); } + #[test] + fn compare_file_to_parent_reports_selected_file_change_in_commit() { + let repo = init_temp_repo("compare_file_to_parent"); + commit_initial_file(&repo.path); + let first_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]); + + fs::write(repo.path.join("old.txt"), "original\nsecond line\n") + .expect("tracked file should change"); + fs::write(repo.path.join("other.txt"), "other file\n") + .expect("other file should be written"); + run_git_test(&repo.path, ["add", "old.txt", "other.txt"]); + run_git_test(&repo.path, ["commit", "-q", "-m", "second"]); + let second_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]); + + let comparison = compare_file_to_parent( + repo.path.to_string_lossy().to_string(), + second_commit.clone(), + "old.txt".to_string(), + None, + ) + .unwrap(); + + assert_eq!(comparison.from_hash, first_commit); + assert_eq!(comparison.to_hash, second_commit); + assert_eq!(comparison.files.len(), 1); + assert_eq!(comparison.files[0].path, "old.txt"); + assert!(comparison.patch.contains("second line")); + assert!(!comparison.patch.contains("other file")); + } + + #[test] + fn compare_file_to_parent_uses_empty_tree_for_initial_commit() { + let repo = init_temp_repo("compare_file_to_parent_initial"); + commit_initial_file(&repo.path); + let initial_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]); + + let comparison = compare_file_to_parent( + repo.path.to_string_lossy().to_string(), + initial_commit.clone(), + "old.txt".to_string(), + None, + ) + .unwrap(); + + assert_eq!(comparison.from_hash, EMPTY_TREE_HASH); + assert_eq!(comparison.from_short, "empty tree"); + assert_eq!(comparison.to_hash, initial_commit); + assert_eq!(comparison.files.len(), 1); + assert_eq!(comparison.files[0].path, "old.txt"); + assert!(comparison.patch.contains("+original")); + } + + #[test] + #[cfg_attr( + windows, + ignore = "Git for Windows can fail local pull tests with a sh signal pipe error" + )] + fn pull_merges_diverging_branch_instead_of_requiring_fast_forward() { + let repo = init_temp_repo("pull_diverged"); + commit_initial_file(&repo.path); + let base_branch = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]); + + run_git_test(&repo.path, ["checkout", "-q", "-b", "remote-change"]); + fs::write(repo.path.join("remote.txt"), "remote change\n") + .expect("remote file should be written"); + run_git_test(&repo.path, ["add", "remote.txt"]); + run_git_test(&repo.path, ["commit", "-q", "-m", "remote change"]); + + run_git_test(&repo.path, ["checkout", "-q", base_branch.as_str()]); + fs::write(repo.path.join("local.txt"), "local change\n") + .expect("local file should be written"); + run_git_test(&repo.path, ["add", "local.txt"]); + run_git_test(&repo.path, ["commit", "-q", "-m", "local change"]); + run_git_test(&repo.path, ["remote", "add", "origin", "."]); + run_git_test( + &repo.path, + ["config", &format!("branch.{base_branch}.remote"), "origin"], + ); + run_git_test( + &repo.path, + [ + "config", + &format!("branch.{base_branch}.merge"), + "refs/heads/remote-change", + ], + ); + + let status = pull(repo.path.to_string_lossy().to_string(), None, None).unwrap(); + + assert!(status.clean, "{:?}", status.files); + assert!(repo.path.join("remote.txt").exists()); + assert!(repo.path.join("local.txt").exists()); + + let parent_count = + git_output_test(&repo.path, ["rev-list", "--parents", "-n", "1", "HEAD"]) + .split_whitespace() + .count() + - 1; + assert_eq!(parent_count, 2); + } + #[test] fn read_and_resolve_conflict_round_trip() { let repo = init_temp_repo("resolve_conflict"); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5ef6de6..996a15e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,15 +4,17 @@ mod git; use git::{ cancel_code_search, checkout_branch, commit, compare_commits, compare_file_to_head, - cred_delete, cred_load, cred_save, diff_file_against_working_tree, get_remote_url, get_status, - list_branches, list_commits, list_file_history, list_repository_files, merge_branch, - open_repository, pull, push, read_conflict, resolve_conflict, resolve_conflict_side, - restore_file_from_commit, restore_files, restore_to_commit, search_code_introductions, - stage_files, unstage_files, SearchCancellationState, + compare_file_to_parent, cred_delete, cred_load, cred_save, diff_file_against_working_tree, + get_remote_url, get_status, list_branches, list_commits, list_file_history, + list_repository_files, merge_branch, open_repository, pull, push, read_conflict, + resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files, + restore_to_commit, search_code_introductions, stage_files, unstage_files, + SearchCancellationState, }; fn main() { tauri::Builder::default() + .plugin(tauri_plugin_updater::Builder::new().build()) .manage(SearchCancellationState::default()) .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ @@ -34,6 +36,7 @@ fn main() { list_file_history, compare_commits, compare_file_to_head, + compare_file_to_parent, diff_file_against_working_tree, search_code_introductions, cancel_code_search, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cb23cc4..5dd288d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -29,6 +29,20 @@ "bundle": { "active": true, "targets": "all", - "icon": ["icons/icon.ico"] + "icon": ["icons/icon.ico"], + "createUpdaterArtifacts": true, + "windows": { + "nsis": { + "installMode": "currentUser" + } + } + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdDNEJGRTZERTREOTc5QjkKUldTNWVkbmtiZjVMZk02YzdpenVLOVltREJpRkFrc2F4dUwzaTV5M3pacFRVR0tmd25xSGtjK1MK", + "endpoints" : [ + "https://cdn.cbsk-tech.de/GitLite/latest.json" + ] + } } } diff --git a/src/App.svelte b/src/App.svelte index dd90f4f..4dca571 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -22,7 +22,7 @@ compareCommits, cancelCodeSearch, diffFileAgainstWorkingTree, - compareFileToHead, + compareFileToParent, getStatus, listBranches, listCommits, @@ -181,6 +181,18 @@ try { return JSON.stringify(error) ?? "Unknown error"; } catch { return "Unknown error"; } } + function isNonFastForwardPushError(message: string): boolean { + const value = message.toLowerCase(); + return value.includes("non-fast-forward") + || value.includes("failed to push some refs") + || value.includes("tip of your current branch is behind") + || value.includes("fetch first"); + } + + function statusHasConflicts(value: GitStatus | null): boolean { + return (value?.files ?? []).some((file) => file.staged === "conflicted" || file.unstaged === "conflicted"); + } + async function runOperation(label: string, task: () => Promise) { if (isBusy) return; operation = label; @@ -201,13 +213,17 @@ }); } - function defaultExpandedExplorerPaths(files: GitRepositoryFile[]): Set { - const expanded = new Set(); + function allExplorerFolderPaths(files: GitRepositoryFile[]): Set { + const folders = new Set(); for (const file of files) { const parts = file.path.split(/[\\/]+/).filter(Boolean); - if (parts.length > 1) expanded.add(parts[0]); + let currentPath = ""; + for (let index = 0; index < parts.length - 1; index++) { + currentPath = currentPath ? `${currentPath}/${parts[index]}` : parts[index]; + folders.add(currentPath); + } } - return expanded; + return folders; } // ── Refresh helpers ──────────────────────────────────────────────────────── @@ -231,7 +247,8 @@ async function refreshExplorerFiles(path = activeRepoPath) { repoFiles = await listRepositoryFiles(path); - if (expandedExplorerPaths.size === 0) expandedExplorerPaths = defaultExpandedExplorerPaths(repoFiles); + const folderPaths = allExplorerFolderPaths(repoFiles); + expandedExplorerPaths = new Set([...expandedExplorerPaths].filter((folder) => folderPaths.has(folder))); if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) { selectedExplorerPath = ""; selectedExplorerKind = "file"; @@ -405,6 +422,50 @@ await refreshCommitHistory(activeRepoPath); await refreshFileHistory(activeRepoPath); }); + + if (errorMessage && !isAuthError(errorMessage) && isNonFastForwardPushError(errorMessage)) { + errorMessage = ""; + const shouldSync = window.confirm( + "Der Remote hat neuere Commits, deshalb wurde der Push abgelehnt.\n\nJetzt Pull/Merge ausfuehren und danach den Push erneut versuchen?", + ); + + if (!shouldSync) { + const message = "Push abgelehnt: Der Remote hat neuere Commits. Pull zuerst ausfuehren, dann erneut pushen."; + if (fromStore) errorMessage = message; + else credDialogError = message; + return; + } + + if (!fromStore) credDialogError = ""; + + await runOperation("Pulling before push", async () => { + applyStatus(await pull(activeRepoPath, username, password)); + await refreshBranchList(activeRepoPath); + await refreshCommitHistory(activeRepoPath); + await refreshExplorerFiles(activeRepoPath); + await refreshFileHistory(activeRepoPath); + }); + + if (errorMessage) { + handleRemoteResult("pull", key, fromStore); + return; + } + + if (statusHasConflicts(status)) { + credDialogOpen = false; + credDialogAction = null; + errorMessage = "Pull hat Merge-Konflikte erzeugt. Loese die Konflikte, committe den Merge und pushe danach erneut."; + return; + } + + await runOperation("Pushing after pull", async () => { + applyStatus(await push(activeRepoPath, username, password)); + await refreshBranchList(activeRepoPath); + await refreshCommitHistory(activeRepoPath); + await refreshFileHistory(activeRepoPath); + }); + } + handleRemoteResult("push", key, fromStore); } @@ -541,7 +602,7 @@ async function previewCommitFileFromHistory(target: GitCommit, file: GitCommitFile) { if (!activeRepoPath) return; await runOperation(`Diffing ${file.path}`, async () => { - const result = await compareFileToHead(activeRepoPath, target.hash, file.path); + const result = await compareFileToParent(activeRepoPath, target.hash, file.path, file.old_path); const matchingFile = result.files.find((diffFile) => diffFile.path === file.path || diffFile.old_path === file.old_path || diffFile.old_path === file.path, ); @@ -561,6 +622,14 @@ expandedExplorerPaths = next; } + function expandAllExplorerFolders() { + expandedExplorerPaths = allExplorerFolderPaths(repoFiles); + } + + function collapseAllExplorerFolders() { + expandedExplorerPaths = new Set(); + } + async function selectExplorerNode(node: ExplorerNode) { if (!activeRepoPath || (selectedExplorerPath === node.path && selectedExplorerKind === node.kind)) return; selectedExplorerPath = node.path; @@ -845,6 +914,8 @@ {hasRepository} {isBusy} onToggleFolder={toggleExplorerFolder} + onExpandAllFolders={expandAllExplorerFolders} + onCollapseAllFolders={collapseAllExplorerFolders} onSelectNode={selectExplorerNode} /> diff --git a/src/app.css b/src/app.css index 6b6d776..70d2417 100644 --- a/src/app.css +++ b/src/app.css @@ -593,6 +593,23 @@ /* --- Explorer --- */ + .explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; } + .explorer-bulk-button { + width: 26px; + min-width: 26px; + min-height: 26px; + padding: 0; + border-color: rgba(65,209,255,0.2); + border-radius: 7px; + color: var(--color-ink-dim); + background: rgba(65,209,255,0.06); + } + .explorer-bulk-button:hover:not(:disabled) { + border-color: rgba(65,209,255,0.45); + color: #ffffff; + background: rgba(65,209,255,0.13); + } + .explorer-row { display: grid; grid-template-columns: auto auto minmax(0, 1fr) auto; diff --git a/src/lib/components/ExplorerPanel.svelte b/src/lib/components/ExplorerPanel.svelte index 863605c..b801204 100644 --- a/src/lib/components/ExplorerPanel.svelte +++ b/src/lib/components/ExplorerPanel.svelte @@ -31,6 +31,8 @@ hasRepository: boolean; isBusy: boolean; onToggleFolder: (node: ExplorerNode) => void; + onExpandAllFolders: () => void; + onCollapseAllFolders: () => void; onSelectNode: (node: ExplorerNode) => void; } @@ -42,6 +44,8 @@ hasRepository = false, isBusy = false, onToggleFolder = () => {}, + onExpandAllFolders = () => {}, + onCollapseAllFolders = () => {}, onSelectNode = () => {}, }: Props = $props(); @@ -152,6 +156,7 @@ let explorerTree = $derived(buildExplorerTree(repoFiles)); let visibleNodes = $derived(flattenExplorerTree(explorerTree, expandedExplorerPaths)); + let hasFolders = $derived(explorerTree.some((node) => node.kind === "folder"));
@@ -160,7 +165,29 @@ Explorer

Files

- {repoFiles.length} +
+ + + {repoFiles.length} +
{#if !hasRepository} diff --git a/src/lib/git.ts b/src/lib/git.ts index 98ca760..e334664 100644 --- a/src/lib/git.ts +++ b/src/lib/git.ts @@ -128,6 +128,15 @@ export function compareFileToHead( return invoke("compare_file_to_head", { path, commit, file }); } +export function compareFileToParent( + path: string, + commit: string, + file: string, + oldFile: string | null = null, +): Promise { + return invoke("compare_file_to_parent", { path, commit, file, oldFile }); +} + export function searchCodeIntroductions( path: string, query: string,