Compare commits

..

4 commits

Author SHA1 Message Date
Sampo Kivistö
00d1420e30
Merge branch 'main' of github.com:Auto-Explore/GitComet into feat/flatpak 2026-03-17 14:47:53 +02:00
Sampo Kivistö
916c19b2cc
Merge branch 'main' of github.com:Auto-Explore/GitComet into feat/flatpak 2026-03-17 14:28:38 +02:00
Sampo Kivistö
a0fe809d3a
Merge branch 'main' of github.com:Auto-Explore/GitComet into feat/flatpak 2026-03-14 20:37:51 +02:00
Sampo Kivistö
dd0d5efda0
flatpak wip 2026-03-13 16:54:30 +02:00
87 changed files with 13274 additions and 2440 deletions

View file

@ -142,14 +142,14 @@ jobs:
- name: Build release binary
shell: pwsh
run: |
cargo build -p gitcomet --release --locked --features ui-gpui,gix --bins
cargo build -p gitcomet-app --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.exe dist\portable\gitcomet.exe -Force
Copy-Item target\release\gitcomet-app.exe dist\portable\gitcomet-app.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\wix\main.wxs")) {
cargo wix init --package gitcomet
if (!(Test-Path "crates\gitcomet-app\wix\main.wxs")) {
cargo wix init --package gitcomet-app
}
$msiName = "gitcomet-v${env:VERSION}-windows-x86_64.msi"
cargo wix --package gitcomet --profile release --nocapture --no-build --output "dist\$msiName"
cargo wix --package gitcomet-app --profile release --nocapture --no-build --output "dist\$msiName"
- name: Upload Windows artifacts
uses: actions/upload-artifact@v7
@ -199,6 +199,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y \
appstream \
desktop-file-utils \
pkg-config \
libxcb1-dev \
@ -212,22 +213,22 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Build release binary
run: cargo build -p gitcomet --release --locked --features ui-gpui,gix
run: cargo build -p gitcomet-app --release --locked --features ui-gpui,gix
- name: Normalize desktop entry metadata
run: |
set -euo pipefail
mkdir -p dist
# Backward compatibility for older tags that used non-standard categories.
sed 's/VersionControl;/RevisionControl;/g' assets/linux/gitcomet.desktop > dist/gitcomet.desktop
desktop-file-validate dist/gitcomet.desktop
cp assets/linux/dev.gitcomet.GitComet.desktop dist/dev.gitcomet.GitComet.desktop
desktop-file-validate dist/dev.gitcomet.GitComet.desktop
appstreamcli validate --no-net flatpak/dev.gitcomet.GitComet.metainfo.xml
- name: Package tar.gz
run: |
set -euo pipefail
root="gitcomet-v${VERSION}-linux-x86_64"
mkdir -p "dist/stage/${root}"
install -m755 target/release/gitcomet "dist/stage/${root}/gitcomet"
install -m755 target/release/gitcomet-app "dist/stage/${root}/gitcomet-app"
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,9 +246,9 @@ jobs:
mkdir -p "${pkg_root}/usr/share/applications"
mkdir -p "${pkg_root}/usr/share/icons/hicolor/512x512/apps"
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"
install -m755 target/release/gitcomet-app "${pkg_root}/usr/bin/gitcomet-app"
install -m644 dist/dev.gitcomet.GitComet.desktop "${pkg_root}/usr/share/applications/dev.gitcomet.GitComet.desktop"
install -m644 assets/gitcomet-512.png "${pkg_root}/usr/share/icons/hicolor/512x512/apps/dev.gitcomet.GitComet.png"
{
echo "Package: gitcomet"
@ -267,15 +268,15 @@ jobs:
set -euo pipefail
appdir="dist/AppDir"
mkdir -p "${appdir}/usr/bin"
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"
install -m755 target/release/gitcomet-app "${appdir}/usr/bin/gitcomet-app"
install -m644 dist/dev.gitcomet.GitComet.desktop "${appdir}/dev.gitcomet.GitComet.desktop"
install -m644 assets/gitcomet-512.png "${appdir}/dev.gitcomet.GitComet.png"
{
echo '#!/usr/bin/env bash'
echo 'set -euo pipefail'
echo 'HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"'
echo 'exec "${HERE}/usr/bin/gitcomet" "$@"'
echo 'exec "${HERE}/usr/bin/gitcomet-app" "$@"'
} > "${appdir}/AppRun"
chmod +x "${appdir}/AppRun"
@ -298,6 +299,45 @@ jobs:
if-no-files-found: error
retention-days: 7
build_flatpak:
name: Build Flatpak artifact
runs-on: ubuntu-latest
timeout-minutes: 120
needs: prepare
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-24.08
options: --privileged
env:
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0
- name: Build Flatpak bundle
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: dev.gitcomet.GitComet.flatpak
manifest-path: flatpak/dev.gitcomet.GitComet.local.yaml
cache-key: flatpak-builder-${{ needs.prepare.outputs.tag }}
upload-artifact: false
- name: Stage Flatpak artifact
run: |
set -euo pipefail
mkdir -p dist
mv dev.gitcomet.GitComet.flatpak "dist/gitcomet-v${VERSION}-linux-x86_64.flatpak"
- name: Upload Flatpak artifact
uses: actions/upload-artifact@v4
with:
name: flatpak-release-artifacts
path: dist/*.flatpak
if-no-files-found: error
retention-days: 7
build_macos:
name: Build macOS artifacts (${{ matrix.arch }})
runs-on: ${{ matrix.runs_on }}
@ -350,7 +390,7 @@ jobs:
name: Attach assets and checksums to GitHub release
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [prepare, build_windows, build_linux, build_macos]
needs: [prepare, build_windows, build_linux, build_flatpak, build_macos]
env:
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
@ -359,7 +399,7 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.tag }}
ref: ${{ env.TAG }}
fetch-depth: 0
- uses: actions/download-artifact@v8
@ -372,6 +412,11 @@ jobs:
name: linux-release-artifacts
path: dist/linux
- uses: actions/download-artifact@v8
with:
name: flatpak-release-artifacts
path: dist/flatpak
- uses: actions/download-artifact@v8
with:
pattern: macos-release-artifacts-*
@ -384,6 +429,7 @@ jobs:
mkdir -p dist/release
find dist/windows -maxdepth 1 -type f -exec cp -f {} dist/release/ \;
find dist/linux -maxdepth 1 -type f -exec cp -f {} dist/release/ \;
find dist/flatpak -maxdepth 1 -type f -exec cp -f {} dist/release/ \;
find dist/macos -maxdepth 1 -type f -exec cp -f {} dist/release/ \;
file_count="$(find dist/release -maxdepth 1 -type f | wc -l | tr -d '[:space:]')"
if [ "${file_count}" = "0" ]; then
@ -392,22 +438,40 @@ jobs:
fi
ls -lah dist/release
- name: Generate Homebrew cask and formula
- name: Generate source tarball
run: |
set -euo pipefail
git archive \
--format=tar.gz \
--prefix="gitcomet-v${VERSION}-source/" \
"$TAG" \
-o "dist/release/gitcomet-v${VERSION}-source.tar.gz"
- name: Render Flathub manifest assets
run: |
set -euo pipefail
source_tar="dist/release/gitcomet-v${VERSION}-source.tar.gz"
source_sha256="$(sha256sum "$source_tar" | awk '{print $1}')"
source_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/gitcomet-v${VERSION}-source.tar.gz"
scripts/render-flathub-manifest.sh \
--source-url "$source_url" \
--source-sha256 "$source_sha256" \
--output "dist/release/dev.gitcomet.GitComet.yaml"
cp flatpak/cargo-sources.json dist/release/cargo-sources.json
cp flatpak/flathub.json dist/release/flathub.json
- name: Generate Homebrew 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}" ] || [ ! -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."
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."
exit 1
fi
@ -417,13 +481,6 @@ 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

View file

@ -163,8 +163,8 @@ jobs:
echo "XDG_CURRENT_DESKTOP=${XDG_CURRENT_DESKTOP:-<unset>}"
- name: Run display-selection integration tests
run: |
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
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
- 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_FEATURES --release --locked
codesign --verify --verbose target/release/gitcomet || true
spctl --assess --type execute --verbose target/release/gitcomet || true
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
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 --no-default-features --features gix --test standalone_tool_mode_integration help_flag_exits_zero -- --exact
run: cargo test -p gitcomet-app --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: |

View file

@ -21,6 +21,10 @@ on:
required: false
type: string
default: ""
repo_base_url:
required: false
type: string
default: ""
distribution:
required: false
type: string
@ -44,7 +48,7 @@ on:
repo_description:
required: false
type: string
default: "GitComet APT repository."
default: "GitComet APT repository"
container_public_access:
required: false
type: string
@ -71,27 +75,32 @@ on:
required: false
type: string
storage_account:
description: "Azure Storage account name. Defaults to repository variable APT_STORAGE_ACCOUNT when omitted."
description: "Azure Storage account name"
required: false
default: ""
type: string
storage_container:
description: "Azure Blob container that holds the APT repository. Defaults to APT_STORAGE_CONTAINER when omitted."
description: "Azure Blob container that holds the APT repository"
required: false
default: "apt"
type: string
storage_prefix:
description: "Optional blob prefix within the container. Defaults to APT_STORAGE_PREFIX when omitted."
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"
required: false
default: ""
type: string
distribution:
description: "APT suite/codename. Defaults to APT_REPO_DISTRIBUTION when omitted."
description: "APT suite/codename"
required: false
default: "stable"
type: string
component:
description: "APT component. Defaults to APT_REPO_COMPONENT when omitted."
description: "APT component"
required: false
default: "main"
type: string
@ -101,22 +110,22 @@ on:
default: "amd64"
type: string
repo_origin:
description: "Origin metadata in Release file. Defaults to APT_REPO_ORIGIN when omitted."
description: "Origin metadata in Release file"
required: false
default: "GitComet"
type: string
repo_label:
description: "Label metadata in Release file. Defaults to APT_REPO_LABEL when omitted."
description: "Label metadata in Release file"
required: false
default: "GitComet"
type: string
repo_description:
description: "Description metadata in Release file. Defaults to APT_REPO_DESCRIPTION when omitted."
description: "Description metadata in Release file"
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. Defaults to APT_STORAGE_PUBLIC_ACCESS when omitted."
description: "Public access used only when the blob container must be created"
required: false
default: "blob"
type: choice
@ -156,33 +165,26 @@ 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 }}
VAR_STORAGE_PREFIX: ${{ vars.APT_STORAGE_PREFIX }}
INPUT_REPO_BASE_URL: ${{ inputs.repo_base_url }}
DISPATCH_REPO_BASE_URL: ${{ github.event.inputs.repo_base_url }}
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: |
@ -190,16 +192,17 @@ jobs:
tag="${INPUT_TAG:-${DISPATCH_TAG:-}}"
version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}"
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}}}"
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}}"
architecture="${INPUT_ARCHITECTURE:-${DISPATCH_ARCHITECTURE:-amd64}}"
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}}}"
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}}"
dry_run="${INPUT_DRY_RUN:-${DISPATCH_DRY_RUN:-false}}"
tag="$(echo "$tag" | tr -d '[:space:]')"
@ -207,6 +210,7 @@ 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:]')"
@ -270,8 +274,7 @@ jobs:
exit 1
fi
repo_base_url=""
if [ -n "$storage_account" ] && [ "$storage_container" != '$web' ]; then
if [ -z "$repo_base_url" ] && [ -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}"
@ -362,7 +365,7 @@ jobs:
echo "Key-Type: RSA"
echo "Key-Length: 3072"
echo "Name-Real: GitComet APT Dry Run"
echo "Name-Email: ci@autoexplore.ai"
echo "Name-Email: ci@example.invalid"
echo "Expire-Date: 0"
echo "%no-protection"
echo "%commit"
@ -416,16 +419,16 @@ jobs:
blob_prefix="${blob_prefix}/"
fi
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"
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
)
if [ "${#existing_blobs[@]}" -eq 0 ]; then
exit 0
@ -469,7 +472,6 @@ 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
@ -493,9 +495,6 @@ 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
@ -506,25 +505,15 @@ jobs:
blob_name="${STORAGE_PREFIX}/${rel_path}"
fi
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
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)
@ -560,3 +549,28 @@ 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"

View file

@ -1,268 +0,0 @@
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<version>."
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"

317
.github/workflows/deploy-flathub.yml vendored Normal file
View file

@ -0,0 +1,317 @@
name: Deploy Flathub Manifest
on:
workflow_call:
inputs:
tag:
required: true
type: string
version:
required: true
type: string
flathub_repo:
required: false
type: string
default: ""
flathub_branch:
required: false
type: string
default: ""
mode:
required: false
type: string
default: "pull_request"
dry_run:
required: false
type: boolean
default: false
secrets:
FLATHUB_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<version>."
required: false
type: string
flathub_repo:
description: "Target Flathub packaging repo in OWNER/REPO form"
required: false
default: ""
type: string
flathub_branch:
description: "Target branch in the Flathub packaging repo (defaults to that repo's default branch)"
required: false
default: ""
type: string
mode:
description: "Update mode: pull_request or push"
required: false
default: "pull_request"
type: string
dry_run:
description: "Validate and report manifest deployment without pushing"
required: true
default: false
type: boolean
permissions:
contents: read
concurrency:
group: deploy-flathub-${{ inputs.tag || github.event.inputs.tag || inputs.version || github.event.inputs.version || github.run_id }}
cancel-in-progress: false
jobs:
deploy:
name: Publish manifest to Flathub packaging repo
runs-on: ubuntu-latest
timeout-minutes: 20
env:
GH_TOKEN: ${{ github.token }}
steps:
- 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_FLATHUB_REPO: ${{ inputs.flathub_repo }}
DISPATCH_FLATHUB_REPO: ${{ github.event.inputs.flathub_repo }}
INPUT_FLATHUB_BRANCH: ${{ inputs.flathub_branch }}
DISPATCH_FLATHUB_BRANCH: ${{ github.event.inputs.flathub_branch }}
INPUT_MODE: ${{ inputs.mode }}
DISPATCH_MODE: ${{ github.event.inputs.mode }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
DISPATCH_DRY_RUN: ${{ github.event.inputs.dry_run }}
run: |
set -euo pipefail
tag="${INPUT_TAG:-${DISPATCH_TAG:-}}"
version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}"
flathub_repo="${INPUT_FLATHUB_REPO:-${DISPATCH_FLATHUB_REPO:-}}"
flathub_branch="${INPUT_FLATHUB_BRANCH:-${DISPATCH_FLATHUB_BRANCH:-}}"
mode="${INPUT_MODE:-${DISPATCH_MODE:-pull_request}}"
dry_run="${INPUT_DRY_RUN:-${DISPATCH_DRY_RUN:-false}}"
tag="$(echo "$tag" | tr -d '[:space:]')"
version="$(echo "$version" | tr -d '[:space:]')"
flathub_repo="$(echo "$flathub_repo" | tr -d '[:space:]')"
flathub_branch="$(echo "$flathub_branch" | tr -d '[:space:]')"
mode="$(echo "$mode" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')"
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 "$flathub_repo" ]; then
flathub_repo="flathub/dev.gitcomet.GitComet"
fi
if ! [[ "$flathub_repo" =~ ^[^/]+/[^/]+$ ]]; then
echo "::error title=Invalid Flathub repo::flathub_repo must be OWNER/REPO."
exit 1
fi
if [[ "$mode" != "pull_request" && "$mode" != "push" ]]; then
echo "::error title=Invalid mode::mode must be pull_request or push."
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 "flathub_repo=$flathub_repo" >> "$GITHUB_OUTPUT"
echo "flathub_branch=$flathub_branch" >> "$GITHUB_OUTPUT"
echo "mode=$mode" >> "$GITHUB_OUTPUT"
echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT"
- name: Download Flathub assets from GitHub release
run: |
set -euo pipefail
mkdir -p dist
gh release view "${{ steps.norm.outputs.tag }}" --repo "$GITHUB_REPOSITORY" >/dev/null
gh release download "${{ steps.norm.outputs.tag }}" \
--repo "$GITHUB_REPOSITORY" \
--pattern "dev.gitcomet.GitComet.yaml" \
--pattern "cargo-sources.json" \
--pattern "flathub.json" \
--dir dist \
--clobber
test -f dist/dev.gitcomet.GitComet.yaml
test -f dist/cargo-sources.json
test -f dist/flathub.json
- name: Emit dry-run summary
if: ${{ steps.norm.outputs.dry_run == 'true' }}
run: |
set -euo pipefail
branch_display="${{ steps.norm.outputs.flathub_branch }}"
if [ -z "$branch_display" ]; then
branch_display="default branch"
fi
{
echo "### Flathub deployment dry run"
echo ""
echo "- Source release: \`${{ steps.norm.outputs.tag }}\`"
echo "- Target repo: \`${{ steps.norm.outputs.flathub_repo }}\`"
echo "- Target branch: \`${branch_display}\`"
echo "- Mode: \`${{ steps.norm.outputs.mode }}\`"
echo ""
echo "Manifest SHA256: \`$(sha256sum dist/dev.gitcomet.GitComet.yaml | awk '{print $1}')\`"
echo "Cargo sources SHA256: \`$(sha256sum dist/cargo-sources.json | awk '{print $1}')\`"
echo "flathub.json SHA256: \`$(sha256sum dist/flathub.json | awk '{print $1}')\`"
} >> "$GITHUB_STEP_SUMMARY"
- name: Publish manifest to Flathub packaging repo
if: ${{ steps.norm.outputs.dry_run != 'true' }}
env:
FLATHUB_TOKEN: ${{ secrets.FLATHUB_TOKEN }}
FLATHUB_REPO: ${{ steps.norm.outputs.flathub_repo }}
FLATHUB_BRANCH: ${{ steps.norm.outputs.flathub_branch }}
FLATHUB_MODE: ${{ steps.norm.outputs.mode }}
TAG: ${{ steps.norm.outputs.tag }}
run: |
set -euo pipefail
if [ -z "${FLATHUB_TOKEN:-}" ]; then
echo "::error title=Missing secret::Set FLATHUB_TOKEN to push to ${FLATHUB_REPO}."
exit 1
fi
clone_args=()
if [ -n "$FLATHUB_BRANCH" ]; then
clone_args+=(--branch "$FLATHUB_BRANCH")
fi
rm -rf flathub-repo
git clone "${clone_args[@]}" "https://x-access-token:${FLATHUB_TOKEN}@github.com/${FLATHUB_REPO}.git" flathub-repo
cp dist/dev.gitcomet.GitComet.yaml flathub-repo/dev.gitcomet.GitComet.yaml
cp dist/cargo-sources.json flathub-repo/cargo-sources.json
cp dist/flathub.json flathub-repo/flathub.json
pushd flathub-repo >/dev/null
base_branch="$(git branch --show-current)"
if [ -z "$base_branch" ]; then
echo "::error title=Unknown branch::Could not determine the checked out branch for ${FLATHUB_REPO}."
popd >/dev/null
exit 1
fi
echo "FLATHUB_BASE_BRANCH=${base_branch}" >> "$GITHUB_ENV"
echo "FLATHUB_MODE_RESOLVED=${FLATHUB_MODE}" >> "$GITHUB_ENV"
if [ -z "$(git status --porcelain -- dev.gitcomet.GitComet.yaml cargo-sources.json flathub.json)" ]; then
echo "No Flathub manifest changes detected; packaging repo 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"
if [ "$FLATHUB_MODE" = "push" ]; then
git add dev.gitcomet.GitComet.yaml cargo-sources.json flathub.json
git commit -m "dev.gitcomet.GitComet ${TAG}"
git push origin "HEAD:${base_branch}"
echo "FLATHUB_TARGET_BRANCH=${base_branch}" >> "$GITHUB_ENV"
else
pr_branch="gitcomet/release-${TAG}"
git checkout -B "$pr_branch"
git add dev.gitcomet.GitComet.yaml cargo-sources.json flathub.json
git commit -m "dev.gitcomet.GitComet ${TAG}"
if git ls-remote --exit-code --heads origin "$pr_branch" >/dev/null 2>&1; then
git push --force-with-lease -u origin "$pr_branch"
else
git push -u origin "$pr_branch"
fi
{
printf 'Update GitComet to %s.\n\n' "$TAG"
printf 'Generated by `.github/workflows/deploy-flathub.yml` from GitHub release assets in %s.\n\n' "$GITHUB_REPOSITORY"
printf 'Assets:\n- `dev.gitcomet.GitComet.yaml`\n- `cargo-sources.json`\n- `flathub.json`\n'
} > /tmp/flathub-pr-body.md
existing_pr="$(GH_TOKEN="$FLATHUB_TOKEN" gh pr list \
--repo "$FLATHUB_REPO" \
--head "$pr_branch" \
--base "$base_branch" \
--json number \
--jq '.[0].number // empty')"
if [ -n "$existing_pr" ]; then
GH_TOKEN="$FLATHUB_TOKEN" gh pr edit "$existing_pr" \
--repo "$FLATHUB_REPO" \
--title "dev.gitcomet.GitComet ${TAG}" \
--body-file /tmp/flathub-pr-body.md >/dev/null
pr_url="$(GH_TOKEN="$FLATHUB_TOKEN" gh pr view "$existing_pr" --repo "$FLATHUB_REPO" --json url --jq '.url')"
else
pr_url="$(GH_TOKEN="$FLATHUB_TOKEN" gh pr create \
--repo "$FLATHUB_REPO" \
--base "$base_branch" \
--head "$pr_branch" \
--title "dev.gitcomet.GitComet ${TAG}" \
--body-file /tmp/flathub-pr-body.md)"
fi
echo "FLATHUB_TARGET_BRANCH=${pr_branch}" >> "$GITHUB_ENV"
echo "FLATHUB_PR_URL=${pr_url}" >> "$GITHUB_ENV"
fi
popd >/dev/null
- name: Emit deployment summary
run: |
set -euo pipefail
branch_display="${FLATHUB_TARGET_BRANCH:-${{ steps.norm.outputs.flathub_branch }}}"
if [ -z "$branch_display" ]; then
branch_display="default branch"
fi
mode_display="${FLATHUB_MODE_RESOLVED:-${{ steps.norm.outputs.mode }}}"
{
echo "### Flathub deployment"
echo ""
echo "- Release: \`${{ steps.norm.outputs.tag }}\`"
echo "- Target repo: \`${{ steps.norm.outputs.flathub_repo }}\`"
echo "- Target branch: \`${branch_display}\`"
echo "- Base branch: \`${FLATHUB_BASE_BRANCH:-default branch}\`"
echo "- Mode: \`${mode_display}\`"
echo "- Dry run: \`${{ steps.norm.outputs.dry_run }}\`"
if [ -n "${FLATHUB_PR_URL:-}" ]; then
echo "- Pull request: ${FLATHUB_PR_URL}"
fi
} >> "$GITHUB_STEP_SUMMARY"

View file

@ -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). Defaults to HOMEBREW_TAP_REPO when omitted."
description: "Target tap repository in OWNER/REPO form (e.g. Auto-Explore/homebrew-gitcomet)"
required: false
default: ""
type: string
tap_branch:
description: "Target branch in tap repo. Defaults to HOMEBREW_TAP_BRANCH when omitted."
description: "Target branch in tap repo"
required: false
default: "main"
type: string
dry_run:
description: "Validate and print cask/formula without pushing to tap"
description: "Validate and print formula without pushing to tap"
required: true
default: false
type: boolean
@ -59,7 +59,7 @@ concurrency:
jobs:
deploy:
name: Publish Homebrew cask and formula
name: Publish formula to Homebrew tap
runs-on: ubuntu-latest
timeout-minutes: 20
env:
@ -74,10 +74,8 @@ 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 }}
@ -86,8 +84,8 @@ jobs:
tag="${INPUT_TAG:-${DISPATCH_TAG:-}}"
version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}"
tap_repo="${INPUT_TAP_REPO:-${DISPATCH_TAP_REPO:-${VAR_TAP_REPO:-}}}"
tap_branch="${INPUT_TAP_BRANCH:-${DISPATCH_TAP_BRANCH:-${VAR_TAP_BRANCH:-main}}}"
tap_repo="${INPUT_TAP_REPO:-${DISPATCH_TAP_REPO:-}}"
tap_branch="${INPUT_TAP_BRANCH:-${DISPATCH_TAP_BRANCH:-main}}"
dry_run="${INPUT_DRY_RUN:-${DISPATCH_DRY_RUN:-false}}"
tag="$(echo "$tag" | tr -d '[:space:]')"
@ -140,7 +138,7 @@ jobs:
echo "tap_branch=$tap_branch" >> "$GITHUB_OUTPUT"
echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT"
- name: Download cask and formula from release assets
- name: Download formula from release assets
run: |
set -euo pipefail
mkdir -p dist
@ -148,17 +146,12 @@ 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 cask and formula syntax
run: |
set -euo pipefail
ruby -c dist/gitcomet.rb
ruby -c dist/gitcomet-cli.rb
- name: Validate formula syntax
run: ruby -c dist/gitcomet.rb
- name: Emit dry-run summary
if: ${{ steps.norm.outputs.dry_run == 'true' }}
@ -171,18 +164,13 @@ jobs:
echo "- Target tap: \`${{ steps.norm.outputs.tap_repo }}\`"
echo "- Target branch: \`${{ steps.norm.outputs.tap_branch }}\`"
echo ""
echo "Cask preview (Casks/gitcomet.rb):"
echo "Formula preview:"
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 cask and formula to tap repository
- name: Publish formula to tap repository
if: ${{ steps.norm.outputs.dry_run != 'true' }}
env:
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
@ -199,24 +187,19 @@ jobs:
rm -rf tap-repo
git clone "https://x-access-token:${TAP_TOKEN}@github.com/${TAP_REPO}.git" tap-repo
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
mkdir -p tap-repo/Formula
cp dist/gitcomet.rb tap-repo/Formula/gitcomet.rb
pushd tap-repo >/dev/null
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."
if git diff --quiet -- Formula/gitcomet.rb; then
echo "No 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

View file

@ -5,27 +5,33 @@ on:
branches: ["main"]
paths:
- ".github/workflows/build-release-artifacts.yml"
- ".github/workflows/deploy-aur.yml"
- ".github/workflows/deployment-ci.yml"
- ".github/workflows/deploy-homebrew-tap.yml"
- ".github/workflows/deploy-flathub.yml"
- ".github/workflows/deploy-apt-repo.yml"
- ".github/workflows/release-manual-main.yml"
- "scripts/update-aur.sh"
- "assets/linux/dev.gitcomet.GitComet.desktop"
- "flatpak/**"
- "scripts/generate-flatpak-cargo-sources.py"
- "scripts/render-flathub-manifest.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/deployment-ci.yml"
- ".github/workflows/deploy-homebrew-tap.yml"
- ".github/workflows/deploy-flathub.yml"
- ".github/workflows/deploy-apt-repo.yml"
- ".github/workflows/release-manual-main.yml"
- "scripts/update-aur.sh"
- "assets/linux/dev.gitcomet.GitComet.desktop"
- "flatpak/**"
- "scripts/generate-flatpak-cargo-sources.py"
- "scripts/render-flathub-manifest.sh"
- "scripts/package-macos.sh"
- "scripts/generate-homebrew-formula.sh"
- "scripts/generate-homebrew-cask.sh"
- "scripts/build-apt-repo.sh"
workflow_dispatch:
@ -45,58 +51,29 @@ 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
bash -n scripts/render-flathub-manifest.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-flathub.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: 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
- name: Generate 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" \
@ -104,137 +81,11 @@ 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
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
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
apt-repo-generation-smoke:
name: Validate APT repo generation script
@ -254,14 +105,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"
chmod +x "${pkg_root}/usr/bin/gitcomet"
printf '%s\n' '#!/usr/bin/env bash' 'echo "GitComet CI"' > "${pkg_root}/usr/bin/gitcomet-app"
chmod +x "${pkg_root}/usr/bin/gitcomet-app"
{
echo "Package: gitcomet"
echo "Version: 0.0.0~ci1"
echo "Architecture: amd64"
echo "Maintainer: GitComet CI <ci@autoexplore.ai>"
echo "Maintainer: GitComet CI <ci@example.invalid>"
echo "Description: Synthetic GitComet package for deployment CI."
} > "${pkg_root}/DEBIAN/control"
@ -279,7 +130,7 @@ jobs:
echo "Key-Type: RSA"
echo "Key-Length: 3072"
echo "Name-Real: GitComet CI"
echo "Name-Email: ci@autoexplore.ai"
echo "Name-Email: ci@example.invalid"
echo "Expire-Date: 0"
echo "%no-protection"
echo "%commit"
@ -292,7 +143,6 @@ 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 \
@ -305,7 +155,6 @@ 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
@ -328,38 +177,6 @@ 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" <<EOF
Types: deb
URIs: file://${repo_root}
Suites: stable
Components: main
Architectures: amd64
Signed-By: ${repo_root}/gitcomet-archive-keyring.gpg
EOF
sudo apt-get update \
-o Dir::Etc::sourcelist=/dev/null \
-o Dir::Etc::sourceparts="${source_dir}" \
-o Dir::State::lists="${lists_dir}" \
-o APT::Get::List-Cleanup=0
apt-cache policy gitcomet \
-o Dir::Etc::sourcelist=/dev/null \
-o Dir::Etc::sourceparts="${source_dir}" \
-o Dir::State::lists="${lists_dir}" \
| grep -q '0.0.0~ci1'
macos-packaging-smoke:
name: macOS packaging smoke (${{ matrix.arch }})
runs-on: ${{ matrix.runs_on }}
@ -382,7 +199,7 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Build release binary
run: cargo build -p gitcomet --release --locked --features ui-gpui,gix --bins
run: cargo build -p gitcomet-app --release --locked --features ui-gpui,gix --bins
- name: Package macOS artifacts
run: |
@ -399,7 +216,7 @@ jobs:
set -euo pipefail
tarball="dist/gitcomet-v0.0.0-ci-macos-${{ matrix.arch }}.tar.gz"
test -f "$tarball"
tar -tzf "$tarball" | grep -q "GitComet.app/Contents/MacOS/gitcomet$"
tar -tzf "$tarball" | grep -q "GitComet.app/Contents/MacOS/gitcomet-app$"
- name: Verify DMG launches binary
run: |
@ -413,5 +230,43 @@ jobs:
exit 1
fi
"${mount_point}/GitComet.app/Contents/MacOS/gitcomet" --help >/dev/null
"${mount_point}/GitComet.app/Contents/MacOS/gitcomet-app" --help >/dev/null
hdiutil detach "$mount_point"
flatpak-packaging-smoke:
name: Flatpak packaging smoke
runs-on: ubuntu-latest
timeout-minutes: 120
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-24.08
options: --privileged
steps:
- uses: actions/checkout@v6
- name: Verify generated cargo sources stay in sync
run: |
set -euo pipefail
python3 scripts/generate-flatpak-cargo-sources.py --output /tmp/cargo-sources.json
cmp /tmp/cargo-sources.json flatpak/cargo-sources.json
- name: Render release manifest
run: |
set -euo pipefail
scripts/render-flathub-manifest.sh \
--source-url "https://example.invalid/gitcomet-v0.0.0-ci-source.tar.gz" \
--source-sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" \
--output /tmp/dev.gitcomet.GitComet.yaml
test -s /tmp/dev.gitcomet.GitComet.yaml
- name: Build Flatpak bundle
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: dev.gitcomet.GitComet.flatpak
manifest-path: flatpak/dev.gitcomet.GitComet.local.yaml
cache-key: deployment-ci-flatpak
upload-artifact: false
- name: Verify Flatpak bundle output
run: |
set -euo pipefail
test -f dev.gitcomet.GitComet.flatpak

View file

@ -82,10 +82,10 @@ jobs:
- name: Verify crate version matches release version
run: |
set -euo pipefail
crate_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)"
crate_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' crates/gitcomet-app/Cargo.toml | head -n1)"
wanted="${{ steps.norm.outputs.version }}"
if [ "$crate_version" != "$wanted" ]; then
echo "::error title=Version mismatch::Cargo.toml version '$crate_version' does not match requested release '$wanted'."
echo "::error title=Version mismatch::crates/gitcomet-app/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 packages
name: Deploy Homebrew tap formula
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,16 +187,17 @@ jobs:
dry_run: false
secrets: inherit
deploy_aur:
name: Deploy AUR mirror metadata
deploy_flathub:
name: Deploy Flathub manifest
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
uses: ./.github/workflows/deploy-flathub.yml
with:
tag: ${{ needs.validate.outputs.tag }}
version: ${{ needs.validate.outputs.version }}
aur_repo: ${{ vars.AUR_GITHUB_REPO }}
aur_branch: ${{ vars.AUR_GITHUB_BRANCH }}
flathub_repo: ${{ vars.FLATHUB_REPO }}
flathub_branch: ${{ vars.FLATHUB_BRANCH }}
mode: ${{ vars.FLATHUB_MODE }}
dry_run: false
secrets: inherit
@ -211,6 +212,7 @@ 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"

View file

@ -18,7 +18,7 @@ concurrency:
env:
CARGO_TERM_COLOR: always
# Build gitcomet in headless mode (no GPUI system deps required).
# Build gitcomet-app 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_FEATURES -- -D warnings
run: cargo clippy -p gitcomet-app $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_FEATURES --verbose
run: cargo build -p gitcomet-app $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 binary (headless)
run: cargo build -p gitcomet $APP_FEATURES
- name: Build gitcomet-app binary (headless)
run: cargo build -p gitcomet-app $APP_FEATURES
- name: Git mergetool E2E (Phase 4A — t7610 parity)
run: cargo test -p gitcomet $APP_FEATURES --test mergetool_git_integration --verbose
run: cargo test -p gitcomet-app $APP_FEATURES --test mergetool_git_integration --verbose
- name: Git difftool E2E (Phase 4B — t7800 parity)
run: cargo test -p gitcomet $APP_FEATURES --test difftool_git_integration --verbose
run: cargo test -p gitcomet-app $APP_FEATURES --test difftool_git_integration --verbose
- name: Standalone tool-mode E2E (exit codes + validation)
run: cargo test -p gitcomet $APP_FEATURES --test standalone_tool_mode_integration --verbose
run: cargo test -p gitcomet-app $APP_FEATURES --test standalone_tool_mode_integration --verbose
- name: Mergetool/difftool runtime unit tests (bin target)
run: cargo test -p gitcomet $APP_FEATURES --bin gitcomet --verbose
run: cargo test -p gitcomet-app $APP_FEATURES --bin gitcomet-app --verbose
# Backend integration — mergetool launcher, status, conflict checkout
backend-integration:

View file

@ -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`: binary entrypoint, CLI (clap), difftool/mergetool/setup/uninstall modes.
- `crates/gitcomet-app`: 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 --features ui,gix
cargo build -p gitcomet-app --features ui,gix
```
To also compile the gpui-based UI crate:
```bash
cargo build -p gitcomet --features ui-gpui,gix
cargo build -p gitcomet-app --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 --features ui-gpui,gix -- /path/to/repo
cargo run -p gitcomet-app --features ui-gpui,gix -- /path/to/repo
```
### Testing
@ -84,14 +84,14 @@ Use `--skip-dmg` when running in restricted/sandboxed environments where `hdiuti
The release workflow `.github/workflows/build-release-artifacts.yml` builds and publishes:
- Windows: portable ZIP + MSI
- Linux: tar.gz + AppImage + .deb
- Linux: tar.gz + AppImage + .deb + Flatpak bundle
- macOS: DMG + tar.gz for `arm64` and `x86_64`
- 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)
- Flathub assets: `gitcomet-v<VERSION>-source.tar.gz`, `dev.gitcomet.GitComet.yaml`, `cargo-sources.json`, and `flathub.json`
- Homebrew formula asset: `gitcomet.rb` (generated from macOS + Linux x86_64 tarballs and their SHA256 values)
### Homebrew deployment
To push `Casks/gitcomet.rb` and `Formula/gitcomet-cli.rb` into a Homebrew tap repo automatically on release:
To push `Formula/gitcomet.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:
@ -104,27 +104,28 @@ This release flow will:
- build and upload release artifacts
- publish the GitHub release
- call `.github/workflows/deploy-homebrew-tap.yml` to update `Casks/gitcomet.rb` and `Formula/gitcomet-cli.rb` in the tap repo
- call `.github/workflows/deploy-homebrew-tap.yml` to update `Formula/gitcomet.rb` in the tap repo
You can also run `.github/workflows/deploy-homebrew-tap.yml` manually for backfills or dry-runs.
### AUR mirror deployment
### Flathub deployment
To push `PKGBUILD` and `.SRCINFO` into a GitHub-hosted AUR mirror repo automatically on release:
The app ID used for Flatpak/Flathub packaging is `dev.gitcomet.GitComet`.
1. Create the target repository (default expected name: `OWNER/aur-gitcomet`).
To push `dev.gitcomet.GitComet.yaml`, `cargo-sources.json`, and `flathub.json` into the Flathub packaging repo automatically on release:
1. Complete the one-time Flathub onboarding so the app has a packaging repo, typically `flathub/dev.gitcomet.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`).
- secret `FLATHUB_TOKEN`: GitHub token with `contents:write` access to the Flathub packaging repository.
- optional variable `FLATHUB_REPO`: packaging repository in `OWNER/REPO` form. Defaults to `flathub/dev.gitcomet.GitComet`.
- optional variable `FLATHUB_BRANCH`: target branch. If unset, the packaging repo default branch is used.
- optional variable `FLATHUB_MODE`: `pull_request` (default, opens/updates a PR against the packaging repo) or `push` (commits straight to the target branch).
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
- build and upload the Flatpak bundle plus Flathub source/manifest assets
- publish the GitHub release so the source tarball URL is public
- call `.github/workflows/deploy-flathub.yml` to update the Flathub packaging repo, either by opening/updating a PR or by pushing directly depending on `FLATHUB_MODE`
You can also run `.github/workflows/deploy-aur.yml` manually for backfills or dry-runs.
You can also run `.github/workflows/deploy-flathub.yml` manually for backfills or dry-runs.

71
Cargo.lock generated
View file

@ -2239,8 +2239,8 @@ dependencies = [
]
[[package]]
name = "gitcomet"
version = "0.1.7"
name = "gitcomet-app"
version = "0.1.0"
dependencies = [
"clap",
"embed-resource",
@ -2256,7 +2256,7 @@ dependencies = [
[[package]]
name = "gitcomet-core"
version = "0.1.7"
version = "0.1.0"
dependencies = [
"regex",
"rustc-hash 2.1.1",
@ -2267,14 +2267,14 @@ dependencies = [
[[package]]
name = "gitcomet-git"
version = "0.1.7"
version = "0.1.0"
dependencies = [
"gitcomet-core",
]
[[package]]
name = "gitcomet-git-gix"
version = "0.1.7"
version = "0.1.0"
dependencies = [
"gitcomet-core",
"gix",
@ -2285,7 +2285,7 @@ dependencies = [
[[package]]
name = "gitcomet-state"
version = "0.1.7"
version = "0.1.0"
dependencies = [
"gitcomet-core",
"gix",
@ -2299,14 +2299,14 @@ dependencies = [
[[package]]
name = "gitcomet-ui"
version = "0.1.7"
version = "0.1.0"
dependencies = [
"gitcomet-core",
]
[[package]]
name = "gitcomet-ui-gpui"
version = "0.1.7"
version = "0.1.0"
dependencies = [
"criterion",
"gitcomet-core",
@ -2399,7 +2399,7 @@ dependencies = [
"bstr",
"gix-date",
"gix-error",
"winnow 0.7.15",
"winnow",
]
[[package]]
@ -2501,7 +2501,7 @@ dependencies = [
"smallvec",
"thiserror 2.0.18",
"unicode-bom",
"winnow 0.7.15",
"winnow",
]
[[package]]
@ -2760,7 +2760,7 @@ dependencies = [
"itoa",
"smallvec",
"thiserror 2.0.18",
"winnow 0.7.15",
"winnow",
]
[[package]]
@ -2859,7 +2859,7 @@ dependencies = [
"maybe-async",
"nonempty",
"thiserror 2.0.18",
"winnow 0.7.15",
"winnow",
]
[[package]]
@ -2891,7 +2891,7 @@ dependencies = [
"gix-validate",
"memmap2",
"thiserror 2.0.18",
"winnow 0.7.15",
"winnow",
]
[[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.5+spec-1.1.0",
"toml_edit 0.25.4+spec-1.1.0",
]
[[package]]
@ -7044,7 +7044,7 @@ dependencies = [
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow 0.7.15",
"winnow",
]
[[package]]
@ -7067,9 +7067,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.0.1+spec-1.1.0"
version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
"serde_core",
]
@ -7085,28 +7085,28 @@ dependencies = [
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.25.5+spec-1.1.0"
version = "0.25.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
dependencies = [
"indexmap",
"toml_datetime 1.0.1+spec-1.1.0",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_parser",
"winnow 1.0.0",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.10+spec-1.1.0"
version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"winnow 1.0.0",
"winnow",
]
[[package]]
@ -7117,9 +7117,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.7+spec-1.1.0"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tower"
@ -8426,15 +8426,6 @@ 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"
@ -8744,7 +8735,7 @@ dependencies = [
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 0.7.15",
"winnow",
"zbus_macros",
"zbus_names",
"zvariant",
@ -8772,7 +8763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
dependencies = [
"serde",
"winnow 0.7.15",
"winnow",
"zvariant",
]
@ -9048,7 +9039,7 @@ dependencies = [
"enumflags2",
"serde",
"url",
"winnow 0.7.15",
"winnow",
"zvariant_derive",
"zvariant_utils",
]
@ -9076,5 +9067,5 @@ dependencies = [
"quote",
"serde",
"syn 2.0.117",
"winnow 0.7.15",
"winnow",
]

View file

@ -1,7 +1,7 @@
[workspace]
resolver = "3"
members = [
"crates/gitcomet",
"crates/gitcomet-app",
"crates/gitcomet-core",
"crates/gitcomet-git",
"crates/gitcomet-git-gix",
@ -11,7 +11,7 @@ members = [
]
default-members = [
"crates/gitcomet",
"crates/gitcomet-app",
"crates/gitcomet-core",
"crates/gitcomet-git",
"crates/gitcomet-git-gix",
@ -21,7 +21,6 @@ default-members = [
]
[workspace.package]
version = "0.1.7"
edition = "2024"
license = "AGPL-3.0-only"
authors = ["AutoExplore Oy <info@autoexplore.ai>"]
@ -83,7 +82,7 @@ opt-level = 1
split-debuginfo = "packed"
[profile.release]
lto = "fat"
lto = "thin"
codegen-units = 1
strip = "symbols"

View file

@ -4,11 +4,8 @@
[![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)
**Fastest Open Source Git GUI**
**Speed is a feature.**
GitComet is built for teams that want fast Git operations with local-first privacy, familiar workflows, and open source freedom.
@ -22,25 +19,21 @@ Download the latest prebuilt binaries/installers from [GitHub Releases](https://
#### Homebrew
install app from tap (recommended):
install from tap:
```bash
brew tap auto-explore/gitcomet
brew install --cask gitcomet
brew install gitcomet
```
#### Flatpak / Flathub
**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:
GitHub releases also publish:
```bash
xattr -d com.apple.quarantine /Applications/GitComet.app
```
optional CLI install:
```bash
brew install gitcomet-cli
```
- a Linux Flatpak bundle: `gitcomet-v<VERSION>-linux-x86_64.flatpak`
- a source tarball for Flathub: `gitcomet-v<VERSION>-source.tar.gz`
- the rendered Flathub manifest: `dev.gitcomet.GitComet.yaml`
- the pinned Cargo source list: `cargo-sources.json`
### Fast, Free, Familiar
@ -87,13 +80,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.dev/#pricing](https://gitcomet.dev/#pricing)
- Join waitlist: [gitcomet.com/#pricing](https://gitcomet.com/#pricing)
### Build from source
```bash
cargo build -p gitcomet --features ui-gpui,gix
cargo run -p gitcomet --features ui-gpui,gix -- /path/to/repo
cargo build -p gitcomet-app --features ui-gpui,gix
cargo run -p gitcomet-app --features ui-gpui,gix -- /path/to/repo
```
### Contributing
@ -108,10 +101,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 setup
gitcomet-app setup
# Remove GitComet integration safely
gitcomet uninstall
gitcomet-app uninstall
```
- Use `--local` to target only the current repository instead of global config.
@ -126,7 +119,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"
GITCOMET_BIN="/absolute/path/to/gitcomet-app"
# Headless tool: algorithm-only merge/diff for CI, scripts, and no-display environments
git config --global merge.tool gitcomet
@ -169,7 +162,7 @@ Built-in `uninstall` restores those backups only when the key still has the setu
**Difftool:**
```bash
gitcomet difftool --local <path> --remote <path> [--path <display_name>] [--label-left <label>] [--label-right <label>]
gitcomet-app difftool --local <path> --remote <path> [--path <display_name>] [--label-left <label>] [--label-right <label>]
```
Also reads `LOCAL`/`REMOTE` from environment as a fallback when invoked by Git.
@ -177,7 +170,7 @@ Also reads `LOCAL`/`REMOTE` from environment as a fallback when invoked by Git.
**Mergetool:**
```bash
gitcomet mergetool --local <path> --remote <path> --merged <path> [--base <path>] [--label-local <label>] [--label-remote <label>] [--label-base <label>]
gitcomet-app mergetool --local <path> --remote <path> --merged <path> [--base <path>] [--label-local <label>] [--label-remote <label>] [--label-base <label>]
```
Also reads `LOCAL`/`REMOTE`/`MERGED`/`BASE` from environment. Base is optional for add/add conflicts.
@ -204,10 +197,6 @@ SourceTree, GitKraken, Zed, GPUI, KDiff3, Meld, Github Desktop, Git, Gix, Rust,
This project has been created with the help of AI tools, including OpenAI Codex and Claude Code.
### Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Auto-Explore/gitcomet&type=Date)](https://star-history.com/#Auto-Explore/gitcomet&Date)
### License
GitComet is licensed under the GNU Affero General Public License Version 3

View file

@ -2,8 +2,9 @@
Type=Application
Name=GitComet
Comment=Git UI built with GPUI
Exec=gitcomet
Icon=gitcomet
StartupWMClass=gitcomet
Exec=gitcomet-app
Icon=dev.gitcomet.GitComet
StartupWMClass=dev.gitcomet.GitComet
Terminal=false
Categories=Development;RevisionControl;
Keywords=git;diff;merge;history;revision-control;

View file

@ -1,6 +1,6 @@
[package]
name = "gitcomet"
version.workspace = true
name = "gitcomet-app"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true

View file

@ -16,7 +16,7 @@ fn main() {
let Some(install_dir) = current_exe.parent() else {
return;
};
let app_exe = install_dir.join("gitcomet.exe");
let app_exe = install_dir.join("gitcomet-app.exe");
let mut cmd = Command::new(app_exe);
cmd.current_dir(install_dir);

View file

@ -1,4 +1,4 @@
//! CLI argument parsing for gitcomet.
//! CLI argument parsing for gitcomet-app.
//!
//! Supports six modes:
//! - Default (no subcommand): open the full repository browser
@ -29,7 +29,7 @@ pub mod exit_code {
// ── Raw CLI argument structs (clap) ──────────────────────────────────
#[derive(Parser, Debug)]
#[command(name = "gitcomet", about = "Git GUI built with GPUI", version)]
#[command(name = "gitcomet-app", about = "Git GUI built with GPUI", version)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,

View file

@ -54,7 +54,7 @@ fn install_linux_script_does_not_use_invalid_debug_flag() {
.unwrap_or_else(|err| panic!("failed to read {}: {err}", script_path.display()));
assert!(
!script.contains("cargo build -p gitcomet --${mode}"),
!script.contains("cargo build -p gitcomet-app --${mode}"),
"install script should not forward mode directly as a cargo flag"
);
}
@ -68,7 +68,7 @@ fn parse_mode_mergetool_drops_empty_base_value_before_clap() {
let mode = parse_mode_for_test(
vec![
"gitcomet".into(),
"gitcomet-app".into(),
"mergetool".into(),
"--base".into(),
"".into(),
@ -100,7 +100,7 @@ fn parse_mode_mergetool_drops_empty_attached_base_value_before_clap() {
let mode = parse_mode_for_test(
vec![
"gitcomet".into(),
"gitcomet-app".into(),
"mergetool".into(),
"--base=".into(),
"--local".into(),
@ -1437,7 +1437,14 @@ fn mergetool_env_only_resolution_without_base() {
#[test]
fn clap_parses_difftool_subcommand() {
let cli = Cli::try_parse_from([
"gitcomet", "difftool", "--local", "/tmp/a", "--remote", "/tmp/b", "--path", "foo.txt",
"gitcomet-app",
"difftool",
"--local",
"/tmp/a",
"--remote",
"/tmp/b",
"--path",
"foo.txt",
])
.unwrap();
@ -1454,7 +1461,7 @@ fn clap_parses_difftool_subcommand() {
#[test]
fn clap_parses_mergetool_subcommand() {
let cli = Cli::try_parse_from([
"gitcomet",
"gitcomet-app",
"mergetool",
"--merged",
"/tmp/m",
@ -1491,7 +1498,7 @@ fn clap_parses_mergetool_subcommand() {
fn clap_parses_mergetool_output_aliases() {
for merged_flag in ["-o", "--output", "--out"] {
let cli = Cli::try_parse_from([
"gitcomet",
"gitcomet-app",
"mergetool",
merged_flag,
"/tmp/m",
@ -1516,7 +1523,7 @@ fn clap_parses_mergetool_output_aliases() {
#[test]
fn clap_parses_mergetool_kdiff3_label_aliases() {
let cli = Cli::try_parse_from([
"gitcomet",
"gitcomet-app",
"mergetool",
"--merged",
"/tmp/m",
@ -1545,7 +1552,7 @@ fn clap_parses_mergetool_kdiff3_label_aliases() {
#[test]
fn clap_parses_setup_subcommand() {
let cli = Cli::try_parse_from(["gitcomet", "setup", "--dry-run", "--local"]).unwrap();
let cli = Cli::try_parse_from(["gitcomet-app", "setup", "--dry-run", "--local"]).unwrap();
match cli.command {
Some(Command::Setup(args)) => {
@ -1558,7 +1565,7 @@ fn clap_parses_setup_subcommand() {
#[test]
fn clap_parses_uninstall_subcommand() {
let cli = Cli::try_parse_from(["gitcomet", "uninstall", "--dry-run", "--local"]).unwrap();
let cli = Cli::try_parse_from(["gitcomet-app", "uninstall", "--dry-run", "--local"]).unwrap();
match cli.command {
Some(Command::Uninstall(args)) => {
@ -1574,7 +1581,7 @@ fn uninstall_mode_resolves_into_app_mode() {
let env = TestEnv::new();
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("uninstall"),
OsString::from("--dry-run"),
OsString::from("--local"),
@ -1594,14 +1601,14 @@ fn uninstall_mode_resolves_into_app_mode() {
#[test]
fn clap_parses_no_subcommand_as_browser() {
let cli = Cli::try_parse_from(["gitcomet"]).unwrap();
let cli = Cli::try_parse_from(["gitcomet-app"]).unwrap();
assert!(cli.command.is_none());
assert!(cli.path.is_none());
}
#[test]
fn clap_parses_path_argument() {
let cli = Cli::try_parse_from(["gitcomet", "/some/repo"]).unwrap();
let cli = Cli::try_parse_from(["gitcomet-app", "/some/repo"]).unwrap();
assert!(cli.command.is_none());
assert_eq!(
cli.path.as_deref(),
@ -1618,7 +1625,7 @@ fn compat_parses_positional_difftool_invocation() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
local.into_os_string(),
remote.into_os_string(),
],
@ -1646,7 +1653,7 @@ fn compat_parses_kdiff3_style_difftool_labels() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--L1"),
OsString::from("LEFT_LABEL"),
OsString::from("--L2"),
@ -1676,7 +1683,7 @@ fn compat_parses_kdiff3_style_difftool_short_numbered_equals_labels() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("-L1=LEFT_LABEL"),
OsString::from("-L2=RIGHT_LABEL"),
local.into_os_string(),
@ -1704,7 +1711,7 @@ fn compat_parses_meld_style_difftool_short_labels() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("-L"),
OsString::from("LEFT_LABEL"),
OsString::from("--label"),
@ -1734,7 +1741,7 @@ fn compat_parses_meld_style_difftool_attached_labels() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("-LLEFT_LABEL"),
OsString::from("--label=RIGHT_LABEL"),
local.into_os_string(),
@ -1764,7 +1771,7 @@ fn compat_parses_kdiff3_style_mergetool_with_base() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
OsString::from("--L1"),
OsString::from("BASE_LABEL"),
@ -1807,7 +1814,7 @@ fn compat_parses_kdiff3_style_mergetool_with_base_flag() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
OsString::from("--L1=BASE_LABEL"),
OsString::from("--L2=LOCAL_LABEL"),
@ -1848,7 +1855,7 @@ fn compat_parses_kdiff3_style_mergetool_with_short_numbered_label_flags() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
OsString::from("-L1"),
OsString::from("BASE_LABEL"),
@ -1891,7 +1898,7 @@ fn compat_parses_kdiff3_style_mergetool_with_attached_short_numbered_label_flags
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
OsString::from("-L1BASE_LABEL"),
OsString::from("-L2=LOCAL_LABEL"),
@ -1931,7 +1938,7 @@ fn compat_parses_kdiff3_style_mergetool_with_attached_output_and_base_flags() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
OsString::from("--L1=BASE_LABEL"),
OsString::from("--L2=LOCAL_LABEL"),
@ -1969,7 +1976,7 @@ fn compat_parses_kdiff3_style_mergetool_without_base() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
OsString::from("--L1"),
OsString::from("LOCAL_LABEL"),
@ -2009,7 +2016,7 @@ fn compat_mergetool_applies_merge_conflictstyle_from_git_config() {
let mode = parse_mode_for_test_with_config(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
OsString::from("-o"),
merged.into_os_string(),
@ -2045,7 +2052,7 @@ fn compat_mergetool_applies_diff_algorithm_from_git_config() {
let mode = parse_mode_for_test_with_config(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
OsString::from("-o"),
merged.into_os_string(),
@ -2081,7 +2088,7 @@ fn compat_parses_meld_style_mergetool_with_output() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--output"),
merged.clone().into_os_string(),
local.clone().into_os_string(),
@ -2117,7 +2124,7 @@ fn compat_parses_meld_style_mergetool_labels() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--output"),
merged.clone().into_os_string(),
OsString::from("--label=LOCAL_LABEL"),
@ -2157,7 +2164,7 @@ fn compat_parses_meld_style_mergetool_with_auto_merge_flag() {
let mode = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto-merge"),
OsString::from("--output"),
merged.clone().into_os_string(),
@ -2190,7 +2197,7 @@ fn compat_auto_merge_requires_output_path() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto-merge"),
local.into_os_string(),
base.into_os_string(),
@ -2217,7 +2224,7 @@ fn compat_rejects_too_many_label_flags() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--output"),
merged.into_os_string(),
OsString::from("--label"),
@ -2251,7 +2258,7 @@ fn compat_auto_requires_output_path() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--auto"),
local.into_os_string(),
remote.into_os_string(),
@ -2275,7 +2282,7 @@ fn compat_merge_requires_two_or_three_positionals_after_output_flag() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--output"),
merged.into_os_string(),
local.into_os_string(),
@ -2302,7 +2309,7 @@ fn compat_merge_rejects_too_many_positionals() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--out"),
merged.into_os_string(),
base.into_os_string(),
@ -2331,7 +2338,7 @@ fn compat_merge_rejects_base_flag_with_extra_positionals() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--base"),
base.into_os_string(),
OsString::from("--out"),
@ -2361,7 +2368,7 @@ fn compat_merge_without_base_rejects_l3_label() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--out"),
merged.into_os_string(),
OsString::from("--L3"),
@ -2388,7 +2395,7 @@ fn compat_diff_rejects_l3_without_output_path() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--L3"),
OsString::from("REMOTE"),
local.into_os_string(),
@ -2414,7 +2421,7 @@ fn compat_diff_rejects_base_without_output_path() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--base"),
base.into_os_string(),
local.into_os_string(),
@ -2440,7 +2447,7 @@ fn compat_diff_rejects_too_many_positionals() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
local.into_os_string(),
remote.into_os_string(),
extra.into_os_string(),
@ -2587,7 +2594,7 @@ fn compat_base_flag_requires_two_positionals_when_output_present() {
let err = parse_mode_for_test(
vec![
OsString::from("gitcomet"),
OsString::from("gitcomet-app"),
OsString::from("--base"),
base.into_os_string(),
OsString::from("--out"),
@ -2827,7 +2834,7 @@ fn mergetool_marker_size_zero_errors() {
#[test]
fn clap_parses_conflict_style_and_diff_algorithm() {
let cli = Cli::try_parse_from([
"gitcomet",
"gitcomet-app",
"mergetool",
"--merged",
"/tmp/m",
@ -2989,7 +2996,7 @@ fn git_config_fallback_combined_style_and_algorithm() {
#[test]
fn clap_parses_mergetool_auto_flag() {
let cli = Cli::try_parse_from([
"gitcomet",
"gitcomet-app",
"mergetool",
"--merged",
"/tmp/m",
@ -3012,7 +3019,7 @@ fn clap_parses_mergetool_auto_flag() {
#[test]
fn clap_parses_mergetool_auto_merge_alias_flag() {
let cli = Cli::try_parse_from([
"gitcomet",
"gitcomet-app",
"mergetool",
"--merged",
"/tmp/m",
@ -3126,7 +3133,7 @@ fn compat_auto_merge_flag_propagates_to_config() {
#[test]
fn clap_parses_extract_merge_fixtures_subcommand() {
let cli = Cli::try_parse_from([
"gitcomet",
"gitcomet-app",
"extract-merge-fixtures",
"--repo",
"/tmp/repo",
@ -3155,7 +3162,7 @@ fn extract_merge_fixtures_mode_resolves_into_app_mode() {
let env = TestEnv::new();
let mode = parse_mode_for_test(
vec![
"gitcomet".into(),
"gitcomet-app".into(),
"extract-merge-fixtures".into(),
"--repo".into(),
"/tmp/repo".into(),

View file

@ -568,7 +568,7 @@ mod tests {
fn parse_crash_log_extracts_fields() {
let log = r#"=== GitComet crash (panic) ===
timestamp_unix_ms=123
crate=gitcomet version=0.1.0
crate=gitcomet-app version=0.1.0
thread=main
location=src/main.rs#L42
message=boom happened
@ -580,7 +580,7 @@ frame 2
let parsed = parse_crash_log(log);
assert_eq!(parsed.timestamp_unix_ms.as_deref(), Some("123"));
assert_eq!(parsed.crate_name.as_deref(), Some("gitcomet"));
assert_eq!(parsed.crate_name.as_deref(), Some("gitcomet-app"));
assert_eq!(parsed.crate_version.as_deref(), Some("0.1.0"));
assert_eq!(parsed.thread.as_deref(), Some("main"));
assert_eq!(parsed.location.as_deref(), Some("src/main.rs#L42"));
@ -602,7 +602,7 @@ frame 2
#[test]
fn build_startup_report_populates_issue_url_and_summary() {
let log = r#"timestamp_unix_ms=123
crate=gitcomet version=0.1.0
crate=gitcomet-app version=0.1.0
thread=main
location=src/main.rs#L42
message=boom happened
@ -633,7 +633,7 @@ frame 2
let dir = tempdir().expect("temp dir");
let crash_log_path = dir.path().join("panic.log");
let crash_log = r#"timestamp_unix_ms=123
crate=gitcomet version=0.1.0
crate=gitcomet-app version=0.1.0
thread=main
location=src/main.rs#L42
message=boom happened

View file

@ -1,9 +1,10 @@
use crate::cli::{DifftoolConfig, DifftoolInputKind, classify_difftool_input, exit_code};
use gitcomet_core::platform::host_tempdir;
use rustc_hash::FxHashSet as HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::{Builder, TempDir};
use tempfile::TempDir;
/// Format a `"Failed to {op} {path}: {err}"` message concisely.
macro_rules! io_err {
@ -255,9 +256,7 @@ fn prepare_diff_inputs(config: &DifftoolConfig) -> Result<PreparedDiffInputs, St
});
}
let tempdir = Builder::new()
.prefix("gitcomet-difftool-")
.tempdir()
let tempdir = host_tempdir("gitcomet-difftool-")
.map_err(|e| io_err!("create temporary directory staging area", e))?;
let staged_local = tempdir.path().join("left");

View file

@ -20,6 +20,9 @@ pub(crate) fn hex_encode(bytes: &[u8]) -> String {
}
use std::io::{self, Write};
#[cfg(all(target_os = "macos", feature = "ui-gpui"))]
use gitcomet_core::platform::APP_ID;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
@ -148,7 +151,7 @@ fn main() {
#[cfg(not(feature = "ui-gpui-runtime"))]
if config.gui {
eprintln!(
"GUI difftool mode is unavailable in this build. Rebuild with `-p gitcomet --features ui-gpui`."
"GUI difftool mode is unavailable in this build. Rebuild with `-p gitcomet-app --features ui-gpui`."
);
std::process::exit(exit_code::ERROR);
}
@ -234,7 +237,7 @@ fn main() {
#[cfg(not(feature = "ui"))]
{
let _ = path;
eprintln!("GitComet UI is disabled. Build with `-p gitcomet --features ui`.");
eprintln!("GitComet UI is disabled. Build with `-p gitcomet-app --features ui`.");
std::process::exit(exit_code::ERROR);
}
}
@ -242,7 +245,7 @@ fn main() {
#[cfg(not(feature = "ui-gpui-runtime"))]
if config.gui {
eprintln!(
"GUI mergetool mode is unavailable in this build. Rebuild with `-p gitcomet --features ui-gpui`."
"GUI mergetool mode is unavailable in this build. Rebuild with `-p gitcomet-app --features ui-gpui`."
);
std::process::exit(exit_code::ERROR);
}
@ -284,45 +287,6 @@ const MACOS_BUNDLE_RELAUNCH_ENV: &str = "GITCOMET_SKIP_APP_BUNDLE_RELAUNCH";
#[cfg(all(target_os = "macos", feature = "ui-gpui-runtime"))]
const MACOS_APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/gitcomet-512.png");
#[cfg(feature = "ui-gpui-runtime")]
fn resolve_executable_path_for_bundle_detection(path: &std::path::Path) -> std::path::PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
#[cfg(feature = "ui-gpui-runtime")]
fn is_macos_app_bundle_executable(path: &std::path::Path) -> bool {
path.to_string_lossy().contains(".app/Contents/MacOS/")
}
#[cfg(all(target_os = "macos", feature = "ui-gpui-runtime"))]
fn macos_user_app_bundle_path_with_home(home: Option<&std::path::Path>) -> std::path::PathBuf {
let base_dir = home
.map(|value| value.join("Library/Application Support/GitComet"))
.unwrap_or_else(|| std::env::temp_dir().join("GitComet"));
base_dir.join("GitComet.app")
}
#[cfg(all(target_os = "macos", feature = "ui-gpui-runtime"))]
fn macos_user_app_bundle_path() -> std::path::PathBuf {
let home = std::env::var_os("HOME").map(std::path::PathBuf::from);
macos_user_app_bundle_path_with_home(home.as_deref())
}
#[cfg(all(target_os = "macos", feature = "ui-gpui-runtime"))]
fn candidate_macos_app_bundle_paths(resolved_exe: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut candidates = Vec::with_capacity(2);
if let Some(bin_dir) = resolved_exe.parent() {
candidates.push(bin_dir.join("GitComet.app"));
}
let fallback = macos_user_app_bundle_path();
if !candidates.iter().any(|candidate| candidate == &fallback) {
candidates.push(fallback);
}
candidates
}
#[cfg(all(target_os = "macos", feature = "ui-gpui-runtime"))]
fn maybe_relaunch_browser_from_macos_app_bundle() -> bool {
if std::env::var_os(MACOS_BUNDLE_RELAUNCH_ENV).is_some() {
@ -332,33 +296,23 @@ fn maybe_relaunch_browser_from_macos_app_bundle() -> bool {
let Ok(current_exe) = std::env::current_exe() else {
return false;
};
let resolved_exe = resolve_executable_path_for_bundle_detection(&current_exe);
if is_macos_app_bundle_executable(&resolved_exe) {
if current_exe
.to_string_lossy()
.contains(".app/Contents/MacOS/")
{
return false;
}
let mut prep_errors = Vec::new();
let mut app_exe = None;
for app_bundle in candidate_macos_app_bundle_paths(&resolved_exe) {
match ensure_macos_dev_app_bundle(&resolved_exe, &app_bundle) {
Ok(path) => {
app_exe = Some(path);
break;
}
Err(err) => {
prep_errors.push(format!("{}: {err}", app_bundle.display()));
}
let Some(bin_dir) = current_exe.parent() else {
return false;
};
let app_bundle = bin_dir.join("GitComet.app");
let app_exe = match ensure_macos_dev_app_bundle(&current_exe, &app_bundle) {
Ok(path) => path,
Err(err) => {
eprintln!("Failed to prepare macOS app bundle icon: {err}");
return false;
}
}
let Some(app_exe) = app_exe else {
let details = if prep_errors.is_empty() {
"no app bundle destination available".to_string()
} else {
prep_errors.join("; ")
};
eprintln!("Failed to prepare macOS app bundle: {details}");
return false;
};
let mut relaunch = std::process::Command::new(app_exe);
@ -385,7 +339,7 @@ fn ensure_macos_dev_app_bundle(
std::fs::create_dir_all(&resources)
.map_err(|e| format!("failed to create Resources dir: {e}"))?;
let app_exe = macos.join("gitcomet");
let app_exe = macos.join("gitcomet-app");
std::fs::copy(current_exe, &app_exe)
.map_err(|e| format!("failed to copy executable into bundle: {e}"))?;
@ -421,9 +375,9 @@ fn ensure_macos_dev_app_bundle(
<key>CFBundleDisplayName</key>
<string>GitComet</string>
<key>CFBundleExecutable</key>
<string>gitcomet</string>
<string>gitcomet-app</string>
<key>CFBundleIdentifier</key>
<string>ai.autoexplore.gitcomet.dev</string>
<string>{app_id}</string>
<key>CFBundleIconFile</key>
<string>GitComet.icns</string>
<key>CFBundleInfoDictionaryVersion</key>
@ -443,6 +397,7 @@ fn ensure_macos_dev_app_bundle(
</dict>
</plist>
"#,
app_id = APP_ID,
version = env!("CARGO_PKG_VERSION")
);
std::fs::write(contents.join("Info.plist"), plist)
@ -729,66 +684,6 @@ mod tests {
assert!(err.contains(&merged.display().to_string()));
}
#[test]
#[cfg(feature = "ui-gpui-runtime")]
fn detects_macos_app_bundle_executable_paths() {
assert!(is_macos_app_bundle_executable(std::path::Path::new(
"/tmp/GitComet.app/Contents/MacOS/gitcomet"
)));
assert!(!is_macos_app_bundle_executable(std::path::Path::new(
"/opt/homebrew/bin/gitcomet"
)));
}
#[test]
#[cfg(all(feature = "ui-gpui-runtime", unix))]
fn canonicalized_symlink_resolves_to_macos_app_bundle_executable() {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().unwrap();
let app_exe = temp.path().join("GitComet.app/Contents/MacOS/gitcomet");
fs::create_dir_all(app_exe.parent().unwrap()).unwrap();
fs::write(&app_exe, b"#!/bin/sh\n").unwrap();
let bin_dir = temp.path().join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let symlink_path = bin_dir.join("gitcomet");
symlink(&app_exe, &symlink_path).unwrap();
assert!(!is_macos_app_bundle_executable(&symlink_path));
let resolved = resolve_executable_path_for_bundle_detection(&symlink_path);
let expected = app_exe.canonicalize().unwrap();
assert_eq!(resolved, expected);
assert!(is_macos_app_bundle_executable(&resolved));
}
#[test]
#[cfg(all(feature = "ui-gpui-runtime", target_os = "macos"))]
fn macos_user_app_bundle_path_uses_home_directory_when_available() {
let home = std::path::Path::new("/Users/example");
let bundle = macos_user_app_bundle_path_with_home(Some(home));
assert_eq!(
bundle,
std::path::PathBuf::from(
"/Users/example/Library/Application Support/GitComet/GitComet.app"
)
);
}
#[test]
#[cfg(all(feature = "ui-gpui-runtime", target_os = "macos"))]
fn candidate_macos_app_bundle_paths_include_executable_dir_and_fallback() {
let exe = std::path::Path::new("/opt/homebrew/bin/gitcomet");
let candidates = candidate_macos_app_bundle_paths(exe);
assert_eq!(
candidates.first(),
Some(&std::path::PathBuf::from("/opt/homebrew/bin/GitComet.app"))
);
assert_eq!(candidates.last(), Some(&macos_user_app_bundle_path()));
}
#[test]
fn emit_result_writes_stdout_stderr_and_flushes() {
let result = Ok(TestRunResult {

View file

@ -1,4 +1,4 @@
//! `gitcomet setup` / `gitcomet uninstall` support.
//! `gitcomet-app setup` / `gitcomet-app uninstall` support.
//!
//! Setup writes the recommended global (or local) git config entries so that
//! `git difftool` and `git mergetool` invoke gitcomet automatically.
@ -91,7 +91,7 @@ fn shell_single_quote(value: &str) -> String {
fn current_exe_path() -> Result<PathBuf, String> {
std::env::current_exe()
.and_then(|p| p.canonicalize())
.map_err(|e| format!("Cannot determine gitcomet binary path: {e}"))
.map_err(|e| format!("Cannot determine gitcomet-app binary path: {e}"))
}
fn executable_path_for_shell(bin_path: &Path) -> Result<String, String> {
@ -920,7 +920,7 @@ mod tests {
#[test]
fn build_config_entries_contains_all_required_keys() {
let entries = build_config_entries("/usr/bin/gitcomet");
let entries = build_config_entries("/usr/bin/gitcomet-app");
let keys: Vec<&str> = entries.iter().map(|e| e.key).collect();
// Headless tool
@ -948,7 +948,7 @@ mod tests {
#[test]
fn gui_tool_uses_separate_tool_name() {
let entries = build_config_entries("/usr/bin/gitcomet");
let entries = build_config_entries("/usr/bin/gitcomet-app");
let merge_guitool = entries.iter().find(|e| e.key == "merge.guitool").unwrap();
let diff_guitool = entries.iter().find(|e| e.key == "diff.guitool").unwrap();
@ -1021,14 +1021,14 @@ mod tests {
#[test]
fn mergetool_cmd_escapes_single_quote_in_binary_path() {
let entries = build_config_entries("/tmp/it's/gitcomet");
let entries = build_config_entries("/tmp/it's/gitcomet-app");
let cmd = entries
.iter()
.find(|e| e.key == "mergetool.gitcomet.cmd")
.unwrap();
assert!(
cmd.value.starts_with("'/tmp/it'\"'\"'s/gitcomet'"),
cmd.value.starts_with("'/tmp/it'\"'\"'s/gitcomet-app'"),
"unexpected cmd quoting: {}",
cmd.value
);
@ -1076,7 +1076,7 @@ mod tests {
#[test]
fn format_commands_global_scope() {
let entries = build_config_entries("/bin/gitcomet");
let entries = build_config_entries("/bin/gitcomet-app");
let output = format_commands(&entries, "--global");
// Headless mergetool entries
@ -1109,14 +1109,14 @@ mod tests {
assert!(output.contains("git config --global difftool.guiDefault"));
assert!(
!output.contains("''/bin/gitcomet'"),
!output.contains("''/bin/gitcomet-app'"),
"dry-run output should not contain broken nested quoting:\n{output}"
);
}
#[test]
fn format_commands_local_scope() {
let entries = build_config_entries("/bin/gitcomet");
let entries = build_config_entries("/bin/gitcomet-app");
let output = format_commands(&entries, "--local");
assert!(output.contains("git config --local merge.tool"));
@ -1155,7 +1155,7 @@ mod tests {
.unwrap();
assert!(init.status.success());
let entries = build_config_entries("/test/gitcomet");
let entries = build_config_entries("/test/gitcomet-app");
let result = std::process::Command::new("git")
.arg("-C")
.arg(dir.path())

View file

@ -59,7 +59,7 @@ fn require_git_shell_for_tool_tests() -> bool {
}
fn gitcomet_bin() -> PathBuf {
for env_key in ["CARGO_BIN_EXE_gitcomet"] {
for env_key in ["CARGO_BIN_EXE_gitcomet-app", "CARGO_BIN_EXE_gitcomet_app"] {
if let Some(path) = std::env::var_os(env_key).map(PathBuf::from) {
if path.is_file() {
return path;
@ -72,7 +72,8 @@ fn gitcomet_bin() -> PathBuf {
}
panic!(
"gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
"gitcomet-app binary path was not found. Tried CARGO_BIN_EXE_gitcomet-app, \
CARGO_BIN_EXE_gitcomet_app, and a fallback relative to current test executable"
);
}
@ -82,7 +83,7 @@ fn gitcomet_bin_from_current_exe() -> Option<PathBuf> {
let profile_dir = deps_dir.parent()?;
let exe_suffix = std::env::consts::EXE_SUFFIX;
for bin_name in ["gitcomet"] {
for bin_name in ["gitcomet-app", "gitcomet_app"] {
let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
if candidate.is_file() {
return Some(candidate);
@ -263,7 +264,7 @@ fn output_text(output: &Output) -> String {
}
#[test]
fn git_difftool_invokes_gitcomet_for_basic_diff() {
fn git_difftool_invokes_gitcomet_app_for_basic_diff() {
if !require_git_shell_for_tool_tests() {
return;
}

View file

@ -17,7 +17,7 @@ fn apply_isolated_git_config_env(cmd: &mut Command) {
}
fn gitcomet_bin() -> PathBuf {
for env_key in ["CARGO_BIN_EXE_gitcomet"] {
for env_key in ["CARGO_BIN_EXE_gitcomet-app", "CARGO_BIN_EXE_gitcomet_app"] {
if let Some(path) = std::env::var_os(env_key).map(PathBuf::from) {
if path.is_file() {
return path;
@ -30,7 +30,8 @@ fn gitcomet_bin() -> PathBuf {
}
panic!(
"gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
"gitcomet-app binary path was not found. Tried CARGO_BIN_EXE_gitcomet-app, \
CARGO_BIN_EXE_gitcomet_app, and a fallback relative to current test executable"
);
}
@ -40,7 +41,7 @@ fn gitcomet_bin_from_current_exe() -> Option<PathBuf> {
let profile_dir = deps_dir.parent()?;
let exe_suffix = std::env::consts::EXE_SUFFIX;
for bin_name in ["gitcomet"] {
for bin_name in ["gitcomet-app", "gitcomet_app"] {
let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
if candidate.is_file() {
return Some(candidate);
@ -58,7 +59,7 @@ where
Command::new(gitcomet_bin())
.args(args)
.output()
.expect("gitcomet command to run")
.expect("gitcomet-app command to run")
}
fn run_git_capture(repo: &Path, args: &[&str]) -> Output {

View file

@ -62,7 +62,7 @@ fn require_git_shell_for_tool_tests() -> bool {
}
fn gitcomet_bin() -> PathBuf {
for env_key in ["CARGO_BIN_EXE_gitcomet"] {
for env_key in ["CARGO_BIN_EXE_gitcomet-app", "CARGO_BIN_EXE_gitcomet_app"] {
if let Some(path) = std::env::var_os(env_key).map(PathBuf::from) {
if path.is_file() {
return path;
@ -75,7 +75,8 @@ fn gitcomet_bin() -> PathBuf {
}
panic!(
"gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
"gitcomet-app binary path was not found. Tried CARGO_BIN_EXE_gitcomet-app, \
CARGO_BIN_EXE_gitcomet_app, and a fallback relative to current test executable"
);
}
@ -85,7 +86,7 @@ fn gitcomet_bin_from_current_exe() -> Option<PathBuf> {
let profile_dir = deps_dir.parent()?;
let exe_suffix = std::env::consts::EXE_SUFFIX;
for bin_name in ["gitcomet"] {
for bin_name in ["gitcomet-app", "gitcomet_app"] {
let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
if candidate.is_file() {
return Some(candidate);
@ -649,11 +650,11 @@ fn git_mergetool_accepts_kdiff3_alias_flags_in_cmd() {
&& !text.contains("unexpected argument '--L1'")
&& !text.contains("unexpected argument '--L2'")
&& !text.contains("unexpected argument '--L3'"),
"expected alias flags to be accepted by gitcomet mergetool\noutput:\n{text}"
"expected alias flags to be accepted by gitcomet-app mergetool\noutput:\n{text}"
);
assert!(
text.contains("Auto-merging file.txt"),
"expected gitcomet mergetool to run\noutput:\n{text}"
"expected gitcomet-app mergetool to run\noutput:\n{text}"
);
assert!(
merged.contains("LOCAL") || merged.contains("REMOTE") || merged.contains("<<<<<<<"),
@ -1179,7 +1180,7 @@ fn git_mergetool_trust_exit_code_conflict_preserves_unmerged_state() {
}
// When our tool exits 1 (unresolved conflict) with trustExitCode=true,
// git should leave the file as unmerged. This verifies the exit code
// contract between gitcomet and git mergetool.
// contract between gitcomet-app and git mergetool.
let tmp = tempfile::tempdir().unwrap();
let repo = tmp.path();
@ -3676,7 +3677,7 @@ fn gitcomet_mergetool_reads_conflictstyle_from_repo_when_cwd_is_outside_repo() {
.arg("--merged")
.arg(&merged)
.output()
.expect("gitcomet mergetool command to run");
.expect("gitcomet-app mergetool command to run");
let text = output_text(&output);
let merged_text = fs::read_to_string(&merged).unwrap_or_else(|e| {

View file

@ -6,7 +6,7 @@ use std::process::{Command, Output};
use std::sync::OnceLock;
fn gitcomet_bin() -> PathBuf {
for env_key in ["CARGO_BIN_EXE_gitcomet"] {
for env_key in ["CARGO_BIN_EXE_gitcomet-app", "CARGO_BIN_EXE_gitcomet_app"] {
if let Some(path) = std::env::var_os(env_key).map(PathBuf::from) {
if path.is_file() {
return path;
@ -19,7 +19,8 @@ fn gitcomet_bin() -> PathBuf {
}
panic!(
"gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
"gitcomet-app binary path was not found. Tried CARGO_BIN_EXE_gitcomet-app, \
CARGO_BIN_EXE_gitcomet_app, and a fallback relative to current test executable"
);
}
@ -29,7 +30,7 @@ fn gitcomet_bin_from_current_exe() -> Option<PathBuf> {
let profile_dir = deps_dir.parent()?;
let exe_suffix = std::env::consts::EXE_SUFFIX;
for bin_name in ["gitcomet"] {
for bin_name in ["gitcomet-app", "gitcomet_app"] {
let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
if candidate.is_file() {
return Some(candidate);
@ -47,7 +48,7 @@ where
Command::new(gitcomet_bin())
.args(args)
.output()
.expect("gitcomet command to run")
.expect("gitcomet-app command to run")
}
fn run_gitcomet_in_dir<I, S>(dir: &Path, args: I) -> Output
@ -59,7 +60,7 @@ where
.current_dir(dir)
.args(args)
.output()
.expect("gitcomet command to run")
.expect("gitcomet-app command to run")
}
fn run_gitcomet_in_dir_with_global_env<I, S>(
@ -77,7 +78,7 @@ where
.current_dir(dir)
.args(args)
.output()
.expect("gitcomet command to run")
.expect("gitcomet-app command to run")
}
fn git_config_get(repo_dir: &Path, key: &str) -> Option<String> {
@ -2446,7 +2447,7 @@ fn standalone_mergetool_auto_crlf_subchunk_preserves_line_endings() {
// ── Setup E2E integration ────────────────────────────────────────────
//
// These tests verify that `gitcomet setup --local` produces config that
// These tests verify that `gitcomet-app setup --local` produces config that
// actually works when Git invokes `git mergetool` / `git difftool`. This
// closes the gap between "config keys are written" and "the configured tool
// is invoked end-to-end" — directly validating acceptance criteria 2-3 from
@ -2591,12 +2592,12 @@ fn git_config_get_global_with_env(env: &IsolatedGlobalGitEnv, key: &str) -> Opti
}
}
/// After `gitcomet setup --local`, `git mergetool` should invoke
/// gitcomet's built-in 3-way merge for conflicted files.
/// After `gitcomet-app setup --local`, `git mergetool` should invoke
/// gitcomet-app's built-in 3-way merge for conflicted files.
///
/// For a true content conflict, gitcomet exits 1 and git mergetool
/// For a true content conflict, gitcomet-app exits 1 and git mergetool
/// restores the original file (expected behavior with trustExitCode=true).
/// We verify the tool was invoked by checking for gitcomet's specific
/// We verify the tool was invoked by checking for gitcomet-app's specific
/// stderr messages, which differ from git's own merge output.
#[test]
fn setup_local_enables_git_mergetool_end_to_end() {
@ -2630,33 +2631,33 @@ fn setup_local_enables_git_mergetool_end_to_end() {
"expected merge conflict but git merge succeeded"
);
// 3. Run `git mergetool` — should invoke gitcomet via setup config.
// 3. Run `git mergetool` — should invoke gitcomet-app via setup config.
// DISPLAY is removed so guiDefault=auto selects the headless tool.
let mt = setup_e2e_git_capture(repo, &["mergetool"]);
let mt_stderr = String::from_utf8_lossy(&mt.stderr);
// 4. Verify gitcomet was invoked by checking for its specific stderr
// messages. "conflict(s) remain" is emitted by gitcomet's mergetool
// 4. Verify gitcomet-app was invoked by checking for its specific stderr
// messages. "conflict(s) remain" is emitted by gitcomet-app's mergetool
// mode and is NOT part of git's own output (git says "fix conflicts and
// then commit the result" instead).
assert!(
mt_stderr.contains("conflict(s) remain"),
"expected gitcomet's conflict message in mergetool stderr\n\
"expected gitcomet-app's conflict message in mergetool stderr\n\
stdout: {}\nstderr: {}",
String::from_utf8_lossy(&mt.stdout),
mt_stderr,
);
// Also verify gitcomet's "CONFLICT (content)" message is present.
// Also verify gitcomet-app's "CONFLICT (content)" message is present.
assert!(
mt_stderr.contains("CONFLICT (content)"),
"expected CONFLICT marker from gitcomet\nstderr: {}",
"expected CONFLICT marker from gitcomet-app\nstderr: {}",
mt_stderr,
);
}
/// After `gitcomet setup --local`, `git difftool` should invoke
/// gitcomet's built-in diff and produce unified diff output.
/// After `gitcomet-app setup --local`, `git difftool` should invoke
/// gitcomet-app's built-in diff and produce unified diff output.
#[test]
fn setup_local_enables_git_difftool_end_to_end() {
if !require_git_shell_for_setup_integration_tests() {
@ -2676,7 +2677,7 @@ fn setup_local_enables_git_difftool_end_to_end() {
setup_e2e_commit(repo, "initial");
write_file(&repo.join("file.txt"), "line1\nMODIFIED\nline3\n");
// 3. Run `git difftool` — should invoke gitcomet difftool.
// 3. Run `git difftool` — should invoke gitcomet-app difftool.
// DISPLAY is removed so guiDefault=auto selects the headless tool.
let dt = setup_e2e_git_capture(repo, &["difftool"]);
let dt_text = output_text(&dt);
@ -2687,7 +2688,7 @@ fn setup_local_enables_git_difftool_end_to_end() {
"git difftool should exit 0\n{dt_text}"
);
// gitcomet difftool produces unified diff output.
// gitcomet-app difftool produces unified diff output.
let stdout = String::from_utf8_lossy(&dt.stdout);
assert!(
stdout.contains("@@"),
@ -2703,7 +2704,7 @@ fn setup_local_enables_git_difftool_end_to_end() {
);
}
/// After `gitcomet setup --local`, quoted stage variables in the generated
/// After `gitcomet-app setup --local`, quoted stage variables in the generated
/// mergetool command must preserve paths containing spaces/unicode.
#[test]
fn setup_local_mergetool_handles_spaced_unicode_path_end_to_end() {
@ -2759,7 +2760,7 @@ fn setup_local_mergetool_handles_spaced_unicode_path_end_to_end() {
);
}
/// After `gitcomet setup --local`, quoted stage variables in the generated
/// After `gitcomet-app setup --local`, quoted stage variables in the generated
/// difftool command must preserve paths containing spaces/unicode.
#[test]
fn setup_local_difftool_handles_spaced_unicode_path_end_to_end() {
@ -2807,7 +2808,7 @@ fn setup_local_difftool_handles_spaced_unicode_path_end_to_end() {
);
}
/// `gitcomet setup` (global scope) should configure an isolated global
/// `gitcomet-app setup` (global scope) should configure an isolated global
/// gitconfig so `git mergetool` works end-to-end without local repo config.
#[test]
fn setup_global_enables_git_mergetool_end_to_end_with_isolated_global_config() {
@ -2884,7 +2885,7 @@ fn setup_global_enables_git_mergetool_end_to_end_with_isolated_global_config() {
);
}
/// `gitcomet setup` (global scope) should configure an isolated global
/// `gitcomet-app setup` (global scope) should configure an isolated global
/// gitconfig so `git difftool` works end-to-end without local repo config.
#[test]
fn setup_global_enables_git_difftool_end_to_end_with_isolated_global_config() {
@ -2941,7 +2942,7 @@ fn setup_global_enables_git_difftool_end_to_end_with_isolated_global_config() {
);
}
/// `gitcomet setup` (global scope) should make both headless and GUI
/// `gitcomet-app setup` (global scope) should make both headless and GUI
/// mergetool entries discoverable via `git mergetool --tool-help`.
#[test]
fn setup_global_mergetool_tool_help_lists_headless_and_gui_entries() {
@ -2993,7 +2994,7 @@ fn setup_global_mergetool_tool_help_lists_headless_and_gui_entries() {
);
}
/// `gitcomet setup` (global scope) should make both headless and GUI
/// `gitcomet-app setup` (global scope) should make both headless and GUI
/// difftool entries discoverable via `git difftool --tool-help`.
#[test]
fn setup_global_difftool_tool_help_lists_headless_and_gui_entries() {
@ -3057,7 +3058,7 @@ fn help_flag_exits_zero() {
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("gitcomet"),
stdout.contains("gitcomet-app"),
"help output should mention the binary name"
);
}
@ -3072,7 +3073,7 @@ fn version_flag_exits_zero() {
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("gitcomet"),
stdout.contains("gitcomet-app"),
"version output should mention the binary name"
);
}

View file

@ -60,16 +60,16 @@
<Component Id='MainBinary' Guid='*'>
<File
Id='MainExeFile'
Name='gitcomet.exe'
Name='gitcomet-app.exe'
DiskId='1'
Source='$(var.CargoTargetBinDir)\gitcomet.exe'
Source='$(var.CargoTargetBinDir)\gitcomet-app.exe'
KeyPath='yes'/>
</Component>
<Component Id='LauncherBinary' Guid='*'>
<File
Id='LauncherExeFile'
Name='gitcomet-launcher.exe'
Name='GitComet.exe'
DiskId='1'
Source='$(var.CargoTargetBinDir)\gitcomet-launcher.exe'
KeyPath='yes'/>
@ -86,7 +86,7 @@
Directory='GitCometProgramMenuDir'
Name='GitComet'
Description='Git GUI built with GPUI'
Target='[Bin]gitcomet-launcher.exe'
Target='[Bin]GitComet.exe'
WorkingDirectory='APPLICATIONFOLDER'
Icon='ProductICO'
IconIndex='0'

View file

@ -1,6 +1,6 @@
[package]
name = "gitcomet-core"
version.workspace = true
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
@ -16,9 +16,7 @@ benchmarks = []
[dependencies]
regex = { workspace = true }
tempfile = { workspace = true }
rustc-hash = { workspace = true }
strsim = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View file

@ -9,5 +9,6 @@ pub mod file_diff;
pub mod merge;
pub mod merge_extraction;
pub mod mergetool_trace;
pub mod platform;
pub mod services;
pub mod text_utils;

View file

@ -0,0 +1,50 @@
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
pub const APP_ID: &str = "dev.gitcomet.GitComet";
pub const APP_DESKTOP_FILE_NAME: &str = "dev.gitcomet.GitComet.desktop";
pub const APP_ICON_NAME: &str = APP_ID;
pub fn is_flatpak_sandbox() -> bool {
cfg!(any(target_os = "linux", target_os = "freebsd")) && Path::new("/.flatpak-info").exists()
}
pub fn host_command<S: AsRef<OsStr>>(program: S) -> Command {
let program = program.as_ref();
if is_flatpak_sandbox() {
let mut command = Command::new("flatpak-spawn");
command.arg("--host").arg(program);
command
} else {
Command::new(program)
}
}
pub fn host_tempdir(prefix: &str) -> io::Result<tempfile::TempDir> {
let mut builder = tempfile::Builder::new();
builder.prefix(prefix);
if let Some(root) = host_visible_temp_root()? {
builder.tempdir_in(root)
} else {
builder.tempdir()
}
}
fn host_visible_temp_root() -> io::Result<Option<PathBuf>> {
if !is_flatpak_sandbox() {
return Ok(None);
}
let cache_home = env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")));
let root = cache_home
.unwrap_or_else(env::temp_dir)
.join("gitcomet/tmp");
fs::create_dir_all(&root)?;
Ok(Some(root))
}

View file

@ -1,6 +1,6 @@
[package]
name = "gitcomet-git-gix"
version.workspace = true
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true

View file

@ -1,6 +1,7 @@
use super::{GixRepo, conflict_stages::gix_index_stage_blob_bytes_optional};
use crate::util::{bytes_to_text_preserving_utf8, run_git_simple};
use gitcomet_core::error::{Error, ErrorKind};
use gitcomet_core::platform::{host_command, host_tempdir};
use gitcomet_core::services::{
CommandOutput, MergetoolResult, Result, validate_conflict_resolution_text,
};
@ -68,7 +69,7 @@ impl GixRepo {
// No custom command — try invoking the tool name directly with
// the standard argument convention used by many merge tools.
let tool_executable = tool_path.as_deref().unwrap_or(&tool_name);
Command::new(tool_executable)
host_command(tool_executable)
.arg(local_path)
.arg(base_path)
.arg(remote_path)
@ -183,14 +184,14 @@ fn env_has_display() -> bool {
#[cfg(all(windows, test))]
fn shell_command(custom_cmd: &str) -> Command {
let mut command = Command::new("cmd");
let mut command = host_command("cmd");
command.arg("/C").arg(custom_cmd);
command
}
#[cfg(not(windows))]
fn shell_command(custom_cmd: &str) -> Command {
let mut command = Command::new("sh");
let mut command = host_command("sh");
command.arg("-c").arg(custom_cmd);
command
}
@ -510,10 +511,8 @@ fn build_stage_paths(
let pid = std::process::id();
if write_to_temp {
let tmp_dir = tempfile::Builder::new()
.prefix("gitcomet-mergetool-")
.tempdir()
.map_err(|e| Error::new(ErrorKind::Io(e.kind())))?;
let tmp_dir =
host_tempdir("gitcomet-mergetool-").map_err(|e| Error::new(ErrorKind::Io(e.kind())))?;
let (tmp_dir_path, temp_dir_guard) = if keep_temporaries {
(tmp_dir.keep(), None)
} else {

View file

@ -3,7 +3,7 @@ use crate::util::{
bytes_to_text_preserving_utf8, run_git_capture, run_git_simple, run_git_with_output,
validate_ref_like_arg,
};
use gitcomet_core::domain::{Remote, RemoteBranch, Upstream};
use gitcomet_core::domain::{Remote, RemoteBranch};
use gitcomet_core::error::{Error, ErrorKind};
use gitcomet_core::services::{CommandOutput, PullMode, RemoteUrlKind, Result};
use gix::bstr::ByteSlice as _;
@ -147,38 +147,22 @@ impl GixRepo {
Ok(Some(head.to_string()))
}
fn branch_upstream(&self, branch_name: &str) -> Result<Option<Upstream>> {
validate_ref_like_arg(branch_name, "branch name")?;
fn branch_has_upstream(&self, branch: &str) -> Result<bool> {
validate_ref_like_arg(branch, "branch name")?;
let repo = self.reopen_repo()?;
let ref_name = format!("refs/heads/{branch_name}");
let ref_name = format!("refs/heads/{branch}");
let Some(reference) = repo
.try_find_reference(ref_name.as_str())
.map_err(|e| Error::new(ErrorKind::Backend(format!("gix try_find_reference: {e}"))))?
else {
return Ok(None);
return Ok(false);
};
let tracking_ref_name =
match reference.remote_tracking_ref_name(gix::remote::Direction::Fetch) {
Some(Ok(name)) => name,
Some(Err(_)) | None => return Ok(None),
};
let upstream_short = tracking_ref_name.shorten().to_str_lossy().into_owned();
let Some((remote, upstream_branch)) = parse_short_remote_branch_name(&upstream_short)
else {
return Ok(None);
};
Ok(Some(Upstream {
remote: remote.to_string(),
branch: upstream_branch.to_string(),
}))
}
fn branch_has_upstream(&self, branch: &str) -> Result<bool> {
Ok(self.branch_upstream(branch)?.is_some())
Ok(matches!(
reference.remote_tracking_ref_name(gix::remote::Direction::Fetch),
Some(Ok(_))
))
}
pub(super) fn list_remotes_impl(&self) -> Result<Vec<Remote>> {
@ -366,51 +350,16 @@ impl GixRepo {
run_git_command_with_optional_output(cmd, &command_label, capture_output)
}
fn push_head_to_branch_with_optional_output_impl(
&self,
remote: &str,
branch: &str,
force_with_lease: bool,
capture_output: bool,
) -> Result<CommandOutput> {
validate_ref_like_arg(remote, "remote name")?;
validate_ref_like_arg(branch, "branch name")?;
let command_label = if force_with_lease {
format!("git push --force-with-lease {remote} HEAD:refs/heads/{branch}")
} else {
format!("git push {remote} HEAD:refs/heads/{branch}")
};
let mut cmd = self.git_workdir_cmd();
cmd.arg("push");
if force_with_lease {
cmd.arg("--force-with-lease");
}
cmd.arg("--")
.arg(remote)
.arg(format!("HEAD:refs/heads/{branch}"));
run_git_command_with_optional_output(cmd, &command_label, capture_output)
}
fn push_with_optional_output_impl(&self, capture_output: bool) -> Result<CommandOutput> {
if let Some(branch) = self.current_branch_name()? {
if let Some(upstream) = self.branch_upstream(&branch)? {
return self.push_head_to_branch_with_optional_output_impl(
&upstream.remote,
&upstream.branch,
false,
capture_output,
);
}
if let Some(remote) = self.preferred_remote_name()? {
return self.push_set_upstream_with_optional_output_impl(
&remote,
&branch,
capture_output,
);
}
if let Some(branch) = self.current_branch_name()?
&& !self.branch_has_upstream(&branch)?
&& let Some(remote) = self.preferred_remote_name()?
{
return self.push_set_upstream_with_optional_output_impl(
&remote,
&branch,
capture_output,
);
}
let mut cmd = self.git_workdir_cmd();
@ -427,17 +376,6 @@ impl GixRepo {
}
fn push_force_with_optional_output_impl(&self, capture_output: bool) -> Result<CommandOutput> {
if let Some(branch) = self.current_branch_name()?
&& let Some(upstream) = self.branch_upstream(&branch)?
{
return self.push_head_to_branch_with_optional_output_impl(
&upstream.remote,
&upstream.branch,
true,
capture_output,
);
}
let mut cmd = self.git_workdir_cmd();
cmd.arg("push").arg("--force-with-lease");
run_git_command_with_optional_output(cmd, "git push --force-with-lease", capture_output)

View file

@ -5,6 +5,7 @@ use gitcomet_core::auth::{
};
use gitcomet_core::domain::{Commit, CommitId, LogPage};
use gitcomet_core::error::{Error, ErrorKind, GitFailure, GitFailureId};
use gitcomet_core::platform::host_tempdir;
use gitcomet_core::services::{CommandOutput, Result};
use std::fs;
use std::path::{Path, PathBuf};
@ -189,7 +190,7 @@ echo %GITCOMET_AUTH_SECRET%
}
fn create_askpass_script() -> Result<AskPassScript> {
let dir = tempfile::tempdir().map_err(io_err)?;
let dir = host_tempdir("gitcomet-askpass-").map_err(|e| Error::new(ErrorKind::Io(e.kind())))?;
#[cfg(windows)]
let script_name = "gitcomet-askpass.cmd";
#[cfg(not(windows))]

View file

@ -201,7 +201,8 @@ fn require_git_local_push_for_remote_management_tests() -> bool {
true
}
fn configure_repo_with_user(repo: &Path) {
fn init_repo_with_user(repo: &Path) {
run_git(repo, &["init"]);
run_git(repo, &["config", "user.email", "you@example.com"]);
run_git(repo, &["config", "user.name", "You"]);
run_git(repo, &["config", "commit.gpgsign", "false"]);
@ -209,11 +210,6 @@ fn configure_repo_with_user(repo: &Path) {
run_git(repo, &["config", "core.eol", "lf"]);
}
fn init_repo_with_user(repo: &Path) {
run_git(repo, &["init"]);
configure_repo_with_user(repo);
}
#[test]
fn remote_add_set_url_and_remove_round_trip() {
let _guard = remote_management_test_lock();
@ -349,64 +345,6 @@ fn push_with_output_sets_upstream_when_missing() {
);
}
#[test]
fn push_with_output_uses_tracked_upstream_when_branch_names_differ() {
let _guard = remote_management_test_lock();
if !require_git_local_push_for_remote_management_tests() {
return;
}
let dir = tempfile::tempdir().expect("create tempdir");
let root = dir.path();
let remote_repo = root.join("remote.git");
let work_repo = root.join("work");
fs::create_dir_all(&remote_repo).expect("create remote repo dir");
fs::create_dir_all(&work_repo).expect("create work repo dir");
run_git(&remote_repo, &["init", "--bare", "-b", "main"]);
run_git(&work_repo, &["init", "-b", "main"]);
configure_repo_with_user(&work_repo);
let remote_str = git_remote_url(&remote_repo);
run_git(&work_repo, &["remote", "add", "origin", &remote_str]);
run_git(&work_repo, &["config", "push.default", "simple"]);
fs::write(work_repo.join("file.txt"), "base\n").expect("write base file");
run_git(&work_repo, &["add", "file.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "base"],
);
run_git(&work_repo, &["push", "-u", "origin", "main"]);
run_git(&work_repo, &["checkout", "-b", "main2"]);
run_git(
&work_repo,
&["branch", "--set-upstream-to", "origin/main", "main2"],
);
fs::write(work_repo.join("file.txt"), "base\nnext\n").expect("write updated file");
run_git(&work_repo, &["add", "file.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "next"],
);
let local_head = run_git_capture(&work_repo, &["rev-parse", "HEAD"])
.trim()
.to_string();
let backend = GixBackend;
let opened = backend.open(&work_repo).expect("open work repo");
let output = opened.push_with_output().expect("push tracked upstream");
assert_eq!(output.exit_code, Some(0));
let remote_head = run_git_capture(&remote_repo, &["rev-parse", "refs/heads/main"])
.trim()
.to_string();
assert_eq!(remote_head, local_head);
}
#[test]
fn delete_remote_branch_with_output_deletes_remote_and_tracking_ref() {
let _guard = remote_management_test_lock();
@ -570,7 +508,7 @@ fn push_force_without_output_updates_remote_head_after_rewrite() {
run_git(&remote_repo, &["init", "--bare", "-b", "main"]);
run_git(&work_repo, &["init", "-b", "main"]);
configure_repo_with_user(&work_repo);
init_repo_with_user(&work_repo);
let remote_str = git_remote_url(&remote_repo);
run_git(&work_repo, &["remote", "add", "origin", &remote_str]);
@ -618,76 +556,6 @@ fn push_force_without_output_updates_remote_head_after_rewrite() {
assert_eq!(remote_head_after, local_head);
}
#[test]
fn push_force_with_output_uses_tracked_upstream_when_branch_names_differ() {
let _guard = remote_management_test_lock();
if !require_git_local_push_for_remote_management_tests() {
return;
}
let dir = tempfile::tempdir().expect("create tempdir");
let root = dir.path();
let remote_repo = root.join("remote.git");
let work_repo = root.join("work");
fs::create_dir_all(&remote_repo).expect("create remote repo dir");
fs::create_dir_all(&work_repo).expect("create work repo dir");
run_git(&remote_repo, &["init", "--bare", "-b", "main"]);
run_git(&work_repo, &["init", "-b", "main"]);
configure_repo_with_user(&work_repo);
let remote_str = git_remote_url(&remote_repo);
run_git(&work_repo, &["remote", "add", "origin", &remote_str]);
run_git(&work_repo, &["config", "push.default", "simple"]);
fs::write(work_repo.join("file.txt"), "base\n").expect("write base file");
run_git(&work_repo, &["add", "file.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "base"],
);
run_git(&work_repo, &["push", "-u", "origin", "main"]);
run_git(&work_repo, &["checkout", "-b", "main2"]);
run_git(
&work_repo,
&["branch", "--set-upstream-to", "origin/main", "main2"],
);
fs::write(work_repo.join("file.txt"), "base\nnext\n").expect("write updated file");
run_git(&work_repo, &["add", "file.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "next"],
);
run_git(&work_repo, &["push", "origin", "HEAD:refs/heads/main"]);
run_git(&work_repo, &["fetch", "origin"]);
run_git(&work_repo, &["reset", "--hard", "HEAD~1"]);
fs::write(work_repo.join("file.txt"), "base\nrewritten\n").expect("write rewritten file");
run_git(&work_repo, &["add", "file.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "rewritten"],
);
let local_head = run_git_capture(&work_repo, &["rev-parse", "HEAD"])
.trim()
.to_string();
let backend = GixBackend;
let opened = backend.open(&work_repo).expect("open work repo");
let output = opened
.push_force_with_output()
.expect("force push tracked upstream");
assert_eq!(output.exit_code, Some(0));
let remote_head = run_git_capture(&remote_repo, &["rev-parse", "refs/heads/main"])
.trim()
.to_string();
assert_eq!(remote_head, local_head);
}
#[test]
fn pull_non_output_supports_all_modes_when_upstream_exists() {
let _guard = remote_management_test_lock();
@ -706,7 +574,7 @@ fn pull_non_output_supports_all_modes_when_upstream_exists() {
run_git(&origin, &["init", "--bare", "-b", "main"]);
run_git(&repo_a, &["init", "-b", "main"]);
configure_repo_with_user(&repo_a);
init_repo_with_user(&repo_a);
fs::write(repo_a.join("a.txt"), "one\n").expect("write initial file");
run_git(&repo_a, &["add", "a.txt"]);
run_git(
@ -725,7 +593,7 @@ fn pull_non_output_supports_all_modes_when_upstream_exists() {
repo_b.to_string_lossy().as_ref(),
],
);
configure_repo_with_user(&repo_b);
init_repo_with_user(&repo_b);
fs::write(repo_a.join("a.txt"), "one\ntwo\n").expect("write updated file");
run_git(&repo_a, &["add", "a.txt"]);
@ -855,7 +723,7 @@ fn pull_branch_with_output_merges_named_remote_branch() {
run_git(&remote_repo, &["init", "--bare", "-b", "main"]);
run_git(&work_repo, &["init", "-b", "main"]);
configure_repo_with_user(&work_repo);
init_repo_with_user(&work_repo);
let remote_str = git_remote_url(&remote_repo);
run_git(&work_repo, &["remote", "add", "origin", &remote_str]);

View file

@ -1,6 +1,6 @@
[package]
name = "gitcomet-git"
version.workspace = true
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "gitcomet-state"
version.workspace = true
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true

View file

@ -684,7 +684,7 @@ mod tests {
"/tmp/target/debug/gitcomet_ui_gpui-3ad1b0fd3f0c0d3e"
)));
assert!(!looks_like_test_binary(Path::new(
"/tmp/target/debug/gitcomet"
"/tmp/target/debug/gitcomet-app"
)));
}

View file

@ -5,6 +5,7 @@ use gitcomet_core::auth::{
GitAuthKind, StagedGitAuth, take_staged_git_auth,
};
use gitcomet_core::error::{Error, ErrorKind};
use gitcomet_core::platform::host_tempdir;
use gitcomet_core::services::CommandOutput;
use std::fs;
use std::io::{BufRead as _, BufReader, Read as _};
@ -199,7 +200,7 @@ echo %GITCOMET_AUTH_SECRET%
}
fn create_askpass_script() -> Result<AskPassScript, Error> {
let dir = tempfile::tempdir().map_err(|e| Error::new(ErrorKind::Io(e.kind())))?;
let dir = host_tempdir("gitcomet-askpass-").map_err(|e| Error::new(ErrorKind::Io(e.kind())))?;
#[cfg(windows)]
let script_name = "gitcomet-askpass.cmd";
#[cfg(not(windows))]

View file

@ -1,6 +1,6 @@
[package]
name = "gitcomet-ui-gpui"
version.workspace = true
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true

View file

@ -4,6 +4,7 @@ use crate::view::{
FocusedMergetoolLabels, FocusedMergetoolViewConfig, GitCometView, GitCometViewConfig,
GitCometViewMode, StartupCrashReport,
};
use gitcomet_core::platform::APP_ID;
use gitcomet_core::services::GitBackend;
use gitcomet_state::session;
use gitcomet_state::store::AppStore;
@ -80,7 +81,7 @@ fn normal_launch_config(
) -> WindowLaunchConfig {
WindowLaunchConfig {
title: "GitComet".to_string(),
app_id: "gitcomet".to_string(),
app_id: APP_ID.to_string(),
view_config: GitCometViewConfig::normal(initial_path, startup_crash_report),
}
}
@ -91,7 +92,7 @@ fn focused_mergetool_launch_config(
) -> WindowLaunchConfig {
WindowLaunchConfig {
title: focused_mergetool_window_title(&config.conflicted_file_path),
app_id: "gitcomet-mergetool".to_string(),
app_id: APP_ID.to_string(),
view_config: GitCometViewConfig {
initial_path: Some(config.repo_path.clone()),
view_mode: GitCometViewMode::FocusedMergetool,
@ -309,7 +310,7 @@ mod tests {
};
let launch = focused_mergetool_launch_config(&config, None);
assert_eq!(launch.app_id, "gitcomet-mergetool");
assert_eq!(launch.app_id, APP_ID);
assert_eq!(launch.title, "GitComet - Mergetool (conflict.txt)");
assert_eq!(launch.view_config.initial_path, Some(config.repo_path));
assert_eq!(

View file

@ -1,4 +1,4 @@
//! Focused diff window for standalone `gitcomet difftool` invocation.
//! Focused diff window for standalone `gitcomet-app difftool` invocation.
//!
//! Opens a GPUI window that displays a unified diff with color-coded lines.
//! The user reviews the diff and closes the window (exit 0).
@ -6,6 +6,7 @@
use crate::assets::GitCometAssets;
use crate::launch_guard::run_with_panic_guard;
use crate::theme::{AppTheme, with_alpha};
use gitcomet_core::platform::APP_ID;
use gpui::prelude::*;
use gpui::{
App, Application, Bounds, FocusHandle, Focusable, FontWeight, KeyBinding, Render, ScrollHandle,
@ -291,7 +292,7 @@ pub fn run_focused_diff(config: FocusedDiffConfig) -> i32 {
appears_transparent: false,
traffic_light_position: Some(point(px(9.0), px(9.0))),
}),
app_id: Some("gitcomet-diff".to_string()),
app_id: Some(APP_ID.to_string()),
window_decorations: Some(WindowDecorations::Server),
is_movable: true,
is_resizable: true,

View file

@ -1601,11 +1601,6 @@ fn popover_closes_when_clicking_outside(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn titlebar_window_controls_update_tooltip_on_hover(cx: &mut gpui::TestAppContext) {
if cfg!(target_os = "macos") {
// The custom Min/Max/Close controls are only rendered on non-macOS.
return;
}
let (store, events) = AppStore::new(Arc::new(TestBackend));
let (view, cx) = cx.add_window_view(|window, cx| {
crate::view::GitCometView::new(store, events, None, window, cx)

View file

@ -1,4 +1,6 @@
use super::*;
#[cfg(any(test, target_os = "linux", target_os = "freebsd"))]
use gitcomet_core::platform::{APP_DESKTOP_FILE_NAME, APP_ICON_NAME, is_flatpak_sandbox};
#[cfg(any(test, target_os = "linux", target_os = "freebsd"))]
fn desktop_entry_exec_path_arg(exe: &std::path::Path) -> Result<String, String> {
@ -29,9 +31,10 @@ fn desktop_entry_exec_path_arg(exe: &std::path::Path) -> Result<String, String>
fn should_auto_install_linux_desktop_integration(
no_desktop_install_flag_present: bool,
_xdg_current_desktop: Option<&str>,
is_flatpak: bool,
) -> bool {
// `.desktop` entries follow the FreeDesktop spec, so installation is not GNOME-specific.
!no_desktop_install_flag_present
!no_desktop_install_flag_present && !is_flatpak
}
impl GitCometView {
@ -47,6 +50,7 @@ impl GitCometView {
if !should_auto_install_linux_desktop_integration(
std::env::var_os("GITCOMET_NO_DESKTOP_INSTALL").is_some(),
desktop.as_deref(),
is_flatpak_sandbox(),
) {
return;
}
@ -59,10 +63,12 @@ impl GitCometView {
return;
};
let desktop_path = data_home.join("applications/gitcomet.desktop");
let desktop_path = data_home.join(format!("applications/{APP_DESKTOP_FILE_NAME}"));
let all_icons_exist = ICON_SIZES.iter().all(|size| {
data_home
.join(format!("icons/hicolor/{size}x{size}/apps/gitcomet.png"))
.join(format!(
"icons/hicolor/{size}x{size}/apps/{APP_ICON_NAME}.png"
))
.exists()
});
if desktop_path.exists() && all_icons_exist {
@ -87,7 +93,7 @@ impl GitCometView {
const DESKTOP_TEMPLATE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../assets/linux/gitcomet.desktop"
"/../../assets/linux/dev.gitcomet.GitComet.desktop"
));
const ICON_32_PNG: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
@ -131,8 +137,9 @@ impl GitCometView {
let applications_dir = data_home.join("applications");
let icons_root = data_home.join("icons/hicolor");
let desktop_path = applications_dir.join("gitcomet.desktop");
let icon_path = icons_root.join("512x512/apps/gitcomet.png");
let desktop_path = applications_dir.join(APP_DESKTOP_FILE_NAME);
let icon_path =
icons_root.join(format!("512x512/apps/{APP_ICON_NAME}.png"));
fs::create_dir_all(&applications_dir)
.map_err(|e| format!("Desktop install failed: {e}"))?;
@ -159,7 +166,7 @@ impl GitCometView {
for (size, icon_bytes) in ICON_ASSETS {
let icon_dir = icons_root.join(format!("{size}x{size}/apps"));
let icon_file = icon_dir.join("gitcomet.png");
let icon_file = icon_dir.join(format!("{APP_ICON_NAME}.png"));
fs::create_dir_all(&icon_dir)
.and_then(|_| fs::write(&icon_file, icon_bytes))
.map_err(|e| format!("Desktop install failed: {e}"))?;
@ -226,22 +233,35 @@ mod tests {
fn auto_install_is_not_limited_to_gnome() {
for desktop in ["GNOME", "KDE", "XFCE", "sway", ""] {
assert!(
should_auto_install_linux_desktop_integration(false, Some(desktop)),
should_auto_install_linux_desktop_integration(false, Some(desktop), false),
"expected desktop '{desktop}' to allow auto install"
);
}
assert!(should_auto_install_linux_desktop_integration(false, None));
assert!(should_auto_install_linux_desktop_integration(
false, None, false
));
}
#[test]
fn auto_install_respects_opt_out_flag() {
assert!(!should_auto_install_linux_desktop_integration(
true,
Some("GNOME")
Some("GNOME"),
false
));
assert!(!should_auto_install_linux_desktop_integration(
true,
Some("KDE")
Some("KDE"),
false
));
}
#[test]
fn auto_install_is_disabled_in_flatpak() {
assert!(!should_auto_install_linux_desktop_integration(
false,
Some("GNOME"),
true
));
}
}

View file

@ -293,15 +293,9 @@ fn annotate_change_hints(
changed_new_lines: &[bool],
) {
for row in &mut old_doc.rows {
if matches!(row.kind, MarkdownPreviewRowKind::Spacer) {
continue;
}
row.change_hint = line_range_change_hint(&row.source_line_range, changed_old_lines, true);
}
for row in &mut new_doc.rows {
if matches!(row.kind, MarkdownPreviewRowKind::Spacer) {
continue;
}
row.change_hint = line_range_change_hint(&row.source_line_range, changed_new_lines, false);
}
}
@ -403,17 +397,13 @@ fn push_aligned_markdown_row_groups(
}
fn markdown_preview_spacer_row() -> MarkdownPreviewRow {
markdown_preview_spacer_row_with_range(0..0)
}
fn markdown_preview_spacer_row_with_range(source_line_range: Range<usize>) -> MarkdownPreviewRow {
MarkdownPreviewRow {
kind: MarkdownPreviewRowKind::Spacer,
text: SharedString::from(""),
inline_spans: Arc::new(Vec::new()),
code_language: None,
code_block_horizontal_scroll_hint: false,
source_line_range,
source_line_range: 0..0,
change_hint: MarkdownChangeHint::None,
indent_level: 0,
blockquote_level: 0,
@ -1217,57 +1207,9 @@ fn flatten_to_rows(source: &str, line_starts: &[usize]) -> Option<Vec<MarkdownPr
}
align_table_columns(&mut rows);
insert_top_level_heading_spacer_rows(&mut rows);
Some(rows)
}
fn insert_top_level_heading_spacer_rows(rows: &mut Vec<MarkdownPreviewRow>) {
if rows.len() < 2 {
return;
}
let mut spaced_rows = Vec::with_capacity(rows.len() + rows.len() / 4);
let mut pending_gap_after_heading: Option<Range<usize>> = None;
for row in rows.drain(..) {
let is_top_level_heading = markdown_row_is_top_level_heading(&row);
if let Some(source_line_range) = pending_gap_after_heading.take()
&& !is_top_level_heading
{
spaced_rows.push(markdown_preview_spacer_row_with_range(source_line_range));
}
if is_top_level_heading {
let has_content_before_heading = matches!(
spaced_rows.last(),
Some(previous_row)
if !matches!(
previous_row.kind,
MarkdownPreviewRowKind::Spacer | MarkdownPreviewRowKind::Heading { .. }
)
);
if has_content_before_heading {
spaced_rows.push(markdown_preview_spacer_row_with_range(
row.source_line_range.clone(),
));
} else {
pending_gap_after_heading = Some(row.source_line_range.clone());
}
}
spaced_rows.push(row);
}
*rows = spaced_rows;
}
fn markdown_row_is_top_level_heading(row: &MarkdownPreviewRow) -> bool {
matches!(row.kind, MarkdownPreviewRowKind::Heading { .. })
&& row.indent_level == 0
&& row.blockquote_level == 0
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum HtmlHandling {
Ignore,
@ -1986,61 +1928,6 @@ mod tests {
assert_eq!(row_texts(&doc), vec!["H1", "H2", "H3", "H4", "H5", "H6"]);
}
#[test]
fn top_level_heading_inserts_section_spacer_before_following_content() {
let doc = parse("# Title\n\nParagraph\n");
assert_eq!(doc.rows.len(), 3);
assert_eq!(
doc.rows[0].kind,
MarkdownPreviewRowKind::Heading { level: 1 }
);
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Spacer);
assert_eq!(doc.rows[1].change_hint, MarkdownChangeHint::None);
assert_eq!(doc.rows[2].kind, MarkdownPreviewRowKind::Paragraph);
}
#[test]
fn content_before_top_level_heading_gets_section_spacer_before_heading() {
let doc = parse("Paragraph\n\n# Title\n");
assert_eq!(doc.rows.len(), 3);
assert_eq!(doc.rows[0].kind, MarkdownPreviewRowKind::Paragraph);
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Spacer);
assert_eq!(
doc.rows[2].kind,
MarkdownPreviewRowKind::Heading { level: 1 }
);
}
#[test]
fn consecutive_headings_do_not_insert_spacers_between_heading_rows() {
let doc = parse("# Title\n## Subtitle\n");
assert_eq!(
row_kinds(&doc),
vec![
&MarkdownPreviewRowKind::Heading { level: 1 },
&MarkdownPreviewRowKind::Heading { level: 2 },
]
);
}
#[test]
fn middle_heading_uses_a_single_section_spacer_before_the_heading() {
let doc = parse("Intro\n\n# Title\n\nBody\n");
assert_eq!(
row_kinds(&doc),
vec![
&MarkdownPreviewRowKind::Paragraph,
&MarkdownPreviewRowKind::Spacer,
&MarkdownPreviewRowKind::Heading { level: 1 },
&MarkdownPreviewRowKind::Paragraph,
]
);
}
// ── Paragraph tests ─────────────────────────────────────────────────
#[test]
@ -2778,29 +2665,6 @@ mod tests {
assert_eq!(new_para.change_hint, MarkdownChangeHint::Added);
}
#[test]
fn heading_spacing_rows_do_not_receive_change_hints() {
let preview =
build_markdown_diff_preview("# Title\n\nOld paragraph\n", "# Title\n\nNew paragraph\n")
.unwrap();
let old_spacer = preview
.old
.rows
.iter()
.find(|row| matches!(row.kind, MarkdownPreviewRowKind::Spacer))
.expect("expected heading spacer row on old side");
let new_spacer = preview
.new
.rows
.iter()
.find(|row| matches!(row.kind, MarkdownPreviewRowKind::Spacer))
.expect("expected heading spacer row on new side");
assert_eq!(old_spacer.change_hint, MarkdownChangeHint::None);
assert_eq!(new_spacer.change_hint, MarkdownChangeHint::None);
}
#[test]
fn partial_change_ranges_use_modified_hint() {
let (mut old_doc, mut new_doc) =
@ -3227,18 +3091,17 @@ code line
";
let doc = parse(src);
// Should have: Heading, Spacer, Paragraph, ListItem, ListItem, CodeLine, ThematicBreak
// Should have: Heading, Paragraph, ListItem, ListItem, CodeLine, ThematicBreak
assert!(
doc.rows.len() >= 7,
"expected at least 7 rows, got {}",
doc.rows.len() >= 6,
"expected at least 6 rows, got {}",
doc.rows.len()
);
assert!(matches!(
doc.rows[0].kind,
MarkdownPreviewRowKind::Heading { level: 1 }
));
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Spacer);
assert_eq!(doc.rows[2].kind, MarkdownPreviewRowKind::Paragraph);
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Paragraph);
}
// ── Internal helpers ────────────────────────────────────────────────
@ -3300,27 +3163,25 @@ code line
#[test]
fn custom_anchor_tags_are_hidden_from_preview() {
let doc = parse("# Section Heading\n\n<a name=\"my-custom-anchor-point\"></a>\nVisible\n");
assert_eq!(doc.rows.len(), 3);
assert_eq!(doc.rows.len(), 2);
assert_eq!(
doc.rows[0].kind,
MarkdownPreviewRowKind::Heading { level: 1 }
);
assert_eq!(doc.rows[0].text.as_ref(), "Section Heading");
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Spacer);
assert_eq!(doc.rows[2].text.as_ref(), "Visible");
assert_eq!(doc.rows[1].text.as_ref(), "Visible");
}
#[test]
fn custom_anchor_id_tags_are_hidden_from_preview() {
let doc = parse("# Section Heading\n\n<a id=\"jump-target\"></a>\nVisible\n");
assert_eq!(doc.rows.len(), 3);
assert_eq!(doc.rows.len(), 2);
assert_eq!(
doc.rows[0].kind,
MarkdownPreviewRowKind::Heading { level: 1 }
);
assert_eq!(doc.rows[0].text.as_ref(), "Section Heading");
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Spacer);
assert_eq!(doc.rows[2].text.as_ref(), "Visible");
assert_eq!(doc.rows[1].text.as_ref(), "Visible");
}
#[test]

View file

@ -1247,33 +1247,20 @@ fn whole_file_conflict_streamed_three_way_syntax_survives_view_mode_switch(
let _ = window.draw(app);
});
wait_for_main_pane_condition_with_timeout(
cx,
&view,
"streamed two-way HTML row cache after three-way switch",
BACKGROUND_SYNTAX_MAIN_PANE_WAIT_TIMEOUT,
|pane| {
conflict_split_cached_styled(
pane,
crate::view::conflict_resolver::ConflictPickSide::Ours,
ours_body_line,
)
.is_some_and(|styled| !styled.highlights.is_empty())
},
|pane| {
let split_cached = conflict_split_cached_styled(
pane,
crate::view::conflict_resolver::ConflictPickSide::Ours,
ours_body_line,
)
.map(styled_debug_info_with_styles);
format!(
"split_cached={split_cached:?} split_cache_len={} three_way_cache_len={}",
pane.conflict_diff_segments_cache_split.len(),
pane.conflict_three_way_segments_cache.len(),
)
},
);
cx.update(|_window, app| {
let pane = view.read(app).main_pane.read(app);
let styled = conflict_split_cached_styled(
&pane,
crate::view::conflict_resolver::ConflictPickSide::Ours,
ours_body_line,
)
.expect("two-way draw should cache the streamed HTML body row after switching from three-way");
assert!(
!styled.highlights.is_empty(),
"streamed two-way rows above the old 20k line gate should stay syntax highlighted after switching from three-way; got {:?}",
styled_debug_info_with_styles(styled),
);
});
cx.update(|_window, app| {
view.update(app, |this, cx| {
@ -1289,28 +1276,18 @@ fn whole_file_conflict_streamed_three_way_syntax_survives_view_mode_switch(
let _ = window.draw(app);
});
wait_for_main_pane_condition_with_timeout(
cx,
&view,
"streamed three-way HTML row cache after toggling back",
BACKGROUND_SYNTAX_MAIN_PANE_WAIT_TIMEOUT,
|pane| {
pane.conflict_three_way_segments_cache
.get(&(2, ThreeWayColumn::Ours))
.is_some_and(|styled| !styled.highlights.is_empty())
},
|pane| {
let three_way_cached = pane
.conflict_three_way_segments_cache
.get(&(2, ThreeWayColumn::Ours))
.map(styled_debug_info_with_styles);
format!(
"three_way_cached={three_way_cached:?} split_cache_len={} three_way_cache_len={}",
pane.conflict_diff_segments_cache_split.len(),
pane.conflict_three_way_segments_cache.len(),
)
},
);
cx.update(|_window, app| {
let pane = view.read(app).main_pane.read(app);
let styled = pane
.conflict_three_way_segments_cache
.get(&(2, ThreeWayColumn::Ours))
.expect("three-way draw should repopulate the streamed HTML body row cache after toggling back");
assert!(
!styled.highlights.is_empty(),
"streamed three-way rows above the old 20k line gate should stay syntax highlighted after toggling back; got {:?}",
styled_debug_info_with_styles(styled),
);
});
fixture.cleanup();
}
@ -3587,27 +3564,26 @@ fn large_conflict_resolved_output_renders_plain_text_then_upgrades_after_backgro
view.update(app, |this, cx| {
this.main_pane.update(cx, |pane, cx| {
pane.ensure_conflict_resolved_output_materialized(cx);
assert!(
pane.conflict_resolved_output_projection.is_none(),
"explicit materialization should drop the streamed projection immediately"
);
assert_eq!(
pane.conflict_resolved_preview_line_count, line_count,
"materialized preview should preserve the streamed output line count"
);
assert_eq!(
pane.conflict_resolved_preview_syntax_language,
Some(rows::DiffSyntaxLanguage::Rust),
"materialized resolved output should still keep the path-derived syntax language"
);
assert!(
pane.conflict_resolved_preview_prepared_syntax_document.is_none(),
"zero foreground budget should keep syntax preparation deferred immediately after materialization"
);
});
});
});
wait_for_main_pane_condition_with_timeout(
cx,
&view,
"large conflict resolved output materialized on demand",
BACKGROUND_SYNTAX_MAIN_PANE_WAIT_TIMEOUT,
|pane| pane.conflict_resolved_output_projection.is_none(),
|pane| {
format!(
"projection_present={} line_count={} prepared_document={:?}",
pane.conflict_resolved_output_projection.is_some(),
pane.conflict_resolved_preview_line_count,
pane.conflict_resolved_preview_prepared_syntax_document,
)
},
);
cx.update(|window, app| {
let _ = window.draw(app);
let pane = view.read(app).main_pane.read(app);
@ -3624,6 +3600,10 @@ fn large_conflict_resolved_output_renders_plain_text_then_upgrades_after_backgro
Some(rows::DiffSyntaxLanguage::Rust),
"materialized resolved output should still keep the path-derived syntax language"
);
assert!(
pane.conflict_resolved_preview_prepared_syntax_document.is_none(),
"zero foreground budget should keep syntax preparation deferred immediately after materialization"
);
let styled = pane
.conflict_resolved_preview_segments_cache_get(target_ix)
.expect("materialized output draw should populate the visible fallback row cache");
@ -3632,6 +3612,10 @@ fn large_conflict_resolved_output_renders_plain_text_then_upgrades_after_backgro
comment_line,
"materialized row cache should preserve the expected resolved-output text"
);
assert!(
styled.highlights.is_empty(),
"materialized output should still render plain text until a later background parse upgrades it"
);
});
std::fs::remove_dir_all(&workdir).expect("cleanup conflict resolver fixture");

View file

@ -46,7 +46,6 @@ fn file_preview_renders_scrollable_syntax_highlighted_rows(cx: &mut gpui::TestAp
let (view, cx) = cx.add_window_view(|window, cx| {
super::super::GitCometView::new(store, events, None, window, cx)
});
disable_view_poller_for_test(cx, &view);
let repo_id = gitcomet_state::model::RepoId(1);
let workdir = std::env::temp_dir().join(format!("gitcomet_ui_test_{}", std::process::id()));
@ -105,40 +104,32 @@ fn file_preview_renders_scrollable_syntax_highlighted_rows(cx: &mut gpui::TestAp
let _ = window.draw(app);
});
wait_for_main_pane_condition_with_timeout(
cx,
&view,
"file preview first visible row syntax cache",
BACKGROUND_SYNTAX_MAIN_PANE_WAIT_TIMEOUT,
|pane| {
let max_offset = pane
.worktree_preview_scroll
.0
.borrow()
.base_handle
.max_offset();
max_offset.height > px(0.0)
&& max_offset.width > px(0.0)
&& pane
.worktree_preview_segments_cache_get(0)
.is_some_and(|styled| !styled.highlights.is_empty())
},
|pane| {
let max_offset = pane
.worktree_preview_scroll
.0
.borrow()
.base_handle
.max_offset();
let row_cache = pane
.worktree_preview_segments_cache_get(0)
.map(styled_debug_info_with_styles);
format!(
"max_offset={max_offset:?} style_epoch={} cache_path={:?} row_cache={row_cache:?}",
pane.worktree_preview_style_cache_epoch, pane.worktree_preview_segments_cache_path,
)
},
);
cx.update(|_window, app| {
let main_pane = view.read(app).main_pane.clone();
let pane = main_pane.read(app);
let max_offset = pane
.worktree_preview_scroll
.0
.borrow()
.base_handle
.max_offset();
assert!(
max_offset.height > px(0.0),
"expected file preview to overflow and be scrollable"
);
assert!(
max_offset.width > px(0.0),
"expected file preview to overflow horizontally"
);
let Some(styled) = pane.worktree_preview_segments_cache_get(0) else {
panic!("expected first visible preview row to populate segment cache");
};
assert!(
!styled.highlights.is_empty(),
"expected syntax highlighting highlights for preview row"
);
});
let _ = std::fs::remove_dir_all(&workdir);
}

View file

@ -324,333 +324,6 @@ fn ctrl_f_from_markdown_file_preview_switches_back_to_text_search(cx: &mut gpui:
std::fs::remove_dir_all(&workdir).expect("cleanup markdown preview fixture");
}
#[gpui::test]
fn split_markdown_diff_uses_preview_level_horizontal_overflow_without_local_code_scrollbar(
cx: &mut gpui::TestAppContext,
) {
let _visual_guard = lock_visual_test();
let (store, events) = AppStore::new(Arc::new(TestBackend));
let (view, cx) = cx.add_window_view(|window, cx| {
super::super::GitCometView::new(store, events, None, window, cx)
});
disable_view_poller_for_test(cx, &view);
let repo_id = gitcomet_state::model::RepoId(71);
let workdir = std::env::temp_dir().join(format!(
"gitcomet_ui_test_{}_markdown_code_block_scrollbar",
std::process::id()
));
let file_rel = std::path::PathBuf::from("docs/overflow.md");
let long_code = "0123456789".repeat(8);
let old_text = "# Guide\n\n```rust\nlet value = 1;\n}\n```\n".to_string();
let new_text = format!("# Guide\n\n```rust\n{long_code}\n}}\n```\n");
let target = gitcomet_core::domain::DiffTarget::WorkingTree {
path: file_rel.clone(),
area: gitcomet_core::domain::DiffArea::Unstaged,
};
let _ = std::fs::remove_dir_all(&workdir);
std::fs::create_dir_all(&workdir).expect("create markdown code block diff workdir");
seed_file_diff_state(
cx, &view, repo_id, &workdir, &file_rel, &old_text, &new_text,
);
wait_for_main_pane_condition(
cx,
&view,
"markdown code block diff target activation",
|pane| {
pane.active_repo()
.and_then(|repo| repo.diff_state.diff_target.clone())
== Some(target.clone())
},
|pane| {
format!(
"active_repo={:?} diff_target={:?}",
pane.active_repo().map(|repo| repo.id),
pane.active_repo()
.and_then(|repo| repo.diff_state.diff_target.clone()),
)
},
);
cx.update(|_window, app| {
view.update(app, |this, cx| {
this.main_pane.update(cx, |pane, cx| {
pane.diff_view = DiffViewMode::Split;
pane.rendered_preview_modes
.set(RenderedPreviewKind::Markdown, RenderedPreviewMode::Rendered);
pane.file_markdown_preview_cache_repo_id = Some(repo_id);
pane.file_markdown_preview_cache_rev = 1;
pane.file_markdown_preview_cache_target = Some(target.clone());
pane.file_markdown_preview = gitcomet_state::model::Loadable::Ready(Arc::new(
crate::view::markdown_preview::build_markdown_diff_preview(
&old_text, &new_text,
)
.expect("markdown diff preview with overflowing code block should parse"),
));
pane.file_markdown_preview_inflight = None;
cx.notify();
});
});
});
for _ in 0..3 {
cx.update(|window, app| {
let _ = window.draw(app);
});
cx.run_until_parked();
}
cx.update(|_window, app| {
let pane = view.read(app).main_pane.read(app);
assert!(pane.is_markdown_preview_active());
assert!(
pane.diff_split_right_scroll
.0
.borrow()
.base_handle
.max_offset()
.width
> px(0.0),
"expected split markdown preview to overflow horizontally for the 80-character code line"
);
});
assert!(
cx.debug_bounds("markdown_preview_code_block_hscrollbar")
.is_none(),
"expected overflowing markdown preview code blocks to rely on preview-level horizontal scrolling, not a local code-block scrollbar"
);
std::fs::remove_dir_all(&workdir).expect("cleanup markdown code block diff workdir");
}
#[gpui::test]
fn worktree_markdown_preview_short_code_block_shell_spans_preview_width(
cx: &mut gpui::TestAppContext,
) {
let _visual_guard = lock_visual_test();
let (store, events) = AppStore::new(Arc::new(TestBackend));
let (view, cx) = cx.add_window_view(|window, cx| {
super::super::GitCometView::new(store, events, None, window, cx)
});
disable_view_poller_for_test(cx, &view);
let repo_id = gitcomet_state::model::RepoId(72);
let workdir = std::env::temp_dir().join(format!(
"gitcomet_ui_test_{}_markdown_code_block_width",
std::process::id()
));
let file_rel = std::path::PathBuf::from("docs/snippet.md");
let abs_path = workdir.join(&file_rel);
let source = "```sh\necho hi\n```\n";
let preview_lines = Arc::new(source.lines().map(ToOwned::to_owned).collect::<Vec<_>>());
let target = gitcomet_core::domain::DiffTarget::WorkingTree {
path: file_rel.clone(),
area: gitcomet_core::domain::DiffArea::Unstaged,
};
let _ = std::fs::remove_dir_all(&workdir);
std::fs::create_dir_all(abs_path.parent().expect("fixture parent dir"))
.expect("create markdown code block width workdir");
std::fs::write(&abs_path, source).expect("write markdown code block width fixture");
cx.update(|_window, app| {
view.update(app, |this, cx| {
let mut repo = opening_repo_state(repo_id, &workdir);
set_test_file_status(
&mut repo,
file_rel.clone(),
gitcomet_core::domain::FileStatusKind::Untracked,
gitcomet_core::domain::DiffArea::Unstaged,
);
let next_state = app_state_with_repo(repo, repo_id);
push_test_state(this, next_state, cx);
});
});
wait_for_main_pane_condition(
cx,
&view,
"worktree markdown code block width target activation",
|pane| {
pane.active_repo()
.and_then(|repo| repo.diff_state.diff_target.clone())
== Some(target.clone())
},
|pane| {
format!(
"active_repo={:?} diff_target={:?}",
pane.active_repo().map(|repo| repo.id),
pane.active_repo()
.and_then(|repo| repo.diff_state.diff_target.clone()),
)
},
);
cx.update(|_window, app| {
view.update(app, |this, cx| {
this.main_pane.update(cx, |pane, cx| {
set_ready_worktree_preview(
pane,
abs_path.clone(),
Arc::clone(&preview_lines),
source.len(),
cx,
);
pane.rendered_preview_modes
.set(RenderedPreviewKind::Markdown, RenderedPreviewMode::Rendered);
pane.worktree_markdown_preview_path = Some(abs_path.clone());
pane.worktree_markdown_preview_source_rev = pane.worktree_preview_content_rev;
pane.worktree_markdown_preview = gitcomet_state::model::Loadable::Ready(Arc::new(
crate::view::markdown_preview::parse_markdown(source)
.expect("short fenced markdown preview should parse"),
));
pane.worktree_markdown_preview_inflight = None;
cx.notify();
});
});
});
for _ in 0..3 {
cx.update(|window, app| {
let _ = window.draw(app);
});
cx.run_until_parked();
}
let container_bounds = cx
.debug_bounds("worktree_markdown_preview_scroll_container")
.expect("expected worktree markdown preview container bounds");
let code_shell_bounds = cx
.debug_bounds("markdown_preview_code_shell_0")
.expect("expected code shell bounds for the first markdown preview row");
let width_ratio = code_shell_bounds.size.width / container_bounds.size.width;
assert!(
width_ratio >= 0.95,
"expected short fenced code block shell to span preview width; ratio={width_ratio}, shell={code_shell_bounds:?}, container={container_bounds:?}"
);
std::fs::remove_dir_all(&workdir).expect("cleanup markdown code block width workdir");
}
#[gpui::test]
fn worktree_markdown_preview_list_text_box_stays_shorter_than_row_shell(
cx: &mut gpui::TestAppContext,
) {
let _visual_guard = lock_visual_test();
let (store, events) = AppStore::new(Arc::new(TestBackend));
let (view, cx) = cx.add_window_view(|window, cx| {
super::super::GitCometView::new(store, events, None, window, cx)
});
disable_view_poller_for_test(cx, &view);
let repo_id = gitcomet_state::model::RepoId(73);
let workdir = std::env::temp_dir().join(format!(
"gitcomet_ui_test_{}_markdown_list_selection_box",
std::process::id()
));
let file_rel = std::path::PathBuf::from("docs/list.md");
let abs_path = workdir.join(&file_rel);
let source = "- first item\n";
let preview_lines = Arc::new(source.lines().map(ToOwned::to_owned).collect::<Vec<_>>());
let target = gitcomet_core::domain::DiffTarget::WorkingTree {
path: file_rel.clone(),
area: gitcomet_core::domain::DiffArea::Unstaged,
};
let _ = std::fs::remove_dir_all(&workdir);
std::fs::create_dir_all(abs_path.parent().expect("fixture parent dir"))
.expect("create markdown list workdir");
std::fs::write(&abs_path, source).expect("write markdown list fixture");
cx.update(|_window, app| {
view.update(app, |this, cx| {
let mut repo = opening_repo_state(repo_id, &workdir);
set_test_file_status(
&mut repo,
file_rel.clone(),
gitcomet_core::domain::FileStatusKind::Untracked,
gitcomet_core::domain::DiffArea::Unstaged,
);
let next_state = app_state_with_repo(repo, repo_id);
push_test_state(this, next_state, cx);
});
});
wait_for_main_pane_condition(
cx,
&view,
"worktree markdown list target activation",
|pane| {
pane.active_repo()
.and_then(|repo| repo.diff_state.diff_target.clone())
== Some(target.clone())
},
|pane| {
format!(
"active_repo={:?} diff_target={:?}",
pane.active_repo().map(|repo| repo.id),
pane.active_repo()
.and_then(|repo| repo.diff_state.diff_target.clone()),
)
},
);
cx.update(|_window, app| {
view.update(app, |this, cx| {
this.main_pane.update(cx, |pane, cx| {
set_ready_worktree_preview(
pane,
abs_path.clone(),
Arc::clone(&preview_lines),
source.len(),
cx,
);
pane.rendered_preview_modes
.set(RenderedPreviewKind::Markdown, RenderedPreviewMode::Rendered);
pane.worktree_markdown_preview_path = Some(abs_path.clone());
pane.worktree_markdown_preview_source_rev = pane.worktree_preview_content_rev;
pane.worktree_markdown_preview = gitcomet_state::model::Loadable::Ready(Arc::new(
crate::view::markdown_preview::parse_markdown(source)
.expect("markdown list preview should parse"),
));
pane.worktree_markdown_preview_inflight = None;
cx.notify();
});
});
});
for _ in 0..3 {
cx.update(|window, app| {
let _ = window.draw(app);
});
cx.run_until_parked();
}
let row_bounds = cx
.debug_bounds("markdown_preview_row_box_0")
.expect("expected list row shell bounds");
let text_bounds = cx
.debug_bounds("markdown_preview_text_box_0")
.expect("expected list row text box bounds");
assert!(
text_bounds.size.height < row_bounds.size.height,
"expected markdown list text box to stay shorter than its row shell so selection matches the text height; text={text_bounds:?}, row={row_bounds:?}"
);
assert!(
row_bounds.size.height <= text_bounds.size.height + px(12.0),
"expected markdown list rows to keep only a small vertical gap around the text; text={text_bounds:?}, row={row_bounds:?}"
);
std::fs::remove_dir_all(&workdir).expect("cleanup markdown list fixture");
}
#[gpui::test]
fn ctrl_f_from_conflict_markdown_preview_switches_back_to_text_search(
cx: &mut gpui::TestAppContext,

View file

@ -1045,6 +1045,8 @@ impl MarkdownPreviewFixture {
theme: self.theme,
bar_color: None,
min_width: px(0.0),
row_id_prefix: "benchmark_markdown_preview",
horizontal_scroll_handle: None,
view: None,
text_region: DiffTextRegion::Inline,
},

View file

@ -154,36 +154,32 @@ fn render_conflict_markdown_preview_rows(
return Vec::new();
};
let document = Arc::clone(document);
let viewport_width = match side {
ThreeWayColumn::Base => {
let (row_id_prefix, horizontal_scroll_handle) = match side {
ThreeWayColumn::Base => (
"conflict_markdown_preview_base",
this.conflict_resolver_diff_scroll
.0
.borrow()
.base_handle
.bounds()
.size
.width
}
ThreeWayColumn::Ours => {
.clone(),
),
ThreeWayColumn::Ours => (
"conflict_markdown_preview_ours",
this.conflict_preview_ours_scroll
.0
.borrow()
.base_handle
.bounds()
.size
.width
}
ThreeWayColumn::Theirs => {
.clone(),
),
ThreeWayColumn::Theirs => (
"conflict_markdown_preview_theirs",
this.conflict_preview_theirs_scroll
.0
.borrow()
.base_handle
.bounds()
.size
.width
}
}
.max(px(0.0));
.clone(),
),
};
this.update_markdown_preview_horizontal_min_width(
document.as_ref(),
range.clone(),
@ -197,7 +193,9 @@ fn render_conflict_markdown_preview_rows(
&super::history::MarkdownPreviewRenderContext {
theme,
bar_color: None,
min_width: this.diff_horizontal_min_width.max(viewport_width),
min_width: this.diff_horizontal_min_width,
row_id_prefix,
horizontal_scroll_handle: Some(horizontal_scroll_handle),
view: None,
text_region: DiffTextRegion::Inline,
},

View file

@ -0,0 +1,122 @@
; CSS highlights query — adapted from Zed (crates/languages/src/css/highlights.scm)
; Provenance: Zed editor project, Apache-2.0 / MIT licensed
(comment) @comment
[
(tag_name)
(nesting_selector)
(universal_selector)
] @tag
[
"~"
">"
"+"
"-"
"|"
"*"
"/"
"="
"^="
"|="
"~="
"$="
"*="
] @operator
[
"and"
"or"
"not"
"only"
] @keyword.operator
(id_name) @selector.id
(class_name) @selector.class
(namespace_name) @namespace
(namespace_selector
(tag_name) @namespace
"|")
(attribute_name) @attribute
(pseudo_element_selector
"::"
(tag_name) @selector.pseudo)
(pseudo_class_selector
":"
(class_name) @selector.pseudo)
[
(feature_name)
(property_name)
] @property
(function_name) @function
[
(plain_value)
(keyframes_name)
(keyword_query)
] @constant.builtin
(attribute_selector
(plain_value) @string)
(parenthesized_query
(keyword_query) @property)
([
(property_name)
(plain_value)
] @variable
(#match? @variable "^--"))
[
"@media"
"@import"
"@charset"
"@namespace"
"@supports"
"@keyframes"
(at_keyword)
(to)
(from)
(important)
] @keyword
(string_value) @string
(color_value) @string.special
[
(integer_value)
(float_value)
] @number
(unit) @type
[
","
":"
"."
"::"
";"
] @punctuation.delimiter
(id_selector
"#" @punctuation.delimiter)
[
"{"
")"
"("
"}"
"["
"]"
] @punctuation.bracket

View file

@ -1,17 +1,27 @@
; Derived from
; gpui-component/crates/ui/src/highlighter/languages/html/highlights.scm
; (Apache-2.0).
; Vendored from Zed (zed/extensions/html/languages/html/highlights.scm)
(tag_name) @tag
(erroneous_end_tag_name) @tag.error
(doctype) @constant
(doctype) @tag.doctype
(attribute_name) @attribute
(attribute_value) @string
[
"\""
"'"
(attribute_value)
] @string
(comment) @comment
(entity) @string.special
"=" @punctuation.delimiter
[
"<"
">"
"<!"
"</"
"/>"
] @punctuation.bracket

View file

@ -1,14 +1,15 @@
; Derived from
; gpui-component/crates/ui/src/highlighter/languages/html/injections.scm
; (Apache-2.0). Local additions preserve inline `style=` and `on*=` injections.
; Vendored from Zed (zed/extensions/html/languages/html/injections.scm)
((script_element
(raw_text) @injection.content)
(#set! injection.language "javascript"))
((comment) @injection.content
(#set! injection.language "comment"))
((style_element
(raw_text) @injection.content)
(#set! injection.language "css"))
(script_element
(raw_text) @injection.content
(#set! injection.language "javascript"))
(style_element
(raw_text) @injection.content
(#set! injection.language "css"))
(attribute
(attribute_name) @_attribute_name

View file

@ -1,28 +1,24 @@
; Derived from
; gpui-component/crates/ui/src/highlighter/languages/javascript/highlights.scm
; (Apache-2.0). Local additions preserve parameter highlighting and JSX-in-.js
; support in GitComet diffs.
; Vendored JavaScript highlights query for tree-sitter-javascript.
; Based on upstream tree-sitter-javascript queries/highlights.scm and
; queries/highlights-jsx.scm, with keyword classification enhancements
; from Zed's JavaScript query assets.
; Variables
;----------
(identifier) @variable
; Properties
;-----------
(property_identifier) @property
(shorthand_property_identifier) @property
(shorthand_property_identifier_pattern) @property
(private_property_identifier) @property
; Function and method definitions
;--------------------------------
(function_expression
name: (identifier) @function)
(function_declaration
name: (identifier) @function)
(method_definition
name: (property_identifier) @function.method)
@ -48,8 +44,6 @@
right: [(function_expression) (arrow_function)])
; Function and method calls
;--------------------------
(call_expression
function: (identifier) @function)
@ -61,8 +55,6 @@
constructor: (identifier) @type)
; Parameters
;-----------
(arrow_function
parameter: (identifier) @variable.parameter)
@ -70,43 +62,31 @@
parameter: (identifier) @variable.parameter)
; Special identifiers
;--------------------
((identifier) @type
(#match? @type "^[A-Z]"))
([
(identifier)
(shorthand_property_identifier)
(shorthand_property_identifier_pattern)
] @constant
(#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
(#match? @constant "^[A-Z_][A-Z\\d_]+$"))
((identifier) @variable.special
(#match? @variable.special "^(arguments|module|console|window|document)$")
(#is-not? local))
((identifier) @function.special
(#eq? @function.special "require")
(#is-not? local))
((identifier) @constructor
(#match? @constructor "^[A-Z]"))
; Literals
;---------
(this) @variable.special
(super) @variable.special
[
(true)
(false)
] @boolean
[
(null)
(undefined)
] @constant.builtin
[
(true)
(false)
] @boolean
(comment) @comment
(hash_bang_line) @comment
@ -118,13 +98,11 @@
(escape_sequence) @string.escape
(regex) @string.special
(regex) @string.regex
(number) @number
; Tokens
;-------
[
";"
(optional_chain)
@ -181,6 +159,9 @@
"..."
] @operator
(regex
"/" @string.regex)
[
"("
")"
@ -196,49 +177,62 @@
":"
] @operator)
; Keywords — split into declaration / import / control for richer styling
[
"as"
"async"
"await"
"break"
"case"
"catch"
"class"
"const"
"continue"
"debugger"
"default"
"delete"
"do"
"else"
"export"
"extends"
"finally"
"for"
"from"
"function"
"get"
"if"
"import"
"in"
"instanceof"
"let"
"new"
"of"
"return"
"set"
"static"
"switch"
"target"
"typeof"
"void"
"with"
] @keyword
[
"const"
"let"
"var"
"function"
"class"
] @keyword.declaration
[
"export"
"from"
"import"
] @keyword.import
[
"break"
"case"
"catch"
"continue"
"do"
"else"
"finally"
"for"
"if"
"return"
"switch"
"throw"
"try"
"typeof"
"var"
"void"
"while"
"with"
"yield"
] @keyword
] @keyword.control
(switch_default
"default" @keyword.control)
(template_substitution
"${" @punctuation.special

View file

@ -1,6 +1,5 @@
; Derived from
; gpui-component/crates/ui/src/highlighter/languages/rust/highlights.scm
; (Apache-2.0). Local additions preserve GitComet's richer diff token classes.
; Vendored from Zed (zed/crates/languages/src/rust/highlights.scm)
; Provides richer capture semantics than tree-sitter-rust crate defaults.
(identifier) @variable
@ -33,39 +32,6 @@
(trait_bounds
(type_identifier) @type.interface)
; Identifier conventions
((identifier) @constant
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
((identifier) @type
(#match? @type "^[A-Z]"))
((scoped_identifier
path: (identifier) @type)
(#match? @type "^[A-Z]"))
((scoped_identifier
path: (scoped_identifier
name: (identifier) @type))
(#match? @type "^[A-Z]"))
((scoped_type_identifier
path: (identifier) @type)
(#match? @type "^[A-Z]"))
((scoped_type_identifier
path: (scoped_identifier
name: (identifier) @type))
(#match? @type "^[A-Z]"))
(struct_pattern
type: (scoped_type_identifier
name: (type_identifier) @type))
(enum_variant
name: (identifier) @type)
; Function calls
(call_expression
function: [
(identifier) @function
@ -84,14 +50,12 @@
field: (field_identifier) @function.method)
])
; Function definitions
(function_item
name: (identifier) @function.definition)
(function_signature_item
name: (identifier) @function.definition)
; Macros
(macro_invocation
macro: [
(identifier) @function.special
@ -105,17 +69,18 @@
(macro_definition
name: (identifier) @function.special.definition)
[
(line_comment)
(block_comment)
] @comment
; Identifier conventions
; Assume uppercase names are types/enum-constructors
((identifier) @type
(#match? @type "^[A-Z]"))
[
(line_comment
(doc_comment))
(block_comment
(doc_comment))
] @comment.doc
; Assume all-caps names are constants
((identifier) @constant
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
; Ensure enum variants are highlighted correctly regardless of naming convention
(enum_variant
name: (identifier) @type)
[
"("
@ -126,11 +91,8 @@
"]"
] @punctuation.bracket
(type_arguments
"<" @punctuation.bracket
">" @punctuation.bracket)
(type_parameters
(_
.
"<" @punctuation.bracket
">" @punctuation.bracket)
@ -206,6 +168,18 @@
(boolean_literal) @boolean
[
(line_comment)
(block_comment)
] @comment
[
(line_comment
(doc_comment))
(block_comment
(doc_comment))
] @comment.doc
[
"!="
"%"
@ -245,6 +219,7 @@
"?"
] @operator
; Avoid highlighting these as operators when used in doc comments.
(unary_expression
"!" @operator)
@ -257,6 +232,32 @@ operator: "/" @operator
(parameter
(identifier) @variable.parameter)
(attribute_item) @attribute
(attribute_item
(attribute
[
(identifier) @attribute
(scoped_identifier
name: (identifier) @attribute)
(token_tree
(identifier) @attribute
(#match? @attribute "^[a-z\\d_]*$"))
(token_tree
(identifier) @none
"::"
(#match? @none "^[a-z\\d_]*$"))
]))
(inner_attribute_item) @attribute
(inner_attribute_item
(attribute
[
(identifier) @attribute
(scoped_identifier
name: (identifier) @attribute)
(token_tree
(identifier) @attribute
(#match? @attribute "^[a-z\\d_]*$"))
(token_tree
(identifier) @none
"::"
(#match? @none "^[a-z\\d_]*$"))
]))

View file

@ -1,13 +0,0 @@
; Derived from
; gpui-component/crates/ui/src/highlighter/languages/rust/injections.scm
; (Apache-2.0).
((macro_invocation
(token_tree) @injection.content)
(#set! injection.language "rust")
(#set! injection.include-children))
((macro_rule
(token_tree) @injection.content)
(#set! injection.language "rust")
(#set! injection.include-children))

View file

@ -1,8 +1,3 @@
; Derived from
; gpui-component/crates/ui/src/highlighter/languages/typescript/highlights.scm
; (Apache-2.0). Local additions preserve JSX and richer TypeScript diff
; highlighting in GitComet.
; Variables
(identifier) @variable
@ -161,14 +156,6 @@
] @constant
(#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
((identifier) @variable.special
(#match? @variable.special "^(arguments|module|console|window|document)$")
(#is-not? local))
((identifier) @function.special
(#eq? @function.special "require")
(#is-not? local))
; Literals
(this) @variable.special

View file

@ -1,8 +1,3 @@
; Derived from
; gpui-component/crates/ui/src/highlighter/languages/typescript/highlights.scm
; (Apache-2.0). Local additions preserve richer TypeScript snippet and diff
; highlighting in GitComet.
; Variables
(identifier) @variable
@ -107,14 +102,6 @@
] @constant
(#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
((identifier) @variable.special
(#match? @variable.special "^(arguments|module|console|window|document)$")
(#is-not? local))
((identifier) @function.special
(#eq? @function.special "require")
(#is-not? local))
; Properties
(property_identifier) @property

View file

@ -25,7 +25,7 @@ const HTML_HIGHLIGHTS_QUERY: &str = include_str!("queries/html_highlights.scm");
#[cfg(feature = "syntax-web")]
const HTML_INJECTIONS_QUERY: &str = include_str!("queries/html_injections.scm");
#[cfg(feature = "syntax-web")]
const CSS_HIGHLIGHTS_QUERY: &str = tree_sitter_css::HIGHLIGHTS_QUERY;
const CSS_HIGHLIGHTS_QUERY: &str = include_str!("queries/css_highlights.scm");
#[cfg(feature = "syntax-web")]
const JAVASCRIPT_HIGHLIGHTS_QUERY: &str = include_str!("queries/javascript_highlights.scm");
#[cfg(feature = "syntax-web")]
@ -34,8 +34,6 @@ const TYPESCRIPT_HIGHLIGHTS_QUERY: &str = include_str!("queries/typescript_highl
const TSX_HIGHLIGHTS_QUERY: &str = include_str!("queries/tsx_highlights.scm");
#[cfg(feature = "syntax-rust")]
const RUST_HIGHLIGHTS_QUERY: &str = include_str!("queries/rust_highlights.scm");
#[cfg(feature = "syntax-rust")]
const RUST_INJECTIONS_QUERY: &str = include_str!("queries/rust_injections.scm");
#[cfg(feature = "syntax-xml")]
const XML_HIGHLIGHTS_QUERY: &str = tree_sitter_xml::XML_HIGHLIGHT_QUERY;
@ -1931,6 +1929,7 @@ impl TreesitterQueryAsset {
}
}
#[cfg(feature = "syntax-web")]
const fn with_injections(highlights: &'static str, injections: &'static str) -> Self {
Self {
highlights,
@ -2735,7 +2734,7 @@ fn tree_sitter_grammar(
#[cfg(feature = "syntax-rust")]
DiffSyntaxLanguage::Rust => Some((
tree_sitter_rust::LANGUAGE.into(),
TreesitterQueryAsset::with_injections(RUST_HIGHLIGHTS_QUERY, RUST_INJECTIONS_QUERY),
TreesitterQueryAsset::highlights(RUST_HIGHLIGHTS_QUERY),
)),
#[cfg(feature = "syntax-python")]
DiffSyntaxLanguage::Python => Some((

View file

@ -105,15 +105,7 @@ impl MainPaneView {
};
let document = Arc::clone(document);
let bar_color = worktree_preview_bar_color(this, theme);
let viewport_width = this
.worktree_preview_scroll
.0
.borrow()
.base_handle
.bounds()
.size
.width
.max(px(0.0));
let horizontal_scroll_handle = this.worktree_preview_scroll.0.borrow().base_handle.clone();
this.update_markdown_preview_horizontal_min_width(
document.as_ref(),
range.clone(),
@ -127,7 +119,9 @@ impl MainPaneView {
&MarkdownPreviewRenderContext {
theme,
bar_color,
min_width: this.diff_horizontal_min_width.max(viewport_width),
min_width: this.diff_horizontal_min_width,
row_id_prefix: "worktree_markdown_preview",
horizontal_scroll_handle: Some(horizontal_scroll_handle),
view: Some(cx.entity().clone()),
text_region: DiffTextRegion::Inline,
},
@ -145,15 +139,7 @@ impl MainPaneView {
return Vec::new();
};
let preview = Arc::clone(preview);
let viewport_width = this
.diff_scroll
.0
.borrow()
.base_handle
.bounds()
.size
.width
.max(px(0.0));
let horizontal_scroll_handle = this.diff_scroll.0.borrow().base_handle.clone();
this.update_markdown_preview_horizontal_min_width(
&preview.old,
range.clone(),
@ -171,7 +157,9 @@ impl MainPaneView {
&MarkdownPreviewRenderContext {
theme,
bar_color: None,
min_width: this.diff_horizontal_min_width.max(viewport_width),
min_width: this.diff_horizontal_min_width,
row_id_prefix: "diff_markdown_preview_left",
horizontal_scroll_handle: Some(horizontal_scroll_handle),
view: Some(cx.entity().clone()),
text_region: region,
},
@ -189,15 +177,7 @@ impl MainPaneView {
return Vec::new();
};
let preview = Arc::clone(preview);
let viewport_width = this
.diff_scroll
.0
.borrow()
.base_handle
.bounds()
.size
.width
.max(px(0.0));
let horizontal_scroll_handle = this.diff_scroll.0.borrow().base_handle.clone();
this.update_markdown_preview_horizontal_min_width(
&preview.inline,
range.clone(),
@ -211,7 +191,9 @@ impl MainPaneView {
&MarkdownPreviewRenderContext {
theme,
bar_color: None,
min_width: this.diff_horizontal_min_width.max(viewport_width),
min_width: this.diff_horizontal_min_width,
row_id_prefix: "diff_markdown_preview_inline",
horizontal_scroll_handle: Some(horizontal_scroll_handle),
view: Some(cx.entity().clone()),
text_region: DiffTextRegion::Inline,
},
@ -229,15 +211,7 @@ impl MainPaneView {
return Vec::new();
};
let preview = Arc::clone(preview);
let viewport_width = this
.diff_split_right_scroll
.0
.borrow()
.base_handle
.bounds()
.size
.width
.max(px(0.0));
let horizontal_scroll_handle = this.diff_split_right_scroll.0.borrow().base_handle.clone();
this.update_markdown_preview_horizontal_min_width(
&preview.new,
range.clone(),
@ -251,7 +225,9 @@ impl MainPaneView {
&MarkdownPreviewRenderContext {
theme,
bar_color: None,
min_width: this.diff_horizontal_min_width.max(viewport_width),
min_width: this.diff_horizontal_min_width,
row_id_prefix: "diff_markdown_preview_right",
horizontal_scroll_handle: Some(horizontal_scroll_handle),
view: Some(cx.entity().clone()),
text_region: DiffTextRegion::SplitRight,
},
@ -281,11 +257,10 @@ impl MainPaneView {
}
}
const MARKDOWN_PREVIEW_ROW_HEIGHT_PX: f32 = 28.0;
const MARKDOWN_PREVIEW_ROW_HEIGHT_PX: f32 = 44.0;
const MARKDOWN_PREVIEW_BASE_FONT_PX: f32 = 13.0;
const MARKDOWN_PREVIEW_BASE_LINE_HEIGHT_PX: f32 = 20.0;
const MARKDOWN_PREVIEW_BASE_LINE_HEIGHT_PX: f32 = 22.0;
const MARKDOWN_PREVIEW_CONTENT_PAD_X_PX: f32 = 18.0;
const MARKDOWN_PREVIEW_BOXED_EDGE_GAP_PX: f32 = 8.0;
const MARKDOWN_PREVIEW_INDENT_STEP_PX: f32 = 24.0;
const MARKDOWN_PREVIEW_CHANGE_BAR_WIDTH_PX: f32 = 3.0;
const MARKDOWN_PREVIEW_BLOCKQUOTE_BAR_WIDTH_PX: f32 = 4.0;
@ -298,6 +273,7 @@ const MARKDOWN_PREVIEW_ALERT_BADGE_PAD_X_PX: f32 = 6.0;
const MARKDOWN_PREVIEW_ALERT_BADGE_GAP_PX: f32 = 10.0;
const MARKDOWN_PREVIEW_SHELL_PAD_X_PX: f32 = 12.0;
const MARKDOWN_PREVIEW_CODE_BORDER_PX: f32 = 1.0;
const MARKDOWN_PREVIEW_CODE_SCROLLBAR_PAD_BOTTOM_PX: f32 = 16.0;
struct MarkdownPreviewRowTypography {
font_size: f32,
@ -311,6 +287,7 @@ struct MarkdownPreviewRowTypography {
struct MarkdownPreviewRowLayout {
top_inset_px: f32,
bottom_inset_px: f32,
shell_bottom_inset_px: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
@ -323,6 +300,8 @@ pub(super) struct MarkdownPreviewRenderContext {
pub(super) theme: AppTheme,
pub(super) bar_color: Option<gpui::Rgba>,
pub(super) min_width: Pixels,
pub(super) row_id_prefix: &'static str,
pub(super) horizontal_scroll_handle: Option<gpui::ScrollHandle>,
pub(super) view: Option<Entity<MainPaneView>>,
pub(super) text_region: DiffTextRegion,
}
@ -355,6 +334,7 @@ fn markdown_preview_row_element(
let theme = context.theme;
let bar_color = context.bar_color;
let min_width = context.min_width;
let row_id_prefix = context.row_id_prefix;
let text_region = context.text_region;
let _perf_scope = perf::span(ViewPerfSpan::MarkdownPreviewStyledRowBuild);
if matches!(row.kind, MarkdownPreviewRowKind::Spacer) {
@ -362,7 +342,7 @@ fn markdown_preview_row_element(
.relative()
.h(px(MARKDOWN_PREVIEW_ROW_HEIGHT_PX))
.min_h(px(MARKDOWN_PREVIEW_ROW_HEIGHT_PX))
.w(min_width)
.w_full()
.min_w(min_width)
.into_any_element();
}
@ -371,23 +351,18 @@ fn markdown_preview_row_element(
let typography = markdown_preview_row_typography(theme, row);
let (display, highlights) = markdown_preview_display_and_highlights(theme, row);
let horizontal_padding = markdown_preview_row_horizontal_padding(row);
let row_text = row.text.clone();
let mut content = div()
.relative()
.flex_grow()
.flex_1()
.min_w(px(0.0))
.w_full()
.h(px(typography.line_height))
.min_h(px(typography.line_height))
.h_full()
.flex()
.items_center()
.overflow_hidden()
.whitespace_nowrap()
.text_size(px(typography.font_size))
.line_height(px(typography.line_height))
.text_color(typography.text_color)
.debug_selector(|| format!("markdown_preview_text_box_{row_ix}"));
.text_color(typography.text_color);
if let Some(font_weight) = typography.font_weight {
content = content.font_weight(font_weight);
@ -395,26 +370,10 @@ fn markdown_preview_row_element(
if let Some(font_family) = typography.font_family {
content = content.font_family(font_family);
}
if let Some(view) = context.view.clone() {
content = content.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(DiffTextSelectionOverlay {
view,
visible_ix: row_ix,
region: text_region,
text: row_text.clone(),
}),
);
}
let body = match row.kind {
MarkdownPreviewRowKind::ThematicBreak => div()
.flex_grow()
.flex_1()
.min_w(px(0.0))
.w_full()
.h_full()
@ -435,7 +394,7 @@ fn markdown_preview_row_element(
};
let mut line = div()
.flex_grow()
.flex_1()
.min_w(px(0.0))
.w_full()
.h_full()
@ -481,7 +440,7 @@ fn markdown_preview_row_element(
};
let mut content_shell = div()
.flex_grow()
.flex_1()
.min_w(px(0.0))
.w_full()
.h_full()
@ -508,7 +467,7 @@ fn markdown_preview_row_element(
shell = shell.border_t_1();
}
if is_last {
shell = shell.border_b_1();
shell = shell.border_b_1().pb(px(row_layout.shell_bottom_inset_px));
}
shell
}
@ -540,20 +499,21 @@ fn markdown_preview_row_element(
_ => content_shell,
};
content_shell = content_shell.child(body);
if matches!(row.kind, MarkdownPreviewRowKind::CodeLine { .. }) {
content_shell =
content_shell.debug_selector(|| format!("markdown_preview_code_shell_{row_ix}"));
if matches!(
row.kind,
MarkdownPreviewRowKind::CodeLine { is_last: true, .. }
) && row.code_block_horizontal_scroll_hint
&& let Some(scroll_handle) = context.horizontal_scroll_handle.clone()
{
content_shell = content_shell.child(
components::Scrollbar::horizontal((row_id_prefix, row_ix), scroll_handle).render(theme),
);
}
let row_content_width = if bar_color.is_some() {
(min_width - px(MARKDOWN_PREVIEW_CHANGE_BAR_WIDTH_PX)).max(px(0.0))
} else {
min_width
};
let mut row_content = div()
.flex_grow()
.flex_1()
.min_w(px(0.0))
.w(row_content_width)
.w_full()
.h_full()
.flex()
.items_center()
@ -566,15 +526,16 @@ fn markdown_preview_row_element(
}
row_content = row_content.child(content_shell);
let row_text = row.text.clone();
if let Some(view) = context.view.clone() {
// Interactive markdown preview row with text selection + context menu.
let row_container = div()
div()
.id(("md_preview_row", row_ix))
.debug_selector(|| format!("markdown_preview_row_box_{row_ix}"))
.relative()
.h(px(MARKDOWN_PREVIEW_ROW_HEIGHT_PX))
.min_h(px(MARKDOWN_PREVIEW_ROW_HEIGHT_PX))
.w(min_width)
.w_full()
.flex()
.items_center()
.pt(px(row_layout.top_inset_px))
@ -591,6 +552,7 @@ fn markdown_preview_row_element(
)
})
.min_w(min_width)
.child(row_content)
.on_mouse_down(gpui::MouseButton::Left, {
let view = view.clone();
move |event, window, cx| {
@ -627,16 +589,20 @@ fn markdown_preview_row_element(
});
}
})
.child(row_content);
row_container.into_any_element()
.child(DiffTextSelectionOverlay {
view,
visible_ix: row_ix,
region: text_region,
text: row_text,
})
.into_any_element()
} else {
// Non-interactive markdown preview row (benchmarks, conflict resolver).
let row_container = div()
.debug_selector(|| format!("markdown_preview_row_box_{row_ix}"))
div()
.relative()
.h(px(MARKDOWN_PREVIEW_ROW_HEIGHT_PX))
.min_h(px(MARKDOWN_PREVIEW_ROW_HEIGHT_PX))
.w(min_width)
.w_full()
.flex()
.items_center()
.pt(px(row_layout.top_inset_px))
@ -653,8 +619,8 @@ fn markdown_preview_row_element(
)
})
.min_w(min_width)
.child(row_content);
row_container.into_any_element()
.child(row_content)
.into_any_element()
}
}
@ -1010,49 +976,59 @@ fn markdown_preview_row_text_color(theme: AppTheme, row: &MarkdownPreviewRow) ->
fn markdown_preview_row_layout(row: &MarkdownPreviewRow) -> MarkdownPreviewRowLayout {
match row.kind {
MarkdownPreviewRowKind::Heading { level: 1 | 2 } => MarkdownPreviewRowLayout {
top_inset_px: 0.0,
bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::Heading { level: 3 } => MarkdownPreviewRowLayout {
top_inset_px: 2.0,
bottom_inset_px: 4.0,
top_inset_px: 4.0,
bottom_inset_px: 8.0,
shell_bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::Heading { .. } => MarkdownPreviewRowLayout {
top_inset_px: 2.0,
bottom_inset_px: 6.0,
top_inset_px: 3.0,
bottom_inset_px: 7.0,
shell_bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::DetailsSummary => MarkdownPreviewRowLayout {
top_inset_px: 0.0,
bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::Paragraph => MarkdownPreviewRowLayout {
top_inset_px: 2.0,
bottom_inset_px: 6.0,
shell_bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::Paragraph => MarkdownPreviewRowLayout {
top_inset_px: 3.0,
bottom_inset_px: 7.0,
shell_bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::BlockquoteLine => MarkdownPreviewRowLayout {
top_inset_px: 2.0,
bottom_inset_px: 6.0,
shell_bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::ListItem { .. } => MarkdownPreviewRowLayout {
top_inset_px: 0.0,
bottom_inset_px: 0.0,
shell_bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::CodeLine { is_first, is_last } => MarkdownPreviewRowLayout {
top_inset_px: if is_first { 5.0 } else { 0.0 },
bottom_inset_px: if is_last { 5.0 } else { 0.0 },
top_inset_px: if is_first { 4.0 } else { 0.0 },
bottom_inset_px: if is_last { 4.0 } else { 0.0 },
shell_bottom_inset_px: if is_last {
MARKDOWN_PREVIEW_CODE_SCROLLBAR_PAD_BOTTOM_PX
} else {
0.0
},
},
MarkdownPreviewRowKind::ThematicBreak => MarkdownPreviewRowLayout {
top_inset_px: 6.0,
bottom_inset_px: 6.0,
shell_bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::Spacer => MarkdownPreviewRowLayout {
top_inset_px: 0.0,
bottom_inset_px: 0.0,
shell_bottom_inset_px: 0.0,
},
MarkdownPreviewRowKind::TableRow { .. } | MarkdownPreviewRowKind::PlainFallback => {
MarkdownPreviewRowLayout {
top_inset_px: 2.0,
bottom_inset_px: 2.0,
shell_bottom_inset_px: 0.0,
}
}
}
@ -1066,77 +1042,77 @@ fn markdown_preview_row_typography(
match row.kind {
MarkdownPreviewRowKind::Heading { level: 1 } => MarkdownPreviewRowTypography {
font_size: 28.0,
line_height: 28.0,
line_height: 32.0,
font_weight: Some(FontWeight::BOLD),
font_family: None,
text_color,
},
MarkdownPreviewRowKind::Heading { level: 2 } => MarkdownPreviewRowTypography {
font_size: 24.0,
line_height: 24.0,
line_height: 28.0,
font_weight: Some(FontWeight::BOLD),
font_family: None,
text_color,
},
MarkdownPreviewRowKind::Heading { level: 3 } => MarkdownPreviewRowTypography {
font_size: 20.0,
line_height: 22.0,
line_height: 24.0,
font_weight: Some(FontWeight::BOLD),
font_family: None,
text_color,
},
MarkdownPreviewRowKind::Heading { level: 4 } => MarkdownPreviewRowTypography {
font_size: 18.0,
line_height: 20.0,
line_height: 22.0,
font_weight: Some(FontWeight::BOLD),
font_family: None,
text_color,
},
MarkdownPreviewRowKind::Heading { level: 5 } => MarkdownPreviewRowTypography {
font_size: 16.0,
line_height: 18.0,
line_height: 20.0,
font_weight: Some(FontWeight::BOLD),
font_family: None,
text_color,
},
MarkdownPreviewRowKind::Heading { level: 6 } => MarkdownPreviewRowTypography {
font_size: 14.0,
line_height: 16.0,
line_height: 18.0,
font_weight: Some(FontWeight::BOLD),
font_family: None,
text_color,
},
MarkdownPreviewRowKind::DetailsSummary => MarkdownPreviewRowTypography {
font_size: MARKDOWN_PREVIEW_BASE_FONT_PX,
line_height: 28.0,
line_height: 32.0,
font_weight: Some(FontWeight::BOLD),
font_family: None,
text_color,
},
MarkdownPreviewRowKind::ListItem { .. } => MarkdownPreviewRowTypography {
font_size: MARKDOWN_PREVIEW_BASE_FONT_PX,
line_height: MARKDOWN_PREVIEW_BASE_LINE_HEIGHT_PX,
line_height: 36.0,
font_weight: None,
font_family: None,
text_color,
},
MarkdownPreviewRowKind::CodeLine { .. } => MarkdownPreviewRowTypography {
font_size: 12.0,
line_height: 18.0,
line_height: 20.0,
font_weight: None,
font_family: Some(UI_MONOSPACE_FONT_FAMILY),
text_color,
},
MarkdownPreviewRowKind::TableRow { is_header } => MarkdownPreviewRowTypography {
font_size: 12.0,
line_height: 18.0,
line_height: 20.0,
font_weight: is_header.then_some(FontWeight::BOLD),
font_family: Some(UI_MONOSPACE_FONT_FAMILY),
text_color,
},
MarkdownPreviewRowKind::PlainFallback => MarkdownPreviewRowTypography {
font_size: 12.0,
line_height: 18.0,
line_height: 20.0,
font_weight: None,
font_family: Some(UI_MONOSPACE_FONT_FAMILY),
text_color,
@ -1167,11 +1143,15 @@ fn markdown_preview_row_horizontal_padding(
MARKDOWN_PREVIEW_CONTENT_PAD_X_PX + indent_steps * MARKDOWN_PREVIEW_INDENT_STEP_PX;
match row.kind {
MarkdownPreviewRowKind::CodeLine { .. } if row.indent_level == 0 => {
MarkdownPreviewRowHorizontalPadding {
left_px: 0.0,
right_px: 0.0,
}
}
MarkdownPreviewRowKind::CodeLine { .. } => MarkdownPreviewRowHorizontalPadding {
// Fenced code blocks ignore surrounding list indentation but keep
// a small edge gap so the boxed shell does not touch the preview edge.
left_px: MARKDOWN_PREVIEW_BOXED_EDGE_GAP_PX,
right_px: MARKDOWN_PREVIEW_BOXED_EDGE_GAP_PX,
left_px: default_left_px,
right_px: 0.0,
},
_ => MarkdownPreviewRowHorizontalPadding {
left_px: default_left_px,
@ -1815,7 +1795,7 @@ mod tests {
}
#[test]
fn markdown_preview_list_rows_match_body_line_height_and_keep_tighter_layout() {
fn markdown_preview_list_rows_tighten_line_height_relative_to_paragraphs() {
let theme = AppTheme::zed_one_light();
let paragraph = markdown_row(MarkdownPreviewRowKind::Paragraph);
let list_item = markdown_row(MarkdownPreviewRowKind::ListItem { number: None });
@ -1825,10 +1805,7 @@ mod tests {
let paragraph_layout = markdown_preview_row_layout(&paragraph);
let list_layout = markdown_preview_row_layout(&list_item);
assert_eq!(
list_typography.line_height,
paragraph_typography.line_height
);
assert!(list_typography.line_height > paragraph_typography.line_height);
assert!(paragraph_layout.bottom_inset_px > list_layout.bottom_inset_px);
}
@ -1849,35 +1826,33 @@ mod tests {
}
#[test]
fn markdown_preview_code_rows_do_not_reserve_bottom_space_for_local_scrollbar() {
let first_row = markdown_row(MarkdownPreviewRowKind::CodeLine {
is_first: true,
is_last: false,
});
let last_row = markdown_row(MarkdownPreviewRowKind::CodeLine {
fn markdown_preview_code_rows_reserve_bottom_space_for_local_scrollbar() {
let row = markdown_row(MarkdownPreviewRowKind::CodeLine {
is_first: false,
is_last: true,
});
let first_layout = markdown_preview_row_layout(&first_row);
let last_layout = markdown_preview_row_layout(&last_row);
let layout = markdown_preview_row_layout(&row);
assert_eq!(first_layout.top_inset_px, 5.0);
assert_eq!(last_layout.bottom_inset_px, 5.0);
assert_eq!(
layout.shell_bottom_inset_px,
super::MARKDOWN_PREVIEW_CODE_SCROLLBAR_PAD_BOTTOM_PX
);
assert_eq!(layout.bottom_inset_px, 4.0);
}
#[test]
fn markdown_preview_nested_code_rows_keep_small_outer_edge_gap() {
fn markdown_preview_top_level_code_rows_drop_outer_horizontal_padding() {
let mut row = markdown_row(MarkdownPreviewRowKind::CodeLine {
is_first: true,
is_last: false,
});
row.indent_level = 3;
row.indent_level = 0;
let padding = markdown_preview_row_horizontal_padding(&row);
assert_eq!(padding.left_px, super::MARKDOWN_PREVIEW_BOXED_EDGE_GAP_PX);
assert_eq!(padding.right_px, super::MARKDOWN_PREVIEW_BOXED_EDGE_GAP_PX);
assert_eq!(padding.left_px, 0.0);
assert_eq!(padding.right_px, 0.0);
}
#[test]
@ -2150,6 +2125,7 @@ mod tests {
assert_eq!(layout.top_inset_px, 0.0);
assert_eq!(layout.bottom_inset_px, 0.0);
assert_eq!(layout.shell_bottom_inset_px, 0.0);
assert_eq!(markdown_preview_row_background(theme, &row), None);
assert_eq!(markdown_preview_row_marker(&row), None);
}

View file

@ -1,6 +1,6 @@
[package]
name = "gitcomet-ui"
version.workspace = true
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true

388
flatpak.md Normal file
View file

@ -0,0 +1,388 @@
# GitComet Flatpak Verification and Flathub Submission
This document covers two things:
1. How to manually verify that the GitComet Flatpak installs and works.
2. How to submit GitComet to Flathub for the first time, then switch to the repo's automated update flow.
The Flatpak app ID used in this repo is `dev.gitcomet.GitComet`.
## 1. What to verify before submission
Before opening a Flathub submission, verify all of these on a Linux machine:
- the Flatpak builds successfully from this repo
- the single-file `.flatpak` bundle installs cleanly
- the app launches from the desktop and from `flatpak run`
- the app can open a real host repository
- normal Git operations work inside the sandbox
- the desktop file, icon, and metainfo look correct
- the declared permissions match what the app really needs
If you develop on macOS or Windows, do this section in a Linux VM.
## 2. Prerequisites
Install Flatpak and `flatpak-builder` from your distro, then make sure the Flathub remote exists:
```bash
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
```
Install the Flathub-maintained builder runtime used by Flathub's docs and linter:
```bash
flatpak install --user -y flathub org.flatpak.Builder
```
## 3. Clean out any previous local test install
If you already installed an older GitComet Flatpak locally, remove it first:
```bash
flatpak uninstall --user -y dev.gitcomet.GitComet || true
rm -rf builddir repo dist/dev.gitcomet.GitComet.flatpak
```
## 4. Build the local Flatpak from this repo
From the repository root:
```bash
flatpak-builder \
--force-clean \
--user \
--install-deps-from=flathub \
--repo=repo \
--install \
builddir \
flatpak/dev.gitcomet.GitComet.local.yaml
```
What this does:
- builds the Flatpak from `flatpak/dev.gitcomet.GitComet.local.yaml`
- exports the result to a local OSTree repo at `./repo`
- installs the Flatpak into your user Flatpak installation
## 5. Build the single-file `.flatpak` bundle
This is the install format you should manually verify before submission:
```bash
mkdir -p dist
flatpak build-bundle \
repo \
dist/dev.gitcomet.GitComet.flatpak \
dev.gitcomet.GitComet \
--runtime-repo=https://dl.flathub.org/repo/flathub.flatpakrepo
```
## 6. Reinstall from the bundle
First remove the locally installed build, then install the actual bundle file:
```bash
flatpak uninstall --user -y dev.gitcomet.GitComet || true
flatpak install --user -y dist/dev.gitcomet.GitComet.flatpak
```
Confirm the installed app ID:
```bash
flatpak info dev.gitcomet.GitComet
```
Inspect the shipped permissions:
```bash
flatpak info --show-permissions dev.gitcomet.GitComet
```
Expected defaults from this repo:
- `--filesystem=host`
- `--share=network`
- `--socket=ssh-auth`
- `--socket=gpg-agent`
- `--socket=wayland`
- `--socket=fallback-x11`
- `--talk-name=org.freedesktop.Flatpak`
- `--talk-name=org.freedesktop.FileManager1`
- `--talk-name=org.freedesktop.Notifications`
## 7. Launch tests
Launch from the terminal:
```bash
flatpak run dev.gitcomet.GitComet
```
Also verify that it appears correctly in the desktop launcher:
- app name is `GitComet`
- icon is present
- the app opens normally from the graphical launcher
## 8. Functional tests inside the sandbox
Use a disposable test repo so you can verify normal flows end to end.
Create one:
```bash
mkdir -p /tmp/gitcomet-flatpak-test
cd /tmp/gitcomet-flatpak-test
git init
printf '# Flatpak test\n' > README.md
git add README.md
git commit -m "Initial commit"
```
Then verify this checklist manually in the Flatpak build:
1. Open `/tmp/gitcomet-flatpak-test` in GitComet.
2. Confirm the commit history appears.
3. Edit `README.md`, then stage and unstage the change.
4. Create a commit from inside GitComet.
5. If you use SSH remotes, add a disposable SSH remote and verify fetch/pull/push.
6. If you sign commits or tags, verify GPG agent access.
7. If you rely on difftool or mergetool paths, test at least one real conflict or diff flow.
Important things to watch for:
- no missing repository access errors
- no missing `git` binary errors
- auth prompts work
- the app is using the host Git successfully
- temp-file-based auth, merge, or diff flows work
## 9. Lint and metadata checks
Run the same basic checks Flathub expects before you submit:
```bash
flatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest flatpak/dev.gitcomet.GitComet.local.yaml
flatpak run --command=flatpak-builder-lint org.flatpak.Builder repo repo
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream flatpak/dev.gitcomet.GitComet.metainfo.xml
desktop-file-validate assets/linux/dev.gitcomet.GitComet.desktop
appstreamcli validate --no-net flatpak/dev.gitcomet.GitComet.metainfo.xml
```
Do not submit until these are clean or you explicitly know which exception you need.
## 10. Prepare the first public release assets
For the first Flathub submission, you need public release assets that Flathub can fetch.
The minimum assets needed for the Flathub PR are:
- `gitcomet-v<VERSION>-source.tar.gz`
- `dev.gitcomet.GitComet.yaml`
- `cargo-sources.json`
- `flathub.json`
### Recommended first-release path
Use a normal public GitHub release first, then use this repo's existing build workflow to attach the Flatpak/Flathub assets.
Example for version `0.2.0`:
```bash
git tag -a v0.2.0 -m "GitComet v0.2.0"
git push origin v0.2.0
gh release create v0.2.0 \
--repo Auto-Explore/GitComet \
--title "GitComet v0.2.0" \
--generate-notes
RELEASE_ID="$(gh release view v0.2.0 --repo Auto-Explore/GitComet --json id --jq '.id')"
gh workflow run build-release-artifacts.yml \
-f tag=v0.2.0 \
-f version=0.2.0 \
-f release_id="$RELEASE_ID"
```
After that workflow finishes, the GitHub release should contain:
- the Linux Flatpak bundle
- the source tarball
- the rendered Flathub manifest
- `cargo-sources.json`
- `flathub.json`
Note:
- for the very first Flathub submission, do not rely on `release-manual-main.yml` unless the Flathub app repo already exists and `FLATHUB_TOKEN` has been configured
- the app-specific Flathub repo does not exist until the initial submission is accepted
## 11. Download the submission files
Download these release assets locally:
- `dev.gitcomet.GitComet.yaml`
- `cargo-sources.json`
- `flathub.json`
You will put those into the Flathub submission PR.
Before you open the PR, lint the exact rendered manifest you are about to submit:
```bash
flatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest dev.gitcomet.GitComet.yaml
```
## 12. Open the first Flathub submission PR
New Flathub app submissions go through `flathub/flathub` and must target the `new-pr` base branch.
### Option A: with GitHub CLI
```bash
gh repo fork --clone flathub/flathub
cd flathub
git checkout --track origin/new-pr
git checkout -b gitcomet-submission
```
### Option B: manual Git setup
1. Fork `flathub/flathub` on GitHub.
2. Make sure "Copy the master branch only" is unchecked when you fork.
3. Clone your fork's `new-pr` branch:
```bash
git clone --branch=new-pr git@github.com:YOUR_GITHUB_USERNAME/flathub.git
cd flathub
git checkout -b gitcomet-submission
```
### Add the GitComet submission files
Copy these into the root of the cloned `flathub` repo:
- `dev.gitcomet.GitComet.yaml`
- `cargo-sources.json`
- `flathub.json`
Then commit and push:
```bash
git add dev.gitcomet.GitComet.yaml cargo-sources.json flathub.json
git commit -m "Add dev.gitcomet.GitComet"
git push origin gitcomet-submission
```
### Create the pull request
Open a PR with:
- base repo: `flathub/flathub`
- base branch: `new-pr`
- title: `Add dev.gitcomet.GitComet`
Do not target `master` or the default branch.
If you use GitHub CLI, this is the equivalent command:
```bash
gh pr create \
--repo flathub/flathub \
--base new-pr \
--head YOUR_GITHUB_USERNAME:gitcomet-submission \
--title "Add dev.gitcomet.GitComet"
```
## 13. Handle review
While the submission is under review:
- keep using the same PR
- push fixes to the same branch
- do not close and reopen the PR just to address comments
- do not merge the default Flathub branch into your submission branch
If reviewers ask for permission changes, metadata fixes, or linter exceptions, update the same PR.
## 14. Verify ownership of `gitcomet.dev`
Once the submission is accepted and you have collaborator access to the app repository, verify the app in the Flathub Developer Portal.
The verification path for this app ID is:
```text
https://gitcomet.dev/.well-known/org.flathub.VerifiedApps.txt
```
The process is:
1. Log in to Flathub.
2. Open the Developer Portal.
3. Open the GitComet app entry.
4. Open `Verification`.
5. Copy the generated token.
6. Publish that token at `https://gitcomet.dev/.well-known/org.flathub.VerifiedApps.txt`.
7. Retry verification in the portal.
If multiple apps are ever verified under `gitcomet.dev`, put each token on its own line.
## 15. Switch to automated updates after the first acceptance
After the first submission is accepted, Flathub will create an app-specific packaging repo, typically:
```text
flathub/dev.gitcomet.GitComet
```
At that point, configure these in the GitComet GitHub repo:
- secret `FLATHUB_TOKEN`
- optional variable `FLATHUB_REPO=flathub/dev.gitcomet.GitComet`
- optional variable `FLATHUB_BRANCH`
- optional variable `FLATHUB_MODE=pull_request`
Recommended setting:
- use `FLATHUB_MODE=pull_request` first
- only switch to `push` if you intentionally want direct commits to the Flathub app repo
This repo already has a release-time Flathub deployment workflow. Once the app repo exists and the token is configured, `release-manual-main.yml` can:
1. publish the GitHub release
2. upload the Flatpak and Flathub assets
3. open or update the Flathub packaging PR automatically
## 16. Why `flathub.json` is included
This repo ships a `flathub.json` with:
```json
{
"disable-external-data-checker": true
}
```
That is intentional.
GitComet's GitHub release flow already creates the Flathub update payload. Disabling Flathub's default hourly external-data-checker avoids duplicate update PRs.
## 17. Quick checklist
Before first submission:
- local Flatpak build passes
- bundle install passes
- app launches and works
- linter checks pass
- public GitHub release exists
- release contains `dev.gitcomet.GitComet.yaml`
- release contains `cargo-sources.json`
- release contains `flathub.json`
- submission PR targets `flathub/flathub:new-pr`
After acceptance:
- app verified with `gitcomet.dev`
- collaborator access to the app repo confirmed
- `FLATHUB_TOKEN` configured in GitHub
- `release-manual-main.yml` used for ongoing releases

11097
flatpak/cargo-sources.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,43 @@
app-id: dev.gitcomet.GitComet
runtime: org.freedesktop.Platform
runtime-version: "24.08"
sdk: org.freedesktop.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: gitcomet-app
finish-args:
- --device=dri
- --share=ipc
- --share=network
- --socket=wayland
- --socket=fallback-x11
- --socket=ssh-auth
- --socket=gpg-agent
- --filesystem=host
- --talk-name=org.freedesktop.FileManager1
- --talk-name=org.freedesktop.Flatpak
- --talk-name=org.freedesktop.Notifications
modules:
- name: gitcomet
buildsystem: simple
build-options:
append-path: /usr/lib/sdk/rust-stable/bin
env:
CARGO_HOME: /run/build/gitcomet/cargo
CARGO_NET_OFFLINE: "true"
build-commands:
- cargo --offline fetch --manifest-path Cargo.toml --locked --verbose
- cargo build --offline --release --locked -p gitcomet-app --features ui-gpui,gix
- install -Dm755 target/release/gitcomet-app ${FLATPAK_DEST}/bin/gitcomet-app
- install -Dm755 flatpak/git-wrapper.sh ${FLATPAK_DEST}/bin/git
- install -Dm644 assets/linux/dev.gitcomet.GitComet.desktop ${FLATPAK_DEST}/share/applications/dev.gitcomet.GitComet.desktop
- install -Dm644 flatpak/dev.gitcomet.GitComet.metainfo.xml ${FLATPAK_DEST}/share/metainfo/dev.gitcomet.GitComet.metainfo.xml
- install -Dm644 assets/linux/hicolor/32x32/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/32x32/apps/dev.gitcomet.GitComet.png
- install -Dm644 assets/linux/hicolor/48x48/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/48x48/apps/dev.gitcomet.GitComet.png
- install -Dm644 assets/linux/hicolor/128x128/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/128x128/apps/dev.gitcomet.GitComet.png
- install -Dm644 assets/linux/hicolor/256x256/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/256x256/apps/dev.gitcomet.GitComet.png
- install -Dm644 assets/linux/hicolor/512x512/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/512x512/apps/dev.gitcomet.GitComet.png
sources:
- type: dir
path: ..
- cargo-sources.json

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>dev.gitcomet.GitComet</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>AGPL-3.0-only</project_license>
<name>GitComet</name>
<summary>Fast, resource-efficient Git GUI written in Rust</summary>
<developer id="dev.gitcomet">
<name>AutoExplore Oy</name>
</developer>
<launchable type="desktop-id">dev.gitcomet.GitComet.desktop</launchable>
<provides>
<binary>gitcomet-app</binary>
</provides>
<description>
<p>GitComet is a fast Git GUI built for large repositories, familiar history browsing, local-first privacy, and drop-in difftool and mergetool workflows.</p>
<p>It supports staging, commits, branches, remotes, worktrees, history, side-by-side diffs, and conflict resolution.</p>
</description>
<categories>
<category>Development</category>
<category>RevisionControl</category>
</categories>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/Auto-Explore/GitComet/main/assets/gitcomet_screenshot.png</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1"/>
<url type="homepage">https://gitcomet.dev</url>
<url type="bugtracker">https://github.com/Auto-Explore/GitComet/issues</url>
<url type="vcs-browser">https://github.com/Auto-Explore/GitComet</url>
</component>

View file

@ -0,0 +1,50 @@
app-id: dev.gitcomet.GitComet
runtime: org.freedesktop.Platform
runtime-version: "24.08"
sdk: org.freedesktop.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: gitcomet-app
finish-args:
- --device=dri
- --share=ipc
- --share=network
- --socket=wayland
- --socket=fallback-x11
- --socket=ssh-auth
- --socket=gpg-agent
- --filesystem=host
- --talk-name=org.freedesktop.FileManager1
- --talk-name=org.freedesktop.Flatpak
- --talk-name=org.freedesktop.Notifications
modules:
- name: gitcomet
buildsystem: simple
build-options:
append-path: /usr/lib/sdk/rust-stable/bin
env:
CARGO_HOME: /run/build/gitcomet/cargo
CARGO_NET_OFFLINE: "true"
build-commands:
- cargo --offline fetch --manifest-path Cargo.toml --locked --verbose
- cargo build --offline --release --locked -p gitcomet-app --features ui-gpui,gix
- install -Dm755 target/release/gitcomet-app ${FLATPAK_DEST}/bin/gitcomet-app
- install -Dm755 flatpak/git-wrapper.sh ${FLATPAK_DEST}/bin/git
- install -Dm644 assets/linux/dev.gitcomet.GitComet.desktop ${FLATPAK_DEST}/share/applications/dev.gitcomet.GitComet.desktop
- install -Dm644 flatpak/dev.gitcomet.GitComet.metainfo.xml ${FLATPAK_DEST}/share/metainfo/dev.gitcomet.GitComet.metainfo.xml
- install -Dm644 assets/linux/hicolor/32x32/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/32x32/apps/dev.gitcomet.GitComet.png
- install -Dm644 assets/linux/hicolor/48x48/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/48x48/apps/dev.gitcomet.GitComet.png
- install -Dm644 assets/linux/hicolor/128x128/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/128x128/apps/dev.gitcomet.GitComet.png
- install -Dm644 assets/linux/hicolor/256x256/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/256x256/apps/dev.gitcomet.GitComet.png
- install -Dm644 assets/linux/hicolor/512x512/apps/gitcomet.png ${FLATPAK_DEST}/share/icons/hicolor/512x512/apps/dev.gitcomet.GitComet.png
sources:
- type: archive
url: @SOURCE_URL@
sha256: @SOURCE_SHA256@
strip-components: 1
x-checker-data:
type: json
url: https://api.github.com/repos/Auto-Explore/GitComet/releases/latest
version-query: .tag_name | sub("^v"; "")
url-query: '"https://github.com/Auto-Explore/GitComet/releases/download/" + .tag_name + "/gitcomet-v" + (.tag_name | sub("^v"; "")) + "-source.tar.gz"'
- cargo-sources.json

3
flatpak/flathub.json Normal file
View file

@ -0,0 +1,3 @@
{
"disable-external-data-checker": true
}

35
flatpak/git-wrapper.sh Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env sh
set -eu
host_home=""
if [ -n "${FLATPAK_ID:-}" ] && [ -n "${HOME:-}" ]; then
suffix="/.var/app/${FLATPAK_ID}"
case "$HOME" in
*"$suffix")
host_home="${HOME%$suffix}"
;;
esac
fi
set -- git "$@"
if [ -n "${host_home:-}" ]; then
set -- "HOME=${host_home}" "$@"
fi
[ -n "${HOST_XDG_CONFIG_HOME:-}" ] && set -- "XDG_CONFIG_HOME=${HOST_XDG_CONFIG_HOME}" "$@"
[ -n "${HOST_XDG_DATA_HOME:-}" ] && set -- "XDG_DATA_HOME=${HOST_XDG_DATA_HOME}" "$@"
[ -n "${HOST_XDG_CACHE_HOME:-}" ] && set -- "XDG_CACHE_HOME=${HOST_XDG_CACHE_HOME}" "$@"
[ -n "${HOST_XDG_STATE_HOME:-}" ] && set -- "XDG_STATE_HOME=${HOST_XDG_STATE_HOME}" "$@"
[ -n "${DISPLAY:-}" ] && set -- "DISPLAY=${DISPLAY}" "$@"
[ -n "${WAYLAND_DISPLAY:-}" ] && set -- "WAYLAND_DISPLAY=${WAYLAND_DISPLAY}" "$@"
[ -n "${GIT_ASKPASS:-}" ] && set -- "GIT_ASKPASS=${GIT_ASKPASS}" "$@"
[ -n "${SSH_ASKPASS:-}" ] && set -- "SSH_ASKPASS=${SSH_ASKPASS}" "$@"
[ -n "${SSH_ASKPASS_REQUIRE:-}" ] && set -- "SSH_ASKPASS_REQUIRE=${SSH_ASKPASS_REQUIRE}" "$@"
[ -n "${GIT_TERMINAL_PROMPT:-}" ] && set -- "GIT_TERMINAL_PROMPT=${GIT_TERMINAL_PROMPT}" "$@"
[ -n "${GITCOMET_ASKPASS_PROMPT_LOG:-}" ] && set -- "GITCOMET_ASKPASS_PROMPT_LOG=${GITCOMET_ASKPASS_PROMPT_LOG}" "$@"
[ -n "${GITCOMET_AUTH_KIND:-}" ] && set -- "GITCOMET_AUTH_KIND=${GITCOMET_AUTH_KIND}" "$@"
[ -n "${GITCOMET_AUTH_USERNAME:-}" ] && set -- "GITCOMET_AUTH_USERNAME=${GITCOMET_AUTH_USERNAME}" "$@"
[ -n "${GITCOMET_AUTH_SECRET:-}" ] && set -- "GITCOMET_AUTH_SECRET=${GITCOMET_AUTH_SECRET}" "$@"
exec flatpak-spawn --host env "$@"

View file

@ -199,10 +199,9 @@ write_release_checksums() {
-printf '%P\n' \
| LC_ALL=C sort
)
echo ""
}
# `Release` is parsed as a single deb822 paragraph. Blank lines would split
# the checksum stanzas into separate paragraphs and make APT ignore them.
{
echo "Origin: ${origin}"
echo "Label: ${label}"
@ -212,6 +211,7 @@ write_release_checksums() {
echo "Architectures: ${architecture}"
echo "Components: ${component}"
echo "Description: ${description}"
echo ""
write_release_checksums "MD5Sum" md5sum
write_release_checksums "SHA256" sha256sum
write_release_checksums "SHA512" sha512sum
@ -220,11 +220,12 @@ write_release_checksums() {
run_gpg() {
local -a args
args=(--batch --yes --pinentry-mode loopback --local-user "$signing_key")
args+=("$@")
if [[ -n "$gpg_passphrase" ]]; then
gpg "${args[@]}" --passphrase-fd 0 "$@" <<<"$gpg_passphrase"
gpg "${args[@]}" --passphrase-fd 0 <<<"$gpg_passphrase"
else
gpg "${args[@]}" "$@"
gpg "${args[@]}"
fi
}

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
RUSTFLAGS="-C symbol-mangling-version=v0" cargo build -p gitcomet --all-targets --profile=release-with-debug
RUSTFLAGS="-C symbol-mangling-version=v0" cargo build -p gitcomet-app --all-targets --profile=release-with-debug

View file

@ -0,0 +1,118 @@
#!/usr/bin/env python3
import argparse
import json
import tomllib
from pathlib import Path
CRATES_IO = "https://static.crates.io/crates"
CARGO_HOME = "cargo"
CARGO_CRATES = f"{CARGO_HOME}/vendor"
VENDORED_SOURCES = "vendored-sources"
CRATES_IO_SOURCE = "registry+https://github.com/rust-lang/crates.io-index"
def package_checksum(package: dict, metadata: dict) -> str:
checksum = package.get("checksum")
if checksum:
return checksum
key = (
f'checksum {package["name"]} {package["version"]} '
f'({package["source"]})'
)
checksum = metadata.get(key)
if checksum:
return checksum
raise SystemExit(
f'missing checksum for {package["name"]} {package["version"]}'
)
def cargo_config_contents() -> str:
return (
"[source.crates-io]\n"
f'replace-with = "{VENDORED_SOURCES}"\n\n'
f"[source.{VENDORED_SOURCES}]\n"
f'directory = "{CARGO_CRATES}"\n'
)
def generate_sources(lock_path: Path) -> list[dict]:
with lock_path.open("rb") as handle:
cargo_lock = tomllib.load(handle)
metadata = cargo_lock.get("metadata", {})
seen = set()
sources = []
for package in cargo_lock["package"]:
source = package.get("source")
if source is None:
continue
if source != CRATES_IO_SOURCE:
raise SystemExit(
f'unsupported non-crates.io dependency: {package["name"]} {source}'
)
key = (package["name"], package["version"])
if key in seen:
continue
seen.add(key)
checksum = package_checksum(package, metadata)
dest = f'{CARGO_CRATES}/{package["name"]}-{package["version"]}'
sources.append(
{
"type": "archive",
"archive-type": "tar-gzip",
"url": (
f'{CRATES_IO}/{package["name"]}/'
f'{package["name"]}-{package["version"]}.crate'
),
"sha256": checksum,
"dest": dest,
}
)
sources.append(
{
"type": "inline",
"contents": json.dumps({"package": checksum, "files": {}}),
"dest": dest,
"dest-filename": ".cargo-checksum.json",
}
)
sources.append(
{
"type": "inline",
"contents": cargo_config_contents(),
"dest": CARGO_HOME,
"dest-filename": "config",
}
)
return sources
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"cargo_lock",
nargs="?",
default="Cargo.lock",
help="Path to Cargo.lock",
)
parser.add_argument(
"-o",
"--output",
default="flatpak/cargo-sources.json",
help="Path to write generated cargo sources JSON",
)
args = parser.parse_args()
sources = generate_sources(Path(args.cargo_lock))
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(sources, indent=4) + "\n", encoding="utf-8")
if __name__ == "__main__":
main()

View file

@ -1,122 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: scripts/generate-homebrew-cask.sh \
--version VERSION \
--github-repo OWNER/REPO \
--arm-dmg PATH \
--intel-dmg PATH \
--output PATH
Generates a Homebrew cask for GitComet from macOS DMG artifacts.
USAGE
}
version=""
github_repo=""
arm_dmg=""
intel_dmg=""
out_path=""
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--github-repo)
github_repo="${2:-}"
shift 2
;;
--arm-dmg)
arm_dmg="${2:-}"
shift 2
;;
--intel-dmg)
intel_dmg="${2:-}"
shift 2
;;
--output)
out_path="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown arg: $1" >&2
usage
exit 2
;;
esac
done
if [[ -z "$version" || -z "$github_repo" || -z "$arm_dmg" || -z "$intel_dmg" || -z "$out_path" ]]; then
echo "All arguments are required." >&2
usage
exit 2
fi
if ! [[ "$github_repo" =~ ^[^/]+/[^/]+$ ]]; then
echo "Invalid --github-repo '$github_repo'. Expected OWNER/REPO." >&2
exit 2
fi
if [[ ! -f "$arm_dmg" ]]; then
echo "arm DMG not found: $arm_dmg" >&2
exit 1
fi
if [[ ! -f "$intel_dmg" ]]; then
echo "intel DMG not found: $intel_dmg" >&2
exit 1
fi
sha256_file() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
return
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
return
fi
echo "No SHA256 tool found (sha256sum or shasum required)." >&2
exit 1
}
arm_sha="$(sha256_file "$arm_dmg")"
intel_sha="$(sha256_file "$intel_dmg")"
mkdir -p "$(dirname "$out_path")"
cat > "$out_path" <<EOF2
cask "gitcomet" do
arch arm: "arm64", intel: "x86_64"
version "${version}"
sha256 arm: "${arm_sha}", intel: "${intel_sha}"
url "https://github.com/${github_repo}/releases/download/v#{version}/gitcomet-v#{version}-macos-#{arch}.dmg"
name "GitComet"
desc "Fast, resource-efficient Git GUI written in Rust"
homepage "https://github.com/${github_repo}"
depends_on macos: ">= :ventura"
app "GitComet.app"
caveats do
<<~EOS
Optional CLI:
brew install gitcomet-cli
EOS
end
end
EOF2
echo "Generated Homebrew cask: $out_path"

View file

@ -110,8 +110,8 @@ linux_name="$(basename "$linux_tar")"
mkdir -p "$(dirname "$out_path")"
cat > "$out_path" <<EOF2
class GitcometCli < Formula
desc "GitComet command-line binary"
class Gitcomet < Formula
desc "Fast, resource-efficient Git GUI written in Rust"
homepage "https://github.com/${github_repo}"
version "${version}"
license "AGPL-3.0-only"
@ -136,11 +136,11 @@ class GitcometCli < Formula
end
def install
bin.install "gitcomet"
bin.install Dir["**/gitcomet-app"].fetch(0)
end
test do
assert_match "Usage", shell_output("#{bin}/gitcomet --help")
assert_match "Usage", shell_output("#{bin}/gitcomet-app --help")
end
end
EOF2

View file

@ -6,9 +6,9 @@ usage() {
Usage: scripts/install-linux.sh [--release|--debug] [--prefix PATH] [--no-build]
Installs:
- binary to <prefix>/bin/gitcomet
- desktop entry to ~/.local/share/applications/gitcomet.desktop
- icons to ~/.local/share/icons/hicolor/<size>x<size>/apps/gitcomet.png
- binary to <prefix>/bin/gitcomet-app
- desktop entry to ~/.local/share/applications/dev.gitcomet.GitComet.desktop
- icons to ~/.local/share/icons/hicolor/<size>x<size>/apps/dev.gitcomet.GitComet.png
sizes: 32, 48, 128, 256, 512
Defaults:
@ -32,14 +32,14 @@ while [[ $# -gt 0 ]]; do
done
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
bin_src="${repo_root}/target/${mode}/gitcomet"
bin_src="${repo_root}/target/${mode}/gitcomet-app"
if [[ $build -eq 1 && ! -x "$bin_src" ]]; then
cargo_mode_flag=()
if [[ "$mode" == "release" ]]; then
cargo_mode_flag=(--release)
fi
(cd "$repo_root" && cargo build -p gitcomet "${cargo_mode_flag[@]}")
(cd "$repo_root" && cargo build -p gitcomet-app "${cargo_mode_flag[@]}")
fi
if [[ ! -x "$bin_src" ]]; then
@ -52,28 +52,35 @@ bindir="${prefix}/bin"
appdir="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
iconsroot="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor"
icon_sizes=(32 48 128 256 512)
desktop_file="dev.gitcomet.GitComet.desktop"
icon_name="dev.gitcomet.GitComet"
install -Dm755 "$bin_src" "${bindir}/gitcomet"
install -Dm755 "$bin_src" "${bindir}/gitcomet-app"
# Install desktop file with absolute Exec path so it works even if ~/.local/bin isn't on PATH.
tmp_desktop="$(mktemp)"
trap 'rm -f "$tmp_desktop"' EXIT
sed "s|^Exec=.*$|Exec=${bindir}/gitcomet|g" \
"${repo_root}/assets/linux/gitcomet.desktop" >"$tmp_desktop"
install -Dm644 "$tmp_desktop" "${appdir}/gitcomet.desktop"
sed "s|^Exec=.*$|Exec=${bindir}/gitcomet-app|g" \
"${repo_root}/assets/linux/${desktop_file}" >"$tmp_desktop"
install -Dm644 "$tmp_desktop" "${appdir}/${desktop_file}"
for size in "${icon_sizes[@]}"; do
install -Dm644 "${repo_root}/assets/linux/hicolor/${size}x${size}/apps/gitcomet.png" \
"${iconsroot}/${size}x${size}/apps/gitcomet.png"
"${iconsroot}/${size}x${size}/apps/${icon_name}.png"
done
rm -f "${appdir}/gitcomet.desktop"
for size in "${icon_sizes[@]}"; do
rm -f "${iconsroot}/${size}x${size}/apps/gitcomet.png"
done
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$appdir" >/dev/null 2>&1 || true
command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache "${iconsroot}" >/dev/null 2>&1 || true
echo "Installed GitComet:"
echo " ${bindir}/gitcomet"
echo " ${appdir}/gitcomet.desktop"
echo " ${bindir}/gitcomet-app"
echo " ${appdir}/${desktop_file}"
for size in "${icon_sizes[@]}"; do
echo " ${iconsroot}/${size}x${size}/apps/gitcomet.png"
echo " ${iconsroot}/${size}x${size}/apps/${icon_name}.png"
done
echo "If GNOME still shows a generic icon, log out/in (or restart GNOME Shell)."

View file

@ -94,14 +94,14 @@ if [[ "$arch" != "$host_arch" ]]; then
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
bin_src="${repo_root}/target/${mode}/gitcomet"
bin_src="${repo_root}/target/${mode}/gitcomet-app"
if [[ $build -eq 1 && ! -x "$bin_src" ]]; then
cargo_mode_flag=()
if [[ "$mode" == "release" ]]; then
cargo_mode_flag=(--release)
fi
(cd "$repo_root" && cargo build -p gitcomet "${cargo_mode_flag[@]}" --locked --features ui-gpui,gix --bins)
(cd "$repo_root" && cargo build -p gitcomet-app "${cargo_mode_flag[@]}" --locked --features ui-gpui,gix --bins)
fi
if [[ ! -x "$bin_src" ]]; then
@ -129,8 +129,8 @@ resources_dir="${contents_dir}/Resources"
rm -rf "$release_dir"
mkdir -p "$macos_dir" "$resources_dir"
install -m755 "$bin_src" "${macos_dir}/gitcomet"
install -m755 "$bin_src" "${release_dir}/gitcomet"
install -m755 "$bin_src" "${macos_dir}/gitcomet-app"
install -m755 "$bin_src" "${release_dir}/gitcomet-app"
install -m644 "${repo_root}/README.md" "${release_dir}/README.md"
install -m644 "${repo_root}/LICENSE-AGPL-3.0" "${release_dir}/LICENSE-AGPL-3.0"
install -m644 "${repo_root}/NOTICE" "${release_dir}/NOTICE"
@ -158,9 +158,9 @@ cat > "${contents_dir}/Info.plist" <<PLIST
<key>CFBundleDisplayName</key>
<string>GitComet</string>
<key>CFBundleExecutable</key>
<string>gitcomet</string>
<string>gitcomet-app</string>
<key>CFBundleIdentifier</key>
<string>ai.autoexplore.gitcomet</string>
<string>dev.gitcomet.GitComet</string>
<key>CFBundleIconFile</key>
<string>GitComet.icns</string>
<key>CFBundleInfoDictionaryVersion</key>

View file

@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: scripts/render-flathub-manifest.sh --source-url URL --source-sha256 SHA256 --output PATH [--template PATH]
EOF
}
template="flatpak/dev.gitcomet.GitComet.yaml.in"
source_url=""
source_sha256=""
output=""
while [[ $# -gt 0 ]]; do
case "$1" in
--template) template="$2"; shift 2 ;;
--source-url) source_url="$2"; shift 2 ;;
--source-sha256) source_sha256="$2"; shift 2 ;;
--output) output="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
if [[ -z "$source_url" || -z "$source_sha256" || -z "$output" ]]; then
usage
exit 2
fi
mkdir -p "$(dirname "$output")"
sed \
-e "s|@SOURCE_URL@|${source_url}|g" \
-e "s|@SOURCE_SHA256@|${source_sha256}|g" \
"$template" >"$output"

View file

@ -1 +1 @@
cargo wix crates\gitcomet\Cargo.toml -p gitcomet --profile release --nocapture --output dist\gitcomet-local-test.msi
cargo wix crates\gitcomet-app\Cargo.toml -p gitcomet-app --profile release --nocapture --output dist\gitcomet-local-test.msi

View file

@ -17,11 +17,17 @@ bindir="${prefix}/bin"
appdir="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
iconsroot="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor"
icon_sizes=(32 48 128 256 512)
desktop_files=("dev.gitcomet.GitComet.desktop" "gitcomet.desktop")
icon_names=("dev.gitcomet.GitComet" "gitcomet")
rm -f "${bindir}/gitcomet"
rm -f "${appdir}/gitcomet.desktop"
rm -f "${bindir}/gitcomet-app"
for desktop_file in "${desktop_files[@]}"; do
rm -f "${appdir}/${desktop_file}"
done
for size in "${icon_sizes[@]}"; do
rm -f "${iconsroot}/${size}x${size}/apps/gitcomet.png"
for icon_name in "${icon_names[@]}"; do
rm -f "${iconsroot}/${size}x${size}/apps/${icon_name}.png"
done
done
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$appdir" >/dev/null 2>&1 || true

View file

@ -1,151 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: scripts/update-aur.sh \
--aur-dir PATH \
--version VERSION \
--binary-tar PATH \
--source-tar PATH \
[--verify-source]
Updates PKGBUILD metadata for the GitHub-hosted AUR mirror repo, regenerates
.SRCINFO, and optionally verifies the referenced sources with makepkg.
USAGE
}
aur_dir=""
version=""
binary_tar=""
source_tar=""
verify_source="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--aur-dir)
aur_dir="${2:-}"
shift 2
;;
--version)
version="${2:-}"
shift 2
;;
--binary-tar)
binary_tar="${2:-}"
shift 2
;;
--source-tar)
source_tar="${2:-}"
shift 2
;;
--verify-source)
verify_source="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown arg: $1" >&2
usage
exit 2
;;
esac
done
if [[ -z "$aur_dir" || -z "$version" || -z "$binary_tar" || -z "$source_tar" ]]; then
echo "All required arguments must be provided." >&2
usage
exit 2
fi
version="${version#v}"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
echo "Invalid --version '$version'. Expected semver like 1.2.3 or 1.2.3-rc.1." >&2
exit 2
fi
pkgbuild="${aur_dir}/PKGBUILD"
srcinfo="${aur_dir}/.SRCINFO"
expected_binary_name="gitcomet-v${version}-linux-x86_64.tar.gz"
expected_source_name="gitcomet-source-v${version}.tar.gz"
if [[ ! -f "$pkgbuild" ]]; then
echo "PKGBUILD not found: $pkgbuild" >&2
exit 1
fi
if [[ ! -f "$binary_tar" ]]; then
echo "Binary tarball not found: $binary_tar" >&2
exit 1
fi
if [[ ! -f "$source_tar" ]]; then
echo "Source tarball not found: $source_tar" >&2
exit 1
fi
if [[ "$(basename "$binary_tar")" != "$expected_binary_name" ]]; then
echo "Binary tarball must be named $expected_binary_name." >&2
exit 2
fi
if [[ "$(basename "$source_tar")" != "$expected_source_name" ]]; then
echo "Source tarball must be named $expected_source_name." >&2
exit 2
fi
sha256_file() {
local file="$1"
sha256sum "$file" | awk '{print $1}'
}
binary_sha="$(sha256_file "$binary_tar")"
source_sha="$(sha256_file "$source_tar")"
GITCOMET_PKGVER="$version" \
GITCOMET_BIN_SHA="$binary_sha" \
GITCOMET_SRC_SHA="$source_sha" \
perl -0pi -e '
my $pkgver = $ENV{GITCOMET_PKGVER};
my $bin_sha = $ENV{GITCOMET_BIN_SHA};
my $src_sha = $ENV{GITCOMET_SRC_SHA};
s/^pkgver=.*/pkgver=$pkgver/m
or die "Failed to update pkgver\n";
s/^sha256sums=\([^)]+\)/sprintf("sha256sums=(\x27%s\x27\n \x27%s\x27)", $bin_sha, $src_sha)/mse
or die "Failed to update sha256sums\n";
' "$pkgbuild"
pushd "$aur_dir" >/dev/null
makepkg --printsrcinfo > "$srcinfo"
cleanup_binary=""
cleanup_source=""
if [[ "$verify_source" == "true" ]]; then
staged_binary="$PWD/$expected_binary_name"
staged_source="$PWD/$expected_source_name"
if [[ "$binary_tar" != "$staged_binary" ]]; then
cp "$binary_tar" "$staged_binary"
cleanup_binary="$staged_binary"
fi
if [[ "$source_tar" != "$staged_source" ]]; then
cp "$source_tar" "$staged_source"
cleanup_source="$staged_source"
fi
cleanup() {
[[ -n "$cleanup_binary" ]] && rm -f "$cleanup_binary"
[[ -n "$cleanup_source" ]] && rm -f "$cleanup_source"
}
trap cleanup EXIT
makepkg --verifysource
fi
popd >/dev/null
echo "Updated AUR metadata in $aur_dir"

View file

@ -7,7 +7,7 @@ Usage: scripts/valgrind_cdp <run|on|off|open> [path]
Commands:
run [binary] Run a binary under callgrind.
Default: ./target/release-with-debug/gitcomet
Default: ./target/release-with-debug/gitcomet-app
on Enable callgrind instrumentation.
off Disable callgrind instrumentation.
open [outfile] Open a callgrind output file in kcachegrind.
@ -18,7 +18,7 @@ EOF
cmd="${1:-run}"
case "$cmd" in
run)
binary="${2:-./target/release-with-debug/gitcomet}"
binary="${2:-./target/release-with-debug/gitcomet-app}"
valgrind \
--tool=callgrind \
--callgrind-out-file=callgrind.out \