diff --git a/.github/workflows/build-release-artifacts.yml b/.github/workflows/build-release-artifacts.yml index 4f92f5e..b85056c 100644 --- a/.github/workflows/build-release-artifacts.yml +++ b/.github/workflows/build-release-artifacts.yml @@ -142,14 +142,14 @@ jobs: - name: Build release binary shell: pwsh run: | - cargo build -p gitcomet-app --release --locked --features ui-gpui,gix --bins + cargo build -p gitcomet --release --locked --features ui-gpui,gix --bins - name: Package portable ZIP shell: pwsh run: | $ErrorActionPreference = "Stop" New-Item -ItemType Directory -Path dist\portable -Force | Out-Null - Copy-Item target\release\gitcomet-app.exe dist\portable\gitcomet-app.exe -Force + Copy-Item target\release\gitcomet.exe dist\portable\gitcomet.exe -Force Copy-Item README.md dist\portable\README.md -Force Copy-Item LICENSE-AGPL-3.0 dist\portable\LICENSE-AGPL-3.0 -Force Copy-Item NOTICE dist\portable\NOTICE -Force @@ -164,11 +164,11 @@ jobs: $ErrorActionPreference = "Stop" choco install wixtoolset --version "${env:WIXTOOLSET_VERSION}" --yes --no-progress cargo install cargo-wix --version "${env:CARGO_WIX_VERSION}" --locked - if (!(Test-Path "crates\gitcomet-app\wix\main.wxs")) { - cargo wix init --package gitcomet-app + if (!(Test-Path "crates\gitcomet\wix\main.wxs")) { + cargo wix init --package gitcomet } $msiName = "gitcomet-v${env:VERSION}-windows-x86_64.msi" - cargo wix --package gitcomet-app --profile release --nocapture --no-build --output "dist\$msiName" + cargo wix --package gitcomet --profile release --nocapture --no-build --output "dist\$msiName" - name: Upload Windows artifacts uses: actions/upload-artifact@v7 @@ -212,7 +212,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Build release binary - run: cargo build -p gitcomet-app --release --locked --features ui-gpui,gix + run: cargo build -p gitcomet --release --locked --features ui-gpui,gix - name: Normalize desktop entry metadata run: | @@ -227,7 +227,7 @@ jobs: set -euo pipefail root="gitcomet-v${VERSION}-linux-x86_64" mkdir -p "dist/stage/${root}" - install -m755 target/release/gitcomet-app "dist/stage/${root}/gitcomet-app" + install -m755 target/release/gitcomet "dist/stage/${root}/gitcomet" install -m644 README.md "dist/stage/${root}/README.md" install -m644 LICENSE-AGPL-3.0 "dist/stage/${root}/LICENSE-AGPL-3.0" install -m644 NOTICE "dist/stage/${root}/NOTICE" @@ -245,7 +245,7 @@ jobs: mkdir -p "${pkg_root}/usr/share/applications" mkdir -p "${pkg_root}/usr/share/icons/hicolor/512x512/apps" - install -m755 target/release/gitcomet-app "${pkg_root}/usr/bin/gitcomet-app" + install -m755 target/release/gitcomet "${pkg_root}/usr/bin/gitcomet" install -m644 dist/gitcomet.desktop "${pkg_root}/usr/share/applications/gitcomet.desktop" install -m644 assets/gitcomet-512.png "${pkg_root}/usr/share/icons/hicolor/512x512/apps/gitcomet.png" @@ -267,7 +267,7 @@ jobs: set -euo pipefail appdir="dist/AppDir" mkdir -p "${appdir}/usr/bin" - install -m755 target/release/gitcomet-app "${appdir}/usr/bin/gitcomet-app" + install -m755 target/release/gitcomet "${appdir}/usr/bin/gitcomet" install -m644 dist/gitcomet.desktop "${appdir}/gitcomet.desktop" install -m644 assets/gitcomet-512.png "${appdir}/gitcomet.png" @@ -275,7 +275,7 @@ jobs: echo '#!/usr/bin/env bash' echo 'set -euo pipefail' echo 'HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' - echo 'exec "${HERE}/usr/bin/gitcomet-app" "$@"' + echo 'exec "${HERE}/usr/bin/gitcomet" "$@"' } > "${appdir}/AppRun" chmod +x "${appdir}/AppRun" @@ -357,6 +357,11 @@ jobs: RELEASE_ID: ${{ needs.prepare.outputs.release_id }} ENABLE_KEYLESS_SIGNING: ${{ vars.ENABLE_KEYLESS_SIGNING }} steps: + - uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.tag }} + fetch-depth: 0 + - uses: actions/download-artifact@v8 with: name: windows-release-artifacts @@ -387,18 +392,22 @@ jobs: fi ls -lah dist/release - - name: Generate Homebrew formula + - name: Generate Homebrew cask and formula run: | set -euo pipefail arm_tar="gitcomet-v${VERSION}-macos-arm64.tar.gz" intel_tar="gitcomet-v${VERSION}-macos-x86_64.tar.gz" linux_tar="gitcomet-v${VERSION}-linux-x86_64.tar.gz" + arm_dmg="gitcomet-v${VERSION}-macos-arm64.dmg" + intel_dmg="gitcomet-v${VERSION}-macos-x86_64.dmg" arm_path="dist/release/${arm_tar}" intel_path="dist/release/${intel_tar}" linux_path="dist/release/${linux_tar}" + arm_dmg_path="dist/release/${arm_dmg}" + intel_dmg_path="dist/release/${intel_dmg}" - if [ ! -f "${arm_path}" ] || [ ! -f "${intel_path}" ] || [ ! -f "${linux_path}" ]; then - echo "::error title=Missing release tarballs::Expected ${arm_tar}, ${intel_tar}, and ${linux_tar} in dist/release." + if [ ! -f "${arm_path}" ] || [ ! -f "${intel_path}" ] || [ ! -f "${linux_path}" ] || [ ! -f "${arm_dmg_path}" ] || [ ! -f "${intel_dmg_path}" ]; then + echo "::error title=Missing release assets::Expected ${arm_tar}, ${intel_tar}, ${linux_tar}, ${arm_dmg}, and ${intel_dmg} in dist/release." exit 1 fi @@ -408,6 +417,13 @@ jobs: --arm-tar "${arm_path}" \ --intel-tar "${intel_path}" \ --linux-tar "${linux_path}" \ + --output "dist/release/gitcomet-cli.rb" + + scripts/generate-homebrew-cask.sh \ + --version "${VERSION}" \ + --github-repo "${GITHUB_REPOSITORY}" \ + --arm-dmg "${arm_dmg_path}" \ + --intel-dmg "${intel_dmg_path}" \ --output "dist/release/gitcomet.rb" - name: Generate checksums diff --git a/.github/workflows/cross-platform-tests.yml b/.github/workflows/cross-platform-tests.yml index b28771f..c4d8044 100644 --- a/.github/workflows/cross-platform-tests.yml +++ b/.github/workflows/cross-platform-tests.yml @@ -163,8 +163,8 @@ jobs: echo "XDG_CURRENT_DESKTOP=${XDG_CURRENT_DESKTOP:-}" - name: Run display-selection integration tests run: | - cargo test -p gitcomet-app $APP_FEATURES --test mergetool_git_integration gui_default --verbose - cargo test -p gitcomet-app $APP_FEATURES --test difftool_git_integration gui_default --verbose + cargo test -p gitcomet $APP_FEATURES --test mergetool_git_integration gui_default --verbose + cargo test -p gitcomet $APP_FEATURES --test difftool_git_integration gui_default --verbose - name: Run UI smoke test under selected profile run: cargo test -p gitcomet-ui-gpui smoke_tests::smoke_view_renders_without_panicking -- --exact @@ -200,9 +200,9 @@ jobs: --verbose - name: Gatekeeper and code-signing check (informational) run: | - cargo build -p gitcomet-app $APP_FEATURES --release --locked - codesign --verify --verbose target/release/gitcomet-app || true - spctl --assess --type execute --verbose target/release/gitcomet-app || true + cargo build -p gitcomet $APP_FEATURES --release --locked + codesign --verify --verbose target/release/gitcomet || true + spctl --assess --type execute --verbose target/release/gitcomet || true windows-tests: name: Windows Tests (PowerShell + CMD) @@ -235,7 +235,7 @@ jobs: --verbose - name: Run standalone smoke test in CMD shell: cmd - run: cargo test -p gitcomet-app --no-default-features --features gix --test standalone_tool_mode_integration help_flag_exits_zero -- --exact + run: cargo test -p gitcomet --no-default-features --features gix --test standalone_tool_mode_integration help_flag_exits_zero -- --exact - name: Show path behavior (CMD + Git Bash) shell: cmd run: | diff --git a/.github/workflows/deploy-apt-repo.yml b/.github/workflows/deploy-apt-repo.yml index 51e3342..21a01d0 100644 --- a/.github/workflows/deploy-apt-repo.yml +++ b/.github/workflows/deploy-apt-repo.yml @@ -21,10 +21,6 @@ on: required: false type: string default: "" - repo_base_url: - required: false - type: string - default: "" distribution: required: false type: string @@ -48,7 +44,7 @@ on: repo_description: required: false type: string - default: "GitComet APT repository" + default: "GitComet APT repository." container_public_access: required: false type: string @@ -75,32 +71,27 @@ on: required: false type: string storage_account: - description: "Azure Storage account name" + description: "Azure Storage account name. Defaults to repository variable APT_STORAGE_ACCOUNT when omitted." required: false default: "" type: string storage_container: - description: "Azure Blob container that holds the APT repository" + description: "Azure Blob container that holds the APT repository. Defaults to APT_STORAGE_CONTAINER when omitted." required: false default: "apt" type: string storage_prefix: - description: "Optional blob prefix within the container" - required: false - default: "" - type: string - repo_base_url: - description: "Public HTTPS base URL for the APT repo root" + description: "Optional blob prefix within the container. Defaults to APT_STORAGE_PREFIX when omitted." required: false default: "" type: string distribution: - description: "APT suite/codename" + description: "APT suite/codename. Defaults to APT_REPO_DISTRIBUTION when omitted." required: false default: "stable" type: string component: - description: "APT component" + description: "APT component. Defaults to APT_REPO_COMPONENT when omitted." required: false default: "main" type: string @@ -110,22 +101,22 @@ on: default: "amd64" type: string repo_origin: - description: "Origin metadata in Release file" + description: "Origin metadata in Release file. Defaults to APT_REPO_ORIGIN when omitted." required: false default: "GitComet" type: string repo_label: - description: "Label metadata in Release file" + description: "Label metadata in Release file. Defaults to APT_REPO_LABEL when omitted." required: false default: "GitComet" type: string repo_description: - description: "Description metadata in Release file" + description: "Description metadata in Release file. Defaults to APT_REPO_DESCRIPTION when omitted." required: false - default: "GitComet APT repository" + default: "GitComet APT repository." type: string container_public_access: - description: "Public access used only when the blob container must be created" + description: "Public access used only when the blob container must be created. Defaults to APT_STORAGE_PUBLIC_ACCESS when omitted." required: false default: "blob" type: choice @@ -165,26 +156,33 @@ jobs: DISPATCH_VERSION: ${{ github.event.inputs.version }} INPUT_STORAGE_ACCOUNT: ${{ inputs.storage_account }} DISPATCH_STORAGE_ACCOUNT: ${{ github.event.inputs.storage_account }} + VAR_STORAGE_ACCOUNT: ${{ vars.APT_STORAGE_ACCOUNT }} INPUT_STORAGE_CONTAINER: ${{ inputs.storage_container }} DISPATCH_STORAGE_CONTAINER: ${{ github.event.inputs.storage_container }} + VAR_STORAGE_CONTAINER: ${{ vars.APT_STORAGE_CONTAINER }} INPUT_STORAGE_PREFIX: ${{ inputs.storage_prefix }} DISPATCH_STORAGE_PREFIX: ${{ github.event.inputs.storage_prefix }} - INPUT_REPO_BASE_URL: ${{ inputs.repo_base_url }} - DISPATCH_REPO_BASE_URL: ${{ github.event.inputs.repo_base_url }} + VAR_STORAGE_PREFIX: ${{ vars.APT_STORAGE_PREFIX }} INPUT_DISTRIBUTION: ${{ inputs.distribution }} DISPATCH_DISTRIBUTION: ${{ github.event.inputs.distribution }} + VAR_DISTRIBUTION: ${{ vars.APT_REPO_DISTRIBUTION }} INPUT_COMPONENT: ${{ inputs.component }} DISPATCH_COMPONENT: ${{ github.event.inputs.component }} + VAR_COMPONENT: ${{ vars.APT_REPO_COMPONENT }} INPUT_ARCHITECTURE: ${{ inputs.architecture }} DISPATCH_ARCHITECTURE: ${{ github.event.inputs.architecture }} INPUT_REPO_ORIGIN: ${{ inputs.repo_origin }} DISPATCH_REPO_ORIGIN: ${{ github.event.inputs.repo_origin }} + VAR_REPO_ORIGIN: ${{ vars.APT_REPO_ORIGIN }} INPUT_REPO_LABEL: ${{ inputs.repo_label }} DISPATCH_REPO_LABEL: ${{ github.event.inputs.repo_label }} + VAR_REPO_LABEL: ${{ vars.APT_REPO_LABEL }} INPUT_REPO_DESCRIPTION: ${{ inputs.repo_description }} DISPATCH_REPO_DESCRIPTION: ${{ github.event.inputs.repo_description }} + VAR_REPO_DESCRIPTION: ${{ vars.APT_REPO_DESCRIPTION }} INPUT_CONTAINER_PUBLIC_ACCESS: ${{ inputs.container_public_access }} DISPATCH_CONTAINER_PUBLIC_ACCESS: ${{ github.event.inputs.container_public_access }} + VAR_CONTAINER_PUBLIC_ACCESS: ${{ vars.APT_STORAGE_PUBLIC_ACCESS }} INPUT_DRY_RUN: ${{ inputs.dry_run }} DISPATCH_DRY_RUN: ${{ github.event.inputs.dry_run }} run: | @@ -192,17 +190,16 @@ jobs: tag="${INPUT_TAG:-${DISPATCH_TAG:-}}" version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}" - storage_account="${INPUT_STORAGE_ACCOUNT:-${DISPATCH_STORAGE_ACCOUNT:-}}" - storage_container="${INPUT_STORAGE_CONTAINER:-${DISPATCH_STORAGE_CONTAINER:-apt}}" - storage_prefix="${INPUT_STORAGE_PREFIX:-${DISPATCH_STORAGE_PREFIX:-}}" - repo_base_url="${INPUT_REPO_BASE_URL:-${DISPATCH_REPO_BASE_URL:-}}" - distribution="${INPUT_DISTRIBUTION:-${DISPATCH_DISTRIBUTION:-stable}}" - component="${INPUT_COMPONENT:-${DISPATCH_COMPONENT:-main}}" + storage_account="${INPUT_STORAGE_ACCOUNT:-${DISPATCH_STORAGE_ACCOUNT:-${VAR_STORAGE_ACCOUNT:-}}}" + storage_container="${INPUT_STORAGE_CONTAINER:-${DISPATCH_STORAGE_CONTAINER:-${VAR_STORAGE_CONTAINER:-apt}}}" + storage_prefix="${INPUT_STORAGE_PREFIX:-${DISPATCH_STORAGE_PREFIX:-${VAR_STORAGE_PREFIX:-}}}" + distribution="${INPUT_DISTRIBUTION:-${DISPATCH_DISTRIBUTION:-${VAR_DISTRIBUTION:-stable}}}" + component="${INPUT_COMPONENT:-${DISPATCH_COMPONENT:-${VAR_COMPONENT:-main}}}" architecture="${INPUT_ARCHITECTURE:-${DISPATCH_ARCHITECTURE:-amd64}}" - repo_origin="${INPUT_REPO_ORIGIN:-${DISPATCH_REPO_ORIGIN:-GitComet}}" - repo_label="${INPUT_REPO_LABEL:-${DISPATCH_REPO_LABEL:-GitComet}}" - repo_description="${INPUT_REPO_DESCRIPTION:-${DISPATCH_REPO_DESCRIPTION:-GitComet APT repository}}" - container_public_access="${INPUT_CONTAINER_PUBLIC_ACCESS:-${DISPATCH_CONTAINER_PUBLIC_ACCESS:-blob}}" + repo_origin="${INPUT_REPO_ORIGIN:-${DISPATCH_REPO_ORIGIN:-${VAR_REPO_ORIGIN:-GitComet}}}" + repo_label="${INPUT_REPO_LABEL:-${DISPATCH_REPO_LABEL:-${VAR_REPO_LABEL:-GitComet}}}" + repo_description="${INPUT_REPO_DESCRIPTION:-${DISPATCH_REPO_DESCRIPTION:-${VAR_REPO_DESCRIPTION:-GitComet APT repository.}}}" + container_public_access="${INPUT_CONTAINER_PUBLIC_ACCESS:-${DISPATCH_CONTAINER_PUBLIC_ACCESS:-${VAR_CONTAINER_PUBLIC_ACCESS:-blob}}}" dry_run="${INPUT_DRY_RUN:-${DISPATCH_DRY_RUN:-false}}" tag="$(echo "$tag" | tr -d '[:space:]')" @@ -210,7 +207,6 @@ jobs: storage_account="$(echo "$storage_account" | tr -d '[:space:]')" storage_container="$(echo "$storage_container" | tr -d '[:space:]')" storage_prefix="$(echo "$storage_prefix" | sed 's#^/*##;s#/*$##')" - repo_base_url="$(echo "$repo_base_url" | sed 's#[[:space:]]##g;s#/*$##')" distribution="$(echo "$distribution" | tr -d '[:space:]')" component="$(echo "$component" | tr -d '[:space:]')" architecture="$(echo "$architecture" | tr -d '[:space:]')" @@ -274,7 +270,8 @@ jobs: exit 1 fi - if [ -z "$repo_base_url" ] && [ -n "$storage_account" ] && [ "$storage_container" != '$web' ]; then + repo_base_url="" + if [ -n "$storage_account" ] && [ "$storage_container" != '$web' ]; then repo_base_url="https://${storage_account}.blob.core.windows.net/${storage_container}" if [ -n "$storage_prefix" ]; then repo_base_url="${repo_base_url}/${storage_prefix}" @@ -365,7 +362,7 @@ jobs: echo "Key-Type: RSA" echo "Key-Length: 3072" echo "Name-Real: GitComet APT Dry Run" - echo "Name-Email: ci@example.invalid" + echo "Name-Email: ci@autoexplore.ai" echo "Expire-Date: 0" echo "%no-protection" echo "%commit" @@ -419,16 +416,16 @@ jobs: blob_prefix="${blob_prefix}/" fi - mapfile -t existing_blobs < <( - az storage blob list \ - --account-name "$STORAGE_ACCOUNT" \ - --container-name "$STORAGE_CONTAINER" \ - --account-key "$AZURE_STORAGE_ACCOUNT_KEY" \ - --prefix "$blob_prefix" \ - --num-results "*" \ - --query "[].name" \ - -o tsv - ) + existing_blobs_file="${RUNNER_TEMP}/apt-existing-blobs.txt" + az storage blob list \ + --account-name "$STORAGE_ACCOUNT" \ + --container-name "$STORAGE_CONTAINER" \ + --account-key "$AZURE_STORAGE_ACCOUNT_KEY" \ + --prefix "$blob_prefix" \ + --num-results "*" \ + --query "[].name" \ + -o tsv > "$existing_blobs_file" + mapfile -t existing_blobs < "$existing_blobs_file" if [ "${#existing_blobs[@]}" -eq 0 ]; then exit 0 @@ -472,6 +469,7 @@ jobs: --label "${{ steps.norm.outputs.repo_label }}" --description "${{ steps.norm.outputs.repo_description }}" --signing-key "${{ steps.gpg.outputs.signing_key }}" + --repo-url "https://apt.gitcomet.dev" ) if [ -n "${{ steps.norm.outputs.repo_base_url }}" ]; then @@ -495,6 +493,9 @@ jobs: run: | set -euo pipefail + metadata_cache_control='no-cache, no-store, max-age=0, must-revalidate' + package_cache_control='public, max-age=31536000, immutable' + mkdir -p "$AZURE_CONFIG_DIR" : > dist/apt-uploaded-blobs.txt @@ -505,15 +506,25 @@ jobs: blob_name="${STORAGE_PREFIX}/${rel_path}" fi - az storage blob upload \ - --account-name "$STORAGE_ACCOUNT" \ - --container-name "$STORAGE_CONTAINER" \ - --account-key "$AZURE_STORAGE_ACCOUNT_KEY" \ - --name "$blob_name" \ - --file "$file_path" \ - --overwrite true \ - --no-progress \ + upload_args=( + az storage blob upload + --account-name "$STORAGE_ACCOUNT" + --container-name "$STORAGE_CONTAINER" + --account-key "$AZURE_STORAGE_ACCOUNT_KEY" + --name "$blob_name" + --file "$file_path" + --overwrite true + --no-progress --output none + ) + + if [[ "$rel_path" == dists/* || "$rel_path" == "gitcomet.sources" || "$rel_path" == "gitcomet.list" || "$rel_path" == "gitcomet-archive-keyring.gpg" || "$rel_path" == "gitcomet-archive-keyring.asc" || "$rel_path" == "README.txt" ]]; then + upload_args+=(--content-cache-control "$metadata_cache_control") + elif [[ "$rel_path" == pool/* ]]; then + upload_args+=(--content-cache-control "$package_cache_control") + fi + + "${upload_args[@]}" printf '%s\n' "$blob_name" >> dist/apt-uploaded-blobs.txt done < <(find dist/apt-repo -type f -print0 | sort -z) @@ -549,28 +560,3 @@ jobs: --delete-snapshots include \ --output none done < "${RUNNER_TEMP}/apt-stale.txt" - - - name: Emit deployment summary - run: | - set -euo pipefail - uploaded="yes" - if [ "${{ steps.norm.outputs.dry_run }}" = "true" ]; then - uploaded="no (dry run)" - fi - - { - echo "### APT repository deployment" - echo "" - echo "- Release: \`${{ steps.norm.outputs.tag }}\`" - echo "- Debian package: \`${{ steps.deb.outputs.name }}\`" - echo "- Distribution: \`${{ steps.norm.outputs.distribution }}\`" - echo "- Component: \`${{ steps.norm.outputs.component }}\`" - echo "- Architecture: \`${{ steps.norm.outputs.architecture }}\`" - echo "- Azure target: \`${{ steps.norm.outputs.storage_account }}/${{ steps.norm.outputs.storage_container }}\`" - echo "- Prefix: \`${{ steps.norm.outputs.storage_prefix }}\`" - echo "- Public repo URL: \`${{ steps.norm.outputs.repo_base_url }}\`" - echo "- Uploaded: \`${uploaded}\`" - echo "" - echo "Generated files:" - find dist/apt-repo -type f | sed 's#^dist/apt-repo/#- `#;s#$#`#' - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/deploy-aur.yml b/.github/workflows/deploy-aur.yml new file mode 100644 index 0000000..2fa60c5 --- /dev/null +++ b/.github/workflows/deploy-aur.yml @@ -0,0 +1,268 @@ +name: Deploy AUR Mirror + +on: + workflow_call: + inputs: + tag: + required: true + type: string + version: + required: true + type: string + aur_repo: + required: false + type: string + default: "" + aur_branch: + required: false + type: string + default: "main" + dry_run: + required: false + type: boolean + default: false + secrets: + AUR_REPO_TOKEN: + required: false + workflow_dispatch: + inputs: + version: + description: "Release version (e.g. 0.2.0 or v0.2.0)" + required: true + type: string + tag: + description: "Optional tag override (e.g. v0.2.0). Defaults to v." + required: false + type: string + aur_repo: + description: "Target GitHub repo in OWNER/REPO form (e.g. Auto-Explore/aur-gitcomet). Defaults to AUR_GITHUB_REPO when omitted." + required: false + default: "" + type: string + aur_branch: + description: "Target branch in aur repo. Defaults to AUR_GITHUB_BRANCH when omitted." + required: false + default: "main" + type: string + dry_run: + description: "Validate and print PKGBUILD/.SRCINFO without pushing" + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: deploy-aur-${{ inputs.tag || github.event.inputs.tag || inputs.version || github.event.inputs.version || github.run_id }} + cancel-in-progress: false + +jobs: + deploy: + name: Publish PKGBUILD and .SRCINFO to AUR mirror repo + runs-on: ubuntu-latest + timeout-minutes: 30 + container: + image: archlinux:base-devel + steps: + - name: Install Arch packaging tooling + run: | + set -euo pipefail + pacman -Sy --noconfirm --needed ca-certificates ca-certificates-utils curl git perl shadow + + - uses: actions/checkout@v6 + + - name: Normalize inputs + id: norm + env: + INPUT_TAG: ${{ inputs.tag }} + DISPATCH_TAG: ${{ github.event.inputs.tag }} + INPUT_VERSION: ${{ inputs.version }} + DISPATCH_VERSION: ${{ github.event.inputs.version }} + INPUT_AUR_REPO: ${{ inputs.aur_repo }} + DISPATCH_AUR_REPO: ${{ github.event.inputs.aur_repo }} + VAR_AUR_REPO: ${{ vars.AUR_GITHUB_REPO }} + INPUT_AUR_BRANCH: ${{ inputs.aur_branch }} + DISPATCH_AUR_BRANCH: ${{ github.event.inputs.aur_branch }} + VAR_AUR_BRANCH: ${{ vars.AUR_GITHUB_BRANCH }} + INPUT_DRY_RUN: ${{ inputs.dry_run }} + DISPATCH_DRY_RUN: ${{ github.event.inputs.dry_run }} + REPO_OWNER: ${{ github.repository_owner }} + run: | + set -euo pipefail + + tag="${INPUT_TAG:-${DISPATCH_TAG:-}}" + version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}" + aur_repo="${INPUT_AUR_REPO:-${DISPATCH_AUR_REPO:-${VAR_AUR_REPO:-}}}" + aur_branch="${INPUT_AUR_BRANCH:-${DISPATCH_AUR_BRANCH:-${VAR_AUR_BRANCH:-main}}}" + dry_run="${INPUT_DRY_RUN:-${DISPATCH_DRY_RUN:-false}}" + + tag="$(echo "$tag" | tr -d '[:space:]')" + version="$(echo "$version" | tr -d '[:space:]')" + aur_repo="$(echo "$aur_repo" | tr -d '[:space:]')" + aur_branch="$(echo "$aur_branch" | tr -d '[:space:]')" + dry_run="$(echo "$dry_run" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')" + + if [ -z "$version" ]; then + echo "::error title=Missing version::Version is required." + exit 1 + fi + + version="${version#v}" + if [ -z "$tag" ]; then + tag="v${version}" + fi + + if [[ "$tag" != v* ]]; then + tag="v${tag}" + fi + + if [ "$tag" != "v${version}" ]; then + echo "::error title=Tag/version mismatch::Tag '$tag' does not match version '$version'." + exit 1 + fi + + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then + echo "::error title=Invalid version::Expected semver like 1.2.3 or 1.2.3-rc.1." + exit 1 + fi + + if [ -z "$aur_repo" ]; then + aur_repo="${REPO_OWNER}/aur-gitcomet" + fi + + if ! [[ "$aur_repo" =~ ^[^/]+/[^/]+$ ]]; then + echo "::error title=Invalid AUR repo::aur_repo must be OWNER/REPO." + exit 1 + fi + + if [ -z "$aur_branch" ]; then + echo "::error title=Missing AUR branch::aur_branch must not be empty." + exit 1 + fi + + if [[ "$dry_run" != "true" && "$dry_run" != "false" ]]; then + echo "::error title=Invalid dry_run::dry_run must be true or false." + exit 1 + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "aur_repo=$aur_repo" >> "$GITHUB_OUTPUT" + echo "aur_branch=$aur_branch" >> "$GITHUB_OUTPUT" + echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT" + + - name: Create non-root packaging user + run: | + set -euo pipefail + id -u builder >/dev/null 2>&1 || useradd -m builder + chown -R builder:builder "$GITHUB_WORKSPACE" + + - name: Download release archives referenced by PKGBUILD + env: + TAG: ${{ steps.norm.outputs.tag }} + VERSION: ${{ steps.norm.outputs.version }} + run: | + set -euo pipefail + mkdir -p dist/aur + binary_name="gitcomet-v${VERSION}-linux-x86_64.tar.gz" + source_name="gitcomet-source-v${VERSION}.tar.gz" + + curl -fL --retry 3 --retry-all-errors \ + "https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${binary_name}" \ + -o "dist/aur/${binary_name}" + + curl -fL --retry 3 --retry-all-errors \ + "https://github.com/${GITHUB_REPOSITORY}/archive/refs/tags/${TAG}.tar.gz" \ + -o "dist/aur/${source_name}" + + - name: Clone AUR mirror repository + env: + AUR_REPO: ${{ steps.norm.outputs.aur_repo }} + AUR_BRANCH: ${{ steps.norm.outputs.aur_branch }} + DRY_RUN: ${{ steps.norm.outputs.dry_run }} + AUR_TOKEN: ${{ secrets.AUR_REPO_TOKEN }} + run: | + set -euo pipefail + clone_url="https://github.com/${AUR_REPO}.git" + + if [ "$DRY_RUN" != "true" ]; then + if [ -z "${AUR_TOKEN:-}" ]; then + echo "::error title=Missing secret::Set AUR_REPO_TOKEN to push to ${AUR_REPO}." + exit 1 + fi + clone_url="https://x-access-token:${AUR_TOKEN}@github.com/${AUR_REPO}.git" + fi + + rm -rf aur-repo + git clone --depth 1 --branch "$AUR_BRANCH" --single-branch "$clone_url" aur-repo + chown -R builder:builder aur-repo dist + + - name: Update PKGBUILD and regenerate .SRCINFO + env: + VERSION: ${{ steps.norm.outputs.version }} + run: | + set -euo pipefail + su builder -c "cd '$GITHUB_WORKSPACE' && scripts/update-aur.sh \ + --aur-dir '$GITHUB_WORKSPACE/aur-repo' \ + --version '$VERSION' \ + --binary-tar '$GITHUB_WORKSPACE/dist/aur/gitcomet-v${VERSION}-linux-x86_64.tar.gz' \ + --source-tar '$GITHUB_WORKSPACE/dist/aur/gitcomet-source-v${VERSION}.tar.gz' \ + --verify-source" + + - name: Emit dry-run summary + if: ${{ steps.norm.outputs.dry_run == 'true' }} + run: | + set -euo pipefail + { + echo "### AUR deployment dry run" + echo "" + echo "- Source release: \`${{ steps.norm.outputs.tag }}\`" + echo "- Target repo: \`${{ steps.norm.outputs.aur_repo }}\`" + echo "- Target branch: \`${{ steps.norm.outputs.aur_branch }}\`" + echo "" + echo "PKGBUILD preview:" + echo '```bash' + cat aur-repo/PKGBUILD + echo '```' + echo "" + echo ".SRCINFO preview:" + echo '```ini' + cat aur-repo/.SRCINFO + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish metadata to AUR mirror repo + if: ${{ steps.norm.outputs.dry_run != 'true' }} + env: + AUR_BRANCH: ${{ steps.norm.outputs.aur_branch }} + TAG: ${{ steps.norm.outputs.tag }} + run: | + set -euo pipefail + git config --global --add safe.directory "$GITHUB_WORKSPACE/aur-repo" + + pushd aur-repo >/dev/null + git add PKGBUILD .SRCINFO + if git diff --cached --quiet -- PKGBUILD .SRCINFO; then + echo "No AUR metadata changes detected; mirror repo is already up to date." + popd >/dev/null + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "gitcomet ${TAG}" + git push origin "HEAD:${AUR_BRANCH}" + popd >/dev/null + + - name: Emit deployment summary + run: | + set -euo pipefail + { + echo "### AUR mirror deployment" + echo "" + echo "- Release: \`${{ steps.norm.outputs.tag }}\`" + echo "- Target repo: \`${{ steps.norm.outputs.aur_repo }}\`" + echo "- Target branch: \`${{ steps.norm.outputs.aur_branch }}\`" + echo "- Dry run: \`${{ steps.norm.outputs.dry_run }}\`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/deploy-homebrew-tap.yml b/.github/workflows/deploy-homebrew-tap.yml index a727a7b..bc37668 100644 --- a/.github/workflows/deploy-homebrew-tap.yml +++ b/.github/workflows/deploy-homebrew-tap.yml @@ -35,17 +35,17 @@ on: required: false type: string tap_repo: - description: "Target tap repository in OWNER/REPO form (e.g. Auto-Explore/homebrew-gitcomet)" + description: "Target tap repository in OWNER/REPO form (e.g. Auto-Explore/homebrew-gitcomet). Defaults to HOMEBREW_TAP_REPO when omitted." required: false default: "" type: string tap_branch: - description: "Target branch in tap repo" + description: "Target branch in tap repo. Defaults to HOMEBREW_TAP_BRANCH when omitted." required: false default: "main" type: string dry_run: - description: "Validate and print formula without pushing to tap" + description: "Validate and print cask/formula without pushing to tap" required: true default: false type: boolean @@ -59,7 +59,7 @@ concurrency: jobs: deploy: - name: Publish formula to Homebrew tap + name: Publish Homebrew cask and formula runs-on: ubuntu-latest timeout-minutes: 20 env: @@ -74,8 +74,10 @@ jobs: DISPATCH_VERSION: ${{ github.event.inputs.version }} INPUT_TAP_REPO: ${{ inputs.tap_repo }} DISPATCH_TAP_REPO: ${{ github.event.inputs.tap_repo }} + VAR_TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }} INPUT_TAP_BRANCH: ${{ inputs.tap_branch }} DISPATCH_TAP_BRANCH: ${{ github.event.inputs.tap_branch }} + VAR_TAP_BRANCH: ${{ vars.HOMEBREW_TAP_BRANCH }} INPUT_DRY_RUN: ${{ inputs.dry_run }} DISPATCH_DRY_RUN: ${{ github.event.inputs.dry_run }} REPO_OWNER: ${{ github.repository_owner }} @@ -84,8 +86,8 @@ jobs: tag="${INPUT_TAG:-${DISPATCH_TAG:-}}" version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}" - tap_repo="${INPUT_TAP_REPO:-${DISPATCH_TAP_REPO:-}}" - tap_branch="${INPUT_TAP_BRANCH:-${DISPATCH_TAP_BRANCH:-main}}" + tap_repo="${INPUT_TAP_REPO:-${DISPATCH_TAP_REPO:-${VAR_TAP_REPO:-}}}" + tap_branch="${INPUT_TAP_BRANCH:-${DISPATCH_TAP_BRANCH:-${VAR_TAP_BRANCH:-main}}}" dry_run="${INPUT_DRY_RUN:-${DISPATCH_DRY_RUN:-false}}" tag="$(echo "$tag" | tr -d '[:space:]')" @@ -138,7 +140,7 @@ jobs: echo "tap_branch=$tap_branch" >> "$GITHUB_OUTPUT" echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT" - - name: Download formula from release assets + - name: Download cask and formula from release assets run: | set -euo pipefail mkdir -p dist @@ -146,12 +148,17 @@ jobs: gh release download "${{ steps.norm.outputs.tag }}" \ --repo "$GITHUB_REPOSITORY" \ --pattern "gitcomet.rb" \ + --pattern "gitcomet-cli.rb" \ --dir dist \ --clobber test -f dist/gitcomet.rb + test -f dist/gitcomet-cli.rb - - name: Validate formula syntax - run: ruby -c dist/gitcomet.rb + - name: Validate cask and formula syntax + run: | + set -euo pipefail + ruby -c dist/gitcomet.rb + ruby -c dist/gitcomet-cli.rb - name: Emit dry-run summary if: ${{ steps.norm.outputs.dry_run == 'true' }} @@ -164,13 +171,18 @@ jobs: echo "- Target tap: \`${{ steps.norm.outputs.tap_repo }}\`" echo "- Target branch: \`${{ steps.norm.outputs.tap_branch }}\`" echo "" - echo "Formula preview:" + echo "Cask preview (Casks/gitcomet.rb):" echo '```ruby' cat dist/gitcomet.rb echo '```' + echo "" + echo "Formula preview (Formula/gitcomet-cli.rb):" + echo '```ruby' + cat dist/gitcomet-cli.rb + echo '```' } >> "$GITHUB_STEP_SUMMARY" - - name: Publish formula to tap repository + - name: Publish cask and formula to tap repository if: ${{ steps.norm.outputs.dry_run != 'true' }} env: TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} @@ -187,19 +199,24 @@ jobs: rm -rf tap-repo git clone "https://x-access-token:${TAP_TOKEN}@github.com/${TAP_REPO}.git" tap-repo - mkdir -p tap-repo/Formula - cp dist/gitcomet.rb tap-repo/Formula/gitcomet.rb + mkdir -p tap-repo/Casks tap-repo/Formula + cp dist/gitcomet.rb tap-repo/Casks/gitcomet.rb + cp dist/gitcomet-cli.rb tap-repo/Formula/gitcomet-cli.rb pushd tap-repo >/dev/null - if git diff --quiet -- Formula/gitcomet.rb; then - echo "No formula changes detected; tap already up to date." + git add Casks/gitcomet.rb Formula/gitcomet-cli.rb + if git ls-files --error-unmatch Formula/gitcomet.rb >/dev/null 2>&1; then + git rm -f Formula/gitcomet.rb + fi + + if git diff --cached --quiet -- Casks/gitcomet.rb Formula/gitcomet-cli.rb Formula/gitcomet.rb; then + echo "No Homebrew cask/formula changes detected; tap already up to date." popd >/dev/null exit 0 fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Formula/gitcomet.rb git commit -m "gitcomet ${TAG}" git push origin "HEAD:${TAP_BRANCH}" popd >/dev/null diff --git a/.github/workflows/deployment-ci.yml b/.github/workflows/deployment-ci.yml index bac7bf3..f1920da 100644 --- a/.github/workflows/deployment-ci.yml +++ b/.github/workflows/deployment-ci.yml @@ -5,21 +5,27 @@ on: branches: ["main"] paths: - ".github/workflows/build-release-artifacts.yml" + - ".github/workflows/deploy-aur.yml" - ".github/workflows/deploy-homebrew-tap.yml" - ".github/workflows/deploy-apt-repo.yml" - ".github/workflows/release-manual-main.yml" + - "scripts/update-aur.sh" - "scripts/package-macos.sh" - "scripts/generate-homebrew-formula.sh" + - "scripts/generate-homebrew-cask.sh" - "scripts/build-apt-repo.sh" push: branches: ["main"] paths: - ".github/workflows/build-release-artifacts.yml" + - ".github/workflows/deploy-aur.yml" - ".github/workflows/deploy-homebrew-tap.yml" - ".github/workflows/deploy-apt-repo.yml" - ".github/workflows/release-manual-main.yml" + - "scripts/update-aur.sh" - "scripts/package-macos.sh" - "scripts/generate-homebrew-formula.sh" + - "scripts/generate-homebrew-cask.sh" - "scripts/build-apt-repo.sh" workflow_dispatch: @@ -39,27 +45,58 @@ jobs: - name: Validate shell scripts syntax run: | + bash -n scripts/update-aur.sh bash -n scripts/package-macos.sh bash -n scripts/generate-homebrew-formula.sh + bash -n scripts/generate-homebrew-cask.sh bash -n scripts/build-apt-repo.sh - name: Validate workflow YAML run: | ruby -e 'require "yaml"; %w[ .github/workflows/build-release-artifacts.yml + .github/workflows/deploy-aur.yml .github/workflows/deploy-homebrew-tap.yml .github/workflows/deploy-apt-repo.yml .github/workflows/deployment-ci.yml .github/workflows/release-manual-main.yml ].each { |path| YAML.load_file(path) }' - - name: Generate formula from synthetic artifacts + - name: Validate deployment workflow config keys + run: | + set -euo pipefail + grep -Fq 'vars.AUR_GITHUB_REPO' .github/workflows/release-manual-main.yml + grep -Fq 'vars.AUR_GITHUB_BRANCH' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_STORAGE_ACCOUNT' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_STORAGE_CONTAINER' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_REPO_DISTRIBUTION' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_REPO_COMPONENT' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_REPO_ORIGIN' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_REPO_LABEL' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_REPO_DESCRIPTION' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_STORAGE_PUBLIC_ACCESS' .github/workflows/release-manual-main.yml + grep -Fq 'vars.APT_STORAGE_ACCOUNT' .github/workflows/deploy-apt-repo.yml + grep -Fq 'vars.APT_STORAGE_CONTAINER' .github/workflows/deploy-apt-repo.yml + grep -Fq 'vars.APT_REPO_DISTRIBUTION' .github/workflows/deploy-apt-repo.yml + grep -Fq 'vars.APT_REPO_COMPONENT' .github/workflows/deploy-apt-repo.yml + grep -Fq 'vars.APT_REPO_ORIGIN' .github/workflows/deploy-apt-repo.yml + grep -Fq 'vars.APT_REPO_LABEL' .github/workflows/deploy-apt-repo.yml + grep -Fq 'vars.APT_REPO_DESCRIPTION' .github/workflows/deploy-apt-repo.yml + grep -Fq 'vars.APT_STORAGE_PUBLIC_ACCESS' .github/workflows/deploy-apt-repo.yml + grep -Fq 'vars.AUR_GITHUB_REPO' .github/workflows/deploy-aur.yml + grep -Fq 'vars.AUR_GITHUB_BRANCH' .github/workflows/deploy-aur.yml + grep -Fq 'AUR_REPO_TOKEN' .github/workflows/deploy-aur.yml + grep -Fq 'HOMEBREW_TAP_TOKEN' .github/workflows/deploy-homebrew-tap.yml + + - name: Generate Homebrew cask and formula from synthetic artifacts run: | set -euo pipefail mkdir -p dist/deployment-ci printf 'arm64-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-macos-arm64.tar.gz printf 'intel-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-macos-x86_64.tar.gz printf 'linux-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-linux-x86_64.tar.gz + printf 'arm64-dmg-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-macos-arm64.dmg + printf 'intel-dmg-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-macos-x86_64.dmg scripts/generate-homebrew-formula.sh \ --version "0.0.0-ci" \ @@ -67,11 +104,137 @@ jobs: --arm-tar "dist/deployment-ci/gitcomet-v0.0.0-ci-macos-arm64.tar.gz" \ --intel-tar "dist/deployment-ci/gitcomet-v0.0.0-ci-macos-x86_64.tar.gz" \ --linux-tar "dist/deployment-ci/gitcomet-v0.0.0-ci-linux-x86_64.tar.gz" \ + --output "dist/deployment-ci/gitcomet-cli.rb" + + scripts/generate-homebrew-cask.sh \ + --version "0.0.0-ci" \ + --github-repo "Auto-Explore/GitComet" \ + --arm-dmg "dist/deployment-ci/gitcomet-v0.0.0-ci-macos-arm64.dmg" \ + --intel-dmg "dist/deployment-ci/gitcomet-v0.0.0-ci-macos-x86_64.dmg" \ --output "dist/deployment-ci/gitcomet.rb" ruby -c dist/deployment-ci/gitcomet.rb - grep -q "on_linux do" dist/deployment-ci/gitcomet.rb - grep -q "gitcomet-v0.0.0-ci-linux-x86_64.tar.gz" dist/deployment-ci/gitcomet.rb + ruby -c dist/deployment-ci/gitcomet-cli.rb + grep -q 'cask "gitcomet" do' dist/deployment-ci/gitcomet.rb + grep -q 'app "GitComet.app"' dist/deployment-ci/gitcomet.rb + grep -q 'brew install gitcomet-cli' dist/deployment-ci/gitcomet.rb + grep -q 'class GitcometCli < Formula' dist/deployment-ci/gitcomet-cli.rb + grep -q "on_linux do" dist/deployment-ci/gitcomet-cli.rb + grep -q 'bin.install "gitcomet"' dist/deployment-ci/gitcomet-cli.rb + + - name: Validate Homebrew tap publish detection + run: | + set -euo pipefail + rm -rf dist/deployment-ci/tap-repo + git init --initial-branch=main dist/deployment-ci/tap-repo + + pushd dist/deployment-ci/tap-repo >/dev/null + git config user.name "GitComet CI" + git config user.email "ci@autoexplore.ai" + echo "# Homebrew Tap" > README.md + mkdir -p Formula + cat > Formula/gitcomet.rb <<'RUBY' + class Gitcomet < Formula + desc "Legacy GitComet formula" + homepage "https://example.com" + url "https://example.com/gitcomet.tar.gz" + sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + version "0.0.0" + end + RUBY + + git add README.md Formula/gitcomet.rb + git commit -m "Initial tap repo" + + mkdir -p Casks Formula + cp ../gitcomet.rb Casks/gitcomet.rb + cp ../gitcomet-cli.rb Formula/gitcomet-cli.rb + + git add Casks/gitcomet.rb Formula/gitcomet-cli.rb + if git ls-files --error-unmatch Formula/gitcomet.rb >/dev/null 2>&1; then + git rm -f Formula/gitcomet.rb + fi + + if git diff --cached --quiet -- Casks/gitcomet.rb Formula/gitcomet-cli.rb Formula/gitcomet.rb; then + echo "Expected Homebrew cask/formula publish to be detected as a change." + exit 1 + fi + popd >/dev/null + + aur-metadata-smoke: + name: Validate AUR metadata update script + runs-on: ubuntu-latest + timeout-minutes: 20 + container: + image: archlinux:base-devel + steps: + - name: Install Arch packaging tooling + run: | + set -euo pipefail + pacman -Sy --noconfirm --needed perl shadow + + - uses: actions/checkout@v6 + + - name: Create non-root packaging user + run: | + set -euo pipefail + id -u builder >/dev/null 2>&1 || useradd -m builder + chown -R builder:builder "$GITHUB_WORKSPACE" + + - name: Prepare synthetic AUR repo and assets + run: | + set -euo pipefail + mkdir -p dist/aur-ci/repo + cat > dist/aur-ci/repo/PKGBUILD <<'EOF' + pkgname=gitcomet + pkgver=0.0.0 + pkgrel=1 + pkgdesc="Fast, resource-efficient Git GUI written in Rust" + arch=('x86_64') + url="https://gitcomet.dev/" + license=('AGPL-3.0-only') + depends=( + 'fontconfig' + 'freetype2' + 'git' + 'libx11' + 'libxcb' + 'libxkbcommon' + 'wayland' + ) + source=( + "gitcomet-v$pkgver-linux-x86_64.tar.gz::https://example.invalid/gitcomet-v$pkgver-linux-x86_64.tar.gz" + "gitcomet-source-v$pkgver.tar.gz::https://example.invalid/gitcomet-source-v$pkgver.tar.gz" + ) + sha256sums=('oldbinary' + 'oldsource') + + package() { + install -D -m755 "gitcomet-v$pkgver-linux-x86_64/gitcomet" "$pkgdir/usr/bin/gitcomet" + } + EOF + + printf 'synthetic-binary' > dist/aur-ci/gitcomet-v0.0.0-linux-x86_64.tar.gz + printf 'synthetic-source' > dist/aur-ci/gitcomet-source-v0.0.0.tar.gz + chown -R builder:builder dist + + - name: Run update-aur.sh + run: | + set -euo pipefail + su builder -c "cd '$GITHUB_WORKSPACE' && scripts/update-aur.sh \ + --aur-dir '$GITHUB_WORKSPACE/dist/aur-ci/repo' \ + --version 0.0.0 \ + --binary-tar '$GITHUB_WORKSPACE/dist/aur-ci/gitcomet-v0.0.0-linux-x86_64.tar.gz' \ + --source-tar '$GITHUB_WORKSPACE/dist/aur-ci/gitcomet-source-v0.0.0.tar.gz' \ + --verify-source" + + - name: Validate generated PKGBUILD and .SRCINFO + run: | + set -euo pipefail + test -f dist/aur-ci/repo/.SRCINFO + grep -q '^pkgver=0.0.0$' dist/aur-ci/repo/PKGBUILD + grep -q 'source = gitcomet-v0.0.0-linux-x86_64.tar.gz::https://example.invalid/gitcomet-v0.0.0-linux-x86_64.tar.gz$' dist/aur-ci/repo/.SRCINFO + grep -q 'source = gitcomet-source-v0.0.0.tar.gz::https://example.invalid/gitcomet-source-v0.0.0.tar.gz$' dist/aur-ci/repo/.SRCINFO apt-repo-generation-smoke: name: Validate APT repo generation script @@ -91,14 +254,14 @@ jobs: pkg_root="dist/apt-ci/pkg" mkdir -p "${pkg_root}/DEBIAN" "${pkg_root}/usr/bin" - printf '%s\n' '#!/usr/bin/env bash' 'echo "GitComet CI"' > "${pkg_root}/usr/bin/gitcomet-app" - chmod +x "${pkg_root}/usr/bin/gitcomet-app" + printf '%s\n' '#!/usr/bin/env bash' 'echo "GitComet CI"' > "${pkg_root}/usr/bin/gitcomet" + chmod +x "${pkg_root}/usr/bin/gitcomet" { echo "Package: gitcomet" echo "Version: 0.0.0~ci1" echo "Architecture: amd64" - echo "Maintainer: GitComet CI " + echo "Maintainer: GitComet CI " echo "Description: Synthetic GitComet package for deployment CI." } > "${pkg_root}/DEBIAN/control" @@ -116,7 +279,7 @@ jobs: echo "Key-Type: RSA" echo "Key-Length: 3072" echo "Name-Real: GitComet CI" - echo "Name-Email: ci@example.invalid" + echo "Name-Email: ci@autoexplore.ai" echo "Expire-Date: 0" echo "%no-protection" echo "%commit" @@ -129,6 +292,7 @@ jobs: - name: Build signed APT repo env: GNUPGHOME: ${{ github.workspace }}/dist/apt-ci/gnupg + APT_GPG_PASSPHRASE: ${{ secrets.APT_GPG_PASSPHRASE }} run: | set -euo pipefail scripts/build-apt-repo.sh \ @@ -141,6 +305,7 @@ jobs: --label GitComet \ --description "GitComet deployment CI repository" \ --repo-url https://example.invalid/gitcomet/apt \ + --gpg-passphrase "${APT_GPG_PASSPHRASE}" \ --signing-key "${APT_TEST_GPG_KEY_ID}" - name: Validate generated repo @@ -163,6 +328,38 @@ jobs: chmod 700 "$GNUPGHOME" gpg --batch --no-default-keyring --keyring dist/apt-ci/repo/gitcomet-archive-keyring.gpg --verify dist/apt-ci/repo/dists/stable/InRelease >/dev/null + - name: Verify generated repo with apt-secure + run: | + set -euo pipefail + + repo_root="$(pwd)/dist/apt-ci/repo" + source_dir="${RUNNER_TEMP}/gitcomet-apt-sources.d" + lists_dir="${RUNNER_TEMP}/gitcomet-apt-lists" + + mkdir -p "$source_dir" + sudo mkdir -p "${lists_dir}/partial" + + cat > "${source_dir}/gitcomet-ci.sources" </dev/null + "${mount_point}/GitComet.app/Contents/MacOS/gitcomet" --help >/dev/null hdiutil detach "$mount_point" diff --git a/.github/workflows/release-manual-main.yml b/.github/workflows/release-manual-main.yml index 62eed85..29e7f22 100644 --- a/.github/workflows/release-manual-main.yml +++ b/.github/workflows/release-manual-main.yml @@ -82,10 +82,10 @@ jobs: - name: Verify crate version matches release version run: | set -euo pipefail - crate_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' crates/gitcomet-app/Cargo.toml | head -n1)" + crate_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)" wanted="${{ steps.norm.outputs.version }}" if [ "$crate_version" != "$wanted" ]; then - echo "::error title=Version mismatch::crates/gitcomet-app/Cargo.toml version '$crate_version' does not match requested release '$wanted'." + echo "::error title=Version mismatch::Cargo.toml version '$crate_version' does not match requested release '$wanted'." exit 1 fi @@ -175,7 +175,7 @@ jobs: gh release view "$tag" --repo "$GITHUB_REPOSITORY" deploy_homebrew_tap: - name: Deploy Homebrew tap formula + name: Deploy Homebrew tap packages needs: [validate, build_and_upload, publish_release] if: ${{ fromJSON(needs.validate.outputs.draft) == false && needs.build_and_upload.result == 'success' }} uses: ./.github/workflows/deploy-homebrew-tap.yml @@ -187,6 +187,19 @@ jobs: dry_run: false secrets: inherit + deploy_aur: + name: Deploy AUR mirror metadata + needs: [validate, build_and_upload, publish_release] + if: ${{ fromJSON(needs.validate.outputs.draft) == false && needs.build_and_upload.result == 'success' }} + uses: ./.github/workflows/deploy-aur.yml + with: + tag: ${{ needs.validate.outputs.tag }} + version: ${{ needs.validate.outputs.version }} + aur_repo: ${{ vars.AUR_GITHUB_REPO }} + aur_branch: ${{ vars.AUR_GITHUB_BRANCH }} + dry_run: false + secrets: inherit + deploy_apt_repo: name: Deploy Azure APT repository needs: [validate, build_and_upload, publish_release] @@ -198,7 +211,6 @@ jobs: storage_account: ${{ vars.APT_STORAGE_ACCOUNT }} storage_container: ${{ vars.APT_STORAGE_CONTAINER }} storage_prefix: ${{ vars.APT_STORAGE_PREFIX }} - repo_base_url: ${{ vars.APT_REPO_BASE_URL }} distribution: ${{ vars.APT_REPO_DISTRIBUTION }} component: ${{ vars.APT_REPO_COMPONENT }} architecture: "amd64" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b02a407..9b719c1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ concurrency: env: CARGO_TERM_COLOR: always - # Build gitcomet-app in headless mode (no GPUI system deps required). + # Build gitcomet in headless mode (no GPUI system deps required). # The UI-only code paths are behind #[cfg(feature = "ui-gpui")] guards. APP_FEATURES: "--no-default-features --features gix" @@ -77,7 +77,7 @@ jobs: echo "Skipping cargo clippy -p gitcomet-ui-gpui in this job." echo "Reason: GPUI requires native windowing/system dependencies that are not available in headless CI." - name: Clippy (app — headless) - run: cargo clippy -p gitcomet-app $APP_FEATURES -- -D warnings + run: cargo clippy -p gitcomet $APP_FEATURES -- -D warnings build: name: Build @@ -92,7 +92,7 @@ jobs: - name: Build (core + state + backend) run: cargo build -p gitcomet-core -p gitcomet-state -p gitcomet-git-gix --verbose - name: Build (app — headless) - run: cargo build -p gitcomet-app $APP_FEATURES --verbose + run: cargo build -p gitcomet $APP_FEATURES --verbose # Core merge algorithm correctness — Phase 1A/1B/1C portability tests merge-algorithm: @@ -148,16 +148,16 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Cache Rust artifacts uses: Swatinem/rust-cache@v2 - - name: Build gitcomet-app binary (headless) - run: cargo build -p gitcomet-app $APP_FEATURES + - name: Build gitcomet binary (headless) + run: cargo build -p gitcomet $APP_FEATURES - name: Git mergetool E2E (Phase 4A — t7610 parity) - run: cargo test -p gitcomet-app $APP_FEATURES --test mergetool_git_integration --verbose + run: cargo test -p gitcomet $APP_FEATURES --test mergetool_git_integration --verbose - name: Git difftool E2E (Phase 4B — t7800 parity) - run: cargo test -p gitcomet-app $APP_FEATURES --test difftool_git_integration --verbose + run: cargo test -p gitcomet $APP_FEATURES --test difftool_git_integration --verbose - name: Standalone tool-mode E2E (exit codes + validation) - run: cargo test -p gitcomet-app $APP_FEATURES --test standalone_tool_mode_integration --verbose + run: cargo test -p gitcomet $APP_FEATURES --test standalone_tool_mode_integration --verbose - name: Mergetool/difftool runtime unit tests (bin target) - run: cargo test -p gitcomet-app $APP_FEATURES --bin gitcomet-app --verbose + run: cargo test -p gitcomet $APP_FEATURES --bin gitcomet --verbose # Backend integration — mergetool launcher, status, conflict checkout backend-integration: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2739728..32937b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ - `crates/gitcomet-state`: MVU state store, reducers, effects, conflict session management. - `crates/gitcomet-ui`: UI model/state (toolkit-independent). - `crates/gitcomet-ui-gpui`: gpui views/components (focused diff/merge windows, conflict resolver, word diff). -- `crates/gitcomet-app`: binary entrypoint, CLI (clap), difftool/mergetool/setup/uninstall modes. +- `crates/gitcomet`: binary entrypoint, CLI (clap), difftool/mergetool/setup/uninstall modes. ### Getting started @@ -28,19 +28,19 @@ cargo build To build the actual app you'll enable features (requires network for dependencies): ```bash -cargo build -p gitcomet-app --features ui,gix +cargo build -p gitcomet --features ui,gix ``` To also compile the gpui-based UI crate: ```bash -cargo build -p gitcomet-app --features ui-gpui,gix +cargo build -p gitcomet --features ui-gpui,gix ``` Run (opens the repo passed as the first arg, or falls back to the current directory): ```bash -cargo run -p gitcomet-app --features ui-gpui,gix -- /path/to/repo +cargo run -p gitcomet --features ui-gpui,gix -- /path/to/repo ``` ### Testing @@ -86,11 +86,12 @@ The release workflow `.github/workflows/build-release-artifacts.yml` builds and - Windows: portable ZIP + MSI - Linux: tar.gz + AppImage + .deb - macOS: DMG + tar.gz for `arm64` and `x86_64` -- Homebrew formula asset: `gitcomet.rb` (generated from macOS + Linux x86_64 tarballs and their SHA256 values) +- Homebrew cask asset: `gitcomet.rb` (generated from macOS DMG artifacts and their SHA256 values) +- Homebrew CLI formula asset: `gitcomet-cli.rb` (generated from macOS + Linux x86_64 tarballs and their SHA256 values) ### Homebrew deployment -To push `Formula/gitcomet.rb` into a Homebrew tap repo automatically on release: +To push `Casks/gitcomet.rb` and `Formula/gitcomet-cli.rb` into a Homebrew tap repo automatically on release: 1. Create a tap repository (default expected name: `OWNER/homebrew-gitcomet`). 2. In this repo, configure: @@ -103,6 +104,27 @@ This release flow will: - build and upload release artifacts - publish the GitHub release -- call `.github/workflows/deploy-homebrew-tap.yml` to update `Formula/gitcomet.rb` in the tap repo +- call `.github/workflows/deploy-homebrew-tap.yml` to update `Casks/gitcomet.rb` and `Formula/gitcomet-cli.rb` in the tap repo You can also run `.github/workflows/deploy-homebrew-tap.yml` manually for backfills or dry-runs. + +### AUR mirror deployment + +To push `PKGBUILD` and `.SRCINFO` into a GitHub-hosted AUR mirror repo automatically on release: + +1. Create the target repository (default expected name: `OWNER/aur-gitcomet`). +2. In this repo, configure: + - secret `AUR_REPO_TOKEN`: GitHub token with `contents:write` access to the AUR mirror repository. + - optional variable `AUR_GITHUB_REPO`: target repository in `OWNER/REPO` form. + - optional variable `AUR_GITHUB_BRANCH`: target branch (default `main`). +3. Run `.github/workflows/release-manual-main.yml` with `draft=false`. + +This release flow will: + +- download the published Linux release tarball and source tarball +- update `PKGBUILD` `pkgver` and `sha256sums` +- regenerate `.SRCINFO` +- validate sources with `makepkg --verifysource` +- push the updated metadata into the configured AUR mirror repository + +You can also run `.github/workflows/deploy-aur.yml` manually for backfills or dry-runs. diff --git a/Cargo.lock b/Cargo.lock index f0d1ab5..faaf34e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2239,8 +2239,8 @@ dependencies = [ ] [[package]] -name = "gitcomet-app" -version = "0.1.0" +name = "gitcomet" +version = "0.1.7" dependencies = [ "clap", "embed-resource", @@ -2256,7 +2256,7 @@ dependencies = [ [[package]] name = "gitcomet-core" -version = "0.1.0" +version = "0.1.7" dependencies = [ "regex", "rustc-hash 2.1.1", @@ -2267,14 +2267,14 @@ dependencies = [ [[package]] name = "gitcomet-git" -version = "0.1.0" +version = "0.1.7" dependencies = [ "gitcomet-core", ] [[package]] name = "gitcomet-git-gix" -version = "0.1.0" +version = "0.1.7" dependencies = [ "gitcomet-core", "gix", @@ -2285,7 +2285,7 @@ dependencies = [ [[package]] name = "gitcomet-state" -version = "0.1.0" +version = "0.1.7" dependencies = [ "gitcomet-core", "gix", @@ -2299,14 +2299,14 @@ dependencies = [ [[package]] name = "gitcomet-ui" -version = "0.1.0" +version = "0.1.7" dependencies = [ "gitcomet-core", ] [[package]] name = "gitcomet-ui-gpui" -version = "0.1.0" +version = "0.1.7" dependencies = [ "criterion", "gitcomet-core", @@ -2399,7 +2399,7 @@ dependencies = [ "bstr", "gix-date", "gix-error", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2501,7 +2501,7 @@ dependencies = [ "smallvec", "thiserror 2.0.18", "unicode-bom", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2760,7 +2760,7 @@ dependencies = [ "itoa", "smallvec", "thiserror 2.0.18", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2859,7 +2859,7 @@ dependencies = [ "maybe-async", "nonempty", "thiserror 2.0.18", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2891,7 +2891,7 @@ dependencies = [ "gix-validate", "memmap2", "thiserror 2.0.18", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -5314,7 +5314,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.4+spec-1.1.0", + "toml_edit 0.25.5+spec-1.1.0", ] [[package]] @@ -7044,7 +7044,7 @@ dependencies = [ "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -7067,9 +7067,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" dependencies = [ "serde_core", ] @@ -7085,28 +7085,28 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.25.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" dependencies = [ "indexmap", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -7117,9 +7117,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tower" @@ -8426,6 +8426,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.55.0" @@ -8735,7 +8744,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -8763,7 +8772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "winnow", + "winnow 0.7.15", "zvariant", ] @@ -9039,7 +9048,7 @@ dependencies = [ "enumflags2", "serde", "url", - "winnow", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] @@ -9067,5 +9076,5 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "winnow", + "winnow 0.7.15", ] diff --git a/Cargo.toml b/Cargo.toml index 482d9fb..5d65900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "3" members = [ - "crates/gitcomet-app", + "crates/gitcomet", "crates/gitcomet-core", "crates/gitcomet-git", "crates/gitcomet-git-gix", @@ -11,7 +11,7 @@ members = [ ] default-members = [ - "crates/gitcomet-app", + "crates/gitcomet", "crates/gitcomet-core", "crates/gitcomet-git", "crates/gitcomet-git-gix", @@ -21,6 +21,7 @@ default-members = [ ] [workspace.package] +version = "0.1.7" edition = "2024" license = "AGPL-3.0-only" authors = ["AutoExplore Oy "] @@ -82,7 +83,7 @@ opt-level = 1 split-debuginfo = "packed" [profile.release] -lto = "thin" +lto = "fat" codegen-units = 1 strip = "symbols" diff --git a/README.md b/README.md index aabc2b7..35ec3bd 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,11 @@ [![Discord](https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/2ufDGP8RnA) [![Website](https://img.shields.io/badge/Website-gitcomet.dev-0A66C2?logo=googlechrome&logoColor=white)](https://gitcomet.dev) [![AutoExplore](https://img.shields.io/badge/AutoExplore-autoexplore.ai-0B7A75?logo=safari&logoColor=white)](https://autoexplore.ai) +[![license](https://img.shields.io/github/license/Auto-Explore/gitcomet.svg)](LICENSE) +[![latest](https://img.shields.io/github/v/release/Auto-Explore/gitcomet.svg)](https://github.com/Auto-Explore/gitcomet/releases/latest) +[![downloads](https://img.shields.io/github/downloads/Auto-Explore/gitcomet/total)](https://github.com/Auto-Explore/gitcomet/releases) -**Speed is a feature.** +**Fastest Open Source Git GUI** GitComet is built for teams that want fast Git operations with local-first privacy, familiar workflows, and open source freedom. @@ -19,11 +22,24 @@ Download the latest prebuilt binaries/installers from [GitHub Releases](https:// #### Homebrew -install from tap: +install app from tap (recommended): ```bash brew tap auto-explore/gitcomet -brew install gitcomet +brew install --cask gitcomet +``` + + +**MacOS Troubleshooting:** If you downloaded the app manually and macOS reports that GitComet *"is damaged and can't be opened"*, this is due to Apple's Gatekeeper quarantine for apps downloaded outside the App Store. To resolve this, run the following command in your terminal to remove the quarantine attribute: + +```bash +xattr -d com.apple.quarantine /Applications/GitComet.app +``` + +optional CLI install: + +```bash +brew install gitcomet-cli ``` ### Fast, Free, Familiar @@ -71,13 +87,13 @@ Measured on Linux 6.19.6-zen (x64), Ryzen 5950x, 128GB DDR4. Detailed test steps - Code test coverage workflows - GitHub and Azure DevOps integrations - Priority improvements during early access -- Join waitlist: [gitcomet.com/#pricing](https://gitcomet.com/#pricing) +- Join waitlist: [gitcomet.dev/#pricing](https://gitcomet.dev/#pricing) ### Build from source ```bash -cargo build -p gitcomet-app --features ui-gpui,gix -cargo run -p gitcomet-app --features ui-gpui,gix -- /path/to/repo +cargo build -p gitcomet --features ui-gpui,gix +cargo run -p gitcomet --features ui-gpui,gix -- /path/to/repo ``` ### Contributing @@ -92,10 +108,10 @@ GitComet can be used as a standalone diff and merge tool invoked by `git difftoo ```bash # Configure Git globally to use GitComet for both difftool + mergetool -gitcomet-app setup +gitcomet setup # Remove GitComet integration safely -gitcomet-app uninstall +gitcomet uninstall ``` - Use `--local` to target only the current repository instead of global config. @@ -110,7 +126,7 @@ This setup registers both headless and GUI variants with `guiDefault=auto`, so G Built-in `setup` writes these Git config entries: ```bash -GITCOMET_BIN="/absolute/path/to/gitcomet-app" +GITCOMET_BIN="/absolute/path/to/gitcomet" # Headless tool: algorithm-only merge/diff for CI, scripts, and no-display environments git config --global merge.tool gitcomet @@ -153,7 +169,7 @@ Built-in `uninstall` restores those backups only when the key still has the setu **Difftool:** ```bash -gitcomet-app difftool --local --remote [--path ] [--label-left